brewery
notashelf /
8e737dd2fff206f1c28e765361ed4bce829121ec

beer

public

A terminal worth pouring time into

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

Commit 8e737dd2fff2

tarball

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

unverified1 files changed+75-3
vt: answer `XTGETTCAP` capability queries

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8011dd7a061b46447b6c9f147b5614e06a6a6964
diff --git a/src/vt.rs b/src/vt.rsindex e8fd8a4..ae7e847 100644--- a/src/vt.rs+++ b/src/vt.rs@@ -73,6 +73,8 @@ pub struct Term {     g0: Charset,     g1: Charset,     shift_out: bool,+    /// Accumulated payload of an in-progress `DCS + q` (XTGETTCAP) query.+    xtgettcap: Option<Vec<u8>>, }  impl Term {@@ -85,6 +87,31 @@ impl Term {             g0: Charset::Ascii,             g1: Charset::Ascii,             shift_out: false,+            xtgettcap: None,+        }+    }++    /// Answer an XTGETTCAP query: for each hex-encoded capability name, reply+    /// with `DCS 1 + r name=value ST` if known, else `DCS 0 + r name ST`.+    fn answer_xtgettcap(&mut self, payload: &[u8]) {+        for name_hex in payload.split(|&b| b == b';') {+            let value = decode_hex(name_hex).and_then(|name| cap_value(&name));+            match value {+                Some(value) => {+                    self.response.extend_from_slice(b"\x1bP1+r");+                    self.response.extend_from_slice(name_hex);+                    self.response.push(b'=');+                    for byte in value.bytes() {+                        let _ = write!(self.response, "{byte:02x}");+                    }+                    self.response.extend_from_slice(b"\x1b\\");+                }+                None => {+                    self.response.extend_from_slice(b"\x1bP0+r");+                    self.response.extend_from_slice(name_hex);+                    self.response.extend_from_slice(b"\x1b\\");+                }+            }         }     } @@ -489,9 +516,45 @@ impl Perform for Term {         }     } -    fn hook(&mut self, _: &Params, _: &[u8], _: bool, _: char) {}-    fn put(&mut self, _: u8) {}-    fn unhook(&mut self) {}+    fn hook(&mut self, _: &Params, intermediates: &[u8], _: bool, action: char) {+        // XTGETTCAP arrives as `DCS + q <names> ST`.+        if action == 'q' && intermediates == [b'+'] {+            self.xtgettcap = Some(Vec::new());+        }+    }++    fn put(&mut self, byte: u8) {+        if let Some(buf) = self.xtgettcap.as_mut() {+            buf.push(byte);+        }+    }++    fn unhook(&mut self) {+        if let Some(payload) = self.xtgettcap.take() {+            self.answer_xtgettcap(&payload);+        }+    }+}++/// Look up a terminfo capability beer reports via XTGETTCAP.+fn cap_value(name: &[u8]) -> Option<&'static str> {+    match name {+        b"TN" => Some("beer"),+        b"Co" | b"colors" => Some("256"),+        b"RGB" => Some("8/8/8"),+        _ => None,+    }+}++/// Decode an even-length lowercase/uppercase hex string into bytes.+fn decode_hex(s: &[u8]) -> Option<Vec<u8>> {+    if s.is_empty() || !s.len().is_multiple_of(2) {+        return None;+    }+    let nibble = |b: u8| (b as char).to_digit(16).map(|d| d as u8);+    s.chunks_exact(2)+        .map(|pair| Some((nibble(pair[0])? << 4) | nibble(pair[1])?))+        .collect() }  #[cfg(test)]@@ -621,6 +684,15 @@ mod tests {     }      #[test]+    fn xtgettcap_known_and_unknown() {+        let mut t = Term::new(20, 1);+        feed(&mut t, b"\x1bP+q544e\x1b\\"); // "TN"+        assert_eq!(t.take_response(), b"\x1bP1+r544e=62656572\x1b\\"); // = "beer"+        feed(&mut t, b"\x1bP+q6162\x1b\\"); // "ab", unknown+        assert_eq!(t.take_response(), b"\x1bP0+r6162\x1b\\");+    }++    #[test]     fn title_stack_push_pop() {         let mut t = Term::new(20, 4);         feed(&mut t, b"\x1b]0;first\x07");