plugin

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package plugin is the hope plugin SDK: build a small JSON-RPC server that hope discovers across a fleet and renders in its UI. A plugin is your container, your language, your endpoint — hope dials in. Extend hope without joining it.

Minimal plugin:

p := plugin.New("badge-directory", "1.0.0").Icon("database")
p.View("counts", "Counts", plugin.KV, func(ctx context.Context) (any, error) {
	return map[string]any{"users": 1402301, "badges": 88123}, nil
})
log.Fatal(p.ListenAndServe(":8080")) // serves JSON-RPC 2.0 at /__hope

The container then declares the labels hope scans for:

hope.plugin=true
hope.plugin.port=8080
hope.plugin.path=/__hope

Index

Constants

View Source
const (
	ToneOK   = "ok"
	ToneWarn = "warn"
	ToneBad  = "bad"
	ToneInfo = "info"
)

Tone names a semantic color for a Badge (matches hope's ok/warn/bad/info).

View Source
const (
	ScopeEventsSubscribe = "events:subscribe" // receive hope events via OnEvent
	ScopeEventsPublish   = "events:publish"   // publish events onto hope's bus
	ScopeStorage         = "storage"          // durable per-install KV (p.Storage)
	ScopeSpecLabel       = "spec:label"       // add/update a service label in the plugin's own stack (p.Hope().AddServiceLabel)
)

Reverse-capability scopes a plugin may request. Plain strings so older/newer peers interoperate — an unknown scope is simply never granted. Additive: publish/storage/ action scopes land with their phases.

View Source
const ProtocolVersion = 1

ProtocolVersion is the plugin-protocol version this SDK speaks. hope sends its own; a mismatch degrades gracefully (unknown surfaces/kinds are skipped) rather than breaking, so a new plugin still works against an older hope and vice-versa.

Variables

View Source
var ErrNoReverseChannel = errors.New("hope reverse channel unavailable (no callback URL from hope.init)")

ErrNoReverseChannel is returned by Publish/Storage when hope hasn't provided a callback URL (an older hope, or one without [plugins] callback_url configured). The plugin still runs; it just can't call back into hope.

Functions

func Input

func Input(ctx context.Context) string

Input returns the "input" string from a Query view's params (the text the user typed into the query box), or "" if absent.

func NewError

func NewError(code int, msg string) error

NewError builds an *rpcError a handler can return to control the JSON-RPC error code sent to hope (e.g. invalid query -> codeInvalidArgs).

func Params

func Params(ctx context.Context, v any) error

Params unmarshals the current call's JSON-RPC params into v. Use inside an action/view/stream handler to read structured input.

func SearchQuery

func SearchQuery(ctx context.Context) string

SearchQuery reads the text a Search (autocomplete) view was called with (the "q" param). Empty when there's no query yet — return no items for that.

Types

type ActionDesc

type ActionDesc struct {
	Method string   `json:"method"`
	Label  string   `json:"label"`
	Icon   string   `json:"icon,omitempty"`
	Danger bool     `json:"danger,omitempty"`
	Fields []Field  `json:"fields,omitempty"`
	Tip    *Tooltip `json:"tip,omitempty"` // hover tooltip on the action button (set with ActionTip)
}

ActionDesc describes an invocable action (a mutation). Danger flags a destructive action so hope confirms before running it.

type ActionFunc

type ActionFunc func(ctx context.Context, in map[string]any) (any, error)

ActionFunc runs an action with the UI-collected field values.

type ActionOpt added in v0.0.3

type ActionOpt func(*ActionDesc)

ActionOpt configures a registered action beyond its label (its icon, …). Pass these to Action/DangerAction; the danger tone is set by DangerAction itself.

func ActionIcon added in v0.0.3

func ActionIcon(name string) ActionOpt

ActionIcon sets the action button's icon — a hope built-in icon name or one of this plugin's Icons keys. e.g. plugin.ActionIcon("rotate").

func ActionTip added in v0.0.4

func ActionTip(text string, pos ...TipPos) ActionOpt

ActionTip sets a hover tooltip on the action button explaining what it does, with an optional placement (see Tip). e.g. plugin.ActionTip("Refresh stats", plugin.TipBottom).

type Capabilities added in v0.0.6

type Capabilities struct {
	ViewKinds []string `json:"view_kinds"`
	Features  []string `json:"features"`
}

Capabilities is what the connected hope advertised it can render: the supported view kinds (kv/table/…/component) and feature flags (static, empty). Read it with Caps and query it with Supports.

func Caps added in v0.0.6

func Caps(ctx context.Context) Capabilities

Caps returns the capabilities the connected hope advertised for the current call. Use it to adapt output across hope versions (see the package example above). Outside a hope-driven call (no headers) it returns an empty Capabilities.

func (Capabilities) Supports added in v0.0.6

func (c Capabilities) Supports(name string) bool

Supports reports whether hope advertised the named capability — a view kind ("component") or a feature ("static", "empty"). False when hope sent no capabilities (an older build), so guard optional output and keep a baseline fallback.

type Card

type Card struct {
	Title    string      `json:"title"`
	Subtitle string      `json:"subtitle,omitempty"`
	Icon     string      `json:"icon,omitempty"`
	Tone     string      `json:"tone,omitempty"` // ok|warn|bad|info accent
	To       string      `json:"to,omitempty"`
	Image    string      `json:"image,omitempty"` // absolute http(s) URL -> a hero image at the card top
	Fields   []CardField `json:"fields,omitempty"`
}

Card is one tile in a Cards view. Fields render as a small label/value list (the values may be rich cells — Badge/Number/…). A non-empty To makes the card navigate on click (plugin-relative, like a Link cell — see DetailLink).

type CardField

type CardField struct {
	Label string `json:"label"`
	Value any    `json:"value"`
}

CardField is one label/value line on a Card; Value may be a rich cell.

type CardsData

type CardsData struct {
	Items []Card `json:"items"`
}

CardsData is what a Cards view returns: a grid of cards.

type Cell added in v0.0.6

type Cell = map[string]any

Cell is a rich table/field cell — a typed shape hope renders specially (a pill, a link, a progress bar, …). It's a type *alias* for map[string]any, so it stays a plain JSON object on the wire and every builder below (and any hand-built map) satisfies it with no conversion; the name just documents intent where a cell is expected (e.g. a Comp's Cell field, or CCell). Build one with Badge/Link/Number/Time/Progress/Code/Image.

func Badge

func Badge(value, tone string) Cell

Badge renders value as a colored pill. tone is one of the Tone* constants ("" = neutral).

func Code

func Code(value string) Cell

Code renders value as inline monospace (an id, hash, snippet).

func DetailLink(value, pageID, arg string) Cell

DetailLink renders value as a link to one of this plugin's DetailPage ids, passing arg as the page's ParamKey — a master-detail link that needs no knowledge of the plugin's hope key. e.g. DetailLink("alice", "user", "42") -> the "user" detail page with param {<paramKey>: "42"}.

func ExternalLink(value, href string) Cell

ExternalLink renders value as a link that opens href in a new tab.

func Image

func Image(src, alt string, opts ...ImageOpt) Cell

Image renders src as an image (click opens the full image in a new tab). alt is the hover/accessible label. Unlike RPC calls, hope does NOT proxy image bytes — the browser loads src directly, so src MUST be an absolute http(s) URL reachable from the browser (e.g. a public on-demand webp/avif image proxy). A non-http(s) src renders as its alt text. Usable as a table cell or a stat/card/cards field value.

With no opts it's a small inline thumbnail. Control the render box with opts:

Image(u, alt)                      // default thumbnail
Image(u, alt, ImgW(240))           // 240px wide, height auto (keeps aspect)
Image(u, alt, ImgBox(110, 110))    // fixed 110×110 box, image centered, contained
Image(u, alt, ImgBox(110,110), ImgFit("cover")) // fill the box, cropping overflow
func Link(value, to string) Cell

Link renders value as an in-app link that navigates to a hope route `to` (e.g. a master-detail page). Use ExternalLink for an off-site URL.

func Number

func Number(n any, unit string) Cell

Number renders n right-formatted with thousands separators; unit ("" = none) is appended (e.g. "MB", "reqs").

func Progress

func Progress(frac float64) Cell

Progress renders frac (0..1) as a small progress bar.

func Time

func Time(unix int64) Cell

Time renders a unix timestamp (seconds or millis) as relative time ("2h ago"), with the absolute time on hover.

type ChartData

type ChartData struct {
	Type   string        `json:"type,omitempty"`
	Labels []string      `json:"labels"`
	Series []ChartSeries `json:"series"`
}

ChartData is what a Chart view returns: categorical labels on the x-axis and one or more named series of values. Type is "bar" (default) or "line". A line chart with one series and many points is a good time-series-at-rest view (use a stream for live). hope draws the axes, gridlines, legend, and scaling.

type ChartSeries

type ChartSeries struct {
	Name   string    `json:"name"`
	Values []float64 `json:"values"`
}

ChartSeries is one named line/bar series; Values aligns with ChartData.Labels.

type Comp added in v0.0.6

type Comp struct {
	Kind     CompKind   `json:"kind"`
	Children []*Comp    `json:"children,omitempty"` // containers
	Text     string     `json:"text,omitempty"`     // text/heading content
	Label    string     `json:"label,omitempty"`    // keyval label
	Cell     Cell       `json:"cell,omitempty"`     // cell primitive: a rich Cell
	Value    any        `json:"value,omitempty"`    // keyval value (a scalar or a rich Cell)
	Level    int        `json:"level,omitempty"`    // heading level 1..4
	Tone     string     `json:"tone,omitempty"`     // ok|warn|bad|info accent (text/heading/keyval/box)
	Icon     string     `json:"icon,omitempty"`     // icon primitive
	Values   []float64  `json:"values,omitempty"`   // sparkline points
	Gap      int        `json:"gap,omitempty"`      // container child gap, px
	Size     int        `json:"size,omitempty"`     // row/grid child weight | spacer height px
	Table    *TableData `json:"table,omitempty"`    // embedded table (CompTable)
}

Comp is one node in a Component tree. Build nodes with the constructors below (Box/Stack/CRow/CGrid for containers; Heading/CText/KeyVal/CIcon/Sparkline/CCell for terminals) rather than filling this struct by hand. Unknown kinds are skipped by hope, never fatal, so a newer primitive degrades gracefully on an older hope.

func Box added in v0.0.6

func Box(children ...*Comp) *Comp

Box builds a vertical container (a tile/card body) from children.

func CCell added in v0.0.6

func CCell(cell Cell) *Comp

CCell wraps any rich Cell (Badge/Link/Number/Time/Progress/Code/Image) as a standalone primitive, so the whole cell vocabulary works inside a Component tree.

func CGrid added in v0.0.6

func CGrid(children ...*Comp) *Comp

CGrid builds a responsive grid of children (see CRow on the C-prefix).

func CIcon added in v0.0.6

func CIcon(name string) *Comp

CIcon builds a single icon (a hope built-in name or one of this plugin's Icons keys). C-prefixed to leave room for future top-level icon helpers.

func CRow added in v0.0.6

func CRow(children ...*Comp) *Comp

CRow builds a horizontal container (children laid out left-to-right). Named CRow (not Row) because Row already builds a layout Node; a Comp tree uses CRow.

func CTable added in v0.0.10

func CTable(data *TableData) *Comp

CTable embeds a full table inside a Component tree — column headers, aligned cells, ellipsis, DetailLink/Image cells, all rendered by hope's table renderer. Build the data with plugin.Table(...). Ideal for a compact list-with-structure inside a flyout/panel (e.g. the badges on a canvas) instead of hand-stacking rows.

func CText added in v0.0.6

func CText(s string) *Comp

CText builds a line/paragraph of text (auto-escaped by hope). C-prefixed because Text is already a ViewKind constant.

func Divider added in v0.0.6

func Divider() *Comp

Divider builds a horizontal rule between sections of a tile.

func Heading added in v0.0.6

func Heading(text string, level int) *Comp

Heading builds a heading; level is clamped to 1..4 by hope (1 = largest).

func KeyVal added in v0.0.6

func KeyVal(label string, value any) *Comp

KeyVal builds a label + value line. value may be a plain scalar or a rich Cell (Badge/Number/…); the `any` mirrors Number's signature — hope renders a Cell specially and stringifies a scalar.

func Spacer added in v0.0.6

func Spacer(px int) *Comp

Spacer builds a vertical gap of px pixels.

func Sparkline added in v0.0.6

func Sparkline(vals ...float64) *Comp

Sparkline builds a tiny inline line chart from the given points.

func Stack added in v0.0.6

func Stack(children ...*Comp) *Comp

Stack builds a tight vertical container — good for a run of KeyVal lines.

func (*Comp) Gapped added in v0.0.6

func (c *Comp) Gapped(px int) *Comp

Gapped sets the gap (px) between a container's children.

func (*Comp) Toned added in v0.0.6

func (c *Comp) Toned(tone string) *Comp

Toned sets a semantic accent (ToneOK/Warn/Bad/Info) on a node — a colored heading, a tinted box border, a status-colored keyval.

func (*Comp) Weight added in v0.0.6

func (c *Comp) Weight(n int) *Comp

Weight sets a child's flex/grid weight inside a CRow/CGrid (0 = equal share), mirroring a layout Node's Weight.

type CompKind added in v0.0.6

type CompKind string

CompKind names one primitive in a Component tree. Containers hold Children; terminals carry their own inline payload (text, a cell, values, an icon).

const (
	CompBox       CompKind = "box"       // vertical container (a card/tile body)
	CompStack     CompKind = "stack"     // vertical container, tighter — labeled rows
	CompRow       CompKind = "row"       // horizontal container (children side by side)
	CompGrid      CompKind = "grid"      // responsive grid of children
	CompText      CompKind = "text"      // a line/paragraph of text
	CompHeading   CompKind = "heading"   // a heading (Level 1..4)
	CompDivider   CompKind = "divider"   // a horizontal rule
	CompSpacer    CompKind = "spacer"    // vertical gap of Size px
	CompKeyval    CompKind = "keyval"    // a Label + Value line (value may be a rich Cell)
	CompIcon      CompKind = "icon"      // a single icon (built-in name or an Icons key)
	CompSparkline CompKind = "sparkline" // a tiny inline line chart from Values
	CompCell      CompKind = "cell"      // any rich Cell (Badge/Link/Number/Time/Progress/Code/Image)
	CompTable     CompKind = "table"     // an embedded table (rich cells, alignment, ellipsis)
)

type Contribution

type Contribution struct {
	Surface Surface    `json:"surface"`
	Title   string     `json:"title,omitempty"` // tab/page title
	Icon    string     `json:"icon,omitempty"`
	Match   *Match     `json:"match,omitempty"`
	Pages   []PageItem `json:"pages,omitempty"`
	Node    *Node      `json:"node"`
	// Actions are method names of registered actions shown as a toolbar at the top of
	// this surface (page/panel/dashboard header) — page-level actions distinct from
	// leaf actions inside the layout. hope collects fields, confirms danger, audits.
	Actions []string `json:"actions,omitempty"`
	// ID is a stable address for a page contribution, so links can target it by name
	// (a plugin does NOT know its hope key or a page's positional path). A DetailPage
	// sets ID + ParamKey and is Hidden from the rail; a Link/DetailLink navigates to
	// it plugin-relative, and hope passes the URL arg as param[ParamKey].
	ID       string `json:"id,omitempty"`
	Hidden   bool   `json:"hidden,omitempty"`    // not listed in the rail (a link/detail target)
	ParamKey string `json:"param_key,omitempty"` // detail pages: the URL arg becomes param[ParamKey]
	// Subtitle sets the page header's sub/meta line (page surfaces) — e.g. a record
	// count or a connection string. {param} placeholders are filled from the page
	// param. Empty => hope shows the plugin name.
	Subtitle string `json:"subtitle,omitempty"`
	// Breadcrumbs render above the page heading (page surfaces). Each Crumb's Label
	// and To may contain {param} placeholders hope fills from the page param — e.g.
	// on a user detail page, [{Users, users}, {"user {id}"}].
	Breadcrumbs []Crumb `json:"breadcrumbs,omitempty"`
}

Contribution mounts one layout tree onto a Surface. For container/stack surfaces, Match decides which containers it applies to. For page surfaces, Pages (optional) turns one node into MANY rail entries that share the layout but each pass a distinct Param.

type Crumb

type Crumb struct {
	Label string `json:"label"`
	To    string `json:"to,omitempty"`
}

Crumb is one breadcrumb. To (optional) is a plugin-relative navigation target (like a Link cell / DetailLink); the last crumb is usually the current page and has no To. {param} placeholders in Label/To are filled from the page param.

type EmitFunc

type EmitFunc func(v any)

EmitFunc pushes one frame to a live stream.

type EmptyOpt added in v0.0.6

type EmptyOpt func(*EmptyState)

EmptyOpt configures a view's EmptyState (its icon, secondary text, or a custom Comp).

func EmptyComp added in v0.0.6

func EmptyComp(c *Comp) EmptyOpt

EmptyComp sets a fully custom empty state (a Comp tree — see component.go), overriding the icon/title/text — e.g. an icon plus a "create one" Link.

func EmptyIcon added in v0.0.6

func EmptyIcon(name string) EmptyOpt

EmptyIcon sets the empty state's leading icon (a built-in name or an Icons key).

func EmptyText added in v0.0.6

func EmptyText(s string) EmptyOpt

EmptyText sets the empty state's dim secondary line under the title.

type EmptyState added in v0.0.6

type EmptyState struct {
	Icon  string `json:"icon,omitempty"`  // leading icon (a built-in name or an Icons key)
	Title string `json:"title,omitempty"` // the headline, e.g. "No slow queries 🎉"
	Text  string `json:"text,omitempty"`  // a dim secondary line
	Comp  *Comp  `json:"comp,omitempty"`  // optional custom empty state (overrides Icon/Title/Text)
}

EmptyState is what hope shows when a view resolves to no data (an empty table, a tree with no nodes, a stat with no blocks) instead of the generic "empty" text. The plain Icon/Title/Text cover the common case; set Comp for a fully custom empty state (an icon + heading + a "create one" Link). Build it with plugin.EmptyView.

type Event added in v0.1.0

type Event struct {
	Seq     uint64          `json:"seq,omitempty"`
	Kind    string          `json:"kind"`
	Host    string          `json:"host,omitempty"`
	Project string          `json:"project,omitempty"`
	IDs     []string        `json:"ids,omitempty"`
	Source  string          `json:"source,omitempty"`
	Ts      int64           `json:"ts,omitempty"`
	Data    json.RawMessage `json:"data,omitempty"`
}

Event is one hope event delivered to an OnEvent handler. Mirrors hope's wire event; Data is kind-specific JSON (may be empty). Delivered only when the plugin holds the events:subscribe grant.

type EventFunc added in v0.1.0

type EventFunc func(ctx context.Context, e Event) error

EventFunc handles a hope event pushed to the plugin (one unary hope.event call per event). It should return quickly; hope treats a slow/erroring handler as a missed delivery and moves on. Requires the events:subscribe permission.

type Facet

type Facet struct {
	Key     string   `json:"key"`
	Label   string   `json:"label"`
	Options []Option `json:"options"`
}

Facet is one dropdown filter on a server table: a key, a label, and the choices.

type Field

type Field struct {
	Key         string   `json:"key"`
	Label       string   `json:"label"`
	Type        string   `json:"type,omitempty"` // text|textarea|select|toggle|kv (default text)
	Placeholder string   `json:"placeholder,omitempty"`
	Hint        string   `json:"hint,omitempty"`
	Value       string   `json:"value,omitempty"`
	Optional    bool     `json:"optional,omitempty"`
	Options     []Option `json:"options,omitempty"`
}

Field mirrors hope's PromptField: the UI collects these before invoking an action, then passes the values map to the action handler.

type Hope added in v0.1.0

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

Hope is a handle to the operator ACTIONS a plugin may perform on hope — the operator pattern (watch events via OnEvent, then reconcile by mutating hope). Each action requires its own operator-granted scope and hope's reverse channel; without the grant hope rejects the call, and every action is audited. Actions target the plugin's OWN stack only.

func (*Hope) AddServiceLabel added in v0.1.0

func (h *Hope) AddServiceLabel(ctx context.Context, service, key, value string) error

AddServiceLabel adds (or updates) a label on a service in the plugin's own stack and hope re-applies the stack, so the label PERSISTS across future redeploys. The classic use is a plugin that auto-adds Prometheus scrape labels on deploy. service is a service name in this plugin's stack; the plugin can't target another stack. Requires the spec:label permission.

type ImageOpt

type ImageOpt func(map[string]any)

ImageOpt configures an Image cell's render box (size + fit).

func ImgBox

func ImgBox(w, h int) ImageOpt

ImgBox fixes both dimensions: the image is centered in a w×h box and, by default, contained (scaled to fit, no crop). Add ImgFit("cover") to fill and crop instead.

func ImgCache added in v0.0.9

func ImgCache() ImageOpt

ImgCache opts THIS image into hope's durable local byte cache. hope caches the flagged image client-side via a service worker (the CORS-free way — it caches the <img>'s own request/response, so the image host needs no CORS/cooperation) and serves it from cache on later views, surviving reloads and sessions. Opt-in per image: only images you flag are cached — nothing else. Prefer it on immutable, content-addressed art (a badge/canvas keyed by hash) that's expensive to re-fetch from a remote host.

func ImgFallback

func ImgFallback(url string) ImageOpt

ImgFallback sets an image shown when src fails to load (broken/404/blocked). It must also be an absolute http(s) URL. Tried once; if it too fails, the cell renders blank (no browser broken-image icon).

func ImgFit

func ImgFit(fit string) ImageOpt

ImgFit sets object-fit: "contain" (default — whole image, letterboxed) or "cover" (fill the box, cropping overflow).

func ImgH

func ImgH(px int) ImageOpt

ImgH fixes the image height in px; width is auto (keeps aspect).

func ImgLightbox

func ImgLightbox() ImageOpt

ImgLightbox makes clicking the image open it in an in-app lightbox (a dimmed full-screen overlay, closed by backdrop click / Esc / the close button) instead of a new browser tab — nicer for viewing badge/avatar art in place.

func ImgW

func ImgW(px int) ImageOpt

ImgW fixes the image width in px; height is auto, so the aspect ratio is kept (e.g. "240×N"). Combine with ImgH for a fixed box (see ImgBox).

type InitContext added in v0.0.7

type InitContext struct {
	Settings map[string]string
	Protocol int
	Caps     Capabilities
}

InitContext is what hope hands a plugin at initialization (hope.init): the settings the operator configured (so the plugin can set up WITH them), plus the hope build's protocol version and capabilities.

type KVData

type KVData = map[string]any

KVData is what a KV view returns: a flat map of label -> value. A value may be a plain scalar or a rich cell (Badge/Link/Image/…). It's a named alias so a KV handler reads as returning KVData, while a map literal still satisfies it.

type Layout

type Layout struct {
	ProtocolVersion int            `json:"protocolVersion"`
	Contributions   []Contribution `json:"contributions"`
}

Layout is the result of the fixed hope.layout method: the plugin's UI contributions, versioned for graceful cross-version degradation.

type Match

type Match struct {
	Always   bool              `json:"always,omitempty"`   // every container
	Images   []string          `json:"images,omitempty"`   // image-ref globs, e.g. "postgres*"
	Labels   map[string]string `json:"labels,omitempty"`   // label == value
	Services []string          `json:"services,omitempty"` // compose service names
}

Match decides which containers a container/stack contribution applies to. The plugin declares this (hope does not map containers to plugins). A nil/empty Match means "the plugin's own container" — the trivial self-describing case. Semantics: set clauses are AND-ed; values within a clause are OR-ed.

type Node

type Node struct {
	Kind  NodeKind `json:"kind"`
	Title string   `json:"title,omitempty"`
	Ref   string   `json:"ref,omitempty"`  // leaf: method name of a view/action/stream
	Size  int      `json:"size,omitempty"` // optional row/grid weight
	Fill  bool     `json:"fill,omitempty"` // grow to fill the remaining height (e.g. a table)
	// Collapsible makes a titled section fold on a title click; Collapsed starts it
	// closed. For dense pages where not everything needs to be open at once.
	Collapsible bool    `json:"collapsible,omitempty"`
	Collapsed   bool    `json:"collapsed,omitempty"`
	Children    []*Node `json:"children,omitempty"`
	// Comp carries an inline Component tree when Kind is NodeComponent (see Component).
	Comp *Comp `json:"comp,omitempty"`
}

Node is one node in the surface-agnostic layout tree. The same tree drives a container panel now and a full page later; hope's renderer walks it without caring which surface hosts it.

func Buttons added in v0.0.3

func Buttons(refs ...string) *Node

Buttons builds a horizontal group of action buttons from action method refs. The buttons size to content and sit together (a toolbar), unlike Row's stretched columns — e.g. Buttons("analyze", "vacuum") for a maintenance section.

func Component added in v0.0.6

func Component(c *Comp) *Node

Component builds an inline component node: a Comp tree (see component.go) rendered straight from the layout, with no per-view round-trip. Use it for a small static tile — e.g. plugin.Component(plugin.Box(plugin.Heading("Fleet", 3), …)).

func Grid

func Grid(children ...*Node) *Node

Grid builds a grid from children.

func Leaf

func Leaf(ref string) *Node

Leaf references a registered view/action/stream by its method name.

func Row

func Row(children ...*Node) *Node

Row builds a horizontal row from children — equal-width columns (each child gets an equal flex share). For a group of action buttons use Buttons instead, so they size to content and don't spread across the row.

func Section

func Section(title string, children ...*Node) *Node

Section builds a titled section node from children.

func Tabs

func Tabs(children ...*Node) *Node

Tabs builds a tabbed node from children (each child's Title is its tab label).

func (*Node) Collapse

func (n *Node) Collapse(collapsed bool) *Node

Collapse makes a titled section fold on a title click. Pass collapsed=true to start it closed.

func (*Node) Filled

func (n *Node) Filled() *Node

Filled marks a node to grow and fill the remaining height (and propagates up its ancestors when rendered) — e.g. a table that should fill the page.

func (*Node) Titled

func (n *Node) Titled(t string) *Node

Titled sets a node's title (useful for wrapping a Leaf inside Tabs).

func (*Node) Weight

func (n *Node) Weight(w int) *Node

Weight sets a child's flex weight inside a Row (or Grid) — the WIDTH proportion it takes relative to its siblings. Default (0) means equal share. e.g. in a two-column Row, Weight(1) beside Weight(2) makes the second column twice as wide:

plugin.Row(
    plugin.Section("Overview", plugin.Leaf("head")).Weight(1),
    plugin.Section("Fields", plugin.Leaf("fields")).Weight(2),
)

type NodeKind

type NodeKind string

NodeKind enumerates layout-tree node types. Containers hold Children; a leaf references a registered view/action/stream by method name.

const (
	NodeSection NodeKind = "section" // titled group
	NodeTabs    NodeKind = "tabs"    // tabbed children
	NodeRow     NodeKind = "row"     // horizontal arrangement (equal-width columns)
	NodeButtons NodeKind = "buttons" // horizontal group of action buttons, sized to content
	NodeGrid    NodeKind = "grid"    // grid arrangement
	NodeLeaf    NodeKind = "leaf"    // a single view/action/stream
	// NodeComponent carries an inline Comp tree (see component.go) rendered directly
	// from the layout — no ref, no per-view round-trip. Build it with Component().
	NodeComponent NodeKind = "component"
)

type Option

type Option struct {
	Label string `json:"label"`
	Value string `json:"value"`
}

Option is a select choice, mirroring hope's PromptOption.

type PageItem

type PageItem struct {
	Title    string         `json:"title"`
	Icon     string         `json:"icon,omitempty"`
	Param    map[string]any `json:"param,omitempty"`
	Children []PageItem     `json:"children,omitempty"`
}

PageItem is one dynamic subpage: it shares the contribution's Node but carries its own Param, which hope merges into every call the page makes — so a plugin can list e.g. every DB table as a rail entry that renders the same view with a different argument. Read it in a handler with plugin.Params(ctx, &v).

Children nests items one level (a group node), so e.g. three databases each listing their tables become nested rail entries. A node with Children is a group (not itself a page); a leaf node (no Children) is the navigable page.

type Permission added in v0.1.0

type Permission struct {
	Scope  string `json:"scope"`
	Reason string `json:"reason,omitempty"`
}

Permission is a reverse capability the plugin REQUESTS from hope. Least privilege: a plugin gets NOTHING on the plugin->hope direction unless it declares the scope here (via Plugin.RequirePermission) AND the operator consents when enabling it. hope records the granted subset and gates every reverse call on it; the plugin's token authenticates identity, the grant set authorizes. Reason is shown on the operator's consent prompt.

type Plugin

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

Plugin is a hope plugin server. Construct with New, register capabilities, then ListenAndServe. Registration is not safe for concurrent use; serving is.

func New

func New(name, version string) *Plugin

New creates a plugin with the given name and semantic version. The endpoint path defaults to /__hope and the request body cap to 4 MiB. If the environment sets HOPE_PLUGIN_TOKEN, calls must present it as a bearer token.

func (*Plugin) Action

func (p *Plugin) Action(method, label string, fields []Field, fn ActionFunc, opts ...ActionOpt) *Plugin

Action registers an invocable action. The UI collects fields, then calls fn with the values. Mark destructive actions with DangerAction.

func (*Plugin) Alert added in v0.1.0

func (p *Plugin) Alert(ctx context.Context, severity, title, detail, dedupeKey string) error

Alert is a convenience over Publish: it publishes an event whose Data is {severity, title, detail, dedupeKey}. hope namespaces the kind to plugin.<identity>.alert. Requires events:publish.

func (*Plugin) Breadcrumbs

func (p *Plugin) Breadcrumbs(crumbs ...Crumb) *Plugin

Breadcrumbs attaches a breadcrumb trail to the most recently added page contribution (call right after Page/DetailPage). {param} placeholders in a crumb's label/to are filled from the page param.

func (*Plugin) CardsView

func (p *Plugin) CardsView(method, label string, fn ViewFunc, opts ...ViewOpt) *Plugin

CardsView registers a Cards view; the handler returns CardsData (a grid of cards / a gallery — e.g. badges, users). Cards with a To navigate on click. opts (Static/EmptyView/…) are optional.

func (*Plugin) ChartView

func (p *Plugin) ChartView(method, label string, fn ViewFunc, opts ...ViewOpt) *Plugin

ChartView registers a Chart view; the handler returns ChartData (bar or line, one or more named series over categorical labels). hope draws axes + legend. opts (Static/EmptyView/…) are optional.

func (*Plugin) ComponentView added in v0.0.6

func (p *Plugin) ComponentView(method, label string, fn ViewFunc, opts ...ViewOpt) *Plugin

ComponentView registers a Component view — the escape hatch. The handler returns a *Comp: a tree of safe primitives (box/row/heading/keyval/sparkline/cell/…) hope composes into a custom widget the built-in kinds don't cover (see component.go). Add plugin.Static() to cache it or plugin.EmptyView(...) to customize its empty state. For a small STATIC tile, prefer an inline plugin.Component node in the layout instead — it needs no per-view round-trip.

func (*Plugin) ContainerPanel

func (p *Plugin) ContainerPanel(title string, match *Match, node *Node) *Plugin

ContainerPanel is a shorthand for a container-surface contribution with the given title, match, and layout node.

func (*Plugin) Contribute

func (p *Plugin) Contribute(c Contribution) *Plugin

Contribute adds an explicit UI contribution. Without any, the plugin auto-generates a single container contribution (see layout) listing every registered capability — so a minimal plugin needs no layout code.

func (*Plugin) DangerAction

func (p *Plugin) DangerAction(method, label string, fields []Field, fn ActionFunc, opts ...ActionOpt) *Plugin

DangerAction is like Action but flags the action destructive, so hope confirms before running it and audit-logs the invocation.

func (*Plugin) DashboardWidget

func (p *Plugin) DashboardWidget(title string, node *Node) *Plugin

DashboardWidget contributes a widget to hope's fleet/host dashboard (the `dashboard` surface): the node renders as a compact panel alongside hope's own dashboard cards. Keep it small — a couple of kv/counter/series leaves.

func (*Plugin) Description

func (p *Plugin) Description(s string) *Plugin

Description sets a one-line description shown in hope.

func (*Plugin) DetailPage

func (p *Plugin) DetailPage(id, title, paramKey string, node *Node) *Plugin

DetailPage contributes a hidden master-detail page addressed by a stable id (not shown in the rail). A Link/DetailLink navigates to it plugin-relative, and hope passes the URL arg as param[paramKey] — read it in a handler with plugin.Params. e.g. DetailPage("user", "User", "id", node) rendered at .../user/42 => {id:"42"}.

func (*Plugin) DynamicPage

func (p *Plugin) DynamicPage(title string, node *Node, items []PageItem) *Plugin

DynamicPage contributes MANY rail pages that share one layout node but each pass a distinct Param (merged into every call the page makes). items may nest one level (groups of pages) — e.g. databases -> tables. Read the param in a handler with plugin.Params. Regenerate the items in getLayout to reflect live state.

func (*Plugin) DynamicPageFunc added in v0.0.2

func (p *Plugin) DynamicPageFunc(title string, node *Node, fn func(ctx context.Context) []PageItem) *Plugin

DynamicPageFunc is DynamicPage with LIVE items: fn runs on every hope.layout to produce the rail entries, so the set reflects current state (a database's tables, a broker's topics) instead of a snapshot frozen at startup — the answer to "my pages depend on data I don't have until runtime." Keep fn fast: hope fetches the layout per surface, so cache if it hits a slow backend.

func (*Plugin) Handler

func (p *Plugin) Handler() http.Handler

Handler returns the http.Handler that serves the JSON-RPC endpoint at the plugin's path. Mount it yourself, or use ListenAndServe.

func (*Plugin) HeaderActions

func (p *Plugin) HeaderActions(refs ...string) *Plugin

HeaderActions attaches action buttons to the most recently added contribution — a page/panel/dashboard toolbar (distinct from leaf actions inside the layout). Each ref names a registered Action; hope collects its fields, confirms danger, and audits. Call it right after Page/ContainerPanel/DashboardWidget.

func (*Plugin) Hope added in v0.1.0

func (p *Plugin) Hope() *Hope

Hope returns the operator-actions handle.

func (*Plugin) Icon

func (p *Plugin) Icon(name string) *Plugin

Icon sets the plugin's default icon — a hope built-in name or one of the Icons keys registered with Icons.

func (*Plugin) Icons

func (p *Plugin) Icons(m map[string]string) *Plugin

Icons registers plugin-scoped icons: name -> inner SVG markup (path/circle/rect elements only, NOT a full <svg>), 24x24 stroke to match hope's icon set. hope resolves and sanitizes these in a per-plugin namespace, so they can't collide with other plugins or shadow hope's built-ins.

func (*Plugin) ListenAndServe

func (p *Plugin) ListenAndServe(addr string) error

ListenAndServe serves the plugin on addr (e.g. ":8080"). Blocks. A ReadHeaderTimeout guards against a slow-header (Slowloris) client holding a goroutine; no WriteTimeout is set because stream handlers write indefinitely.

func (*Plugin) MaxBodyBytes

func (p *Plugin) MaxBodyBytes(n int64) *Plugin

MaxBodyBytes overrides the request body cap (default 4 MiB).

func (*Plugin) OnEvent added in v0.1.0

func (p *Plugin) OnEvent(fn EventFunc) *Plugin

OnEvent registers a handler for hope events (stack/container/image/plugin/agent changes, and plugin-published events). hope pushes each relevant event as a unary hope.event call. Registering a handler auto-declares the events:subscribe permission, so the operator is asked to consent on enable; without the grant hope never calls it. Call RequirePermission(ScopeEventsSubscribe, "<reason>") first to set a custom consent reason.

func (*Plugin) OnInit added in v0.0.7

func (p *Plugin) OnInit(fn func(ctx context.Context, in InitContext) error) *Plugin

OnInit registers a handler hope calls once the plugin is reachable and being initialized (hope.init) — and again if the plugin restarts. It receives the current settings, so a plugin that needs its config at startup (a pool sized by a setting, a mode flag) can initialize with them instead of booting on defaults and waiting for a later hope.settings push. Optional: without it, settings are still applied so SettingValue works immediately. Errors are returned to hope (the install surfaces them). Register before ListenAndServe.

func (*Plugin) Page

func (p *Plugin) Page(title string, node *Node) *Plugin

Page contributes a full custom nav page (the `page` surface). hope lists it in the rail under the plugin and renders the node tree as a full page.

func (*Plugin) PageID

func (p *Plugin) PageID(id string) *Plugin

PageID gives the most recently added page contribution a stable id so links and breadcrumbs can target it by name (a plugin doesn't know positional page paths). Call right after Page.

func (*Plugin) Path

func (p *Plugin) Path(path string) *Plugin

Path overrides the endpoint path (default /__hope). Must match the hope.plugin.path label.

func (*Plugin) Publish added in v0.1.0

func (p *Plugin) Publish(ctx context.Context, e Event) error

Publish emits an event onto hope's bus. Requires (a) the events:publish permission granted by the operator and (b) hope's reverse channel configured. hope stamps the Source and namespaces the Kind (plugin.<identity>.<kind>), so only Kind + Data here are meaningful — a plugin can never spoof another's events or a core hope kind.

func (*Plugin) QueryView

func (p *Plugin) QueryView(method, label, lang, def string, fn ViewFunc, opts ...ViewOpt) *Plugin

QueryView registers a Query view whose input editor syntax-highlights lang and prepopulates with def (a template where {param} placeholders are filled from the page's param, e.g. "select * from {table}"). Read the input with plugin.Input.

func (*Plugin) RequestPermission added in v0.1.0

func (p *Plugin) RequestPermission(ctx context.Context, scope, reason string) error

RequestPermission asks the operator, at runtime, to grant a scope this plugin doesn't yet hold (the Android runtime-permission model). It does not grant anything — hope raises a consent prompt; the plugin gains the capability only if the operator allows it. Safe to call repeatedly: an already-granted/denied/pending scope is a no-op on hope's side. Declaring the scope up front via RequirePermission (so it's asked at enable) is usually better; use this when a capability is only needed conditionally at runtime.

func (*Plugin) RequirePermission added in v0.1.0

func (p *Plugin) RequirePermission(scope, reason string) *Plugin

RequirePermission declares that the plugin wants a reverse capability (an events:*/storage/... scope). It appears in hope.schema; the operator consents when enabling the plugin, and hope gates the capability on the grant. Idempotent per scope — a later call only updates the reason if one is given.

func (*Plugin) ResolveAlert added in v0.1.0

func (p *Plugin) ResolveAlert(ctx context.Context, title, dedupeKey string) error

ResolveAlert clears a previously-raised alert (matched by dedupeKey): hope surfaces it as a "resolved" confirmation and drops the alert from any active view. Use it when a monitored condition recovers. Requires events:publish.

func (*Plugin) SearchView

func (p *Plugin) SearchView(method, label string, fn ViewFunc) *Plugin

SearchView registers a Search (autocomplete) view: hope calls fn with {q: <text>} as the user types and renders the returned SearchData as a live dropdown; selecting a SearchItem navigates to its To. Read the query with plugin.SearchQuery(ctx). Great for a "go to <entity>" jump box.

func (*Plugin) Setting

func (p *Plugin) Setting(s Setting) *Plugin

Setting declares an operator-managed configuration field. hope renders these in the plugin inspector, persists the values (encrypted), and pushes them to the plugin; read a value in any handler with SettingValue. Settings are config the operator SETS — distinct from the panel the plugin SHOWS.

func (*Plugin) SettingValue

func (p *Plugin) SettingValue(key string) string

SettingValue returns the current value of an operator-managed setting (the value hope last pushed), falling back to the declared default. Safe for concurrent use.

func (*Plugin) StackWidget added in v0.0.2

func (p *Plugin) StackWidget(title string, match *Match, node *Node) *Plugin

StackWidget contributes a widget to the stack view (the `stack` surface): the node renders as a panel on a stack's page, for stacks whose containers the Match selects. It's the container panel's whole-stack analog — same Match semantics (images/labels/services/always), but evaluated against the stack's set of containers rather than one. Keep it focused: a stack-scoped overview or action.

func (*Plugin) StatView

func (p *Plugin) StatView(method, label string, fn ViewFunc, opts ...ViewOpt) *Plugin

StatView registers a Stat view: the handler returns StatData (one or more big-number blocks — counts, totals, sizes). Add plugin.Refreshable() for a manual refresh button (e.g. "count rows in my table" on demand).

func (*Plugin) Storage added in v0.1.0

func (p *Plugin) Storage() *Storage

Storage returns this plugin's storage handle.

func (*Plugin) Stream

func (p *Plugin) Stream(method, label string, kind StreamKind, fn StreamFunc) *Plugin

Stream registers a live stream emitted as NDJSON frames.

func (*Plugin) Subtitle

func (p *Plugin) Subtitle(s string) *Plugin

Subtitle sets the page header's sub/meta line for the most recently added page contribution ({param} placeholders filled from the page param). Call after Page.

func (*Plugin) TableView

func (p *Plugin) TableView(method, label string, fn ViewFunc, opts ...TableOpt) *Plugin

TableView registers a Table view. Add RowDetail/RowActions opts to make rows interactive — e.g. TableView("rows","Rows",fn, plugin.RowDetail("inspect"), plugin.RowActions(plugin.RowAction{Method:"del",Label:"Delete",Danger:true})).

func (*Plugin) TextView

func (p *Plugin) TextView(method, label string, fn ViewFunc, opts ...ViewOpt) *Plugin

TextView registers a Text view: the handler returns {text: "…"} (or a raw string) rendered as a monospace scrollable block — logs, config, command output. opts (Static/EmptyView/…) are optional.

func (*Plugin) Token

func (p *Plugin) Token(token string) *Plugin

Token pins the shared secret hope must present as a bearer token, overriding HOPE_PLUGIN_TOKEN. Use when you configure the token in code rather than env.

func (*Plugin) View

func (p *Plugin) View(method, label string, kind ViewKind, fn ViewFunc, opts ...ViewOpt) *Plugin

View registers a read-only data view rendered per kind. opts (Static/EmptyView/ Refreshable/RefreshEvery) are optional and apply to any kind.

type RowAction

type RowAction struct {
	Method string   `json:"method"`
	Label  string   `json:"label"`
	Icon   string   `json:"icon,omitempty"`
	Danger bool     `json:"danger,omitempty"` // hope confirms before running and audit-logs it
	Fields []Field  `json:"fields,omitempty"` // optional input collected before the call
	Tip    *Tooltip `json:"tip,omitempty"`    // hover tooltip on the row action button (build with Tip)
}

RowAction is one author-declared action bound to a table row. hope calls Method with {row: {column: value}} (plus the page param); use for row-scoped mutations. If Fields is set, hope collects them first and merges the values into the call params alongside row — e.g. a "Rename" action with a new-name field.

type Schema

type Schema struct {
	ProtocolVersion int               `json:"protocolVersion"`
	Name            string            `json:"name"`
	Version         string            `json:"version"`
	Description     string            `json:"description,omitempty"`
	Icon            string            `json:"icon,omitempty"`  // default icon: a hope built-in OR an Icons key
	Icons           map[string]string `json:"icons,omitempty"` // plugin-scoped {name: inner-svg-markup}
	Actions         []ActionDesc      `json:"actions"`
	Views           []ViewDesc        `json:"views"`
	Streams         []StreamDesc      `json:"streams"`
	Settings        []Setting         `json:"settings,omitempty"`    // operator-managed config (see Setting)
	Permissions     []Permission      `json:"permissions,omitempty"` // reverse-capability requests (see Permission)
}

Schema is the plugin's identity + capability manifest — the result of the fixed hope.schema method. It is the only method hope calls before the operator enables the plugin (discovery), so it must be safe to expose unauthenticated.

type SearchData

type SearchData struct {
	Items []SearchItem `json:"items"`
}

SearchData is what a Search view returns for a query: the current suggestion list.

type SearchItem

type SearchItem struct {
	Label string `json:"label"`
	Sub   string `json:"sub,omitempty"`
	Image string `json:"image,omitempty"`
	To    string `json:"to"`
}

SearchItem is one autocomplete suggestion. Label is the primary text; Sub is a dim secondary line (e.g. an id); Image is an optional thumbnail (absolute http(s) URL); To is where selecting it navigates — plugin-relative like a Link/DetailLink cell (e.g. "creator/42").

type Setting

type Setting struct {
	Key     string      `json:"key"`
	Label   string      `json:"label"`
	Kind    SettingKind `json:"kind,omitempty"` // default text
	Default string      `json:"default,omitempty"`
	Hint    string      `json:"hint,omitempty"`
	Options []Option    `json:"options,omitempty"` // for kind=select
}

Setting is one operator-managed configuration field the plugin exposes. Unlike an action's Fields (per-invocation input), settings are configured once in the plugin inspector, persisted by hope (encrypted at rest), and pushed to the plugin via the reserved hope.settings method — read them in a handler with plugin.SettingValue(key). This is distinct from the plugin's rendered panel & metrics, which live on the container inspector: settings are what the operator CONFIGURES, the panel is what the plugin SHOWS.

type SettingKind

type SettingKind string

SettingKind enumerates the input type for an operator-managed plugin setting.

const (
	SettingText     SettingKind = "text"
	SettingTextarea SettingKind = "textarea"
	SettingSelect   SettingKind = "select"
	SettingToggle   SettingKind = "toggle"
	SettingNumber   SettingKind = "number"
	SettingSecret   SettingKind = "secret" // masked input; hope stores it encrypted, never renders it back
)

type SortSpec

type SortSpec struct {
	Column string `json:"column"`
	Dir    string `json:"dir"`
}

SortSpec is a column + direction ("asc" | "desc"). Used for a table's DefaultSort.

type StatBlock

type StatBlock struct {
	Label string   `json:"label"`
	Value any      `json:"value"`
	Unit  string   `json:"unit,omitempty"`
	Sub   string   `json:"sub,omitempty"`
	Tone  string   `json:"tone,omitempty"` // ok|warn|bad|info
	Icon  string   `json:"icon,omitempty"`
	Tip   *Tooltip `json:"tip,omitempty"` // hover tooltip explaining the metric (build with Tip)
}

StatBlock is one stat: a big Value with a Label, optional Unit, a Sub line (e.g. a delta or context), a semantic Tone, and an optional Icon.

type StatData

type StatData struct {
	Stats []StatBlock `json:"stats"`
}

StatData is what a Stat view returns: one or more big-number stat blocks (counts, totals, sizes). Return {stats: [...]} for a row, or a single Stat.

type Storage added in v0.1.0

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

Storage is a handle to this plugin install's durable key/value store, persisted by hope. Values are opaque JSON hope never interprets, namespaced to this install's stable identity (two installs of the same image, or the same install on different hosts, are isolated; replicas of one install share it — last write wins). Requires the storage permission and hope's reverse channel; every op returns ErrNoReverseChannel until hope.init delivers a callback URL.

Use it for config a stateless plugin has nowhere else to keep — e.g. alert rules the operator defined. It's for small config, not bulk data (hope caps value size).

func (*Storage) Delete added in v0.1.0

func (s *Storage) Delete(ctx context.Context, key string) error

Delete removes key.

func (*Storage) Get added in v0.1.0

func (s *Storage) Get(ctx context.Context, key string, out any) (bool, error)

Get decodes the value at key into out. Returns (false, nil) when the key is absent.

func (*Storage) List added in v0.1.0

func (s *Storage) List(ctx context.Context, prefix string) ([]string, error)

List returns the keys under prefix (empty prefix = all this plugin's keys).

func (*Storage) Set added in v0.1.0

func (s *Storage) Set(ctx context.Context, key string, v any) error

Set stores v (JSON-encoded) at key.

type StreamDesc

type StreamDesc struct {
	Method string     `json:"method"`
	Label  string     `json:"label"`
	Kind   StreamKind `json:"kind"`
	Icon   string     `json:"icon,omitempty"`
}

StreamDesc describes a live stream and how to render it.

type StreamFunc

type StreamFunc func(ctx context.Context, emit EmitFunc) error

StreamFunc emits frames until it returns or ctx is cancelled (which hope does the moment the UI disconnects — so a dropped viewer never leaves you emitting forever). Always select on ctx.Done() in long loops.

type StreamKind

type StreamKind string

StreamKind tells hope how to render a live NDJSON stream.

const (
	Counter StreamKind = "counter" // number(s) ticking -> stat
	Log     StreamKind = "log"     // append-only lines
	Series  StreamKind = "series"  // time series -> sparkline
)

type Surface

type Surface string

Surface names a mount point in the hope UI. The schema defines them all so the wire is forward-compatible; hope renders the surfaces it knows and silently ignores any it doesn't, so a newer plugin degrades gracefully on older hope.

const (
	SurfaceContainer Surface = "container" // panel/tab in the container inspector
	SurfacePage      Surface = "page"      // full custom nav page (+ dynamic nested pages)
	SurfaceRail      Surface = "rail"      // rail/nav entry + actions
	SurfaceDashboard Surface = "dashboard" // fleet/host dashboard widget
	SurfaceStack     Surface = "stack"     // stack-view widget (matched to the stack's containers)
	SurfaceCommand   Surface = "command"   // command-palette entry
)

type TableData

type TableData struct {
	Columns []string `json:"columns"`
	Rows    [][]any  `json:"rows"`
	Total   int      `json:"total"`
	Hidden  []string `json:"hidden,omitempty"`
	// ColumnTips maps a column name to a hover tooltip on its header — for clarifying
	// a terse or computed column (e.g. "health": Tip("seq-scan / bloat state")).
	ColumnTips map[string]*Tooltip `json:"column_tips,omitempty"`
	// RowMethod makes rows clickable, opening the row-detail modal via that method —
	// the data-side equivalent of the RowDetail table option (for views without opts,
	// like a QueryView).
	RowMethod string `json:"row_method,omitempty"`
	// RowFlyout makes rows clickable into a right-side DRAWER instead of a modal. The
	// method receives the clicked {row} and returns a component tree (plugin.Box(...)) hope
	// renders in the flyout. Data-side equivalent of the RowFlyout table option. When both
	// RowFlyout and RowMethod are set, the flyout wins.
	RowFlyout string `json:"row_flyout,omitempty"`
	// Flush renders the table full-height with no toolbar (filter/pager) and no inner scroll
	// box — it flows and the containing panel/flyout scrolls instead. Meant for an embedded
	// CTable showing a bounded list (e.g. the badges on a canvas), not a big paged view.
	Flush bool `json:"flush,omitempty"`
}

TableData is what a Table view returns: the column names, the rows (each a slice of cells — plain scalars or rich cells like Badge/Link/Image), the total row count (for the pager; for a server table this is the full count, not len(Rows)), and optional hidden column names (kept in each row for row-detail/actions but not rendered). Build it with plugin.Table(...).

type TableOpt

type TableOpt func(*ViewDesc)

TableOpt configures an interactive Table view (row-click detail, row actions).

func DefaultSort

func DefaultSort(column, dir string) TableOpt

DefaultSort sets the sort a server table applies on first load (before the user touches a column header) — e.g. DefaultSort("indexed", "desc") for newest-first. dir is "asc" or "desc"; column must be one your handler's sort map accepts.

func Editable

func Editable(method string, columns ...string) TableOpt

Editable makes cells editable: editing one calls method with {row, column, value}. Pass column names to limit which are editable (none => every column).

func Facets

func Facets(facets ...Facet) TableOpt

Facets adds dropdown filters to a server table; the selected values arrive in the query as filters[key] (read with ReadTableQuery). Apply them in your store.

func NoFilter

func NoFilter() TableOpt

NoFilter hides a table's search box — for a plain paged list with no user search.

func NoSort

func NoSort() TableOpt

NoSort makes a table's column headers non-interactive — the order is fixed by your handler (e.g. always newest-first), not user-sortable. Pair with NoFilter for a pure paged list.

func PageSize

func PageSize(n int) TableOpt

PageSize sets how many rows hope shows per page for this table (0 => hope default).

func RowActions

func RowActions(actions ...RowAction) TableOpt

RowActions adds per-row action buttons; each click calls the action's Method with {row: {column: value}}. Use for row-scoped mutations like "delete row".

func RowDetail

func RowDetail(method string) TableOpt

RowDetail makes table rows clickable: a click calls method with {row: {column: value}} and shows the returned kv/table in a modal (an author-controlled RPC).

func RowDetailButton

func RowDetailButton(method string) TableOpt

RowDetailButton is like RowDetail but triggers from a per-row button instead of a whole-row click — use it when the row is also inline-editable so the click to edit a cell and the click to open the detail don't collide.

func RowFlyout added in v0.0.8

func RowFlyout(method string) TableOpt

RowFlyout makes table rows clickable into a right-side DRAWER (not a modal): a click calls method with {row: {column: value}} and hope renders the returned component tree (plugin.Box(...) — image, key/values, Buttons bound to actions) in the flyout. Richer than RowDetail's modal; good for a compact record shown beside the table. If both RowFlyout and RowDetail are set, the flyout wins.

func ServerSide

func ServerSide() TableOpt

ServerSide marks a table server-driven: hope sends the query state each call and expects one page + a total back, so a table too large to ship whole still works. Read the query with plugin.ReadTableQuery and return {columns, rows, total}.

type TableQuery

type TableQuery struct {
	Page     int               `json:"page"`
	PageSize int               `json:"page_size"`
	Sort     TableSort         `json:"sort"`
	Filter   string            `json:"filter"`
	Filters  map[string]string `json:"filters"`
}

TableQuery is hope's per-call query state for a ServerSide table: which page (0- based) of what size, an optional sort, the free-text filter, and any Facet selections (filters[key] = chosen value). Use it to run just that slice of your data and return {columns, rows, total}.

func ReadTableQuery

func ReadTableQuery(ctx context.Context) (q TableQuery, ok bool)

ReadTableQuery reads the server-table query state hope sends (under "_q"). ok is false when the call carried none (e.g. a non-server table) — treat that as page 0.

type TableSort

type TableSort struct {
	Column string `json:"column"`
	Dir    int    `json:"dir"`
}

TableSort is the sort request for a server-driven table: which column, and the direction (1 ascending, -1 descending; 0 = unsorted).

type TextData

type TextData struct {
	Text string `json:"text"`
}

TextData is what a Text view returns: a block of monospace text (logs, config, command output). A Text handler may also return a raw string.

type TipPos added in v0.0.4

type TipPos string

TipPos is where a tooltip points relative to its target.

const (
	TipTop       TipPos = "top" // default
	TipBottom    TipPos = "bottom"
	TipTopEnd    TipPos = "top-end"
	TipBottomEnd TipPos = "bottom-end"
)

type Tooltip added in v0.0.4

type Tooltip struct {
	Text string `json:"text"`
	Pos  TipPos `json:"pos,omitempty"`
}

Tooltip is hover help with an optional placement. Build one with Tip("text") or Tip("text", plugin.TipBottom). Empty Pos renders at the top.

func Tip added in v0.0.4

func Tip(text string, pos ...TipPos) *Tooltip

Tip builds a Tooltip — hover help text with an optional placement (the author's control over where it points): Tip("Reclaims space", plugin.TipBottom). Extra pos args are ignored.

type TreeData added in v0.0.5

type TreeData struct {
	Nodes []TreeNode `json:"nodes"`
}

TreeData is what a Tree view returns: a hierarchy of nodes. Return it (or a bare map literal) from a Tree handler.

type TreeNode added in v0.0.5

type TreeNode struct {
	Label     string     `json:"label"`
	Icon      string     `json:"icon,omitempty"` // built-in icon name or an Icons key
	Tone      string     `json:"tone,omitempty"` // ok|warn|bad|info dot
	To        string     `json:"to,omitempty"`   // plugin-relative nav target (like a Link cell)
	Collapsed bool       `json:"collapsed,omitempty"`
	Tip       *Tooltip   `json:"tip,omitempty"`
	Children  []TreeNode `json:"children,omitempty"`
}

TreeNode is one node in a Tree view. Children make it a collapsible group (Collapsed starts it closed). A non-empty To makes the label a plugin-relative link (like a Link/DetailLink cell), so a tree can navigate — e.g. To: "table/public.users". Icon and Tone add a leading icon and a status dot; Tip adds a hover tooltip. A node can be both a group and a link: the caret toggles children, the label navigates.

type ViewDesc

type ViewDesc struct {
	Method string   `json:"method"`
	Label  string   `json:"label"`
	Kind   ViewKind `json:"kind"`
	Icon   string   `json:"icon,omitempty"`
	// Empty customizes the "no data" state (see EmptyState); unset => hope's generic text.
	Empty   *EmptyState `json:"empty,omitempty"`
	Lang    string      `json:"lang,omitempty"`    // query views: syntax-highlight language (sql, json, …)
	Default string      `json:"default,omitempty"` // query views: initial text; {param} placeholders are filled from the page param
	// RowMethod (table/query views): a method hope calls to open a row-detail modal,
	// with params {row: {column: value}}. The result (kv or table) is shown in a
	// modal — a fully author-controlled row-detail RPC.
	RowMethod string `json:"row_method,omitempty"`
	// RowFlyout (table/query views): like RowMethod but opens a right-side DRAWER instead
	// of a modal. The method receives {row} and returns a component tree hope renders in
	// the flyout. When both are set, the flyout wins.
	RowFlyout string `json:"row_flyout,omitempty"`
	// RowDetailButton triggers RowMethod from a dedicated per-row button instead of a
	// whole-row click. Use when the row body is otherwise interactive (e.g. inline-
	// editable cells) so the two don't fight.
	RowDetailButton bool `json:"row_detail_button,omitempty"`
	// RowActions (table/query views): per-row action buttons. hope renders each in a
	// trailing column (and in the row-detail modal); clicking one calls its Method
	// with {row: {column: value}} — an author-controlled mutation like "delete row".
	// A Danger action is confirmed first; on success hope refetches the table.
	RowActions []RowAction `json:"row_actions,omitempty"`
	// PageSize (table/query views) sets how many rows hope shows per page — the
	// author knows the shape of their data, so paging is plugin-level. 0 => hope's
	// default.
	PageSize int `json:"page_size,omitempty"`
	// EditMethod (table/query views): editing a cell calls this method with
	// {row: {column: value}, column, value}. Return ok (and optionally a message).
	// EditColumns limits which columns are editable (empty => all). An
	// author-controlled inline-edit RPC — hope refetches the table on success.
	EditMethod  string   `json:"edit_method,omitempty"`
	EditColumns []string `json:"edit_columns,omitempty"`
	// Server marks a table server-driven: hope does NOT ship every row and page in
	// the browser. Instead it sends the query state ({_q: {page, page_size, sort,
	// filter}}) on each call and expects one page back plus a total row count
	// ({columns, rows, total}). Required for tables too large to send whole — read
	// the query with plugin.ReadTableQuery.
	Server bool `json:"server,omitempty"`
	// Refresh adds a manual refresh button to the view header that re-fetches it —
	// e.g. a stat/counter you want to recompute on demand.
	Refresh bool `json:"refresh,omitempty"`
	// RefreshInterval auto-refetches the view every N seconds (0 = off) — a live-ish
	// view without a stream. hope stops the timer when the view leaves the DOM.
	RefreshInterval int `json:"refresh_interval,omitempty"`
	// Static marks a view's data as fixed for the life of the surface: hope fetches it
	// once and serves the cached result on tab re-entry / re-navigation instead of
	// re-calling the plugin — cutting round-trips and easing the per-plugin rate limit.
	// A manual Refresh() button still forces a re-fetch, so Static()+Refresh() = "load
	// once, refresh on demand". Don't combine with RefreshInterval.
	Static bool `json:"static,omitempty"`
	// Facets (server tables) are dropdown filters hope renders in the toolbar; the
	// selected values arrive in the query as filters[key] (see TableQuery.Filters),
	// which you apply in your store. Distinct from the free-text search box.
	Facets []Facet `json:"facets,omitempty"`
	// DefaultSort (server tables) is the sort hope applies on FIRST load, before the
	// user clicks any column header — e.g. newest-indexed first. hope seeds the query
	// state with it (so it arrives in TableQuery.Sort) and shows the arrow on that
	// column. The user can still re-sort. Column must be one your handler accepts.
	DefaultSort *SortSpec `json:"default_sort,omitempty"`
	// NoFilter hides the search box and NoSort makes column headers non-interactive —
	// for a plain paged list (a fixed order your handler controls, no user search/sort).
	NoFilter bool `json:"no_filter,omitempty"`
	NoSort   bool `json:"no_sort,omitempty"`
}

ViewDesc describes a read-only data view and how to render it.

type ViewFunc

type ViewFunc func(ctx context.Context) (any, error)

ViewFunc returns the data for a view; hope renders it per the view's ViewKind. For a Query view, the user's input arrives in the request params — read it with plugin.Input(ctx) (or plugin.Params for structured input).

type ViewKind

type ViewKind string

ViewKind tells hope how to render a view's returned data.

const (
	KV    ViewKind = "kv"    // flat key/value map -> hope-kvlist
	Table ViewKind = "table" // {columns, rows} -> paginated grid
	Query ViewKind = "query" // user-edited input -> tabular result grid (e.g. a SQL box)
	Tree  ViewKind = "tree"  // hierarchy -> tree browser (e.g. a schema)
	Chart ViewKind = "chart" // {type, labels, series} -> bar/line chart (see ChartData)
	Cards ViewKind = "cards" // {items:[Card]} -> a responsive grid of cards (a gallery)
	Stat  ViewKind = "stat"  // {stats:[Stat]} (or one Stat) -> big-number stat blocks (see StatData)
	Text  ViewKind = "text"  // {text:"…"} (or a raw string) -> a monospace scrollable block (logs, config, output)
	// Search is an autocomplete box: hope calls the method with {q: <typed text>} as the
	// user types (debounced) and renders the returned SearchItems as a live dropdown.
	// Selecting one navigates to its To (a DetailPage/link target) — a "go to X" jump.
	Search ViewKind = "search"
	// CompView is the escape hatch: the handler returns a *Comp — a tree of safe
	// primitives (box/row/heading/keyval/sparkline/cell/…) hope composes into a custom
	// widget the built-in kinds don't cover. See component.go. Also usable inline in a
	// layout via plugin.Component (no per-view round-trip).
	CompView ViewKind = "component"
)

type ViewOpt

type ViewOpt = TableOpt

ViewOpt configures a view (Refreshable, PageSize on tables, …). TableOpt is the same underlying type, so table options compose here too.

func EmptyView added in v0.0.6

func EmptyView(title string, opts ...EmptyOpt) ViewOpt

EmptyView customizes the "no data" state shown when a view resolves empty (an empty table, a stat with no blocks) instead of hope's generic text — a title plus optional icon/text (or a custom Comp). Attach it wherever view opts are accepted (TableView, StatView, ComponentView), e.g.

p.TableView("slow", "Slow queries", fn,
    plugin.EmptyView("No slow queries 🎉", plugin.EmptyIcon("check")))

func RefreshEvery

func RefreshEvery(seconds int) ViewOpt

RefreshEvery auto-refetches the view every n seconds (a live-ish view without a stream). hope stops the timer when the view leaves the DOM.

func Refreshable

func Refreshable() ViewOpt

Refreshable adds a manual refresh button to the view header (re-fetches on click).

func Static added in v0.0.6

func Static() ViewOpt

Static marks a view's data fixed for the life of the surface: hope fetches it once and reuses the cached result on tab re-entry / re-navigation instead of re-calling the plugin — fewer round-trips, less rate-limit pressure. Pair with Refreshable() for a "load once, refresh on demand" view. Don't combine with RefreshEvery.

Jump to

Keyboard shortcuts

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