diff --git a/src/terminal/element.rs b/src/terminal/element.rs index 08cce883..78de9894 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -8,7 +8,7 @@ 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::cell::{Cell, Flags}; -use alacritty_terminal::vte::ansi::{Color as AnsiColor, NamedColor, Rgb}; +use alacritty_terminal::vte::ansi::{Color as AnsiColor, CursorShape, NamedColor, Rgb}; use gpui::{ App, BorderStyle, Bounds, ContentMask, CursorStyle, Element, ElementId, Font, FontStyle, FontWeight, GlobalElementId, Hitbox, HitboxBehavior, HitboxId, Hsla, IntoElement, LayoutId, @@ -841,7 +841,7 @@ struct GridCursor { row: usize, col: usize, hidden: bool, - /// The shape to draw (from `Config::cursor_style`). + /// The shape to draw after resolving terminal DECSCUSR/default state. style: crate::core::config::CursorStyle, } @@ -893,6 +893,14 @@ fn paint_cursor( } } +fn cursor_style_from_shape(shape: CursorShape) -> crate::core::config::CursorStyle { + match shape { + CursorShape::Beam => crate::core::config::CursorStyle::Bar, + CursorShape::Underline => crate::core::config::CursorStyle::Underline, + _ => crate::core::config::CursorStyle::Block, + } +} + /// Paint IME pre-edit (composing) text over the cursor cell, underlined so it /// reads as provisional. fn paint_marked( @@ -1046,13 +1054,8 @@ impl TerminalElement { cursor = Some(GridCursor { row: row as usize, col, - hidden: matches!( - cur.shape, - alacritty_terminal::vte::ansi::CursorShape::Hidden - ), - // Shape is a user preference, not app-driven: the config style - // wins regardless of what DECSCUSR requested. - style: cx.global::().cursor_style, + hidden: matches!(cur.shape, CursorShape::Hidden), + style: cursor_style_from_shape(cur.shape), }); } } @@ -1778,6 +1781,25 @@ mod tests { assert_eq!(drag_overshoot(px(230.), bounds, px(10.)), -3.); } + #[test] + fn terminal_cursor_shape_maps_to_painted_cursor_style() { + use crate::core::config::CursorStyle; + + assert_eq!(cursor_style_from_shape(CursorShape::Beam), CursorStyle::Bar); + assert_eq!( + cursor_style_from_shape(CursorShape::Underline), + CursorStyle::Underline + ); + assert_eq!( + cursor_style_from_shape(CursorShape::Block), + CursorStyle::Block + ); + assert_eq!( + cursor_style_from_shape(CursorShape::HollowBlock), + CursorStyle::Block + ); + } + // ---- segment_row ---- fn cell(c: char) -> RenderCell { diff --git a/src/terminal/remote.rs b/src/terminal/remote.rs index ad68b8ec..9e861faf 100644 --- a/src/terminal/remote.rs +++ b/src/terminal/remote.rs @@ -31,11 +31,12 @@ use std::thread::JoinHandle; use alacritty_terminal::event::{Event as AlacEvent, EventListener}; use alacritty_terminal::sync::FairMutex; use alacritty_terminal::term::{Config, Term, TermMode}; -use alacritty_terminal::vte::ansi; +use alacritty_terminal::vte::ansi::{self, CursorShape, CursorStyle}; use std::collections::VecDeque; use crate::core::cli_agent::{AgentSessionState, CLIAgent}; +use crate::core::config::CursorStyle as ConfigCursorStyle; use crate::core::osc::OscTokenizer; use crate::daemon::protocol::{ AuthPromptKind, AuthResponse, ClientMsg, DaemonMsg, KnownHostEntry, KnownHostId, @@ -302,10 +303,8 @@ impl RemoteTerminal { // Scrollback depth comes from user config (clamped in `Config::sanitize` // to alacritty's ceiling). Read fresh from disk here: a pane spawn/attach // is rare, and this runs on the daemon side too, which has no GPUI global. - let config = Config { - scrolling_history: crate::core::config::Config::load().scrollback_limit, - ..Config::default() - }; + let user_config = crate::core::config::Config::load(); + let config = terminal_config_from_user(&user_config); let term = Term::new(config, &size, proxy.clone()); let term = Arc::new(FairMutex::new(term)); @@ -363,6 +362,11 @@ impl RemoteTerminal { }) } + pub fn apply_user_config(&self, user_config: &crate::core::config::Config) { + let mut term = self.term.lock(); + term.set_options(terminal_config_from_user(user_config)); + } + /// The reader thread: decodes framed `DaemonMsg`s off the socket and applies /// each. `Snapshot`/`Output` feed the same `ansi::Processor` → `Term` path as /// the in-process backend (so a multi-MB Snapshot is one `advance` call), @@ -1461,6 +1465,26 @@ fn connect() -> anyhow::Result { }) } +fn terminal_config_from_user(user_config: &crate::core::config::Config) -> Config { + Config { + scrolling_history: user_config.scrollback_limit, + default_cursor_style: alacritty_cursor_style(user_config.cursor_style), + ..Config::default() + } +} + +fn alacritty_cursor_style(style: ConfigCursorStyle) -> CursorStyle { + let shape = match style { + ConfigCursorStyle::Block => CursorShape::Block, + ConfigCursorStyle::Bar => CursorShape::Beam, + ConfigCursorStyle::Underline => CursorShape::Underline, + }; + CursorStyle { + shape, + blinking: false, + } +} + /// Build the protocol `WinSize` from our `TermSize` + cell pixel size. fn win_size(size: TermSize, cell_w: u16, cell_h: u16) -> WinSize { WinSize { @@ -1557,6 +1581,49 @@ mod tests { assert!(term.exited_flag.load(Ordering::SeqCst)); } + #[test] + fn cursor_style_sequence_overrides_and_resets_to_user_default() { + use alacritty_terminal::vte::ansi::CursorShape; + + let (client_side, mut daemon_side) = UnixStream::pair().unwrap(); + let term = RemoteTerminal::from_stream(client_side, TermSize::new(80, 24)).unwrap(); + let mut user_config = crate::core::config::Config::default(); + user_config.cursor_style = ConfigCursorStyle::Underline; + term.apply_user_config(&user_config); + + let mut shape = term.term.lock().cursor_style().shape; + assert_eq!(shape, CursorShape::Underline); + + // DECSCUSR 6 = steady beam, the sequence nvim uses for insert mode. + DaemonMsg::Output(b"\x1b[6 q".to_vec()) + .encode(&mut daemon_side) + .unwrap(); + daemon_side.flush().unwrap(); + for _ in 0..200 { + shape = term.term.lock().cursor_style().shape; + if shape == CursorShape::Beam { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + assert_eq!(shape, CursorShape::Beam); + + // DECSCUSR 0 clears the application override, so the configured + // terminal default is visible again. + DaemonMsg::Output(b"\x1b[0 q".to_vec()) + .encode(&mut daemon_side) + .unwrap(); + daemon_side.flush().unwrap(); + for _ in 0..200 { + shape = term.term.lock().cursor_style().shape; + if shape == CursorShape::Underline { + break; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + assert_eq!(shape, CursorShape::Underline); + } + /// Native-SSH `AuthPrompt` and `SshStatus` frames must surface through the /// reader thread into the per-pane queue / phase cell the auth sheet reads. #[test] diff --git a/src/ui/app.rs b/src/ui/app.rs index 1cd4c8a5..711ada02 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -15,7 +15,9 @@ use std::rc::Rc; use std::sync::Arc; use crate::core::actions::*; -use crate::core::config::{Config, NewTabPosition, ShellConfig, TabBarPosition}; +use crate::core::config::{ + Config, CursorStyle as ConfigCursorStyle, NewTabPosition, ShellConfig, TabBarPosition, +}; use crate::core::session::{Session, SessionAxis, SessionPane, SessionTab}; use crate::core::shells::DetectedShell; use crate::core::ssh_config; @@ -262,6 +264,10 @@ pub struct Tty7App { /// Currently-applied OpenType features for terminal fonts. `None` means the /// terminal-safe default (ligatures disabled). pub(crate) font_features: Option, + /// Currently-applied terminal-emulator defaults. Tracked so hot-reload can + /// push only the alacritty-backed options that actually changed. + terminal_cursor_style: ConfigCursorStyle, + terminal_scrollback_limit: usize, /// Keeps the `observe_global::` subscription alive for the app's /// lifetime so external edits to `config.json` (swapped in by the watcher in /// `main.rs`) live-apply font size / line height / family. Never read. @@ -445,12 +451,28 @@ impl Tty7App { cx: &mut Context, ) -> Self { // Font size from config (borrow ends before the mutable theme apply). - let font_size = cx.global::().font_size; - let line_height = cx.global::().line_height; - let font_family = cx.global::().font_family.clone(); - let font_family_bold = cx.global::().font_family_bold.clone(); - let font_family_italic = cx.global::().font_family_italic.clone(); - let font_features = cx.global::().font_features.clone(); + let ( + font_size, + line_height, + font_family, + font_family_bold, + font_family_italic, + font_features, + terminal_cursor_style, + terminal_scrollback_limit, + ) = { + let cfg = cx.global::(); + ( + cfg.font_size, + cfg.line_height, + cfg.font_family.clone(), + cfg.font_family_bold.clone(), + cfg.font_family_italic.clone(), + cfg.font_features.clone(), + cfg.cursor_style, + cfg.scrollback_limit, + ) + }; let sftp_panel = crate::ui::sftp::SftpPanelState::new(window, cx); // Managed-forward add-form inputs (native-SSH panes). let mf_bind_host = cx.new(|cx| InputState::new(window, cx).default_value("127.0.0.1")); @@ -533,6 +555,8 @@ impl Tty7App { font_family_bold, font_family_italic, font_features, + terminal_cursor_style, + terminal_scrollback_limit, _config_watch: config_watch, _keystroke_watch: keystroke_watch, _activation_watch: activation_watch, @@ -1054,21 +1078,27 @@ impl Tty7App { cx.notify(); } - /// Switch the cursor shape and repaint. The element reads `cursor_style` from - /// the global each frame, so we just persist and nudge every pane to redraw. - pub(crate) fn set_cursor_style( - &mut self, - style: crate::core::config::CursorStyle, - cx: &mut Context, - ) { - self.update_config(cx, |cfg| cfg.cursor_style = style); + fn apply_terminal_config_to_panes(&self, config: &Config, cx: &mut Context) { for tab in &self.tabs { for leaf in tab.pane.leaves() { - leaf.update(cx, |_v, cx| cx.notify()); + leaf.update(cx, |v, cx| { + v.terminal.apply_user_config(config); + cx.notify(); + }); } } } + /// Switch the default cursor shape, update each pane's terminal defaults, + /// and repaint. App-requested DECSCUSR shapes still override this at runtime. + pub(crate) fn set_cursor_style(&mut self, style: ConfigCursorStyle, cx: &mut Context) { + self.update_config(cx, |cfg| cfg.cursor_style = style); + let cfg = cx.global::().clone(); + self.terminal_cursor_style = cfg.cursor_style; + self.terminal_scrollback_limit = cfg.scrollback_limit; + self.apply_terminal_config_to_panes(&cfg, cx); + } + // ── Config setters (Terminal / Window & Tabs / Cursor settings) ───────── // Each goes through `update_config` (mutate the global, persist, repaint). // Effect points read the global live (blink task, `poll_foreground`, link @@ -1371,6 +1401,10 @@ impl Tty7App { self.update_config(cx, |cfg| { cfg.scrollback_limit = lines.clamp(100, crate::core::config::MAX_SCROLLBACK) }); + let cfg = cx.global::().clone(); + self.terminal_cursor_style = cfg.cursor_style; + self.terminal_scrollback_limit = cfg.scrollback_limit; + self.apply_terminal_config_to_panes(&cfg, cx); } pub(crate) fn set_new_tab_position(&mut self, pos: NewTabPosition, cx: &mut Context) { @@ -2915,6 +2949,14 @@ impl Tty7App { /// writes it), and — because we never write the global or `save()` from here /// — closes the save → watch → reload loop that would otherwise oscillate. fn reload_from_config(&mut self, cx: &mut Context) { + let config = cx.global::().clone(); + if config.cursor_style != self.terminal_cursor_style + || config.scrollback_limit != self.terminal_scrollback_limit + { + self.terminal_cursor_style = config.cursor_style; + self.terminal_scrollback_limit = config.scrollback_limit; + self.apply_terminal_config_to_panes(&config, cx); + } let (font_size, line_height, font_family, font_features) = { let cfg = cx.global::(); (