chezmoi-tui 0.2.0

A visual TUI wrapper around chezmoi
mod actions;
mod app;
mod backend;
mod cli;
mod config;
mod diagnostics;
mod domain;
mod handlers;
mod ignore;
mod infra;
mod preview;
mod terminal;
mod ui;
mod ui_diff;

use crate::actions::{run_foreground_action, send_task};
use crate::app::{App, BackendEvent, BackendTask};
use crate::backend::worker_loop;
use crate::cli::CliArgs;
use crate::config::AppConfig;
use crate::handlers::{handle_backend_event, handle_key_event};
use crate::infra::{ChezmoiClient, ShellChezmoiClient};
use crate::terminal::TerminalGuard;
use anyhow::{Context, Result};
use clap::Parser;
use crossterm::event::{self, Event, KeyEventKind};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use std::io;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> Result<()> {
    let config = AppConfig::from_cli(CliArgs::parse())?;
    diagnostics::init(config.log_file.as_deref())?;
    tracing::info!(?config, "starting chezmoi-tui");
    let mut terminal_guard = TerminalGuard::enter()?;
    let mut terminal =
        Terminal::new(CrosstermBackend::new(io::stdout())).context("failed to create terminal")?;

    let run_result = run_app(&mut terminal, config);

    terminal_guard.restore(&mut terminal)?;
    if let Err(err) = run_result {
        eprintln!("{err:#}");
        std::process::exit(1);
    }

    Ok(())
}

fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, config: AppConfig) -> Result<()> {
    tracing::info!(?config, "initializing app");
    let mut app = App::new(config);
    let client: Arc<dyn ChezmoiClient> = Arc::new(ShellChezmoiClient::new(
        "chezmoi",
        app.home_dir.clone(),
        app.config.working_dir.clone(),
        app.config.source_dir.clone(),
    ));

    let (task_tx, task_rx) = mpsc::unbounded_channel::<BackendTask>();
    let (event_tx, mut event_rx) = mpsc::unbounded_channel::<BackendEvent>();

    tokio::spawn(worker_loop(client, task_rx, event_tx));

    send_task(&mut app, &task_tx, BackendTask::RefreshAll)?;

    while !app.should_quit {
        while let Ok(event) = event_rx.try_recv() {
            handle_backend_event(&mut app, &task_tx, event)?;
        }

        if let Some(request) = app.pending_foreground.take() {
            run_foreground_action(terminal, &mut app, &task_tx, &request)?;
        }

        app.flush_staged_filter(Instant::now());
        terminal.draw(|frame| ui::draw(frame, &mut app))?;

        if event::poll(Duration::from_millis(100)).context("event poll failed")?
            && let Event::Key(key) = event::read().context("event read failed")?
            && key.kind == KeyEventKind::Press
        {
            handle_key_event(&mut app, key, &task_tx)?;
        }
    }

    Ok(())
}