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,
|
FontDecrease,
|
||||||
FontReset,
|
FontReset,
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
|
NewWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
|
|
@ -37,6 +38,7 @@ impl Action {
|
||||||
"font-decrease" => Self::FontDecrease,
|
"font-decrease" => Self::FontDecrease,
|
||||||
"font-reset" => Self::FontReset,
|
"font-reset" => Self::FontReset,
|
||||||
"fullscreen" => Self::Fullscreen,
|
"fullscreen" => Self::Fullscreen,
|
||||||
|
"new-window" => Self::NewWindow,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -171,6 +173,7 @@ const DEFAULT_BINDINGS: &[(&str, &str)] = &[
|
||||||
("Ctrl+minus", "font-decrease"),
|
("Ctrl+minus", "font-decrease"),
|
||||||
("Ctrl+0", "font-reset"),
|
("Ctrl+0", "font-reset"),
|
||||||
("F11", "fullscreen"),
|
("F11", "fullscreen"),
|
||||||
|
("Ctrl+Shift+N", "new-window"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Map a key token to a keysym: a single character, or a named special key.
|
/// 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,
|
theme: Theme,
|
||||||
/// Set when the child rings the bell (`BEL`); cleared by the front-end.
|
/// Set when the child rings the bell (`BEL`); cleared by the front-end.
|
||||||
bell: bool,
|
bell: bool,
|
||||||
|
/// Working directory reported by the shell via OSC 7, for new windows.
|
||||||
|
cwd: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Term {
|
impl Term {
|
||||||
|
|
@ -118,9 +120,15 @@ impl Term {
|
||||||
clipboard_ops: Vec::new(),
|
clipboard_ops: Vec::new(),
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
bell: false,
|
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.
|
/// Take and clear the pending bell flag.
|
||||||
pub fn take_bell(&mut self) -> bool {
|
pub fn take_bell(&mut self) -> bool {
|
||||||
std::mem::take(&mut self.bell)
|
std::mem::take(&mut self.bell)
|
||||||
|
|
@ -653,6 +661,12 @@ impl Perform for Term {
|
||||||
self.title = Some(String::from_utf8_lossy(text).into_owned());
|
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).
|
// OSC 4: set/query palette entries (pairs of index;spec).
|
||||||
Some(&n) if n == b"4" => self.osc_palette(params, bell),
|
Some(&n) if n == b"4" => self.osc_palette(params, bell),
|
||||||
// OSC 104: reset palette (all, or the listed indices).
|
// OSC 104: reset palette (all, or the listed indices).
|
||||||
|
|
@ -796,6 +810,38 @@ fn base64_decode(data: &[u8]) -> Option<Vec<u8>> {
|
||||||
Some(out)
|
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.
|
/// Decode an even-length lowercase/uppercase hex string into bytes.
|
||||||
fn decode_hex(s: &[u8]) -> Option<Vec<u8>> {
|
fn decode_hex(s: &[u8]) -> Option<Vec<u8>> {
|
||||||
if s.is_empty() || !s.len().is_multiple_of(2) {
|
if s.is_empty() || !s.len().is_multiple_of(2) {
|
||||||
|
|
@ -1068,6 +1114,18 @@ mod tests {
|
||||||
assert_eq!(t.grid().row_text(0), "─│");
|
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]
|
#[test]
|
||||||
fn title_via_osc() {
|
fn title_via_osc() {
|
||||||
let mut t = Term::new(20, 1);
|
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::FontDecrease => self.change_font_size(self.font_size.saturating_sub(1)),
|
||||||
Action::FontReset => self.change_font_size(self.config.main.font_size),
|
Action::FontReset => self.change_font_size(self.config.main.font_size),
|
||||||
Action::Fullscreen => self.toggle_fullscreen(),
|
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