forked from NotAShelf/beer
vt: track cwd via OSC 7; add a new-window action
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I498a1938ca0129d10cf0e230d27188ed6a6a6964
This commit is contained in:
parent
a5249b2315
commit
0c0da3d035
3 changed files with 87 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
58
src/vt.rs
58
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) {
|
||||
|
|
@ -1068,6 +1114,18 @@ mod tests {
|
|||
assert_eq!(t.grid().row_text(0), "─│");
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue