pub mod app;
pub mod events;
pub mod features;
pub mod layout;
pub mod utils;
pub mod widgets;
use std::io;
use std::panic;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::Result;
use crossterm::event::{self, Event};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use crate::app::App;
pub fn run(mut repo_path: Option<PathBuf>) -> Result<()> {
if repo_path.is_none() {
repo_path = gitkraft_core::features::persistence::get_last_tui_repo()
.ok()
.flatten();
}
let default_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
default_hook(info);
}));
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let _ = execute!(
stdout,
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
)
);
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_app(&mut terminal, repo_path);
let _ = execute!(io::stdout(), crossterm::event::PopKeyboardEnhancementFlags);
restore_terminal()?;
terminal.show_cursor()?;
result
}
pub fn run_app<B: ratatui::backend::Backend>(
terminal: &mut Terminal<B>,
repo_path: Option<PathBuf>,
) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
let mut app = App::new();
if let Some(path) = repo_path {
app.open_repo(path);
} else {
if let Ok(settings) = gitkraft_core::features::persistence::load_tui_settings() {
let paths: Vec<PathBuf> = settings
.open_tabs
.into_iter()
.filter(|p| p.exists())
.collect();
let active = settings.active_tab_index;
if !paths.is_empty() {
app.tabs.clear();
for _ in &paths {
app.tabs.push(crate::app::RepoTab::new());
}
for (i, path) in paths.into_iter().enumerate() {
app.active_tab_index = i;
app.open_repo(path);
}
app.active_tab_index = active.min(app.tabs.len().saturating_sub(1));
app.screen = crate::app::AppScreen::Main;
}
}
}
let mut git_watcher_thread: Option<std::thread::JoinHandle<()>> = None;
loop {
app.tick_count = app.tick_count.wrapping_add(1);
app.poll_background();
if git_watcher_thread
.as_ref()
.map(|t| t.is_finished())
.unwrap_or(true)
{
if let Some(ref path) = app.tab().repo_path.clone() {
let git_dir = path.join(".git");
let tx = app.bg_tx.clone();
git_watcher_thread = Some(gitkraft_core::spawn_git_watcher(git_dir, move || {
tx.send(crate::app::BackgroundResult::GitStateChanged)
.is_ok()
}));
}
}
terminal.draw(|frame| layout::render(&mut app, frame))?;
if event::poll(Duration::from_millis(33))? {
if let Event::Key(key) = event::read()? {
if key.kind == crossterm::event::KeyEventKind::Press {
events::handle_key(&mut app, key);
}
}
}
if let Some(paths) = app.pending_editor_open.take() {
disable_raw_mode()?;
execute!(io::stdout(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
let candidates = app.editor.binary_candidates();
tracing::debug!(
"[gitkraft] opening {:?} with {} (candidates: {})",
paths,
app.editor,
candidates.join(", ")
);
let mut opened = false;
let mut error_msg: Option<String> = None;
for bin in &candidates {
let parts: Vec<&str> = bin.split_whitespace().collect();
if let Some((cmd, args)) = parts.split_first() {
tracing::debug!("[gitkraft] trying binary: {cmd}");
match std::process::Command::new(cmd)
.args(args.iter())
.args(paths.iter()) .stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
{
Ok(status) => {
tracing::debug!("[gitkraft] editor exited with {status}");
opened = true;
break;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::debug!("[gitkraft] binary '{cmd}' not found, trying next");
continue;
}
Err(e) => {
let msg = format!("Editor '{cmd}' failed to launch: {e}");
tracing::warn!("[gitkraft] {msg}");
error_msg = Some(msg);
break;
}
}
}
}
if !opened {
let msg = error_msg.unwrap_or_else(|| {
format!(
"Could not find {} in PATH — tried: {} \
(check that the binary is installed and in your $PATH)",
app.editor,
candidates.join(", ")
)
});
tracing::warn!("[gitkraft] {msg}");
app.tab_mut().error_message = Some(msg);
} else {
let count = paths.len();
app.tab_mut().status_message = Some(format!(
"Returned from {} — {} file(s) edited",
app.editor, count
));
}
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
terminal.clear()?;
}
if app.should_quit {
break;
}
}
Ok(())
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}