Skip to content

Architecture Overview

This document describes the high-level architecture of gdbforge: subsystems, boundaries, data flow, and the design principles that govern implementation decisions.

gdbforge is not a clone of Vim. It is a generic application framework inspired by Vim's interaction model. Vim has a single data model (text buffers); this framework supports multiple application-specific data models. The GDB debugger is the first application built on it.

Companion docs: UI_ARCHITECTURE.md · PTY_ARCHITECTURE.md · COMMAND_SYSTEM.md · DEBUGGER_INTEGRATION.md · DIRECTORY_STRUCTURE.md


MVC (current)

The debugger app is organized as Model–View–Controller with a composition root:

One shared model, two peer controllers (GUI *Ctl + MCP/AI), views only for humans. DebuggerApp wires everything; domain logic lives on host-backed controllers (breakCtl, consoleCtl, …). AI tools call the same domain surface as the GUI. A future Lua controller can bind that surface too (must use the PTY write mux). Raw gdb_command remains an escape hatch only.

flowchart TB
    subgraph root ["Composition root"]
        App["DebuggerApp<br/>cmd/gdbforge"]
        WS["Workspace<br/>pane policy"]
        BE["backend.Backend<br/>GDB · Delve"]
    end

    subgraph controllers ["Controllers"]
        GUI["GUI · *Ctl<br/>break · asm · console · …"]
        MCP["MCP / AI · GdbMcpService<br/>peer on same Session"]
    end

    subgraph model ["Shared model"]
        Sess["Session · core.Session"]
        Dom["Domain snapshots<br/>BreakpointList · ThreadList · CallStack · AssemblyList · AppState · …"]
        Surf["gdbforge/domain.DebugDomain"]
    end

    subgraph views ["Views · humans only"]
        W["Widgets<br/>Code · Asm · GDB · Threads · Call Stack · Breakpoints · IO · …"]
    end

    App --> WS
    App --> BE
    App -->|"wires host = a"| GUI
    BE --> Sess
    GUI -->|"Send / Query / Merge"| Sess
    GUI -->|"update"| Dom
    App -->|"implements"| Surf
    MCP -->|"domain tools + gdb_command"| Surf
    MCP -->|"Send / Query / WithWrite"| Sess
    Surf -->|"reads/writes"| Dom

    Dom -->|"SetItems / Paint"| W
    W -->|"Host intents / OnSubmit"| App
    App -->|"forwards"| GUI
Layer Owns Lives in
Composition root Wire hosts, chrome, stop pipeline, layouts DebuggerApp (app.go, facade.go, controllers.go)
Workspace Pane marks, Code/GDB placement, layout apply cmd/gdbforge/workspace*.go above termui.TabWidget
Backend GDB vs Delve policy; owns concrete client internal/gdbforge/backend
Model Session + domain snapshots (on *Ctl, not app fields) breakCtl.list, debugInfoCtl, asmCtl, internal/gdbforge/models
Domain surface Peer ops (list/set BP, threads, frames) for AI / future Lua internal/gdbforge/domain · cmd/gdbforge/debug_domain.go
Controller Intents → mutate model → push paint / Send GUI: *Ctl · MCP/AI: internal/mcp (peer on app.GDB())
View Paint + host intents / callbacks (SetItems, BreakpointHost, SetOnSubmit, …) internal/gdbforge/widgets, internal/termui
View (widget)  --Host / OnSubmit-->  DebuggerApp (forwards)
*Ctl           --Send / Query-->     Model (Session via Backend, BreakpointList, …)
*Ctl           --SetItems / Paint--> View
MCP / AI       --Send / Query-->     same Model  (no widget ownership)

GUI and MCP/AI share the same models (e.g. breaks.listBreakpointList); widgets never own Backend / Session, ptyx.TTY, or domain merge logic. MCP does not paint — views exist for the human TUI only.

Controllers and hosts

Controller Domain Notes
breakCtl Breakpoints, gutter paint (BreakGutter) Code Space + BP list e/d
asmCtl Assembly list/widget, preferAsm / autoAsm :b asm, missing-source swap
bufferCtl Per-path CodeWidget map :b / :edit
debugInfoCtl Threads / call stack Stop refresh
consoleCtl GDB or Delve console Submit / paint / interrupt
inferiorIOCtl Inferior PTY → IO pane
completionCtl / searchCtl / luaCtl / dlvCtl Completion, / search, Lua, Delve confirm/frame sync

List widgets take *DebuggerApp as a host interface (BreakpointHost, ThreadHost, CallStackHost, AssemblyHost, …). Consoles still use SetOn* via wireConsole. Controllers talk to the app through private *Host interfaces (breakHost, consoleHost, …) set in initControllers().

Orthogonal input mini-machines

Global job-control keys are not Mode policy. Three orthogonal mini-machines compose at withGlobalKeys:

Machine Owns Lives in
Mode Keymaps, Esc, : / /, ModeLua platform.Mode + mode handlers
Activity Ctrl-C / Ctrl-Z from inferior + Lua job busy cmd/gdbforge/activity.go
Confirm Ctrl-D quit / y-n gates; confirming interrupt cmd/gdbforge/confirm_router.go + QuitGate / ConfirmGate

See INPUT.md § Dispatch.

What DebugDomain means (naming)

In architecture, domain is the debugger problem space (breakpoints, threads, stack) and its data in internal/gdbforge/models.

The Go type DebugDomain is not “the whole domain” and not “many ways to set a breakpoint.” It is a port / facade: a small menu of domain operations so peer controllers (AI today, Lua later) can call the app without importing DebuggerApp.

Piece Role
models/ (BreakpointList, …) Domain data (shared truth)
domain.DebugDomain interface Domain operations exposed to peers
cmd/gdbforge/debug_domain.go One real implementation (same BP path as GUI Space)
GUI widgets May call app helpers directly; they do not need the interface
AI / future Lua Call through DebugDomain only

There is still one way to place a breakpoint (ToggleInsertClearSend). The interface only adds doors into that path (and allows a fake domain in tests).

C++ analogy: an abstract class / pure virtual API. Architecture labels that fit: port, use-case API, facade. The name DebugDomain means “operations belonging to the debugger domain,” not “interface = domain” in every codebase.


Table of contents


System context

gdbforge runs as a terminal application. It owns the UI event loop, renders into an off-screen grid, and communicates with debugger backends through backend.Backend (-g gdb|dlv). GDB (MI2) and Delve share the same UI controllers via that policy surface.

flowchart LR
    User["Developer"]
    Term["Terminal"]
    gdbforge["gdbforge · TermApp"]
    BE["backend.Backend"]
    GDB["GDB MI2 / Delve"]
    Target["Debug target"]

    User --> Term
    Term <--> gdbforge
    gdbforge --> BE
    BE <-->|"PTY#1 debugger"| GDB
    BE <-->|"PTY#2 stdio"| Target
    GDB -.->|"inferior tty"| Target

GDB and the inferior use separate PTYs: MI on PTY #1, program stdin/stdout on PTY #2 (IO console). Master/slave map, Delve --tty vs TCP headless, and external terminals: PTY_ARCHITECTURE.md. Protocol details: DEBUGGER_INTEGRATION.md.


Application framework

The central idea is that Vim's interaction model maps cleanly onto a broader class of applications — not only text editors.

Vim This framework
Single data model (text buffers) Multiple application-specific data models
Buffers hold file content Models hold domain state (breakpoints, orders, registers, …)
Windows display buffers Widgets display models
:buffer opens a file :buffer displays an application model

Each application defines its own set of models during startup. Examples:

GDB application

  • CodeModel, BreakpointModel, ThreadModel, RegisterModel, MemoryModel, ConsoleModel, LoggerModel

Trader application (hypothetical)

  • OrdersModel, PortfolioModel, WatchlistModel, ChartModel, LoggerModel

MSP application (hypothetical)

  • MSPV2InfoModel, LoggerModel, …

All models are created during application initialization. They live for the entire lifetime of the application, subscribe to application events, and continuously maintain their state.

The same termui framework (split tree, :buffer, :split, :tab) serves all applications; only the models and services differ.


Startup

When an application starts, it creates:

Component Lifetime Role
Services Application Communicate with external systems; produce events
Event bus Application Distributes events to subscribers
Models Application Own state; subscribe to events; update continuously
Logger Application Structured logging infrastructure
Runtime infrastructure Application Event loop, window manager, command line

Widgets are not created at startup. Models exist for the entire lifetime of the application and continuously receive updates from underlying services. The window manager creates widget instances only when the user asks to display a model.


Services

Services communicate with the outside world. They publish events through the event bus and never communicate directly with widgets (target architecture).

Service Application
backend.Backend GDB vs Delve policy — owned by DebuggerApp; wraps gdb.GDBClient or dlv.Client
core.Session (app.GDB()) Shared debugger session (name is historical; works for -g dlv too)
execcli.ExecClient Vim-style :! shell / SSH PTYs — owned by DebuggerApp
mcp.GdbMcpService In-app :AI / tool access to the live Session (app.GDB())
IBKRClient Trader (planned)
MSPV2Client MSP monitoring (planned)

Each application wires its own services during startup. Controllers update domain models and push snapshots to views; MCP is a peer controller on the same Session. Prefer Backend methods over new isDLV() branches.


Application data flow

Application state flows in one direction:

Service
Event Bus
Model
Widget (View)

Widgets never subscribe directly to external services. Models own application state; widgets simply display models.

Examples:

Backend / MCP     →  breakCtl.list (BreakpointList)  →  BreakpointWidget + Code/Asm gutters
Backend           →  consoleCtl                      →  GDBWidget (paint + OnSubmit)
Backend           →  debugInfoCtl (CallStack/Threads) → CallStackWidget / ThreadWidget
Backend           →  asmCtl (AssemblyList)           → AssemblyWidget (when supported)
GdbMcpService     →  same Session (WithWrite + Subscribe)
Layer Responsibility
Backend / Session Talk to external systems; GDB/Delve policy; Send / Subscribe / WithWrite
Event bus Route events to controllers / model refresh
Model Own application state on *Ctl; live for the app lifetime
Controller *Ctl (+ app orchestration) — intents, refresh, sync views
Widget Display a model / console chrome; host intents / callbacks only; no Send

The sections below on terminal input, domain events, and GDB output describe how this flow is wired in the current Go implementation (termui.Event, HandleCoreEvents, ptyx, etc.).


Application models

Models are the source of truth for application state. They are created at startup and live until the application exits.

Property Behavior
Creation Declared and initialized during application startup
Updates Subscribe to the event bus; react to service events
Lifetime Independent of any widget
Sharing Multiple widgets may display the same model simultaneously
OrdersModel
      |
+-----+------+
|            |
OrdersWidget OrdersWidget

A widget's lifetime is independent from its model. Closing a pane destroys the widget, not the model. Opening :buffer orders again creates (or activates) a new widget bound to the existing OrdersModel.

Generic model interfaces

Models are application-specific (BreakpointModel, OrdersModel, MSPV2InfoModel, …), but they expose generic interfaces understood by reusable widgets. Widgets never depend on application-specific model types.

TextWidget   →  TextModel
GraphWidget  →  GraphModel
TableWidget  →  TableModel
TreeWidget   →  TreeModel

The application implements concrete models; the widget depends only on the small interface it needs.


Widget philosophy

Widgets are views. A widget should contain little or no business logic. It receives a model (usually through an interface) and renders it.

Widget Role
LoggerWidget Scrollable log output
GraphWidget Time series, histograms, scatter plots
TableWidget Tabular data
TreeWidget Hierarchical data
TextWidget Line-oriented text

Widgets should be reusable across applications whenever possible. The same TableWidget can display breakpoints in a debugger, orders in a trading app, or MSP telemetry in a monitoring app — as long as the bound model implements TableModel.


Generic widgets

Widgets operate on small interfaces rather than concrete model implementations.

For example, GraphWidget depends on GraphModel. GraphModel represents graph data only — not how it should be drawn. Different applications may implement GraphModel:

Application model Implements
StockChartModel GraphModel
MSPV2InfoModel GraphModel
CPULoadModel GraphModel

The same GraphWidget displays all of them.

Rendering style (line graph, histogram, scatter, etc.) is a responsibility of the widget, not the model. The model provides data; the widget decides how to render it.


Buffer concept

The meaning of :buffer differs from Vim:

Vim This framework
Buffer Text file Named application model

:buffer does not open a file. It creates (or activates) a widget displaying the corresponding model. The model already exists — only the view is created on demand.

GDB application examples:

:buffer code
:buffer breakpoints
:buffer threads
:buffer registers
:buffer memory
:buffer console
:buffer logger

Trader application examples:

:buffer orders
:buffer portfolio
:buffer chart

MSP application examples:

:buffer msp
:buffer logger

Models are created during application startup — not on demand when the user runs :buffer.

See WINDOW_MANAGEMENT.md and INPUT.md.


Why not :attach

The architecture intentionally avoids a runtime attachment mechanism such as:

:attach logger
:attach breakpoints

Such commands would expose the internal dependency graph between services, models, and widgets. The user should not be required to understand that wiring.

The relationship between services, models, and widgets is defined during application startup. Commands should express user intent (:buffer logger) rather than implementation details (:attach logger).

Instead, every application declares its available models during initialization. The user only chooses which model to display — via :buffer, :split, :vsplit, or :tab. All models already exist; the window manager binds widgets to them.


Design philosophy

The framework extends Vim's interaction model rather than copying its implementation.

Vim has one data model: the text buffer. This framework supports many application-defined data models. Each application declares its available models at startup; the user interacts with them using familiar Vim commands.

Vim:

File
Text Buffer
Window

This framework:

Application
Application Models
Widgets (Views)
Window Manager

Internally, :buffer, :split, :vsplit, and :tab create views over existing models instead of opening files. The user still works with familiar Vim concepts, but the underlying objects are no longer limited to text files — they can represent breakpoints, orders, telemetry, or any other domain state.


Platform layer

The Platform package contains reusable infrastructure independent from any specific application.

Component Role
Logger Structured logging
EventBus Event distribution between services and models
Buffer Reusable line-oriented data structure (no UI knowledge)
Lua Scripting and plugin host
SSH Remote access primitives
Runtime utilities Shared helpers used across applications

Platform components do not import terminal or widget packages. Today many of these live in or near internal/core; the target is a dedicated platform layer that applications and TermUI both depend on.

Design decision: Buffer belongs to Platform because it is a reusable data structure with no UI knowledge. Scroll position and cursor visibility are presentation concerns — see TermUI layer.


TermUI layer

TermUI is responsible for presentation: turning model state into terminal output and routing local input.

Component Role
Canvas Local-coordinate drawing context
Grid Off-screen cell framebuffer
Viewport Scroll window, cursor visibility, visible region over a model
Widget View interface (Draw, DrawStatusLine, HandleEvent)
WidgetTree Split-tree geometry + focus
Window manager Tabs, splits, model-to-widget binding

Design decision: Viewport belongs to TermUI because it manages scrolling, cursor visibility, and rendering. Buffer belongs to Platform because it holds data with no presentation logic.

Implementation today: internal/termui (Canvas, Grid, WidgetTree) plus scroll/view helpers still migrating from internal/core.


High-level architecture

flowchart TB
    subgraph Presentation["Presentation · internal/termui"]
        TermApp["TermApp"]
        RootLayout["Root: TabBar / Workspace / CmdLine"]
        SplitTree["Split tree · WidgetTree"]
        Widgets["Widgets: Code, GDB, Cmd, …"]
        Render["Canvas → Grid → tcell"]
    end

    subgraph Application["Application · cmd/gdbforge + internal/gdbforge"]
        DebuggerApp["DebuggerApp · composition root"]
        Ctls["*Ctl · break · asm · console · …"]
        WS["Workspace · pane policy"]
        BE["backend.Backend"]
        AppState["AppState · modes"]
        HandleCore["HandleCoreEvents"]
    end

    subgraph Domain["Domain · internal/core + termui events"]
        Events["termui.Event bus"]
        Buffer["Buffer / Viewport"]
        History["History / Autocomplete · termui"]
        DebuggerIF["Debugger / Session"]
    end

    subgraph Infrastructure["Infrastructure · gdb / dlv / ptyx"]
        Client["GDBClient · dlv.Client · PTY"]
        MI["MI / Delve parse"]
    end

    TermApp --> RootLayout --> SplitTree --> Widgets --> Render
    Widgets --> Application
    DebuggerApp --> Ctls
    DebuggerApp --> WS
    DebuggerApp --> BE
    Application --> Domain
    Domain --> Infrastructure

Source: diagrams/module_boundaries.mermaid


Main subsystems

Subsystem Package Responsibility
Services App layer (cmd/gdbforge, internal/gdb, internal/dlv, …) Communicate with external systems; produce events
Event bus termui.Event channel Distribute events to models and application dispatch
Models internal/gdbforge/models on *Ctl Own application state; controllers push SetItems / paint
Workspace cmd/gdbforge/workspace*.go Pane marks, placement, focus policy, layout apply above Tab
Window manager termui (WidgetTree, TabWidget) Generic layout / focus / splits (no debugger roles)
Terminal application termui.TermApp Event loop, screen init, widget registry, redraw orchestration
Root layout termui (planned RootLayout) Fixed TabBar, flexible Workspace band, fixed CmdLine
Split tree termui.WidgetTree, Node Recursive pane division inside Workspace
Widget layer termui.Widget + gdbforge/widgets Views; host intents / callbacks; no business logic
Rendering Canvas, Grid, Cell Local coordinates, border composition, terminal flush
Domain events termui.Event bus Decouple widgets from app logic; all events → HandleCoreEvents
Text model (legacy) core.Buffer, core.Viewport Scrollable line storage — used today by console/source widgets; target is explicit domain models per pane
CmdLine helpers termui.History, termui.AutoCompleter Command-line UX (no tcell in API surface)
Key sequences termui.Trie Prefix-tree matcher for multi-key bindings
App modes platform.AppState Interaction mode + PTY owner + layout policy (equalalways)
Debugger backend gdbforge/backend, ptyx, gdb / dlv, core.Session Policy surface + MI/Delve PTY + inferior stdio PTY
AI / tools mcp.GdbMcpService Same-process :AI on live Session
Application shell cmd/gdbforge (DebuggerApp + *Ctl) Composition root: UI, Backend, controllers, MCP; modes + HandleCoreEvents

Data flow

Service → model → widget (application layer)

At the application level, state always flows downward:

Service → Event Bus → Data Model → Widget

Widgets display models. Models subscribe to application events. Services never talk to widgets directly. The GDB and terminal sections below describe the current wiring toward this target.

Input → action → redraw

gdbforge uses two parallel event planes:

Plane Type Path
Terminal tcell.Event PollEventTermApp.HandleEventAppApi.HandleKey / HandleResize
Domain termui.Event Any producer → TermApp.events channel → HandleCoreEvents

Widgets handle terminal input locally (keys, cursor). When a widget needs the application to act — submit a : command, quit, forward to GDB — it publishes a termui.Event onto the bus. The main loop drains the channel and forwards every domain event to a single application hook: AppApi.HandleCoreEvents.

sequenceDiagram
    participant Input as Keyboard / Mouse
    participant App as TermApp
    participant Dbg as DebuggerApp
    participant Widget as Widget · Tab / CmdWidget
    participant Bus as termui.Event channel
    participant Core as HandleCoreEvents
    participant Render as Redraw

    Input ->> App: PollEvent · tcell.Event
    App ->> App: HandleEvent(ev)
    App ->> Dbg: HandleKey(ev) · on EventKey
    Dbg ->> Dbg: mode + trie routing
    Dbg ->> Widget: HandleEvent(ev)
    Widget ->> Bus: Events <- SubmitMsg / other termui.Event
    App ->> Bus: drain channel
    Bus ->> Core: AppApi.HandleCoreEvents(ev)
    Core ->> Core: dispatch by CommandID / type
    App ->> Render: Draw → Grid → Screen

Sources: diagrams/event_flow.mermaid · diagrams/event_bus.mermaid

Debugger output → UI

GDB output arrives asynchronously on a ptyx reader goroutine, is fan-out via Subscribe, posted into the tcell event loop as EventInterrupt(GdbOutputMsg), and handled by the app controller (gdb_console.go), which paints the GDB view.

sequenceDiagram
    participant GDB as GDB process
    participant PTY as ptyx reader
    participant Fan as Subscribe fan-out
    participant Screen as tcell.Screen
    participant Ctrl as DebuggerApp controller
    participant Widget as GDBWidget
    participant Cons as ConsolePane

    GDB-->>PTY: MI output chunk
    PTY->>Fan: PtyOutputMsg
    Fan->>Screen: PostEvent GdbOutputMsg
    Screen->>Ctrl: HandleInterrupt
    Ctrl->>Ctrl: GdbInputState.PushRaw → MiUpdate
    Ctrl->>Widget: PaintMiDisplay / AppendLines
    Widget->>Cons: Draw on next frame

Source: diagrams/debugger_integration.mermaid

Layering: InputLine (edit) → ConsolePane (REPL shell) → GDBWidget (view) ← controller owns MI + Session.

End-to-end data flow

flowchart TB
    subgraph Input["Input paths"]
        User["User keyboard / mouse"]
        Async["Async sources · GDB PTY"]
    end

    subgraph TermApp["TermApp event loop"]
        Poll["PollEvent · tcell.Event"]
        Bus["events chan · termui.Event"]
        TermHandler["TermApp.HandleEvent"]
        HandleKey["AppApi.HandleKey"]
        Widgets["TabWidget / CmdWidget HandleEvent"]
        CoreHub["HandleCoreEvents"]
        Draw["Draw pipeline"]
        Screen["Terminal screen"]
    end

    subgraph App["Application layer"]
        Dispatch["Command / event dispatch"]
        Debugger["Debugger backend"]
        Model["Buffer / Viewport / state"]
    end

    User --> Poll
    Async --> Poll
    Poll --> TermHandler
    TermHandler --> HandleKey --> Widgets
    Widgets -->|"publish domain events"| Bus
    Async -.->|"planned: publish"| Bus
    Bus --> CoreHub --> Dispatch
    Dispatch --> Debugger
    Debugger --> Model
    Dispatch --> Model
    Widgets --> Draw
    Draw --> Screen

Source: diagrams/data_flow.mermaid

Design decision: domain events do not fan out to widgets directly. Every termui.Event on the TermApp channel is handled in one place — HandleCoreEvents on the application object (DebuggerApp in cmd/gdbforge/). The app decides whether to exit, talk to GDB, change layout, or push state back into widgets on the next draw.

Typed app notifications use platform.EventBus (Subscribe / Publish) so producers and consumers wire without constructor injection:

Message Publisher Subscriber
CompletionMsg CmdWidget (Tab) CompletionBarWidget
BreakpointsChangedMsg onBreakpointsChanged (MI / MCP / :e) DebuggerApp.onBreakpointsChangedMsg → coalesced -break-list

Breakpoint sync details: DEBUGGER_INTEGRATION.md.

Terminal input routing (modes, trie, widget dispatch) is also centralized in DebuggerApp, keeping TermApp a generic event loop and draw orchestrator.


Design principles

These principles are non-negotiable for gdbforge. They explain many seemingly verbose abstractions (Canvas, WidgetTree, Grid).

# Principle Rationale
1 Widgets should not know screen coordinates Enables layout changes without touching widget code
2 Widgets draw only inside their assigned Rect Prevents bleed-over; simplifies testing
3 Canvas provides local drawing coordinates Single translation point from local to screen space
4 Layout engine owns positioning Centralizes split ratios, resize, and border gutters
5 Rendering backend should be replaceable Grid → tcell today; could swap to alternate terminal libs
6 Business logic lives in models; widgets are views Services update models via events; widgets never call services
7 Widgets depend on generic model interfaces Same GraphWidget works across applications; models stay app-specific
8 TabBar, CmdLine, Workspace are top-level Split tree stays scoped to Workspace only
9 Only Workspace contains the split tree TabBar/CmdLine never participate in recursive splits
10 Debugger backends must not import UI Keeps GDB/OpenOCD/JTAG testable without a terminal
11 Buffer is Platform; Viewport is TermUI Data storage vs scroll/cursor/rendering concerns stay separated

Layer responsibilities

The Platform layer and TermUI layer sections above define the reusable infrastructure vs presentation split. The subsections below map those roles onto today's packages and wiring.

Services

  • Communicate with external systems (backend.Backend → GDB/Delve, IBKRClient, MSPV2Client, SSHClient, …).
  • Publish events on the event bus; never import UI packages; never talk to widgets directly.
  • Example: internal/gdbforge/backend wraps gdb.GDBClient / dlv.Client — PTY I/O, MI2 / Delve parse.

Event bus

  • Distributes events from services and UI producers.
  • Models and application dispatch subscribe here.
  • Implementation: termui.Event channel on TermApp; all events also routed through HandleCoreEvents.

Models

  • Own application state for a domain concern (breakpoints, source, console output, …).
  • Application-specific types (BreakpointList, AssemblyList, …) held by *Ctl controllers.
  • Subscribe to events; update internal data continuously.
  • Exist for the application lifetime; independent of widget lifetime.
  • Current: breakCtl.list, debugInfoCtl threads/stack, asmCtl assembly, bufferCtl.files, plus AppState (source files, location, colors). Controllers sync views via SetItems / paint APIs.
  • Still aspirational: generic TextModel / TableModel interfaces for reusable widgets across apps.

Widgets

  • Display models; list panes use host interfaces (BreakpointHost, …); consoles use SetOn*.
  • Never own business logic; never communicate directly with services / Backend.
  • Created on demand when the user displays a model (:buffer, splits); destroyed when a pane closes.
  • Multiple widgets may bind to the same model.
  • Rendering style (line graph vs histogram, table columns, etc.) is decided by the widget, not the model.

Window manager

  • Manages layout (split tree, tabs).
  • Creates and destroys widget instances.
  • Binds widgets to existing models.
  • Implementation: termui.WidgetTree, TabWidget; gdbforge Workspace for pane marks / sticky GDB / layout apply.

Presentation (internal/termui)

See TermUI layer. Owns:

  • tcell.Screen lifecycle.
  • Canvas, Grid, WidgetTree, Viewport (target).
  • Top-level widget registration (today: flat list; target: structured Root).
  • Poll/draw loop.
  • Must not parse GDB MI records directly — delegates to app widgets that use internal/gdb.

Application (cmd/gdbforge + internal/gdbforge)

  • Declares available models and services at startup.
  • DebuggerApp embeds termui.TermApp, implements AppApi, and is the composition root:
  • HandleCoreEvents — single dispatch hub for domain events.
  • AppState (platform.AppState via TermApp.State()) — interaction mode, PTY write owner (ui/mcp), layout policy (equalalways).
  • keyBindings — multi-key chords (InitKeyBindings, SearchPartial in normal mode).
  • ws *Workspace — pane policy; owns TabWidget; CmdWidget for command-line routing.
  • backendbackend.Backend (GDB or Delve); app.GDB() exposes core.Session; widgets never hold it.
  • *Ctl controllers — own domain models (breaks.list, debugInfo, asm, bufs.files, …).
  • gdbMcpGdbMcpService peer on app.GDB() (does not own Close of the session).
  • Defines app-specific CommandID values (break, continue, quit, …).
  • ModeNormal / ModeCommand are wired; focus and search modes are reserved.

Domain (internal/core + termui event types)

See Platform layer. Today internal/core holds platform primitives migrating toward a dedicated platform package:

  • termui.Event bus typesEvent, CommandEvent, SubmitMsg (internal/termui/event.go, command.go).
  • core PTY / UI eventsPtyOutputMsg, GdbOutputMsg, ExecOutputMsg, InferiorOutputMsg (internal/core/events.go).
  • CommandID — infra constant CmdUnknown in termui; app-specific command IDs live in cmd/gdbforge.
  • Buffer — line-oriented storage (Platform; no UI knowledge).
  • History, AutoCompleter for command-line UX (termui).
  • Debugger / Session / PTYWriter — send API, exclusive write, shared Subscribe.

Infrastructure (internal/ptyx, internal/gdb, internal/mcp)

  • ptyx.Client — process PTY (GDB / exec): exclusive WithWrite, Subscribe fan-out, SetSize, Close.
  • ptyx.TTY — bare master/slave PTY for inferior stdin/stdout (OpenTTY, SlaveName).
  • gdb.GDBClient — embeds *ptyx.Client, owns *ptyx.TTY, sends -inferior-tty-set at startup.
  • mcp.GdbMcpServiceGdbCommand + in-app LLM agent on core.Session.
  • MI parsing: MiMsg, GdbInputState in internal/gdb.

Dependency rule: termuicoregdb / ptyx / mcp. Never gdbtermui.


Core events layer

The event bus decouples UI widgets from application logic. Any subsystem may publish a termui.Event; the main loop delivers every event to HandleCoreEvents on the application object. Widgets stay thin — they parse local input and emit domain events; the app owns side effects.

flowchart TB
    subgraph Producers["Event producers (any subsystem)"]
        CmdW["CmdWidget"]
        GDB["GDB backend / goroutines"]
        Widgets["Other widgets"]
        Future["Plugins · planned"]
    end

    subgraph Bus["termui event bus"]
        Chan["TermApp.events chan termui.Event"]
    end

    subgraph Loop["TermApp.Run main loop"]
        Select["select: channel vs PollEvent"]
        CoreDispatch["AppApi.HandleCoreEvents(ev)"]
    end

    subgraph App["Application · DebuggerApp"]
        Handler["HandleCoreEvents — single dispatch hub"]
        CmdSwitch["switch CommandID / event type"]
        Modes["AppState · mode router"]
        TrieNode["Trie · key sequences"]
    end

    CmdW -->|"Events <- SubmitMsg"| Chan
    GDB -.->|"planned"| Chan
    Widgets -.-> Chan
    Chan --> Select --> CoreDispatch --> Handler --> CmdSwitch

Source: diagrams/event_bus.mermaid

Mode and key-sequence routing happen in DebuggerApp.HandleKey before widgets see terminal keys — see INPUT.md.

Interfaces and types

classDiagram
    direction TB

    class Event {
        <<interface>>
        +Type() string
    }

    class CommandEvent {
        <<interface>>
        +CommandID() CommandID
    }

    class SubmitMsg {
        +Text string
        +CmdID CommandID
        +Args string
    }

    class PtyOutputMsg {
        +Data string
        +Err error
    }

    class GdbOutputMsg {
        +Data string
        +Err error
    }

    class Quit {
        +Text string
    }

    Event <|.. SubmitMsg
    Event <|.. PtyOutputMsg
    Event <|.. GdbOutputMsg
    Event <|.. Quit
    CommandEvent <|.. SubmitMsg
Type Purpose
Event Base domain event — identified by Type() string
CommandEvent Events carrying a resolved CommandID (e.g. after : command entry)
SubmitMsg CmdLine submitted — Text, CmdID, Args
PtyOutputMsg Raw PTY chunk from Session.Subscribe (GDB/exec/MCP)
GdbOutputMsg UI-routed GDB chunk (EventInterrupt → GDBWidget)
InferiorOutputMsg UI-routed inferior PTY chunk (EventInterrupt → IO / OutputWidget)

Command IDs and colon commands

Colon commands use a hierarchical command tree (internal/commands). See COMMAND_SYSTEM.md for ownership (CommandNode / CommandRegistry / CommandParser), the DSL, and tab completion.

Layer Owns
commands.CommandNode Tree nodes — Name, Children, Action
commands.CommandRegistry Root tree + key-binding trie
commands.CommandParser Runtime cursor — current, token, path
termui.CmdWidget : input + parser for Tab sync; SetOnExecute → app runs ExecuteParsed()

Legacy termui.CommandID / SubmitMsg remain for infra events (CmdExitMode, CmdUnknown). Tree leaf commands execute via app-owned ExecuteParsed()CommandNode.Action.

Wiring (current)

// cmd/gdbforge/setup.go
a.commandReg = commands.NewCommandRegistry()
a.ExapData()  // cmd/gdbforge/command_tree.go

a.cmdWidget = termui.NewCmdWidget(a.commandReg)
a.cmdWidget.Ctx = a.ctx
a.completionBar = termui.NewCompletionBarWidget(a.ctx) // Subscribes to CompletionMsg

Implementation: internal/commands/, internal/termui/cmd_widget.go, internal/platform/event_bus.go, cmd/gdbforge/.


Current vs target architecture

The debugger app follows MVC today (see MVC (current)). Remaining gaps are mostly generic framework polish, not session-in-widget ownership.

Component Target Current state
Application models Explicit model per domain; created at startup Done — on *Ctl (BP / threads / call stack / assembly / buffers); AppState for files/location
Generic model interfaces Widgets bind via TextModel, GraphModel, TableModel, … Not yet — widgets use concrete DTOs / host ifaces
Model → widget binding :buffer activates widget for existing model Partial — builtins + :b / :e; models on controllers
Backend → controller → model → view Debugger events update models; widgets paint snapshots Donebackend.Backend + *Ctl; views are hosts / Set*
Composition root Thin app wires controllers DoneDebuggerApp + host-backed *Ctl + Workspace
Platform layer Buffer, EventBus, Logger in platform package Partial — primitives in internal/core / platform
Viewport ownership Viewport in TermUI; Buffer in Platform Partial — both migrating
Root layout Tab + CompletionBar + CmdLine Flat AddWidget list; HandleResize assigns rects
TabBar Multi-tab with header render TabWidget — single tab, no header
Workspace Split tree + app pane policy WidgetTree / TabWidget + gdbforge Workspace
CmdLine Global : command input CmdWidget; Execute via app (SetOnExecute)
Event bus termui.EventHandleCoreEvents Channel on TermApp; CmdWidget wired
Key chords Configurable multi-key sequences Trie on DebuggerApp; Ctrl+W focus chords
Interaction modes Mode + Activity + Confirm mini-machines Done — Mode via AppState; Activity activity.go; Confirm confirm_router.go
Rendering Diff-based grid flush PartialBackCells diff in Grid.Draw
Focus Mode-aware routing WidgetTree.focus + trie focus movement
Split commands :vs, :split Partial — wired in HandleCoreEvents
Debugger App-owned Session via Backend; MCP peer; view-only consoles Working — GDB + Delve (-g); DEBUGGER_INTEGRATION.md

Entry point: cmd/gdbforge/ (main.go + app.go, setup.go, …).

Detailed tracker: ROADMAP.md.


Topic Document
Widgets, canvas, grid UI_ARCHITECTURE.md
Splits, tabs, command line WINDOW_MANAGEMENT.md
Cells, borders, Unicode RENDERING.md
Keyboard, modes INPUT.md
Command tree, DSL, parser COMMAND_SYSTEM.md
PTY master/slave, IO, external tty PTY_ARCHITECTURE.md
GDB MI2 / Delve details DEBUGGER_INTEGRATION.md
Package map DIRECTORY_STRUCTURE.md