Lines
97.62 %
Functions
50 %
Branches
100 %
//! Palette command form builder.
//!
//! Converts a resolved [`CommandNode`] + parsed args into a nomiscript
//! form string ready for submission via [`crate::app::App::submit_console_form`].
//! Each eval-able leaf maps to a concrete native form; leaves that are
//! not eval-able (reports, sql, ssh-key, config set) return `None` so
//! the caller falls back to the existing modal / status behaviour.
use cli_core::eval::escape_str;
use cli_core::reports::coerce_date_arg;
use cli_core::{CommandError, CommandNode};
use sqlx::types::Uuid;
use std::str::FromStr;
use crate::tabs::reports::ReportKind;
/// Describe a report request resolved from the palette.
pub struct ReportRequest {
pub kind: ReportKind,
/// Pre-coerced RFC3339 string (start of day). Empty for Balance.
pub from: String,
/// Pre-coerced RFC3339 string (end of day). Empty for Balance.
pub to: String,
pub chart: String,
}
/// Try to build a `ReportRequest` from a `reports <kind>` palette command.
///
/// Balance ignores from/to; Activity and Breakdown require them.
/// Returns `Ok(None)` when from/to are absent for date-needing reports
/// (caller should open the params modal instead).
/// # Errors
/// Returns an error if date coercion fails for a date that was actually provided.
pub fn build_report_request(
path: &[String],
args: &[(String, String)],
) -> Result<Option<ReportRequest>, CommandError> {
let kind = match path_key(path).as_str() {
"reports balance" => ReportKind::Balance,
"reports activity" => ReportKind::Activity,
"reports breakdown" => ReportKind::Breakdown,
_ => return Ok(None),
};
let chart = opt_arg(args, "chart").unwrap_or_else(|| "bar".to_string());
if kind == ReportKind::Balance {
return Ok(Some(ReportRequest {
kind,
from: String::new(),
to: String::new(),
chart,
}));
let from_raw = opt_arg(args, "from");
let to_raw = opt_arg(args, "to");
match (from_raw, to_raw) {
(Some(f), Some(t)) => {
let from = coerce_date_arg(&f, false)?;
let to = coerce_date_arg(&t, true)?;
Ok(Some(ReportRequest {
from,
to,
}))
_ => Ok(None),
/// Attempt to build a nomiscript form for `node` given `path` and `args`.
/// `path` is the full resolved command path (e.g. `["account", "list"]`),
/// used to disambiguate leaves that share a name across different parents.
/// Returns `Ok(Some(form))` for eval-able leaves, `Ok(None)` when the
/// leaf is intentionally handled outside the eval path (modal / status),
/// and `Err` when a required arg is missing or malformed.
pub fn build_form(
_node: &CommandNode,
) -> Result<Option<String>, CommandError> {
// Use the full path for unambiguous dispatch.
match path_key(path).as_str() {
"version" => Ok(Some("(get-version)".to_string())),
"account list" => Ok(Some("(list-accounts)".to_string())),
"account balance" => {
let account = require_uuid(args, "account")?;
Ok(Some(format!(
"(get-balances {})",
escape_str(&account.to_string())
)))
// account create / tag open the modal — not eval leaves.
"account create" | "account tag" => Ok(None),
"transaction list" => {
let account = opt_uuid(args, "account")?;
"(list-transactions {})",
escape_str(&account)
// transaction create / tag open the modal — not eval leaves.
"transaction create" | "transaction tag" => Ok(None),
"commodity list" => Ok(Some("(list-commodities)".to_string())),
// commodity create / convert open the modal — not eval leaves.
"commodity create" | "commodity convert" => Ok(None),
"config get" => {
let name = require_str(args, "name")?;
Ok(Some(format!("(get-config {})", escape_str(&name))))
// config set opens the modal — not an eval leaf.
"config set" => Ok(None),
// Reports route through build_report_request; sql/ssh-key have no TUI surface yet.
/// Flatten `path` into a space-joined key for matching.
fn path_key(path: &[String]) -> String {
path.join(" ")
// ── Arg helpers ───────────────────────────────────────────────────────────────
fn opt_arg(args: &[(String, String)], key: &str) -> Option<String> {
args.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
fn require_str(args: &[(String, String)], key: &str) -> Result<String, CommandError> {
opt_arg(args, key)
.filter(|v| !v.is_empty())
.ok_or_else(|| CommandError::Argument(format!("missing required arg: {key}")))
fn require_uuid(args: &[(String, String)], key: &str) -> Result<Uuid, CommandError> {
let s = require_str(args, key)?;
Uuid::from_str(&s).map_err(|_| CommandError::Argument(format!("invalid UUID for {key}: {s}")))
/// Optional UUID arg: absent/empty yields `""` (the native's "no filter" / "no
/// parent" form); a present-but-malformed value is a hard arg error so it never
/// reaches eval as a bogus form.
fn opt_uuid(args: &[(String, String)], key: &str) -> Result<String, CommandError> {
match opt_arg(args, key).filter(|v| !v.is_empty()) {
None => Ok(String::new()),
Some(s) => Uuid::from_str(&s)
.map(|u| u.to_string())
.map_err(|_| CommandError::Argument(format!("invalid UUID for {key}: {s}"))),
#[cfg(test)]
mod tests;