gitui/src/main.rs

410 lines
8.1 KiB
Rust

#![forbid(unsafe_code)]
#![deny(
unused_imports,
unused_must_use,
dead_code,
unstable_name_collisions,
unused_assignments
)]
#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]
#![deny(
clippy::unwrap_used,
clippy::filetype_is_file,
clippy::cargo,
clippy::unwrap_used,
clippy::panic,
clippy::match_like_matches_macro
)]
#![allow(clippy::module_name_repetitions)]
#![allow(
clippy::multiple_crate_versions,
clippy::bool_to_int_with_if,
clippy::module_name_repetitions
)]
// high number of false positives on nightly (as of Oct 2022 with 1.66.0-nightly)
#![allow(clippy::missing_const_for_fn)]
//TODO:
// #![deny(clippy::expect_used)]
mod app;
mod args;
mod bug_report;
mod clipboard;
mod cmdbar;
mod components;
mod input;
mod keys;
mod notify_mutex;
mod options;
mod popup_stack;
mod profiler;
mod queue;
mod spinner;
mod string_utils;
mod strings;
mod tabs;
mod ui;
mod version;
mod watcher;
use crate::{app::App, args::process_cmdline};
use anyhow::{bail, Result};
use app::QuitState;
use asyncgit::{
sync::{utils::repo_work_dir, RepoPath},
AsyncGitNotification,
};
use backtrace::Backtrace;
use crossbeam_channel::{never, tick, unbounded, Receiver, Select};
use crossterm::{
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
LeaveAlternateScreen,
},
ExecutableCommand,
};
use input::{Input, InputEvent, InputState};
use keys::KeyConfig;
use profiler::Profiler;
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use scopeguard::defer;
use scopetime::scope_time;
use spinner::Spinner;
use std::{
cell::RefCell,
io::{self, Write},
panic, process,
time::{Duration, Instant},
};
use ui::style::Theme;
use watcher::RepoWatcher;
static TICK_INTERVAL: Duration = Duration::from_secs(5);
static SPINNER_INTERVAL: Duration = Duration::from_millis(80);
///
#[derive(Clone)]
pub enum QueueEvent {
Tick,
Notify,
SpinnerUpdate,
AsyncEvent(AsyncNotification),
InputEvent(InputEvent),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SyntaxHighlightProgress {
Progress,
Done,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AsyncAppNotification {
///
SyntaxHighlighting(SyntaxHighlightProgress),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AsyncNotification {
///
App(AsyncAppNotification),
///
Git(AsyncGitNotification),
}
#[derive(Clone, Copy, PartialEq)]
enum Updater {
Ticker,
NotifyWatcher,
}
fn main() -> Result<()> {
let app_start = Instant::now();
let cliargs = process_cmdline()?;
let _profiler = Profiler::new();
asyncgit::register_tracing_logging();
if !valid_path(&cliargs.repo_path) {
bail!("invalid path\nplease run gitui inside of a non-bare git repository");
}
let key_config = KeyConfig::init()
.map_err(|e| eprintln!("KeyConfig loading error: {e}"))
.unwrap_or_default();
let theme = Theme::init(&cliargs.theme);
setup_terminal()?;
defer! {
shutdown_terminal();
}
set_panic_handlers()?;
let mut terminal = start_terminal(io::stdout())?;
let mut repo_path = cliargs.repo_path;
let input = Input::new();
let updater = if cliargs.notify_watcher {
Updater::NotifyWatcher
} else {
Updater::Ticker
};
loop {
let quit_state = run_app(
app_start,
repo_path.clone(),
theme,
key_config.clone(),
&input,
updater,
&mut terminal,
)?;
match quit_state {
QuitState::OpenSubmodule(p) => {
repo_path = p;
}
_ => break,
}
}
Ok(())
}
fn run_app(
app_start: Instant,
repo: RepoPath,
theme: Theme,
key_config: KeyConfig,
input: &Input,
updater: Updater,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<QuitState, anyhow::Error> {
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(&repo)?.as_str());
(never(), repo_watcher.receiver())
}
Updater::Ticker => (tick(TICK_INTERVAL), never()),
};
let spinner_ticker = tick(SPINNER_INTERVAL);
let mut app = App::new(
RefCell::new(repo),
&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());
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<()> {
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
Ok(())
}
fn shutdown_terminal() {
let leave_screen =
io::stdout().execute(LeaveAlternateScreen).map(|_f| ());
if let Err(e) = leave_screen {
eprintln!("leave_screen failed:\n{e}");
}
let leave_raw_mode = disable_raw_mode();
if let Err(e) = leave_raw_mode {
eprintln!("leave_raw_mode failed:\n{e}");
}
}
fn draw<B: Backend>(
terminal: &mut Terminal<B>,
app: &App,
) -> io::Result<()> {
if app.requires_redraw() {
terminal.resize(terminal.size()?)?;
}
terminal.draw(|f| {
if let Err(e) = app.draw(f) {
log::error!("failed to draw: {:?}", e);
}
})?;
Ok(())
}
fn valid_path(repo_path: &RepoPath) -> bool {
let error = asyncgit::sync::repo_open_error(repo_path);
if let Some(error) = &error {
eprintln!("repo open error: {error}");
}
error.is_none()
}
fn select_event(
rx_input: &Receiver<InputEvent>,
rx_git: &Receiver<AsyncGitNotification>,
rx_app: &Receiver<AsyncAppNotification>,
rx_ticker: &Receiver<Instant>,
rx_notify: &Receiver<()>,
rx_spinner: &Receiver<Instant>,
) -> Result<QueueEvent> {
let mut sel = Select::new();
sel.recv(rx_input);
sel.recv(rx_git);
sel.recv(rx_app);
sel.recv(rx_ticker);
sel.recv(rx_notify);
sel.recv(rx_spinner);
let oper = sel.select();
let index = oper.index();
let ev = match index {
0 => oper.recv(rx_input).map(QueueEvent::InputEvent),
1 => oper.recv(rx_git).map(|e| {
QueueEvent::AsyncEvent(AsyncNotification::Git(e))
}),
2 => oper.recv(rx_app).map(|e| {
QueueEvent::AsyncEvent(AsyncNotification::App(e))
}),
3 => oper.recv(rx_ticker).map(|_| QueueEvent::Notify),
4 => oper.recv(rx_notify).map(|_| QueueEvent::Notify),
5 => oper.recv(rx_spinner).map(|_| QueueEvent::SpinnerUpdate),
_ => bail!("unknown select source"),
}?;
Ok(ev)
}
fn start_terminal<W: Write>(
buf: W,
) -> io::Result<Terminal<CrosstermBackend<W>>> {
let backend = CrosstermBackend::new(buf);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
terminal.clear()?;
Ok(terminal)
}
// do log::error! and eprintln! in one line, pass sting, error and backtrace
macro_rules! log_eprintln {
($string:expr, $e:expr, $bt:expr) => {
log::error!($string, $e, $bt);
eprintln!($string, $e, $bt);
};
}
fn set_panic_handlers() -> Result<()> {
// regular panic handler
panic::set_hook(Box::new(|e| {
let backtrace = Backtrace::new();
log_eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
shutdown_terminal();
}));
// global threadpool
rayon_core::ThreadPoolBuilder::new()
.panic_handler(|e| {
let backtrace = Backtrace::new();
log_eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
shutdown_terminal();
process::abort();
})
.num_threads(4)
.build_global()?;
Ok(())
}