diff --git a/README.md b/README.md index 646b09c..95fb0c7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # tihole +[![CI](https://github.com/z19r/tihole/actions/workflows/ci.yml/badge.svg)](https://github.com/z19r/tihole/actions/workflows/ci.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/z19r/tihole.svg)](https://pkg.go.dev/github.com/z19r/tihole) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +![Go 1.25+](https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go&logoColor=white) + A fast, keyboard-driven terminal UI for [Pi-hole v6](https://pi-hole.net/), built in Go on the [Charm](https://charm.sh/) v2 stack (Bubble Tea, Bubbles, Lip Gloss). It aims for feature parity with the Pi-hole web admin — query log, @@ -28,8 +33,9 @@ tools — without leaving your terminal. change themes, or switch instances. - **Help overlay** (`?`) — context-aware cheat-sheet of global and per-screen keys. -- **Themes** — `deep-night`, `light-luxury`, `pihole-classic`, plus automatic - adoption of your [Omarchy](https://omarchy.org/) theme when present. +- **Themes** — the signature **Gloss** default (multi-stop gradient treatment), + plus `deep-night`, `light-luxury`, `pihole-classic`, and automatic adoption of + your [Omarchy](https://omarchy.org/) theme when present. ## Install @@ -67,16 +73,19 @@ also open the editor any time with `tihole config` or from the command palette | Key | Action | | --- | --- | | `1`–`9` | Jump to a screen by number | -| `tab` / `shift+tab` | Cycle screens forward / back | -| `↑↓` / `j` `k` | Move within a list | +| `↑↓` / `j` `k` | Move the selection (screens on the rail, rows in a panel) | +| `enter` / `→` / `tab` | Descend from the sidebar into the active screen | +| `esc` | Climb back to the sidebar | | `ctrl+k` | Command palette | | `s` | Switch to the next instance | -| `space` | Toggle blocking | +| `d` | Toggle blocking | +| `ctrl+t` | Cycle theme | | `?` | Help overlay | | `q` / `ctrl+c` | Quit | Per-screen actions (add `a`, edit `e`, delete `x`, refresh `r`, …) are shown in -the help bar and the `?` overlay. +the help bar and the `?` overlay. See [`docs/keys.md`](docs/keys.md) for the +complete reference and an explanation of the two-zone focus model. ## Configuration @@ -123,9 +132,18 @@ go test -cover ./internal/... # with coverage The codebase separates concerns strictly: the domain packages (`internal/pihole`, `internal/config`, `internal/theme`) never import the TUI, and screens receive -their dependencies through a shared `core.AppContext`. See -`docs/CHARM_V2_API.md` for Charm v2 specifics and -`docs/superpowers/specs/2026-07-23-tihole-design.md` for the design spec. +their dependencies through a shared `core.AppContext`. + +### Documentation + +- [`docs/architecture.md`](docs/architecture.md) — package layout, dependency + injection, the `Screen` contract, and message flow. +- [`docs/keys.md`](docs/keys.md) — complete key reference and the focus model. +- [`docs/troubleshooting.md`](docs/troubleshooting.md) — config, TLS, auth, and + destructive-action issues. +- [`docs/CHARM_V2_API.md`](docs/CHARM_V2_API.md) — Charm v2 specifics. +- [`docs/superpowers/specs/2026-07-23-tihole-design.md`](docs/superpowers/specs/2026-07-23-tihole-design.md) + — the original design spec. ## License diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..09ee4ac --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,111 @@ +# Architecture + +tihole is a Bubble Tea (Charm v2) terminal UI over the Pi-hole v6 REST API. The +guiding rule is a strict one-way dependency: **the domain talks to Pi-hole and +knows nothing about the UI; the UI depends on the domain and never the reverse.** + +## Package layout + +``` +cmd/tihole/ entrypoint, CLI subcommands (config, changelog-sync, help) +internal/ +├── pihole/ Pi-hole v6 API client — the only package that speaks HTTP +├── config/ config.yaml load/save/validate (~/.config/tihole) +├── theme/ semantic color themes + Omarchy desktop-theme adapter +└── tui/ everything Bubble Tea + ├── core/ shared contracts: AppContext, Screen, KeyMap, nav, messages + ├── components/ reusable widgets: sidebar, statusbar, palette, help, + │ section tabs, splash, gradient, confirm, table + └── screens/ one package per screen (dashboard, querylog, domains, …) +``` + +The three domain packages — `pihole`, `config`, `theme` — **never import +`internal/tui`.** That boundary is what keeps the client and config testable in +isolation and the TUI swappable. + +## Dependency injection: `AppContext` + +There are no package-level globals. Every screen is constructed with a shared +[`core.AppContext`](../internal/tui/core/context.go), the single injected +dependency set: + +```go +type AppContext struct { + API *pihole.Client // active instance's client; swapped on instance switch + Theme *theme.Theme // pointer, so a live re-theme is instant everywhere + Keys KeyMap // global key map + InstanceName string // currently active instance + Config *config.Config // pointer, shared with the Settings screen + ConfigPath string // where Config persists (0600) +} +``` + +`Theme` and `Config` are held behind pointers deliberately: a theme change or an +edit to the instance list is visible to every screen immediately, without +re-plumbing anything. + +## The `Screen` contract + +Every content screen implements [`core.Screen`](../internal/tui/core/nav.go), +which extends `tea.Model` with lifecycle hooks the root uses to manage focus and +pollers: + +```go +type Screen interface { + tea.Model + Title() string // shown in the header / status bar + Focus() tea.Cmd // becoming active: start pollers, fetch + Blur() // leaving: cancel in-flight work + Help() []key.Binding // screen-local bindings for the help bar + SetSize(w, h int) // inner content area (chrome already subtracted) +} +``` + +Two optional capabilities refine behavior: + +- **`InputCapturer`** — a screen returns `true` while a text field is focused, so + the root delivers raw keys to it instead of firing single-key globals. +- **`PanelInteractor`** — a screen returns `false` when it has no actionable + content (e.g. the read-only Dashboard), so the rail refuses to descend into it. + +## The root model and message flow + +[`internal/tui/app.go`](../internal/tui/app.go) holds the root `AppModel`: it +owns the sidebar, status bar, palette, help overlay, the boot splash, and the +map of constructed screens. Its `Update` implements the two-zone focus model +(see [keys.md](keys.md)) and routes cross-cutting messages defined in `core`: + +- `NavigateMsg` — switch to another page (blurs the old screen, focuses the new). +- `SetThemeMsg` — live re-theme across all screens. +- `SwitchInstanceMsg` — activate a different configured instance. +- `InstancesChangedMsg` — Settings edited the instance list; swap in new config. +- `ErrorMsg` — surface a non-fatal error as an inline banner instead of crashing. + +Screens communicate outward by returning these messages as `tea.Cmd`s; they +never reach into the root or each other directly. + +## The Pi-hole client + +[`internal/pihole`](../internal/pihole) is the sole HTTP boundary. It manages a +Pi-hole v6 session via the `X-FTL-SID` header, re-authenticating transparently +when a session expires and logging out on exit so it doesn't leak a session seat. +Passwords and session IDs are never logged. Destructive endpoints (restart DNS, +flush logs/network) return a `403 APIError` unless the server has +`webserver.api.allow_destructive` enabled; the UI surfaces that as a clear hint +rather than a stack trace. + +## Themes + +[`internal/theme`](../internal/theme) defines themes as semantic tokens rather +than raw colors, so screens ask for `Surface`/`Accent`/`Muted` and the active +theme resolves them. Built-ins: the signature **Gloss** default (multi-stop +gradient treatment), `deep-night`, `light-luxury`, and `pihole-classic`. When +Omarchy is present, `omarchy` adapts your desktop theme by mapping its Alacritty +ANSI palette onto tihole's tokens; if that load fails it falls back to +`deep-night` rather than erroring. + +## Further reading + +- [`docs/CHARM_V2_API.md`](CHARM_V2_API.md) — Charm v2 specifics this codebase relies on. +- [`docs/keys.md`](keys.md) — the full key reference and focus model. +- [`docs/troubleshooting.md`](troubleshooting.md) — config, TLS, auth, and destructive-action issues. diff --git a/docs/keys.md b/docs/keys.md new file mode 100644 index 0000000..9f3d593 --- /dev/null +++ b/docs/keys.md @@ -0,0 +1,95 @@ +# Key Bindings + +tihole is fully keyboard-driven. This is the complete reference. The compact +help bar at the bottom of the screen always shows the most relevant keys, and +`?` opens a context-aware overlay with the global keys plus whatever the active +screen adds. + +## The focus model + +tihole borrows an [Omarchy](https://omarchy.org/)-style two-zone focus model. +At any moment input belongs to one of two zones: + +- **Rail** — the sidebar of screens down the left. This is where focus starts. +- **Panel** — the content of the active screen (a table, a form, a log tail). + +You move the selection on the rail, then *descend* into the panel to act on its +contents, and press `esc` to *climb* back to the rail. A handful of keys are +**global** and work in both zones; everything else is scoped to the zone that +owns input, so a screen's keys can never be ambushed by a global shortcut and +vice-versa. + +## Global keys (both zones) + +These are alive everywhere — on the rail, inside a panel, on any screen. + +| Key | Action | +| --- | --- | +| `ctrl+k` | Open the command palette (fuzzy-jump to any screen, toggle blocking, change theme, switch instance) | +| `s` | Switch to the next configured instance | +| `d` | Toggle Pi-hole blocking on the active instance | +| `ctrl+t` | Cycle to the next theme | +| `i` | Replay the ASCII boot splash | +| `?` | Toggle the help overlay | +| `q` / `ctrl+c` | Quit | + +> **Note:** while a text field is focused (adding or editing a record), single-key +> globals are suppressed and the keystroke goes to the field instead — so typing +> `s` or `d` into a form does what you'd expect. `ctrl+c` still quits. + +## On the rail + +The rail owns input on startup and whenever you `esc` out of a panel. It never +forwards keys to the screen. + +| Key | Action | +| --- | --- | +| `↑` / `k` | Move selection to the previous screen | +| `↓` / `j` | Move selection to the next screen | +| `1`–`9` | Jump straight to a screen by its sidebar position | +| `enter` / `→` / `l` / `tab` | Descend into the panel (interactive screens only) | +| `r` | Refresh / refetch the active screen | + +Read-only screens (like the Dashboard) opt out of panel focus, so `enter`/`→`/ +`tab` stay on the rail instead of trapping you in a panel where nothing responds. + +## In a panel + +Once you've descended, the screen owns input. Only one key is reserved: + +| Key | Action | +| --- | --- | +| `esc` | Climb back to the rail | + +Everything else falls through to the active screen. The common list/table keys: + +| Key | Action | +| --- | --- | +| `↑↓` / `j` `k` | Move within the list | +| `enter` | Select / open the highlighted row | + +### Per-screen actions + +Management screens (Domains, Groups, Clients, Adlists, Local DNS, …) add their +own bindings on top. The conventional set: + +| Key | Action | +| --- | --- | +| `a` | Add a new record | +| `e` | Edit the highlighted record | +| `x` | Delete the highlighted record | +| `r` | Refresh the list | + +The Query Log adds one-key allow/block on the highlighted query. The exact set +for the screen you're on is always shown in the help bar and the `?` overlay, +which is the source of truth — this table documents the convention, not an +exhaustive per-screen list. + +## Command palette (`ctrl+k`) + +The palette is the fastest way around. Open it from anywhere and fuzzy-type: + +- a screen name to jump there, +- `blocking` to toggle blocking, +- a theme name to switch themes, +- an instance name to switch instances. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..95a00d1 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,113 @@ +# Troubleshooting + +tihole is designed to fail *soft*: a wrong address or password never aborts +startup. Authentication happens lazily, so connection problems show up as in-app +error banners, and a structurally broken config drops you straight into the +config editor to fix it. Most issues below are diagnosed and fixed without ever +leaving the app. + +## Config file + +Config lives at `~/.config/tihole/config.yaml` (more precisely +`/tihole/config.yaml`), created mode `0600` in a `0700` +directory. It's written and edited by the app, but you can hand-author it — see +the [README](../README.md#configuration) for the full schema. + +Open the editor any time with `tihole config`, or from the command palette +(`ctrl+k` → Settings → Connections). + +### "no instances configured" / "active does not match any instance" + +The config failed structural validation. tihole checks that: + +- at least one instance is configured, +- every instance has a non-empty, **unique** `name`, +- every instance `url` parses as an absolute `http`/`https` URL with a host, +- `active` names exactly one existing instance. + +All problems are reported together. Fix them in the config editor (`tihole +config`) — it opens automatically when validation fails. + +### "instance has no password or password_env" / "password_env is empty or unset" + +An instance needs an app password supplied one of two ways: + +```yaml +instances: + - name: home + url: https://pi.hole + password_env: TIHOLE_HOME_PASSWORD # preferred: read from the environment + - name: cabin + url: http://10.0.0.53 + password: inline-ok-but-env-is-better +``` + +If you use `password_env`, make sure the named variable is actually exported in +the environment tihole runs in (`export TIHOLE_HOME_PASSWORD=…`). An unset or +empty variable produces the "empty or unset" error. + +## Connection & authentication + +### Login fails / "authentication" banner + +tihole authenticates lazily using Pi-hole v6's `X-FTL-SID` session header, and +re-authenticates transparently when a session expires. A persistent auth banner +almost always means the **app password is wrong** or points at the wrong +instance. Check: + +- The password (or the value of `password_env`) matches the Pi-hole admin + **app password**, not your web-UI login if they differ. +- `url` points at the right host and scheme. + +Passwords and session IDs are never logged, so you won't find them in any output +— that's by design. + +### TLS certificate errors (self-signed Pi-hole) + +If your Pi-hole uses a self-signed certificate or no TLS, set `verify_tls: false` +on that instance: + +```yaml + - name: cabin + url: https://10.0.0.53 + verify_tls: false +``` + +`verify_tls` defaults to `true` when omitted. Only disable it for instances you +control on a trusted network. + +## Destructive actions rejected (403) + +Some actions — **restart DNS**, **flush logs**, **flush network table** — are +destructive and require `webserver.api.allow_destructive` to be enabled on the +Pi-hole server. Without it, the API returns `403` and tihole surfaces a clear +hint. Enable the setting in the Pi-hole admin (Settings → System / API) or via +the FTL config, then retry. + +## Display & rendering + +### Misaligned sidebar / broken box drawing + +tihole uses single-cell BMP glyphs for its sidebar icons specifically to avoid +the double-width emoji-presentation problem. If labels still look misaligned, +the culprit is usually the **terminal font or an ambiguous-width setting** — +ensure a font with proper box-drawing coverage and that ambiguous-width +characters are treated as single-width. + +### Colors look wrong / theme not applying + +- Cycle themes with `ctrl+t` or pick one from the palette (`ctrl+k`) to confirm + theming works at all. +- The `omarchy` theme reads `~/.config/omarchy/current/theme/alacritty.toml`; if + that file is missing or malformed, tihole falls back to `deep-night` rather + than erroring. +- Full-color themes need a true-color (24-bit) terminal. In a 256-color terminal + gradients degrade; set `COLORTERM=truecolor` if your terminal supports it but + doesn't advertise it. + +## Still stuck? + +- `tihole help` — usage. +- [`docs/keys.md`](keys.md) — full key reference. +- [`docs/architecture.md`](architecture.md) — how the pieces fit together. +- Open an issue at .