[feat] improve CLI commands, controls and npm packaging

This commit is contained in:
dijunkun
2026-10-30 13:32:34 +08:00
parent 5d0a8f857f
commit e6e7e4c0f0
19 changed files with 359 additions and 102 deletions
+10 -10
View File
@@ -29,14 +29,8 @@ jobs:
- runner: windows-2022 - runner: windows-2022
target: win32-x64 target: win32-x64
arch: x64 arch: x64
- runner: windows-11-arm
target: win32-arm64
arch: arm64
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
timeout-minutes: 120 timeout-minutes: 120
defaults:
run:
shell: bash
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -58,12 +52,18 @@ jobs:
run: sh apps/cli/scripts/prepare-deps.sh run: sh apps/cli/scripts/prepare-deps.sh
- name: Configure macOS - name: Configure macOS
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: xmake f -y -a ${{ matrix.arch }} -m release --target_minver=13.0 --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n run: xmake f -y -p macosx -a ${{ matrix.arch }} -m release --target_minver=13.0 --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n
- name: Configure Linux or Windows - name: Configure Linux
if: runner.os != 'macOS' if: runner.os == 'Linux'
run: xmake f -y -a ${{ matrix.arch }} -m release --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n run: xmake f -y -p linux -a ${{ matrix.arch }} -m release --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n
- name: Configure Windows
if: runner.os == 'Windows'
run: xmake f -y -p windows -a ${{ matrix.arch }} -m release --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n
- name: Build - name: Build
run: xmake build crossdesk-cli run: xmake build crossdesk-cli
- name: Sign macOS CLI
if: runner.os == 'macOS'
run: codesign --force --sign - build/macosx/${{ matrix.arch }}/release/crossdesk-cli
- name: Report native environment - name: Report native environment
run: xmake run crossdesk-cli doctor --json run: xmake run crossdesk-cli doctor --json
- name: Package native program and npm entry - name: Package native program and npm entry
+60 -6
View File
@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk 标志:图标在左,双色文字在右" /> <img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk 标志:图标在左,双色文字在右" />
<p>轻量、跨平台的远程桌面,支持电脑、浏览器和移动端连接,用桌面墙集中查看与操控多台设备。</p> <p>轻量、跨平台的远程桌面,支持电脑、浏览器、移动端和终端连接,用桌面墙集中查看与操控多台设备。</p>
<p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p> <p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p>
</div> </div>
@@ -19,9 +19,9 @@
</div> </div>
CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。 CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也能通过 **桌面墙(DeskHub)** 集中查看与操控多台设备。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),支持自托管。
[主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [IP 直连](#lan-direct) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor) [主要功能](#features) · [下载与平台支持](#download) · [快速连接](#quick-start) · [终端 CLI](#cli) · [IP 直连](#lan-direct) · [界面预览](#preview) · [使用指南](docs/USER_GUIDE.md) · [自托管](#self-hosting) · [构建与反馈](#development) · [赞助](#sponsor)
<a id="features"></a> <a id="features"></a>
@@ -29,6 +29,7 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。 - **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。 - **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
- **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。 - **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。 - **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。 - **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
@@ -50,8 +51,9 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
| 鸿蒙 PC(开发版) | **控制 / 被控**;API 26,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 | | 鸿蒙 PC(开发版) | **控制 / 被控**;API 26,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 |
| 鸿蒙手机 / 平板(开发版) | **仅控制**;API 20+,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 | | 鸿蒙手机 / 平板(开发版) | **仅控制**;API 20+,arm64;使用 DevEco 构建并签名 HAP,仍在进行真机验收 |
| Web | **仅控制**;支持 WebRTC 的浏览器;无需在控制端安装客户端 | | Web | **仅控制**;支持 WebRTC 的浏览器;无需在控制端安装客户端 |
| 终端 CLI(开发版) | **仅控制**;构建目标为 macOS / Linux(x64、arm64)和 Windows(x64);从源码构建,平台验收范围见 [CLI 说明](apps/cli/README.md) |
系统要求按当前源码与构建配置整理,具体安装包以对应 Release 说明为准。各客户端的功能范围见 [移动端与鸿蒙](#mobile)。 系统要求按当前源码与构建配置整理,具体安装包以对应 Release 说明为准。各客户端的功能范围见 [终端 CLI](#cli) 与 [移动端与鸿蒙](#mobile)。
- **Linux:** 已有桌面选普通版 `crossdesk-linux-…deb`;需要独立 Xfce 桌面的服务器选 `crossdesk-virtual-desktop-xfce-…deb`。两者选择其一,安装与启动方法见 [使用指南](docs/USER_GUIDE.md#installation) 和 [无头运行](docs/HEADLESS.md)。 - **Linux:** 已有桌面选普通版 `crossdesk-linux-…deb`;需要独立 Xfce 桌面的服务器选 `crossdesk-virtual-desktop-xfce-…deb`。两者选择其一,安装与启动方法见 [使用指南](docs/USER_GUIDE.md#installation) 和 [无头运行](docs/HEADLESS.md)。
- **macOS:** 首次运行时按提示授予屏幕录制及辅助功能权限;前者用于桌面采集,后者用于远程键鼠输入。[权限说明](docs/USER_GUIDE.md#installation) - **macOS:** 首次运行时按提示授予屏幕录制及辅助功能权限;前者用于桌面采集,后者用于远程键鼠输入。[权限说明](docs/USER_GUIDE.md#installation)
@@ -122,6 +124,59 @@ CrossDesk 让你从电脑、浏览器或手机连接远端电脑,也能通过
- [客户端设置与更新](docs/USER_GUIDE.md#settings):常规设置、桌面墙传输及版本检查。 - [客户端设置与更新](docs/USER_GUIDE.md#settings):常规设置、桌面墙传输及版本检查。
- [桌面墙技术说明](docs/deskhub.md):预览订阅、带宽分配、协议与兼容性。 - [桌面墙技术说明](docs/deskhub.md):预览订阅、带宽分配、协议与兼容性。
<a id="cli"></a>
## 终端客户端 CLI
`crossdesk-cli` 在终端中通过 9 位设备 ID 连接被控电脑,默认只读、静音。普通终端使用 ANSI 字符画面,支持 Kitty 图形协议的终端可显示像素画面;可切换显示器、调整画质、放大细节和平移,也可明确开启键鼠控制。CLI 可以独立于桌面 GUI 构建,被控电脑仍需运行 CrossDesk 并提供可捕获的图形会话,信令服务需支持 `cli` 临时身份注册。
### 从源码构建并连接
准备 Git、Xmake 3.1.1 或以上、C++17 编译工具链及对应平台依赖。以下以 macOS / Linux shell 为例;Windows 的 Git Bash / 开发者 PowerShell 步骤见 [CLI 构建说明](apps/cli/docs/terminal-client.md#构建与安装)。
```sh
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
./apps/cli/scripts/prepare-deps.sh
xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release
xmake build crossdesk-cli
xmake run crossdesk-cli doctor --json
xmake run crossdesk-cli 123456789
```
已有仓库时从仓库根目录执行依赖准备及后续命令。将 `123456789` 换成被控电脑的 ID,密码在连接时隐藏输入。`doctor` 只检查本机环境,不发起连接。仓库默认构建桌面端;以上配置只构建 CLI,需要同时构建桌面端时将 `CROSSDESK_BUILD_DESKTOP` 设为 `y`。产物位于 `build/<platform>/<arch>/release/`。
ANSI 全景受终端字符网格限制。阅读小字可添加 `--quality high --zoom native`,或进入画面后按 `d` 查看细节;`--renderer kitty` 需要终端支持对应协议。显示、鼠标报告、SSH 和终端兼容性见 [使用说明](apps/cli/docs/terminal-client.md)。
### 控制、查看与退出
| 模式 / 按键 | 行为 |
| --- | --- |
| 只读模式 | `c` 开启远端控制;`q` 或 `Ctrl+C` 退出 CLI |
| 控制模式 | 键鼠输入传给远端;`Ctrl+]` 进入本地命令模式并暂停远端输入 |
| 本地命令模式 | 执行命令后持续保持;`c` 选择手动返回后是否启用控制,`q` 退出 CLI |
| `Esc` / `Ctrl+C` | 在本地命令模式中手动返回所选的控制或只读模式 |
| `+` / `=` / `-` | 在只读或本地命令模式中放大 / 缩小 |
| `d` / `f` | 在只读或本地命令模式中切换原始细节 / 完整桌面;只读时也可点击目标进入细节 |
| 方向键 / `i j k l` | 在只读或本地命令模式中平移;`i` 上、`j` 左、`k` 下、`l` 右 |
`Ctrl+]` 模式不会因为缩放、平移或按 `c` 自动结束。状态栏中的 `CONTROL PAUSED` / `READ ONLY` 表示返回后是否启用控制。控制模式中的普通 `c`、`q` 和 `Ctrl+C` 会发送到远端,需要先按 `Ctrl+]` 执行本地操作;再次按 `Ctrl+]` 也可返回,若返回控制模式则向远端发送一次该组合键。
### Pi 扩展与功能范围
CLI 附带 Pi 自定义 TUI 扩展,使用同样的输入模式。在仓库根目录启动,例如 macOS Apple Silicon 的 release 构建:
```sh
export CROSSDESK_CLI="$PWD/build/macosx/arm64/release/crossdesk-cli"
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 直连、音频播放、文件传输或剪贴板同步。macOS arm64 已完成本地构建和连接验证;Linux / Windows 完整运行、SSH / Kitty 实机显示及真实 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)
<a id="mobile"></a> <a id="mobile"></a>
## 移动端与鸿蒙 ## 移动端与鸿蒙
@@ -169,8 +224,7 @@ PC 端可控制和被控制,复用桌面界面与桌面墙;手机和平板
## 构建与反馈 ## 构建与反馈
- [终端客户端 CLI](apps/cli/README.md):ANSI / Kitty 远程桌面观看、可选键鼠控制和 Pi 扩展;支持独立于桌面 GUI 的构建,npm 分发尚未发布。 - [终端客户端 CLI](apps/cli/README.md):独立构建、终端使用、Pi 扩展、桥接协议和 npm 打包。
- [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。 - [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。
- [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。 - [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。
- [连接模式架构](docs/connection-architecture.md):ID 连接与 IP 直连的寻址、认证、协商、数据路径及生命周期。 - [连接模式架构](docs/connection-architecture.md):ID 连接与 IP 直连的寻址、认证、协商、数据路径及生命周期。
+60 -6
View File
@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk logo with the icon on the left and a two-tone wordmark on the right" /> <img src="docs/images/crossdesk-banner.svg" width="420" alt="CrossDesk logo with the icon on the left and a two-tone wordmark on the right" />
<p>A lightweight, cross-platform remote desktop for computers, browsers, and mobile devices, with DeskHub for viewing and controlling multiple desktops.</p> <p>A lightweight, cross-platform remote desktop for computers, browsers, mobile devices, and terminals, with DeskHub for viewing and controlling multiple desktops.</p>
<p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p> <p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p>
</div> </div>
@@ -19,9 +19,9 @@
</div> </div>
CrossDesk connects your computers from desktop, browser, and mobile 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) · [Direct IP](#lan-direct) · [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) · [Preview](#preview) · [User guide](docs/USER_GUIDE_EN.md) · [Self-hosting](#self-hosting) · [Build and feedback](#development) · [Sponsor](#sponsor)
<a id="features"></a> <a id="features"></a>
@@ -29,6 +29,7 @@ CrossDesk connects your computers from desktop, browser, and mobile clients, wit
- **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 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.
@@ -50,8 +51,9 @@ Get a package matching your OS and CPU architecture from the [website](https://w
| HarmonyOS PC (development) | **Control + host**. API 26, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing | | HarmonyOS PC (development) | **Control + host**. API 26, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing |
| HarmonyOS phones / tablets (development) | **Control only**. API 20+, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing | | HarmonyOS phones / tablets (development) | **Control only**. API 20+, arm64; build and sign a HAP with DevEco; physical-device validation is ongoing |
| Web | **Control only**. A WebRTC-capable browser; no client installation on the controlling device | | Web | **Control only**. A WebRTC-capable browser; no client installation on the controlling device |
| Terminal CLI (development) | **Control only**. Build targets: macOS / Linux (x64, arm64) and Windows (x64). Build from source; see the [CLI guide](apps/cli/README.md) (Chinese) for platform validation status |
Requirements follow the current source and build configuration. Check the release notes for the downloaded package. Client capabilities are summarized under [Mobile and HarmonyOS](#mobile). Requirements follow the current source and build configuration. Check the release notes for the downloaded package. Client capabilities are summarized under [Terminal CLI](#cli) and [Mobile and HarmonyOS](#mobile).
- **Linux:** use `crossdesk-linux-…deb` for an existing desktop, or `crossdesk-virtual-desktop-xfce-…deb` for a server needing a separate Xfce desktop. Choose one. See [installation details](docs/USER_GUIDE_EN.md#installation) and [headless operation](docs/HEADLESS_EN.md). - **Linux:** use `crossdesk-linux-…deb` for an existing desktop, or `crossdesk-virtual-desktop-xfce-…deb` for a server needing a separate Xfce desktop. Choose one. See [installation details](docs/USER_GUIDE_EN.md#installation) and [headless operation](docs/HEADLESS_EN.md).
- **macOS:** grant Screen Recording and Accessibility when prompted on first launch. These enable desktop capture and remote keyboard/mouse input. See [permissions](docs/USER_GUIDE_EN.md#installation). - **macOS:** grant Screen Recording and Accessibility when prompted on first launch. These enable desktop capture and remote keyboard/mouse input. See [permissions](docs/USER_GUIDE_EN.md#installation).
@@ -122,6 +124,59 @@ Remote sessions support monitor switching, video quality and frame-rate controls
- [Client settings and updates](docs/USER_GUIDE_EN.md#settings): general options, DeskHub video settings, and version checks. - [Client settings and updates](docs/USER_GUIDE_EN.md#settings): general options, DeskHub video settings, and version checks.
- [DeskHub technical guide](docs/deskhub.md) (Chinese): preview subscriptions, bandwidth allocation, protocol, and compatibility. - [DeskHub technical guide](docs/deskhub.md) (Chinese): preview subscriptions, bandwidth allocation, protocol, and compatibility.
<a id="cli"></a>
## 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.
### Build and connect
Install Git, Xmake 3.1.1 or later, a C++17 toolchain, and the platform dependencies. The following example uses a macOS / Linux shell; Windows Git Bash / Developer PowerShell steps are in the [CLI build guide](apps/cli/docs/terminal-client.md#构建与安装) (Chinese).
```sh
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
./apps/cli/scripts/prepare-deps.sh
xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release
xmake build crossdesk-cli
xmake run crossdesk-cli doctor --json
xmake run crossdesk-cli 123456789
```
For an existing checkout, run the dependency preparation and subsequent commands from the repository root. Replace `123456789` with the host's ID; the password prompt hides your input. `doctor` reports the local environment without connecting. The repository builds the desktop client by default; this configuration builds only the CLI. Set `CROSSDESK_BUILD_DESKTOP=y` to also build the desktop client. Binaries are placed in `build/<platform>/<arch>/release/`.
An ANSI overview is limited by the terminal's character grid. Add `--quality high --zoom native`, or press `d` in the viewer, to inspect small text. `--renderer kitty` requires a terminal supporting that protocol. See the [terminal guide](apps/cli/docs/terminal-client.md) (Chinese) for rendering, mouse reporting, SSH, and terminal compatibility.
### Control, navigation, and exit
| Mode / key | Behavior |
| --- | --- |
| Read-only mode | `c` enables remote control; `q` or `Ctrl+C` exits the CLI |
| 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 |
| `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 |
| `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 |
| Arrow keys / `i j k l` | Pan in read-only or local command mode: `i` up, `j` left, `k` down, `l` right |
Zooming, panning, or pressing `c` does not end `Ctrl+]` mode. The status bar shows `CONTROL PAUSED` / `READ ONLY` to indicate whether control will resume. In control mode, ordinary `c`, `q`, and `Ctrl+C` go to the host, so enter local command mode first for local actions. Pressing `Ctrl+]` again also leaves local mode and forwards that shortcut if control resumes.
### Pi extension and feature scope
The CLI includes a Pi custom TUI extension with the same input modes. Launch it from the repository root; for example, with a macOS Apple Silicon release build:
```sh
export CROSSDESK_CLI="$PWD/build/macosx/arm64/release/crossdesk-cli"
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. 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 build configurations and npm packaging do not establish that every platform is validated or that packages have been published.
[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)
<a id="mobile"></a> <a id="mobile"></a>
## Mobile and HarmonyOS ## Mobile and HarmonyOS
@@ -169,8 +224,7 @@ Host your own signaling and TURN services, then enable them under **Settings →
## Build and feedback ## Build and feedback
- [Terminal client CLI](apps/cli/README.md) (Chinese): ANSI / Kitty remote viewing, optional keyboard and mouse control, and a Pi extension. The CLI can be built without the desktop GUI; npm packages are not published yet. - [Terminal client CLI](apps/cli/README.md) (Chinese): independent builds, terminal usage, Pi extension, bridge protocol, and npm packaging.
- [Build from source](docs/BUILD_EN.md): desktop dependencies, build options, and packaging. Use the platform guides above for mobile builds. - [Build from source](docs/BUILD_EN.md): desktop dependencies, build options, and packaging. Use the platform guides above for mobile builds.
- [GUI architecture](docs/gui-architecture.md) (Chinese): desktop UI, platform implementation, and project structure. - [GUI architecture](docs/gui-architecture.md) (Chinese): desktop UI, platform implementation, and project structure.
- [Connection architecture](docs/connection-architecture.md) (Chinese): discovery, authentication, negotiation, data paths, and lifecycle for ID and direct IP modes. - [Connection architecture](docs/connection-architecture.md) (Chinese): discovery, authentication, negotiation, data paths, and lifecycle for ID and direct IP modes.
+5 -5
View File
@@ -2,14 +2,14 @@
CrossDesk 的终端主控客户端,源码位于本仓库的 `apps/cli`。通过真实 MiniRTC ID 会话观看远程桌面;默认只读、静音。包含 ANSI 象限字符画面、局部放大和平移、Kitty 图形后端,以及 Pi 自定义 TUI 扩展。 CrossDesk 的终端主控客户端,源码位于本仓库的 `apps/cli`。通过真实 MiniRTC ID 会话观看远程桌面;默认只读、静音。包含 ANSI 象限字符画面、局部放大和平移、Kitty 图形后端,以及 Pi 自定义 TUI 扩展。
分发目标为 macOS、Linux 和 Windows 的原生终端客户端,包含 x64 / arm64。npm 包发布后可统一使用: 分发目标为 macOS / Linux(x64、arm64)和 Windows(x64)的原生终端客户端。npm 包发布后可统一使用:
```sh ```sh
npx crossdesk-cli@latest doctor --json npx crossdesk-cli@latest doctor --json
npx crossdesk-cli@latest view 123456789 npx crossdesk-cli@latest 123456789
``` ```
**当前尚未发布到 npm,也尚未完成三端实机验收。** 已加入平台包生成、自动选包入口和六个平台的 CI 构建配置;运行要求、打包与本地安装见 [npm 分发](docs/npm-distribution.md)。 **当前尚未发布到 npm,也尚未完成三端实机验收。** 已加入平台包生成、自动选包入口和五个平台的 CI 构建配置;运行要求、打包与本地安装见 [npm 分发](docs/npm-distribution.md)。
在 CrossDesk 仓库根目录执行以下命令构建 CLI: 在 CrossDesk 仓库根目录执行以下命令构建 CLI:
@@ -18,10 +18,10 @@ npx crossdesk-cli@latest view 123456789
xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release xmake f -y --CROSSDESK_BUILD_DESKTOP=n --CROSSDESK_BUILD_CLI=y --USE_CUDA=n -m release
xmake build crossdesk-cli xmake build crossdesk-cli
xmake run crossdesk-cli doctor --json xmake run crossdesk-cli doctor --json
xmake run crossdesk-cli view 123456789 --quality high --zoom native xmake run crossdesk-cli 123456789 --quality high --zoom native
``` ```
最后一个设备 ID 是格式示例,使用时替换为你有权访问的设备。密码通过隐藏提示输入。只读时 `c` 开启控制、`q` 或 `Ctrl+C` 退出;点击目标或按 `d` 进入原始细节模式,`+/-` 或滚轮缩放,方向键或 `i` 上、`j` 左、`k` 下、`l` 右平移,`f` 回到完整桌面。任何模式下先按 `Ctrl+]` 打开本地命令提示,再按 `c` 切换控制、`q` 退出或视图按键调整一次画面,`Esc` 取消。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。ANSI 全屏缩略图仍受字符网格限制,默认请求 high 画质,阅读小字建议使用细节模式;支持 Kitty 协议的终端可使用 `--renderer kitty` 获得像素画面。 `crossdesk-cli <设备ID> [选项]` 为默认连接方式,`view <设备ID>` 保留为兼容别名。最后一个设备 ID 是格式示例,使用时替换为你有权访问的设备。密码通过隐藏提示输入。只读时 `c` 开启控制、`q` 或 `Ctrl+C` 退出;点击目标或按 `d` 进入原始细节模式,`+/-` 或滚轮缩放,方向键或 `i` 上、`j` 左、`k` 下、`l` 右平移,`f` 回到完整桌面。任何模式下先按 `Ctrl+]` 进入持续本地命令模式,再按 `c` 选择返回后的控制状态、`q` 退出或视图按键连续调整画面,`Esc` / `Ctrl+C` 手动返回。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。ANSI 全屏缩略图仍受字符网格限制,默认请求 high 画质,阅读小字建议使用细节模式;支持 Kitty 协议的终端可使用 `--renderer kitty` 获得像素画面。
- [构建、使用、Pi 安装和故障排查](docs/terminal-client.md) - [构建、使用、Pi 安装和故障排查](docs/terminal-client.md)
- [私有桥接协议](docs/bridge-protocol.md) - [私有桥接协议](docs/bridge-protocol.md)
+2 -2
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 退出,d/鼠标点击进入细节,+/-/滚轮缩放,方向键或 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 | 无 | 释放输入、结束当前媒体会话 |
@@ -30,7 +30,7 @@
状态事件每秒最多一次:`{"event":"state","status":{...},"output_bytes":123,"submitted_frames":3,"elapsed_seconds":1.5}`。status 含 registering/connecting/connected/failed/closed/idle、client_id(当前或刚结束会话的 `cli-xxxxxxxxx`,注册前为空)、error、control、generation、显示器、文本/预览能力、预览确认状态、请求的 preview_width/height/quality/bitrate_bps、帧计数与网络统计。接收/提交 FPS 由相应计数的增量除以时间增量计算。 状态事件每秒最多一次:`{"event":"state","status":{...},"output_bytes":123,"submitted_frames":3,"elapsed_seconds":1.5}`。status 含 registering/connecting/connected/failed/closed/idle、client_id(当前或刚结束会话的 `cli-xxxxxxxxx`,注册前为空)、error、control、generation、显示器、文本/预览能力、预览确认状态、请求的 preview_width/height/quality/bitrate_bps、帧计数与网络统计。接收/提交 FPS 由相应计数的增量除以时间增量计算。
状态和成功响应中的 `status.command_mode` 表示正在等待本地命令,此时 `control=false`,远端输入暂停;执行或取消命令后恢复此前模式。`status.exit_requested` 表示用户按键请求退出,宿主可据此关闭组件。宿主应从 `input` 响应立即更新命令提示,不必等待下一次状态事件;不要自行截获 `Ctrl+]` 退出。显式 `control` 请求会结束命令状态。具体快捷键见 [终端客户端](terminal-client.md)。 状态和成功响应中的 `status.command_mode` 表示处于持续本地命令模式,此时 `control=false`,远端输入暂停;`status.resume_control` 表示手动返回后是否启用控制。执行命令、未知按键或粘贴不会退出此模式,失焦或输入溢出只会将返回模式设为只读。`Esc` / `Ctrl+C` 或再次 `Ctrl+]` 手动返回;`status.exit_requested` 表示用户按键请求退出,宿主可据此关闭组件。宿主应从 `input` 响应立即更新命令提示,不必等待下一次状态事件;不要自行截获 `Ctrl+]` 退出。显式 `control` 请求只更新所选控制状态,在本地模式中仍暂停远端输入。新建连接或断开会重置本地模式。具体快捷键见 [终端客户端](terminal-client.md)。
边界:输入每条及当前解析缓冲上限64KiB、嵌套32层;响应队列64条;输出一条在途消息上限2MiB;1MiB/s;最多一条待 ACK 帧;一个最新待渲染 NV12 加一个最近画面的重绘缓存。协议错误明确响应;超限缓冲后结束。EOF 清理会话,完整的已收请求按序处理,不完整末尾报错;最多两秒排空输出后退出。宿主应持续消费响应,不能无限发送而不读取。失去连接后的释放只能尽力发送。 边界:输入每条及当前解析缓冲上限64KiB、嵌套32层;响应队列64条;输出一条在途消息上限2MiB;1MiB/s;最多一条待 ACK 帧;一个最新待渲染 NV12 加一个最近画面的重绘缓存。协议错误明确响应;超限缓冲后结束。EOF 清理会话,完整的已收请求按序处理,不完整末尾报错;最多两秒排空输出后退出。宿主应持续消费响应,不能无限发送而不读取。失去连接后的释放只能尽力发送。
+13 -8
View File
@@ -6,9 +6,9 @@
```sh ```sh
npx crossdesk-cli@latest doctor --json npx crossdesk-cli@latest doctor --json
npx crossdesk-cli@latest view 123456789 npx crossdesk-cli@latest 123456789
npm install -g crossdesk-cli npm install -g crossdesk-cli
crossdesk-cli view 123456789 crossdesk-cli 123456789
``` ```
需要 Node.js 22 或以上和 npm。启动器根据 Node 的 `process.platform` / `process.arch` 选择同版本的平台包,直接启动原生 C++ 程序,继承终端、工作目录、环境变量和命令参数。安装过程不下载外部可执行文件、不执行 postinstall、不在用户机器编译。不要使用 `--omit=optional`,否则对应平台包不会安装。 需要 Node.js 22 或以上和 npm。启动器根据 Node 的 `process.platform` / `process.arch` 选择同版本的平台包,直接启动原生 C++ 程序,继承终端、工作目录、环境变量和命令参数。安装过程不下载外部可执行文件、不执行 postinstall、不在用户机器编译。不要使用 `--omit=optional`,否则对应平台包不会安装。
@@ -22,9 +22,8 @@ crossdesk-cli view 123456789
| Linux x64 | `crossdesk-cli-linux-x64` | glibc、libstdc++、zlib;所需 GLIBC / GLIBCXX 版本写入 `build-info.json` | | Linux x64 | `crossdesk-cli-linux-x64` | glibc、libstdc++、zlib;所需 GLIBC / GLIBCXX 版本写入 `build-info.json` |
| Linux arm64 | `crossdesk-cli-linux-arm64` | 同上 | | Linux arm64 | `crossdesk-cli-linux-arm64` | 同上 |
| Windows x64 | `crossdesk-cli-win32-x64` | Windows 10 或以上,原生 Console / ConPTY 与 VT 输出 | | Windows x64 | `crossdesk-cli-win32-x64` | Windows 10 或以上,原生 Console / ConPTY 与 VT 输出 |
| Windows arm64 | `crossdesk-cli-win32-arm64` | Windows 11 ARM,原生 Console / ConPTY |
这些是构建与分发目标,不代表均已实机通过。当前完整构建与 npm 运行验证只覆盖 macOS arm64;CLI 主程序与平台 I/O 已通过 Windows / Linux x64、arm64 的交叉编译检查。其他平台的完整 MiniRTC 链接、实际视频连接、键鼠、终端恢复仍需对应机器验收。 这些是构建与分发目标,不代表均已实机通过。当前完整构建与 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 图形协议是可选增强。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。
@@ -32,7 +31,13 @@ CI 的 macOS 构建配置指定 `--target_minver=13.0`,但最终必须检查
## 生成包 ## 生成包
先按 [构建说明](terminal-client.md) 构建 release,再执行: 先按 [构建说明](terminal-client.md) 构建 release。macOS 打包前需要签名,CI 使用 ad-hoc 签名;本地可执行以下命令(Intel 将 `arm64` 换成 `x86_64`):
```sh
codesign --force --sign - build/macosx/arm64/release/crossdesk-cli
```
然后生成 npm 包:
```sh ```sh
npm --prefix apps/cli run pack:npm npm --prefix apps/cli run pack:npm
@@ -46,7 +51,7 @@ npm --prefix apps/cli run pack:npm
npm --prefix apps/cli run pack:npm -- --binary /absolute/path/crossdesk-cli --out-dir /absolute/path/output npm --prefix apps/cli run pack:npm -- --binary /absolute/path/crossdesk-cli --out-dir /absolute/path/output
``` ```
每台构建机生成当前平台的原生包与相同版本的通用启动包。`apps/cli/npm/platforms.json` 集中定义六个平台;启动包通过精确版本的 `optionalDependencies` 让 npm 仅安装匹配系统和 CPU 的原生包。原生包包含二进制、运行要求、SHA-256,以及当前控制端和已应用修补的 MiniRTC 源码快照。外部 Xmake 依赖的许可证、对应源码和重链接材料需要随正式版本一并提供,参见 [源码来源](source-provenance.md)。源码快照已含 MiniRTC,解包后无需运行 `prepare-deps.sh`;在 `source/` 目录按上述构建说明显式关闭桌面、开启 CLI,再构建 `crossdesk-cli`。源码包仅包含 CLI 及其依赖,不包含桌面或移动应用。 每台构建机生成当前平台的原生包与相同版本的通用启动包。`apps/cli/npm/platforms.json` 集中定义五个平台;启动包通过精确版本的 `optionalDependencies` 让 npm 仅安装匹配系统和 CPU 的原生包。原生包包含二进制、运行要求、SHA-256,以及当前控制端和已应用修补的 MiniRTC 源码快照。外部 Xmake 依赖的许可证、对应源码和重链接材料需要随正式版本一并提供,参见 [源码来源](source-provenance.md)。源码快照已含 MiniRTC,解包后无需运行 `prepare-deps.sh`;在 `source/` 目录按上述构建说明显式关闭桌面、开启 CLI,再构建 `crossdesk-cli`。源码包仅包含 CLI 及其依赖,不包含桌面或移动应用。
`apps/cli/package.json` 的 `private: true` 表示源码目录不是发布产物。只发布打包脚本生成的 tarball;生成的包不带 private 标记。 `apps/cli/package.json` 的 `private: true` 表示源码目录不是发布产物。只发布打包脚本生成的 tarball;生成的包不带 private 标记。
@@ -67,13 +72,13 @@ Pi 等宿主应直接启动原生程序以可靠控制进程和专用密码描
## CI 与发布 ## CI 与发布
`.github/workflows/build-cli.yml` 配置六个平台的原生 release 构建、doctor 和打包,并上传 tarball;该工作流不执行 npm publish。工作流随 CLI 整合到 CrossDesk;本次本地整合未触发远程 CI。 `.github/workflows/build-cli.yml` 配置五个平台的原生 release 构建、doctor 和打包,并上传 tarball;该工作流不执行 npm publish。更新 MiniRTC 引用时,必须先将对应提交推送到子模块远端,再推送 CrossDesk,否则全新检出和 CI 会在拉取子模块时失败。
正式发布时,先发布全部对应版本的原生包,再发布通用 `crossdesk-cli` 包。例如: 正式发布时,先发布全部对应版本的原生包,再发布通用 `crossdesk-cli` 包。例如:
```sh ```sh
npm publish crossdesk-cli-darwin-arm64-0.1.0.tgz --access public npm publish crossdesk-cli-darwin-arm64-0.1.0.tgz --access public
# 同样发布其余五个平台的原生包后,再发布入口包。 # 同样发布其余四个平台的原生包后,再发布入口包。
npm publish crossdesk-cli-0.1.0.tgz --access public npm publish crossdesk-cli-0.1.0.tgz --access public
``` ```
+22 -18
View File
@@ -17,21 +17,21 @@ xmake run crossdesk-cli doctor --json
macOS 的 MiniRTC 依赖仍链接 VideoToolbox/CoreMedia 和 GLib 声明的 AppKit 等系统框架;CLI 不创建 GUI、采集器或音频设备。硬件解码参数显式关闭。Linux 无需 DISPLAY/WAYLAND_DISPLAY;本轮尚未在 Linux 主机实际构建验证。 macOS 的 MiniRTC 依赖仍链接 VideoToolbox/CoreMedia 和 GLib 声明的 AppKit 等系统框架;CLI 不创建 GUI、采集器或音频设备。硬件解码参数显式关闭。Linux 无需 DISPLAY/WAYLAND_DISPLAY;本轮尚未在 Linux 主机实际构建验证。
Windows 产物为 `build/windows/<arch>/release/crossdesk-cli.exe`;先在 Git Bash 中执行 `sh apps/cli/scripts/prepare-deps.sh`,然后可在开发者 PowerShell 中执行上述 Xmake 命令。Windows 使用 Console / ConPTY 的原生键盘、鼠标事件和 VT 输出,保存并恢复控制台模式与代码页;无需 WSL。Windows Terminal、现代 PowerShell / CMD 和 VS Code 的 ConPTY 终端属于目标运行环境,仍需实机验收。Git Bash 的独立 mintty 窗口没有原生控制台时,应从 Windows Terminal 的 Git Bash 配置启动。 Windows 仅支持 x64,产物为 `build/windows/x64/release/crossdesk-cli.exe`;先在 Git Bash 中执行 `sh apps/cli/scripts/prepare-deps.sh`,然后在开发者 PowerShell 中执行上述 Xmake 命令,配置时额外指定 `-p windows -a x64`。在 Git Bash 中构建时同样必须指定 `-p windows`,避免被识别为 MSYS 平台。Windows 使用 Console / ConPTY 的原生键盘、鼠标事件和 VT 输出,保存并恢复控制台模式与代码页;无需 WSL。Windows Terminal、现代 PowerShell / CMD 和 VS Code 的 ConPTY 终端属于目标运行环境,仍需实机验收。Git Bash 的独立 mintty 窗口没有原生控制台时,应从 Windows Terminal 的 Git Bash 配置启动。
预编译 npm 包的用户无需构建工具,见 [npm 分发](npm-distribution.md)。 预编译 npm 包的用户无需构建工具,见 [npm 分发](npm-distribution.md)。
## 连接与身份 ## 连接与身份
```sh ```sh
crossdesk-cli view 123456789 crossdesk-cli 123456789
crossdesk-cli view 123456789 --renderer ansi --fps 8 --quality high --zoom native crossdesk-cli 123456789 --renderer ansi --fps 8 --quality high --zoom native
crossdesk-cli view 123456789 --renderer kitty --quality ultra --zoom native --display 0 crossdesk-cli 123456789 --renderer kitty --quality ultra --zoom native --display 0
crossdesk-cli view 123456789 --control crossdesk-cli 123456789 --control
crossdesk-cli view 123456789 --server your-server.example --port 9099 crossdesk-cli 123456789 --server your-server.example --port 9099
``` ```
设备 ID 必须是 9 位数字,以上数字仅示例。默认信令地址沿用当前 CrossDesk:`api.crossdesk.cn:9099`。密码从本地终端隐藏输入(POSIX `/dev/tty`、Windows `CONIN$` / `CONOUT$`),不提供密码命令行参数。CLI 不读取、不修改桌面配置。每次连接直接用 `cli` 请求服务端分配临时 `cli-xxxxxxxxx` ID(后缀为 9 位数字),注册、Join 和媒体使用同一个 Peer。CLI 仅作为控制端,不申请普通数字身份或 `C-` 身份,也不保存本机密码或重连凭据。每次启动或重新连接都会申请新的 ID;断开后服务端按 Web 临时身份的规则清理注册记录。 直接将设备 ID 作为第一个参数即可连接;`crossdesk-cli view <设备ID> [选项]` 保留为兼容别名,支持相同选项。设备 ID 必须是 9 位数字,保留前导零,以上数字仅示例。默认信令地址沿用当前 CrossDesk:`api.crossdesk.cn:9099`。密码从本地终端隐藏输入(POSIX `/dev/tty`、Windows `CONIN$` / `CONOUT$`),不提供密码命令行参数。CLI 不读取、不修改桌面配置。每次连接直接用 `cli` 请求服务端分配临时 `cli-xxxxxxxxx` ID(后缀为 9 位数字),注册、Join 和媒体使用同一个 Peer。CLI 仅作为控制端,不申请普通数字身份或 `C-` 身份,也不保存本机密码或重连凭据。每次启动或重新连接都会申请新的 ID;断开后服务端按 Web 临时身份的规则清理注册记录。
需先部署支持 `cli` 注册的新版 crossdesk-server;旧服务端会登录失败,本客户端不会回退为普通设备身份。桥接 `status.client_id` 返回服务端分配的 ID。 需先部署支持 `cli` 注册的新版 crossdesk-server;旧服务端会登录失败,本客户端不会回退为普通设备身份。桥接 `status.client_id` 返回服务端分配的 ID。
@@ -41,7 +41,7 @@ crossdesk-cli view 123456789 --server your-server.example --port 9099
## ANSI、Kitty 与 SSH ## ANSI、Kitty 与 SSH
ANSI 默认使用四象限字符:每个字符从 2×2 样本选择误差最小的两个颜色和象限形状,比原先上下半方块多一倍水平采样点;每个字符仍只有前景/背景两色。使用 `--ansi-detail half` 可回到上下半方块以兼容字体。缩小时采用面积加权滤波,避免最近邻采样漏掉细线。支持 TrueColor、256 色和基础 8 色,256 色选择实际颜色立方体或灰阶条中更接近的颜色。环境的 COLORTERM/TERM 决定颜色模式;非 UTF-8 locale 使用背景色空格。TERM=dumb、非 TTY 输出明确拒绝 view,不输出全屏转义序列。 ANSI 默认使用四象限字符:每个字符从 2×2 样本选择误差最小的两个颜色和象限形状,比原先上下半方块多一倍水平采样点;每个字符仍只有前景/背景两色。使用 `--ansi-detail half` 可回到上下半方块以兼容字体。缩小时采用面积加权滤波,避免最近邻采样漏掉细线。支持 TrueColor、256 色和基础 8 色,256 色选择实际颜色立方体或灰阶条中更接近的颜色。环境的 COLORTERM/TERM 决定颜色模式;非 UTF-8 locale 使用背景色空格。TERM=dumb、非 TTY 输出明确拒绝交互连接,不输出全屏转义序列。
画面按字符单元比例等比缩放,留黑边及状态栏,默认假定字符宽高比 0.5;TTY 提供像素尺寸时使用其比例。每次只输出变化行,合并连续颜色设置;缩放在完整输出边界清理旧画面。上限 240 列、100 画面行;默认预留两行及一列,避免自动换行。 画面按字符单元比例等比缩放,留黑边及状态栏,默认假定字符宽高比 0.5;TTY 提供像素尺寸时使用其比例。每次只输出变化行,合并连续颜色设置;缩放在完整输出边界清理旧画面。上限 240 列、100 画面行;默认预留两行及一列,避免自动换行。
@@ -53,7 +53,7 @@ Kitty 通过终端字节流发送 zlib 压缩的内联 RGB/Base64(每块至多
```sh ```sh
ssh -t your-server ssh -t your-server
crossdesk-cli view 123456789 --renderer ansi --fps 8 crossdesk-cli 123456789 --renderer ansi --fps 8
``` ```
用户本地终端负责显示 SSH 字节流。ANSI、Kitty over SSH 和 SSH+tmux 的实机互通尚未验证。无 GUI 指主控客户端;被控端仍需要可捕获的图形会话。 用户本地终端负责显示 SSH 字节流。ANSI、Kitty over SSH 和 SSH+tmux 的实机互通尚未验证。无 GUI 指主控客户端;被控端仍需要可捕获的图形会话。
@@ -75,24 +75,26 @@ crossdesk-cli view 123456789 --renderer ansi --fps 8
转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),以 1MiB/s 令牌预算分块非阻塞写入;不能开始新画面时保留最新解码帧。ANSI 差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。 转换与输出在拥有会话的事件循环,媒体回调独立。输出至多一个完整消息(2MiB),以 1MiB/s 令牌预算分块非阻塞写入;不能开始新画面时保留最新解码帧。ANSI 差分基准只在整帧输出完成后提交。Pi 桥接另要求消费帧 ACK,最多一帧等待 ACK,输入映射只在对应 ACK 后切换到新网格。状态栏优先在完整画面输出之间更新,避免大图持续刷新时状态饥饿;可写事件唤醒短写重试,仍遵守字节预算。控制事件至多 64 条,协议输入 64KiB/嵌套 32 层,输入粘贴缓冲 32KiB,文本单次最多 16KiB。
统计包括收到帧数、被替换帧数、非法帧、等待帧数、实际完整提交次数、输出字节数、运行秒数、最近本地排队时间、网络码率/RTT/SRTP。通过计数差/时间差可得到区间 FPS;view 结束日志另给平均提交 FPS。写入 TTY 完成不等于屏幕显示完成,本地排队时间不是端到端延迟。 统计包括收到帧数、被替换帧数、非法帧、等待帧数、实际完整提交次数、输出字节数、运行秒数、最近本地排队时间、网络码率/RTT/SRTP。通过计数差/时间差可得到区间 FPS;交互连接结束日志另给平均提交 FPS。写入 TTY 完成不等于屏幕显示完成,本地排队时间不是端到端延迟。
## 输入与退出 ## 输入与退出
默认只读、静音。`--control` 或只读时按 `c` 明确启用输入。`Ctrl+]` 是本地命令前缀:先按下并松开,再按下表中的命令键。状态栏即时显示当前模式与可用按键,单按前缀不会退出。 默认只读、静音。`--control` 或只读时按 `c` 明确启用输入。`Ctrl+]` 进入持续的本地命令模式:先按下并松开,再按下表中的命令键,可以连续执行多个命令。状态栏即时显示当前模式与可用按键。
macOS 自带 Terminal 还需要在当前窗口勾选“显示 → 允许鼠标报告”(View → Allow Mouse Reporting)。CLI 会启用鼠标报告,但无法覆盖 Terminal 关闭此选项的设置;未勾选时,点击、拖动和滚轮事件不会传给 CLI。参见 [Apple 官方说明](https://support.apple.com/zh-cn/guide/terminal/trmlc69728a5/mac)。
| 操作 | 只读模式 | 控制模式 | | 操作 | 只读模式 | 控制模式 |
|---|---|---| |---|---|---|
| 开启/关闭远端控制 | `c` | `Ctrl+]`,再按 `c` | | 开启/关闭远端控制 | `c` | `Ctrl+]`,再按 `c` |
| 退出 CLI | `q` 或 `Ctrl+C` | `Ctrl+]`,再按 `q` | | 退出 CLI | `q` 或 `Ctrl+C` | `Ctrl+]`,再按 `q` |
| 显示本地命令提示 | `?` 或 `Ctrl+]` | `Ctrl+]` | | 进入本地命令模式 | `?` 或 `Ctrl+]` | `Ctrl+]` |
| 放大/缩小 | `+` / `=` / `-` | `Ctrl+]`,再按对应键 | | 放大/缩小 | `+` / `=` / `-` | `Ctrl+]`,再按对应键 |
| 原始细节/完整画面 | `d`(detail)/ `f`(fit),相邻字母键 | `Ctrl+]`,再按对应键 | | 原始细节/完整画面 | `d`(detail)/ `f`(fit),相邻字母键 | `Ctrl+]`,再按对应键 |
| 平移 | 方向键或 `i` 上、`j` 左、`k` 下、`l` 右 | `Ctrl+]`,再按对应键 | | 平移 | 方向键或 `i` 上、`j` 左、`k` 下、`l` 右 | `Ctrl+]`,再按对应键 |
进入本地命令状态时暂停远端输入,并释放已记录按键/鼠标;执行一个命令后恢复原模式,`c` 则切换模式。`Esc` 或 `Ctrl+C` 取消命令并恢复原模式;未知命令显示提示,不发送给远端。命令状态中的鼠标事件被忽略,bracketed paste 内容被丢弃并取消命令,不会触发快捷键。控制模式下普通 `c/q/?`、`Ctrl+T`、`Ctrl+C` 和单独 `Esc` 均传给远端;连续按两次 `Ctrl+]` 可向远端发送一次 `Ctrl+]`。目前不支持重绑定。 进入本地命令模式时暂停远端输入,并释放已记录按键/鼠标。执行缩放、平移、`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` 恢复完整画面。黑边点击被忽略。这些按键和鼠标在控制模式下仍传给远端;使用本地命令前缀可调整一次视图,持续查看时可先切回只读。放大、平移和只读鼠标定位不会向远端发送操作,也适用于静止画面。 只读时,`d` 或在画面中单击进入该位置的原始细节模式;`+` / `=` 放大、`-` 缩小(1~32倍),滚轮以鼠标指向为中心调整倍率。方向键或 `i/j/k/l` 分别向上/左/下/右平移,`f` 恢复完整画面。黑边点击被忽略。这些按键和鼠标在控制模式下仍传给远端;进入本地命令模式后可连续调整视图,直至手动返回。放大、平移和只读鼠标定位不会向远端发送操作,也适用于静止画面。
支持 SGR 鼠标移动、左右中键、拖动和滚轮;黑边不发送新点击,黑边处松开仍释放旧按钮。输入坐标映射到所选显示器,`--display` 是 HostInformation 列表的零起始索引,视频流为 Display1、Display2…… 支持 SGR 鼠标移动、左右中键、拖动和滚轮;黑边不发送新点击,黑边处松开仍释放旧按钮。输入坐标映射到所选显示器,`--display` 是 HostInformation 列表的零起始索引,视频流为 Display1、Display2……
@@ -113,9 +115,9 @@ 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 图形或自行打开鼠标报告。
需要全局发现时,把整个 `apps/cli/plugins/pi` 目录复制到 `~/.pi/agent/extensions/crossdesk/` 后重启 Pi 或执行 `/reload`。卸载时先关闭视图,删除该目录并 `/reload`;使用 `-e` 时从启动参数移除即可。无需修改包中的密码或设备配置。默认连接官方信令;自建信令当前通过独立 view 命令使用,插件尚无服务器设置界面。 需要全局发现时,把整个 `apps/cli/plugins/pi` 目录复制到 `~/.pi/agent/extensions/crossdesk/` 后重启 Pi 或执行 `/reload`。卸载时先关闭视图,删除该目录并 `/reload`;使用 `-e` 时从启动参数移除即可。无需修改包中的密码或设备配置。默认连接官方信令;自建信令当前通过独立 CLI 连接命令使用,插件尚无服务器设置界面。
仅在 `ctx.mode === "tui"`、hasUI 且进程双向 TTY 时打开组件。RPC/print/json 返回独立播放器提示,不声称内嵌视频。UI 关闭/会话退出先关闭 bridge stdin,超时 SIGTERM,再超时 SIGKILL。bridge 尚未就绪或已经停止时,组件可直接用 `q`、`Esc` 或 `Ctrl+C` 关闭。SDK 类型和模拟 UI 生命周期已验证,真实 Pi TUI 视觉刷新、增强按键与鼠标传递未实机验收。真实设备的 bridge 与独立 view 验证见 [验证记录](validation.md)。 仅在 `ctx.mode === "tui"`、hasUI 且进程双向 TTY 时打开组件。RPC/print/json 返回独立播放器提示,不声称内嵌视频。UI 关闭/会话退出先关闭 bridge stdin,超时 SIGTERM,再超时 SIGKILL。bridge 尚未就绪或已经停止时,组件可直接用 `q`、`Esc` 或 `Ctrl+C` 关闭。SDK 类型和模拟 UI 生命周期已验证,真实 Pi TUI 视觉刷新、增强按键与鼠标传递未实机验收。真实设备的 bridge 与独立 CLI 连接验证见 [验证记录](validation.md)。
官方依据:[扩展文档](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md)、[TUI 文档](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/tui.md)、[Kitty 图形协议](https://sw.kovidgoyal.net/kitty/graphics-protocol/)。 官方依据:[扩展文档](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md)、[TUI 文档](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/tui.md)、[Kitty 图形协议](https://sw.kovidgoyal.net/kitty/graphics-protocol/)。
@@ -130,8 +132,10 @@ pi -e ./apps/cli/plugins/pi/index.ts
- `display_not_available`:换成远端声明的索引;单会话一次只显示一个屏幕。 - `display_not_available`:换成远端声明的索引;单会话一次只显示一个屏幕。
- `media_initialization_failed`:检查编译依赖和本地媒体运行环境。 - `media_initialization_failed`:检查编译依赖和本地媒体运行环境。
- 只读下按键不会发送到远端;远端未声明文本协议时文本输入不可用。 - 只读下按键不会发送到远端;远端未声明文本协议时文本输入不可用。
- 日志在进程创建的私有临时目录(POSIX `/tmp/crossdesk-cli-*`、Windows `%TEMP%\crossdesk-cli-*`)。view 的摘要在 client.log;底层诊断默认关闭且敏感 Join 日志已修补。目录不保存密码;使用完可删除。错误退出信息在恢复终端后显示。 - 日志在进程创建的私有临时目录(POSIX `/tmp/crossdesk-cli-*`、Windows `%TEMP%\crossdesk-cli-*`)。交互连接的摘要在 client.log,包含收到的终端鼠标事件数 `mouse_reports`;transport.log 只记录白名单中的 ICE/DTLS 状态及本地控制开关,不记录密码、SDP 或原始信令。使用完可删除。错误退出信息在恢复终端后显示,并给出本次日志目录。
- `doctor --json` 是环境报告,不会联网登录、主动探测 Kitty 或证明远端可连接。 - `connection_failed` 表示连接建立失败;`connection_lost` 表示连接成功后传输失效。xmake 随后的 `execv ... failed(1)` 表示程序以错误码 1 退出,本身不代表进程崩溃。遇到控制失效时,对照 transport.log 中的控制开关和连接状态;`mouse_reports` 为 0 表示终端没有向 CLI 提供可识别的 SGR 鼠标事件。
- 如果整个 macOS Terminal 窗口退出且 CLI 记录 `output closed`,还应检查系统 DiagnosticReports 中的 Terminal 报告。已观察到 Terminal 2.15 在创建标签页并调整窗口尺寸时发生 `TTLogicalScreen expandUnicharOffsetCacheToLine:` 行缓存越界;这与 CLI 的 `connection_failed` 不同。临时可在独立窗口运行,避开该标签页操作,不能据此认为 Terminal 缺陷已修复。
- `doctor --json` 是环境报告,不会联网登录、主动探测 Kitty 或证明远端可连接。 `input_shortcuts=local_commands_v2` 表示支持本文的持续本地命令模式;启动日志同时记录实际执行路径,以便区分不同工程的构建产物。
第一轮无音频播放或音频订阅 API,SSH 服务器上的声音不会传回本地。无后台守护进程、共享媒体 Broker、IP 直连、MCP 或自动截图。其他宿主可以使用 [私有桥接](bridge-protocol.md),没有可靠自定义 UI 时打开独立终端或给出播放器命令。 第一轮无音频播放或音频订阅 API,SSH 服务器上的声音不会传回本地。无后台守护进程、共享媒体 Broker、IP 直连、MCP 或自动截图。其他宿主可以使用 [私有桥接](bridge-protocol.md),没有可靠自定义 UI 时打开独立终端或给出播放器命令。
+8 -1
View File
@@ -33,6 +33,13 @@
此次没有连接真实远端设备,也没有进行 Linux/Windows 或真实 Pi TUI 实机验证。新增测试代码与产物均保存在工程外系统临时目录。 此次没有连接真实远端设备,也没有进行 Linux/Windows 或真实 Pi TUI 实机验证。新增测试代码与产物均保存在工程外系统临时目录。
## 直接 ID 入口与 npm 源码包(2026-10-30)
- 直接输入 9 位设备 ID 和 `view` 兼容别名均通过参数检查,保留前导零,支持相同的连接选项;无效 ID 在密码提示前拒绝。macOS arm64 终端中的密码提示与 Ctrl+C 取消通过。
- 本地 npm 包离线安装、启动器帮助和 `doctor --json` 通过;源码包包含完整的 `libs/controller/include`,补齐诊断头文件后,从安装包内源码独立配置、release 构建及 `doctor` 通过。
- 原有仓库结构、wire 和键盘状态测试通过;未新增或修改工程内测试,额外验证和产物均位于工程外临时目录。
- Build and Release #899 和 CLI native builds and npm packages #1 的首次运行均因 MiniRTC 提交 `b9ee1fa7` 尚未推送而在检出阶段失败。将该提交推送到 MiniRTC 远端后,全新递归浅克隆通过。
## 独立开发阶段的验证范围 ## 独立开发阶段的验证范围
以下为迁入前的验证记录摘要,不表示本次重新连接了远程设备: 以下为迁入前的验证记录摘要,不表示本次重新连接了远程设备:
@@ -46,6 +53,6 @@
## 实测边界 ## 实测边界
本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。六个平台 CI 配置与 npm 包名均不代表已发布或已验收。 本地检查不涵盖 Linux / Windows 完整构建、跨平台远程控制、Kitty/SSH 实际终端显示、真实 Pi TUI 或长时间资源占用。五个平台 CI 配置与 npm 包名均不代表已发布或已验收。
CLI 需要支持 `cli` 临时注册的服务端。`doctor --json` 仅报告本机环境,不会联网登录或证明远端可连接。 CLI 需要支持 `cli` 临时注册的服务端。`doctor --json` 仅报告本机环境,不会联网登录或证明远端可连接。
+44 -20
View File
@@ -201,7 +201,7 @@ int View(SessionOptions options, const std::string &device,
const std::string &renderer, bool control, int fd, AnsiDetail detail, const std::string &renderer, bool control, int fd, AnsiDetail detail,
double zoom, bool native) { double zoom, bool native) {
if (!IsTty(0) || !IsTty(fd) || Env("TERM") == "dumb") if (!IsTty(0) || !IsTty(fd) || Env("TERM") == "dumb")
throw std::runtime_error("view needs an interactive ANSI terminal; use " throw std::runtime_error("connecting needs an interactive ANSI terminal; use "
"bridge --stdio for a host"); "bridge --stdio for a host");
auto password = Password(); auto password = Password();
if (interrupted) if (interrupted)
@@ -288,9 +288,10 @@ int View(SessionOptions options, const std::string &device,
auto st = s.Status(); auto st = s.Status();
const auto last_state = st["state"].get<std::string>(); const auto last_state = st["state"].get<std::string>();
auto line = (input.error.empty() ? last_state : input.error) + " " + auto line = (input.error.empty() ? last_state : input.error) + " " +
(input.command_mode ? "LOCAL COMMAND" (input.command_mode
: input.control ? "CONTROL" ? (input.control ? "LOCAL COMMAND / CONTROL PAUSED"
: "READ ONLY") + : "LOCAL COMMAND / READ ONLY")
: input.control ? "CONTROL" : "READ ONLY") +
" " + " " +
(kitty ? "KITTY" (kitty ? "KITTY"
: detail == AnsiDetail::Quadrant ? "QUAD" : detail == AnsiDetail::Quadrant ? "QUAD"
@@ -302,8 +303,8 @@ int View(SessionOptions options, const std::string &device,
: "") + : "") +
" | Ctrl+] commands"; " | Ctrl+] commands";
const std::string hint = const std::string hint =
input.command_mode ? "c control q quit Esc cancel | i up j left k down " input.command_mode ? "Esc/Ctrl+C return | c toggle q quit | "
"l right | +/- zoom f fit d detail" "arrows/ijkl pan +/- zoom f fit d detail"
: input.control : input.control
? "Keys/mouse -> remote | Ctrl+] then c release, q quit" ? "Keys/mouse -> remote | Ctrl+] then c release, q quit"
: "c control q quit ? help | i up j left k down l right | " : "c control q quit ? help | i up j left k down l right | "
@@ -355,6 +356,7 @@ int View(SessionOptions options, const std::string &device,
// These diagnostics go to the private file, never through the active screen. // These diagnostics go to the private file, never through the active screen.
std::cerr std::cerr
<< json{{"session", final}, << json{{"session", final},
{"mouse_reports", input.mouse_reports},
{"submitted", submitted}, {"submitted", submitted},
{"output_bytes", output.bytes}, {"output_bytes", output.bytes},
{"last_queue_ms", queue_ms}, {"last_queue_ms", queue_ms},
@@ -377,6 +379,7 @@ int Bridge(int fd, SessionOptions options, int password_fd) {
auto status = [&] { auto status = [&] {
auto value = s.Status(); auto value = s.Status();
value["command_mode"] = input.command_mode; value["command_mode"] = input.command_mode;
value["resume_control"] = input.control;
value["exit_requested"] = input.exit; value["exit_requested"] = input.exit;
return value; return value;
}; };
@@ -487,8 +490,7 @@ int Bridge(int fd, SessionOptions options, int password_fd) {
awaiting = 0; awaiting = 0;
} else if (method == "control") { } else if (method == "control") {
input.control = Boolean(j, "enabled", false); input.control = Boolean(j, "enabled", false);
input.command_mode = false; s.SetControl(input.control && !input.command_mode);
s.SetControl(input.control);
} else if (method == "input") { } else if (method == "input") {
auto text = j.at("data").get<std::string>(); auto text = j.at("data").get<std::string>();
if (text.size() > 16384) if (text.size() > 16384)
@@ -621,18 +623,19 @@ int Bridge(int fd, SessionOptions options, int password_fd) {
s.Disconnect(); s.Disconnect();
return interrupted ? 130 : 0; return interrupted ? 130 : 0;
} }
const char *help = R"(crossdesk-cli doctor [--json] const char *help = R"(crossdesk-cli <9-digit-device-id> [--renderer auto|ansi|kitty] [--fps 1..30]
crossdesk-cli view <9-digit-device-id> [--renderer auto|ansi|kitty] [--fps 1..30] [--display 0..63] [--control] [--server host] [--port 9099]
[--display 0..63] [--control] [--server host] [--port 9099] [--quality low|balanced|high|ultra] [--ansi-detail quadrant|half] [--zoom 1..32|native]
[--quality low|balanced|high|ultra] [--ansi-detail quadrant|half] [--zoom 1..32|native] crossdesk-cli view <9-digit-device-id> [options] (compatibility alias)
crossdesk-cli doctor [--json]
crossdesk-cli bridge --stdio [--password-fd 3] [--server host] [--port 9099] crossdesk-cli bridge --stdio [--password-fd 3] [--server host] [--port 9099]
view: 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+] opens local commands: c toggles control, q quits, Esc/Ctrl+C cancels. 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: click/d native detail, wheel/+/- zoom, arrows/ijkl pan (i up, j left, k down, l right), f fit.
In control mode, Ctrl+] then a view key adjusts the view once; ordinary keys go to the remote. Local mode stays active after commands, paste or focus loss; remote input remains paused.
Ctrl+T and Ctrl+C pass through in control mode; Ctrl+] twice sends remote Ctrl+]. 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.
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.
--fps controls local output; negotiated previews use supported 1/8/15 FPS. --fps controls local output; negotiated previews use supported 1/8/15 FPS.
@@ -658,6 +661,9 @@ int main(int argc, char **argv) {
{"stdin_tty", IsTty(0)}, {"stdin_tty", IsTty(0)},
{"stdout_tty", IsTty(1)}, {"stdout_tty", IsTty(1)},
{"term", SafeText(Env("TERM"))}, {"term", SafeText(Env("TERM"))},
{"terminal_program", SafeText(Env("TERM_PROGRAM"))},
{"input_shortcuts", "local_commands_v2"},
{"transport_diagnostics", true},
{"utf8", v.utf8}, {"utf8", v.utf8},
{"colors", v.colors}, {"colors", v.colors},
{"multiplexer", !Env("TMUX").empty() || !Env("STY").empty()}, {"multiplexer", !Env("TMUX").empty() || !Env("STY").empty()},
@@ -668,19 +674,27 @@ int main(int argc, char **argv) {
std::cout << d.dump(argc == 3 ? -1 : 2) << "\n"; std::cout << d.dump(argc == 3 ? -1 : 2) << "\n";
return 0; return 0;
} }
if (command != "view" && command != "bridge") int i = 2;
throw std::invalid_argument("unknown command; use --help"); if (command != "view" && command != "bridge") {
if (command.find_first_not_of("0123456789") != std::string::npos)
throw std::invalid_argument(
"expected a 9-digit device ID or a command; use --help");
command = "view";
i = 1;
}
SessionOptions options; SessionOptions options;
std::string renderer = "auto", device; std::string renderer = "auto", device;
AnsiDetail detail = AnsiDetail::Quadrant; AnsiDetail detail = AnsiDetail::Quadrant;
double zoom = 1; double zoom = 1;
bool control = false, stdio = false, native = false; bool control = false, stdio = false, native = false;
int secret_fd = 3; int secret_fd = 3;
int i = 2;
if (command == "view") { if (command == "view") {
if (argc <= 2) if (i >= argc)
throw std::invalid_argument("device ID required"); throw std::invalid_argument("device ID required");
device = argv[i++]; device = argv[i++];
if (device.size() != 9 ||
device.find_first_not_of("0123456789") != std::string::npos)
throw std::invalid_argument("device ID must contain 9 digits");
} }
auto number = [](const std::string &s, int lo, int hi) { auto number = [](const std::string &s, int lo, int hi) {
size_t used = 0; size_t used = 0;
@@ -739,6 +753,13 @@ int main(int argc, char **argv) {
} }
if (Redirect(2, 1) < 0) if (Redirect(2, 1) < 0)
throw std::runtime_error("cannot isolate protocol output"); throw std::runtime_error("cannot isolate protocol output");
std::cerr << json{{"event", "startup"},
{"executable", SafeText(argv[0], 4096)},
{"terminal_program", SafeText(Env("TERM_PROGRAM"))},
{"input_shortcuts", "local_commands_v2"},
{"log_directory", options.log_directory}}
.dump()
<< "\n";
try { try {
return command == "view" ? View(options, device, renderer, control, return command == "view" ? View(options, device, renderer, control,
output.value, detail, zoom, native) output.value, detail, zoom, native)
@@ -746,6 +767,9 @@ int main(int argc, char **argv) {
} catch (const std::exception &e) { } catch (const std::exception &e) {
if (command == "view") { if (command == "view") {
std::string msg = "crossdesk-cli: " + SafeText(e.what()) + "\r\n"; std::string msg = "crossdesk-cli: " + SafeText(e.what()) + "\r\n";
if (std::string(e.what()) == "connection_lost")
msg += "The remote connection was lost after connecting.\r\n";
msg += "Diagnostics: " + SafeText(options.log_directory) + "\r\n";
Write(output.value, msg.data(), msg.size()); Write(output.value, msg.data(), msg.size());
} }
throw; throw;
-1
View File
@@ -3,6 +3,5 @@
"darwin-x64": { "os": "darwin", "cpu": "x64", "xmakePlatform": "macosx", "xmakeArch": "x86_64" }, "darwin-x64": { "os": "darwin", "cpu": "x64", "xmakePlatform": "macosx", "xmakeArch": "x86_64" },
"linux-arm64": { "os": "linux", "cpu": "arm64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "arm64" }, "linux-arm64": { "os": "linux", "cpu": "arm64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "arm64" },
"linux-x64": { "os": "linux", "cpu": "x64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "x86_64" }, "linux-x64": { "os": "linux", "cpu": "x64", "libc": "glibc", "xmakePlatform": "linux", "xmakeArch": "x86_64" },
"win32-arm64": { "os": "win32", "cpu": "arm64", "xmakePlatform": "windows", "xmakeArch": "arm64" },
"win32-x64": { "os": "win32", "cpu": "x64", "xmakePlatform": "windows", "xmakeArch": "x64" } "win32-x64": { "os": "win32", "cpu": "x64", "xmakePlatform": "windows", "xmakeArch": "x64" }
} }
+5 -3
View File
@@ -25,7 +25,7 @@ export default function crossdesk(pi: ExtensionAPI) {
description: "Open a human-only remote desktop view (Ctrl+] for commands)", description: "Open a human-only remote desktop view (Ctrl+] for commands)",
handler: async (args, ctx) => { handler: async (args, ctx) => {
if (ctx.mode !== "tui" || !ctx.hasUI || !process.stdin.isTTY || !process.stdout.isTTY) { if (ctx.mode !== "tui" || !ctx.hasUI || !process.stdin.isTTY || !process.stdout.isTTY) {
ctx.ui.notify("CrossDesk embedded view requires Pi interactive TUI. Run crossdesk-cli view <device-id> in a terminal.", "warning"); ctx.ui.notify("CrossDesk embedded view requires Pi interactive TUI. Run crossdesk-cli <device-id> in a terminal.", "warning");
return; return;
} }
if (stop) { ctx.ui.notify("A CrossDesk view is already open", "warning"); return; } if (stop) { ctx.ui.notify("A CrossDesk view is already open", "warning"); return; }
@@ -118,7 +118,9 @@ export default function crossdesk(pi: ExtensionAPI) {
if (Number.isSafeInteger(message.status.generation)) generation = message.status.generation; if (Number.isSafeInteger(message.status.generation)) generation = message.status.generation;
control = message.status.control === true; control = message.status.control === true;
commandMode = message.status.command_mode === true; commandMode = message.status.command_mode === true;
status = clean(message.status.state) + (commandMode ? " LOCAL COMMAND" : control ? " CONTROL" : " READ ONLY"); status = clean(message.status.state) + (commandMode
? " LOCAL COMMAND / " + (message.status.resume_control === true ? "CONTROL PAUSED" : "READ ONLY")
: control ? " CONTROL" : " READ ONLY");
if (message.status.error) error = clean(message.status.error); if (message.status.error) error = clean(message.status.error);
if (message.status.state !== "connected") lines = []; if (message.status.state !== "connected") lines = [];
} }
@@ -157,7 +159,7 @@ 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 ? "c control q quit Esc cancel | i up j left k down l right | +/- zoom f fit d detail" : commandMode ? "Esc/Ctrl+C return | c toggle q quit | arrows/ijkl pan +/- zoom f fit d detail"
: 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 ? help | i up j left k down l right | +/- zoom f fit d detail";
return [truncateToWidth(`CrossDesk ${device} | ${status} ${native ? "detail" : "fit"} x${zoom} | Ctrl+] commands`, width), return [truncateToWidth(`CrossDesk ${device} | ${status} ${native ? "detail" : "fit"} x${zoom} | Ctrl+] commands`, width),
+3 -3
View File
@@ -63,7 +63,7 @@ function inspect(binary, target) {
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1); .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1);
} else { } else {
const offset = data.length >= 64 ? data.readUInt32LE(60) : data.length; const offset = data.length >= 64 ? data.readUInt32LE(60) : data.length;
const machine = process.arch === 'arm64' ? 0xaa64 : 0x8664; const machine = 0x8664;
if (data.toString('ascii', 0, 2) !== 'MZ' || offset + 6 > data.length || if (data.toString('ascii', 0, 2) !== 'MZ' || offset + 6 > data.length ||
data.readUInt32LE(offset) !== 0x4550 || data.readUInt16LE(offset + 4) !== machine) data.readUInt32LE(offset) !== 0x4550 || data.readUInt16LE(offset + 4) !== machine)
throw new Error('Expected a Windows PE executable matching the current CPU.'); throw new Error('Expected a Windows PE executable matching the current CPU.');
@@ -76,7 +76,7 @@ function inspect(binary, target) {
!fs.existsSync(path.join(process.env.SystemRoot, 'System32', dependency)))) !fs.existsSync(path.join(process.env.SystemRoot, 'System32', dependency))))
throw new Error(`Unbundled Windows runtime: ${dependency}. Build with static runtimes (MT).`); throw new Error(`Unbundled Windows runtime: ${dependency}. Build with static runtimes (MT).`);
} }
info.minimumOS = process.arch === 'arm64' ? 'Windows 11' : 'Windows 10 (VT console)'; info.minimumOS = 'Windows 10 (VT console)';
} }
const doctor = JSON.parse(run(binary, ['doctor', '--json'], { timeout: 15000 })); const doctor = JSON.parse(run(binary, ['doctor', '--json'], { timeout: 15000 }));
if (doctor.bridge_version !== 1) throw new Error('Native binary does not report the expected bridge version.'); if (doctor.bridge_version !== 1) throw new Error('Native binary does not report the expected bridge version.');
@@ -135,7 +135,7 @@ try {
// Keep desktop build definitions for Xmake's initial option discovery; // Keep desktop build definitions for Xmake's initial option discovery;
// desktop application sources and resources are intentionally not bundled. // desktop application sources and resources are intentionally not bundled.
for (const entry of ['apps/cli', 'apps/desktop/xmake', for (const entry of ['apps/cli', 'apps/desktop/xmake',
'libs/controller/include/controller_session.h', 'libs/controller/include',
'libs/controller/src', 'libs/terminal', 'libs/wire/include', 'libs/controller/src', 'libs/terminal', 'libs/wire/include',
'libs/wire/src', 'deps/submodules/minirtc', 'libs/wire/src', 'deps/submodules/minirtc',
'deps/submodules/xmake.lua', 'deps/thirdparty', 'deps/patches', 'xmake.lua', 'deps/submodules/xmake.lua', 'deps/thirdparty', 'deps/patches', 'xmake.lua',
-1
View File
@@ -6,7 +6,6 @@ local credential_patch = path.join(source_dir, "deps/patches/minirtc-credential-
add_requires("spdlog 1.14.1", {system = false}) add_requires("spdlog 1.14.1", {system = false})
if is_plat("windows") then if is_plat("windows") then
set_runtimes("MT")
add_requires("zlib 1.3.1", {system = false, configs = {shared = false}}) add_requires("zlib 1.3.1", {system = false, configs = {shared = false}})
end end
@@ -0,0 +1,16 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-30
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CONTROLLER_DIAGNOSTICS_H_
#define _CONTROLLER_DIAGNOSTICS_H_
#include <string>
namespace crossdesk {
void InitializeControllerDiagnostics(const std::string &directory);
}
#endif
@@ -0,0 +1,79 @@
#include "controller_diagnostics.h"
#include <log.h>
#include <string_view>
namespace crossdesk {
namespace {
// Signaling errors and SDP can contain credentials. Only audited transport
// events reach this sink; arbitrary MiniRTC messages remain suppressed.
std::string DiagnosticEvent(const spdlog::details::log_msg &message) {
std::string_view source = message.source.filename ? message.source.filename : "";
const auto slash = source.find_last_of("/\\");
if (slash != std::string_view::npos)
source.remove_prefix(slash + 1);
const std::string_view text(message.payload.data(), message.payload.size());
if (source == "controller_session.cpp")
return std::string(text); // Local states/counters only at these call sites.
if (source == "ice_transport.cpp") {
const auto marker = text.rfind("state_change: ");
if (marker != std::string_view::npos) {
const auto state = text.substr(marker + 14);
for (const auto allowed : {"disconnected", "gathering", "connecting",
"connected", "ready", "failed"})
if (state == allowed)
return "ICE state: " + std::string(state);
}
for (const auto allowed : {"Traversal using relay server", "Traversal using p2p",
"Selected ICE pair is not available yet"})
if (text == allowed)
return std::string(text);
}
if (source == "ice_agent.cpp") {
for (const auto allowed : {"DTLS handshake started, awaiting peer packets...",
"DTLS handshake retransmission failed",
"DTLS peer certificate missing",
"DTLS peer fingerprint mismatch",
"DTLS peer fingerprint verified",
"DTLS peer not verified", "No SRTP profile selected",
"SSL_export_keying_material failed", "StartDtls failed",
"libnice has no relay upgrade extension; rebuild dependencies"})
if (text == allowed)
return std::string(text);
for (const auto prefix : {"DTLS handshake timed out after ",
"DTLS handshake start failed, err=",
"DTLS packet processing failed, err="})
if (text.substr(0, std::char_traits<char>::length(prefix)) == prefix)
return std::string(prefix); // Omit variable payload entirely.
}
return {};
}
class DiagnosticSink final : public spdlog::sinks::base_sink<std::mutex> {
public:
explicit DiagnosticSink(const std::string &directory)
: file_(directory + "/transport.log", 1024 * 1024, 2) {}
private:
void sink_it_(const spdlog::details::log_msg &message) override {
const auto event = DiagnosticEvent(message);
if (event.empty())
return;
auto sanitized = message;
sanitized.payload = spdlog::string_view_t(event.data(), event.size());
file_.log(sanitized);
file_.flush();
}
void flush_() override { file_.flush(); }
spdlog::sinks::rotating_file_sink_mt file_;
};
} // namespace
void InitializeControllerDiagnostics(const std::string &directory) {
auto sink = std::make_shared<DiagnosticSink>(directory);
minirtc::InitLogger(directory);
auto logger = minirtc::get_logger();
// Connect calls this after the previous peer and its callbacks stop.
logger->sinks() = {std::move(sink)};
logger->set_level(spdlog::level::info);
}
} // namespace crossdesk
+10 -3
View File
@@ -1,4 +1,5 @@
#include "controller_session.h" #include "controller_session.h"
#include "controller_diagnostics.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
@@ -136,8 +137,8 @@ void ControllerSession::Connect(SessionOptions options, std::string device,
// The server assigns a temporary controller identity on this connection. // The server assigns a temporary controller identity on this connection.
// Never register a desktop identity or persist local credentials. // Never register a desktop identity or persist local credentials.
client_id_.clear(); client_id_.clear();
minirtc::InitLogger(options_.log_directory); InitializeControllerDiagnostics(options_.log_directory);
minirtc::get_logger()->set_level(spdlog::level::off); SPDLOG_LOGGER_INFO(minirtc::get_logger(), "Session registering");
auto p = Parameters(); auto p = Parameters();
peer_ = CreatePeer(&p); peer_ = CreatePeer(&p);
if (!peer_) { if (!peer_) {
@@ -169,6 +170,7 @@ void ControllerSession::Tick() {
if (Finished()) if (Finished())
break; break;
if (e.kind == 0) { if (e.kind == 0) {
SPDLOG_LOGGER_INFO(minirtc::get_logger(), "Signal status: {}", e.code);
if (e.code == SignalTlsCertError) { if (e.code == SignalTlsCertError) {
Fail("tls_certificate_error"); Fail("tls_certificate_error");
break; break;
@@ -199,6 +201,8 @@ void ControllerSession::Tick() {
password_.clear(); password_.clear();
} }
} else if (e.kind == 1) { } else if (e.kind == 1) {
SPDLOG_LOGGER_INFO(minirtc::get_logger(), "Connection status: {} (session {})",
e.code, state_);
if (e.code == ::Connected && !Connected()) { if (e.code == ::Connected && !Connected()) {
state_ = "connected"; state_ = "connected";
auto hello = MakeHostInformation("CrossDesk CLI", {}, false); auto hello = MakeHostInformation("CrossDesk CLI", {}, false);
@@ -215,7 +219,7 @@ void ControllerSession::Tick() {
else if (e.code == RemoteUnavailable) else if (e.code == RemoteUnavailable)
Fail("device_offline_or_unavailable"); Fail("device_offline_or_unavailable");
else if (e.code == Failed) else if (e.code == Failed)
Fail("connection_failed"); Fail(Connected() ? "connection_lost" : "connection_failed");
else if (e.code == Closed || e.code == Disconnected) else if (e.code == Closed || e.code == Disconnected)
Fail("remote_disconnected"); Fail("remote_disconnected");
} else if (e.kind == 2 && Connected()) { } else if (e.kind == 2 && Connected()) {
@@ -275,6 +279,7 @@ void ControllerSession::ClosePeers() {
password_.clear(); password_.clear();
} }
void ControllerSession::Fail(const char *reason) { void ControllerSession::Fail(const char *reason) {
SPDLOG_LOGGER_INFO(minirtc::get_logger(), "Session failed: {}", reason);
ReleaseInput(); ReleaseInput();
error_ = reason; error_ = reason;
state_ = "failed"; state_ = "failed";
@@ -345,6 +350,8 @@ void ControllerSession::Preview() {
Send(kControlStream, s.ToJson().dump()); Send(kControlStream, s.ToJson().dump());
} }
void ControllerSession::SetControl(bool enabled) { void ControllerSession::SetControl(bool enabled) {
if (control_ != enabled)
SPDLOG_LOGGER_INFO(minirtc::get_logger(), "Remote control: {}", enabled);
if (!enabled) if (!enabled)
ReleaseInput(); ReleaseInput();
control_ = enabled; control_ = enabled;
+2 -1
View File
@@ -54,7 +54,7 @@ bool KittyReply(const std::string &response);
// Incremental parser: protocol replies and unknown escapes are consumed // Incremental parser: protocol replies and unknown escapes are consumed
// locally. Key taps always release immediately; paste uses the UTF-8 text // locally. Key taps always release immediately; paste uses the UTF-8 text
// protocol. In command_mode, control records the mode to restore while the // protocol. In command_mode, control records the mode to restore while the
// session's remote input is suspended. // session's remote input is suspended until the user explicitly leaves the mode.
class Input { class Input {
public: public:
void Feed(const std::string &bytes, ControllerSession &, const Grid *grid); void Feed(const std::string &bytes, ControllerSession &, const Grid *grid);
@@ -62,6 +62,7 @@ public:
std::string error; std::string error;
double zoom = 1, center_x = .5, center_y = .5; double zoom = 1, center_x = .5, center_y = .5;
uint64_t view_revision = 0; uint64_t view_revision = 0;
uint64_t mouse_reports = 0;
private: private:
std::string pending_; std::string pending_;
+15 -14
View File
@@ -31,10 +31,14 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
s.SetControl(false); s.SetControl(false);
return true; return true;
} }
bool command = command_mode; if (command_mode && (key == 27 || key == 3)) {
command_mode = false; command_mode = false;
if (key == 'q' || (key == 3 && !command)) { s.SetControl(control);
return true;
}
if (key == 'q' || key == 3) {
exit = true; exit = true;
command_mode = false;
control = false; control = false;
} else if (key == 'c') { } else if (key == 'c') {
control = !control; control = !control;
@@ -42,12 +46,12 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
key == 'd' || key == 'i' || key == 'j' || key == 'k' || key == 'd' || key == 'i' || key == 'j' || key == 'k' ||
key == 'l') { key == 'l') {
Navigate(char(key), grid); Navigate(char(key), grid);
} else if (!command) { } else if (!command_mode) {
return false; return false;
} else if (key != 27 && key != 3) { } else {
error = "Unknown local command; Ctrl+] for help"; error = "Unknown local command; ? for help";
} }
s.SetControl(control); s.SetControl(control && !command_mode);
return true; return true;
} }
void Input::Navigate(char key, const Grid *grid) { void Input::Navigate(char key, const Grid *grid) {
@@ -94,7 +98,6 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
if (pending_.size() + bytes.size() > 32768) { if (pending_.size() + bytes.size() > 32768) {
pending_.clear(); pending_.clear();
pasting_ = false; pasting_ = false;
command_mode = false;
s.SetControl(false); s.SetControl(false);
control = false; control = false;
return; return;
@@ -117,11 +120,8 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
auto end = pending_.find("\033[201~"); auto end = pending_.find("\033[201~");
if (end == std::string::npos) if (end == std::string::npos)
return; // bounded by 32 KiB; one UTF-8 transaction return; // bounded by 32 KiB; one UTF-8 transaction
if (command_mode) { // Drop local-mode paste without leaving the mode or running commands.
// Pasted text must never become local commands or leak to the remote. if (control && !command_mode && !s.Text(pending_.substr(0, end)))
command_mode = false;
s.SetControl(control);
} else if (control && !s.Text(pending_.substr(0, end)))
error = "Remote UTF-8 text input unavailable"; error = "Remote UTF-8 text input unavailable";
pending_.erase(0, end + 6); pending_.erase(0, end + 6);
pasting_ = false; pasting_ = false;
@@ -159,7 +159,6 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
pending_.erase(0, end + 1); pending_.erase(0, end + 1);
if (seq == "\033[O") { if (seq == "\033[O") {
control = false; control = false;
command_mode = false;
s.SetControl(false); s.SetControl(false);
continue; continue;
} }
@@ -224,6 +223,8 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
} else } else
mouse = mouse && cur == limit; mouse = mouse && cur == limit;
} }
if (mouse)
++mouse_reports;
if (grid && mouse && !command_mode) { if (grid && mouse && !command_mode) {
auto pos = MapPointer(*grid, x - 1, y - 1); auto pos = MapPointer(*grid, x - 1, y - 1);
if (!control) { if (!control) {
+5
View File
@@ -9,6 +9,11 @@ local desktop_platform = is_plat("windows", "macosx", "linux") or
add_rules("mode.release", "mode.debug") add_rules("mode.release", "mode.debug")
set_languages("c++17") set_languages("c++17")
set_encodings("utf-8") set_encodings("utf-8")
if is_plat("windows") then
-- Shared wire targets and third-party packages must use the same CRT as
-- the native applications; a setting inside apps/cli does not reach them.
set_runtimes("MT")
end
add_requires("nlohmann_json 3.11.3") add_requires("nlohmann_json 3.11.3")
option("CROSSDESK_BUILD_DESKTOP") option("CROSSDESK_BUILD_DESKTOP")