From 90ac20b06ca5059c8a2d4b57635b4145cc006d62 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 23 Jul 2026 08:55:30 +0100 Subject: [PATCH 1/2] build(cli): pin dappco.re/go/html v0.16.0 (adds the tui runtime) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.16.0 ships dappco.re/go/html/tui — the terminal-UI runtime that wraps charmbracelet — so cli/tui can drop its direct charm imports next. Co-Authored-By: Virgil --- cli/go.mod | 2 +- cli/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/go.mod b/cli/go.mod index 0187d4ca..16167fde 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -33,7 +33,7 @@ require ( charm.land/log/v2 v2.0.0 // indirect charm.land/wish/v2 v2.0.0 // indirect dappco.re/go/cgo v0.11.2 // indirect - dappco.re/go/html v0.15.0 // direct: cli/tui renders .ctml (tabs.go) + dappco.re/go/html v0.16.0 // direct: cli/tui renders .ctml (tabs.go) dappco.re/go/i18n v0.12.1 // indirect dappco.re/go/log v0.13.1 // indirect dappco.re/go/ratelimit v0.12.1 // indirect diff --git a/cli/go.sum b/cli/go.sum index 213c68f0..fd728e2b 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -32,6 +32,8 @@ dappco.re/go/html v0.14.0 h1:vUDiFe4b5IXF2xFCFPSPwwlXwWW5Cf+OhDQq0gnt/iY= dappco.re/go/html v0.14.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4= dappco.re/go/html v0.15.0 h1:XMFA7fKY07F3gskoF/9+NYo1WlLaNmoGEFmOofWvnRw= dappco.re/go/html v0.15.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4= +dappco.re/go/html v0.16.0 h1:xaQxd7TYh6pCnagQuCLnAsQsX0IskCigDHqor0ysQD4= +dappco.re/go/html v0.16.0/go.mod h1:1UKrgjMWNEJ9IyVy/emc2T60q0Y4AzTLBy07Ly6/QlA= dappco.re/go/i18n v0.12.1 h1:Pm1DF9I0O8bckTr815OrnR+G+7EvbQhP2Vk4ItC0mB4= dappco.re/go/i18n v0.12.1/go.mod h1:AYx1QkPfW+qvzLCfJnuOlc4J+grwlgFzoHnoke4TAYg= dappco.re/go/inference v0.14.0 h1:3Lddj4W8T7/Mg8iTmvS+hCUkUqYYhRPYGe6i/ILF1+o= From bbba1e1d4f84ebfbce8cc3e43507f82857d20fd4 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 23 Jul 2026 09:09:53 +0100 Subject: [PATCH 2/2] refactor(tui): charmbracelet behind dappco.re/go/html/tui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 19 cli/tui files swapped onto go-html's tui runtime — widget path-swaps (bubbletea/bubbles) plus style/markdown semantic renames (lipgloss→style, glamour→markdown, x/ansi folded into style). Behaviour-preserving: go test ./tui/... green. No direct charmbracelet imports remain in cli/tui non-test files. Co-Authored-By: Virgil --- cli/tui/agentoverlay.go | 8 +-- cli/tui/app.go | 32 +++++----- cli/tui/dataoverlay.go | 6 +- cli/tui/datapanel.go | 23 ++++---- cli/tui/inspector.go | 5 +- cli/tui/keymap.go | 2 +- cli/tui/layout.go | 27 ++++----- cli/tui/markdown.go | 20 +++---- cli/tui/overlayframe.go | 9 ++- cli/tui/palette.go | 27 ++++----- cli/tui/picker.go | 15 +++-- cli/tui/service.go | 2 +- cli/tui/settings.go | 5 +- cli/tui/stream.go | 2 +- cli/tui/style.go | 128 ++++++++++++++++++++-------------------- cli/tui/tabs.go | 13 ++-- cli/tui/tools.go | 5 +- cli/tui/tui.go | 2 +- cli/tui/work.go | 10 ++-- 19 files changed, 166 insertions(+), 175 deletions(-) diff --git a/cli/tui/agentoverlay.go b/cli/tui/agentoverlay.go index ae3bd8b6..ffeedf6a 100644 --- a/cli/tui/agentoverlay.go +++ b/cli/tui/agentoverlay.go @@ -5,10 +5,10 @@ package tui import ( _ "embed" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/textarea" + "dappco.re/go/html/tui/textinput" + "dappco.re/go/html/tui/viewport" core "dappco.re/go" "dappco.re/go/html/ctml" diff --git a/cli/tui/app.go b/cli/tui/app.go index 743b080f..4cd29db9 100644 --- a/cli/tui/app.go +++ b/cli/tui/app.go @@ -8,13 +8,13 @@ import ( "sync" "time" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/key" + "dappco.re/go/html/tui/list" + "dappco.re/go/html/tui/spinner" + "dappco.re/go/html/tui/style" + "dappco.re/go/html/tui/textarea" + "dappco.re/go/html/tui/viewport" core "dappco.re/go" "dappco.re/go/html" @@ -3076,7 +3076,7 @@ func (a app) renderTranscript() string { if err != nil { return "" } - return lipgloss.NewStyle().Width(a.view.Width).Render(html.RenderTerm(tree, html.NewContext())) + return style.New().Width(a.view.Width).Render(html.RenderTerm(tree, html.NewContext())) } // transcriptBindings supplies transcript.ctml's two row sequences. notice is @@ -3267,7 +3267,7 @@ func (a app) panelView() string { return a.svc.view(a.modelName, a.width, a.styles) case panelWork: if a.work == nil { - return lipgloss.JoinVertical(lipgloss.Left, + return style.Column(style.Left, a.styles.title.Render("Work"), "", a.styles.status.Render("○ No work yet"), @@ -3281,7 +3281,7 @@ func (a app) panelView() string { return a.work.View(metrics.mainWidth, metrics.mainHeight, a.styles) case panelData: if a.data == nil { - return lipgloss.JoinVertical(lipgloss.Left, + return style.Column(style.Left, a.styles.title.Render("Data"), "", a.styles.status.Render("○ No dataset store connected"), @@ -3300,7 +3300,7 @@ func (a app) panelView() string { if a.model == nil { return "\n " + a.spin.View() + a.styles.status.Render(" loading "+displayName(a.loading)+" …") } - return lipgloss.JoinVertical(lipgloss.Left, + return style.Column(style.Left, a.view.View(), a.styles.inputBorder.Render(a.input.View()), ) @@ -3313,7 +3313,7 @@ func (a app) bootView() string { if a.boot.err != nil { reason = a.boot.err.Error() } - return lipgloss.JoinVertical(lipgloss.Left, + return style.Column(style.Left, a.styles.err.Render("Workspace could not open"), "", a.styles.answer.Render(reason), @@ -3322,7 +3322,7 @@ func (a app) bootView() string { a.styles.status.Render("Q Quit without changing storage"), ) } - return lipgloss.JoinVertical(lipgloss.Left, + return style.Column(style.Left, a.spin.View()+a.styles.title.Render(" Opening ~/.lem workspace"), "", a.styles.status.Render("Preparing durable sessions, preferences, work, and local knowledge…"), @@ -3449,7 +3449,7 @@ func (a app) sessionStrip() string { if hidden > 0 { candidate = append(candidate, core.Sprintf("+%d", hidden)) } - if lipgloss.Width(core.Join(" · ", candidate...)) <= maxWidth { + if style.Measure(core.Join(" · ", candidate...)) <= maxWidth { break } parts = parts[:len(parts)-1] @@ -3486,11 +3486,11 @@ func sessionStripMarker(session *chatSession, activeID string) string { func compactStripTitle(value string, maxCells int) string { value = core.Trim(value) - if maxCells < 2 || lipgloss.Width(value) <= maxCells { + if maxCells < 2 || style.Measure(value) <= maxCells { return value } runes := []rune(value) - for len(runes) > 1 && lipgloss.Width(string(runes)+"…") > maxCells { + for len(runes) > 1 && style.Measure(string(runes)+"…") > maxCells { runes = runes[:len(runes)-1] } return string(runes) + "…" diff --git a/cli/tui/dataoverlay.go b/cli/tui/dataoverlay.go index ac90646e..a6ac2974 100644 --- a/cli/tui/dataoverlay.go +++ b/cli/tui/dataoverlay.go @@ -5,9 +5,9 @@ package tui import ( _ "embed" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/textarea" + "dappco.re/go/html/tui/textinput" core "dappco.re/go" "dappco.re/go/html/ctml" diff --git a/cli/tui/datapanel.go b/cli/tui/datapanel.go index d6483bd2..4542414d 100644 --- a/cli/tui/datapanel.go +++ b/cli/tui/datapanel.go @@ -7,10 +7,9 @@ import ( "sort" "time" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/list" + "dappco.re/go/html/tui/style" core "dappco.re/go" "dappco.re/go/html" @@ -504,13 +503,13 @@ func (panel *dataPanel) View(width, height int, styles uiStyles) string { var view string if width >= 100 { separator := fitPane("│", 1, height, styles.separator) - view = lipgloss.JoinHorizontal(lipgloss.Top, + view = style.Row(style.Top, fitPane(listView, listWidth, height, styles.panel), separator, fitPane(detailView, detailWidth, height, styles.panel), ) } else { - view = lipgloss.JoinVertical(lipgloss.Left, + view = style.Column(style.Left, fitPane(listView, listWidth, listHeight, styles.panel), "", fitPane(detailView, detailWidth, detailHeight, styles.panel), @@ -572,13 +571,13 @@ func dataListBindings(panel *dataPanel, width int) ctml.Bindings { // The F band renders its blocks at width-2 (the frame's own inner // margin), so the row budget subtracts that before the cursor, the // two fixed segments, and their two 2-cell gaps. - budget := max(1, width-2-lipgloss.Width(cursor)-lipgloss.Width(status)-lipgloss.Width(meta)-4) + budget := max(1, width-2-style.Measure(cursor)-style.Measure(status)-style.Measure(meta)-4) sequences["rows"] = append(sequences["rows"], map[string]any{ "state": state, "cursor": cursor, "status": status, "meta": meta, - "slug": ansi.Truncate(item.row.Dataset.Slug, budget, "…"), + "slug": style.Truncate(item.row.Dataset.Slug, budget, "…"), }) } if len(visible) == 0 { @@ -597,9 +596,9 @@ func dataListBindings(panel *dataPanel, width int) ctml.Bindings { func dataListTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer - theme.Header = lipgloss.NewStyle() - theme.Footer = lipgloss.NewStyle() - theme.Classes = map[string]lipgloss.Style{ + theme.Header = style.New() + theme.Footer = style.New() + theme.Classes = map[string]style.Style{ "list-title": styles.title, "list-meta": styles.status, "list-filter": styles.thought, @@ -692,7 +691,7 @@ func (panel *dataPanel) dataDetailBindings(width int) ctml.Bindings { func dataDetailTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "detail-empty": styles.status, "detail-title": styles.title, "detail-status": styles.status, diff --git a/cli/tui/inspector.go b/cli/tui/inspector.go index 900dc5e1..84c7c365 100644 --- a/cli/tui/inspector.go +++ b/cli/tui/inspector.go @@ -5,11 +5,10 @@ package tui import ( _ "embed" - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" + "dappco.re/go/html/tui/style" ) type inspectorControl uint8 @@ -164,7 +163,7 @@ func (inspector inspectorState) View(target app, width, height int) string { func inspectorTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.inspector - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "inspector-title": styles.title, "label": styles.accent, "control-active": styles.accent, diff --git a/cli/tui/keymap.go b/cli/tui/keymap.go index 7858e442..af9fa207 100644 --- a/cli/tui/keymap.go +++ b/cli/tui/keymap.go @@ -2,7 +2,7 @@ package tui -import "github.com/charmbracelet/bubbles/key" +import "dappco.re/go/html/tui/key" type keyMap struct { NewSession key.Binding diff --git a/cli/tui/layout.go b/cli/tui/layout.go index 69ac1616..616428dc 100644 --- a/cli/tui/layout.go +++ b/cli/tui/layout.go @@ -5,11 +5,10 @@ package tui import ( _ "embed" - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" + "dappco.re/go/html/tui/style" ) type layoutKind uint8 @@ -145,8 +144,8 @@ var shellWideCTML []byte // it byte-exact. func shellFrameTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() - theme.Header = lipgloss.NewStyle() - theme.Footer = lipgloss.NewStyle() + theme.Header = style.New() + theme.Footer = style.New() return theme } @@ -159,7 +158,7 @@ func shellFrameTheme(styles uiStyles) *html.TermTheme { // and gets word-wrapped onto a spurious extra row (S:15.5). func shellRegionTheme(styles uiStyles) *html.TermTheme { theme := shellFrameTheme(styles) - theme.Content = lipgloss.NewStyle() + theme.Content = style.New() return theme } @@ -176,7 +175,7 @@ func shellRegionTheme(styles uiStyles) *html.TermTheme { // default border colour. func shellWideTheme(styles uiStyles) *html.TermTheme { theme := shellRegionTheme(styles) - theme.Aside = lipgloss.NewStyle() + theme.Aside = style.New() theme.GutterRule = "│" theme.Rule = styles.separator return theme @@ -245,7 +244,7 @@ func renderWideLayout(src []byte, width int, theme *html.TermTheme, bindings ... // // The ONE shape that still cannot join header+region+footer into a single // call -- Overlay with the inspector open -- stays a host-side -// lipgloss.JoinVertical of three independently rendered pieces: shell.ctml's +// style.Column of three independently rendered pieces: shell.ctml's // H/F-only layout (split by renderBandFrame, unchanged), and // renderInspectorStack's own region render sat between them. What changed // this slice (docs/ctml.md S:15.7, go-html v0.15.0) is that the REGION @@ -288,7 +287,7 @@ func renderFrame(spec frameSpec, styles uiStyles) string { case metrics.kind == layoutOverlay && spec.InspectorOpen: region := renderInspectorStack(spec, metrics, styles) head, foot := renderBandFrame(shellCTML, metrics.innerWidth, shellFrameTheme(styles), shellBindings(header, footer)) - inside = lipgloss.JoinVertical(lipgloss.Left, head, region, foot) + inside = style.Column(style.Left, head, region, foot) case metrics.kind == layoutWide: main := fitPane(spec.Main, metrics.mainWidth, metrics.mainHeight, styles.panel) inspector := fitPane(spec.Inspector, metrics.inspectorWidth, metrics.inspectorHeight, styles.inspector) @@ -332,10 +331,10 @@ var shellInspectorPairCTML []byte // history), not go-html's own similar-but-different default border colour. func inspectorPairTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() - theme.Header = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, false, true, false). + theme.Header = style.New(). + Border(style.Normal(), false, false, true, false). BorderForeground(styles.theme.border) - theme.Content = lipgloss.NewStyle() + theme.Content = style.New() return theme } @@ -362,7 +361,7 @@ func inspectorPairBindings(inspector, main string) ctml.Bindings { // (zero-chrome) and this pair's own H (bordered) different styles from the // one flat TermTheme a render call threads through every nested Layout. // renderFrame still joins this function's output between the (unchanged, -// separately rendered) header/footer bands with lipgloss.JoinVertical -- +// separately rendered) header/footer bands with style.Column -- // that outer join, not this region's own composition, is what remains // host-side. func renderInspectorStack(spec frameSpec, metrics frameMetrics, styles uiStyles) string { @@ -371,14 +370,14 @@ func renderInspectorStack(spec frameSpec, metrics frameMetrics, styles uiStyles) return renderBandLayout(shellInspectorPairCTML, metrics.innerWidth, inspectorPairTheme(styles), inspectorPairBindings(inspector, main)) } -func fitLine(content string, width int, style lipgloss.Style) string { +func fitLine(content string, width int, style style.Style) string { if width <= 0 { return "" } return style.Width(width).MaxWidth(width).MaxHeight(1).Render(content) } -func fitPane(content string, width, height int, style lipgloss.Style) string { +func fitPane(content string, width, height int, style style.Style) string { if width <= 0 || height <= 0 { return "" } diff --git a/cli/tui/markdown.go b/cli/tui/markdown.go index c4c1288d..28b37843 100644 --- a/cli/tui/markdown.go +++ b/cli/tui/markdown.go @@ -7,8 +7,8 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/markdown" ) type markdownCacheKey struct { @@ -30,7 +30,7 @@ type markdownRenderer struct { mu sync.Mutex theme string cache map[markdownCacheKey]string - renderers map[int]*glamour.TermRenderer + renderers map[int]*markdown.Renderer stats markdownStats } @@ -38,7 +38,7 @@ func newMarkdownRenderer(theme string) *markdownRenderer { return &markdownRenderer{ theme: theme, cache: make(map[markdownCacheKey]string), - renderers: make(map[int]*glamour.TermRenderer), + renderers: make(map[int]*markdown.Renderer), } } @@ -63,12 +63,12 @@ func (renderer *markdownRenderer) Render(turnID, content string, width int) stri renderer.stats.Misses++ term := renderer.renderers[width] if term == nil { - built, err := glamour.NewTermRenderer( - glamour.WithStandardStyle("dark"), - glamour.WithWordWrap(width), - glamour.WithTableWrap(true), - glamour.WithPreservedNewLines(), - glamour.WithEmoji(), + built, err := markdown.New( + markdown.WithStandardStyle("dark"), + markdown.WithWordWrap(width), + markdown.WithTableWrap(true), + markdown.WithPreservedNewLines(), + markdown.WithEmoji(), ) if err != nil { renderer.cache[key] = content diff --git a/cli/tui/overlayframe.go b/cli/tui/overlayframe.go index ea04ac0c..10307cf2 100644 --- a/cli/tui/overlayframe.go +++ b/cli/tui/overlayframe.go @@ -3,11 +3,10 @@ package tui import ( - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" + "dappco.re/go/html/tui/style" ) // The overlay layer renders its text chrome through .ctml @@ -80,9 +79,9 @@ func overlayFrameTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer theme.Heading = styles.title // the

overlay titles - theme.Header = lipgloss.NewStyle() - theme.Footer = lipgloss.NewStyle().Padding(1, 0, 0, 0) - theme.Classes = map[string]lipgloss.Style{ + theme.Header = style.New() + theme.Footer = style.New().Padding(1, 0, 0, 0) + theme.Classes = map[string]style.Style{ "overlay-hint": styles.thought, "overlay-warn": styles.attention, "overlay-error": styles.err, diff --git a/cli/tui/palette.go b/cli/tui/palette.go index 31c39a08..b8d3afff 100644 --- a/cli/tui/palette.go +++ b/cli/tui/palette.go @@ -5,12 +5,11 @@ package tui import ( _ "embed" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/help" + "dappco.re/go/html/tui/list" + "dappco.re/go/html/tui/style" + "dappco.re/go/html/tui/textinput" core "dappco.re/go" "dappco.re/go/html" @@ -201,8 +200,8 @@ func commandPaletteBindings(palette *commandPalette, width int) ctml.Bindings { "state": state, "marker": marker, "id": string(entry.command.ID), - "title": ansi.Truncate(entry.Title(), budget, "…"), - "detail": ansi.Truncate(entry.Description(), budget, "…"), + "title": style.Truncate(entry.Title(), budget, "…"), + "detail": style.Truncate(entry.Description(), budget, "…"), }) } if len(visible) == 0 && palette.list.FilterState() != list.Filtering { @@ -230,9 +229,9 @@ func commandPaletteTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer theme.Heading = styles.title - theme.Header = lipgloss.NewStyle() - theme.Footer = lipgloss.NewStyle().Padding(1, 0, 0, 0) - theme.Classes = map[string]lipgloss.Style{ + theme.Header = style.New() + theme.Footer = style.New().Padding(1, 0, 0, 0) + theme.Classes = map[string]style.Style{ "row-idle": styles.answer, "row-active": styles.accent, "row-hint": styles.thought, @@ -892,7 +891,7 @@ func (search *historySearch) View(width, height int) string { } search.input.Width = max(12, width-4) search.list.SetSize(max(1, width), max(4, height-2)) - return lipgloss.JoinVertical(lipgloss.Left, search.input.View(), search.list.View()) + return style.Column(style.Left, search.input.View(), search.list.View()) } type overlayKind uint8 @@ -936,7 +935,7 @@ func (overlay *helpOverlay) View(width int) string { return "" } overlay.model.Width = max(20, width) - return lipgloss.JoinVertical(lipgloss.Left, "Keyboard help", "", overlay.model.View(overlay.keys), "", "esc closes help") + return style.Column(style.Left, "Keyboard help", "", overlay.model.View(overlay.keys), "", "esc closes help") } func renderOverlay(body string, width, height int, styles uiStyles) string { @@ -952,7 +951,7 @@ func renderOverlay(body string, width, height int, styles uiStyles) string { MaxWidth(overlayWidth). MaxHeight(overlayHeight). Render(body) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Top, box) + return style.Place(width, height, style.Center, style.Top, box) } // overlayEmpty keeps unavailable overlays legible without adding a component. diff --git a/cli/tui/picker.go b/cli/tui/picker.go index ccef5bb5..07ffddf0 100644 --- a/cli/tui/picker.go +++ b/cli/tui/picker.go @@ -6,10 +6,9 @@ import ( _ "embed" "sort" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/list" + "dappco.re/go/html/tui/style" core "dappco.re/go" "dappco.re/go/html" @@ -83,7 +82,7 @@ func displayName(path string) string { // items, cursor, filter, pagination — and its default delegate survives as // the page-size driver (height 2 + spacing 1, the same three cells a // picker.ctml row occupies); rendering goes through renderPicker, so the -// list carries no lipgloss styling of its own. +// list carries no styling of its own. func newPicker() list.Model { l := list.New(nil, list.NewDefaultDelegate(), 0, 0) l.Title = "Models" @@ -141,8 +140,8 @@ func modelPickerBindings(picker list.Model, width int) ctml.Bindings { "state": state, "marker": marker, "id": id, - "name": ansi.Truncate(entry.Title(), budget, "…"), - "detail": ansi.Truncate(entry.Description(), budget, "…"), + "name": style.Truncate(entry.Title(), budget, "…"), + "detail": style.Truncate(entry.Description(), budget, "…"), }) } if len(visible) == 0 && picker.FilterState() != list.Filtering { @@ -162,7 +161,7 @@ func modelPickerTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer theme.Heading = styles.title // the

panel title - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "row-idle": styles.answer, "row-active": styles.accent, "row-hint": styles.thought, diff --git a/cli/tui/service.go b/cli/tui/service.go index 0ac1e871..d6061d3d 100644 --- a/cli/tui/service.go +++ b/cli/tui/service.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - tea "github.com/charmbracelet/bubbletea" + tea "dappco.re/go/html/tui" core "dappco.re/go" "dappco.re/go/inference" diff --git a/cli/tui/settings.go b/cli/tui/settings.go index 0d1d03fb..8a418945 100644 --- a/cli/tui/settings.go +++ b/cli/tui/settings.go @@ -5,11 +5,10 @@ package tui import ( _ "embed" - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" + "dappco.re/go/html/tui/style" ) // The Settings tab: a cursor list of knobs adjusted with ←/→ (or h/l). Values @@ -142,7 +141,7 @@ func settingsFormTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer theme.Heading = styles.title // the

form title - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "row-idle": styles.answer, "row-active": styles.accent, "row-value": styles.title, diff --git a/cli/tui/stream.go b/cli/tui/stream.go index 33161982..1c1e0a9f 100644 --- a/cli/tui/stream.go +++ b/cli/tui/stream.go @@ -5,7 +5,7 @@ package tui import ( "context" - tea "github.com/charmbracelet/bubbletea" + tea "dappco.re/go/html/tui" "dappco.re/go/inference" "dappco.re/go/inference/decode/parser" diff --git a/cli/tui/style.go b/cli/tui/style.go index 8ee699f3..859be56b 100644 --- a/cli/tui/style.go +++ b/cli/tui/style.go @@ -5,33 +5,33 @@ // built on Bubble Tea + Bubbles + Lip Gloss. package tui -import "github.com/charmbracelet/lipgloss" +import "dappco.re/go/html/tui/style" // theme contains semantic colours rather than component-specific paint. Every // colour is adaptive so the same hierarchy remains legible on light terminals. type theme struct { name string - text lipgloss.AdaptiveColor - muted lipgloss.AdaptiveColor - border lipgloss.AdaptiveColor - focus lipgloss.AdaptiveColor - assistant lipgloss.AdaptiveColor - attention lipgloss.AdaptiveColor - success lipgloss.AdaptiveColor - error lipgloss.AdaptiveColor + text style.Color + muted style.Color + border style.Color + focus style.Color + assistant style.Color + attention style.Color + success style.Color + error style.Color } func midnightTheme() theme { return theme{ name: "midnight", - text: lipgloss.AdaptiveColor{Light: "#172033", Dark: "#E2E8F0"}, - muted: lipgloss.AdaptiveColor{Light: "#475569", Dark: "#718096"}, - border: lipgloss.AdaptiveColor{Light: "#94A3B8", Dark: "#334155"}, - focus: lipgloss.AdaptiveColor{Light: "#007A89", Dark: "#67E8F9"}, - assistant: lipgloss.AdaptiveColor{Light: "#6D28D9", Dark: "#C4B5FD"}, - attention: lipgloss.AdaptiveColor{Light: "#92400E", Dark: "#FBBF24"}, - success: lipgloss.AdaptiveColor{Light: "#047857", Dark: "#6EE7B7"}, - error: lipgloss.AdaptiveColor{Light: "#BE123C", Dark: "#FB7185"}, + text: style.Color{Light: "#172033", Dark: "#E2E8F0"}, + muted: style.Color{Light: "#475569", Dark: "#718096"}, + border: style.Color{Light: "#94A3B8", Dark: "#334155"}, + focus: style.Color{Light: "#007A89", Dark: "#67E8F9"}, + assistant: style.Color{Light: "#6D28D9", Dark: "#C4B5FD"}, + attention: style.Color{Light: "#92400E", Dark: "#FBBF24"}, + success: style.Color{Light: "#047857", Dark: "#6EE7B7"}, + error: style.Color{Light: "#BE123C", Dark: "#FB7185"}, } } @@ -40,15 +40,15 @@ func themeForName(name string) theme { case "aurora": value := midnightTheme() value.name = "aurora" - value.focus = lipgloss.AdaptiveColor{Light: "#047857", Dark: "#5EEAD4"} - value.assistant = lipgloss.AdaptiveColor{Light: "#7E22CE", Dark: "#E879F9"} - value.attention = lipgloss.AdaptiveColor{Light: "#B45309", Dark: "#FDE68A"} + value.focus = style.Color{Light: "#047857", Dark: "#5EEAD4"} + value.assistant = style.Color{Light: "#7E22CE", Dark: "#E879F9"} + value.attention = style.Color{Light: "#B45309", Dark: "#FDE68A"} return value case "daylight": value := midnightTheme() value.name = "daylight" - value.focus = lipgloss.AdaptiveColor{Light: "#0369A1", Dark: "#7DD3FC"} - value.assistant = lipgloss.AdaptiveColor{Light: "#5B21B6", Dark: "#DDD6FE"} + value.focus = style.Color{Light: "#0369A1", Dark: "#7DD3FC"} + value.assistant = style.Color{Light: "#5B21B6", Dark: "#DDD6FE"} return value default: return midnightTheme() @@ -60,53 +60,53 @@ func themeForName(name string) theme { type uiStyles struct { theme theme - title lipgloss.Style - user lipgloss.Style - answer lipgloss.Style - thought lipgloss.Style - err lipgloss.Style - status lipgloss.Style - accent lipgloss.Style - assistant lipgloss.Style - attention lipgloss.Style - success lipgloss.Style + title style.Style + user style.Style + answer style.Style + thought style.Style + err style.Style + status style.Style + accent style.Style + assistant style.Style + attention style.Style + success style.Style - brand lipgloss.Style - navActive lipgloss.Style - navInactive lipgloss.Style - header lipgloss.Style - session lipgloss.Style - panel lipgloss.Style - inspector lipgloss.Style - footer lipgloss.Style - separator lipgloss.Style - outerFrame lipgloss.Style - inputBorder lipgloss.Style + brand style.Style + navActive style.Style + navInactive style.Style + header style.Style + session style.Style + panel style.Style + inspector style.Style + footer style.Style + separator style.Style + outerFrame style.Style + inputBorder style.Style } func newUIStyles(t theme) uiStyles { return uiStyles{ theme: t, - title: lipgloss.NewStyle().Bold(true).Foreground(t.text), - user: lipgloss.NewStyle().Bold(true).Foreground(t.focus), - answer: lipgloss.NewStyle().Foreground(t.text), - thought: lipgloss.NewStyle().Italic(true).Foreground(t.muted), - err: lipgloss.NewStyle().Foreground(t.error), - status: lipgloss.NewStyle().Foreground(t.muted), - accent: lipgloss.NewStyle().Foreground(t.focus), - assistant: lipgloss.NewStyle().Bold(true).Foreground(t.assistant), - attention: lipgloss.NewStyle().Bold(true).Foreground(t.attention), - success: lipgloss.NewStyle().Foreground(t.success), - brand: lipgloss.NewStyle().Bold(true).Foreground(t.focus), - navActive: lipgloss.NewStyle().Bold(true).Foreground(t.text).Underline(true).UnderlineSpaces(false), - navInactive: lipgloss.NewStyle().Foreground(t.muted), - header: lipgloss.NewStyle().Foreground(t.text), - session: lipgloss.NewStyle().Foreground(t.muted), - panel: lipgloss.NewStyle().Foreground(t.text), - inspector: lipgloss.NewStyle().Foreground(t.muted), - footer: lipgloss.NewStyle().Foreground(t.muted), - separator: lipgloss.NewStyle().Foreground(t.border), - outerFrame: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(t.border), - inputBorder: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(t.border).Padding(0, 1), + title: style.New().Bold(true).Foreground(t.text), + user: style.New().Bold(true).Foreground(t.focus), + answer: style.New().Foreground(t.text), + thought: style.New().Italic(true).Foreground(t.muted), + err: style.New().Foreground(t.error), + status: style.New().Foreground(t.muted), + accent: style.New().Foreground(t.focus), + assistant: style.New().Bold(true).Foreground(t.assistant), + attention: style.New().Bold(true).Foreground(t.attention), + success: style.New().Foreground(t.success), + brand: style.New().Bold(true).Foreground(t.focus), + navActive: style.New().Bold(true).Foreground(t.text).Underline(true).UnderlineSpaces(false), + navInactive: style.New().Foreground(t.muted), + header: style.New().Foreground(t.text), + session: style.New().Foreground(t.muted), + panel: style.New().Foreground(t.text), + inspector: style.New().Foreground(t.muted), + footer: style.New().Foreground(t.muted), + separator: style.New().Foreground(t.border), + outerFrame: style.New().Border(style.Rounded()).BorderForeground(t.border), + inputBorder: style.New().Border(style.Rounded()).BorderForeground(t.border).Padding(0, 1), } } diff --git a/cli/tui/tabs.go b/cli/tui/tabs.go index 81b7afab..3a5deffe 100644 --- a/cli/tui/tabs.go +++ b/cli/tui/tabs.go @@ -5,12 +5,11 @@ package tui import ( _ "embed" - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" "dappco.re/go/html/teabox" + "dappco.re/go/html/tui/style" ) type panelID uint8 @@ -111,10 +110,10 @@ func panelBarBindings(active panelID, kind layoutKind) ctml.Bindings { func panelBarTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.header - theme.Sidebar = lipgloss.NewStyle(). - Border(lipgloss.Border{Left: " ", Right: " "}). + theme.Sidebar = style.New(). + Border(style.Border{Left: " ", Right: " "}). BorderTop(false).BorderBottom(false) - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "brand": styles.brand, "nav-active": styles.navActive, "nav-inactive": styles.navInactive, @@ -185,14 +184,14 @@ func panelBarHit(boxes html.BoxMap, x, y int, active panelID, kind layoutKind) ( pos := tabs.Col + 1 // the C content's one-column alignment gutter for _, cell := range panelBarCells(active, kind) { start := pos + cell.gap - end := min(pos+lipgloss.Width(cell.cell), edge) + end := min(pos+style.Measure(cell.cell), edge) if start >= edge { break } if x >= start && x < end { return cell.panel, true } - pos += lipgloss.Width(cell.cell) + pos += style.Measure(cell.cell) } return 0, false } diff --git a/cli/tui/tools.go b/cli/tui/tools.go index 85759a87..672f8f7f 100644 --- a/cli/tui/tools.go +++ b/cli/tui/tools.go @@ -6,11 +6,10 @@ import ( _ "embed" "time" - "github.com/charmbracelet/lipgloss" - core "dappco.re/go" "dappco.re/go/html" "dappco.re/go/html/ctml" + "dappco.re/go/html/tui/style" "dappco.re/go/inference" "dappco.re/go/inference/decode/parser" ) @@ -152,7 +151,7 @@ func toolsPanelTheme(styles uiStyles) *html.TermTheme { theme := html.DefaultTermTheme() theme.Text = styles.answer theme.Heading = styles.title // the

section titles - theme.Classes = map[string]lipgloss.Style{ + theme.Classes = map[string]style.Style{ "tool-label": styles.accent, "tool-state": styles.answer, "tool-name": styles.answer, diff --git a/cli/tui/tui.go b/cli/tui/tui.go index b5da9505..65b6f770 100644 --- a/cli/tui/tui.go +++ b/cli/tui/tui.go @@ -7,7 +7,7 @@ import ( "flag" "io" - tea "github.com/charmbracelet/bubbletea" + tea "dappco.re/go/html/tui" core "dappco.re/go" ) diff --git a/cli/tui/work.go b/cli/tui/work.go index 54a251f9..53e86af6 100644 --- a/cli/tui/work.go +++ b/cli/tui/work.go @@ -7,9 +7,9 @@ import ( "sort" "time" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "dappco.re/go/html/tui" + "dappco.re/go/html/tui/list" + "dappco.re/go/html/tui/style" core "dappco.re/go" ) @@ -358,13 +358,13 @@ func (panel *workPanel) View(width, height int, styles uiStyles) string { var view string if width >= 100 { separator := fitPane("│", 1, height, styles.separator) - view = lipgloss.JoinHorizontal(lipgloss.Top, + view = style.Row(style.Top, fitPane(listView, listWidth, height, styles.panel), separator, fitPane(detailView, detailWidth, height, styles.panel), ) } else { - view = lipgloss.JoinVertical(lipgloss.Left, + view = style.Column(style.Left, fitPane(listView, listWidth, listHeight, styles.panel), "", fitPane(detailView, detailWidth, detailHeight, styles.panel),