mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 08:02:02 +00:00
feat(cursor): give the shell prompt its own cursor shape (#963)
A new prompt_cursor_style setting (follow, block, bar, underline) shapes the caret while the shell waits at a prompt, whether tty7's inline editor or the shell's own line editor draws it. `follow`, the default, keeps cursor_style everywhere, so nothing changes until it is set. Any other value leaves cursor_style to the programs the shell runs: bar here with cursor_style block gives kitty and ghostty's bar at the prompt and a block inside a TUI that never sets a shape, such as Claude Code. A vi-mode prompt keeps the shell's own insert/normal shapes. Settings shows both cursor shapes as dropdowns, kept in step with resets, language switches and config edits made outside the window. Closes #958
This commit is contained in:
@@ -188,6 +188,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
tree is remembered in the config. Both are built from the status the host
|
||||
already sends, so a remote repository gets them unchanged. Searching the
|
||||
diff text itself is not part of this.
|
||||
- **The cursor at the shell prompt can have its own shape** (#958). A new
|
||||
`prompt_cursor_style` setting (Settings → Appearance → Cursor) takes
|
||||
`follow`, `block`, `bar` or `underline`. `follow`, the default, keeps
|
||||
`cursor_style` everywhere, exactly as before. Any other value is used at the
|
||||
prompt, drawn by tty7's inline editor or by the shell's own line editor, and
|
||||
leaves `cursor_style` to the programs the shell runs — so `bar` with
|
||||
`cursor_style: "block"` gives kitty and ghostty's bar-at-the-prompt, block in
|
||||
a TUI that never sets a shape itself, such as Claude Code. A shell prompt in
|
||||
vi mode keeps its own insert/normal shapes.
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
@@ -331,6 +331,13 @@ pub struct Config {
|
||||
|
||||
#[serde(default, deserialize_with = "de_lenient")]
|
||||
pub cursor_style: CursorStyle,
|
||||
/// The caret's shape while the shell waits at a prompt, whether tty7's
|
||||
/// inline editor or the shell's own line editor draws it. `follow` keeps
|
||||
/// `cursor_style` everywhere; any other value leaves `cursor_style` to the
|
||||
/// programs the shell runs, which is how kitty and ghostty put a bar at
|
||||
/// the prompt and a block inside a TUI that never sets a shape itself.
|
||||
#[serde(default, deserialize_with = "de_lenient")]
|
||||
pub prompt_cursor_style: PromptCursorStyle,
|
||||
|
||||
pub macos_option_as_alt: bool,
|
||||
pub mouse_hide_while_typing: bool,
|
||||
@@ -536,6 +543,28 @@ pub enum CursorStyle {
|
||||
Underline,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PromptCursorStyle {
|
||||
#[default]
|
||||
Follow,
|
||||
Block,
|
||||
Bar,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl PromptCursorStyle {
|
||||
/// The shape this names, or `None` when the prompt follows `cursor_style`.
|
||||
pub fn shape(self) -> Option<CursorStyle> {
|
||||
match self {
|
||||
Self::Follow => None,
|
||||
Self::Block => Some(CursorStyle::Block),
|
||||
Self::Bar => Some(CursorStyle::Bar),
|
||||
Self::Underline => Some(CursorStyle::Underline),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum NewTabPosition {
|
||||
@@ -763,6 +792,7 @@ impl Default for Config {
|
||||
tab_completion: true,
|
||||
history_search: true,
|
||||
cursor_style: CursorStyle::Block,
|
||||
prompt_cursor_style: PromptCursorStyle::Follow,
|
||||
macos_option_as_alt: false,
|
||||
mouse_hide_while_typing: true,
|
||||
focus_follows_mouse: false,
|
||||
@@ -3057,4 +3087,24 @@ mod tests {
|
||||
assert_eq!(back.ssh_tab_title, mode);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_cursor_style_defaults_to_following_cursor_style() {
|
||||
assert_eq!(
|
||||
Config::default().prompt_cursor_style,
|
||||
PromptCursorStyle::Follow
|
||||
);
|
||||
assert_eq!(PromptCursorStyle::Follow.shape(), None);
|
||||
|
||||
let cfg: Config =
|
||||
serde_json::from_str(r#"{"cursor_style": "block", "prompt_cursor_style": "bar"}"#)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.cursor_style, CursorStyle::Block);
|
||||
assert_eq!(cfg.prompt_cursor_style.shape(), Some(CursorStyle::Bar));
|
||||
|
||||
// A value this build does not know falls back rather than failing the
|
||||
// whole config.
|
||||
let cfg: Config = serde_json::from_str(r#"{"prompt_cursor_style": "beam"}"#).unwrap();
|
||||
assert_eq!(cfg.prompt_cursor_style, PromptCursorStyle::Follow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ their id from the file name. [More about themes →](/customization/themes)
|
||||
| `env` | object | `{}` | Extra environment variables for every pane. |
|
||||
| `scrollback_limit` | number | `10000` | Lines per pane (100–100,000). New panes only. |
|
||||
| `cursor_style` | enum | `"block"` | `block`, `bar`, `underline`. |
|
||||
| `prompt_cursor_style` | enum | `"follow"` | The cursor at the shell prompt: `follow`, `block`, `bar`, `underline`. `follow` uses `cursor_style` everywhere; any other value leaves `cursor_style` to the programs the shell runs, e.g. `bar` here with `cursor_style: "block"` gives a bar at the prompt and a block in a TUI that sets no shape of its own. A shell prompt in vi mode keeps its own shapes. |
|
||||
| `cursor_blink` | bool | `true` | |
|
||||
| `bell` | enum | `"visual"` | `none`, `visual`, `audible`, `both`. |
|
||||
| `per_pane_history` | bool | `false` | Give each pane its own shell history file. |
|
||||
|
||||
+18
-1
@@ -4,6 +4,7 @@ use std::collections::HashMap;
|
||||
use alacritty_terminal::grid::Dimensions as _;
|
||||
use alacritty_terminal::index::{Column as AlacColumn, Line as AlacLine, Point as AlacPoint};
|
||||
use alacritty_terminal::selection::SelectionRange;
|
||||
use alacritty_terminal::term::TermMode;
|
||||
use alacritty_terminal::term::cell::{Cell, Flags};
|
||||
use alacritty_terminal::vte::ansi::{Color as AnsiColor, CursorShape, NamedColor, Rgb};
|
||||
use gpui::{
|
||||
@@ -1817,6 +1818,17 @@ impl GridSnapshot {
|
||||
}
|
||||
|
||||
impl TerminalElement {
|
||||
/// The shape `prompt_cursor_style` puts on the grid cursor while the shell
|
||||
/// waits at a prompt with its own line editor — `prompt_editor` off, or a
|
||||
/// line handed back to ZLE/readline. Shell integration has no mark for "a
|
||||
/// prompt plugin set this shape", so the one it does report, vi mode, keeps
|
||||
/// the shell's own insert/normal shapes.
|
||||
pub(super) fn prompt_cursor_shape(&self, cx: &App) -> Option<crate::core::config::CursorStyle> {
|
||||
let shape = cx.global::<Config>().prompt_cursor_style.shape()?;
|
||||
let terminal = &self.view.read(cx).terminal;
|
||||
(terminal.at_prompt() && !terminal.shell_vi_mode()).then_some(shape)
|
||||
}
|
||||
|
||||
pub(super) fn build_grid(
|
||||
&self,
|
||||
colors: &PaintColors,
|
||||
@@ -1834,6 +1846,7 @@ impl TerminalElement {
|
||||
let mut any_selected = false;
|
||||
let display_offset;
|
||||
let history_size;
|
||||
let prompt_shape = self.prompt_cursor_shape(cx);
|
||||
{
|
||||
let mut palette = self.view.read(cx).terminal.palette;
|
||||
if let Some(active) = cx.try_global::<crate::terminal::palette::ActivePalette>() {
|
||||
@@ -1949,7 +1962,11 @@ impl TerminalElement {
|
||||
col,
|
||||
ime_col,
|
||||
hidden: cursor_hidden,
|
||||
style: cursor_style_from_shape(cur.shape),
|
||||
// Checked under the grid lock: `on_alt_screen` would take
|
||||
// it a second time.
|
||||
style: prompt_shape
|
||||
.filter(|_| !term.mode().contains(TermMode::ALT_SCREEN))
|
||||
.unwrap_or_else(|| cursor_style_from_shape(cur.shape)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+73
-1
@@ -6599,7 +6599,11 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_style = cx.global::<Config>().cursor_style;
|
||||
// This caret only exists at a prompt, so the prompt's own shape wins.
|
||||
let cursor_style = {
|
||||
let cfg = cx.global::<Config>();
|
||||
cfg.prompt_cursor_style.shape().unwrap_or(cfg.cursor_style)
|
||||
};
|
||||
let cursor_paint = input_caret_paint(focused, self.cursor_visible, cursor_style);
|
||||
let cursor_on = cursor_paint.is_some();
|
||||
let block_cursor = cursor_paint == Some(InputCaretPaint::Block);
|
||||
@@ -16451,6 +16455,74 @@ mod gpui_tests {
|
||||
"a spawned pane counts its first agent, once"
|
||||
);
|
||||
}
|
||||
|
||||
/// #958: `prompt_cursor_style` shapes the shell's own caret at a prompt,
|
||||
/// leaves the running program's cursor alone, and steps aside for a vi-mode
|
||||
/// prompt, whose insert/normal shapes are the shell's to set.
|
||||
#[gpui::test]
|
||||
fn prompt_cursor_style_shapes_only_the_prompt(cx: &mut TestAppContext) {
|
||||
use crate::core::config::{CursorStyle, PromptCursorStyle};
|
||||
|
||||
let (window, mut daemon) = harness(cx);
|
||||
let shape = |cx: &mut TestAppContext| {
|
||||
cx.update(|cx| {
|
||||
let view = window.entity(cx).unwrap();
|
||||
TerminalElement::new(view).prompt_cursor_shape(cx)
|
||||
})
|
||||
};
|
||||
let wait_for = |cx: &mut TestAppContext, want: &dyn Fn(&TerminalView) -> bool| {
|
||||
for _ in 0..200 {
|
||||
cx.run_until_parked();
|
||||
if window.update(cx, |view, _, _| want(view)).unwrap() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("the pane never reached the expected prompt state");
|
||||
};
|
||||
cx.update(|cx| cx.global_mut::<Config>().prompt_cursor_style = PromptCursorStyle::Bar);
|
||||
|
||||
assert_eq!(
|
||||
shape(cx),
|
||||
None,
|
||||
"no prompt reported yet: a program owns the cursor"
|
||||
);
|
||||
|
||||
DaemonMsg::Prompt {
|
||||
active: true,
|
||||
at_prompt: true,
|
||||
last_exit: None,
|
||||
}
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
wait_for(cx, &|view| view.terminal.at_prompt());
|
||||
assert_eq!(shape(cx), Some(CursorStyle::Bar));
|
||||
|
||||
cx.update(|cx| cx.global_mut::<Config>().prompt_cursor_style = PromptCursorStyle::Follow);
|
||||
assert_eq!(shape(cx), None, "follow leaves the prompt on cursor_style");
|
||||
cx.update(|cx| cx.global_mut::<Config>().prompt_cursor_style = PromptCursorStyle::Bar);
|
||||
|
||||
DaemonMsg::Output(b"\x1b]133;V;1\x07\x1b]133;B\x07".to_vec())
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
wait_for(cx, &|view| view.terminal.shell_vi_mode());
|
||||
assert_eq!(
|
||||
shape(cx),
|
||||
None,
|
||||
"a vi-mode prompt keeps the shell's own shapes"
|
||||
);
|
||||
|
||||
DaemonMsg::Prompt {
|
||||
active: true,
|
||||
at_prompt: false,
|
||||
last_exit: None,
|
||||
}
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
wait_for(cx, &|view| !view.terminal.at_prompt());
|
||||
assert_eq!(shape(cx), None, "a running command gets cursor_style back");
|
||||
}
|
||||
|
||||
/// Seeds a pane's history for the ghost tests: `(command, dirs, exit)`,
|
||||
/// oldest first, with `cwd` as the directory the pane is in.
|
||||
fn seed_ghost_history(
|
||||
|
||||
+117
@@ -3015,6 +3015,19 @@ impl Tty7App {
|
||||
self.apply_terminal_config_to_panes(&cfg, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_prompt_cursor_style(
|
||||
&mut self,
|
||||
style: crate::core::config::PromptCursorStyle,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_config(cx, |cfg| cfg.prompt_cursor_style = style);
|
||||
for tab in &self.tabs {
|
||||
for leaf in tab.pane.terminals() {
|
||||
leaf.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persist_settings_config(&mut self, cx: &mut Context<Self>) {
|
||||
let config = cx.global::<Config>().clone();
|
||||
let error = config.try_save().err().map(|error| error.to_string());
|
||||
@@ -6235,6 +6248,9 @@ impl Tty7App {
|
||||
}
|
||||
L10nKey::SettingsCursorBlink => self.set_cursor_blink(defaults.cursor_blink, cx),
|
||||
L10nKey::SettingsCursorShape => self.set_cursor_style(defaults.cursor_style, cx),
|
||||
L10nKey::SettingsPromptCursorShape => {
|
||||
self.set_prompt_cursor_style(defaults.prompt_cursor_style, cx)
|
||||
}
|
||||
L10nKey::SettingsScrollback => self.set_scrollback_limit(defaults.scrollback_limit, cx),
|
||||
L10nKey::SettingsNewTabPosition => {
|
||||
self.set_new_tab_position(defaults.new_tab_position, cx)
|
||||
@@ -6410,6 +6426,9 @@ impl Tty7App {
|
||||
s.ui_font_select = ui;
|
||||
}
|
||||
}
|
||||
L10nKey::SettingsCursorShape | L10nKey::SettingsPromptCursorShape => {
|
||||
self.sync_cursor_selects(window, cx)
|
||||
}
|
||||
L10nKey::SettingsLanguage => {
|
||||
let value = self.build_language_select(&mut subs, window, cx);
|
||||
if let Some(s) = self.active_settings_mut() {
|
||||
@@ -6537,6 +6556,8 @@ impl Tty7App {
|
||||
let (font_select, font_bold_select, font_italic_select, ui_font_select) =
|
||||
self.build_font_selects(&mut subs, window, cx);
|
||||
let language_select = self.build_language_select(&mut subs, window, cx);
|
||||
let (cursor_style_select, prompt_cursor_style_select) =
|
||||
self.build_cursor_selects(&mut subs, window, cx);
|
||||
#[cfg(target_os = "windows")]
|
||||
let window_backdrop_select = self.build_window_backdrop_select(&mut subs, window, cx);
|
||||
let (shell_program_input, shell_args_input, wd_path_input) =
|
||||
@@ -6628,6 +6649,8 @@ impl Tty7App {
|
||||
font_italic_select,
|
||||
ui_font_select,
|
||||
language_select,
|
||||
cursor_style_select,
|
||||
prompt_cursor_style_select,
|
||||
#[cfg(target_os = "windows")]
|
||||
window_backdrop_select,
|
||||
shell_program_input,
|
||||
@@ -6833,6 +6856,98 @@ impl Tty7App {
|
||||
language_select
|
||||
}
|
||||
|
||||
/// The cursor shape and prompt cursor shape dropdowns. Each resolves the
|
||||
/// picked label back through the row list it was built from.
|
||||
fn build_cursor_selects(
|
||||
&mut self,
|
||||
subs: &mut Vec<Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (
|
||||
Entity<SelectState<SearchableVec<String>>>,
|
||||
Entity<SelectState<SearchableVec<String>>>,
|
||||
) {
|
||||
use crate::ui::settings::{
|
||||
CURSOR_SHAPES, PROMPT_CURSOR_SHAPES, cursor_shape_labels, prompt_cursor_shape_labels,
|
||||
};
|
||||
let (shape_ix, prompt_ix) = Self::cursor_select_rows(cx);
|
||||
let shape = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
SearchableVec::new(cursor_shape_labels()),
|
||||
Some(IndexPath::default().row(shape_ix)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let prompt = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
SearchableVec::new(prompt_cursor_shape_labels()),
|
||||
Some(IndexPath::default().row(prompt_ix)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
subs.push(cx.subscribe_in(
|
||||
&shape,
|
||||
window,
|
||||
|this, _select, ev: &SelectEvent<SearchableVec<String>>, _window, cx| {
|
||||
if let SelectEvent::Confirm(Some(label)) = ev {
|
||||
if let Some(ix) = cursor_shape_labels().iter().position(|r| r == label) {
|
||||
this.set_cursor_style(CURSOR_SHAPES[ix], cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
subs.push(cx.subscribe_in(
|
||||
&prompt,
|
||||
window,
|
||||
|this, _select, ev: &SelectEvent<SearchableVec<String>>, _window, cx| {
|
||||
if let SelectEvent::Confirm(Some(label)) = ev {
|
||||
if let Some(ix) = prompt_cursor_shape_labels().iter().position(|r| r == label) {
|
||||
this.set_prompt_cursor_style(PROMPT_CURSOR_SHAPES[ix], cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
(shape, prompt)
|
||||
}
|
||||
|
||||
/// The dropdown rows the config's two cursor shapes sit on.
|
||||
fn cursor_select_rows(cx: &App) -> (usize, usize) {
|
||||
use crate::ui::settings::{CURSOR_SHAPES, PROMPT_CURSOR_SHAPES};
|
||||
let cfg = cx.global::<Config>();
|
||||
let shape = CURSOR_SHAPES.iter().position(|s| *s == cfg.cursor_style);
|
||||
let prompt = PROMPT_CURSOR_SHAPES
|
||||
.iter()
|
||||
.position(|s| *s == cfg.prompt_cursor_style);
|
||||
(shape.unwrap_or(0), prompt.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Puts both cursor dropdowns back on the config's values, relabelled in
|
||||
/// the current language — after a reset, a language switch, or a config
|
||||
/// edit made outside this window.
|
||||
fn sync_cursor_selects(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
use crate::ui::settings::{cursor_shape_labels, prompt_cursor_shape_labels};
|
||||
let Some((shape, prompt)) = self.active_settings().map(|s| {
|
||||
(
|
||||
s.cursor_style_select.clone(),
|
||||
s.prompt_cursor_style_select.clone(),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let (shape_ix, prompt_ix) = Self::cursor_select_rows(cx);
|
||||
for (select, rows, ix) in [
|
||||
(shape, cursor_shape_labels(), shape_ix),
|
||||
(prompt, prompt_cursor_shape_labels(), prompt_ix),
|
||||
] {
|
||||
select.update(cx, |state, cx| {
|
||||
state.set_items(SearchableVec::new(rows), window, cx);
|
||||
state.set_selected_index(Some(IndexPath::default().row(ix)), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_gui_language(code: &str) -> &'static str {
|
||||
crate::ui::i18n::find_language(code)
|
||||
.map(|lang| lang.code)
|
||||
@@ -6902,6 +7017,7 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_locale_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.sync_cursor_selects(window, cx);
|
||||
self.sidebar_search.update(cx, |state, cx| {
|
||||
state.set_placeholder(t(L10nKey::SearchTabs), window, cx)
|
||||
});
|
||||
@@ -7430,6 +7546,7 @@ impl Tty7App {
|
||||
self.terminal_scrollback_limit = config.scrollback_limit;
|
||||
self.apply_terminal_config_to_panes(&config, cx);
|
||||
}
|
||||
self.sync_cursor_selects(window, cx);
|
||||
let (font_size, line_height, font_family, font_fallbacks, font_features) = {
|
||||
let cfg = cx.global::<Config>();
|
||||
(
|
||||
|
||||
@@ -140,6 +140,10 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsCursor => "Cursor",
|
||||
L10nKey::SettingsCursorShape => "Cursor shape",
|
||||
L10nKey::SettingsCursorShapeDesc => "How the terminal cursor is drawn.",
|
||||
L10nKey::SettingsPromptCursorShape => "Prompt cursor shape",
|
||||
L10nKey::SettingsPromptCursorShapeDesc => {
|
||||
"The cursor at the shell prompt. Follow uses the cursor shape above everywhere."
|
||||
}
|
||||
L10nKey::SettingsCursorBlink => "Cursor blink",
|
||||
L10nKey::SettingsCursorBlinkDesc => "Pulse the cursor while the terminal is focused.",
|
||||
L10nKey::SettingsLanguage => "Language",
|
||||
@@ -839,6 +843,9 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsSearchCopyOnSelectKeywords => "clipboard selection yank mouse",
|
||||
L10nKey::SettingsSearchCursorBlinkKeywords => "caret blinking flash",
|
||||
L10nKey::SettingsSearchCursorShapeKeywords => "caret block bar underline beam",
|
||||
L10nKey::SettingsSearchPromptCursorShapeKeywords => {
|
||||
"prompt cursor shape caret block bar underline beam shell integration"
|
||||
}
|
||||
L10nKey::SettingsSearchCustomThemesKeywords => {
|
||||
"theme duplicate edit colors folder yaml import background image wallpaper"
|
||||
}
|
||||
@@ -1825,6 +1832,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::CursorShapeBlock => "Block",
|
||||
L10nKey::CursorShapeBar => "Bar",
|
||||
L10nKey::CursorShapeUnderline => "Underline",
|
||||
L10nKey::PromptCursorShapeFollow => "Follow",
|
||||
L10nKey::PaletteTryDifferentSearch => "Try a different search.",
|
||||
L10nKey::CompletionListingRemote => "listing remote…",
|
||||
L10nKey::CompletionRemoteListingFailed => "remote listing failed — {error}",
|
||||
|
||||
@@ -142,6 +142,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsCursor => "カーソル",
|
||||
L10nKey::SettingsCursorShape => "カーソルの形状",
|
||||
L10nKey::SettingsCursorShapeDesc => "ターミナルカーソルの描画方法",
|
||||
L10nKey::SettingsPromptCursorShape => "プロンプトのカーソル形状",
|
||||
L10nKey::SettingsPromptCursorShapeDesc => {
|
||||
"シェルプロンプトでのカーソル。「追従」は上のカーソル形状をどこでも使う"
|
||||
}
|
||||
L10nKey::SettingsCursorBlink => "カーソルの点滅",
|
||||
L10nKey::SettingsCursorBlinkDesc => {
|
||||
"ターミナルがフォーカスされている間、カーソルを点滅させる"
|
||||
@@ -865,6 +869,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchCursorShapeKeywords => {
|
||||
"カーソル 形状 ブロック バー アンダーライン ビーム cursor shape caret block bar underline beam"
|
||||
}
|
||||
L10nKey::SettingsSearchPromptCursorShapeKeywords => {
|
||||
"プロンプト カーソル 形状 ブロック バー 下線 prompt cursor shape caret block bar underline beam"
|
||||
}
|
||||
L10nKey::SettingsSearchCustomThemesKeywords => {
|
||||
"テーマ 複製 編集 色 フォルダ 背景画像 壁紙 yaml インポート custom themes duplicate edit colors folder import background image wallpaper"
|
||||
}
|
||||
@@ -1897,6 +1904,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CursorShapeBlock => "ブロック",
|
||||
L10nKey::CursorShapeBar => "バー",
|
||||
L10nKey::CursorShapeUnderline => "下線",
|
||||
L10nKey::PromptCursorShapeFollow => "追従",
|
||||
L10nKey::PaletteTryDifferentSearch => "別のキーワードを試してください。",
|
||||
L10nKey::CompletionListingRemote => "リモートを一覧しています…",
|
||||
L10nKey::CompletionRemoteListingFailed => "リモートの一覧に失敗しました — {error}",
|
||||
|
||||
@@ -186,6 +186,8 @@ l10n_keys! {
|
||||
SettingsCursor,
|
||||
SettingsCursorShape,
|
||||
SettingsCursorShapeDesc,
|
||||
SettingsPromptCursorShape,
|
||||
SettingsPromptCursorShapeDesc,
|
||||
SettingsCursorBlink,
|
||||
SettingsCursorBlinkDesc,
|
||||
SettingsLanguage,
|
||||
@@ -642,6 +644,7 @@ l10n_keys! {
|
||||
SettingsSearchCopyOnSelectKeywords,
|
||||
SettingsSearchCursorBlinkKeywords,
|
||||
SettingsSearchCursorShapeKeywords,
|
||||
SettingsSearchPromptCursorShapeKeywords,
|
||||
SettingsSearchCustomThemesKeywords,
|
||||
SettingsSearchDetectUrlsKeywords,
|
||||
SettingsSearchDiffPreviewFromCountsKeywords,
|
||||
@@ -1475,6 +1478,7 @@ l10n_keys! {
|
||||
CursorShapeBlock,
|
||||
CursorShapeBar,
|
||||
CursorShapeUnderline,
|
||||
PromptCursorShapeFollow,
|
||||
PaletteTryDifferentSearch,
|
||||
CompletionListingRemote,
|
||||
CompletionRemoteListingFailed,
|
||||
|
||||
@@ -126,6 +126,10 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsCursor => "光标",
|
||||
L10nKey::SettingsCursorShape => "光标形状",
|
||||
L10nKey::SettingsCursorShapeDesc => "终端光标的绘制方式。",
|
||||
L10nKey::SettingsPromptCursorShape => "提示符光标形状",
|
||||
L10nKey::SettingsPromptCursorShapeDesc => {
|
||||
"shell 提示符处的光标。跟随表示各处都用上面的光标形状。"
|
||||
}
|
||||
L10nKey::SettingsCursorBlink => "光标闪烁",
|
||||
L10nKey::SettingsCursorBlinkDesc => "终端获得焦点时让光标闪烁。",
|
||||
L10nKey::SettingsLanguage => "语言",
|
||||
@@ -760,6 +764,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchCursorShapeKeywords => {
|
||||
"光标形状 光标 块 竖线 下划线 cursor shape caret block bar underline beam"
|
||||
}
|
||||
L10nKey::SettingsSearchPromptCursorShapeKeywords => {
|
||||
"提示符 光标形状 光标 块 竖线 下划线 prompt cursor shape caret block bar underline beam"
|
||||
}
|
||||
L10nKey::SettingsSearchCustomThemesKeywords => {
|
||||
"自定义主题 复制 编辑 颜色 文件夹 背景图片 壁纸 yaml 导入 theme custom edit duplicate colors import background image wallpaper"
|
||||
}
|
||||
@@ -1720,6 +1727,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CursorShapeBlock => "块状",
|
||||
L10nKey::CursorShapeBar => "竖线",
|
||||
L10nKey::CursorShapeUnderline => "下划线",
|
||||
L10nKey::PromptCursorShapeFollow => "跟随",
|
||||
L10nKey::PaletteTryDifferentSearch => "换个关键词试试。",
|
||||
L10nKey::CompletionListingRemote => "正在列出远程目录…",
|
||||
L10nKey::CompletionRemoteListingFailed => "远程目录列表失败——{error}",
|
||||
|
||||
+65
-24
@@ -24,7 +24,8 @@ use uuid::Uuid;
|
||||
|
||||
use crate::core::config::{
|
||||
BellMode, Config, CursorStyle, LinkFileOpen, MouseZoomModifier, NewTabPosition, NotifyMode,
|
||||
SshTabTitle, TabBarPosition, UI_FONT_SIZE_DEFAULT, UpdateChannel, WindowBackdrop,
|
||||
PromptCursorStyle, SshTabTitle, TabBarPosition, UI_FONT_SIZE_DEFAULT, UpdateChannel,
|
||||
WindowBackdrop,
|
||||
};
|
||||
use crate::core::keychain::{
|
||||
CredentialRef, CredentialStore as _, OsCredentialStore, key_account_from_contents,
|
||||
@@ -573,6 +574,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: SettingsCursorShape,
|
||||
keywords: SettingsSearchCursorShapeKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Appearance,
|
||||
title: SettingsPromptCursorShape,
|
||||
keywords: SettingsSearchPromptCursorShapeKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Appearance,
|
||||
title: SettingsCursorBlink,
|
||||
@@ -902,12 +908,43 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
]
|
||||
}
|
||||
|
||||
/// The rows of the cursor shape dropdown, in `CURSOR_SHAPES` order.
|
||||
pub(crate) fn cursor_shape_labels() -> Vec<String> {
|
||||
[
|
||||
L10nKey::CursorShapeBlock,
|
||||
L10nKey::CursorShapeBar,
|
||||
L10nKey::CursorShapeUnderline,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|key| t(key).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) const CURSOR_SHAPES: [CursorStyle; 3] =
|
||||
[CursorStyle::Block, CursorStyle::Bar, CursorStyle::Underline];
|
||||
|
||||
/// The rows of the prompt cursor shape dropdown, in `PROMPT_CURSOR_SHAPES`
|
||||
/// order: follow, then the shapes.
|
||||
pub(crate) fn prompt_cursor_shape_labels() -> Vec<String> {
|
||||
let mut rows = vec![t(L10nKey::PromptCursorShapeFollow).to_string()];
|
||||
rows.extend(cursor_shape_labels());
|
||||
rows
|
||||
}
|
||||
|
||||
pub(crate) const PROMPT_CURSOR_SHAPES: [PromptCursorStyle; 4] = [
|
||||
PromptCursorStyle::Follow,
|
||||
PromptCursorStyle::Block,
|
||||
PromptCursorStyle::Bar,
|
||||
PromptCursorStyle::Underline,
|
||||
];
|
||||
|
||||
impl SearchEntry {
|
||||
fn config_key(&self) -> &'static str {
|
||||
match self.title {
|
||||
L10nKey::SettingsDimInactivePanes => "dim_inactive_panes",
|
||||
L10nKey::SettingsCursorBlink => "cursor_blink",
|
||||
L10nKey::SettingsCursorShape => "cursor_style",
|
||||
L10nKey::SettingsPromptCursorShape => "prompt_cursor_style",
|
||||
L10nKey::SettingsScrollback => "scrollback_limit",
|
||||
L10nKey::SettingsNewTabPosition => "new_tab_position",
|
||||
L10nKey::SettingsTabBarPosition => "tab_bar_position",
|
||||
@@ -994,6 +1031,7 @@ impl SearchEntry {
|
||||
L10nKey::SettingsFontLigatures => t(L10nKey::SettingsFontLigaturesDesc),
|
||||
L10nKey::SettingsFontThicken => t(L10nKey::SettingsFontThickenDesc),
|
||||
L10nKey::SettingsCursorShape => t(L10nKey::SettingsCursorShapeDesc),
|
||||
L10nKey::SettingsPromptCursorShape => t(L10nKey::SettingsPromptCursorShapeDesc),
|
||||
L10nKey::SettingsCursorBlink => t(L10nKey::SettingsCursorBlinkDesc),
|
||||
L10nKey::SettingsBackgroundImage => t(L10nKey::SettingsBackgroundImageDesc),
|
||||
L10nKey::SettingsImageOpacity => t(L10nKey::SettingsImageOpacityDesc),
|
||||
@@ -1060,6 +1098,9 @@ impl SearchEntry {
|
||||
}
|
||||
L10nKey::SettingsCursorBlink => cfg.cursor_blink != defaults.cursor_blink,
|
||||
L10nKey::SettingsCursorShape => cfg.cursor_style != defaults.cursor_style,
|
||||
L10nKey::SettingsPromptCursorShape => {
|
||||
cfg.prompt_cursor_style != defaults.prompt_cursor_style
|
||||
}
|
||||
L10nKey::SettingsScrollback => cfg.scrollback_limit != defaults.scrollback_limit,
|
||||
L10nKey::SettingsNewTabPosition => cfg.new_tab_position != defaults.new_tab_position,
|
||||
L10nKey::SettingsTabBarPosition => cfg.tab_bar_position != defaults.tab_bar_position,
|
||||
@@ -1311,6 +1352,8 @@ pub(crate) struct SettingsState {
|
||||
pub(crate) font_italic_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
pub(crate) ui_font_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
pub(crate) language_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
pub(crate) cursor_style_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
pub(crate) prompt_cursor_style_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) window_backdrop_select: Entity<SelectState<SearchableVec<String>>>,
|
||||
pub(crate) shell_program_input: Entity<InputState>,
|
||||
@@ -3535,8 +3578,14 @@ impl Tty7App {
|
||||
),
|
||||
None => return div().into_any_element(),
|
||||
};
|
||||
let (cursor_style_select, prompt_cursor_style_select) = match self.active_settings() {
|
||||
Some(s) => (
|
||||
s.cursor_style_select.clone(),
|
||||
s.prompt_cursor_style_select.clone(),
|
||||
),
|
||||
None => return div().into_any_element(),
|
||||
};
|
||||
let cfg = cx.global::<Config>();
|
||||
let cursor_style = cfg.cursor_style;
|
||||
let cursor_blink = cfg.cursor_blink;
|
||||
let font_thicken = cfg.font_thicken;
|
||||
let font_ligatures = cfg.font_features.as_ref().is_some_and(|features| {
|
||||
@@ -3656,29 +3705,15 @@ impl Tty7App {
|
||||
)
|
||||
});
|
||||
|
||||
let cursor_idx = match cursor_style {
|
||||
CursorStyle::Block => 0,
|
||||
CursorStyle::Bar => 1,
|
||||
CursorStyle::Underline => 2,
|
||||
let cursor_dropdown = |state: &Entity<SelectState<SearchableVec<String>>>| {
|
||||
Select::new(state)
|
||||
.small()
|
||||
.w(px(FIELD_W))
|
||||
.h(px(24.))
|
||||
.into_any_element()
|
||||
};
|
||||
let cursor_style_control = self.segmented(
|
||||
"cursor-style",
|
||||
&[
|
||||
t(L10nKey::CursorShapeBlock),
|
||||
t(L10nKey::CursorShapeBar),
|
||||
t(L10nKey::CursorShapeUnderline),
|
||||
],
|
||||
cursor_idx,
|
||||
cx,
|
||||
|this, ix, _w, cx| {
|
||||
let style = match ix {
|
||||
0 => CursorStyle::Block,
|
||||
1 => CursorStyle::Bar,
|
||||
_ => CursorStyle::Underline,
|
||||
};
|
||||
this.set_cursor_style(style, cx);
|
||||
},
|
||||
);
|
||||
let cursor_style_control = cursor_dropdown(&cursor_style_select);
|
||||
let prompt_cursor_style_control = cursor_dropdown(&prompt_cursor_style_select);
|
||||
let blink_switch = crate::ui::theme::switch("cursor-blink", cx)
|
||||
.checked(cursor_blink)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_cursor_blink(*on, cx)))
|
||||
@@ -3755,6 +3790,12 @@ impl Tty7App {
|
||||
cursor_style_control,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
t(L10nKey::SettingsPromptCursorShape),
|
||||
t(L10nKey::SettingsPromptCursorShapeDesc),
|
||||
prompt_cursor_style_control,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
t(L10nKey::SettingsCursorBlink),
|
||||
t(L10nKey::SettingsCursorBlinkDesc),
|
||||
|
||||
Reference in New Issue
Block a user