glyph v0.8.0
Happy to announce Glyph 0.8.0 - it's a much larger release than usual with lots of new stuff and fixes. The new features have been led by things I'm building that are putting it through its paces in complex real-world scenarios, but I think the best part of the release is how much more stable and predictable it feels compared to 0.7.0.
Upgrading from 0.7.0
Three behaviour changes can break working code with no compile error. go fix cannot help with these.
- Fixed-height containers now clip.
OverflowHiddenis the zero value, so a container with an explicitHeightwhose content overflowed silently loses the overflow..Overflow(visible)restores the old behaviour. Most likely of the three to bite. - A present dynamic width binding means explicitly sized. A binding evaluating to 0 now reads as "explicitly sized, currently zero" rather than unset, so it no longer falls through to implicit flex.
- Explicit text widths clip in both render paths, where one path previously let content through.
Deprecated, removed in 0.9.0
22 renamed identifiers, all still working in 0.8.0. Each carries a //go:fix inline directive, so go fix ./... or a gopls quick-fix migrates call sites automatically - the whole list is a one-command job.
NewFilter -> Filter
Scroll -> Scrollbar
Widget -> Custom
BasicColor -> Ansi16
PaletteColor -> Ansi256
BlendColor -> Blend
LerpColor -> Lerp
BlendColorDodge -> BlendDodge
BlendColorBurn -> BlendBurn
SEDimAll -> SEDim
Bool -> FormatBool
Bytes -> FormatBytes
Currency -> FormatCurrency
Number -> FormatNumber
Percent -> FormatPercent
PercentChange -> FormatPercentChange
LayerViewC.ViewWidth -> Width
LayerViewC.ViewHeight -> Height
App.Push -> PushRouter
App.Pop -> PopRouter
MatchNode -> MatchC
SwitchNode -> SwitchC
Added
Components
Complete- native autocomplete over any field: anchored dropdown, prefix filter, single-router navigation. Esc and arrows fall through to the parent when the dropdown is closed.KeyHelpand named bindings -Key(...).Named(),App.ActiveBindings, live what's-bound-now help styled withKeyFG,DescFG,GapandRaw.App.LoadKeyBindingsreads a user rebind config.Trigger- focusable form control withActivate,OnActivateandBindFocus.term(experimental) - embeddable pty-backed terminal in its own package.term.New()forks a shell;term.Stream(rw, onResize)drives a byte stream the caller owns. A VT interpreter renders pty output into a cell grid composited as an ordinary Layer-backed component: SGR to truecolor, UTF-8 and DEC special graphics, double-width cells, the alternate screen, cursor-position and device-attribute replies. The API may change.
Animation and rendering
- Oscillators and self-gated animation -
Osc(hz)value nodes bindable to widths and parameters, withSine,Saw,Square,Triangle,Steps,Phase,Range,Speed,EaseandLerpshaping. Self-animating spinner viaSpinnerC.Fps; effects can request animation throughPostContext.RequestAnimation. - Cache-effects frames - effect-only frames re-run screen effects over the cached render instead of re-executing the template; effect-param oscillators drive those frames.
- Paint-only animation frames -
App.EnablePaintFrames(opt-in): an animation tick that proves nothing geometric moved skips the two geometry passes and repaints from the last layout. App.Apply(fn)- render-thread closure queue drained at frame top, so off-thread state changes marshal safely onto the render goroutine.- Adaptive render pacing - input renders coalesce when the terminal is behind.
Template.ExecutePaintandTemplate.RunEffectEvals- the paint-only and effect-only entry points.App.ForceRedraw;App.SetViewDiagnosticsilences the view-redefine nudge.Buffer.PushClip/PopClip- explicit clip-rect stack.
Layout and text
MaxWidthon VBox and HBox - a pure upper-bound cap: containers wrap at a bound and hug the longest line..Overflow(visible)- opt out of the new fixed-height clip.TruncateWidth/TruncateWidthEllipsis- display-width-aware clip that never splits a wide rune.Rich(&s).Markdown()- bound inline markdown (bold, italic, code, strike, bullet), parse-on-change cached, ForEach-composable.MarkdownWidthmeasures for content-hug sizing;MarkdownSpansand the zero-allocMarkdownSpansIntoexpose the spans.PreserveBGon text and rich text - a write mode that keeps the destination cell background.TextViewC.WordWrapand an explicit wrap text mode; char-wrap chainable on rich text.- New exported types:
RichTextNode,OverflowMode,StyleRange,ForEachFn. - ForEach
Limitand remaining bindings via the configure-then-call chain.
ScrollView, Layer and List
- Feather - scroll-overflow edge fade, native in the blit, fading toward the actual background behind each cell.
Layer.SetFeatherfor raw layers. - Bound scroll offset (
ScrollState/ScrollOffset) - Animate can drive scrolling from a single source of truth. AnchorBottom- hug underflowing content to the bottom edge.- ScrollView scrollbars;
ListC.ScrollStateplusScrollbarDynso a List carries a live scrollbar like a Layer. ListC.BindHalfPageNav- half-page up and down.ListC.ScrollEase- a list paints through an eased row offset instead of snapping to its window;Layer.DisplayedScrollYexposes the displayed offset.- Scrollbar surface:
ScrollbarForLayer, and on ScrollViewScrollbar,ScrollbarVisible,ScrollbarOpacity,ScrollbarOpacityMode,ScrollbarThumbStyle,ScrollbarTrackStyle;ScrollbarC.OpacityandOpacityMode. ScrollViewC.ScrollTo,ScrollViewFn.Height,LayerViewC.HeightPtr.
Input and focus
InputC.MultiLine- wrap long text across lines instead of horizontal scroll, with alt+enter / ctrl+j newlines wired toAllowNewlines.Input.StyleRanges- style sub-ranges of the live editable value (mention or search highlight); zero cost when unset.CheckListC.Selection(*int)- external-selection setter, parity with List.FilterListC.OnFilterChange/OnSelect.
Jump mode
App.EnterJumpScope- restrict jump targets to NodeRef regions;JumpItem/JumpItemRefper-item constructors;App.JumpScopeKey.JumpMode.Activate()/Deactivate().- Incremental label feedback - the typed prefix dims along with non-matching labels; backspace undoes a typed key and restores the previous labels.
- Per-item jump targets for ForEach templates; off-screen targets culled.
App and diagnostics
App.Suspend/App.Resume- gate rendering while an external program owns the terminal.App.LastInputUnixNano()- last-input stamp for idle detection.App.CurrentView();Screen.FrontBuffer(); rich span rendered refs; whole-frame guardrail benches.
Changed
- The example apps moved from
cmd/toexamples/, all 60 in one place;go run ./cmd/xbecomesgo run ./examples/x. - Fixed-height containers now clip their content to the box;
.Overflow(visible)opts out, and the blit honours the clip. WidthPctfails loud on a wrong type at build; static over/under-percent is accepted for animation headroom, and the [0,1] range clamp is gone.MaxWidthis a pure upper-bound cap, decoupled from content-hug;maxwidthpctremoved before it ever shipped.- Present dynamic width bindings mean explicitly sized - zero stays zero, no implicit flex.
- Checklist
Style/SelectedStyleaccept live*Style, restoring theme-reactivity in build-once views. - Explicit text widths clip content in both render paths.
OnFilterChangefires on every query-mutation path with changed-only semantics; a FilterList query change resets selection to the top.- Initial terminal size is delivered as a resize event.
- ForEach per-item caches are bounded - realloc-orphaned per-item state and orphaned markdown span keys are evicted.
- Redefining a named view emits a one-time stderr nudge toward If/Switch.
- An animation ticker could keep running after its view left the screen. Tickers stop on every routing path and retire when the animation settles.
- An async render requested right after an input flush was silently dropped. Requests carry a generation now, so none are discarded.
- The App running flag is atomic, so
Stopis callable from any goroutine. - ForEach item height is pinned as intrinsic in layout; text measurement is memoised per unchanged string.
Fixed
Races, deadlocks and memory
- Two goroutines writing to the terminal at once could interleave mid-escape-sequence and corrupt the display. Writes now go through a single lock.
- Toggling jump mode while a frame rendered could race, or deadlock the app outright. The active flag is atomic, targets rebuild by swap, and no lock is held across a render.
- Scrolling a layer while it rendered raced on the scroll position. The scroll fields sit behind a mutex that is never held across the render callback.
- The render goroutine could read the cursor's scroll position while the input goroutine wrote it. Reads are guarded now.
- Rich-text span strings were stored as raw pointers the garbage collector could not see, so the strings could be freed while still on screen. They are typed pointers now.
- Resizing the terminal mid-render could tear the frame buffer's storage. Resize waits for the frame to finish.
- Drawing vertical rules allocated on every frame. A VBox of any child count renders allocation-free.
Layout and rendering
- A ForEach used directly as a branch or as the root view failed to lay out. It lays out like any other component.
- Scrolling a list could leave a blank row at the bottom for one frame. The window fills in a single pass from the final offset.
- A list with no
MaxVisiblecould not scroll back up once clipped. It scrolls both ways. - Rows partially visible at the list edge were dropped instead of clipped. They render, clipped to the viewport.
- A
MaxWidthcap was ignored when the parent sized itself from content, so parent and child disagreed on width. Both sizing paths clamp. - Content inside an If branch could overflow the parent's width, and flex children in a branch could land in the wrong place. Branches size from their data, clamp to the parent, and redistribute flex after stretch.
- Wrapped text could spill below a fixed-height container. It clips.
- TextBlock wrapped by rune count, so wide characters (CJK, emoji) overflowed their line. It wraps by display width with a placeholder for the cut rune.
- A layer re-rendered when its viewport width changed but not when only the height did, leaving a stale frame on screen. Height changes re-render too, and the viewport height can bind to a pointer.
- ScrollView capped its render buffer at 500 rows, truncating tall content such as a long chat. The buffer grows to fit and the fitting height is cached across frames.
- A queued scroll could fire late and yank the view away from where a manual scroll had put it. Stale pending scrolls are dropped.
- Rich text reported the wrong natural width; jump wrappers claimed more rows than their content; dynamic styles applied one frame late. All three measure and prime correctly.
Per-item (ForEach) binding
- A per-item border colour pointer silently dropped the border. It binds to each rendered element.
- Numeric text, fill and flex values bound to an item field could show the first item's value on every row. Each row resolves its own field.
- Spinner, leader, counter, sparkline, tabs and scrollbar bindings inside a ForEach read from the wrong item. Every leaf binding is offset to the rendered element.
- Tweens inside a ForEach could animate the wrong row, and per-item exit animations detached from their item. Both pin to the rendered element.
- A tween's
OnCompletecould fire mid-frame, so mutating bound state from the callback raced the render. It fires at the end of the frame.
Focus, modals and forms
- Closing an overlay with an exit animation left its key handling active behind it. The modal router releases while the overlay exits.
- Swapping route branches under a modal dropped the modal's handlers. They survive the swap.
- Escape could blur a field and have the focus snap back the next frame. Focus managers push on the show edge only, so the blur persists and reaches the modal router.
- A form could validate stale values because bound inputs synced after validation ran. Values sync first, then validation, then the submit callback.
- Text typed into an input was lost when a jump pushed a new view. The input carries over.
- A closed or hidden node kept reporting its last on-screen position. Its ref reads zero when the node is not rendered this frame.
- Jump-label feedback could fail to engage while typing. Typed keys accumulate directly.
- KeyHelp stamped solid default-background chips over styled fills. It composites over the underlying fill.
- Leaving the alternate screen after a resize could land the cursor on the wrong row. It lands on the row it left.
- A log scrolled away from the tail jumped when new lines arrived. The position holds unless the log is following.
- Shrinking a pane could drop its bottom rows. The component keeps them, not only the grid.
- Jump targets over a slice of pointers resolved the slice slot rather than the struct it points at, so a jump could land on stale data. They resolve the pointed-to struct.