From 349f38b31c1216bdd40571279b343d8c3068803d Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Tue, 8 Sep 2026 04:18:01 +0200 Subject: [PATCH] rounded panes, cwd fix mac --- result | 2 +- src/app.rs | 21 ++++++++++++--- src/pane.rs | 20 +++++++++++++-- src/pty.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/result b/result index ff4d06d..f6fbe7b 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/6vj9g5xzxd0whdnxd0zsg02m1rwsrlrn-slerm-0.1.0 \ No newline at end of file +/nix/store/3b7xvjwsg42myksf85lyj1x89269p0sb-slerm-0.1.0 \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 8dbe636..22e547d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,7 @@ use crate::{ render::color, }; use anyhow::{Result, ensure}; -use egui::{Color32, Rect, Stroke, vec2}; +use egui::{Color32, CornerRadius, Rect, Stroke, vec2}; use std::{ collections::{BTreeMap, HashSet}, path::PathBuf, @@ -543,6 +543,9 @@ fn grab_keys(ctx: &egui::Context) { }); } +/// Corner rounding shared by a pane's border and its title bar. +const CORNER: u8 = 5; + fn show_pane( ui: &mut egui::Ui, id: PaneId, @@ -553,9 +556,21 @@ fn show_pane( ) -> bool { let accent = color(config::rgb(&config.accent).unwrap()); let title_rect = Rect::from_min_size(rect.min, vec2(rect.width(), 25.0_f32.min(rect.height()))); + // The title bar only rounds off the top of the pane, unless the pane is so + // short that the bar is all there is of it. + let bottom = if title_rect.height() < rect.height() { + 0 + } else { + CORNER + }; ui.painter().rect_filled( title_rect, - 0, + CornerRadius { + nw: CORNER, + ne: CORNER, + sw: bottom, + se: bottom, + }, Color32::from_gray(if active { 43 } else { 31 }), ); ui.painter() @@ -585,7 +600,7 @@ fn show_pane( } ui.painter().rect_stroke( rect, - 0, + CORNER, Stroke::new( 1.0_f32, if active { diff --git a/src/pane.rs b/src/pane.rs index 849ff04..482a819 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -46,13 +46,29 @@ fn local_pwd(pwd: &str) -> Option { return pwd.starts_with('/').then(|| PathBuf::from(pwd)); }; let (host, path) = rest.split_once('/')?; + // A shell may report either the short name or the FQDN, so compare the leading + // label of each. + let short = |name: &str| name.split('.').next().unwrap_or(name).to_ascii_lowercase(); let local = host.is_empty() || host.eq_ignore_ascii_case("localhost") - || std::fs::read_to_string("/proc/sys/kernel/hostname") - .is_ok_and(|name| name.trim().eq_ignore_ascii_case(host)); + || hostname().is_some_and(|name| short(&name) == short(host)); local.then(|| PathBuf::from(percent_decode(&format!("/{path}")))) } +fn hostname() -> Option { + unsafe extern "C" { + fn gethostname(name: *mut std::ffi::c_char, len: usize) -> std::ffi::c_int; + } + let mut buf = [0u8; 256]; + // SAFETY: at most `len` bytes are written, leaving the final byte of `buf` as + // the terminator even when the name is truncated. + if unsafe { gethostname(buf.as_mut_ptr().cast(), buf.len() - 1) } != 0 { + return None; + } + let end = buf.iter().position(|&b| b == 0)?; + String::from_utf8(buf[..end].to_vec()).ok() +} + fn percent_decode(path: &str) -> String { let mut out = Vec::with_capacity(path.len()); let mut bytes = path.bytes(); diff --git a/src/pty.rs b/src/pty.rs index e80a594..6b15bea 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -111,11 +111,10 @@ impl Pty { }) } - /// The working directory of the foreground process group, via /proc. Shells - /// that emit OSC 7 are reported by the terminal instead; this covers the rest. + /// The working directory of the foreground process group. Shells that emit + /// OSC 7 are reported by the terminal instead; this covers the rest. pub fn cwd(&self) -> Option { - let pid = self.master.process_group_leader()?; - std::fs::read_link(format!("/proc/{pid}/cwd")).ok() + process_cwd(self.master.process_group_leader()?) } pub fn resize(&self, cols: u16, rows: u16, cw: u32, ch: u32) -> Result<()> { @@ -129,6 +128,72 @@ impl Pty { } } +#[cfg(target_os = "linux")] +fn process_cwd(pid: i32) -> Option { + std::fs::read_link(format!("/proc/{pid}/cwd")).ok() +} + +/// macOS has no /proc, so the current directory comes from `proc_pidinfo`. +#[cfg(target_os = "macos")] +fn process_cwd(pid: i32) -> Option { + use std::{ffi::CStr, os::unix::ffi::OsStrExt}; + + // . `vinfo_stat` is spelled as its 136 bytes of padding + // because only the path that follows it is of interest here. + #[repr(C)] + struct VnodeInfo { + stat: [u64; 17], + vtype: i32, + pad: i32, + fsid: [i32; 2], + } + #[repr(C)] + struct VnodeInfoPath { + info: VnodeInfo, + path: [std::ffi::c_char; 1024], + } + #[repr(C)] + struct VnodePathInfo { + cdir: VnodeInfoPath, + rdir: VnodeInfoPath, + } + const PROC_PIDVNODEPATHINFO: i32 = 9; + + unsafe extern "C" { + fn proc_pidinfo( + pid: i32, + flavor: i32, + arg: u64, + buffer: *mut std::ffi::c_void, + buffersize: i32, + ) -> i32; + } + + let size = size_of::(); + // SAFETY: the call writes at most `size` bytes into `info`, and reports how + // many it wrote. A short write leaves the path unset, so it is rejected. + unsafe { + let mut info = std::mem::zeroed::(); + let written = proc_pidinfo( + pid, + PROC_PIDVNODEPATHINFO, + 0, + (&raw mut info).cast(), + size as i32, + ); + if written < size as i32 { + return None; + } + let path = CStr::from_ptr(info.cdir.path.as_ptr()); + (!path.is_empty()).then(|| PathBuf::from(std::ffi::OsStr::from_bytes(path.to_bytes()))) + } +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn process_cwd(_pid: i32) -> Option { + None +} + impl Drop for Pty { fn drop(&mut self) { let _ = self.shutdown.send(());