use std::path::Path;
pub fn extract_worktree_name(
session_name: &str,
window_name: &str,
window_prefix: &str,
path: &Path,
) -> (String, bool) {
if let Some(stripped) = window_name.strip_prefix(window_prefix) {
(stripped.to_string(), false)
} else if let Some(stripped) = session_name.strip_prefix(window_prefix) {
(stripped.to_string(), false)
} else {
derive_worktree_name_from_path(path)
}
}
fn derive_worktree_name_from_path(path: &Path) -> (String, bool) {
let components: Vec<_> = path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
for (i, comp) in components.iter().enumerate().rev() {
if comp.ends_with("__worktrees")
&& let Some(&name) = components.get(i + 1)
{
return (name.to_string(), false);
}
if *comp == ".worktrees"
&& let Some(&name) = components.get(i + 1)
{
return (name.to_string(), false);
}
}
("main".to_string(), true)
}
pub fn extract_project_name(path: &Path) -> String {
for ancestor in path.ancestors() {
let git_path = ancestor.join(".git");
if git_path.is_dir()
&& let Some(name) = ancestor.file_name()
{
return name.to_string_lossy().to_string();
}
if let Some(name) = ancestor.file_name() {
let name_str = name.to_string_lossy();
if name_str.ends_with("__worktrees") {
return name_str
.strip_suffix("__worktrees")
.unwrap_or(&name_str)
.to_string();
}
}
}
path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LabelSource {
Worktree,
Window,
Session,
Project,
}
pub fn resolve_labels(
project: &str,
session: &str,
worktree: &str,
window: &str,
window_cmd: Option<&str>,
) -> (String, String) {
let candidates: [(LabelSource, &str, bool); 4] = [
(
LabelSource::Worktree,
worktree,
is_worktree_meaningful(worktree),
),
(
LabelSource::Window,
window,
is_window_meaningful(window, window_cmd),
),
(
LabelSource::Session,
session,
is_session_meaningful(session, project),
),
(LabelSource::Project, project, !project.is_empty()),
];
let mut meaningful = candidates.iter().filter(|(_, _, m)| *m);
let primary = meaningful.next();
let (primary_src, primary_text) = match primary {
Some((src, text, _)) => (*src, (*text).to_string()),
None => (LabelSource::Project, project.to_string()),
};
let secondary = if primary_src == LabelSource::Worktree {
if !project.is_empty() && project != worktree {
project.to_string()
} else {
String::new()
}
} else {
let secondary_base = meaningful
.next()
.map(|(_, t, _)| (*t).to_string())
.unwrap_or_default();
let wt = if worktree.is_empty() {
"main".to_string()
} else {
worktree.to_string()
};
if secondary_base.is_empty() {
wt
} else {
format!("{} ยท {}", secondary_base, wt)
}
};
(primary_text, secondary)
}
fn is_generic_window_name(name: &str) -> bool {
matches!(
name,
"zsh"
| "bash"
| "sh"
| "fish"
| "dash"
| "ksh"
| "csh"
| "tcsh"
| "nu"
| "xonsh"
| "elvish"
| "pwsh"
| "powershell"
| "cmd"
| "tmux"
| "default"
| ""
)
}
fn is_worktree_meaningful(w: &str) -> bool {
let w = w.trim();
!w.is_empty() && !matches!(w, "main" | "master")
}
fn is_window_meaningful(window: &str, cmd: Option<&str>) -> bool {
let Some(cmd) = cmd else {
return false;
};
let window = window.trim();
let cmd = cmd.trim();
if window.is_empty() {
return false;
}
if is_generic_window_name(window) {
return false;
}
window != cmd
}
fn is_session_meaningful(session: &str, project: &str) -> bool {
let session = session.trim();
let project = project.trim();
if session.is_empty() {
return false;
}
if session == project {
return false;
}
if session.eq_ignore_ascii_case("default") {
return false;
}
if session.chars().all(|c| c.is_ascii_digit()) {
return false;
}
true
}
pub fn sanitize_pane_title<'a>(
raw: Option<&'a str>,
worktree: &str,
project: &str,
) -> Option<&'a str> {
let title = raw?.trim();
if title.is_empty() {
return None;
}
let title = title
.trim_start_matches(|c: char| {
('\u{2800}'..='\u{28FF}').contains(&c)
|| matches!(c, 'โณ' | 'โ ' | 'โ' | 'โ' | 'โ' | 'โ' | 'โ')
})
.trim();
let title = strip_oc_title_prefix(title);
if title.is_empty() {
return None;
}
if title.starts_with("Claude Code") {
return None;
}
if matches!(title, "zsh" | "bash" | "sh" | "fish") {
return None;
}
if title == worktree || title == project {
return None;
}
Some(title)
}
pub fn strip_oc_title_prefix(mut title: &str) -> &str {
while let Some((prefix, rest)) = title.split_once('|') {
if prefix.trim() != "OC" {
break;
}
title = rest.trim();
}
title
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_extract_worktree_name_window_mode() {
let path = Path::new("/home/user/myproject__worktrees/fix-bug");
let (name, is_main) =
extract_worktree_name("main-session", "workmux:fix-bug", "workmux:", path);
assert_eq!(name, "fix-bug");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_session_mode() {
let path = Path::new("/home/user/myproject__worktrees/feature-auth");
let (name, is_main) =
extract_worktree_name("workmux:feature-auth", "zsh", "workmux:", path);
assert_eq!(name, "feature-auth");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_window_preferred_over_session() {
let path = Path::new("/home/user/myproject__worktrees/from-window");
let (name, is_main) = extract_worktree_name(
"workmux:from-session",
"workmux:from-window",
"workmux:",
path,
);
assert_eq!(name, "from-window");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_path_fallback_sibling() {
let path = Path::new("/home/user/myproject__worktrees/fix-bug");
let (name, is_main) = extract_worktree_name("0", "zsh", "workmux:", path);
assert_eq!(name, "fix-bug");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_path_fallback_subdir() {
let path = Path::new("/home/user/myproject/.worktrees/fix-bug");
let (name, is_main) = extract_worktree_name("0", "zsh", "workmux:", path);
assert_eq!(name, "fix-bug");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_path_fallback_nested_cwd() {
let path = Path::new("/home/user/myproject__worktrees/fix-bug/src/lib");
let (name, is_main) = extract_worktree_name("0", "zsh", "workmux:", path);
assert_eq!(name, "fix-bug");
assert!(!is_main);
}
#[test]
fn test_extract_worktree_name_path_fallback_main() {
let path = Path::new("/home/user/myproject");
let (name, is_main) = extract_worktree_name("0", "zsh", "workmux:", path);
assert_eq!(name, "main");
assert!(is_main);
}
#[test]
fn test_extract_project_name_worktrees() {
let path = PathBuf::from("/home/user/myproject__worktrees/fix-bug");
assert_eq!(extract_project_name(&path), "myproject");
}
#[test]
fn test_extract_project_name_fallback() {
let path = PathBuf::from("/home/user/myproject");
assert_eq!(extract_project_name(&path), "myproject");
}
#[test]
fn test_extract_project_name_git_root() {
let temp = tempfile::TempDir::new().unwrap();
let project_dir = temp.path().join("myproject");
std::fs::create_dir_all(project_dir.join(".git")).unwrap();
std::fs::create_dir_all(project_dir.join(".worktrees").join("fix-bug")).unwrap();
let worktree_path = project_dir.join(".worktrees").join("fix-bug");
assert_eq!(extract_project_name(&worktree_path), "myproject");
}
#[test]
fn test_extract_project_name_git_file_skipped() {
let temp = tempfile::TempDir::new().unwrap();
let project_dir = temp.path().join("myproject");
let worktree_dir = project_dir.join(".worktrees").join("fix-bug");
std::fs::create_dir_all(&worktree_dir).unwrap();
std::fs::write(worktree_dir.join(".git"), "gitdir: /somewhere/else").unwrap();
std::fs::create_dir_all(project_dir.join(".git")).unwrap();
assert_eq!(extract_project_name(&worktree_dir), "myproject");
}
#[test]
fn resolve_labels_feature_branch_keeps_worktree_primary() {
let (primary, secondary) = resolve_labels("api", "api", "fix-auth", "zsh", Some("zsh"));
assert_eq!(primary, "fix-auth");
assert_eq!(secondary, "api");
}
#[test]
fn resolve_labels_promotes_sticky_window_on_main_worktree() {
let (primary, secondary) =
resolve_labels("api", "api", "main", "db-migration", Some("sleep"));
assert_eq!(primary, "db-migration");
assert_eq!(secondary, "api ยท main");
}
#[test]
fn resolve_labels_promotes_session_when_window_auto_tracks() {
let (primary, secondary) =
resolve_labels("monorepo", "frontend-refactor", "main", "zsh", Some("zsh"));
assert_eq!(primary, "frontend-refactor");
assert_eq!(secondary, "monorepo ยท main");
}
#[test]
fn resolve_labels_falls_back_to_project_when_nothing_else_meaningful() {
let (primary, secondary) = resolve_labels("api", "0", "main", "zsh", Some("zsh"));
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_window_never_promoted_without_cmd() {
let (primary, secondary) = resolve_labels("api", "api", "main", "looks-meaningful", None);
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_session_equal_to_project_not_meaningful() {
let (primary, secondary) = resolve_labels("api", "api", "main", "zsh", Some("zsh"));
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_default_session_not_meaningful() {
let (primary, secondary) = resolve_labels("api", "default", "main", "zsh", Some("zsh"));
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_master_treated_like_main() {
let (primary, secondary) =
resolve_labels("api", "release-prep", "master", "zsh", Some("zsh"));
assert_eq!(primary, "release-prep");
assert_eq!(secondary, "api ยท master");
}
#[test]
fn resolve_labels_worktree_primary_keeps_project_secondary_even_with_sticky_window() {
let (primary, secondary) =
resolve_labels("api", "session-x", "fix-auth", "scratchpad", Some("zsh"));
assert_eq!(primary, "fix-auth");
assert_eq!(secondary, "api");
}
#[test]
fn resolve_labels_window_zsh_not_promoted_when_pane_runs_node() {
let (primary, secondary) = resolve_labels("api", "0", "main", "zsh", Some("node"));
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_window_default_blocked() {
let (primary, secondary) = resolve_labels("api", "0", "main", "default", Some("node"));
assert_eq!(primary, "api");
assert_eq!(secondary, "main");
}
#[test]
fn resolve_labels_worktree_primary_no_project_yields_empty_secondary() {
let (primary, secondary) = resolve_labels("", "", "fix-auth", "", None);
assert_eq!(primary, "fix-auth");
assert_eq!(secondary, "");
}
#[test]
fn resolve_labels_blocks_pwsh_powershell_nu() {
for shell in ["pwsh", "powershell", "nu", "csh", "tcsh", "xonsh", "elvish"] {
let (primary, _) = resolve_labels("api", "0", "main", shell, Some("node"));
assert_eq!(primary, "api", "expected {shell} to be blocked");
}
}
#[test]
fn resolve_labels_trims_whitespace_in_predicates() {
let (primary, _) = resolve_labels("api", " default ", "main", " zsh ", Some("zsh"));
assert_eq!(primary, "api");
let (primary, _) = resolve_labels("api", " 0 ", " main ", " zsh ", Some("zsh"));
assert_eq!(primary, "api");
}
#[test]
fn test_strip_oc_title_prefix() {
assert_eq!(
strip_oc_title_prefix("OC | Investigating..."),
"Investigating..."
);
assert_eq!(
strip_oc_title_prefix("OC | OC | Investigating..."),
"Investigating..."
);
assert_eq!(
strip_oc_title_prefix("Claude Code | Investigating..."),
"Claude Code | Investigating..."
);
}
}