clone
ssh://git@git.notashelf.dev:33/notashelf/beer.gitCommit 0738ce3b6f70
tarballunverified4 files changed+90-1
@@ -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.@@ -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`.@@ -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(),@@ -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 {