mod align;
mod app;
mod diff;
mod diff_engine;
mod edits;
mod git;
mod log_tab;
mod stashes_tab;
mod status_tab;
use crate::status_tab::StatusFocus;
mod theme;
use crate::stashes_tab::StashDepth;
use crate::app::App;
use crate::theme::Theme;
use anyhow::Result;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{io, path::Path};
use terminal_colorsaurus::{color_scheme, QueryOptions};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let mut repo_path = std::env::current_dir()?;
let mut theme_path = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--theme" | "-t" => {
i += 1;
theme_path = args.get(i).map(|s| Path::new(s).to_path_buf());
}
_ => {
if !args[i].starts_with('-') {
repo_path = Path::new(&args[i]).to_path_buf();
}
}
}
i += 1;
}
let theme = if let Some(path) = &theme_path {
Theme::from_path(path).unwrap_or_else(|e| {
eprintln!("Warning: failed to load theme from {:?}: {e}", path);
eprintln!("Using default theme");
if is_dark_mode() { Theme::dark() } else { Theme::light() }
})
} else {
if is_dark_mode() { Theme::dark() } else { Theme::light() }
};
diff_engine::init_bat_assets();
let app = App::new(&repo_path, theme)?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let res = run_app(&mut terminal, app);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(e) = res {
eprintln!("Error: {e}");
}
Ok(())
}
fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, mut app: App) -> Result<()> {
loop {
terminal.draw(|f| app.render(f))?;
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Char('h') => {
app.toggle_help();
}
KeyCode::Char('d') => {
match app.current_tab {
app::Tab::Status => {
if app.status_tab.focus == StatusFocus::Diff {
app.status_tab.focus = StatusFocus::Unstaged;
app.diff_fullscreen = false;
} else if app.status_tab.current_file().is_some() {
app.status_tab.focus = StatusFocus::Diff;
app.diff_fullscreen = true;
}
}
app::Tab::Log => {
if app.log_tab.depth >= log_tab::LogDepth::FilesDiff {
app.log_tab_back();
} else {
app.log_tab_enter();
}
}
app::Tab::Stashes => {
if app.stashes_tab.depth >= StashDepth::FilesDiff {
app.stash_tab_back();
} else {
app.stash_tab_enter();
}
}
}
}
KeyCode::Char('f') => {
if app.current_tab == app::Tab::Status && app.status_tab.focus == StatusFocus::Diff {
app.diff_fullscreen = !app.diff_fullscreen;
} else {
app.toggle_diff_fullscreen();
}
}
KeyCode::Char('m') => {
app.toggle_diff_mode();
}
KeyCode::Char('1') => {
app.current_tab = app::Tab::Status;
app.show_diff = false;
app.diff_fullscreen = false;
app.refresh_current_tab();
}
KeyCode::Char('2') => {
app.current_tab = app::Tab::Log;
app.show_diff = false;
app.diff_fullscreen = false;
app.refresh_current_tab();
}
KeyCode::Char('3') => {
app.current_tab = app::Tab::Stashes;
app.show_diff = false;
app.diff_fullscreen = false;
app.refresh_current_tab();
}
KeyCode::Up | KeyCode::Char('k') => {
app.move_up();
}
KeyCode::Down | KeyCode::Char('j') => {
app.move_down();
}
KeyCode::PageUp => {
if app.is_any_diff_active() {
app.diff_view.page_up();
}
}
KeyCode::PageDown => {
if app.is_any_diff_active() {
app.diff_view.page_down();
}
}
KeyCode::Home => {
if app.is_any_diff_active() {
app.diff_view.go_to_top();
}
}
KeyCode::End => {
if app.is_any_diff_active() {
app.diff_view.go_to_end();
}
}
KeyCode::Tab => {
app.next_tab();
}
KeyCode::BackTab => {
app.prev_tab();
}
KeyCode::Right => {
if app.current_tab == app::Tab::Status {
app.status_tab.focus_right();
if app.status_tab.focus == StatusFocus::Diff {
app.diff_fullscreen = true;
}
} else if app.current_tab == app::Tab::Log {
app.log_tab_enter();
} else if app.current_tab == app::Tab::Stashes {
app.stash_tab_enter();
}
}
KeyCode::Left => {
if app.current_tab == app::Tab::Status {
if app.status_tab.focus == StatusFocus::Diff {
app.status_tab.focus = StatusFocus::Unstaged;
app.diff_fullscreen = false;
} else {
app.status_tab.focus_left();
}
}
}
KeyCode::Esc => {
if app.show_help {
app.toggle_help();
} else if app.current_tab == app::Tab::Status
&& app.status_tab.focus == StatusFocus::Diff
{
app.status_tab.focus = StatusFocus::Unstaged;
app.diff_fullscreen = false;
} else if app.current_tab == app::Tab::Log
&& app.log_tab.depth != log_tab::LogDepth::Commits
{
app.log_tab_back();
} else if app.show_diff {
app.toggle_diff();
} else if app.current_tab == app::Tab::Stashes
&& app.stashes_tab.depth != StashDepth::List
{
app.stash_tab_back();
}
}
KeyCode::Enter => {
match app.current_tab {
app::Tab::Status => {
if app.status_tab.focus == StatusFocus::Diff {
app.status_tab.focus = StatusFocus::Unstaged;
app.diff_fullscreen = false;
} else if app.status_tab.current_file().is_some() {
app.status_tab.focus = StatusFocus::Diff;
app.diff_fullscreen = true;
}
}
app::Tab::Log => {
app.log_tab_enter();
}
app::Tab::Stashes => {
app.stash_tab_enter();
}
}
}
_ => {
if app.show_help {
app.toggle_help();
}
}
}
}
}
}
fn is_dark_mode() -> bool {
color_scheme(QueryOptions::default())
.map(|cs| matches!(cs, terminal_colorsaurus::ColorScheme::Dark))
.unwrap_or(true)
}