#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
Log,
Branches,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Branches,
Log,
Detail,
}
impl Focus {
#[must_use]
pub fn cycle(self, mode: AppMode) -> Self {
match mode {
AppMode::Branches => match self {
Focus::Branches => Focus::Log,
Focus::Log => Focus::Detail,
Focus::Detail => Focus::Branches,
},
AppMode::Log => match self {
Focus::Log => Focus::Detail,
Focus::Detail | Focus::Branches => Focus::Log,
},
}
}
#[must_use]
pub fn cycle_back(self, mode: AppMode) -> Self {
match mode {
AppMode::Branches => match self {
Focus::Branches => Focus::Detail,
Focus::Log => Focus::Branches,
Focus::Detail => Focus::Log,
},
AppMode::Log => match self {
Focus::Log | Focus::Branches => Focus::Detail,
Focus::Detail => Focus::Log,
},
}
}
}
pub struct App {
pub mode: AppMode,
pub focus: Focus,
pub branches: Vec<crate::workspace::Workspace>,
pub snapshots: Vec<crate::snapshot::Snapshot>,
pub current_branch: String,
pub branch_scroll: super::VirtualScroll,
pub log_scroll: super::VirtualScroll,
pub should_quit: bool,
}
impl App {
#[must_use]
pub fn for_log(snapshots: Vec<crate::snapshot::Snapshot>, current_branch: String) -> Self {
let count = snapshots.len();
Self {
mode: AppMode::Log,
focus: Focus::Log,
branches: vec![],
snapshots,
current_branch,
branch_scroll: super::VirtualScroll::new(0),
log_scroll: super::VirtualScroll::new(count),
should_quit: false,
}
}
#[must_use]
pub fn for_branches(
branches: Vec<crate::workspace::Workspace>,
snapshots: Vec<crate::snapshot::Snapshot>,
current_branch: String,
) -> Self {
let branch_count = branches.len();
let log_count = snapshots.len();
Self {
mode: AppMode::Branches,
focus: Focus::Branches,
branches,
snapshots,
current_branch,
branch_scroll: super::VirtualScroll::new(branch_count),
log_scroll: super::VirtualScroll::new(log_count),
should_quit: false,
}
}
pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
use crossterm::event::{KeyCode, KeyModifiers};
match (key.modifiers, key.code) {
(_, KeyCode::Char('q') | KeyCode::Esc) => {
self.should_quit = true;
return true;
}
(_, KeyCode::Tab) => {
self.focus = self.focus.cycle(self.mode);
}
(KeyModifiers::SHIFT, KeyCode::BackTab) => {
self.focus = self.focus.cycle_back(self.mode);
}
(_, KeyCode::Up | KeyCode::Char('k')) => self.scroll_up(),
(_, KeyCode::Down | KeyCode::Char('j')) => self.scroll_down(),
(KeyModifiers::CONTROL, KeyCode::Char('b')) => {
if self.mode == AppMode::Log {
self.mode = AppMode::Branches;
self.focus = Focus::Branches;
} else {
self.mode = AppMode::Log;
self.focus = Focus::Log;
}
}
(_, KeyCode::Enter) if self.mode == AppMode::Log && self.focus == Focus::Log => {
self.focus = Focus::Detail;
}
_ => {}
}
false
}
fn scroll_up(&mut self) {
match self.focus {
Focus::Branches => self.branch_scroll.scroll_up(1),
Focus::Log => self.log_scroll.scroll_up(1),
Focus::Detail => {}
}
}
fn scroll_down(&mut self) {
match self.focus {
Focus::Branches => self.branch_scroll.scroll_down(1),
Focus::Log => self.log_scroll.scroll_down(1),
Focus::Detail => {}
}
}
#[must_use]
pub fn selected_snapshot(&self) -> Option<&crate::snapshot::Snapshot> {
let idx = self.log_scroll.selected_index()?;
self.snapshots.get(idx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn make_test_app() -> App {
App {
mode: AppMode::Log,
focus: Focus::Log,
branches: vec![],
snapshots: vec![],
current_branch: "default".to_string(),
branch_scroll: crate::tui::VirtualScroll::new(0),
log_scroll: crate::tui::VirtualScroll::new(0),
should_quit: false,
}
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn test_quit_on_q() {
let mut app = make_test_app();
let result = app.handle_key(key(KeyCode::Char('q')));
assert!(result);
assert!(app.should_quit);
}
#[test]
fn test_quit_on_esc() {
let mut app = make_test_app();
let result = app.handle_key(key(KeyCode::Esc));
assert!(result);
assert!(app.should_quit);
}
#[test]
fn test_tab_cycles_focus_in_log_mode() {
let mut app = make_test_app();
assert_eq!(app.focus, Focus::Log);
app.handle_key(key(KeyCode::Tab));
assert_eq!(app.focus, Focus::Detail);
app.handle_key(key(KeyCode::Tab));
assert_eq!(app.focus, Focus::Log);
}
#[test]
fn test_enter_focuses_detail_in_log_mode() {
let mut app = make_test_app();
app.mode = AppMode::Log;
app.focus = Focus::Log;
app.handle_key(key(KeyCode::Enter));
assert_eq!(app.focus, Focus::Detail);
}
#[test]
fn test_enter_does_nothing_in_branches_mode() {
let mut app = make_test_app();
app.mode = AppMode::Branches;
app.focus = Focus::Log;
app.handle_key(key(KeyCode::Enter));
assert_eq!(app.focus, Focus::Log);
}
#[test]
fn test_ctrl_b_toggles_mode() {
let mut app = make_test_app();
assert_eq!(app.mode, AppMode::Log);
app.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL));
assert_eq!(app.mode, AppMode::Branches);
assert_eq!(app.focus, Focus::Branches);
app.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL));
assert_eq!(app.mode, AppMode::Log);
assert_eq!(app.focus, Focus::Log);
}
#[test]
fn test_unknown_key_does_not_quit() {
let mut app = make_test_app();
let result = app.handle_key(key(KeyCode::Char('x')));
assert!(!result);
assert!(!app.should_quit);
}
}