diff --git a/Cargo.lock b/Cargo.lock index c955575e..66d994d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,18 +465,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.59.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -814,12 +802,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -1210,9 +1192,7 @@ dependencies = [ "filetreelist", "fuzzy-matcher", "gh-emoji", - "git2-testing", "indexmap", - "insta", "itertools 0.14.0", "log", "notify", @@ -2286,18 +2266,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "insta" -version = "1.44.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" -dependencies = [ - "console", - "once_cell", - "regex", - "similar", -] - [[package]] name = "instability" version = "0.3.6" @@ -3495,12 +3463,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "simplelog" version = "0.12.2" diff --git a/Cargo.toml b/Cargo.toml index 4962404f..fbcaad79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,8 +89,6 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } [dev-dependencies] env_logger = "0.11" -git2-testing = { path = "./git2-testing" } -insta = { version = "1.41.0", features = ["filters"] } pretty_assertions = "1.4" tempfile = "3" diff --git a/git2-testing/src/lib.rs b/git2-testing/src/lib.rs index 838e3013..40e16790 100644 --- a/git2-testing/src/lib.rs +++ b/git2-testing/src/lib.rs @@ -20,18 +20,13 @@ pub fn repo_init_empty() -> (TempDir, Repository) { (td, repo) } -/// initialize test repo in temp path with given suffix and an empty first commit -pub fn repo_init_suffix>( - suffix: Option, -) -> (TempDir, Repository) { +/// initialize test repo in temp path with an empty first commit +pub fn repo_init() -> (TempDir, Repository) { init_log(); sandbox_config_files(); - let td = match suffix { - Some(suffix) => TempDir::with_suffix(suffix).unwrap(), - None => TempDir::new().unwrap(), - }; + let td = TempDir::new().unwrap(); let repo = Repository::init(td.path()).unwrap(); { let mut config = repo.config().unwrap(); @@ -50,11 +45,6 @@ pub fn repo_init_suffix>( (td, repo) } -/// initialize test repo in temp path with an empty first commit -pub fn repo_init() -> (TempDir, Repository) { - repo_init_suffix::<&std::ffi::OsStr>(None) -} - // init log fn init_log() { let _ = env_logger::builder() diff --git a/src/gitui.rs b/src/gitui.rs deleted file mode 100644 index a593614b..00000000 --- a/src/gitui.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::time::Instant; - -use anyhow::Result; -use asyncgit::{sync::utils::repo_work_dir, AsyncGitNotification}; -use crossbeam_channel::{never, tick, unbounded, Receiver}; -use scopetime::scope_time; - -#[cfg(test)] -use crossterm::event::{KeyCode, KeyModifiers}; - -use crate::{ - app::{App, QuitState}, - args::CliArgs, - draw, - input::{Input, InputEvent, InputState}, - keys::KeyConfig, - select_event, - spinner::Spinner, - ui::style::Theme, - watcher::RepoWatcher, - AsyncAppNotification, AsyncNotification, QueueEvent, Updater, - SPINNER_INTERVAL, TICK_INTERVAL, -}; - -pub struct Gitui { - app: crate::app::App, - rx_input: Receiver, - rx_git: Receiver, - rx_app: Receiver, - rx_ticker: Receiver, - rx_watcher: Receiver<()>, -} - -impl Gitui { - pub(crate) fn new( - cliargs: CliArgs, - theme: Theme, - key_config: &KeyConfig, - updater: Updater, - ) -> Result { - let (tx_git, rx_git) = unbounded(); - let (tx_app, rx_app) = unbounded(); - - let input = Input::new(); - - let (rx_ticker, rx_watcher) = match updater { - Updater::NotifyWatcher => { - let repo_watcher = RepoWatcher::new( - repo_work_dir(&cliargs.repo_path)?.as_str(), - ); - - (never(), repo_watcher.receiver()) - } - Updater::Ticker => (tick(TICK_INTERVAL), never()), - }; - - let app = App::new( - cliargs, - tx_git, - tx_app, - input.clone(), - theme, - key_config.clone(), - )?; - - Ok(Self { - app, - rx_input: input.receiver(), - rx_git, - rx_app, - rx_ticker, - rx_watcher, - }) - } - - pub(crate) fn run_main_loop( - &mut self, - terminal: &mut ratatui::Terminal, - ) -> Result { - let spinner_ticker = tick(SPINNER_INTERVAL); - let mut spinner = Spinner::default(); - - self.app.update()?; - - loop { - let event = select_event( - &self.rx_input, - &self.rx_git, - &self.rx_app, - &self.rx_ticker, - &self.rx_watcher, - &spinner_ticker, - )?; - - { - if matches!(event, QueueEvent::SpinnerUpdate) { - spinner.update(); - spinner.draw(terminal)?; - continue; - } - - scope_time!("loop"); - - match event { - QueueEvent::InputEvent(ev) => { - if matches!( - ev, - InputEvent::State(InputState::Polling) - ) { - //Note: external ed closed, we need to re-hide cursor - terminal.hide_cursor()?; - } - self.app.event(ev)?; - } - QueueEvent::Tick | QueueEvent::Notify => { - self.app.update()?; - } - QueueEvent::AsyncEvent(ev) => { - if !matches!( - ev, - AsyncNotification::Git( - AsyncGitNotification::FinishUnchanged - ) - ) { - self.app.update_async(ev)?; - } - } - QueueEvent::SpinnerUpdate => unreachable!(), - } - - self.draw(terminal)?; - - spinner.set_state(self.app.any_work_pending()); - spinner.draw(terminal)?; - - if self.app.is_quit() { - break; - } - } - } - - Ok(self.app.quit_state()) - } - - fn draw( - &self, - terminal: &mut ratatui::Terminal, - ) -> std::io::Result<()> { - draw(terminal, &self.app) - } - - #[cfg(test)] - fn update_async(&mut self, event: crate::AsyncNotification) { - self.app.update_async(event).unwrap(); - } - - #[cfg(test)] - fn input_event( - &mut self, - code: KeyCode, - modifiers: KeyModifiers, - ) { - let event = crossterm::event::KeyEvent::new(code, modifiers); - self.app - .event(crate::input::InputEvent::Input( - crossterm::event::Event::Key(event), - )) - .unwrap(); - } - - #[cfg(test)] - fn wait_for_async_git_notification( - &self, - expected: AsyncGitNotification, - ) { - loop { - let actual = self - .rx_git - .recv_timeout(std::time::Duration::from_millis(100)) - .unwrap(); - - if actual == expected { - break; - } - } - } - - #[cfg(test)] - fn update(&mut self) { - self.app.update().unwrap(); - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use asyncgit::{sync::RepoPath, AsyncGitNotification}; - use crossterm::event::{KeyCode, KeyModifiers}; - use git2_testing::repo_init_suffix; - use insta::assert_snapshot; - use ratatui::{backend::TestBackend, Terminal}; - - use crate::{ - args::CliArgs, gitui::Gitui, keys::KeyConfig, - ui::style::Theme, AsyncNotification, Updater, - }; - - // Macro adapted from: https://insta.rs/docs/cmd/ - macro_rules! apply_common_filters { - {} => { - let mut settings = insta::Settings::clone_current(); - // Windows and MacOS - // We don't match on the full path, but on the suffix we pass to `repo_init_suffix` below. - settings.add_filter(r" *\[…\]\S+-insta/?", "[TEMP_FILE]"); - // Linux Temp Folder - settings.add_filter(r" */tmp/\.tmp\S+-insta/", "[TEMP_FILE]"); - // Commit ids that follow a vertical bar - settings.add_filter(r"│[a-z0-9]{7} ", "│[AAAAA] "); - let _bound = settings.bind_to_scope(); - } - } - - #[test] - fn gitui_starts() { - apply_common_filters!(); - - let (temp_dir, _repo) = repo_init_suffix(Some("-insta")); - let path: RepoPath = temp_dir.path().to_str().unwrap().into(); - let cliargs = CliArgs { - theme: PathBuf::from("theme.ron"), - select_file: None, - repo_path: path, - notify_watcher: false, - key_bindings_path: None, - key_symbols_path: None, - }; - - let theme = Theme::init(&PathBuf::new()); - let key_config = KeyConfig::default(); - - let mut gitui = - Gitui::new(cliargs, theme, &key_config, Updater::Ticker) - .unwrap(); - - let mut terminal = - Terminal::new(TestBackend::new(90, 12)).unwrap(); - - gitui.draw(&mut terminal).unwrap(); - - assert_snapshot!("app_loading", terminal.backend()); - - let event = - AsyncNotification::Git(AsyncGitNotification::Status); - gitui.update_async(event); - - gitui.draw(&mut terminal).unwrap(); - - assert_snapshot!("app_loading_finished", terminal.backend()); - - gitui.input_event(KeyCode::Char('2'), KeyModifiers::empty()); - gitui.input_event( - key_config.keys.tab_log.code, - key_config.keys.tab_log.modifiers, - ); - - gitui.wait_for_async_git_notification( - AsyncGitNotification::Log, - ); - - gitui.update(); - - gitui.draw(&mut terminal).unwrap(); - - assert_snapshot!( - "app_log_tab_showing_one_commit", - terminal.backend() - ); - } -} diff --git a/src/main.rs b/src/main.rs index 2159a185..12fbb71d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,6 @@ mod bug_report; mod clipboard; mod cmdbar; mod components; -mod gitui; mod input; mod keys; mod notify_mutex; @@ -86,9 +85,12 @@ use crate::{ }; use anyhow::{anyhow, bail, Result}; use app::QuitState; -use asyncgit::{sync::RepoPath, AsyncGitNotification}; +use asyncgit::{ + sync::{utils::repo_work_dir, RepoPath}, + AsyncGitNotification, +}; use backtrace::Backtrace; -use crossbeam_channel::{Receiver, Select}; +use crossbeam_channel::{never, tick, unbounded, Receiver, Select}; use crossterm::{ terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, @@ -96,11 +98,12 @@ use crossterm::{ }, ExecutableCommand, }; -use gitui::Gitui; -use input::InputEvent; +use input::{Input, InputEvent, InputState}; use keys::KeyConfig; use ratatui::backend::CrosstermBackend; use scopeguard::defer; +use scopetime::scope_time; +use spinner::Spinner; use std::{ io::{self, Stdout}, panic, @@ -108,6 +111,7 @@ use std::{ time::{Duration, Instant}, }; use ui::style::Theme; +use watcher::RepoWatcher; type Terminal = ratatui::Terminal>; @@ -183,6 +187,7 @@ fn main() -> Result<()> { let mut terminal = start_terminal(io::stdout(), &cliargs.repo_path)?; + let input = Input::new(); let updater = if cliargs.notify_watcher { Updater::NotifyWatcher @@ -197,7 +202,8 @@ fn main() -> Result<()> { app_start, args.clone(), theme.clone(), - &key_config, + key_config.clone(), + &input, updater, &mut terminal, )?; @@ -224,15 +230,106 @@ fn run_app( app_start: Instant, cliargs: CliArgs, theme: Theme, - key_config: &KeyConfig, + key_config: KeyConfig, + input: &Input, updater: Updater, terminal: &mut Terminal, ) -> Result { - let mut gitui = Gitui::new(cliargs, theme, key_config, updater)?; + let (tx_git, rx_git) = unbounded(); + let (tx_app, rx_app) = unbounded(); + + let rx_input = input.receiver(); + + let (rx_ticker, rx_watcher) = match updater { + Updater::NotifyWatcher => { + let repo_watcher = RepoWatcher::new( + repo_work_dir(&cliargs.repo_path)?.as_str(), + ); + + (never(), repo_watcher.receiver()) + } + Updater::Ticker => (tick(TICK_INTERVAL), never()), + }; + + let spinner_ticker = tick(SPINNER_INTERVAL); + + let mut app = App::new( + cliargs, + tx_git, + tx_app, + input.clone(), + theme, + key_config, + )?; + + let mut spinner = Spinner::default(); + let mut first_update = true; log::trace!("app start: {} ms", app_start.elapsed().as_millis()); - gitui.run_main_loop(terminal) + loop { + let event = if first_update { + first_update = false; + QueueEvent::Notify + } else { + select_event( + &rx_input, + &rx_git, + &rx_app, + &rx_ticker, + &rx_watcher, + &spinner_ticker, + )? + }; + + { + if matches!(event, QueueEvent::SpinnerUpdate) { + spinner.update(); + spinner.draw(terminal)?; + continue; + } + + scope_time!("loop"); + + match event { + QueueEvent::InputEvent(ev) => { + if matches!( + ev, + InputEvent::State(InputState::Polling) + ) { + //Note: external ed closed, we need to re-hide cursor + terminal.hide_cursor()?; + } + app.event(ev)?; + } + QueueEvent::Tick | QueueEvent::Notify => { + app.update()?; + } + QueueEvent::AsyncEvent(ev) => { + if !matches!( + ev, + AsyncNotification::Git( + AsyncGitNotification::FinishUnchanged + ) + ) { + app.update_async(ev)?; + } + } + QueueEvent::SpinnerUpdate => unreachable!(), + } + + draw(terminal, &app)?; + + spinner.set_state(app.any_work_pending()); + spinner.draw(terminal)?; + + if app.is_quit() { + break; + } + } + } + + Ok(app.quit_state()) } fn setup_terminal() -> Result<()> { @@ -256,10 +353,7 @@ fn shutdown_terminal() { } } -fn draw( - terminal: &mut ratatui::Terminal, - app: &App, -) -> io::Result<()> { +fn draw(terminal: &mut Terminal, app: &App) -> io::Result<()> { if app.requires_redraw() { terminal.clear()?; } diff --git a/src/snapshots/gitui__gitui__tests__app_loading.snap b/src/snapshots/gitui__gitui__tests__app_loading.snap deleted file mode 100644 index 6a8025c3..00000000 --- a/src/snapshots/gitui__gitui__tests__app_loading.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: src/gitui.rs -expression: terminal.backend() -snapshot_kind: text ---- -" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " -" ──────────────────────────────────────────────────────────────────────────────────────── " -"┌Unstaged Changes───────────────────────────┐┌Diff: ─────────────────────────────────────┐" -"│Loading ... ││ │" -"│ ││ │" -"│ ││ │" -"└───────────────────────────────────{master}┘│ │" -"┌Staged Changes─────────────────────────────┐│ │" -"│Loading ... ││ │" -"│ ││ │" -"└───────────────────────────────────────────┘└───────────────────────────────────────────┘" -" " diff --git a/src/snapshots/gitui__gitui__tests__app_loading_finished.snap b/src/snapshots/gitui__gitui__tests__app_loading_finished.snap deleted file mode 100644 index 97229001..00000000 --- a/src/snapshots/gitui__gitui__tests__app_loading_finished.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: src/gitui.rs -expression: terminal.backend() -snapshot_kind: text ---- -" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " -" ──────────────────────────────────────────────────────────────────────────────────────── " -"┌Unstaged Changes───────────────────────────┐┌Diff: ─────────────────────────────────────┐" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"└───────────────────────────────────{master}┘│ │" -"┌Staged Changes─────────────────────────────┐│ │" -"│ ││ │" -"│ ││ │" -"└───────────────────────────────────────────┘└───────────────────────────────────────────┘" -"Branches [b] Push [p] Fetch [⇧F] Pull [f] Undo Commit [⇧U] Submodules [⇧S] more [.]" diff --git a/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap b/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap deleted file mode 100644 index bbdd5be8..00000000 --- a/src/snapshots/gitui__gitui__tests__app_log_tab_showing_one_commit.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: src/gitui.rs -expression: terminal.backend() -snapshot_kind: text ---- -" Status [1] | Log [2] | Files [3] | Stashing [4] | Stashes [5][TEMP_FILE] " -" ──────────────────────────────────────────────────────────────────────────────────────── " -"┌Commit 1/1──────────────────────────────────────────────────────────────────────────────┐" -"│[AAAAA] <1m ago name initial █" -"│ ║" -"│ ║" -"│ ║" -"│ ║" -"│ ║" -"│ ║" -"└────────────────────────────────────────────────────────────────────────────────────────┘" -"Scroll [↑↓] Mark [˽] Details [⏎] Branches [b] Compare [⇧C] Copy Hash [y] Tag [t] more [.]" diff --git a/src/spinner.rs b/src/spinner.rs index c8066ae6..2fc6b3a2 100644 --- a/src/spinner.rs +++ b/src/spinner.rs @@ -1,4 +1,7 @@ -use ratatui::{backend::Backend, Terminal}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + Terminal, +}; use std::{cell::Cell, char, io}; // static SPINNER_CHARS: &[char] = &['◢', '◣', '◤', '◥']; @@ -36,9 +39,9 @@ impl Spinner { } /// draws or removes spinner char depending on `pending` state - pub fn draw( + pub fn draw( &self, - terminal: &mut Terminal, + terminal: &mut Terminal>, ) -> io::Result<()> { let idx = self.idx;