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
21 changes: 18 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<![CDATA[` marker. Parse text output (`df`, `top`) with `api.InnerText()`, never `string(resp.Result.Inner)`. Tests that feed bare text will pass while hardware fails.
- **Dashboards clamp to the terminal.** `DashboardBase.ClampToHeight` windows the panel stack and `ScrollBy` moves the offset; each dashboard's `View()` is a thin wrapper over `content()` so `ContentHeight()` can measure the untrimmed stack. Dashboard scrolling is handled in the TUI layer (`dashboard_scroll.go`), not per-dashboard `Update`, because `handleViewKeys` routes `ViewDashboard` keys to `m.dashboard` (Overview) whatever is on screen.
- **`HasData()` gates the app-wide spinner** via `Model.anyLoading()`. It must report settled only when *every* source has produced data **or** an error; returning true early drops the tick chain and freezes other panels' spinners mid-frame.
- **Table rows share one column grid.** `TableRowSelectedStyle`/`TableRowDisabledStyle` carry `Padding(0, 1)`; tables that render an unpadded header must use the `TableSelectedRowStyle()`/`TableDisabledRowStyle()` flush variants, or the selected row shifts a column and the table twitches as the cursor moves.
- **Detail panels size to `contentWidth()`**, not `m.Width` — they render inside the view's bordered, padded panel, and the raw terminal width makes their border wrap.
- **Paste is routed explicitly.** `tea.PasteMsg` is not a `KeyPressMsg`; `handlePasteMsg` forwards it to the focused input. New text inputs need a case there or paste will silently do nothing.

## Code Style

Expand Down Expand Up @@ -97,20 +104,28 @@ logger at a file.

## Go 1.26 (Current Version)

`go.mod` is pinned to `go 1.26.4`; CI pins `go-version: '1.26.4'` (the six
`go-version` lines across `.github/workflows/` plus `go.mod` move together).
The 1.26.x series has shipped three stdlib CVE patches:
`go.mod` is pinned to `go 1.26.5`; CI pins `go-version: '1.26.5'` (the
`go-version` lines across `.github/workflows/` plus `go.mod` move together —
verify the count with `grep -rc "go-version:" .github/workflows/` rather than
trusting a number written here).
The 1.26.x series has shipped these stdlib CVE patches:

- **1.26.2** — `crypto/tls` / `crypto/x509` issues from 1.26.0–1.26.1.
- **1.26.3** — GO-2026-4971 (`net.Dial` / `LookupPort` NUL-byte panic on
Windows), GO-2026-4918 (HTTP/2 infinite loop in `golang.org/x/net`).
- **1.26.4** — GO-2026-5039 (`net/textproto` error escaping) and GO-2026-5037
(`crypto/x509` hostname parsing); both reached this codebase's keygen and
TLS paths and were caught by `govulncheck`.
- **1.26.5** — current pin, bumped as part of the remediation pass (#47).

When a new patch lands, bump `go.mod` + the CI pins together and re-run
`govulncheck ./...`.

**Lint:** CI runs `golangci-lint` (version pinned in `.github/workflows/ci.yml`)
and it catches things `go vet` does not — `prealloc`, for one. Install the
pinned version and run `golangci-lint run ./...` before pushing; `go vet` +
`go test` alone will not predict CI.

**1.26 idioms this project uses:** the ones in Code Style above (`for range N`,
`for i := range N`, `max()`/`min()`, `wg.Go`), plus `reflect` `Value.Fields()`
iteration (`internal/api/sanitize.go`) and `go fix ./...` modernizers
Expand Down
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ for network engineers who want answers fast.

## Install

Download a binary from [Releases](https://github.com/jp2195/pyre/releases).
Download from [Releases](https://github.com/jp2195/pyre/releases).
Archives ship with an SPDX SBOM and a shared `checksums.txt`.

```bash
# macOS / Linux
tar -xzf pyre_<version>_<os>_<arch>.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
```
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
69 changes: 60 additions & 9 deletions docs/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,41 @@

## 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 |
|-----|---------|-------------------------------------------------------------------------------------|
| `1` | Monitor | Overview · Network · Security · VPN |
| `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`
Expand All @@ -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) |
Expand All @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
21 changes: 20 additions & 1 deletion docs/views/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 5 additions & 1 deletion docs/views/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
9 changes: 7 additions & 2 deletions docs/views/objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 12 additions & 7 deletions internal/tui/dashboard_scroll.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tui

import (
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"

"github.com/jp2195/pyre/internal/tui/views"
Expand Down Expand Up @@ -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
Expand Down
Loading