pub(crate) mod legacy;
pub(crate) mod source;
pub(crate) mod two_panel;
use colored::Colorize as _;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr as _;
pub(crate) struct NoColorGuard {
active: bool,
}
impl NoColorGuard {
pub(crate) fn new(disable: bool) -> Self {
if disable {
colored::control::set_override(false);
}
Self { active: disable }
}
}
impl Drop for NoColorGuard {
fn drop(&mut self) {
if self.active {
colored::control::unset_override();
}
}
}
pub(crate) use trusty_common::banner::shade_bucket;
pub(crate) fn shade_image(art: &str, use_color: bool) -> (Vec<String>, usize) {
let raw_lines: Vec<&str> = art.lines().collect();
let max_cols = raw_lines.iter().map(|l| l.width()).max().unwrap_or(0);
if max_cols == 0 || raw_lines.is_empty() {
return (Vec::new(), 0);
}
let rows = raw_lines
.iter()
.map(|line| {
let mut row = String::with_capacity(max_cols * 24);
let mut display_cols: usize = 0;
for c in line.chars() {
let cw = UnicodeWidthChar::width(c).unwrap_or(1);
if display_cols + cw > max_cols {
break; }
if c == ' ' {
row.push(' ');
} else if use_color {
let (r, g, b) = shade_bucket(c);
row.push_str(&format!("\x1B[38;2;{r};{g};{b}m{c}\x1B[0m"));
} else {
row.push(c);
}
display_cols += cw;
}
for _ in display_cols..max_cols {
row.push(' ');
}
row
})
.collect();
(rows, max_cols)
}
pub(crate) fn load_and_shade_banner() -> (Vec<String>, usize) {
let art = source::load_banner_art();
let use_color = "a".truecolor(0, 0, 0).to_string().contains('\x1B');
shade_image(&art, use_color)
}
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 print_launch_banner(
workdir: &str,
tmux_name: &str,
_prompt_path: Option<&std::path::Path>,
managed_path: Option<&std::path::Path>,
) {
let w = terminal_width();
let daemon = super::info_box::DaemonInfo::from_lock_file_with_probe();
let data =
super::info_box::gather_welcome_data(workdir, tmux_name, false, daemon, managed_path);
if let Some(panel) = two_panel::render_two_panel_banner(&data, w, false) {
println!("\x1B[2J\x1B[1;1H");
print!("{panel}");
let _ = std::io::Write::flush(&mut std::io::stdout());
std::thread::sleep(std::time::Duration::from_secs(1));
} else {
println!("\x1B[2J\x1B[1;1H");
println!("Trusty MPM v{}", env!("CARGO_PKG_VERSION"));
println!("Launching...");
}
}
pub(crate) fn print_launch_banner_reconnecting(workdir: &str, tmux_name: &str) {
let w = terminal_width();
let daemon = super::info_box::DaemonInfo::from_lock_file_with_probe();
let data = super::info_box::gather_welcome_data(workdir, tmux_name, true, daemon, None);
if let Some(panel) = two_panel::render_two_panel_banner(&data, w, true) {
println!("\x1B[2J\x1B[1;1H");
print!("{panel}");
let _ = std::io::Write::flush(&mut std::io::stdout());
std::thread::sleep(std::time::Duration::from_secs(1));
} else {
println!("\x1B[2J\x1B[1;1H");
println!("Trusty MPM v{}", env!("CARGO_PKG_VERSION"));
println!("Reconnecting...");
}
}
pub(crate) fn print_daily_banner(workdir: &str, daemon: &super::info_box::DaemonInfo) {
use std::io::IsTerminal as _;
let is_tty = std::io::stdout().is_terminal();
let _color_guard = NoColorGuard::new(!is_tty);
let w = terminal_width();
let d = super::info_box::DaemonInfo {
addr: daemon.addr.clone(),
online: daemon.online,
session_count: daemon.session_count,
};
let data = super::info_box::gather_welcome_data(workdir, "", false, d, None);
if let Some(panel) = two_panel::render_two_panel_banner(&data, w, false) {
print!("\n{panel}");
let _ = std::io::Write::flush(&mut std::io::stdout());
} else {
println!("Trusty MPM v{}", env!("CARGO_PKG_VERSION"));
}
}
pub(crate) fn print_banner_preview(reconnecting: bool) {
use std::io::IsTerminal as _;
let is_tty = std::io::stdout().is_terminal();
let _color_guard = NoColorGuard::new(!is_tty);
let w = terminal_width();
let workdir = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| ".".to_string());
let session_name = fallback_session_name(std::path::Path::new(&workdir));
let daemon = super::info_box::DaemonInfo::from_lock_file_with_probe();
let data =
super::info_box::gather_welcome_data(&workdir, &session_name, reconnecting, daemon, None);
if let Some(panel) = two_panel::render_two_panel_banner(&data, w, reconnecting) {
print!("\n{panel}");
let _ = std::io::Write::flush(&mut std::io::stdout());
} else {
println!("Trusty MPM v{}", env!("CARGO_PKG_VERSION"));
if reconnecting {
println!("Reconnecting...");
}
}
}
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!(
trusty_mpm::core::tmux::run_tmux(&trusty_mpm::core::tmux::TmuxCommand::HasSession {
name: name.to_string(),
}),
Ok(output) if output.status.success()
)
}