use std::path::{Path, PathBuf};
pub fn convert_path_to_project_dir_name(path: &Path) -> String {
path.to_string_lossy()
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'-'
}
})
.collect()
}
pub fn format_short_name_from_path(path: &Path) -> String {
let path_str = path.to_string_lossy();
if let Some(wt_pos) = path_str
.find("__worktrees/")
.or_else(|| path_str.find("/.worktrees/"))
{
let is_hidden = path_str[wt_pos..].starts_with("/.");
let separator_len = if is_hidden {
"/.worktrees/".len()
} else {
"__worktrees/".len()
};
let before = &path_str[..wt_pos];
let main_project = Path::new(before)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let after = &path_str[wt_pos + separator_len..];
let worktree = after.split('/').next().unwrap_or("");
if !main_project.is_empty() && !worktree.is_empty() {
return format!("{}/{}", main_project, worktree);
}
}
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path_str.into_owned())
}
const WORKTREE_MARKER: &str = "--worktrees-";
pub fn encoded_project_root(encoded: &str) -> &str {
encoded
.split_once(WORKTREE_MARKER)
.map_or(encoded, |(root, _)| root)
}
pub fn is_same_project(a: &str, b: &str) -> bool {
encoded_project_root(a) == encoded_project_root(b)
}
pub fn decode_project_dir_name_to_path(encoded: &str) -> PathBuf {
PathBuf::from(decode_with_double_dash_as(encoded, "__"))
}
fn decode_with_double_dash_as(encoded: &str, double_dash_replacement: &str) -> String {
let mut result = String::with_capacity(encoded.len());
let mut chars = encoded.chars().peekable();
while let Some(c) = chars.next() {
if c == '-' {
let mut count = 1;
while chars.peek() == Some(&'-') {
chars.next();
count += 1;
}
match count {
1 => result.push('/'),
2 => result.push_str(double_dash_replacement),
n => {
result.push('/');
for _ in 0..((n - 1) / 2) {
result.push_str(double_dash_replacement);
}
if (n - 1) % 2 == 1 {
result.push('/');
}
}
}
} else {
result.push(c);
}
}
result
}
pub fn decode_project_dir_name(encoded: &str) -> String {
let mut result = String::with_capacity(encoded.len());
let mut chars = encoded.chars().peekable();
while let Some(c) = chars.next() {
if c == '-' {
let mut count = 1;
while chars.peek() == Some(&'-') {
chars.next();
count += 1;
}
if count % 2 == 1 {
result.push('/');
for _ in 0..(count - 1) {
result.push('_');
}
} else {
for _ in 0..count {
result.push('_');
}
}
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_various_separators_and_punctuation() {
let path = Path::new("/Users/raine/code/workmux/.worktrees/uncommitted");
let converted = convert_path_to_project_dir_name(path);
assert_eq!(
converted,
"-Users-raine-code-workmux--worktrees-uncommitted"
);
}
#[test]
fn preserves_alphanumeric_and_existing_dashes() {
let path = Path::new("/tmp/foo-Bar123");
let converted = convert_path_to_project_dir_name(path);
assert_eq!(converted, "-tmp-foo-Bar123");
}
#[test]
fn encodes_worktree_with_double_underscore() {
let path = Path::new("/Users/raine/code/claude-history__worktrees/claude-search");
let converted = convert_path_to_project_dir_name(path);
assert_eq!(
converted,
"-Users-raine-code-claude-history--worktrees-claude-search"
);
}
#[test]
fn encodes_hidden_directory() {
let path = Path::new("/Users/raine/dotfiles/.config/karabiner");
let converted = convert_path_to_project_dir_name(path);
assert_eq!(converted, "-Users-raine-dotfiles--config-karabiner");
}
#[test]
fn decodes_consecutive_dashes_to_underscores() {
let encoded = "-Users-raine-code-myproject--worktrees-feature";
let decoded = decode_project_dir_name(encoded);
assert_eq!(decoded, "/Users/raine/code/myproject__worktrees/feature");
let encoded = "-Users-raine-code-myproject---worktrees-feature";
let decoded = decode_project_dir_name(encoded);
assert_eq!(decoded, "/Users/raine/code/myproject/__worktrees/feature");
}
#[test]
fn decodes_single_dashes_to_slashes() {
let encoded = "-tmp-foo-Bar123";
let decoded = decode_project_dir_name(encoded);
assert_eq!(decoded, "/tmp/foo/Bar123");
}
#[test]
fn decode_with_double_dash_as_underscore() {
let encoded = "-Users-raine-code-project--worktrees-feature";
let decoded = decode_with_double_dash_as(encoded, "__");
assert_eq!(decoded, "/Users/raine/code/project__worktrees/feature");
}
#[test]
fn decode_with_double_dash_as_hidden_dir() {
let encoded = "-Users-raine-dotfiles--config-karabiner";
let decoded = decode_with_double_dash_as(encoded, "/.");
assert_eq!(decoded, "/Users/raine/dotfiles/.config/karabiner");
}
#[test]
fn decode_preserves_dashes_in_folder_names_in_fallback() {
let encoded = "-Users-raine-code-claude-history";
let decoded = decode_with_double_dash_as(encoded, "__");
assert_eq!(decoded, "/Users/raine/code/claude/history");
}
#[test]
fn worktree_encoded_pattern() {
let path = Path::new("/Users/raine/code/WalkingMate__worktrees/template-engine");
let encoded = convert_path_to_project_dir_name(path);
assert_eq!(
encoded,
"-Users-raine-code-WalkingMate--worktrees-template-engine"
);
assert!(encoded.contains("--worktrees-"));
}
#[test]
fn extract_worktree_name_from_encoded() {
let encoded = "-Users-raine-code-WalkingMate--worktrees-template-engine";
let wt_pos = encoded.find("--worktrees-").unwrap();
let worktree_name = &encoded[wt_pos + "--worktrees-".len()..];
assert_eq!(worktree_name, "template-engine");
}
#[test]
fn extract_project_name_before_worktrees() {
let encoded = "-Users-raine-code-WalkingMate--worktrees-template-engine";
let wt_pos = encoded.find("--worktrees-").unwrap();
let before_wt = &encoded[..wt_pos];
assert_eq!(before_wt, "-Users-raine-code-WalkingMate");
let decoded = decode_with_double_dash_as(before_wt, "__");
assert_eq!(decoded, "/Users/raine/code/WalkingMate");
}
#[test]
fn format_short_name_extracts_worktree_pattern() {
let path = "/Users/raine/code/WalkingMate__worktrees/template-engine";
assert!(path.contains("__worktrees/"));
let wt_pos = path.find("__worktrees/").unwrap();
let before = &path[..wt_pos];
let main_project = Path::new(before)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap();
assert_eq!(main_project, "WalkingMate");
let after = &path[wt_pos + "__worktrees/".len()..];
let worktree = after.split('/').next().unwrap();
assert_eq!(worktree, "template-engine");
let display = format!("{}/{}", main_project, worktree);
assert_eq!(display, "WalkingMate/template-engine");
}
#[test]
fn format_short_name_hidden_worktrees() {
let path = "/Users/raine/code/workmux/.worktrees/uncommitted";
assert!(path.contains("/.worktrees/"));
let wt_pos = path.find("/.worktrees/").unwrap();
let before = &path[..wt_pos];
let main_project = Path::new(before)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap();
assert_eq!(main_project, "workmux");
let after = &path[wt_pos + "/.worktrees/".len()..];
let worktree = after.split('/').next().unwrap();
assert_eq!(worktree, "uncommitted");
}
#[test]
fn encoded_project_root_strips_worktree_suffix() {
assert_eq!(
encoded_project_root("-Users-raine-code-project--worktrees-branch"),
"-Users-raine-code-project"
);
}
#[test]
fn encoded_project_root_returns_full_name_without_worktree() {
assert_eq!(
encoded_project_root("-Users-raine-code-project"),
"-Users-raine-code-project"
);
}
#[test]
fn is_same_project_matches_main_and_worktree() {
let main = "-Users-raine-code-project";
let worktree = "-Users-raine-code-project--worktrees-fix-search";
assert!(is_same_project(main, worktree));
assert!(is_same_project(worktree, main));
}
#[test]
fn is_same_project_matches_two_worktrees() {
let wt1 = "-Users-raine-code-project--worktrees-branch-a";
let wt2 = "-Users-raine-code-project--worktrees-branch-b";
assert!(is_same_project(wt1, wt2));
}
#[test]
fn is_same_project_matches_identical() {
let name = "-Users-raine-code-project";
assert!(is_same_project(name, name));
}
#[test]
fn is_same_project_rejects_different_projects() {
let a = "-Users-raine-code-project-a";
let b = "-Users-raine-code-project-b";
assert!(!is_same_project(a, b));
}
#[test]
fn is_same_project_hidden_worktrees() {
let main = convert_path_to_project_dir_name(Path::new("/Users/raine/code/myproject"));
let worktree = convert_path_to_project_dir_name(Path::new(
"/Users/raine/code/myproject/.worktrees/feature",
));
assert!(is_same_project(&main, &worktree));
}
#[test]
fn is_same_project_double_underscore_worktrees() {
let main = convert_path_to_project_dir_name(Path::new("/Users/raine/code/myproject"));
let worktree = convert_path_to_project_dir_name(Path::new(
"/Users/raine/code/myproject__worktrees/feature",
));
assert!(is_same_project(&main, &worktree));
}
}