pub(crate) fn project_segment(cwd: &str) -> Option<String> {
if cwd.is_empty() {
return None;
}
let project = git_owner_repo(cwd).unwrap_or_else(|| {
std::path::Path::new(cwd)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
});
let branch = select_branch_label(tmux_session_name(), || git_branch(cwd));
render_project_segment(project, branch.as_deref())
}
fn select_branch_label(
tmux_session: Option<String>,
fallback: impl FnOnce() -> Option<String>,
) -> Option<String> {
tmux_session.filter(|s| !s.is_empty()).or_else(fallback)
}
fn render_project_segment(project: String, branch: Option<&str>) -> Option<String> {
if project.is_empty() {
return None;
}
Some(match branch {
Some(b) if !b.is_empty() && b != "HEAD" && !is_managed_session_branch(b) => {
format!("{project} \u{2387} {b}")
}
_ => project,
})
}
fn is_managed_session_branch(branch: &str) -> bool {
branch == "session" || branch.starts_with("session/")
}
fn git_branch(cwd: &str) -> Option<String> {
use std::sync::mpsc;
use std::time::Duration;
let cwd = cwd.to_string();
let (tx, rx) = mpsc::channel::<Option<String>>();
std::thread::spawn(move || {
let result = (|| -> Option<String> {
let args = [
"-C".to_string(),
cwd.clone(),
"rev-parse".to_string(),
"--abbrev-ref".to_string(),
"HEAD".to_string(),
];
let out = trusty_mpm::core::spawn_disclaim::disclaimed_output("git", &args).ok()?;
if !out.status.success() {
return None;
}
let branch = String::from_utf8_lossy(&out.stdout).trim().to_string();
if branch.is_empty() {
None
} else {
Some(branch)
}
})();
let _ = tx.send(result);
});
rx.recv_timeout(Duration::from_millis(100)).ok().flatten()
}
fn tmux_session_name() -> Option<String> {
use std::sync::mpsc;
use std::time::Duration;
std::env::var_os("TMUX")?;
let (tx, rx) = mpsc::channel::<Option<String>>();
std::thread::spawn(move || {
let result = (|| -> Option<String> {
let out = std::process::Command::new("tmux")
.args(["display-message", "-p", "#S"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
if name.is_empty() { None } else { Some(name) }
})();
let _ = tx.send(result);
});
rx.recv_timeout(Duration::from_millis(100)).ok().flatten()
}
fn git_owner_repo(cwd: &str) -> Option<String> {
use std::sync::mpsc;
use std::time::Duration;
let cwd = cwd.to_string();
let (tx, rx) = mpsc::channel::<Option<String>>();
std::thread::spawn(move || {
let result = trusty_common::github_path::derive_github_path(std::path::Path::new(&cwd))
.map(|gh| format!("{}/{}", gh.owner, gh.repo));
let _ = tx.send(result);
});
rx.recv_timeout(Duration::from_millis(100)).ok().flatten()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_managed_session_branch_matches_session_prefix() {
assert!(is_managed_session_branch("session/2dfb23d1-579f-4168-9d28"));
assert!(is_managed_session_branch("session/anything"));
assert!(is_managed_session_branch("session"));
assert!(!is_managed_session_branch("main"));
assert!(!is_managed_session_branch("fix/foo"));
assert!(!is_managed_session_branch("sessions/foo"));
}
#[test]
fn render_project_segment_omits_managed_session_branch() {
let seg = render_project_segment(
"bobmatnyc/trusty-tools".to_string(),
Some("session/2dfb23d1-579f-4168-9d28"),
);
assert_eq!(seg.as_deref(), Some("bobmatnyc/trusty-tools"));
let s = seg.unwrap();
assert!(!s.contains('\u{2387}'), "managed branch must be omitted");
assert!(!s.contains("2dfb23d1"), "session UUID must not appear");
}
#[test]
fn render_project_segment_keeps_real_branch() {
let seg = render_project_segment("bobmatnyc/trusty-tools".to_string(), Some("main"));
assert_eq!(seg.as_deref(), Some("bobmatnyc/trusty-tools \u{2387} main"));
assert_eq!(
render_project_segment("owner/repo".to_string(), Some("HEAD")).as_deref(),
Some("owner/repo")
);
assert_eq!(
render_project_segment("owner/repo".to_string(), None).as_deref(),
Some("owner/repo")
);
}
#[test]
fn render_project_segment_empty_label() {
assert_eq!(render_project_segment(String::new(), Some("main")), None);
}
#[test]
fn select_branch_label_selection_contract() {
let panics = || -> Option<String> {
panic!("fallback must not run when tmux session name is present")
};
assert_eq!(
select_branch_label(Some("tm-trusty-tools-01".to_string()), panics).as_deref(),
Some("tm-trusty-tools-01")
);
assert_eq!(
select_branch_label(None, || Some("main".to_string())).as_deref(),
Some("main")
);
assert_eq!(
select_branch_label(Some(String::new()), || Some("main".to_string())).as_deref(),
Some("main")
);
assert_eq!(select_branch_label(None, || None), None);
}
#[test]
fn project_segment_basename_fallback_non_git() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let cwd = tmp.path().to_string_lossy().to_string();
let seg = project_segment(&cwd).expect("basename segment");
let basename = tmp
.path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
match tmux_session_name() {
Some(session) => assert_eq!(
seg,
format!("{basename} \u{2387} {session}"),
"inside tmux, non-git cwd → basename + tmux session label"
),
None => {
assert_eq!(seg, basename, "non-git cwd → basename, no branch");
assert!(!seg.contains('\u{2387}'), "no branch marker for a bare dir");
}
}
}
#[test]
fn git_owner_repo_reads_remote() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let dir = tmp.path();
let git = |args: &[&str]| {
std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return; }
assert!(git(&[
"remote",
"add",
"origin",
"git@github.com:bobmatnyc/trusty-tools.git",
]));
let cwd = dir.to_string_lossy().to_string();
assert_eq!(
git_owner_repo(&cwd).as_deref(),
Some("bobmatnyc/trusty-tools"),
"scp-style remote → owner/repo slug"
);
}
#[test]
fn git_owner_repo_none_outside_repo() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let cwd = tmp.path().to_string_lossy().to_string();
assert_eq!(git_owner_repo(&cwd), None);
}
}