cape.tui.tuiapp: Textual app for the CAPE terminal UI

This module contains the OpenCode-style terminal user interface to CAPE, built on the optional textual package. It provides, in one persistent app with a scroll log and a pinned command composer:

  • In-process CAPE commands

    Commands starting with cape, pycart, pyfun, etc. run in-process using cape.cfdx.cli.main() in a worker thread, with their output streamed into the scroll log.

  • External commands

    Any other command runs as a subprocess in its own process group; its combined output is streamed into the scroll log.

  • Tab completion

    Pressing TAB in the editor completes the current word using the same cape.ui.promptutils.CfdxCompleter logic as cape.ui, plus TUI meta-commands. Multiple matches appear in a selectable box immediately above the editor.

  • Ctrl-C / ESC interrupts

    Pressing Ctrl-C (or ESC) while a command is running interrupts it. External commands receive SIGINT in their process group; in-process CAPE commands get a KeyboardInterrupt injected into their worker thread. With nothing running, Ctrl-C clears the editor.

  • Prompt bridge

    Interactive prompts raised by commands (via cape.promptutils.prompt_color()) mount as clickable widgets in the stream. Answering with Ctrl-C aborts the command.

  • TUI meta-commands

    Commands starting with : are handled by the TUI itself: :help [<cmd>], :history [<n>], :!N, :status, :cd <dir> (plain cd also works), :pwd, :clear, and :exit / :quit.

  • OpenCode-style chrome

    Dark command composer, context and command discovery below the prompt, a live spinner while a command runs, and backgrounded command bubbles in the scroll log. Ctrl-P opens the command picker; clicking a blue command chevron folds or expands its output.

  • Text selection

    Dragging the mouse over log or editor text highlights it, and releasing copies the selection to the system clipboard (OSC 52), like OpenCode.

History (loaded from and saved to the CAPE TUI history file) is recalled with the up/down arrow keys.

class cape.tui.tuiapp.CapeTuiApp(cls: type, *a, histfile: str | None = None, **kw)

OpenCode-style CAPE terminal user interface

A scroll log plus a pinned command composer. Commands run in worker threads: CAPE CLI commands in-process with redirected sys.stdout/sys.stderr, other commands as subprocesses in their own process group. Interactive prompts mount clickable widgets above the editor while the worker waits.

Attributes:
_log: textual.widgets.RichLog

Scroll log of commands and output

_input: ComposerTextArea

Command editor

_status: textual.widgets.Static

One-line status bar below the composer

_prompt_widget: None | textual.widget.Widget

Currently mounted interactive prompt, if any

_completer: None | CapeTuiCompleter

Tab-completer for the editor

_proc: None | subprocess.Popen

Currently running external command, if any

_worker: None | threading.Thread

Currently running command’s worker thread, if any

_running_cmd: None | str

Text of the command currently running, if any

_histfile: None | str

Name of the command history file

_json_files: tuple[str]

Successfully loaded JSON files, least to most recently used

_last_json_file: None | str

Most recently used JSON file

_last_json_display_file: None | str

Most recent JSON file relative to its controller’s root

_history: list[str]

Session command history (oldest first)

_hist_ix: None | int

Index being browsed during history recall, if any

_hist_draft: str

Editor text saved when history browsing started

_stats: dict

Session statistics (commands, failures, etc.)

_t0_cmd: float

Start time of the running command

_t0_session: float

Start time of the session

_spin_ix: int

Current frame of the busy spinner

_spin_timer: None | textual.timer.Timer

Timer advancing the busy spinner

BINDINGS = [Binding(key='ctrl+d', action='quit', description='Quit', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='ctrl+c', action='interrupt', description='Interrupt', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='escape', action='interrupt', description='Interrupt', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='tab', action='tab_complete', description='Complete', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='up', action='history_prev', description='Previous command', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='down', action='history_next', description='Next command', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='enter', action='submit_input', description='Submit', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='shift+enter', action='insert_newline', description='New line', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None), Binding(key='ctrl+p', action='command_palette', description='Commands', show=False, key_display=None, priority=True, tooltip='', id=None, system=False, group=None)]

The default key bindings.

CSS = '\n    Screen > .screen--selection {\n        background: #264f78;\n        color: #ffffff;\n    }\n    App, #body, RichLog {\n        background: #0d0d0d;\n        color: #d6d6d6;\n    }\n    #body {\n        height: 100%;\n    }\n    RichLog {\n        height: 1fr;\n        padding: 1 2 0 2;\n        scrollbar-color: #343434;\n        scrollbar-color-hover: #565f89;\n    }\n    #suggestions {\n        display: none;\n        width: 64;\n        max-width: 90%;\n        height: auto;\n        max-height: 9;\n        margin: 0 2;\n        padding: 0 1;\n        background: #202020;\n        border-left: solid #7aa2f7;\n        color: #d6d6d6;\n    }\n    #suggestions > .option-list--option-highlighted {\n        background: #343a48;\n    }\n    #composer {\n        height: auto;\n        min-height: 4;\n        max-height: 6;\n        margin: 0 2;\n        padding: 1 1 0 1;\n        background: #202020;\n        border-left: solid #7aa2f7;\n    }\n    #composer.busy {\n        border-left: solid #ff9e64;\n    }\n    #prompt-input {\n        height: 1;\n        min-height: 1;\n        max-height: 3;\n        border: none;\n        background: transparent;\n        color: #eeeeee;\n        padding: 0;\n        scrollbar-size-vertical: 0;\n        scrollbar-size-horizontal: 0;\n    }\n    #composer-hint {\n        height: 1;\n        color: #858585;\n    }\n    #status-bar {\n        height: 2;\n        margin: 0 2;\n        padding: 0 0 1 0;\n        background: #0d0d0d;\n        color: #888888;\n    }\n    '

Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, and therefore takes priority in the event of a specificity clash.

action_command_palette() → None

Open a picker for built-in TUI commands (Ctrl-P).

action_insert_newline() → None

Insert a literal newline without submitting the editor.

action_interrupt() → None

Interrupt the running command, or clear the editor

External commands run in their own process group and are sent SIGINT; in-process CAPE commands get a KeyboardInterrupt injected into the worker thread. With nothing running, the editor is cleared.

action_submit_input() → None

Accept a completion or submit the current editor contents.

action_tab_complete() → None

Complete the word left of the cursor (TAB action)

Completions come from CapeTuiCompleter. A unique match is inserted directly; multiple matches open a picker directly above the command editor.

check_action(action: str, parameters: Tuple[object, ...]) → bool | None

Check whether an action is enabled.

Implement this method to add logic for [dynamic actions](/guide/actions#dynamic-actions) / bindings.

Args:

action: The name of an action. parameters: A tuple of any action parameters.

Returns:
True if the action is enabled+visible,

False if the action is disabled+hidden, None if the action is disabled+visible (grayed out in footer)

compose() → Iterable[Widget]

Yield child widgets for a container.

This method should be implemented in a subclass.

finalize_stats() → dict

Compute the session duration and return the stats dict

save_history() → None

Write the (trimmed) session history back to the file

class cape.tui.tuiapp.CapeTuiCompleter(cls, app: CapeTuiApp)

CAPE tab-completer that reads the line from the TUI editor

This overrides CfdxCompleter.get_line_buffer() to source the command line from the app’s ComposerTextArea (text before the cursor) instead of readline, and adds suggestions for the TUI : meta-commands.

Attributes:
app: CapeTuiApp

App whose editor is being completed

genr8_suggestions(text: str) → list[str]

Generate suggestions, including TUI meta-commands

Call:
>>> suggestions = comp.genr8_suggestions(text)
Inputs:
comp: CapeTuiCompleter

TUI autocompleter

text: str

Current text of current word

Outputs:
suggestions: list[str]

List of suggested completions for current word

get_line_buffer() → str

Get the editor text before the cursor

Call:
>>> line = comp.get_line_buffer()
Inputs:
comp: CapeTuiCompleter

TUI autocompleter

Outputs:
line: str

Command input text, truncated at the cursor

class cape.tui.tuiapp.CommandLog(**kwargs)

Scroll log whose command markers fold their associated output.

Text selection is implemented here because RichLog renders its lines to cached strips: by itself it neither highlights the selection nor can it extract the selected text for copying.

can_focus = True

Widget may receive focus.

can_focus_children = True

Widget’s children may receive focus.

clear()

Clear the text log.

Returns:

The RichLog instance.

end_group() → None

Route subsequent writes to the top-level transcript.

get_selection(selection)

Get the text under the selection

Call:
>>> text, ending = app.get_selection(selection)
Inputs:

selection: selection information

Outputs:
text: str

Text extracted from selection

ending: "\n"

Ending

render_line(y: int)

Render a line of content.

Args:

y: Y Coordinate of line.

Returns:

A rendered line.

start_section(name: str, header: Text, folded: Text, collapsed: bool = False) → None

Start a separately foldable output section.

write(content, *args, **kwargs)

Writre a string or Rich renderable to the bottom of the log

Call:
>>> content = app.write(content, *args, **kwargs)
Inputs:
content: str | renderable

Rich renderable (or a string) to write

width: {None} | int

Minimum width; None -> RichLog.min-width

expand: True | False

Permit expanding of content to the width of content region; ignored if width specified

shrink: True | False

Permit shrinking of content to the width of content region; ignored if width specified

scroll_end: None | True | False

Enable automatic croll to end

Outputs:
content: RichLog

The RichLog intsance

class cape.tui.tuiapp.CommandPalette(name: str | None = None, id: str | None = None, classes: str | None = None)

Small picker for the TUI’s built-in commands.

BINDINGS = [Binding(key='escape', action='dismiss_palette', description='Close', show=False, key_display=None, priority=False, tooltip='', id=None, system=False, group=None), Binding(key='ctrl+p', action='dismiss_palette', description='Close', show=False, key_display=None, priority=False, tooltip='', id=None, system=False, group=None)]

A list of key bindings.

CSS = '\n    CommandPalette {\n        align: center middle;\n        background: #000000 65%;\n    }\n    #command-list {\n        width: 60;\n        max-width: 90%;\n        height: auto;\n        max-height: 70%;\n        background: #202020;\n        border-left: solid #7aa2f7;\n        padding: 1 2;\n    }\n    #command-list > .option-list--option-highlighted {\n        background: #343a48;\n    }\n    '

Inline CSS, useful for quick scripts. Rules here take priority over CSS_PATH.

Note:

This CSS applies to the whole app.

can_focus = False

Widget may receive focus.

can_focus_children = True

Widget’s children may receive focus.

compose() → Iterable[Widget]

Called by Textual to create child widgets

This method is called when a widget is mounted or by setting recompose=True when calling textual.widget.Widget.refresh().

Note that you don’t typically need to explicitly call this method.

Example:

def compose(self) -> ComposeResult:
    yield Header()
    yield Label("Press the button below:")
    yield Button()
    yield Footer()
class cape.tui.tuiapp.ComposerTextArea(**kwargs)

One-to-three-line soft-wrapping editor for the TUI composer.

can_cursor_down() → bool

Return whether the cursor has a visual row below it.

can_cursor_up() → bool

Return whether the cursor has a visual row above it.

can_focus = True

Widget may receive focus.

can_focus_children = True

Widget’s children may receive focus.

property cursor_position: int

Return the cursor as an offset into value.

insert_newline() → None

Replace the current selection with an explicit newline.

property value: str

Alias matching the former Input interface.

class cape.tui.tuiapp.LogWriter(app: CapeTuiApp)

Redirect writes (e.g. sys.stdout) into a RichLog

Lines are buffered; complete lines are posted to the app’s scroll log from the writing thread via call_from_thread. ANSI color codes are converted to Rich Text styling.

Attributes:
app: CapeTuiApp

App whose log receives written lines

_buf: str

Buffer holding an incomplete line

flush() → None

Flush write buffers, if applicable.

This is not implemented for read-only and non-blocking streams.

write(s: str) → int

Write string s to stream.

Return the number of characters written (which is always equal to the length of the string).