brewery
notashelf /
f42924c4735e34dfb21c3a6e5a7183df4a5db419

beer

public

A terminal worth pouring time into

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

Commit f42924c4735e

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-26 18:56 UTC

unverified6 files changed+503-4
beer: render text-sizing runs as scaled multicell blocks

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I599225a2532409a2e7a804cb748391246a6a6964
diff --git a/crates/beer/src/font.rs b/crates/beer/src/font.rsindex d232707..0f66047 100644--- a/crates/beer/src/font.rs+++ b/crates/beer/src/font.rs@@ -221,6 +221,21 @@ impl Fonts {         Ok(self.gcache.get(&key).expect("glyph was just inserted"))     } +    /// Rasterize `c` in `style` at `scale` times the base size, uncached.+    ///+    /// Scaled glyphs come from the text-sizing protocol (`OSC 66`); they are+    /// rare and transient, so they bypass the glyph cache. The scale is applied+    /// as an outline transform, which leaves the face's configured pixel size -+    /// and therefore the cell metrics every other glyph depends on - untouched.+    /// Embedded-bitmap (colour) glyphs ignore the transform; the caller scales+    /// those at blit time instead.+    pub fn glyph_scaled(&mut self, c: char, style: Style, scale: f32) -> Result<Glyph, FontError> {+        let idx = self.face_for(c, style)?;+        let face = &self.faces[idx].face;+        let (synth_bold, synth_italic) = synth_flags(face, style);+        rasterize_scaled(face, c, scale.max(0.01), synth_bold, synth_italic)+    }+     /// Shape `base` plus its combining `marks` into positioned glyphs using     /// HarfBuzz, so marks land where the font's GPOS table wants them rather     /// than stacked at the origin. Returns `None` when shaping is unavailable or@@ -473,6 +488,58 @@ fn rasterize_index(     }) } +/// Rasterize `c` with the outline scaled by `scale` (and sheared if italic is+/// synthesized). The transform is reset before returning so the face is left as+/// it was found.+fn rasterize_scaled(+    face: &Face,+    c: char,+    scale: f32,+    synth_bold: bool,+    synth_italic: bool,+) -> Result<Glyph, FontError> {+    // A scale matrix on the diagonal, in 16.16 fixed point; the off-diagonal+    // `xy` term shears for a synthetic italic (~0.2 of the glyph height). `as _`+    // takes the field's `FT_Fixed` type, as the identity/shear matrices do.+    let mut matrix = Matrix {+        xx: (scale * 65536.0).round() as _,+        xy: if synth_italic {+            (scale * 0.2 * 65536.0).round() as _+        } else {+            0+        },+        yx: 0,+        yy: (scale * 65536.0).round() as _,+    };+    face.set_transform(&mut matrix, &mut Vector { x: 0, y: 0 });+    let result = face.load_char(c as usize, LoadFlag::RENDER | LoadFlag::COLOR);+    face.set_transform(&mut identity_matrix(), &mut Vector { x: 0, y: 0 });+    result?;++    let slot = face.glyph();+    let bitmap = slot.bitmap();+    let width = bitmap.width().max(0) as usize;+    let height = bitmap.rows().max(0) as usize;+    let pitch = bitmap.pitch();+    let src = bitmap.buffer();+    let mut data = match bitmap.pixel_mode()? {+        PixelMode::Gray => GlyphData::Mask(pack_rows(src, width, pitch, height)),+        PixelMode::Bgra => GlyphData::Color(pack_rows(src, width * 4, pitch, height)),+        PixelMode::Mono => GlyphData::Mask(expand_mono(src, width, pitch, height)),+        _ => GlyphData::Mask(vec![0; width * height]),+    };+    if synth_bold && let GlyphData::Mask(mask) = &mut data {+        embolden(mask, width, height);+    }+    Ok(Glyph {+        left: slot.bitmap_left(),+        top: slot.bitmap_top(),+        width: width as u32,+        height: height as u32,+        data,+    })+}+ fn rasterize_with(     face: &Face,     synth_bold: bool,diff --git a/crates/beer/src/grid/mod.rs b/crates/beer/src/grid/mod.rsindex ef06572..b1cf307 100644--- a/crates/beer/src/grid/mod.rs+++ b/crates/beer/src/grid/mod.rs@@ -19,6 +19,7 @@ const SCROLLBACK_CAP: usize = 10_000; /// The protocol vocabulary an SGR/DECSET stream selects lives in /// `beer-protocols` and is re-exported here so the grid and renderer keep /// referring to it as `grid::Color`, `grid::Underline`, and so on.+pub use beer_protocols::text_size::TextSize; pub use beer_protocols::{Color, CursorShape, MouseEncoding, MouseProtocol, PromptKind, Underline};  /// Per-cell style flags, packed into a `u16`.@@ -36,6 +37,9 @@ impl Flags {     /// Trailing column of a double-width glyph; holds no character of its own.     pub const WIDE_CONT: Self = Self(1 << 8);     pub const OVERLINE: Self = Self(1 << 9);+    /// A cell of a text-sizing (`OSC 66`) block that is not the block's leading+    /// cell; it holds no character of its own and is drawn by the leading cell.+    pub const SIZED_CONT: Self = Self(1 << 10);      pub const fn empty() -> Self {         Self(0)@@ -58,6 +62,31 @@ impl Flags {     } } +/// The text-sizing (`OSC 66`) descriptor a scaled cell carries. A run is drawn+/// as a block `cols` cells wide and `rows` high; this cell sits at `(dx, dy)`+/// within it. The leading cell (`dx == 0, dy == 0`) carries the run text and is+/// what the renderer draws; the rest are flagged [`Flags::SIZED_CONT`].+///+/// Boxed on the cell so the common, unscaled path keeps `Cell` lean: only the+/// rare scaled cell pays an allocation.+#[derive(Clone, PartialEq, Eq, Debug)]+pub struct Sized {+    /// Scale, fractional scale, and alignment parsed from the metadata.+    pub size: TextSize,+    /// Block width in cells: `s * w`, or `s * grapheme_width` when `w == 0`.+    pub cols: u8,+    /// Block height in cells: `s`.+    pub rows: u8,+    /// This cell's column within the block.+    pub dx: u8,+    /// This cell's row within the block.+    pub dy: u8,+    /// The run text, on the leading cell only. For the per-grapheme (`w == 0`)+    /// form this is `None` and the grapheme lives in the cell's `c`/`combining`;+    /// for the packed (`w > 0`) form it holds the whole run.+    pub run: Option<Box<str>>,+}+ /// One grid cell: a character plus its rendering style. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Cell {@@ -74,6 +103,8 @@ pub struct Cell {     pub combining: Option<Box<str>>,     /// OSC 8 hyperlink: a 1-based index into the grid's link table, or `None`.     pub link: Option<NonZeroU16>,+    /// Text-sizing block membership (`OSC 66`), or `None` for ordinary cells.+    pub sized: Option<Box<Sized>>, }  impl Default for Cell {@@ -87,6 +118,7 @@ impl Default for Cell {             underline_color: Color::Default,             combining: None,             link: None,+            sized: None,         }     } }@@ -210,6 +242,24 @@ fn is_word(c: char, delims: &str) -> bool {     !c.is_whitespace() && !delims.contains(c) } +/// Split text into grapheme-ish units for `OSC 66 w=0` layout: each base+/// character with its trailing zero-width combining marks and display width.+/// Leading combining marks with no base are dropped, as in normal printing.+fn graphemes(text: &str) -> Vec<(char, String, usize)> {+    let mut out: Vec<(char, String, usize)> = Vec::new();+    for c in text.chars() {+        match c.width().unwrap_or(0) {+            0 => {+                if let Some(last) = out.last_mut() {+                    last.1.push(c);+                }+            }+            w => out.push((c, String::new(), w)),+        }+    }+    out+}+ impl Grid {     pub fn new(cols: usize, rows: usize) -> Self {         let cols = cols.max(1);@@ -314,6 +364,7 @@ impl Grid {     pub fn resize(&mut self, cols: usize, rows: usize) {         let cols = cols.max(1);         let rows = rows.max(1);+        self.clear_sized_runs();         if self.alt_saved.is_some() {             self.clip_resize(cols, rows);         } else {@@ -560,9 +611,16 @@ impl Grid {         }          let (x, y) = (self.cursor.x, self.cursor.y);+        // Overwriting any cell of a text-sizing block dissolves the whole block,+        // so no orphaned continuation cells are left for the renderer to draw.+        self.clear_sized_at(x, y);+        if width == 2 && x + 1 < self.cols {+            self.clear_sized_at(x + 1, y);+        }         let mut cell = self.pen.clone();         cell.c = c;         cell.combining = None;+        cell.sized = None;         cell.flags.remove(Flags::WIDE_CONT);         self.lines[y].cells[x] = cell;         self.last_base = Some((x, y));@@ -570,6 +628,7 @@ impl Grid {             let mut cont = self.pen.clone();             cont.c = ' ';             cont.combining = None;+            cont.sized = None;             cont.flags.insert(Flags::WIDE_CONT);             self.lines[y].cells[x + 1] = cont;         }@@ -604,6 +663,137 @@ impl Grid {         cell.combining = Some(s.into_boxed_str());     } +    /// Lay out a text-sizing run (`OSC 66`) as scaled multicell blocks at the+    /// cursor, advancing it on the same row by the total block width, as the+    /// protocol requires. With `width == 0` each grapheme gets its own `s` by `s`+    /// block (the font scaled by `s`); with `width > 0` the whole run is packed+    /// into one block `s * width` cells wide and `s` high. A plain descriptor+    /// (scale 1, no width, no fraction) falls back to ordinary printing.+    pub fn print_sized(&mut self, text: &str, size: TextSize) {+        if size.is_plain() {+            for c in text.chars() {+                self.print(c);+            }+            return;+        }+        let rows = size.cell_height().clamp(1, self.rows);+        if size.width == 0 {+            for (base, marks, w) in graphemes(text) {+                let cols = (rows * w).clamp(1, self.cols);+                self.place_block(base, marks, None, size, cols, rows);+            }+        } else {+            let cols = (rows * size.width as usize).clamp(1, self.cols);+            self.place_block(' ', String::new(), Some(text.into()), size, cols, rows);+        }+    }++    /// Write one scaled block of `cols` by `rows` cells at the cursor and+    /// advance past it. The leading cell carries the text; the rest are flagged+    /// [`Flags::SIZED_CONT`]. A block that will not fit wraps to the next line.+    fn place_block(+        &mut self,+        lead: char,+        marks: String,+        run: Option<Box<str>>,+        size: TextSize,+        cols: usize,+        rows: usize,+    ) {+        if self.wrap_pending || (self.autowrap && self.cursor.x + cols > self.cols) {+            self.cursor.x = 0;+            self.lines[self.cursor.y].wrapped = true;+            self.line_feed();+            self.wrap_pending = false;+        }+        let (x0, y0) = (self.cursor.x, self.cursor.y);+        let marks = (!marks.is_empty()).then(|| marks.into_boxed_str());+        for dy in 0..rows {+            let cy = y0 + dy;+            if cy >= self.rows {+                break;+            }+            for dx in 0..cols {+                let cx = x0 + dx;+                if cx >= self.cols {+                    break;+                }+                // Dissolve any block already occupying this cell before reusing it.+                self.clear_sized_at(cx, cy);+                let lead_cell = dx == 0 && dy == 0;+                let mut cell = self.pen.clone();+                cell.c = if lead_cell { lead } else { ' ' };+                cell.combining = if lead_cell { marks.clone() } else { None };+                cell.flags.remove(Flags::WIDE_CONT);+                if !lead_cell {+                    cell.flags.insert(Flags::SIZED_CONT);+                }+                cell.sized = Some(Box::new(Sized {+                    size,+                    cols: cols as u8,+                    rows: rows as u8,+                    dx: dx as u8,+                    dy: dy as u8,+                    run: if lead_cell { run.clone() } else { None },+                }));+                self.lines[cy].cells[cx] = cell;+            }+        }+        self.last_base = None;+        if self.cursor.x + cols >= self.cols {+            self.cursor.x = self.cols - 1;+            self.wrap_pending = self.autowrap;+        } else {+            self.cursor.x += cols;+        }+    }++    /// If `(x, y)` belongs to a text-sizing block, blank every cell of that+    /// block so a write into it cannot orphan continuation cells.+    fn clear_sized_at(&mut self, x: usize, y: usize) {+        let Some(s) = self+            .lines+            .get(y)+            .and_then(|l| l.cells.get(x))+            .and_then(|c| c.sized.as_deref())+        else {+            return;+        };+        let (dx, dy, cols, rows) = (+            s.dx as usize,+            s.dy as usize,+            s.cols as usize,+            s.rows as usize,+        );+        let x0 = x.saturating_sub(dx);+        let y0 = y.saturating_sub(dy);+        let blank = self.pen_blank();+        let x_end = (x0 + cols).min(self.cols);+        for by in y0..(y0 + rows).min(self.rows) {+            for cell in &mut self.lines[by].cells[x0..x_end] {+                *cell = blank.clone();+            }+        }+    }++    /// Drop all text-sizing blocks: their cells become plain characters. Used+    /// before a resize, since a scaled block must not be split across a rewrap;+    /// applications using `OSC 66` repaint on resize regardless.+    fn clear_sized_runs(&mut self) {+        let strip = |line: &mut Line| {+            for cell in &mut line.cells {+                if cell.sized.take().is_some() {+                    cell.flags.remove(Flags::SIZED_CONT);+                }+            }+        };+        self.lines.iter_mut().for_each(strip);+        self.scrollback.iter_mut().for_each(strip);+        if let Some(alt) = self.alt_saved.as_mut() {+            alt.iter_mut().for_each(strip);+        }+    }+     fn shift_right(&mut self, n: usize) {         let (x, y) = (self.cursor.x, self.cursor.y);         let end = self.cols;@@ -982,6 +1172,29 @@ impl Grid {         }     } +    /// For a viewport cell `(y, x)` that is the left edge of a text-sizing block+    /// (its `dx == 0`), return the block's leading cell - which holds the text+    /// and the full descriptor - together with this row's `dy` within the block.+    /// `None` if `(y, x)` is not a left-edge sized cell, or the leading row is+    /// scrolled above the viewport top (the block is then clipped, not drawn).+    pub fn sized_lead(&self, y: usize, x: usize) -> Option<(&Cell, usize)> {+        let dy = {+            let s = self.view_row(y).get(x)?.sized.as_ref()?;+            if s.dx != 0 {+                return None;+            }+            s.dy as usize+        };+        if dy > y {+            return None;+        }+        let lead = self.view_row(y - dy).get(x)?;+        lead.sized+            .as_ref()+            .filter(|ls| ls.dx == 0 && ls.dy == 0)+            .map(|_| (lead, dy))+    }+     // --- selection ---      /// The absolute row currently shown at viewport row `y`.@@ -1130,7 +1343,7 @@ impl Grid {         self.lines[y]             .cells             .iter()-            .filter(|c| !c.flags.contains(Flags::WIDE_CONT))+            .filter(|c| !c.flags.contains(Flags::WIDE_CONT) && !c.flags.contains(Flags::SIZED_CONT))             .map(|c| c.c)             .collect::<String>()             .trim_end()@@ -1300,6 +1513,61 @@ mod tests {     }      #[test]+    fn sized_scale_lays_out_a_block_and_advances() {+        // `s=2` with width 0: 'X' fills a 2x2 block, cursor advances 2 cells.+        let mut g = Grid::new(8, 4);+        g.print_sized("X", TextSize::parse_str("s=2"));+        let lead = g.cell(0, 0);+        assert_eq!(lead.c, 'X');+        assert!(!lead.flags.contains(Flags::SIZED_CONT));+        let s = lead.sized.as_ref().expect("leading cell is sized");+        assert_eq!((s.cols, s.rows, s.dx, s.dy), (2, 2, 0, 0));+        // The other three cells of the block are continuations.+        for (x, y) in [(1, 0), (0, 1), (1, 1)] {+            assert!(g.cell(x, y).flags.contains(Flags::SIZED_CONT));+        }+        assert_eq!(g.cursor(), (2, 0));+    }++    #[test]+    fn sized_packed_run_round_trips_as_text() {+        // `w=1` packs the whole run into one cell; selection yields it whole.+        let mut g = Grid::new(8, 2);+        g.print_sized("ab", TextSize::parse_str("n=1:d=2:w=1"));+        assert!(g.cell(0, 0).sized.as_ref().unwrap().run.is_some());+        g.start_selection(0, 0);+        g.extend_selection(0, 0);+        assert_eq!(g.selection_text().as_deref(), Some("ab"));+        assert_eq!(g.cursor(), (1, 0));+    }++    #[test]+    fn overwriting_a_sized_block_dissolves_it() {+        let mut g = Grid::new(8, 4);+        g.print_sized("X", TextSize::parse_str("s=2"));+        // Print a normal char into a continuation cell of the block.+        g.move_to(1, 1);+        g.print('z');+        // The whole block is gone: no cell still claims to be sized.+        for y in 0..2 {+            for x in 0..2 {+                assert!(g.cell(x, y).sized.is_none(), "({x},{y}) still sized");+                assert!(!g.cell(x, y).flags.contains(Flags::SIZED_CONT));+            }+        }+        assert_eq!(g.cell(1, 1).c, 'z');+    }++    #[test]+    fn resize_dissolves_sized_runs() {+        let mut g = Grid::new(8, 4);+        g.print_sized("X", TextSize::parse_str("s=2"));+        g.resize(6, 4);+        assert!(g.cell(0, 0).sized.is_none());+        assert!(!g.cell(1, 0).flags.contains(Flags::SIZED_CONT));+    }++    #[test]     fn wide_char_occupies_two_columns() {         let mut g = Grid::new(6, 1);         g.print('世');diff --git a/crates/beer/src/grid/selection.rs b/crates/beer/src/grid/selection.rsindex 74d1fe3..a3bcbd8 100644--- a/crates/beer/src/grid/selection.rs+++ b/crates/beer/src/grid/selection.rs@@ -169,8 +169,9 @@ impl Grid {         Some(out)     } -    /// The characters of an absolute row in `[from, to)`, skipping wide-    /// continuation cells.+    /// The characters of an absolute row in `[from, to)`, skipping wide and+    /// text-sizing continuation cells. A leading text-sizing cell that holds a+    /// packed run (`OSC 66 w>0`) contributes that whole run.     pub(super) fn row_slice_text(&self, row: usize, from: usize, to: usize) -> String {         let mut out = String::new();         for cell in self@@ -178,8 +179,12 @@ impl Grid {             .get(from..to.min(self.abs_row(row).len()))             .unwrap_or(&[])             .iter()-            .filter(|c| !c.flags.contains(Flags::WIDE_CONT))+            .filter(|c| !c.flags.contains(Flags::WIDE_CONT) && !c.flags.contains(Flags::SIZED_CONT))         {+            if let Some(run) = cell.sized.as_ref().and_then(|s| s.run.as_ref()) {+                out.push_str(run);+                continue;+            }             out.push(cell.c);             if let Some(marks) = &cell.combining {                 out.push_str(marks);diff --git a/crates/beer/src/render.rs b/crates/beer/src/render.rsindex d7206f0..b64bdd7 100644--- a/crates/beer/src/render.rs+++ b/crates/beer/src/render.rs@@ -7,6 +7,8 @@  use std::num::NonZeroU16; +use beer_protocols::text_size::{HAlign, VAlign};+ use crate::font::{CellMetrics, Fonts, Glyph, GlyphData, Style}; use crate::grid::{Cell, CursorShape, Flags, Grid, Underline}; use crate::theme::{Plane, Rgb, Theme};@@ -200,6 +202,18 @@ impl Renderer {             if cell.flags.contains(Flags::WIDE_CONT) {                 continue;             }+            // Text-sizing (OSC 66) blocks are drawn from their per-row left edge,+            // clipped to this row's band; every other block cell is skipped.+            if let Some(sized) = &cell.sized {+                if sized.dx == 0+                    && let Some((lead, dy)) = grid.sized_lead(y, x)+                {+                    let origin_x = pad_x + x as i32 * m.width as i32;+                    let (fg, _) = cell_colors(lead, theme);+                    self.draw_sized(&mut canvas, lead, dy, (origin_x, row_top), m, fg);+                }+                continue;+            }             if cell.flags.contains(Flags::BLINK) && !blink_on {                 continue;             }@@ -444,6 +458,125 @@ impl Renderer {         };         blit_glyph(canvas, glyph, m, origin_x, cell_top, 0, fg);     }++    /// Draw the slice of a text-sizing (`OSC 66`) block that falls in one row.+    ///+    /// `lead` is the block's leading cell (holding the run text); `dy` is this+    /// row's offset within the block; `pos` is this row's left edge and top in+    /// pixels. The full block is `cols * width` by `rows * height` pixels,+    /// starting `dy` rows above the row top; glyphs are rasterized at the block's+    /// font scale and clipped to this row's band, so the block paints correctly+    /// across its several per-row repaints.+    fn draw_sized(+        &mut self,+        canvas: &mut Canvas,+        lead: &Cell,+        dy: usize,+        pos: (i32, i32),+        m: CellMetrics,+        fg: Rgb,+    ) {+        let (block_left_x, row_top) = pos;+        let Some(s) = lead.sized.as_deref() else {+            return;+        };+        let scale = s.size.font_scale();+        let (cols, rows) = (s.cols as i32, s.rows as i32);+        let block_top = row_top - dy as i32 * m.height as i32;+        let (block_w, block_h) = (cols * m.width as i32, rows * m.height as i32);+        let clip = (row_top, row_top + m.height as i32);++        let advance = (m.width as f32 * scale).round().max(1.0) as i32;+        let render_h = (m.height as f32 * scale).round().max(1.0) as i32;++        // The run: a packed string (w>0) or the leading grapheme (w==0).+        let run: Vec<char> = match s.run.as_deref() {+            Some(text) => text.chars().collect(),+            None => vec![lead.c],+        };+        let render_w = advance * run.len() as i32;++        // A fractional scale renders into an area smaller than the block, placed+        // by the v/h alignment; a whole scale fills the block (offsets zero).+        let (ox, oy) = if s.size.has_fraction() {+            let ox = match s.size.halign {+                HAlign::Left => 0,+                HAlign::Right => block_w - render_w,+                HAlign::Center => (block_w - render_w) / 2,+            };+            let oy = match s.size.valign {+                VAlign::Top => 0,+                VAlign::Bottom => block_h - render_h,+                VAlign::Middle => (block_h - render_h) / 2,+            };+            (ox.max(0), oy.max(0))+        } else {+            (0, 0)+        };++        let baseline = block_top + oy + (m.ascent as f32 * scale).round() as i32;+        let style = cell_style(lead);+        let mut pen_x = block_left_x + ox;+        for c in run {+            if c != ' '+                && let Ok(glyph) = self.fonts.glyph_scaled(c, style, scale)+            {+                blit_glyph_clipped(canvas, &glyph, pen_x, baseline, clip, fg, render_h);+            }+            pen_x += advance;+        }+    }+}++/// Composite a rasterized glyph, clipping to the vertical band `clip = (y0, y1)`+/// so a tall text-sizing glyph paints only the part belonging to the current+/// row. Mask glyphs are tinted with `fg`; colour glyphs are scaled to+/// `target_h` (the outline transform does not scale embedded bitmaps).+fn blit_glyph_clipped(+    canvas: &mut Canvas,+    glyph: &Glyph,+    pen_x: i32,+    baseline: i32,+    clip: (i32, i32),+    fg: Rgb,+    target_h: i32,+) {+    let (gw, gh) = (glyph.width as i32, glyph.height as i32);+    match &glyph.data {+        GlyphData::Mask(mask) => {+            for gy in 0..gh {+                let py = baseline - glyph.top + gy;+                if py < clip.0 || py >= clip.1 {+                    continue;+                }+                for gx in 0..gw {+                    let a = mask[(gy * gw + gx) as usize];+                    if a != 0 {+                        canvas.blend(pen_x + glyph.left + gx, py, fg, a);+                    }+                }+            }+        }+        GlyphData::Color(bgra) if gh > 0 => {+            let sc = target_h as f32 / gh as f32;+            let tw = (gw as f32 * sc).round() as i32;+            // Place the scaled bitmap so most of it sits above the baseline.+            let top = baseline - target_h * 4 / 5;+            for ty in 0..target_h {+                let py = top + ty;+                if py < clip.0 || py >= clip.1 {+                    continue;+                }+                let sy = ((ty as f32 / sc) as i32).min(gh - 1);+                for tx in 0..tw {+                    let sx = ((tx as f32 / sc) as i32).min(gw - 1);+                    let i = ((sy * gw + sx) * 4) as usize;+                    canvas.over(pen_x + tx, py, &bgra[i..i + 4]);+                }+            }+        }+        GlyphData::Color(_) => {}+    } }  /// Composite a rasterized glyph into the canvas. `origin_x`/`cell_top` are thediff --git a/crates/beer/src/vt/mod.rs b/crates/beer/src/vt/mod.rsindex 4021cc5..4039665 100644--- a/crates/beer/src/vt/mod.rs+++ b/crates/beer/src/vt/mod.rs@@ -501,6 +501,19 @@ mod tests {     }      #[test]+    fn text_sizing_osc66_lays_out_a_scaled_block() {+        // `OSC 66 ; s=2 ; X BEL`: a 2x2 scaled block, cursor advances two cells.+        let mut t = Term::new(20, 4);+        feed(&mut t, b"\x1b]66;s=2;X\x07");+        let g = t.grid();+        assert_eq!(g.cell(0, 0).c, 'X');+        let s = g.cell(0, 0).sized.as_ref().expect("leading cell is scaled");+        assert_eq!((s.cols, s.rows), (2, 2));+        assert!(g.cell(1, 1).flags.contains(crate::grid::Flags::SIZED_CONT));+        assert_eq!(g.cursor(), (2, 0));+    }++    #[test]     fn device_attributes_levels() {         let mut t = Term::new(20, 4);         feed(&mut t, b"\x1b[c");diff --git a/crates/beer/src/vt/perform.rs b/crates/beer/src/vt/perform.rsindex 202f29f..dd681da 100644--- a/crates/beer/src/vt/perform.rs+++ b/crates/beer/src/vt/perform.rs@@ -225,6 +225,19 @@ impl Perform for Term {                     self.clipboard_ops.push(ClipboardOp::Set { primary, text });                 }             }+            // OSC 66: kitty text-sizing protocol. `OSC 66 ; metadata ; text`,+            // where metadata is a colon-separated key=value list and the text+            // (which may itself contain ';') is rejoined and laid out scaled.+            Some(&n) if n == b"66" => {+                let size = beer_protocols::text_size::parse(params.get(1).copied().unwrap_or(b""));+                let text = params+                    .get(2..)+                    .map(|parts| parts.join(&b';'))+                    .unwrap_or_default();+                if let Ok(text) = std::str::from_utf8(&text) {+                    self.grid.print_sized(text, size);+                }+            }             _ => {}         }     }