brewery
notashelf /
2d319b7e7366d88340f4aea106d121fff60450f1

beer

public

A terminal worth pouring time into

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

Commit 2d319b7e7366

tarball

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

unverified3 files changed+101-28
render: inner terminal padding (pad-x/pad-y)

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I190f63ca86a8cf976e4d018df73897ab6a6a6964
diff --git a/src/config.rs b/src/config.rsindex cffc726..726c118 100644--- a/src/config.rs+++ b/src/config.rs@@ -52,6 +52,9 @@ pub struct Main {     /// Initial size in character cells.     pub initial_cols: u16,     pub initial_rows: u16,+    /// Inner padding in pixels between the window edge and the cell grid.+    pub pad_x: u32,+    pub pad_y: u32,     /// Characters that break a word for double-click selection. Empty/unset     /// keeps the built-in default.     pub word_delimiters: Option<String>,@@ -65,6 +68,8 @@ impl Default for Main {             term: "beer".to_string(),             initial_cols: 80,             initial_rows: 24,+            pad_x: 2,+            pad_y: 2,             word_delimiters: None,         }     }diff --git a/src/render.rs b/src/render.rsindex 4f5229e..89e2f77 100644--- a/src/render.rs+++ b/src/render.rs@@ -99,17 +99,36 @@ pub struct Frame<'a> { #[derive(Debug)] pub struct Renderer {     fonts: Fonts,+    /// Inner padding `(x, y)` in pixels between the window edge and the grid.+    pad: (i32, i32), }  impl Renderer {     pub fn new(fonts: Fonts) -> Self {-        Self { fonts }+        Self { fonts, pad: (0, 0) }     }      pub fn metrics(&self) -> CellMetrics {         self.fonts.metrics()     } +    pub fn set_padding(&mut self, pad_x: u32, pad_y: u32) {+        self.pad = (pad_x as i32, pad_y as i32);+    }++    /// Fill the whole buffer (including the padding margins) with the background+    /// colour. Called once per fresh shm buffer; per-row repaints then leave the+    /// margins untouched.+    pub fn clear(&self, pixels: &mut [u8], dims: (usize, usize), theme: &Theme) {+        let (width, height) = dims;+        let mut canvas = Canvas {+            pixels,+            width,+            height,+        };+        canvas.fill_rect_a(0, 0, width as u32, height as u32, theme.bg, theme.alpha);+    }+     /// Repaint a single grid row `y` into `pixels` (BGRA, `width`×`height` px):     /// clear the row band, fill backgrounds (and selection), draw glyphs and     /// decorations, then the cursor if it sits on this row. `blink_on` is the@@ -132,8 +151,9 @@ impl Renderer {             height,         };         let m = self.fonts.metrics();+        let (pad_x, pad_y) = self.pad;         let cols = grid.cols();-        let row_top = y as i32 * m.height as i32;+        let row_top = pad_y + y as i32 * m.height as i32;         canvas.fill_rect_a(0, row_top, width as u32, m.height, theme.bg, theme.alpha);          // Rows come through the scrollback viewport and may be shorter than@@ -156,7 +176,13 @@ impl Renderer {                 None => cell_colors(cell, theme).1,             };             if bg != theme.bg {-                canvas.fill_rect(x as i32 * m.width as i32, row_top, m.width, m.height, bg);+                canvas.fill_rect(+                    pad_x + x as i32 * m.width as i32,+                    row_top,+                    m.width,+                    m.height,+                    bg,+                );             }         } @@ -168,7 +194,7 @@ impl Renderer {                 continue;             }             let (fg, _) = cell_colors(cell, theme);-            let origin_x = x as i32 * m.width as i32;+            let origin_x = pad_x + x as i32 * m.width as i32;             if cell.c != ' ' {                 self.draw_glyph(&mut canvas, cell.c, cell_style(cell), origin_x, row_top, fg);             }@@ -189,6 +215,7 @@ impl Renderer {         pixels: &mut [u8],         dims: (usize, usize),         theme: &Theme,+        row: usize,         text: &str,     ) {         let (width, height) = dims;@@ -198,14 +225,14 @@ impl Renderer {             height,         };         let m = self.fonts.metrics();-        let rows = (height / m.height as usize).max(1);-        let row_top = (rows - 1) as i32 * m.height as i32;+        let (pad_x, pad_y) = self.pad;+        let row_top = pad_y + row as i32 * m.height as i32;         canvas.fill_rect(0, row_top, width as u32, m.height, theme.search_bar_bg);         let style = Style {             bold: false,             italic: false,         };-        let mut x = 0i32;+        let mut x = pad_x;         for c in text.chars() {             if x as usize + m.width as usize > width {                 break;@@ -232,8 +259,8 @@ impl Renderer {             return;         }         let (cx, cy) = grid.cursor();-        let x0 = cx as i32 * m.width as i32;-        let top = cy as i32 * m.height as i32;+        let x0 = self.pad.0 + cx as i32 * m.width as i32;+        let top = self.pad.1 + cy as i32 * m.height as i32;         // OSC 12 cursor colour wins, then the configured cursor colour, then fg.         let color = grid             .cursor_color()diff --git a/src/wayland.rs b/src/wayland.rsindex e508dd5..f305373 100644--- a/src/wayland.rs+++ b/src/wayland.rs@@ -196,13 +196,14 @@ pub fn run(config: Config) -> anyhow::Result<ExitCode> {     window.commit();      let fonts = Fonts::new(&config.main.font, config.main.font_size).context("load font")?;-    let renderer = Renderer::new(fonts);+    let mut renderer = Renderer::new(fonts);+    renderer.set_padding(config.main.pad_x, config.main.pad_y); -    // Start at the configured cell geometry; the compositor may override it on-    // the first configure.+    // Start at the configured cell geometry plus padding; the compositor may+    // override it on the first configure.     let m = renderer.metrics();-    let width = (u32::from(config.main.initial_cols) * m.width).max(1);-    let height = (u32::from(config.main.initial_rows) * m.height).max(1);+    let width = (u32::from(config.main.initial_cols) * m.width + 2 * config.main.pad_x).max(1);+    let height = (u32::from(config.main.initial_rows) * m.height + 2 * config.main.pad_y).max(1);     let pool = SlotPool::new(         (width * height * 4).max(DEFAULT_W * DEFAULT_H) as usize,         &shm,@@ -285,10 +286,16 @@ pub fn run(config: Config) -> anyhow::Result<ExitCode> {     Ok(app.exit_code) } -/// Columns and rows that fit a `width`×`height` px window at `metrics`.-fn grid_size(metrics: crate::font::CellMetrics, width: u32, height: u32) -> (u16, u16) {-    let cols = (width / metrics.width).max(1);-    let rows = (height / metrics.height).max(1);+/// Columns and rows that fit a `width`×`height` px window at `metrics`, after+/// reserving `2 * pad` pixels of inner padding on each axis.+fn grid_size(+    metrics: crate::font::CellMetrics,+    width: u32,+    height: u32,+    pad: (u32, u32),+) -> (u16, u16) {+    let cols = (width.saturating_sub(2 * pad.0) / metrics.width).max(1);+    let rows = (height.saturating_sub(2 * pad.1) / metrics.height).max(1);     (cols as u16, rows as u16) } @@ -387,7 +394,12 @@ struct App { impl App {     /// Spawn the shell at the current window size and start reading its output.     fn spawn_session(&mut self) {-        let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);+        let (cols, rows) = grid_size(+            self.renderer.metrics(),+            self.width,+            self.height,+            (self.config.main.pad_x, self.config.main.pad_y),+        );         let pty = match Pty::spawn(cols, rows, &self.config.main.term) {             Ok(pty) => pty,             Err(err) => {@@ -557,13 +569,24 @@ impl App {         self.needs_draw = true;     } +    /// Inner padding `(x, y)` in pixels.+    fn padding(&self) -> (f64, f64) {+        (+            f64::from(self.config.main.pad_x),+            f64::from(self.config.main.pad_y),+        )+    }+     /// Map window pixel coordinates to an absolute `(row, col)` grid point.     fn cell_at(&self, px: f64, py: f64) -> Option<(usize, usize)> {         let session = self.session.as_ref()?;         let m = self.renderer.metrics();+        let (pad_x, pad_y) = self.padding();         let grid = session.term.grid();-        let col = (px.max(0.0) as usize / m.width as usize).min(grid.cols().saturating_sub(1));-        let vrow = (py.max(0.0) as usize / m.height as usize).min(grid.rows().saturating_sub(1));+        let col =+            ((px - pad_x).max(0.0) as usize / m.width as usize).min(grid.cols().saturating_sub(1));+        let vrow =+            ((py - pad_y).max(0.0) as usize / m.height as usize).min(grid.rows().saturating_sub(1));         Some((grid.view_to_abs(vrow), col))     } @@ -683,10 +706,11 @@ impl App {     fn report_screen_cell(&self) -> Option<(usize, usize)> {         let session = self.session.as_ref()?;         let m = self.renderer.metrics();+        let (pad_x, pad_y) = self.padding();         let grid = session.term.grid();-        let col = (self.pointer_pos.0.max(0.0) as usize / m.width as usize)+        let col = ((self.pointer_pos.0 - pad_x).max(0.0) as usize / m.width as usize)             .min(grid.cols().saturating_sub(1));-        let row = (self.pointer_pos.1.max(0.0) as usize / m.height as usize)+        let row = ((self.pointer_pos.1 - pad_y).max(0.0) as usize / m.height as usize)             .min(grid.rows().saturating_sub(1));         Some((col, row))     }@@ -968,7 +992,12 @@ impl App {     /// 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) {-        let (cols, rows) = grid_size(self.renderer.metrics(), self.width, self.height);+        let (cols, rows) = grid_size(+            self.renderer.metrics(),+            self.width,+            self.height,+            (self.config.main.pad_x, self.config.main.pad_y),+        );         let Some(session) = self.session.as_mut() else {             return;         };@@ -1122,6 +1151,10 @@ impl App {             return;         } +        // A buffer used for the first time has uninitialized margins; paint the+        // whole thing (background + padding) once, then damage it in full below.+        let fresh = self.frames[idx].rows.is_empty();+        let pad_y = self.config.main.pad_y as i32;         let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else {             return;         };@@ -1131,6 +1164,9 @@ impl App {             focused,             blink_on,         };+        if fresh {+            self.renderer.clear(canvas, dims, theme);+        }         for &y in &dirty {             self.renderer.render_row(canvas, dims, grid, &frame, y);         }@@ -1138,7 +1174,8 @@ impl App {         if let Some(text) = &bar_text             && dirty.contains(&(rows - 1))         {-            self.renderer.render_search_bar(canvas, dims, theme, text);+            self.renderer+                .render_search_bar(canvas, dims, theme, rows - 1, text);         }         self.frames[idx].rows = cur; @@ -1147,9 +1184,13 @@ impl App {             tracing::error!("attach buffer: {err}");             return;         }-        for &y in &dirty {-            let top = y as i32 * m.height as i32;-            surface.damage_buffer(0, top, w as i32, m.height as i32);+        if fresh {+            surface.damage_buffer(0, 0, w as i32, h as i32);+        } else {+            for &y in &dirty {+                let top = pad_y + y as i32 * m.height as i32;+                surface.damage_buffer(0, top, w as i32, m.height as i32);+            }         }         surface.frame(&self.qh, surface.clone());         self.window.commit();