Architecture
lazy-cuh is a reusable framework for building lazygit-style TUIs on top of
Textual.
The project focuses on reusable terminal UI architecture: app-specific behavior should stay outside the framework, while generic terminal UI concerns become reusable primitives.
The framework should make it straightforward to build TUIs with:
- bordered panels with numbered titles and tabs
- terminal-palette styling that works across themes
- vim-like item navigation
- clear keybars and modal flows
- reusable list/tree content views
- consistent line numbers, scrolling, highlighting, and wrapping
- explicit action handling instead of ad hoc key handlers everywhere
The framework should remain pragmatic. Do not introduce generic abstractions unless they remove duplication, clarify ownership, or move reusable behavior out of application-specific code.
Architecture Rules
Section titled “Architecture Rules”- Keep domain state out of widgets.
- Keep Textual lifecycle details out of domain/controllers.
- Prefer composition over deep widget inheritance.
- Use typed events/messages for widget-to-app communication.
- Use commands for user intentions with side effects.
- Keep renderers pure where possible.
- Make ownership explicit before adding behavior.
When adding a behavior, decide which layer owns it:
- item data
- item rendering/layout
- navigation
- viewport/scrolling
- actions/commands
- panel/tab composition
- app/domain controller
If behavior applies to both lists and trees, it should not live in a settings-specific widget or a channel-specific tree.
Runtime Boundary: Textual
Section titled “Runtime Boundary: Textual”Textual is the runtime adapter. Use it where it is strong:
AppWidgetScrollViewMessageBinding- focus and lifecycle
- workers
- terminal rendering primitives
Textual should not become the whole app architecture.
Do not use Textual as:
- the domain state container
- the action model
- the item model
- the layout architecture
- the only event vocabulary
Framework and app state should mostly be plain Python dataclasses, enums, and controllers. Textual widgets adapt those models to the terminal and emit typed messages upward.
Vocabulary
Section titled “Vocabulary”Use these terms consistently.
A bordered screen region with one or more tabs. A panel with only one visible view is still a one-tab panel; it should use the same tab/title rendering path as multi-tab panels so focus and active styling stay consistent.
Panels own:
- title rendering
- focus styling
- tab title rendering
- the content widget for the active tab
Panels should not own domain logic.
A named view inside a panel.
Examples:
ChannelsRun LogDetailsProfileSettings
Tabs choose which content widget is visible. They should not implement navigation or item rendering directly.
A semantic object the user can navigate to or act on.
Examples:
- a channel
- a settings field
- a run-log event
- a tree parent/category
An item is not the same thing as a terminal row. One item may render as one line or multiple lines.
Row / Line
Section titled “Row / Line”A rendered terminal line.
Rows are a rendering detail. Navigation should normally move by item, not by visual row, even if an item wraps to multiple rows.
Action
Section titled “Action”A user intent inside the TUI.
Examples:
- edit
- unset
- expand
- select
- save
- preview
- run
Actions are not necessarily domain effects. For example, edit may open a
modal; run may first require confirmation.
Key Sequence / Prefix / Count
Section titled “Key Sequence / Prefix / Count”A key sequence is a fixed ordered set of keys, such as g g, z h, or ?.
A prefix is a named, configurable sequence namespace, such as a pane prefix
that expands to z. A prefixed sequence such as <pane> h can therefore render
or resolve as z h, , h, or another configured prefix.
Counts are separate from sequences. 10j should be interpreted as count 10
plus motion j, not as a hardcoded key sequence.
Binding registries validate scoped keybindings before runtime resolution. They should catch duplicate sequences, unintended global/local overrides, ambiguous prefixes, unknown configured prefixes, and bare digit bindings that would conflict with count prefixes. Local overrides should be explicit. Validation is an opt-in function call, suitable for tests, CI, setup checks, or development mode.
Aliases for the same action should be represented as multiple sequences on one
binding, such as h and left both resolving to the same motion. Those aliases
are not collisions.
Key handling profiles decide which rules are active in a scope. Counts are a profile capability, not a framework-wide assumption. List/tree widgets may use a Vim-like profile with counts enabled, while dashboards, modals, or panel shortcuts may use simple numeric keys with counts disabled.
Input resolvers consume the active scoped bindings and profile. They should return pending, matched, or unmatched results instead of executing behavior directly. Side effects still belong to actions and commands.
Command
Section titled “Command”A validated app/domain effect triggered by an action.
Examples:
SaveProfileReloadChannelsStartPreviewStartRunEditSettingUnsetSetting
Commands are where locking, validation, confirmation, worker startup, and error handling should converge.
Package Layout
Section titled “Package Layout”Current layout:
lazy_cuh/ core/ framework models, typed events, and commands input/ actions, bindings, and navigation state machines keybar/ keybar models and renderers layout/ item rendering, wrapping, and line production modals/ modal models, flows, and Textual adapters styles/ terminal palette and theme helpers viewport/ item-to-visual-line mapping and scroll state view/ panel and tab composition models widgets/ Textual runtime adaptersThe intended boundary is:
- pure model modules should avoid Textual imports
- widget adapter modules may import Textual
- application-specific domain behavior should live in downstream apps
- examples and dogfood integrations should be separate from framework core
- compatibility modules may re-export stable names, but new implementation code should live under the responsibility-focused packages
This public surface is the lazy-cuh API. Keep it explicit and documented so downstream apps can depend on stable concepts instead of copying widget internals.
Patterns We Use
Section titled “Patterns We Use”Component Composition
Section titled “Component Composition”Primary pattern.
Use composition for:
- panels
- tabs
- content views
- item renderers
- navigation controllers
- viewport controllers
- action maps
Avoid making every behavior a subclass.
Target shape:
Panel Tab ContentView ItemSource ItemRenderer NavigationController ViewportController ActionMapMVC-ish / MVU-ish Split
Section titled “MVC-ish / MVU-ish Split”Use a practical split rather than strict textbook MVC.
- Model: controller state and domain data
- View: Textual widgets plus pure renderers/layout
- Update: app/controller handlers for events/actions/commands
This keeps Textual widgets from becoming domain objects.
Command Pattern
Section titled “Command Pattern”Use commands for user intentions that may cause side effects.
Good command candidates:
- save profile
- reload data
- start preview
- start real run
- edit setting
- unset setting
Commands give us one place for:
- locks
- validation
- confirmation
- worker startup
- status/error reporting
Observer / Event Pattern
Section titled “Observer / Event Pattern”Use typed Textual messages for widget-to-app communication.
Examples:
ItemHighlightedItemSelectedActionRequestedTabChangedModalConfirmedModalCancelled
Do not introduce a global event bus unless there is a clear need. Textual’s message bubbling is enough for now.
Strategy Pattern
Section titled “Strategy Pattern”Use strategies for interchangeable behavior.
Good strategy candidates:
- truncation vs wrapping vs explicit multiline layout
- item renderer selection
- redaction policy
- selection policy
- key/action mapping
This is especially useful for making lists and trees share behavior without forcing them into a deep inheritance hierarchy.
Adapter Pattern
Section titled “Adapter Pattern”Use adapters around Textual-specific APIs.
Framework concepts should be plain Python where possible. Textual widgets should adapt those concepts into the runtime.
Example direction:
Framework concept: Navigable item viewTextual adapter: Textual ScrollView widgetWe do not need a perfect split immediately, but new reusable concepts should not require importing Textual unless they are actual widgets.
State Machines
Section titled “State Machines”Use small state machines only for lifecycle-heavy features.
Good fits:
- run lifecycle: idle / previewing / running / finished / failed
- modal lifecycle: closed / input / confirm / info
- selection mode: normal / visual
Do not turn every object into a state machine.
Builder / Factory Specs
Section titled “Builder / Factory Specs”Use declarative specs for panels, tabs, keybars, and content construction.
Current examples:
PanelSpecTabSpec- keybar specs
This direction lets future apps define layout and actions without copying application-specific wiring.
Patterns We Avoid
Section titled “Patterns We Avoid”Avoid these unless there is a concrete reason:
- global event bus
- full Redux clone
- service locator
- domain models embedded in Textual widgets
- large inheritance hierarchies
- generic abstractions that only have one app-specific use
- raw strings for state where enums make ownership clearer
Event And Command Flow
Section titled “Event And Command Flow”Target flow:
keypress -> Textual widget receives key -> ActionMap maps key to ActionId -> widget emits ActionRequested(action, item_id) -> app/controller validates intent -> controller runs command or updates state -> render/layout builds new view data -> Textual widget receives updated itemsWidgets should generally mutate only their own local UI state:
- cursor/highlight
- scroll offset
- local visual mode
- expanded/collapsed view state if no domain state is affected
App/domain state changes should go through controllers or commands.
Core Layers
Section titled “Core Layers”1. Item Data
Section titled “1. Item Data”Defines what exists semantically.
Item data may include:
- stable ID
- label or structured content
- disabled/selectable state
- metadata
- action capability flags
Item data should not know terminal width, cursor position, wrapping, keybindings, or panel focus.
Current primitives:
NavigableItemKeyValueItem
2. Item Rendering And Layout
Section titled “2. Item Rendering And Layout”Turns items into terminal lines.
Rendering/layout owns:
- wrapping or truncation
- indentation
- highlight styles
- disabled/selected/partial styles
- line-number gutter interaction
- composing independently styled row fragments without style bleed
- mapping one item to one or more visual lines
Target output shape:
RenderedItem( item_id="keep_within", lines=[Text("keep_within 2w")],)Long term, list and tree views should use the same item rendering model.
3. Navigation
Section titled “3. Navigation”Moves through semantic items.
Navigation owns:
- current item index
- enabled/selectable indexes
- vim counts
j,k,gg,Gctrl+d,ctrl+u- scroll margin policy
Navigation should not care whether one item renders to one line or five lines.
Current primitive:
ListNavigationState
4. Viewport
Section titled “4. Viewport”Maps rendered visual lines to the terminal viewport.
Viewport behavior owns:
- vertical scroll position
- item-to-line ranges
- visual-line-to-item lookup
- ensuring the cursor item remains visible
- line-number width
- horizontal scrolling policy
Default policy:
- vertical scrolling enabled
- horizontal scrolling disabled
- long content truncated or wrapped by the renderer
5. Actions
Section titled “5. Actions”Defines what a focused item, tab, or panel can do.
Examples:
- select/toggle
- expand/collapse
- edit
- unset
- preview
- run
- save
- reload data
Actions should become declarative enough that a content view can define its keybindings without baking them into a large app class.
Example target shape:
ActionMap( enter="edit", u="unset", space=None,)A navigable list should own:
- item selection/highlighting
- vim-style movement
- line-number rendering
- vertical scrolling
- item selected/highlighted messages
It should not own:
- settings-specific field formatting
- channel-specific selection states
- run-log-specific event formatting
Application code should provide item data and action handlers. The framework should provide navigation, viewport, rendering/layout, and event plumbing.
A tree should eventually be modeled as a specialized navigable list of visible tree items:
TreeModel -> flattened visible TreeItem list -> NavigableItemViewThat would let trees and lists share:
- line numbers
- cursor movement
- viewport behavior
- wrapping/truncation
- search
- visual selection
Textual Tree may still be useful as an adapter while the framework matures,
but reusable logic should not depend on Textual Tree internals.
Wrapping And Multiline Items
Section titled “Wrapping And Multiline Items”Horizontal scrolling is disabled by default.
The next framework step should be a shared item layout layer that supports:
truncatewrap- explicit multiline content
Important rule:
Navigation remains item-based. If one item renders to three visual lines, j
moves to the next item, not to the next wrapped line.
The viewport must maintain mappings:
item index -> visual line rangevisual line -> item indexHighlighting should apply to all visual lines belonging to the focused item.
Styling Principles
Section titled “Styling Principles”The toolkit should preserve the lazygit-inspired style:
- rounded panel borders
- green focused panel titles
- blue keybar text
- terminal palette colors where possible
- no hard dependency on one custom color scheme
- focused rows should remain readable
- value colors should not be destroyed by cursor highlight
Use semantic styles where possible rather than hard-coded domain colors.
Refactor Roadmap
Section titled “Refactor Roadmap”Recommended next steps:
- Strengthen option editing, modal flows, and focused examples.
- Keep action maps, binding registries, and keybar projection aligned.
- Improve shared list/tree rendering around wrapping, line numbers, and scroll policy.
- Grow declarative panel/tab/action composition without hiding Textual.
- Dogfood the framework in real applications.
- Stabilize public APIs only after real integration feedback.