mod app;
mod arrange;
mod chart;
mod config;
mod dateinput;
mod frame;
mod glyphs;
mod grid;
mod ical;
mod layout_edit;
mod migrate;
mod note;
mod panel;
mod picker;
mod poll;
mod prompt;
mod quote;
mod samples;
mod selection;
mod state;
mod store;
mod task;
mod textarea;
mod textfield;
mod theme;
mod themes;
mod update;
mod watch;
mod widgets;
mod zones;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context, Result};
use ratatui::crossterm::event::{
DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture,
};
use ratatui::crossterm::execute;
use crate::app::App;
use crate::config::Config;
const HELP: &str = concat!(
"mirador ",
env!("CARGO_PKG_VERSION"),
" — a personal information dashboard for your terminal
USAGE:
mirador [OPTIONS]
OPTIONS:
-c, --config <PATH> Use a specific config file
--print-config Print the default config to stdout and exit
--config-path Print the resolved config path and exit
--migrate-config Update a config written by an older version
-h, --help Print this help and exit
-V, --version Print the version and exit
KEYS:
Tab / Shift+Tab Move focus between panels
1 - 9 Jump straight to a panel
w Choose which panels are shown
m Rearrange the panels
Ctrl+arrows Resize the focused panel
? Show all key bindings, and the version
q / Ctrl+C Quit
On first run mirador writes a commented config file you can edit. Run
`mirador --config-path` to find it.
"
);
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
struct Args {
config: Option<PathBuf>,
print_config: bool,
show_config_path: bool,
migrate_config: bool,
help: bool,
version: bool,
}
fn parse_args(raw: impl Iterator<Item = String>) -> Result<Args> {
let mut args = Args::default();
let mut iter = raw.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-h" | "--help" => args.help = true,
"-V" | "--version" => args.version = true,
"--print-config" => args.print_config = true,
"--config-path" => args.show_config_path = true,
"--migrate-config" => args.migrate_config = true,
"-c" | "--config" => {
let value = iter.next().ok_or_else(|| {
anyhow::anyhow!("`{arg}` needs a path, e.g. `{arg} ~/mirador.toml`")
})?;
args.config = Some(PathBuf::from(value));
}
other if other.starts_with("--config=") => {
args.config = Some(PathBuf::from(&other["--config=".len()..]));
}
other => {
anyhow::bail!("unrecognised argument `{other}`. Run `mirador --help` for usage.")
}
}
}
Ok(args)
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("mirador: {error:#}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let args = parse_args(std::env::args().skip(1))?;
if args.help {
print!("{HELP}");
return Ok(());
}
if args.version {
println!("mirador {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if args.print_config {
print!("{}", config::DEFAULT_CONFIG);
return Ok(());
}
if args.show_config_path {
let path = match args.config {
Some(p) => p,
None => Config::default_path()?,
};
println!("{}", path.display());
return Ok(());
}
if args.migrate_config {
let path = match args.config {
Some(p) => p,
None => Config::default_path()?,
};
let report = migrate::migrate_file(&path)?;
if report.is_empty() {
println!("{} is already current; nothing to do.", path.display());
} else {
println!("Updated {}:", path.display());
for change in &report.changes {
println!(" - {change}");
}
if let Some(backup) = &report.backup {
println!("\nYour original is at {}.", backup.display());
}
}
return Ok(());
}
let (mut config, config_path) = Config::load(args.config)?;
let state_path = Config::state_path().ok();
let saved = state_path
.as_deref()
.map(crate::state::UiState::load)
.unwrap_or_default();
let baseline = crate::state::UiState::from_config(&config);
config.apply_state(&saved);
let mouse = config.general.mouse;
let updates = crate::update::spawn(
config.general.check_for_updates,
Config::update_cache_path().ok(),
);
let mut app = App::new(config)?;
app.watch_for_updates(updates);
app.write_layout_to(config_path);
if let Some(path) = state_path {
app.remember_preferences_at(path, saved, baseline);
}
let mut terminal = ratatui::init();
let _ = execute!(std::io::stdout(), EnableFocusChange);
{
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = execute!(std::io::stdout(), DisableFocusChange);
previous(info);
}));
}
if mouse {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = execute!(std::io::stdout(), DisableMouseCapture);
previous(info);
}));
execute!(std::io::stdout(), EnableMouseCapture).context("enabling mouse reporting")?;
}
let result = app.run(&mut terminal);
if mouse {
let _ = execute!(std::io::stdout(), DisableMouseCapture);
}
let _ = execute!(std::io::stdout(), DisableFocusChange);
ratatui::restore();
result
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Result<Args> {
parse_args(args.iter().map(|s| (*s).to_string()))
}
#[test]
fn no_arguments_is_valid() {
let args = parse(&[]).unwrap();
assert!(args.config.is_none());
assert!(!args.help);
}
#[test]
fn flags_parse_in_both_short_and_long_form() {
assert!(parse(&["-h"]).unwrap().help);
assert!(parse(&["--help"]).unwrap().help);
assert!(parse(&["-V"]).unwrap().version);
assert!(parse(&["--version"]).unwrap().version);
assert!(parse(&["--print-config"]).unwrap().print_config);
assert!(parse(&["--config-path"]).unwrap().show_config_path);
assert!(parse(&["--migrate-config"]).unwrap().migrate_config);
}
#[test]
fn config_accepts_separate_and_joined_values() {
assert_eq!(
parse(&["-c", "/tmp/a.toml"]).unwrap().config,
Some(PathBuf::from("/tmp/a.toml"))
);
assert_eq!(
parse(&["--config", "/tmp/b.toml"]).unwrap().config,
Some(PathBuf::from("/tmp/b.toml"))
);
assert_eq!(
parse(&["--config=/tmp/c.toml"]).unwrap().config,
Some(PathBuf::from("/tmp/c.toml"))
);
}
#[test]
fn a_config_flag_without_a_value_explains_itself() {
let err = parse(&["--config"]).expect_err("must fail");
assert!(err.to_string().contains("needs a path"), "got: {err}");
}
#[test]
fn unknown_arguments_point_at_help() {
let err = parse(&["--wat"]).expect_err("must fail");
assert!(err.to_string().contains("--help"), "got: {err}");
}
#[test]
fn the_default_config_is_valid_toml_and_parses_into_config() {
let parsed: Config =
toml::from_str(config::DEFAULT_CONFIG).expect("bundled config must parse");
assert!(!parsed.layout.rows.is_empty());
}
#[test]
fn help_text_documents_every_flag_the_parser_accepts() {
for flag in [
"--config",
"--print-config",
"--config-path",
"--migrate-config",
"--help",
"--version",
] {
assert!(HELP.contains(flag), "{flag} is undocumented");
}
}
}