1// Package ui provides the unified interactive session for keld's TUI.
2//
3// The session runs a single [tea.Program] that coordinates navigation
4// between screens, manages the visual chrome (breadcrumb, title, help
5// bar), and handles back navigation and cancellation.
6package ui
7
8import (
9 "charm.land/bubbles/v2/key"
10 tea "charm.land/bubbletea/v2"
11)
12
13// Screen is the interface that every step in an interactive flow must
14// satisfy. The session delegates input handling and rendering to the
15// active screen without knowing its internals.
16//
17// Screens are constructed before use and may be re-activated when the
18// user navigates back. Implementations should preserve their state
19// across deactivation/reactivation cycles so previous answers are
20// visible when the user returns.
21type Screen interface {
22 // Init returns the command to run when this screen becomes active.
23 // Called both on first activation and when the user navigates back
24 // to this screen.
25 Init() tea.Cmd
26
27 // Update handles a message and returns the updated screen plus any
28 // command. The session forwards all messages to the active screen,
29 // including Esc key presses.
30 //
31 // Screens that use Esc internally (e.g. closing a filter, navigating
32 // to a parent directory) should handle it and return normally.
33 // Screens that do not need Esc should return a [BackCmd] to signal
34 // that the session should navigate back.
35 Update(msg tea.Msg) (Screen, tea.Cmd)
36
37 // View renders the screen's content. The session wraps this with
38 // chrome (breadcrumb, title, help bar), so the screen should not
39 // render those elements itself.
40 View() string
41
42 // Title returns the screen's display title, shown below the
43 // breadcrumb.
44 Title() string
45
46 // KeyBindings returns the key bindings to display in the help bar
47 // for this screen. The session adds the global bindings (Esc, Ctrl+C)
48 // automatically.
49 //
50 // The returned slice must not be modified by the caller.
51 KeyBindings() []key.Binding
52
53 // Selection returns a short string representing the user's choice
54 // on this screen, for display in the breadcrumb. Returns "" if the
55 // screen has not been completed yet.
56 Selection() string
57}
58
59// BackMsg signals that the active screen wants the session to navigate
60// back to the previous screen. Screens return this via [BackCmd] when
61// they receive an Esc press they do not handle internally.
62type BackMsg struct{}
63
64// BackCmd is a [tea.Cmd] that produces a [BackMsg]. Screens should
65// return this from their Update method when they receive an Esc press
66// and have no internal use for it.
67func BackCmd() tea.Msg {
68 return BackMsg{}
69}
70
71// DoneMsg signals that the active screen has been completed and the
72// session should advance to the next screen. Screens return this via
73// [DoneCmd] when the user confirms their selection.
74type DoneMsg struct{}
75
76// DoneCmd is a [tea.Cmd] that produces a [DoneMsg]. Screens should
77// return this from their Update method when the user makes or confirms
78// a selection.
79func DoneCmd() tea.Msg {
80 return DoneMsg{}
81}
82
83// ExtendMsg asks the session to append screens after the current
84// cursor position, replacing any screens that were already queued
85// beyond it. This allows a screen (or external code via [tea.Cmd])
86// to build the next part of the flow dynamically — for example,
87// after resolving a config to determine which command-specific
88// screens are needed.
89//
90// If Screens is empty, the message is a no-op.
91type ExtendMsg struct {
92 Screens []Screen
93}