mod probes;
pub(crate) mod render;
use std::path::Path;
#[cfg(test)]
pub(crate) const LOGO: [&str; 3] = [
"▐▛███▜▌ ", "▝▜█████▛▘ ", "▘▘ ▝▝ ", ];
#[cfg(test)]
pub(crate) const LOGO_COLS: usize = 13;
#[derive(Debug, Default)]
pub(crate) struct DaemonInfo {
pub(crate) addr: String,
pub(crate) session_count: Option<usize>,
pub(crate) online: bool,
}
impl DaemonInfo {
pub(crate) fn from_lock_file() -> Self {
match read_lock_addr() {
Some(addr) => DaemonInfo {
addr,
online: true,
session_count: None,
},
None => DaemonInfo::default(),
}
}
pub(crate) fn from_lock_file_with_probe() -> Self {
let addr = read_lock_addr().or_else(probe_default_addr);
let Some(addr) = addr else {
return DaemonInfo::default();
};
let probe_addr = url_to_probe_addr(&addr).unwrap_or_else(|| addr.clone());
let online = tcp_probe(&probe_addr);
DaemonInfo {
addr,
online,
session_count: None,
}
}
pub(crate) fn with_count(mut self, count: usize) -> Self {
self.session_count = Some(count);
self
}
}
pub(crate) struct WelcomeData {
pub(crate) project: String,
pub(crate) workspace: String,
pub(crate) user: String,
pub(crate) reconnecting: bool,
pub(crate) session_name: String,
pub(crate) daemon: DaemonInfo,
pub(crate) recent_commits: Vec<CommitLine>,
pub(crate) memory_status: String,
pub(crate) search_status: String,
pub(crate) review_status: String,
}
#[derive(Debug)]
pub(crate) struct CommitLine {
pub(crate) sha: String,
pub(crate) age: String,
pub(crate) subject: String,
}
pub(crate) fn gather_welcome_data(
workdir: &str,
session_name: &str,
reconnecting: bool,
daemon: DaemonInfo,
managed_path: Option<&Path>,
) -> WelcomeData {
let workdir_path = Path::new(workdir);
let github_path = trusty_common::github_path::derive_github_path(workdir_path);
let project = github_path
.as_ref()
.map(|gp| gp.rel_path())
.unwrap_or_else(|| {
workdir_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| workdir.to_string())
});
let workspace = if let Some(mp) = managed_path {
mp.to_string_lossy().into_owned()
} else {
match &github_path {
Some(gp) => {
let cfg = trusty_mpm::core::trusty_tools_config::TrustyToolsConfig::load();
let managed = trusty_mpm::core::trusty_tools_config::workspace_subpath(&cfg, gp);
managed.to_string_lossy().into_owned()
}
None => workdir.to_string(),
}
};
let user = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_default();
let recent_commits = probes::probe_recent_commits(workdir);
let memory_status = super::banner::detect_memory();
let search_status = super::banner::detect_tool("trusty-search");
let review_status = super::banner::detect_tool("trusty-review");
WelcomeData {
project,
workspace,
user,
reconnecting,
session_name: session_name.to_string(),
daemon,
recent_commits,
memory_status,
search_status,
review_status,
}
}
pub(crate) fn tcp_probe(addr: &str) -> bool {
use std::net::{TcpStream, ToSocketAddrs as _};
use std::time::Duration;
addr.to_socket_addrs()
.ok()
.and_then(|mut a| a.next())
.map(|sa| TcpStream::connect_timeout(&sa, Duration::from_millis(150)).is_ok())
.unwrap_or(false)
}
fn probe_default_addr() -> Option<String> {
let url = std::env::var("TRUSTY_MPM_URL")
.unwrap_or_else(|_| trusty_mpm::core::DEFAULT_DAEMON_URL.to_string());
url_to_probe_addr(&url)
}
pub(crate) fn url_to_probe_addr(url: &str) -> Option<String> {
let without_scheme = url
.trim_start_matches("https://")
.trim_start_matches("http://");
let authority = without_scheme.split('/').next().unwrap_or(without_scheme);
if authority.is_empty() {
return None;
}
let with_port = if authority.contains(':') {
authority.to_string()
} else {
let default_port = trusty_mpm::core::DEFAULT_DAEMON_ADDR
.rsplit(':')
.next()
.unwrap_or("7880");
format!("{authority}:{default_port}")
};
Some(with_port)
}
pub(crate) fn abbreviate_home(path: &str) -> String {
let home = match dirs::home_dir() {
Some(h) => h.to_string_lossy().into_owned(),
None => return path.to_string(),
};
if path.starts_with(&home) {
format!("~{}", &path[home.len()..])
} else {
path.to_string()
}
}
fn read_lock_addr() -> Option<String> {
let path = trusty_mpm::core::lock_file_path();
let content = std::fs::read_to_string(&path).ok()?;
let mut addr: Option<String> = None;
let mut pid: Option<u32> = None;
for line in content.lines() {
if let Some(v) = line.strip_prefix("addr = ") {
addr = Some(v.trim_matches('"').to_string());
}
if let Some(v) = line.strip_prefix("pid = ") {
pid = v.trim().parse().ok();
}
}
#[cfg(unix)]
if let Some(p) = pid
&& unsafe { libc::kill(p as libc::pid_t, 0) } != 0
{
return None;
}
let _ = pid;
addr
}
pub(crate) fn render_info_box_rows(data: &WelcomeData) -> Vec<String> {
let mut rows = render::build_rows(data);
if !rows.is_empty() {
rows.remove(0);
}
rows
}
#[cfg(test)]
pub(crate) fn render_info_box(
workdir: &str,
tmux_name: &str,
reconnecting: bool,
daemon: &DaemonInfo,
) -> String {
let data = WelcomeData {
project: Path::new(workdir)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| workdir.to_string()),
workspace: workdir.to_string(),
user: String::new(),
reconnecting,
session_name: tmux_name.to_string(),
daemon: DaemonInfo {
addr: daemon.addr.clone(),
online: daemon.online,
session_count: daemon.session_count,
},
recent_commits: vec![],
memory_status: super::banner::detect_memory(),
search_status: super::banner::detect_tool("trusty-search"),
review_status: super::banner::detect_tool("trusty-review"),
};
render::render_welcome_panel(&data)
}
#[cfg(test)]
mod tests {
use super::*;
fn online(port: u16) -> DaemonInfo {
DaemonInfo {
addr: format!("127.0.0.1:{port}"),
online: true,
session_count: None,
}
}
fn offline() -> DaemonInfo {
DaemonInfo::default()
}
#[test]
fn info_box_renders_online() {
let out = render_info_box(
"/home/user/my-project",
"tmpm-my-project",
false,
&online(7880),
);
assert!(out.contains('\u{25cf}'), "expected ● online marker");
assert!(out.contains(":7880"), "expected port in daemon row");
assert!(out.contains("my-project"), "expected project name");
assert!(!out.contains("reconnecting"), "must not show reconnecting");
}
#[test]
fn info_box_renders_offline() {
let out = render_info_box(
"/home/user/my-project",
"tmpm-my-project",
false,
&offline(),
);
assert!(out.contains('\u{25cb}'), "expected ○ marker");
assert!(out.contains("offline"), "expected offline label");
assert!(!out.contains("reconnecting"), "must not show reconnecting");
}
#[test]
fn info_box_renders_reconnecting() {
let out = render_info_box("/home/user/my-project", "tmpm-my-project", true, &offline());
assert!(out.contains("reconnecting"), "expected reconnecting hint");
assert!(
out.contains("tmpm-my-project"),
"expected session name in reconnect row"
);
}
#[test]
fn info_box_renders_with_count() {
let d = online(7880).with_count(3);
let out = render_info_box("/home/user/proj", "sess", false, &d);
assert!(out.contains("(3)"), "expected session count");
}
#[test]
fn info_box_no_checkmark_on_not_detected_lines() {
let out = render_info_box("/tmp/proj", "sess", false, &offline());
for line in out.lines() {
if line.contains("not detected") {
assert!(
!line.contains('\u{2713}'),
"✓ must not appear on a not-detected line: {line:?}"
);
}
}
}
#[test]
fn probe_with_nonlistening_addr_returns_offline() {
assert!(
!tcp_probe("127.0.0.1:1"),
"TCP probe to port 1 must return offline (connection refused)"
);
}
#[test]
fn probe_with_invalid_addr_returns_offline() {
assert!(
!tcp_probe("not-a-valid-addr"),
"TCP probe to invalid addr must return offline"
);
}
#[test]
fn abbreviate_home_replaces_prefix() {
if let Some(home) = dirs::home_dir() {
let path = format!("{}/projects/repo", home.display());
let result = abbreviate_home(&path);
assert!(result.starts_with('~'), "expected ~ prefix: {result:?}");
}
}
#[test]
fn url_to_probe_addr_handles_url_without_port() {
let addr = url_to_probe_addr("http://localhost/")
.expect("should return Some for http://localhost/");
assert!(
addr.contains(':'),
"addr must include a port separator: {addr:?}"
);
let port_str = addr.rsplit(':').next().expect("addr has a colon");
port_str
.parse::<u16>()
.expect("port portion of url_to_probe_addr output must be a valid u16");
}
#[test]
fn url_to_probe_addr_preserves_explicit_port() {
let addr = url_to_probe_addr("http://127.0.0.1:9999/")
.expect("should return Some for http://127.0.0.1:9999/");
assert_eq!(addr, "127.0.0.1:9999");
}
#[test]
fn url_to_probe_addr_strips_path() {
let addr =
url_to_probe_addr("http://127.0.0.1:7880/api/v1/health").expect("should return Some");
assert_eq!(addr, "127.0.0.1:7880");
}
#[test]
fn url_to_probe_addr_empty_returns_none() {
assert!(url_to_probe_addr("").is_none());
assert!(url_to_probe_addr("http://").is_none());
assert!(url_to_probe_addr("http:///").is_none());
}
#[test]
fn url_to_probe_addr_strips_http_scheme_from_lock_file_format() {
let lock_file_addr = "http://127.0.0.1:7880";
let probe_addr = url_to_probe_addr(lock_file_addr)
.expect("must convert http://127.0.0.1:7880 to a probe addr");
assert_eq!(
probe_addr, "127.0.0.1:7880",
"url_to_probe_addr must strip scheme from lock-file URL format"
);
use std::net::ToSocketAddrs as _;
assert!(
probe_addr.to_socket_addrs().is_ok(),
"probe_addr must be parseable by to_socket_addrs: {probe_addr:?}"
);
}
}