tui

package module
v0.4.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 22, 2026 License: GPL-3.0 Imports: 20 Imported by: 1

README

config-manager-tui

Terminal user interface for Config Manager. Provides a raspi-config style interactive menu built with Bubble Tea for headless Debian-based nodes.

Features

  • Raspi-config style menu navigation (arrow keys, Enter, q to quit)
  • Dynamic plugin menu discovery from the core plugin registry
  • System info display (hostname, OS, kernel, arch, uptime)
  • Plugin-specific submenus with back-navigation (esc/q/backspace)
  • Nested submenus for interface selection (Set Static IP, Delete, Rollback)
  • Network write operations — set static IP, set DNS, delete static IP, rollback interface, rollback DNS with confirmation dialogs
  • Write-policy awareness — network write operations that are denied by interface policy display a clear, actionable message instead of a raw API error
  • Confirmation dialogs for destructive actions (updates, network changes)
  • Status bar showing hostname and uptime in the footer (main, sub-menu, and input screens)
  • Boolean config values displayed as ON / OFF for readability
  • Plugin endpoint paths normalised for clean menu descriptions
  • Theme system with centralised colours, glyphs, and badges (DefaultTheme)
  • Job history — view recent execution runs for update jobs

Documentation

Development

# lint
golangci-lint run

# test
go test ./...

CI runs automatically on push/PR to main via GitHub Actions (.github/workflows/ci.yml).

Contributing

See CONTRIBUTING.md for guidelines.

Security

The TUI applies multiple layers of defense when handling untrusted data from the Config Manager API and plugin registry:

  • Input sanitization — All API response text is passed through sanitizeText (or sanitizeBody for multi-line output) before terminal rendering. These helpers strip C0 control characters (U+0000–U+001F, U+007F), Unicode C1 control codes (U+0080–U+009F), ANSI escape sequences, and Unicode BiDi control characters (overrides, embeddings, and isolates), preventing terminal injection and text reordering attacks.
  • Path validation — Every API path built from user or plugin data is validated by validateAPIPath, which URL-decodes the path first and then checks for directory traversal sequences (including percent-encoded variants such as %2e%2e). cleanPluginPath further canonicalises plugin endpoint paths and verifies they stay under the expected route prefix.
  • Plugin name validation — Plugin identifiers are matched against validPluginName (^[a-z0-9]([a-z0-9-]*[a-z0-9])?$), rejecting any names that could be used to construct malicious API paths.
  • Interface name validation — Network interface names are matched against validIfaceName (^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,14}$), preventing path traversal, null bytes, and injection via interface name parameters in network write operations.
  • Route prefix validation — Plugin route prefixes received from the registry are decoded and checked for traversal sequences and control characters as defense-in-depth against a compromised registry.
  • Response body size limits — API responses capped at 1 MB (JSON) / 10 MB (raw) to prevent OOM on constrained devices.

For vulnerability reporting see SECURITY.md.

License

See LICENSE for details.

Documentation

Overview

Package tui provides a raspi-config style terminal user interface for Config Manager. It is built with Bubble Tea and styled with Lip Gloss.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuiltinThemeNames

func BuiltinThemeNames() []string

BuiltinThemeNames returns the sorted list of available built-in theme names.

Types

type APIClient

type APIClient struct {
	// contains filtered or unexported fields
}

APIClient calls the local CM REST API.

func NewAPIClient

func NewAPIClient(baseURL string) *APIClient

NewAPIClient returns a client pointing at the given base URL.

func NewAPIClientWithToken

func NewAPIClientWithToken(baseURL, token string) *APIClient

NewAPIClientWithToken returns a client that sends a Bearer token with every request (except unauthenticated endpoints handled server-side).

func (*APIClient) DeleteStaticIP

func (c *APIClient) DeleteStaticIP(name string, dryRun bool) (*NetworkWriteResult, error)

DeleteStaticIP removes static IP config, reverting to DHCP.

func (*APIClient) GetDNS

func (c *APIClient) GetDNS() (*DNSConfig, error)

GetDNS fetches DNS configuration.

func (*APIClient) GetJobRunLatest

func (c *APIClient) GetJobRunLatest(jobID string) (*JobRun, error)

GetJobRunLatest fetches the most recent execution record for a job. Job IDs are either dot-separated (e.g. "update.full") matching validJobID, or single-word identifiers (e.g. "cleanup") matching validPluginName.

func (*APIClient) GetNetworkInterfaces

func (c *APIClient) GetNetworkInterfaces() ([]NetworkInterface, error)

GetNetworkInterfaces lists all network interfaces.

func (*APIClient) GetNetworkStatus

func (c *APIClient) GetNetworkStatus() (*NetworkStatus, error)

GetNetworkStatus fetches overall network status.

func (*APIClient) GetNode

func (c *APIClient) GetNode() (*NodeInfo, error)

GetNode fetches system information.

func (*APIClient) GetPluginSettings

func (c *APIClient) GetPluginSettings(name string) (*PluginSettings, error)

GetPluginSettings fetches a plugin's configurable settings via the core settings endpoint (GET /api/v1/plugins/{name}/settings).

func (*APIClient) GetPlugins

func (c *APIClient) GetPlugins() ([]PluginRegistryEntry, error)

GetPlugins fetches the plugin registry.

func (*APIClient) GetRaw

func (c *APIClient) GetRaw(apiPath string) (string, error)

GetRaw fetches an arbitrary endpoint and returns its raw body string.

func (*APIClient) GetUpdateConfig

func (c *APIClient) GetUpdateConfig() (*UpdateConfig, error)

GetUpdateConfig fetches the update plugin configuration.

func (*APIClient) GetUpdateLogs

func (c *APIClient) GetUpdateLogs() (*RunStatus, error)

GetUpdateLogs fetches the last update run status.

func (*APIClient) GetUpdateStatus

func (c *APIClient) GetUpdateStatus() ([]PendingUpdate, error)

GetUpdateStatus fetches pending updates.

func (*APIClient) GoString

func (c *APIClient) GoString() string

GoString masks the bearer token for %#v formatting.

func (*APIClient) ListJobRuns

func (c *APIClient) ListJobRuns(jobID string, limit, offset int) ([]JobRun, error)

ListJobRuns fetches paginated job execution history (newest-first). Job IDs are either dot-separated (e.g. "update.full") matching validJobID, or single-word identifiers (e.g. "cleanup") matching validPluginName.

func (*APIClient) PostRaw

func (c *APIClient) PostRaw(apiPath string) (string, error)

PostRaw sends a POST to an arbitrary endpoint and returns the status message.

func (*APIClient) RollbackDNS

func (c *APIClient) RollbackDNS(dryRun bool) (*NetworkWriteResult, error)

RollbackDNS restores previous DNS configuration.

func (*APIClient) RollbackInterface

func (c *APIClient) RollbackInterface(name string, dryRun bool) (*NetworkWriteResult, error)

RollbackInterface restores previous interface configuration.

func (*APIClient) RunUpdate

func (c *APIClient) RunUpdate(mode string) (*UpdateRunResult, error)

RunUpdate triggers an update run.

func (*APIClient) SetDNS

func (c *APIClient) SetDNS(cfg DNSWriteConfig, dryRun bool) (*NetworkWriteResult, error)

SetDNS configures DNS servers.

func (*APIClient) SetStaticIP

func (c *APIClient) SetStaticIP(name string, cfg StaticIPConfig, dryRun bool) (*NetworkWriteResult, error)

SetStaticIP configures a static IP on the given interface.

func (*APIClient) String

func (c *APIClient) String() string

String masks the bearer token to prevent accidental credential leakage in logs or fmt output.

func (*APIClient) TriggerJob

func (c *APIClient) TriggerJob(jobID string) (*TriggerJobResult, error)

TriggerJob fires a job by ID via the core scheduler endpoint.

func (*APIClient) UpdatePluginSetting

func (c *APIClient) UpdatePluginSetting(name, key string, value any) (*PluginSettingsUpdateResult, error)

UpdatePluginSetting changes a single setting key via the core settings endpoint (PUT /api/v1/plugins/{name}/settings).

type APIError

type APIError struct {
	StatusCode int
	Message    string
}

APIError represents an error response from the API, preserving the HTTP status code alongside the human-readable message for programmatic handling.

func (*APIError) Error

func (e *APIError) Error() string

type ConnectionMode

type ConnectionMode int

ConnectionMode indicates how the TUI is connected to the API.

const (
	// ModeStandalone means the TUI started its own embedded API server.
	ModeStandalone ConnectionMode = iota
	// ModeConnected means the TUI is connected to an external running service.
	ModeConnected
)

type DNSConfig

type DNSConfig struct {
	Nameservers []string `json:"nameservers"`
	Search      []string `json:"search"`
}

DNSConfig represents DNS settings from /api/v1/plugins/network/dns.

type DNSWriteConfig

type DNSWriteConfig struct {
	Nameservers []string `json:"nameservers"`
	Search      []string `json:"search,omitempty"`
}

DNSWriteConfig is the request body for PUT /api/v1/plugins/network/dns.

type JobRun

type JobRun struct {
	JobID     string  `json:"job_id"`
	Status    string  `json:"status"` // "running", "completed", "failed"
	StartedAt string  `json:"started_at"`
	EndedAt   *string `json:"ended_at,omitempty"`
	Error     string  `json:"error,omitempty"`
	Duration  string  `json:"duration,omitempty"`
}

JobRun holds a job execution record returned by GET /api/v1/jobs/{id}/runs/latest and GET /api/v1/jobs/{id}/runs (paginated list).

type MenuItem struct {
	// Title is the display name shown in the menu list.
	Title string
	// Description is a short help line shown below the title.
	Description string
	// Action returns a tea.Cmd that performs the menu item's work.
	Action func() tea.Cmd
	// IsQuit exits the TUI when this item is selected.
	IsQuit bool
	// NeedsConfirm shows a confirmation dialog before executing the action.
	NeedsConfirm bool
	// ConfirmMsg is the explanatory text shown in the confirmation dialog.
	ConfirmMsg string
}

MenuItem represents a single entry in a TUI menu.

func MainMenu(plugins []PluginInfo) []MenuItem

MainMenu is a legacy static menu builder kept for backward compatibility. It returns menu items without plugin actions wired; only Quit has an action.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the main Bubble Tea model for the Config Manager TUI.

func New

func New(plugins []PluginInfo) Model

New returns an initialised TUI model with default API URL. Prefer NewWithAPI when the caller knows the configured host/port.

func NewWithAPI

func NewWithAPI(plugins []PluginInfo, apiBaseURL string) Model

NewWithAPI returns an initialised TUI model using the given API base URL.

func NewWithAuth

func NewWithAuth(plugins []PluginInfo, apiBaseURL, token string) Model

NewWithAuth returns an initialised TUI model that sends a Bearer token with every API request. Pass empty token to disable auth.

The default connection mode is ModeStandalone. Callers that connect to an external API server must call SetConnectionMode(ModeConnected) before Init() so the status bar reflects the correct state.

func (Model) Init

func (m Model) Init() tea.Cmd

Init implements tea.Model.

func (*Model) SetConnectionMode

func (m *Model) SetConnectionMode(mode ConnectionMode)

SetConnectionMode sets the TUI's connection mode indicator.

func (*Model) SetTheme

func (m *Model) SetTheme(t Theme)

SetTheme replaces the model's active theme. Call before Run() to apply a custom or built-in theme.

func (Model) Update

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model. It handles keyboard input for menu navigation.

func (Model) View

func (m Model) View() string

View implements tea.Model. It renders the full TUI screen.

type NetworkInterface

type NetworkInterface struct {
	Name  string `json:"name"`
	MAC   string `json:"mac"`
	IP    string `json:"ip"`
	State string `json:"state"`
}

NetworkInterface represents a network interface from /api/v1/plugins/network/interfaces.

type NetworkStatus

type NetworkStatus struct {
	DefaultGateway    string `json:"default_gateway"`
	DNSReachable      bool   `json:"dns_reachable"`
	InternetReachable bool   `json:"internet_reachable"`
}

NetworkStatus represents /api/v1/plugins/network/status.

type NetworkWriteResult

type NetworkWriteResult struct {
	Valid    bool           `json:"valid,omitempty"`
	Changes  []string       `json:"changes,omitempty"`
	Current  map[string]any `json:"current,omitempty"`
	Proposed map[string]any `json:"proposed,omitempty"`
	Message  string         `json:"message,omitempty"`
}

NetworkWriteResult is the response from network write operations.

type NodeInfo

type NodeInfo struct {
	Arch          string `json:"arch"`
	Hostname      string `json:"hostname"`
	Kernel        string `json:"kernel"`
	OS            string `json:"os"`
	UptimeSeconds int    `json:"uptime_seconds"`
}

NodeInfo represents the response from /api/v1/node.

type PendingUpdate

type PendingUpdate struct {
	Package        string `json:"package"`
	CurrentVersion string `json:"current_version"`
	NewVersion     string `json:"new_version"`
	Security       bool   `json:"security"`
}

PendingUpdate represents a single pending update from /api/v1/plugins/update/status.

type PluginEndpoint

type PluginEndpoint struct {
	Method      string `json:"method"`
	Path        string `json:"path"`
	Description string `json:"description"`
}

PluginEndpoint describes a single HTTP endpoint exposed by a plugin.

type PluginInfo

type PluginInfo struct {
	// Name is the plugin registry identifier (e.g. "update", "network").
	Name string
	// Description is the user-facing summary shown in menus.
	Description string
	// RoutePrefix is the base API path (e.g. "/api/v1/plugins/update").
	RoutePrefix string
	// Endpoints lists the HTTP endpoints registered by the plugin.
	Endpoints []PluginEndpoint
}

PluginInfo describes a registered plugin for menu rendering. The core binary populates this from its plugin registry — the TUI has no direct dependency on the core plugin package.

type PluginRegistryEntry

type PluginRegistryEntry struct {
	Name        string           `json:"name"`
	Version     string           `json:"version"`
	Description string           `json:"description"`
	RoutePrefix string           `json:"route_prefix"`
	Endpoints   []PluginEndpoint `json:"endpoints"`
}

PluginRegistryEntry describes a plugin as returned by GET /api/v1/plugins.

type PluginSettings

type PluginSettings struct {
	Config map[string]any `json:"config"`
}

PluginSettings models the response from GET /api/v1/plugins/{name}/settings.

type PluginSettingsUpdateResult

type PluginSettingsUpdateResult struct {
	Config  map[string]any `json:"config"`
	Warning string         `json:"warning,omitempty"`
}

PluginSettingsUpdateResult models the response from PUT /api/v1/plugins/{name}/settings.

type RunStatus

type RunStatus struct {
	Type      string `json:"type"`
	Status    string `json:"status"`
	StartedAt string `json:"started_at,omitempty"`
	Duration  string `json:"duration"`
	Packages  int    `json:"packages"`
	Log       string `json:"log"`
}

RunStatus represents the response from /api/v1/plugins/update/logs.

type StaticIPConfig

type StaticIPConfig struct {
	Address string `json:"address"`
	Gateway string `json:"gateway,omitempty"`
	Netmask string `json:"netmask,omitempty"`
}

StaticIPConfig is the request body for PUT /api/v1/plugins/network/interfaces/{name}.

type Theme

type Theme struct {
	// Header is the style for the top title bar.
	Header lipgloss.Style
	// Footer is the style for footer help text.
	Footer lipgloss.Style
	// Selected is the style for the currently highlighted menu item.
	Selected lipgloss.Style
	// Normal is the style for non-selected menu items.
	Normal lipgloss.Style
	// Description is the style for item descriptions below titles.
	Description lipgloss.Style

	// Cursor is the string shown before the selected item (e.g. "▸").
	Cursor string
	// CursorStyle is the lipgloss style applied to the cursor glyph.
	CursorStyle lipgloss.Style
	// Separator is the repeating character for horizontal rules (e.g. "─").
	Separator string
	// SepWidth is the number of times Separator is repeated.
	SepWidth int

	// ConnBadgeText is the label shown when connected to a service.
	ConnBadgeText string
	// ConnBadgeStyle is the style for the connected badge.
	ConnBadgeStyle lipgloss.Style
	// StandBadgeText is the label shown in standalone mode.
	StandBadgeText string
	// StandBadgeStyle is the style for the standalone badge.
	StandBadgeStyle lipgloss.Style

	// ConfirmYes is the style for the [Y] Yes button in confirmation dialogs.
	ConfirmYes lipgloss.Style
	// ConfirmNo is the style for the [N] No button in confirmation dialogs.
	ConfirmNo lipgloss.Style

	// StatusBar is the style for the hostname/uptime bar in the footer.
	StatusBar lipgloss.Style

	// Spinner is the style for progress spinners.
	Spinner lipgloss.Style
}

Theme holds all visual styles used by the TUI. Create one with DefaultTheme(), or parse from YAML with ThemeFromYAML().

func BuiltinTheme

func BuiltinTheme(name string) (Theme, bool)

BuiltinTheme returns a Theme for the given built-in name. The second return value is false if the name is not recognised.

func DefaultTheme

func DefaultTheme() Theme

DefaultTheme returns the built-in colour scheme matching the original hardcoded styles.

func ThemeFromYAML

func ThemeFromYAML(data []byte) (Theme, error)

ThemeFromYAML parses YAML bytes into a Theme, using DefaultTheme() as the base. Only fields present in the YAML are overridden; all others keep their default values. Returns an error if the YAML is malformed.

type TriggerJobResult

type TriggerJobResult struct {
	Status string `json:"status"`
	JobID  string `json:"job_id"`
}

TriggerJobResult represents the response from POST /api/v1/jobs/trigger.

type UpdateConfig

type UpdateConfig struct {
	Schedule          string `json:"schedule"`
	AutoSecurity      *bool  `json:"auto_security"`
	SecuritySource    string `json:"security_source"`
	SecurityAvailable *bool  `json:"security_available"`
}

UpdateConfig models the response from /api/v1/plugins/update/config.

type UpdateRunResult

type UpdateRunResult struct {
	Status string `json:"status"`
	Type   string `json:"type"`
}

UpdateRunResult represents the response from POST /api/v1/plugins/update/run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL