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.
- Declare the shell with panels, tabs, navigation, and action metadata.
- Compose the shell inside your Textual app.
- Route matched actions through app-owned handlers.
- 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”For normal app shells, start with lc.composition.shell.declare(...). The shell
declaration keeps panel structure, content ids, focus navigation, action
behavior, keybar decisions, and Textual binding projection together.
import lazy_cuh as lc
panels = ( lc.composition.shell.panel("files", index=1, content_id="files-tree"), lc.composition.shell.panel("work", index=2, content_id="work-list"),)
shell = lc.composition.shell.declare( panels=panels, actions=( *lc.composition.shell.panel_focus_bindings( panels=panels, actions={ "files": AppAction.FOCUS_FILES, "work": AppAction.FOCUS_WORK, }, ), ), navigation=lc.composition.shell.navigation( *lc.composition.shell.connect("files", lc.view.Direction.RIGHT, "work"), ),)
bindings = shell.textual_bindings()Inside the Textual app, compose the shell against mounted widgets and app-owned handlers:
import lazy_cuh as lc
composition = shell.with_handlers( { AppAction.FOCUS_FILES: focus_files, AppAction.FOCUS_WORK: focus_work, }).compose(app)
composition.runtime.refresh(focus=True)Use lower-level ShellController, ShellViewAdapter, or
ShellRuntime.from_widgets(...) directly only when you want manual control
without the composition 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
import lazy_cuh as lc
class AppAction(Enum): FOCUS_FILES = auto() FOCUS_WORK = auto()
actions = lc.builders.actions.action_map( lc.builders.actions.bind("1", AppAction.FOCUS_FILES, label="Files"), lc.builders.actions.bind("2", AppAction.FOCUS_WORK, label="Work"),)
dispatcher = lc.inputs.ActionDispatcher( lc.builders.actions.shell( { AppAction.FOCUS_FILES: lc.inputs.ShellCommand.FOCUS_1, AppAction.FOCUS_WORK: lc.inputs.ShellCommand.FOCUS_2, }, controller=lambda: runtime.controller, set_controller=runtime.set_controller, ))For tab changes, use lc.builders.actions.tabs(...).
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, use the option builder helpers to keep modal routing and result application consistent:
modal = lc.builders.options.edit_modal_for_item(options, selected_item)if modal is not None: modal_host.open(modal)When options change, refresh the list model with
lc.builders.options.refresh_view_model(...) to preserve cursor position and
view settings.
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.