Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ From a local checkout, use the shorter equivalents:
./run.sh man disk
./run.sh clean
./run.sh fetch --plain
./run.sh apps snapshot timer --seconds 1500 --elapsed 60 --json
./run.sh games list --json
./run.sh science snapshot orbit --json
./run.sh screensaver snapshot rain --seed 1 --json
Expand Down
97 changes: 78 additions & 19 deletions dev/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class RunSelection:
script: ScriptSpec
flags: tuple[str, ...]
working_directory: Path
routing_tokens: tuple[str, ...] = ()


@dataclass(frozen=True)
Expand Down Expand Up @@ -1112,6 +1113,32 @@ def selected_arguments(self) -> tuple[str, ...] | None:
)
return tuple(arguments)

def routing_tokens(self) -> tuple[str, ...]:
tokens: list[str] = []
selected = self.controller_values()
for index, parameter in enumerate(self.script.parameters):
if not conditions_match(parameter.when, selected):
continue
widget = self.query_one(f"#parameter-{index}", Input | Select)
value = (
widget.value.strip()
if isinstance(widget, Input)
else ""
if widget.value is Select.NULL
else str(widget.value)
)
if value and parameter.choices:
tokens.append(value)
if value and parameter.flag is not None:
tokens.append(parameter.flag)
tokens.extend(
option.flag
for index, option in enumerate(self.script.options)
if conditions_match(option.when, selected)
and self.query_one(f"#option-{index}", Checkbox).value
)
return tuple(dict.fromkeys(tokens))

def working_directory(self) -> Path | None:
value = self.query_one("#cwd", Input).value.strip()
if not value:
Expand All @@ -1138,13 +1165,12 @@ def update_step(self) -> None:
"wizard-review",
)[self.step]
self.query_one("#wizard-back", Button).disabled = self.step == 0
arguments = self.selected_arguments() if self.step == 2 else ()
self.query_one("#wizard-next", Button).label = (
(
"Preview"
if self.script.needs_apply(arguments or ())
if self.script.needs_apply(self.routing_tokens())
else "Show command"
if self.script.needs_direct_terminal(arguments or ())
if self.script.needs_direct_terminal(self.routing_tokens())
else "Run"
)
if self.step == 2
Expand Down Expand Up @@ -1184,14 +1210,15 @@ def advance(self) -> None:
and parameter.warning
)
caution = "\n".join(f"- {warning}" for warning in warnings)
needs_apply = self.script.needs_apply(arguments)
routing_tokens = self.routing_tokens()
needs_apply = self.script.needs_apply(routing_tokens)
flow = (
"This runs a read-only preview first, then shows the apply "
"command for a direct interactive terminal."
if needs_apply and self.script.needs_direct_terminal(arguments)
if needs_apply and self.script.needs_direct_terminal(routing_tokens)
else "This interactive tool needs the terminal. Confirm to show the "
"exact command to run."
if self.script.needs_direct_terminal(arguments)
if self.script.needs_direct_terminal(routing_tokens)
else "This runs a read-only preview first. Apply repeats "
"discovery with the same settings, so targets may change "
"between runs. A successful preview must be followed by `CLEAN`."
Expand All @@ -1210,7 +1237,14 @@ def advance(self) -> None:

arguments = self.selected_arguments()
if arguments is not None:
self.dismiss(RunSelection(self.script, arguments, directory))
self.dismiss(
RunSelection(
self.script,
arguments,
directory,
self.routing_tokens(),
)
)


class ConfirmApply(ModalScreen[bool]):
Expand Down Expand Up @@ -2140,9 +2174,9 @@ async def run_once(
@work(group="script", exclusive=True)
async def run_selection(self, selection: RunSelection) -> None:
try:
needs_apply = selection.script.needs_apply(selection.flags)
needs_apply = selection.script.needs_apply(selection.routing_tokens)
if (
selection.script.needs_direct_terminal(selection.flags)
selection.script.needs_direct_terminal(selection.routing_tokens)
and not needs_apply
):
command = display_command(
Expand All @@ -2165,7 +2199,7 @@ async def run_selection(self, selection: RunSelection) -> None:
return
assert selection.script.apply_flag is not None
assert selection.script.yes_flag is not None
if selection.script.needs_direct_terminal(selection.flags):
if selection.script.needs_direct_terminal(selection.routing_tokens):
direct_flags = (*selection.flags, selection.script.apply_flag)
command = display_command(command_for(selection.script, direct_flags))
self.query_one("#activity-heading", Label).update(
Expand Down Expand Up @@ -2308,8 +2342,8 @@ def layout_snapshot(app: ScriptsApp) -> dict[str, tuple[int, int, int, int]]:
"privilege": "user",
"applyFlag": "--apply",
"yesFlag": "--yes",
"applyWhen": ["fix"],
"directWhen": ["fix"],
"applyWhen": ["fix", "--query"],
"directWhen": ["--force"],
"findingExitCodes": [1],
"launchable": False,
"parameters": [
Expand Down Expand Up @@ -2339,11 +2373,18 @@ def layout_snapshot(app: ScriptsApp) -> dict[str, tuple[int, int, int, int]]:
],
},
],
"options": [
{
"flag": "--force",
"label": "Force",
"when": {"action": ["fix"]},
}
],
}
help_path = script_path.with_name("probe.py.man")
probe_markdown = (
"# Probe\n\n`action`, `--query`, `format`, `--apply`, and `--yes` "
"exercise typed parameters.\n"
"# Probe\n\n`action`, `--query`, `format`, `--force`, `--apply`, "
"and `--yes` exercise typed parameters.\n"
)
help_path.write_text(
f"---\n{json.dumps(metadata)}\n---\n{probe_markdown}",
Expand All @@ -2356,7 +2397,17 @@ def layout_snapshot(app: ScriptsApp) -> dict[str, tuple[int, int, int, int]]:
assert not probe.launchable
assert not probe.needs_apply(("show",))
assert probe.needs_apply(("fix",))
assert probe.needs_direct_terminal(("fix",))
assert probe.needs_apply(("--query",))
assert probe.needs_direct_terminal(("--force",))
assert not probe.needs_direct_terminal(("fix",))
safe_selection = RunSelection(
probe,
("show", "--query", "fix", "text"),
contract_root,
("show", "text"),
)
assert not probe.needs_apply(safe_selection.routing_tokens)
assert not probe.needs_direct_terminal(safe_selection.routing_tokens)
assert probe.has_findings(1) and not probe.has_findings(2)
assert not conditions_match(
probe.parameters[1].when,
Expand Down Expand Up @@ -2447,14 +2498,14 @@ def layout_snapshot(app: ScriptsApp) -> dict[str, tuple[int, int, int, int]]:
assert compact.query_one("#details-tabs", TabbedContent).active == "help-pane"
assert compact.focused is help_scroll
await pilot.press("end")
await pilot.pause()
await pilot.wait_for_scheduled_animations()
assert help_scroll.scroll_y > 0
help_scroll.scroll_home(animate=False)
await pilot.pause()
help_scroll.post_message(
MouseScrollDown(help_scroll, 1, 1, 0, 1, 0, False, False, False)
)
await pilot.pause()
await pilot.wait_for_scheduled_animations()
assert help_scroll.scroll_y > 0
await pilot.press("f3")
assert compact.query_one("#details-tabs", TabbedContent).active == "files-pane"
Expand Down Expand Up @@ -2483,16 +2534,24 @@ def layout_snapshot(app: ScriptsApp) -> dict[str, tuple[int, int, int, int]]:
assert isinstance(conditional_wizard, RunWizard)
assert conditional_wizard.query_one("#parameter-2", Select).value is Select.NULL
hidden_query = conditional_wizard.query_one("#parameter-1", Input)
hidden_query.value = "ignored"
hidden_query.value = "fix"
assert not conditional_wizard.query_one("#parameter-row-1").display
assert conditional_wizard.selected_arguments() == ("show",)
assert conditional_wizard.routing_tokens() == ("show",)
conditional_wizard.query_one("#parameter-0", Select).value = "fix"
await pilot.pause()
assert conditional_wizard.query_one("#parameter-row-1").display
assert conditional_wizard.selected_arguments() == (
"fix",
"--query",
"ignored",
"fix",
)
assert conditional_wizard.routing_tokens() == ("fix", "--query")
conditional_wizard.query_one("#option-0", Checkbox).value = True
assert conditional_wizard.routing_tokens() == (
"fix",
"--query",
"--force",
)
await pilot.press("escape")
await pilot.pause()
Expand Down
158 changes: 158 additions & 0 deletions packs/apps.man
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
{
"summary": "Read text, inspect tables, present slides, paint text canvases, and run timers locally.",
"platform": "any",
"privilege": "user",
"executable": "apps",
"directWhen": ["open"],
"parameters": [
{
"name": "action",
"label": "Action",
"required": true,
"default": "snapshot",
"choices": [
{"value": "snapshot", "label": "Print a snapshot"},
{"value": "open", "label": "Open interactive app"}
]
},
{
"name": "app",
"label": "App",
"required": true,
"default": "read",
"choices": [
{"value": "read", "label": "Text reader"},
{"value": "table", "label": "CSV or TSV table"},
{"value": "slides", "label": "Markdown slides"},
{"value": "paint", "label": "Text canvas"},
{"value": "timer", "label": "Countdown timer"}
]
},
{
"name": "input",
"label": "Input file",
"required": true,
"placeholder": "notes.md, data.csv, or talk.md",
"when": {"app": ["read", "table", "slides"]}
},
{
"name": "canvas",
"label": "Existing text canvas",
"placeholder": "optional drawing.txt",
"when": {"app": ["paint"]}
},
{
"name": "find",
"label": "Find text",
"flag": "--find",
"placeholder": "case-insensitive query",
"when": {"app": ["read"]}
},
{
"name": "delimiter",
"label": "Table delimiter",
"flag": "--delimiter",
"choices": [
{"value": "comma", "label": "Comma"},
{"value": "tab", "label": "Tab"}
],
"when": {"app": ["table"]}
},
{
"name": "limit",
"label": "Snapshot rows or lines",
"flag": "--limit",
"placeholder": "1 to 1000, default 20",
"when": {"action": ["snapshot"], "app": ["read", "table"]}
},
{
"name": "page",
"label": "Slide page",
"flag": "--page",
"placeholder": "1 to 512, default 1",
"when": {"app": ["slides"]}
},
{
"name": "width",
"label": "New canvas width",
"flag": "--width",
"placeholder": "1 to 240, default 40",
"when": {"app": ["paint"]}
},
{
"name": "height",
"label": "New canvas height",
"flag": "--height",
"placeholder": "1 to 80, default 16",
"when": {"app": ["paint"]}
},
{
"name": "output",
"label": "New paint output",
"flag": "--output",
"placeholder": "new drawing.txt",
"when": {"action": ["open"], "app": ["paint"]}
},
{
"name": "seconds",
"label": "Timer seconds",
"flag": "--seconds",
"required": true,
"placeholder": "1 to 86400",
"when": {"app": ["timer"]}
},
{
"name": "elapsed",
"label": "Elapsed seconds",
"flag": "--elapsed",
"placeholder": "0 to timer duration",
"when": {"action": ["snapshot"], "app": ["timer"]}
},
{
"name": "label",
"label": "Timer label",
"flag": "--label",
"placeholder": "focus",
"when": {"app": ["timer"]}
},
{
"name": "format",
"label": "Snapshot format",
"required": true,
"default": "--plain",
"choices": [
{"value": "--plain", "label": "Plain text"},
{"value": "--json", "label": "JSON"}
],
"when": {"action": ["snapshot"]}
}
]
}
---
# Apps

`apps` is the native local terminal workbench from
[`keys-i/tools`](https://github.com/keys-i/tools). It owns five focused modes:
`read` searches UTF-8 text, `table` parses quoted CSV or TSV, `slides` presents
Markdown pages separated by `---`, `paint` edits a plain-text canvas, and
`timer` runs a monotonic countdown.

The `action` selects `snapshot` or `open`; `app` selects the mode. Supply an
`input` for read, table, or slides, or an optional paint `canvas`. Reader search
uses `--find`; tables accept `--delimiter`; read/table snapshots accept
`--limit`; slides accept `--page`. A new paint canvas accepts `--width` and
`--height`; do not set them with an existing canvas. Interactive paint accepts
`--output` and refuses to replace an existing file. Timers require `--seconds`
and accept snapshot `--elapsed` plus `--label`. Choose the snapshot `format` as
plain text or JSON.

All input is opened once and capped at 32 MiB. Tables stop at 100,000 rows, 256
columns, and two million cells. Paint saves a completed temporary file through
an atomic no-overwrite link. There is no network, TLS, browser, PDF codec,
Wayland compositor, PTY server, plugin, Python process, shell, or upstream
executable.

Keyboard controls are shown in every mode. `q`, Escape, or Ctrl-C exits and the
terminal is restored. This pack contains only the typed UI contract; the
prepared `keys-tools` PyPI package has not been published.
Loading