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