glyph 0.7.0

v0.7.0 is a cleanup and capability release. It removes a lot of old public surface area, tightens the main component API, and adds several pieces that make larger Glyph apps easier to build: declarative routing, modal route scopes, stronger nested dynamic templates, interactive rich text, better forms, and more reliable screen effects.

Highlights

Added

Declarative event routing

Added On and Key so key handlers can live in the component tree and follow the same branch/scope lifecycle as the UI they belong to.

VBox(
	On(
		Key("<C-s>", save),
		Key("<Esc>", close),
	),
	Text("settings"),
)

Conditional route scopes

Handlers declared inside conditional UI now activate and deactivate with that branch.

If(&editing).Then(
	VBox(
		On(
			Key("<Enter>", saveEdit),
			Key("<Esc>", cancelEdit),
		),
		Text("editing"),
	),
)

Modal route scopes

Added On.Modal for handlers that should push a modal input scope while their UI is active.

If(&showConfirm).Then(
	Overlay(
		VBox(
			On.Modal(
				Key("<Enter>", confirm),
				Key("<Esc>", func() { showConfirm = false }),
			),
			Text("confirm deploy?"),
		),
	),
)

Rich span jump targets

Rich text spans can now register jump targets directly, including inside scroll views and repeated content.

spans := []Span{
	{Text: "open docs", OnSelect: openDocs},
}

Rich(&spans)

Live rich spans

Rich now accepts live span slices directly, so updating the backing slice updates the rendered rich text.

status := []Span{
	{Text: "idle", Style: Style{FG: Ansi16(8)}},
}

view := Rich(&status)

status = []Span{
	{Text: "running", Style: Style{FG: Ansi16(10)}},
}

Custom effect compilation

Added EffectCompiler, EffectFloat64, and EffectCompilable so custom effects can compile dynamic values, animated values, and conditional values through the template system.

type shadeEffect struct {
	strengthArg any
	strength    EffectFloat64
}

func (e shadeEffect) CompileEffect(c EffectCompiler) Effect {
	e.strength = c.Float64(e.strengthArg)
	return e
}

func (e shadeEffect) Apply(buf *Buffer, ctx PostContext) {
	strength := e.strength.Float64()
	for y := 0; y < ctx.Height; y++ {
		for x := 0; x < ctx.Width; x++ {
			cell := buf.Get(x, y)
			if cell.Rune == 0 || cell.Style.FG.Mode == ColorDefault {
				continue
			}
			cell.Style.FG = Lerp(cell.Style.FG, RGB(0, 0, 0), strength)
			buf.Set(x, y, cell)
		}
	}
}

ScreenEffect(
	shadeEffect{
		strengthArg: In(Animate.Duration(time.Second)(0.5)).
			Out(Animate.Duration(time.Second)(0.0)),
	},
)

Readable colour helpers

Added helpers for contrast-aware colour work.

bg := RGB(18, 18, 22)
fg := RGB(120, 130, 145)
accent := RGB(60, 130, 255)

readable := ReadableTint(bg, accent, fg, 4.5, 0.65)
ratio := ContrastRatio(readable, bg)

Improved

Nested ForEach

Nested repeated UI can now bind to slice fields on the current item and preserve the correct outer and inner item contexts.

type Project struct {
	Name  string
	Tasks []Task
}

type Task struct {
	Title string
}

ForEach(&projects, func(project *Project) Component {
	return VBox(
		Text(project.Name),
		ForEach(&project.Tasks, func(task *Task) Component {
			return Text(task.Title)
		}),
	)
})

Scoped route lifecycle

Route handlers declared inside conditional and switched UI now attach only while that branch is active.

Switch(&mode).
	Case("list", VBox(
		On(Key("<Enter>", openSelected)),
		List(&items, renderItem),
	)).
	Case("search", VBox(
		On(Key("<Esc>", exitSearch)),
		Input(&query).Bind(),
	))

Modal input precedence

Modal route scopes now take input precedence while active, so modal handlers can shadow root handlers without leaking after the modal closes.

VBox(
	On(Key("<Esc>", quit)),
	If(&confirming).Then(
		Overlay(
			VBox(
				On.Modal(Key("<Esc>", func() { confirming = false })),
				Text("cancel deploy?"),
			),
		),
	),
)

Screen effect opacity and dynamic values

Screen effects now compose better with live UI state: focus-based effects track node opacity, drop shadows and SpinGlow have clearer opacity controls, and effect strength values can be compiled from dynamic inputs.

var cardRef NodeRef

VBox.Ref(&cardRef).Opacity(cardOpacity)(
	Text("syncing"),
)

ScreenEffect(
	SEGlow().
		Focus(&cardRef).
		Strength(&glowStrength).
		Radius(5),
	SEDropShadow().
		Focus(&cardRef).
		Opacity(&shadowOpacity).
		OpacityMode(OpacitySmooth).
		Radius(6),
	SESpinGlow(&cardRef).
		Opacity(&glowOpacity).
		OpacityMode(OpacityPaint).
		Radius(4).
		Speed(1.2),
)

Selection list layout

Selection list rendering and row layout are more robust for complex rows, shrinking lists, and constrained viewports.

List(&items, func(item *Item) Component {
	return HBox(
		Text(item.Name),
		Text(item.Status),
	)
})

Text view navigation naming

Text view navigation now uses the same naming pattern as other navigable components.

TextView(content).
	BindNav("j", "k").
	BindPageNav("<C-d>", "<C-u>")

Radio navigation naming

Radio navigation parameter names now match down/up navigation order.

Radio(&selected, "small", "medium", "large").
	BindNav("j", "k")

Changed

Fixed

Removed

Migration notes

If you're running any migrations from v0.6.0 or previous you should first run go fix ./... which should automate the vast majority of the API cleanup for you.

Most migration work in this release is mechanical API cleanup.

// before
Widget(measure, render)

// after
Custom(measure, render)
// before
Scroll(contentSize, viewSize, &position)

// after
Scrollbar(contentSize, viewSize, &position)
// before
BasicColor(2)
PaletteColor(42)

// after
Ansi16(2)
Ansi256(42)
// before
BlendColor(base, top, BlendColorDodge)
LerpColor(a, b, 0.5)

// after
Blend(base, top, BlendDodge)
Lerp(a, b, 0.5)
// before
SEDimAll()

// after
SEDim()
// before
LayerView(layer).ViewWidth(40).ViewHeight(8)

// after
LayerView(layer).Width(40).Height(8)
// before
app.Push(router)
app.Pop()

// after
app.PushRouter(router)
app.PopRouter()
// before
TextView(content).BindScroll("j", "k")
TextView(content).BindPageScroll("<C-d>", "<C-u>")

// after
TextView(content).BindNav("j", "k")
TextView(content).BindPageNav("<C-d>", "<C-u>")
// before
CheckboxPtr(&enabled, &label)

// after
Checkbox(&enabled, &label)

Some deprecated shims remain for renamed helpers, but this release intentionally removes a lot of older public API surface as part of the cleanup.

Updated demos and examples

Full changelog on GitHub