pub(crate) const BANNER_INDENT: &str = " ";
pub(crate) const BANNER_SEPARATOR_WIDTH: usize = 53;
pub(crate) const BANNER_ROBOT: &[&str] = &[
" ",
" .}##- ",
" }#+#} ",
" .}#-#}. ",
" ^]}#}##+##}#}]^ ",
" ^}}]<^++]#<#]++^^<}}< ",
" <}]^++++++<#}#<++---+^]}] ",
" ^}]+--++++++<###<+---+---+<}^ ",
" <}<+---++---+<###<-+--------^}< ",
" -}<+-+^<<^+---+}##+---+^^<+---^}+ ",
" .]}++<}}<<]}]+-+}##--+]}]<<]}<--]]. ",
" ^#}#]^]]. ^#^-}##-^#< ]]+]#}#^ ",
" }}]#]<#+ ]]+}#}-]} -#<]#<}} ",
" ]##}<#]^}^ .}<-<#<.<}- ^}^<#<}##] ",
"<}^}}<#]-^}}- <#<--<#<--<#< -}}^-<}^}}+}<",
"-#]}}^#]---^]##}^---.^#^-..-^}##]+..-]#^}}]#-",
" +#}^#]-----.----....+..............<#^]#+ ",
" }}^#]-----..--....................<}^}} ",
" }}<#]--...--........ ........ ..<#^}} ",
" ]}<#]-....--......... .........<#^}] ",
" ^}}#]^^++--------------.-...---++^]#]}^ ",
" }}]]}}}}##}}}}}}}}}}}}}}}}}##}}]]]]}} ",
" }}+-----<#+ . . -#<-.----}] ",
" -#<-----<}+ .. -}<-...-<#- ",
" .}}<+--<#+.. -}<-.-^}}. ",
" ^}#}##}}}}}}}}}}}}}}}}}##}#}^ ",
" ",
];
pub(crate) const BANNER_TITLE: &[&str] = &[
"████████╗██████╗ ██╗ ██╗███████╗████████╗██╗ ██╗",
"╚══██╔══╝██╔══██╗██║ ██║██╔════╝╚══██╔══╝╚██╗ ██╔╝",
" ██║ ██████╔╝██║ ██║███████╗ ██║ ╚████╔╝ ",
" ██║ ██╔══██╗██║ ██║╚════██║ ██║ ╚██╔╝ ",
" ██║ ██║ ██║╚██████╔╝███████║ ██║ ██║ ",
" ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ",
" M U L T I - A G E N T P M",
];
pub(crate) fn terminal_width() -> usize {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &raw mut ws) == 0 && ws.ws_col > 0 {
return ws.ws_col as usize;
}
}
if let Ok(cols) = std::env::var("COLUMNS")
&& let Ok(n) = cols.parse::<usize>()
&& n > 0
{
return n;
}
80
}
pub(crate) fn render_launch_banner(
workdir: &str,
tmux_name: &str,
prompt_path: Option<&std::path::Path>,
reconnect_session: Option<&str>,
) -> String {
let mut out = String::new();
out.push_str("\x1B[2J\x1B[1;1H");
out.push('\n');
for line in BANNER_ROBOT {
out.push_str(BANNER_INDENT);
out.push_str(line);
out.push('\n');
}
out.push('\n');
for line in BANNER_TITLE {
out.push_str(BANNER_INDENT);
out.push_str(line);
out.push('\n');
}
out.push('\n');
let separator = "─".repeat(BANNER_SEPARATOR_WIDTH);
let field =
|label: &str, value: &str| -> String { format!("{BANNER_INDENT}{label:<9}: {value}\n") };
let memory = detect_memory();
let search = detect_tool("trusty-search");
let prompt = match prompt_path {
Some(p) => p.display().to_string(),
None => "(default)".to_string(),
};
out.push_str(BANNER_INDENT);
out.push_str(&separator);
out.push('\n');
out.push_str(&field("Project", workdir));
out.push_str(&field("Session", tmux_name));
if let Some(session) = reconnect_session {
out.push_str(&field(
"Status",
&format!("↩ reconnecting to existing session ({session})"),
));
} else {
out.push_str(&field("Memory", &format!("{memory} ✓")));
out.push_str(&field("Search", &format!("{search} ✓")));
out.push_str(&field("Prompt", &prompt));
}
out.push_str(BANNER_INDENT);
out.push_str(&separator);
out.push('\n');
out.push('\n');
let action = if reconnect_session.is_some() {
"Reconnecting..."
} else {
"Launching claude..."
};
out.push_str(BANNER_INDENT);
out.push_str(action);
out.push('\n');
out
}
pub(crate) fn print_launch_banner(
workdir: &str,
tmux_name: &str,
prompt_path: Option<&std::path::Path>,
) {
let _ = terminal_width();
print!(
"{}",
render_launch_banner(workdir, tmux_name, prompt_path, None)
);
let _ = std::io::Write::flush(&mut std::io::stdout());
std::thread::sleep(std::time::Duration::from_secs(1));
}
pub(crate) fn print_launch_banner_reconnecting(workdir: &str, tmux_name: &str) {
let _ = terminal_width();
print!(
"{}",
render_launch_banner(workdir, tmux_name, None, Some(tmux_name))
);
let _ = std::io::Write::flush(&mut std::io::stdout());
std::thread::sleep(std::time::Duration::from_secs(1));
}
pub(crate) fn detect_memory() -> String {
let config = dirs_config_dir().map(|c| c.join("trusty-memory"));
let has_config = config.map(|c| c.exists()).unwrap_or(false);
if has_config || binary_on_path("trusty-memory") {
"trusty-memory".to_string()
} else {
"(not detected)".to_string()
}
}
pub(crate) fn detect_tool(name: &str) -> String {
if binary_on_path(name) {
name.to_string()
} else {
"(not detected)".to_string()
}
}
pub(crate) fn dirs_config_dir() -> Option<std::path::PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
&& !xdg.is_empty()
{
return Some(std::path::PathBuf::from(xdg));
}
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
}
pub(crate) fn binary_on_path(name: &str) -> bool {
let Some(paths) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&paths).any(|dir| dir.join(name).is_file())
}
pub(crate) fn fallback_session_name(path: &std::path::Path) -> String {
trusty_mpm::core::names::name_from_dir(path)
}
pub(crate) fn normalize_workdir(workdir: &str) -> String {
let path = std::path::Path::new(workdir);
if let Ok(canonical) = path.canonicalize() {
return canonical.to_string_lossy().to_string();
}
workdir.trim_end_matches('/').to_string()
}
pub(crate) fn tmux_has_session(name: &str) -> bool {
matches!(
std::process::Command::new("tmux")
.args(["has-session", "-t", name])
.status(),
Ok(status) if status.success()
)
}