1pub use crate::desktop::DesktopNames;
9use std::path::{Path, PathBuf};
10use std::sync::OnceLock;
11
12pub fn display_name(
21 key: &str,
22 desktop: &DesktopNames,
23 comm: Option<&str>,
24 exe_dir: Option<&Path>,
25) -> String {
26 let base = key.split('@').next().unwrap_or(key);
27 let program = program_name(base, comm);
28 let name = desktop
29 .programs
30 .get(program)
31 .or_else(|| desktop.programs.get(base))
32 .or_else(|| {
33 let runtime = crate::desktop::is_launcher(program);
34 exe_dir
35 .filter(|_| !runtime)
36 .and_then(|d| desktop.dirs.get(d))
37 });
38 if let Some(name) = name {
39 return name.clone();
40 }
41 if program.is_empty() {
42 return "An app".to_string();
43 }
44 let mut chars = program.chars();
45 match chars.next() {
46 Some(first) => first.to_uppercase().chain(chars).collect(),
47 None => String::new(),
48 }
49}
50
51pub fn program_name<'a>(exe: &'a str, comm: Option<&'a str>) -> &'a str {
55 let has_letters = |s: &str| s.chars().any(char::is_alphabetic);
56 match comm {
57 Some(c) if !has_letters(exe) && has_letters(c) => c.trim(),
58 _ => exe,
59 }
60}
61
62static NAMES: OnceLock<DesktopNames> = OnceLock::new();
63
64pub fn desktop_names() -> &'static DesktopNames {
68 NAMES.get_or_init(crate::desktop::installed_names)
69}
70
71pub fn loaded_desktop_names() -> Option<&'static DesktopNames> {
74 NAMES.get()
75}
76
77pub fn friendly_name(exe: &str, comm: Option<&str>, exe_dir: Option<&Path>) -> String {
82 let desktop = loaded_desktop_names();
83 display_name(
84 exe,
85 desktop.unwrap_or(&DesktopNames::default()),
86 comm,
87 exe_dir,
88 )
89}
90
91pub fn exe_of_pid(pid: u32) -> Option<String> {
95 if pid == 0 {
96 return None;
97 }
98 if let Some(exe) = crate::guard::cgfs::exe_basename(pid).filter(|e| !e.is_empty()) {
99 return Some(exe);
100 }
101 let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
102 argv0_basename(&cmdline)
103}
104
105pub fn exe_dir_of_pid(pid: u32) -> Option<PathBuf> {
107 if pid == 0 {
108 return None;
109 }
110 let exe = std::fs::read_link(format!("/proc/{pid}/exe")).ok()?;
111 exe.parent().map(Path::to_path_buf)
112}
113
114fn argv0_basename(cmdline: &[u8]) -> Option<String> {
116 let first = cmdline.split(|b| *b == 0).next()?;
117 let first = std::str::from_utf8(first).ok()?;
118 let base = first.rsplit('/').next()?.trim();
119 (!base.is_empty()).then(|| base.to_string())
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 use std::collections::HashMap;
127
128 fn programs(pairs: &[(&str, &str)]) -> DesktopNames {
129 DesktopNames {
130 programs: pairs
131 .iter()
132 .map(|(p, n)| (p.to_string(), n.to_string()))
133 .collect(),
134 dirs: HashMap::new(),
135 }
136 }
137
138 #[test]
139 fn display_names() {
140 let desktop = programs(&[("code", "Visual Studio Code"), ("claude", "Claude")]);
141 let none = DesktopNames::default();
142 let name = |key: &str, desktop: &DesktopNames, comm| display_name(key, desktop, comm, None);
143 assert_eq!(name("code", &desktop, None), "Visual Studio Code");
144 assert_eq!(name("firefox", &desktop, None), "Firefox");
145 assert_eq!(name("node@app-x.scope", &desktop, None), "Node");
146 assert_eq!(name("python3@run-u12.service", &desktop, None), "Python3");
147 assert_eq!(name("2.1.283", &none, Some("claude")), "Claude");
148 assert_eq!(name("2.1.283", &desktop, Some("claude")), "Claude");
149 assert_eq!(name("2.1.283", &desktop, None), "2.1.283");
150 assert_eq!(
151 name("firefox", &desktop, Some("Isolated Web Co")),
152 "Firefox",
153 "comm is only used when the basename has no letters"
154 );
155 assert_eq!(name("élan", &desktop, None), "Élan");
156 assert_eq!(name("", &desktop, None), "An app");
157 }
158
159 #[test]
160 fn an_app_is_named_by_its_install_directory_after_its_program() {
161 let mut desktop = programs(&[
164 ("google-chrome-stable", "Google Chrome"),
165 ("chrome-tool", "Chrome Tool"),
166 ]);
167 desktop
168 .dirs
169 .insert(PathBuf::from("/opt/google/chrome"), "Google Chrome".into());
170 let chrome = Some(Path::new("/opt/google/chrome"));
171 assert_eq!(
172 display_name("chrome", &desktop, Some("chrome"), chrome),
173 "Google Chrome"
174 );
175 assert_eq!(
176 display_name("chrome_crashpad_handler", &desktop, None, chrome),
177 "Google Chrome"
178 );
179 assert_eq!(
180 display_name("chrome-tool", &desktop, None, chrome),
181 "Chrome Tool",
182 "a program's own entry comes first"
183 );
184 assert_eq!(display_name("chrome", &desktop, None, None), "Chrome");
185 desktop.dirs.insert(
187 PathBuf::from("/usr/lib/jvm/java-17/bin"),
188 "OpenJDK 17 Monitoring & Management Console".into(),
189 );
190 let jvm = Some(Path::new("/usr/lib/jvm/java-17/bin"));
191 assert_eq!(display_name("java", &desktop, Some("java"), jvm), "Java");
192 assert_eq!(
193 display_name(
194 "electron30",
195 &desktop,
196 None,
197 Some(Path::new("/opt/google/chrome"))
198 ),
199 "Electron30"
200 );
201 assert_eq!(
202 display_name("chrome", &desktop, None, Some(Path::new("/opt/other"))),
203 "Chrome"
204 );
205 }
206
207 #[test]
208 fn program_names() {
209 assert_eq!(program_name("chrome", Some("chrome")), "chrome");
210 assert_eq!(
211 program_name("gnome-calculator", Some("gnome-calculato")),
212 "gnome-calculator"
213 );
214 assert_eq!(program_name("2.1.284", Some("claude")), "claude");
215 assert_eq!(program_name("2.1.284", Some("1.0")), "2.1.284");
216 assert_eq!(program_name("2.1.284", None), "2.1.284");
217 }
218
219 #[test]
220 fn argv0_is_the_basename_of_the_first_argument() {
221 assert_eq!(
222 argv0_basename(b"/usr/bin/gnome-calculator\0--flag\0").as_deref(),
223 Some("gnome-calculator")
224 );
225 assert_eq!(
226 argv0_basename(b"python3\0x.py\0").as_deref(),
227 Some("python3")
228 );
229 assert_eq!(argv0_basename(b""), None);
230 assert_eq!(argv0_basename(b"\0"), None);
231 }
232
233 #[test]
234 fn our_own_exe_is_found() {
235 let me = exe_of_pid(std::process::id()).expect("own exe");
236 assert!(!me.is_empty());
237 assert_eq!(exe_of_pid(0), None);
238 }
239}