mod app;
mod config;
mod export;
mod grafana;
mod prom;
mod theme;
mod ui;
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use anyhow::{Context, Result};
use clap::Parser;
use config::Config;
use crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode},
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use theme::Theme;
mod cli;
use cli::Args;
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if let Some(cmd) = args.command {
match cmd {
cli::Commands::Completions { shell } => {
use clap::CommandFactory;
clap_complete::generate(
shell,
&mut Args::command(),
"grafatui",
&mut std::io::stdout(),
);
}
cli::Commands::Man => {
use clap::CommandFactory;
let man = clap_mangen::Man::new(Args::command());
man.render(&mut std::io::stdout())?;
}
}
return Ok(());
}
let config = match args.config.clone() {
Some(path) => Config::load(Some(path))?,
None => Config::load(None).unwrap_or_default(),
};
let prometheus_url = args
.prometheus_url
.or(config.prometheus_url)
.unwrap_or_else(|| "http://localhost:9090".to_string());
let range_str = args
.range
.or(config.time_range)
.unwrap_or_else(|| "5m".to_string());
let range = app::parse_duration(&range_str).context("--range")?;
let step_str = args
.step
.or(config.step)
.unwrap_or_else(|| "5s".to_string());
let step = app::parse_duration(&step_str).context("--step")?;
let export_dir = args
.export_dir
.or(config.export_dir)
.map(|p| config::expand_path(&p))
.unwrap_or_else(|| std::path::PathBuf::from("./grafatui-exports"));
let export_format = args
.export_format
.or(config.export_format)
.unwrap_or_default();
let record_max_frames = args
.record_max_frames
.or(config.record_max_frames)
.unwrap_or(300);
let autogrid_enabled = config.autogrid.unwrap_or(true);
let autogrid_color = args
.autogrid_color
.or(config.autogrid_color)
.map(|color| theme::parse_grafana_color(&color))
.filter(|color| *color != ratatui::style::Color::Reset)
.unwrap_or(ratatui::style::Color::DarkGray);
let mut vars: HashMap<String, String> = HashMap::new();
let mut query_vars = Vec::new();
let mut dashboard_refresh_rate_ms = None;
let prom = prom::PromClient::new(prometheus_url);
let (title, panels, skipped_panels) = if let Some(path) = args
.grafana_json
.or(config.grafana_json)
.map(|p| config::expand_path(&p))
{
let d = grafana::load_grafana_dashboard(&path)?;
dashboard_refresh_rate_ms = d.refresh_rate_ms;
for (k, v) in d.vars {
vars.insert(k, v);
}
query_vars = d.query_vars;
let ps = d
.queries
.into_iter()
.map(|q| app::PanelState {
title: q.title,
exprs: q.exprs,
legends: q.legends,
query_modes: q.query_modes,
series: vec![],
last_error: None,
last_url: None,
last_samples: 0,
grid: q.grid.map(|g| app::GridUnit {
x: g.x,
y: g.y,
w: g.w,
h: g.h,
}),
y_axis_mode: app::YAxisMode::Auto,
panel_type: q.panel_type,
thresholds: q.thresholds,
min: q.min,
max: q.max,
autogrid: q.autogrid,
display: q.display,
})
.collect();
(format!("{} (imported)", d.title), ps, d.skipped_panels)
} else {
("grafatui".to_string(), app::default_queries(args.query), 0)
};
let mut pinned_vars = HashSet::new();
if let Some(config_vars) = config.vars {
for (k, v) in config_vars {
pinned_vars.insert(k.clone());
vars.insert(k, v);
}
}
for (k, v) in &args.var {
pinned_vars.insert(k.clone());
vars.insert(k.clone(), v.clone());
}
query_vars.retain(|var| !pinned_vars.contains(&var.name));
let theme_name = args
.theme
.or(config.theme)
.unwrap_or_else(|| "default".to_string());
let theme = Theme::from_str(&theme_name);
let marker_name = args
.threshold_marker
.or(config.threshold_marker)
.unwrap_or_else(|| "dashed-line".to_string());
let refresh_rate = resolve_refresh_rate_ms(
args.refresh_rate,
config.refresh_rate,
dashboard_refresh_rate_ms,
);
let refresh_every = Duration::from_millis(refresh_rate);
let mut state = app::AppState::new(
prom,
range,
step,
refresh_every,
title,
panels,
skipped_panels,
theme,
marker_name,
export::ExportOptions {
dir: export_dir,
format: export_format,
record_max_frames,
}
.validate()?,
);
state.autogrid_enabled = autogrid_enabled;
state.autogrid_color = autogrid_color;
state.vars = vars; state.query_vars = query_vars;
state.refresh().await?;
crossterm::terminal::enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(
stdout,
EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = app::run_app(
&mut terminal,
&mut state,
Duration::from_millis(args.tick_rate),
)
.await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
crossterm::event::DisableMouseCapture
)?;
terminal.show_cursor()?;
res
}
fn resolve_refresh_rate_ms(
cli_refresh_rate: Option<u64>,
config_refresh_rate: Option<u64>,
dashboard_refresh_rate: Option<u64>,
) -> u64 {
cli_refresh_rate
.or(config_refresh_rate)
.or(dashboard_refresh_rate)
.unwrap_or(1000)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_refresh_rate_precedence() {
assert_eq!(
resolve_refresh_rate_ms(Some(2000), Some(3000), Some(4000)),
2000
);
assert_eq!(resolve_refresh_rate_ms(None, Some(3000), Some(4000)), 3000);
assert_eq!(resolve_refresh_rate_ms(None, None, Some(4000)), 4000);
assert_eq!(resolve_refresh_rate_ms(None, None, None), 1000);
}
}