config: default cursor style/blink and visual bell

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibd512084374fe4723ee267a916187af56a6a6964
This commit is contained in:
raf 2026-06-25 10:59:01 +03:00
commit 0738ce3b6f
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
4 changed files with 90 additions and 1 deletions

View file

@ -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.

View file

@ -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`.

View file

@ -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(),

View file

@ -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 {