Skip to content

v0.8.0

Latest

Choose a tag to compare

@kungfusheep kungfusheep released this 28 Aug 16:49

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. OverflowHidden is the zero value, so a container with an explicit Height whose 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.
  • KeyHelp and named bindings - Key(...).Named(), App.ActiveBindings, live what's-bound-now help styled with KeyFG, DescFG, Gap and Raw. App.LoadKeyBindings reads a user rebind config.
  • Trigger - focusable form control with Activate, OnActivate and BindFocus.
  • 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, with Sine, Saw, Square, Triangle, Steps, Phase, Range, Speed, Ease and Lerp shaping. Self-animating spinner via SpinnerC.Fps; effects can request animation through PostContext.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.ExecutePaint and Template.RunEffectEvals - the paint-only and effect-only entry points.
  • App.ForceRedraw; App.SetViewDiagnostic silences the view-redefine nudge.
  • Buffer.PushClip / PopClip - explicit clip-rect stack.

Layout and text

  • MaxWidth on 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. MarkdownWidth measures for content-hug sizing; MarkdownSpans and the zero-alloc MarkdownSpansInto expose the spans.
  • PreserveBG on text and rich text - a write mode that keeps the destination cell background.
  • TextViewC.WordWrap and an explicit wrap text mode; char-wrap chainable on rich text.
  • New exported types: RichTextNode, OverflowMode, StyleRange, ForEachFn.
  • ForEach Limit and 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.SetFeather for 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.ScrollState plus ScrollbarDyn so 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.DisplayedScrollY exposes the displayed offset.
  • Scrollbar surface: ScrollbarForLayer, and on ScrollView Scrollbar, ScrollbarVisible, ScrollbarOpacity, ScrollbarOpacityMode, ScrollbarThumbStyle, ScrollbarTrackStyle; ScrollbarC.Opacity and OpacityMode.
  • 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 to AllowNewlines.
  • 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 / JumpItemRef per-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/ to examples/, all 60 in one place; go run ./cmd/x becomes go run ./examples/x.
  • Fixed-height containers now clip their content to the box; .Overflow(visible) opts out, and the blit honours the clip.
  • WidthPct fails loud on a wrong type at build; static over/under-percent is accepted for animation headroom, and the [0,1] range clamp is gone.
  • MaxWidth is a pure upper-bound cap, decoupled from content-hug; maxwidthpct removed before it ever shipped.
  • Present dynamic width bindings mean explicitly sized - zero stays zero, no implicit flex.
  • Checklist Style / SelectedStyle accept live *Style, restoring theme-reactivity in build-once views.
  • Explicit text widths clip content in both render paths.
  • OnFilterChange fires 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 Stop is 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 MaxVisible could 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 MaxWidth cap 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 OnComplete could 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.