8.9 KiB
Footer-Log + Pager + Header Polish Plan
Follow-on plan after the ncurses_tui_migration completed. Addresses three observations:
- The header should display
NOSTR TERMINAL - <version> - <user>, not justNOSTR TERMINAL <version>. - "Debug-style" prints after actions (publish, fetch, etc.) collide with the menu redraw loop and produce ncurses wackiness.
- The library footer is currently unused; instead of removing it, we can promote it to a footer-log: a live one-line status feed of recent events, expandable into a full-screen scrollable log viewer.
1. The three-tier output model
After this plan, every "show something to the user" call site fits into one of three tiers:
| Tier | API | When to use |
|---|---|---|
| Footer-log | nt_log(fmt, ...) |
Passive informational stream — publishes, network events, action completions, status ticks. Latest line shown in footer. Full history available via the View log entry on the main menu. |
| Modal dialog | tuin_notice(), tuin_confirm() |
Must-acknowledge errors, destructive confirmations. Forced acknowledgment matters. |
| Full-screen pager | nt_pager_show_text(breadcrumb, text) |
Intentional content viewing — DM thread bodies, event JSON dumps, AI responses, blog post bodies, on-demand log viewer. |
nt_print() is retained for in-screen composition (custom screens that build their own body content between header/footer), but is no longer the right tool for "result of an action" prints.
2. Library additions (in ~/lt/aesthetics/lib/tui_ncurses/)
2.1 tuin_pager_run
/* Scrollable full-screen text viewer. Same pinned-pane layout as
* tuin_menu_run: header (frame), body (scrollable text), footer (status).
* Library-owned input loop:
* Up / Down : scroll one line
* PgUp / PgDn : scroll one page
* Home / End : jump top / bottom
* Esc / q / x / Enter : return
* KEY_RESIZE : silent repaint
* Long lines wrap to body width. '\n' splits.
*
* Returns 0 on normal exit, -1 on uninitialized library. */
int tuin_pager_run(const TuiFrame *frame,
const char *text,
const TuiStatus *status);
Rationale: complements tuin_notice (popup, short text, press-any-key) with a full-screen scrollable variant for arbitrary-length text. Both tui_continuous and tui_ncurses are missing this primitive; every TUI app eventually needs it.
2.2 README + sample updates
- README documents
tuin_pager_run. samples/ncurses/ncurses-tui.cgains a "View pager demo" entry.
Note: an earlier draft proposed a
tuin_set_global_key_handlerso a global keybinding (Ctrl+L) could open the log from any screen. That has been dropped — the log is reachable via a regularView logentry on the main menu, which keeps discoverability high and avoids per-app library extensions.
3. Adapter additions (in nostr_terminal/src/nt_tui_adapter.c)
3.1 Header composition
/* Set the optional user label. Empty string or NULL hides the user segment. */
void nt_set_user(const char *user);
nt_frame() now composes a single title:
- Logged in:
"NOSTR TERMINAL - v0.0.13 - alice" - Pre-login:
"NOSTR TERMINAL - v0.0.13"
It packs everything into TuiFrame.app_name and leaves app_version empty so the library renders one centered title line. (Library renders app_name + " " + app_version already, so packing into app_name is the cleanest path with no library changes.)
3.2 Footer-log
Internal state: a fixed-size ring buffer (e.g. 1024 entries) of {timestamp, message} records. Backing storage uses heap-allocated strings stored in a circular array; oldest entry evicted on overflow.
Public API:
/* Append a log entry. Timestamp captured automatically. Footer auto-updates
* on next library repaint. Safe to call from any context except inside the
* footer renderer itself. */
void nt_log(const char *fmt, ...);
/* Open the full log in a scrollable pager (calls tuin_pager_run with
* formatted log text + breadcrumb '> Log'). Newest entries at the bottom. */
void nt_log_show(void);
/* Clear the entire log buffer (e.g. user request from log viewer). */
void nt_log_clear(void);
/* Return current latest log line (NULL or empty if none). Used internally
* by nt_status() to populate the footer. */
const char *nt_log_latest(void);
nt_status() is changed to return a TuiStatus whose text is nt_log_latest() (or empty if no entries). Footer thus always reflects the latest log line.
3.3 Pager wrapper
/* Convenience wrapper around tuin_pager_run for app-specific content viewing.
* Builds TuiFrame from breadcrumb + current title, builds TuiStatus from
* footer-log latest line, calls tuin_pager_run. */
void nt_pager_show_text(const char *breadcrumb, const char *text);
4. Footer behavior matrix
| State | Footer text |
|---|---|
| No log entries (fresh start) | empty / blank line |
| Log has entries | latest line, e.g. "[16:42:04] Tweet published to 3/5 relays." |
Inside nt_log_show() (pager viewing the log) |
scroll position hint, e.g. "Up/Down PgUp/PgDn Esc to return" |
Inside other pager (nt_pager_show_text) |
scroll position hint |
The full log is reached via the View log entry on the main menu (added between Kind/event dump and Quit).
5. Live-update latency
When nt_log() is called while the user is inside tuin_menu_run, the library is blocked on wgetch. The footer updates next time the user hits a key. This is acceptable for the MVP because:
- Most user-initiated actions immediately return control before the user hits another key.
- Async background events (network) usually result in multiple log entries; the user sees them as soon as they touch any key.
A future enhancement could add an internal pipe / wtimeout() heartbeat to repaint the footer on a timer, but that requires deeper library changes and is not in scope.
6. Sweep targets — convert prints to log/pager/notice
Across the codebase, search for nt_print( call sites that fall into these categories:
6.1 → nt_log(...) (informational stream)
- "Publishing event…" / "Connecting to relay…"
- "Relay X: OK" / "Relay Y: timeout"
- "Tweet completed. 3/5 relays succeeded."
- "Loaded N follows."
- "Fetched N notifications."
- DB / cache messages.
6.2 → tuin_notice(...) (must-ack errors)
- "Failed to parse nsec."
- "Network error: …"
- "Invalid input."
- "Authentication failed."
6.3 → tuin_confirm(...) (destructive)
- "Delete this relay? (y/n)"
- "Send this tweet now? (y/n)"
6.4 → nt_pager_show_text(...) (intentional content viewing)
- DM thread bodies after picking a conversation.
- Event JSON dumps (the existing
menu_show_loaded_eventsalready does this). - Blog post body in
menu_posts.c. - AI streamed response in
menu_ai.c(final shown text). - NIP-11 relay info in
menu_relays.c.
6.5 → keep nt_print(...) (in-screen composition)
- Custom body screens that compose between header/footer (e.g. live feed in
menu_live.c). - Anything inside a custom
tuin_get_key/wtimeoutloop.
7. Migration order
- Aesthetics library: add
tuin_pager_run. Update README + ncurses sample. - Re-vendor
tui_ncurses.{c,h}intoresources/tui_ncurses/. - Adapter: implement ring-buffer log,
nt_log/nt_log_show/nt_log_clear/nt_log_latest,nt_set_user,nt_frame()composition,nt_pager_show_text. src/main.c: wirent_set_user(g_state.user_name)(empty when not logged in) insident_update_app_title(). Add aView logentry toMAIN_ITEMS(beforeQuit) that callsnt_log_show().- Sweep menu files per §6 categories. Replace
nt_printcalls one bucket at a time. - Docs:
- Update
docs/tui_style.mdwith the three-tier model. - Update
README.mdwith the newView logmain-menu entry and footer behavior. - Update
plans/ncurses_tui_migration.mdwith a "follow-on" pointer to this plan.
- Update
8. Acceptance criteria
- Header reads
NOSTR TERMINAL - <version> - <user>when logged in,NOSTR TERMINAL - <version>otherwise. - Footer always shows the latest log line (or blank if no log entries yet).
- The main menu has a
View logentry that opens the full log viewer. - After publishing a tweet, the footer immediately (after next keystroke) shows e.g.
"[hh:mm:ss] Tweet published.". - Long output (event JSON, NIP-11 dump) opens cleanly in the full-screen pager.
- No more cases of
nt_printoutput being clobbered by menu redraws. - Library additions documented and present in aesthetics repo.
cd build && cmake .. && makesucceeds; static Alpine build still works.