brewery
notashelf /
0c0da3d035ae9bfeec4bf921699f2bd07905f2ab

beer

public

A terminal worth pouring time into

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

Commit 0c0da3d035ae

tarball

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

unverified3 files changed+87-0
vt: track cwd via OSC 7; add a new-window action

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I498a1938ca0129d10cf0e230d27188ed6a6a6964
diff --git a/src/bindings.rs b/src/bindings.rsindex 0b29cc7..8e23f05 100644--- a/src/bindings.rs+++ b/src/bindings.rs@@ -20,6 +20,7 @@ pub enum Action {     FontDecrease,     FontReset,     Fullscreen,+    NewWindow, }  impl Action {@@ -37,6 +38,7 @@ impl Action {             "font-decrease" => Self::FontDecrease,             "font-reset" => Self::FontReset,             "fullscreen" => Self::Fullscreen,+            "new-window" => Self::NewWindow,             _ => return None,         })     }@@ -171,6 +173,7 @@ const DEFAULT_BINDINGS: &[(&str, &str)] = &[     ("Ctrl+minus", "font-decrease"),     ("Ctrl+0", "font-reset"),     ("F11", "fullscreen"),+    ("Ctrl+Shift+N", "new-window"), ];  /// Map a key token to a keysym: a single character, or a named special key.diff --git a/src/vt.rs b/src/vt.rsindex a7f413a..4104e05 100644--- a/src/vt.rs+++ b/src/vt.rs@@ -102,6 +102,8 @@ pub struct Term {     theme: Theme,     /// Set when the child rings the bell (`BEL`); cleared by the front-end.     bell: bool,+    /// Working directory reported by the shell via OSC 7, for new windows.+    cwd: Option<String>, }  impl Term {@@ -118,9 +120,15 @@ impl Term {             clipboard_ops: Vec::new(),             theme: Theme::default(),             bell: false,+            cwd: None,         }     } +    /// The working directory last reported by the shell (OSC 7), if any.+    pub fn cwd(&self) -> Option<&str> {+        self.cwd.as_deref()+    }+     /// Take and clear the pending bell flag.     pub fn take_bell(&mut self) -> bool {         std::mem::take(&mut self.bell)@@ -653,6 +661,12 @@ impl Perform for Term {                     self.title = Some(String::from_utf8_lossy(text).into_owned());                 }             }+            // OSC 7: the shell reports its cwd as a `file://host/path` URI.+            Some(&n) if n == b"7" => {+                if let Some(uri) = params.get(1) {+                    self.cwd = file_uri_path(uri);+                }+            }             // OSC 4: set/query palette entries (pairs of index;spec).             Some(&n) if n == b"4" => self.osc_palette(params, bell),             // OSC 104: reset palette (all, or the listed indices).@@ -796,6 +810,38 @@ fn base64_decode(data: &[u8]) -> Option<Vec<u8>> {     Some(out) } +/// Extract the local path from an OSC 7 `file://host/path` URI, percent-decoding+/// `%XX` escapes. The host part is ignored (we only spawn locally). Returns+/// `None` if it is not a usable absolute path.+fn file_uri_path(uri: &[u8]) -> Option<String> {+    let rest = uri.strip_prefix(b"file://").unwrap_or(uri);+    // Skip the authority (host) up to the first '/', which begins the path.+    let slash = rest.iter().position(|&b| b == b'/')?;+    let path_bytes = percent_decode(&rest[slash..]);+    let path = String::from_utf8(path_bytes).ok()?;+    path.starts_with('/').then_some(path)+}++/// Percent-decode `%XX` byte escapes in a URI path, passing other bytes through.+fn percent_decode(s: &[u8]) -> Vec<u8> {+    let mut out = Vec::with_capacity(s.len());+    let mut i = 0;+    while i < s.len() {+        if s[i] == b'%' && i + 2 < s.len() {+            let hi = (s[i + 1] as char).to_digit(16);+            let lo = (s[i + 2] as char).to_digit(16);+            if let (Some(hi), Some(lo)) = (hi, lo) {+                out.push((hi * 16 + lo) as u8);+                i += 3;+                continue;+            }+        }+        out.push(s[i]);+        i += 1;+    }+    out+}+ /// 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) {@@ -1069,6 +1115,18 @@ mod tests {     }      #[test]+    fn osc7_tracks_cwd_and_decodes_percent() {+        let mut t = Term::new(20, 1);+        feed(&mut t, b"\x1b]7;file://hermes/home/user/my%20dir\x07");+        assert_eq!(t.cwd(), Some("/home/user/my dir"));+        // A non-file or relative URI leaves the previous value untouched? It+        // simply does not match a path, so cwd stays None here.+        let mut t2 = Term::new(20, 1);+        feed(&mut t2, b"\x1b]7;file://host\x07");+        assert_eq!(t2.cwd(), None);+    }++    #[test]     fn title_via_osc() {         let mut t = Term::new(20, 1);         feed(&mut t, b"\x1b]0;hello\x07");diff --git a/src/wayland.rs b/src/wayland.rsindex 77263b7..03f9066 100644--- a/src/wayland.rs+++ b/src/wayland.rs@@ -638,6 +638,32 @@ impl App {             Action::FontDecrease => self.change_font_size(self.font_size.saturating_sub(1)),             Action::FontReset => self.change_font_size(self.config.main.font_size),             Action::Fullscreen => self.toggle_fullscreen(),+            Action::NewWindow => self.spawn_new_window(),+        }+    }++    /// Launch another beer process in the shell's reported working directory+    /// (OSC 7), inheriting the same config. The child is fully detached.+    fn spawn_new_window(&mut self) {+        let exe = match std::env::current_exe() {+            Ok(exe) => exe,+            Err(err) => {+                tracing::warn!("locate beer executable: {err}");+                return;+            }+        };+        let mut cmd = std::process::Command::new(exe);+        if let Some(path) = self.config_path.as_ref() {+            cmd.arg("--config").arg(path);+        }+        if let Some(cwd) = self.session.as_ref().and_then(|s| s.term.cwd()) {+            cmd.current_dir(cwd);+        }+        cmd.stdin(std::process::Stdio::null())+            .stdout(std::process::Stdio::null())+            .stderr(std::process::Stdio::null());+        if let Err(err) = cmd.spawn() {+            tracing::warn!("spawn new window: {err}");         }     }