Skip to content

App Shell

ShellState keeps panel focus and active tabs outside Textual widgets. ShellController combines that state with shell command key handling and line-number mode for apps that want a higher-level pure shell model. ShellViewAdapter is the Textual runtime adapter that applies a controller to panel containers, tab content widgets, and an optional keybar widget. ShellRuntime can sit above the adapter when an app also wants input resolver context and expanded keybar help to follow the focused panel.

Panel and shell specs validate setup errors early. Panel IDs, panel indexes, and tab IDs must be unique. Focus graph edges must reference existing panels. These checks belong in the pure view layer so invalid app layout fails before Textual widgets are mounted.

from lazy_cuh import Direction, FocusGraph, LineNumberMode, PanelSpec, ShellController, ShellState, TabSpec
left = PanelSpec(id="left", index=1, tabs=(TabSpec("channels", "Channels"),))
right = PanelSpec(
id="right",
index=2,
tabs=(TabSpec("details", "Details"), TabSpec("profile", "Profile")),
)
shell = ShellState.from_panels(
(left, right),
focus_graph=FocusGraph(edges={("left", Direction.RIGHT): "right"}),
)
shell = shell.focus_direction(Direction.RIGHT)
shell, change = shell.cycle_tab(right, 1)
controller = ShellController.from_panels(
(left, right),
focus_graph=FocusGraph(edges={("left", Direction.RIGHT): "right"}),
line_number_mode=LineNumberMode.RELATIVE,
)
result = controller.handle_shell_key("z")
result = result.controller.handle_shell_key("2")
controller = result.controller

Apps that route action IDs through ActionDispatcher can use shell_command_handlers and shell_tab_handlers to build the common controller-update handlers without repeating focus and tab lambdas.

Textual app code should use this state to decide which panel receives focus and which tab content is visible.

from lazy_cuh.widgets import ContentWidgetSpec, PanelWidgetSpec, ShellViewAdapter
shell_view = ShellViewAdapter(
app,
panels=(
PanelWidgetSpec(left, "left-panel"),
PanelWidgetSpec(right, "right-panel"),
),
content=(
ContentWidgetSpec("left", "channels", "channels"),
ContentWidgetSpec("right", "details", "details"),
ContentWidgetSpec("right", "profile", "profile"),
),
)
shell_view.refresh(controller, focus=True)

If your app uses lazy-cuh input bindings and contextual keybar help, create a runtime from the same widget specs:

from lazy_cuh import BindingRegistry
from lazy_cuh.widgets import ContentWidgetSpec, PanelWidgetSpec, ShellRuntime
runtime = ShellRuntime.from_widgets(
app,
controller=controller,
panels=(
PanelWidgetSpec(left, "left-panel"),
PanelWidgetSpec(right, "right-panel"),
),
content=(
ContentWidgetSpec("left", "channels", "channels"),
ContentWidgetSpec("right", "details", "details"),
ContentWidgetSpec("right", "profile", "profile"),
),
registry=BindingRegistry(),
)
runtime.refresh(focus=True)

The runtime is still a Textual-boundary object. Keep app state changes in your own controller or command handlers, then pass the updated ShellController to runtime.set_controller(...).