brewery
notashelf /
0738ce3b6f70d7848889218f41239f396b523b4f

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git

Commit 0738ce3b6f70

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-26 07:21 UTC

unverified4 files changed+90-1
config: default cursor style/blink and visual bell

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibd512084374fe4723ee267a916187af56a6a6964
diff --git a/src/config.rs b/src/config.rsindex 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<String>,+    /// 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.rsindex 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.rsindex 685162e..a7f413a 100644--- a/src/vt.rs+++ b/src/vt.rs@@ -100,6 +100,8 @@ pub struct Term {     clipboard_ops: Vec<ClipboardOp>,     /// 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<ClipboardOp> {         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.rsindex 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<ExitCode> {         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<RegistrationToken>,+    /// The visual bell is inverting the screen.+    flashing: bool,+    /// Timer that ends the visual-bell flash.+    flash_timer: Option<RegistrationToken>,     /// 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<RowSnap> = (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<CursorShape> {+    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<u8> {     match button {