source

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package source coordinates a windowed view over a paged remote source — the bookkeeping between "the user scrolled here" and "ask the server for that range", without ever doing the I/O itself.

It is shaped like pkg/poll: it owns no data and performs no requests. It tracks which window is held, which is in flight, and which query each request belongs to, and it emits RequestMsg when the rows on screen stop being rows it has. Your screen answers that with whatever HTTP, gRPC, or database call it likes, passing Query.Ctx so a superseded request is cancelled, then hands the result back through Deliver and pushes the rows into the component. Keeping the fetch in the screen is deliberate: every component in tuilib is synchronous, and a coordinator that owned a retry policy would drag it into places that have no business holding it.

It deliberately does not import pkg/table — nothing from tuilib but pkg/query, and internal/tick for its timers. The table reports what happened (ViewportChangedMsg, QueryChangedMsg) and the screen translates those into Viewport and SetQuery calls here, which keeps this package usable for any component that can say which rows are on screen — and keeps the dependency pointing one way.

The loop, in full:

func (s *Screen) Init() tea.Cmd { return s.src.Init() }

case table.ViewportChangedMsg:
    cmds = append(cmds, s.src.Viewport(msg.FirstVisible, msg.LastVisible))
case table.QueryChangedMsg:
    cmds = append(cmds, s.src.SetQuery(msg.Raw, msg.Terms, msg.Sort, msg.Desc))
case source.RequestMsg:
    return s, s.fetch(msg.Query)          // your call, under msg.Query.Ctx
case fetchedMsg:
    ok, cmd := s.src.Deliver(msg.Page)
    switch {
    case !ok:                             // superseded; drop it
    case msg.Page.Err != nil:
        s.table.SetFailed(answered(msg.Query))
    default:
        s.table.SetWindow(msg.Rows, msg.Page.Offset, msg.Page.Total, answered(msg.Query))
    }
    return s, cmd
}
cmds = append(cmds, s.src.Update(msg))     // every message, like pkg/poll

Installing the window makes the component emit a fresh ViewportChangedMsg, which closes the loop: a short page that still doesn't cover the screen asks for the rest on its own.

Three things keep a slow source bearable. Every request carries its own context, cancelled the moment a newer request supersedes it — a new query, or a newer window of the same one — so at most one request per source is live on the server. Scrolling asks only once the viewport has been still for Options.ViewportDelay, so a held key or a scrollbar drag is one request, not one per page crossed. And while a committed query has no answer yet, scrolling asks for nothing at all: the rows on screen answer the previous query, and their positions mean nothing in the next.

The query history — each committed query answered, failed or cancelled — leaves as QueryAnsweredMsg, QueryFailedMsg and QueryCancelledMsg in the commands SetQuery and Deliver return. They are deliberately neutral, like pkg/runner's messages: pkg/app turns them into output-console records.

Index

Constants

View Source
const DefaultFollow = 2 * time.Second

DefaultFollow is the poll interval for Growing data when Options.Follow is zero.

View Source
const DefaultPageSize = 100

DefaultPageSize is the window size used when Options.PageSize is unset.

View Source
const DefaultViewportDelay = 150 * time.Millisecond

DefaultViewportDelay is how long the viewport must be still before a scroll asks for rows, when Options.ViewportDelay is zero.

Variables

This section is empty.

Functions

This section is empty.

Types

type Anchor added in v0.31.0

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

Anchor is where a view of Anchored data begins.

func At added in v0.31.0

func At(c string) Anchor

At anchors at the item whose cursor is c: a search hit, a time, a link.

func Newest added in v0.31.0

func Newest() Anchor

Newest anchors at the newest item — the tail. The zero Anchor.

func Oldest added in v0.31.0

func Oldest() Anchor

Oldest anchors at the oldest item.

func (Anchor) Cursor added in v0.34.0

func (a Anchor) Cursor() (string, bool)

Cursor returns the cursor an At anchor names, and false for Newest and Oldest.

func (Anchor) IsNewest added in v0.31.0

func (a Anchor) IsNewest() bool

IsNewest reports whether a is the Newest anchor.

func (Anchor) IsOldest added in v0.34.0

func (a Anchor) IsOldest() bool

IsOldest reports whether a is the Oldest anchor.

type Anchored added in v0.31.0

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

Anchored coordinates a view over Anchored data — items reachable only by walking forwards or backwards from an anchor, with no offsets and no reliable total: Elasticsearch search_after, a log API filtered by timestamp. It is Model's counterpart and shares its request plumbing, cancellation, viewport delay and query history, but its surface is edges rather than offsets.

The component holds a Span: items keyed by their cursors, grown at either edge and trimmed at the one furthest from the viewport. The source never stores or interprets a cursor. When it asks for more at an edge, the screen reads that edge's cursor from the component and encodes it for its API:

case source.RequestMsg:
    older, newer := s.log.Edges()
    return s, s.fetch(m.Query, older, newer)   // m.Query.Dir says which

The request that starts a view has Query.FromAnchor set: page from the anchor, not from an edge — the span may still hold the previous query's items. Every other request extends the edge Query.Dir names.

One request is in flight at a time. The viewport reports how far it is from each edge; an edge with more beyond it is extended once the viewport comes within PageSize of it, nearest edge first.

func NewAnchored added in v0.31.0

func NewAnchored(opts AnchoredOptions) Anchored

NewAnchored constructs an Anchored source anchored at opts.Anchor. Nothing is requested until Init.

func (Anchored) Anchor added in v0.31.0

func (m Anchored) Anchor() Anchor

Anchor returns the current anchor.

func (*Anchored) Cancel added in v0.31.0

func (m *Anchored) Cancel()

Cancel cancels whatever is in flight.

func (*Anchored) Deliver added in v0.31.0

func (m *Anchored) Deliver(p Page) (bool, tea.Cmd)

Deliver records a fetched page — or a failure, in p.Err — and reports whether it was accepted. For an edge request, p.More says whether that edge has more beyond it. The command carries the query history and the next request, when the page left an edge still wanted.

func (*Anchored) Find added in v0.31.0

func (m *Anchored) Find(term string, dir Dir) tea.Cmd

Find asks for the first item matching term beyond the edge in direction dir, within the committed filter. The screen fills the Cursor from the component's edge, as for any edge request. A reply with Page.Found and Page.Cursor is the hit: re-anchor with SetAnchor(At(cursor)).

func (Anchored) ID added in v0.31.0

func (c Anchored) ID() int64

ID identifies this source in the RequestMsg values it emits.

func (*Anchored) Init added in v0.31.0

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

Init requests the anchor's first page.

func (Anchored) More added in v0.31.0

func (m Anchored) More() (older, newer bool)

More reports whether each edge has more beyond it.

func (Anchored) PageSize added in v0.31.0

func (m Anchored) PageSize() int

PageSize is the configured page size.

func (Anchored) Pending added in v0.31.0

func (m Anchored) Pending() bool

Pending reports whether a request is outstanding.

func (*Anchored) Poke added in v0.31.0

func (m *Anchored) Poke() tea.Cmd

Poke makes a Growing source poll now — for a push channel saying there is news.

func (Anchored) PollsFailing added in v0.31.0

func (m Anchored) PollsFailing() bool

PollsFailing reports whether follow polls are failing.

func (*Anchored) Refresh added in v0.31.0

func (m *Anchored) Refresh() tea.Cmd

Refresh re-anchors at the current anchor under the same query.

func (*Anchored) SetAnchor added in v0.31.0

func (m *Anchored) SetAnchor(a Anchor) tea.Cmd

SetAnchor moves the view: the span is replaced by one starting at a. A search hit re-anchors with At(hit).

func (*Anchored) SetGrowing added in v0.31.0

func (m *Anchored) SetGrowing(b bool) tea.Cmd

SetGrowing says whether items are arriving at the newest end. While they are, the source polls every Follow: a fetch of what is newer than the newest edge while the viewport shows it (following), a Probe otherwise. Setting it false stops after one final read.

func (*Anchored) SetQuery added in v0.31.0

func (m *Anchored) SetQuery(raw string, terms []query.Term, sort string, desc bool) tea.Cmd

SetQuery installs a new filter and sort and re-anchors at the current anchor. Everything in flight is cancelled; a query abandoned before it was answered is reported as QueryCancelledMsg.

func (*Anchored) Update added in v0.31.0

func (m *Anchored) Update(msg tea.Msg) tea.Cmd

Update handles the source's timers. Forward every message to it.

func (*Anchored) Viewport added in v0.31.0

func (m *Anchored) Viewport(toOlder, toNewer int) tea.Cmd

Viewport reports how many items lie between the viewport and each edge of the span: toOlder above the first visible item, toNewer below the last. Feed it from the component's viewport message. An edge with more beyond it is extended once the viewport comes within PageSize of it, after Options.ViewportDelay.

type AnchoredOptions added in v0.31.0

type AnchoredOptions struct {
	// PageSize is how many items one request asks for, and how close to an
	// edge the viewport must come before that edge is extended. Defaults to
	// DefaultPageSize.
	PageSize int
	// Name, ViewportDelay, Context and Follow mean what they do on Options.
	Name          string
	ViewportDelay time.Duration
	Context       context.Context
	Follow        time.Duration
	// Anchor is where the view starts. The zero value is Newest.
	Anchor Anchor
}

AnchoredOptions configures an Anchored source.

type Dir added in v0.31.0

type Dir int

Dir is a direction through ordered data.

const (
	// Newer walks towards the end — later offsets, newer items.
	Newer Dir = iota
	// Older walks towards the start.
	Older
)

type Mode

type Mode int

Mode selects how the source is addressed.

const (
	// ByOffset addresses rows by numeric offset ("?offset=200&limit=100").
	// Windows can jump anywhere, so scrolling to the middle of a large set
	// fetches exactly the range on screen. Zero value.
	ByOffset Mode = iota
	// ByCursor addresses rows by an opaque continuation token. Only
	// forward, sequential paging is possible, so the screen accumulates
	// rows and installs them as one growing window at offset 0 — pass
	// Total -1 to SetWindow until the source runs out.
	ByCursor
)

type Model

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

Model is the coordinator. Embed as a value; drive it through the methods.

func New

func New(opts Options) Model

New constructs a coordinator. Nothing is requested until Init.

func (*Model) Cancel added in v0.31.0

func (m *Model) Cancel()

Cancel cancels whatever is in flight. A reply that arrives anyway is refused by Deliver.

func (*Model) Deliver

func (m *Model) Deliver(p Page) (bool, tea.Cmd)

Deliver records a fetched page — or a failed fetch, when p.Err is set — and reports whether it was accepted. A false return means the page answers a superseded request: drop it. The command carries the query history (QueryAnsweredMsg, QueryFailedMsg) and, when a query's first answer leaves part of the screen uncovered, the request for the rest.

func (Model) Exhausted

func (m Model) Exhausted() bool

Exhausted reports whether a ByCursor walk has run out of pages. Always false under ByOffset, where the total says the same thing.

func (*Model) Find added in v0.31.0

func (m *Model) Find(term string, dir Dir, from int) tea.Cmd

Find asks for the first item matching term after from (Newer) or before it (Older), within the committed filter. The reply's Page.Found and Page.Offset say where the match is; Deliver accepts it like any page but installs nothing, so the screen moves the component there.

func (Model) Growing added in v0.31.0

func (m Model) Growing() bool

Growing reports whether the source is polling for new items.

func (Model) Held

func (m Model) Held() (start, count int)

Held reports the window the source has supplied: its first row's logical index and how many rows it holds.

func (Model) ID added in v0.31.0

func (c Model) ID() int64

ID identifies this source in the RequestMsg values it emits.

func (*Model) Init

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

Init returns the first request — the opening page of the current query. Batch it into your screen's Init, the way poll.Init is batched. It is needed because an empty component reports no viewport, so nothing else would ever ask for the first page.

func (Model) PageSize

func (m Model) PageSize() int

PageSize is the configured window size.

func (Model) Pending

func (m Model) Pending() bool

Pending reports whether a request is outstanding.

func (*Model) Poke added in v0.31.0

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

Poke makes a Growing source poll now rather than at the next interval — for a push channel (a websocket) saying there is news. Push channels are hints: they can drop messages, so the data still comes from the poll.

func (Model) PollsFailing added in v0.31.0

func (m Model) PollsFailing() bool

PollsFailing reports whether follow polls are failing. The first failure was reported as QueryFailedMsg; the first success will be reported as QueryRecoveredMsg.

func (*Model) Refresh

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

Refresh re-requests the window currently on screen without changing the query — the poll-driven "same view, fresh data" case, and the retry after a failure. Under ByCursor it restarts from the first page, since a cursor walk cannot be resumed from the middle.

func (*Model) SetGrowing added in v0.31.0

func (m *Model) SetGrowing(b bool) tea.Cmd

SetGrowing says whether the data is gaining items at its end — a job or flow run still running. While it is, the source polls every Options.Follow: while the viewport shows the newest item (the component is following) a poll fetches the tail, and otherwise it is a probe of one item that only learns the grown total. Setting it false stops polling after one final read, which catches items saved after the run ended.

func (*Model) SetHeld added in v0.31.0

func (m *Model) SetHeld(start, count int)

SetHeld adopts the range the component actually holds, after it trimmed past its cap. Only meaningful with Options.MaxHeld.

func (*Model) SetQuery

func (m *Model) SetQuery(raw string, terms []query.Term, sort string, desc bool) tea.Cmd

SetQuery installs a new filter and sort, discards the held window, and requests the first page of the new query. Everything in flight is cancelled, and Deliver refuses replies to the query that was just replaced, so a slow response to the previous filter cannot land under the new one. A query abandoned before it was answered is reported as QueryCancelledMsg.

It always requests, even when the arguments match the current query — a caller that has gone to the trouble of calling it wants a fetch.

func (Model) Total

func (m Model) Total() int

Total is the logical row count last reported, or -1 while unknown. Pass it straight to the component's window setter.

func (*Model) Update added in v0.31.0

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

Update handles the source's own timer. Forward every message to it, the way pkg/poll is forwarded; anything that isn't the source's returns nil.

func (*Model) Viewport

func (m *Model) Viewport(first, last int) tea.Cmd

Viewport reports the logical row range now on screen, inclusive. Feed it from the component's viewport message. When those rows are not ones the source has supplied, it asks for them once the viewport has been still for Options.ViewportDelay — so calling it on every scroll tick is fine.

type Options

type Options struct {
	// Name labels this source's query history, so a screen with several
	// sources can tell their lines apart ("books: filter x → answered").
	// Empty leaves the lines unlabelled.
	Name string
	// Mode selects offset or cursor addressing. Defaults to ByOffset.
	Mode Mode
	// PageSize is how many rows one request asks for, and the boundary
	// windows align to. Defaults to DefaultPageSize.
	PageSize int
	// Prefetch is how many extra pages to pull beyond the range actually
	// on screen. Zero (the default) fetches only what is needed, so the
	// user sees placeholders briefly at each page boundary; one page of
	// prefetch usually hides that at the cost of an extra request.
	Prefetch int
	// ViewportDelay is how long the viewport must be still before a
	// scroll asks for rows. Zero means DefaultViewportDelay; negative
	// means ask at once. Init, SetQuery and Refresh never wait.
	ViewportDelay time.Duration
	// Context is the parent of every request's context, so cancelling it
	// cancels everything in flight. Defaults to context.Background().
	Context context.Context

	// MaxHeld keeps a contiguous range of up to this many items rather
	// than one window: a page adjacent to what is held extends it, a
	// disjoint one replaces it. Zero keeps one window, which is what a
	// table wants; a timeline read back and forth wants a range. The
	// component trims the range itself and reports it through SetHeld.
	MaxHeld int

	// Follow is how often a Growing source is polled (see SetGrowing).
	// Zero means DefaultFollow; negative never polls.
	Follow time.Duration
}

Options configures a Model.

type Page

type Page struct {
	// Gen must echo the Query.Gen that produced this page.
	Gen int
	// Offset is where these rows start (ByOffset). Ignored under ByCursor,
	// where pages always extend the accumulated window.
	Offset int
	// Count is how many rows arrived. A zero count is remembered, so an
	// offset the source has nothing for is not asked for twice.
	Count int
	// Total is the logical row count, or -1 when the source can't say.
	Total int
	// Next is the continuation token for the following page (ByCursor).
	// Empty means the source is exhausted, which is also what finally
	// establishes the total.
	Next string
	// Err is set when the fetch failed. Deliver it like any other page:
	// Deliver decides whether anyone is still waiting for it.
	Err error

	// Found answers a Find: whether a match exists, at Offset (or Cursor,
	// on Anchored).
	Found bool
	// Cursor is the match's cursor when a Find on Anchored data found one.
	Cursor string
	// More reports, on Anchored, whether the edge this page extended has
	// more beyond it. False closes that edge.
	More bool
}

Page reports what a fetch returned. Rows themselves never come here — they go straight from the screen into the component.

type Query

type Query struct {
	// Offset is the first row wanted (ByOffset only).
	Offset int
	// Limit is how many rows are wanted.
	Limit int
	// Cursor is the continuation token to resume from (ByCursor only).
	// Empty on the first request of a query.
	Cursor string

	// Raw is the filter text the user committed, as typed.
	Raw string
	// Terms is Raw parsed. Scoped terms carry their resolved column Title,
	// so building "?region=europe" needs no lookup.
	Terms []query.Term
	// Sort is the column title to order by, "" when unsorted.
	Sort string
	// Desc reverses the order.
	Desc bool

	// Gen identifies this request. Echo it back in Page.Gen; anything
	// older is refused by Deliver, which is what stops an out-of-order
	// reply from painting a window the user has already scrolled past.
	Gen int

	// Ctx is cancelled as soon as this request is superseded — by a new
	// query, a newer window, Refresh, or Cancel — and once its page has
	// been delivered. Pass it to the call that does the fetch.
	Ctx context.Context

	// Poll marks a follow poll: a request the source made on its own
	// because the data is Growing (see Options.Follow). Fetch it like any
	// other window.
	Poll bool
	// Probe marks a poll made while the user is not following (Anchored):
	// fetch it, but only count what came back — the component shows how
	// much is new without appending it.
	Probe bool
	// Inclusive, on Anchored, includes the item at Cursor itself: the
	// first request of an At anchor, so the anchor appears exactly once.
	Inclusive bool
	// FromAnchor, on Anchored, marks the request that starts a view: page
	// from the anchor — Cursor for At, the end for Newest or Oldest —
	// rather than from the component's edge.
	FromAnchor bool

	// Find asks for the first item matching Term, starting after Offset
	// (Dir Newer) or before it (Dir Older), within the committed filter.
	// Limit is 1. Answer with Page.Found and the match's position in
	// Page.Offset (Model) or Page.Cursor (Anchored).
	Find bool
	Term string
	// Dir is the direction a Find — or, on Anchored, an edge request —
	// walks.
	Dir Dir
}

Query is one request's worth of parameters. Everything the source needs to answer is here; nothing about how to reach it is.

type QueryAnsweredMsg added in v0.31.0

type QueryAnsweredMsg struct {
	Name    string
	Query   Query
	Elapsed time.Duration
}

QueryAnsweredMsg reports that a committed query got its first answer — or its first answer after failing. One per committed query, never one per page.

type QueryCancelledMsg added in v0.31.0

type QueryCancelledMsg struct {
	Name  string
	Query Query
	By    Query
}

QueryCancelledMsg reports a committed query abandoned before it was answered, because By was committed in its place.

type QueryFailedMsg added in v0.31.0

type QueryFailedMsg struct {
	Name    string
	Query   Query
	Err     error
	Elapsed time.Duration
	Window  bool
}

QueryFailedMsg reports a fetch that failed while someone was still waiting for it. Window is true when the query already had an answer and only a page of it failed.

type QueryRecoveredMsg added in v0.31.0

type QueryRecoveredMsg struct {
	Name  string
	Query Query
}

QueryRecoveredMsg reports that follow polls are succeeding again after failing. QueryFailedMsg reported the start of the outage, once.

type RequestMsg

type RequestMsg struct {
	Query  Query
	Source int64
}

RequestMsg asks the screen to fetch Query. Source names the model that asked, so a screen with several sources — or pkg/remote, binding one — answers only its own.

Jump to

Keyboard shortcuts

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