mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(ci): format code and platform-gate the ctrl glyph in keymap test (#67)
cargo fmt over app.rs/pane.rs/settings.rs/keymap.rs (rustfmt CI job). key_chords_splits_a_sequence_into_keycap_groups hard-coded ⌃ for the ctrl modifier, which only renders that way on macOS; elsewhere key_tokens maps ctrl to "Ctrl", so the test failed on the Linux and Windows runners. Gate the expected glyph behind target_os like the existing SECONDARY and SHIFT constants. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: repomix-reference-tty7
|
||||
description: Reference codebase for Tty7. Use this skill when you need to understand the structure, implementation patterns, or code details of the Tty7 project.
|
||||
---
|
||||
|
||||
# Tty7 Codebase Reference
|
||||
|
||||
76 files | 29369 lines | 318031 tokens
|
||||
|
||||
## Overview
|
||||
|
||||
Use this skill when you need to:
|
||||
- Understand project structure and file organization
|
||||
- Find where specific functionality is implemented
|
||||
- Read source code for any file
|
||||
- Search for code patterns or keywords
|
||||
|
||||
## Files
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `references/summary.md` | **Start here** - Purpose, format explanation, and statistics |
|
||||
| `references/project-structure.md` | Directory tree with line counts per file |
|
||||
| `references/files.md` | All file contents (search with `## File: <path>`) |
|
||||
| `references/tech-stacks.md` | Languages, frameworks, and dependencies per package (search with `## Tech Stack: <path>`) |
|
||||
|
||||
## How to Use
|
||||
|
||||
### 1. Find file locations
|
||||
|
||||
Check `project-structure.md` for the directory tree:
|
||||
|
||||
```
|
||||
src/
|
||||
index.ts (42 lines)
|
||||
utils/
|
||||
helpers.ts (128 lines)
|
||||
```
|
||||
|
||||
### 2. Read file contents
|
||||
|
||||
Grep in `files.md` for the file path:
|
||||
|
||||
```
|
||||
## File: src/utils/helpers.ts
|
||||
```
|
||||
|
||||
### 3. Search for code
|
||||
|
||||
Grep in `files.md` for keywords:
|
||||
|
||||
```
|
||||
function calculateTotal
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
**Understand a feature:**
|
||||
1. Search `project-structure.md` for related file names
|
||||
2. Read the main implementation file in `files.md`
|
||||
3. Search for imports/references to trace dependencies
|
||||
|
||||
**Debug an error:**
|
||||
1. Grep the error message or class name in `files.md`
|
||||
2. Check line counts in `project-structure.md` to find large files
|
||||
|
||||
**Find all usages:**
|
||||
1. Grep function or variable name in `files.md`
|
||||
|
||||
## Tips
|
||||
|
||||
- Use line counts in `project-structure.md` to estimate file complexity
|
||||
- Search `## File:` pattern to jump between files
|
||||
- Check `summary.md` for excluded files, format details, and file statistics
|
||||
- Check `tech-stacks.md` for languages, frameworks, and dependencies (search `## Tech Stack:` to list packages)
|
||||
|
||||
---
|
||||
|
||||
This skill was generated by [Repomix](https://github.com/yamadashy/repomix)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
# Directory Structure
|
||||
|
||||
```
|
||||
.cargo/
|
||||
config.toml (5 lines)
|
||||
.claude/
|
||||
skills/
|
||||
verify/
|
||||
SKILL.md (46 lines)
|
||||
.github/
|
||||
ISSUE_TEMPLATE/
|
||||
config.yml (8 lines)
|
||||
issue.yml (43 lines)
|
||||
scripts/
|
||||
bundle-linux.sh (35 lines)
|
||||
bundle-macos.sh (127 lines)
|
||||
bundle-windows.ps1 (45 lines)
|
||||
windows-installer.iss (59 lines)
|
||||
workflows/
|
||||
ci.yml (67 lines)
|
||||
release.yml (106 lines)
|
||||
dependabot.yml (22 lines)
|
||||
scripts/
|
||||
bench/
|
||||
doom-fire-fps.patch (24 lines)
|
||||
fire.sh (41 lines)
|
||||
io.sh (27 lines)
|
||||
mem.sh (79 lines)
|
||||
README.md (142 lines)
|
||||
run_one.sh (102 lines)
|
||||
setup.sh (46 lines)
|
||||
fig-convert/
|
||||
.gitignore (8 lines)
|
||||
convert.mjs (74 lines)
|
||||
README.md (70 lines)
|
||||
src/
|
||||
core/
|
||||
actions.rs (13 lines)
|
||||
config.rs (897 lines)
|
||||
mod.rs (29 lines)
|
||||
osc.rs (248 lines)
|
||||
session.rs (186 lines)
|
||||
shells.rs (440 lines)
|
||||
threads.rs (42 lines)
|
||||
update.rs (381 lines)
|
||||
daemon/
|
||||
mod.rs (80 lines)
|
||||
pane.rs (2145 lines)
|
||||
pidfile.rs (121 lines)
|
||||
protocol.rs (609 lines)
|
||||
server.rs (646 lines)
|
||||
shell_integration.rs (977 lines)
|
||||
spawn.rs (511 lines)
|
||||
transport.rs (689 lines)
|
||||
winproc.rs (274 lines)
|
||||
terminal/
|
||||
cmd_editor.rs (641 lines)
|
||||
completion.rs (732 lines)
|
||||
element.rs (1625 lines)
|
||||
fps.rs (157 lines)
|
||||
fuzzy.rs (228 lines)
|
||||
highlight.rs (170 lines)
|
||||
history.rs (730 lines)
|
||||
hold.rs (265 lines)
|
||||
input.rs (788 lines)
|
||||
mod.rs (66 lines)
|
||||
palette.rs (134 lines)
|
||||
remote.rs (1549 lines)
|
||||
reverse_search.rs (305 lines)
|
||||
search.rs (558 lines)
|
||||
signature.rs (347 lines)
|
||||
size.rs (34 lines)
|
||||
typeahead.rs (352 lines)
|
||||
view.rs (4530 lines)
|
||||
ui/
|
||||
app.rs (1711 lines)
|
||||
hints.rs (240 lines)
|
||||
home.rs (243 lines)
|
||||
keymap.rs (181 lines)
|
||||
mod.rs (26 lines)
|
||||
palette.rs (409 lines)
|
||||
pane.rs (548 lines)
|
||||
presets.rs (287 lines)
|
||||
settings.rs (1035 lines)
|
||||
tab_strip.rs (382 lines)
|
||||
theme.rs (242 lines)
|
||||
main.rs (307 lines)
|
||||
.gitignore (28 lines)
|
||||
build.rs (31 lines)
|
||||
Cargo.toml (172 lines)
|
||||
CHANGELOG.md (334 lines)
|
||||
LICENSE (201 lines)
|
||||
README.md (148 lines)
|
||||
README.zh-CN.md (137 lines)
|
||||
repomix.config.json (32 lines)
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
This file is a merged representation of a subset of the codebase, containing files not matching ignore patterns, combined into a single document by Repomix.
|
||||
The content has been processed where content has been compressed (code blocks are separated by ⋮---- delimiter).
|
||||
|
||||
# Summary
|
||||
|
||||
## Purpose
|
||||
|
||||
This is a reference codebase organized into multiple files for AI consumption.
|
||||
It is designed to be easily searchable using grep and other text-based tools.
|
||||
|
||||
## File Structure
|
||||
|
||||
This skill contains the following reference files:
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `project-structure.md` | Directory tree with line counts per file |
|
||||
| `files.md` | All file contents (search with `## File: <path>`) |
|
||||
| `tech-stacks.md` | Languages, frameworks, and dependencies per package (search with `## Tech Stack: <path>`) |
|
||||
| `summary.md` | This file - purpose and format explanation |
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
- This file should be treated as read-only. Any changes should be made to the
|
||||
original repository files, not this packed version.
|
||||
- When processing this file, use the file path to distinguish
|
||||
between different files in the repository.
|
||||
- Be aware that this file may contain sensitive information. Handle it with
|
||||
the same level of security as you would the original repository.
|
||||
|
||||
## Notes
|
||||
|
||||
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
|
||||
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
|
||||
- Files matching these patterns are excluded: target/**, dist/**, Cargo.lock, assets/**, repomix-output.*, target/**, dist/**, Cargo.lock, assets/**, repomix-output.*
|
||||
- Files matching patterns in .gitignore are excluded
|
||||
- Files matching default ignore patterns are excluded
|
||||
- Content has been compressed - code blocks are separated by ⋮---- delimiter
|
||||
- Files are sorted by Git change count (files with more changes are at the bottom)
|
||||
|
||||
## Statistics
|
||||
|
||||
76 files | 29,369 lines
|
||||
|
||||
| Language | Files | Lines |
|
||||
|----------|------:|------:|
|
||||
| Rust | 48 | 27,141 |
|
||||
| Shell | 7 | 457 |
|
||||
| Markdown | 6 | 877 |
|
||||
| YAML | 5 | 246 |
|
||||
| No Extension | 3 | 237 |
|
||||
| TOML | 2 | 177 |
|
||||
| ISS | 1 | 59 |
|
||||
| PATCH | 1 | 24 |
|
||||
| JavaScript (ESM) | 1 | 74 |
|
||||
| JSON | 1 | 32 |
|
||||
| Other | 1 | 45 |
|
||||
|
||||
**Largest files:**
|
||||
- `src/terminal/view.rs` (4,530 lines)
|
||||
- `src/daemon/pane.rs` (2,145 lines)
|
||||
- `src/ui/app.rs` (1,711 lines)
|
||||
- `src/terminal/element.rs` (1,625 lines)
|
||||
- `src/terminal/remote.rs` (1,549 lines)
|
||||
- `src/ui/settings.rs` (1,035 lines)
|
||||
- `src/daemon/shell_integration.rs` (977 lines)
|
||||
- `src/core/config.rs` (897 lines)
|
||||
- `src/terminal/input.rs` (788 lines)
|
||||
- `src/terminal/completion.rs` (732 lines)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Tech Stacks
|
||||
|
||||
## Tech Stack: .
|
||||
|
||||
### Languages
|
||||
|
||||
- Rust
|
||||
|
||||
### Dependencies
|
||||
|
||||
- gpui
|
||||
- gpui_platform
|
||||
- gpui-component
|
||||
- gpui-component-assets
|
||||
- serde
|
||||
- reqwest_client
|
||||
- alacritty_terminal
|
||||
- notify-rust
|
||||
- notify
|
||||
- portable-pty
|
||||
- memchr
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- .gitignore
|
||||
- Cargo.toml
|
||||
@@ -0,0 +1,117 @@
|
||||
# Plan — issue #61「tmux like input model」落地方案
|
||||
|
||||
> Issue:<https://github.com/l0ng-ai/tty7/issues/61>
|
||||
> 方向已在 issue 评论中定调:**不解析 `~/.tmux.conf`**、不改默认零配置体验;分三步 —— ① Settings 内直接改快捷键 → ② 补齐 pane/tab 操作 → ③ 一键 tmux preset。
|
||||
> 本文把三步落成三个可独立发版的 PR,含文件级落点、验收标准与已核实的框架事实。
|
||||
|
||||
## 0. 已核实的技术事实(方案成立的前提)
|
||||
|
||||
| 事实 | 出处 | 对方案的意义 |
|
||||
|---|---|---|
|
||||
| gpui 原生支持多击序列绑定 + pending 回放:`ctrl-b n` 这类绑定会把 `ctrl-b` 置为 pending,1s 超时后 flush 并**回放**给下层(终端能收到);后继键未命中绑定时两键都回放 | zed rev `1d217ee` `gpui/src/window.rs:4744-4806` | tmux prefix 不需要自造输入模型;裸 `ctrl-b` 到 shell 有 1s 延迟(可接受,需文案说明) |
|
||||
| `App::bind_keys` 后绑定优先;有 `NoAction`/`Unbind` 空绑定可压掉旧键 | `gpui/src/action.rs:4`、`gpui/src/keymap.rs:66,196` | 热重绑可以**只追加**:NoAction 压旧键 + 追加新表,不必 clear |
|
||||
| `cx.clear_key_bindings()` 清的是**全局** keymap | `gpui/src/app.rs:2011` | 禁用——会连 gpui-component 的 input/list/menu 绑定一起清掉(`gpui-component …/src/{input/state.rs:133, list/list.rs:28, root.rs:25} …`) |
|
||||
| `App::intercept_keystrokes` 在 keymap 派发**之前**执行,可吞键 | `gpui/src/app.rs:1982`、`gpui/src/window.rs:4742` | 录键控件的实现基础:录键时按 ⌘T 必须被录下而不是触发 NewTab |
|
||||
| config 里的 keystroke 值**今天就接受序列**(按空白拆 chord 逐个 `Keystroke::parse`) | `src/ui/keymap.rs:168`(`keystroke_is_valid`) | preset 只是数据;但 `key_tokens`(`keymap.rs:105`)不会渲染序列,Settings 列表显示有缺口,需补 |
|
||||
| ⌘/Ctrl+1-9 切标签是裸 `on_key_down` 硬编码,不是 action | `src/ui/app.rs:1213-1230` | 目前不可重绑、preset 也够不着;需迁移成正式 action |
|
||||
| defaults→overrides 合并逻辑写了两份 | `src/ui/keymap.rs:19-29`(init)与 `src/ui/settings.rs:1302-1312`(渲染) | 先收敛成单一 `effective_bindings()`,PR 3 还要在中间插 preset 层 |
|
||||
| pane 树是纯二叉 split 树,leaf 泛型可用 `Pane<u32>` 纯值测试;ratio 夹在 0.1..0.9 | `src/ui/pane.rs:25,57,404` | 方向聚焦/resize/swap 全部可做成纯树算法 + 纯值单测 |
|
||||
|
||||
## 1. PR 1 — Settings 内直接改快捷键(录键 + 即时生效)
|
||||
|
||||
**目标**:Keybindings 页从只读变可编辑;改完立即生效并 `Config::save()` 写回 `config.json`;删掉 "restart to apply" 文案。config 格式不变(`keybindings: action名 → keystroke`,`core/config.rs:49`)。
|
||||
|
||||
### 落点
|
||||
|
||||
- **`ui/keymap.rs` — 抽 `effective_bindings(cx) -> Vec<(String, String)>`**
|
||||
- defaults → user overrides 的合并收敛为单一来源;`init`、`effective_key`、Settings 渲染全部改走它。
|
||||
- **`ui/keymap.rs` — 新增 `rebind(cx)`(热更新,不 clear)**
|
||||
- 记录上一份生效表(存 gpui global 或 keymap.rs 内部 static);对其中每个被改动/删除的 keystroke 追加一条 `KeyBinding::new(key, NoAction, ctx)` 压掉旧绑定,再整体追加新的生效表(后绑定优先,新表在 NoAction 之后追加即可复用同一个键)。
|
||||
- keymap 仅随「用户编辑次数」线性增长,可接受;`secondary-+` 别名与 `tab`/`shift-tab` 的 Terminal 特绑(`keymap.rs:44-51`)在 rebind 中原样保留。
|
||||
- **`ui/settings.rs` — Keybindings 行内编辑**
|
||||
- 点击行 → 该行进入录键态(显示 "Press new shortcut… / Esc to cancel");录键期间挂 `cx.intercept_keystrokes` 吞键并记录;Esc 取消,Backspace/Delete = 清除 override 回默认。
|
||||
- 归一化:录到的平台主修饰键(mac ⌘ / 其它 Ctrl)写成 `secondary`,保持 config 跨平台;其余照实写。
|
||||
- **冲突检测**:新键与生效表其他 action 相同 → 行内警告 + 「覆盖并解绑原 action」二次确认(原 action 写入 NoAction 语义 = override 为空?→ 直接把原 action 的 override 设为被让出,UI 显示 "—")。
|
||||
- 每行 reset 按钮 + 顶部 "Restore all defaults"(清空 `config.keybindings`)。
|
||||
- 持久化路径:`cx.update_global::<Config>` → `Config::save()`(`core/config.rs:334`,原子写已就绪)→ `keymap::rebind(cx)` → `cx.notify()`。
|
||||
- **顺手修显示缺口**:`key_tokens` 支持空白分隔序列,渲染成两组 keycap(`⌃B` `N`);Settings/palette 的 hint 同步受益(palette 走 `effective_key`,已动态读 config,无需改)。
|
||||
|
||||
### 验收 & 测试
|
||||
|
||||
- [ ] 改键后不重启:新键立即触发、旧键立即失效;写盘后重启行为一致。
|
||||
- [ ] 录键时按 ⌘T/⌘W 等被录下而非触发原 action;Esc 退出录键态无副作用。
|
||||
- [ ] 单测:override 合并优先级;NoAction 压键后同键重绑生效;录键 `Keystroke` → spec 字符串 → `Keystroke::parse` 往返一致;`key_tokens` 序列渲染。
|
||||
|
||||
## 2. PR 2 — 补齐 pane / tab 操作(tmux 用户依赖的缺口)
|
||||
|
||||
**目标**:方向聚焦 / resize / swap / 相邻及编号切 tab 全部成为正式 action(可绑键、进命令面板、被 preset 引用)。
|
||||
|
||||
### 新 action 与实现要点
|
||||
|
||||
| Action(`core/actions.rs`) | 实现(落点) | 默认键 |
|
||||
|---|---|---|
|
||||
| `FocusPaneLeft/Right/Up/Down` | `pane.rs` 纯树算法:根 = 单位矩形,按 split 轴 + ratio 递归算每个 leaf 的归一化 rect;从焦点 leaf 沿方向找「边缘最近、垂直轴重叠最大」的 leaf(tmux 语义) | `secondary-alt-left/right/up/down` |
|
||||
| `ResizePaneLeft/Right/Up/Down` | 找包含焦点 leaf 且轴向匹配的**最近祖先 split**,按焦点在 `a`/`b` 侧调 ratio ±0.05(沿用 `MIN/MAX_RATIO` 夹紧,`pane.rs:17`);结构变更后 `save_session` | 不绑(palette + 自行绑) |
|
||||
| `SwapPaneNext/Prev` | 交换两个 leaf 的 payload(`Entity<TerminalView>`),焦点跟随;`maximized` 置 None | 不绑 |
|
||||
| `NextTab` / `PrevTab` | `app.rs` `activate(active±1 mod len)`;目前相邻切 tab 完全没有 action | `ctrl-tab` / `ctrl-shift-tab`(需验证与系统/终端冲突,冲突则不绑) |
|
||||
| `ActivateTab1..9`(9 个 unit action) | 迁移 `app.rs:1213-1230` 硬编码逻辑 → keymap 正式绑定,行为不变;删掉裸 `on_key_down` 分支 | `secondary-1..9` |
|
||||
|
||||
- unit action 而非带参 action:`make_binding`(`keymap.rs:181`)是「字符串 action 名 → 绑定」的模型,config/Settings 面板全按名字索引,9 个 unit struct 最贴合。
|
||||
- **palette**:`ui/palette.rs` 的 `CommandKind` 补全部新 action,未绑键的操作至少可从面板执行。
|
||||
- **Settings 列表**:`default_bindings()` 表加新行;未绑默认键的 action 显示 "—",靠 PR 1 的编辑器绑定。
|
||||
- **文档**:README Keybindings 表 + zh-CN 同步更新。
|
||||
|
||||
### 验收 & 测试
|
||||
|
||||
- [ ] `Pane<u32>` 纯值单测:rect 计算(嵌套 split + 非 0.5 ratio)、方向查找(含并列取重叠最大)、resize 祖先选择(焦点在 `a`/`b` 两侧、无匹配轴时 no-op)、swap 后 `leaves()` 顺序。
|
||||
- [ ] ⌘1-9 迁移后行为与现状逐键一致,且可在 Settings 重绑。
|
||||
- [ ] 结构操作(resize/swap)后 session 持久化正确,重启还原布局。
|
||||
|
||||
## 3. PR 3 — 一键 tmux preset
|
||||
|
||||
**目标**:Settings 一键切换 tmux 风格前缀键位;纯数据层(映射表)+ 一层合并,零新输入模型。
|
||||
|
||||
### 落点
|
||||
|
||||
- **`core/config.rs`**:新增 `keybinding_preset: "default" | "tmux"`(serde 宽松解析,未知值回 default)+ `prefix: String`(默认 `"ctrl-b"`,校验 `Keystroke::parse`;常见备选 `ctrl-a`)。
|
||||
- **`ui/keymap.rs`**:生效表改为三层合并 **defaults → preset(前缀代入)→ user overrides**,全走 PR 1 的 `effective_bindings()`;切换 preset = 改 config + `save()` + `rebind()`。
|
||||
- **preset 映射表**(tmux 肌肉记忆 → action;`P` = prefix):
|
||||
|
||||
| tmux | tty7 action | 备注 |
|
||||
|---|---|---|
|
||||
| `P c` | NewTab | |
|
||||
| `P x` | CloseActiveTab | 语义即「关焦点 pane,最后一个则关 tab」(`app.rs:1913` → `close_pane`),与 kill-pane 吻合;`&` 不做 |
|
||||
| `P %` / `P "` | SplitRight / SplitDown | |
|
||||
| `P ←→↑↓` | FocusPane 方向聚焦 | PR 2 |
|
||||
| `P ctrl-←→↑↓` | ResizePane 方向 | PR 2;不支持按住连发,每次完整序列(先接受) |
|
||||
| `P {` / `P }` | SwapPanePrev / Next | PR 2 |
|
||||
| `P z` | ToggleMaximizePane | |
|
||||
| `P o` / `P ;` | FocusNextPane / FocusPrevPane | `;` 是 last-pane 的近似 |
|
||||
| `P n` / `P p` | NextTab / PrevTab | PR 2 |
|
||||
| `P 1..9` | ActivateTab1..9 | PR 2 |
|
||||
| `P [`、send-keys、choose-tree 等 | **不做** | 无对应语义,维持 issue 回复立场 |
|
||||
|
||||
- **`ui/settings.rs`**:Keybindings 页顶部加 preset 选择(Default / tmux)+ prefix 选择;列表实时显示序列 keycap(PR 1 已支持渲染)。
|
||||
- **文案(必须)**:开启 preset 后,裸 prefix 键到 shell 有 1s 延迟(gpui pending 超时回放);`prefix+未绑定键` 会原样回放进终端。
|
||||
- **issue 收尾**:三个 PR 合入后在 #61 回复(英文)并关闭。
|
||||
|
||||
### 验收 & 测试
|
||||
|
||||
- [ ] 三层合并优先级单测:preset 覆盖 default、user override 覆盖 preset;prefix 代入后所有序列可 parse。
|
||||
- [ ] 实测:开 preset 后 `ctrl-b %` 分屏、裸 `ctrl-b` 1s 后到达 readline(光标左移);关 preset 立即还原。
|
||||
- [ ] 终端内 `ctrl-b` 序列 pending 期间,焦点在 settings/输入框时无异常吞键。
|
||||
|
||||
## 4. 风险与权衡
|
||||
|
||||
- **热重绑(PR 1)是全案风险最高点**:NoAction 层叠语义、intercept 吞键边界(录键中途失焦/关窗)需 dogfood;建议 PR 1 先行合入留观察期。
|
||||
- **裸 prefix 1s 延迟**:tmux 用户可接受(tmux 本身就吞 prefix),但必须在 UI 与 README 写明;若反馈强烈,后续可探索缩短 gpui pending 超时(上游改动,本期不做)。
|
||||
- **`ctrl-tab` 默认键**:部分平台/输入法占用,合入前逐平台验证,冲突平台不给默认键。
|
||||
- **keymap 追加式增长**:仅随编辑次数增长、进程内有界;若未来做「频繁热切 preset」再考虑上游暴露分层 keymap。
|
||||
|
||||
## 5. 顺序与工作量
|
||||
|
||||
| PR | 依赖 | 估算 |
|
||||
|---|---|---|
|
||||
| PR 1 编辑器 + 热重绑 | — | ~400-600 行(含测试),2-3 天 |
|
||||
| PR 2 pane/tab action | 与 PR 1 无硬依赖,可并行 | ~500-700 行(树算法 + 单测占大头),2-3 天 |
|
||||
| PR 3 tmux preset | 依赖 PR 1(合并层/渲染)+ PR 2(action) | ~200-300 行,1 天 |
|
||||
@@ -0,0 +1,103 @@
|
||||
# Plan — 垂直标签侧边栏(vertical tab sidebar)落地方案
|
||||
|
||||
> 目标:在保留现有顶部水平标签条的前提下,新增一个**左侧垂直标签侧边栏**,作为标签列表的另一种呈现。
|
||||
> 设计立场(借鉴 Warp `warp-src` 的做法):**模型 / 操作 / 标签文本 / 颜色 token 全部复用,只有「壳」(方向、行形状、布局位置)是新写的**。tty7 的一致性成本天然很低——`Tab` 模型、`tab_label()`、`activate/close_tab/move_tab`、`DragTab`、以及全部颜色都已走 `cx.theme()` 单一来源,不需要「抽取共享」,只需换一个方向复用。
|
||||
> 本文分三个可独立发版的阶段(P0 骨架 / P1 打磨 / P2 增强),含文件级落点、验收标准与已核实的框架事实。
|
||||
|
||||
## 0. 已核实的技术事实(方案成立的前提)
|
||||
|
||||
| 事实 | 出处 | 对方案的意义 |
|
||||
|---|---|---|
|
||||
| `Tab { pane, name: Option<String>, settings }`——**无 id/color/pinned/group**,标签身份靠 `Vec<Tab>` 里的位置索引;active 是独立 `usize` | `src/ui/app.rs:66`、`:114-115` | 侧边栏直接读同一个 `self.tabs`/`self.active`,**零新状态**,任一处改动天然同步 |
|
||||
| 标签文本逻辑 `tab_label(tab, i, cx)`(name → `short_title(leaf_title)` → `"Session N"`)与布局方向无关 | `src/ui/tab_strip.rs:151`、`short_title`:60 | 侧边栏**原样调用**,文本与顶部条一致 |
|
||||
| 全部标签操作已是 `pub(crate)` 方法:`activate`(`app.rs:1125`)、`close_tab`(`:1161`)、`move_tab`(`:1206`)、`start_rename`(`:1237`)、`new_tab`(`:886`)、`new_tab_with_shell`(`:892`) | `src/ui/app.rs` | 侧边栏只是新增一批**点击目标**,不新增业务逻辑 |
|
||||
| 顶部条颜色**全部**读命名 token,无一处 hex:active=`secondary`+`foreground`、inactive=`muted_foreground`+hover `muted`、拖放高亮=`drag_border.opacity(0.2)` | `src/ui/tab_strip.rs:316-324` | 侧边栏复用同批 token,视觉一致是**构造上保证** |
|
||||
| 侧边栏专属配色 token **已存在且已接线**:`sidebar/sidebar_sel/sidebar_fg`(`presets.rs:133-135`)已喂给 gpui-component 的 `tokens.sidebar` 等(`theme.rs:156-164`) | `src/ui/presets.rs:133`、`src/ui/theme.rs:156` | 若想让侧边栏比正文沉一档,现成可用;⚠️ gpui-component `Sidebar` 读 `tokens.sidebar` 而非普通字段,手写 `v_flex` 则无此坑 |
|
||||
| 拖拽重排今天就有:`DragTab { index, label }`(mini `Render` 预览)+ `drag_over` + `on_drop → move_tab` | `src/ui/tab_strip.rs:126-145`、`:323-327` | `DragTab` 提为 `pub(crate)` 即可在侧边栏复用,甚至条↔栏互拖 |
|
||||
| 布局根是 `flex_col`:`TitleBar(h40).child(strip)` + `div.flex_1(body)`;body 是单一全宽子节点 | `src/ui/app.rs:2088`、TitleBar:2226、body:2238 | 侧边栏 = 把 `body` 那行包成 `h_flex`,首子放 sidebar、`body` 作 `flex_1` 第二子 |
|
||||
| 顶部条**刻意不放 per-chip 图标**,label 承载整个 chip("No leading context glyph") | `src/ui/tab_strip.rs:339-341` | 侧边栏延续同样克制,行也以 label 为主,视觉才对得上 |
|
||||
| config 用 `#[serde(default, deserialize_with = "de_lenient")]` 收窄枚举(如 `NewTabPosition`),未知值回默认 | `src/core/config.rs:79-84`、`:18` | 新增 `tab_bar_position` 枚举照抄此模式,前向兼容 |
|
||||
| 持久化模型 `Session/SessionTab`(`session.rs:55-68`)同样**无 id/color/pin/group** | `src/core/session.rs:55` | P0/P1 不碰;将来若给标签加颜色/分组,须**同时**扩展 `Tab` 与 `SessionTab` |
|
||||
| tmux 键位工作正在 `feat/keybindings-input-model` 分支进行(见 `plan-issue61-keybindings.md`) | 本仓库 | 新增 `ToggleTabSidebar` action 正好并入当前键位体系 |
|
||||
|
||||
## 决策点(需拍板,已在本方案取推荐默认)
|
||||
|
||||
- **❓ 模式切换 vs 并存**:本方案按**模式切换**设计(配置 `tab_bar_position: top | left`,对应 Warp 的 `uses_vertical_tabs`)——`left` 时顶部条收起 chips 只留 `+`/`⋯`,侧边栏显示列表。对终端更干净、不冗余。若要「顶部窄条 + 左侧富列表」并存,P0 的布局分叉与顶部条收起逻辑需改。**推荐:模式切换。**
|
||||
- **取色策略**:P0 用**顶部条同批 token**(`secondary`/`muted`/`muted_foreground`),像素级同色最一致;是否改用专属 `sidebar*` token(IDE 活动栏观感)留到 P1 再定。**推荐:先同批 token。**
|
||||
|
||||
## 1. P0 — 骨架(功能完整、视觉即一致,可发版)
|
||||
|
||||
**目标**:新增 `tab_bar_position` 配置 + 左侧 `v_flex` 侧边栏(纯 label 行)+ 布局分叉 + 切换 action。此阶段结束,垂直模式已可用且与顶部条同源。
|
||||
|
||||
### 落点
|
||||
|
||||
- **`src/core/config.rs` — 新增 `tab_bar_position`**
|
||||
- `pub enum TabBarPosition { Top, Left }`(`#[serde(rename_all="kebab-case")]` + `de_lenient`,默认 `Top`),照抄 `NewTabPosition` 模式(`config.rs:79`)。字段挂到 `Config`(`config.rs:18`)。
|
||||
- **`src/core/actions.rs` — 新增 `ToggleTabSidebar`**
|
||||
- 加进 `actions!` 列表(`actions.rs:8`);语义 = 在 `Top`/`Left` 间切换并 `Config::save()`。
|
||||
- **`src/ui/tab_sidebar.rs` — 新文件,`impl Tty7App` 块**(与 `tab_strip.rs` 完全同构的拆分)
|
||||
- `fn tab_sidebar(&self, window, cx) -> impl IntoElement`:`v_flex()` 固定宽(`w(px(220.))`)、`overflow_y` 竖直滚动;遍历 `self.tabs` 渲染整行。
|
||||
- 每行 `h_flex().id(("tab-row", i)).w_full().h(px(34.)).pl_3().pr_2().rounded_lg()`:
|
||||
- `child(tab_label(...))` 左对齐 `.truncate()`,active 加 `FontWeight::MEDIUM`;
|
||||
- active → `.bg(secondary).text_color(foreground)`;inactive → `.text_color(muted_foreground).hover(bg(muted))`(**照抄 `tab_strip.rs:316-322`**);
|
||||
- 单击 `activate(i)` / 双击 `start_rename(i)`(复用 `self.renaming` 的 `Input` 分支,照抄 `tab_strip.rs:227-264`);
|
||||
- 尾部 close `Button`(hover 显隐,active 常显,照抄 `tab_strip.rs:349-389`);
|
||||
- `on_drag(DragTab{...})` + `drag_over` + `on_drop(move_tab)`(照抄 `tab_strip.rs:323-327`)。
|
||||
- 底部或顶部放一枚 `+` 按钮,复用 `tab_strip.rs:402-475` 的 shell dropdown(可先只 `new_tab`,P1 再补 shell 菜单)。
|
||||
- 空态:`self.tabs` 空时不进垂直模式(与首页逻辑一致),无需空态文案。
|
||||
- **`src/ui/mod.rs`** — 挂 `mod tab_sidebar;`。
|
||||
- **`src/ui/app.rs::render` — 布局分叉**(`app.rs:2088`、`:2238`)
|
||||
- 读 `cx.global::<Config>().tab_bar_position`。
|
||||
- `Top`(现状):不变。
|
||||
- `Left`:① `strip` 收起 chips(顶部条只渲染 `+`/`⋯`,或整体隐藏——见决策点);② 把 `.child(div().flex_1()...child(body))` 换成 `.child(h_flex().flex_1().child(self.tab_sidebar(..)).child(div().flex_1()...child(body)))`。
|
||||
- 根元素加 `.on_action(cx.listener(|this,_:&ToggleTabSidebar,w,cx| this.toggle_tab_sidebar(w,cx)))`(照抄 `app.rs:2128` 一众 `on_action`)。
|
||||
- **`src/ui/keymap.rs`** — 给 `ToggleTabSidebar` 一个默认键(建议无默认或 `secondary-shift-e`,避免冲突;进 Settings/palette 可绑),并加进 palette 的命令目录(`ui/palette.rs`)。
|
||||
|
||||
### 验收 & 测试
|
||||
|
||||
- [ ] 配置 `tab_bar_position: left` 后启动:左侧出现标签列表,顶部条 chips 按决策收起;`top` 恢复现状。
|
||||
- [ ] 侧边栏行的 active/hover/拖放高亮与顶部条**同色**(同 token,肉眼比对无差)。
|
||||
- [ ] 侧边栏内:单击切换、双击改名、close 关闭、拖拽重排,行为与顶部条逐项一致(共用同一批方法)。
|
||||
- [ ] `ToggleTabSidebar`(键位 + palette)即时切换并写盘;重启保持。
|
||||
- [ ] 顶部/侧边两模式下 `⌘1-9`、`⌘T`、`⌘W`、`ctrl-tab` 等既有 action 行为不变(action 派发不受布局影响)。
|
||||
|
||||
## 2. P1 — 打磨(交互质感)
|
||||
|
||||
**目标**:侧边栏达到「可长期使用」的完成度——可改宽、宽度持久化、副标题、滚动细节。
|
||||
|
||||
### 落点
|
||||
|
||||
- **可拖拽改宽**:侧边栏右缘加拖拽条,`w` 夹在 `[180, 视窗宽*0.5]`(参考 Warp `MIN_PANEL_WIDTH`/`MAX_PANEL_WIDTH_RATIO`)。优先用 gpui-component 现成 resizable;否则手写 drag 更新一个 `sidebar_width: f32` 状态。
|
||||
- **宽度持久化**:`sidebar_width` 存入 `Config`(或 session),`Config::save()`;重启还原。
|
||||
- **行内副标题(可选)**:竖排空间富余,行改两行——主行 `tab_label`,副行 dimmed `muted_foreground` 的 cwd(取 `leaf_title`/cwd)。默认可先关,配置项 `sidebar_show_subtitle: bool`。若要更克制则跳过。
|
||||
- **取色策略定稿**:决定沿用 `secondary`/`muted`,还是切 `sidebar*` token(`theme.rs:156-164` 已接线;注意 `tokens.sidebar` 坑)。
|
||||
- **滚动 & 溢出**:标签很多时竖直滚动顺滑;active 行自动滚入可视区。
|
||||
|
||||
### 验收 & 测试
|
||||
|
||||
- [ ] 拖拽改宽夹紧上下限;宽度重启保持。
|
||||
- [ ] 副标题(若启用)不撑高行、超长 `.truncate()`。
|
||||
- [ ] 数十个标签时滚动顺滑,切到不可见标签自动滚入视区。
|
||||
|
||||
## 3. P2 — 增强(可选,视需求做)
|
||||
|
||||
- **`DragTab` 提共享**:移到公共位置并 `pub(crate)`,支持顶部条 ↔ 侧边栏互拖(同一 payload,`move_tab` 复用)。
|
||||
- **行内副标题富化**:pane 数、运行中命令等(仍保持克制,别做成 icon-per-row)。
|
||||
- **右键菜单**:rename / close / duplicate / (未来)move,复用现有方法。
|
||||
- **小重构(非必需)**:把顶部 chip body 的点击/改名/拖拽绑定抽成 `tab_row(tab,i,is_active)` inline helper,`h_flex` 与 `v_flex` 共调——但因这些本就是 `Tty7App` 方法,各自调也无重复逻辑,收益有限。
|
||||
- **标签着色/分组(大改)**:需**同时**扩展 `Tab` 与 `SessionTab`(`session.rs:55`)加字段并处理迁移;超出本方案范围,单独立项。
|
||||
|
||||
## 4. 风险与权衡
|
||||
|
||||
- **布局分叉是主要改动面**:`app.rs::render`(`:2238`)从「TitleBar + 单 body」变「TitleBar(收起) + [sidebar | body] 行」,需确保 `maximized`、focus ring、命令面板 overlay 在两模式下都正确;建议 P0 先 dogfood 一版再上打磨。
|
||||
- **顶部条收起的取舍**:`left` 模式下顶部条留 `+`/`⋯` 还是整条隐藏,影响标题栏空白与窗口拖拽区(gpui-component `TitleBar` 的 `WindowControlArea::Drag`)。倾向保留 `+`/`⋯` 一行,避免标题栏空荡。
|
||||
- **无 per-tab 图标是刻意的**:侧边栏若擅自加图标会与顶部条风格背离;保持 label 优先。
|
||||
- **持久化字段扩展**:P2 的着色/分组会动到 `Session` 结构,牵涉存量 `config`/session 迁移,单独发版。
|
||||
|
||||
## 5. 顺序与工作量
|
||||
|
||||
| 阶段 | 依赖 | 估算 |
|
||||
|---|---|---|
|
||||
| P0 骨架(配置 + `tab_sidebar.rs` + 布局分叉 + action) | — | ~300-400 行(含测试),1-2 天 |
|
||||
| P1 打磨(改宽 + 持久化 + 副标题 + 取色定稿) | P0 | ~200-300 行,1-2 天 |
|
||||
| P2 增强(互拖 / 右键菜单 / 着色分组) | P0(着色分组另依赖 Session 扩展) | 按选做项计,0.5-N 天 |
|
||||
+56
-30
@@ -22,7 +22,9 @@ use crate::terminal::view::{ChildExited, TerminalView};
|
||||
use crate::ui::palette::{Command, CommandKind, PaletteEvent, PaletteView};
|
||||
use crate::ui::pane::{CloseOutcome, Dir, Pane};
|
||||
use crate::ui::presets::Fill;
|
||||
use crate::ui::settings::{Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action};
|
||||
use crate::ui::settings::{
|
||||
Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action,
|
||||
};
|
||||
use crate::ui::theme::{apply_theme, set_menus};
|
||||
|
||||
/// One editable color of a user theme, targeted by the in-app color editor. Maps
|
||||
@@ -2217,7 +2219,10 @@ impl Tty7App {
|
||||
"backspace" | "delete" => {
|
||||
if has_chords {
|
||||
// Edit the sequence: drop the last chord and keep capturing.
|
||||
if let Some(r) = self.active_settings_mut().and_then(|s| s.recording.as_mut()) {
|
||||
if let Some(r) = self
|
||||
.active_settings_mut()
|
||||
.and_then(|s| s.recording.as_mut())
|
||||
{
|
||||
r.chords.pop();
|
||||
}
|
||||
let still_has = self
|
||||
@@ -2244,7 +2249,10 @@ impl Tty7App {
|
||||
let Some(spec) = crate::ui::keymap::spec_from_keystroke(keystroke) else {
|
||||
return;
|
||||
};
|
||||
if let Some(r) = self.active_settings_mut().and_then(|s| s.recording.as_mut()) {
|
||||
if let Some(r) = self
|
||||
.active_settings_mut()
|
||||
.and_then(|s| s.recording.as_mut())
|
||||
{
|
||||
r.chords.push(spec);
|
||||
}
|
||||
self.schedule_recording_commit(cx);
|
||||
@@ -2492,27 +2500,45 @@ impl Render for Tty7App {
|
||||
.on_action(cx.listener(|this, _: &ResizePaneDown, window, cx| {
|
||||
this.resize_pane(Dir::Down, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &SwapPaneNext, window, cx| {
|
||||
this.swap_pane(true, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &SwapPanePrev, window, cx| {
|
||||
this.swap_pane(false, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &NextTab, window, cx| {
|
||||
this.cycle_tab(true, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &PrevTab, window, cx| {
|
||||
this.cycle_tab(false, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab1, window, cx| this.activate(0, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab2, window, cx| this.activate(1, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab3, window, cx| this.activate(2, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab4, window, cx| this.activate(3, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab5, window, cx| this.activate(4, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab6, window, cx| this.activate(5, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab7, window, cx| this.activate(6, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab8, window, cx| this.activate(7, window, cx)))
|
||||
.on_action(cx.listener(|this, _: &ActivateTab9, window, cx| this.activate(8, window, cx)))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &SwapPaneNext, window, cx| this.swap_pane(true, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &SwapPanePrev, window, cx| this.swap_pane(false, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &NextTab, window, cx| this.cycle_tab(true, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &PrevTab, window, cx| this.cycle_tab(false, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab1, window, cx| this.activate(0, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab2, window, cx| this.activate(1, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab3, window, cx| this.activate(2, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab4, window, cx| this.activate(3, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab5, window, cx| this.activate(4, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab6, window, cx| this.activate(5, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab7, window, cx| this.activate(6, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab8, window, cx| this.activate(7, window, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &ActivateTab9, window, cx| this.activate(8, window, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|this, _: &IncreaseFontSize, _window, cx| {
|
||||
this.change_font_size(FONT_SIZE_STEP, cx)
|
||||
}))
|
||||
@@ -2781,11 +2807,7 @@ mod keybinding_gpui_tests {
|
||||
}
|
||||
|
||||
/// Open Settings → Keybindings and begin capturing `action`.
|
||||
fn begin_capture(
|
||||
window: &WindowHandle<Tty7App>,
|
||||
vcx: &mut VisualTestContext,
|
||||
action: &str,
|
||||
) {
|
||||
fn begin_capture(window: &WindowHandle<Tty7App>, vcx: &mut VisualTestContext, action: &str) {
|
||||
let action = action.to_string();
|
||||
window
|
||||
.update(vcx, |app, window, cx| {
|
||||
@@ -2831,7 +2853,11 @@ mod keybinding_gpui_tests {
|
||||
app.active_settings().map(|s| s.recording.is_some())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(recording, Some(false), "capture should end after committing");
|
||||
assert_eq!(
|
||||
recording,
|
||||
Some(false),
|
||||
"capture should end after committing"
|
||||
);
|
||||
}
|
||||
|
||||
// A two-chord sequence (the tmux-style `ctrl-b x`) records as one binding.
|
||||
|
||||
+12
-2
@@ -453,6 +453,11 @@ mod tests {
|
||||
const SHIFT: &str = "⇧";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const SHIFT: &str = "Shift";
|
||||
// Literal `ctrl` renders as ⌃ on macOS, "Ctrl" elsewhere.
|
||||
#[cfg(target_os = "macos")]
|
||||
const CTRL: &str = "⌃";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const CTRL: &str = "Ctrl";
|
||||
|
||||
#[test]
|
||||
fn key_tokens_maps_modifiers_to_glyphs() {
|
||||
@@ -475,7 +480,10 @@ mod tests {
|
||||
// A tmux-style sequence renders as two distinct clusters.
|
||||
assert_eq!(
|
||||
key_chords("ctrl-b n"),
|
||||
vec![vec!["⌃".to_string(), "B".to_string()], vec!["N".to_string()]]
|
||||
vec![
|
||||
vec![CTRL.to_string(), "B".to_string()],
|
||||
vec!["N".to_string()]
|
||||
]
|
||||
);
|
||||
// A single chord is one group.
|
||||
assert_eq!(key_chords("secondary-t"), vec![vec![SECONDARY, "T"]]);
|
||||
@@ -594,7 +602,9 @@ mod gpui_tests {
|
||||
rebind(cx);
|
||||
let eff = effective_bindings(cx);
|
||||
assert_eq!(
|
||||
eff.iter().find(|(a, _)| a == "SplitRight").map(|(_, k)| k.as_str()),
|
||||
eff.iter()
|
||||
.find(|(a, _)| a == "SplitRight")
|
||||
.map(|(_, k)| k.as_str()),
|
||||
Some("secondary-d")
|
||||
);
|
||||
});
|
||||
|
||||
+46
-8
@@ -266,7 +266,9 @@ impl<L: Clone> Pane<L> {
|
||||
fn collect_rects(&self, area: Rect, out: &mut Vec<(L, Rect)>) {
|
||||
match self {
|
||||
Pane::Leaf(v) => out.push((v.clone(), area)),
|
||||
Pane::Split { axis, a, b, ratio, .. } => {
|
||||
Pane::Split {
|
||||
axis, a, b, ratio, ..
|
||||
} => {
|
||||
let r = ratio.get().clamp(MIN_RATIO, MAX_RATIO);
|
||||
match axis {
|
||||
Axis::Horizontal => {
|
||||
@@ -1021,7 +1023,10 @@ mod tests {
|
||||
fn assert_rect(got: Rect, want: Rect) {
|
||||
let close = |a: f32, b: f32| (a - b).abs() < 1e-5;
|
||||
assert!(
|
||||
close(got.x, want.x) && close(got.y, want.y) && close(got.w, want.w) && close(got.h, want.h),
|
||||
close(got.x, want.x)
|
||||
&& close(got.y, want.y)
|
||||
&& close(got.w, want.w)
|
||||
&& close(got.h, want.h),
|
||||
"rect {got:?} != {want:?}"
|
||||
);
|
||||
}
|
||||
@@ -1038,12 +1043,39 @@ mod tests {
|
||||
Pane::Leaf(0),
|
||||
TestPane::split_node(Axis::Vertical, 0.6, Pane::Leaf(1), Pane::Leaf(2)),
|
||||
);
|
||||
assert_rect(rect_of(&pane, 0), Rect { x: 0.0, y: 0.0, w: 0.25, h: 1.0 });
|
||||
assert_rect(rect_of(&pane, 1), Rect { x: 0.25, y: 0.0, w: 0.75, h: 0.6 });
|
||||
assert_rect(rect_of(&pane, 2), Rect { x: 0.25, y: 0.6, w: 0.75, h: 0.4 });
|
||||
assert_rect(
|
||||
rect_of(&pane, 0),
|
||||
Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.25,
|
||||
h: 1.0,
|
||||
},
|
||||
);
|
||||
assert_rect(
|
||||
rect_of(&pane, 1),
|
||||
Rect {
|
||||
x: 0.25,
|
||||
y: 0.0,
|
||||
w: 0.75,
|
||||
h: 0.6,
|
||||
},
|
||||
);
|
||||
assert_rect(
|
||||
rect_of(&pane, 2),
|
||||
Rect {
|
||||
x: 0.25,
|
||||
y: 0.6,
|
||||
w: 0.75,
|
||||
h: 0.4,
|
||||
},
|
||||
);
|
||||
// Rects come back in leaves() order.
|
||||
assert_eq!(
|
||||
pane.leaf_rects().iter().map(|(v, _)| *v).collect::<Vec<_>>(),
|
||||
pane.leaf_rects()
|
||||
.iter()
|
||||
.map(|(v, _)| *v)
|
||||
.collect::<Vec<_>>(),
|
||||
pane.leaves()
|
||||
);
|
||||
}
|
||||
@@ -1129,10 +1161,16 @@ mod tests {
|
||||
// Inner split moved; outer untouched.
|
||||
match &pane {
|
||||
Pane::Split { ratio, b, .. } => {
|
||||
assert!((ratio.get() - 0.5).abs() < 1e-6, "outer split must not move");
|
||||
assert!(
|
||||
(ratio.get() - 0.5).abs() < 1e-6,
|
||||
"outer split must not move"
|
||||
);
|
||||
match &**b {
|
||||
Pane::Split { ratio, .. } => {
|
||||
assert!((ratio.get() - 0.55).abs() < 1e-6, "inner split should grow 1");
|
||||
assert!(
|
||||
(ratio.get() - 0.55).abs() < 1e-6,
|
||||
"inner split should grow 1"
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
+22
-40
@@ -1428,11 +1428,7 @@ impl Tty7App {
|
||||
// and the wider picker instead of clustering at the left edge. Rows stay
|
||||
// ragged-right like real terminal text.
|
||||
let bar = |frac: f32, color: gpui::Rgba| {
|
||||
div()
|
||||
.h(px(4.))
|
||||
.w(relative(frac))
|
||||
.rounded(px(1.5))
|
||||
.bg(color)
|
||||
div().h(px(4.)).w(relative(frac)).rounded(px(1.5)).bg(color)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
@@ -1736,25 +1732,19 @@ impl Tty7App {
|
||||
};
|
||||
|
||||
// A preset toggle button, highlighted when active.
|
||||
let preset_button = |id: &'static str, label: &'static str, value: &'static str, on: bool| {
|
||||
Button::new(id)
|
||||
.label(label)
|
||||
.small()
|
||||
.selected(on)
|
||||
.on_click(cx.listener(move |this, _, _w, cx| {
|
||||
this.set_keybinding_preset(value, cx)
|
||||
}))
|
||||
};
|
||||
let preset_button =
|
||||
|id: &'static str, label: &'static str, value: &'static str, on: bool| {
|
||||
Button::new(id).label(label).small().selected(on).on_click(
|
||||
cx.listener(move |this, _, _w, cx| this.set_keybinding_preset(value, cx)),
|
||||
)
|
||||
};
|
||||
// A prefix choice button (tmux preset only).
|
||||
let prefix_button = |id: &'static str, label: &'static str, value: &'static str, on: bool| {
|
||||
Button::new(id)
|
||||
.label(label)
|
||||
.small()
|
||||
.selected(on)
|
||||
.on_click(cx.listener(move |this, _, _w, cx| {
|
||||
this.set_keybinding_prefix(value, cx)
|
||||
}))
|
||||
};
|
||||
let prefix_button =
|
||||
|id: &'static str, label: &'static str, value: &'static str, on: bool| {
|
||||
Button::new(id).label(label).small().selected(on).on_click(
|
||||
cx.listener(move |this, _, _w, cx| this.set_keybinding_prefix(value, cx)),
|
||||
)
|
||||
};
|
||||
|
||||
let preset_row = h_flex()
|
||||
.items_center()
|
||||
@@ -1812,9 +1802,7 @@ impl Tty7App {
|
||||
let count = effective.len();
|
||||
let mut list = v_flex().mt_2();
|
||||
for (i, (action, key)) in effective.into_iter().enumerate() {
|
||||
let is_recording = recording
|
||||
.as_ref()
|
||||
.is_some_and(|(a, _)| a == &action);
|
||||
let is_recording = recording.as_ref().is_some_and(|(a, _)| a == &action);
|
||||
let is_overridden = overridden.contains(&action);
|
||||
|
||||
// Keycap clusters for a spec: one cluster per whitespace-separated
|
||||
@@ -1837,12 +1825,7 @@ impl Tty7App {
|
||||
.unwrap_or_default();
|
||||
let row = h_flex().gap_2().items_center();
|
||||
let row = if chords.is_empty() {
|
||||
row.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(accent)
|
||||
.child("Press keys…"),
|
||||
)
|
||||
row.child(div().text_xs().text_color(accent).child("Press keys…"))
|
||||
} else {
|
||||
row.child(keycaps(&chords.join(" "))).child(
|
||||
div()
|
||||
@@ -1873,9 +1856,7 @@ impl Tty7App {
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.when(is_recording, |d| {
|
||||
d.border_1().border_color(accent)
|
||||
})
|
||||
.when(is_recording, |d| d.border_1().border_color(accent))
|
||||
.hover(|d| d.bg(kbd_bg))
|
||||
.child(captured)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -1883,9 +1864,11 @@ impl Tty7App {
|
||||
}));
|
||||
|
||||
let action_for_reset = action.clone();
|
||||
let right = h_flex().items_center().gap_1().child(capture).when(
|
||||
is_overridden,
|
||||
|r| {
|
||||
let right = h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(capture)
|
||||
.when(is_overridden, |r| {
|
||||
r.child(
|
||||
Button::new(SharedString::from(format!("reset-{action}")))
|
||||
.label("Reset")
|
||||
@@ -1894,8 +1877,7 @@ impl Tty7App {
|
||||
this.reset_keybinding(action_for_reset.clone(), cx)
|
||||
})),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
list = list.child(
|
||||
h_flex()
|
||||
|
||||
Reference in New Issue
Block a user