Add context listing and switching to the TUI

Press c to list cv4pve-cli contexts and switch between them. The
active context is always shown in the header.

- Switching runs `cv4pve-cli config use`, which changes the global
  current context. cv4pve-cli 2.4.0 has no per-call context option.
- `config list` has no JSON output, so its text lines are parsed.
- The guest table is cleared as soon as another context is chosen, so
  the old cluster's guests cannot be acted on.
- A generation counter drops guest listings that started before a
  switch, and g and c are refused while a switch runs. Without this,
  a reload already in flight could refill the table with the old
  cluster's guests, and VMIDs collide across clusters.

103 tests pass. The switch was tested live from mainframe to
mainframe-edge and back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 20:22:53 +01:00
1 parent f51aff3db5
commit 62b79ab29e
6 files changed
+543 -9

No files matched your search

+63
View File
@@ -116,3 +116,66 @@ sockets/vCPU/hotplug settings, live auto-refresh.
## Open questions
None blocking.
---
# Context switching
## Problem
Several cv4pve-cli contexts (clusters) exist; switch between them inside the TUI
instead of quitting and running `cv4pve-cli config use`.
## Decision
Option A: switching runs `cv4pve-cli config use <name>`, which changes the **global**
current context (all shells and scripts follow) and stays after the app exits. cv4pve-cli
2.4.0 has no per-call context flag or env var (verified), so there is no app-local option.
Observed CLI behaviour:
- `config list` has no `-o json`. Text output, one line per context; current one
prefixed with `* `, others with two spaces:
`* mainframe mainframe.iot.home.arpa:8006 (api-token)`
` mainframe-edge mainframe-edge.home.arpa:8006 (api-token)`
- `config use <name>` -> exit 0 on success; unknown name -> exit 3,
`Error: Context '<name>' not found.`, current context unchanged.
### `cli.py`
- `contexts() -> list[dict]` - `run("config", "list")`, each line parsed to
`{"name", "host", "auth", "current": bool}` (regex
`^(\*)?\s*(\S+)\s+(\S+)\s+\((.*)\)\s*$`); non-matching lines are ignored.
- `use_context(name)` - `run("config", "use", name)`.
### `app.py` (UI contract)
- The active context is always visible: `app.sub_title == f"context: {name}"`
(shown in the Header). Set on mount from `cli.contexts()` and after every switch.
If `contexts()` fails: error notify, sub_title `context: ?`.
- Key `c` opens `ContextScreen(ModalScreen[str | None])` with `OptionList#contexts`,
one option per context, option id = context name, current one marked with `*`.
Selecting an option dismisses with its name; `escape` dismisses with `None`.
- `c` while any guest is busy -> warning notify, no screen (VMIDs collide across
clusters, so the busy guard would point at the wrong cluster).
- Selecting the current context or dismissing -> no CLI call.
- A reload started before a switch must not refill the table (generation counter);
`g` and `c` are refused while a switch runs; the post-action reload finishes before
the guest stops being busy.
- Selecting another context: table cleared immediately (old cluster's guests must not
stay actionable), then in a worker `cli.use_context(name)`, sub_title updated, guests
reloaded. On `CliError`: error notify with summary, sub_title re-read from
`cli.contexts()`, guests reloaded.
## Out of scope
Adding/editing/deleting contexts, `config verify`, restoring the original context on exit.
## Steps
- [x] Tests for `contexts`/`use_context` and the UI contract above - red first.
- [x] Implementation - full suite green, ruff clean.
- [x] Live: open the list, switch to `mainframe-edge` and back to `mainframe`
(only `config use` and read-only listing; no guest actions); final context
is `mainframe`.
- [ ] Push `feat/guest-tui-mvp`, merge to `main` (user-approved).