brewery
notashelf /
6b3c8dc059450aa6adc7665ac65f383a94df42dc

beer

public

A terminal worth pouring time into

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

Commit 6b3c8dc05945

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-24 12:36 UTC

unverified2 files changed+125-9
vt: answer XTVERSION, DA3, DECRQM, and the title stack

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib74e56cfd26b029064e5683ba20b70606a6a6964
diff --git a/src/grid.rs b/src/grid.rsindex 51933a3..27a7b85 100644--- a/src/grid.rs+++ b/src/grid.rs@@ -177,6 +177,22 @@ impl Grid {         self.insert = on;     } +    pub fn autowrap(&self) -> bool {+        self.autowrap+    }++    pub fn origin(&self) -> bool {+        self.origin+    }++    pub fn insert(&self) -> bool {+        self.insert+    }++    pub fn alt_active(&self) -> bool {+        self.alt_saved.is_some()+    }+     // --- printing ---      /// Place a printable character at the cursor, honouring width and autowrap.diff --git a/src/vt.rs b/src/vt.rsindex fb9d7e0..4eea03b 100644--- a/src/vt.rs+++ b/src/vt.rs@@ -13,11 +13,25 @@ enum Charset {     DecSpecial, } +/// Which device-attributes query is being answered.+#[derive(Clone, Copy, Debug)]+enum DaLevel {+    Primary,+    Secondary,+    Tertiary,+}++/// DECRQM mode-state code: 1 = set, 2 = reset.+fn set_reset(on: bool) -> u8 {+    if on { 1 } else { 2 }+}+ /// The terminal model: a grid plus the escape-sequence state around it. #[derive(Debug)] pub struct Term {     grid: Grid,     title: Option<String>,+    title_stack: Vec<Option<String>>,     response: Vec<u8>,     g0: Charset,     g1: Charset,@@ -29,6 +43,7 @@ impl Term {         Self {             grid: Grid::new(cols, rows),             title: None,+            title_stack: Vec::new(),             response: Vec::new(),             g0: Charset::Ascii,             g1: Charset::Ascii,@@ -141,16 +156,25 @@ impl Term {         }     } -    fn device_attrs(&mut self, secondary: bool) {-        // Claim a VT220 with ANSI colour (62;22) for DA1, and a generic-        // firmware level for DA2.-        if secondary {-            self.response.extend_from_slice(b"\x1b[>0;276;0c");-        } else {-            self.response.extend_from_slice(b"\x1b[?62;22c");+    /// Device attributes. DA1 claims a VT220 with ANSI colour; DA2 a generic+    /// firmware level; DA3 a (zero) unit ID.+    fn device_attrs(&mut self, level: DaLevel) {+        match level {+            DaLevel::Primary => self.response.extend_from_slice(b"\x1b[?62;22c"),+            DaLevel::Secondary => self.response.extend_from_slice(b"\x1b[>0;276;0c"),+            DaLevel::Tertiary => self.response.extend_from_slice(b"\x1bP!|00000000\x1b\\"),         }     } +    /// XTVERSION (`CSI > q`): report the terminal name and version.+    fn report_version(&mut self) {+        let _ = write!(+            self.response,+            "\x1bP>|beer({})\x1b\\",+            env!("CARGO_PKG_VERSION")+        );+    }+     fn device_status(&mut self, params: &Params) {         match params.iter().next().and_then(|p| p.first().copied()) {             Some(5) => self.response.extend_from_slice(b"\x1b[0n"),@@ -161,6 +185,34 @@ impl Term {             _ => {}         }     }++    /// DECRQM (`CSI [?] Ps $ p`): report whether a mode is set (1), reset (2),+    /// or unrecognized (0). Only the modes we actually track are reported.+    fn report_mode(&mut self, params: &Params, private: bool) {+        let code = raw(params, 0);+        let state = match (private, code) {+            (true, 6) => set_reset(self.grid.origin()),+            (true, 7) => set_reset(self.grid.autowrap()),+            (true, 47 | 1047 | 1049) => set_reset(self.grid.alt_active()),+            (false, 4) => set_reset(self.grid.insert()),+            _ => 0,+        };+        let prefix = if private { "?" } else { "" };+        let _ = write!(self.response, "\x1b[{prefix}{code};{state}$y");+    }++    /// Title stack (`CSI 22/23 ; Ps t`): push or pop the window title.+    fn title_stack_op(&mut self, params: &Params) {+        match raw(params, 0) {+            22 => self.title_stack.push(self.title.clone()),+            23 => {+                if let Some(title) = self.title_stack.pop() {+                    self.title = title;+                }+            }+            _ => {}+        }+    } }  /// First param value, with 0/absent folded to `default` (xterm convention for@@ -291,7 +343,6 @@ impl Perform for Term {      fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {         let private = intermediates.first() == Some(&b'?');-        let secondary = intermediates.first() == Some(&b'>');         match action {             'A' => self.grid.cursor_up(n(params, 0, 1)),             'B' | 'e' => self.grid.cursor_down(n(params, 0, 1)),@@ -328,10 +379,17 @@ impl Perform for Term {             }             'h' => self.set_mode(params, private, true),             'l' => self.set_mode(params, private, false),-            'c' => self.device_attrs(secondary),+            'c' => self.device_attrs(match intermediates.first() {+                Some(b'>') => DaLevel::Secondary,+                Some(b'=') => DaLevel::Tertiary,+                _ => DaLevel::Primary,+            }),+            'q' if intermediates.first() == Some(&b'>') => self.report_version(),+            'p' if intermediates.contains(&b'$') => self.report_mode(params, private),             'n' => self.device_status(params),             's' => self.grid.save_cursor(),             'u' => self.grid.restore_cursor(),+            't' => self.title_stack_op(params),             'g' => match raw(params, 0) {                 3 => self.grid.clear_all_tabs(),                 _ => self.grid.clear_tab(),@@ -413,6 +471,48 @@ mod tests {     }      #[test]+    fn device_attributes_levels() {+        let mut t = Term::new(20, 4);+        feed(&mut t, b"\x1b[c");+        assert_eq!(t.take_response(), b"\x1b[?62;22c");+        feed(&mut t, b"\x1b[>c");+        assert_eq!(t.take_response(), b"\x1b[>0;276;0c");+        feed(&mut t, b"\x1b[=c");+        assert_eq!(t.take_response(), b"\x1bP!|00000000\x1b\\");+    }++    #[test]+    fn xtversion_reports_name() {+        let mut t = Term::new(20, 4);+        feed(&mut t, b"\x1b[>q");+        let resp = t.take_response();+        assert!(resp.starts_with(b"\x1bP>|beer("));+        assert!(resp.ends_with(b")\x1b\\"));+    }++    #[test]+    fn decrqm_reports_known_modes() {+        let mut t = Term::new(20, 4);+        feed(&mut t, b"\x1b[?7$p"); // autowrap, on by default+        assert_eq!(t.take_response(), b"\x1b[?7;1$y");+        feed(&mut t, b"\x1b[?7l\x1b[?7$p"); // turn it off, re-query+        assert_eq!(t.take_response(), b"\x1b[?7;2$y");+        feed(&mut t, b"\x1b[?9999$p"); // unknown mode+        assert_eq!(t.take_response(), b"\x1b[?9999;0$y");+    }++    #[test]+    fn title_stack_push_pop() {+        let mut t = Term::new(20, 4);+        feed(&mut t, b"\x1b]0;first\x07");+        feed(&mut t, b"\x1b[22t"); // push "first"+        feed(&mut t, b"\x1b]0;second\x07");+        assert_eq!(t.title(), Some("second"));+        feed(&mut t, b"\x1b[23t"); // pop -> "first"+        assert_eq!(t.title(), Some("first"));+    }++    #[test]     fn sgr_sets_pen_colours() {         let mut t = Term::new(20, 1);         feed(&mut t, b"\x1b[31;1mX");