Skip to content

Architecture

jiranon khemklad edited this page Jul 24, 2026 · 1 revision

Architecture

AI assistants: stop and read CLAUDE.md (and its pointers — AGENTS.md, .cursorrules, .github/copilot-instructions.md) before editing anything in this repo. This page is the human-facing summary; CLAUDE.md is the authoritative, exhaustive version and is kept in sync with every change by convention — if the two ever disagree, CLAUDE.md wins.

The shape of the app

Mongoterm is a full-screen neo-blessed TUI with no component re-render cycle. Each panel is a factory function that builds a blessed widget once, wires its own key/event handlers, and gets appended to the screen a single time by MognoTermLayout. From then on, panels mutate their own widgets directly (box.setContent(...), table.setData(...)) and call appInstance.renderScreen() to flush — there's no virtual DOM, no diffing, no subscriptions.

src/
  app.ts            entry point — bootstraps config, builds MongoTermApp
  core/              screen ownership, global keybindings, event bus, theme
  layout/            constructs all panels once, appends to screen
  panels/            one file per widget (tree, workspace, query, modal, shell, ...)
  services/          EVENTS enum, Mongo driver glue, connection I/O, query history
  shared/            state.ts — one mutable object, not a reactive store
  config/            theme + cross-platform filesystem paths

State: one mutable object, not a store

src/shared/state.ts holds everything — selected connection/db/collection indices, the live mongoClient, pagination, sort. There's no subscription model; modules just import state and read/write it directly. This is why panels have to explicitly call renderScreen() after mutating state — nothing re-renders automatically.

The event bus: how mutations actually happen

Panels never call MongoDB directly for anything beyond simple reads through state. Every mutation goes through one path:

  1. A panel emits an event from EVENTS (src/services/enum.ts) — e.g. eventBus.emit(EVENTS.RECORD_UPDATE, { updated, query }).
  2. EventMongoService (src/services/mongodb/mongodb.events.ts) has the matching eventBus.on(...) handler, which calls into MongodbRepository for the actual driver call, then emits a follow-up event — usually QUERY_SEND to refresh the visible results, or TOAST_SHOW for user feedback.

Adding a new mutation always means: new EVENTS entry → emit it from the panel → handle it in mongodb.events.ts → implement the driver call in mongodb.repository.ts. Never skip straight to the repository from a panel.

One sharp edge here: those event handlers are async with no surrounding try/catch, and Node kills the whole process on an unhandled rejection. Any repository method reachable before a collection/db is guaranteed selected needs its own guard (see fetchQuery's no-db/no-collection check) — the UI won't always prevent that state for you.

Keybindings: two layers that don't know about each other

  1. Global/screen-level (src/core/keybindings.ts) — bound once on screen, dispatched by a condition() check (usually "is this panel focused?"). Used for cross-panel navigation.
  2. Per-widget (widget.key([...], handler) inside a panel factory) — only fires while that exact widget has focus.

Both layers can fire on the same keypress — a global handler can move focus away from a widget, and that widget's own .key() binding still fires afterward using the pre-move focus reference. This is real, observed, and intentional (e.g. pressing l on the tree both expands the node and moves focus to the workspace in one keypress) — not a bug to "fix" unless a specific case is reported.

keybindbar.config.ts is the single source of truth for every keybind shown to the user — it drives both the bottom status bar and the ? help popup. Never hand-edit help.panel.ts's content directly.

Known neo-blessed traps (the short version — see CLAUDE.md for the full list with fixes)

  • Textbox/Textarea never implemented cursor-aware editing natively — cursorInput.service.ts patches this in. Any new free-text input should use it.
  • A text input with keys: true but no inputOnFocus and no immediate .readInput() call will silently launch $EDITOR on the letter "e".
  • Percentage-based ("N%-M") layout only works if every panel agrees on the same fixed-height bands (header/footer row counts) — hand-tuned percentages drift on resize.
  • Removing a modal overlay only removes the elements you explicitly hand to removeScreenElement — remove every direct child, not just the top-level box, or you get ghost renders.

Where to go next