diff --git a/README.md b/README.md
index d5bcb283..a5e87638 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
-- **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。
+- **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty / Sixel 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
@@ -150,7 +150,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
## 终端客户端 CLI
-`crossdesk-cli` 在终端中通过 9 位设备 ID 连接被控电脑,默认只读、静音。普通终端使用 ANSI 字符画面,支持 Kitty 图形协议的终端可显示像素画面;可切换显示器、调整画质、放大细节和平移,也可明确开启键鼠控制。CLI 可以独立于桌面 GUI 构建,被控电脑仍需运行 CrossDesk 并提供可捕获的图形会话,信令服务需支持 `cli` 临时身份注册。
+`crossdesk-cli` 在终端中通过 9 位设备 ID 连接被控电脑,默认只读、静音。普通终端使用 ANSI 字符画面,支持 Kitty 或 Sixel 图形协议的终端可显示像素画面;可切换显示器、调整画质、放大细节和平移,也可明确开启键鼠控制。CLI 可以独立于桌面 GUI 构建,被控电脑仍需运行 CrossDesk 并提供可捕获的图形会话,信令服务需支持 `cli` 临时身份注册。
### 画面预览
@@ -202,7 +202,7 @@ xmake run crossdesk-cli 123456789
### 选择画面与连接参数
-默认 `--renderer auto` 自动探测 Kitty 图形协议,未获支持时回退为 ANSI。macOS「终端」等普通终端可显式选择 `ansi`;Ghostty 等支持该协议的终端可选择 `kitty`。显式指定 `kitty` 但探测失败时会报错,此时改用 `ansi`。
+默认 `--renderer auto` 按 Kitty → Sixel → ANSI 的顺序选择。macOS「终端」等普通终端可显式选择 `ansi`;支持对应图形协议的终端可选择 `kitty` 或 `sixel`。显式像素模式探测失败时会报错,此时改用 `ansi`。Xshell 是否能显示像素图片取决于终端自身的协议支持,CLI 无法单方面补齐。
以下示例使用 npm 全局安装后的命令;从源码构建时,可在仓库根目录将 `crossdesk-cli` 替换为 `xmake run crossdesk-cli`。
@@ -219,7 +219,7 @@ crossdesk-cli 123456789 --quality balanced --fps 8
| 参数 | 用途 |
| --- | --- |
-| `--renderer auto\|ansi\|kitty` | 自动选择、字符画面或像素画面;默认 `auto` |
+| `--renderer auto\|ansi\|kitty\|sixel` | 自动选择、字符画面或像素画面;默认 `auto` |
| `--zoom native` | 从原始细节模式开始;`--zoom 2` 表示相对全景放大 2 倍 |
| `--quality low\|balanced\|high\|ultra` | 请求最高 360p / 720p / 1080p / 2160p;默认 `high`,实际画质由被控端决定 |
| `--fps 8` | 固定本地输出目标和相应远端预览请求;允许 1–30,默认本地目标为 30,未指定时远端供帧会自适应 |
@@ -256,7 +256,7 @@ pi -e ./apps/cli/plugins/pi/index.ts
其他平台将 `CROSSDESK_CLI` 改为原生可执行文件的绝对路径。在 Pi 中输入 `/crossdesk` 或 `/crossdesk 123456789`,按提示输入密码。安装要求与验证范围见 [Pi 扩展说明](apps/cli/docs/terminal-client.md#pi-扩展)。
-CLI 当前仅提供 ID 连接与远程观看 / 输入,不提供 IP 直连、音频播放、文件传输或剪贴板同步,也暂不支持连接开启[可信设备限制](#trusted-devices)的被控端。macOS arm64 已完成本地构建和连接验证;Linux / Windows 完整运行、SSH / Kitty 实机显示及真实 Pi TUI 的验收范围见 [验证记录](apps/cli/docs/validation.md)。平台构建与 npm 发布不代表全部平台已完成实机验收。
+CLI 当前仅提供 ID 连接与远程观看 / 输入,不提供 IP 直连、音频播放、文件传输或剪贴板同步,也暂不支持连接开启[可信设备限制](#trusted-devices)的被控端。macOS arm64 已完成本地构建和连接验证;Linux / Windows 完整运行、SSH / Kitty / Sixel 实机显示及真实 Pi TUI 的验收范围见 [验证记录](apps/cli/docs/validation.md)。平台构建与 npm 发布不代表全部平台已完成实机验收。
[CLI 文档入口](apps/cli/README.md) · [终端使用与故障排查](apps/cli/docs/terminal-client.md) · [桥接协议](apps/cli/docs/bridge-protocol.md) · [npm 打包与分发](apps/cli/docs/npm-distribution.md)
diff --git a/README_EN.md b/README_EN.md
index 51715412..1875f118 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -32,7 +32,7 @@ CrossDesk connects your computers from desktop, browser, mobile, and terminal cl
- **Cross-platform access:** control Windows, macOS, and Linux desktops from computers, browsers, or native iOS / Android clients. HarmonyOS PC and mobile clients are in development.
- **Direct IP over LAN:** connect desktop clients using an IP address, port, and six-character password without a cloud signaling service. Each local IP has its own port; passwords can be remembered and recent devices are separated by mode.
-- **Terminal controller:** view and control remote desktops by device ID, with ANSI / Kitty rendering, zoom and pan, persistent local command mode, a Pi extension, and a build independent of the desktop GUI.
+- **Terminal controller:** view and control remote desktops by device ID, with ANSI / Kitty / Sixel rendering, zoom and pan, persistent local command mode, a Pi extension, and a build independent of the desktop GUI.
- **DeskHub:** live multi-device previews, groups, enlarged control, group input, and batch file distribution.
- **Video and audio:** H.264 / AV1, hardware codecs, remote audio, and in-session quality and frame-rate controls. Capabilities depend on the platform and device.
- **Everyday controls:** monitor switching, keyboard and mouse input, shortcuts, text clipboard, and file transfer.
@@ -150,7 +150,7 @@ Remote sessions support monitor switching, video quality and frame-rate controls
## Terminal client CLI
-`crossdesk-cli` connects to a host by its 9-digit device ID and starts in read-only, silent mode. Standard terminals use ANSI character rendering; terminals supporting the Kitty graphics protocol can display pixel graphics. The CLI supports monitor selection, quality settings, detail zoom, panning, and optional keyboard/mouse control. It builds independently of the desktop GUI. The host still needs CrossDesk and a capturable graphical session, and the signaling service must support temporary `cli` identities.
+`crossdesk-cli` connects to a host by its 9-digit device ID and starts in read-only, silent mode. Standard terminals use ANSI character rendering; terminals supporting the Kitty or Sixel graphics protocol can display pixel graphics. The CLI supports monitor selection, quality settings, detail zoom, panning, and optional keyboard/mouse control. It builds independently of the desktop GUI. The host still needs CrossDesk and a capturable graphical session, and the signaling service must support temporary `cli` identities.
### Display preview
@@ -202,7 +202,7 @@ For an existing checkout, run the dependency preparation and subsequent commands
### Rendering and connection options
-The default `--renderer auto` probes for Kitty graphics support and falls back to ANSI. Use `ansi` explicitly in standard terminals such as macOS Terminal, or `kitty` in compatible terminals such as Ghostty. Explicit `kitty` mode reports an error if the probe fails; use `ansi` in that case.
+The default `--renderer auto` selects Kitty, then Sixel, then ANSI according to terminal capability replies. Use `ansi` in standard terminals such as macOS Terminal, or `kitty` / `sixel` in terminals supporting the corresponding protocol. Explicit pixel modes report an error if the probe fails; use `ansi` in that case. Sixel output alone does not guarantee Xshell compatibility: the terminal itself must support the protocol.
These examples use the command installed globally by npm. For a source build, replace `crossdesk-cli` with `xmake run crossdesk-cli` and run it from the repository root.
@@ -219,7 +219,7 @@ crossdesk-cli 123456789 --quality balanced --fps 8
| Option | Purpose |
| --- | --- |
-| `--renderer auto\|ansi\|kitty` | Automatic selection, character rendering, or pixel graphics; default: `auto` |
+| `--renderer auto\|ansi\|kitty\|sixel` | Automatic selection, character rendering, or pixel graphics; default: `auto` |
| `--zoom native` | Start in native detail mode; `--zoom 2` zooms to twice the full-desktop scale |
| `--quality low\|balanced\|high\|ultra` | Request up to 360p / 720p / 1080p / 2160p; default: `high`. The host determines actual quality |
| `--fps 8` | Fix the local output target and corresponding remote preview request; accepts 1–30. The default local target is 30, with adaptive remote requests when omitted |
@@ -256,7 +256,7 @@ pi -e ./apps/cli/plugins/pi/index.ts
On other platforms, set `CROSSDESK_CLI` to the absolute path of the native executable. Enter `/crossdesk` or `/crossdesk 123456789` in Pi and follow the password prompt. See the [Pi extension guide](apps/cli/docs/terminal-client.md#pi-扩展) (Chinese) for requirements and validation scope.
-The CLI currently provides ID-based viewing and input, without direct IP connections, audio playback, file transfer, or clipboard synchronization. It also cannot yet connect to hosts with [trusted-device restrictions](#trusted-devices) enabled. Local builds and remote connections have been validated on macOS arm64. See the [validation record](apps/cli/docs/validation.md) (Chinese) for the remaining Linux / Windows, SSH / Kitty terminal, and actual Pi TUI checks. Platform builds and npm publication do not establish that every platform has been validated on real devices.
+The CLI currently provides ID-based viewing and input, without direct IP connections, audio playback, file transfer, or clipboard synchronization. It also cannot yet connect to hosts with [trusted-device restrictions](#trusted-devices) enabled. Local builds and remote connections have been validated on macOS arm64. See the [validation record](apps/cli/docs/validation.md) (Chinese) for the remaining Linux / Windows, SSH / Kitty / Sixel terminal, and actual Pi TUI checks. Platform builds and npm publication do not establish that every platform has been validated on real devices.
[CLI documentation](apps/cli/README.md) · [Terminal usage and troubleshooting](apps/cli/docs/terminal-client.md) · [Bridge protocol](apps/cli/docs/bridge-protocol.md) · [npm packaging and distribution](apps/cli/docs/npm-distribution.md) (detailed guides in Chinese)
diff --git a/apps/cli/README.md b/apps/cli/README.md
index ee9bdcd9..5a931271 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -1,6 +1,6 @@
# CrossDesk CLI
-CrossDesk 的终端主控客户端,源码位于本仓库的 `apps/cli`。通过真实 MiniRTC ID 会话观看远程桌面;默认只读、静音。包含 ANSI 象限字符画面、局部放大和平移、Kitty 图形后端,以及 Pi 自定义 TUI 扩展。
+CrossDesk 的终端主控客户端,源码位于本仓库的 `apps/cli`。通过真实 MiniRTC ID 会话观看远程桌面;默认只读、静音。包含 ANSI 象限字符画面、局部放大和平移、Kitty / Sixel 图形后端,以及 Pi 自定义 TUI 扩展。
## 画面预览
@@ -8,7 +8,7 @@ CrossDesk 的终端主控客户端,源码位于本仓库的 `apps/cli`。通
| --- | --- |
|
|
|
-图示来自 **2026-10-31** 的实际只读连接,通过伪终端采集并渲染协议输出;来源与边界见 [图示说明](../../docs/images/README.md#cli-captures)。默认 `auto` 探测 Kitty 支持,失败时使用 ANSI;普通终端可指定 `--renderer ansi`,Ghostty 等支持 Kitty 图形协议的终端可指定 `--renderer kitty`。ANSI 全景适合查看桌面轮廓,阅读小字需进入细节模式。
+图示来自 **2026-10-31** 的实际只读连接,通过伪终端采集并渲染协议输出;来源与边界见 [图示说明](../../docs/images/README.md#cli-captures)。默认 `auto` 依次选择 Kitty、Sixel、ANSI;普通终端可指定 `--renderer ansi`,支持对应协议的终端可指定 `--renderer kitty` 或 `--renderer sixel`。显式像素模式需要通过能力探测;增加 Sixel 输出不代表所有 Xshell 版本均可显示图片,终端本身也必须支持该协议。ANSI 全景适合查看桌面轮廓,阅读小字需进入细节模式。
## 安装与连接
@@ -53,6 +53,7 @@ xmake run crossdesk-cli 123456789 --quality high --zoom native
```sh
crossdesk-cli 123456789 --renderer kitty
+crossdesk-cli 123456789 --renderer sixel --fps 8
crossdesk-cli 123456789 --renderer ansi --zoom native
crossdesk-cli 123456789 --quality balanced --fps 8
crossdesk-cli 123456789 --display 1
@@ -82,4 +83,4 @@ crossdesk-cli 123456789 --display 1
CLI 仅作为控制端,每次连接通过服务端获得新的 `cli-xxxxxxxxx` 临时 ID。使用前须部署支持 `cli` 注册的 crossdesk-server;旧服务端不会回退为普通设备身份。
-终端 I/O 包含 POSIX(Linux/macOS)和 Windows Console / ConPTY 实现。macOS arm64 已完整构建,之前已连接真实被控端验证 ANSI 观看与预览协商。Linux / Windows 全量构建和真实连接、SSH/Kitty 实机显示及真实 Pi TUI 仍需验收;交叉编译检查不等于完整运行验证。此项目不创建被控端虚拟桌面,不提供音频播放、文件传输、剪贴板同步或 MCP 服务。
+终端 I/O 包含 POSIX(Linux/macOS)和 Windows Console / ConPTY 实现。macOS arm64 已完整构建,之前已连接真实被控端验证 ANSI 观看与预览协商。Linux / Windows 全量构建和真实连接、SSH/Kitty/Sixel 实机显示及真实 Pi TUI 仍需验收;交叉编译检查不等于完整运行验证。此项目不创建被控端虚拟桌面,不提供音频播放、文件传输、剪贴板同步或 MCP 服务。
diff --git a/apps/cli/docs/npm-distribution.md b/apps/cli/docs/npm-distribution.md
index 94a962c7..89c79cca 100644
--- a/apps/cli/docs/npm-distribution.md
+++ b/apps/cli/docs/npm-distribution.md
@@ -39,7 +39,7 @@ npx @chunkdash/crossdesk-cli@latest 123456789
这些是构建与分发目标,不代表均已实机通过。当前完整构建与 npm 运行验证只覆盖 macOS arm64;CLI 主程序与平台 I/O 已通过 Windows x64 与 Linux x64、arm64 的交叉编译检查。其他平台的完整 MiniRTC 链接、实际视频连接、键鼠、终端恢复仍需对应机器验收。
-普通终端使用 ANSI 渲染,按能力选择 TrueColor / 256 色 / 基础色;Kitty 图形协议是可选增强。macOS Terminal、iTerm2、Linux 常用终端、Windows Terminal 和 VS Code 集成终端属于目标场景。SSH 应分配 PTY(`ssh -t`);tmux / screen 使用 ANSI。不支持 `TERM=dumb` 或把交互观看输出重定向到文件;非交互宿主使用 `bridge --stdio`。Alpine / musl 当前没有预编译包。Windows 独立 mintty 窗口可能不提供 Console / ConPTY,应使用 Windows Terminal 承载 Git Bash。
+普通终端使用 ANSI 渲染,按能力选择 TrueColor / 256 色 / 基础色;Kitty / Sixel 图形协议是可选增强,需终端自身支持并通过能力探测。macOS Terminal、iTerm2、Linux 常用终端、Windows Terminal 和 VS Code 集成终端属于目标场景。SSH 应分配 PTY(`ssh -t`);tmux / screen 使用 ANSI。不支持 `TERM=dumb` 或把交互观看输出重定向到文件;非交互宿主使用 `bridge --stdio`。Alpine / musl 当前没有预编译包。Windows 独立 mintty 窗口可能不提供 Console / ConPTY,应使用 Windows Terminal 承载 Git Bash。
CI 的 macOS 构建配置指定 `--target_minver=13.0`,但最终必须检查实际产物及依赖。当前开发机的本地二进制最低要求为 **macOS 27.0**,不应将这个本地产物宣称为旧 macOS 通用版。
diff --git a/apps/cli/docs/terminal-client.md b/apps/cli/docs/terminal-client.md
index e4e25268..4c3af6b8 100644
--- a/apps/cli/docs/terminal-client.md
+++ b/apps/cli/docs/terminal-client.md
@@ -41,7 +41,7 @@ crossdesk-cli 123456789 --server your-server.example --port 9099
保留 MiniRTC WSS 证书链与主机名验证、TURN 临时凭据下发及 SRTP 协商。未提供跳过证书检查的参数。SRTP 是否实际协商成功通过状态统计呈现;沿用现有 MiniRTC 对旧端的兼容性,不宣称所有旧端必然启用 SRTP。自建证书问题应修复证书/系统信任链。
-## ANSI、Kitty 与 SSH
+## ANSI、Kitty、Sixel 与 SSH
ANSI 默认使用四象限字符:每个字符从 2×2 样本选择误差最小的两个颜色和象限形状,比原先上下半方块多一倍水平采样点;每个字符仍只有前景/背景两色。使用 `--ansi-detail half` 可回到上下半方块以兼容字体。缩小时采用面积加权滤波,避免最近邻采样漏掉细线。支持 TrueColor、256 色和基础 8 色,256 色选择实际颜色立方体或灰阶条中更接近的颜色。环境的 COLORTERM/TERM 决定颜色模式;非 UTF-8 locale 使用背景色空格。TERM=dumb、非 TTY 输出明确拒绝交互连接,不输出全屏转义序列。
@@ -63,7 +63,7 @@ Windows 的可见传统控制台优先读取当前字体尺寸;Windows Termina
ANSI 在完整桌面缩略图中仍无法保留普通字号的文字。先增加终端列数(缩小终端字体或扩大窗口),阅读局部时用 `--zoom native`。只读全景中点击目标位置可直接进入该区域的细节模式;按 `d` 则以当前中心进入。细节模式依据视口和字符宽高比选择源区域,避免为了完整装入桌面而把文字缩小;ANSI 视野较小,借助方向键平移阅读。`--zoom 1..32` 是相对整桌面的倍率;在细节模式中 `+/-` 是相对原始细节尺寸的倍率。画面缩放完全在本地进行,控制鼠标坐标映射回原始桌面的正确区域。
-auto 模式发送 Kitty 查询与设备属性查询,等待最多 250ms;没有有效 Kitty 回应就用 ANSI。显式 kitty 模式探测失败时报错。tmux/screen 中 auto 使用 ANSI,显式 kitty 报不支持;不修改用户终端配置。
+auto 模式发送 Kitty 查询与主设备属性查询(DA1),等待最多 250ms;优先选择 Kitty,否则在 DA1 的能力参数中找到 `4` 时选择 Sixel,再回退为 ANSI。显式 `kitty` 或 `sixel` 模式探测失败时报错,不强制向未知终端输出图像。tmux/screen 中 auto 使用 ANSI,显式像素模式报不支持;不修改用户终端配置。
本地 macOS/Linux 的 Kitty 模式会另用一个 1×1 像素的共享内存对象探测 `t=s`,最多等待 250ms;探测成功后,将未压缩 RGB 写入权限为 0600 的 POSIX 共享内存,仅通过终端字节流发送名字和显示命令。每次使用唯一名字,最多保留一个待确认的共享缓冲区(1920×1080×3 字节)。收到上传成功确认后才显示新图并释放缓冲区;上传报错、确认超过 500ms 或本地分配失败时,本次会话退回内联传输,继续保留旧画面。终端和 CLI 都会清理共享对象,退出时也会释放未确认的对象。SSH 中不尝试共享内存;Windows 暂用内联传输。
@@ -71,16 +71,24 @@ auto 模式发送 Kitty 查询与设备属性查询,等待最多 250ms;没
依据终端报告的像素尺寸生成画面(未知时按 10×20 像素字符估算),上限 1920×1080。保留面积滤波、黑边、宽高比和裁剪坐标;原始细节模式按像素视口裁取源区域。共享内存路径使用可复用水平行计算的整数面积权重,避免逐像素重复浮点运算,也不再因 Base64 字节预算额外降采样。没有可靠终端像素尺寸时使用估算值,因此不承诺显示设备的物理像素一一对应。内联压缩结果超出单次输出预算时仍逐级降低尺寸;高熵视频的实际帧率可能低于目标。RGB 传输和压缩不丢失像素,不代表面积采样或上游视频编码无损。
+Sixel 使用终端字节流传图,无需额外图像库或临时图片文件:每六行像素按颜色平面编码,使用重复压缩。每帧最多 248 色(RGB 色立方体加灰阶),复杂画面超过约 1.9MB 时逐级降低到 43 色、16 色,必要时降到 4 级灰阶,以保留原有 2MiB 单消息上限。Sixel 有调色板量化损失;它没有 Kitty 的图像缩放 placement,因此依据字符像素尺寸限制画面行列,使图像保持在 1920×1080 内,缩放、细节模式和鼠标映射使用相同视口。每帧覆盖整个图像区域(包括黑边),最后不足六行的部分不会绘制到状态栏;通过保存/恢复 DECSDM 模式使图像从视频区光标位置开始,退出时恢复。未收到像素尺寸时按 10×20 字符估算,实际对齐仍取决于终端尺寸报告。高熵画面或慢速 SSH 建议先用 `--fps 8`。
+
+```sh
+crossdesk-cli 123456789 --renderer sixel --fps 8
+```
+
+CLI 支持 Sixel 不等于 Xshell 必然支持。查阅的 [Xshell 官方完整功能列表](https://www.netsarang.com/en/xshell-all-features/) 与 [更新记录](https://www.netsarang.com/en/xshell-update-history/) 尚未提供 Sixel 支持说明;以终端实际能力回应及显示测试为准,不能仅修改 `TERM` 就获得像素图像支持。无能力回应时可继续使用 `--renderer ansi --zoom native`。Sixel 协议与 DA1 能力位依据 [XTerm 控制序列文档](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Sixel-Graphics)。
+
```sh
ssh -t your-server
crossdesk-cli 123456789 --renderer ansi --fps 8
```
-用户本地终端负责显示 SSH 字节流。ANSI、Kitty over SSH 和 SSH+tmux 的实机互通尚未验证。无 GUI 指主控客户端;被控端仍需要可捕获的图形会话。
+用户本地终端负责显示 SSH 字节流。ANSI、Kitty / Sixel over SSH 和 SSH+tmux 的实机互通尚未验证。无 GUI 指主控客户端;被控端仍需要可捕获的图形会话。
## 预览与背压
-`--fps 1..30` 是本地输出目标。交互 CLI 的 ANSI、内联 Kitty 和共享内存 Kitty 默认均为 30 FPS;共享内存失败回退时保持该目标。bridge 仍按宿主协议默认使用 8 FPS。
+`--fps 1..30` 是本地输出目标。交互 CLI 的 ANSI、Sixel、内联 Kitty 和共享内存 Kitty 默认均为 30 FPS;共享内存失败回退时保持该目标。bridge 仍按宿主协议默认使用 8 FPS。
未显式指定 `--fps` 时,远端预览从 30 FPS 开始,根据完整一帧从转换开始到输出完成的耗时自动选择 8/15/30 FPS 请求;共享内存的上传确认等待也计入,等待源帧的空闲时间不计入。使用平滑估计、不同升降档阈值和至少 8 个新帧样本,持续变慢 2 秒才降档、持续恢复 4 秒才升档,保留供帧余量。15 FPS 仅在对端声明支持时使用;预览未获接受时不追加自动调节。视口变化、显示器切换和共享内存回退会重置耗时估计。显式 `--fps` 固定本地目标和对应的远端预览请求,关闭自动调节;例如 `--fps 30` 可用于与自动策略对比。
@@ -99,7 +107,7 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
媒体回调仅同步复制借用的 NV12 数据,校验尺寸、容量、native 描述符与 stride;不保留借用指针。上限单帧 16,777,216 像素、每边 8192、偶数尺寸。最多一个待转换帧,替换前先完成校验与复制,无效帧不会挤掉已有画面;播放器另保留最近转换的一帧用于静止桌面的本地放大/重绘,并在接收新帧时回收上一帧缓冲区供后续复制使用。只丢弃解码后图像。显示器切换/关闭提升 generation,旧帧失效。
-转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),按令牌预算分块非阻塞写入,突发额度上限仍为 1MiB。本地交互 ANSI 和 Kitty 使用 8MiB/s;存在 `SSH_CONNECTION`、`SSH_CLIENT` 或 `SSH_TTY` 时,两者保持 1MiB/s,Pi 桥接始终保持 1MiB/s。不能开始新画面时保留最新解码帧。ANSI 差分按终端实际使用的颜色与字形比较,忽略量化后相同的颜色和空格不可见的前景色;同一行可输出多个变化片段,并按光标定位、颜色切换和字符的实际字节成本合并短间隔。差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。
+转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),按令牌预算分块非阻塞写入,突发额度上限仍为 1MiB。本地交互 ANSI、Kitty 和 Sixel 使用 8MiB/s;存在 `SSH_CONNECTION`、`SSH_CLIENT` 或 `SSH_TTY` 时,三者保持 1MiB/s,Pi 桥接始终保持 1MiB/s。不能开始新画面时保留最新解码帧。ANSI 差分按终端实际使用的颜色与字形比较,忽略量化后相同的颜色和空格不可见的前景色;同一行可输出多个变化片段,并按光标定位、颜色切换和字符的实际字节成本合并短间隔。差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。
测量像素渲染帧率时使用 release 构建;debug 的 `-O0` 面积采样开销明显更高。共享内存中的像素不经过终端字节预算,但仍受转换和终端消费速度限制;8MiB/s 是内联路径的输出上限,不是帧率保证。解码、像素转换、压缩、终端消费速度和帧大小都会影响结果。退出时,未完成画面最多继续写入 2s;取消图像传输及恢复屏幕的短写最多另重试 500ms。POSIX 立即恢复 termios 并清理输入,不等待输出排空;如果终端始终不消费输出,屏幕清理只能尽力发送。
@@ -169,7 +177,7 @@ Pi 扩展读取宿主的字符尺寸,并通过宿主终端写入/输入监
- 日志在进程创建的私有临时目录(POSIX `/tmp/crossdesk-cli-*`、Windows `%TEMP%\crossdesk-cli-*`)。交互连接的摘要在 client.log,包含收到的终端鼠标报告数 `mouse_reports`、其中的传统报告数 `mouse_legacy_reports`、成功交给传输层的鼠标事件数 `mouse_forwarded`、发送失败数 `mouse_send_failures`。一次点击报告可能生成定位和按键两条事件,所以发送数可能大于报告数;发送成功不表示被控端已执行。transport.log 只记录白名单中的 ICE/DTLS 状态及本地控制开关,不记录密码、SDP 或原始信令。使用完可删除。错误退出信息在恢复终端后显示,并给出本次日志目录。
- `connection_failed` 表示连接建立失败;`connection_lost` 表示连接成功后传输失效。xmake 随后的 `execv ... failed(1)` 表示程序以错误码 1 退出,本身不代表进程崩溃。遇到控制失效时,对照 transport.log 中的控制开关和连接状态;`mouse_reports` 为 0 表示终端没有向 CLI 提供可识别的鼠标事件。报告数增长但 `mouse_forwarded` 为 0 时,检查是否仍为只读/本地命令模式、是否已有画面,以及点击是否落在视频区域;`mouse_send_failures` 增长时检查连接状态。
- 如果整个 macOS Terminal 窗口退出且 CLI 记录 `output closed`,还应检查系统 DiagnosticReports 中的 Terminal 报告。已观察到 Terminal 2.15 在创建标签页并调整窗口尺寸时发生 `TTLogicalScreen expandUnicharOffsetCacheToLine:` 行缓存越界;这与 CLI 的 `connection_failed` 不同。临时可在独立窗口运行,避开该标签页操作,不能据此认为 Terminal 缺陷已修复。
-- `doctor --json` 是环境报告,不会联网登录、主动探测 Kitty 或证明远端可连接。 `input_shortcuts=local_commands_v2` 表示支持本文的持续本地命令模式;启动日志同时记录实际执行路径,以便区分不同工程的构建产物。
+- `doctor --json` 是环境报告,不会联网登录、主动探测 Kitty / Sixel 或证明远端可连接。 `input_shortcuts=local_commands_v2` 表示支持本文的持续本地命令模式;启动日志同时记录实际执行路径,以便区分不同工程的构建产物。
- 画面比例异常时查看 `client.log` 摘要的 `terminal_geometry`:`cell_aspect` 为实际采用的字符宽高比,`reported` 表示收到有效的终端尺寸报告,`manual_aspect` 表示使用了手动覆盖。`doctor` 中的比例和像素尺寸只反映系统 TTY 信息,不主动查询终端,可能与交互连接自动检测后的值不同。
第一轮无音频播放或音频订阅 API,SSH 服务器上的声音不会传回本地。无后台守护进程、共享媒体 Broker、IP 直连、MCP 或自动截图。其他宿主可以使用 [私有桥接](bridge-protocol.md),没有可靠自定义 UI 时打开独立终端或给出播放器命令。
diff --git a/apps/cli/docs/validation.md b/apps/cli/docs/validation.md
index 64745e6a..0253cd6b 100644
--- a/apps/cli/docs/validation.md
+++ b/apps/cli/docs/validation.md
@@ -132,8 +132,16 @@
- 暂停 PTY 读取 1.3s,并在暂停期间发送退出命令,恢复读取后完整帧计数与提交计数一致,约 1.17s 内退出,备用屏幕、termios 和非阻塞标志恢复。没有连接真实远端,也未验证真实终端的 GPU 绘制、SSH 或 tmux 实际显示;上述帧率为合成源的完整输出接收率。
- 额外检查源码、脚本、数据和产物均位于工程外系统临时目录,未新增或修改工程内测试文件/用例。
+## Sixel 图形输出(2026-11-02)
+
+- 新增 `--renderer sixel`;auto 按 Kitty → Sixel → ANSI 选择,Sixel 通过 DA1 能力参数 `4` 探测。显式像素模式没有有效回应或处于 tmux/screen 时拒绝启动;`doctor --json` 增加 `sixel=probe_on_view`,不主动探测。
+- macOS arm64 CLI debug 构建、帮助信息和 `doctor --json` 通过。工程外 ASan/UBSan 检查覆盖能力回复解析、20,000 组随机尺寸的视口限制及重复计算一致性、7 组合成 NV12 图像(纯黑、纯白、渐变、彩色、部分六行、裁剪缩放和大视口)。
+- 工程外独立 Sixel 解码器验证完整像素覆盖、颜色量化误差、图像边界和光标保存/恢复;1920×1080 高熵样本触发 4 级灰阶回退,输出约 1.30MB,保持原尺寸且低于单消息上限。
+- 生产 CLI/终端 I/O 配合工程外假会话的 11 组 POSIX PTY 检查通过:Kitty 优先、Sixel 自动及显式选择、分片 DA1 回应、错误/无回应回退、显式模式失败、tmux/screen 限制、窗口变化和细节缩放;SSH 字节预算保持 1MiB/s,退出后 termios 与备用屏幕恢复,Sixel 路径发送 DECSDM 恢复命令。
+- 未连接真实远端,未进行 Xshell、其他 Sixel 终端或真实 SSH 显示验收,不保证任何 Xshell 版本支持该协议。额外测试源码、脚本、数据及产物均保存在工程外系统临时目录,未新增或修改工程内测试文件/用例。
+
## 实测边界
-本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。五个平台 CI 配置与 npm 包名均不代表已发布或已验收。
+本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/Sixel/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。五个平台 CI 配置与 npm 包名均不代表已发布或已验收。
CLI 需要支持 `cli` 临时注册的服务端。`doctor --json` 仅报告本机环境,不会联网登录或证明远端可连接。
diff --git a/apps/cli/main.cpp b/apps/cli/main.cpp
index b50bcebf..2b2cf01d 100644
--- a/apps/cli/main.cpp
+++ b/apps/cli/main.cpp
@@ -143,6 +143,7 @@ private:
};
struct Tty {
int out;
+ bool sixel = false;
RawTerminal terminal;
Tty(int input, int output) : out(output), terminal(input, output) {
// Fall back to button/drag reporting if all-motion tracking is unsupported.
@@ -153,6 +154,7 @@ struct Tty {
~Tty() {
// Cancel any incomplete string/control sequence before restoration.
std::string end = "\030\033\\" + std::string(kKittyDelete) +
+ (sixel ? "\033[?80r" : "") +
"\033[0m\033[?1003l\033[?1002l\033[?1000l"
"\033[?1006l\033[?1004l\033[?2004l\033["
"?7h\033[?25h\033[?1049l";
@@ -332,9 +334,9 @@ int View(SessionOptions options, const std::string &device,
return 130;
Tty tty(0, fd);
Output output(fd);
- bool kitty = false;
+ bool kitty = false, sixel = false;
if (renderer != "ansi" && Env("TMUX").empty() && Env("STY").empty()) {
- output.Put(std::string(kKittyQuery) + "\033[c");
+ output.Put((renderer == "sixel" ? std::string{} : kKittyQuery) + "\033[c");
std::string reply;
auto until = Clock::now() + std::chrono::milliseconds(250);
while (Clock::now() < until && !interrupted) {
@@ -345,11 +347,21 @@ int View(SessionOptions options, const std::string &device,
if (n > 0 && reply.size() < 8192)
reply.append(b, n);
}
- kitty = KittyReply(reply);
+ kitty = renderer != "sixel" && KittyReply(reply);
+ sixel = renderer != "kitty" && !kitty && SixelReply(reply);
}
if (renderer == "kitty" && !kitty)
throw std::runtime_error(
"Kitty probe failed or multiplexer active; use --renderer ansi");
+ if (renderer == "sixel" && !sixel)
+ throw std::runtime_error(
+ "Sixel probe failed or multiplexer active; use --renderer ansi");
+ if (sixel) {
+ // DECSDM reset places graphics at the cursor. Save the user's setting
+ // and restore it on exit, including interrupted image writes.
+ output.Put("\033[?80s\033[?80l");
+ tty.sixel = true;
+ }
// Both pixel frames and large true-colour character grids need local
// throughput. Keep SSH conservative and retain the one-message/burst limits
// and nonblocking writes so slow terminals still apply backpressure.
@@ -520,10 +532,12 @@ int View(SessionOptions options, const std::string &device,
auto v = Size(&status_rows, &input.terminal_metrics, cell_aspect);
v.detail = detail;
v.native = input.native;
- v.pixel_mode = kitty;
+ v.pixel_mode = kitty || sixel;
v.zoom = input.zoom;
v.center_x = input.center_x;
v.center_y = input.center_y;
+ if (sixel)
+ v = SixelViewport(v);
if (input.view_revision != view_revision) {
view_revision = input.view_revision;
redraw = true;
@@ -591,6 +605,7 @@ int View(SessionOptions options, const std::string &device,
bar.command_mode = input.command_mode;
bar.fps = "FPS " + std::to_string(output_fps);
bar.view = std::string(kitty ? "KITTY"
+ : sixel ? "SIXEL"
: detail == AnsiDetail::Quadrant ? "QUAD"
: "HALF") +
(input.native ? " | Detail x" : " | Fit x") +
@@ -651,15 +666,15 @@ int View(SessionOptions options, const std::string &device,
now - last_frame->queued)
.count();
}
- pending = kitty ? Layout(*last_frame, v) : Convert(*last_frame, v);
- // Clear waiting text and Kitty margins without erasing the status bars.
+ pending = v.pixel_mode ? Layout(*last_frame, v) : Convert(*last_frame, v);
+ // Clear waiting text and image margins without erasing the status bars.
std::string clear;
if (waiting_visible)
for (int row = 0; row < v.rows; ++row)
clear += "\033[" + std::to_string(v.row_offset + row + 1) +
";1H\033[0m\033[2K";
if (shared_enabled) {
- const auto pixels = KittyPixels(*last_frame, v);
+ const auto pixels = ImagePixels(*last_frame, v);
if (shared_image.Assign(pixels.rgb.data(), pixels.rgb.size())) {
output.Put(KittySharedUpload(shared_image.Name(), pixels.width,
pixels.height, submitted % 2 != 0));
@@ -673,7 +688,8 @@ int View(SessionOptions options, const std::string &device,
if (fresh && !frame_started)
frame_started = Clock::now();
output.Put(clear + (kitty ? Kitty(*last_frame, v, submitted % 2 != 0)
- : Difference(*pending, displayed)));
+ : sixel ? Sixel(*last_frame, v)
+ : Difference(*pending, displayed)));
waiting_visible = false;
pending_frame = true;
}
@@ -729,7 +745,7 @@ int View(SessionOptions options, const std::string &device,
{"reported", input.terminal_metrics.Known()},
{"manual_aspect", cell_aspect > 0}}},
{"submitted", submitted},
- {"renderer", kitty ? "kitty" : "ansi"},
+ {"renderer", kitty ? "kitty" : sixel ? "sixel" : "ansi"},
{"kitty_transport", !kitty ? "none"
: shared_enabled ? "shared_memory"
: "inline"},
@@ -1011,7 +1027,7 @@ int Bridge(int fd, SessionOptions options, int password_fd) {
s.Disconnect();
return interrupted ? 130 : 0;
}
-const char *help = R"(crossdesk-cli <9-digit-device-id> [--renderer auto|ansi|kitty] [--fps 1..30]
+const char *help = R"(crossdesk-cli <9-digit-device-id> [--renderer auto|ansi|kitty|sixel] [--fps 1..30]
[--display 0..63] [--control] [--server host] [--port 9099]
[--quality low|balanced|high|ultra] [--ansi-detail quadrant|half] [--zoom 1..32|native]
[--cell-aspect auto|0.2..2]
@@ -1028,6 +1044,7 @@ Ctrl+T and Ctrl+C pass through in control mode; another Ctrl+] leaves local mode
Default ANSI detail: quadrant. Default quality: high (1920x1080); ultra requests up to 3840x2160.
Cell aspect is character width/height: auto detects terminal geometry; use --cell-aspect to calibrate.
Display index is zero-based. ANSI cannot reproduce full desktop text at fit scale.
+Auto rendering prefers Kitty, then Sixel, then ANSI. Explicit pixel renderers require a successful probe.
--fps controls local output (default 30). Without it, remote preview adapts to measured output speed.
Explicit --fps keeps a fixed preview request. Kitty transport fallback keeps the local target unchanged.
Preview requests above 15 use the negotiated interactive 30 FPS stream; lower requests use 1/8/15 FPS.
@@ -1064,6 +1081,7 @@ int main(int argc, char **argv) {
{"multiplexer", !Env("TMUX").empty() || !Env("STY").empty()},
{"software_decode", true},
{"kitty", "probe_on_view"},
+ {"sixel", "probe_on_view"},
{"audio_playback", false},
{"bridge_version", 1}};
std::cout << d.dump(argc == 3 ? -1 : 2) << "\n";
@@ -1142,7 +1160,8 @@ int main(int argc, char **argv) {
throw std::invalid_argument("unsupported option");
}
}
- if (renderer != "auto" && renderer != "ansi" && renderer != "kitty")
+ if (renderer != "auto" && renderer != "ansi" && renderer != "kitty" &&
+ renderer != "sixel")
throw std::invalid_argument("unknown renderer");
if (command == "bridge" && !stdio)
throw std::invalid_argument("bridge requires --stdio");
diff --git a/libs/terminal/include/terminal_renderer.h b/libs/terminal/include/terminal_renderer.h
index 65917a84..0e9b8437 100644
--- a/libs/terminal/include/terminal_renderer.h
+++ b/libs/terminal/include/terminal_renderer.h
@@ -67,7 +67,12 @@ struct PixelFrame {
int width = 0, height = 0;
std::vector rgb;
};
-PixelFrame KittyPixels(const DecodedFrame &, const Viewport &);
+PixelFrame ImagePixels(const DecodedFrame &, const Viewport &);
+// Sixel has no placement scaling. Limit the cell viewport, not the bitmap,
+// so the displayed pixels and pointer mapping keep the same geometry.
+Viewport SixelViewport(Viewport);
+std::string Sixel(const DecodedFrame &, const Viewport &);
+bool SixelReply(const std::string &response);
std::string KittySharedUpload(const std::string &name, int width, int height,
bool alternate_buffer, bool query = false);
std::string KittyPresent(const Viewport &, bool alternate_buffer);
diff --git a/libs/terminal/src/sixel_renderer.cpp b/libs/terminal/src/sixel_renderer.cpp
new file mode 100644
index 00000000..0303c75c
--- /dev/null
+++ b/libs/terminal/src/sixel_renderer.cpp
@@ -0,0 +1,169 @@
+#include "terminal_renderer.h"
+#include
+#include
+#include
+#include
+
+namespace crossdesk::terminal {
+Viewport SixelViewport(Viewport v) {
+ if (v.columns < 1 || v.columns > 240 || v.rows < 1 || v.rows > 100)
+ throw std::invalid_argument("invalid Sixel viewport");
+ const auto limit = [](int &cells, int &pixels, int fallback, int maximum) {
+ const int original_cells = cells;
+ if (pixels <= 0)
+ pixels = cells * fallback;
+ cells =
+ std::max(1, std::min(cells, int(int64_t(maximum) * cells / pixels)));
+ pixels = std::clamp(
+ int((int64_t(pixels) * cells + original_cells / 2) / original_cells), 2,
+ maximum);
+ };
+ limit(v.columns, v.pixel_width, 10, 1920);
+ limit(v.rows, v.pixel_height, 20, 1080);
+ v.pixel_mode = true;
+ return v;
+}
+
+bool SixelReply(const std::string &response) {
+ for (size_t pos = 0;
+ (pos = response.find("\033[?", pos)) != std::string::npos; ++pos) {
+ const auto end = response.find('c', pos + 3);
+ if (end == std::string::npos || end - pos > 1024)
+ continue;
+ const char *cur = response.data() + pos + 3;
+ const char *last = response.data() + end;
+ bool first = true, supported = false;
+ while (cur < last) {
+ unsigned value = 0;
+ const auto parsed = std::from_chars(cur, last, value);
+ if (parsed.ec != std::errc{})
+ break;
+ // The first parameter identifies the terminal, not a capability.
+ supported = supported || (!first && value == 4);
+ first = false;
+ cur = parsed.ptr;
+ if (cur == last) {
+ if (supported)
+ return true;
+ break;
+ }
+ if (*cur++ != ';')
+ break;
+ }
+ }
+ return false;
+}
+
+namespace {
+struct Palette {
+ int levels, grays;
+ std::vector> colors;
+ Palette(int cube_levels, int gray_levels)
+ : levels(cube_levels), grays(gray_levels) {
+ for (int r = 0; r < levels; ++r)
+ for (int g = 0; g < levels; ++g)
+ for (int b = 0; b < levels; ++b)
+ colors.push_back({Level(r), Level(g), Level(b)});
+ for (int i = 0; i < grays; ++i) {
+ const int value = Gray(i);
+ colors.push_back({value, value, value});
+ }
+ }
+ int Level(int i) const { return (i * 255 + (levels - 1) / 2) / (levels - 1); }
+ int Gray(int i) const { return (i * 255 + (grays - 1) / 2) / (grays - 1); }
+ uint8_t Index(const uint8_t *rgb) const {
+ const int gray =
+ ((int(rgb[0]) + rgb[1] + rgb[2]) * (grays - 1) + 382) / 765;
+ const int gray_index = levels * levels * levels + gray;
+ if (!levels)
+ return uint8_t(gray_index);
+ int index = 0, cube_error = 0, gray_error = 0;
+ for (int c = 0; c < 3; ++c) {
+ const int step = (rgb[c] * (levels - 1) + 127) / 255;
+ index = index * levels + step;
+ const int dc = rgb[c] - Level(step), dg = rgb[c] - Gray(gray);
+ cube_error += dc * dc;
+ gray_error += dg * dg;
+ }
+ return uint8_t(gray_error < cube_error ? gray_index : index);
+ }
+};
+
+void Run(std::string &out, uint8_t bits, int count) {
+ if (count > 3) {
+ out += '!';
+ out += std::to_string(count);
+ out += char('?' + bits);
+ } else {
+ out.append(size_t(count), char('?' + bits));
+ }
+}
+
+std::string Encode(const PixelFrame &pixels, const Viewport &v,
+ const Palette &palette) {
+ constexpr size_t budget = 1900000; // Leave room for terminal/UI commands.
+ const int width = pixels.width, height = pixels.height;
+ std::string out = "\0337\033[" + std::to_string(v.row_offset + 1) +
+ ";1H\033P0;1;0q\"1;1;" + std::to_string(width) + ";" +
+ std::to_string(height);
+ // Define a complete palette for every frame, including black margins.
+ // Pb=1 preserves pixels outside the image in its final partial six-row band.
+ for (size_t i = 0; i < palette.colors.size(); ++i) {
+ out += '#' + std::to_string(i) + ";2";
+ for (int c : palette.colors[i])
+ out += ';' + std::to_string((c * 100 + 127) / 255);
+ }
+ std::vector planes(palette.colors.size() * width);
+ std::vector ends(palette.colors.size());
+ for (int y = 0; y < height; y += 6) {
+ std::fill(planes.begin(), planes.end(), 0);
+ std::fill(ends.begin(), ends.end(), 0);
+ for (int dy = 0; dy < std::min(6, height - y); ++dy)
+ for (int x = 0; x < width; ++x) {
+ const auto color =
+ palette.Index(&pixels.rgb[(size_t(y + dy) * width + x) * 3]);
+ planes[size_t(color) * width + x] |= uint8_t(1 << dy);
+ ends[color] = std::max(ends[color], x + 1);
+ }
+ bool first = true;
+ for (size_t color = 0; color < ends.size(); ++color) {
+ if (!ends[color])
+ continue;
+ if (!first)
+ out += '$'; // Return to the left edge without advancing the band.
+ first = false;
+ out += '#' + std::to_string(color);
+ const auto *plane = &planes[color * width];
+ for (int x = 0; x < ends[color];) {
+ int end = x + 1;
+ while (end < ends[color] && plane[end] == plane[x])
+ ++end;
+ Run(out, plane[x], end - x);
+ x = end;
+ }
+ if (out.size() > budget)
+ return {};
+ }
+ if (y + 6 < height)
+ out += '-';
+ }
+ out += "\033\\\0338";
+ return out;
+}
+} // namespace
+
+std::string Sixel(const DecodedFrame &frame, const Viewport &viewport) {
+ const auto v = SixelViewport(viewport);
+ const auto pixels = ImagePixels(frame, v);
+ // Sixel cannot scale a smaller bitmap to a cell placement. Reduce color
+ // detail on high-entropy frames instead of shrinking or misplacing them.
+ for (const auto &sizes :
+ {std::pair{6, 32}, std::pair{3, 16}, std::pair{2, 8}, std::pair{0, 4}}) {
+ auto out = Encode(pixels, v, Palette(sizes.first, sizes.second));
+ if (!out.empty())
+ return out;
+ }
+ // Four planes at 1920 x 1080 always fit the budget above.
+ throw std::runtime_error("Sixel output budget exceeded");
+}
+} // namespace crossdesk::terminal
diff --git a/libs/terminal/src/terminal_renderer.cpp b/libs/terminal/src/terminal_renderer.cpp
index 2fc6286f..e7e2e4c7 100644
--- a/libs/terminal/src/terminal_renderer.cpp
+++ b/libs/terminal/src/terminal_renderer.cpp
@@ -543,7 +543,7 @@ static std::string Base64(const std::vector &data) {
}
return out;
}
-PixelFrame KittyPixels(const DecodedFrame &f, const Viewport &requested) {
+PixelFrame ImagePixels(const DecodedFrame &f, const Viewport &requested) {
auto v = requested;
v.pixel_mode = true;
Validate(f, v);