git.lucas.co / cce-terminal
terminal emulator
git clone https://git.lucas.co/cce-terminal.git

commit9aa74b8281044f5cff2162ac18d5459ad8b2f2ec
parent89ed08fc48
authorLucas Galante <[email protected]>
date2026-08-10 19:10
KDL config: font size, scrollback, palette — live-reloaded

A `terminal { … }` section (shared config.kdl or the per-app override,
via the toolkit's merged mtime-cached config) supplies font_size
(clamped 6–72, cell metrics re-derived by the toolkit conventions),
scrollback, and a colors block: foreground/background/cursor/selection
plus the 16 ANSI slots as hex strings, parsed with the toolkit's
parse_hex_bytes. The palette becomes a first-class Palette struct
threaded through color resolution — precedence is OSC override >
configured palette > compiled default; cursor tracks a configured
foreground unless set explicitly.

Edits apply live, per the DE's edit-the-file convention: tick polls
config_files_modified and re-derives settings + grid, resizing the pty
and Term when the cell metrics change. Scrollback capacity is the one
startup-only knob (baked into the Term).

Live-verified under a scratch XDG_CONFIG_HOME: 20px golden/pink/mint
scheme applied at launch; editing the file to 11px cyan re-rendered the
running instance on its next frame, grid resize included. 14 headless
tests.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/colors.rs | 144 ++++++++++++++++++++++++++++++++++++++++++++------------
 src/main.rs   | 149 +++++++++++++++++++++++++++++++++++++++++++---------------
 2 files changed, 225 insertions(+), 68 deletions(-)

diff --git a/src/colors.rs b/src/colors.rs
index f3238a1..e9f8e1d 100644
--- a/src/colors.rs
+++ b/src/colors.rs
@@ -1,5 +1,6 @@
-//! Terminal color resolution: the default 269-entry palette (16 ANSI + 6×6×6
-//! cube + grayscale ramp + named specials) and the mapping from a cell's
+//! Terminal color resolution: the configurable palette (16 ANSI + named
+//! specials, from the `terminal.colors` KDL section) plus the computed
+//! 6×6×6 cube and grayscale ramp, and the mapping from a cell's
 //! `vte::ansi::Color` through runtime overrides (OSC 4/10/11) to concrete RGB.
 
 use alacritty_terminal::term::color::Colors;
@@ -7,6 +8,7 @@ use alacritty_terminal::vte::ansi::{Color, NamedColor, Rgb};
 
 pub const FOREGROUND: Rgb = Rgb { r: 0xd8, g: 0xd8, b: 0xde };
 pub const BACKGROUND: Rgb = Rgb { r: 0x22, g: 0x26, b: 0x2e };
+const SELECTION: Rgb = Rgb { r: 0xc8, g: 0xd0, b: 0xe0 };
 
 /// Base16 default-dark ANSI colors (alacritty's classic defaults) — muted
 /// enough to sit on the DE plate.
@@ -37,12 +39,89 @@ fn scale(rgb: Rgb, f: f32) -> Rgb {
     }
 }
 
+/// The user-configurable colors: the 16 ANSI slots plus the named specials
+/// and the selection highlight. Loaded from the `terminal.colors` KDL
+/// section; every field falls back to the compiled default.
+#[derive(Clone, Copy, PartialEq)]
+pub struct Palette {
+    pub foreground: Rgb,
+    pub background: Rgb,
+    pub cursor: Rgb,
+    pub selection: Rgb,
+    pub ansi: [Rgb; 16],
+}
+
+impl Default for Palette {
+    fn default() -> Self {
+        Palette {
+            foreground: FOREGROUND,
+            background: BACKGROUND,
+            cursor: FOREGROUND,
+            selection: SELECTION,
+            ansi: ANSI,
+        }
+    }
+}
+
+const ANSI_NAMES: [&str; 16] = [
+    "black",
+    "red",
+    "green",
+    "yellow",
+    "blue",
+    "magenta",
+    "cyan",
+    "white",
+    "bright_black",
+    "bright_red",
+    "bright_green",
+    "bright_yellow",
+    "bright_blue",
+    "bright_magenta",
+    "bright_cyan",
+    "bright_white",
+];
+
+fn config_rgb(key: &str) -> Option<Rgb> {
+    let hex = cce_ui::config::get_string(&format!("/terminal/colors/{key}"))?;
+    let [r, g, b, _] = cce_ui::color::parse_hex_bytes(&hex)?;
+    Some(Rgb { r, g, b })
+}
+
+impl Palette {
+    /// The compiled defaults with any `terminal.colors` config entries
+    /// (shared config.kdl merged with the per-app override) applied on top.
+    pub fn from_config() -> Self {
+        let mut palette = Palette::default();
+        if let Some(rgb) = config_rgb("foreground") {
+            palette.foreground = rgb;
+            // Cursor tracks the foreground unless set explicitly.
+            palette.cursor = rgb;
+        }
+        if let Some(rgb) = config_rgb("background") {
+            palette.background = rgb;
+        }
+        if let Some(rgb) = config_rgb("cursor") {
+            palette.cursor = rgb;
+        }
+        if let Some(rgb) = config_rgb("selection") {
+            palette.selection = rgb;
+        }
+        for (slot, name) in palette.ansi.iter_mut().zip(ANSI_NAMES) {
+            if let Some(rgb) = config_rgb(name) {
+                *slot = rgb;
+            }
+        }
+        palette
+    }
+}
+
 /// Default color for a palette index (0..269), matching alacritty's layout:
 /// 0–15 ANSI, 16–231 the xterm 6×6×6 cube, 232–255 the grayscale ramp, then
 /// the `NamedColor` specials (256 = Foreground …).
-pub fn default_color(index: usize) -> Rgb {
+pub fn default_color(index: usize, palette: &Palette) -> Rgb {
     match index {
-        0..=15 => ANSI[index],
+        0..=15 => palette.ansi[index],
         16..=231 => {
             let i = index - 16;
             let comp = |v: usize| if v == 0 { 0 } else { (55 + v * 40) as u8 };
@@ -52,27 +131,27 @@ pub fn default_color(index: usize) -> Rgb {
             let v = (8 + (index - 232) * 10) as u8;
             Rgb { r: v, g: v, b: v }
         }
-        i if i == NamedColor::Foreground as usize => FOREGROUND,
-        i if i == NamedColor::Background as usize => BACKGROUND,
-        i if i == NamedColor::Cursor as usize => FOREGROUND,
-        i if i == NamedColor::BrightForeground as usize => FOREGROUND,
-        i if i == NamedColor::DimForeground as usize => scale(FOREGROUND, 0.66),
+        i if i == NamedColor::Foreground as usize => palette.foreground,
+        i if i == NamedColor::Background as usize => palette.background,
+        i if i == NamedColor::Cursor as usize => palette.cursor,
+        i if i == NamedColor::BrightForeground as usize => palette.foreground,
+        i if i == NamedColor::DimForeground as usize => scale(palette.foreground, 0.66),
         // DimBlack..=DimWhite
         i if (NamedColor::DimBlack as usize..=NamedColor::DimWhite as usize).contains(&i) => {
-            scale(ANSI[i - NamedColor::DimBlack as usize], 0.66)
+            scale(palette.ansi[i - NamedColor::DimBlack as usize], 0.66)
         }
-        _ => FOREGROUND,
+        _ => palette.foreground,
     }
 }
 
 /// Palette index → RGB through the terminal's runtime overrides.
-pub fn indexed(index: usize, overrides: &Colors) -> Rgb {
-    overrides[index].unwrap_or_else(|| default_color(index))
+pub fn indexed(index: usize, overrides: &Colors, palette: &Palette) -> Rgb {
+    overrides[index].unwrap_or_else(|| default_color(index, palette))
 }
 
 /// A cell color → RGB. `dim` applies the SGR 2 treatment: named colors route
 /// to their Dim* palette slots, direct/indexed colors scale by 0.66.
-pub fn resolve(color: Color, overrides: &Colors, dim: bool) -> Rgb {
+pub fn resolve(color: Color, overrides: &Colors, dim: bool, palette: &Palette) -> Rgb {
     match color {
         Color::Spec(rgb) => {
             if dim {
@@ -83,10 +162,10 @@ pub fn resolve(color: Color, overrides: &Colors, dim: bool) -> Rgb {
         }
         Color::Named(name) => {
             let name = if dim { name.to_dim() } else { name };
-            indexed(name as usize, overrides)
+            indexed(name as usize, overrides, palette)
         }
         Color::Indexed(i) => {
-            let rgb = indexed(i as usize, overrides);
+            let rgb = indexed(i as usize, overrides, palette);
             if dim {
                 scale(rgb, 0.66)
             } else {
@@ -102,33 +181,36 @@ mod tests {
 
     #[test]
     fn cube_and_gray_ramp() {
+        let p = Palette::default();
         // 16 = cube (0,0,0); 231 = cube (255,255,255); 244 = mid gray.
-        assert_eq!(default_color(16), Rgb { r: 0, g: 0, b: 0 });
-        assert_eq!(default_color(231), Rgb { r: 255, g: 255, b: 255 });
-        assert_eq!(default_color(196), Rgb { r: 255, g: 0, b: 0 }); // cube (5,0,0)
-        assert_eq!(default_color(244), Rgb { r: 128, g: 128, b: 128 });
+        assert_eq!(default_color(16, &p), Rgb { r: 0, g: 0, b: 0 });
+        assert_eq!(default_color(231, &p), Rgb { r: 255, g: 255, b: 255 });
+        assert_eq!(default_color(196, &p), Rgb { r: 255, g: 0, b: 0 }); // cube (5,0,0)
+        assert_eq!(default_color(244, &p), Rgb { r: 128, g: 128, b: 128 });
     }
 
     #[test]
     fn named_specials() {
-        assert_eq!(default_color(NamedColor::Foreground as usize), FOREGROUND);
-        assert_eq!(default_color(NamedColor::Background as usize), BACKGROUND);
-        assert_eq!(
-            default_color(NamedColor::DimRed as usize),
-            scale(ANSI[1], 0.66)
-        );
+        let p = Palette::default();
+        assert_eq!(default_color(NamedColor::Foreground as usize, &p), FOREGROUND);
+        assert_eq!(default_color(NamedColor::Background as usize, &p), BACKGROUND);
+        assert_eq!(default_color(NamedColor::DimRed as usize, &p), scale(ANSI[1], 0.66));
     }
 
     #[test]
-    fn resolve_respects_overrides() {
+    fn resolve_respects_overrides_and_palette() {
         let mut overrides = Colors::default();
         let custom = Rgb { r: 1, g: 2, b: 3 };
         overrides[NamedColor::Red as usize] = Some(custom);
-        assert_eq!(resolve(Color::Named(NamedColor::Red), &overrides, false), custom);
+        let mut p = Palette::default();
+        p.ansi[2] = Rgb { r: 9, g: 9, b: 9 }; // configured green
+        // OSC override wins over the configured palette; palette wins over
+        // the compiled default.
+        assert_eq!(resolve(Color::Named(NamedColor::Red), &overrides, false, &p), custom);
         assert_eq!(
-            resolve(Color::Named(NamedColor::Green), &overrides, false),
-            ANSI[2]
+            resolve(Color::Named(NamedColor::Green), &overrides, false, &p),
+            Rgb { r: 9, g: 9, b: 9 }
         );
-        assert_eq!(resolve(Color::Indexed(1), &overrides, false), custom);
+        assert_eq!(resolve(Color::Indexed(1), &overrides, false, &p), custom);
     }
 }
diff --git a/src/main.rs b/src/main.rs
index 741776a..18018dc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,8 +19,16 @@
 //! reports at cell coordinates, plus focus in/out (1004); holding Shift
 //! bypasses reporting so selection stays reachable, per convention.
 //!
+//! Config (KDL, live-reloaded on file change): a `terminal { … }` section in
+//! the shared `~/.config/cce/config.kdl` or the per-app
+//! `~/.config/cce/cce-terminal/config.kdl` (app file wins) with `font_size`,
+//! `scrollback`, and a `colors { … }` block naming `foreground`, `background`,
+//! `cursor`, `selection`, and the 16 ANSI slots (`black` … `bright_white`)
+//! as hex strings. The font family comes from fontconfig's `terminal` alias,
+//! like the rest of the DE's font routing.
+//!
 //! Not yet: measured cell metrics (0.60 em / 1.2 em estimates — exact for
-//! Berkeley Mono in practice), KDL config for font size and palette.
+//! Berkeley Mono in practice).
 
 mod clip;
 mod colors;
@@ -44,21 +52,42 @@ use cce_ui::scene::paint::{DisplayList, PaintCtx, TextAttrs};
 use cce_ui::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey};
 use wayland_client::QueueHandle;
 
-const FONT_SIZE: f32 = 14.0;
-/// Toolkit conventions: ceil(font_size × 1.2) line height (`text_leaf_height`),
-/// 0.60 × font_size mono advance (`estimate_label_width_helper`).
-const LINE_H: f32 = 17.0;
-const CELL_W: f32 = FONT_SIZE * 0.60;
 const INIT_W: u32 = 840;
 const INIT_H: u32 = 520;
-const SCROLLBACK: usize = 5000;
 /// Wheel notches → scrollback lines.
 const SCROLL_LINES_PER_NOTCH: f32 = 3.0;
 /// Presses in the same cell within this window escalate Simple → Semantic →
 /// Lines selection.
 const MULTI_CLICK_WINDOW: Duration = Duration::from_millis(400);
-/// Selection highlight, over cell backgrounds and under glyphs.
-const SELECTION_RGB: Rgb = Rgb { r: 0xc8, g: 0xd0, b: 0xe0 };
+
+/// The KDL-configurable knobs (`terminal { … }` in the shared config.kdl or
+/// the per-app `~/.config/cce/cce-terminal/config.kdl`, app file winning) and
+/// the cell metrics derived from them. Toolkit conventions for the metrics:
+/// ceil(font_size × 1.2) line height (`text_leaf_height`), 0.60 × font_size
+/// mono advance (`estimate_label_width_helper`).
+#[derive(Clone, Copy, PartialEq)]
+struct Settings {
+    font_size: f32,
+    line_h: f32,
+    cell_w: f32,
+    scrollback: usize,
+    palette: colors::Palette,
+}
+
+impl Settings {
+    fn load() -> Self {
+        let font_size = cce_ui::config::get_f32("/terminal/font_size", 14.0).clamp(6.0, 72.0);
+        let scrollback =
+            cce_ui::config::get_i64("/terminal/scrollback", 5000).clamp(0, 200_000) as usize;
+        Settings {
+            font_size,
+            line_h: (font_size * 1.2).ceil(),
+            cell_w: font_size * 0.60,
+            scrollback,
+            palette: colors::Palette::from_config(),
+        }
+    }
+}
 
 #[derive(Clone)]
 enum Msg {
@@ -115,7 +144,13 @@ struct TerminalApp {
     /// Multi-click escalation state: last press instant + cell.
     last_click: Option<(Instant, Point)>,
     click_count: u8,
-    /// Current window size (for selection edge-autoscroll bounds).
+    settings: Settings,
+    /// Config-file mtime at the last (re)load — tick polls it so palette and
+    /// font-size edits apply live, per the DE's edit-the-file convention.
+    config_stamp: Option<std::time::SystemTime>,
+    /// Current window size (for grid re-derivation on config reload and
+    /// selection edge-autoscroll bounds).
+    win_w: f32,
     win_h: f32,
     /// Modifier state tracked from key events (mouse handlers receive no
     /// modifiers). Shift bypasses mouse reporting; ctrl/alt ride report codes.
@@ -130,9 +165,7 @@ struct TerminalApp {
 
 impl TerminalApp {
     fn grid_for(&self, w: f32, h: f32) -> (u16, u16) {
-        let cols = (((w - 2.0 * self.pad) / CELL_W).floor() as i64).clamp(2, u16::MAX as i64);
-        let rows = (((h - 2.0 * self.pad) / LINE_H).floor() as i64).clamp(1, u16::MAX as i64);
-        (cols as u16, rows as u16)
+        grid_dims(w, h, self.pad, &self.settings)
     }
 
     fn write_pty(&mut self, bytes: &[u8]) {
@@ -141,18 +174,18 @@ impl TerminalApp {
 
     fn cell_rect(&self, row: usize, col: usize, width_cells: usize) -> Rect {
         Rect {
-            x: self.pad + col as f32 * CELL_W,
-            y: self.pad + row as f32 * LINE_H,
-            width: width_cells as f32 * CELL_W,
-            height: LINE_H,
+            x: self.pad + col as f32 * self.settings.cell_w,
+            y: self.pad + row as f32 * self.settings.line_h,
+            width: width_cells as f32 * self.settings.cell_w,
+            height: self.settings.line_h,
         }
     }
 
     /// Pixel position → 0-based viewport cell (clamped).
     fn viewport_cell(&self, pos: LogicalPosition) -> (usize, usize) {
-        let col = (((pos.x as f32 - self.pad) / CELL_W).max(0.0) as usize)
+        let col = (((pos.x as f32 - self.pad) / self.settings.cell_w).max(0.0) as usize)
             .min(self.cols as usize - 1);
-        let row = (((pos.y as f32 - self.pad) / LINE_H).max(0.0) as usize)
+        let row = (((pos.y as f32 - self.pad) / self.settings.line_h).max(0.0) as usize)
             .min(self.rows as usize - 1);
         (col, row)
     }
@@ -162,7 +195,7 @@ impl TerminalApp {
     /// cell was hit.
     fn grid_point(&self, pos: LogicalPosition) -> (Point, Side) {
         let (col, row) = self.viewport_cell(pos);
-        let col_f = ((pos.x as f32 - self.pad) / CELL_W).max(0.0);
+        let col_f = ((pos.x as f32 - self.pad) / self.settings.cell_w).max(0.0);
         let line = Line(row as i32 - self.term.grid().display_offset() as i32);
         let side = if col_f.fract() > 0.5 { Side::Right } else { Side::Left };
         (Point::new(line, Column(col)), side)
@@ -245,6 +278,12 @@ fn paste_bytes(text: &str, bracketed: bool) -> Vec<u8> {
     }
 }
 
+fn grid_dims(w: f32, h: f32, pad: f32, settings: &Settings) -> (u16, u16) {
+    let cols = (((w - 2.0 * pad) / settings.cell_w).floor() as i64).clamp(2, u16::MAX as i64);
+    let rows = (((h - 2.0 * pad) / settings.line_h).floor() as i64).clamp(1, u16::MAX as i64);
+    (cols as u16, rows as u16)
+}
+
 /// Terminal-space RGB → cce-ui quad color (the geometry pipeline is linear;
 /// terminal colors are sRGB).
 fn quad_color(rgb: Rgb, alpha: f32) -> [f32; 4] {
@@ -353,8 +392,9 @@ impl Application for TerminalApp {
         sender: calloop::channel::Sender<Self::Message>,
     ) -> Self {
         let pad = cce_ui::layout::backplate_padding();
-        let cols = (((INIT_W as f32 - 2.0 * pad) / CELL_W) as i64).clamp(2, u16::MAX as i64) as u16;
-        let rows = (((INIT_H as f32 - 2.0 * pad) / LINE_H) as i64).clamp(1, u16::MAX as i64) as u16;
+        let settings = Settings::load();
+        let config_stamp = cce_ui::config::config_files_modified();
+        let (cols, rows) = grid_dims(INIT_W as f32, INIT_H as f32, pad, &settings);
 
         // `cce-terminal -e <cmd> [args…]` runs a command instead of $SHELL.
         let args: Vec<String> = std::env::args().collect();
@@ -387,7 +427,8 @@ impl Application for TerminalApp {
             let _ = pty_sender.send(Msg::PtyClosed);
         });
 
-        let config = TermConfig { scrolling_history: SCROLLBACK, ..TermConfig::default() };
+        let config =
+            TermConfig { scrolling_history: settings.scrollback, ..TermConfig::default() };
         let term = Term::new(config, &TermDims { cols, rows }, EventProxy(sender));
 
         // Index 6 of the preferred-fonts tuple is the `terminal` alias
@@ -409,6 +450,9 @@ impl Application for TerminalApp {
             selecting: false,
             last_click: None,
             click_count: 0,
+            settings,
+            config_stamp,
+            win_w: INIT_W as f32,
             win_h: INIT_H as f32,
             shift_down: false,
             ctrl_down: false,
@@ -453,7 +497,7 @@ impl Application for TerminalApp {
                     let rgb = self
                         .term
                         .colors()[index]
-                        .unwrap_or_else(|| colors::default_color(index));
+                        .unwrap_or_else(|| colors::default_color(index, &self.settings.palette));
                     let response = format(rgb);
                     self.write_pty(response.as_bytes());
                 }
@@ -461,8 +505,8 @@ impl Application for TerminalApp {
                     let response = format(WindowSize {
                         num_lines: self.rows,
                         num_cols: self.cols,
-                        cell_width: CELL_W as u16,
-                        cell_height: LINE_H as u16,
+                        cell_width: self.settings.cell_w as u16,
+                        cell_height: self.settings.line_h as u16,
                     });
                     self.write_pty(response.as_bytes());
                 }
@@ -489,9 +533,32 @@ impl Application for TerminalApp {
         }
     }
 
-    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
+    /// Config edits apply live: poll the config files' mtime (the toolkit's
+    /// own getters stat them on every call anyway) and re-derive settings,
+    /// grid, and pty size on change. Scrollback capacity is the exception —
+    /// it's baked into the Term at startup.
+    fn tick(&mut self, _dt: f32, needs_rebuild: &mut bool) {
+        let stamp = cce_ui::config::config_files_modified();
+        if stamp == self.config_stamp {
+            return;
+        }
+        self.config_stamp = stamp;
+        let reloaded = Settings::load();
+        let old = std::mem::replace(&mut self.settings, reloaded);
+        if reloaded != old {
+            *needs_rebuild = true;
+            let (cols, rows) = self.grid_for(self.win_w, self.win_h);
+            if (cols, rows) != (self.cols, self.rows) {
+                self.cols = cols;
+                self.rows = rows;
+                self.pty.resize(cols, rows);
+                self.term.resize(TermDims { cols, rows });
+            }
+        }
+    }
 
     fn handle_resize(&mut self, width: f32, height: f32, _scale: f64) {
+        self.win_w = width;
         self.win_h = height;
         let (cols, rows) = self.grid_for(width, height);
         if (cols, rows) != (self.cols, self.rows) {
@@ -522,7 +589,8 @@ impl Application for TerminalApp {
 
         let content = self.term.renderable_content();
         let display_offset = content.display_offset as i32;
-        let palette = content.colors;
+        let overrides = content.colors;
+        let cfg_palette = self.settings.palette;
 
         // One ordered pass over the viewport cells, batching backgrounds and
         // same-style text into runs. Emission order: bg quads → text →
@@ -557,7 +625,7 @@ impl Application for TerminalApp {
                 (cell.fg, cell.bg)
             };
             let dim = flags.intersects(Flags::DIM);
-            let fg = colors::resolve(fg_color, palette, dim);
+            let fg = colors::resolve(fg_color, overrides, dim, &cfg_palette);
 
             // Selection highlight span.
             if content.selection.is_some_and(|sel| sel.contains(cell.point)) {
@@ -569,7 +637,7 @@ impl Application for TerminalApp {
 
             // Background run (skip the default background: the plate shows through).
             let bg = (bg_color != AnsiColor::Named(NamedColor::Background))
-                .then(|| colors::resolve(bg_color, palette, false));
+                .then(|| colors::resolve(bg_color, overrides, false, &cfg_palette));
             match (&mut cur_bg, bg) {
                 (Some((r, _s, e, rgb)), Some(new)) if *r == row && *e == col && *rgb == new => {
                     *e = col + width_cells;
@@ -658,21 +726,24 @@ impl Application for TerminalApp {
         let cursor_shape = cursor.shape;
         let cursor_row = cursor.point.line.0 + display_offset;
         let cursor_col = cursor.point.column.0;
-        let cursor_rgb = colors::indexed(NamedColor::Cursor as usize, palette);
+        let cursor_rgb = colors::indexed(NamedColor::Cursor as usize, overrides, &cfg_palette);
         let mode = content.mode;
 
         for (row, start, end, rgb) in bg_runs {
             pc.quad(self.cell_rect(row, start, end - start), quad_color(rgb, 1.0));
         }
         for (row, start, end) in sel_runs {
-            pc.quad(self.cell_rect(row, start, end - start), quad_color(SELECTION_RGB, 0.28));
+            pc.quad(
+                self.cell_rect(row, start, end - start),
+                quad_color(cfg_palette.selection, 0.28),
+            );
         }
         for run in text_runs {
             pc.text_attrs(
                 run.text,
-                pad + run.col as f32 * CELL_W,
-                pad + run.row as f32 * LINE_H,
-                FONT_SIZE,
+                pad + run.col as f32 * self.settings.cell_w,
+                pad + run.row as f32 * self.settings.line_h,
+                self.settings.font_size,
                 [run.fg.r, run.fg.g, run.fg.b],
                 Some(font.clone()),
                 Some(bounds),
@@ -681,7 +752,11 @@ impl Application for TerminalApp {
         }
         for (row, start, end, rgb, is_strike) in deco_runs {
             let mut rect = self.cell_rect(row, start, end - start);
-            rect.y += if is_strike { LINE_H * 0.55 } else { LINE_H - 2.0 };
+            rect.y += if is_strike {
+                self.settings.line_h * 0.55
+            } else {
+                self.settings.line_h - 2.0
+            };
             rect.height = 1.0;
             pc.quad(rect, quad_color(rgb, 1.0));
         }
@@ -712,7 +787,7 @@ impl Application for TerminalApp {
                     }
                     CursorShape::Underline => {
                         pc.quad(
-                            Rect { y: rect.y + LINE_H - 2.0, height: 2.0, ..rect },
+                            Rect { y: rect.y + self.settings.line_h - 2.0, height: 2.0, ..rect },
                             quad_color(cursor_rgb, 0.9),
                         );
                     }