mod app;
mod error;
mod handler;
mod input;
mod model;
mod output;
mod persistence;
mod syntax;
mod theme;
mod ui;
mod vcs;
use std::io;
use std::time::{Duration, Instant};
use crossterm::{
event::{
self, Event, KeyEventKind, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
},
execute,
terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
supports_keyboard_enhancement,
},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use app::{App, FocusedPanel, InputMode};
use handler::{
handle_command_action, handle_comment_action, handle_commit_select_action,
handle_confirm_action, handle_diff_action, handle_file_list_action, handle_help_action,
handle_search_action,
};
use input::{Action, map_key_to_action};
use theme::{parse_theme_arg, resolve_theme};
const CTRL_C_EXIT_TIMEOUT: Duration = Duration::from_secs(2);
fn main() -> anyhow::Result<()> {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
original_hook(panic_info);
}));
let keyboard_enhancement_supported = matches!(supports_keyboard_enhancement(), Ok(true));
let theme_arg = parse_theme_arg();
let theme = resolve_theme(theme_arg);
let mut app = match App::new(theme) {
Ok(mut app) => {
app.supports_keyboard_enhancement = keyboard_enhancement_supported;
app
}
Err(e) => {
eprintln!("Error: {}", e);
#[cfg(all(feature = "hg", feature = "jj"))]
eprintln!(
"\nMake sure you're in a git, jujutsu, or mercurial repository with uncommitted changes."
);
#[cfg(all(feature = "hg", not(feature = "jj")))]
eprintln!(
"\nMake sure you're in a git or mercurial repository with uncommitted changes."
);
#[cfg(all(feature = "jj", not(feature = "hg")))]
eprintln!(
"\nMake sure you're in a git or jujutsu repository with uncommitted changes."
);
#[cfg(not(any(feature = "hg", feature = "jj")))]
eprintln!("\nMake sure you're in a git repository with uncommitted changes.");
std::process::exit(1);
}
};
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
if keyboard_enhancement_supported {
let _ = execute!(
stdout,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
);
}
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut pending_z = false;
let mut pending_d = false;
let mut pending_semicolon = false;
let mut pending_ctrl_c: Option<Instant> = None;
loop {
terminal.draw(|frame| {
ui::render(frame, &mut app);
})?;
if let Some(first_press) = pending_ctrl_c
&& first_press.elapsed() >= CTRL_C_EXIT_TIMEOUT
{
pending_ctrl_c = None;
app.message = None;
}
if event::poll(Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
{
if key.code == crossterm::event::KeyCode::Char('c')
&& key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL)
{
if app.input_mode == InputMode::Comment {
app.exit_comment_mode();
}
if let Some(first_press) = pending_ctrl_c
&& first_press.elapsed() < CTRL_C_EXIT_TIMEOUT
{
app.should_quit = true;
continue;
}
pending_ctrl_c = Some(Instant::now());
app.set_message("Press Ctrl+C again to exit");
continue;
}
if pending_ctrl_c.is_some() {
pending_ctrl_c = None;
app.message = None;
}
if pending_z {
pending_z = false;
if key.code == crossterm::event::KeyCode::Char('z') {
app.center_cursor();
continue;
}
}
if pending_d {
pending_d = false;
if key.code == crossterm::event::KeyCode::Char('d') {
if !app.delete_comment_at_cursor() {
app.set_message("No comment at cursor");
}
continue;
}
}
if pending_semicolon {
pending_semicolon = false;
match key.code {
crossterm::event::KeyCode::Char('e') => {
app.toggle_file_list();
continue;
}
crossterm::event::KeyCode::Char('h') => {
app.focused_panel = app::FocusedPanel::FileList;
continue;
}
crossterm::event::KeyCode::Char('l') => {
app.focused_panel = app::FocusedPanel::Diff;
continue;
}
_ => {}
}
}
let action = map_key_to_action(key, app.input_mode);
match action {
Action::PendingZCommand => {
pending_z = true;
continue;
}
Action::PendingDCommand => {
pending_d = true;
continue;
}
Action::PendingSemicolonCommand => {
pending_semicolon = true;
continue;
}
_ => {}
}
match app.input_mode {
InputMode::Help => handle_help_action(&mut app, action),
InputMode::Command => handle_command_action(&mut app, action),
InputMode::Search => handle_search_action(&mut app, action),
InputMode::Comment => handle_comment_action(&mut app, action),
InputMode::Confirm => handle_confirm_action(&mut app, action),
InputMode::CommitSelect => handle_commit_select_action(&mut app, action),
InputMode::Normal => match app.focused_panel {
FocusedPanel::FileList => handle_file_list_action(&mut app, action),
FocusedPanel::Diff => handle_diff_action(&mut app, action),
},
}
}
if app.should_quit {
break;
}
}
let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}