Files
nostr_terminal/plans/footer_log_and_pager.md

8.9 KiB

Footer-Log + Pager + Header Polish Plan

Follow-on plan after the ncurses_tui_migration completed. Addresses three observations:

  1. The header should display NOSTR TERMINAL - <version> - <user>, not just NOSTR TERMINAL <version>.
  2. "Debug-style" prints after actions (publish, fetch, etc.) collide with the menu redraw loop and produce ncurses wackiness.
  3. 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

Note: an earlier draft proposed a tuin_set_global_key_handler so a global keybinding (Ctrl+L) could open the log from any screen. That has been dropped — the log is reachable via a regular View log entry 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.)

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);

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_events already 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/wtimeout loop.

7. Migration order

  1. Aesthetics library: add tuin_pager_run. Update README + ncurses sample.
  2. Re-vendor tui_ncurses.{c,h} into resources/tui_ncurses/.
  3. 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.
  4. src/main.c: wire nt_set_user(g_state.user_name) (empty when not logged in) inside nt_update_app_title(). Add a View log entry to MAIN_ITEMS (before Quit) that calls nt_log_show().
  5. Sweep menu files per §6 categories. Replace nt_print calls one bucket at a time.
  6. Docs:

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 log entry 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_print output being clobbered by menu redraws.
  • Library additions documented and present in aesthetics repo.
  • cd build && cmake .. && make succeeds; static Alpine build still works.