Skip to content
Back to skills

Tui App Scaffolder

ASecurity

Scaffolds a complete reactive Textual terminal Todo application with header, input, DataTable, footer, status label, custom keybindings and CRUD handlers, and includes a fully written Go Bubbletea counter alternative.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 27, 2026
developmentpythongoshellreactgit

Works with

  • cursor
  • terminal

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill tui-app-scaffolder --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tui App Scaffolder?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tui App Scaffolder
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-tui-app-scaffolder/badge)](https://www.skillsdirectory.com/skills/rmazrim-tui-app-scaffolder)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Interactive TUI Application Scaffolder
description: Scaffolds a complete reactive Textual terminal Todo application with header, input, DataTable, footer, status label, custom keybindings and CRUD handlers, and includes a fully written Go Bubbletea counter alternative.
metadata:
  source: skills/tui-app-scaffolder/tui-app-scaffolder.md
---

# Interactive TUI Application Scaffolder

## 1. System Architecture & Prerequisites

Scaffolds rich terminal UI applications with **Textual/Rich** (Python) or
**Bubbletea** (Go). The reference implementation is a full Todo app on
Textual: a composable widget tree (Header, Input, DataTable, status Label,
Footer), reactive event handlers, custom keybindings, and an inline CSS
stylesheet. A complete Bubbletea counter is supplied as the cross-language Go
alternative.

Prerequisites:

- **Python 3.9+** and `pip install textual` (Textual ≥ 0.50). Rich is pulled
  in as a Textual dependency; no separate install needed.
- For terminals: Textual needs a real TTY. On Windows use Windows Terminal
  (or `cmd`/PowerShell with VT enabled); on Linux/macOS any modern terminal.
- Optional Bubbletea path: Go 1.21+ and
  `go get github.com/charmbracelet/bubbletea@latest`.

## 2. Input/Output Data Contracts

### Input: application definition (in code)

The scaffolder requires a widget tree specification + interaction contract:

| Element | Contract |
| ------- | -------- |
| `Header`   | app title bar with clock/guidance |
| `Input`    | `id="task-input"`, Submit fires `add_task` |
| `DataTable`| columns `#`, `Task`, `Status`; row cursor selection |
| `Label`    | `id="status"`, user-facing state messages |
| `Footer`   | lists current `BINDINGS` keymap |
| Key bindings | `ctrl+d` delete selected, `ctrl+i` focus input, `ctrl+q` quit |

### Output

- `tui_app.py` — runnable Textual app (`python tui_app.py`).
- Optional `app.tcss` — the same stylesheet in external-file form for
  `App.CSS_PATH` deployment.
- Optional `main.go` — Bubbletea counter (fully written, `go run .`).

No JSON manifests or config files are required — the contract is the widget
tree + bindings above.

## 3. Production Reference Implementation

Save as `tui_app.py`. Complete, importable, and runnable with
`python tui_app.py`.

```python
#!/usr/bin/env python3
"""Textual Todo application — the TUI scaffolder reference implementation.

Features:
  - compose(): Header, Input, DataTable, status Label, Footer
  - reactive handlers: on_input_submitted (add) and on_input_changed (status)
  - custom keybindings: ctrl+d (delete selected row), ctrl+i (focus input)
  - DataTable cursor selection with explicit row keys
Run:  python tui_app.py
"""

from __future__ import annotations

from textual.app import App
from textual.binding import Binding
from textual.widgets import DataTable, Footer, Header, Input, Label

# Inline stylesheet. Also works as an external `app.tcss` via CSS_PATH.
CSS = """
Screen {
    background: $surface;
    layout: vertical;
}

#task-input {
    dock: top;
    margin: 1 2 0 2;
    border: tall $primary 30%;
}

DataTable {
    height: 1fr;
    margin: 1 2 0 2;
    border: round $accent;
}

DataTable > .datatable--cursor {
    background: $accent 35%;
}

#status {
    height: 1;
    padding: 0 2;
    background: $panel;
    color: $text-muted;
}

Footer {
    dock: bottom;
}
"""


class TodoApp(App):
    TITLE = "Todo"
    SUB_TITLE = "zero-to-terminal in 60 seconds"
    CSS = CSS

    BINDINGS = [
        Binding("ctrl+d", "delete_task", "Delete selected"),
        Binding("ctrl+i", "focus_input", "Focus input"),
        Binding("ctrl+q", "quit", "Quit"),
    ]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._id_counter = 0
        self._task_titles: dict[int, str] = {}

    def compose(self):
        yield Header()
        yield Input(placeholder="Type a task and press Enter", id="task-input")
        yield DataTable(id="tasks")
        yield Label("Ready.", id="status")
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one("#tasks", DataTable)
        table.add_columns("#", "Task", "Status")
        table.cursor_type = "row"
        table.zebra_stripes = True
        self._set_status("No tasks yet. Type one below and press Enter.")

    # -- reactive handlers ---------------------------------------------

    def on_input_submitted(self, event: Input.Submitted) -> None:
        """Enter on the Input widget: add a task row."""
        event.stop()
        text = event.value.strip()
        input_widget = self.query_one("#task-input", Input)
        input_widget.value = ""
        if not text:
            self._set_status("Empty task ignored.")
            return
        self._id_counter += 1
        task_id = self._id_counter
        self._task_titles[task_id] = text
        table = self.query_one("#tasks", DataTable)
        table.add_row(str(task_id), text, "open", key=str(task_id))
        self._set_status(f"Added task #{task_id}: {text}")

    def on_input_changed(self, event: Input.Changed) -> None:
        """Reactive status line while typing (demonstrates state updates)."""
        event.stop()
        length = len(event.value)
        if length:
            self._set_status(f"{length} characters typed.")
        else:
            self._set_status("Ready.")

    # -- actions --------------------------------------------------------

    def action_delete_task(self) -> None:
        table = self.query_one("#tasks", DataTable)
        if table.row_count == 0:
            self._set_status("Nothing to delete.")
            return
        try:
            cell_key = table.coordinate_to_cell_key(table.cursor_coordinate)
        except KeyError:
            self._set_status("No row selected.")
            return
        row_key = cell_key.row_key
        task_id = int(row_key.value)
        title = self._task_titles.pop(task_id, "<unknown>")
        table.remove_row(row_key)
        self._set_status(f"Deleted task #{task_id}: {title}")

    def action_focus_input(self) -> None:
        self.query_one("#task-input", Input).focus()

    # -- helpers ---------------------------------------------------------

    def _set_status(self, message: str) -> None:
        self.query_one("#status", Label).update(message)


def main() -> None:
    TodoApp().run()


if __name__ == "__main__":
    main()
```

### External stylesheet variant — `app.tcss`

```css
Screen {
    background: $surface;
    layout: vertical;
}

#task-input {
    dock: top;
    margin: 1 2 0 2;
    border: tall $primary 30%;
}

DataTable {
    height: 1fr;
    margin: 1 2 0 2;
    border: round $accent;
}

#status {
    height: 1;
    padding: 0 2;
    background: $panel;
    color: $text-muted;
}

Footer {
    dock: bottom;
}
```

Switch with `CSS_PATH = "app.tcss"` and `CSS = None` (or delete the inline
attribute) — Textual merges in reverse priority, so external or inline works
identically.

## 4. Execution Protocol & Step-by-Step Workflow

1. **Bootstrap.** `pip install textual`, then copy `tui_app.py` into the
   project. Run `python tui_app.py` to confirm the toy runs.
2. **Define the widget tree** in `compose()` (top to bottom: Header, Input,
   DataTable, status Label, Footer).
3. **Add bindings** in `BINDINGS` and name private `action_*` methods exactly
   after the binding action ids (`ctrl+d` -> `action_delete_task`).
4. **Wire reactive handlers.** `on_input_submitted` for writes,
   `on_input_changed` for keystroke-level state feedback, `on_mount` for
   one-time table configuration. Prefer Textual's `Subject`/`reactive` for
   data the UI reflects.
5. **Persist.** After each `add`/`delete` call `table.save`-equivalent
   semantics — i.e. write `self._task_titles` to a local JSON file on change
   so restarts are not lost.
6. **Extend** with a `ModalScreen("Confirm delete?")`, worker threads for
   long jobs (`@work`/`run_worker`), and `Input(disabled=True)` while the
   DataTable is dirty.
7. **Package** — wrap in a console script entry point and call
   `TodoApp().run()` from `if __name__ == "__main__"`, exactly as shown.

### Cross-language option — Bubbletea (Go), complete `main.go`

```go
package main

import (
	"fmt"
	"os"

	tea "github.com/charmbracelet/bubbletea"
)

type model struct {
	count int
}

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case "up", "k", "+":
			m.count++
		case "down", "j", "-":
			m.count--
		}
	}
	return m, nil
}

func (m model) View() string {
	return fmt.Sprintf(
		"Count: %d\n\n\033[32m+\033[0m/up increase   \033[33m-\033[0m/down decrease   \033[31mq\033[0m/ctrl+c quit\n",
		m.count,
	)
}

func main() {
	program := tea.NewProgram(model{count: 0})
	if _, err := program.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "bubbletea error: %v\n", err)
		os.Exit(1)
	}
}
```

Go build/run: `go mod init tui-counter && go get github.com/charmbracelet/bubbletea@latest && go run .` — the model/update/view triangle maps 1:1 to Textual's `compose`/actions/Screen-render philosophy.

## 5. Edge Cases & Error Handling

- **Textual not installed** — `ModuleNotFoundError: textual` at import;
  the fix is `pip install textual` (stated in Section 1). On a missing TTY,
  Textual raises `TextualError` — run inside a real terminal, not a pipe.
- **DataTable row deletion off-by-one** — the reference stores explicit
  `key=str(task_id)` row keys so `remove_row(row_key)` never deletes the wrong
  row after filtering/ordering; never index by cursor position alone.
- **Delete on empty table** — guarded by `table.row_count == 0`; the
  `KeyError` from `coordinate_to_cell_key` with no selection is also caught.
- **Empty input submit** — `event.value.strip()` guards blank tasks from
  entering the table (a classic scaffold bug in tutorials).
- **`Binding` action name typos** — Textual raises on bad bindings at startup
  (`AttributeError: action_xxx not found`); keep action ids and method names
  welded together.
- **Version drift on `coordinate_to_cell_key`** — older Textual releases used
  `get_cell_at`/`row_key_at`; if the reader's edition differs, prefer the
  stable `DataTable.cursor_coordinate` + `coordinate_to_cell_key` documented
  here (Textual ≥ 0.28).
- **Windows terminal quirks** — use Windows Terminal, set `TERM=xterm-256color`
  if colours misrender, and avoid racing `App.run()` from Jupyter/headless
  shells (run `.run_test()` for headless smoke tests).
- **Resource cleanup** — `on_unmount` should save state; long-running workers
  must be cancelled if the app is dismissed, or the event loop lingers.
- **Bubbletea port** — the Go mirror guards missing dependencies (the
  `go.mod` is created before `go run .`) and quits cleanly on `ctrl+c`,
  matching the Textual esc/quit contract.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.