From 042834c7d515ac0cf751742d0d141e140e202c14 Mon Sep 17 00:00:00 2001 From: TomZz Date: Thu, 25 Jun 2026 02:25:32 +0800 Subject: [PATCH] feat: support custom terminal cursor style and blinking setting --- locales/en.yml | 5 ++++ locales/zh-CN.yml | 5 ++++ src/app/dialogs.rs | 52 +++++++++++++++++++++++++++++++++++++++++ src/app/mod.rs | 15 +++++++++++- src/session/config.rs | 21 +++++++++++++++++ src/session/mod.rs | 13 +++++++++++ src/terminal/element.rs | 34 +++++++++++++++++++++++---- 7 files changed, 139 insertions(+), 6 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index a95b9ac..c7f71bd 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -67,6 +67,11 @@ terminal_font_size: "Terminal Font Size" system_default: "System Default" ui_font_family: "UI Font" terminal_font_family: "Terminal Font" +cursor_style: "Cursor Style" +cursor_style_default: "Default" +cursor_style_blink: "Blink" +cursor_style_beam: "Line" +cursor_style_beam_blink: "Line Blink" monitoring_position: "Monitoring Position" position_bottom: "Bottom" position_sidebar: "Sidebar" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index bbe7e3f..65c50e6 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -68,6 +68,11 @@ terminal_font_size: "终端字体大小" system_default: "系统默认" ui_font_family: "界面字体" terminal_font_family: "终端字体" +cursor_style: "光标样式" +cursor_style_default: "默认" +cursor_style_blink: "闪烁" +cursor_style_beam: "细线" +cursor_style_beam_blink: "细线闪烁" monitoring_position: "系统监控显示位置" position_bottom: "底部" position_sidebar: "侧边栏" diff --git a/src/app/dialogs.rs b/src/app/dialogs.rs index 60ec5eb..44b20a3 100644 --- a/src/app/dialogs.rs +++ b/src/app/dialogs.rs @@ -1521,6 +1521,58 @@ impl Ashell { }) ) ) + .item( + SettingItem::new( + t!("cursor_style").to_string(), + SettingField::render({ + let view = view_clone_for_general.clone(); + move |_, _window, cx| { + use crate::session::config::CursorStyle; + let current = view.read(cx).cursor_style; + Button::new("cursor-style-dropdown") + .small() + .icon(IconName::ChevronsUpDown) + .label(match current { + CursorStyle::Default => t!("cursor_style_default").to_string(), + CursorStyle::Blink => t!("cursor_style_blink").to_string(), + CursorStyle::Beam => t!("cursor_style_beam").to_string(), + CursorStyle::BeamBlink => t!("cursor_style_beam_blink").to_string(), + }) + .dropdown_menu_with_anchor(Anchor::BottomRight, { + let view = view.clone(); + move |mut menu, window, cx| { + use crate::session::config::CursorStyle; + let current = view.read(cx).cursor_style; + menu = menu.min_w(160.).max_h(px(320.)).scrollable(true); + for style in [ + CursorStyle::Default, + CursorStyle::Blink, + CursorStyle::Beam, + CursorStyle::BeamBlink, + ] { + let checked = style == current; + let label = match style { + CursorStyle::Default => t!("cursor_style_default").to_string(), + CursorStyle::Blink => t!("cursor_style_blink").to_string(), + CursorStyle::Beam => t!("cursor_style_beam").to_string(), + CursorStyle::BeamBlink => t!("cursor_style_beam_blink").to_string(), + }; + menu = menu.item( + PopupMenuItem::new(label) + .checked(checked) + .on_click(window.listener_for(&view, move |this, _, _window, cx| { + this.change_cursor_style(style, cx); + })) + ); + } + menu + } + }) + .into_any_element() + } + }) + ) + ) ) .group( SettingGroup::new() diff --git a/src/app/mod.rs b/src/app/mod.rs index 5a5138f..64aa652 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -275,6 +275,7 @@ pub(crate) struct Ashell { pub(crate) status: SharedString, pub(crate) config: ConfigStore, pub(crate) active_title_bar_style: crate::session::config::TitleBarStyle, + pub(crate) cursor_style: crate::session::config::CursorStyle, pub(crate) system_sampler: SystemSampler, pub(crate) recording_action: Option, pub(crate) active_dialog: Option, @@ -545,6 +546,7 @@ impl Ashell { dark_theme_name, ui_font_size: config.ui_font_size(), terminal_font_size: config.terminal_font_size(), + cursor_style: config.cursor_style(), ui_font_family, terminal_font_family, tabs: Vec::new(), @@ -699,6 +701,7 @@ impl Ashell { pub(crate) fn start_event_pump(&self, window: &mut Window, cx: &mut Context) { cx.spawn_in(window, async move |this, mut cx| { let mut idle_frames = 0u32; + let mut last_blink_time = std::time::Instant::now(); loop { cx.background_executor() .timer(Duration::from_millis(16)) @@ -710,9 +713,19 @@ impl Ashell { changed = this.drain_backend_events(window, cx); system_sampled = this.sample_system_if_due(); this.sync_theme_if_due(cx); - if changed || system_sampled { + let is_blinking = matches!( + this.cursor_style, + crate::session::config::CursorStyle::Blink + | crate::session::config::CursorStyle::BeamBlink + ); + let now = std::time::Instant::now(); + let blink_due = is_blinking && now.duration_since(last_blink_time) >= std::time::Duration::from_millis(600); + if changed || system_sampled || blink_due { cx.notify(); idle_frames = 0; + if blink_due { + last_blink_time = now; + } } else { idle_frames += 1; if idle_frames >= 60 { diff --git a/src/session/config.rs b/src/session/config.rs index ee37c32..caa7491 100644 --- a/src/session/config.rs +++ b/src/session/config.rs @@ -128,6 +128,16 @@ pub enum TitleBarStyle { Integrated, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "kebab-case")] +pub enum CursorStyle { + #[default] + Default, + Blink, + Beam, + BeamBlink, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigFile { #[serde(default = "default_follow_system_theme")] @@ -155,6 +165,8 @@ pub struct ConfigFile { #[serde(default)] pub title_bar_style: TitleBarStyle, #[serde(default)] + pub cursor_style: CursorStyle, + #[serde(default)] pub sessions: Vec, #[serde(default)] pub window_bounds: Option, @@ -249,6 +261,7 @@ impl Default for ConfigFile { ui_font_family: default_ui_font_family(), terminal_font_family: default_terminal_font_family(), title_bar_style: TitleBarStyle::default(), + cursor_style: CursorStyle::default(), sessions: Vec::new(), window_bounds: None, workspace_panels: None, @@ -603,6 +616,14 @@ impl ConfigStore { self.cache.title_bar_style = style; } + pub fn cursor_style(&self) -> CursorStyle { + self.cache.cursor_style + } + + pub fn set_cursor_style(&mut self, style: CursorStyle) { + self.cache.cursor_style = style; + } + pub fn show_hidden_files(&self) -> bool { self.cache.show_hidden_files } diff --git a/src/session/mod.rs b/src/session/mod.rs index f249a39..87d058f 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -297,6 +297,19 @@ impl Ashell { cx.notify(); } + pub(crate) fn change_cursor_style( + &mut self, + style: crate::session::config::CursorStyle, + cx: &mut Context, + ) { + self.cursor_style = style; + self.config.set_cursor_style(style); + if let Err(err) = self.config.save() { + tracing::warn!("failed to save cursor style: {err:#}"); + } + cx.notify(); + } + pub(crate) fn reset_layout(&mut self, _window: &mut Window, cx: &mut Context) { self.config.set_layout_state(None, None, None); let _ = self.config.save(); diff --git a/src/terminal/element.rs b/src/terminal/element.rs index 8930bdc..dfbc3c2 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -464,11 +464,35 @@ impl TerminalElement { } fn cursor_layout(&self, cx: &App) -> Option { - self.snapshot.cursor.map(|cursor| CursorLayout { - row: cursor.row, - col: cursor.col, - shape: cursor.shape, - color: cx.theme().primary, + use crate::session::config::CursorStyle; + let cursor_style = self.view.read(cx).cursor_style; + let show_cursor = match cursor_style { + CursorStyle::Blink | CursorStyle::BeamBlink => { + if let Ok(duration) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + (duration.as_millis() / 600) % 2 == 0 + } else { + true + } + } + _ => true, + }; + + self.snapshot.cursor.map(|cursor| { + let mut shape = match cursor_style { + CursorStyle::Default => cursor.shape, + CursorStyle::Blink => CursorShape::Block, + CursorStyle::Beam => CursorShape::Beam, + CursorStyle::BeamBlink => CursorShape::Beam, + }; + if !show_cursor { + shape = CursorShape::Hidden; + } + CursorLayout { + row: cursor.row, + col: cursor.col, + shape, + color: cx.theme().primary, + } }) } }