retch_sysinfo/
terminal.rs1use sysinfo::System;
7
8pub(crate) fn detect_terminal(sys: &System) -> Option<String> {
9 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
10 if !prog.is_empty() {
11 return Some(prog);
12 }
13 }
14 if let Ok(prog) = std::env::var("TERMINAL_EMULATOR") {
15 if !prog.is_empty() {
16 return Some(prog);
17 }
18 }
19 if std::env::var("ALACRITTY_LOG").is_ok() || std::env::var("ALACRITTY_WINDOW_ID").is_ok() {
20 return Some("alacritty".to_string());
21 }
22
23 let current_pid = sysinfo::Pid::from_u32(std::process::id());
24 let mut current_proc = sys.process(current_pid);
25
26 let known_terms = [
27 "kitty",
28 "alacritty",
29 "wezterm",
30 "gnome-terminal",
31 "konsole",
32 "iterm2",
33 "Terminal",
34 "rio",
35 "foot",
36 "tilix",
37 "xfce4-terminal",
38 "terminator",
39 "st",
40 "urxvt",
41 "ptyxis",
42 ];
43
44 let mut depth = 0;
45 while let Some(proc) = current_proc {
46 if depth > 5 {
47 break;
48 }
49 let name = proc.name().to_string_lossy().to_lowercase();
50 for term in &known_terms {
51 if name == *term || name.ends_with(term) || (name.contains(term) && term.len() > 3) {
52 return Some(term.to_string());
53 }
54 }
55 if let Some(parent_pid) = proc.parent() {
56 current_proc = sys.process(parent_pid);
57 } else {
58 break;
59 }
60 depth += 1;
61 }
62
63 if let Ok(term) = std::env::var("TERM") {
64 if term != "xterm-256color" && term != "xterm" && term != "linux" && term != "cygwin" {
65 if let Some(stripped) = term.strip_prefix("xterm-") {
66 return Some(stripped.to_string());
67 }
68 return Some(term);
69 }
70 }
71
72 None
73}
74
75pub(crate) fn detect_terminal_font(terminal: Option<&str>) -> Option<String> {
76 let term = terminal?;
77 let term_lower = term.to_lowercase();
78 let home = dirs::home_dir()?;
79
80 if term_lower.contains("kitty") {
81 let conf_path = home.join(".config/kitty/kitty.conf");
82 if let Ok(content) = std::fs::read_to_string(&conf_path) {
83 let mut family = None;
84 let mut size = None;
85 for line in content.lines() {
86 let line = line.trim();
87 if line.starts_with("font_family") {
88 let parts: Vec<&str> = line.split_whitespace().collect();
89 if parts.len() >= 2 {
90 family = Some(parts[1..].join(" "));
91 }
92 } else if line.starts_with("font_size") {
93 let parts: Vec<&str> = line.split_whitespace().collect();
94 if parts.len() >= 2 {
95 size = Some(parts[1].to_string());
96 }
97 }
98 }
99 match (family, size) {
100 (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
101 (Some(f), None) => return Some(f),
102 (None, Some(s)) => {
103 let fallback = crate::theme::get_default_monospace_font()
104 .unwrap_or_else(|| "Default".to_string());
105 return Some(format!("{} ({})", fallback, s));
106 }
107 (None, None) => {}
108 }
109 }
110 } else if term_lower.contains("alacritty") {
111 let paths = [
112 home.join(".config/alacritty/alacritty.toml"),
113 home.join(".config/alacritty/alacritty.yml"),
114 home.join(".alacritty.toml"),
115 home.join(".alacritty.yml"),
116 ];
117 for path in paths {
118 if let Ok(content) = std::fs::read_to_string(&path) {
119 let mut family = None;
120 let mut size = None;
121 for line in content.lines() {
122 let line = line.trim();
123 if line.starts_with("family") {
124 if let Some(idx) = line.find('=') {
125 let val = line[idx + 1..].trim().trim_matches('"').trim_matches('\'');
126 family = Some(val.to_string());
127 } else if let Some(idx) = line.find(':') {
128 let val = line[idx + 1..].trim().trim_matches('"').trim_matches('\'');
129 family = Some(val.to_string());
130 }
131 } else if line.starts_with("size") {
132 if let Some(idx) = line.find('=') {
133 size = Some(line[idx + 1..].trim().to_string());
134 } else if let Some(idx) = line.find(':') {
135 size = Some(line[idx + 1..].trim().to_string());
136 }
137 }
138 }
139 match (family, size) {
140 (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
141 (Some(f), None) => return Some(f),
142 (None, Some(s)) => {
143 let fallback = crate::theme::get_default_monospace_font()
144 .unwrap_or_else(|| "Default".to_string());
145 return Some(format!("{} ({})", fallback, s));
146 }
147 (None, None) => {}
148 }
149 }
150 }
151 } else if term_lower.contains("wezterm") {
152 let paths = [
153 home.join(".wezterm.lua"),
154 home.join(".config/wezterm/wezterm.lua"),
155 ];
156 for path in paths {
157 if let Ok(content) = std::fs::read_to_string(&path) {
158 let mut family = None;
159 let mut size = None;
160 for line in content.lines() {
161 if line.contains("wezterm.font") {
162 if let Some(start) = line.find("wezterm.font") {
163 let rest = &line[start..];
164 if let Some(quote1) = rest.find('\'').or(rest.find('"')) {
165 let quote_char = rest.chars().nth(quote1).unwrap();
166 if let Some(quote2) = rest[quote1 + 1..].find(quote_char) {
167 family =
168 Some(rest[quote1 + 1..quote1 + 1 + quote2].to_string());
169 }
170 }
171 }
172 }
173 if line.contains("font_size") {
174 if let Some(idx) = line.find('=') {
175 let val = line[idx + 1..].trim().trim_end_matches(',');
176 size = Some(val.to_string());
177 }
178 }
179 }
180 match (family, size) {
181 (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
182 (Some(f), None) => return Some(f),
183 (None, Some(s)) => {
184 let fallback = crate::theme::get_default_monospace_font()
185 .unwrap_or_else(|| "Default".to_string());
186 return Some(format!("{} ({})", fallback, s));
187 }
188 (None, None) => {}
189 }
190 }
191 }
192 } else if term_lower.contains("foot") {
193 let conf_path = home.join(".config/foot/foot.ini");
194 if let Ok(content) = std::fs::read_to_string(&conf_path) {
195 for line in content.lines() {
196 let line = line.trim();
197 if line.starts_with("font=") {
198 let val = line.trim_start_matches("font=");
199 let parts: Vec<&str> = val.split(':').collect();
200 let family = parts[0].trim();
201 let mut size = None;
202 for part in &parts[1..] {
203 if part.starts_with("size=") {
204 size = Some(part.trim_start_matches("size=").trim());
205 }
206 }
207 if let Some(s) = size {
208 return Some(format!("{} ({})", family, s));
209 } else {
210 return Some(family.to_string());
211 }
212 }
213 }
214 }
215 } else if term_lower.contains("ptyxis") {
216 #[cfg(target_os = "linux")]
217 if let Ok(output) = std::process::Command::new("gsettings")
218 .args(["get", "org.gnome.Ptyxis", "use-system-font"])
219 .output()
220 {
221 if output.status.success() {
222 let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
223 if s == "false" {
224 if let Ok(font_out) = std::process::Command::new("gsettings")
225 .args(["get", "org.gnome.Ptyxis", "font-name"])
226 .output()
227 {
228 if font_out.status.success() {
229 let mut font_str =
230 String::from_utf8_lossy(&font_out.stdout).trim().to_string();
231 font_str = font_str.trim_matches('\'').to_string();
232 if !font_str.is_empty() {
233 if let Some(last_space) = font_str.rfind(' ') {
234 let family = &font_str[..last_space];
235 let size = &font_str[last_space + 1..];
236 if size.chars().all(|c| c.is_ascii_digit() || c == '.') {
237 return Some(format!("{} ({})", family, size));
238 }
239 }
240 return Some(font_str);
241 }
242 }
243 }
244 } else {
245 if let Ok(font_out) = std::process::Command::new("gsettings")
246 .args(["get", "org.gnome.desktop.interface", "monospace-font-name"])
247 .output()
248 {
249 if font_out.status.success() {
250 let mut font_str =
251 String::from_utf8_lossy(&font_out.stdout).trim().to_string();
252 font_str = font_str.trim_matches('\'').to_string();
253 if !font_str.is_empty() {
254 if let Some(last_space) = font_str.rfind(' ') {
255 let family = &font_str[..last_space];
256 let size = &font_str[last_space + 1..];
257 if size.chars().all(|c| c.is_ascii_digit() || c == '.') {
258 return Some(format!("{} ({})", family, size));
259 }
260 }
261 return Some(font_str);
262 }
263 }
264 }
265 return crate::theme::get_default_monospace_font();
266 }
267 }
268 }
269 } else if term_lower.contains("konsole") {
270 let rc_path = home.join(".config/konsolerc");
271 let mut profile_name = "Default.profile".to_string();
272 if let Ok(content) = std::fs::read_to_string(&rc_path) {
273 for line in content.lines() {
274 let line = line.trim();
275 if line.starts_with("DefaultProfile=") {
276 profile_name = line.trim_start_matches("DefaultProfile=").to_string();
277 break;
278 }
279 }
280 }
281 let profile_path = home.join(".local/share/konsole").join(profile_name);
282 if let Ok(content) = std::fs::read_to_string(&profile_path) {
283 for line in content.lines() {
284 let line = line.trim();
285 if line.starts_with("Font=") {
286 let val = line.trim_start_matches("Font=");
287 let parts: Vec<&str> = val.split(',').collect();
288 if !parts.is_empty() {
289 let family = parts[0];
290 if parts.len() > 1 {
291 let size = parts[1];
292 return Some(format!("{} ({})", family, size));
293 }
294 return Some(family.to_string());
295 }
296 }
297 }
298 }
299 return crate::theme::get_default_monospace_font();
300 }
301
302 #[cfg(target_os = "macos")]
303 if term_lower == "iterm.app" || term_lower.contains("iterm2") {
304 if let Ok(output) = std::process::Command::new("defaults")
305 .args(["read", "com.googlecode.iterm2", "Normal Font"])
306 .output()
307 {
308 if let Ok(s) = String::from_utf8(output.stdout) {
309 let font = s.trim();
310 if !font.is_empty() {
311 return Some(font.to_string());
312 }
313 }
314 }
315 }
316
317 None
318}