aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-23 00:36:52 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-27 21:37:05 +0300
commita57b97285c3308fbdcc167ab3da9ae1dd78767b6 (patch)
tree25a0eb4df100ea9b09c7551fa7cc25f8d86027ef /src/platform
initial release
terminal workspace manager for AI coding agents. single rust binary, runs inside your existing terminal. workspaces, tiled panes, intelligent agent state detection. https://herdr.dev
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/fallback.rs11
-rw-r--r--src/platform/linux.rs27
-rw-r--r--src/platform/macos.rs98
-rw-r--r--src/platform/mod.rs19
4 files changed, 155 insertions, 0 deletions
diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs
new file mode 100644
index 00000000..735f7c11
--- /dev/null
+++ b/src/platform/fallback.rs
@@ -0,0 +1,11 @@
+use std::path::PathBuf;
+
+/// Unsupported platform stub.
+pub fn foreground_process_name(_child_pid: u32) -> Option<String> {
+ None
+}
+
+/// Unsupported platform stub.
+pub fn process_cwd(_pid: u32) -> Option<PathBuf> {
+ None
+}
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
new file mode 100644
index 00000000..001ef03e
--- /dev/null
+++ b/src/platform/linux.rs
@@ -0,0 +1,27 @@
+use std::path::PathBuf;
+
+/// Get the foreground process name for a given child PID.
+/// Uses /proc/<pid>/stat to find the tpgid, then /proc/<tpgid>/comm.
+pub fn foreground_process_name(child_pid: u32) -> Option<String> {
+ // /proc/<pid>/stat format: "pid (comm) state ppid pgrp session tty_nr tpgid ..."
+ // The (comm) field can contain spaces and parens, so we find the last ')' first.
+ let stat = std::fs::read_to_string(format!("/proc/{child_pid}/stat")).ok()?;
+ let rest = stat.get(stat.rfind(')')? + 2..)?;
+ let fields: Vec<&str> = rest.split_whitespace().collect();
+ // After (comm): state(0) ppid(1) pgrp(2) session(3) tty_nr(4) tpgid(5)
+ let tpgid: i32 = fields.get(5)?.parse().ok()?;
+ if tpgid <= 0 {
+ return None;
+ }
+ let comm = std::fs::read_to_string(format!("/proc/{tpgid}/comm")).ok()?;
+ Some(comm.trim().to_string())
+}
+
+/// Get the current working directory of a process.
+/// Uses /proc/<pid>/cwd symlink.
+pub fn process_cwd(pid: u32) -> Option<PathBuf> {
+ if pid == 0 {
+ return None;
+ }
+ std::fs::read_link(format!("/proc/{pid}/cwd")).ok()
+}
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
new file mode 100644
index 00000000..218d00e9
--- /dev/null
+++ b/src/platform/macos.rs
@@ -0,0 +1,98 @@
+use std::ffi::OsStr;
+use std::os::unix::ffi::OsStrExt;
+use std::path::PathBuf;
+
+/// Get the foreground process name for a given child PID.
+///
+/// Uses `proc_pidinfo(PROC_PIDTBSDINFO)` to read `e_tpgid` — the foreground
+/// process group ID of the child's controlling terminal. Then `proc_name()`
+/// to resolve that PID to a short command name.
+///
+/// This is the macOS equivalent of Linux's `/proc/{pid}/stat` → tpgid → `/proc/{tpgid}/comm`.
+pub fn foreground_process_name(child_pid: u32) -> Option<String> {
+ if child_pid == 0 {
+ return None;
+ }
+
+ // Get BSD info for the child process — contains e_tpgid
+ let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
+ let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
+
+ let ret = unsafe {
+ libc::proc_pidinfo(
+ child_pid as libc::c_int,
+ libc::PROC_PIDTBSDINFO,
+ 0,
+ &mut info as *mut _ as *mut libc::c_void,
+ size,
+ )
+ };
+
+ if ret != size {
+ return None;
+ }
+
+ let fg_pid = info.e_tpgid;
+ if fg_pid == 0 {
+ return None;
+ }
+
+ // Resolve the foreground PID to a process name
+ let mut buf = [0u8; libc::MAXCOMLEN + 1];
+ let len = unsafe {
+ libc::proc_name(
+ fg_pid as libc::c_int,
+ buf.as_mut_ptr() as *mut libc::c_void,
+ buf.len() as u32,
+ )
+ };
+
+ if len <= 0 {
+ return None;
+ }
+
+ let len = len as usize;
+ let end = buf[..len].iter().position(|&b| b == 0).unwrap_or(len);
+ Some(String::from_utf8_lossy(&buf[..end]).into_owned())
+}
+
+/// Get the current working directory of a process.
+///
+/// Uses `proc_pidinfo(PROC_PIDVNODEPATHINFO)` to read `pvi_cdir.vip_path`.
+pub fn process_cwd(pid: u32) -> Option<PathBuf> {
+ if pid == 0 {
+ return None;
+ }
+
+ let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() };
+ let size = std::mem::size_of::<libc::proc_vnodepathinfo>() as libc::c_int;
+
+ let ret = unsafe {
+ libc::proc_pidinfo(
+ pid as libc::c_int,
+ libc::PROC_PIDVNODEPATHINFO,
+ 0,
+ &mut pathinfo as *mut _ as *mut libc::c_void,
+ size,
+ )
+ };
+
+ if ret != size {
+ return None;
+ }
+
+ // vip_path is [[c_char; 32]; 32] in libc (workaround for old Rust const generics).
+ // Reinterpret as flat bytes (total MAXPATHLEN = 1024).
+ let vip_path = unsafe {
+ std::slice::from_raw_parts(
+ pathinfo.pvi_cdir.vip_path.as_ptr() as *const u8,
+ libc::MAXPATHLEN as usize,
+ )
+ };
+
+ let nul = vip_path.iter().position(|&b| b == 0)?;
+ if nul == 0 {
+ return None;
+ }
+ Some(PathBuf::from(OsStr::from_bytes(&vip_path[..nul])))
+}
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
new file mode 100644
index 00000000..df8869c7
--- /dev/null
+++ b/src/platform/mod.rs
@@ -0,0 +1,19 @@
+//! Platform-specific process and filesystem operations.
+//!
+//! Centralizes OS-dependent behavior behind a clean boundary so core
+//! modules don't scatter `#[cfg]` branches through product logic.
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "linux")]
+pub use linux::*;
+
+#[cfg(target_os = "macos")]
+mod macos;
+#[cfg(target_os = "macos")]
+pub use macos::*;
+
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
+mod fallback;
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
+pub use fallback::*;