Documentation
¶
Overview ¶
Backs design-docs/ErrorOutput.md: a real user report that "Playbook failed" gave no hint of *why* until the TUI was closed (stderr can't be printed live - it would corrupt the alternate screen - so it used to be collected silently and only ever dumped after the whole session ended). This surfaces it inline instead, as ordinary rows right below the status line, the same "just append more rows" mechanism the recap section (recap.go) already uses for its own post-run summary.
Implements the drill-down view's "File" tab (design-docs/ ShowFileContents.md): a task-modified remote file's *current* contents, pulled via a throwaway ansible-playbook run using ansible.builtin.fetch - the same reasoning resolved.go's resolveTaskValues already applies to resolving a task's own values (ansible's own mechanisms, on ansible's own terms, rather than reimplementing an SSH/become/vault-aware file transfer ourselves).
Shells out to ansible-playbook using tangsible's own bundled aggregate callback plugin (design-docs/OwnCallbackPlugin.md, a GPL-3.0 fork of ansible.posix.jsonl) and streams events live into an interactive TUI as they arrive.
Implements the recap section (design-docs/Recap.md): a per-host ansible-style summary line ("hostname : ok=159 changed=94 ...") appended below the tree once a run finishes, expandable into its own non-empty outcome categories, each expandable into the individual tasks that landed in it. Deliberately built entirely on data PlaybookState already tracks (a scan over Plays/Tasks/Hosts) - no aggregate.go changes needed for most of it. Still no "rescued" category: a rescued task reports as an entirely ordinary v2_runner_on_ok, with nothing in the event stream distinguishing it from one that just never failed, so there's no data to build one from. "ignored" (TaskNode. Ignored) is no longer in that boat - design-docs/OwnCallbackPlugin.md's bundled callback plugin fork does emit ignore_errors (jsonl.py itself never did) - so unlike rescued, this one has real per-task data behind it now.
Implements the drill-down view's "Resolved" section (design-docs/ Drilldown, Resolved Values.md): a task's own source, re-rendered with its variables filled in, by feeding it back into ansible as a real ansible.builtin.template task rather than reimplementing variable resolution ourselves - ansible's own templating engine, on ansible's own terms, is the only thing that can correctly resolve a value without duplicating its ~14-level variable precedence chain (or reaching into undocumented internals no supported plugin API exposes - confirmed live in the design conversation this follows from that even a custom callback plugin doesn't get handed the VariableManager either).
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewLiveTUI ¶
func NewLiveTUI(state *playbook.PlaybookState, playbookName string, isRole bool, procH *runner.ProcHandle, processDone, quitting *atomic.Bool, exitCode *atomic.Int32, lastStderr *atomic.Pointer[[]string], outLog *atomic.Pointer[runner.OutputLog], sourceIndex source.TaskSourceIndex, knownTags, knownPlayNames, knownGroups []string, startExpanded, twoPaneLayout, colorEnabled bool, initialPlay, initialTags, initialSkipTags, initialHosts string, initialRerunDefaults runner.InitialRerunDefaults, startWithRerunDialog, showDialogAtStartup bool, requestRerun func(startAtPlay, tags, skipTags, hosts string), passthroughArgs []string, progH *atomic.Pointer[runner.ProgressTracker], revisitReturn func(), targetPlaybook, targetRole string) (*tview.Application, func(playbook.RawEvent))
NewLiveTUI builds an initially-empty list UI and wires it to state's hooks so it grows as events arrive. It does not block — the caller must call s.app.Run() and feed events through s.applyLive.
procH holds ansible-playbook's current process, used so Ctrl-C/q can forward SIGINT to it while it's still running (tcell's raw mode disables the OS's own Ctrl-C-to-SIGINT delivery, so without this the child would stop receiving the interrupt it used to get for free — see Purpose.md's Ctrl-C decision). A mutable holder rather than a plain *os.Process - unlike everything else this function reads at construction time and never again - because a rerun (Rerun.md, see requestRerun below) points it at a freshly spawned child mid-session; SetInputCapture always reads whichever process is current via procH.Load(). processDone/quitting are shared with the caller: this function only reads processDone and only writes quitting.
initialPlay/initialTags/initialSkipTags/initialHosts pre-fill the re-run dialog's own Play/Tags/Skip tags/Hosts fields the first time it's opened (and every time after, until the user edits them - the dialog's own fields, once opened, keep whatever the user last left in them across repeated 'r' presses) - the --start-at-play/--tags/--skip-tags/--limit values this process was itself invoked with, parsed out by main.go (ExtractStartAtPlay/ParsePassthroughArgs respectively) - design-docs/ StartWithPlay.md's own "small follow-on" for --start-at-play, once "run"/"rerun" could both already resolve one, mirroring how --tags/ --limit already pre-fill Tags/Hosts (Rerun.md's "if tags were already specified in the previous run... pre-filled").
requestRerun is called once the re-run dialog is confirmed (Enter), to start a new generation with the dialog's own fields: startAtPlay (empty unless a top-level play was chosen - design-docs/StartWithPlay.md; spawns against a trimmed, temporary copy of the playbook with every play before it dropped, rather than editing the real file) and the edited tags/skipTags/hosts. main.go's implementation resets processDone/exitCode/ state, records the new invocation into .tangsible's history, and spawns a fresh ansible-playbook invocation; this function's own job is only to reset its own view state (s.expanded/s.currentID/s.following/the freeze latches) and restart the heartbeat ticker to match - see submitRerun below.
sourceIndex (source.go) backs the output drill-down view's TASK: section (see formatHostOutput) - a lookup miss (any task whose path wasn't found while building the index) just means no TASK: section for that entry, never an error.
knownTags (source.go's BuildTaskSourceIndex, its own second return value) is every literal tags: value found in the playbook/role tree, unioned with source.ReservedTagNames - the re-run dialog's Tags/Skip tags fields' own autocomplete candidate list (design-docs/Autocomplete.md). knownPlayNames (source.ListTopLevelPlayNames) is the same idea for the "Start with play" field (design-docs/StartWithPlay.md) - every named top-level play, in file order; v1 deliberately doesn't follow import_playbook, so a play defined in an included file simply isn't a candidate. knownGroups (main.go's own best-effort inventory.GroupNames scan) is the Limit hosts field's own second candidate source, alongside state.AllHosts (already in scope, needing no separate parameter) - a group name is never reported by any jsonl event, so it can't come from state the way a literal host can.
initialRerunDefaults (runner.InitialRerunDefaults) is design-docs/ Rerun.md's "Extend rerun dialog" own data for the three checkboxes' very first appearance: the "rerun" verb's own --only-failed/--only- unreachable/--resume-where-failed flags (main.go, pre-validated against data actually available before this is ever called) plus the failed/ unreachable hosts and earliest failing play replayed from that target's last saved run log (runner.ReplayRunLog), since nothing has run yet in *this* process for state itself to answer that from. Every other case - "run"/"role"'s own first 'r' press, a revisit session, or any dialog reopen once a generation has actually produced events in this process - ignores this and recomputes the same three things fresh from state itself instead (rerunFieldSync.rebuild, livesession_rerundialog.go); zero value for every Verb but "rerun".
startExpanded governs the very first task row's own initial expand/collapse state (`.tangsible`'s general.default_tree_state - see DefaultTreeExpanded, resolve.go), read once by main.go before construction. Every task after the first inherits whatever the previous task's current expand state is at the moment it's added - see state.OnTaskAdded below - so this value only ever actually governs one row per generation (the very first task added since the last Reset()).
startWithRerunDialog is true for the "rerun" Verb's own startup (Rerun.md), and - since design-docs/RerunDialog.md - also for a "run"/ "role" session whose dialog was forced on (--dialog or run_dialog=always): either way, no ansible-playbook invocation exists yet at all - not even a first one in flight, unlike every other case this function handles - so the dialog-or-auto-submit startup flow below runs instead of waiting for 'r', and processDone is expected to already be true when this is called (main.go sets it before constructing the TUI): accurate ("no generation is currently in flight"), and what safely unlocks the dialog-opening/quit-outright behavior the rest of this function already has for a frozen run - see s.everStarted below for the one place that distinction actually matters once frozen means "genuinely nothing has run yet" rather than "a run finished."
showDialogAtStartup, consulted only when startWithRerunDialog is true, is design-docs/RerunDialog.md's own resolved decision for whether that dialog should actually render (true) or be auto-submitted immediately with its own pre-filled values, never visibly shown (false) - see the startWithRerunDialog handling at the very end of this function. Always true for "run"/"role" (main.go only ever leaves their own generation unspawned - the condition startWithRerunDialog tests - when this was already true); the one case where it can differ from startWithRerunDialog is "rerun" with --no-dialog/run_dialog=never, which still has nothing spawned yet (startWithRerunDialog true) but skips rendering the dialog to get there (showDialogAtStartup false). passthroughArgs is this session's own current-generation passthrough args (main.go's originalArgs.Rest - importantly -i/-e, never -l/--limit, which ParsePassthroughArgs already extracts separately) - threaded through only so showOutput's own resolveTaskValues calls (see design-docs/Drilldown, Resolved Values.md) see the same inventory/extra-vars context the real run did. Not used for anything else in this function.
colorEnabled is general.color's own resolved value (ColorEnabledByUser, resolve.go), read once by main.go before construction - one of three independent inputs (alongside the terminal's own detected color capability and the NO_COLOR environment variable) combined below into s.useColor, design-docs/Morehosts.md's own gate on whether the collapsed task row's per-host summary may render in color at all. revisitReturn, if non-nil, marks this session as design-docs/Revisit.md's "revisit" Verb showing a replayed (historical) run rather than a live run/rerun/role session: state/processDone/exitCode are already fully populated by the time this constructor is called (see revisit.go), chrome switches to ReplayBarStyle for as long as s.revisitActive stays true, and pressing Esc at the bare tree level (not in a dialog, not viewing output - nothing else has ever claimed that key there) calls revisitReturn, which is expected to stop s.app.Run() and let the caller show the run list again. nil for every other Verb - Esc keeps doing nothing at that level, exactly as before this existed.
targetPlaybook/targetRole (exactly one non-empty, mirroring AppendInvocation's own playbook/role parameters) is this session's own target identity, exactly as recorded in state.toml - distinct from playbookName, which is display-only (main.go passes it filepath.Base(playbook), not the full path state.toml itself keys on). Needed for design-docs/Diff.md's own 'd' key, to look up this session's own history entry and filter comparison candidates against it (RunDiffFlow, diff.go).
func Render ¶
func Render(w io.Writer, s *playbook.PlaybookState)
Render prints a plain-text dump of the current play/task/host tree. This is not the TUI — it always shows every host line under every task, since there's no navigation/expand state yet to decide what to hide.
func RunVersion ¶ added in v0.1.1
RunVersion implements the "version" verb: print the build stamps plus the versions of the ansible components tangsible shells out to, then exit. No TTY, no inventory, no config - it runs before any of that is set up, same as "vault". Every external lookup is best-effort: a missing binary or collection prints a note, never an error, and never changes the exit code - the point is a copy-pasteable environment summary for a bug report.
Types ¶
type BuildInfo ¶ added in v0.1.1
type BuildInfo struct {
Version string // semver tag, e.g. "0.1.0"; "dev" for an un-stamped build
Commit string // full git SHA
Date string // commit timestamp, RFC3339
}
BuildInfo carries the version stamps the root main package injects at release time via -ldflags -X (see .goreleaser.yaml). Every field is the zero value for a plain `go build`; RunVersion then fills the gaps from the VCS metadata the Go toolchain embeds into the binary automatically.
Source Files
¶
- autocomplete.go
- errorrows.go
- fetchfile.go
- helptables.go
- livesession.go
- livesession_events.go
- livesession_filter.go
- livesession_heartbeat.go
- livesession_input.go
- livesession_mouse.go
- livesession_nav.go
- livesession_output.go
- livesession_rebuild.go
- livesession_rerundialog.go
- livesession_stdout.go
- livesession_tabsearch.go
- main.go
- recap.go
- render.go
- rerundialog.go
- resolved.go
- tui.go
- version.go