progstatus

package module
v0.1.1-0...-c4ddd0f Latest Latest
Warning

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

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

README

progstatus

Go Reference

tuios.dev/progstatus is a Go package for the Program Status Protocol, OSC 7501. A program writes one escape sequence to say what it does: idle, working, done, blocked or error. A terminal reads the sequence and shows the state.

The package has the parts that every user of the protocol needs:

  • The parser. It applies every discard rule of the specification before it returns a report.
  • The record store. It has the limits, the eviction, the id hierarchy and the lifetime rules.
  • The summary rule. It picks the record that a pane shows.
  • The encoder. A program uses it to write a report.

The package uses only the standard library. tuios uses it in its terminal emulator, its daemon, its client and the tuios status command.

The specification is at superlogical.com/rex/docs/build/program-status. This package follows revision 0.2 (2026-10-06).

Install

go get tuios.dev/progstatus

The go command reads the import path from tuios.dev. Go 1.25 or later can fetch it directly. With the default module proxy, any Go version can fetch it.

Write a report from a program

package main

import (
	"os"

	"tuios.dev/progstatus"
)

func main() {
	tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0)
	if err != nil {
		return // no terminal: nothing to report to
	}
	defer tty.Close()

	tty.WriteString(progstatus.Sequence(progstatus.Report{
		State:    progstatus.Working,
		App:      "build",
		Msg:      "Compiling",
		Progress: 40,
	}))
}

Write the sequence to /dev/tty. Then the report reaches the terminal when the output of the program goes to a file or a pipe.

A terminal discards a report that breaks a rule, such as a message that is too long. Use FitText for free text and FitSegment for an id or an app name that comes from outside input:

seq := progstatus.Sequence(progstatus.Report{
	State:    progstatus.Working,
	ID:       progstatus.FitSegment(branch, progstatus.MaxSegment),
	Msg:      progstatus.FitText(line, progstatus.MaxMsgDecoded),
	Progress: progstatus.NoProgress,
})

Read reports in a terminal

Give Parse the bytes after 7501; and the length of the whole sequence. Store each report that it accepts.

var store progstatus.Store

func onOSC7501(body []byte, seqLen int, reply func(string)) {
	if progstatus.IsQuery(body) {
		reply(progstatus.QueryReply + "\x1b\\") // use the terminator of the query
		return
	}
	report, ok := progstatus.Parse(body, seqLen)
	if !ok {
		return // discarded: apply nothing
	}
	store.Apply(report, time.Now().UnixNano())
}

func paneStatus() (progstatus.Record, bool) {
	return progstatus.Summary(store.Records())
}

SequenceLen gives the length of a sequence from the length of its payload. Call Store.DropTransient when a shell prompt appears (OSC 133 A) or the process exits. Call Store.Reset on a full reset (RIS).

Stability

The module is at v0. The API can change between minor versions. The tags have the folder as a prefix, such as progstatus/v0.1.0.

Issues

The code is in the tuios repository, in the progstatus folder. Report issues in the tuios issue tracker.

Licence

MIT. See LICENSE.

Documentation

Overview

Package progstatus implements the terminal side of the Program Status Protocol, OSC 7501, revision 0.2 (2026-10-06): https://www.superlogical.com/rex/docs/build/program-status

A program writes OSC 7501 ; key=value:key=value ST to say what it is doing (idle, working, done, blocked or error), and the terminal keeps one record per id. This package holds the parts every user of the protocol shares: the parser, which applies every discard rule before anything is stored; the record store, with its limits, eviction, id hierarchy and lifetime events; the summary rule that picks the record a pane shows; and the encoder a program uses to write a report.

A terminal emulator calls Parse on the body of each OSC 7501 sequence and hands the result to a Store. A program calls Sequence to build a report. The package uses only the standard library. tuios uses it in its emulator, its daemon and its client, and for the tuios status command.

The API is at v0 and can change between minor versions.

Index

Examples

Constants

View Source
const (
	// MaxSequence is the most bytes of one sequence, OSC through ST.
	MaxSequence = 4096
	// MaxKey is the longest key, in bytes.
	MaxKey = 16
	// MaxMsgEncoded and MaxMsgDecoded bound msg before and after base64.
	MaxMsgEncoded = 2732
	MaxMsgDecoded = 2048
	// MaxTitleEncoded and MaxTitleDecoded bound title before and after base64.
	MaxTitleEncoded = 256
	MaxTitleDecoded = 192
	// MaxApp is the longest app value, in bytes.
	MaxApp = 32
	// MaxID, MaxSegment and MaxDepth bound an id.
	MaxID      = 128
	MaxSegment = 32
	MaxDepth   = 8
	// MaxRecords is how many records one terminal holds. The specification
	// requires at least 64 and caps it at 256.
	MaxRecords = 256
)

Limits from the specification's Limits section. A report that breaks one is discarded whole.

View Source
const Command = 7501

Command is the OSC number of the protocol.

View Source
const NoProgress = -1

NoProgress is the Progress of a report or record that carries none.

View Source
const QueryReply = "\x1b]7501;?"

QueryReply is the only thing a terminal writes back, without its ST.

Variables

This section is empty.

Functions

func Encode

func Encode(r Report) string

Encode builds the body of a report for r, without OSC and ST: the pairs joined with ":", title and msg in standard base64. It does not check r: a caller that builds a report from outside input runs the result through Parse, or makes each value fit first with FitText and FitSegment.

func FitSegment

func FitSegment(s string, limit int) string

FitSegment makes s a valid id segment or app value: bytes outside [A-Za-z0-9_.+-] become "-", and the result is cut to max bytes. An empty result becomes "-".

func FitText

func FitText(s string, limit int) string

FitText makes s fit a free-text value: control characters become spaces, invalid UTF-8 is dropped, and the text is cut on a rune boundary to max bytes (limit). A producer uses it so a report it builds is never discarded.

Example

A program builds a report from text it does not control. FitText and FitSegment make each value fit, so the terminal never discards the report.

package main

import (
	"fmt"

	"tuios.dev/progstatus"
)

func main() {
	branch := "feature/new login"
	msg := "Testing " + branch + "\n"
	seq := progstatus.Sequence(progstatus.Report{
		State:    progstatus.Working,
		ID:       progstatus.FitSegment(branch, progstatus.MaxSegment),
		App:      progstatus.FitSegment("ci", progstatus.MaxApp),
		Msg:      progstatus.FitText(msg, progstatus.MaxMsgDecoded),
		Progress: 40,
	})
	fmt.Printf("%q\n", seq)
}

func InheritApp

func InheritApp(recs []Record)

InheritApp fills EffectiveApp on every record of recs from the record's own app or its nearest ancestor's. A parent does not have to exist, so the search walks up the id and skips missing levels.

func IsControl

func IsControl(r rune) bool

IsControl reports whether r is a control character as the specification defines one: U+0000 to U+001F, U+007F and U+0080 to U+009F.

func IsQuery

func IsQuery(body []byte) bool

IsQuery reports whether body, the bytes after "7501;", is the feature detection query.

func Sequence

func Sequence(r Report) string

Sequence is Encode wrapped in OSC 7501 and ESC \.

Example

A program reports that it waits for an approval. The sequence goes to the terminal, so a program whose output goes to a file writes it to /dev/tty.

package main

import (
	"fmt"
	"os"

	"tuios.dev/progstatus"
)

func main() {
	seq := progstatus.Sequence(progstatus.Report{
		State: progstatus.Blocked,
		Kind:  progstatus.Permission,
		App:   "deploy",
		Msg:   "Approve deploy to production?",
	})
	fmt.Fprint(os.Stdout, seq)
}

func SequenceLen

func SequenceLen(payloadLen int, bel bool) int

SequenceLen is the length of a whole sequence, OSC through ST, whose payload (the bytes between OSC and ST, "7501;..." included) is payloadLen bytes and which ended with BEL when bel is true.

func Urgency

func Urgency(s State) int

Urgency orders states for the summary: the higher, the more a person needs to see it.

func ValidApp

func ValidApp(app string) bool

ValidApp reports whether app matches the app grammar, [A-Za-z0-9_.+-]{1,32}.

func ValidID

func ValidID(id string) bool

ValidID reports whether id matches the id grammar and its limits: one to eight segments of 1 to 32 bytes of [A-Za-z0-9_.+-], joined with "/", at most 128 bytes in all. The root record has no id, so "" is not valid here.

Types

type Event

type Event struct {
	Report Report
	Reset  bool
}

Event is what an emulator hands its owner: a report, or a full reset (RIS), which removes every record.

type Kind

type Kind string

Kind is what a blocked record waits for.

const (
	Permission Kind = "permission"
	Question   Kind = "question"
	Auth       Kind = "auth"
)

The kinds of the specification.

type Record

type Record struct {
	ID       string
	State    State
	Kind     Kind
	Progress int
	// App is the record's own app. EffectiveApp is it or the nearest
	// ancestor's, filled in by Records.
	App          string
	EffectiveApp string
	Title        string
	Msg          string
	// Seq orders updates: the record updated least recently has the lowest.
	Seq uint64
	// At is when the record was last replaced, in Unix nanoseconds.
	At int64
	// Group is the process group that held the terminal's foreground when
	// the report came in, for a working, blocked or idle record from a
	// program in the foreground, and 0 otherwise. When that group ends, the
	// program that reported has exited (DropEnded). The owner of the store
	// sets it; the protocol knows nothing of it.
	Group int
}

Record is what the store keeps for one id.

func Summary

func Summary(recs []Record) (Record, bool)

Summary picks the record a pane shows: the most urgent state wins (blocked, then error, done, working, idle). Among records of the same urgency the root wins, and after it the one updated most recently. It reports false for no records.

func (Record) IsRoot

func (r Record) IsRoot() bool

IsRoot reports whether r is the root record.

type Report

type Report struct {
	State State
	// ID is the record's id, empty for the root record.
	ID string
	// Kind is set only on a blocked report with a recognised kind.
	Kind Kind
	// Progress is 0 to 100 on a working or blocked report that carried a
	// valid one, and NoProgress otherwise.
	Progress int
	// App is the program's own name, empty when absent or invalid.
	App string
	// Title and Msg are the decoded texts, empty when absent.
	Title string
	Msg   string
}

Report is one OSC 7501 report that passed every check.

func Parse

func Parse(body []byte, seqLen int) (Report, bool)

Parse reads the body of a report, the bytes after "7501;", from a sequence seqLen bytes long. It returns false when the report is discarded or ignored; nothing from such a report may be applied. Every pair is checked before the report is returned, so a caller never applies half of one.

type State

type State string

State is the state key of a report.

const (
	Idle    State = "idle"
	Working State = "working"
	Done    State = "done"
	Blocked State = "blocked"
	Error   State = "error"
	Clear   State = "clear"
)

The states of the specification. Clear is not a state a record can hold: it removes records.

type Store

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

Store holds one terminal's records. Its zero value is empty and ready. It is safe for concurrent use, and it never calls out while it holds its lock.

Example

A terminal emulator reads the body of an OSC 7501 sequence, the bytes after "7501;", and keeps the result in a Store. Summary picks the record to show.

package main

import (
	"fmt"
	"strings"
	"time"

	"tuios.dev/progstatus"
)

func main() {
	var store progstatus.Store

	// The sequence as it came from the program, ended with ST (ESC \).
	seq := "\x1b]7501;state=working:app=build:progress=40\x1b\\"
	payload := strings.TrimSuffix(strings.TrimPrefix(seq, "\x1b]"), "\x1b\\")
	body := []byte(strings.TrimPrefix(payload, "7501;"))

	if progstatus.IsQuery(body) {
		// Answer the feature query with the terminator the question used.
		fmt.Print(progstatus.QueryReply + "\x1b\\")
		return
	}
	report, ok := progstatus.Parse(body, progstatus.SequenceLen(len(payload), false))
	if !ok {
		return // discarded: apply nothing
	}
	store.Apply(report, time.Now().UnixNano())

	if rec, ok := progstatus.Summary(store.Records()); ok {
		fmt.Println(rec.EffectiveApp, rec.State, rec.Progress)
	}
}

func (*Store) Apply

func (s *Store) Apply(r Report, now int64) bool

Apply stores one report that Parse returned, and reports whether the records changed. A report replaces its record whole. A clear removes the addressed record and every record beneath it, and with no id removes every record.

func (*Store) ApplyFrom

func (s *Store) ApplyFrom(r Report, now int64, group int) bool

ApplyFrom is Apply for a report that came from the process group group (see Record.Group), 0 for none. The group is kept only on a working, blocked or idle record, the states a program's exit ends.

func (*Store) DropEnded

func (s *Store) DropEnded(ended func(group int) bool) bool

DropEnded removes the working, blocked and idle records whose group ended says has ended: the program that reported them has exited. Records from any other group, and records with none, stay. It reports whether anything was removed. ended is called with the store's lock held, so it must not call back into the store.

func (*Store) DropFinished

func (s *Store) DropFinished() bool

DropFinished removes done and error records. tuios calls it when the person types into the pane, which is when the specification suggests a terminal stops showing them.

func (*Store) DropTransient

func (s *Store) DropTransient() bool

DropTransient is what a shell prompt (OSC 133 A) and the exit of the terminal's process do: working and blocked records go, and so do idle ones, which the specification allows. done and error stay.

func (*Store) GroupOf

func (s *Store) GroupOf(id string) int

GroupOf is the group record id came from, 0 when it has none or does not exist.

func (*Store) Groups

func (s *Store) Groups() []int

Groups lists the distinct groups records came from, 0 left out.

func (*Store) Handle

func (s *Store) Handle(ev Event, now int64) bool

Handle applies an emulator event at now (Unix nanoseconds) and reports whether the records changed.

func (*Store) HandleFrom

func (s *Store) HandleFrom(ev Event, now int64, group int) bool

HandleFrom is Handle for a report that came from the process group group (see Record.Group), 0 for none.

func (*Store) HasFinished

func (s *Store) HasFinished() bool

HasFinished reports whether a done or error record is held.

func (*Store) Len

func (s *Store) Len() int

Len is how many records the store holds.

func (*Store) Records

func (s *Store) Records() []Record

Records returns a copy of every record, the root first and the rest in id order, with EffectiveApp filled in from the nearest ancestor that has an app.

func (*Store) Reset

func (s *Store) Reset() bool

Reset is a full reset (RIS): every record goes, and OSC 9;4 may be read again. A soft reset (DECSTR) does not call it.

func (*Store) Seen

func (s *Store) Seen() bool

Seen reports whether a report has arrived since the last full reset.

Jump to

Keyboard shortcuts

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