agentsclimarketplace

Glyph

Skill kungfusheep/glyph-skill/skills/glyph

Idiomatic patterns for the glyph Go TUI framework. Use when the user is writing, debugging, or extending a glyph application, when code imports github.com/kungfusheep/glyph, or when asked to build a terminal UI in Go.From its SKILL.md

Install
npx -y skills add kungfusheep/glyph-skill --skill glyph

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

17.0 KB, ~4.7k tokens by cl100k_base, as published. Nobody here has run it

glyph

Declarative terminal UI framework for Go. Always refer to useglyph.sh/api for the full API reference before assuming an API does not exist.

Use dot-import:

import . "github.com/kungfusheep/glyph"

How glyph works

You declare a view tree once. glyph compiles it to a template. Every frame, it dereferences your pointers to read current state.

Rules:

  • Call SetView or app.View() once. Never in a loop.
  • Bind dynamic values with pointers (&name, &items)
  • Mutate the pointed-to value, then call app.RequestRender() from goroutines (handlers auto-render)
  • Use If/Switch/ForEach inside the tree for conditional/dynamic content, not Go control flow around SetView

Guidelines for writing glyph code

Use the functional API, not the struct API glyph has two APIs: a legacy struct API (VBoxNode{Children: []any{...}}, TextNode{Content: &name}) and the current functional API (VBox(children...), Text(&name).Bold()). Always use the functional API. The struct types still exist in the package but are no longer idiomatic.

// wrong: struct API
VBoxNode{Children: []any{TextNode{Content: &name}}}

// correct: functional API
VBox(Text(&name))

Use text alignment styles Instead of manually padding strings, use Style{Align: AlignCenter} or Style{Align: AlignRight} with a fixed-width container:

Text("centered").Style(Style{Align: AlignCenter}).Width(40)
Text("right-aligned").Style(Style{Align: AlignRight}).Width(40)

Complete example: single-view app

package main

import (
    "fmt"
    "log"
    "time"
    . "github.com/kungfusheep/glyph"
)

func main() {
    app, err := NewApp()
    if err != nil {
        log.Fatal(err)
    }

    count := 0
    countLabel := "0"
    frame := 0
    status := "ready"

    app.SetView(
        VBox.Border(BorderRounded).Title("Demo")(
            HBox.Gap(2)(
                Text("Count:"),
                Text(&countLabel).Bold().FG(Cyan),
                Spinner(&frame).Frames(SpinnerDots),
            ),
            Progress(&count).Width(30).FG(Green),
            If(&status).Eq("done").
                Then(Text("Complete!").FG(Green)).
                Else(Text(&status).Dim()),
        ),
    )

    app.Handle("j", func() { count++; countLabel = fmt.Sprintf("%d", count) })
    app.Handle("k", func() { count--; countLabel = fmt.Sprintf("%d", count) })
    app.Handle("q", app.Stop)

    go func() {
        for range time.Tick(80 * time.Millisecond) {
            frame++
            app.RequestRender()
        }
    }()

    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

Complete example: multi-view app with form

package main

import (
    "fmt"
    "log"
    "time"
    . "github.com/kungfusheep/glyph"
)

func main() {
    app, err := NewApp()
    if err != nil {
        log.Fatal(err)
    }

    // form state
    var name string
    var target int

    // deploy state
    steps := []string{"Build", "Test", "Deploy", "Verify"}
    activeStep := ""
    progress := 0
    progressLabel := "0%"
    frame := 0
    logs := []string{}
    showError := false

    // named func; reused from submit and retry handlers
    deploy := func() {
        progress = 0
        progressLabel = "0%"
        logs = nil
        showError = false
        app.RequestRender()
        go func() {
            for i, step := range steps {
                activeStep = step
                app.RequestRender()
                time.Sleep(500 * time.Millisecond)
                if i == 2 {
                    logs = append(logs, "ERROR: health check failed")
                    showError = true
                    app.RequestRender()
                    return
                }
                progress = (i + 1) * 25
                progressLabel = fmt.Sprintf("%d%%", progress)
                logs = append(logs, fmt.Sprintf("%s complete", step))
                app.RequestRender()
            }
            progress = 100
            progressLabel = "100%"
            logs = append(logs, "deployment complete!")
            app.RequestRender()
        }()
    }

    var form *FormC
    form = Form.LabelBold().OnSubmit(func() {
        if form.ValidateAll() {
            app.Go("main")
            deploy()
        }
    })(
        Field("Name", Input(&name).Placeholder("release name").Validate(VRequired)),
        Field("Target", Radio(&target, "staging", "production")),
    )

    app.View("form",
        VBox.Border(BorderRounded).Title("Config")(form),
    ).NoCounts().Handle("q", app.Stop)

    app.View("main",
        VBox.Gap(1)(
            HBox.Gap(1)(
                // ForEach with conditional per-item rendering:
                // compare external state pointer against each item
                VBox.WidthPct(0.3).Border(BorderRounded).Title("Steps")(
                    ForEach(&steps, func(step *string) any {
                        return If(&activeStep).Eq(*step).
                            Then(HBox.Gap(1)(
                                Spinner(&frame).Frames(SpinnerDots).FG(Cyan),
                                Text(step).Bold(),
                            )).
                            Else(Text(step).Dim())
                    }),
                ),
                VBox.WidthPct(0.4).Border(BorderRounded).Title("Progress")(
                    Text(&progressLabel).Bold(),
                    Progress(&progress).FG(Green),
                ),
                VBox.WidthPct(0.3).Border(BorderRounded).Title("Log")(
                    ForEach(&logs, func(line *string) any {
                        return Text(line).Dim()
                    }),
                ),
            ),
            If(&showError).Then(
                Overlay.Centered().Backdrop().BackdropFG(BrightBlack)(
                    VBox.Border(BorderRounded).Title("Error")(
                        Text("Failed at 75%").FG(Red),
                        Text("[r] retry  [c] cancel").Dim(),
                    ),
                ),
            ),
        ),
    ).Handle("q", app.Stop).
        Handle("r", func() { if showError { showError = false; deploy() } }).
        Handle("c", func() { if showError { showError = false; app.Go("form") } })

    // spinner animation; always running
    go func() {
        for range time.Tick(80 * time.Millisecond) {
            frame++
            app.RequestRender()
        }
    }()

    if err := app.RunFrom("form"); err != nil {
        log.Fatal(err)
    }
}

Complete example: inline prompt

Input() without a pointer returns an *InputC. Read the value after Run() via .Value().

package main

import (
    "fmt"
    "log"
    . "github.com/kungfusheep/glyph"
)

func main() {
    name := Input().Placeholder("your name").Width(30)
    token := Input().Placeholder("ghp_...").Width(30).Mask('*')

    app, err := NewInlineApp()
    if err != nil {
        log.Fatal(err)
    }

    app.ClearOnExit(true).
        SetView(Form.LabelFG(Cyan)(
            Field("Name", name),
            Field("Token", token),
        )).
        Handle("<Enter>", app.Stop).
        Handle("<Escape>", app.Stop)

    if err := app.Run(); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("name=%q token=%q\n", name.Value(), token.Value())
}

Pitfalls

MistakeCorrect
Text(&myInt) renders nothingText only accepts string or *string. Format numbers into a *string first.
WidthPct(30) means 3000%WidthPct(0.3), scale is 0.0–1.0
Progress(&myFloat)Progress(&myInt) takes *int, range 0–100
Expecting spinner to animateIncrement frame yourself in a goroutine
Calling SetView in a loopCall once, mutate pointers, call RequestRender()
Go if/else around SetViewUse If(&val).Then(...).Else(...) inside the tree
RequestRender() in a handlerhandlers auto-render
Forgetting RequestRender() in goroutinegoroutines don't auto-render
Digits not reaching text inputAdd .NoCounts() to the view
Importing riffkey directlyuse func() handler signatures; riffkey is an internal dependency

API reference

App

MethodPurpose
NewApp()fullscreen app (alternate buffer)
NewInlineApp()inline app (renders at cursor)
app.SetView(tree)set single view
app.View(name, tree) *ViewBuilderregister named view
app.Handle(key, fn)key binding; handler is func()
app.Run() errorblock until Stop
app.RunFrom(name) errorstart on named view
app.RunNonInteractive() errorrender-only, no input loop (inline only)
app.Go(name)switch view
app.Back()return to previous view
app.PushView(name) / app.PopView()modal view stack
app.Stop()exit
app.RequestRender()schedule re-render (thread-safe)
app.JumpKey(key)enable easymotion-style jump labels
app.Height(h)set inline app height
app.ClearOnExit(bool)clear on exit (inline)
app.OnBeforeRender(fn)callback before each render. Use for derived state.
vb.Handle(key, fn) / vb.NoCounts()ViewBuilder; returned by app.View()

Layout

ComponentConstructorKey options
Vertical stackVBox(children...).Gap(n) .Border(style) .Title(s) .BorderFG(c) .Width(n) .Height(n) .WidthPct(0-1) .Grow(n) .FitContent() .Fill(c) .CascadeStyle(&s) .Margin(n) .MarginVH(v,h) .MarginTRBL(t,r,b,l)
Horizontal stackHBox(children...)same as VBox
Modal overlayOverlay(children...).Centered() .Backdrop() .BackdropFG(c) .BG(c) .Size(w,h) .At(x,y)
Custom layoutArrange(layoutFn)(children...)layout returns []Rect
Flexible spaceSpace().Grow(n) .Char(r)
Fixed vertical gapSpaceH(n)
Fixed horizontal gapSpaceW(n)
Horizontal ruleHRule().Char(r) .FG(c)
Vertical ruleVRule().Height(n) .Char(r) .FG(c)

Border styles: BorderRounded, BorderSingle, BorderDouble.

Display

ComponentConstructorState typeKey options
Static textText("str").FG(c) .BG(c) .Bold() .Dim() .Italic() .Underline() .Inverse() .Width(n)
Dynamic textText(&str)*string onlysame
Rich textTextf(parts...)mix of string, *string, Bold(x), Dim(x), FG(x,c)
Progress barProgress(&val)*int (0–100).Width(n) .FG(c) .BG(c)
SpinnerSpinner(&frame)*int (manual increment).Frames([]string) .FG(c)
SparklineSparkline(&vals)*[]float64.Width(n) .Range(min,max) .FG(c)
Leader dotsLeader(label, &val)*string.Fill(r) .FG(c)
TabsTabs(labels, &idx)*int.Kind(style) .ActiveStyle(s) .InactiveStyle(s) .Gap(n)

Text only accepts string or *string. To display a number, format it: label := fmt.Sprintf("%d%%", pct) then Text(&label).

Spinner frame sets: SpinnerBraille, SpinnerDots, SpinnerLine, SpinnerCircle. Tab styles: TabsStyleUnderline, TabsStyleBox, TabsStyleBracket.

String display helpers return string. Use inside Text() or format into a *string:

FunctionSignatureOutput
LED indicatorLED(on bool) string or
Multiple LEDsLEDs(states ...bool) string●●○
Bracketed LEDsLEDsBracket(states ...bool) string[●●○]
Segmented barBar(filled, total int) string▮▮▮▯▯
Bracketed barBarBracket(filled, total int) string[▮▮▮▯▯]
Analog meterMeter(value, max, width int) string├──●──┤

Lists

ComponentConstructorState typeKey options
Navigable listList[T](&items)*[]T.Selection(&int) .Render(fn) .OnSelect(fn) .Marker(s) .MarkerStyle(s) .MaxVisible(n) .SelectedStyle(s) .BindNav(down,up) .BindVimNav() .Ref(fn)
Filterable listFilterList[T](&items, extractFn)*[]T.Placeholder(s) .Render(fn) .MaxVisible(n) .Border(style) .Title(s) .Handle(key, fn) .HandleClear(key, fn) .Ref(fn)
Checkbox listCheckList[T](&items)*[]T.Render(fn) .Checked(fn) .BindNav(d,u) .BindToggle(key) .BindDelete(key) .Ref(fn)
IterationForEach[T](&items, fn)*[]T

FilterList query syntax: foo fuzzy, 'exact, ^prefix, suffix$, !negate, a b AND, a | b OR.

CheckList struct tags: tag struct fields and glyph resolves .Checked() and .Render() from them:

type Task struct {
    Name    string `glyph:"render"`
    Done    bool   `glyph:"checked"`
}

CheckList(&tasks)  // render and checked resolved via struct tags

Without struct tags, configure manually: .Render(func(t *Task) any { return Text(&t.Name) }) and .Checked(func(t *Task) *bool { return &t.Done }).

.Ref() captures a component handle at build time:

var myList *ListC[Item]
List(&items).Render(fn).Ref(func(l *ListC[Item]) { myList = l })

.Ref() is available on List, CheckList, FilterList, Log, FilterLog, Input, Checkbox, Radio, and App/ViewBuilder.

Tables

ComponentConstructorKey options
Auto tableAutoTable(&rows).Columns(names...) .Headers(names...) .Column(name, formatter) .HeaderStyle(s) .RowStyle(s) .AltRowStyle(s) .Sortable() .SortBy(field, asc) .Scrollable(n) .BindVimNav() .Gap(n) .Border(style)

Column formatters: Number(decimals), Percent(decimals), Currency(symbol, decimals), PercentChange(decimals), Bytes(), Bool(yes, no).

Forms

ComponentConstructorState typeKey options
Form containerForm(fields...).LabelBold() .LabelFG(c) .Gap(n) .OnSubmit(fn) .ValidateAll() bool
Form fieldField(label, component)
Text inputInput(&str)*string.Placeholder(s) .Width(n) .Mask(r) .Validate(fn, when...) .Bind() .ManagedBy(&fm)
CheckboxCheckbox(&bool, label)*bool.Marks(on, off) .BindToggle(key) .Validate(fn, when...)
RadioRadio(&idx, options...)*int.Marks(on, off) .Gap(n) .Horizontal() .BindNav(next, prev)

Validators: VRequired, VEmail, VMinLen(n), VMaxLen(n), VMatch(regex), VTrue. Trigger flags: VOnChange, VOnBlur, VOnSubmit.

TextInput outside forms. Use TextInput + app.BindField() for standalone text input:

var field InputState

app.BindField(&field)  // routes unmatched keystrokes to this field

// in view tree:
TextInput{Field: &field, Placeholder: "search..."}

InputState bundles Value string and Cursor int. Call field.Clear() to reset. For multiple focusable fields share a FocusGroup and set FocusIndex on each TextInput.

Conditionals

PatternUsage
Bool toggleIf(&boolVar).Then(a).Else(b)
EqualityIf(&strVar).Eq("val").Then(a).Else(b)
Ordered comparisonIfOrd(&intVar).Gte(10).Then(a).Else(b)
Multi-waySwitch(&strVar).Case("a", viewA).Case("b", viewB).Default(viewC)

IfOrd operators: .Gt(), .Lt(), .Gte(), .Lte(), .Eq(), .Ne().

Switch requires .End() when it is not the last child in a container; omitting it is a compile error.

Styling

Colors: Black Red Green Yellow Blue Magenta Cyan White (and Bright variants), PaletteColor(0-255), RGB(r,g,b), Hex(0xRRGGBB), LerpColor(a, b, t).

Style struct: Style{FG: c, BG: c, Fill: c, Attr: AttrBold | AttrItalic, Align: AlignCenter}.

Attributes: AttrBold, AttrDim, AttrItalic, AttrUnderline, AttrInverse, AttrStrikethrough.

CascadeStyle applies a style to a container. Children inherit it and can override:

theme := Style{FG: Cyan}
VBox.CascadeStyle(&theme)(children...)

Built-in themes: ThemeDark, ThemeLight, ThemeMonochrome.

Key binding patterns

"q"          single key
"gg"         sequence
"<Enter>"    special key
"<C-c>"      ctrl+c
"<S-Tab>"    shift+tab
"<Up>"       arrow key

Special keys: <Enter>, <Escape>, <Tab>, <S-Tab>, <Space>, <Backspace>, <Delete>, <Up>, <Down>, <Left>, <Right>, <PageUp>, <PageDown>, <Home>, <End>, <C-x> (ctrl), <S-x> (shift), <A-x> (alt).

Advanced

ComponentConstructorPurpose
Jump targetJump(child, onSelect)easymotion-style label
Layer viewLayerView(&layer)scrollable pre-rendered buffer
Custom widgetWidget(measureFn, renderFn)fully custom rendering
Scoped blockDefine(func() any { ... })local helpers at compile time
Log viewerLog(reader)streaming text from io.Reader. .BindVimNav() to scroll with j/k/ctrl-d/ctrl-u
Filterable logFilterLog(reader)log with fzf search. .BindVimNav() to scroll

Check useglyph.sh/api for anything not listed here.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,512. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.