Compare commits

...
4 Commits
15 changed files with 190 additions and 87 deletions
+9 -6
View File
@@ -22,7 +22,7 @@
</div> </div>
CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。 CrossDesk 让你从电脑、浏览器、手机或终端(桌面 CLI)连接远端电脑,也能通过桌面墙(DeskHub)集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。
[主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [终端 CLI](#cli) · [IP 直连](#lan-direct) · [可信设备](#trusted-devices) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor) [主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [终端 CLI](#cli) · [IP 直连](#lan-direct) · [可信设备](#trusted-devices) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor)
@@ -33,7 +33,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。 - **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。 - **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
- **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty / Sixel 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。 - **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty / Sixel 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。 - 桌面墙:多设备实时预览、分组、放大操控、群控和批量文件分发。
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。 - **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。 - **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
- **可信设备:** 桌面端可选开启可信设备限制,在连接密码之外校验设备身份;支持复制、粘贴配对信息,提前添加允许访问本机的设备。 - **可信设备:** 桌面端可选开启可信设备限制,在连接密码之外校验设备身份;支持复制、粘贴配对信息,提前添加允许访问本机的设备。
@@ -157,7 +157,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
| ANSI 字符画面 | Kitty 协议像素画面 | | ANSI 字符画面 | Kitty 协议像素画面 |
| --- | --- | | --- | --- |
| <img src="docs/images/cli-ansi-overview.png" width="480" alt="CrossDesk CLI 的 ANSI 四象限全景:顶部显示只读模式和连接状态,底部显示操作快捷键" /> | <img src="docs/images/cli-kitty-overview.png" width="480" alt="CrossDesk CLI 的 Kitty 协议全景:同一远程桌面以像素画面显示,保留状态栏和操作快捷键" /> | | <img src="docs/images/cli-ansi-overview.png" width="480" alt="CrossDesk CLI 的 ANSI 四象限全景:顶部显示只读模式和连接状态,底部显示操作快捷键" /> | <img src="docs/images/cli-kitty-overview.png" width="480" alt="CrossDesk CLI 的 Kitty 协议全景:同一远程桌面以像素画面显示,保留状态栏和操作快捷键" /> |
| 用彩色字符显示桌面轮廓,适合普通终端;查看局部可按 `d` 放大。 | 直接显示像素画面,适合支持 Kitty 图形协议的终端,如 Ghostty。 | | 用彩色字符显示桌面轮廓,适合普通终端;查看局部可用 `+/-` 或滚轮以鼠标位置缩放。 | 直接显示像素画面,适合支持 Kitty 图形协议的终端,如 Ghostty。 |
两图采集于 **2026-10-31**,来自同一授权设备的实际只读连接,以 128 × 40 字符视口还原 CLI 的 ANSI / Kitty 协议输出。图中 FPS 是采集时的实际输出计数,静止画面下 ANSI 无变化可显示 `FPS 0`。图示为输出渲染,终端实窗效果受字体、窗口大小及协议支持影响;采集方法见 [图示来源](docs/images/README.md#cli-captures)。 两图采集于 **2026-10-31**,来自同一授权设备的实际只读连接,以 128 × 40 字符视口还原 CLI 的 ANSI / Kitty 协议输出。图中 FPS 是采集时的实际输出计数,静止画面下 ANSI 无变化可显示 `FPS 0`。图示为输出渲染,终端实窗效果受字体、窗口大小及协议支持影响;采集方法见 [图示来源](docs/images/README.md#cli-captures)。
@@ -227,7 +227,7 @@ crossdesk-cli 123456789 --quality balanced --fps 8
| `--control` | 连接后直接启用键鼠控制;省略时默认只读 | | `--control` | 连接后直接启用键鼠控制;省略时默认只读 |
| `--server your-server.example --port 9099` | 使用自建信令服务,两端需接入同一服务 | | `--server your-server.example --port 9099` | 使用自建信令服务,两端需接入同一服务 |
ANSI 全景受字符网格限制,提高画质不能让整屏小字变得清晰;可扩大终端、缩小字体,再按 `d` 查看局部,按 `f` 回到全景。顶部状态栏显示模式、连接进度、渲染器、缩放、实时输出 FPS 和网络信息,窄窗口会省略部分字段。通过 SSH 使用时分配 PTY(`ssh -t`);tmux / screen 使用 ANSI。更多显示、鼠标报告和兼容性说明见 [终端使用指南](apps/cli/docs/terminal-client.md)。 ANSI 全景受字符网格限制,提高画质不能让整屏小字变得清晰;可扩大终端、缩小字体,再用 `+/-` 或滚轮查看局部,按 `f` 回到全景并重置平移。顶部状态栏显示模式、连接进度、渲染器、缩放、实时输出 FPS 和网络信息,窄窗口会省略部分字段。通过 SSH 使用时分配 PTY(`ssh -t`);tmux / screen 使用 ANSI。更多显示、鼠标报告和兼容性说明见 [终端使用指南](apps/cli/docs/terminal-client.md)。
### 控制、查看与退出 ### 控制、查看与退出
@@ -237,10 +237,13 @@ ANSI 全景受字符网格限制,提高画质不能让整屏小字变得清晰
| 控制模式 | 键鼠输入传给远端;`Ctrl+]` 进入本地命令模式并暂停远端输入 | | 控制模式 | 键鼠输入传给远端;`Ctrl+]` 进入本地命令模式并暂停远端输入 |
| 本地命令模式 | 执行命令后持续保持;`c` 选择手动返回后是否启用控制,`q` 退出 CLI | | 本地命令模式 | 执行命令后持续保持;`c` 选择手动返回后是否启用控制,`q` 退出 CLI |
| `Esc` / `Ctrl+C` | 在本地命令模式中手动返回所选的控制或只读模式 | | `Esc` / `Ctrl+C` | 在本地命令模式中手动返回所选的控制或只读模式 |
| `+` / `=` / `-` | 在只读或本地命令模式中放大 / 缩小 | | `+` / `=` / `-` | 在只读或本地命令模式中以鼠标位置放大 / 缩小 |
| `d` / `f` | 在只读或本地命令模式中切换原始细节 / 完整桌面;只读时也可点击目标进入细节 | | 滚轮 / 点击 | 只读模式中,滚轮以鼠标位置缩放;点击目标进入原始细节 |
| `f` | 在只读或本地命令模式中恢复完整桌面,同时重置倍率、平移和细节模式 |
| 方向键 / `i j k l` | 在只读或本地命令模式中平移;`i` 上、`j` 左、`k` 下、`l` 右 | | 方向键 / `i j k l` | 在只读或本地命令模式中平移;`i` 上、`j` 左、`k` 下、`l` 右 |
鼠标下的画面内容在缩放时尽量保持原位,到达桌面边缘时限制裁剪范围;没有有效鼠标位置时沿用当前中心。本地命令模式中鼠标仅更新缩放锚点,点击和滚轮不执行视图操作。
**控制模式下退出:先按 `Ctrl+]`,再按 `q`。** 普通 `c`、`q` 和 `Ctrl+C` 在控制模式中会发送到远端。 **控制模式下退出:先按 `Ctrl+]`,再按 `q`。** 普通 `c`、`q` 和 `Ctrl+C` 在控制模式中会发送到远端。
`Ctrl+]` 模式不会因为缩放、平移或按 `c` 自动结束。状态栏中的 `CONTROL PAUSED` / `READ ONLY` 表示返回后是否启用控制;按 `Esc` / `Ctrl+C` 手动返回。再次按 `Ctrl+]` 也可返回,若返回控制模式则向远端发送一次该组合键。 `Ctrl+]` 模式不会因为缩放、平移或按 `c` 自动结束。状态栏中的 `CONTROL PAUSED` / `READ ONLY` 表示返回后是否启用控制;按 `Esc` / `Ctrl+C` 手动返回。再次按 `Ctrl+]` 也可返回,若返回控制模式则向远端发送一次该组合键。
+9 -6
View File
@@ -22,7 +22,7 @@
</div> </div>
CrossDesk connects your computers from desktop, browser, mobile, and terminal clients, with **DeskHub** for viewing and controlling multiple devices together. It is built on [MiniRTC](https://github.com/kunkundi/minirtc) and supports self-hosting. CrossDesk connects your computers from desktop, browser, mobile, and terminal clients, with DeskHub for viewing and controlling multiple devices together. It is built on [MiniRTC](https://github.com/kunkundi/minirtc) and supports self-hosting.
[Features](#features) · [Download and platforms](#download) · [Quick start](#quick-start) · [Terminal CLI](#cli) · [Direct IP](#lan-direct) · [Trusted devices](#trusted-devices) · [Preview](#preview) · [User guide](docs/USER_GUIDE_EN.md) · [Self-hosting](#self-hosting) · [Build and feedback](#development) · [Sponsor](#sponsor) [Features](#features) · [Download and platforms](#download) · [Quick start](#quick-start) · [Terminal CLI](#cli) · [Direct IP](#lan-direct) · [Trusted devices](#trusted-devices) · [Preview](#preview) · [User guide](docs/USER_GUIDE_EN.md) · [Self-hosting](#self-hosting) · [Build and feedback](#development) · [Sponsor](#sponsor)
@@ -33,7 +33,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. - **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. - **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 / Sixel 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. - 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. - **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. - **Everyday controls:** monitor switching, keyboard and mouse input, shortcuts, text clipboard, and file transfer.
- **Trusted devices:** optionally restrict desktop access to trusted device identities in addition to the connection password. Copy and paste pairing information to approve devices before they connect. - **Trusted devices:** optionally restrict desktop access to trusted device identities in addition to the connection password. Copy and paste pairing information to approve devices before they connect.
@@ -157,7 +157,7 @@ Remote sessions support monitor switching, video quality and frame-rate controls
| ANSI character rendering | Kitty pixel graphics | | ANSI character rendering | Kitty pixel graphics |
| --- | --- | | --- | --- |
| <img src="docs/images/cli-ansi-overview.png" width="480" alt="CrossDesk CLI ANSI quadrant overview, with read-only mode and connection status at the top and shortcuts at the bottom" /> | <img src="docs/images/cli-kitty-overview.png" width="480" alt="CrossDesk CLI Kitty protocol overview of the same remote desktop, rendered as pixels with the status bar and shortcuts" /> | | <img src="docs/images/cli-ansi-overview.png" width="480" alt="CrossDesk CLI ANSI quadrant overview, with read-only mode and connection status at the top and shortcuts at the bottom" /> | <img src="docs/images/cli-kitty-overview.png" width="480" alt="CrossDesk CLI Kitty protocol overview of the same remote desktop, rendered as pixels with the status bar and shortcuts" /> |
| Colored characters show the desktop in standard terminals; press `d` to inspect a smaller region. | Pixel graphics display the desktop in terminals supporting the Kitty graphics protocol, such as Ghostty. | | Colored characters show the desktop in standard terminals; use `+/-` or the wheel to zoom at the mouse position. | Pixel graphics display the desktop in terminals supporting the Kitty graphics protocol, such as Ghostty. |
Both images were captured on **2026-10-31** from real read-only connections to the same authorized host. They render the CLI's ANSI / Kitty protocol output in a 128 × 40 character viewport. FPS counts actual output during capture; an unchanged ANSI view can show `FPS 0`. Actual terminal appearance depends on the font, window size, and protocol support. See [capture notes](docs/images/README.md#cli-captures) for the method. Both images were captured on **2026-10-31** from real read-only connections to the same authorized host. They render the CLI's ANSI / Kitty protocol output in a 128 × 40 character viewport. FPS counts actual output during capture; an unchanged ANSI view can show `FPS 0`. Actual terminal appearance depends on the font, window size, and protocol support. See [capture notes](docs/images/README.md#cli-captures) for the method.
@@ -227,7 +227,7 @@ crossdesk-cli 123456789 --quality balanced --fps 8
| `--control` | Enable keyboard/mouse control immediately after connecting; omitted by default for read-only viewing | | `--control` | Enable keyboard/mouse control immediately after connecting; omitted by default for read-only viewing |
| `--server your-server.example --port 9099` | Use a self-hosted signaling service; both ends must use the same service | | `--server your-server.example --port 9099` | Use a self-hosted signaling service; both ends must use the same service |
ANSI overviews are limited by the character grid: higher video quality alone cannot make full-desktop text legible. Enlarge the terminal or reduce its font size, press `d` for detail, and `f` to return to the full desktop. The top bar shows input mode, connection progress, renderer, zoom, live output FPS, and network information; narrow windows omit some fields. Allocate a PTY over SSH (`ssh -t`), and use ANSI inside tmux / screen. See the [terminal guide](apps/cli/docs/terminal-client.md) (Chinese) for rendering, mouse reporting, and compatibility details. ANSI overviews are limited by the character grid: higher video quality alone cannot make full-desktop text legible. Enlarge the terminal or reduce its font size, use `+/-` or the wheel for detail, and `f` to restore the full desktop and reset panning. The top bar shows input mode, connection progress, renderer, zoom, live output FPS, and network information; narrow windows omit some fields. Allocate a PTY over SSH (`ssh -t`), and use ANSI inside tmux / screen. See the [terminal guide](apps/cli/docs/terminal-client.md) (Chinese) for rendering, mouse reporting, and compatibility details.
### Control, navigation, and exit ### Control, navigation, and exit
@@ -237,10 +237,13 @@ ANSI overviews are limited by the character grid: higher video quality alone can
| Control mode | Keyboard/mouse input goes to the host; `Ctrl+]` enters local command mode and pauses remote input | | Control mode | Keyboard/mouse input goes to the host; `Ctrl+]` enters local command mode and pauses remote input |
| Local command mode | Stays active after commands; `c` selects whether control resumes when you leave; `q` exits the CLI | | Local command mode | Stays active after commands; `c` selects whether control resumes when you leave; `q` exits the CLI |
| `Esc` / `Ctrl+C` | Manually leave local command mode and return to the selected control or read-only mode | | `Esc` / `Ctrl+C` | Manually leave local command mode and return to the selected control or read-only mode |
| `+` / `=` / `-` | Zoom in / out in read-only or local command mode | | `+` / `=` / `-` | Zoom in / out at the mouse position in read-only or local command mode |
| `d` / `f` | Switch to native detail / full-desktop view in read-only or local command mode; clicking in read-only mode also opens detail at that point | | Wheel / click | In read-only mode, zoom at the mouse position with the wheel; click a point to open native detail |
| `f` | Restore the full desktop and reset zoom, pan, and native detail mode in read-only or local command mode |
| Arrow keys / `i j k l` | Pan in read-only or local command mode: `i` up, `j` left, `k` down, `l` right | | Arrow keys / `i j k l` | Pan in read-only or local command mode: `i` up, `j` left, `k` down, `l` right |
Zoom keeps the content under the pointer in place where possible, clamping the crop at desktop edges. Without a valid mouse position, zoom uses the current center. In local command mode, mouse reports only update the zoom anchor; clicks and the wheel do not change the view.
**To quit while controlling the host, press `Ctrl+]`, then `q`.** Ordinary `c`, `q`, and `Ctrl+C` go to the host in control mode. **To quit while controlling the host, press `Ctrl+]`, then `q`.** Ordinary `c`, `q`, and `Ctrl+C` go to the host in control mode.
Zooming, panning, or pressing `c` does not end `Ctrl+]` mode. The status bar shows `CONTROL PAUSED` / `READ ONLY` to indicate whether control will resume; press `Esc` / `Ctrl+C` to return manually. Pressing `Ctrl+]` again also leaves local mode and forwards that shortcut if control resumes. Zooming, panning, or pressing `c` does not end `Ctrl+]` mode. The status bar shows `CONTROL PAUSED` / `READ ONLY` to indicate whether control will resume; press `Esc` / `Ctrl+C` to return manually. Pressing `Ctrl+]` again also leaves local mode and forwards that shortcut if control resumes.
+8 -4
View File
@@ -59,20 +59,24 @@ crossdesk-cli 123456789 --quality balanced --fps 8
crossdesk-cli 123456789 --display 1 crossdesk-cli 123456789 --display 1
``` ```
默认请求 `high` 画质(最高 1920 × 1080),`--display` 的索引从 `0` 开始。交互 CLI 默认本地输出目标为 30 FPS,远端供帧会随输出耗时自适应;显式 `--fps` 固定目标和相应预览请求,实际帧率仍取决于设备、终端与链路。更多参数见 [主 README](../../README.md#cli) 和 [终端使用指南](docs/terminal-client.md)。 默认请求 `high` 画质(最高 1920 × 1080)和清晰度优先策略,`--display` 的索引从 `0` 开始。交互 CLI 默认本地输出目标为 30 FPS,远端供帧会随输出耗时自适应;显式 `--fps` 固定目标和相应预览请求,各帧率下均优先保持分辨率,实际画质与帧率仍取决于设备、终端与链路。更多参数见 [主 README](../../README.md#cli) 和 [终端使用指南](docs/terminal-client.md)。
## 查看、控制与退出 ## 查看、控制与退出
| 操作 | 按键 | | 操作 | 按键 |
| --- | --- | | --- | --- |
| 开启远端控制 | 只读模式下按 `c` | | 开启远端控制 | 只读模式下按 `c` |
| 放大局部 / 回到全景 | 只读或本地命令模式下按 `d` / `f`;只读时也可点击目标进入细节 | | 缩放 | 只读或本地命令模式下用 `+` / `=` / `-` 以鼠标位置缩放;只读时也可用滚轮 |
| 缩放 / 平移 | 只读或本地命令模式下用 `+/-` 或滚轮缩放;方向键或 `i` 上、`j` 左、`k` 下、`l` 右平移 | | 回到全景 | 只读或本地命令模式下按 `f`,同时重置倍率、平移和细节模式 |
| 平移 | 只读或本地命令模式下用方向键或 `i` 上、`j` 左、`k` 下、`l` 右 |
| 原始细节 | 只读时点击目标位置,或以 `--zoom native` 启动 |
| 暂停远端输入并进入本地命令模式 | `Ctrl+]` | | 暂停远端输入并进入本地命令模式 | `Ctrl+]` |
| 选择返回后的控制状态 | 本地命令模式下按 `c`,再按 `Esc` / `Ctrl+C` 返回 | | 选择返回后的控制状态 | 本地命令模式下按 `c`,再按 `Esc` / `Ctrl+C` 返回 |
| 退出连接 | 只读模式下按 `q` / `Ctrl+C`;控制模式下先按 `Ctrl+]`,再按 `q` | | 退出连接 | 只读模式下按 `q` / `Ctrl+C`;控制模式下先按 `Ctrl+]`,再按 `q` |
本地命令模式在缩放、平移和按 `c` 后持续保持,直到手动返回。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。再次按 `Ctrl+]` 也可离开本地命令模式,若返回控制模式则同时向远端发送该组合键。 缩放尽量保持鼠标下的画面内容原位;没有有效鼠标位置时沿用当前中心。底部提示随模式和窗口宽度调整,`Zoom at mouse` 表示鼠标锚点缩放,`Full view` / `Full` 表示恢复全景。
本地命令模式在缩放、平移和按 `c` 后持续保持,直到手动返回。此模式中鼠标仅更新缩放锚点,点击和滚轮不执行视图操作。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。再次按 `Ctrl+]` 也可离开本地命令模式,若返回控制模式则同时向远端发送该组合键。
## 更多文档与功能范围 ## 更多文档与功能范围
+1 -1
View File
@@ -12,7 +12,7 @@
| connect | device:9位数字字符串, display:0..63(默认0) | 从专用 password fd 读取至换行/EOF(最多256字节、等待5秒),开始异步注册/连接;ok不是远端已连接 | | connect | device:9位数字字符串, display:0..63(默认0) | 从专用 password fd 读取至换行/EOF(最多256字节、等待5秒),开始异步注册/连接;ok不是远端已连接 |
| status | 无 | 返回当前状态 | | status | 无 | 返回当前状态 |
| control | enabled:boolean | 显式启停控制;关闭时释放输入 | | control | enabled:boolean | 显式启停控制;关闭时释放输入 |
| input | data:string,最多16KiB | 终端原始按键/UTF-8/bracketed paste/SGR鼠标;只读时 c 开启控制、q/Ctrl+C 退出,d/鼠标点击进入细节,+/-/滚轮缩放,方向键或 i 上、j 左、k 下、l 右平移,f 全景;Ctrl+] 打开本地命令,随后 c 选择返回后的控制状态、q 退出或视图键连续调整画面,Esc/Ctrl+C 手动返回;控制模式中 Ctrl+T/Ctrl+C 传给远端,本地模式中再按 Ctrl+] 手动返回,并在返回控制模式时传递远端 Ctrl+] | | input | data:string,最多16KiB | 终端原始按键/UTF-8/bracketed paste/SGR鼠标;只读时 c 开启控制、q/Ctrl+C 退出,鼠标点击进入细节,+/-/滚轮以鼠标位置缩放,方向键或 i 上、j 左、k 下、l 右平移,f 恢复全景并重置倍率与平移;Ctrl+] 打开本地命令,随后 c 选择返回后的控制状态、q 退出或视图键连续调整画面,Esc/Ctrl+C 手动返回;控制模式中 Ctrl+T/Ctrl+C 传给远端,本地模式中再按 Ctrl+] 手动返回,并在返回控制模式时传递远端 Ctrl+] |
| display | index:0..63 | 选择已广告的零起始显示器,提升 generation | | display | index:0..63 | 选择已广告的零起始显示器,提升 generation |
| ack | frame_id、generation:非负整数 | 确认宿主消费帧;匹配当前等待帧才开放下一帧 | | ack | frame_id、generation:非负整数 | 确认宿主消费帧;匹配当前等待帧才开放下一帧 |
| disconnect | 无 | 释放输入、结束当前媒体会话 | | disconnect | 无 | 释放输入、结束当前媒体会话 |
+9 -6
View File
@@ -57,11 +57,11 @@ Windows 的可见传统控制台优先读取当前字体尺寸;Windows Termina
缩放在完整输出边界清理旧画面。上限 240 列、100 画面行;默认顶部预留一行状态栏、底部预留两行快捷键,另预留一列避免自动换行。顶部状态栏用反色区分,左侧为控制模式和连接进度,右侧为画面参数及网络信息;底部两行分别为会话和视图快捷键。视频和等待提示从顶部状态栏下方开始,鼠标坐标按视频区域映射,点击状态栏不会传给远端。按键分组使用统一列宽和左右留白,窄窗口自动减少列数、合并按键组并省略次要参数,累计帧数和输出字节数保留在日志中。控制模式下仅提示本地命令入口及后续操作,进入本地命令模式后才显示平移和缩放按键;终端高度不足时优先收起底部提示行。 缩放在完整输出边界清理旧画面。上限 240 列、100 画面行;默认顶部预留一行状态栏、底部预留两行快捷键,另预留一列避免自动换行。顶部状态栏用反色区分,左侧为控制模式和连接进度,右侧为画面参数及网络信息;底部两行分别为会话和视图快捷键。视频和等待提示从顶部状态栏下方开始,鼠标坐标按视频区域映射,点击状态栏不会传给远端。按键分组使用统一列宽和左右留白,窄窗口自动减少列数、合并按键组并省略次要参数,累计帧数和输出字节数保留在日志中。控制模式下仅提示本地命令入口及后续操作,进入本地命令模式后才显示平移和缩放按键;终端高度不足时优先收起底部提示行。
快捷键行使用 `KEYS |`、`VIEW |` 等固定宽度标题,标题和按键组之间用竖线分隔;键名用 `<c>`、`<+/->` 等标签标识。宽窗口中的分组分隔线按列对齐,窄窗口保留标题和完整按键标签,按可用宽度合并或减少分组。 快捷键行使用 `KEYS |`、`VIEW |` 等固定宽度标题,标题和按键组之间用竖线分隔;键名用 `<c>`、`<+/->` 等标签标识。`Zoom at mouse` 表示鼠标锚点缩放,`Full view` / `Full` 表示恢复全景,`Pan` 表示平移。只读模式提示滚轮缩放,宽窗口还显示点击进入细节;本地命令模式提示返回、控制切换及键盘视图操作,宽窗口显示鼠标选择锚点和 `=` 放大。宽窗口中的分组分隔线按列对齐,窄窗口保留完整按键标签,优先保留缩放和全景,按可用宽度合并或减少分组。
顶部状态栏以整数显示实时帧率(例如 `FPS 30`),约每秒按最近统计区间内完整输出到终端的帧数计算;没有新画面输出时更新为 `FPS 0`。该数值是实际提交速率,不是 `--fps` 目标值或终端 GPU 呈现频率;窄窗口优先省略画面参数和网络信息,保留帧率。 顶部状态栏以整数显示实时帧率(例如 `FPS 30`),约每秒按最近统计区间内完整输出到终端的帧数计算;没有新画面输出时更新为 `FPS 0`。该数值是实际提交速率,不是 `--fps` 目标值或终端 GPU 呈现频率;窄窗口优先省略画面参数和网络信息,保留帧率。
ANSI 在完整桌面缩略图中仍无法保留普通字号的文字。先增加终端列数(缩小终端字体或扩大窗口),阅读局部时用 `--zoom native`。只读全景中点击目标位置可直接进入该区域的细节模式;按 `d` 则以当前中心进入。细节模式依据视口和字符宽高比选择源区域,避免为了完整装入桌面而把文字缩小;ANSI 视野较小,借助方向键平移阅读。`--zoom 1..32` 是相对整桌面的倍率;在细节模式中 `+/-` 是相对原始细节尺寸的倍率。画面缩放完全在本地进行,控制鼠标坐标映射回原始桌面的正确区域。 ANSI 在完整桌面缩略图中仍无法保留普通字号的文字。先增加终端列数(缩小终端字体或扩大窗口),阅读局部时用 `--zoom native`。只读时可用 `+/-` 或滚轮以鼠标位置缩放,也可点击目标位置直接进入该区域的细节模式。细节模式依据视口和字符宽高比选择源区域,避免为了完整装入桌面而把文字缩小;ANSI 视野较小,借助方向键平移阅读。`--zoom 1..32` 是相对整桌面的倍率;在细节模式中 `+/-` 是相对原始细节尺寸的倍率。画面缩放完全在本地进行,控制鼠标坐标映射回原始桌面的正确区域。
auto 模式发送 Kitty 查询与主设备属性查询(DA1),等待最多 250ms;优先选择 Kitty,否则在 DA1 的能力参数中找到 `4` 时选择 Sixel,再回退为 ANSI。显式 `kitty` 或 `sixel` 模式探测失败时报错,不强制向未知终端输出图像。tmux/screen 中 auto 使用 ANSI,显式像素模式报不支持;不修改用户终端配置。 auto 模式发送 Kitty 查询与主设备属性查询(DA1),等待最多 250ms;优先选择 Kitty,否则在 DA1 的能力参数中找到 `4` 时选择 Sixel,再回退为 ANSI。显式 `kitty` 或 `sixel` 模式探测失败时报错,不强制向未知终端输出图像。tmux/screen 中 auto 使用 ANSI,显式像素模式报不支持;不修改用户终端配置。
@@ -90,6 +90,8 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
`--fps 1..30` 是本地输出目标。交互 CLI 的 ANSI、Sixel、内联 Kitty 和共享内存 Kitty 默认均为 30 FPS;共享内存失败回退时保持该目标。bridge 仍按宿主协议默认使用 8 FPS。 `--fps 1..30` 是本地输出目标。交互 CLI 的 ANSI、Sixel、内联 Kitty 和共享内存 Kitty 默认均为 30 FPS;共享内存失败回退时保持该目标。bridge 仍按宿主协议默认使用 8 FPS。
独立 CLI 与 bridge 的远端预览统一请求清晰度优先(保持分辨率),包括默认 30 FPS、显式 `--fps` 和自动切换预览帧率。提高帧率不会将编码策略切回帧率优先;实际分辨率仍受画质上限、源画面尺寸、被控端支持及编码/传输条件限制。
未显式指定 `--fps` 时,远端预览从 30 FPS 开始,根据完整一帧从转换开始到输出完成的耗时自动选择 8/15/30 FPS 请求;共享内存的上传确认等待也计入,等待源帧的空闲时间不计入。使用平滑估计、不同升降档阈值和至少 8 个新帧样本,持续变慢 2 秒才降档、持续恢复 4 秒才升档,保留供帧余量。15 FPS 仅在对端声明支持时使用;预览未获接受时不追加自动调节。视口变化、显示器切换和共享内存回退会重置耗时估计。显式 `--fps` 固定本地目标和对应的远端预览请求,关闭自动调节;例如 `--fps 30` 可用于与自动策略对比。 未显式指定 `--fps` 时,远端预览从 30 FPS 开始,根据完整一帧从转换开始到输出完成的耗时自动选择 8/15/30 FPS 请求;共享内存的上传确认等待也计入,等待源帧的空闲时间不计入。使用平滑估计、不同升降档阈值和至少 8 个新帧样本,持续变慢 2 秒才降档、持续恢复 4 秒才升档,保留供帧余量。15 FPS 仅在对端声明支持时使用;预览未获接受时不追加自动调节。视口变化、显示器切换和共享内存回退会重置耗时估计。显式 `--fps` 固定本地目标和对应的远端预览请求,关闭自动调节;例如 `--fps 30` 可用于与自动策略对比。
本地调度只在有新帧或需要重绘时消耗刷新时隙;短暂迟到保持原刷新节奏,长时间阻塞后跳过旧时隙,不连续补画过时帧。目标帧率不代表终端实际显示帧率。 本地调度只在有新帧或需要重绘时消耗刷新时隙;短暂迟到保持原刷新节奏,长时间阻塞后跳过旧时隙,不连续补画过时帧。目标帧率不代表终端实际显示帧率。
@@ -125,12 +127,13 @@ macOS 自带 Terminal 还需要在当前窗口勾选“显示 → 允许鼠标
| 退出 CLI | `q` 或 `Ctrl+C` | `Ctrl+]`,再按 `q` | | 退出 CLI | `q` 或 `Ctrl+C` | `Ctrl+]`,再按 `q` |
| 进入本地命令模式 | `?` 或 `Ctrl+]` | `Ctrl+]` | | 进入本地命令模式 | `?` 或 `Ctrl+]` | `Ctrl+]` |
| 放大/缩小 | `+` / `=` / `-` | `Ctrl+]`,再按对应键 | | 放大/缩小 | `+` / `=` / `-` | `Ctrl+]`,再按对应键 |
| 原始细节/完整画面 | `d`(detail)/ `f`(fit),相邻字母键 | `Ctrl+]`,再按对应键 | | 恢复完整画面 | `f`,重置倍率、平移和细节模式 | `Ctrl+]`,再按 `f` |
| 鼠标查看 | 滚轮以鼠标位置缩放;点击进入原始细节 | 进入本地命令模式后仅更新缩放锚点,使用缩放键调整 |
| 平移 | 方向键或 `i` 上、`j` 左、`k` 下、`l` 右 | `Ctrl+]`,再按对应键 | | 平移 | 方向键或 `i` 上、`j` 左、`k` 下、`l` 右 | `Ctrl+]`,再按对应键 |
进入本地命令模式时暂停远端输入,并释放已记录按键/鼠标。执行缩放、平移、`c` 或未知命令后均保持此模式;`c` 切换手动返回后是否启用远端控制,状态栏显示 `CONTROL PAUSED` 或 `READ ONLY`。`Esc` 或 `Ctrl+C` 手动返回所选模式;`q` 退出 CLI。再次按 `Ctrl+]` 也会返回,并在所选模式为控制时向远端发送一次 `Ctrl+]`。命令模式中的鼠标事件被忽略,bracketed paste 内容被丢弃,均不会退出模式或触发快捷键。窗口失焦或输入缓冲区溢出时继续保持本地命令模式,但将返回模式设为只读。控制模式下普通 `c/q/?`、`Ctrl+T`、`Ctrl+C` 和单独 `Esc` 均传给远端。目前不支持重绑定。 进入本地命令模式时暂停远端输入,并释放已记录按键/鼠标。执行缩放、平移、`c` 或未知命令后均保持此模式;`c` 切换手动返回后是否启用远端控制,状态栏显示 `CONTROL PAUSED` 或 `READ ONLY`。`Esc` 或 `Ctrl+C` 手动返回所选模式;`q` 退出 CLI。再次按 `Ctrl+]` 也会返回,并在所选模式为控制时向远端发送一次 `Ctrl+]`。命令模式中的鼠标事件仅更新本地缩放锚点,不执行点击、拖动或滚轮操作;bracketed paste 内容被丢弃,均不会退出模式或触发快捷键。窗口失焦或输入缓冲区溢出时继续保持本地命令模式,但将返回模式设为只读。控制模式下普通 `c/q/?`、`Ctrl+T`、`Ctrl+C` 和单独 `Esc` 均传给远端。目前不支持重绑定。
只读时,`d` 或在画面中单击进入该位置的原始细节模式;`+` / `=` 放大、`-` 缩小(1~32倍),滚轮以鼠标指向为中心调整倍率。方向键或 `i/j/k/l` 分别向上/左/下/右平移,`f` 恢复完整画面。黑边点击被忽略。这些按键和鼠标在控制模式下仍传给远端;进入本地命令模式后可连续调整视图,直至手动返回。放大、平移和只读鼠标定位不会向远端发送操作,也适用于静止画面。 只读时,在画面中单击进入该位置的原始细节模式;`+` / `=` 放大、`-` 缩小(1~32倍)。缩放键与滚轮均以鼠标位置为锚点,尽量保持鼠标下的画面内容在原位置;到达桌面边缘时限制裁剪范围。没有收到鼠标位置,或鼠标位于黑边、状态栏时,缩放键沿用当前中心;窗口失焦会清除鼠标位置。方向键或 `i/j/k/l` 分别向上/左/下/右平移,`f` 恢复完整画面并重置倍率、平移和细节模式。黑边点击被忽略。这些按键和鼠标在控制模式下仍传给远端;进入本地命令模式后可移动鼠标选择锚点并连续按缩放键调整视图,直至手动返回。放大、平移和只读鼠标定位不会向远端发送操作,也适用于静止画面。
支持 SGR 和传统 `CSI M` 鼠标报告,包括移动、左右中键、拖动和滚轮;完整缓冲跨读取分片的传统报告,坐标字节不会作为按键执行。启动时依次启用普通、拖动和全移动报告,兼容只支持部分跟踪模式的终端;退出时关闭这些模式。传统报告的坐标上限为 223,较大窗口应使用支持 SGR 的终端。协议格式依据 [XTerm 鼠标协议](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking)。 支持 SGR 和传统 `CSI M` 鼠标报告,包括移动、左右中键、拖动和滚轮;完整缓冲跨读取分片的传统报告,坐标字节不会作为按键执行。启动时依次启用普通、拖动和全移动报告,兼容只支持部分跟踪模式的终端;退出时关闭这些模式。传统报告的坐标上限为 223,较大窗口应使用支持 SGR 的终端。协议格式依据 [XTerm 鼠标协议](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking)。
@@ -153,7 +156,7 @@ export CROSSDESK_CLI="$PWD/build/macosx/arm64/release/crossdesk-cli"
pi -e ./apps/cli/plugins/pi/index.ts pi -e ./apps/cli/plugins/pi/index.ts
``` ```
在 Pi 输入 `/crossdesk` 或 `/crossdesk 123456789`。先输入 ID,再在自定义遮蔽组件输入密码。扩展使用独立 fd 3 写密码,随后关闭;不写配置、聊天、工具结果或模型上下文。查看组件与独立 CLI 使用相同的 `Ctrl+]` 命令前缀和上述模式按键,由 bridge 统一解析。插件默认请求 high 预览并显示 ANSI 象限字符行,支持同样的只读细节/放大/平移键,颜色深度依据终端环境回退,由 Pi 自身布局和刷新;不直接占用 TTY,不发送 Kitty 图形或自行打开鼠标报告。 在 Pi 输入 `/crossdesk` 或 `/crossdesk 123456789`。先输入 ID,再在自定义遮蔽组件输入密码。扩展使用独立 fd 3 写密码,随后关闭;不写配置、聊天、工具结果或模型上下文。查看组件与独立 CLI 使用相同的 `Ctrl+]` 命令前缀和上述模式按键,由 bridge 统一解析。插件默认请求 high 预览并显示 ANSI 象限字符行,支持同样的缩放、全景和平移键,颜色深度依据终端环境回退,由 Pi 自身布局和刷新;不直接占用 TTY,不发送 Kitty 图形或自行打开鼠标报告。
Pi 扩展读取宿主的字符尺寸,并通过宿主终端写入/输入监听接口查询字符尺寸或完整文本区尺寸,每两秒及窗口变化时刷新;尺寸回复不会传给远端。得到的宽高比通过 bridge 的 `viewport.cell_aspect` 传入,比例变化时重绘静止画面,并丢弃仍在途的旧比例帧。自动检测不可用或不准确时,可在启动 Pi 前设置环境变量 `CROSSDESK_CELL_ASPECT=0.45`(宽/高,允许 0.2~2;`auto` 恢复自动检测)。视图关闭或 bridge 停止时移除查询定时器和输入监听。 Pi 扩展读取宿主的字符尺寸,并通过宿主终端写入/输入监听接口查询字符尺寸或完整文本区尺寸,每两秒及窗口变化时刷新;尺寸回复不会传给远端。得到的宽高比通过 bridge 的 `viewport.cell_aspect` 传入,比例变化时重绘静止画面,并丢弃仍在途的旧比例帧。自动检测不可用或不准确时,可在启动 Pi 前设置环境变量 `CROSSDESK_CELL_ASPECT=0.45`(宽/高,允许 0.2~2;`auto` 恢复自动检测)。视图关闭或 bridge 停止时移除查询定时器和输入监听。
+9 -2
View File
@@ -22,12 +22,19 @@
新增回归代码、基准、假传输和验证产物均位于工程外临时目录,未向工程或子模块添加测试。此次完整构建与运行验证覆盖 macOS arm64,未重新进行 Linux/Windows 实机验证。 新增回归代码、基准、假传输和验证产物均位于工程外临时目录,未向工程或子模块添加测试。此次完整构建与运行验证覆盖 macOS arm64,未重新进行 Linux/Windows 实机验证。
## 控制按键调整(2026-10-30) ## 鼠标锚点缩放与快捷键提示(2026-11-02)
- 移除 `d` 本地快捷键,保留鼠标点击进入细节及 `--zoom native`。`f` 恢复完整桌面并重置倍率、平移和细节模式;`+/-`、`=` 和只读滚轮使用鼠标锚点缩放。
- 工程外 ASan/UBSan 检查通过 900 次缩放几何检查,覆盖 ASCII、ANSI half/quadrant、Kitty/Sixel 视口、全景/细节、连续输入、边缘限制、SGR/传统鼠标报告、失焦与模式切换;确认 `d` 不再改变本地视图,控制模式仍转发普通字母。
- 四种控制/本地模式组合、1~240 列共 960 组状态栏布局检查通过,覆盖完整按键标签、缩放/全景提示及模式可用操作。Pi 提示通过 Node.js 语法检查;没有进行真实 Pi TUI 或远端设备连接验证。
- 新增验证代码及产物均保存在工程外临时目录。
## 控制按键调整(2026-10-30,历史记录)
- 当前桌面与 CLI 同时启用的 debug 配置下,`xmake build crossdesk-cli`、`xmake run crossdesk-cli doctor --json` 和命令帮助检查通过。 - 当前桌面与 CLI 同时启用的 debug 配置下,`xmake build crossdesk-cli`、`xmake run crossdesk-cli doctor --json` 和命令帮助检查通过。
- 工程外 ASan/UBSan 检查覆盖本地命令前缀、只读快捷键、远端 Ctrl+T/Ctrl+C/Ctrl+]、鼠标释放、取消命令、粘贴隔离、UTF-8/CSI 分片、失焦与退出;原有会话/渲染检查通过。 - 工程外 ASan/UBSan 检查覆盖本地命令前缀、只读快捷键、远端 Ctrl+T/Ctrl+C/Ctrl+]、鼠标释放、取消命令、粘贴隔离、UTF-8/CSI 分片、失焦与退出;原有会话/渲染检查通过。
- 平移字母键采用 `i` 上、`j` 左、`k` 下、`l` 右;工程外 ASan/UBSan 检查确认四个方向与普通/应用模式方向键一致,控制模式中普通字母仍传给远端。 - 平移字母键采用 `i` 上、`j` 左、`k` 下、`l` 右;工程外 ASan/UBSan 检查确认四个方向与普通/应用模式方向键一致,控制模式中普通字母仍传给远端。
- 细节/全景采用相邻的 `d`/`f`;工程外 ASan/UBSan 检查确认细节保留中心、全景复位缩放与中心、前缀命令和鼠标点击行为正确,控制模式中普通字母仍传给远端。 - 当时细节/全景采用相邻的 `d`/`f`(`d` 已于 2026-11-02 移除);工程外 ASan/UBSan 检查确认细节保留中心、全景复位缩放与中心、前缀命令和鼠标点击行为正确,控制模式中普通字母仍传给远端。
- 实际二进制 bridge 验证命令状态、错误恢复和退出响应;生产 view 配合假传输的 PTY 检查验证快捷键、模式提示、隐藏密码和终端恢复。实际二进制的 bridge 与 PTY 回归同样通过。 - 实际二进制 bridge 验证命令状态、错误恢复和退出响应;生产 view 配合假传输的 PTY 检查验证快捷键、模式提示、隐藏密码和终端恢复。实际二进制的 bridge 与 PTY 回归同样通过。
- Pi 1.4.0 SDK 严格类型检查和模拟组件运行通过,覆盖控制按键转发、命令提示、取消、缩放、退出与 bridge 启动失败后的关闭操作。 - Pi 1.4.0 SDK 严格类型检查和模拟组件运行通过,覆盖控制按键转发、命令提示、取消、缩放、退出与 bridge 启动失败后的关闭操作。
+6 -2
View File
@@ -608,7 +608,7 @@ int View(SessionOptions options, const std::string &device,
: sixel ? "SIXEL" : sixel ? "SIXEL"
: detail == AnsiDetail::Quadrant ? "QUAD" : detail == AnsiDetail::Quadrant ? "QUAD"
: "HALF") + : "HALF") +
(input.native ? " | Detail x" : " | Fit x") + (input.native ? " | Detail x" : " | Zoom x") +
std::to_string(int(input.zoom)) + std::to_string(int(input.zoom)) +
(last_frame ? " | " + std::to_string(last_frame->width) + "x" + (last_frame ? " | " + std::to_string(last_frame->width) + "x" +
std::to_string(last_frame->height) std::to_string(last_frame->height)
@@ -1038,10 +1038,14 @@ crossdesk-cli bridge --stdio [--password-fd 3] [--server host] [--port 9099]
Connect by device ID: read-only and silent by default; hidden password prompt on the local terminal. Connect by device ID: read-only and silent by default; hidden password prompt on the local terminal.
Read-only: c enables control; q or Ctrl+C quits; ? shows local commands. Read-only: c enables control; q or Ctrl+C quits; ? shows local commands.
Ctrl+] enters persistent local mode: c selects control on return; q quits; Esc/Ctrl+C returns. Ctrl+] enters persistent local mode: c selects control on return; q quits; Esc/Ctrl+C returns.
View keys: click/d native detail, wheel/+/- zoom, arrows/ijkl pan (i up, j left, k down, l right), f fit. View keys (read-only/local): +/- zoom at mouse (= also zooms in); f restores the full desktop and resets pan.
Pan: arrows or i up, j left, k down, l right. Read-only mouse: wheel zooms at pointer; click opens native detail.
Local mode: move the mouse to choose the zoom anchor; clicks and wheel do not change the view.
Zoom uses the current center when no mouse position is available or the pointer is outside the picture.
Local mode stays active after commands, paste or focus loss; remote input remains paused. Local mode stays active after commands, paste or focus loss; remote input remains paused.
Ctrl+T and Ctrl+C pass through in control mode; another Ctrl+] leaves local mode and forwards Ctrl+] if control is enabled. Ctrl+T and Ctrl+C pass through in control mode; another Ctrl+] leaves local mode and forwards Ctrl+] if control is enabled.
Default ANSI detail: quadrant. Default quality: high (1920x1080); ultra requests up to 3840x2160. Default ANSI detail: quadrant. Default quality: high (1920x1080); ultra requests up to 3840x2160.
Remote previews prioritize resolution at every frame rate.
Cell aspect is character width/height: auto detects terminal geometry; use --cell-aspect to calibrate. 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. 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. Auto rendering prefers Kitty, then Sixel, then ANSI. Explicit pixel renderers require a successful probe.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@chunkdash/crossdesk-cli", "name": "@chunkdash/crossdesk-cli",
"version": "0.1.5", "version": "0.1.6",
"private": true, "private": true,
"description": "CrossDesk remote desktop controller for the terminal", "description": "CrossDesk remote desktop controller for the terminal",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
+3 -3
View File
@@ -227,10 +227,10 @@ export default function crossdesk(pi: ExtensionAPI) {
const ack = frame; frame = undefined; const ack = frame; frame = undefined;
if (ack) queueMicrotask(() => send("ack", ack)); // Host consumed the frame; not a physical display timestamp. if (ack) queueMicrotask(() => send("ack", ack)); // Host consumed the frame; not a physical display timestamp.
const hint = bridgeStopped ? "q / Esc / Ctrl+C close" const hint = bridgeStopped ? "q / Esc / Ctrl+C close"
: commandMode ? "Esc/Ctrl+C return | c toggle q quit | arrows/ijkl pan +/- zoom f fit d detail" : commandMode ? "Esc/Ctrl+C return | c toggle | q quit | +/- zoom | f full | arrows/ijkl pan"
: control ? "Keys -> remote | Ctrl+] then c release, q quit" : control ? "Keys -> remote | Ctrl+] then c release, q quit"
: "c control q quit ? help | i up j left k down l right | +/- zoom f fit d detail"; : "c control | q quit | +/- zoom | f full | arrows/ijkl pan | ? commands";
return [truncateToWidth(`CrossDesk ${device} | ${status} ${native ? "detail" : "fit"} x${zoom} | Ctrl+] commands`, width), return [truncateToWidth(`CrossDesk ${device} | ${status} ${native ? "detail" : "zoom"} x${zoom} | Ctrl+] commands`, width),
...lines.slice(0, h).map(line => truncateToWidth(line, width)), ...lines.slice(0, h).map(line => truncateToWidth(line, width)),
truncateToWidth(`${hint}${error ? " | " + error : lines.length ? "" : " | Waiting for remote video…"}`, width)]; truncateToWidth(`${hint}${error ? " | " + error : lines.length ? "" : " | Waiting for remote video…"}`, width)];
}, },
+1 -1
View File
@@ -32,7 +32,7 @@ target_end()
target("crossdesk-cli") target("crossdesk-cli")
set_kind("binary") set_kind("binary")
set_version("0.1.5") set_version("0.1.6")
add_deps("crossdesk_terminal") add_deps("crossdesk_terminal")
add_files("main.cpp") add_files("main.cpp")
add_files(is_plat("windows") and "terminal_io_windows.cpp" or "terminal_io_posix.cpp") add_files(is_plat("windows") and "terminal_io_windows.cpp" or "terminal_io_posix.cpp")
@@ -342,6 +342,9 @@ void ControllerSession::Preview() {
// 60 is the existing interactive-mode sentinel, not a 60 FPS request. // 60 is the existing interactive-mode sentinel, not a 60 FPS request.
// The encoder cadence is independently specified by frame_rate. // The encoder cadence is independently specified by frame_rate.
s.frame_rate = 30; s.frame_rate = 30;
// Preserve desktop text detail at every cadence, including 30 FPS previews.
s.preference =
static_cast<int>(VideoDegradationPreference::MaintainResolution);
s.audio = false; s.audio = false;
s.quality = options_.quality; s.quality = options_.quality;
if (resolution_) { if (resolution_) {
@@ -58,6 +58,8 @@ struct Grid {
Rect Fit(int width, int height, int columns, int pixel_rows, Rect Fit(int width, int height, int columns, int pixel_rows,
double pixel_aspect = 1); double pixel_aspect = 1);
Grid Layout(const DecodedFrame &, Viewport); Grid Layout(const DecodedFrame &, Viewport);
// Calculate crop and pointer geometry without decoded pixel storage.
Grid Layout(unsigned source_width, unsigned source_height, Viewport);
Grid Convert(const DecodedFrame &, Viewport); Grid Convert(const DecodedFrame &, Viewport);
std::optional<std::pair<float, float>> MapPointer(const Grid &, int column, std::optional<std::pair<float, float>> MapPointer(const Grid &, int column,
int row); int row);
@@ -119,9 +121,11 @@ private:
bool pasting_ = false; bool pasting_ = false;
unsigned mouse_buttons_ = 0; unsigned mouse_buttons_ = 0;
float pointer_x_ = .5f, pointer_y_ = .5f; float pointer_x_ = .5f, pointer_y_ = .5f;
int mouse_column_ = -1, mouse_row_ = -1;
void Tap(ControllerSession &, int key, int modifier = 0); void Tap(ControllerSession &, int key, int modifier = 0);
bool LocalKey(int key, ControllerSession &, const Grid *); bool LocalKey(int key, ControllerSession &, const Grid *);
void Navigate(char key, const Grid *); void Navigate(char key, const Grid *);
void Zoom(double next_zoom, const Grid *);
void Mouse(int button, int x, int y, bool release, ControllerSession &, void Mouse(int button, int x, int y, bool release, ControllerSession &,
const Grid *); const Grid *);
}; };
+56 -12
View File
@@ -45,8 +45,7 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
} else if (key == 'c') { } else if (key == 'c') {
control = !control; control = !control;
} else if (key == '+' || key == '=' || key == '-' || key == 'f' || } else if (key == '+' || key == '=' || key == '-' || key == 'f' ||
key == 'd' || key == 'i' || key == 'j' || key == 'k' || key == 'i' || key == 'j' || key == 'k' || key == 'l') {
key == 'l') {
Navigate(char(key), grid); Navigate(char(key), grid);
} else if (!command_mode) { } else if (!command_mode) {
return false; return false;
@@ -58,20 +57,52 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
mouse_buttons_ = 0; mouse_buttons_ = 0;
return true; return true;
} }
void Input::Zoom(double next_zoom, const Grid *grid) {
if (next_zoom == zoom)
return;
if (grid && mouse_column_ >= 0 && mouse_row_ >= 0) {
auto v = grid->viewport;
v.zoom = zoom;
v.native = native;
v.center_x = center_x;
v.center_y = center_y;
// Rebuild from the requested view so queued key/wheel repeats compose
// even when the terminal is still displaying an earlier frame.
const auto before = Layout(grid->source_width, grid->source_height, v);
if (const auto anchor = MapPointer(before, mouse_column_, mouse_row_)) {
v.zoom = next_zoom;
const auto after = Layout(grid->source_width, grid->source_height, v);
if (const auto moved = MapPointer(after, mouse_column_, mouse_row_)) {
const auto center = [](int origin, int size, unsigned extent,
double shift) {
const double half = size * .5 / extent;
return std::clamp((origin + size * .5 + shift * (extent - 1)) /
extent,
half, 1 - half);
};
// Use the actual, clamped crop center and the same source-pixel
// mapping as remote input. Letterboxing and status rows stay aligned.
center_x = center(after.source.x, after.source.width,
after.source_width,
double(anchor->first) - moved->first);
center_y = center(after.source.y, after.source.height,
after.source_height,
double(anchor->second) - moved->second);
}
}
}
zoom = next_zoom;
}
void Input::Navigate(char key, const Grid *grid) { void Input::Navigate(char key, const Grid *grid) {
if (key == '+' || key == '=') if (key == '+' || key == '=')
zoom = std::min(32.0, zoom * 2); Zoom(std::min(32.0, zoom * 2), grid);
if (key == '-') if (key == '-')
zoom = std::max(1.0, zoom / 2); Zoom(std::max(1.0, zoom / 2), grid);
if (key == 'f') { if (key == 'f') {
native = false; native = false;
zoom = 1; zoom = 1;
center_x = center_y = .5; center_x = center_y = .5;
} }
if (key == 'd') {
native = true;
zoom = 1;
}
// Use the displayed crop as the pan step, including native pixel mode. // Use the displayed crop as the pan step, including native pixel mode.
const double dx = grid && grid->source_width const double dx = grid && grid->source_width
? double(grid->source.width) / grid->source_width ? double(grid->source.width) / grid->source_width
@@ -99,14 +130,26 @@ void Input::Mouse(int b, int x, int y, bool release, ControllerSession &s,
const Grid *grid) { const Grid *grid) {
++mouse_reports; ++mouse_reports;
// Extra mouse buttons must not alias the three supported remote buttons. // Extra mouse buttons must not alias the three supported remote buttons.
if (!grid || command_mode || b < 0 || b >= 128) if (b < 0 || b >= 128 || x <= 0 || y <= 0)
return; return;
auto pos = MapPointer(*grid, x - 1, y - 1); // Track terminal coordinates even in local command mode. Re-map them for
// every zoom; a stationary mouse must follow subsequent view changes.
mouse_column_ = x - 1;
mouse_row_ = y - 1;
if (!grid || command_mode)
return;
auto pos = MapPointer(*grid, mouse_column_, mouse_row_);
if (!control) { if (!control) {
if (pos && !release && ((b & 64) || b == 0)) { if (pos && !release) {
if (b & 64) {
Navigate((b & 1) ? '-' : '+', grid);
} else if (b == 0) {
center_x = pos->first; center_x = pos->first;
center_y = pos->second; center_y = pos->second;
Navigate((b & 64) ? ((b & 1) ? '-' : '+') : 'd', grid); native = true;
zoom = 1;
++view_revision;
}
} }
return; return;
} }
@@ -248,6 +291,7 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
control = false; control = false;
s.SetControl(false); s.SetControl(false);
mouse_buttons_ = 0; mouse_buttons_ = 0;
mouse_column_ = mouse_row_ = -1;
continue; continue;
} }
int key = 0; int key = 0;
+24 -14
View File
@@ -25,21 +25,25 @@ static uint32_t Pixel(const DecodedFrame &f, int x, int y) {
(clamp((298 * yy - 100 * u - 208 * v + 128) >> 8) << 8) | (clamp((298 * yy - 100 * u - 208 * v + 128) >> 8) << 8) |
clamp((298 * yy + 516 * u + 128) >> 8); clamp((298 * yy + 516 * u + 128) >> 8);
} }
static void Validate(const DecodedFrame &f, const Viewport &v) { static void Validate(unsigned width, unsigned height, const Viewport &v) {
if (v.columns < 1 || v.rows < 1 || v.columns > 240 || v.rows > 100 || if (v.columns < 1 || v.rows < 1 || v.columns > 240 || v.rows > 100 ||
v.row_offset < 0 || v.row_offset > 100 || v.row_offset < 0 || v.row_offset > 100 ||
!std::isfinite(v.cell_aspect) || v.cell_aspect < .2 || !std::isfinite(v.cell_aspect) || v.cell_aspect < .2 ||
v.cell_aspect > 2 || !std::isfinite(v.zoom) || v.zoom < 1 || v.cell_aspect > 2 || !std::isfinite(v.zoom) || v.zoom < 1 ||
v.zoom > 32 || !std::isfinite(v.center_x) || !std::isfinite(v.center_y) || v.zoom > 32 || !std::isfinite(v.center_x) || !std::isfinite(v.center_y) ||
v.center_x < 0 || v.center_x > 1 || v.center_y < 0 || v.center_y > 1 || v.center_x < 0 || v.center_x > 1 || v.center_y < 0 || v.center_y > 1 ||
!f.width || !f.height || f.width > 8192 || f.height > 8192 || !width || !height || width > 8192 || height > 8192 || (width & 1) ||
(f.width & 1) || (f.height & 1) || (height & 1) || size_t(width) * height > 16777216)
size_t(f.width) * f.height > 16777216 ||
f.nv12.size() < size_t(f.width) * f.height * 3 / 2)
throw std::invalid_argument("invalid frame or viewport"); throw std::invalid_argument("invalid frame or viewport");
} }
static Rect Crop(const DecodedFrame &f, const Viewport &v) { static void Validate(const DecodedFrame &f, const Viewport &v) {
double width = f.width, height = f.height; Validate(f.width, f.height, v);
if (f.nv12.size() < size_t(f.width) * f.height * 3 / 2)
throw std::invalid_argument("invalid frame or viewport");
}
static Rect Crop(unsigned source_width, unsigned source_height,
const Viewport &v) {
double width = source_width, height = source_height;
if (v.native) { if (v.native) {
const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1; const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1;
const int sy = v.utf8 ? 2 : 1; const int sy = v.utf8 ? 2 : 1;
@@ -60,8 +64,10 @@ static Rect Crop(const DecodedFrame &f, const Viewport &v) {
} }
int w = std::max(1, int(width / v.zoom)), int w = std::max(1, int(width / v.zoom)),
h = std::max(1, int(height / v.zoom)); h = std::max(1, int(height / v.zoom));
int x = std::clamp(int(v.center_x * f.width - w * .5), 0, int(f.width) - w); int x = std::clamp(int(v.center_x * source_width - w * .5), 0,
int y = std::clamp(int(v.center_y * f.height - h * .5), 0, int(f.height) - h); int(source_width) - w);
int y = std::clamp(int(v.center_y * source_height - h * .5), 0,
int(source_height) - h);
return {x, y, w, h}; return {x, y, w, h};
} }
// Area resampling integrates the entire source footprint. Thin lines cannot // Area resampling integrates the entire source footprint. Thin lines cannot
@@ -249,16 +255,20 @@ static void AppendNumber(std::string &out, uint32_t value) {
} }
Grid Layout(const DecodedFrame &f, Viewport v) { Grid Layout(const DecodedFrame &f, Viewport v) {
Validate(f, v); Validate(f, v);
return Layout(f.width, f.height, v);
}
Grid Layout(unsigned source_width, unsigned source_height, Viewport v) {
Validate(source_width, source_height, v);
const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1, const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1,
sy = v.utf8 ? 2 : 1; sy = v.utf8 ? 2 : 1;
const int width = v.columns * sx, height = v.rows * sy; const int width = v.columns * sx, height = v.rows * sy;
Rect crop = Crop(f, v); Rect crop = Crop(source_width, source_height, v);
Grid g{v, Grid g{v,
Fit(crop.width, crop.height, width, height, v.cell_aspect * sy / sx), Fit(crop.width, crop.height, width, height, v.cell_aspect * sy / sx),
{}}; {}};
g.source = crop; g.source = crop;
g.source_width = f.width; g.source_width = source_width;
g.source_height = f.height; g.source_height = source_height;
g.samples_x = sx; g.samples_x = sx;
g.samples_y = sy; g.samples_y = sy;
return g; return g;
@@ -553,7 +563,7 @@ PixelFrame ImagePixels(const DecodedFrame &f, const Viewport &requested) {
pixels.height = pixels.height =
std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080); std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080);
pixels.rgb.resize(size_t(pixels.width) * pixels.height * 3, 0); pixels.rgb.resize(size_t(pixels.width) * pixels.height * 3, 0);
const auto crop = Crop(f, v); const auto crop = Crop(f.width, f.height, v);
const auto r = Fit(crop.width, crop.height, pixels.width, pixels.height, const auto r = Fit(crop.width, crop.height, pixels.width, pixels.height,
double(v.columns) * v.cell_aspect * pixels.height / double(v.columns) * v.cell_aspect * pixels.height /
(v.rows * pixels.width)); (v.rows * pixels.width));
@@ -678,7 +688,7 @@ std::string Kitty(const DecodedFrame &f, const Viewport &requested,
std::clamp(v.pixel_width > 0 ? v.pixel_width : v.columns * 10, 2, 1920); std::clamp(v.pixel_width > 0 ? v.pixel_width : v.columns * 10, 2, 1920);
int h = int h =
std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080); std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080);
const auto crop = Crop(f, v); const auto crop = Crop(f.width, f.height, v);
std::string b64; std::string b64;
for (;;) { for (;;) {
auto r = Fit(crop.width, crop.height, w, h, auto r = Fit(crop.width, crop.height, w, h,
+38 -20
View File
@@ -79,13 +79,17 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
std::string keys, view; std::string keys, view;
if (inner >= 63) { if (inner >= 63) {
const size_t count = inner >= 99 ? 4 : inner >= 77 ? 3 : 2; const size_t count = inner >= 99 ? 4 : inner >= 89 ? 3 : 2;
if (bar.command_mode) { if (bar.command_mode) {
keys = keys = Columns(
Columns("LOCAL", "LOCAL",
count >= 3 ? std::vector<std::string>{"<Esc/Ctrl+C> Return", count == 4
"<c> Toggle control", ? std::vector<std::string>{"<Esc/Ctrl+C> Return",
"<q> Quit"} "<c> Toggle control", "<q> Quit",
"<mouse> Set anchor"}
: count == 3
? std::vector<std::string>{"<Esc/Ctrl+C> Return",
"<c> Toggle control", "<q> Quit"}
: std::vector<std::string>{"<Esc/Ctrl+C> Return", : std::vector<std::string>{"<Esc/Ctrl+C> Return",
"<c> Toggle; <q> Quit"}, "<c> Toggle; <q> Quit"},
inner, count); inner, count);
@@ -93,10 +97,14 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
keys = Columns("KEYS", {"<Ctrl+]> Commands", "Input -> remote"}, inner, keys = Columns("KEYS", {"<Ctrl+]> Commands", "Input -> remote"}, inner,
count); count);
} else { } else {
keys = Columns("KEYS", keys = Columns(
count >= 3 "KEYS",
count == 4
? std::vector<std::string>{"<c> Control", "<q> Quit", ? std::vector<std::string>{"<c> Control", "<q> Quit",
"<Ctrl+]> Commands"} "<Ctrl+]> Commands", "<click> Detail"}
: count == 3
? std::vector<std::string>{"<c> Control; <q> Quit",
"<Ctrl+]> Commands", "<click> Detail"}
: std::vector<std::string>{"<c> Control; <q> Quit", : std::vector<std::string>{"<c> Control; <q> Quit",
"<Ctrl+]> Commands"}, "<Ctrl+]> Commands"},
inner, count); inner, count);
@@ -104,16 +112,19 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
if (bar.control && !bar.command_mode) { if (bar.control && !bar.command_mode) {
view = Columns("THEN", {"<c> Toggle control", "<q> Quit"}, inner, count); view = Columns("THEN", {"<c> Toggle control", "<q> Quit"}, inner, count);
} else { } else {
const auto zoom = bar.command_mode ? "<+/-> Zoom at mouse"
: "<+/wheel/-> Zoom at mouse";
view = Columns( view = Columns(
"VIEW", "VIEW",
count == 4 count == 4
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom", ? std::vector<std::string>{"<+/-> Zoom at mouse",
"<f> Fit", "<d> Detail"} bar.command_mode ? "<=> Zoom in"
: "<wheel> Zoom",
"<arrows/ijkl> Pan", "<f> Full view"}
: count == 3 : count == 3
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom", ? std::vector<std::string>{zoom, "<arrows/ijkl> Pan",
"<f> Fit; <d> Detail"} "<f> Full view"}
: std::vector<std::string>{"<arrows> Pan; <+/-> Zoom", : std::vector<std::string>{zoom, "<arrows> Pan; <f> Full"},
"<f> Fit; <d> Detail"},
inner, count); inner, count);
} }
} else { } else {
@@ -139,11 +150,18 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
? std::vector<std::string>{"<c> Toggle control | <q> Quit", ? std::vector<std::string>{"<c> Toggle control | <q> Quit",
"<c> Toggle; <q> Quit", "<c> / <q>", "<c> Toggle; <q> Quit", "<c> / <q>",
"<q>", "q"} "<q>", "q"}
: std::vector<std::string>{"<arrows> Pan | <+/-> Zoom | <f> Fit | " : bar.command_mode
"<d> Detail", ? std::vector<
"<+/-> Zoom | <f> Fit | <d> Detail", std::string>{"<+/-> Zoom at mouse | <arrows> Pan | <f> Full",
"<f> Fit | <d> Detail", "<f> Fit", "<f>", "<+/-> Zoom at mouse | <f> Full",
"f"}, "<+/-> Zoom | <f> Full", "<+/-> | <f>", "<+/->",
"+"}
: std::vector<std::string>{"<+/wheel/-> Zoom at mouse | <arrows> "
"Pan | <f> Full",
"<+/wheel/-> Zoom at mouse | <f> Full",
"<+/wheel/-> Zoom | <f> Full",
"<+/-> Zoom | <f> Full", "<+/-> | <f>",
"<+/->", "+"},
inner); inner);
} }
std::vector<std::string> lines{status, keys, view}; std::vector<std::string> lines{status, keys, view};