mod app; mod async_result; mod cache; mod commands; mod components; mod config; mod core; mod effects; mod errors; mod git; mod input; mod jobs; mod middleware; mod palette; mod services; mod ui;
use anyhow::Result;
use tracing_subscriber::EnvFilter;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use crossterm::terminal::{enable_raw_mode, disable_raw_mode};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::event::{EnableMouseCapture, DisableMouseCapture};
use std::sync::Arc;
use tokio::sync::{watch, mpsc};
use notify::{Watcher, RecursiveMode, RecommendedWatcher};
fn init_tracing() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.without_time()
.with_writer(std::io::stderr)
.init();
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let (args, cli_path) = parse_cli_args();
if handle_info_flags(&args)? {
return Ok(());
}
init_tracing();
tracing::info!("Starting eazygit");
if let Err(e) = config::create_default_config() {
tracing::warn!("config create failed: {e}");
}
let settings = config::load().unwrap_or_default();
tracing::info!("Loaded theme: {}", settings.ui.theme);
let themes = config::list_themes();
let git_service = Arc::new(services::GitService::new());
let state = initialize_state(&settings, cli_path, themes, &git_service);
let (async_tx, async_rx) = mpsc::unbounded_channel::<crate::async_result::AsyncResult>();
let (cancel_tx, cancel_rx) = watch::channel(false);
spawn_cancellation_handler(cancel_tx);
let watcher = setup_file_watcher(state.repo_path.clone(), async_tx.clone());
let component_manager = components::ComponentManager::new(git_service.clone());
let mut terminal = setup_terminal()?;
let mut application = app::Application::new(
state,
component_manager,
git_service,
cancel_rx,
async_tx,
async_rx,
watcher,
)?;
if let Err(e) = application.run(&mut terminal).await {
tracing::error!(error = %e, "Application error");
}
restore_terminal()?;
tracing::info!("Exiting eazygit");
Ok(())
}
fn parse_cli_args() -> (Vec<String>, Option<String>) {
let args: Vec<String> = std::env::args().collect();
let mut cli_path = None;
for arg in args.iter().skip(1) {
if !arg.starts_with('-') {
cli_path = Some(arg.clone());
break;
}
}
(args, cli_path)
}
fn handle_info_flags(args: &[String]) -> Result<bool> {
if args.iter().any(|arg| arg == "--version" || arg == "-v") {
println!("eazygit {}", env!("CARGO_PKG_VERSION"));
return Ok(true);
}
if args.iter().any(|arg| arg == "--list-themes") {
init_tracing();
let themes = config::list_themes();
println!("Discovered {} themes:", themes.len());
for (i, theme) in themes.iter().enumerate() {
println!(" {}. {}", i + 1, theme);
}
return Ok(true);
}
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
print_help();
return Ok(true);
}
Ok(false)
}
fn print_help() {
println!("eazygit - A fast, easy-to-use Git TUI");
println!();
println!("USAGE:");
println!(" eazygit [OPTIONS]");
println!();
println!("OPTIONS:");
println!(" -v, --version Print version information");
println!(" -h, --help Print this help message");
println!(" --list-themes List all discovered themes (for debugging)");
println!();
println!("Run eazygit in a Git repository to start the TUI.");
}
fn initialize_state(
settings: &config::Settings,
cli_path: Option<String>,
themes: Vec<String>,
_git_service: &services::GitService ) -> app::AppState {
let mut state = app::AppState::new();
state.repo_path = ".".to_string();
if let Some(repo) = cli_path {
state.repo_path = repo;
} else if let Ok(cwd) = std::env::current_dir() {
state.repo_path = cwd.to_string_lossy().to_string();
}
if let Ok(Some(recovery_info)) = app::rebase::operations::RebaseRecovery::detect_interrupted_rebase(&state.repo_path) {
state.rebase_recovery_open = true;
state.rebase_session.phase = if recovery_info.has_conflicts {
app::rebase::RebasePhase::Conflict
} else {
app::rebase::RebasePhase::Active
};
}
state.theme = settings.theme.clone();
state.available_themes = themes;
state.pull_timeout_secs = settings.core.pull_timeout_secs;
state.rebase_timeout_secs = settings.core.rebase_timeout_secs;
state.font_family = settings.ui.font_family.clone();
state.font_size = settings.ui.font_size;
state.commit_template = settings.commit_template.clone();
state.custom_commands = settings.custom_commands.clone();
state.background_image_path = settings.ui.background_image_path.clone();
state.background_opacity = settings.ui.background_opacity;
if settings.core.auto_fetch {
state.auto_fetch_interval_secs = settings.core.auto_fetch_interval;
state.auto_fetch_remote = settings.core.auto_fetch_remote.clone();
}
state.push_ff_only_enforce = settings.core.push_ff_only_enforce;
state.pull_ff_only = settings.core.pull_ff_only;
state.pull_autostash = settings.core.pull_autostash;
if let Some(font) = &state.font_family {
ui::font::set_font(font);
}
let git_cli = git::cli::GitCli::new();
state = app::reducer(state, app::Action::SetHasDelta(git_cli.has_delta()));
state
}
fn spawn_cancellation_handler(tx: watch::Sender<bool>) {
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
let _ = tx.send(true);
}
});
}
fn setup_file_watcher(watch_path: String, tx: mpsc::UnboundedSender<crate::async_result::AsyncResult>) -> Option<RecommendedWatcher> {
let debounce = Arc::new(std::sync::Mutex::new(std::time::Instant::now()));
let watcher_res = notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
let dominated_by_write = event.kind.is_modify() || event.kind.is_create() || event.kind.is_remove();
if dominated_by_write {
match debounce.lock() {
Ok(mut last) => {
if last.elapsed().as_millis() > 200 {
*last = std::time::Instant::now();
let _ = tx.send(crate::async_result::AsyncResult::FileChanged);
}
}
Err(_) => {
tracing::warn!("File watcher debounce mutex poisoned, sending event anyway");
let _ = tx.send(crate::async_result::AsyncResult::FileChanged);
}
}
}
}
});
match watcher_res {
Ok(mut w) => {
if w.watch(std::path::Path::new(&watch_path), RecursiveMode::Recursive).is_ok() {
tracing::info!("File watcher enabled for {}", watch_path);
Some(w)
} else {
tracing::warn!("File watcher failed to watch path");
None
}
}
Err(e) => {
tracing::warn!("File watcher not available: {}", e);
None
}
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<std::io::Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
Terminal::new(backend).map_err(Into::into)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
let mut out = io::stdout();
execute!(out, DisableMouseCapture, LeaveAlternateScreen)?;
Ok(())
}