use std::path::Path;
use std::time::Instant;
use super::branch::{self, Worktree};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitRailHit {
Branch(usize),
Worktree(usize),
Pull(usize),
Stash(usize),
Tag(usize),
ToggleBranches,
}
#[derive(Debug, Clone)]
pub struct BranchRow {
pub name: String,
pub is_current: bool,
}
#[derive(Debug, Clone)]
pub struct PullRow {
pub host_tag: &'static str,
pub number_label: String,
pub title: String,
pub source_branch: Option<String>,
pub is_current_branch: bool,
pub web_url: String,
}
#[derive(Debug)]
pub struct GitRail {
pub branches: Vec<BranchRow>,
pub remote_branches: Vec<String>,
pub stashes: Vec<branch::StashRow>,
pub tags: Vec<String>,
pub worktrees: Vec<Worktree>,
pub pulls: Vec<PullRow>,
pub current_branch: Option<String>,
pub cursor: usize,
pub scroll: usize,
pub last_refresh: Option<Instant>,
}
impl Default for GitRail {
fn default() -> Self {
GitRail::empty()
}
}
impl GitRail {
pub fn empty() -> Self {
GitRail {
branches: Vec::new(),
remote_branches: Vec::new(),
stashes: Vec::new(),
tags: Vec::new(),
worktrees: Vec::new(),
pulls: Vec::new(),
current_branch: None,
cursor: 0,
scroll: 0,
last_refresh: None,
}
}
pub fn refresh(&mut self, workspace: &Path) {
let current = branch::current(workspace);
self.remote_branches = branch::remote_branches(workspace);
self.stashes = branch::stashes(workspace);
self.tags = branch::tags(workspace);
self.branches = branch::local_branches(workspace)
.into_iter()
.map(|name| BranchRow {
is_current: Some(&name) == current.as_ref(),
name,
})
.collect();
self.worktrees = branch::worktrees(workspace);
self.current_branch = current;
let max = self.row_count().saturating_sub(1);
self.cursor = self.cursor.min(max);
self.scroll = 0;
self.last_refresh = Some(Instant::now());
}
pub fn row_count(&self) -> usize {
self.branches.len()
+ self.worktrees.len()
+ self.pulls.len()
+ self.stashes.len()
+ self.tags.len()
}
pub fn is_empty(&self) -> bool {
self.row_count() == 0
}
pub fn move_up(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn move_down(&mut self) {
let max = self.row_count().saturating_sub(1);
self.cursor = (self.cursor + 1).min(max);
}
pub fn set_cursor(&mut self, idx: usize) {
let max = self.row_count().saturating_sub(1);
self.cursor = idx.min(max);
}
pub fn selected(&self) -> Option<GitRailHit> {
let nb = self.branches.len();
let nw = self.worktrees.len();
let np = self.pulls.len();
let ns = self.stashes.len();
if self.cursor < nb {
Some(GitRailHit::Branch(self.cursor))
} else if self.cursor < nb + nw {
Some(GitRailHit::Worktree(self.cursor - nb))
} else if self.cursor < nb + nw + np {
Some(GitRailHit::Pull(self.cursor - nb - nw))
} else if self.cursor < nb + nw + np + ns {
Some(GitRailHit::Stash(self.cursor - nb - nw - np))
} else if self.cursor < nb + nw + np + ns + self.tags.len() {
Some(GitRailHit::Tag(self.cursor - nb - nw - np - ns))
} else {
None
}
}
pub fn focus(&mut self, hit: GitRailHit) {
let nb = self.branches.len();
let nw = self.worktrees.len();
let np = self.pulls.len();
let ns = self.stashes.len();
let idx = match hit {
GitRailHit::Branch(i) => i.min(nb.saturating_sub(1)),
GitRailHit::Worktree(i) => nb + i.min(nw.saturating_sub(1)),
GitRailHit::Pull(i) => nb + nw + i.min(np.saturating_sub(1)),
GitRailHit::Stash(i) => nb + nw + np + i.min(ns.saturating_sub(1)),
GitRailHit::Tag(i) => nb + nw + np + ns + i.min(self.tags.len().saturating_sub(1)),
GitRailHit::ToggleBranches => return,
};
self.set_cursor(idx);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rail_with(branches: &[&str], current: Option<&str>, worktrees: usize) -> GitRail {
let mut r = GitRail::empty();
r.current_branch = current.map(str::to_string);
r.branches = branches
.iter()
.map(|b| BranchRow {
is_current: current == Some(b),
name: b.to_string(),
})
.collect();
r.worktrees = (0..worktrees)
.map(|i| Worktree {
path: std::path::PathBuf::from(format!("/tmp/wt{i}")),
label: format!("wt{i}"),
is_current: i == 0,
})
.collect();
r
}
#[test]
fn selected_maps_cursor_to_hit() {
let mut r = rail_with(&["main", "feature/x", "feature/y"], Some("main"), 2);
assert_eq!(r.row_count(), 5);
assert_eq!(r.selected(), Some(GitRailHit::Branch(0)));
r.move_down();
r.move_down();
r.move_down();
assert_eq!(r.selected(), Some(GitRailHit::Worktree(0)));
r.move_down();
assert_eq!(r.selected(), Some(GitRailHit::Worktree(1)));
r.move_down();
assert_eq!(r.selected(), Some(GitRailHit::Worktree(1)));
r.move_up();
r.move_up();
r.move_up();
r.move_up();
r.move_up();
r.move_up();
assert_eq!(r.selected(), Some(GitRailHit::Branch(0)));
}
#[test]
fn empty_rail_has_no_selection() {
let r = GitRail::empty();
assert!(r.is_empty());
assert_eq!(r.selected(), None);
}
#[test]
fn focus_jumps_to_typed_hit() {
let mut r = rail_with(&["main", "feat"], Some("main"), 3);
r.focus(GitRailHit::Worktree(2));
assert_eq!(r.selected(), Some(GitRailHit::Worktree(2)));
r.focus(GitRailHit::Branch(1));
assert_eq!(r.selected(), Some(GitRailHit::Branch(1)));
r.focus(GitRailHit::Branch(99));
assert_eq!(r.selected(), Some(GitRailHit::Branch(1)));
}
}