agentmux_launcher/
splash_info.rs1#[cfg(windows)]
12use agentmux_common::win32::NoWindow;
13use std::process::Command;
14
15pub struct SplashInfo {
16 pub user: String,
17 pub host: String,
18 pub version: String,
19 pub dev_label: Option<String>,
22}
23
24impl SplashInfo {
25 pub fn gather() -> Self {
26 SplashInfo {
27 user: username(),
28 host: hostname(),
29 version: env!("CARGO_PKG_VERSION").to_string(),
30 dev_label: dev_label(),
31 }
32 }
33
34 pub fn footer_lines(&self, max_chars: usize) -> Vec<String> {
37 let l1 = ellipsize(&format!("{}@{}", self.user, self.host), max_chars);
38 let l2 = match &self.dev_label {
39 Some(d) => format!("v{} {}", self.version, d.to_uppercase()),
40 None => format!("v{}", self.version),
41 };
42 vec![l1, ellipsize(&l2, max_chars)]
43 }
44}
45
46fn username() -> String {
47 for k in ["USER", "USERNAME", "LOGNAME"] {
48 if let Ok(v) = std::env::var(k) {
49 let v = sanitize(v.trim());
50 if !v.is_empty() {
51 return v;
52 }
53 }
54 }
55 cmd_first_line("whoami", &[])
56 .map(|s| sanitize(s.trim()))
57 .filter(|s| !s.is_empty())
58 .unwrap_or_else(|| "user".into())
59}
60
61fn hostname() -> String {
62 for k in ["HOSTNAME", "COMPUTERNAME", "HOST"] {
64 if let Ok(v) = std::env::var(k) {
65 let v = short_host(v.trim());
66 if !v.is_empty() {
67 return v;
68 }
69 }
70 }
71 cmd_first_line("hostname", &[])
72 .map(|s| short_host(s.trim()))
73 .filter(|s| !s.is_empty())
74 .unwrap_or_else(|| "host".into())
75}
76
77fn dev_label() -> Option<String> {
83 if std::env::var("AGENTMUX_DEV")
84 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
85 .unwrap_or(false)
86 {
87 return Some("dev".into());
88 }
89 if let Ok(ch) = std::env::var("AGENTMUX_CHANNEL") {
90 let ch = sanitize(ch.trim());
91 if !ch.is_empty() && ch != "stable" {
92 return Some(ch);
93 }
94 }
95 None
96}
97
98fn cmd_first_line(cmd: &str, args: &[&str]) -> Option<String> {
100 let mut command = Command::new(cmd);
101 command.args(args);
102 #[cfg(windows)]
103 {
104 command.no_window();
106 }
107 let out = command.output().ok()?;
108 if !out.status.success() {
109 return None;
110 }
111 String::from_utf8_lossy(&out.stdout)
112 .lines()
113 .map(|l| l.trim().to_string())
114 .find(|l| !l.is_empty())
115}
116
117fn sanitize(s: &str) -> String {
120 s.chars()
121 .map(|c| if (' '..='~').contains(&c) { c } else { '?' })
122 .collect()
123}
124
125fn short_host(s: &str) -> String {
127 sanitize(s.split('.').next().unwrap_or(s))
128}
129
130fn ellipsize(s: &str, max: usize) -> String {
132 let chars: Vec<char> = s.chars().collect();
133 if chars.len() <= max {
134 return s.to_string();
135 }
136 if max <= 3 {
137 return chars.iter().take(max).collect();
138 }
139 let keep = max - 3;
140 let left = keep.div_ceil(2);
141 let right = keep - left;
142 let head: String = chars[..left].iter().collect();
143 let tail: String = chars[chars.len() - right..].iter().collect();
144 format!("{head}...{tail}")
145}