Workbench (interactive TUI)¶
The workbench is psc's interactive Textual
terminal UI — a keyboard-driven cockpit that glues every engine together around a
persistent selection buffer and a git-like staged changelist. It is at
full CLI parity: everything you can do from the command line you can do here, and
mutations batch into one reviewed apply.
It is a pure psc.core / psc.output frontend — it never imports the CLI layer
— so it inherits every safety invariant unchanged: dry-run by default, a hard
blocker gate, repoint-before-delete, offline
never overwrites the source export, and live never commits.
Launching¶
The source is chosen the same way as any other command:
psc --config panorama.xml workbench # offline: an exported config
psc --profile prod workbench # live: a configured profile
psc workbench # live: the default profile
The output mode decides how a staged batch is finally applied — pick it at launch (or change your mind before applying by relaunching):
--output-mode |
What "apply" does |
|---|---|
set (default) |
Render the combined PAN-OS set script; push nothing. |
offline-apply |
Write the compounded config to --apply-out <file>. |
live-apply |
Push the batch to the live candidate (never commits). |
psc -c panorama.xml workbench --output-mode offline-apply --apply-out fixed.xml
psc -p prod workbench --output-mode live-apply
Passing --apply-out implies offline-apply; choosing offline-apply without
--apply-out fails fast at launch rather than at apply time.
The hub¶
The home screen is a hub: a search box, a staged (N) strip, a results
table, and the selection buffer. The search box and the staged strip share the
top row; the results and selection tables stack vertically below it, each
at the full terminal width — the results table is the widest content in the
app, so it no longer splits the screen with the selection panel next to it.
┌ search: IP / value / name ────────────────┬ staged (N) ┐
├───────────────────────────────────────────┴────────────┤
│ results │
│ kind name location value │
├────────────────────────────────────────────────────────┤
│ selection │
│ kind name location │
└────────────────────────────────────────────────────────┘
? keys ctrl+p commands
At launch, focus is in #search, which — deliberately — consumes q as a
typed character rather than quitting (see below), so the
Footer shows only two keys until you tab or click off the search box; q quit
joins them once focus moves to the results or selection table.
The flow is hub → search → select → spoke → staged changelist → apply:
- Search by IP, CIDR, range, value, or name; matches fill the results table.
- Select the rows you want to act on into the selection buffer.
- Open a spoke (dedup, move, rename, …) that consumes the selection and builds a plan you review.
- Stage the plan — it compounds into the changelist (each new plan is built against the already-staged working config, so plans never go stale).
- Apply the whole batch at once, in your chosen output mode.
While a spoke is open, the hub keys are inert — you must finish or cancel the spoke first. This prevents a second spoke stacking over the first and letting a plan go stale.
Finding the keys¶
The footer shows three keys, not the whole binding table:
| Key | What it opens |
|---|---|
? |
The keymap — every command, grouped by what it does (Navigate / Objects / Analyze / Names / Session), each with a real description. Dismiss with escape, ?, or q. |
ctrl+p |
The command palette — fuzzy-search every command by title, category, or description. Entries read Category › Title (e.g. Analyze › Dedup vs. Analyze › Duplicate scan), so the two are finally distinguishable. Textual's own commands (theme, screenshot) are ranked below psc's. |
q |
Quit. |
No key was reassigned to get here — every spoke hotkey in the tables below
still works exactly as before, it's just no longer listed across the bottom of
the screen. Press ? when you forget one.
? always works, even while the search box has focus (which is where the app
starts) — it's a priority binding, since it's the only way to discover the
other ~22 hidden hotkeys. The one cost: you can't type a literal ? into a
search, but ? is never a meaningful character in an IP, value, or PAN-OS
object name.
q, by contrast, is a plain printable key, so a focused text field swallows
it as a typed character instead of quitting — this is deliberate, so you can
type a "q" into a search (e.g. an object name containing "q"). Tab or click
into the results or selection table first if you want q to quit. ctrl+p
works from the search box or anywhere else in the hub — but only in the hub:
like every other hub key, it's inert while a spoke is open, so it can't stack
a second plan over one you haven't finished or cancelled yet.
The selection buffer¶
The selection is heterogeneous (addresses, groups, services, tags can coexist) and persistent across spokes — build it once, route it into several spokes.
| Key | Action |
|---|---|
space |
Toggle the highlighted results row in/out of the selection. |
delete / backspace |
Drop the focused row from the selection panel. |
Spokes¶
Each spoke maps to one core engine. Read-only spokes just report; mutating spokes
build a ChangeSet you review and stage (ctrl+y) or cancel (escape).
| Key | Spoke | What it does |
|---|---|---|
d |
dedup | Collapse the duplicate bucket in the selection toward a chosen survivor (whole-bucket merge; the rest are repointed and removed), or promote it. Buckets can be addresses, services, address-groups, or tags (tags bucket by name, since they carry no value; they are promote-only — no in-place merge). A destination dropdown picks either an existing member (name@location — merge in place) or a location the bucket doesn't already occupy (dedup promote: create it there, delete every source copy, no repoints). A group bucket also shows a cascade checkbox, pulling its members up with it on the promote path. |
D |
duplicates scan | Config-wide duplicate buckets (read-only), with a kind toggle for addresses / services / address-groups. The discovery counterpart of d: d merges the selection, D finds every duplicate in the config. |
u |
usage | Where-used for the whole selection (read-only), with an owner column naming which selected object each reference resolves to. |
a |
audit | Read-only, with a mode toggle: address overlap/containment involving the selection, or custom services duplicating a well-known / predefined port. |
f |
diff | Device-group-vs-device-group drift (read-only): added/removed/changed objects between two scopes, picked from dropdowns. |
o |
export | Write objects of one kind to an NDJSON file (read-only export; never overwrites the source config). |
v |
inspect | Open the focused results row read-only: its member tree and effective leaves (the TUI form of show). Nested groups start collapsed — drill in with enter. Acts on the highlighted row, no selection needed. |
m |
move | Promote selected objects toward shared; a destination drop-down offers the valid ancestors. |
x |
decommission | Reference-safe cascading teardown of the selected addresses. |
r |
rename | Reference-aware rename; choose which selected entry to rename and its new name. |
e |
rule | Add the selected objects as members of an existing rule field. |
G |
group | Add the selected objects as members of an existing address-/service-group (the TUI form of group edit-member --add; removal is CLI-only). |
N |
new group | Build a new group out of the selection (see below). The kind follows what you picked — addresses make an address-group, services a service-group — and the location picker defaults to the narrowest one that can see every member. |
c |
create | Object creation (address / group / service / service-group / tag), the TUI form for psc set. The form is dynamic — it shows only the fields the chosen kind uses, and predefined values (address type, service protocol, tag color) are dropdowns. |
i |
refs-unused | List objects no rule reaches (read-only). |
g |
dangling | List references to names that resolve to nothing (read-only). |
l |
name-lint | Report objects that drift from the configured naming scheme. |
n |
name-apply | Rename drifting object(s) to their scheme name; choose an entry to apply. |
p |
profiles | CRUD live connection profiles, persisted to ~/.psc/config.yaml. Also switches the active source (ctrl+r) — reload the session onto the focused profile or an offline export path; discards the selection + staged batch (with a confirm when a batch is staged). |
s |
staged | Inspect the staged changelist, drop individual changes, and apply (ctrl+a) — the only place apply is reachable (see below). |
The mutating spokes are the same engines as their CLI counterparts, so the behaviour — and the blockers that refuse an unsafe plan — is identical. A spoke with an empty or unusable selection rings the bell instead of staging.
N — a group from the selection¶
The find session's payoff: search, space the objects you want, N, name the
group, ctrl+y. G adds the selection to a group that already exists; N makes
one out of it.
search: 10.0.5. → ▸ web-srv-01 shared 10.0.5.10/32 [x]
▸ web-srv-02 shared 10.0.5.11/32 [x]
▸ web-srv-03 shared 10.0.5.12/32 [x]
N → New address-group from web-srv-01, web-srv-02, web-srv-03
name: web-tier
location: shared ← the narrowest location that sees every member
ctrl+y → set shared address-group web-tier static [ web-srv-01 web-srv-02 web-srv-03 ]
The kind is derived from the selection: addresses and address-groups make a static address-group (a group nested inside a group is valid PAN-OS and allowed); services and service-groups make a service-group. A selection that mixes the two namespaces belongs in no group and is refused, as is a tag. Staging clears the selection — its members have been consumed into the group.
Group members are bare names, resolved upward from the group's own location,
which gives two ways to write a group that does not mean what you picked. N
refuses both:
-
A member the location cannot see. A
sharedgroup naming an object that lives inDG-NYCdangles —sharedcannot see into a device-group, and neither can a sibling. This is why the location picker defaults to the narrowest location whose visibility cone (itself, its ancestors,shared) covers every member; when the selection spans sibling device-groups no such location exists, and every choice blocks. -
A member whose name is shadowed there. You selected
web@shared, but the group lives inDG-NYC, which defines its ownweb: the group would bind to that one. PAN-OS has no syntax for "the shared one", so the intent is inexpressible and the plan is refused rather than quietly pointed at the wrong object. (SelectingDG-NYC's ownwebis fine — that is what the group resolves to.)
A group of that kind already at that name and location also blocks: N creates,
and growing an existing group is G. A same-named group elsewhere in the
hierarchy is legal and only warns — the two shadow each other, and a bare
reference resolves to whichever is nearest.
The CLI equivalent is psc set address-group,
which takes member names directly. It has no visibility blockers: it cannot know
which object you meant by a name, only the workbench's selection carries that.
The staged changelist¶
Every stage compounds into a git-like changelist. The hub shows only a
staged (N) counter; the staged spoke (s) is the full view:
- inspect any staged change to see its complete rendered
set-script, - drop a single change (
d) without discarding the rest of the batch (if the dropped change is a dependency of a later one, the drop is refused and the batch is kept intact), - apply the batch (
ctrl+a) — the only place apply is reachable, so you always review what's staged before emitting it (see below).
Because each plan is built against the working config with the prior stages already applied, the batch is internally consistent — no stage silently invalidates another.
Applying the batch¶
Apply is reached only from the staged changelist (s), so you can't emit a
batch you haven't looked at: open the staged spoke, then ctrl+a opens the
apply screen, where you choose the output format and destination after
reviewing the batch — no need to decide at launch. Pick one:
- Print the set script here — the combined PAN-OS
setscript, shown inline. - Save a set-command file (
.set) — the same script written to a file. - Save a full XML config — the whole compounded config (offline apply).
- Save a minimal partial XML config — only the touched subtrees (offline apply).
- Push to the live candidate — pushes over the XML API and never commits (offered only when the session is backed by a live profile).
The --output-mode / --apply-out launch flags still work; they just pre-select
the default here, overridable in-app. The set-script options are exports (they
keep your staging); a full/partial config write or a live push commits the batch
and clears staging. A live push and overwriting an existing file each need
an explicit second ctrl+a to confirm.
The safety model is unchanged: a blocked batch is refused, an offline write never overwrites the source export, and a live push never commits. Any apply failure is surfaced on the screen; the app does not crash and your batch is preserved.
Quitting¶
q quits. The staged changelist lives only for the session — apply it (or write
it to a set/config artifact) before you leave.