Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Cursor blinking presentation

## Status
Implemented.

## Context
The existing cursor timer only toggled visibility. Smooth fading, breathing and
vertical expansion need presentation frames without changing terminal geometry.

## Evidence
[`TerminalView`](../../../../../nebula_app/src/gpui_shell/terminal/view.rs) already owns an epoch-cancelled cursor timer, focus/IME subscriptions
and input-triggered restarts. `cursor_painter.rs` supplies the same cursor bounds
to ordinary rendering, glyph inversion and the optional shader effect frame.

## Decision
`nebula_settings::CursorBlinkStyle` owns `blink`, `smooth`, `phase`, `expand`, and
`solid` under `cursor_blink_style`. An explicit valid named style wins over the
legacy boolean. Missing/invalid styles honor `cursor_blink=0` as `solid`; otherwise
the default is `blink`. The effective boolean remains available to old consumers.
Reset removes both preferences.

The GPUI presentation adapter samples opacity/vertical scale over a 1500ms cycle.
`blink` retains the 750ms binary timer; animated modes use 16ms ticks. Typing,
style changes and focus changes restart visibly. Epoch cancellation remains owned
by each terminal entity. Hidden output, unfocused windows/panes, IME preedit and
VT-hidden/steady cursors stop scheduling. `solid` suppresses even VT requests to
blink. Reduced motion substitutes binary blinking for the three animated modes.

Painting scales the cursor about its vertical center, fades glyph inversion with
the block, and feeds the same rectangle/color into shader cursor uniforms. The
terminal snapshot, input/IME anchor, grid and movement trajectory are unchanged.

## Rejected alternatives
A global animation timer would keep hidden panes alive. Animating the terminal
cursor coordinates would move input/IME anchors. Treating `solid` as an animation
with an always-visible frame would leave unnecessary wakeups.

## Consequences
At most the visible focused pane runs a smooth blink timer. Steady cursors and
applications that hide the VT cursor retain their protocol semantics.

## Validation
[Preference compatibility/round trips/reset](../../../../../nebula_settings/src/cursor_blink.rs); [waveform](../../../../../nebula_app/src/gpui_shell/terminal/cursor_blink.rs) and shape geometry tests;
rendered settings choices, reopen/cancel and existing focus/IME/motion regressions;
hot application preserves the session and logical cursor, including VT requests.

## Supersedes
None.

## Revisit when
Blinks need user-controlled timing or the rendering engine exposes a shared
animation clock that preserves these lifecycle gates.
8 changes: 8 additions & 0 deletions nebula_app/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Cursor-Blinkstil",
"blink": "Blinken",
"smooth": "Sanft",
"phase": "Pulsieren",
"expand": "Ausdehnen",
"solid": "Statisch"
},
"sidebar": {
"application": "Anwendung",
"appearance": "Darstellung",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@
"refresh": "Refresh system environment for new terminals",
"refresh_description": "Read current Windows environment settings when opening a terminal. Turn off to inherit the environment used to launch Pebrel, including temporary PATH entries. Open terminals are unchanged."
},
"cursor_blink": {
"title": "Cursor blinking style",
"blink": "Blink",
"smooth": "Smooth",
"phase": "Phase",
"expand": "Expand",
"solid": "Solid"
},
"cursor_motion": {
"title": "Smooth cursor",
"description": "Animate terminal cursor movement; large jumps remain instant.",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Estilo de parpadeo del cursor",
"blink": "Parpadeo",
"smooth": "Suave",
"phase": "Pulsación",
"expand": "Expansión",
"solid": "Fijo"
},
"sidebar": {
"application": "Aplicación",
"appearance": "Apariencia",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Style de clignotement du curseur",
"blink": "Clignotement",
"smooth": "Fondu",
"phase": "Pulsation",
"expand": "Expansion",
"solid": "Fixe"
},
"sidebar": {
"application": "Application",
"appearance": "Apparence",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Stile di lampeggio del cursore",
"blink": "Lampeggio",
"smooth": "Dissolvenza",
"phase": "Pulsazione",
"expand": "Espansione",
"solid": "Fisso"
},
"sidebar": {
"application": "Applicazione",
"appearance": "Aspetto",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "カーソルの点滅スタイル",
"blink": "点滅",
"smooth": "フェード",
"phase": "呼吸",
"expand": "伸縮",
"solid": "常時表示"
},
"completion": {
"mode": "補完モード",
"cycle": "補完モードを切り替え",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "커서 깜박임 스타일",
"blink": "깜박임",
"smooth": "페이드",
"phase": "호흡",
"expand": "확장",
"solid": "항상 표시"
},
"sidebar": {
"application": "애플리케이션",
"appearance": "모양",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Estilo de intermitência do cursor",
"blink": "Piscar",
"smooth": "Suave",
"phase": "Pulsação",
"expand": "Expansão",
"solid": "Fixo"
},
"sidebar": {
"application": "Aplicativo",
"appearance": "Aparência",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "Стиль мигания курсора",
"blink": "Мигание",
"smooth": "Плавное затухание",
"phase": "Пульсация",
"expand": "Расширение",
"solid": "Постоянный"
},
"sidebar": {
"application": "Приложение",
"appearance": "Внешний вид",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@
"refresh": "新建终端时刷新系统环境变量",
"refresh_description": "打开终端时读取 Windows 当前环境设置。关闭后继承启动 Pebrel 时的环境,包括临时添加的 PATH。已打开的终端不受影响。"
},
"cursor_blink": {
"title": "光标闪烁样式",
"blink": "闪烁",
"smooth": "平滑",
"phase": "呼吸",
"expand": "伸缩",
"solid": "常亮"
},
"cursor_motion": {
"title": "平滑光标",
"description": "让终端光标平滑移动,大范围跳转仍立即定位。",
Expand Down
8 changes: 8 additions & 0 deletions nebula_app/i18n/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
"en_us": "English"
},
"settings": {
"cursor_blink": {
"title": "游標閃爍樣式",
"blink": "閃爍",
"smooth": "平滑",
"phase": "呼吸",
"expand": "伸縮",
"solid": "恆亮"
},
"completion": {
"mode": "補齊模式",
"cycle": "切換補齊模式",
Expand Down
2 changes: 2 additions & 0 deletions nebula_app/src/gpui_shell/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub struct Settings {
pub(crate) resolved_theme: Arc<crate::gpui_shell::theme::ResolvedTheme>,
pub cursor_shape: Option<CursorShape>,
pub cursor_blink: Option<bool>,
pub cursor_blink_style: nebula_settings::CursorBlinkStyle,
pub cursor_motion: nebula_settings::CursorMotion,
/// 选区完成即复制(旧壳 `copy_on_select` 设置)。
pub copy_on_select: bool,
Expand Down Expand Up @@ -293,6 +294,7 @@ impl Settings {
CursorShapeName::Hollow => CursorShape::HollowBlock,
}),
cursor_blink: runtime.cursor_blink,
cursor_blink_style: runtime.cursor_blink_style,
cursor_motion: runtime.cursor_motion,
copy_on_select: runtime.copy_on_select,
scrollback_lines: runtime.scrollback_lines,
Expand Down
3 changes: 2 additions & 1 deletion nebula_app/src/gpui_shell/settings_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use setting_help::{SettingHelp, help};
use std::sync::Arc;
use std::time::Duration;

use crate::gpui_shell::config::{DEFAULT_CURSOR_BLINK, effective_cursor_blink};
use crate::gpui_shell::config::effective_cursor_blink;
use crate::gpui_shell::prelude::*;
use crate::gpui_shell::widgets::{NebulaButton, settings_control_height};

Expand Down Expand Up @@ -798,6 +798,7 @@ impl SettingsPane {
"accept" => pick!(accept),
"completion_style" => pick!(completion_style),
"tabs_position" => pick!(tabs_position),
"cursor_blink_style" => pick!(cursor_blink_style),
"tab_reveal" => pick!(tab_reveal),
"density" => pick!(density),
"new_tab_position" => pick!(new_tab_position),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ impl SettingsPane {
help("cursor_shape", language),
cx,
))
.child(self.switch_row(
"cursor_blink",
language.pick("光标闪烁", "Blink cursor"),
help("cursor_blink", language),
self.runtime.cursor_blink.unwrap_or(DEFAULT_CURSOR_BLINK),
.child(self.select_row(
"cursor_blink_style",
language.text(crate::i18n::Message::SettingsCursorBlinkTitle),
"",
cx,
));
let interface = self
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use super::*;

#[gpui::test]
fn cursor_blink_language_changes_keep_the_selected_style(cx: &mut TestAppContext) {
use crate::gpui_shell::config::Settings;
use crate::i18n::{Message, UiLanguage};
let _lock = lock_theme_studio();
let _bytes = SettingsBytesGuard::capture();
std::fs::create_dir_all(nebula_settings::settings_dir()).unwrap();
std::fs::write(nebula_settings::settings_path(), TEST_SETTINGS).unwrap();
let (pane, mut window) = open_settings(cx);
pane.update(&mut window, |pane, cx| {
pane.active_section = 1;
cx.notify();
});
window.simulate_resize(size(px(1200.0), px(2400.0)));
draw(&mut window);
click("settings-select-cursor_blink_style", &mut window);
press("down", &mut window);
press("enter", &mut window);
for language in UiLanguage::ALL {
pane.update(&mut window, |pane, cx| {
pane.persist(&[("language", language.code().to_owned())], cx);
});
pane.update_in(&mut window, |pane, window, cx| {
pane.refresh_localized_controls(window, cx);
});
draw(&mut window);
click("settings-select-cursor_blink_style", &mut window);
press("enter", &mut window);
assert_eq!(
RuntimeSettings::load().cursor_blink_style,
nebula_settings::CursorBlinkStyle::Smooth
);
pane.read_with(&window, |pane, cx| {
assert_eq!(cx.global::<Settings>().ui_language, *language);
let select = pane.select_of("cursor_blink_style").unwrap().read(cx);
assert_eq!(select.selected_index(cx).unwrap().row, 1);
assert_eq!(
select.selected_value().unwrap().as_ref(),
language.text(Message::SettingsCursorBlinkSmooth)
);
});
}
}

#[gpui::test]
fn cursor_blink_style_persists_keyboard_choices_reopens_and_cancels(cx: &mut TestAppContext) {
use crate::gpui_shell::config::Settings;
let _lock = lock_theme_studio();
let _bytes = SettingsBytesGuard::capture();
std::fs::create_dir_all(nebula_settings::settings_dir()).unwrap();
std::fs::write(nebula_settings::settings_path(), TEST_SETTINGS).unwrap();
let (pane, mut window) = open_settings(cx);
pane.update(&mut window, |pane, cx| {
pane.active_section = 1;
cx.notify();
});
window.simulate_resize(size(px(1200.0), px(2400.0)));
let values = nebula_settings::CursorBlinkStyle::VALUES;
for (row, &value) in values.iter().enumerate().skip(1) {
draw(&mut window);
click("settings-select-cursor_blink_style", &mut window);
press("down", &mut window);
press("enter", &mut window);
assert_eq!(RuntimeSettings::load().cursor_blink_style.settings_value(), value);
window.read(|cx| {
let settings = cx.global::<Settings>();
assert_eq!(settings.cursor_blink_style.settings_value(), value);
if value == "solid" {
assert!(!settings.term_config().default_cursor_style.blinking);
}
});
let reopened = window.update(|window, cx| cx.new(|cx| SettingsPane::new(window, cx)));
reopened.read_with(&mut window, |pane, cx| {
assert_eq!(
pane.select_of("cursor_blink_style")
.unwrap()
.read(cx)
.selected_index(cx)
.unwrap()
.row,
row
);
});
}
click("settings-select-cursor_blink_style", &mut window);
press("up", &mut window);
press("escape", &mut window);
pane.read_with(&mut window, |pane, _| {
assert_eq!(
pane.runtime.cursor_blink_style.settings_value(),
*values.last().unwrap(),
"Escape cancels"
);
});
}
Loading
Loading