feat: support custom terminal cursor style and blinking setting

This commit is contained in:
TomZz
2026-06-25 02:25:32 +08:00
parent 3c6b9e3b46
commit 042834c7d5
7 changed files with 139 additions and 6 deletions
+5
View File
@@ -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"
+5
View File
@@ -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: "侧边栏"
+52
View File
@@ -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()
+14 -1
View File
@@ -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<String>,
pub(crate) active_dialog: Option<DialogKind>,
@@ -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<Self>) {
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 {
+21
View File
@@ -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<Session>,
#[serde(default)]
pub window_bounds: Option<SavedWindowBounds>,
@@ -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
}
+13
View File
@@ -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>,
) {
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>) {
self.config.set_layout_state(None, None, None);
let _ = self.config.save();
+29 -5
View File
@@ -464,11 +464,35 @@ impl TerminalElement {
}
fn cursor_layout(&self, cx: &App) -> Option<CursorLayout> {
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,
}
})
}
}