swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-04-25 15:53:24 -0400
committermain <main@swarm.moe>2026-04-25 15:53:24 -0400
commitfa8140a124d083d4d936174d4e300b3b0d085ec4 (patch)
tree8889576b0ae6d0afc1e730e682533e519458723f /crates
parent0d04617ce57ae4925d6bdf91fc40a9e50e971149 (diff)
downloadmemview-fa8140a124d083d4d936174d4e300b3b0d085ec4.zip
Show process working directories
Diffstat (limited to 'crates')
-rw-r--r--crates/memview/src/linux/app/rows.rs4
-rw-r--r--crates/memview/src/linux/app/tests.rs61
-rw-r--r--crates/memview/src/linux/model.rs22
-rw-r--r--crates/memview/src/linux/probe.rs13
-rw-r--r--crates/memview/src/linux/ui.rs13
5 files changed, 105 insertions, 8 deletions
diff --git a/crates/memview/src/linux/app/rows.rs b/crates/memview/src/linux/app/rows.rs
index d6d904d..740c3e3 100644
--- a/crates/memview/src/linux/app/rows.rs
+++ b/crates/memview/src/linux/app/rows.rs
@@ -313,6 +313,10 @@ fn push_process_search_tree(
fn process_matches(search: &Search, node: &ProcessNode) -> bool {
search.matches(&node.name)
|| search.matches(&node.command)
+ || node
+ .cwd
+ .as_ref()
+ .is_some_and(|cwd| search.matches(cwd.as_str()))
|| search.matches(&node.username)
|| search.matches(&node.state)
|| search.matches(&node.pid.to_string())
diff --git a/crates/memview/src/linux/app/tests.rs b/crates/memview/src/linux/app/tests.rs
index 583863a..62230e7 100644
--- a/crates/memview/src/linux/app/tests.rs
+++ b/crates/memview/src/linux/app/tests.rs
@@ -1,3 +1,4 @@
+use super::super::model::{MemoryRollup, ProcessCwd, ProcessTree, ProcessTreeStats};
use super::*;
#[derive(Clone, Debug)]
@@ -60,7 +61,7 @@ fn tmpfs_snapshot(mounts: Vec<TmpfsMount>) -> Snapshot {
elapsed: Duration::ZERO,
meminfo: Meminfo::default(),
overview: super::super::model::Overview::default(),
- process_tree: super::super::model::ProcessTree::default(),
+ process_tree: ProcessTree::default(),
shared_objects: Vec::new(),
sysv_segments: Vec::new(),
tmpfs_mounts: mounts,
@@ -68,6 +69,48 @@ fn tmpfs_snapshot(mounts: Vec<TmpfsMount>) -> Snapshot {
}
}
+fn process_snapshot(nodes: Vec<ProcessNode>) -> Snapshot {
+ Snapshot {
+ captured_at: SystemTime::UNIX_EPOCH,
+ elapsed: Duration::ZERO,
+ meminfo: Meminfo::default(),
+ overview: super::super::model::Overview::default(),
+ process_tree: ProcessTree {
+ roots: (0..nodes.len()).collect(),
+ nodes,
+ stats: ProcessTreeStats::default(),
+ },
+ shared_objects: Vec::new(),
+ sysv_segments: Vec::new(),
+ tmpfs_mounts: Vec::new(),
+ warnings: Vec::new(),
+ }
+}
+
+fn process_node(pid: i32, command: &str, cwd: Option<&str>) -> ProcessNode {
+ let rollup = MemoryRollup {
+ pss: Bytes(1024),
+ rss: Bytes(1024),
+ ..MemoryRollup::default()
+ };
+ ProcessNode {
+ pid: Pid(pid),
+ ppid: None,
+ name: format!("p{pid}"),
+ command: command.to_string(),
+ cwd: cwd.map(|path| ProcessCwd::new(PathBuf::from(path))),
+ username: "test".to_string(),
+ state: "S".to_string(),
+ threads: 1,
+ rollup,
+ subtree: rollup,
+ children: Vec::new(),
+ objects: Vec::new(),
+ rollup_state: LedgerState::Exact,
+ mappings_state: LedgerState::Deferred,
+ }
+}
+
fn regex(pattern: &str) -> Search {
Search::compile(pattern.to_string())
.expect("test regex compiles")
@@ -134,6 +177,22 @@ fn fold_policy_respects_roots_leaves_and_manual_overrides() {
}
#[test]
+fn process_search_matches_cwd() {
+ let mut app = App::new();
+ app.snapshot = Some(process_snapshot(vec![process_node(
+ 42,
+ "rust-analyzer",
+ Some("/home/main/programming/projects/memview"),
+ )]));
+ app.search = Some(regex("memview"));
+
+ app.rebuild_process_rows();
+
+ assert_eq!(app.process_rows().len(), 1);
+ assert_eq!(app.process_rows()[0].pid, Pid(42));
+}
+
+#[test]
fn pane_rows_can_select_deleted_row_successor_slot() {
let mut rows = PaneRows::default();
rows.install(vec![TestRow(1), TestRow(2), TestRow(3)]);
diff --git a/crates/memview/src/linux/model.rs b/crates/memview/src/linux/model.rs
index a680332..421b6f8 100644
--- a/crates/memview/src/linux/model.rs
+++ b/crates/memview/src/linux/model.rs
@@ -313,6 +313,7 @@ pub struct ProcessNode {
pub ppid: Option<Pid>,
pub name: String,
pub command: String,
+ pub cwd: Option<ProcessCwd>,
pub username: String,
pub state: String,
pub threads: u32,
@@ -335,6 +336,27 @@ impl ProcessNode {
}
}
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ProcessCwd(String);
+
+impl ProcessCwd {
+ #[must_use]
+ pub fn new(path: PathBuf) -> Self {
+ Self(path.display().to_string())
+ }
+
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl Display for ProcessCwd {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
#[derive(Clone, Debug, Default)]
pub struct ProcessTree {
pub roots: Vec<usize>,
diff --git a/crates/memview/src/linux/probe.rs b/crates/memview/src/linux/probe.rs
index b47a5fa..93d4d6a 100644
--- a/crates/memview/src/linux/probe.rs
+++ b/crates/memview/src/linux/probe.rs
@@ -1,7 +1,7 @@
use super::model::{
Bytes, LedgerState, Meminfo, MeminfoEntry, MemoryRollup, Metric, ObjectConsumer, ObjectKind,
- ObjectUsage, Overview, Pid, ProcessNode, ProcessTree, ProcessTreeStats, SharedObject, Snapshot,
- SysvSegment, TmpfsMount, TmpfsNode, TmpfsNodeKind,
+ ObjectUsage, Overview, Pid, ProcessCwd, ProcessNode, ProcessTree, ProcessTreeStats,
+ SharedObject, Snapshot, SysvSegment, TmpfsMount, TmpfsNode, TmpfsNodeKind,
};
use color_eyre::eyre::{Context, Result, eyre};
use std::cmp::Reverse;
@@ -452,6 +452,7 @@ struct ScannedProcess {
ppid: Option<Pid>,
name: String,
command: String,
+ cwd: Option<ProcessCwd>,
username: String,
state: String,
threads: u32,
@@ -515,6 +516,7 @@ fn scan_process_shell(
let status = parse_status(&status_text);
let command = read_cmdline(&root).unwrap_or_else(|| status.name.clone());
+ let cwd = read_cwd(&root);
let username = lookup_username(status.uid, usernames);
let fallback_rollup = MemoryRollup {
rss: status.vm_rss,
@@ -536,6 +538,7 @@ fn scan_process_shell(
ppid: status.ppid,
name: status.name,
command,
+ cwd,
username,
state: status.state,
threads: status.threads,
@@ -677,6 +680,10 @@ fn read_cmdline(root: &Path) -> Option<String> {
}
}
+fn read_cwd(root: &Path) -> Option<ProcessCwd> {
+ fs::read_link(root.join("cwd")).ok().map(ProcessCwd::new)
+}
+
fn lookup_username(uid: u32, cache: &mut BTreeMap<u32, String>) -> String {
cache
.entry(uid)
@@ -947,6 +954,7 @@ fn build_process_tree(processes: Vec<ScannedProcess>, stats: ProcessTreeStats) -
ppid: process.ppid,
name: process.name,
command: process.command,
+ cwd: process.cwd,
username: process.username,
state: process.state,
threads: process.threads,
@@ -1354,6 +1362,7 @@ mod tests {
ppid: None,
name: format!("p{pid}"),
command: format!("p{pid} --serve"),
+ cwd: None,
username: "test".to_string(),
state: "S".to_string(),
threads: 1,
diff --git a/crates/memview/src/linux/ui.rs b/crates/memview/src/linux/ui.rs
index 8d0b8a2..853d839 100644
--- a/crates/memview/src/linux/ui.rs
+++ b/crates/memview/src/linux/ui.rs
@@ -295,7 +295,7 @@ fn render_processes(frame: &mut Frame<'_>, app: &App, snapshot: &Snapshot, area:
.split(area);
let right = Layout::default()
.direction(Direction::Vertical)
- .constraints([Constraint::Length(12), Constraint::Min(8)])
+ .constraints([Constraint::Length(16), Constraint::Min(8)])
.split(columns[1]);
if snapshot.process_tree.nodes.is_empty() && app.process_scan_started_at.is_some() {
@@ -357,11 +357,14 @@ fn render_processes(frame: &mut Frame<'_>, app: &App, snapshot: &Snapshot, area:
if let Some(process) = app.selected_process() {
let mut details = search_summary_lines(app, capacity);
+ details.push(Line::from(vec![Span::styled(
+ process.title(),
+ Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
+ )]));
+ if let Some(cwd) = &process.cwd {
+ details.push(detail_line("CWD", cwd.as_str()));
+ }
details.extend([
- Line::from(vec![Span::styled(
- process.title(),
- Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
- )]),
detail_line("State", &process.state),
detail_line("Threads", &process.threads.to_string()),
detail_line("PSS", &process.rollup.pss.human_exact()),