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
- Declarative event routing with
OnandKey. - Modal route scopes for overlays, dialogs, and temporary input layers.
- Much stronger nested dynamic UI support, especially around
ForEach. - Rich text can now wrap, update live, and expose jump targets from spans.
- Screen effects now compose better with live UI state such as focus refs and opacity.
- Forms and inputs gained more styling and state-control options.
- The public API has been tidied up, with legacy struct-literal node APIs removed.
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
- The public API has been renamed in several places for consistency.
- Default animation easing is now
EaseOutQuart. - Default animation duration is now
280ms. - Concrete screen effect types are exported, making configured effects easier to pass around.
ScreenEffectnow returns a component rather than exposing the old node struct type.TextfandRichnow return component values rather than public compile-form node structs.Checkboxaccepts eitherstringor*stringfor static and dynamic labels.- App router methods have been renamed to
PushRouterandPopRouter. - Layer view sizing methods have been renamed to
WidthandHeight. - Text view scrolling bindings have been renamed to
BindNavandBindPageNav. - Colour helpers have been renamed to shorter names such as
Ansi16,Ansi256,Blend, andLerp. - Blend mode names now use
BlendDodgeandBlendBurn. SEDimAllhas been renamed toSEDim.MatchNodeandSwitchNodehave been renamed toMatchCandSwitchC.
Fixed
- Fixed rich text wrapping across lines.
- Corrected nested repeated-template bindings, including item field captures and nested slice fields.
- Corrected
ForEachrow layout insideHBox, including parent-width constraints. - Reworked repeated-template rendering to reduce direct-vs-repeated inconsistencies for layers, overlays, screen effects, and autotables.
- Corrected dynamic match, switch, and container style values inside repeated templates.
- Improved screen effect fade behaviour for glow, bloom, shadow, SpinGlow, and vignette-style effects.
- Propagated detected terminal backgrounds into compositing and effect rendering.
- Fixed selection list clamping when source data shrinks or becomes empty.
- Fixed rich span jump target registration after wrapping.
- Fixed rich span jump targets inside scroll views.
- Fixed dynamic checkbox labels through the main
Checkboxconstructor. - Improved jump label rendering across rich spans, scroll views, and composed layouts.
- Fixed jump mode behaviour when no jump mode state has been initialised yet.
Removed
- Removed legacy struct-literal compile paths for tabs, overlays, tables, custom nodes, rich text nodes, text inputs, and screen effect nodes.
- Removed old public struct-literal node APIs from the preferred component-building path.
- Removed the legacy table renderer in favour of the newer table/autotable paths.
- Removed direct animation
Outchaining from tweens. Use theIn(...).Out(...)presence pattern instead. - Removed redundant examples that no longer matched the preferred API shape.
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
- Updated remaining demos away from legacy struct-literal node APIs.
- Updated showcase, table, widget, selection, postprocess, nesting, avionics, minivim, todo, and stream demos for the current API.
- Added
cmd/ondemofor declarative route handling. - Added
cmd/easeharnessfor easing behaviour. - Updated examples for renamed helpers and preferred component constructors.