App Structure
Use lazy-cuh as a set of small layers. Keep app state in your app, keep shell state in pure models, and let Textual widgets adapt those models to the terminal.
Recommended Order
Section titled “Recommended Order”- Define item models and option sets.
- Define panel and tab specs.
- Create a
ShellController. - Create
ActionMapbindings and aBindingRegistry. - Create
ShellRuntime.from_widgets(...)inside your Textual app. - Route matched actions through
ActionDispatcher. - Handle widget messages by updating app state and refreshing models.
This order keeps the dependency direction clear: app state feeds models, models feed widgets, widgets emit messages, and action handlers update app state again.
Shell Setup
Section titled “Shell Setup”Define panel specs before mounting widgets. Specs are plain data and validate layout mistakes early.
from lazy_cuh import Direction, FocusGraph, PanelSpec, ShellController, TabSpec
left = PanelSpec(id="files", index=1, tabs=(TabSpec("files", "Files"),))right = PanelSpec(id="work", index=2, tabs=(TabSpec("list", "List"),))
controller = ShellController.from_panels( (left, right), focus_graph=FocusGraph(edges={("files", Direction.RIGHT): "work"}),)At the Textual boundary, connect those specs to mounted widget IDs:
from lazy_cuh import BindingRegistryfrom lazy_cuh.widgets import ContentWidgetSpec, PanelWidgetSpec, ShellRuntime
runtime = ShellRuntime.from_widgets( app, controller=controller, panels=( PanelWidgetSpec(left, "files-panel"), PanelWidgetSpec(right, "work-panel"), ), content=( ContentWidgetSpec("files", "files", "files-tree"), ContentWidgetSpec("work", "list", "work-list"), ), registry=BindingRegistry(),)Use ShellViewAdapter directly only when you want manual control without the
runtime input and keybar helpers.
Actions
Section titled “Actions”Use ActionMap as metadata and ActionDispatcher as execution. Bindings stay
renderable and testable; handlers stay app-owned.
from enum import Enum, auto
from lazy_cuh import ActionBinding, ActionDispatcher, ActionMap, ShellCommandfrom lazy_cuh import shell_command_handlers
class AppAction(Enum): FOCUS_FILES = auto() FOCUS_WORK = auto()
actions = ActionMap( ( ActionBinding.from_key("1", AppAction.FOCUS_FILES, label="Files"), ActionBinding.from_key("2", AppAction.FOCUS_WORK, label="Work"), ))
dispatcher = ActionDispatcher( shell_command_handlers( { AppAction.FOCUS_FILES: ShellCommand.FOCUS_1, AppAction.FOCUS_WORK: ShellCommand.FOCUS_2, }, controller=lambda: runtime.controller, set_controller=runtime.set_controller, ))For tab changes, use shell_tab_handlers(...).
Widget Messages
Section titled “Widget Messages”Widgets should emit messages upward; they should not mutate app domain state.
Typical handlers:
ItemHighlightedMessage: update a details text model.ItemSelectedMessage: open a modal, expand a tree node, or dispatch an app action.ModalResultMessage: apply the modal result to app state, then refresh affected models.
For editable options, OptionSet owns the modal routing:
modal = options.input_modal_for_item(selected_item)if modal is not None: modal_host.open(modal)When options change, preserve list cursor position with ListViewModel.with_items(...).
What To Avoid
Section titled “What To Avoid”- Do not store domain state in Textual widgets.
- Do not make widgets parse app-specific result IDs.
- Do not duplicate list/tree rendering in app code.
- Do not create a new abstraction just to hide five clear lines of setup.
If a pattern repeats across examples and keeps the same ownership boundaries, extract it into lazy-cuh. If it needs app-specific decisions, keep it in the app.