tui

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 58 Imported by: 0

Documentation

Index

Constants

View Source
const AnyBoxChanged int64 = 0

AnyBoxChanged stands for "something changed, we don't know what" — a watcher sends it after a reconnect, where the changes broadcast while it was away were missed.

View Source
const Wordmark = `` /* 874-byte string literal not displayed */

Wordmark is the HEY braille-art logo shown in root help and on the setup wizard's welcome screen.

Variables

View Source
var ErrCanceled = errors.New("canceled")

ErrCanceled reports that the user dismissed a prompt (esc or ctrl+c).

View Source
var ErrNoRunningTUI = errors.New("no running HEY TUI")

ErrNoRunningTUI means no active HEY TUI accepted an open request.

View Source
var ErrNotInteractive = errors.New("interactive prompt requires a terminal")

ErrNotInteractive reports that a prompt was requested without a terminal.

Functions

func Confirm added in v0.2.0

func Confirm(message string, defaultYes bool) (bool, error)

Confirm shows a yes/no prompt on the terminal (rendered to stderr so stdout stays data) and reports the choice. Enter accepts the highlighted answer, y/n answer directly, arrows or h/l move, esc and ctrl+c cancel.

func OpenInRunningTUI added in v1.0.0

func OpenInRunningTUI(instance string, request OpenRequest) error

OpenInRunningTUI sends a destination to the active named TUI.

func RenderWordmark added in v0.2.0

func RenderWordmark(colorEnabled bool) string

RenderWordmark returns the wordmark, tinted when color is enabled.

func Run

func Run(rootSDK, sdk *hey.Client, selected string, watchers Watchers, options Options) error

Run starts the TUI with the resolved mail account, the identity root client used for interactive account switching, the live watchers, and an optional initial destination.

Types

type Calendar added in v0.2.0

type Calendar struct {
	ID   int64
	Name string
	// OwnerEmail is the account the calendar belongs to, which is what tells two calendars of
	// the same name apart — a reader with a work account and a personal one has two "Maybe"s
	// and no way to know which is which from the name. It is what the web app shows under a
	// calendar in its own list.
	OwnerEmail string
	Color      string
	Personal   bool
	// External is a calendar HEY subscribes to rather than owns — haystack's `internal`
	// scope is `where.missing(:subscription)`, and this is the other side of it.
	External bool
}

Calendar is one of the reader's calendars, as the picker and the habit form need it. Personal is the one HEY files a habit or a todo on when no calendar is named, and it has no name of its own — HEY leaves the field empty and the web app labels the row from the identity instead.

type CalendarWatcher added in v1.0.0

type CalendarWatcher func(ctx, connectionCtx context.Context) (<-chan struct{}, error)

CalendarWatcher opens the stream that says a calendar changed. It subscribes every calendar the account can see and folds them into one doorbell: which calendar rang does not matter, because the TUI re-reads whatever span is on screen either way. A watcher discovers calendars added or removed while it runs on its own and rings for those too. The stream closes when ctx is done, or when whatever is behind it has given up for good.

type CalendarYear added in v0.2.0

type CalendarYear struct {
	// PaddingDays is how many cells sit before January 1st, so the grid lines up under
	// the reader's first weekday.
	PaddingDays   int
	Days          []YearDay
	SpannedEvents []Recording
}

CalendarYear is a year as HEY draws one: a grid of days, and the events that span more than one of them. It is deliberately not a year's worth of recordings — see renderYearView.

type Contact added in v0.2.0

type Contact struct {
	ID           int64
	Name         string
	EmailAddress string
	Aliases      []Contact
}

Contact is someone in the reader's address book. An alias is a contact in its own right — another address the same person writes from — which is why Aliases nests.

type MailConnection added in v0.2.0

type MailConnection uint8

MailConnection is a transition reported by the connection behind a mail watch.

const (
	MailConnectionUnchanged MailConnection = iota
	MailConnectionDisconnected
	MailConnectionReconnected
)

type MailWatchEvent added in v0.2.0

type MailWatchEvent struct {
	BoxID         int64
	Connection    MailConnection
	WillReconnect bool
}

MailWatchEvent reports either a changed box or a connection transition. A disconnected event says whether the connection is already retrying; a reconnect asks the TUI to catch up the box on screen because broadcasts sent during the gap were missed.

type MailWatcher added in v0.2.0

type MailWatcher func(ctx context.Context) (<-chan MailWatchEvent, error)

MailWatcher opens the stream of mail and connection events that keeps the TUI live. Box events ask it to re-read changed mail, while connection events let it show when live updates are reconnecting. The stream closes when ctx is done, or when whatever is behind it has given up for good.

type OngoingTrack added in v0.2.0

type OngoingTrack struct {
	ID        int64
	Category  string
	StartedAt time.Time
}

OngoingTrack is the time track HEY has running, for everything that says so: the time tracking menu, and the day's now row.

Category is there because HEY serves it, and it is empty for every track that is still going. That is HEY's shape rather than a gap here — the only write that files a track under a category is the same one that ends it — so the field fills in the day a running track can carry one.

func (OngoingTrack) Elapsed added in v0.2.0

func (t OngoingTrack) Elapsed(now time.Time) time.Duration

Elapsed is how long the track has been going, to the second. A clock that has gone backwards reads as nothing rather than as time owed.

type OpenRequest added in v1.0.0

type OpenRequest struct {
	TopicID   int64  `json:"topic_id,omitempty"`
	AccountID int64  `json:"account_id,omitempty"`
	Title     string `json:"title,omitempty"`
	Screener  bool   `json:"screener,omitempty"`
}

OpenRequest identifies a destination to open in the TUI. AccountID selects a linked account when the request comes from another process.

type Options added in v0.2.1

type Options struct {
	Open     OpenRequest
	Instance string
}

Options configures the TUI's initial destination.

type Recording added in v0.2.0

type Recording struct {
	ID       int64
	ParentID int64
	Title    string
	AllDay   bool
	StartsAt time.Time
	EndsAt   time.Time
	Type     string
	// StartsAtZone and EndsAtZone are the IANA names of the zones the event was set in, and
	// they are empty for most events — HEY serves them only for one saved with a zone of its
	// own. They do not move the event: it is at one instant whatever zone it was set in, and
	// the calendar draws it where the reader's clock puts it. What they are for is the form,
	// which shows a zoned event on the clock it was written on.
	StartsAtZone string
	EndsAtZone   string
	CompletedAt  time.Time
	Label        string
	Icon         string
	// Color is a habit's own color. An event has none — what it wears is its
	// calendar's, which is CalendarColor, and the two are different fields in HEY too.
	Color string
	// Notes, Location, Link, Attendees and AttachedEntryID are what an event carries besides
	// when it is. They are read as well as written because HEY's update clears the lot of them
	// on any write that leaves them out — so an edit form that did not know them would wipe an
	// event's notes and location every time somebody changed its name.
	//
	// Notes arrive as plain text however they were written: HEY serves the description through
	// to_plain_text, so formatting cannot survive a round trip through any client.
	Notes           string
	Location        string
	Link            string
	Attendees       []string
	AttachedEntryID int64
	// Highlighted is what HEY calls the web app's "Circle event".
	Highlighted bool
	// Recurring and RepeatKind are how an event repeats. The kind is served but neither the
	// date it runs until nor the number of times, which is why an edit form can keep a
	// schedule or replace it but cannot show what it is bounded by.
	Recurring  bool
	RepeatKind string
	// ParentTitle is the title of the recording this one hangs off, which is the only place
	// some recordings have one: a countdown carries a label — "10 days before" — and no title
	// of its own, because what it is counting down to is the event above it.
	ParentTitle string
	// OccurrenceID names one instance of a repeating event — "153688907_2026-08-21", the
	// series and the day — and is what HEY serves instead of an id for an occurrence it has
	// not written down yet. Such a recording arrives with ID 0, which is why the arrows hold
	// on to key() rather than to the id: selecting by the id alone meant a repeating event
	// could never be picked out at all.
	OccurrenceID string
	// CalendarID is which calendar this is filed on, and CalendarColor how a reader tells
	// whose event they are looking at. HEY leaves the color empty for the personal calendar
	// and two calendars can wear the same one, so the id is what the edit form matches on and
	// the color only what it falls back to.
	CalendarID    int64
	CalendarColor string
	Days          []int32
}

Recording is anything HEY keeps on a calendar — an event, a todo, a habit, a time track — told apart by Type.

Its times are time.Time and stay that way. They used to be strings rendered in UTC and parsed back, which is how the whole calendar came to be drawn in UTC: an event at 14:00Z sat on the 14:00 column wherever the reader was. Read Starts and Ends rather than these fields — those answer in the zone a reader thinks in.

func (Recording) Done added in v0.2.0

func (r Recording) Done() bool

Done is whether a habit or a todo has been completed.

func (Recording) Ends added in v0.2.0

func (r Recording) Ends() time.Time

func (Recording) Starts added in v0.2.0

func (r Recording) Starts() time.Time

Starts and Ends are when a recording begins and ends, in the zone the reader is in.

An all-day event is the exception, and it is not one HEY leaves to guesswork: its timestamp is a calendar date, which haystack serves as UTC midnight on purpose — `_recording.jbuilder` wraps it in `Time.use_zone("UTC")` so no offset creeps in. Convert that and a birthday moves to the day before for every reader west of UTC.

type ScreenerWatcher added in v0.2.0

type ScreenerWatcher func(ctx, connectionCtx context.Context, signedStreamName string) (<-chan struct{}, error)

ScreenerWatcher opens the stream that says The Screener changed. ctx owns this signed subscription; connectionCtx owns the shared TUI connection that remains live when a signed stream is replaced. HEY serves the signed name alongside the pending count, so a watcher opens after that name has been read.

type Theme added in v0.2.0

type Theme struct {
	Accent    color.Color
	Selection color.Color // nil when the theme gives no selection background
	Muted     color.Color
	Bright    color.Color
	Error     color.Color

	// Background is the theme's own paper, and Hues are the colors it renders the ANSI
	// slots as — both nil when no theme file said. They matter for anything drawn *on*
	// a hue: an ANSI slot's nominal value says nothing about what a reader sees, since
	// a theme retints the running terminal over OSC 4. ANSI blue is #000080 nominally
	// and a light periwinkle in a dark Omarchy theme, so ink picked against the nominal
	// value comes out backwards. Keyed by the color names HEY uses.
	Background color.Color
	Hues       map[string]color.Color

	// Dark reports whether the theme is for a dark background. HasMode is true when
	// a theme file said so; otherwise Dark is a guess the terminal can correct.
	Dark    bool
	HasMode bool

	// Trusted marks a theme the user pointed at explicitly (HEY_THEME). Trusted
	// values skip the accent and selection readability gates: the gates exist for
	// machine-derived palettes, not for a file chosen by hand.
	Trusted bool

	// Source names the file the overlay came from, "" for the ANSI defaults and
	// "NO_COLOR" when color is disabled.
	Source string
}

Theme is the accent overlay the TUI lays over the terminal's own ANSI palette.

hey-cli deliberately styles with ANSI-16 colors so a terminal retint restyles the running TUI for free. A Theme only replaces the handful of semantic colors where a desktop theme has a better answer than "bright blue": the accent, the selection background, the muted tone, the emphasized foreground and the error color. Anything a theme file leaves out keeps its ANSI default.

func ResolveTheme added in v0.2.0

func ResolveTheme() Theme

ResolveTheme picks the active theme from the environment, in order:

  1. NO_COLOR set → no color at all
  2. HEY_THEME → a theme file at that path
  3. Omarchy's rendered hey.toml in the current theme, when a theme ships or templates one
  4. Omarchy's colors.toml in the current theme
  5. the ANSI defaults

Only the first file that exists is read; within it, only the keys it provides override the defaults.

type Watchers added in v0.2.0

type Watchers struct {
	Mail     MailWatcher
	Screener ScreenerWatcher
	Calendar CalendarWatcher
}

Watchers are the streams the TUI follows to stay live. All are optional: without them a list is the snapshot it was read as.

type YearDay added in v0.2.0

type YearDay struct {
	Date         time.Time
	Backgrounded bool
}

YearDay is one cell of that grid. Backgrounded is whether the day carries a background image, which the web app paints behind the cell.

Jump to

Keyboard shortcuts

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