gotermboard

package module
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2026 License: MIT Imports: 17 Imported by: 0

README

gotermboard

A self-refreshing terminal board for Go command-line tools, on Bubble Tea v2.

A program built on it has a tab per view. Each view is one table of folding groups sharing one set of columns, with a side panel that follows the cursor. Each source of data refreshes at its own pace, and the board draws its last good read with that read's age, so a failing backend leaves the table drawn and says the read is old.

type board struct{ jobs *gotermboard.Source[[]job] }

func newBoard() *board {
 return &board{jobs: gotermboard.NewSource("jobs", 5*time.Second, readJobs)}
}

func (b *board) Sources() []gotermboard.Refresher { return []gotermboard.Refresher{b.jobs} }
func (b *board) Groupings() []string              { return nil }

func (b *board) Frame(now time.Time, _ string) gotermboard.Frame {
 group := gotermboard.Group{Key: "jobs", Title: gotermboard.Line{gotermboard.C("JOBS", gotermboard.Bold)}}
 for _, j := range b.jobs.Value() {
  group.Rows = append(group.Rows, gotermboard.Row{Key: j.Name, Cells: []gotermboard.Cell{
   gotermboard.C(j.Name, ""),
   gotermboard.C(j.State, gotermboard.Cyan),
   gotermboard.Ticking(gotermboard.Ago(now.Sub(j.Started)), ""),
  }})
 }
 return gotermboard.Frame{
  Strip:   []gotermboard.Line{gotermboard.ReadStatus([]gotermboard.Status{b.jobs.Status()}, now)},
  Columns: []gotermboard.Column{{Title: ""}, {Title: "STATE"}, {Title: "AGE"}},
  Groups:  []gotermboard.Group{group},
 }
}

func main() {
 pal := gotermstyle.PaletteFor(os.Stdout)
 tabs, err := gotermboard.NewTabs([]gotermboard.Tab{{Name: "jobs", Board: newBoard()}}, pal, "")
 if err != nil {
  log.Fatal(err)
 }
 if _, err := tea.NewProgram(tabs).Run(); err != nil {
  log.Fatal(err)
 }
}

Tabs read only while they show

NewTabs takes the tabs in the order Tab cycles them, and the name of the one to open on. Each tab keeps its own cursor, folds and filter. A hidden tab starts no read, builds no frame and moves no strip. On its return it draws its last good read with that read's age, and every source past its interval reads at once. A source inside its interval keeps its pace, so cycling the tabs quickly starts nothing extra. A program with one tab draws no tab line, and Tab does nothing in it.

The tab line marks the current tab by color and weight rather than a box, as a tmux status line marks its window: bold bright yellow, with the rest in bright black, each padded by a space and two spaces apart. Both are ANSI slots, so the terminal's theme sets them.

Once draws one frame from what a board's sources already hold, and OnceSize picks its size: COLUMNS by LINES, then the terminal, then a fallback you pass. A screen that is no board picks its printed frame's size the same way with printedframe.Size, which imports nothing that draws.

One line says how the reads stand

ReadStatus is a line for a view's strip. While every read is on schedule it says read 3s ago. A failed read is named in red with its reason, one past twice its interval in yellow, and a note on a read that covered less follows plain. The ? overlay lists every source's age, interval, cost and fault.

A row that changed wears a cue

The board compares each row's and group's key and cells between frames, and marks a changed one for CueFor: a dot in the pointer's column and its first cell in bold. No column moves. A cell built with Ticking follows the clock, as an age or a gauge does, so its text changing is no change to the row, though its color changing is. A first read, a tab's return and a new grouping mark nothing.

Group rows share the columns

A Group with Cells draws them on the columns past the first, which holds its title, and the column titles then take a line of their own at the top. Below holds lines drawn under the header whether it is folded or not, which the cursor moves past as one node. WrapLast on a Row or a Group is the last column as cells of their own colors, continuing on further lines at that column, so a bar wider than the row keeps every cell. TallyBar is such a bar, one cell per unit, because a fixed width rounds one blocked unit in sixty away. Each kind has its own glyph as well as its color, so a frame printed without color still shows which units are done and which are blocked. Line.Paint turns a line into text in its colors, so a command that prints rather than runs a board draws the same bar.

Items makes a line of Below or Beside that the draw cuts to the items that fit, ending in +N, because only the draw knows the width and it moves with the side panel. Under is a summary on a └─ line while f has them on: a row's under the row, and a group's wrapping under Below. They start off, and a board implementing TwoLineBoard starts them on, for rows whose identity reads dim on that second line. UnderWhereCut on a row draws its Under only where the last column is too narrow for the row's cell there, for a summary that repeats that cell. f leaves the footer while no group or row has a summary to draw.

Span draws a long value whole past a group's title, wrapping under itself, so it never sets the first column's width. The group's cells then take the next line, with Beside ahead of them in the first column. Column.Min holds that column wide enough to be useful.

A row can open a document laid out at its width

Document on a Row or a Group is a function of width, because hanging bullets, tables and boxes of code cannot be wrapped after they are drawn. A row's opens under it on space, however tall it is. enter opens it the same way, or on the full-screen reader where it runs past the screen or the side panel draws it, whether or not it shows in place. StartsOpen on a row shows its text from the first frame, so a view can draw its newest entry whole and still fold it to its first line. A group's draws under Below whether the group is folded or not. The markdown package makes one from markdown, in ANSI 0-15 so it follows the theme:

renderer := markdown.New(nil)
row.Document = func(width int) []gotermboard.Line { return renderer.Lines(description, min(width, 100)) }

A fence naming a language wraps a line wider than its box, marking each continued row with ↪, as bat and delta do. A text or bare fence cuts that line instead, so a diagram or a table of output stays lined up. WrapEveryFence on the renderer wraps those too, for a text whose every word has to reach the reader.

Its argument colors what a caller recognizes in prose, such as a number that names a record.

What holds the text stays at the top while it scrolls

Reading down an open row's text keeps the row and the groups above it on the band at the top of the table, as VS Code's sticky scroll and nvim-treesitter-context do. The band shows the first line of each node holding the first line it leaves showing, so the cursor's row carries its pointer there. A node joins once its own line has scrolled off, and a sibling arriving under the band takes its predecessor's place.

  • The band covers lines rather than pushing them down, so nothing under it moves as it grows.
  • It holds a few lines, never a large share of the table, and drops the outermost first.
  • A band line is no cursor stop. A cursor moved under the band brings its row out below it.
  • Only a row or group with lines under it and words to name them by joins, so a folded group, a closed row and a group with no title never take a line of the band. A Still group at the top of the frame stays above the table already, so it never joins either.

Every board gets the band with no field to set, because gotermboard draws the table.

A Still group has no fold mark and the cursor passes over it. At the top of a frame it stays on screen with the column titles while the table scrolls, which suits a row of totals.

A chevron marks only what opens

The board decides the mark from what a node holds. A row draws ▸ or ▾ where it holds Detail or a Document, and a group where it holds a row or a group the table draws. Anywhere else enter and space change nothing, so no mark promises otherwise and the footer and ? leave both keys out on that line. A filter holds every group open, so a group draws no mark while one is typed.

A row's chevron sits in the last cell of its indent, one cell in from its parent's, with Symbol beside it. MarkGlyph sits in the cell before the chevron. No column moves for either.

A filter can reach past what shows

FilterOnly keeps a group out of the table until a filter is typed, so a view that shows the first few of something still finds the rest. Text is more the filter matches beyond what a group draws, and Heading says a title only labels what sits under it, so a word in it matches nothing.

The side panel follows the cursor

From PanelFrom columns the panel grows with the screen, up to ReadingWidth. It draws where any row in the frame holds Panel, Document or Detail, or any group but a Still one holds Panel or Detail, counting lines folded away or held back for a filter. A frame with none of them draws no panel, and the table takes the width. The frame decides rather than the cursor's line, so the table never jumps as the cursor moves.

Panel on a row or a group is what the side panel shows in Detail's place, a function of the panel's width whose lines wrap at spaces with each cell's own color. A row with no Panel shows its Document, under its ReaderTitle in bold where it has one. Such a row then reads its Document beside the table rather than under itself: StartsOpen leaves it folded while the panel shows, and enter opens it on the full-screen reader. A board implementing CursorBoard is told each key the cursor lands on, so a read that only one row needs can follow it. Frame.StartAt names the key the cursor starts on. Without one it starts on the first line holding cells, or the last on a TailBoard, past any group with no panel text. With no such line it starts on the first line with text to read.

A tall document reads on a full screen

Reader is a full screen over one Document. It lays the text out at up to 100 columns, centers it on a wider screen, and keeps the same words at the top through a resize. j and k, ctrl+d and ctrl+u, g and G move through it, [ and ] jump between sections, and / searches with n and N. A query with no capital in it ignores case. y copies CopyText, or the text as drawn, over OSC 52. e opens EditFile in $VISUAL or $EDITOR and lays the text out again after. EditorCommand(path) is that editor on any file, for a board's own key to hand the terminal with tea.ExecProcess.

Its top line names where the text sits: the groups the table's band would name over the row that opened it, its title, then each heading still open above the top row. A heading joins once its own row scrolls off, as the table's band does. Where the line runs long, the longest name is cut first, and the outermost leaves once every name is down to a few words.

That cut is breadcrumb.Fit, over the names' widths alone. A screen that is no board fits its own path line the same way: the breadcrumb package imports nothing that draws, so Bubble Tea stays out of that screen's build.

Model.Read opens one over the board from a board's own key, and esc or space returns to the board. Model.Tall says whether a document runs past the screen. NewReader runs as a program of its own, which is all cmd/read-markdown does with a file or a pipe:

go install github.com/datapointchris/gotermboard/cmd/read-markdown@latest
read-markdown README.md

A Cell with Section set starts the words of a heading, and its value is the heading's depth, 1 outermost. A heading closes every open one at its depth or deeper. The markdown package sets it on each heading's first word, past the bar that marks it.

A board's strip can move between its frames

A board implementing MovingBoard gets its strip from StripAt(now), redrawn every MoveEvery(), for a diagram of work in motion between its reads. Only the strip redraws at that pace, so the table, its change cue and the side panel wait for the next frame. Only the tab showing runs the clock, and a reader over the board holds it until the key that closes the reader. A MoveEvery of zero holds the strip still until the board's next tick asks again, so a board with nothing moving spends nothing.

func (b *board) MoveEvery() time.Duration { return 100 * time.Millisecond }

func (b *board) StripAt(now time.Time) []gotermboard.Line {
 return []gotermboard.Line{b.lane(b.runs.Value(), now)}
}

StripAt replaces Frame.Strip on every draw, so it returns the lines that hold still as well. A strip changing its height between beats redraws the table, because the table's height moves with it.

A strip that turns a spinner, such as one beside a read under way, draws SpinnerAt(now) and returns SpinEvery from MoveEvery while it turns. Those are the footer's drawings at the footer's pace, so the two spinners on one screen match.

A beat builds no frame and draws no table, so a moving strip costs a fraction of rebuilding the whole frame at the same pace. What a beat does cost is mostly Bubble Tea's renderer, which compares the whole screen on every frame that changed.

Charts take the cells the draw gives them

The chart package draws plain series as lines of cells:

  • TimeSeries draws braille lines over a time axis and a value axis. A Dotted series reads as a forecast, a Band shades the cells between two edges, and Legend names each beside its mark. Readings that all share one time draw an hour either side of it.
  • Heatmap shades a grid such as hour against weekday, from ░ to █ in one color. A shade orders by density in any theme, where a ramp of the sixteen colors orders differently in each. Given fewer lines than it has rows, it drops its column names first, then ends on a +N line for the rest.
  • Sparkline draws eighth-block columns, averaging more values than columns so the whole span shows. Any value above zero draws at least ▁, so a quiet stretch reads apart from an empty one.
  • Bars draws an upright bar per value in its own color, over an axis labeled at the top, the middle and zero.

Each fills exactly the width and height it is given, so chart.Beside lays them side by side. A chart colors its cells with the codes the caller passes, and the board's palette paints them. It draws through ntcharts and reads each cell back, so ntcharts never writes an escape of its own.

A board knows the screen's size only at the draw, so Frame.Charts is a function of it, drawn between the strip and the table:

frame.Charts = func(width, height int) []gotermboard.Line {
 week := chart.TimeSeries{Series: []chart.Series{{Name: "used", Points: used, Code: gotermboard.Cyan}}, Max: 100}
 return append([]gotermboard.Line{week.Legend()}, week.Lines(width, min(14, height/5))...)
}

A row's Panel takes a chart the same way, at the panel's width: chart.Document(week, 12). The charts are laid out once a frame, so a moving strip's beat leaves them as they stand.

A board can add keys

A board implementing Keyed adds Actions, which take precedence over the table's and appear in the footer and the ? overlay. A board implementing TailBoard reads from its newest row, as a log does.

enter and space are separate keys in the table, so a board that takes enter leaves space folding in place. Model.Toggle is what the table's enter does, for a board's enter that acts only on some rows. Bind it to enter alone. A row open in place that turns tall, as on a pane shrink, reads on Toggle and closes only on the table's space.

Model.Ask puts a Question on the footer's lines. With Typed set it takes one line typed under the question, and enter hands Answer what was typed, trimmed. Without it enter alone answers, for an act that takes no text. esc or ctrl+c takes the question away and runs Cancel where it is set, so a board can say that nothing was done. Until one of them every key goes to the question: q and ? type themselves, tab does nothing, and a paste lands as one line.

{Keys: []string{"m"}, Label: "merge the row into a key you type", Footer: "m merge", Run: func(m *gotermboard.Model) tea.Cmd {
 key := m.Cursor()
 m.Ask(gotermboard.Question{Text: "merge " + key + " into:", Typed: true, Answer: func(into string) tea.Cmd {
  m.Say(merge(key, into))
  return nil
 }})
 return nil
}}

The question draws in bold as written, so its own punctuation ends it. It wraps rather than being cut, and so does what Say puts on the footer. Each stops at a few lines, the last ending in … where more was left. The typed line is cut from the left, so its end and the cursor stay on the screen, as the filter and the reader's search are. A footer that grows keeps the cursor's row in view. The answer runs after the question closes, so it can Say what it did, ask another question, or return a command, such as one that opens an editor.

A key's command reports back to its board

A key's Run returns a command, and a command that runs in the background or hands the terminal over with tea.ExecProcess ends in a message. A board implementing ReceivingBoard is handed every message the model does not handle itself, with the model, so it can Say what came of the command, Rebuild, or return the next command. Tabs hands such a message to every tab, because the tab that started the command may be hidden by the time it ends, so a board switches on its own types.

A board implementing BusyBoard names its work under way from UnderWay(). The footer draws it after a spinner, above what the last key did, and a key does not clear it. The spinner turns only while the tab shows and nothing is open over the board.

func (b *board) merge(m *gotermboard.Model) tea.Cmd {
 b.underWay = "merging #12"
 return func() tea.Msg { return mergedMsg{err: exec.Command("gh", "pr", "merge", "12", "--merge").Run()} }
}

func (b *board) UnderWay() string { return b.underWay }

func (b *board) Receive(m *gotermboard.Model, msg tea.Msg) tea.Cmd {
 if merged, ok := msg.(mergedMsg); ok {
  b.underWay = ""
  m.Say(mergeResult(merged.err))
 }
 return nil
}

Documentation

Overview

Package gotermboard draws a self-refreshing terminal board on Bubble Tea v2: a tab per view, folding groups on shared columns, a side panel that follows the cursor, and one action table the keys, the footer and the ? overlay all read.

A view is a read model, as in CQRS. Its reader joins sources into a snapshot, and its renderer turns the snapshot into a Frame this package draws. A failed read never gives up a good value: a Source keeps its last good read and says how old it is.

Index

Constants

View Source
const (
	CueGlyph = "•"
	CueCode  = BoldCyan
	CueFor   = 30 * time.Second
)

The cue a row or group wears after it changed. It sits in the pointer's column, so no column moves for it.

View Source
const (
	Red     = gotermstyle.CodeRed
	Yellow  = gotermstyle.CodeYellow
	Green   = gotermstyle.CodeGreen
	Cyan    = gotermstyle.CodeCyan
	Blue    = gotermstyle.CodeBlue
	Magenta = gotermstyle.CodeMagenta
	White   = gotermstyle.CodeWhite
	Bold    = gotermstyle.CodeBold

	BoldRed     = "\033[1;91m"
	BoldYellow  = "\033[1;93m"
	BoldGreen   = "\033[1;92m"
	BoldCyan    = "\033[1;96m"
	BoldBlue    = "\033[1;94m"
	BoldMagenta = "\033[1;95m"
	Reverse     = "\033[7m"
)

The codes a view colors a cell with, all in ANSI 0-15 so they follow the theme.

View Source
const (
	MarkGlyph = "◆"
	MarkCode  = BoldMagenta
)

The mark a Marked row draws.

View Source
const PanelFrom = 160

PanelFrom is the narrowest screen that gets the side panel.

View Source
const ReadingWidth = 100

ReadingWidth is the widest a reader lays text out, because a longer line is hard to follow back to its start. A wider screen keeps the rest as margin. A view that lays the same text out in its table reads this, so the two wrap alike.

View Source
const RuleCode = "\033[0;90m"

RuleCode is the gray a rule draws in, which the terminal palette's theme sets.

View Source
const SpinEvery = 100 * time.Millisecond

SpinEvery is how long a spinner holds each drawing: the footer's beside work under way, and one a board turns on its own clock.

Variables

This section is empty.

Functions

func Ago

func Ago(d time.Duration) string

Ago is a duration as one unit: 45s, 14m, 26h, 3d.

func EditorCommand added in v0.22.0

func EditorCommand(path string) *exec.Cmd

EditorCommand is the user's editor on path: $VISUAL, then $EDITOR, then vi, split on spaces, so EDITOR="code --wait" works. A board's key hands it the terminal with tea.ExecProcess, as the Reader's e does.

func Once

func Once(board Board, pal gotermstyle.Palette, width, height int) string

Once is one frame at a size, drawn from what the board's sources already hold.

func OnceSize

func OnceSize(fallbackWidth, fallbackHeight int) (width, height int)

OnceSize is the size one printed frame draws at: COLUMNS by LINES, then the terminal's own size, then the fallback. It is printedframe.Size.

func RefreshAll

func RefreshAll(ctx context.Context, sources ...Refresher)

RefreshAll reads every source once, all at once, and returns when each ended.

func SpinnerAt added in v0.23.0

func SpinnerAt(now time.Time) string

SpinnerAt is the spinner's drawing at now, one cell wide, for a board that turns one on its own clock, such as a MovingBoard's strip while a read runs. It turns through the footer's drawings at the footer's pace.

func TableKeys added in v0.19.0

func TableKeys() []string

TableKeys is every key the built-in table binds, and tab and shift+tab, which Tabs binds where it shows more than one board. A board's key hides the table's, so a board taking keys from its own config asks this which ones would take a key from the table.

Types

type Action

type Action struct {
	Keys   []string
	Label  string
	Footer string
	Run    func(*Model) tea.Cmd
	// Applies says whether this view can use the key now. Nil means always.
	// A board's key hides the table's action for it even while Applies is false.
	Applies func(*Model) bool
}

Action is one row of the key table the handler, the footer and the ? overlay all read.

type Board

type Board interface {
	Sources() []Refresher
	Groupings() []string
	Frame(now time.Time, grouping string) Frame
}

Board is one view, as this package draws it: sources to keep fresh, and the frame their last good reads make now.

type BusyBoard added in v0.21.0

type BusyBoard interface {
	UnderWay() string
}

BusyBoard is a Board with work under way, such as a command its key started. The footer names it after a spinner, above what the last key did, until UnderWay is empty again. A key never clears it.

type Cell

type Cell struct {
	Text  string
	Code  string
	Ticks bool
	// Section says this cell starts the words of a heading, where a Reader's [ and ] land, and
	// how deep the heading sits: 1 is outermost, and 0 is no heading. A heading closes every
	// open one at its depth or deeper, which is the path a Reader's top line draws.
	Section int
	// contains filtered or unexported fields
}

Cell is text and its color. Ticks says the text follows the clock, as an age does, so its changing is no change to the row, though its color changing still is.

func C

func C(text, code string) Cell

C is a Cell.

func Ticking

func Ticking(text, code string) Cell

Ticking is a Cell whose text follows the clock.

type Column

type Column struct {
	Title string
	// Min holds the column at least this wide, for what a group's Beside fits into.
	Min   int
	Max   int
	Right bool
}

Column is one column of the table. The last is unpadded, so the screen's edge cuts it first. Max caps its width, where one long value must not take the row.

type CursorBoard added in v0.3.0

type CursorBoard interface {
	CursorOn(key string)
}

CursorBoard is a Board told which key the cursor is on, for a read that follows it.

type Document added in v0.4.0

type Document func(width int) []Line

Document is text that lays itself out at a width, for what only the draw can place: hanging bullets, tables and boxes of code. Its lines are drawn as they come.

type Frame

type Frame struct {
	Strip []Line
	// Charts is drawn under the strip at the screen's width, for what has to be laid out at the
	// width it gets, as a chart is. It is handed the screen's height too, so a board can keep it to
	// a share of a short screen. A moving strip leaves it as the frame drew it.
	Charts  func(width, height int) []Line
	Columns []Column
	Groups  []Group
	// StartAt is the key the cursor starts on, where that key is showing. Without one it
	// starts on the first line holding cells, or the last on a TailBoard, past any group with
	// no panel text. With no such line it starts on the first line with text to read.
	StartAt string
}

Frame is one screen of a view, before it is laid out to a size.

type Group

type Group struct {
	Key   string
	Title Line
	// Count is drawn after the title as " · n", where Counted says to.
	Count   int
	Counted bool
	// Cells draw on the columns past the first, which holds the title, so a group's
	// totals line up with the rows under it.
	Cells []Cell
	// WrapLast is the last column as Row.WrapLast has it.
	WrapLast Line
	// Below are lines drawn under the header whether the group is folded or not. The
	// cursor moves past them as one node.
	Below []Line
	// Span draws whole past the title on the header's line, wrapping under itself, so a
	// long value never sets the first column's width. The cells then take a line of their
	// own beneath it, and Beside sits ahead of them in the first column.
	Span Line
	// Beside is what the first column holds on a Span group's line of cells, under the
	// Span's start. An Items line there is cut to fit.
	Beside Line
	// Under is the group's summary, wrapping on a └─ row under Below while those are on.
	Under Cell
	// Document draws under Below whether the group is folded or not, so a view can open
	// a group's text in place. The cursor moves past it too.
	Document Document
	// Summary is drawn past the title while the group is folded.
	Summary Line
	Folded  bool
	// Gap draws a blank line after the group.
	Gap bool
	// Rule draws a thin line after the group, in place of Gap's blank one.
	Rule bool
	// BlankAboveRule draws a blank line between the group's last line and its Rule.
	BlankAboveRule bool
	// Tone is the code the group's overline and underline draw in, where not the rule gray.
	Tone string
	// Overline draws a line across the table above the header.
	Overline RuleWeight
	// Underline draws a line across the table under the header, ahead of Below.
	Underline RuleWeight
	// Still keeps the header open with no fold mark, and the cursor passes over it. At
	// the top of a frame it stays on screen with the column titles while the table scrolls.
	Still bool
	// Heading says the title only labels what sits under it, so the filter looks past it.
	Heading bool
	// FilterOnly keeps the group out of the table until a filter is typed, so a search
	// reaches past what a view shows.
	FilterOnly bool
	Rows       []Row
	Groups     []Group
	Detail     []Cell
	// Panel is what the side panel shows in Detail's place, as Row.Panel has it.
	Panel Document
	// Text is more the filter matches beyond what the group draws.
	Text string
	// contains filtered or unexported fields
}

Group is a header carrying its count, with rows and groups under it.

type Keyed

type Keyed interface {
	Actions() []Action
}

Keyed is a Board with keys of its own. They are read ahead of the table's, so a view can add a key or take one over, and the footer and the ? overlay list them.

type Line

type Line []Cell

Line is cells drawn one after another.

func Items added in v0.3.0

func Items(code string, items ...string) Line

Items is a line of a Group's Below that the draw cuts to the items that fit, ending in +N for the rest, rather than letting the screen's edge cut one in half. The width it has moves with the side panel, so only the draw knows it. The filter and the change cue read every item, shown or not.

func ReadStatus

func ReadStatus(statuses []Status, now time.Time) Line

ReadStatus is the one line a strip says about its reads. While every read is on schedule it says how old the newest is. A failed read is named in red with its reason, a late one in yellow, and a note follows plain, because a read that covered less still read. The screen's edge cuts them in that order.

func TallyBar added in v0.14.0

func TallyBar(done, underWay, blocked, rest int) Line

TallyBar is one cell per unit counted: done ▰ in green, under way ▱ in yellow, blocked ✕ in red and the rest ▱ plain. Its length shows how many there are, and one blocked unit among sixty still shows, where a fixed width would round it away. Without color only done is filled and blocked is crossed, so no unfinished unit reads as done. In a WrapLast it keeps every cell however narrow the row.

func (Line) Paint added in v0.16.0

func (l Line) Paint(pal gotermstyle.Palette) string

Paint is the line as text in its cells' colors, so a TallyBar can print outside a board.

func (Line) Text

func (l Line) Text() string

Text is the line with its colors out.

type Model

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

Model is the Bubble Tea program of one view. Tabs holds one per tab.

func NewModel

func NewModel(board Board, pal gotermstyle.Palette) *Model

NewModel is a model reading board, sized until the terminal says otherwise.

func (*Model) Ask added in v0.17.0

func (m *Model) Ask(q Question)

Ask puts q on the footer's lines. Until enter or esc every key and paste goes to it, so q and ? type themselves and tab changes no tab.

func (*Model) Cursor

func (m *Model) Cursor() string

Cursor is the key of the group or row the cursor is on, for a board's own keys.

func (*Model) HalfScreen

func (m *Model) HalfScreen() int

HalfScreen is half the table's height in lines.

func (*Model) Init

func (m *Model) Init() tea.Cmd

Init starts every source's first read and the clock.

func (*Model) Read added in v0.6.0

func (m *Model) Read(title string, document Document)

Read opens document on a full-screen Reader over the board, until esc or space closes it. The reader's top line names the groups holding the cursor's line, then title.

func (*Model) Rebuild

func (m *Model) Rebuild()

Rebuild is for a key that changed what the board shows from what it already holds.

func (*Model) Restart

func (m *Model) Restart() tea.Cmd

Restart puts the cursor back at the top and reads every source now, for a board's key that changes what the view shows.

func (*Model) Reveal added in v0.3.0

func (m *Model) Reveal()

Reveal scrolls the cursor's row and what opens under it into view, for a board's key that opens text under a row.

func (*Model) Say

func (m *Model) Say(text string)

Say puts text where the footer says what the last key did.

func (*Model) ScrollBy

func (m *Model) ScrollBy(lines int)

ScrollBy moves the screen over the table by lines and leaves the cursor, for reading through a row's detail taller than the screen. Moving the cursor ends it.

func (*Model) Tall added in v0.6.0

func (m *Model) Tall(document Document) bool

Tall says document runs past the screen, so enter reads it on a Reader.

func (*Model) Toggle

func (m *Model) Toggle() tea.Cmd

Toggle is what enter does, for a board's key that takes enter and acts only on some rows: a row's Document that runs past the screen opens on a Reader, open in place or not, and anything else folds or opens in place. Bind it to enter alone and leave space to the table, which folds in place however tall the text. A row open in place that turns tall closes only on that space.

func (*Model) ToggleEveryRow

func (m *Model) ToggleEveryRow() tea.Cmd

ToggleEveryRow opens every showing row's detail, or closes them all where every one is open already, so a whole group reads as one document.

func (*Model) Update

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

Update is Bubble Tea's.

func (*Model) View

func (m *Model) View() tea.View

View is Bubble Tea's.

type MovingBoard added in v0.12.0

type MovingBoard interface {
	// MoveEvery is how long the strip holds each drawing. Zero or less holds it still, and the
	// next tick asks again, so a board with nothing moving spends nothing.
	MoveEvery() time.Duration
	// StripAt is the whole strip at now. It stands in for Frame.Strip on every draw, so a line
	// holding still is returned as it is. A strip that changes its height redraws the table.
	StripAt(now time.Time) []Line
}

MovingBoard is a Board whose strip moves between its frames, as a diagram of work in motion does. Only the strip redraws at this pace, so the table and its change cue wait for their frame. Only the tab showing runs it, and a reader over the board holds it.

type Question added in v0.17.0

type Question struct {
	// Text draws in bold as written, so its own punctuation ends it.
	Text string
	// Typed takes one line typed under the question as the answer. Without it enter alone
	// answers, for an act that takes no text, and Answer is handed an empty string.
	Typed bool
	// Answer runs on enter with what was typed, trimmed. The question has closed by then, so it
	// can Say what it did, ask another question, or return a command such as an editor.
	Answer func(typed string) tea.Cmd
	// Cancel, where set, runs on esc or ctrl+c, so a board can say that nothing was done.
	Cancel func() tea.Cmd
}

Question is what a key asks on the footer's lines, until enter answers it or esc takes it away.

type Reader added in v0.6.0

type Reader struct {
	// CopyText is what y copies. Nil copies the text as drawn.
	CopyText func() string
	// EditFile is the file e opens in the editor, after which the text is laid out again.
	// Empty leaves e unbound.
	EditFile string
	// contains filtered or unexported fields
}

Reader is a full screen over one Document. It lays out every row at the width and draws the window that fits, because Bubble Tea keeps only the last lines of a taller view. It runs as a program of its own, or a Model opens one with Read.

func NewReader added in v0.6.0

func NewReader(title string, document Document, pal gotermstyle.Palette) *Reader

NewReader is a Reader over document, sized until the terminal says otherwise.

func (*Reader) Init added in v0.6.0

func (r *Reader) Init() tea.Cmd

Init is Bubble Tea's.

func (*Reader) Once added in v0.6.0

func (r *Reader) Once(width int) string

Once is the whole document at width, for output nobody scrolls.

func (*Reader) Update added in v0.6.0

func (r *Reader) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update is Bubble Tea's.

func (*Reader) View added in v0.6.0

func (r *Reader) View() tea.View

View is Bubble Tea's.

type ReceivingBoard added in v0.21.0

type ReceivingBoard interface {
	Receive(m *Model, msg tea.Msg) tea.Cmd
}

ReceivingBoard is a Board handed every message the model does not handle itself, such as the one a command its key started sends when it ends. Receive runs with the model, so it can Say what came of the command, Rebuild, or return the next command. Tabs hands such a message to every tab, because the tab whose key started the command may be hidden by the time it ends, so a board ignores a message of a type it does not own.

type Refresher

type Refresher interface {
	// Claim reports whether a read is due at now, and marks one under way.
	Claim(now time.Time) bool
	Refresh(ctx context.Context)
	// Force makes the next Claim true.
	Force()
	Status() Status
}

Refresher is a Source with its value's type out of the way, for the loop that only starts reads.

type Row

type Row struct {
	Key    string
	Symbol Cell
	Cells  []Cell
	Span   Line
	// WrapLast is the last column as cells of their own colors. It continues on
	// further lines at that column, where a cell in Cells is cut at the screen's edge.
	WrapLast Line
	// Under is the row's summary, drawn on a └─ row while those are on.
	Under Cell
	// UnderWhereCut draws Under only where the last column is too narrow for the row's cell
	// there, for a summary that repeats that cell. Only the draw knows the column's width.
	UnderWhereCut bool
	// Detail is what the side panel shows while the cursor is on the row, and what opens
	// under the row. Each cell is a paragraph that wraps. A row holding Detail or a
	// Document draws a chevron in its indent.
	Detail []Cell
	// Document is what opens under the row and the side panel shows, in Detail's place.
	Document Document
	// ReaderTitle heads the Reader enter opens on a tall Document, and the side panel where
	// it draws the Document. Empty, the row's own words head the Reader.
	ReaderTitle string
	// StartsOpen shows the row's Detail or Document under it until a key closes it, so a
	// view can draw its newest entry whole and still fold it to its first line. A row whose
	// Document the side panel draws starts folded while the panel shows, since the panel
	// draws it whole beside the table.
	StartsOpen bool
	// Panel is what the side panel shows in place of both, each line wrapping at spaces
	// with its cells' own colors. Without one, the panel draws the Document.
	Panel Document
	// Text is more the filter matches beyond what the row draws.
	Text string
	// Marked draws MarkGlyph in the row's indent and its first cell in MarkCode. No
	// column moves for it.
	Marked bool
	// contains filtered or unexported fields
}

Row is one line of the table: Cells on the columns, or Span across them.

type RuleWeight added in v0.8.0

type RuleWeight int

RuleWeight is how heavy a line across the table is drawn.

const (
	RuleNone RuleWeight = iota
	RuleThin
	RuleDouble
)

The weights a rule takes.

type Source

type Source[T any] struct {
	// contains filtered or unexported fields
}

Source is one read at its own interval, holding its last good value.

func NewSource

func NewSource[T any](name string, every time.Duration, read func(context.Context) (T, string, error)) *Source[T]

NewSource declares a read. read answers the value, a note on what it could not cover, and a failure, which leaves the last good value in place.

func (*Source[T]) Claim

func (s *Source[T]) Claim(now time.Time) bool

Claim is Refresher's. A read still under way is never started twice, so a slow source falls behind rather than piling up.

func (*Source[T]) Force

func (s *Source[T]) Force()

Force is Refresher's.

func (*Source[T]) Refresh

func (s *Source[T]) Refresh(ctx context.Context)

Refresh reads once, keeping the last good value where the read fails.

func (*Source[T]) Status

func (s *Source[T]) Status() Status

Status is how the source stands now.

func (*Source[T]) Value

func (s *Source[T]) Value() T

Value is the last good read, and the zero value before the first.

type Status

type Status struct {
	Name    string    `json:"name"`
	Every   string    `json:"every"`
	ReadAt  time.Time `json:"read_at,omitzero"`
	Took    string    `json:"took,omitempty"`
	TriedAt time.Time `json:"tried_at,omitzero"`
	// Fault is why the newest try failed, and empty where it read.
	Fault string `json:"fault,omitempty"`
	// Note is what a read that succeeded could not cover, such as a host asleep.
	Note string `json:"note,omitempty"`
	// Reading says a read is under way.
	Reading bool `json:"reading,omitempty"`
	// contains filtered or unexported fields
}

Status is what a view says about one source, on its strip and in its --json.

type Tab

type Tab struct {
	Name  string
	Board Board
}

Tab is one named view of a program.

type Tabs

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

Tabs is the Bubble Tea program of one Model per tab, the Elm Architecture's composition. Each tab keeps its own cursor, folds and filter. A hidden tab starts no read, builds no frame and moves no strip. On its return it draws its last good read with that read's age, and whatever is due reads at once.

func NewTabs

func NewTabs(tabs []Tab, pal gotermstyle.Palette, open string) (*Tabs, error)

NewTabs is a program of these tabs, opening on the one named open, or the first where open is empty. A name no tab has is an error naming the ones there are.

func (*Tabs) Init

func (t *Tabs) Init() tea.Cmd

Init is Bubble Tea's. One clock runs, and it goes to whichever tab is showing.

func (*Tabs) Update

func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update is Bubble Tea's. A size goes to every tab, and so does any message this package does not own, which is a ReceivingBoard's. A key, a paste and the clocks go to the tab showing.

func (*Tabs) View

func (t *Tabs) View() tea.View

View is Bubble Tea's.

type TailBoard

type TailBoard interface {
	Tails() bool
}

TailBoard is a Board read from its newest row, as a log is. Its cursor starts on the last row, and stays on the last row as rows land while it is there.

type TwoLineBoard added in v0.26.0

type TwoLineBoard interface {
	TwoLines() bool
}

TwoLineBoard is a Board whose rows read in two lines, the second a row's Under, so its summaries start shown. f still hides them.

Directories

Path Synopsis
Package breadcrumb fits a path of names, such as "uv › add › Options", on one line.
Package breadcrumb fits a path of names, such as "uv › add › Options", on one line.
Package chart draws plain series as lines of cells for a board: a time series with a forecast and a band, a heatmap, a sparkline and columns.
Package chart draws plain series as lines of cells for a board: a time series with a forecast and a band, a heatmap, a sparkline and columns.
cmd
read-markdown command
read-markdown draws a markdown file, or what is piped to it, on a full-screen reader.
read-markdown draws a markdown file, or what is piped to it, on a full-screen reader.
Package markdown lays markdown out as a board's lines at a given width, in colors that follow the terminal's theme.
Package markdown lays markdown out as a board's lines at a given width, in colors that follow the terminal's theme.
Package printedframe picks the size one printed frame draws at, as a TUI's --once prints it.
Package printedframe picks the size one printed frame draws at, as a TUI's --once prints it.

Jump to

Keyboard shortcuts

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