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
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
111 changes: 111 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -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.
95 changes: 95 additions & 0 deletions docs/keys.md
Original file line number Diff line number Diff line change
@@ -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.
113 changes: 113 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -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
`<UserConfigDir>/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 <https://github.com/z19r/tihole/issues>.
Loading