use clap::Parser;
use ratatui::crossterm::{
ExecutableCommand,
cursor::SetCursorStyle,
event::{self, Event},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use rootle::app::App;
use rootle::cli::Cli;
use rootle::config::Config;
use rootle::theme::Theme;
use std::io::{self, stdout};
use std::time::Duration;
fn main() -> io::Result<()> {
let cli = Cli::parse();
ratatui::crossterm::style::Colored::set_ansi_color_disabled(false);
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = disable_raw_mode();
let _ = stdout().execute(SetCursorStyle::DefaultUserShape);
let _ = stdout().execute(LeaveAlternateScreen);
default_hook(info);
}));
let result = run(&mut terminal, cli);
disable_raw_mode()?;
stdout().execute(SetCursorStyle::DefaultUserShape)?;
stdout().execute(LeaveAlternateScreen)?;
result
}
fn terminated_flag() -> &'static std::sync::Arc<std::sync::atomic::AtomicBool> {
static FLAG: std::sync::OnceLock<std::sync::Arc<std::sync::atomic::AtomicBool>> =
std::sync::OnceLock::new();
FLAG.get_or_init(|| std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)))
}
fn run(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, cli: Cli) -> io::Result<()> {
{
use signal_hook::consts::{SIGINT, SIGTERM};
let _ = signal_hook::flag::register(SIGTERM, terminated_flag().clone());
let _ = signal_hook::flag::register(SIGINT, terminated_flag().clone());
}
let config = match &cli.config {
Some(path) => Config::load_from(path),
None => Config::load(),
};
let theme = Theme::load(cli.theme.as_deref().unwrap_or(&config.theme.name));
let (tx, rx) = rootle::event::channel();
let mut app = App::new(tx, config, theme);
if let Some((owner, name)) = cli.repo_parts() {
app.handle_action(rootle::action::Action::RepoSelected { owner, name });
}
let mut last_cursor_style: Option<SetCursorStyle> = None;
loop {
while let Ok(event) = rx.try_recv() {
app.handle_app_event(event);
}
if app.force_redraw {
app.force_redraw = false;
terminal.clear()?;
}
terminal.draw(|frame| {
let area = frame.area();
app.render(frame, area);
})?;
let style = app.cursor_style();
let changed = !matches!(
(style, last_cursor_style),
(None, None)
| (
Some(SetCursorStyle::SteadyBar),
Some(SetCursorStyle::SteadyBar)
)
| (
Some(SetCursorStyle::SteadyBlock),
Some(SetCursorStyle::SteadyBlock)
)
| (
Some(SetCursorStyle::DefaultUserShape),
Some(SetCursorStyle::DefaultUserShape)
)
);
if changed {
if let Some(style) = style {
stdout().execute(style)?;
}
last_cursor_style = style;
}
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
app.handle_key(key);
}
}
if let Some(text) = app.take_clipboard() {
rootle::clipboard::copy(&text);
}
if let Some(job) = app.take_editor_job() {
run_editor(terminal, job)?;
last_cursor_style = None; }
if app.should_quit || terminated_flag().load(std::sync::atomic::Ordering::Relaxed) {
return Ok(());
}
}
}
fn run_editor(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
job: rootle::editor::EditorJob,
) -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
stdout().execute(SetCursorStyle::DefaultUserShape)?;
let status = std::process::Command::new(&job.program)
.args(&job.args)
.status();
rootle::app::trace(&format!(
"editor {} exited: {}",
job.program,
status
.map(|s| s.to_string())
.unwrap_or_else(|e| e.to_string())
));
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
terminal.clear()?;
while event::poll(Duration::from_millis(0))? {
let _ = event::read()?;
}
Ok(())
}