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.controllerApps 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 BindingRegistryfrom 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(...).