Skip to main content

thor_wt/
go.rs

1use thor_core::{find_repo, list_worktrees};
2use std::path::PathBuf;
3
4/// What the user wants to navigate to.
5pub enum GoQuery {
6    /// Fuzzy substring match on branch name.
7    Fuzzy(String),
8    /// Jump by 1-based index in the worktree list.
9    Index(usize),
10    /// Return to the previous worktree.
11    Previous,
12}
13
14/// Result of a go operation.
15pub enum GoResult {
16    Found { path: PathBuf, branch: String },
17    NotFound(String),
18    Ambiguous(Vec<String>),
19}
20
21/// Persist the current worktree path for `go -`.
22pub fn save_last_worktree(path: &std::path::Path) {
23    if let Some(data_dir) = dirs::data_dir() {
24        let thor_dir = data_dir.join("thor");
25        let _ = std::fs::create_dir_all(&thor_dir);
26        let _ = std::fs::write(thor_dir.join("last_worktree"), path.display().to_string());
27    }
28}
29
30fn load_last_worktree() -> Option<PathBuf> {
31    let data_dir = dirs::data_dir()?;
32    let path_str = std::fs::read_to_string(data_dir.join("thor").join("last_worktree")).ok()?;
33    let path = PathBuf::from(path_str.trim());
34    if path.is_dir() { Some(path) } else { None }
35}
36
37/// Navigate to a worktree by fuzzy match, index, or previous.
38pub async fn go(query: &GoQuery) -> anyhow::Result<GoResult> {
39    let repo = find_repo()?;
40    let worktrees = list_worktrees(&repo).await?;
41
42    match query {
43        GoQuery::Previous => {
44            match load_last_worktree() {
45                Some(saved) => {
46                    // Match saved path against worktree roots (cwd may be a subdirectory)
47                    match worktrees.iter().find(|wt| saved.starts_with(&wt.path)) {
48                        Some(wt) => Ok(GoResult::Found {
49                            path: wt.path.clone(),
50                            branch: wt.display_name(),
51                        }),
52                        None => Ok(GoResult::Found { path: saved, branch: "unknown".to_string() }),
53                    }
54                }
55                None => Ok(GoResult::NotFound("No previous worktree".to_string())),
56            }
57        }
58        GoQuery::Index(idx) => {
59            match worktrees.get(idx.saturating_sub(1)) {
60                Some(wt) => Ok(GoResult::Found {
61                    path: wt.path.clone(),
62                    branch: wt.display_name(),
63                }),
64                None => Ok(GoResult::NotFound(format!("Index {} out of range (1-{})", idx, worktrees.len()))),
65            }
66        }
67        GoQuery::Fuzzy(query) => {
68            let query_lower = query.to_lowercase();
69
70            // Try exact match first
71            if let Some(wt) = worktrees.iter().find(|wt| {
72                wt.branch.as_ref().map(|b| b == query).unwrap_or(false)
73            }) {
74                return Ok(GoResult::Found {
75                    path: wt.path.clone(),
76                    branch: wt.display_name(),
77                });
78            }
79
80            // Try substring match (case-insensitive)
81            let matches: Vec<_> = worktrees.iter()
82                .filter(|wt| {
83                    wt.branch.as_ref()
84                        .map(|b| b.to_lowercase().contains(&query_lower))
85                        .unwrap_or(false)
86                })
87                .collect();
88
89            match matches.len() {
90                0 => Ok(GoResult::NotFound(format!("No worktree matching '{}'", query))),
91                1 => Ok(GoResult::Found {
92                    path: matches[0].path.clone(),
93                    branch: matches[0].display_name(),
94                }),
95                _ => Ok(GoResult::Ambiguous(
96                    matches.iter().map(|wt| wt.display_name()).collect()
97                )),
98            }
99        }
100    }
101}