mod api;
mod app;
mod config;
mod github;
mod model;
mod notify;
mod ui;
use app::App;
use clap::{CommandFactory, Parser, Subcommand};
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind};
use std::path::PathBuf;
use std::time::Duration;
const LONG_ABOUT: &str = "A fast, keyboard-driven terminal UI for GoCD pipelines. Browse \
pipeline groups, trigger and monitor runs, drill into stages and jobs, tail console logs, and \
open builds in the browser without leaving the terminal. Connection details come from \
config.toml, the in-app setup prompt, or GOCD_* environment variables.";
#[derive(Parser)]
#[command(name = "lazygocd", version, about, long_about = LONG_ABOUT)]
struct Cli {
#[arg(long, value_name = "PATH")]
config_dir: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
#[command(hide = true)]
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
#[command(hide = true)]
Man,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Command::Completions { shell }) => {
clap_complete::generate(
shell,
&mut Cli::command(),
"lazygocd",
&mut std::io::stdout(),
);
return Ok(());
}
Some(Command::Man) => {
clap_mangen::Man::new(Cli::command()).render(&mut std::io::stdout())?;
return Ok(());
}
None => {}
}
if let Some(dir) = cli.config_dir {
config::set_config_dir_override(dir);
}
let cfg = match config::load() {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
};
let mut app = App::new(&cfg)?;
let mut terminal = ratatui::init();
let _ = crossterm::execute!(std::io::stdout(), EnableMouseCapture);
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = crossterm::execute!(std::io::stdout(), DisableMouseCapture);
prev_hook(info);
}));
let result = run(&mut terminal, &mut app);
let _ = crossterm::execute!(std::io::stdout(), DisableMouseCapture);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal, app: &mut App) -> anyhow::Result<()> {
let mut dirty = true;
loop {
let animating = app.needs_animation();
if dirty || animating {
terminal.draw(|f| ui::draw(f, app))?;
app.tick = app.tick.wrapping_add(1);
dirty = false;
}
let timeout = if animating || app.hover_target.is_some() {
Duration::from_millis(40)
} else {
Duration::from_millis(250)
};
if event::poll(timeout)? {
loop {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
app.handle_key(key);
dirty = true;
}
Event::Mouse(m) => dirty |= app.handle_mouse(m),
Event::Resize(_, _) => dirty = true,
_ => {}
}
if app.should_quit || !event::poll(Duration::ZERO)? {
break;
}
}
}
while let Ok(ev) = app.rx.try_recv() {
app.handle_api_event(ev);
dirty = true;
}
app.maybe_prefetch();
app.maybe_poll();
app.maybe_poll_console();
if app.should_quit {
break;
}
}
Ok(())
}