Main

NAME

App::Moneymoor::Screen::Main - the application shell: banner, tab strip, content host, hint footer, and the caches every tab reads from.

SYNOPSIS


use App::Moneymoor::Screen::Main;

my $main = App::Moneymoor::Screen::Main.new(
    app       => $app,
    db        => $db,
    workspace => $workspace,
    config    => $config,
    theme     => $theme,
);
$app.add-screen('main', $main.build);
$app.switch-screen('main');
$main.focus-content;    # the budget table, on the tab that is mounted

# Everything downstream reads the cache, never the gateways:
$main.view.rta($main.viewed-period);     # the cached BudgetView
$main.category($id).name;                # id โ†’ model, rebuilt per recompute

DESCRIPTION

VBox(flex)
     โ”œ BannerBar(fixed 1)      ' App::Moneymoor ยท Budget ยท August 2026'  + clock
     โ”œ TabBar(fixed 1)         Budget / Accounts / Reports
     โ”œ VBox(flex)              content host โ€” one child, swapped per tab
     โ”” RichText(fixed 1)       hint footer

A plain class, not a widget: it owns the tree rather than being part of one. Its job is the four things a tab builder should not have to think about โ€” the palette, the glyph tier, the cached BudgetView plus the id โ†’ model lookup hashes, and the store โ€” and it exposes all of them through public accessors, which are the module boundary for the Keybinds / Modals / Subscriptions siblings.

The cache, and why it is not in the store

Selkie::Store digests subscription values to decide whether anything changed, and it keys objects by identity. A selector handing back a BudgetView โ€” or a Hash of Model::Category objects โ€” would read as "changed" on every tick and repaint the whole screen sixty times a second. So the view and the four lookup hashes live here, and the store carries budget/digest: a byte-stable, order-independent string the derivation computes for exactly this purpose. Subscriptions watch the digest; their callbacks pull from these accessors.

set-view is the sink the ws/mutate effect calls (wired in !register-handlers), so the cache and the digest can never disagree โ€” they are written by the same recompute.

Tab switching

The tab strip, 1 / 2 / 3 and Ctrl+1 / Ctrl+2 / Ctrl+3 all dispatch app/tab-selected. A store subscription on app/tab then drives !refresh-content-layout, which clears the content host โ€” destroying the departing subtree and, with it, its subscriptions โ€” builds the new tab's panes, pushes the store down to them, and re-installs that tab's subscriptions and keybinds.

Doing it through the store rather than straight from the tab callback is what makes the two entry points converge: a keybind switch and a click both end as one write to app/tab, and the strip re-syncs from the same subscription.

The last step of a re-mount is focus-content: the widget that had focus was destroyed with the departing subtree, and Selkie does not move focus on its own, so without it the new tab would be on screen with the keyboard still pointing at nothing and the footer showing the generic hints. The one mount that does not focus is the first, from inside build โ€” the screen has not been switched to yet, and Selkie::App.switch-screen restores its own remembered focus after the fact, so placing the initial focus is UI's job.

The tab builders

!build-budget-pane hands off to App::Moneymoor::Screen::Budget, which owns the envelope table, the Ready-to-Assign pill and the detail rail, and which the shell keeps a reference to in budget-tab so the budget keybinds and modals can find the table after a rebuild. !build-accounts-pane does the same for App::Moneymoor::Screen::Accounts and accounts-tab: the sidebar, the register and the nine dialogs that change what either says. !build-reports-pane for App::Moneymoor::Screen::Reports and reports-tab: the period's cash-flow strip and the chart of what it went on.

All three go through the same seams โ€” the builder contract, mount-pane, the per-tab subscription and keybind installs, the per-pane hint-context map, the store cascade โ€” which is what let each tab land inside them without the shell changing shape.

One resize callback, for the whole app

Selkie::App's resize callbacks accumulate and there is no way to remove one, so a tab that registered its own would leak a closure โ€” holding a destroyed widget tree โ€” on every visit. The shell therefore owns the single callback and fans it out: the hint footer refits, and the accounts tab is told the new width so it can decide whether the register's running-balance column still fits.

ACCESSORS

  • app, db, workspace, config, theme, store โ€” the injected collaborators.

  • icons โ€” the active App::Moneymoor::Icons tier, resolved lazily from config.icons and refreshed by apply-theme-live.

  • root, top-bar, tabs, content-host, hint-footer โ€” the shell's own widgets.

  • view โ€” the cached BudgetView. A type object until the first recompute lands.

  • payee($id) / category($id) / group($id) / account($id) โ€” id โ†’ model, from the caches set-view rebuilds. A type object for an unknown id.

  • viewed-period โ€” app/period, or the period containing today if the store has not been seeded yet.

  • current-tab, tab-label($name), tab-names.

  • pane-contexts โ€” the current tab's < widget => hint context > pairs, which is how the footer resolves focus to a context.

  • initial-focus-widget โ€” the current tab's primary focus target.

  • focus-content() โ€” put focus there. Called after every content rebuild; UI calls it once more after switch-screen.

  • budget-tab / accounts-tab / reports-tab โ€” the tab controller while its tab is mounted; a type object otherwise.

  • pane-border($title, $sizing) / mount-pane($parent, $border, $pane, $context) โ€” the pane-mounting API the tab builders work through.

  • rebuild-content() โ€” re-mount the current tab's panes (a live theme swap; the inspector toggle).

SEE ALSO

  • The hint bar drops whole groups to fit, so a wider terminal has to get its groups back and a narrower one has to shed them.

  • The register's running-balance column only exists above a width threshold, and crossing it rebuilds the column set.

  • The reports chart draws one bar per row and cannot scroll, so a taller pane shows categories a shorter one had to cut.

  • 1. Re-resolve $!theme by name โ€” several places in this class hold it, and every subscription closure captured it.

  • 2. $!app.set-theme โ€” cascades through every registered screen's widget tree and repaints the plane bases.

  • 3. Re-derive what the shell baked in at build time. The banner resolved its gradient stops itself; the footer's colours live in its spans. Neither is a Selkie::Style the cascade can see.

  • 4. Re-read the glyph tier โ€” Settings writes both keys and closes through this one path.

  • 5. rebuild-content โ€” re-registers every per-tab subscription closure against the new palette, and puts focus back on the pane that just got destroyed and rebuilt. ) method apply-theme-live() { return without $!config; my $new = App::Moneymoor::Themes::load($!config.theme); $!theme = $new; $!app.set-theme($new.to-selkie) if $!app.defined; $!icons = self!load-icons;

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.