Skip to content

Commit b797424

Browse files
committed
feat(terminal_view): 新增vim/TUI鼠标滚轮映射为方向键功能
- 移除shell集成中对vim鼠标支持的代码和测试 - 新增设置项“vim_scroll_to_arrow_keys”,默认启用 - 在TUI中将鼠标滚轮事件转为方向键发送到PTY,兼顾滚动与选区复制 - 扩展终端视图相关配置、事件和面板,实现功能开关控制 - 更新多语言资源,去除vim鼠标相关描述并新增滚轮映射说明 - 修正默认设置,实现功能在main模块中的默认启用
1 parent e77428b commit b797424

9 files changed

Lines changed: 98 additions & 268 deletions

File tree

crates/core/src/storage/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ pub struct SshParams {
220220
/// 初始化脚本
221221
#[serde(skip_serializing_if = "Option::is_none")]
222222
pub init_script: Option<String>,
223-
/// 关闭 shell integration 注入(走裸 request_shell,牺牲 prompt hook / 命令记录 / vim 鼠标)
223+
/// 关闭 shell integration 注入(走裸 request_shell,牺牲 prompt hook / 命令记录)
224224
#[serde(skip_serializing_if = "Option::is_none")]
225225
pub disable_shell_integration: Option<bool>,
226226
/// 跳板机配置

crates/terminal/src/shell_integration.rs

Lines changed: 0 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ pub(crate) fn embedded_shell_integration_script() -> String {
99
#[cfg(test)]
1010
mod tests {
1111
use super::{embedded_shell_integration_script, normalized_shell_integration_script};
12-
#[cfg(unix)]
13-
use std::{fs, os::unix::fs::PermissionsExt, process::Command};
1412

1513
#[test]
1614
fn normalized_shell_integration_script_converts_crlf_to_lf() {
@@ -28,227 +26,4 @@ mod tests {
2826
"嵌入式 shell integration 脚本不应保留 CR,避免远端 shell 解析失败"
2927
);
3028
}
31-
32-
#[test]
33-
fn embedded_shell_integration_script_enables_vim_mouse() {
34-
let script = embedded_shell_integration_script();
35-
assert!(script.contains("--cmd 'set mouse=a'"));
36-
assert!(script.contains("--cmd 'nnoremap <ScrollWheelUp> gkzz'"));
37-
assert!(script.contains("--cmd 'nnoremap <ScrollWheelDown> gjzz'"));
38-
assert!(script.contains("function vim {"));
39-
assert!(script.contains("__onetcli_can_wrap_command vim"));
40-
}
41-
42-
#[cfg(unix)]
43-
#[test]
44-
fn bash_vim_wrapper_preserves_args() {
45-
assert_vim_wrapper_preserves_args("bash");
46-
}
47-
48-
#[cfg(unix)]
49-
#[test]
50-
fn zsh_vim_wrapper_preserves_args() {
51-
if !shell_available("zsh") {
52-
return;
53-
}
54-
assert_vim_wrapper_preserves_args("zsh");
55-
}
56-
57-
#[cfg(unix)]
58-
#[test]
59-
fn bash_vim_wrapper_does_not_override_alias() {
60-
assert_vim_wrapper_does_not_override_alias("bash");
61-
}
62-
63-
#[cfg(unix)]
64-
#[test]
65-
fn zsh_vim_wrapper_does_not_override_alias() {
66-
if !shell_available("zsh") {
67-
return;
68-
}
69-
assert_vim_wrapper_does_not_override_alias("zsh");
70-
}
71-
72-
#[cfg(unix)]
73-
#[test]
74-
fn bash_vim_wrapper_does_not_override_function() {
75-
assert_vim_wrapper_does_not_override_function("bash");
76-
}
77-
78-
#[cfg(unix)]
79-
#[test]
80-
fn zsh_vim_wrapper_does_not_override_function() {
81-
if !shell_available("zsh") {
82-
return;
83-
}
84-
assert_vim_wrapper_does_not_override_function("zsh");
85-
}
86-
87-
#[cfg(unix)]
88-
#[test]
89-
fn bash_vim_mouse_can_be_disabled() {
90-
assert_vim_mouse_can_be_disabled("bash");
91-
}
92-
93-
#[cfg(unix)]
94-
#[test]
95-
fn zsh_vim_mouse_can_be_disabled() {
96-
if !shell_available("zsh") {
97-
return;
98-
}
99-
assert_vim_mouse_can_be_disabled("zsh");
100-
}
101-
102-
#[cfg(unix)]
103-
#[test]
104-
fn bash_nvim_wrapper_preserves_args() {
105-
assert_nvim_wrapper_preserves_args("bash");
106-
}
107-
108-
#[cfg(unix)]
109-
#[test]
110-
fn zsh_nvim_wrapper_preserves_args() {
111-
if !shell_available("zsh") {
112-
return;
113-
}
114-
assert_nvim_wrapper_preserves_args("zsh");
115-
}
116-
117-
#[cfg(unix)]
118-
fn assert_vim_wrapper_preserves_args(shell: &str) {
119-
let output = run_interactive_shell(
120-
shell,
121-
"source \"$ONETCLI_TEST_SCRIPT\"\nvim 'a b.txt' -- '--weird;$HOME'",
122-
);
123-
124-
assert!(output.status.success());
125-
assert_eq!(
126-
strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)),
127-
"--cmd\nset mouse=a\n--cmd\nnnoremap <ScrollWheelUp> gkzz\n--cmd\nnnoremap <ScrollWheelDown> gjzz\n--cmd\ninoremap <ScrollWheelUp> <C-o>gk<C-o>zz\n--cmd\ninoremap <ScrollWheelDown> <C-o>gj<C-o>zz\na b.txt\n--\n--weird;$HOME\n"
128-
);
129-
}
130-
131-
#[cfg(unix)]
132-
fn assert_vim_wrapper_does_not_override_alias(shell: &str) {
133-
let output = run_interactive_shell(
134-
shell,
135-
"shopt -s expand_aliases 2>/dev/null || true\nalias vim='echo alias-safe'\nsource \"$ONETCLI_TEST_SCRIPT\"\nvim",
136-
);
137-
138-
assert!(output.status.success());
139-
assert_eq!(
140-
strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)),
141-
"alias-safe\n"
142-
);
143-
}
144-
145-
#[cfg(unix)]
146-
fn assert_vim_wrapper_does_not_override_function(shell: &str) {
147-
let output = run_interactive_shell(
148-
shell,
149-
"vim() { echo function-safe; }\nsource \"$ONETCLI_TEST_SCRIPT\"\nvim",
150-
);
151-
152-
assert!(output.status.success());
153-
assert_eq!(
154-
strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)),
155-
"function-safe\n"
156-
);
157-
}
158-
159-
#[cfg(unix)]
160-
fn assert_vim_mouse_can_be_disabled(shell: &str) {
161-
let output = run_interactive_shell(
162-
shell,
163-
"source \"$ONETCLI_TEST_SCRIPT\"\nONETCLI_VIM_MOUSE=0 vim file.txt",
164-
);
165-
166-
assert!(output.status.success());
167-
assert_eq!(
168-
strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)),
169-
"file.txt\n"
170-
);
171-
}
172-
173-
#[cfg(unix)]
174-
fn assert_nvim_wrapper_preserves_args(shell: &str) {
175-
let output = run_interactive_shell(shell, "source \"$ONETCLI_TEST_SCRIPT\"\nnvim file.txt");
176-
177-
assert!(output.status.success());
178-
assert_eq!(
179-
strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)),
180-
"--cmd\nset mouse=a\n--cmd\nnnoremap <ScrollWheelUp> gkzz\n--cmd\nnnoremap <ScrollWheelDown> gjzz\n--cmd\ninoremap <ScrollWheelUp> <C-o>gk<C-o>zz\n--cmd\ninoremap <ScrollWheelDown> <C-o>gj<C-o>zz\nfile.txt\n"
181-
);
182-
}
183-
184-
#[cfg(unix)]
185-
fn shell_available(shell: &str) -> bool {
186-
let available = Command::new(shell).arg("--version").output().is_ok();
187-
if !available {
188-
eprintln!("跳过 {shell} 行为测试:当前环境未安装该 shell");
189-
}
190-
available
191-
}
192-
193-
#[cfg(unix)]
194-
fn run_interactive_shell(shell: &str, command: &str) -> std::process::Output {
195-
let temp_dir = std::env::temp_dir().join(format!(
196-
"onetcli-shell-integration-test-{}-{:?}",
197-
std::process::id(),
198-
std::thread::current().id()
199-
));
200-
let bin_dir = temp_dir.join("bin");
201-
let home_dir = temp_dir.join("home");
202-
let zdot_dir = temp_dir.join("zsh");
203-
fs::create_dir_all(&bin_dir).expect("应创建测试 bin 目录");
204-
fs::create_dir_all(&home_dir).expect("应创建测试 HOME 目录");
205-
fs::create_dir_all(&zdot_dir).expect("应创建测试 ZDOTDIR 目录");
206-
207-
let script_path = temp_dir.join("shell_integration.sh");
208-
fs::write(&script_path, embedded_shell_integration_script()).expect("应写入集成脚本");
209-
210-
write_fake_editor(&bin_dir.join("vim"));
211-
write_fake_editor(&bin_dir.join("nvim"));
212-
213-
let command_path = temp_dir.join("command.sh");
214-
fs::write(&command_path, command).expect("应写入测试命令脚本");
215-
216-
let path = format!(
217-
"{}:{}",
218-
bin_dir.display(),
219-
std::env::var("PATH").unwrap_or_default()
220-
);
221-
let output = Command::new(shell)
222-
.arg("-i")
223-
.arg(&command_path)
224-
.env("PATH", path)
225-
.env("HOME", &home_dir)
226-
.env("ZDOTDIR", &zdot_dir)
227-
.env("ONETCLI_TEST_SCRIPT", &script_path)
228-
.output()
229-
.expect("应执行 shell 行为测试");
230-
231-
let _ = fs::remove_dir_all(&temp_dir);
232-
output
233-
}
234-
235-
#[cfg(unix)]
236-
fn write_fake_editor(path: &std::path::Path) {
237-
fs::write(
238-
path,
239-
"#!/bin/sh\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done\n",
240-
)
241-
.expect("应写入 fake editor");
242-
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
243-
.expect("应设置 fake editor 可执行权限");
244-
}
245-
246-
#[cfg(unix)]
247-
fn strip_shell_integration_osc(output: &str) -> String {
248-
output
249-
.replace("\u{1b}]133;C\u{7}", "")
250-
.replace("\u{1b}]133;D;0\u{7}", "")
251-
.replace("\u{1b}]133;A\u{7}", "")
252-
.replace("\u{1b}]133;B\u{7}", "")
253-
}
25429
}

crates/terminal/src/shell_integration.sh

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,45 +40,6 @@ __onetcli_last_history_command() {
4040
fi
4141
}
4242

43-
__onetcli_enable_vim_mouse() {
44-
local editor="$1"
45-
shift
46-
if [[ "${ONETCLI_VIM_MOUSE:-1}" == "0" ]]; then
47-
command "$editor" "$@"
48-
return
49-
fi
50-
command "$editor" \
51-
--cmd 'set mouse=a' \
52-
--cmd 'nnoremap <ScrollWheelUp> gkzz' \
53-
--cmd 'nnoremap <ScrollWheelDown> gjzz' \
54-
--cmd 'inoremap <ScrollWheelUp> <C-o>gk<C-o>zz' \
55-
--cmd 'inoremap <ScrollWheelDown> <C-o>gj<C-o>zz' \
56-
"$@"
57-
}
58-
59-
__onetcli_can_wrap_command() {
60-
local name="$1"
61-
if [[ -n "${ZSH_VERSION:-}" ]]; then
62-
local command_type
63-
command_type="$(whence -w "$name" 2>/dev/null)"
64-
[[ "$command_type" == "$name: command" || "$command_type" == "$name: hashed" ]]
65-
else
66-
[[ "$(type -t "$name" 2>/dev/null)" == "file" ]]
67-
fi
68-
}
69-
70-
if __onetcli_can_wrap_command vim; then
71-
function vim {
72-
__onetcli_enable_vim_mouse vim "$@"
73-
}
74-
fi
75-
76-
if __onetcli_can_wrap_command nvim; then
77-
function nvim {
78-
__onetcli_enable_vim_mouse nvim "$@"
79-
}
80-
fi
81-
8243
__onetcli_emit_recorded_command() {
8344
local command_text encoded
8445
command_text="$(__onetcli_last_history_command)"

crates/terminal_view/locales/terminal_view.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,9 @@ SSH:
321321
zh-CN: 禁用 Shell 集成
322322
zh-HK: 禁用 Shell 集成
323323
disable_shell_integration_desc:
324-
en: Run native login shell without OSC injection (no prompt hook, command recording, or vim mouse)
325-
zh-CN: 走裸 login shell,不注入 OSC(失去命令记录 / prompt hook / vim 鼠标
326-
zh-HK: 走裸 login shell,不注入 OSC(失去命令記錄 / prompt hook / vim 鼠標
324+
en: Run native login shell without OSC injection (no prompt hook or command recording)
325+
zh-CN: 走裸 login shell,不注入 OSC(失去命令记录 / prompt hook)
326+
zh-HK: 走裸 login shell,不注入 OSC(失去命令記錄 / prompt hook)
327327
# 其他设置
328328
remark:
329329
en: Remark
@@ -411,6 +411,10 @@ Settings:
411411
en: Middle click paste
412412
zh-CN: 中键粘贴
413413
zh-HK: 中鍵貼上
414+
vim_scroll_to_arrow_keys:
415+
en: Mouse wheel as arrow keys in vim/TUI
416+
zh-CN: vim/TUI 中鼠标滚轮映射为方向键
417+
zh-HK: vim/TUI 中滑鼠滾輪映射為方向鍵
414418
file_manager_section:
415419
en: FILE MANAGER
416420
zh-CN: 文件管理器

crates/terminal_view/src/settings.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ pub struct TerminalSettings {
4141
pub cursor_blink: bool,
4242
pub confirm_multiline_paste: bool,
4343
pub confirm_high_risk_command: bool,
44+
/// 在 alt-screen TUI(vim/less/man 等)中把鼠标滚轮事件转为方向键发送给 PTY,
45+
/// 让 vim 等程序不开启鼠标报告也能滚动,同时保留终端原生选区/复制能力。
46+
#[serde(default = "default_vim_scroll_to_arrow_keys")]
47+
pub vim_scroll_to_arrow_keys: bool,
4448
#[serde(default)]
4549
pub builtin_highlights_initialized: bool,
4650
#[serde(default)]
4751
pub custom_highlights: Vec<TerminalHighlightRule>,
4852
}
4953

54+
fn default_vim_scroll_to_arrow_keys() -> bool {
55+
true
56+
}
57+
5058
impl Default for TerminalSettings {
5159
fn default() -> Self {
5260
Self {
@@ -59,6 +67,7 @@ impl Default for TerminalSettings {
5967
cursor_blink: false,
6068
confirm_multiline_paste: true,
6169
confirm_high_risk_command: true,
70+
vim_scroll_to_arrow_keys: default_vim_scroll_to_arrow_keys(),
6271
builtin_highlights_initialized: true,
6372
custom_highlights: builtin_highlight_rules(),
6473
}
@@ -237,6 +246,7 @@ mod tests {
237246
cursor_blink: true,
238247
confirm_multiline_paste: false,
239248
confirm_high_risk_command: false,
249+
vim_scroll_to_arrow_keys: false,
240250
builtin_highlights_initialized: true,
241251
custom_highlights: Vec::new(),
242252
};

crates/terminal_view/src/sidebar/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ pub enum TerminalSidebarEvent {
119119
AutocompleteChanged(bool),
120120
/// 中键粘贴开关
121121
MiddleClickPasteChanged(bool),
122+
/// vim/TUI 滚轮转方向键开关
123+
VimScrollToArrowKeysChanged(bool),
122124
/// 路径与终端同步开关
123125
SyncPathChanged(bool),
124126
/// 自定义高亮规则变更
@@ -179,6 +181,7 @@ impl TerminalSidebar {
179181
true,
180182
true,
181183
sync_path_enabled,
184+
true,
182185
window,
183186
cx,
184187
)
@@ -268,6 +271,9 @@ impl TerminalSidebar {
268271
settings_panel::SettingsPanelEvent::MiddleClickPasteChanged(enabled) => {
269272
cx.emit(TerminalSidebarEvent::MiddleClickPasteChanged(*enabled));
270273
}
274+
settings_panel::SettingsPanelEvent::VimScrollToArrowKeysChanged(enabled) => {
275+
cx.emit(TerminalSidebarEvent::VimScrollToArrowKeysChanged(*enabled));
276+
}
271277
settings_panel::SettingsPanelEvent::SyncPathChanged(enabled) => {
272278
this.sync_path_enabled = *enabled;
273279
cx.emit(TerminalSidebarEvent::SyncPathChanged(*enabled));
@@ -433,6 +439,12 @@ impl TerminalSidebar {
433439
});
434440
}
435441

442+
pub fn set_vim_scroll_to_arrow_keys(&mut self, enabled: bool, cx: &mut Context<Self>) {
443+
self.settings_panel.update(cx, |panel, cx| {
444+
panel.set_vim_scroll_to_arrow_keys(enabled, cx);
445+
});
446+
}
447+
436448
pub fn set_sync_path_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
437449
self.sync_path_enabled = enabled;
438450
self.settings_panel.update(cx, |panel, cx| {

0 commit comments

Comments
 (0)