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:
l0ng-ai
2026-09-27 09:35:18 +08:00
committed by GitHub
parent 334734b0b6
commit 39ceb35fdd
11 changed files with 361 additions and 26 deletions
+9
View File
@@ -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
+50
View File
@@ -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);
}
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>();
(
+8
View File
@@ -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}",
+8
View File
@@ -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}",
+4
View File
@@ -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,
+8
View File
@@ -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
View File
@@ -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),