From 63adf6c5174e7861e8b149ab12db3674d2d8ba74 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 16 May 2026 12:44:12 -0700 Subject: [PATCH] update docs --- AGENTS.md | 71 +++++++++++++++++++++++++++++++++++++++++++++---------- README.md | 59 ++++++++++++++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6ccdae1..8e1d6b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,7 +94,15 @@ function genuinely needs to do I/O for other reasons. - `cache/store.zig` — cache entry timestamps and TTL math - `service.zig` — per-fetch `FetchResult.timestamp` - `net/RateLimiter.zig` — token-bucket refill +- `commands/audit.zig`, `commands/cache.zig`, `commands/history.zig` + — per-invocation `now_s` captures for staleness math, "X ago" + age displays, and rollup `#!created=` directives. Each call + site has a justifying comment. - TUI per-frame "now" captures for relative-time display + (e.g. earnings, options, quote tabs) +- `tui.zig` `shouldDebounceWheel` — uses `.awake` (monotonic + clock) for sub-millisecond input-event debounce; resists + system clock jumps that `.real` would expose - The single `Timestamp.now` capture in `main.zig`'s dispatch entry that produces `today` and `now_s` for the rest of the invocation @@ -147,8 +155,10 @@ already exist and have caught me out: inner type via `Padded(T)` so the same wrapper works for any `format`-bearing type. - `format.fmtIntCommas` — "1,234,567" without `$`. -- `format.formatReturn` — signed percent for trailing-returns - and gain/loss displays. +- `analytics/performance.formatReturn` — signed percent for + trailing-returns and gain/loss displays. (Lives in + `analytics/performance.zig` because returns are a + performance-domain concept; reusing it from elsewhere is fine.) **Search recipes that catch the most cases:** @@ -371,19 +381,22 @@ User input → main.zig (CLI dispatch) or tui.zig (TUI event loop) - **Negative cache entries.** When a provider fetch fails permanently (not rate-limited), a negative cache entry is written to prevent repeated retries for nonexistent symbols. +- **TUI tab framework.** The TUI is a registry-driven framework with eight tabs sharing infrastructure. The single source of truth is `tab_modules` in `src/tui.zig` (an anonymous struct literal mapping tag → module). Everything else — the `Tab` enum, tab-bar labels, the `TabStates` aggregator, key/mouse dispatch, help overlay rows, status-line hints, draw routing — is derived from `tab_modules` at comptime. Each tab module conforms to a contract documented in `src/tui/tab_framework.zig`: declare an `Action` enum, a `State` struct, a `tab` namespace with required hooks, and exactly one of `buildStyledLines` or `drawContent`. A comptime validator (`tab_framework.validateTabModule`) checks every registered module at build time, including hook signatures and a "tabs cannot bind globally-bound keys" rule. See "Adding a new TUI tab" below for the workflow. + ### Module map | Directory | Purpose | |-----------|---------| | `src/models/` | Data types: `Candle`, `Dividend`, `Split`, `Lot`, `Portfolio`, `OptionContract`, `EarningsEvent`, `EtfProfile`, `Quote`. (`Date` and `Money` are top-level types in `src/Date.zig` and `src/Money.zig`.) | | `src/providers/` | API clients: each provider has its own struct with `init(allocator, api_key)` + fetch methods. `json_utils.zig` has shared JSON parsing helpers. | -| `src/analytics/` | Pure computation: `performance.zig` (Morningstar-style trailing returns), `risk.zig` (Sharpe, drawdown), `valuation.zig` (portfolio summary), `analysis.zig` (breakdowns by class/sector/geo) | -| `src/commands/` | CLI command handlers: each has a `run()` function taking `(allocator, *DataService, symbol, color, *Writer)`. `common.zig` has shared CLI helpers and color constants. | -| `src/tui/` | TUI tab renderers: each tab (portfolio, quote, perf, options, earnings, analysis) is a separate file. `keybinds.zig` and `theme.zig` handle configurable input/colors. `chart.zig` renders pixel charts via Kitty graphics protocol. | -| `src/views/` | View models producing renderer-agnostic display data with `StyleIntent` | -| `src/cache/` | `store.zig`: SRF cache read/write with TTL freshness checks | +| `src/analytics/` | Pure computation: `performance.zig` (Morningstar-style trailing returns), `risk.zig` (Sharpe, drawdown), `valuation.zig` (portfolio summary), `analysis.zig` (breakdowns by class/sector/geo), `benchmark.zig` (per-position benchmark returns), `indicators.zig` (SMA/Bollinger/RSI), `milestones.zig` (retirement-attainment grid), `projections.zig` (Monte Carlo + percentile bands), `timeline.zig` (history-tab tier rollup). | +| `src/data/` | Static / semi-static datasets: `imported_values.zig` (back-history values), `shiller.zig` (S&P + CPI series, Shiller's data set), `staleness.zig` (account-cadence checks). `ie_data.csv` is the raw Shiller dataset. | +| `src/views/` | View models producing renderer-agnostic display data with `StyleIntent`: `compare.zig`, `history.zig`, `portfolio_sections.zig`, `projections.zig`. | +| `src/commands/` | CLI command handlers: each has a `pub fn run(...)` entry point. Signatures vary by command's needs (some take `as_of`, `now_s`, `args`, etc.); `common.zig` has shared CLI helpers and color constants. See "Adding a new CLI command" below. | +| `src/tui/` | Eight-tab interactive TUI. Each tab is a separate file conforming to the framework contract documented in `tab_framework.zig`: `portfolio_tab.zig`, `quote_tab.zig`, `performance_tab.zig`, `options_tab.zig`, `earnings_tab.zig`, `analysis_tab.zig`, `history_tab.zig`, `projections_tab.zig`. Plus `keybinds.zig` (configurable input + scoped bindings), `theme.zig` (configurable colors), `chart.zig` (Kitty graphics chart renderer), `projection_chart.zig` (percentile-band overlay), `input_buffer.zig` (modal text-input state machine). The `App` orchestrator lives in the parent `src/tui.zig`. | +| `src/cache/` | `store.zig`: SRF cache read/write with TTL freshness checks. | | `src/net/` | `http.zig`: HTTP client with retry and error classification. `RateLimiter.zig`: token-bucket rate limiter. | -| `build/` | Build-time support: `Coverage.zig` (kcov integration) | +| `build/` | Build-time support: `Coverage.zig` (kcov integration), `download_kcov.zig` (kcov binary fetcher), `gen_shiller.zig` (CSV → comptime data converter), `bcov.css` (kcov report styling). | ## Code patterns and conventions @@ -543,9 +556,41 @@ will fail to catch real bugs later. ### Adding a new TUI tab -1. Create `src/tui/newtab_tab.zig` -2. Add the tab variant to `tui.Tab` enum -3. Wire rendering in `tui.zig`'s draw and event handling +The TUI uses a comptime-derived tab registry (`tab_modules` in +`src/tui.zig`). Adding a new tab is one append to the registry +plus a tab module that conforms to the framework contract. The +`Tab` enum, tab-bar label, `TabStates` aggregator, key/mouse +dispatch, help overlay, status hint, and draw routing all flow +from the registry at comptime — App needs no hand-edits. + +1. **Create `src/tui/newtab_tab.zig`** with: + - `pub const Action = enum { ... };` — tab-local keybind + actions (or empty). + - `pub const State = struct { ... };` — tab-private state + (cursor, expansion flags, cached load state, etc). + - `pub const tab = struct { ... };` — the framework + contract: `label`, `default_bindings`, `action_labels`, + `status_hints`, lifecycle hooks (`init`, `deinit`, + `activate`, `deactivate`, `reload`, `tick`), + `handleAction`, optional event hooks (`handleKey`, + `handleMouse`, `handlePaste`, `statusOverride`, + `onSymbolChange`, `onScroll`, `onCursorMove`, + `isDisabled`). + - **Exactly one** of `pub fn buildStyledLines(state, app, arena)` + (line-list rendering) or `pub fn drawContent(state, app, + arena, buf, width, height)` (direct cell-buffer rendering, + for charts). The framework validator enforces this. +2. **Append to `tab_modules`** at the top of `src/tui.zig`: + `.newtab = @import("tui/newtab_tab.zig"),` +3. Done. The compiler's framework validator (in + `tab_framework.validateTabModule`) will reject the build + with a precise message if any required hook is missing or + has the wrong signature. There's also a comptime check that + the tab's keybindings don't conflict with the global keymap. + +For the contract details, read the doc-block at the top of +`src/tui/tab_framework.zig` — it shows the full required +shape with example signatures. ### `anytype` is almost never the right answer — pause and ask first @@ -645,8 +690,8 @@ command. | Dependency | Purpose | |------------|---------| | [SRF](https://git.lerch.org/lobo/srf) | Cache file format, portfolio/watchlist parsing, serialization | -| [libvaxis](https://github.com/rockorager/libvaxis) (v0.5.1) | Terminal UI rendering | -| [z2d](https://github.com/vancluever/z2d) (v0.10.0) | Pixel chart rendering (Kitty graphics protocol) | +| [libvaxis](https://github.com/rockorager/libvaxis) (v0.6.0) | Terminal UI rendering | +| [z2d](https://github.com/vancluever/z2d) (v0.11.0) | Pixel chart rendering (Kitty graphics protocol) | ## Build system rules diff --git a/README.md b/README.md index a66a225..1b73df6 100644 --- a/README.md +++ b/README.md @@ -251,11 +251,11 @@ If no portfolio or symbol is specified and `portfolio.srf` exists in the current ## Interactive TUI -The TUI has six tabs: Portfolio, Quote, Performance, Options, Earnings, and Analysis. +The TUI has eight tabs: Portfolio, Quote, Performance, Options, Earnings, Analysis, History, and Projections. ### Tabs -**Portfolio** -- navigable list of positions with market value, gain/loss, weight, and purchase date. Multi-lot positions can be expanded to show individual lots with per-lot gain/loss, capital gains indicator (ST/LT), and account name. +**Portfolio** -- navigable list of positions with market value, gain/loss, weight, and purchase date. Multi-lot positions can be expanded to show individual lots with per-lot gain/loss, capital gains indicator (ST/LT), and account name. Press `a` to open an account-filter picker (with `/` search). **Quote** -- current price, OHLCV, daily change, and a 60-day ASCII chart with recent history table. @@ -267,6 +267,10 @@ The TUI has six tabs: Portfolio, Quote, Performance, Options, Earnings, and Anal **Analysis** -- portfolio breakdown by asset class, sector, geographic region, account, and tax type. Uses classification data from `metadata.srf` and account tax types from `accounts.srf`. Displays horizontal bar charts with sub-character precision using Unicode block elements. +**History** -- portfolio value over time, sourced from snapshot files in `/history/` plus optional `imported_values.srf`. Cycle the metric column with `m` (liquid / total / contributions / etc.) and the time-bucket resolution with `t` (week / month / quarter / year). Press `s` (or space) to mark a row for compare; mark a second row, then `c` to commit a side-by-side compare against the live portfolio. Esc cancels an in-flight compare. + +**Projections** -- Monte Carlo retirement projection with percentile bands. Press `d` to set an as-of date (back-date the projection to a historical snapshot), `o` to overlay realized actuals from snapshots / `imported_values.srf` on top of the bands, `v` to toggle the chart vs the text-only report, and `e` to toggle simulated lifecycle events (RMDs, lump-sum withdrawals). Esc clears an active as-of override. + ### Keybindings All keybindings are configurable via `~/.config/zfin/keys.srf`. Generate the default config: @@ -275,7 +279,9 @@ All keybindings are configurable via `~/.config/zfin/keys.srf`. Generate the def zfin i --default-keys > ~/.config/zfin/keys.srf ``` -Default keybindings: +The generated file has two parts: a global section (keys that work in every tab) and a per-tab section (keys that only fire when that tab is active). Tab-local bindings cannot override globally-bound keys — zfin refuses to start if your config creates that conflict. + +Default global keybindings: | Key | Action | |------------------------|------------------------------------------------------| @@ -284,30 +290,45 @@ Default keybindings: | `R` | Reload portfolio from disk (no network) | | `h`, Left, Shift+Tab | Previous tab | | `l`, Right, Tab | Next tab | -| `1`-`6` | Jump to tab | +| `1`-`8` | Jump to tab N | | `j`, Down | Select next row | | `k`, Up | Select previous row | -| `Enter` | Expand/collapse (positions, expirations, calls/puts) | -| `s` | Select symbol from portfolio for other tabs | -| `/` | Enter symbol search | -| `e` | Edit portfolio/watchlist in `$EDITOR` | -| `<` | Sort by previous column (portfolio tab) | -| `>` | Sort by next column (portfolio tab) | -| `o` | Reverse sort direction (portfolio tab) | -| `[` | Previous chart timeframe (quote tab) | -| `]` | Next chart timeframe (quote tab) | -| `c` | Toggle all calls collapsed/expanded (options tab) | -| `p` | Toggle all puts collapsed/expanded (options tab) | -| `Ctrl+1`-`Ctrl+9` | Set options near-the-money filter to +/- N strikes | | `g` | Scroll to top | | `G` | Scroll to bottom | | `Ctrl+d` | Half-page down | | `Ctrl+u` | Half-page up | -| `PageDown` | Page down | -| `PageUp` | Page up | +| `PageDown`, `Ctrl+f` | Page down | +| `PageUp`, `Ctrl+b` | Page up | +| `/` | Symbol input prompt | | `?` | Help screen | -Mouse: scroll wheel navigates, left-click selects rows and switches tabs, double-click expands/collapses. +Default tab-local keybindings (only active on the matching tab): + +| Tab | Key | Action | +|-------------|--------------------|----------------------------------------------------------| +| Portfolio | `Enter` | Expand/collapse position | +| Portfolio | `>` / `<` | Sort by next / previous column | +| Portfolio | `o` | Reverse sort direction | +| Portfolio | `a` | Open account-filter picker (`/` to search inside picker) | +| Portfolio | `Esc` | Clear active account filter | +| Portfolio | `s`, `Space` | Select symbol (sets active symbol for other tabs) | +| Quote | `[` / `]` | Previous / next chart timeframe | +| Options | `Enter` | Expand/collapse expiration or section | +| Options | `c` / `p` | Toggle all calls / puts collapsed | +| Options | `Ctrl+1`-`Ctrl+9` | Set near-the-money filter to +/- N strikes | +| History | `Enter` | Expand/collapse tier | +| History | `m` | Cycle metric column | +| History | `t` | Cycle time-bucket resolution | +| History | `s`, `Space` | Mark / unmark row for compare | +| History | `c` | Commit compare (after two rows marked) | +| History | `Esc` | Cancel in-flight compare selection | +| Projections | `d` | Set as-of date prompt | +| Projections | `Esc` | Clear as-of date | +| Projections | `o` | Toggle realized-actuals overlay | +| Projections | `v` | Toggle chart vs text-only report | +| Projections | `e` | Toggle simulated lifecycle events | + +Mouse: scroll wheel navigates, left-click selects rows and switches tabs. ### Theme