mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
315 lines
6.6 KiB
Rust
315 lines
6.6 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::filetype_is_file)]
|
|
#![deny(clippy::cargo)]
|
|
#![deny(clippy::unwrap_used)]
|
|
#![deny(clippy::panic)]
|
|
#![deny(clippy::match_like_matches_macro)]
|
|
#![deny(clippy::needless_update)]
|
|
#![allow(clippy::module_name_repetitions)]
|
|
#![allow(clippy::multiple_crate_versions)]
|
|
//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 profiler;
|
|
mod queue;
|
|
mod spinner;
|
|
mod string_utils;
|
|
mod strings;
|
|
mod tabs;
|
|
mod ui;
|
|
mod version;
|
|
|
|
use crate::{app::App, args::process_cmdline};
|
|
use anyhow::{bail, Result};
|
|
use asyncgit::AsyncGitNotification;
|
|
use backtrace::Backtrace;
|
|
use crossbeam_channel::{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 scopeguard::defer;
|
|
use scopetime::scope_time;
|
|
use spinner::Spinner;
|
|
use std::{
|
|
io::{self, Write},
|
|
panic, process,
|
|
time::{Duration, Instant},
|
|
};
|
|
use tui::{
|
|
backend::{Backend, CrosstermBackend},
|
|
Terminal,
|
|
};
|
|
use ui::style::Theme;
|
|
|
|
static TICK_INTERVAL: Duration = Duration::from_secs(5);
|
|
static SPINNER_INTERVAL: Duration = Duration::from_millis(80);
|
|
|
|
///
|
|
#[derive(Clone, Copy)]
|
|
pub enum QueueEvent {
|
|
Tick,
|
|
SpinnerUpdate,
|
|
AsyncEvent(AsyncNotification),
|
|
InputEvent(InputEvent),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum SyntaxHighlightProgress {
|
|
Progress,
|
|
Done,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum AsyncAppNotification {
|
|
///
|
|
SyntaxHighlighting(SyntaxHighlightProgress),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum AsyncNotification {
|
|
///
|
|
App(AsyncAppNotification),
|
|
///
|
|
Git(AsyncGitNotification),
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let cliargs = process_cmdline()?;
|
|
|
|
let _profiler = Profiler::new();
|
|
|
|
asyncgit::register_tracing_logging();
|
|
|
|
if !valid_path()? {
|
|
eprintln!("invalid path\nplease run gitui inside of a non-bare git repository");
|
|
return Ok(());
|
|
}
|
|
|
|
let key_config = KeyConfig::init(KeyConfig::get_config_file()?)
|
|
.map_err(|e| eprintln!("KeyConfig loading error: {}", e))
|
|
.unwrap_or_default();
|
|
let theme = Theme::init(cliargs.theme)
|
|
.map_err(|e| eprintln!("Theme loading error: {}", e))
|
|
.unwrap_or_default();
|
|
|
|
setup_terminal()?;
|
|
defer! {
|
|
shutdown_terminal();
|
|
}
|
|
|
|
set_panic_handlers()?;
|
|
|
|
let mut terminal = start_terminal(io::stdout())?;
|
|
|
|
let (tx_git, rx_git) = unbounded();
|
|
let (tx_app, rx_app) = unbounded();
|
|
|
|
let input = Input::new();
|
|
|
|
let rx_input = input.receiver();
|
|
let ticker = tick(TICK_INTERVAL);
|
|
let spinner_ticker = tick(SPINNER_INTERVAL);
|
|
|
|
let mut app =
|
|
App::new(&tx_git, &tx_app, input, theme, key_config);
|
|
|
|
let mut spinner = Spinner::default();
|
|
let mut first_update = true;
|
|
|
|
loop {
|
|
let event = if first_update {
|
|
first_update = false;
|
|
QueueEvent::Tick
|
|
} else {
|
|
select_event(
|
|
&rx_input,
|
|
&rx_git,
|
|
&rx_app,
|
|
&ticker,
|
|
&spinner_ticker,
|
|
)?
|
|
};
|
|
|
|
{
|
|
if let QueueEvent::SpinnerUpdate = event {
|
|
spinner.update();
|
|
spinner.draw(&mut terminal)?;
|
|
continue;
|
|
}
|
|
|
|
scope_time!("loop");
|
|
|
|
match event {
|
|
QueueEvent::InputEvent(ev) => {
|
|
if let InputEvent::State(InputState::Polling) = ev
|
|
{
|
|
//Note: external ed closed, we need to re-hide cursor
|
|
terminal.hide_cursor()?;
|
|
}
|
|
app.event(ev)?;
|
|
}
|
|
QueueEvent::Tick => app.update()?,
|
|
QueueEvent::AsyncEvent(ev) => {
|
|
if !matches!(
|
|
ev,
|
|
AsyncNotification::Git(
|
|
AsyncGitNotification::FinishUnchanged
|
|
)
|
|
) {
|
|
app.update_async(ev)?;
|
|
}
|
|
}
|
|
QueueEvent::SpinnerUpdate => unreachable!(),
|
|
}
|
|
|
|
draw(&mut terminal, &app)?;
|
|
|
|
spinner.set_state(app.any_work_pending());
|
|
spinner.draw(&mut terminal)?;
|
|
|
|
if app.is_quit() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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() -> Result<bool> {
|
|
Ok(asyncgit::sync::is_repo(asyncgit::CWD)
|
|
&& !asyncgit::sync::is_bare_repo(asyncgit::CWD)?)
|
|
}
|
|
|
|
fn select_event(
|
|
rx_input: &Receiver<InputEvent>,
|
|
rx_git: &Receiver<AsyncGitNotification>,
|
|
rx_app: &Receiver<AsyncAppNotification>,
|
|
rx_ticker: &Receiver<Instant>,
|
|
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_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::Tick),
|
|
4 => 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)
|
|
}
|
|
|
|
fn set_panic_handlers() -> Result<()> {
|
|
// regular panic handler
|
|
panic::set_hook(Box::new(|e| {
|
|
let backtrace = Backtrace::new();
|
|
//TODO: create macro to do both in one
|
|
log::error!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
|
eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
|
shutdown_terminal();
|
|
}));
|
|
|
|
// global threadpool
|
|
rayon_core::ThreadPoolBuilder::new()
|
|
.panic_handler(|e| {
|
|
let backtrace = Backtrace::new();
|
|
//TODO: create macro to do both in one
|
|
log::error!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
|
eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
|
shutdown_terminal();
|
|
process::abort();
|
|
})
|
|
.num_threads(4)
|
|
.build_global()?;
|
|
|
|
Ok(())
|
|
}
|