clone
ssh://git@git.notashelf.dev:33/notashelf/beer.gitCommit 6b3c8dc05945
tarballunverified2 files changed+125-9
@@ -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.@@ -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");