mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
Merge pull request #101 from ayamir/fix/terminal-cursor-shape
fix(terminal): respect app cursor shape
This commit is contained in:
+31
-9
@@ -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::<Config>().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 {
|
||||
|
||||
+72
-5
@@ -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<Stream> {
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
+58
-16
@@ -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<gpui::FontFeatures>,
|
||||
/// 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::<Config>` 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>,
|
||||
) -> Self {
|
||||
// Font size from config (borrow ends before the mutable theme apply).
|
||||
let font_size = cx.global::<Config>().font_size;
|
||||
let line_height = cx.global::<Config>().line_height;
|
||||
let font_family = cx.global::<Config>().font_family.clone();
|
||||
let font_family_bold = cx.global::<Config>().font_family_bold.clone();
|
||||
let font_family_italic = cx.global::<Config>().font_family_italic.clone();
|
||||
let font_features = cx.global::<Config>().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::<Config>();
|
||||
(
|
||||
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>,
|
||||
) {
|
||||
self.update_config(cx, |cfg| cfg.cursor_style = style);
|
||||
fn apply_terminal_config_to_panes(&self, config: &Config, cx: &mut Context<Self>) {
|
||||
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>) {
|
||||
self.update_config(cx, |cfg| cfg.cursor_style = style);
|
||||
let cfg = cx.global::<Config>().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::<Config>().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<Self>) {
|
||||
@@ -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<Self>) {
|
||||
let config = cx.global::<Config>().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::<Config>();
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user