Skip to main content

gitkraft_tui/
lib.rs

1//! GitKraft TUI — terminal user interface built with Ratatui.
2//!
3//! This crate provides a full-featured Git IDE experience in the terminal,
4//! powered by [`gitkraft_core`] for all Git operations and [`ratatui`] for
5//! rendering.
6//!
7//! # Entry point
8//!
9//! Call [`run`] with an optional repository path to start the TUI application.
10
11pub mod app;
12pub mod events;
13pub mod features;
14pub mod layout;
15pub mod utils;
16pub mod widgets;
17
18use std::io;
19use std::panic;
20use std::path::PathBuf;
21use std::time::Duration;
22
23use anyhow::Result;
24use crossterm::event::{self, Event};
25use crossterm::execute;
26use crossterm::terminal::{
27    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
28};
29use ratatui::backend::CrosstermBackend;
30use ratatui::Terminal;
31
32use crate::app::App;
33
34/// Run the TUI application.
35///
36/// If `repo_path` is `Some`, the repository at that path is opened immediately.
37/// Otherwise the Welcome screen is shown, letting the user choose a repository.
38pub fn run(mut repo_path: Option<PathBuf>) -> Result<()> {
39    // If no repo path was given, try loading the last-opened repo from settings.
40    if repo_path.is_none() {
41        repo_path = gitkraft_core::features::persistence::get_last_repo()
42            .ok()
43            .flatten();
44    }
45
46    // Install a panic hook that restores the terminal before printing the
47    // panic message.  Without this the user is left with a broken terminal
48    // after an unexpected panic.
49    let default_hook = panic::take_hook();
50    panic::set_hook(Box::new(move |info| {
51        let _ = restore_terminal();
52        default_hook(info);
53    }));
54
55    enable_raw_mode()?;
56    let mut stdout = io::stdout();
57    execute!(stdout, EnterAlternateScreen)?;
58    let backend = CrosstermBackend::new(stdout);
59    let mut terminal = Terminal::new(backend)?;
60
61    let result = run_app(&mut terminal, repo_path);
62
63    restore_terminal()?;
64    terminal.show_cursor()?;
65    result
66}
67
68/// The inner event loop — draw, poll for input, dispatch, repeat.
69pub fn run_app<B: ratatui::backend::Backend>(
70    terminal: &mut Terminal<B>,
71    repo_path: Option<PathBuf>,
72) -> Result<()>
73where
74    B::Error: Send + Sync + 'static,
75{
76    let mut app = App::new();
77
78    if let Some(path) = repo_path {
79        // CLI argument takes priority — open in the first tab
80        app.open_repo(path);
81    } else {
82        // Try to restore the saved session (multiple tabs)
83        if let Ok(settings) = gitkraft_core::features::persistence::load_settings() {
84            let paths: Vec<PathBuf> = settings
85                .open_tabs
86                .into_iter()
87                .filter(|p| p.exists())
88                .collect();
89            let active = settings.active_tab_index;
90            if !paths.is_empty() {
91                app.tabs.clear();
92                for _ in &paths {
93                    app.tabs.push(crate::app::RepoTab::new());
94                }
95                // Open each repo in its corresponding tab
96                for (i, path) in paths.into_iter().enumerate() {
97                    app.active_tab_index = i;
98                    app.open_repo(path);
99                }
100                // Restore the originally active tab
101                app.active_tab_index = active.min(app.tabs.len().saturating_sub(1));
102                app.screen = crate::app::AppScreen::Main;
103            }
104        }
105    }
106
107    loop {
108        app.tick_count = app.tick_count.wrapping_add(1);
109
110        // Drain any results from background tasks (open_repo, refresh,
111        // stage, commit, etc.) before drawing so the UI reflects the
112        // latest state.
113        app.poll_background();
114        app.maybe_auto_refresh();
115
116        terminal.draw(|frame| layout::render(&mut app, frame))?;
117
118        // Use a shorter poll interval (33 ms ≈ 30 fps) so background-task
119        // results are picked up promptly and the loading indicator updates
120        // without a noticeable lag.
121        if event::poll(Duration::from_millis(33))? {
122            if let Event::Key(key) = event::read()? {
123                // Ignore key-release events on platforms that send them
124                if key.kind == crossterm::event::KeyEventKind::Press {
125                    events::handle_key(&mut app, key);
126                }
127            }
128        }
129
130        if app.should_quit {
131            break;
132        }
133    }
134
135    Ok(())
136}
137
138/// Restore the terminal to its original state (disable raw mode, leave the
139/// alternate screen).
140fn restore_terminal() -> Result<()> {
141    disable_raw_mode()?;
142    execute!(io::stdout(), LeaveAlternateScreen)?;
143    Ok(())
144}