mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-02 20:12:44 +08:00
Compare commits
4
Commits
cli-v0.1.5
...
cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fab61871e | ||
|
|
d776889562 | ||
|
|
30d409ff49 | ||
|
|
6de7165fb0 |
@@ -22,7 +22,7 @@
|
||||
|
||||
</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)
|
||||
|
||||
@@ -33,7 +33,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
|
||||
- **跨平台远控:** Windows、macOS、Linux 桌面互控,浏览器及原生 iOS / Android 客户端访问电脑;鸿蒙 PC 与移动端处于开发阶段。
|
||||
- **局域网 IP 直连:** 桌面客户端通过 IP、端口和六位密码连接,无需云端信令服务;支持独立端口、记住密码及按模式显示近期设备。
|
||||
- **终端主控:** 在终端中通过设备 ID 观看和操控远程桌面,支持 ANSI / Kitty / Sixel 画面、局部缩放与平移、持续本地命令模式及 Pi 扩展,可单独构建。
|
||||
- **桌面墙:** 多设备实时预览、分组、放大操控、群控和批量文件分发。
|
||||
- 桌面墙:多设备实时预览、分组、放大操控、群控和批量文件分发。
|
||||
- **画面与声音:** H.264 / AV1、硬件编解码、远端音频,以及会话内画质和帧率调整;可用能力随平台与设备而异。
|
||||
- **日常协作:** 多显示器切换、键鼠与组合键、文本剪贴板、文件传输。
|
||||
- **可信设备:** 桌面端可选开启可信设备限制,在连接密码之外校验设备身份;支持复制、粘贴配对信息,提前添加允许访问本机的设备。
|
||||
@@ -157,7 +157,7 @@ CrossDesk 让你从电脑、浏览器、手机或终端连接远端电脑,也
|
||||
| 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 协议全景:同一远程桌面以像素画面显示,保留状态栏和操作快捷键" /> |
|
||||
| 用彩色字符显示桌面轮廓,适合普通终端;查看局部可按 `d` 放大。 | 直接显示像素画面,适合支持 Kitty 图形协议的终端,如 Ghostty。 |
|
||||
| 用彩色字符显示桌面轮廓,适合普通终端;查看局部可用 `+/-` 或滚轮以鼠标位置缩放。 | 直接显示像素画面,适合支持 Kitty 图形协议的终端,如 Ghostty。 |
|
||||
|
||||
两图采集于 **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` | 连接后直接启用键鼠控制;省略时默认只读 |
|
||||
| `--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+]` 进入本地命令模式并暂停远端输入 |
|
||||
| 本地命令模式 | 执行命令后持续保持;`c` 选择手动返回后是否启用控制,`q` 退出 CLI |
|
||||
| `Esc` / `Ctrl+C` | 在本地命令模式中手动返回所选的控制或只读模式 |
|
||||
| `+` / `=` / `-` | 在只读或本地命令模式中放大 / 缩小 |
|
||||
| `d` / `f` | 在只读或本地命令模式中切换原始细节 / 完整桌面;只读时也可点击目标进入细节 |
|
||||
| `+` / `=` / `-` | 在只读或本地命令模式中以鼠标位置放大 / 缩小 |
|
||||
| 滚轮 / 点击 | 只读模式中,滚轮以鼠标位置缩放;点击目标进入原始细节 |
|
||||
| `f` | 在只读或本地命令模式中恢复完整桌面,同时重置倍率、平移和细节模式 |
|
||||
| 方向键 / `i j k l` | 在只读或本地命令模式中平移;`i` 上、`j` 左、`k` 下、`l` 右 |
|
||||
|
||||
鼠标下的画面内容在缩放时尽量保持原位,到达桌面边缘时限制裁剪范围;没有有效鼠标位置时沿用当前中心。本地命令模式中鼠标仅更新缩放锚点,点击和滚轮不执行视图操作。
|
||||
|
||||
**控制模式下退出:先按 `Ctrl+]`,再按 `q`。** 普通 `c`、`q` 和 `Ctrl+C` 在控制模式中会发送到远端。
|
||||
|
||||
`Ctrl+]` 模式不会因为缩放、平移或按 `c` 自动结束。状态栏中的 `CONTROL PAUSED` / `READ ONLY` 表示返回后是否启用控制;按 `Esc` / `Ctrl+C` 手动返回。再次按 `Ctrl+]` 也可返回,若返回控制模式则向远端发送一次该组合键。
|
||||
|
||||
+9
-6
@@ -22,7 +22,7 @@
|
||||
|
||||
</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)
|
||||
|
||||
@@ -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.
|
||||
- **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.
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -157,7 +157,7 @@ Remote sessions support monitor switching, video quality and frame-rate controls
|
||||
| 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" /> |
|
||||
| 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.
|
||||
|
||||
@@ -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 |
|
||||
| `--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
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| `+` / `=` / `-` | Zoom in / out at the mouse position in read-only or local command mode |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
||||
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
@@ -59,20 +59,24 @@ crossdesk-cli 123456789 --quality balanced --fps 8
|
||||
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` |
|
||||
| 放大局部 / 回到全景 | 只读或本地命令模式下按 `d` / `f`;只读时也可点击目标进入细节 |
|
||||
| 缩放 / 平移 | 只读或本地命令模式下用 `+/-` 或滚轮缩放;方向键或 `i` 上、`j` 左、`k` 下、`l` 右平移 |
|
||||
| 缩放 | 只读或本地命令模式下用 `+` / `=` / `-` 以鼠标位置缩放;只读时也可用滚轮 |
|
||||
| 回到全景 | 只读或本地命令模式下按 `f`,同时重置倍率、平移和细节模式 |
|
||||
| 平移 | 只读或本地命令模式下用方向键或 `i` 上、`j` 左、`k` 下、`l` 右 |
|
||||
| 原始细节 | 只读时点击目标位置,或以 `--zoom native` 启动 |
|
||||
| 暂停远端输入并进入本地命令模式 | `Ctrl+]` |
|
||||
| 选择返回后的控制状态 | 本地命令模式下按 `c`,再按 `Esc` / `Ctrl+C` 返回 |
|
||||
| 退出连接 | 只读模式下按 `q` / `Ctrl+C`;控制模式下先按 `Ctrl+]`,再按 `q` |
|
||||
|
||||
本地命令模式在缩放、平移和按 `c` 后持续保持,直到手动返回。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。再次按 `Ctrl+]` 也可离开本地命令模式,若返回控制模式则同时向远端发送该组合键。
|
||||
缩放尽量保持鼠标下的画面内容原位;没有有效鼠标位置时沿用当前中心。底部提示随模式和窗口宽度调整,`Zoom at mouse` 表示鼠标锚点缩放,`Full view` / `Full` 表示恢复全景。
|
||||
|
||||
本地命令模式在缩放、平移和按 `c` 后持续保持,直到手动返回。此模式中鼠标仅更新缩放锚点,点击和滚轮不执行视图操作。控制模式中的 `Ctrl+T`、`Ctrl+C` 和普通按键均传给远端。再次按 `Ctrl+]` 也可离开本地命令模式,若返回控制模式则同时向远端发送该组合键。
|
||||
|
||||
## 更多文档与功能范围
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
| connect | device:9位数字字符串, display:0..63(默认0) | 从专用 password fd 读取至换行/EOF(最多256字节、等待5秒),开始异步注册/连接;ok不是远端已连接 |
|
||||
| status | 无 | 返回当前状态 |
|
||||
| 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 |
|
||||
| ack | frame_id、generation:非负整数 | 确认宿主消费帧;匹配当前等待帧才开放下一帧 |
|
||||
| disconnect | 无 | 释放输入、结束当前媒体会话 |
|
||||
|
||||
@@ -57,11 +57,11 @@ Windows 的可见传统控制台优先读取当前字体尺寸;Windows Termina
|
||||
|
||||
缩放在完整输出边界清理旧画面。上限 240 列、100 画面行;默认顶部预留一行状态栏、底部预留两行快捷键,另预留一列避免自动换行。顶部状态栏用反色区分,左侧为控制模式和连接进度,右侧为画面参数及网络信息;底部两行分别为会话和视图快捷键。视频和等待提示从顶部状态栏下方开始,鼠标坐标按视频区域映射,点击状态栏不会传给远端。按键分组使用统一列宽和左右留白,窄窗口自动减少列数、合并按键组并省略次要参数,累计帧数和输出字节数保留在日志中。控制模式下仅提示本地命令入口及后续操作,进入本地命令模式后才显示平移和缩放按键;终端高度不足时优先收起底部提示行。
|
||||
|
||||
快捷键行使用 `KEYS |`、`VIEW |` 等固定宽度标题,标题和按键组之间用竖线分隔;键名用 `<c>`、`<+/->` 等标签标识。宽窗口中的分组分隔线按列对齐,窄窗口保留标题和完整按键标签,按可用宽度合并或减少分组。
|
||||
快捷键行使用 `KEYS |`、`VIEW |` 等固定宽度标题,标题和按键组之间用竖线分隔;键名用 `<c>`、`<+/->` 等标签标识。`Zoom at mouse` 表示鼠标锚点缩放,`Full view` / `Full` 表示恢复全景,`Pan` 表示平移。只读模式提示滚轮缩放,宽窗口还显示点击进入细节;本地命令模式提示返回、控制切换及键盘视图操作,宽窗口显示鼠标选择锚点和 `=` 放大。宽窗口中的分组分隔线按列对齐,窄窗口保留完整按键标签,优先保留缩放和全景,按可用宽度合并或减少分组。
|
||||
|
||||
顶部状态栏以整数显示实时帧率(例如 `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,显式像素模式报不支持;不修改用户终端配置。
|
||||
|
||||
@@ -90,6 +90,8 @@ crossdesk-cli 123456789 --renderer ansi --fps 8
|
||||
|
||||
`--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` 可用于与自动策略对比。
|
||||
|
||||
本地调度只在有新帧或需要重绘时消耗刷新时隙;短暂迟到保持原刷新节奏,长时间阻塞后跳过旧时隙,不连续补画过时帧。目标帧率不代表终端实际显示帧率。
|
||||
@@ -125,12 +127,13 @@ macOS 自带 Terminal 还需要在当前窗口勾选“显示 → 允许鼠标
|
||||
| 退出 CLI | `q` 或 `Ctrl+C` | `Ctrl+]`,再按 `q` |
|
||||
| 进入本地命令模式 | `?` 或 `Ctrl+]` | `Ctrl+]` |
|
||||
| 放大/缩小 | `+` / `=` / `-` | `Ctrl+]`,再按对应键 |
|
||||
| 原始细节/完整画面 | `d`(detail)/ `f`(fit),相邻字母键 | `Ctrl+]`,再按对应键 |
|
||||
| 恢复完整画面 | `f`,重置倍率、平移和细节模式 | `Ctrl+]`,再按 `f` |
|
||||
| 鼠标查看 | 滚轮以鼠标位置缩放;点击进入原始细节 | 进入本地命令模式后仅更新缩放锚点,使用缩放键调整 |
|
||||
| 平移 | 方向键或 `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)。
|
||||
|
||||
@@ -153,7 +156,7 @@ export CROSSDESK_CLI="$PWD/build/macosx/arm64/release/crossdesk-cli"
|
||||
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 停止时移除查询定时器和输入监听。
|
||||
|
||||
|
||||
@@ -22,12 +22,19 @@
|
||||
|
||||
新增回归代码、基准、假传输和验证产物均位于工程外临时目录,未向工程或子模块添加测试。此次完整构建与运行验证覆盖 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` 和命令帮助检查通过。
|
||||
- 工程外 ASan/UBSan 检查覆盖本地命令前缀、只读快捷键、远端 Ctrl+T/Ctrl+C/Ctrl+]、鼠标释放、取消命令、粘贴隔离、UTF-8/CSI 分片、失焦与退出;原有会话/渲染检查通过。
|
||||
- 平移字母键采用 `i` 上、`j` 左、`k` 下、`l` 右;工程外 ASan/UBSan 检查确认四个方向与普通/应用模式方向键一致,控制模式中普通字母仍传给远端。
|
||||
- 细节/全景采用相邻的 `d`/`f`;工程外 ASan/UBSan 检查确认细节保留中心、全景复位缩放与中心、前缀命令和鼠标点击行为正确,控制模式中普通字母仍传给远端。
|
||||
- 当时细节/全景采用相邻的 `d`/`f`(`d` 已于 2026-11-02 移除);工程外 ASan/UBSan 检查确认细节保留中心、全景复位缩放与中心、前缀命令和鼠标点击行为正确,控制模式中普通字母仍传给远端。
|
||||
- 实际二进制 bridge 验证命令状态、错误恢复和退出响应;生产 view 配合假传输的 PTY 检查验证快捷键、模式提示、隐藏密码和终端恢复。实际二进制的 bridge 与 PTY 回归同样通过。
|
||||
- Pi 1.4.0 SDK 严格类型检查和模拟组件运行通过,覆盖控制按键转发、命令提示、取消、缩放、退出与 bridge 启动失败后的关闭操作。
|
||||
|
||||
|
||||
+6
-2
@@ -608,7 +608,7 @@ int View(SessionOptions options, const std::string &device,
|
||||
: sixel ? "SIXEL"
|
||||
: detail == AnsiDetail::Quadrant ? "QUAD"
|
||||
: "HALF") +
|
||||
(input.native ? " | Detail x" : " | Fit x") +
|
||||
(input.native ? " | Detail x" : " | Zoom x") +
|
||||
std::to_string(int(input.zoom)) +
|
||||
(last_frame ? " | " + std::to_string(last_frame->width) + "x" +
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
Remote previews prioritize resolution at every frame rate.
|
||||
Cell aspect is character width/height: auto detects terminal geometry; use --cell-aspect to calibrate.
|
||||
Display index is zero-based. ANSI cannot reproduce full desktop text at fit scale.
|
||||
Auto rendering prefers Kitty, then Sixel, then ANSI. Explicit pixel renderers require a successful probe.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chunkdash/crossdesk-cli",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"description": "CrossDesk remote desktop controller for the terminal",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -227,10 +227,10 @@ export default function crossdesk(pi: ExtensionAPI) {
|
||||
const ack = frame; frame = undefined;
|
||||
if (ack) queueMicrotask(() => send("ack", ack)); // Host consumed the frame; not a physical display timestamp.
|
||||
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"
|
||||
: "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),
|
||||
: "c control | q quit | +/- zoom | f full | arrows/ijkl pan | ? commands";
|
||||
return [truncateToWidth(`CrossDesk ${device} | ${status} ${native ? "detail" : "zoom"} x${zoom} | Ctrl+] commands`, width),
|
||||
...lines.slice(0, h).map(line => truncateToWidth(line, width)),
|
||||
truncateToWidth(`${hint}${error ? " | " + error : lines.length ? "" : " | Waiting for remote video…"}`, width)];
|
||||
},
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ target_end()
|
||||
|
||||
target("crossdesk-cli")
|
||||
set_kind("binary")
|
||||
set_version("0.1.5")
|
||||
set_version("0.1.6")
|
||||
add_deps("crossdesk_terminal")
|
||||
add_files("main.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.
|
||||
// The encoder cadence is independently specified by frame_rate.
|
||||
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.quality = options_.quality;
|
||||
if (resolution_) {
|
||||
|
||||
@@ -58,6 +58,8 @@ struct Grid {
|
||||
Rect Fit(int width, int height, int columns, int pixel_rows,
|
||||
double pixel_aspect = 1);
|
||||
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);
|
||||
std::optional<std::pair<float, float>> MapPointer(const Grid &, int column,
|
||||
int row);
|
||||
@@ -119,9 +121,11 @@ private:
|
||||
bool pasting_ = false;
|
||||
unsigned mouse_buttons_ = 0;
|
||||
float pointer_x_ = .5f, pointer_y_ = .5f;
|
||||
int mouse_column_ = -1, mouse_row_ = -1;
|
||||
void Tap(ControllerSession &, int key, int modifier = 0);
|
||||
bool LocalKey(int key, ControllerSession &, 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 &,
|
||||
const Grid *);
|
||||
};
|
||||
|
||||
+58
-14
@@ -45,8 +45,7 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
|
||||
} else if (key == 'c') {
|
||||
control = !control;
|
||||
} else if (key == '+' || key == '=' || key == '-' || key == 'f' ||
|
||||
key == 'd' || key == 'i' || key == 'j' || key == 'k' ||
|
||||
key == 'l') {
|
||||
key == 'i' || key == 'j' || key == 'k' || key == 'l') {
|
||||
Navigate(char(key), grid);
|
||||
} else if (!command_mode) {
|
||||
return false;
|
||||
@@ -58,20 +57,52 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
|
||||
mouse_buttons_ = 0;
|
||||
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) {
|
||||
if (key == '+' || key == '=')
|
||||
zoom = std::min(32.0, zoom * 2);
|
||||
Zoom(std::min(32.0, zoom * 2), grid);
|
||||
if (key == '-')
|
||||
zoom = std::max(1.0, zoom / 2);
|
||||
Zoom(std::max(1.0, zoom / 2), grid);
|
||||
if (key == 'f') {
|
||||
native = false;
|
||||
zoom = 1;
|
||||
center_x = center_y = .5;
|
||||
}
|
||||
if (key == 'd') {
|
||||
native = true;
|
||||
zoom = 1;
|
||||
}
|
||||
// Use the displayed crop as the pan step, including native pixel mode.
|
||||
const double dx = grid && 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) {
|
||||
++mouse_reports;
|
||||
// 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;
|
||||
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 (pos && !release && ((b & 64) || b == 0)) {
|
||||
center_x = pos->first;
|
||||
center_y = pos->second;
|
||||
Navigate((b & 64) ? ((b & 1) ? '-' : '+') : 'd', grid);
|
||||
if (pos && !release) {
|
||||
if (b & 64) {
|
||||
Navigate((b & 1) ? '-' : '+', grid);
|
||||
} else if (b == 0) {
|
||||
center_x = pos->first;
|
||||
center_y = pos->second;
|
||||
native = true;
|
||||
zoom = 1;
|
||||
++view_revision;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -248,6 +291,7 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
|
||||
control = false;
|
||||
s.SetControl(false);
|
||||
mouse_buttons_ = 0;
|
||||
mouse_column_ = mouse_row_ = -1;
|
||||
continue;
|
||||
}
|
||||
int key = 0;
|
||||
|
||||
@@ -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 + 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 ||
|
||||
v.row_offset < 0 || v.row_offset > 100 ||
|
||||
!std::isfinite(v.cell_aspect) || v.cell_aspect < .2 ||
|
||||
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.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 ||
|
||||
(f.width & 1) || (f.height & 1) ||
|
||||
size_t(f.width) * f.height > 16777216 ||
|
||||
f.nv12.size() < size_t(f.width) * f.height * 3 / 2)
|
||||
!width || !height || width > 8192 || height > 8192 || (width & 1) ||
|
||||
(height & 1) || size_t(width) * height > 16777216)
|
||||
throw std::invalid_argument("invalid frame or viewport");
|
||||
}
|
||||
static Rect Crop(const DecodedFrame &f, const Viewport &v) {
|
||||
double width = f.width, height = f.height;
|
||||
static void Validate(const DecodedFrame &f, const Viewport &v) {
|
||||
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) {
|
||||
const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 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)),
|
||||
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 y = std::clamp(int(v.center_y * f.height - h * .5), 0, int(f.height) - h);
|
||||
int x = std::clamp(int(v.center_x * source_width - w * .5), 0,
|
||||
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};
|
||||
}
|
||||
// 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) {
|
||||
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,
|
||||
sy = v.utf8 ? 2 : 1;
|
||||
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,
|
||||
Fit(crop.width, crop.height, width, height, v.cell_aspect * sy / sx),
|
||||
{}};
|
||||
g.source = crop;
|
||||
g.source_width = f.width;
|
||||
g.source_height = f.height;
|
||||
g.source_width = source_width;
|
||||
g.source_height = source_height;
|
||||
g.samples_x = sx;
|
||||
g.samples_y = sy;
|
||||
return g;
|
||||
@@ -553,7 +563,7 @@ PixelFrame ImagePixels(const DecodedFrame &f, const Viewport &requested) {
|
||||
pixels.height =
|
||||
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);
|
||||
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,
|
||||
double(v.columns) * v.cell_aspect * pixels.height /
|
||||
(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);
|
||||
int h =
|
||||
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;
|
||||
for (;;) {
|
||||
auto r = Fit(crop.width, crop.height, w, h,
|
||||
|
||||
@@ -79,41 +79,52 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
|
||||
|
||||
std::string keys, view;
|
||||
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) {
|
||||
keys =
|
||||
Columns("LOCAL",
|
||||
count >= 3 ? std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle control",
|
||||
"<q> Quit"}
|
||||
: std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle; <q> Quit"},
|
||||
inner, count);
|
||||
keys = Columns(
|
||||
"LOCAL",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<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",
|
||||
"<c> Toggle; <q> Quit"},
|
||||
inner, count);
|
||||
} else if (bar.control) {
|
||||
keys = Columns("KEYS", {"<Ctrl+]> Commands", "Input -> remote"}, inner,
|
||||
count);
|
||||
} else {
|
||||
keys = Columns("KEYS",
|
||||
count >= 3
|
||||
? std::vector<std::string>{"<c> Control", "<q> Quit",
|
||||
"<Ctrl+]> Commands"}
|
||||
: std::vector<std::string>{"<c> Control; <q> Quit",
|
||||
"<Ctrl+]> Commands"},
|
||||
inner, count);
|
||||
keys = Columns(
|
||||
"KEYS",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<c> Control", "<q> Quit",
|
||||
"<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",
|
||||
"<Ctrl+]> Commands"},
|
||||
inner, count);
|
||||
}
|
||||
if (bar.control && !bar.command_mode) {
|
||||
view = Columns("THEN", {"<c> Toggle control", "<q> Quit"}, inner, count);
|
||||
} else {
|
||||
const auto zoom = bar.command_mode ? "<+/-> Zoom at mouse"
|
||||
: "<+/wheel/-> Zoom at mouse";
|
||||
view = Columns(
|
||||
"VIEW",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom",
|
||||
"<f> Fit", "<d> Detail"}
|
||||
? std::vector<std::string>{"<+/-> Zoom at mouse",
|
||||
bar.command_mode ? "<=> Zoom in"
|
||||
: "<wheel> Zoom",
|
||||
"<arrows/ijkl> Pan", "<f> Full view"}
|
||||
: count == 3
|
||||
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom",
|
||||
"<f> Fit; <d> Detail"}
|
||||
: std::vector<std::string>{"<arrows> Pan; <+/-> Zoom",
|
||||
"<f> Fit; <d> Detail"},
|
||||
? std::vector<std::string>{zoom, "<arrows/ijkl> Pan",
|
||||
"<f> Full view"}
|
||||
: std::vector<std::string>{zoom, "<arrows> Pan; <f> Full"},
|
||||
inner, count);
|
||||
}
|
||||
} else {
|
||||
@@ -139,11 +150,18 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
|
||||
? std::vector<std::string>{"<c> Toggle control | <q> Quit",
|
||||
"<c> Toggle; <q> Quit", "<c> / <q>",
|
||||
"<q>", "q"}
|
||||
: std::vector<std::string>{"<arrows> Pan | <+/-> Zoom | <f> Fit | "
|
||||
"<d> Detail",
|
||||
"<+/-> Zoom | <f> Fit | <d> Detail",
|
||||
"<f> Fit | <d> Detail", "<f> Fit", "<f>",
|
||||
"f"},
|
||||
: bar.command_mode
|
||||
? std::vector<
|
||||
std::string>{"<+/-> Zoom at mouse | <arrows> Pan | <f> Full",
|
||||
"<+/-> Zoom at mouse | <f> Full",
|
||||
"<+/-> 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);
|
||||
}
|
||||
std::vector<std::string> lines{status, keys, view};
|
||||
|
||||
Reference in New Issue
Block a user