diff --git a/CLAUDE.md b/CLAUDE.md index ba8db7d..966070e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,6 +39,13 @@ go fix ./... # Apply modernizers (safe, behavior-preserving) - **Bubble Tea v2 View composition**: only the top-level `tui.Model.View()` returns `tea.View`; every sub-view model returns `string`. The top-level composes sub-view strings and sets program options (alt-screen, mouse mode, window title, cursor) on the returned `tea.View` rather than on `tea.NewProgram`. - Use `tea.KeyPressMsg` in key handler type switches (not `tea.KeyMsg`, which in v2 is the union interface of press and release). Construct test messages as `tea.KeyPressMsg{Code: tea.KeyDown}` or `tea.KeyPressMsg{Code: 'j', Text: "j"}` — `Runes`/`Type` from v1 no longer exist. - Theme palette fields are `image/color.Color`, not a string alias. Construct concrete values via `lipgloss.Color("#RRGGBB")`. +- **Three navigation levels, three key sets**: `1`/`2`/`3` switch groups, `Tab`/`Shift+Tab` move between views in a group, `[`/`]` switch sub-tabs *within* a view (Objects, Routes, Logs). A view must never consume `Tab` — Objects used to, and became the one view you could not `Tab` out of. +- **Free-form op output is CDATA-wrapped on real hardware.** `Result.Inner` is `xml:",innerxml"`, so it keeps the literal `__.tar.gz +# macOS / Linux — set VERSION, OS (darwin|linux), ARCH (arm64|amd64) +VERSION=1.5.3 OS=darwin ARCH=arm64 +curl -sSL "https://github.com/jp2195/pyre/releases/download/v${VERSION}/pyre_${VERSION}_${OS}_${ARCH}.tar.gz" | tar xz chmod +x pyre sudo mv pyre /usr/local/bin/pyre ``` @@ -65,6 +66,26 @@ Or build from source (Go 1.26+): go install github.com/jp2195/pyre/cmd/pyre@latest ``` +### macOS: "cannot be opened because the developer cannot be verified" + +pyre isn't notarized with Apple, so macOS may refuse to run it and send +you to System Settings → Privacy & Security to click "Open Anyway". + +**Installing with `curl` (above) avoids this entirely.** The quarantine +flag that triggers Gatekeeper is applied by the *downloading program* — +browsers set it, `curl` doesn't — so a curl-installed binary runs with +no prompt. + +If you already downloaded the archive in a browser, clear the flag +instead of digging through System Settings: + +```bash +xattr -d com.apple.quarantine ./pyre +``` + +`go install` is likewise unaffected, since nothing is downloaded through +a browser. + ## Verifying releases Every release is checksummed, signed, and attested at build time: @@ -143,13 +164,17 @@ connection config instead of `--insecure`. ## Navigation -Three numbered groups: `1` Monitor (dashboards), `2` Analyze (list -views), `3` Tools (config). Same number again — or `Tab` — cycles -sub-views in the group. `Ctrl+P` opens a fuzzy command palette that -jumps anywhere. +Three levels, three sets of keys: + +- `1` Monitor (dashboards), `2` Analyze (list views), `3` Tools (config) +- `Tab` / `Shift+Tab` move between views in the active group +- `[` / `]` switch sub-tabs *within* a view (Objects, Routes, Logs) + +`Ctrl+P` opens a fuzzy command palette that jumps anywhere. Inside a list view: `/` filter, `s` cycle sort, `Enter` open detail, -`r` refresh, `?` help, `q` quit. +`r` refresh, `?` help, `q` quit. Dashboards taller than your terminal +scroll with `j`/`k`. New to pyre? The **"first 60 seconds"** section of [Getting Started](docs/getting-started.md) walks the model in one diff --git a/docs/getting-started.md b/docs/getting-started.md index 921dcc2..32ba16a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -84,7 +84,13 @@ Footer shows the keys that apply right now — when in doubt, look down. Press the same number again, or `Tab`, to cycle through sub-views in that group. Try `2`, `2`, `2` to walk through Policies → NAT → Objects. -**3. Inside a list view, four keys do almost everything:** +**3. Some views have sub-tabs — those are `[` and `]`.** Objects splits +into Address / Service, Routes into Routes / Neighbors, Logs into +System / Traffic / Threat. `[` and `]` move between them and never +leave the view; `Tab` always leaves. Three levels, three sets of keys: +numbers for groups, `Tab` for views, brackets for sub-tabs. + +**4. Inside a list view, four keys do almost everything:** - `/` filter (substring match, case-insensitive) - `s` cycle sort field @@ -94,18 +100,22 @@ that group. Try `2`, `2`, `2` to walk through Policies → NAT → Objects. Try it: press `2` to land on Policies, `/web` to filter for "web", `s` to flip sort fields, `Enter` to see the full rule, `Esc` to close. -**4. `Ctrl+P` jumps anywhere.** Don't memorize keybindings. Type the +Dashboards work the same way for scrolling: if the panel stack is +taller than your terminal, `j`/`k` scroll it and an indicator at the +bottom tells you how much is left. + +**5. `Ctrl+P` jumps anywhere.** Don't memorize keybindings. Type the view name (or any partial — `obj`, `sess`, `logs`) and Enter. Same muscle memory as VS Code's command palette. -**5. Two modal pickers.** +**6. Two modal pickers.** - `:` opens the connection picker — switch between saved firewalls. - `d` opens the device picker — switch between managed devices when you're connected to a Panorama. (On a standalone firewall, `d` falls through to the view's own handlers.) -**6. `?` toggles help.** `q` or `Ctrl+C` quits. +**7. `?` toggles help.** `q` or `Ctrl+C` quits. That's the whole navigation model. Everything else is a refinement. diff --git a/docs/keybindings.md b/docs/keybindings.md index 75c9c3f..756fdf5 100644 --- a/docs/keybindings.md +++ b/docs/keybindings.md @@ -2,9 +2,16 @@ ## Navigation model -Views are organized into three numbered groups. Press a number to switch -groups; press it again (or `Tab`) to cycle through views within the -current group. +Navigation has three levels, and each level has its own keys: + +| Level | Keys | Moves between | +|-------|-------------------|----------------------------------------| +| 1 | `1` / `2` / `3` | Groups — Monitor, Analyze, Tools | +| 2 | `Tab` / `Shift+Tab` | Views within the active group | +| 3 | `[` / `]` | Sub-tabs *within* a view | + +Level 1 — press a number to switch groups; press it again to cycle +through views in that group. | Key | Group | Views | |-----|---------|-------------------------------------------------------------------------------------| @@ -12,9 +19,24 @@ current group. | `2` | Analyze | Policies · NAT · Objects · Sessions · Interfaces · Routes · IPSec · GP Users · Logs | | `3` | Tools | Config | +Level 3 applies only to the views that have sub-tabs — Objects +(Address / Service), Routes (Routes / Neighbors) and Logs (System / +Traffic / Threat). `[` and `]` never leave the current view, and `Tab` +always does, in every view. + The header shows the group tabs on top and the sub-tabs for the active group underneath. +## Pasting + +Bracketed paste is supported wherever text is entered: the login form, +the connection form, the command palette, and any view's `/` filter +input while it is focused. Use your terminal's normal paste shortcut +(`Cmd+V` on macOS, `Ctrl+Shift+V` on most Linux terminals). + +Outside a text input, paste is ignored rather than being interpreted as +a burst of navigation keys. + ## Filter-mode guard (M8) When any view's `/` filter input is focused, **all keys except `Ctrl+C` @@ -31,6 +53,7 @@ Active only when a main view is displayed and no filter input is focused. | `1` / `2` / `3` | Switch (or cycle within) navigation group | | `Tab` | Next view in the current group | | `Shift+Tab` | Previous view in the current group | +| `[` / `]` | Previous / next sub-tab (views that have them) | | `Ctrl+P` | Command palette — fuzzy jump anywhere | | `:` | Connection picker (switch between firewalls) | | `d` | Device picker (Panorama only; falls through to view on standalone firewall) | @@ -53,6 +76,18 @@ Logs, Interfaces, …). | `Ctrl+U` / `PgUp` | Page up | | `Enter` | Toggle detail panel | +## Dashboard scrolling + +Dashboards render a stack of panels that is often taller than the +terminal. When it doesn't fit, the visible portion is windowed and a +scroll indicator appears at the bottom (`↓ 15 more j/k scroll`). + +The same keys as table navigation apply — `j`/`k`, `g`/`G`, +`Ctrl+D`/`Ctrl+U`, `PgDn`/`PgUp` — and they scroll whichever dashboard +is on screen. Switching dashboards resets the scroll position to the +top. A dashboard that already fits is never trimmed and shows no +indicator. + ## Filter | Key | Action | @@ -88,11 +123,11 @@ GP Users) and Logs. ### Objects (group 2) -| Key | Action | -|---------|---------------------------------------------------------------------| -| `Tab` | Cycle Address ↔ Service tab | -| `a` | Jump to Address tab | -| `s` | Jump to Service tab | +| Key | Action | +|-------------|-----------------------------------------------------------------| +| `[` / `]` | Cycle Address ↔ Service tab | +| `a` | Jump to Address tab | +| `s` | Jump to Service tab | | `S` | Cycle sort field for the active tab (always resets to ascending) | | `/` | Enter filter mode for the active tab | | `Enter` | Toggle detail panel for the selected object | @@ -221,5 +256,21 @@ While the delete confirmation is shown: | `Shift+Tab` | Previous field | | `Space` | Toggle insecure-skip-verify checkbox | | `Enter` | Submit (when all required fields are filled) | -| `Esc` | Return to Connection Hub; form buffers are cleared | +| `Esc` | Cancel; return to Connection Hub, clearing buffers | | `Ctrl+C` | Quit | + +While authenticating, the form shows a spinner and +`Authenticating…`, and the fields are frozen: + +| Key | Action | +|----------|---------------------------------------------------------------| +| `Enter` | **Ignored** — see below | +| `Esc` | Cancel the in-flight login and return to the Connection Hub | +| `Ctrl+C` | Quit | + +> [!IMPORTANT] +> Enter is deliberately ignored while a login is in flight. PAN-OS +> counts every keygen request as a login attempt, so repeatedly pressing +> Enter while waiting on an MFA push burns through the failed-attempt +> budget and locks the account out. Wait for the MFA prompt, or press +> `Esc` to cancel. diff --git a/docs/views/dashboard.md b/docs/views/dashboard.md index 44b34b2..bcc93d3 100644 --- a/docs/views/dashboard.md +++ b/docs/views/dashboard.md @@ -101,4 +101,23 @@ columns (left: statistics and pending changes; right: rule analysis). ## Keys Standard navigation applies — see [keybindings.md](../keybindings.md). -View-specific: none beyond cycling sub-views with `1` / `Tab`. +Cycle sub-views with `1` (or `3` for Config) and `Tab`. + +### Scrolling + +A dashboard's panel stack is frequently taller than the terminal. When +it doesn't fit, the visible portion is windowed and a scroll indicator +appears on the last line (`↓ 15 more j/k scroll`). + +| Key | Action | +|---------------------|----------------| +| `j` / `Down` | Scroll down | +| `k` / `Up` | Scroll up | +| `Ctrl+D` / `PgDn` | Page down | +| `Ctrl+U` / `PgUp` | Page up | +| `g` / `Home` | Jump to top | +| `G` / `End` | Jump to bottom | + +Scrolling applies to whichever dashboard is on screen, and switching +dashboards resets to the top. A dashboard that already fits its terminal +is never trimmed and shows no indicator. diff --git a/docs/views/interfaces.md b/docs/views/interfaces.md index e676e0b..aec7c17 100644 --- a/docs/views/interfaces.md +++ b/docs/views/interfaces.md @@ -37,7 +37,11 @@ type, virtual router. ## Detail panel (`enter`) -Two-column layout at ≥ 100 wide; single column otherwise. Sections: +Two-column layout at ≥ 100 wide; single column otherwise. Sections are +distributed across the two columns whole — a heading always stays with +its own fields — and the split is chosen to keep the columns close in +height, so which section lands in which column varies with the data. +Sections: - **Basic Information** — State (● UP / ○ DOWN), Type, Zone, Mode, Vsys (if set). diff --git a/docs/views/objects.md b/docs/views/objects.md index 097f9b7..700ce29 100644 --- a/docs/views/objects.md +++ b/docs/views/objects.md @@ -8,12 +8,17 @@ Two sub-tabs with independent filter, sort, cursor, and detail state: | Key | Action | |-----|--------| -| `Tab` | Cycle Address ↔ Service | +| `[` / `]` | Cycle Address ↔ Service | | `a` | Jump to Address tab | | `s` | Jump to Service tab | The header shows `[Address] Service` or `Address [Service]` with a -`(a/s/Tab to switch)` hint. +`([/] or a/s to switch)` hint. + +`[` and `]` are the sub-tab keys across every view that has sub-tabs +(see [Navigation model](../keybindings.md#navigation-model)). `Tab` is +reserved for moving to the next view in the Analyze group, so it leaves +Objects rather than switching tabs within it. ## Address tab diff --git a/internal/tui/dashboard_scroll.go b/internal/tui/dashboard_scroll.go index c020c91..c95c430 100644 --- a/internal/tui/dashboard_scroll.go +++ b/internal/tui/dashboard_scroll.go @@ -1,6 +1,7 @@ package tui import ( + "charm.land/bubbles/v2/key" tea "charm.land/bubbletea/v2" "github.com/jp2195/pyre/internal/tui/views" @@ -51,22 +52,26 @@ func (m Model) resetDashboardScroll() Model { // dashboardScrollDelta maps a key press to a scroll distance in lines, // reporting false when the key is not a scroll key. +// +// Bindings come from the shared KeyMap rather than literal strings so +// dashboard scrolling stays identical to table navigation — j/k, g/G, +// Ctrl+D/Ctrl+U and the page keys all behave the same in both places. func (m Model) dashboardScrollDelta(msg tea.KeyPressMsg) (int, bool) { // One screenful minus a line of overlap for context. page := max(m.height-5, 1) - switch msg.String() { - case "j", "down": + switch { + case key.Matches(msg, m.keys.Down): return 1, true - case "k", "up": + case key.Matches(msg, m.keys.Up): return -1, true - case "pgdown", "ctrl+f": + case key.Matches(msg, m.keys.PageDown): return page, true - case "pgup", "ctrl+b": + case key.Matches(msg, m.keys.PageUp): return -page, true - case "G", "end": + case key.Matches(msg, m.keys.End): return scrollJump, true - case "home": + case key.Matches(msg, m.keys.Home): return -scrollJump, true } return 0, false diff --git a/internal/tui/dashboard_scroll_test.go b/internal/tui/dashboard_scroll_test.go new file mode 100644 index 0000000..07224d9 --- /dev/null +++ b/internal/tui/dashboard_scroll_test.go @@ -0,0 +1,48 @@ +package tui + +import ( + "testing" + + tea "charm.land/bubbletea/v2" +) + +// TestDashboardScrollKeysMatchTableNavigation pins that scrolling a dashboard +// uses the same bindings as scrolling a table. They were briefly divergent +// (Ctrl+F/Ctrl+B, and no `g`), which would have been an odd thing to have to +// document. +func TestDashboardScrollKeysMatchTableNavigation(t *testing.T) { + m := newTestModel(t, ViewDashboard) + m.height = 40 + + cases := []struct { + name string + key tea.KeyPressMsg + want int + }{ + {"j", tea.KeyPressMsg{Code: 'j', Text: "j"}, 1}, + {"down", tea.KeyPressMsg{Code: tea.KeyDown}, 1}, + {"k", tea.KeyPressMsg{Code: 'k', Text: "k"}, -1}, + {"up", tea.KeyPressMsg{Code: tea.KeyUp}, -1}, + {"ctrl+d", tea.KeyPressMsg{Code: 'd', Mod: tea.ModCtrl}, 35}, + {"ctrl+u", tea.KeyPressMsg{Code: 'u', Mod: tea.ModCtrl}, -35}, + {"g", tea.KeyPressMsg{Code: 'g', Text: "g"}, -scrollJump}, + {"G", tea.KeyPressMsg{Code: 'G', Text: "G"}, scrollJump}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := m.dashboardScrollDelta(tc.key) + if !ok { + t.Fatalf("%s is not recognised as a scroll key", tc.name) + } + if got != tc.want { + t.Errorf("%s delta = %d, want %d", tc.name, got, tc.want) + } + }) + } + + // A non-scroll key must fall through to the view. + if _, ok := m.dashboardScrollDelta(tea.KeyPressMsg{Code: 'x', Text: "x"}); ok { + t.Error("'x' should not be treated as a scroll key") + } +}