Lines
81.52 %
Functions
33.33 %
Branches
100 %
//! Reports tab: balance / activity / category-breakdown.
//!
//! `ReportsTab` holds the current fetch state and the last successful
//! `ChartSpec`. `on_reply` translates a wire frame into a spec via the
//! shared `cli_core::reports` parse helpers and the `plotting::adapters`
//! chain. `draw` renders the spec inline with `plotting::ratatui`.
use cli_core::render::{WireValue, parse_wire};
use cli_core::reports::{
parse_chart_shape, value_to_activity_periods, value_to_balance_rows, value_to_breakdown_periods,
};
use plotting::{
ChartSpec,
adapters::{
ActivityChartOpts, BalanceChartOpts, BreakdownChartOpts, SortOrder, activity_chart,
balance_chart, breakdown_chart,
},
ratatui::render_ratatui,
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::tabs::fetch::Fetch;
/// Which report is currently loaded or being fetched.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportKind {
Balance,
Activity,
Breakdown,
}
impl ReportKind {
#[must_use]
pub fn label(self) -> &'static str {
match self {
ReportKind::Balance => "Balance",
ReportKind::Activity => "Activity",
ReportKind::Breakdown => "Category Breakdown",
pub struct ReportsTab {
pub state: Fetch<ChartSpec>,
/// Which report produced the current (or pending) fetch.
pub kind: ReportKind,
/// Chart shape used for the last build.
pub chart: String,
impl ReportsTab {
pub fn new() -> Self {
Self {
state: Fetch::Idle,
kind: ReportKind::Balance,
chart: "bar".to_string(),
/// Set loading state once an eval id is assigned.
pub fn set_loading(&mut self, id: i64, kind: ReportKind, chart: impl Into<String>) {
self.kind = kind;
self.chart = chart.into();
self.state = Fetch::Loading { id };
/// Process a routed wire reply and update the fetch state.
/// Apply a routed reply, parsing it with the `kind`/`chart` that produced
/// the request (carried on the route), not the tab's current selection —
/// the two can differ if a newer request was issued before this reply.
pub fn on_reply(&mut self, wire: &str, kind: ReportKind, chart: &str) {
self.chart = chart.to_string();
self.state = parse_report_reply(wire, kind, chart);
/// Reset to idle so the next invocation re-fetches.
pub fn reset(&mut self) {
self.state = Fetch::Idle;
/// Render the tab into `area`.
pub fn draw(&self, frame: &mut Frame, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.title(self.kind.label());
match &self.state {
Fetch::Idle => {
let msg = "Use :reports balance | :reports activity from=YYYY-MM-DD to=YYYY-MM-DD | :reports breakdown from=... to=...";
frame.render_widget(Paragraph::new(msg).block(block), area);
Fetch::Loading { .. } => {
frame.render_widget(Paragraph::new("Loading...").block(block), area);
Fetch::Error(e) => {
let msg = format!("[error] {e}");
Fetch::Loaded(spec) => {
let inner = block.inner(area);
frame.render_widget(block, area);
let chart = render_ratatui(spec);
chart.draw(frame, inner);
impl Default for ReportsTab {
fn default() -> Self {
Self::new()
/// Parse a wire reply frame into a `Fetch<ChartSpec>` for the given kind.
fn parse_report_reply(wire: &str, kind: ReportKind, chart: &str) -> Fetch<ChartSpec> {
match parse_wire(wire) {
Err(e) => Fetch::Error(e.to_string()),
Ok(WireValue::Value(value)) => match build_chart_spec(&value, kind, chart) {
Ok(spec) => Fetch::Loaded(spec),
Err(e) => Fetch::Error(e),
fn build_chart_spec(
value: &scripting::nomiscript::Value,
kind: ReportKind,
chart: &str,
) -> Result<ChartSpec, String> {
let chart_kind = parse_chart_shape(chart);
match kind {
ReportKind::Balance => {
let rows = value_to_balance_rows(value).map_err(|e| e.to_string())?;
Ok(balance_chart(
&rows,
BalanceChartOpts {
kind: chart_kind,
top_n: 10,
sort_order: SortOrder::MagnitudeDesc,
))
ReportKind::Activity => {
let periods = value_to_activity_periods(value).map_err(|e| e.to_string())?;
Ok(activity_chart(
&periods,
ActivityChartOpts {
include_net: true,
ReportKind::Breakdown => {
let periods = value_to_breakdown_periods(value).map_err(|e| e.to_string())?;
Ok(breakdown_chart(
BreakdownChartOpts {
#[cfg(test)]
mod tests;