1use std::collections::HashMap;
9use std::sync::OnceLock;
10
11pub fn display_name(key: &str, desktop: &HashMap<String, String>, comm: Option<&str>) -> String {
19 let base = key.split('@').next().unwrap_or(key);
20 let program = program_name(base, comm);
21 if let Some(name) = desktop.get(program).or_else(|| desktop.get(base)) {
22 return name.clone();
23 }
24 if program.is_empty() {
25 return "An app".to_string();
26 }
27 let mut chars = program.chars();
28 match chars.next() {
29 Some(first) => first.to_uppercase().chain(chars).collect(),
30 None => String::new(),
31 }
32}
33
34pub fn program_name<'a>(exe: &'a str, comm: Option<&'a str>) -> &'a str {
38 let has_letters = |s: &str| s.chars().any(char::is_alphabetic);
39 match comm {
40 Some(c) if !has_letters(exe) && has_letters(c) => c.trim(),
41 _ => exe,
42 }
43}
44
45pub fn desktop_names() -> &'static HashMap<String, String> {
48 static NAMES: OnceLock<HashMap<String, String>> = OnceLock::new();
49 NAMES.get_or_init(crate::desktop::names_by_program)
50}
51
52pub fn friendly_name(exe: &str, comm: Option<&str>) -> String {
55 display_name(exe, desktop_names(), comm)
56}
57
58pub fn exe_of_pid(pid: u32) -> Option<String> {
62 if pid == 0 {
63 return None;
64 }
65 if let Some(exe) = crate::guard::cgfs::exe_basename(pid).filter(|e| !e.is_empty()) {
66 return Some(exe);
67 }
68 let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
69 argv0_basename(&cmdline)
70}
71
72fn argv0_basename(cmdline: &[u8]) -> Option<String> {
74 let first = cmdline.split(|b| *b == 0).next()?;
75 let first = std::str::from_utf8(first).ok()?;
76 let base = first.rsplit('/').next()?.trim();
77 (!base.is_empty()).then(|| base.to_string())
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn display_names() {
86 let mut desktop = HashMap::new();
87 desktop.insert("code".to_string(), "Visual Studio Code".to_string());
88 desktop.insert("claude".to_string(), "Claude".to_string());
89 assert_eq!(display_name("code", &desktop, None), "Visual Studio Code");
90 assert_eq!(display_name("firefox", &desktop, None), "Firefox");
91 assert_eq!(display_name("node@app-x.scope", &desktop, None), "Node");
92 assert_eq!(
93 display_name("python3@run-u12.service", &desktop, None),
94 "Python3"
95 );
96 assert_eq!(
97 display_name("2.1.283", &HashMap::new(), Some("claude")),
98 "Claude"
99 );
100 assert_eq!(display_name("2.1.283", &desktop, Some("claude")), "Claude");
101 assert_eq!(display_name("2.1.283", &desktop, None), "2.1.283");
102 assert_eq!(
103 display_name("firefox", &desktop, Some("Isolated Web Co")),
104 "Firefox",
105 "comm is only used when the basename has no letters"
106 );
107 assert_eq!(display_name("élan", &desktop, None), "Élan");
108 assert_eq!(display_name("", &desktop, None), "An app");
109 }
110
111 #[test]
112 fn program_names() {
113 assert_eq!(program_name("chrome", Some("chrome")), "chrome");
114 assert_eq!(
115 program_name("gnome-calculator", Some("gnome-calculato")),
116 "gnome-calculator"
117 );
118 assert_eq!(program_name("2.1.284", Some("claude")), "claude");
119 assert_eq!(program_name("2.1.284", Some("1.0")), "2.1.284");
120 assert_eq!(program_name("2.1.284", None), "2.1.284");
121 }
122
123 #[test]
124 fn argv0_is_the_basename_of_the_first_argument() {
125 assert_eq!(
126 argv0_basename(b"/usr/bin/gnome-calculator\0--flag\0").as_deref(),
127 Some("gnome-calculator")
128 );
129 assert_eq!(
130 argv0_basename(b"python3\0x.py\0").as_deref(),
131 Some("python3")
132 );
133 assert_eq!(argv0_basename(b""), None);
134 assert_eq!(argv0_basename(b"\0"), None);
135 }
136
137 #[test]
138 fn our_own_exe_is_found() {
139 let me = exe_of_pid(std::process::id()).expect("own exe");
140 assert!(!me.is_empty());
141 assert_eq!(exe_of_pid(0), None);
142 }
143}