From 0738ce3b6f70d7848889218f41239f396b523b4f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 25 Jun 2026 10:59:01 +0300 Subject: [PATCH] config: default cursor style/blink and visual bell Signed-off-by: NotAShelf Change-Id: Ibd512084374fe4723ee267a916187af56a6a6964 --- src/config.rs | 20 ++++++++++++++++++ src/theme.rs | 7 +++++++ src/vt.rs | 9 +++++++++ src/wayland.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 726c118..74095d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,27 @@ use serde::Deserialize; pub struct Config { pub main: Main, pub colors: Colors, + pub cursor: Cursor, pub scrollback: Scrollback, + pub bell: Bell, +} + +/// `[cursor]`: the default cursor presentation (DECSCUSR may override at runtime). +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct Cursor { + /// `block`, `beam`/`bar`, or `underline`. + pub style: Option, + /// Whether the cursor blinks by default. + pub blink: bool, +} + +/// `[bell]`: what happens on `BEL` (0x07). +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct Bell { + /// Briefly flash the screen. + pub visual: bool, } /// `[colors]`: foreground/background, the 16 base palette entries, and accents. diff --git a/src/theme.rs b/src/theme.rs index ac627ee..bdf6069 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -138,6 +138,13 @@ impl Theme { pub fn reset_bg(&mut self) { self.bg = self.default_bg; } + + /// A copy with foreground and background swapped, for the visual bell flash. + pub fn inverted(&self) -> Self { + let mut t = self.clone(); + std::mem::swap(&mut t.fg, &mut t.bg); + t + } } /// Foreground/background used for `Color::Default`. diff --git a/src/vt.rs b/src/vt.rs index 685162e..a7f413a 100644 --- a/src/vt.rs +++ b/src/vt.rs @@ -100,6 +100,8 @@ pub struct Term { clipboard_ops: Vec, /// The active colour scheme (seeded from config, mutated by OSC escapes). theme: Theme, + /// Set when the child rings the bell (`BEL`); cleared by the front-end. + bell: bool, } impl Term { @@ -115,9 +117,15 @@ impl Term { xtgettcap: None, clipboard_ops: Vec::new(), theme: Theme::default(), + bell: false, } } + /// Take and clear the pending bell flag. + pub fn take_bell(&mut self) -> bool { + std::mem::take(&mut self.bell) + } + /// Drain the OSC 52 clipboard requests accumulated since the last call. pub fn take_clipboard_ops(&mut self) -> Vec { std::mem::take(&mut self.clipboard_ops) @@ -535,6 +543,7 @@ impl Perform for Term { fn execute(&mut self, byte: u8) { match byte { + 0x07 => self.bell = true, 0x08 => self.grid.backspace(), 0x09 => self.grid.tab(), 0x0A..=0x0C => self.grid.line_feed(), diff --git a/src/wayland.rs b/src/wayland.rs index f305373..fa31dea 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -113,6 +113,9 @@ const SYNC_TIMEOUT_MS: u64 = 150; /// Interval between autoscroll steps while a drag selection runs off an edge. const AUTOSCROLL_MS: u64 = 40; +/// How long the visual bell inverts the screen. +const FLASH_MS: u64 = 80; + /// What determines one rendered row's pixels: its cells, the cursor on it, the /// selection span over it, and the blink phase. Two equal `RowSnap`s render /// identically, so a buffer holding an equal snapshot needs no repaint. @@ -255,6 +258,8 @@ pub fn run(config: Config) -> anyhow::Result { buf_dims: (0, 0), blink_on: true, sync_timeout: None, + flashing: false, + flash_timer: None, searching: false, focused: true, exit: false, @@ -382,6 +387,10 @@ struct App { blink_on: bool, /// Armed while synchronized output holds the screen, to force it open. sync_timeout: Option, + /// The visual bell is inverting the screen. + flashing: bool, + /// Timer that ends the visual-bell flash. + flash_timer: Option, /// Whether incremental search mode is active (the query lives in the grid). searching: bool, /// Whether the toplevel currently has keyboard focus (drives the cursor). @@ -454,6 +463,10 @@ impl App { let grid = term.grid_mut(); grid.set_word_delimiters(self.config.main.word_delimiters.clone()); grid.set_scrollback_cap(self.config.scrollback.lines); + if let Some(shape) = cursor_shape_from(self.config.cursor.style.as_deref()) { + grid.set_cursor_shape(shape); + } + grid.set_cursor_blink(self.config.cursor.blink); self.session = Some(Session { pty, term }); } @@ -982,13 +995,38 @@ impl App { self.window .set_title(self.title.clone().unwrap_or_default()); } + let rang = session.term.take_bell(); let ops = session.term.take_clipboard_ops(); if !ops.is_empty() { self.handle_clipboard_ops(ops); } + if rang && self.config.bell.visual { + self.start_flash(); + } self.needs_draw = true; } + /// Begin a visual-bell flash: invert the screen for a moment. Clearing the + /// buffer ring forces a full repaint with the inverted theme. + fn start_flash(&mut self) { + self.flashing = true; + self.frames.clear(); + self.needs_draw = true; + if self.flash_timer.is_none() { + let timer = Timer::from_duration(Duration::from_millis(FLASH_MS)); + self.flash_timer = self + .loop_handle + .insert_source(timer, |_, _, app: &mut App| { + app.flashing = false; + app.frames.clear(); + app.needs_draw = true; + app.flash_timer = None; + TimeoutAction::Drop + }) + .ok(); + } + } + /// Recompute the grid size for the current window and tell the grid and the /// PTY about it if it changed. fn resize_grid(&mut self) { @@ -1084,7 +1122,9 @@ impl App { return; }; let grid = session.term.grid(); - let theme = session.term.theme(); + // The visual bell inverts fg/bg for the duration of the flash. + let flashed = self.flashing.then(|| session.term.theme().inverted()); + let theme = flashed.as_ref().unwrap_or(session.term.theme()); let rows = grid.rows(); let mut cur: Vec = (0..rows) .map(|y| row_snap(grid, y, focused, blink_on)) @@ -1440,6 +1480,19 @@ impl KeyboardHandler for App { } } +/// Parse a configured cursor-style name into a [`CursorShape`]. +fn cursor_shape_from(style: Option<&str>) -> Option { + match style? { + "block" => Some(CursorShape::Block), + "beam" | "bar" => Some(CursorShape::Beam), + "underline" => Some(CursorShape::Underline), + other => { + tracing::warn!("unknown cursor style {other:?}"); + None + } + } +} + /// Map a Wayland button code to the terminal mouse base code, if reportable. fn button_code(button: u32) -> Option { match button {