Documentation
¶
Overview ¶
Package keys is the single source of truth for every keybinding in the app.
Before this package, a key lived in two places: the component that handled it, and the hand-written list in the footer bar that advertised it. Nothing held the two together, so the bar could promise a key that no handler implemented, or stay silent about one that did.
Two rules follow from that, and they are the reason this package exists:
- A key is declared here exactly once. Components match against these bindings; the footer and the help overlay render from them.
- One verb is one binding. Start is the same key on the group panel and the service panel because both read Details.Start - not because two switch statements happen to agree.
The bindings carry their own help text, which is what the footer prints.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Backup = BackupKeys{ Navigate: key.NewBinding(key.WithHelp("↑/↓", "navigate")), Restore: key.NewBinding(key.WithKeys("enter", "r"), key.WithHelp("r", "restore")), }
var Details = DetailsKeys{ Start: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "start")), Stop: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "stop")), Restart: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "restart")), Pull: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "pull")), Remove: key.NewBinding(key.WithKeys("x"), key.WithHelp("x", "remove")), Logs: key.NewBinding(key.WithKeys("L"), key.WithHelp("L", "logs")), EditService: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")), EditFile: key.NewBinding(key.WithKeys("E"), key.WithHelp("E", "file")), Save: key.NewBinding(key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "save")), OpenEditor: key.NewBinding(key.WithKeys("ctrl+o"), key.WithHelp("ctrl+o", "editor")), CopyURL: key.NewBinding(key.WithKeys("y"), key.WithHelp("y", "copy url")), Healthcheck: key.NewBinding(key.WithKeys("H"), key.WithHelp("H", "healthcheck")), Boot: key.NewBinding(key.WithKeys("B"), key.WithHelp("B", "boot")), }
var Editor = EditorKeys{ NewLine: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "new line")), Indent: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "indent")), Outdent: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "outdent")), }
var Files = FilesKeys{ Scroll: key.NewBinding(key.WithHelp("↑/↓", "scroll")), Browse: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "browse")), }
var Global = GlobalKeys{ NextPanel: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next")), PrevPanel: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev")), Quit: key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit")), ForceQuit: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "force quit")), Page: key.NewBinding( key.WithHelp(fmt.Sprintf("1-%d", len(apptypes.PageTitles)), "page"), ), NextPage: key.NewBinding(key.WithKeys("]"), key.WithHelp("]", "next page")), PrevPage: key.NewBinding(key.WithKeys("["), key.WithHelp("[", "prev page")), Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")), Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), About: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "about")), Theme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "theme")), Usage: key.NewBinding(key.WithKeys("u"), key.WithHelp("u", "usage")), EditEnv: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "env")), }
var List = ListKeys{ Navigate: key.NewBinding(key.WithHelp("↑/↓", "navigate")), New: key.NewBinding(key.WithKeys("n"), key.WithHelp("n", "new")), Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")), Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")), Rename: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rename")), AdoptUngrouped: key.NewBinding(key.WithKeys("A"), key.WithHelp("A", "adopt")), ReleaseUngrouped: key.NewBinding(key.WithKeys("A"), key.WithHelp("A", "release")), Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")), ClearFilter: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "clear filter")), ApplyFilter: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "apply")), CancelFilter: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "cancel")), GoToStart: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("g", "first row")), GoToEnd: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("G", "last row")), }
var Overlay = OverlayKeys{ Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")), Cancel: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "cancel")), NextField: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next field")), Toggle: key.NewBinding(key.WithKeys("space"), key.WithHelp("space", "toggle")), Yes: key.NewBinding(key.WithKeys("y", "Y"), key.WithHelp("y", "yes")), No: key.NewBinding(key.WithKeys("n", "N"), key.WithHelp("n", "no")), Follow: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "follow")), Navigation: key.NewBinding(key.WithKeys("up", "down", "k", "j"), key.WithHelp("↑/↓", "navigate")), }
EnvKeys act on the Env page's key/value table. Reveal, Copy and RawEdit are new to this page; New, Edit, Delete and EditFile are reused from existing bindings to keep "one verb is one binding".
Functions ¶
func Active ¶
Active returns the bindings the user can press right now, in the order they should be shown.
It returns a filtered slice rather than disabling bindings in place: key.Binding.Enabled gates matching as well as help, and these bindings are package-level values shared with the components, so disabling one to tidy the footer would stop the key working everywhere.
Selection is automatic on cursor move, so there is no left/right panel focus abstraction: the page and the selection state alone decide the bindings. Tab is dead on body pages - overlays and the editor handle their own tab.
func Globals ¶
Globals are the always-available keys the footer pins to its right-hand side, away from the context-dependent ones.
func ListKeyMap ¶
ListKeyMap is the keymap the two body lists install on their inner bubbles list, replacing list.DefaultKeyMap.
The default map is written for a list that is the whole program, so it claims keys this app spends elsewhere: d and f page forward while d deletes a group, h, l, b and u page while l opens logs, and q, esc and ? are the app's. The results were visible - pressing d both opened the delete confirm and paged the list backwards - so the list has to be told which keys are not its own.
What stays is what only the list can answer: where its cursor is, and the filter. Filtering is worth keeping with forty services, and while it is active the list owns the keyboard the way a modal does.
Keys the app owns are left with no keystrokes rather than removed, because list.Model reads every field: an empty binding matches nothing, which is the intent, whereas a missing one would be a nil-safe accident.
Types ¶
type BackupKeys ¶ added in v0.4.0
type BackupKeys struct {
Navigate key.Binding
// Restore writes the selected .bak back over the live file. enter or r;
// r is chosen so it never collides with the service panel's lowercase r
// (restart) - the Backups page is the only context this binding is live.
Restore key.Binding
}
BackupKeys act on the Backups page's version list: navigating the list and requesting a restore of the selected copy. Reveal/copy/edit live in the env modal, not here.
type Context ¶
type Context struct {
Page string
// ListEmpty reports whether the body list has any rows to act on. An
// empty list offers no selection-dependent verbs.
ListEmpty bool
// Selected reports whether the panel has a subject to act on - a chosen
// group on Home, a chosen service on Services. Without one, the action
// keys do nothing and are not offered.
Selected bool
// ReadOnlyGroup is true when the selected group is the reserved
// apptypes.UngroupedGroup row. Its membership is derived and it has no
// profile tag, so the list-management verbs are not offered on it - the
// docker verbs still are.
ReadOnlyGroup bool
// UngroupedMaterialized is true when the reserved ungrouped row is backed
// by a written profile tag rather than derived. It decides whether the
// ungrouped row's 'A' verb is adopt or release.
UngroupedMaterialized bool
// Editing is true when the service details panel is in inline edit mode.
// The editor owns the keyboard, so the panel's action keys and the page
// digits are dead; the footer shows the editor-specific keys instead.
Editing bool
// PendingAction is true when a docker action is running. Action keys are
// disabled and a spinner is shown in the panel.
PendingAction bool
// Filter is the focused list's filter state. Its zero value is
// list.Unfiltered, so a caller that has no list to report about gets the
// ordinary keys.
Filter list.FilterState
}
Context is what the footer knows about the screen: enough to decide which bindings are live, and nothing more.
type DetailsKeys ¶
type DetailsKeys struct {
Start key.Binding
Stop key.Binding
Restart key.Binding
Pull key.Binding
Remove key.Binding
Logs key.Binding
EditService key.Binding
EditFile key.Binding
Save key.Binding
OpenEditor key.Binding
// CopyURL exists only on the service panel, alongside EditService and
// EditFile - the group panel has no single URL to copy.
CopyURL key.Binding
// Healthcheck opens the template picker (docs/plans/healthcheck-insertion.md).
Healthcheck key.Binding
// Boot cycles the service's restart: policy (no -> on-failure ->
// unless-stopped -> always -> no). Uppercase, alongside Healthcheck's
// uppercase H, to leave b free for Files.Browse - the same shape as
// EditFile's E next to EditService's e.
Boot key.Binding
}
DetailsKeys act on whatever the body's right panel is showing. The first six are shared verbatim between the group panel and the service panel: same key, same meaning, one scope wider or narrower. EditService, EditFile, CopyURL, Healthcheck and Boot exist only on the service panel, which is the only place a single service is the subject. Save and OpenEditor are only live while the inline editor is open.
type EditorKeys ¶
EditorKeys act inside the inline YAML editor, and only there. The editor owns the whole keyboard while it is open (see detailspanel.Model.OwnsKeyboard), which is what makes tab and shift+tab available here at all - they are the panel-switching keys everywhere else, and the app stands down from them while the editor holds the keyboard.
type Entry ¶
Entry is one row of the help overlay: the binding to render, and whether the user can press it right now. Rows that cannot be pressed are dimmed.
type FilesKeys ¶
FilesKeys act on the Files page's read-only file viewer. Scroll is the viewport's own (the viewport answers the keystrokes); it is declared here so the footer and the help overlay advertise it from the same place as everything else - the same pattern as the list's navigation keys. The viewer's edit key is Details.EditFile, reused rather than redeclared.
type GlobalKeys ¶
type GlobalKeys struct {
NextPanel key.Binding
PrevPanel key.Binding
// Quit is q, and it is the one global key that yields: a modal or a
// filtering list needs the letter for typing. ForceQuit is separate from it
// precisely so that ctrl+c yields to nothing.
Quit key.Binding
ForceQuit key.Binding
// Back is esc away from everything that has a stronger claim on it: a
// modal, a filter being typed, a filter standing on a focused list. What
// is left is the details panel, so esc there means "back to the list" -
// which is why the footer offers it in the details contexts and nowhere
// else. See model.AppModel.escKept.
Back key.Binding
// Help opens the help overlay. The overlay renders from this package, so
// what it says is what the handlers do.
Help key.Binding
// About opens the About modal: the brand mark, version, license and repo
// link. A read-only overlay like Help, closed by the same three keys.
About key.Binding
// Theme opens the theme picker: a list of registered themes with live
// preview on cursor movement and persist-on-confirm. T (shift+t) so it
// does not collide with the details panel's lowercase t (stop).
Theme key.Binding
// Usage opens the usage overlay: disk and memory usage bars.
Usage key.Binding
// EditEnv opens the env modal: the .env variable table, editor, and
// raw edit, all inside one modal. v (mnemonic: variables), free at the
// global tier now that the old Env page's v was only its reveal key.
EditEnv key.Binding
// Page is advertised but not matched: the digits are recognised by their
// key code and the alt+<letter> alias by its modifier, so that 1 as filter
// text and alt+shift+g are both left alone. See model.pageForNavKey. The
// bracket pair steps through the pages in order; it is not in the footer's
// global group for width, but lives here so the help overlay renders it
// from the same place as everything else.
Page key.Binding
NextPage key.Binding
PrevPage key.Binding
}
GlobalKeys work anywhere that no overlay owns the keyboard.
type ListKeys ¶
type ListKeys struct {
New key.Binding
Edit key.Binding
Delete key.Binding
// R renames the highlighted group. Uppercase so it does not collide with
// the details panel's lowercase r (restart); the same shape as E next to e
// on the services panel.
Rename key.Binding
// A on the reserved ungrouped row toggles its materialization: adopt
// writes profiles: [ungrouped] onto every profile-less service, release
// removes it. One key, two faces - the footer shows the state-appropriate
// one (see Context.UngroupedMaterialized), the handler matches both.
AdoptUngrouped key.Binding
ReleaseUngrouped key.Binding
Filter key.Binding
ClearFilter key.Binding
ApplyFilter key.Binding
CancelFilter key.Binding
GoToStart key.Binding
GoToEnd key.Binding
}
ListKeys act on the body's left panel: the groups list and the services list. New, Edit and Delete only mean something on the groups list, which is the only list whose contents the app can modify. The services list is read-only; its services are created by editing the compose file.
Filter, ClearFilter, GoToStart and GoToEnd belong to the bubbles list rather than to a handler here, and are declared anyway so the footer and the help overlay advertise them from the same place as everything else. See ListKeyMap.
type OverlayKeys ¶
type OverlayKeys struct {
Submit key.Binding
Cancel key.Binding
NextField key.Binding
Toggle key.Binding
Yes key.Binding
No key.Binding
Follow key.Binding
// overlay already means "move within me" with, matched directly since
// the overlay has no list to hand the keystrokes to.
Navigation key.Binding
}
OverlayKeys are the keys every modal answers to. Cancel is one binding for every overlay in the app, including the logs viewer, so "esc backs out" needs no exceptions.