InspectorPane

NAME

App::Moneymoor::View::InspectorPane - the detail rail's lines, and the Explain dialog's, both derived from the one equation that defines an envelope's balance.

SYNOPSIS


use App::Moneymoor::View::InspectorPane;

# The rail beside the envelope table (fixed 30 columns, 26 inside the
# frame and its padding):
my @lines = inspector-lines($view, '2026-08-01', $groceries-id,
                            width => RAIL-TEXT-COLS, icons => $icons);
@linesΒ».<text>;
# ('Carry-in            Β£0.00',
#  'Assigned          Β£400.00',
#  'Activity          -Β£72.50',
#  'Moved in            Β£0.00',
#  'Moved out           Β£0.00',
#  '──────────────────────────',
#  'Available         Β£327.50')

# With a target, two or three more lines under it. The figures are
# gathered first, by the one sub in this module that knows what a
# Model::Category is:
my %target = target-figures($view, $scheme, $groceries, '2026-08-01');
inspector-lines($view, '2026-08-01', $groceries-id, :%target)Β».<text>.tail(2);
# ('Target            Β£400.00', 'To fund            +Β£72.50')

# A goal, which carries its schedule with it:
target-lines(target-figures($view, $scheme, $tax, '2026-12-01'))Β».<text>;
# ('Target         Β£50,000.00',
#  'by April 2027 Β· 5 of 9',
#  'To fund         +Β£2,777.77')

# Straight to a RichText, coloured per Β§2:
$rail.set-content(inspector-spans($view, $period, $id,
                                  theme => $theme, icons => $icons));

# The Explain dialog is the same equation plus the derived moves:
explain-lines($view, '2026-08-01', $visa-payment-id,
              names => %categories-by-name, width => 60)Β».<text>;
# (… , '', 'Derived moves (2)',
#  'card coverage  Groceries β†’ Visa  Β£72.50',
#  'card refund  Visa β†’ Dining Out  Β£5.00')

cause-label('card-inflow-to-rta');   # 'card inflow β†’ RTA'

DESCRIPTION

Pure line builders. Every function takes a BudgetView, a period and a category id, and returns a list of < { text, severity } > hashes; the *-spans wrappers turn those into Selkie::Widget::RichText spans using App::Moneymoor::View::BudgetRow::severity-style, so the rail, the grid and the Explain dialog cannot drift apart on colour.

Rule 1, written out

An envelope's balance is

available = carry-in + assigned + activity + moved-in - moved-out

and the rail is that sum, one term per line, with a rule and the total underneath. moved-out is rendered negated for exactly that reason: a column of figures that does not add up to the number at the bottom is worse than no column at all.

moved-in and moved-out are the derivation's own credit-card moves, not anything the user typed β€” which is why the rail says how many there are and offers x to see them, rather than trying to explain them in twenty-six columns.

The target block is not part of the equation

An envelope with a target gets two or three more lines under the flags: what it wants, what its plan says about this period, and what is still missing (To fund +Β£37.50) or that it has it. They sit below the rule and the total on purpose. available is a derived fact and the five terms above it are why it is what it is; a target is a plan, and the derivation has never heard of one. Putting the target inside the sum would make the column stop adding up, which is the one thing the rail must never do.

Underfunded is not computed here

It used to be β€” max(0, target - available), in target-lines, and that is only right for one of the three target kinds. The kind-aware answer is App::Moneymoor::Service::Target's target-ask, and there is exactly one of it in the app: the rail, the grid, f and the assign field's bare = all ask the same function the same question. What is still true is that the figure is never negative β€” an envelope over its target is funded, not "minus Β£20 to fund".

…but the figures are gathered here, and rendered separately

target-figures is the seam. It takes the model and the scheme, calls Service::Target four times and answers a flat Hash of numbers and strings; target-lines takes that Hash and knows nothing else. Two reasons for the split, and both of them are about keeping this module honest:

  • Presentation stays testable without a budget. Every wording below β€” and there are now three kinds' worth β€” can be asserted against a hand-built Hash, with no view, no facts and no schedule arithmetic in the way.

  • The Model::Category stops at one function. Every other builder in this module still takes a view and an id, which is what lets the Explain dialog reuse them. The target block cannot: a kind is a property of the row, not of the derivation. So the model goes into target-figures and comes no further.

What each kind's block says

Kind Line 1 Line 2 Line 3
refill Target Β£400.00 To fund +Β£72.50 β€” or Target met
set_aside Target Β£100.00 /period To fund +Β£40.00 β€” or Funded this period
by_period Target Β£50,000.00 by April 2027 Β· 5 of 9 To fund +Β£2,777.77 β€” or On track, or Target met

set_aside says /period rather than Target met on the second line because its question is per-period: Β£900 already saved does not mean this period's Β£100 is done, and "met" would claim it does. describe-target's full caption ("Set aside Β£100.00 each period") is five columns too wide for the rail, so the rail says the same thing in its own shorthand.

by_period's schedule line is packed to fit: the clauses are the goal period ("by April 2027"), the position in the plan ("3 of 9") and, when the goal repeats, how often ("every 3 periods"). They are joined with a middle dot while they fit in $width and broken onto another line when they do not. That is not decoration β€” a monthly/14 budget's period label is "14 Aug – 13 Sep 2026", which with a position clause after it is thirty-two columns in a rail that has twenty-six.

Its last line tells three states apart: Target met when the envelope is at or past the goal (the plan is done, whatever period it is), On track when this period asks for nothing but the goal is still ahead, and the amber To fund otherwise.

Why the width is a parameter

The rail is 30 cells wide, less two for the frame and two for its padding: 26. The Explain dialog is a modal and much wider. Both want the same lines with the money flushed right against a different edge, so the width is an argument and RAIL-TEXT-COLS is just the default the rail passes.

A label and figure that will not fit in the given width fall back to "label space figure" rather than being truncated β€” a squeezed layout that still says the number beats a tidy one that does not.

EXPORTS

  • inspector-lines($view, $period, $id, :$width, :%target, :$icons -- List)>

  • inspector-spans($view, $period, $id, :$theme!, :$width, :%target, :$icons -- List)>

  • equation-lines($view, $period, $id, :$width -- List)>

  • flag-lines($cm, :$width, :$icons -- List)>

  • target-figures($view, $scheme, $category, Str $period -- Hash)> β€” the block's numbers and captions, or an empty Hash when there is no target. Tolerates an uncomputed view, a missing scheme and a Category type object.

  • target-lines(%target, :$width -- List)> β€” empty for an empty Hash.

  • moves-footer($view, $period, $id -- Str)> β€” Str (the type object) when the period has no derived moves for the category.

  • explain-lines($view, $period, $id, :%names!, :$width, :$icons -- List)>

  • explain-spans(…, :$theme! -- List)>

  • cause-label(Str $cause -- Str)>

  • pad-line(Str $label, Str $value, Int $width -- Str)>

  • RAIL-COLS (30) / RAIL-TEXT-COLS (26)

SEE ALSO

App::Moneymoor v0.4.2

YNAB-style envelope budgeting: a derivation engine

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

DBIish:ver<0.6.7>:auth<zef:raku-community-modules>Notcurses::Native:ver<0.6.5+>:auth<zef:apogee>Selkie:ver<0.16.0+>:auth<zef:apogee>JSON::Fast:ver<0.19>:auth<cpan:TIMOTIMO>MacOS::NativeLib:ver<0.0.6>:auth<zef:lizmat>

Test Dependencies

Provides

  • App::Moneymoor
  • App::Moneymoor::Config
  • App::Moneymoor::DB
  • App::Moneymoor::Gateway::Account
  • App::Moneymoor::Gateway::Assignment
  • App::Moneymoor::Gateway::Category
  • App::Moneymoor::Gateway::Payee
  • App::Moneymoor::Gateway::Transaction
  • App::Moneymoor::Handlers::Boot
  • App::Moneymoor::Model::Account
  • App::Moneymoor::Model::Assignment
  • App::Moneymoor::Model::Category
  • App::Moneymoor::Model::CategoryGroup
  • App::Moneymoor::Model::Payee
  • App::Moneymoor::Model::Split
  • App::Moneymoor::Model::Transaction
  • App::Moneymoor::Screen::Accounts
  • App::Moneymoor::Screen::Budget
  • App::Moneymoor::Screen::Login
  • App::Moneymoor::Screen::Main
  • App::Moneymoor::Screen::Main::Keybinds
  • App::Moneymoor::Screen::Main::Modals
  • App::Moneymoor::Screen::Main::Subscriptions
  • App::Moneymoor::Screen::Reports
  • App::Moneymoor::Service::Budget
  • App::Moneymoor::Service::Icons
  • App::Moneymoor::Service::Target
  • App::Moneymoor::Service::Workspace
  • App::Moneymoor::StoreHandlers
  • App::Moneymoor::Theme
  • App::Moneymoor::Theme::Catppuccin
  • App::Moneymoor::Theme::Dracula
  • App::Moneymoor::Theme::Everforest
  • App::Moneymoor::Theme::Gruvbox
  • App::Moneymoor::Theme::Kanagawa
  • App::Moneymoor::Theme::Monokai
  • App::Moneymoor::Theme::Nord
  • App::Moneymoor::Theme::OneDark
  • App::Moneymoor::Theme::RosePine
  • App::Moneymoor::Theme::Solarized
  • App::Moneymoor::Theme::TokyoNight
  • App::Moneymoor::Themes
  • App::Moneymoor::UI
  • App::Moneymoor::Util::Money
  • App::Moneymoor::Util::Period
  • App::Moneymoor::View::BudgetRow
  • App::Moneymoor::View::EmptyState
  • App::Moneymoor::View::HintBar
  • App::Moneymoor::View::InspectorPane
  • App::Moneymoor::View::ModalChrome
  • App::Moneymoor::View::RegisterRow
  • App::Moneymoor::View::ReportRow
  • App::Moneymoor::Widget::BannerBar
  • App::Moneymoor::Widget::BootProgressModal

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite β€” the markup and publishing tools behind this site.