Lines
74.32 %
Functions
55 %
Branches
100 %
use clap::Parser;
use cli_core::start_server;
use exitfailure::ExitFailure;
use log::LevelFilter;
use sqlx::types::Uuid;
use tui::{App, EditMode, LocalTransport, Transport, chart, run_loop};
#[derive(Parser, Debug)]
#[command(name = "nomisync-tui", about = "Nomisync interactive TUI")]
struct Cli {
#[arg(short = 'u', long)]
userid: Uuid,
#[arg(short = 'd', long)]
database: Option<String>,
#[arg(long, default_value = "warn")]
loglevel: LevelFilter,
#[arg(long, value_parser = parse_edit_mode, default_value = "emacs")]
edit_mode: EditMode,
}
fn parse_edit_mode(s: &str) -> Result<EditMode, String> {
match s.to_ascii_lowercase().as_str() {
"emacs" => Ok(EditMode::Emacs),
"vim" => Ok(EditMode::Vim),
_ => Err(format!("edit-mode must be `emacs` or `vim`, got `{s}`")),
/// Combine the event loop's result with the transport cleanup's result
/// into a single outcome. The loop error (user-facing) wins over the
/// cleanup error (mostly cosmetic) when both fire; the cleanup error
/// is still logged via `on_cleanup_error` so we leave a trace.
///
/// Pulled out of `main` so it can be unit-tested — `#[tokio::main]` is
/// otherwise opaque to the test harness.
fn combine_outcomes<F>(
loop_result: std::io::Result<()>,
finish_result: std::io::Result<()>,
on_cleanup_error: F,
) -> std::io::Result<()>
where
F: FnOnce(&std::io::Error),
{
match (loop_result, finish_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(loop_err), Ok(())) => Err(loop_err),
(Ok(()), Err(finish_err)) => Err(finish_err),
(Err(loop_err), Err(finish_err)) => {
on_cleanup_error(&finish_err);
Err(loop_err)
#[tokio::main]
async fn main() -> Result<(), ExitFailure> {
let cli = Cli::parse();
env_logger::Builder::new()
.filter_level(cli.loglevel)
.target(env_logger::Target::Stderr)
.init();
start_server(cli.database, None).await?;
let mut transport = LocalTransport::new()?;
let mut app = App::new(cli.userid, cli.edit_mode);
let backend_kind = chart::detect_from_env();
app.set_status(format!("user={} chart={backend_kind:?}", app.user_id));
let loop_result = run_loop(&mut transport, &mut app);
// Always attempt to restore the terminal, even on a loop error —
// otherwise the user's shell inherits raw-mode + alternate-screen
// state and becomes unusable.
let finish_result = transport.finish();
combine_outcomes(loop_result, finish_result, |cleanup_err| {
eprintln!("Cleanup error: {cleanup_err:?}");
})?;
Ok(())
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::io;
#[test]
fn parse_edit_mode_accepts_canonical_names() {
assert_eq!(parse_edit_mode("emacs").unwrap(), EditMode::Emacs);
assert_eq!(parse_edit_mode("VIM").unwrap(), EditMode::Vim);
fn parse_edit_mode_rejects_unknown() {
assert!(parse_edit_mode("nano").is_err());
fn silent(_: &io::Error) {}
fn combine_outcomes_all_ok() {
assert!(combine_outcomes(Ok(()), Ok(()), silent).is_ok());
fn combine_outcomes_loop_err_wins_when_alone() {
let err = combine_outcomes::<fn(&io::Error)>(Err(io::Error::other("loop")), Ok(()), silent)
.unwrap_err();
assert_eq!(err.to_string(), "loop");
fn combine_outcomes_cleanup_err_surfaces_when_alone() {
let err =
combine_outcomes::<fn(&io::Error)>(Ok(()), Err(io::Error::other("cleanup")), silent)
assert_eq!(err.to_string(), "cleanup");
fn combine_outcomes_reports_cleanup_when_both_fail() {
let saw_cleanup = Cell::new(false);
let err = combine_outcomes(
Err(io::Error::other("loop")),
Err(io::Error::other("cleanup")),
|cleanup_err| {
saw_cleanup.set(true);
assert_eq!(cleanup_err.to_string(), "cleanup");
},
)
assert_eq!(err.to_string(), "loop", "loop error should be returned");
assert!(saw_cleanup.get(), "cleanup callback must fire");