From 53a48017162b8675dbe9a0044910559de754c188 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 31 Mar 2026 21:12:37 -0400 Subject: [PATCH 1/5] Revert "fix: revert charm v2 upgrades" This reverts commit 5f59f5326ee6eb02f3b4f94fdd7b11631e54edc1. --- container.go | 5 +- container_test.go | 15 +-- go.mod | 11 +- go.sum | 14 ++- program.go | 2 +- sample/types/views.go | 2 +- testdata/TestCollectionOutput.golden | 162 ++++++++++++++------------- testdata/TestEntityOutput.golden | 160 +++++++++++++------------- testdata/TestErrorOutput.golden | 20 ++-- testdata/TestFrameOutput.golden | 2 +- testdata/TestLoadingOutput.golden | 18 +-- testdata/TestMarkdownOutput.golden | 160 +++++++++++++------------- themes/base.go | 100 +++++++++-------- themes/theme.go | 6 +- themes/themes.go | 2 +- types/msgs.go | 2 +- utils/utils.go | 75 +++++++++++++ views/archive.go | 8 +- views/collection.go | 6 +- views/entity.go | 18 +-- views/errors.go | 2 +- views/form.go | 21 ++-- views/frame.go | 3 +- views/loading.go | 4 +- views/markdown.go | 18 +-- 25 files changed, 465 insertions(+), 371 deletions(-) create mode 100644 utils/utils.go diff --git a/container.go b/container.go index 99220f3..6a13557 100644 --- a/container.go +++ b/container.go @@ -9,8 +9,8 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -19,6 +19,7 @@ import ( type View interface { tea.Model + tea.ViewModel ShowFooter() bool HelpMsg() string diff --git a/container_test.go b/container_test.go index d2d81f2..6061e2f 100644 --- a/container_test.go +++ b/container_test.go @@ -8,10 +8,11 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/exp/teatest" - "github.com/muesli/termenv" + oldTea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/lipgloss/v2/compat" + "github.com/charmbracelet/x/exp/teatest/v2" "github.com/flowexec/tuikit" sampleTypes "github.com/flowexec/tuikit/sample/types" @@ -20,7 +21,7 @@ import ( ) func init() { - lipgloss.SetColorProfile(termenv.Ascii) + compat.Profile = colorprofile.Ascii } func TestFrameOutput(t *testing.T) { @@ -209,10 +210,10 @@ func TestFormOutput(t *testing.T) { teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { return bytes.Contains(bts, []byte("Are you sure?")) }, teatest.WithCheckInterval(100*time.Millisecond), teatest.WithDuration(3*time.Second)) - container.Send(tea.KeyMsg{Type: tea.KeyEnter}, 100*time.Millisecond) + container.Send(oldTea.KeyMsg{Type: oldTea.KeyEnter}, 100*time.Millisecond) teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { return bytes.Contains(bts, []byte("Thank you for confirming!")) }, teatest.WithCheckInterval(100*time.Millisecond), teatest.WithDuration(5*time.Second)) - container.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}, 100*time.Millisecond) + container.Send(tea.KeyPressMsg{Text: "q"}, 100*time.Millisecond) tm.WaitFinished(t, teatest.WithFinalTimeout(3*time.Second)) } diff --git a/go.mod b/go.mod index c83cd06..a12e272 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,15 @@ module github.com/flowexec/tuikit go 1.23.0 require ( - github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1 + github.com/charmbracelet/colorprofile v0.3.1 github.com/charmbracelet/huh v0.7.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 github.com/charmbracelet/log v0.4.2 - github.com/charmbracelet/x/exp/teatest v0.0.0-20250623112707-45752038d08d + github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4 github.com/jahvon/glamour v0.8.1-patch3 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 @@ -24,12 +27,14 @@ require ( github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/windows v0.2.1 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect diff --git a/go.sum b/go.sum index 4b8e6c1..d7284a0 100644 --- a/go.sum +++ b/go.sum @@ -18,14 +18,20 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1 h1:yaxFt97mvofGY7bYZn8U/aSVoamXGE3O4AEvWhshUDI= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1/go.mod h1:qbcZLI5z8R49v9xBdU5V5Dh5D2uccx8wSwBqxQyErqc= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= @@ -40,12 +46,16 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHE github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250623112707-45752038d08d h1:b8GXylLbV6WaBxHjj4fyBqVzWW66vScY5bbJCwoMBOA= -github.com/charmbracelet/x/exp/teatest v0.0.0-20250623112707-45752038d08d/go.mod h1:MhV4atqUTcHvdaA7Qbkgb0Tvvr+BrH6IW7/i2XW39R8= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4 h1:eks/O/4hrXL3FLsPescG7Xv4xBtgKskYM1isahjT8/M= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4/go.mod h1:4XR/281pVTamsxUzWiHnVxVVvhfSfd/L1+lWvsl23Io= +github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM= +github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= +github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= diff --git a/program.go b/program.go index f0e2a80..289d8a4 100644 --- a/program.go +++ b/program.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" ) type Program struct { diff --git a/sample/types/views.go b/sample/types/views.go index 5cc8fc7..9c70e69 100644 --- a/sample/types/views.go +++ b/sample/types/views.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" "gopkg.in/yaml.v3" "github.com/flowexec/tuikit/types" diff --git a/testdata/TestCollectionOutput.golden b/testdata/TestCollectionOutput.golden index a7a7ae8..07fbdc8 100644 --- a/testdata/TestCollectionOutput.golden +++ b/testdata/TestCollectionOutput.golden @@ -1,80 +1,82 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - 3 Colors - - ║ Blue Violet (Tertiary Color) - Green (Secondary Color) - Red (Primary Color) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ]  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l +╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ +    +  3 Colors  +    +  ║ Blue Violet (Tertiary Color)  +    Green (Secondary Color)   +    Red (Primary Color)   +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +    +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ] +[?25h[?2004l diff --git a/testdata/TestEntityOutput.golden b/testdata/TestEntityOutput.golden index afb8775..25e5e9d 100644 --- a/testdata/TestEntityOutput.golden +++ b/testdata/TestEntityOutput.golden @@ -1,80 +1,80 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -   Green  -  -  I am a(n) Color. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ]  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + Green  + +I am a(n) Color. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ] [?25h[?2004l diff --git a/testdata/TestErrorOutput.golden b/testdata/TestErrorOutput.golden index 125c400..2799ef5 100644 --- a/testdata/TestErrorOutput.golden +++ b/testdata/TestErrorOutput.golden @@ -1,10 +1,10 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -!! encountered error !! - -something went wrong -please try again - -──────────────────────────────────────────────────────────────────────────────── -  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ +!! encountered error !! + +something went wrong +please try again + +──────────────────────────────────────────────────────────────────────────────── +[?25h[?2004l diff --git a/testdata/TestFrameOutput.golden b/testdata/TestFrameOutput.golden index e328c5d..3ddddb5 100644 --- a/testdata/TestFrameOutput.golden +++ b/testdata/TestFrameOutput.golden @@ -1 +1 @@ -[?25l[?2004h]2;tuikit-test Hello, world! [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l Hello, world! [?25h[?2004l diff --git a/testdata/TestLoadingOutput.golden b/testdata/TestLoadingOutput.golden index 9f393bd..dd0a918 100644 --- a/testdata/TestLoadingOutput.golden +++ b/testdata/TestLoadingOutput.golden @@ -1,9 +1,9 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - - ●∙∙ thinking... - - -  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + + + ●∙∙ thinking... + + +[?25h[?2004l diff --git a/testdata/TestMarkdownOutput.golden b/testdata/TestMarkdownOutput.golden index 94d14f7..8b5ae39 100644 --- a/testdata/TestMarkdownOutput.golden +++ b/testdata/TestMarkdownOutput.golden @@ -1,80 +1,80 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -   Hello!  -  -  I am a Markdown document. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ ↑/↓: navigate ]  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file +[?2004h]2;tuikit-test[?25l ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + Hello!  + +I am a Markdown document. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ ↑/↓: navigate ] [?25h[?2004l diff --git a/themes/base.go b/themes/base.go index 73cd16c..65db285 100644 --- a/themes/base.go +++ b/themes/base.go @@ -6,10 +6,12 @@ import ( "strings" "text/template" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/v2/list" + "github.com/charmbracelet/bubbles/v2/spinner" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" + lipglossv1 "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/lipgloss/v2/compat" "github.com/charmbracelet/log" ) @@ -192,23 +194,23 @@ func (t baseTheme) RenderInContainer(text string) string { } func (t baseTheme) ListStyles() list.Styles { - s := list.DefaultStyles() + s := list.DefaultStyles(compat.HasDarkBackground) s.StatusBar = s.StatusBar. Padding(0, 0, 1, 0). Italic(true). - Foreground(lipgloss.Color(t.Colors.Tertiary)) + Foreground(lipglossv1.Color(t.Colors.Tertiary)) return s } func (t baseTheme) ListItemStyles() list.DefaultItemStyles { - s := list.NewDefaultItemStyles() - s.NormalTitle = s.NormalTitle.Foreground(lipgloss.Color(t.Colors.Secondary)) - s.NormalDesc = s.NormalDesc.Foreground(lipgloss.Color(t.Colors.Body)) + s := list.NewDefaultItemStyles(compat.HasDarkBackground) + s.NormalTitle = s.NormalTitle.Foreground(lipglossv1.Color(t.Colors.Secondary)) + s.NormalDesc = s.NormalDesc.Foreground(lipglossv1.Color(t.Colors.Body)) s.SelectedTitle = s.SelectedTitle. Border(lipgloss.DoubleBorder(), false, false, false, true). - Foreground(lipgloss.Color(t.Colors.Primary)). - BorderForeground(lipgloss.Color(t.Colors.Primary)). + Foreground(lipglossv1.Color(t.Colors.Primary)). + BorderForeground(lipglossv1.Color(t.Colors.Primary)). Bold(true) s.SelectedDesc = s.SelectedDesc. Border(lipgloss.HiddenBorder(), false, false, false, true). @@ -218,18 +220,18 @@ func (t baseTheme) ListItemStyles() list.DefaultItemStyles { func (t baseTheme) LoggerStyles() *log.Styles { baseStyles := log.DefaultStyles() - baseStyles.Timestamp = baseStyles.Timestamp.Foreground(lipgloss.Color(t.Colors.Gray)) - baseStyles.Levels = map[log.Level]lipgloss.Style{ - log.InfoLevel: baseStyles.Levels[log.InfoLevel].Foreground(lipgloss.Color(t.Colors.Info)).SetString("INF"), - LogNoticeLevel: baseStyles.Levels[LogNoticeLevel].Foreground(lipgloss.Color(t.Colors.Warning)).SetString("NTC"), - log.WarnLevel: baseStyles.Levels[log.WarnLevel].Foreground(lipgloss.Color(t.Colors.Warning)).SetString("WRN"), - log.ErrorLevel: baseStyles.Levels[log.ErrorLevel].Foreground(lipgloss.Color(t.Colors.Error)).SetString("ERR"), - log.DebugLevel: baseStyles.Levels[log.DebugLevel].Foreground(lipgloss.Color(t.Colors.Emphasis)).SetString("DBG"), - log.FatalLevel: baseStyles.Levels[log.FatalLevel].Foreground(lipgloss.Color(t.Colors.Error)).SetString("ERR"), + baseStyles.Timestamp = baseStyles.Timestamp.Foreground(lipglossv1.Color(t.Colors.Gray)) + baseStyles.Levels = map[log.Level]lipglossv1.Style{ + log.InfoLevel: baseStyles.Levels[log.InfoLevel].Foreground(lipglossv1.Color(t.Colors.Info)).SetString("INF"), + LogNoticeLevel: baseStyles.Levels[LogNoticeLevel].Foreground(lipglossv1.Color(t.Colors.Warning)).SetString("NTC"), + log.WarnLevel: baseStyles.Levels[log.WarnLevel].Foreground(lipglossv1.Color(t.Colors.Warning)).SetString("WRN"), + log.ErrorLevel: baseStyles.Levels[log.ErrorLevel].Foreground(lipglossv1.Color(t.Colors.Error)).SetString("ERR"), + log.DebugLevel: baseStyles.Levels[log.DebugLevel].Foreground(lipglossv1.Color(t.Colors.Emphasis)).SetString("DBG"), + log.FatalLevel: baseStyles.Levels[log.FatalLevel].Foreground(lipglossv1.Color(t.Colors.Error)).SetString("ERR"), } - baseStyles.Message = baseStyles.Message.Foreground(lipgloss.Color(t.Colors.Body)) - baseStyles.Key = baseStyles.Key.Foreground(lipgloss.Color(t.Colors.Secondary)) - baseStyles.Value = baseStyles.Value.Foreground(lipgloss.Color(t.Colors.Gray)) + baseStyles.Message = baseStyles.Message.Foreground(lipglossv1.Color(t.Colors.Body)) + baseStyles.Key = baseStyles.Key.Foreground(lipglossv1.Color(t.Colors.Secondary)) + baseStyles.Value = baseStyles.Value.Foreground(lipglossv1.Color(t.Colors.Gray)) return baseStyles } @@ -252,50 +254,50 @@ func (t baseTheme) GlamourMarkdownStyleJSON() (string, error) { func (t baseTheme) HuhTheme() *huh.Theme { baseTheme := huh.ThemeBase() - baseTheme.FieldSeparator = lipgloss.NewStyle().SetString("\n\n") + baseTheme.FieldSeparator = lipglossv1.NewStyle().SetString("\n\n") - baseTheme.Focused.Base = baseTheme.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) - baseTheme.Focused.Title = baseTheme.Focused.Title.Foreground(lipgloss.Color(t.Colors.Primary)).Bold(true) - baseTheme.Focused.Description = baseTheme.Focused.Description.Foreground(lipgloss.Color(t.Colors.Body)) - baseTheme.Focused.ErrorMessage = baseTheme.Focused.ErrorMessage.Foreground(lipgloss.Color(t.Colors.Error)) - baseTheme.Focused.FocusedButton = baseTheme.Focused.FocusedButton.Foreground(lipgloss.Color(t.Colors.Primary)). - Background(lipgloss.Color(t.Colors.Tertiary)) - baseTheme.Focused.BlurredButton = baseTheme.Focused.BlurredButton.Foreground(lipgloss.Color(t.Colors.Secondary)). - Background(lipgloss.Color(t.Colors.Gray)) + baseTheme.Focused.Base = baseTheme.Focused.Base.BorderStyle(lipglossv1.HiddenBorder()) + baseTheme.Focused.Title = baseTheme.Focused.Title.Foreground(lipglossv1.Color(t.Colors.Primary)).Bold(true) + baseTheme.Focused.Description = baseTheme.Focused.Description.Foreground(lipglossv1.Color(t.Colors.Body)) + baseTheme.Focused.ErrorMessage = baseTheme.Focused.ErrorMessage.Foreground(lipglossv1.Color(t.Colors.Error)) + baseTheme.Focused.FocusedButton = baseTheme.Focused.FocusedButton.Foreground(lipglossv1.Color(t.Colors.Primary)). + Background(lipglossv1.Color(t.Colors.Tertiary)) + baseTheme.Focused.BlurredButton = baseTheme.Focused.BlurredButton.Foreground(lipglossv1.Color(t.Colors.Secondary)). + Background(lipglossv1.Color(t.Colors.Gray)) baseTheme.Focused.TextInput.Placeholder = - baseTheme.Focused.TextInput.Placeholder.Foreground(lipgloss.Color(t.Colors.Body)) + baseTheme.Focused.TextInput.Placeholder.Foreground(lipglossv1.Color(t.Colors.Body)) baseTheme.Focused.TextInput.Cursor = - baseTheme.Focused.TextInput.Cursor.Foreground(lipgloss.Color(t.Colors.Secondary)) + baseTheme.Focused.TextInput.Cursor.Foreground(lipglossv1.Color(t.Colors.Secondary)) baseTheme.Focused.TextInput.CursorText = - baseTheme.Focused.TextInput.Cursor.Foreground(lipgloss.Color(t.Colors.Secondary)) + baseTheme.Focused.TextInput.Cursor.Foreground(lipglossv1.Color(t.Colors.Secondary)) baseTheme.Focused.TextInput.Placeholder = - baseTheme.Focused.TextInput.Placeholder.Foreground(lipgloss.Color(t.Colors.Gray)) + baseTheme.Focused.TextInput.Placeholder.Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Focused.TextInput.Text = - baseTheme.Focused.TextInput.Text.Foreground(lipgloss.Color(t.Colors.Body)) + baseTheme.Focused.TextInput.Text.Foreground(lipglossv1.Color(t.Colors.Body)) baseTheme.Focused.TextInput.Prompt = - baseTheme.Focused.TextInput.Prompt.Foreground(lipgloss.Color(t.Colors.Tertiary)) + baseTheme.Focused.TextInput.Prompt.Foreground(lipglossv1.Color(t.Colors.Tertiary)) - baseTheme.Blurred.Title = baseTheme.Blurred.Title.Foreground(lipgloss.Color(t.Colors.White)) - baseTheme.Blurred.Description = baseTheme.Blurred.Description.Foreground(lipgloss.Color(t.Colors.Gray)) - baseTheme.Blurred.ErrorMessage = baseTheme.Blurred.ErrorMessage.Foreground(lipgloss.Color(t.Colors.Emphasis)) - baseTheme.Blurred.FocusedButton = baseTheme.Blurred.FocusedButton.Foreground(lipgloss.Color(t.Colors.Secondary)). - Background(lipgloss.Color(t.Colors.Gray)) - baseTheme.Blurred.BlurredButton = baseTheme.Blurred.BlurredButton.Foreground(lipgloss.Color(t.Colors.Gray)). - Background(lipgloss.Color(t.Colors.Gray)) + baseTheme.Blurred.Title = baseTheme.Blurred.Title.Foreground(lipglossv1.Color(t.Colors.White)) + baseTheme.Blurred.Description = baseTheme.Blurred.Description.Foreground(lipglossv1.Color(t.Colors.Gray)) + baseTheme.Blurred.ErrorMessage = baseTheme.Blurred.ErrorMessage.Foreground(lipglossv1.Color(t.Colors.Emphasis)) + baseTheme.Blurred.FocusedButton = baseTheme.Blurred.FocusedButton.Foreground(lipglossv1.Color(t.Colors.Secondary)). + Background(lipglossv1.Color(t.Colors.Gray)) + baseTheme.Blurred.BlurredButton = baseTheme.Blurred.BlurredButton.Foreground(lipglossv1.Color(t.Colors.Gray)). + Background(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Placeholder = baseTheme.Blurred.TextInput.Placeholder. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Cursor = baseTheme.Blurred.TextInput.Cursor. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.CursorText = baseTheme.Blurred.TextInput.CursorText. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Placeholder = baseTheme.Blurred.TextInput.Placeholder. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Text = baseTheme.Blurred.TextInput.Text. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Prompt = baseTheme.Blurred.TextInput.Prompt. - Foreground(lipgloss.Color(t.Colors.Gray)) + Foreground(lipglossv1.Color(t.Colors.Gray)) return baseTheme } diff --git a/themes/theme.go b/themes/theme.go index 0e27ede..d9011ec 100644 --- a/themes/theme.go +++ b/themes/theme.go @@ -1,10 +1,10 @@ package themes import ( - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/v2/list" + "github.com/charmbracelet/bubbles/v2/spinner" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/log" ) diff --git a/themes/themes.go b/themes/themes.go index cb5bf3c..26a8680 100644 --- a/themes/themes.go +++ b/themes/themes.go @@ -3,7 +3,7 @@ package themes import ( _ "embed" - "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/v2/spinner" ) const ( diff --git a/types/msgs.go b/types/msgs.go index 37796bb..fa15b50 100644 --- a/types/msgs.go +++ b/types/msgs.go @@ -3,7 +3,7 @@ package types import ( "time" - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..787df05 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,75 @@ +package utils + +import ( + tea "github.com/charmbracelet/bubbletea" + teaV2 "github.com/charmbracelet/bubbletea/v2" +) + +func CmdToV2Cmd(oldCmd tea.Cmd) (newCmd teaV2.Cmd) { + newCmd = func() teaV2.Msg { + if oldCmd != nil { + return oldCmd() + } + return nil + } + return newCmd +} + +func V2CmdToCmd(newCmd teaV2.Cmd) (oldCmd tea.Cmd) { + oldCmd = func() tea.Msg { + if newCmd != nil { + return newCmd() + } + return nil + } + return oldCmd +} + +func V2MsgToMsg(msg teaV2.Msg) (oldMsg tea.Msg) { + switch m := msg.(type) { + case teaV2.KeyMsg: + switch { + case m.Key().Text != "": + oldMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(m.Key().Text)} + case m.Key().Code != 0: + switch m.Key().Code { + case teaV2.KeySpace: + oldMsg = tea.KeyMsg{Type: tea.KeySpace} + case teaV2.KeyBackspace: + oldMsg = tea.KeyMsg{Type: tea.KeyBackspace} + case teaV2.KeyEnter: + oldMsg = tea.KeyMsg{Type: tea.KeyEnter} + case teaV2.KeyTab: + oldMsg = tea.KeyMsg{Type: tea.KeyTab} + case teaV2.KeyDown: + oldMsg = tea.KeyMsg{Type: tea.KeyDown} + case teaV2.KeyUp: + oldMsg = tea.KeyMsg{Type: tea.KeyUp} + case teaV2.KeyLeft: + oldMsg = tea.KeyMsg{Type: tea.KeyLeft} + case teaV2.KeyRight: + oldMsg = tea.KeyMsg{Type: tea.KeyRight} + case teaV2.KeyPgUp: + oldMsg = tea.KeyMsg{Type: tea.KeyPgUp} + case teaV2.KeyPgDown: + oldMsg = tea.KeyMsg{Type: tea.KeyPgDown} + case teaV2.KeyHome: + oldMsg = tea.KeyMsg{Type: tea.KeyHome} + case teaV2.KeyEnd: + oldMsg = tea.KeyMsg{Type: tea.KeyEnd} + case teaV2.KeyEsc: + oldMsg = tea.KeyMsg{Type: tea.KeyEsc} + case teaV2.KeyDelete: + oldMsg = tea.KeyMsg{Type: tea.KeyDelete} + case teaV2.KeyInsert: + oldMsg = tea.KeyMsg{Type: tea.KeyInsert} + } + default: + // unsupported for now + panic("unsupported key type") + } + default: + oldMsg = msg + } + return oldMsg +} diff --git a/views/archive.go b/views/archive.go index a1f46c1..a625fda 100644 --- a/views/archive.go +++ b/views/archive.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/bubbles/v2/list" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" "github.com/muesli/reflow/wordwrap" "github.com/flowexec/tuikit/io" @@ -125,7 +125,7 @@ func (v *LogArchiveView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } v.model.SetItems(v.items) - case tea.KeyEnter.String(): + case "enter": if v.activeEntry != nil { return v, nil } diff --git a/views/collection.go b/views/collection.go index 9d2b9e6..ef678dd 100644 --- a/views/collection.go +++ b/views/collection.go @@ -4,8 +4,8 @@ import ( "fmt" "sort" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/bubbles/v2/list" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/jahvon/glamour" "github.com/flowexec/tuikit/themes" @@ -107,7 +107,7 @@ func (v *CollectionView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, nil } v.format = types.CollectionFormatJSON - case tea.KeyEnter.String(): + case "enter": if v.selectedFunc == nil { return v, nil } diff --git a/views/entity.go b/views/entity.go index ae23ed7..57b8134 100644 --- a/views/entity.go +++ b/views/entity.go @@ -4,8 +4,8 @@ import ( "fmt" "math" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/bubbles/v2/viewport" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/jahvon/glamour" "github.com/flowexec/tuikit/themes" @@ -42,7 +42,7 @@ func NewEntityView( format = types.EntityFormatDocument } - vp := viewport.New(state.ContentWidth, state.ContentHeight) + vp := viewport.New(viewport.WithWidth(state.ContentWidth), viewport.WithHeight(state.ContentHeight)) vp.Style = state.Theme.EntityViewStyle().Width(state.ContentWidth) return &EntityView{ entity: entity, @@ -69,8 +69,8 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case types.RenderState: - v.viewport.Width = msg.ContentWidth - v.viewport.Height = msg.ContentHeight + v.viewport.SetWidth(msg.ContentWidth) + v.viewport.SetHeight(msg.ContentHeight) v.viewport.SetContent(v.renderedContent()) case tea.KeyMsg: switch msg.String() { @@ -92,10 +92,10 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } v.format = types.CollectionFormatJSON v.viewport.GotoTop() - case tea.KeyUp.String(): - v.viewport.ScrollUp(1) - case tea.KeyDown.String(): - v.viewport.ScrollDown(1) + case "up": + v.viewport.LineUp(1) + case "down": + v.viewport.LineDown(1) default: for _, cb := range v.callbacks { if cb.Key == msg.String() { diff --git a/views/errors.go b/views/errors.go index fd9f57b..aac9a15 100644 --- a/views/errors.go +++ b/views/errors.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" diff --git a/views/form.go b/views/form.go index 057749e..26e52c5 100644 --- a/views/form.go +++ b/views/form.go @@ -13,11 +13,13 @@ import ( "sync" tea "github.com/charmbracelet/bubbletea" + teaV2 "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/huh" "golang.org/x/term" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" + "github.com/flowexec/tuikit/utils" ) type FormFieldType uint @@ -193,7 +195,7 @@ func NewFormView( WithWidth(state.ContentWidth). WithHeight(state.ContentHeight). WithShowHelp(true) - hf.SubmitCmd = types.Submit + hf.SubmitCmd = utils.V2CmdToCmd(types.Submit) hf.CancelCmd = tea.Quit if len(fields) > 5 { hf = hf.WithLayout(huh.LayoutColumns(2)) // TODO: make this configurable or auto-dynamic @@ -229,16 +231,11 @@ func (f *Form) Completed() bool { return f.completed } -func (f *Form) Init() tea.Cmd { - f.mu.Lock() - defer f.mu.Unlock() - return f.form.Init() +func (f *Form) Init() teaV2.Cmd { + return utils.CmdToV2Cmd(f.form.Init()) } -func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - f.mu.Lock() - defer f.mu.Unlock() - +func (f *Form) Update(msg teaV2.Msg) (teaV2.Model, teaV2.Cmd) { if f.err != nil { return f.err.Update(msg) } @@ -256,14 +253,14 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return f, types.ReplaceView } - model, cmd := f.form.Update(msg) + model, cmd := f.form.Update(utils.V2MsgToMsg(msg)) var ok bool f.form, ok = model.(*huh.Form) if !ok { f.err = NewErrorView(fmt.Errorf("unable to cast form model to huh.Form"), f.theme) - return f, cmd + return f, utils.CmdToV2Cmd(cmd) } - return f, cmd + return f, utils.CmdToV2Cmd(cmd) } func (f *Form) View() string { diff --git a/views/frame.go b/views/frame.go index 162d5c4..15864c7 100644 --- a/views/frame.go +++ b/views/frame.go @@ -1,13 +1,14 @@ package views import ( - tea "github.com/charmbracelet/bubbletea" + tea "github.com/charmbracelet/bubbletea/v2" ) const FrameViewType = "frame" type FramedModel interface { tea.Model + tea.ViewModel } type FrameView struct { diff --git a/views/loading.go b/views/loading.go index b32aeab..02b54ef 100644 --- a/views/loading.go +++ b/views/loading.go @@ -3,8 +3,8 @@ package views import ( "fmt" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/bubbles/v2/spinner" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) diff --git a/views/markdown.go b/views/markdown.go index 9094cbf..87775be 100644 --- a/views/markdown.go +++ b/views/markdown.go @@ -4,8 +4,8 @@ import ( "math" "sync" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/bubbles/v2/viewport" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/jahvon/glamour" "github.com/flowexec/tuikit/themes" @@ -22,7 +22,7 @@ type MarkdownView struct { } func NewMarkdownView(state *types.RenderState, content string) *MarkdownView { - vp := viewport.New(state.ContentWidth, state.ContentHeight) + vp := viewport.New(viewport.WithWidth(state.ContentWidth), viewport.WithHeight(state.ContentHeight)) vp.Style = state.Theme.EntityViewStyle().Width(state.ContentWidth).Height(state.ContentHeight) return &MarkdownView{ content: content, @@ -48,14 +48,14 @@ func (v *MarkdownView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case types.RenderState: v.width = msg.ContentWidth v.height = msg.ContentHeight - v.viewport.Width = v.width - v.viewport.Height = v.height + v.viewport.SetWidth(v.width) + v.viewport.SetHeight(v.height) case tea.KeyMsg: switch msg.String() { - case tea.KeyUp.String(): - v.viewport.ScrollUp(1) - case tea.KeyDown.String(): - v.viewport.ScrollDown(1) + case "up": + v.viewport.LineUp(1) + case "down": + v.viewport.LineDown(1) } } var cmd tea.Cmd From fa8a9ba7f7cbc42be3cb4d0ccd44262f37d3db97 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 31 Mar 2026 22:20:22 -0400 Subject: [PATCH 2/5] upgrade --- .github/dependabot.yml | 1 + container.go | 16 +-- container_test.go | 113 ++++++++++++++++++- go.mod | 58 +++++----- go.sum | 119 +++++++++----------- io/logger.go | 7 +- program.go | 2 +- sample/types/views.go | 8 +- testdata/TestCollectionOutput.golden | 162 +++++++++++++-------------- testdata/TestDetailOutput.golden | 80 +++++++++++++ testdata/TestEntityOutput.golden | 16 +-- testdata/TestErrorOutput.golden | 16 +-- testdata/TestFrameOutput.golden | 2 +- testdata/TestLoadingOutput.golden | 10 +- testdata/TestMarkdownOutput.golden | 14 +-- testdata/TestTableMiniOutput.golden | 80 +++++++++++++ testdata/TestTableOutput.golden | 80 +++++++++++++ themes/base.go | 115 ++++++++++--------- themes/colors.go | 29 ++--- themes/theme.go | 12 +- themes/themes.go | 11 +- themes/types.go | 2 +- types/msgs.go | 2 +- utils/utils.go | 75 ------------- views/archive.go | 12 +- views/collection.go | 18 +-- views/detail.go | 18 +-- views/entity.go | 24 ++-- views/errors.go | 6 +- views/form.go | 24 ++-- views/frame.go | 5 +- views/loading.go | 8 +- views/markdown.go | 16 +-- views/table.go | 16 +-- 34 files changed, 722 insertions(+), 455 deletions(-) create mode 100644 testdata/TestDetailOutput.golden create mode 100644 testdata/TestTableMiniOutput.golden create mode 100644 testdata/TestTableOutput.golden delete mode 100644 utils/utils.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 98977e2..bed24e7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -30,6 +30,7 @@ updates: - "github.com/onsi/gomega" charmbracelet-deps: patterns: + - "charm.land/*" - "github.com/charmbracelet/*" - package-ecosystem: "docker" directory: "/" diff --git a/container.go b/container.go index 6a13557..05dd12e 100644 --- a/container.go +++ b/container.go @@ -9,8 +9,8 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss/v2" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -19,7 +19,6 @@ import ( type View interface { tea.Model - tea.ViewModel ShowFooter() bool HelpMsg() string @@ -140,7 +139,6 @@ func (c *Container) Init() tea.Cmd { cmd := c.CurrentView().Init() cmds = append( cmds, - tea.SetWindowTitle(c.app.Name), c.doTick(), cmd, ) @@ -184,7 +182,7 @@ func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if err != nil { c.HandleError(err) } - case tea.KeyMsg: + case tea.KeyPressMsg: if c.CurrentView().Type() == views.FormViewType { fwdMsg = nil _, cmd := c.CurrentView().Update(msg) @@ -228,12 +226,12 @@ func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return c, tea.Batch(cmds...) } -func (c *Container) View() string { +func (c *Container) View() tea.View { var footer string var footerPrefix string if !c.Ready() && c.CurrentView().Type() != views.LoadingViewType { - return "" + return tea.NewView("") } switch { case c.CurrentView().Type() == views.FrameViewType: @@ -267,7 +265,9 @@ func (c *Container) View() string { } header := c.render.Theme.RenderHeader(c.app.Name, c.app.stateKey, c.app.stateVal, c.render.Width) - return lipgloss.JoinVertical(lipgloss.Top, header, c.CurrentView().View(), footer) + v := tea.NewView(lipgloss.JoinVertical(lipgloss.Top, header, c.CurrentView().View().Content, footer)) + v.WindowTitle = c.app.Name + return v } func (c *Container) Ready() bool { diff --git a/container_test.go b/container_test.go index 6061e2f..91f42b1 100644 --- a/container_test.go +++ b/container_test.go @@ -5,13 +5,13 @@ import ( "context" "errors" "io" + "os" "testing" "time" - oldTea "github.com/charmbracelet/bubbletea" - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/charmbracelet/colorprofile" - "github.com/charmbracelet/lipgloss/v2/compat" "github.com/charmbracelet/x/exp/teatest/v2" "github.com/flowexec/tuikit" @@ -20,8 +20,14 @@ import ( "github.com/flowexec/tuikit/views" ) -func init() { - compat.Profile = colorprofile.Ascii +func TestMain(m *testing.M) { + // Force ASCII color profile so lipgloss strips all color/style escape + // sequences, producing identical output regardless of the terminal + // capabilities of the host (local dev vs CI). + os.Setenv("NO_COLOR", "1") + os.Setenv("TERM", "dumb") + lipgloss.Writer.Profile = colorprofile.Ascii + os.Exit(m.Run()) } func TestFrameOutput(t *testing.T) { @@ -175,6 +181,101 @@ func TestCollectionOutput(t *testing.T) { teatest.RequireEqualOutput(t, out) } +func TestDetailOutput(t *testing.T) { + ctx := context.Background() + app := &tuikit.Application{Name: "tuikit-test"} + container, err := tuikit.NewContainer(ctx, app) + if err != nil { + t.Errorf("Failed to create container: %v", err) + } + + tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) + container.SetSendFunc(tm.Send) + view := views.NewDetailView( + container.RenderState(), + "This is the body content of the detail view.", + views.DetailField{Key: "Name", Value: "Test Item"}, + views.DetailField{Key: "Status", Value: "Active"}, + ) + if err := container.SetView(view); err != nil { + t.Errorf("Failed to set view: %v", err) + } + + container.Send(tea.Quit(), 100*time.Millisecond) + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) + out, err := io.ReadAll(tm.FinalOutput(t)) + if err != nil { + t.Error(err) + } + teatest.RequireEqualOutput(t, out) +} + +func TestTableOutput(t *testing.T) { + ctx := context.Background() + app := &tuikit.Application{Name: "tuikit-test"} + container, err := tuikit.NewContainer(ctx, app) + if err != nil { + t.Errorf("Failed to create container: %v", err) + } + + tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) + container.SetSendFunc(tm.Send) + columns := []views.TableColumn{ + {Title: "Name", Percentage: 50}, + {Title: "Status", Percentage: 50}, + } + rows := []views.TableRow{ + {Data: []string{"Alpha", "Active"}}, + {Data: []string{"Beta", "Inactive"}, Children: []views.TableRow{ + {Data: []string{"Beta-1", "Running"}}, + }}, + } + view := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayFull) + if err := container.SetView(view); err != nil { + t.Errorf("Failed to set view: %v", err) + } + + container.Send(tea.Quit(), 100*time.Millisecond) + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) + out, err := io.ReadAll(tm.FinalOutput(t)) + if err != nil { + t.Error(err) + } + teatest.RequireEqualOutput(t, out) +} + +func TestTableMiniOutput(t *testing.T) { + ctx := context.Background() + app := &tuikit.Application{Name: "tuikit-test"} + container, err := tuikit.NewContainer(ctx, app) + if err != nil { + t.Errorf("Failed to create container: %v", err) + } + + tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) + container.SetSendFunc(tm.Send) + columns := []views.TableColumn{ + {Title: "Command", Percentage: 100}, + } + rows := []views.TableRow{ + {Data: []string{"build"}}, + {Data: []string{"test"}}, + {Data: []string{"deploy"}}, + } + view := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayMini) + if err := container.SetView(view); err != nil { + t.Errorf("Failed to set view: %v", err) + } + + container.Send(tea.Quit(), 100*time.Millisecond) + tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) + out, err := io.ReadAll(tm.FinalOutput(t)) + if err != nil { + t.Error(err) + } + teatest.RequireEqualOutput(t, out) +} + func TestFormOutput(t *testing.T) { ctx := context.Background() app := &tuikit.Application{Name: "tuikit-test"} @@ -210,7 +311,7 @@ func TestFormOutput(t *testing.T) { teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { return bytes.Contains(bts, []byte("Are you sure?")) }, teatest.WithCheckInterval(100*time.Millisecond), teatest.WithDuration(3*time.Second)) - container.Send(oldTea.KeyMsg{Type: oldTea.KeyEnter}, 100*time.Millisecond) + container.Send(tea.KeyPressMsg{Code: tea.KeyEnter}, 100*time.Millisecond) teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { return bytes.Contains(bts, []byte("Thank you for confirming!")) }, teatest.WithCheckInterval(100*time.Millisecond), teatest.WithDuration(5*time.Second)) diff --git a/go.mod b/go.mod index a12e272..68cf8c4 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,16 @@ module github.com/flowexec/tuikit -go 1.23.0 +go 1.25.8 require ( - github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 - github.com/charmbracelet/bubbletea v1.3.5 - github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1 - github.com/charmbracelet/colorprofile v0.3.1 - github.com/charmbracelet/huh v0.7.0 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 - github.com/charmbracelet/log v0.4.2 - github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4 - github.com/jahvon/glamour v0.8.1-patch3 + charm.land/bubbles/v2 v2.0.0 + charm.land/bubbletea/v2 v2.0.0 + charm.land/glamour/v2 v2.0.0 + charm.land/huh/v2 v2.0.0 + charm.land/lipgloss/v2 v2.0.1 + charm.land/log/v2 v2.0.0 + github.com/charmbracelet/colorprofile v0.4.2 + github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260330094520-2dce04b6f8a4 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 go.uber.org/mock v0.5.2 @@ -24,40 +22,40 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymanbagabas/go-udiff v0.2.0 // indirect + github.com/aymanbagabas/go-udiff v0.4.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f // indirect + github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/charmbracelet/x/windows v0.2.1 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.4 // indirect - github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index d7284a0..ab03a07 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,15 @@ +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ= +charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= +charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= +charm.land/huh/v2 v2.0.0 h1:fvZlEc/PyLgl1fQrr1P30QW7CVY9k8Q1e+LD9jwMur8= +charm.land/huh/v2 v2.0.0/go.mod h1:0WOQ7ZIycEMUsvhcmBMda7tAGkEy9Tvvs6OreNllufA= +charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= +charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= +charm.land/log/v2 v2.0.0 h1:SY3Cey7ipx86/MBXQHwsguOT6X1exT94mmJRdzTNs+s= +charm.land/log/v2 v2.0.0/go.mod h1:c3cZSRqm20qUVVAR1WmS/7ab8bgha3C6G7DjPcaVZz0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= @@ -10,54 +22,44 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1 h1:yaxFt97mvofGY7bYZn8U/aSVoamXGE3O4AEvWhshUDI= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta1/go.mod h1:qbcZLI5z8R49v9xBdU5V5Dh5D2uccx8wSwBqxQyErqc= -github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= -github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= -github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= -github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= -github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= -github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg= -github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81/go.mod h1:poPFOXFTsJsnLbkV3H2KxAAXT7pdjxxLujLocWjkyzM= -github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= -github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= +github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw= -github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f h1:8CnFOYzrMArVN42jYaGvnBo3mxdONgt09fly+9B96GY= +github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f/go.mod h1:V8n/g3qVKNxr2FR37Y+otCsMySvZr601T0C7coEP0bw= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4 h1:eks/O/4hrXL3FLsPescG7Xv4xBtgKskYM1isahjT8/M= -github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20250520131230-8df7c22c09d4/go.mod h1:4XR/281pVTamsxUzWiHnVxVVvhfSfd/L1+lWvsl23Io= -github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM= -github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260330094520-2dce04b6f8a4 h1:d9JS80lSTLjo8vQzIOFlos7p9XADnbkcCJsvBU/4yqs= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260330094520-2dce04b6f8a4/go.mod h1:aRoQwQWmN9LBG2xi3sVByMFt2fdkPCagd0GAJ1qwOfw= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= -github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= -github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= -github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= +github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -66,33 +68,25 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/jahvon/glamour v0.8.1-patch3 h1:LfyMACZavV8yxK4UsPENNQQOqafWuq4ZdLuEAP2ZLE8= -github.com/jahvon/glamour v0.8.1-patch3/go.mod h1:30MVJwG3rcEHrN277NrA4DKzndSL9lBtEmpcfOygwCQ= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -112,28 +106,27 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= -github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/io/logger.go b/io/logger.go index ff1ef7a..14c6b7f 100644 --- a/io/logger.go +++ b/io/logger.go @@ -5,7 +5,8 @@ import ( "os" "time" - "github.com/charmbracelet/log" + "charm.land/log/v2" + "github.com/charmbracelet/colorprofile" "github.com/muesli/termenv" "github.com/flowexec/tuikit/themes" @@ -140,7 +141,7 @@ func applyHumanReadableFormat(handler *log.Logger, style themes.Theme, mode LogM handler.SetFormatter(log.TextFormatter) handler.SetTimeFormat(time.Kitchen) - handler.SetColorProfile(termenv.ColorProfile()) + handler.SetColorProfile(colorprofile.Profile(termenv.ColorProfile())) handler.SetStyles(style.LoggerStyles()) } @@ -279,7 +280,7 @@ func (l *StandardLogger) Warnf(msg string, args ...any) { } func (l *StandardLogger) FatalErr(err error) { - l.Fatalf(err.Error()) + l.Fatalf("%s", err.Error()) } func (l *StandardLogger) Fatalf(msg string, args ...any) { diff --git a/program.go b/program.go index 289d8a4..2fae1a5 100644 --- a/program.go +++ b/program.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" ) type Program struct { diff --git a/sample/types/views.go b/sample/types/views.go index 9c70e69..a048184 100644 --- a/sample/types/views.go +++ b/sample/types/views.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" "gopkg.in/yaml.v3" "github.com/flowexec/tuikit/types" @@ -22,11 +22,11 @@ func (m *Echo) Update(_ tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m *Echo) View() string { +func (m *Echo) View() tea.View { if m.Content == "" { - return "Hello, World!" + return tea.NewView("Hello, World!") } - return m.Content + return tea.NewView(m.Content) } type Thing struct { diff --git a/testdata/TestCollectionOutput.golden b/testdata/TestCollectionOutput.golden index 07fbdc8..982354d 100644 --- a/testdata/TestCollectionOutput.golden +++ b/testdata/TestCollectionOutput.golden @@ -1,82 +1,80 @@ -[?2004h]2;tuikit-test[?25l -╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -    -  3 Colors  -    -  ║ Blue Violet (Tertiary Color)  -    Green (Secondary Color)   -    Red (Primary Color)   -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -    -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ] -[?25h[?2004l +[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + + 3 Colors + +║ Blue Violet (Tertiary Color) +Green (Secondary Color) +Red (Primary Color) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2;[?2026$p[?2027$p \ No newline at end of file diff --git a/testdata/TestDetailOutput.golden b/testdata/TestDetailOutput.golden new file mode 100644 index 0000000..7001356 --- /dev/null +++ b/testdata/TestDetailOutput.golden @@ -0,0 +1,80 @@ +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ +╭──────────────────────────────────────────────────────────╮ +│ Name │ Test Item │ +│ Status │ Active │ +╰──────────────────────────────────────────────────────────╯ + +╭──────────────────────────────────────────────────────────────────────────╮ +│ │ +│ This is the body content of the detail view. │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestEntityOutput.golden b/testdata/TestEntityOutput.golden index 25e5e9d..2dab911 100644 --- a/testdata/TestEntityOutput.golden +++ b/testdata/TestEntityOutput.golden @@ -1,9 +1,10 @@ -[?2004h]2;tuikit-test[?25l ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - Green  +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + Green + + I am a(n) Color. -I am a(n) Color. @@ -75,6 +76,5 @@ - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ] [?25h[?2004l +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestErrorOutput.golden b/testdata/TestErrorOutput.golden index 2799ef5..a13c7db 100644 --- a/testdata/TestErrorOutput.golden +++ b/testdata/TestErrorOutput.golden @@ -1,10 +1,10 @@ -[?2004h]2;tuikit-test[?25l ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -!! encountered error !! +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ +!! encountered error !! -something went wrong -please try again +something went wrong +please try again -──────────────────────────────────────────────────────────────────────────────── -[?25h[?2004l +──────────────────────────────────────────────────────────────────────────────── +[>4m[=0;1u[?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestFrameOutput.golden b/testdata/TestFrameOutput.golden index 3ddddb5..7bf58be 100644 --- a/testdata/TestFrameOutput.golden +++ b/testdata/TestFrameOutput.golden @@ -1 +1 @@ -[?2004h]2;tuikit-test[?25l Hello, world! [?25h[?2004l +[?2026$p[?2027$p[?25l[?2004h[>4;2m[=1;1u[?u Hello, world![>4m[=0;1u [?25h[?2004l \ No newline at end of file diff --git a/testdata/TestLoadingOutput.golden b/testdata/TestLoadingOutput.golden index dd0a918..720ec92 100644 --- a/testdata/TestLoadingOutput.golden +++ b/testdata/TestLoadingOutput.golden @@ -1,9 +1,9 @@ -[?2004h]2;tuikit-test[?25l ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ - ●∙∙ thinking... + ●∙∙ thinking... -[?25h[?2004l +[>4m[=0;1u[?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestMarkdownOutput.golden b/testdata/TestMarkdownOutput.golden index 8b5ae39..ada68ac 100644 --- a/testdata/TestMarkdownOutput.golden +++ b/testdata/TestMarkdownOutput.golden @@ -1,9 +1,9 @@ -[?2004h]2;tuikit-test[?25l ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - Hello!  +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + Hello! -I am a Markdown document. +I am a Markdown document. @@ -76,5 +76,5 @@ -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ ↑/↓: navigate ] [?25h[?2004l +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ ↑/↓: navigate ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestTableMiniOutput.golden b/testdata/TestTableMiniOutput.golden new file mode 100644 index 0000000..053c1a9 --- /dev/null +++ b/testdata/TestTableMiniOutput.golden @@ -0,0 +1,80 @@ +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ + +╭─────────────────────────────────────────────────────╮ +│ │ +│ Command │ +│ ─────────────────────────────────────────────────── │ +│ ◌ build │ +│ ◌ test │ +│ ◌ deploy │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰─────────────────────────────────────────────────────╯ + + + + +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestTableOutput.golden b/testdata/TestTableOutput.golden new file mode 100644 index 0000000..eaa81ee --- /dev/null +++ b/testdata/TestTableOutput.golden @@ -0,0 +1,80 @@ +[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ +│ tuikit-test │───────────────────────────────────────────────────────────────── +╰─────────────╯ +Name Status +────────────────────────────────────────────────────────────────────────────── +◌ Alpha Active +● Beta Inactive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +──────────────────────────────────────────────────────────────────────────────── +[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/themes/base.go b/themes/base.go index 65db285..61e30a2 100644 --- a/themes/base.go +++ b/themes/base.go @@ -6,13 +6,11 @@ import ( "strings" "text/template" - "github.com/charmbracelet/bubbles/v2/list" - "github.com/charmbracelet/bubbles/v2/spinner" - "github.com/charmbracelet/huh" - lipglossv1 "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/v2" - "github.com/charmbracelet/lipgloss/v2/compat" - "github.com/charmbracelet/log" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/spinner" + "charm.land/huh/v2" + "charm.land/lipgloss/v2" + "charm.land/log/v2" ) const ( @@ -27,6 +25,7 @@ type baseTheme struct { Name string `json:"-" yaml:"-"` SpinnerType spinner.Spinner `json:"-" yaml:"-"` Colors *ColorPalette + isDark bool } func (t baseTheme) String() string { @@ -194,23 +193,23 @@ func (t baseTheme) RenderInContainer(text string) string { } func (t baseTheme) ListStyles() list.Styles { - s := list.DefaultStyles(compat.HasDarkBackground) + s := list.DefaultStyles(t.isDark) s.StatusBar = s.StatusBar. Padding(0, 0, 1, 0). Italic(true). - Foreground(lipglossv1.Color(t.Colors.Tertiary)) + Foreground(lipgloss.Color(t.Colors.Tertiary)) return s } func (t baseTheme) ListItemStyles() list.DefaultItemStyles { - s := list.NewDefaultItemStyles(compat.HasDarkBackground) - s.NormalTitle = s.NormalTitle.Foreground(lipglossv1.Color(t.Colors.Secondary)) - s.NormalDesc = s.NormalDesc.Foreground(lipglossv1.Color(t.Colors.Body)) + s := list.NewDefaultItemStyles(t.isDark) + s.NormalTitle = s.NormalTitle.Foreground(lipgloss.Color(t.Colors.Secondary)) + s.NormalDesc = s.NormalDesc.Foreground(lipgloss.Color(t.Colors.Body)) s.SelectedTitle = s.SelectedTitle. Border(lipgloss.DoubleBorder(), false, false, false, true). - Foreground(lipglossv1.Color(t.Colors.Primary)). - BorderForeground(lipglossv1.Color(t.Colors.Primary)). + Foreground(lipgloss.Color(t.Colors.Primary)). + BorderForeground(lipgloss.Color(t.Colors.Primary)). Bold(true) s.SelectedDesc = s.SelectedDesc. Border(lipgloss.HiddenBorder(), false, false, false, true). @@ -220,18 +219,18 @@ func (t baseTheme) ListItemStyles() list.DefaultItemStyles { func (t baseTheme) LoggerStyles() *log.Styles { baseStyles := log.DefaultStyles() - baseStyles.Timestamp = baseStyles.Timestamp.Foreground(lipglossv1.Color(t.Colors.Gray)) - baseStyles.Levels = map[log.Level]lipglossv1.Style{ - log.InfoLevel: baseStyles.Levels[log.InfoLevel].Foreground(lipglossv1.Color(t.Colors.Info)).SetString("INF"), - LogNoticeLevel: baseStyles.Levels[LogNoticeLevel].Foreground(lipglossv1.Color(t.Colors.Warning)).SetString("NTC"), - log.WarnLevel: baseStyles.Levels[log.WarnLevel].Foreground(lipglossv1.Color(t.Colors.Warning)).SetString("WRN"), - log.ErrorLevel: baseStyles.Levels[log.ErrorLevel].Foreground(lipglossv1.Color(t.Colors.Error)).SetString("ERR"), - log.DebugLevel: baseStyles.Levels[log.DebugLevel].Foreground(lipglossv1.Color(t.Colors.Emphasis)).SetString("DBG"), - log.FatalLevel: baseStyles.Levels[log.FatalLevel].Foreground(lipglossv1.Color(t.Colors.Error)).SetString("ERR"), + baseStyles.Timestamp = baseStyles.Timestamp.Foreground(lipgloss.Color(t.Colors.Gray)) + baseStyles.Levels = map[log.Level]lipgloss.Style{ + log.InfoLevel: baseStyles.Levels[log.InfoLevel].Foreground(lipgloss.Color(t.Colors.Info)).SetString("INF"), + LogNoticeLevel: baseStyles.Levels[LogNoticeLevel].Foreground(lipgloss.Color(t.Colors.Warning)).SetString("NTC"), + log.WarnLevel: baseStyles.Levels[log.WarnLevel].Foreground(lipgloss.Color(t.Colors.Warning)).SetString("WRN"), + log.ErrorLevel: baseStyles.Levels[log.ErrorLevel].Foreground(lipgloss.Color(t.Colors.Error)).SetString("ERR"), + log.DebugLevel: baseStyles.Levels[log.DebugLevel].Foreground(lipgloss.Color(t.Colors.Emphasis)).SetString("DBG"), + log.FatalLevel: baseStyles.Levels[log.FatalLevel].Foreground(lipgloss.Color(t.Colors.Error)).SetString("ERR"), } - baseStyles.Message = baseStyles.Message.Foreground(lipglossv1.Color(t.Colors.Body)) - baseStyles.Key = baseStyles.Key.Foreground(lipglossv1.Color(t.Colors.Secondary)) - baseStyles.Value = baseStyles.Value.Foreground(lipglossv1.Color(t.Colors.Gray)) + baseStyles.Message = baseStyles.Message.Foreground(lipgloss.Color(t.Colors.Body)) + baseStyles.Key = baseStyles.Key.Foreground(lipgloss.Color(t.Colors.Secondary)) + baseStyles.Value = baseStyles.Value.Foreground(lipgloss.Color(t.Colors.Gray)) return baseStyles } @@ -252,52 +251,58 @@ func (t baseTheme) GlamourMarkdownStyleJSON() (string, error) { return builder.String(), nil } -func (t baseTheme) HuhTheme() *huh.Theme { - baseTheme := huh.ThemeBase() - baseTheme.FieldSeparator = lipglossv1.NewStyle().SetString("\n\n") +func (t baseTheme) HuhTheme() huh.Theme { + return huh.ThemeFunc(func(isDark bool) *huh.Styles { + return t.huhStyles(isDark) + }) +} + +func (t baseTheme) huhStyles(isDark bool) *huh.Styles { + baseTheme := huh.ThemeBase(isDark) + baseTheme.FieldSeparator = lipgloss.NewStyle().SetString("\n\n") - baseTheme.Focused.Base = baseTheme.Focused.Base.BorderStyle(lipglossv1.HiddenBorder()) - baseTheme.Focused.Title = baseTheme.Focused.Title.Foreground(lipglossv1.Color(t.Colors.Primary)).Bold(true) - baseTheme.Focused.Description = baseTheme.Focused.Description.Foreground(lipglossv1.Color(t.Colors.Body)) - baseTheme.Focused.ErrorMessage = baseTheme.Focused.ErrorMessage.Foreground(lipglossv1.Color(t.Colors.Error)) - baseTheme.Focused.FocusedButton = baseTheme.Focused.FocusedButton.Foreground(lipglossv1.Color(t.Colors.Primary)). - Background(lipglossv1.Color(t.Colors.Tertiary)) - baseTheme.Focused.BlurredButton = baseTheme.Focused.BlurredButton.Foreground(lipglossv1.Color(t.Colors.Secondary)). - Background(lipglossv1.Color(t.Colors.Gray)) + baseTheme.Focused.Base = baseTheme.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + baseTheme.Focused.Title = baseTheme.Focused.Title.Foreground(lipgloss.Color(t.Colors.Primary)).Bold(true) + baseTheme.Focused.Description = baseTheme.Focused.Description.Foreground(lipgloss.Color(t.Colors.Body)) + baseTheme.Focused.ErrorMessage = baseTheme.Focused.ErrorMessage.Foreground(lipgloss.Color(t.Colors.Error)) + baseTheme.Focused.FocusedButton = baseTheme.Focused.FocusedButton.Foreground(lipgloss.Color(t.Colors.Primary)). + Background(lipgloss.Color(t.Colors.Tertiary)) + baseTheme.Focused.BlurredButton = baseTheme.Focused.BlurredButton.Foreground(lipgloss.Color(t.Colors.Secondary)). + Background(lipgloss.Color(t.Colors.Gray)) baseTheme.Focused.TextInput.Placeholder = - baseTheme.Focused.TextInput.Placeholder.Foreground(lipglossv1.Color(t.Colors.Body)) + baseTheme.Focused.TextInput.Placeholder.Foreground(lipgloss.Color(t.Colors.Body)) baseTheme.Focused.TextInput.Cursor = - baseTheme.Focused.TextInput.Cursor.Foreground(lipglossv1.Color(t.Colors.Secondary)) + baseTheme.Focused.TextInput.Cursor.Foreground(lipgloss.Color(t.Colors.Secondary)) baseTheme.Focused.TextInput.CursorText = - baseTheme.Focused.TextInput.Cursor.Foreground(lipglossv1.Color(t.Colors.Secondary)) + baseTheme.Focused.TextInput.Cursor.Foreground(lipgloss.Color(t.Colors.Secondary)) baseTheme.Focused.TextInput.Placeholder = - baseTheme.Focused.TextInput.Placeholder.Foreground(lipglossv1.Color(t.Colors.Gray)) + baseTheme.Focused.TextInput.Placeholder.Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Focused.TextInput.Text = - baseTheme.Focused.TextInput.Text.Foreground(lipglossv1.Color(t.Colors.Body)) + baseTheme.Focused.TextInput.Text.Foreground(lipgloss.Color(t.Colors.Body)) baseTheme.Focused.TextInput.Prompt = - baseTheme.Focused.TextInput.Prompt.Foreground(lipglossv1.Color(t.Colors.Tertiary)) + baseTheme.Focused.TextInput.Prompt.Foreground(lipgloss.Color(t.Colors.Tertiary)) - baseTheme.Blurred.Title = baseTheme.Blurred.Title.Foreground(lipglossv1.Color(t.Colors.White)) - baseTheme.Blurred.Description = baseTheme.Blurred.Description.Foreground(lipglossv1.Color(t.Colors.Gray)) - baseTheme.Blurred.ErrorMessage = baseTheme.Blurred.ErrorMessage.Foreground(lipglossv1.Color(t.Colors.Emphasis)) - baseTheme.Blurred.FocusedButton = baseTheme.Blurred.FocusedButton.Foreground(lipglossv1.Color(t.Colors.Secondary)). - Background(lipglossv1.Color(t.Colors.Gray)) - baseTheme.Blurred.BlurredButton = baseTheme.Blurred.BlurredButton.Foreground(lipglossv1.Color(t.Colors.Gray)). - Background(lipglossv1.Color(t.Colors.Gray)) + baseTheme.Blurred.Title = baseTheme.Blurred.Title.Foreground(lipgloss.Color(t.Colors.White)) + baseTheme.Blurred.Description = baseTheme.Blurred.Description.Foreground(lipgloss.Color(t.Colors.Gray)) + baseTheme.Blurred.ErrorMessage = baseTheme.Blurred.ErrorMessage.Foreground(lipgloss.Color(t.Colors.Emphasis)) + baseTheme.Blurred.FocusedButton = baseTheme.Blurred.FocusedButton.Foreground(lipgloss.Color(t.Colors.Secondary)). + Background(lipgloss.Color(t.Colors.Gray)) + baseTheme.Blurred.BlurredButton = baseTheme.Blurred.BlurredButton.Foreground(lipgloss.Color(t.Colors.Gray)). + Background(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Placeholder = baseTheme.Blurred.TextInput.Placeholder. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Cursor = baseTheme.Blurred.TextInput.Cursor. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.CursorText = baseTheme.Blurred.TextInput.CursorText. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Placeholder = baseTheme.Blurred.TextInput.Placeholder. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Text = baseTheme.Blurred.TextInput.Text. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) baseTheme.Blurred.TextInput.Prompt = baseTheme.Blurred.TextInput.Prompt. - Foreground(lipglossv1.Color(t.Colors.Gray)) + Foreground(lipgloss.Color(t.Colors.Gray)) return baseTheme } diff --git a/themes/colors.go b/themes/colors.go index 43b9036..8c1cb7c 100644 --- a/themes/colors.go +++ b/themes/colors.go @@ -3,11 +3,12 @@ package themes import ( "encoding/json" "fmt" + "image/color" "os" "path/filepath" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "gopkg.in/yaml.v3" ) @@ -109,54 +110,54 @@ func WithDefaultColors(orig ColorPalette) ColorPalette { return cp } -func (cp ColorPalette) PrimaryColor() lipgloss.Color { +func (cp ColorPalette) PrimaryColor() color.Color { return lipgloss.Color(cp.Primary) } -func (cp ColorPalette) SecondaryColor() lipgloss.Color { +func (cp ColorPalette) SecondaryColor() color.Color { return lipgloss.Color(cp.Secondary) } -func (cp ColorPalette) TertiaryColor() lipgloss.Color { +func (cp ColorPalette) TertiaryColor() color.Color { return lipgloss.Color(cp.Tertiary) } -func (cp ColorPalette) SuccessColor() lipgloss.Color { +func (cp ColorPalette) SuccessColor() color.Color { return lipgloss.Color(cp.Success) } -func (cp ColorPalette) WarningColor() lipgloss.Color { +func (cp ColorPalette) WarningColor() color.Color { return lipgloss.Color(cp.Warning) } -func (cp ColorPalette) ErrorColor() lipgloss.Color { +func (cp ColorPalette) ErrorColor() color.Color { return lipgloss.Color(cp.Error) } -func (cp ColorPalette) InfoColor() lipgloss.Color { +func (cp ColorPalette) InfoColor() color.Color { return lipgloss.Color(cp.Info) } -func (cp ColorPalette) BodyColor() lipgloss.Color { +func (cp ColorPalette) BodyColor() color.Color { return lipgloss.Color(cp.Body) } -func (cp ColorPalette) EmphasisColor() lipgloss.Color { +func (cp ColorPalette) EmphasisColor() color.Color { return lipgloss.Color(cp.Emphasis) } -func (cp ColorPalette) BorderColor() lipgloss.Color { +func (cp ColorPalette) BorderColor() color.Color { return lipgloss.Color(cp.Border) } -func (cp ColorPalette) BlackColor() lipgloss.Color { +func (cp ColorPalette) BlackColor() color.Color { return lipgloss.Color(cp.Black) } -func (cp ColorPalette) WhiteColor() lipgloss.Color { +func (cp ColorPalette) WhiteColor() color.Color { return lipgloss.Color(cp.White) } -func (cp ColorPalette) GrayColor() lipgloss.Color { +func (cp ColorPalette) GrayColor() color.Color { return lipgloss.Color(cp.Gray) } diff --git a/themes/theme.go b/themes/theme.go index d9011ec..615a05b 100644 --- a/themes/theme.go +++ b/themes/theme.go @@ -1,11 +1,11 @@ package themes import ( - "github.com/charmbracelet/bubbles/v2/list" - "github.com/charmbracelet/bubbles/v2/spinner" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss/v2" - "github.com/charmbracelet/log" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/spinner" + "charm.land/huh/v2" + "charm.land/lipgloss/v2" + "charm.land/log/v2" ) type Theme interface { @@ -38,5 +38,5 @@ type Theme interface { LoggerStyles() *log.Styles GlamourMarkdownStyleJSON() (string, error) - HuhTheme() *huh.Theme + HuhTheme() huh.Theme } diff --git a/themes/themes.go b/themes/themes.go index 26a8680..233333f 100644 --- a/themes/themes.go +++ b/themes/themes.go @@ -2,8 +2,10 @@ package themes import ( _ "embed" + "os" - "github.com/charmbracelet/bubbles/v2/spinner" + "charm.land/bubbles/v2/spinner" + "charm.land/lipgloss/v2" ) const ( @@ -16,7 +18,12 @@ const ( func NewTheme(name string, cp ColorPalette) Theme { colors := WithDefaultColors(cp) - return baseTheme{Name: name, SpinnerType: spinner.Points, Colors: &colors} + return baseTheme{ + Name: name, + SpinnerType: spinner.Points, + Colors: &colors, + isDark: lipgloss.HasDarkBackground(os.Stdin, os.Stdout), + } } type ThemeFunc func() Theme diff --git a/themes/types.go b/themes/types.go index 1824887..93c8315 100644 --- a/themes/types.go +++ b/themes/types.go @@ -1,7 +1,7 @@ package themes import ( - "github.com/charmbracelet/log" + "charm.land/log/v2" ) type OutputLevel string diff --git a/types/msgs.go b/types/msgs.go index fa15b50..980d70a 100644 --- a/types/msgs.go +++ b/types/msgs.go @@ -3,7 +3,7 @@ package types import ( "time" - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index 787df05..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,75 +0,0 @@ -package utils - -import ( - tea "github.com/charmbracelet/bubbletea" - teaV2 "github.com/charmbracelet/bubbletea/v2" -) - -func CmdToV2Cmd(oldCmd tea.Cmd) (newCmd teaV2.Cmd) { - newCmd = func() teaV2.Msg { - if oldCmd != nil { - return oldCmd() - } - return nil - } - return newCmd -} - -func V2CmdToCmd(newCmd teaV2.Cmd) (oldCmd tea.Cmd) { - oldCmd = func() tea.Msg { - if newCmd != nil { - return newCmd() - } - return nil - } - return oldCmd -} - -func V2MsgToMsg(msg teaV2.Msg) (oldMsg tea.Msg) { - switch m := msg.(type) { - case teaV2.KeyMsg: - switch { - case m.Key().Text != "": - oldMsg = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(m.Key().Text)} - case m.Key().Code != 0: - switch m.Key().Code { - case teaV2.KeySpace: - oldMsg = tea.KeyMsg{Type: tea.KeySpace} - case teaV2.KeyBackspace: - oldMsg = tea.KeyMsg{Type: tea.KeyBackspace} - case teaV2.KeyEnter: - oldMsg = tea.KeyMsg{Type: tea.KeyEnter} - case teaV2.KeyTab: - oldMsg = tea.KeyMsg{Type: tea.KeyTab} - case teaV2.KeyDown: - oldMsg = tea.KeyMsg{Type: tea.KeyDown} - case teaV2.KeyUp: - oldMsg = tea.KeyMsg{Type: tea.KeyUp} - case teaV2.KeyLeft: - oldMsg = tea.KeyMsg{Type: tea.KeyLeft} - case teaV2.KeyRight: - oldMsg = tea.KeyMsg{Type: tea.KeyRight} - case teaV2.KeyPgUp: - oldMsg = tea.KeyMsg{Type: tea.KeyPgUp} - case teaV2.KeyPgDown: - oldMsg = tea.KeyMsg{Type: tea.KeyPgDown} - case teaV2.KeyHome: - oldMsg = tea.KeyMsg{Type: tea.KeyHome} - case teaV2.KeyEnd: - oldMsg = tea.KeyMsg{Type: tea.KeyEnd} - case teaV2.KeyEsc: - oldMsg = tea.KeyMsg{Type: tea.KeyEsc} - case teaV2.KeyDelete: - oldMsg = tea.KeyMsg{Type: tea.KeyDelete} - case teaV2.KeyInsert: - oldMsg = tea.KeyMsg{Type: tea.KeyInsert} - } - default: - // unsupported for now - panic("unsupported key type") - } - default: - oldMsg = msg - } - return oldMsg -} diff --git a/views/archive.go b/views/archive.go index a625fda..2b831a8 100644 --- a/views/archive.go +++ b/views/archive.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/v2/list" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss/v2" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/muesli/reflow/wordwrap" "github.com/flowexec/tuikit/io" @@ -86,7 +86,7 @@ func (v *LogArchiveView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { time.Sleep(time.Second) return v, tea.Quit } - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "x": if v.activeEntry != nil { @@ -147,7 +147,7 @@ func (v *LogArchiveView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, cmd } -func (v *LogArchiveView) View() string { +func (v *LogArchiveView) View() tea.View { if v.err != nil { return v.err.View() } @@ -172,7 +172,7 @@ func (v *LogArchiveView) View() string { style := v.styles.BoxStyle().Width(v.width) content = style.Render(v.model.View()) } - return content + return tea.View{Content: content} } func (v *LogArchiveView) HelpMsg() string { diff --git a/views/collection.go b/views/collection.go index ef678dd..de48c88 100644 --- a/views/collection.go +++ b/views/collection.go @@ -4,9 +4,9 @@ import ( "fmt" "sort" - "github.com/charmbracelet/bubbles/v2/list" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/jahvon/glamour" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "charm.land/glamour/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -90,7 +90,7 @@ func (v *CollectionView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v.width = msg.ContentWidth v.height = msg.ContentHeight v.model.SetSize(v.width, v.height) - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "-", "l": if v.format == types.CollectionFormatList { @@ -152,7 +152,7 @@ func (v *CollectionView) Items() []list.Item { return v.model.Items() } -func (v *CollectionView) renderedContent() string { +func (v *CollectionView) renderedView() tea.View { var content string var isMkdwn bool var err error @@ -184,7 +184,7 @@ func (v *CollectionView) renderedContent() string { } if !isMkdwn { - return content + return tea.View{Content: content} } mdStyles, err := v.styles.GlamourMarkdownStyleJSON() @@ -206,15 +206,15 @@ func (v *CollectionView) renderedContent() string { v.err = NewErrorView(err, v.styles) return v.err.View() } - return viewStr + return tea.View{Content: viewStr} } -func (v *CollectionView) View() string { +func (v *CollectionView) View() tea.View { if v.err != nil { return v.err.View() } - return v.renderedContent() + return v.renderedView() } func (v *CollectionView) HelpMsg() string { diff --git a/views/detail.go b/views/detail.go index 659c14b..d5055e1 100644 --- a/views/detail.go +++ b/views/detail.go @@ -3,9 +3,9 @@ package views import ( "strings" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -59,8 +59,8 @@ func (v *DetailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v.height = msg.ContentHeight v.theme = msg.Theme v.syncViewport() - case tea.KeyMsg: - halfPage := max(v.viewport.Height/2, 1) + case tea.KeyPressMsg: + halfPage := max(v.viewport.Height()/2, 1) switch msg.String() { case "k": v.viewport.ScrollUp(1) @@ -82,7 +82,7 @@ func (v *DetailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, cmd } -func (v *DetailView) View() string { +func (v *DetailView) View() tea.View { metaStr := v.renderMetadata() v.viewport.SetContent(v.body) @@ -92,7 +92,7 @@ func (v *DetailView) View() string { } sections = append(sections, v.renderBodyBox()) - return lipgloss.JoinVertical(lipgloss.Left, sections...) + return tea.View{Content: lipgloss.JoinVertical(lipgloss.Left, sections...)} } func (v *DetailView) HelpMsg() string { @@ -122,8 +122,8 @@ func (v *DetailView) syncViewport() { bodyChrome := 4 vpHeight := max(v.height-v.metadataHeight-bodyChrome, 1) - v.viewport.Width = v.width - 8 // account for border + padding - v.viewport.Height = vpHeight + v.viewport.SetHeight(vpHeight) + v.viewport.SetWidth(v.width - 8) // account for border + padding } func (v *DetailView) calcMetadataHeight() int { diff --git a/views/entity.go b/views/entity.go index 57b8134..3501499 100644 --- a/views/entity.go +++ b/views/entity.go @@ -4,9 +4,9 @@ import ( "fmt" "math" - "github.com/charmbracelet/bubbles/v2/viewport" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/jahvon/glamour" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/glamour/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -71,8 +71,8 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case types.RenderState: v.viewport.SetWidth(msg.ContentWidth) v.viewport.SetHeight(msg.ContentHeight) - v.viewport.SetContent(v.renderedContent()) - case tea.KeyMsg: + v.viewport.SetContent(v.renderedView().Content) + case tea.KeyPressMsg: switch msg.String() { case "-", "d": if v.format == types.EntityFormatDocument { @@ -93,9 +93,9 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v.format = types.CollectionFormatJSON v.viewport.GotoTop() case "up": - v.viewport.LineUp(1) + v.viewport.ScrollUp(1) case "down": - v.viewport.LineDown(1) + v.viewport.ScrollDown(1) default: for _, cb := range v.callbacks { if cb.Key == msg.String() { @@ -110,7 +110,7 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, cmd } -func (v *EntityView) renderedContent() string { +func (v *EntityView) renderedView() tea.View { var content string var err error switch v.format { @@ -155,15 +155,15 @@ func (v *EntityView) renderedContent() string { v.err = NewErrorView(err, v.styles) return v.err.View() } - return viewStr + return tea.View{Content: viewStr} } -func (v *EntityView) View() string { +func (v *EntityView) View() tea.View { if v.err != nil { return v.err.View() } - v.viewport.SetContent(v.renderedContent()) - return v.viewport.View() + v.viewport.SetContent(v.renderedView().Content) + return tea.View{Content: v.viewport.View()} } func (v *EntityView) HelpMsg() string { diff --git a/views/errors.go b/views/errors.go index aac9a15..28140af 100644 --- a/views/errors.go +++ b/views/errors.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -37,8 +37,8 @@ func (v *ErrorView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, nil } -func (v *ErrorView) View() string { - return v.theme.RenderError(errorString(v.err)) +func (v *ErrorView) View() tea.View { + return tea.View{Content: v.theme.RenderError(errorString(v.err))} } func (v *ErrorView) HelpMsg() string { diff --git a/views/form.go b/views/form.go index 26e52c5..fd13846 100644 --- a/views/form.go +++ b/views/form.go @@ -12,14 +12,12 @@ import ( "strings" "sync" - tea "github.com/charmbracelet/bubbletea" - teaV2 "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/huh" + tea "charm.land/bubbletea/v2" + "charm.land/huh/v2" "golang.org/x/term" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" - "github.com/flowexec/tuikit/utils" ) type FormFieldType uint @@ -195,7 +193,7 @@ func NewFormView( WithWidth(state.ContentWidth). WithHeight(state.ContentHeight). WithShowHelp(true) - hf.SubmitCmd = utils.V2CmdToCmd(types.Submit) + hf.SubmitCmd = types.Submit hf.CancelCmd = tea.Quit if len(fields) > 5 { hf = hf.WithLayout(huh.LayoutColumns(2)) // TODO: make this configurable or auto-dynamic @@ -231,11 +229,11 @@ func (f *Form) Completed() bool { return f.completed } -func (f *Form) Init() teaV2.Cmd { - return utils.CmdToV2Cmd(f.form.Init()) +func (f *Form) Init() tea.Cmd { + return f.form.Init() } -func (f *Form) Update(msg teaV2.Msg) (teaV2.Model, teaV2.Cmd) { +func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if f.err != nil { return f.err.Update(msg) } @@ -253,17 +251,17 @@ func (f *Form) Update(msg teaV2.Msg) (teaV2.Model, teaV2.Cmd) { return f, types.ReplaceView } - model, cmd := f.form.Update(utils.V2MsgToMsg(msg)) + model, cmd := f.form.Update(msg) var ok bool f.form, ok = model.(*huh.Form) if !ok { f.err = NewErrorView(fmt.Errorf("unable to cast form model to huh.Form"), f.theme) - return f, utils.CmdToV2Cmd(cmd) + return f, cmd } - return f, utils.CmdToV2Cmd(cmd) + return f, cmd } -func (f *Form) View() string { +func (f *Form) View() tea.View { f.mu.RLock() defer f.mu.RUnlock() @@ -271,7 +269,7 @@ func (f *Form) View() string { return f.err.View() } - return f.form.View() + return tea.View{Content: f.form.View()} } func (f *Form) HelpMsg() string { diff --git a/views/frame.go b/views/frame.go index 15864c7..a6be28c 100644 --- a/views/frame.go +++ b/views/frame.go @@ -1,14 +1,13 @@ package views import ( - tea "github.com/charmbracelet/bubbletea/v2" + tea "charm.land/bubbletea/v2" ) const FrameViewType = "frame" type FramedModel interface { tea.Model - tea.ViewModel } type FrameView struct { @@ -23,7 +22,7 @@ func (v *FrameView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v.model.Update(msg) } -func (v *FrameView) View() string { +func (v *FrameView) View() tea.View { return v.model.View() } diff --git a/views/loading.go b/views/loading.go index 02b54ef..a91081f 100644 --- a/views/loading.go +++ b/views/loading.go @@ -3,8 +3,8 @@ package views import ( "fmt" - "github.com/charmbracelet/bubbles/v2/spinner" - tea "github.com/charmbracelet/bubbletea/v2" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) @@ -36,14 +36,14 @@ func (v *LoadingView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, cmd } -func (v *LoadingView) View() string { +func (v *LoadingView) View() tea.View { var txt string if v.msg == "" { txt = fmt.Sprintf("\n\n %s %s\n\n", v.spinner.View(), v.theme.RenderInfo(DefaultLoading)) } else { txt = fmt.Sprintf("\n\n %s %s\n\n", v.spinner.View(), v.theme.RenderInfo(v.msg)) } - return txt + return tea.View{Content: txt} } func (v *LoadingView) HelpMsg() string { diff --git a/views/markdown.go b/views/markdown.go index 87775be..66a05f8 100644 --- a/views/markdown.go +++ b/views/markdown.go @@ -4,9 +4,9 @@ import ( "math" "sync" - "github.com/charmbracelet/bubbles/v2/viewport" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/jahvon/glamour" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/glamour/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -50,12 +50,12 @@ func (v *MarkdownView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v.height = msg.ContentHeight v.viewport.SetWidth(v.width) v.viewport.SetHeight(v.height) - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "up": - v.viewport.LineUp(1) + v.viewport.ScrollUp(1) case "down": - v.viewport.LineDown(1) + v.viewport.ScrollDown(1) } } var cmd tea.Cmd @@ -63,7 +63,7 @@ func (v *MarkdownView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, cmd } -func (v *MarkdownView) View() string { +func (v *MarkdownView) View() tea.View { v.mu.Lock() defer v.mu.Unlock() @@ -91,7 +91,7 @@ func (v *MarkdownView) View() string { return v.err.View() } v.viewport.SetContent(viewStr) - return v.viewport.View() + return tea.View{Content: v.viewport.View()} } func (v *MarkdownView) HelpMsg() string { diff --git a/views/table.go b/views/table.go index e53c39d..d10c445 100644 --- a/views/table.go +++ b/views/table.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/flowexec/tuikit/types" ) @@ -78,13 +78,13 @@ func (t *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case *types.RenderState: t.render = msg - case tea.KeyMsg: + case tea.KeyPressMsg: return t, t.handleKeyMsg(msg) } return t, nil } -func (t *Table) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { +func (t *Table) handleKeyMsg(msg tea.KeyPressMsg) tea.Cmd { switch msg.String() { case "up", "k": t.moveCursor(-1) @@ -92,7 +92,7 @@ func (t *Table) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { t.moveCursor(1) case "enter": return t.selectRow() - case " ", "tab": + case "space", "tab": t.toggleExpansion() t.buildVisibleRows() t.ensureSelectedVisible() @@ -125,9 +125,9 @@ func (t *Table) selectRow() tea.Cmd { } } -func (t *Table) View() string { +func (t *Table) View() tea.View { if t.render == nil || len(t.visibleRows) == 0 { - return "No data" + return tea.View{Content: "No data"} } tableWidth := t.calculateTableWidth() @@ -179,7 +179,7 @@ func (t *Table) View() string { Width(t.render.ContentWidth). Height(t.render.ContentHeight). Render(result) - return rendered + return tea.View{Content: rendered} } func (t *Table) HelpMsg() string { From 3fb8040e4e3c1b31c3ba991797fc9db184990251 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 31 Mar 2026 22:26:04 -0400 Subject: [PATCH 3/5] fies --- .github/workflows/ci.yaml | 8 ++++---- execs.flow | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index afa9ccf..2455ee6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - uses: flowexec/action@v1 with: executable: 'lint --param CI=true' @@ -40,7 +40,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - uses: flowexec/action@v1 with: executable: 'test tui --param CI=true' @@ -61,7 +61,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - name: Install mockgen run: go install go.uber.org/mock/mockgen@v0.4.0 - uses: flowexec/action@v1 @@ -83,7 +83,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - uses: flowexec/action@v1 with: executable: 'scan security' diff --git a/execs.flow b/execs.flow index c4de466..10c8e7d 100644 --- a/execs.flow +++ b/execs.flow @@ -133,10 +133,10 @@ executables: - cmd: | if ! command -v golangci-lint &> /dev/null; then echo "Installing golangci-lint..." - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s v2.1.6 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s v2.11.4 export PATH="$PATH:./bin" fi - + if [ "$CI" = "true" ]; then echo "Running golangci-lint with sarif output..." golangci-lint run ./... --fix --output.sarif.path lint.sarif @@ -155,7 +155,7 @@ executables: echo "Installing govulncheck..." go install golang.org/x/vuln/cmd/govulncheck@latest fi - + if [ "$CI" = "true" ]; then govulncheck -format sarif ./... > govuln.sarif echo "Security scan completed. Results saved to govuln.sarif" From 6a3b5fbfe265432f87dec1691a1675d69212eaca Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 31 Mar 2026 22:31:49 -0400 Subject: [PATCH 4/5] races --- views/form.go | 5 +++++ views/loading.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/views/form.go b/views/form.go index fd13846..e9b517d 100644 --- a/views/form.go +++ b/views/form.go @@ -230,10 +230,15 @@ func (f *Form) Completed() bool { } func (f *Form) Init() tea.Cmd { + f.mu.Lock() + defer f.mu.Unlock() return f.form.Init() } func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + f.mu.Lock() + defer f.mu.Unlock() + if f.err != nil { return f.err.Update(msg) } diff --git a/views/loading.go b/views/loading.go index a91081f..315b72d 100644 --- a/views/loading.go +++ b/views/loading.go @@ -2,6 +2,7 @@ package views import ( "fmt" + "sync" "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" @@ -18,13 +19,18 @@ type LoadingView struct { theme themes.Theme msg string spinner spinner.Model + mu sync.RWMutex } func (v *LoadingView) Init() tea.Cmd { + v.mu.RLock() + defer v.mu.RUnlock() return v.spinner.Tick } func (v *LoadingView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + v.mu.Lock() + defer v.mu.Unlock() var cmd tea.Cmd switch msg := msg.(type) { case error: @@ -37,6 +43,8 @@ func (v *LoadingView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (v *LoadingView) View() tea.View { + v.mu.RLock() + defer v.mu.RUnlock() var txt string if v.msg == "" { txt = fmt.Sprintf("\n\n %s %s\n\n", v.spinner.View(), v.theme.RenderInfo(DefaultLoading)) From 876f25f77412b641124fb949c8ce08f2e12410b6 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 31 Mar 2026 22:47:07 -0400 Subject: [PATCH 5/5] simplify tests --- container_test.go | 350 ++++++++++++--------------- testdata/TestCollectionOutput.golden | 80 ------ testdata/TestDetailOutput.golden | 80 ------ testdata/TestEntityOutput.golden | 80 ------ testdata/TestErrorOutput.golden | 10 - testdata/TestFormOutput.golden | 50 ---- testdata/TestFrameOutput.golden | 1 - testdata/TestLoadingOutput.golden | 9 - testdata/TestMarkdownOutput.golden | 80 ------ testdata/TestTableMiniOutput.golden | 80 ------ testdata/TestTableOutput.golden | 80 ------ themes/base.go | 4 +- types/msgs.go | 7 + views/archive.go | 2 +- views/collection.go | 2 +- views/entity.go | 4 +- views/markdown.go | 4 +- views/table.go | 6 +- 18 files changed, 174 insertions(+), 755 deletions(-) delete mode 100644 testdata/TestCollectionOutput.golden delete mode 100644 testdata/TestDetailOutput.golden delete mode 100644 testdata/TestEntityOutput.golden delete mode 100644 testdata/TestErrorOutput.golden delete mode 100644 testdata/TestFormOutput.golden delete mode 100644 testdata/TestFrameOutput.golden delete mode 100644 testdata/TestLoadingOutput.golden delete mode 100644 testdata/TestMarkdownOutput.golden delete mode 100644 testdata/TestTableMiniOutput.golden delete mode 100644 testdata/TestTableOutput.golden diff --git a/container_test.go b/container_test.go index 91f42b1..e4cf6aa 100644 --- a/container_test.go +++ b/container_test.go @@ -2,10 +2,9 @@ package tuikit_test import ( "bytes" - "context" "errors" - "io" "os" + "strings" "testing" "time" @@ -16,210 +15,128 @@ import ( "github.com/flowexec/tuikit" sampleTypes "github.com/flowexec/tuikit/sample/types" + "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" "github.com/flowexec/tuikit/views" ) func TestMain(m *testing.M) { - // Force ASCII color profile so lipgloss strips all color/style escape - // sequences, producing identical output regardless of the terminal - // capabilities of the host (local dev vs CI). os.Setenv("NO_COLOR", "1") os.Setenv("TERM", "dumb") lipgloss.Writer.Profile = colorprofile.Ascii os.Exit(m.Run()) } -func TestFrameOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - view := views.NewFrameView(&sampleTypes.Echo{Content: "Hello, world!"}) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) +// testRenderState returns a RenderState suitable for unit-testing views +// outside of a full bubbletea program. +func testRenderState() *types.RenderState { + return &types.RenderState{ + Width: 80, + Height: 40, + ContentWidth: 80, + ContentHeight: 35, + Theme: themes.EverforestTheme(), } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) - } - teatest.RequireEqualOutput(t, out) } -func TestLoadingOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - view := views.NewLoadingView("thinking...", container.RenderState().Theme) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) - } +// --- View unit tests --- +// These test View().Content directly, avoiding bubbletea's renderer +// and the non-deterministic terminal escape sequences it produces. - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(1*time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) +func TestFrameView(t *testing.T) { + inner := &sampleTypes.Echo{Content: "Hello, world!"} + view := views.NewFrameView(inner) + content := view.View().Content + if content != "Hello, world!" { + t.Errorf("expected 'Hello, world!', got %q", content) } - teatest.RequireEqualOutput(t, out) } -func TestErrorOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) +func TestLoadingView(t *testing.T) { + state := testRenderState() + view := views.NewLoadingView("thinking...", state.Theme) + content := view.View().Content + if !strings.Contains(content, "thinking...") { + t.Errorf("expected loading message, got %q", content) } +} - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - view := views.NewErrorView(errors.New("something went wrong - please try again"), container.RenderState().Theme) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) +func TestErrorView(t *testing.T) { + state := testRenderState() + view := views.NewErrorView(errors.New("something went wrong"), state.Theme) + content := view.View().Content + if !strings.Contains(content, "something went wrong") { + t.Errorf("expected error message, got %q", content) } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "encountered error") { + t.Errorf("expected error header, got %q", content) } - teatest.RequireEqualOutput(t, out) } -func TestMarkdownOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - md := "# Hello!\n\n I am a **Markdown** document." - view := views.NewMarkdownView(container.RenderState(), md) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) +func TestMarkdownView(t *testing.T) { + state := testRenderState() + md := "# Hello!\n\nI am a **Markdown** document." + view := views.NewMarkdownView(state, md) + content := view.View().Content + if !strings.Contains(content, "Hello") { + t.Errorf("expected heading, got %q", content) } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "Markdown") { + t.Errorf("expected bold text, got %q", content) } - teatest.RequireEqualOutput(t, out) } -func TestEntityOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) +func TestEntityView(t *testing.T) { + state := testRenderState() e := &sampleTypes.Thing{Name: "Green", Type: "Color"} - view := views.NewEntityView(container.RenderState(), e, types.EntityFormatDocument) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + view := views.NewEntityView(state, e, types.EntityFormatDocument) + content := view.View().Content + if !strings.Contains(content, "Green") { + t.Errorf("expected entity name, got %q", content) } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "Color") { + t.Errorf("expected entity type, got %q", content) } - teatest.RequireEqualOutput(t, out) } -func TestCollectionOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) +func TestCollectionView(t *testing.T) { + state := testRenderState() collection := sampleTypes.NewThingList("Color", &types.EntityInfo{ID: "red", Header: "Red", SubHeader: "Primary Color"}, &types.EntityInfo{ID: "green", Header: "Green", SubHeader: "Secondary Color"}, - &types.EntityInfo{ID: "blue-violet", Header: "Blue Violet", SubHeader: "Tertiary Color"}, ) - view := views.NewCollectionView(container.RenderState(), collection, types.CollectionFormatList, nil) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + view := views.NewCollectionView(state, collection, types.CollectionFormatList, nil) + content := view.View().Content + if !strings.Contains(content, "Red") { + t.Errorf("expected 'Red' in collection, got %q", content) } - - container.Send(tea.Quit(), 10*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "Green") { + t.Errorf("expected 'Green' in collection, got %q", content) } - teatest.RequireEqualOutput(t, out) } -func TestDetailOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) - } - - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) +func TestDetailView(t *testing.T) { + state := testRenderState() view := views.NewDetailView( - container.RenderState(), - "This is the body content of the detail view.", + state, + "Body content here.", views.DetailField{Key: "Name", Value: "Test Item"}, views.DetailField{Key: "Status", Value: "Active"}, ) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + content := view.View().Content + if !strings.Contains(content, "Name") { + t.Errorf("expected metadata key, got %q", content) } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "Test Item") { + t.Errorf("expected metadata value, got %q", content) } - teatest.RequireEqualOutput(t, out) -} - -func TestTableOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) + if !strings.Contains(content, "Body content here.") { + t.Errorf("expected body content, got %q", content) } +} - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) +func TestTableView(t *testing.T) { + state := testRenderState() columns := []views.TableColumn{ {Title: "Name", Percentage: 50}, {Title: "Status", Percentage: 50}, @@ -230,30 +147,21 @@ func TestTableOutput(t *testing.T) { {Data: []string{"Beta-1", "Running"}}, }}, } - view := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayFull) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + view := views.NewTable(state, columns, rows, views.TableDisplayFull) + content := view.View().Content + if !strings.Contains(content, "Name") { + t.Errorf("expected column header, got %q", content) } - - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) + if !strings.Contains(content, "Alpha") { + t.Errorf("expected row data, got %q", content) } - teatest.RequireEqualOutput(t, out) -} - -func TestTableMiniOutput(t *testing.T) { - ctx := context.Background() - app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) - if err != nil { - t.Errorf("Failed to create container: %v", err) + if !strings.Contains(content, "Beta") { + t.Errorf("expected row data, got %q", content) } +} - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) +func TestTableMiniView(t *testing.T) { + state := testRenderState() columns := []views.TableColumn{ {Title: "Command", Percentage: 100}, } @@ -262,29 +170,85 @@ func TestTableMiniOutput(t *testing.T) { {Data: []string{"test"}}, {Data: []string{"deploy"}}, } - view := views.NewTable(container.RenderState(), columns, rows, views.TableDisplayMini) - if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + view := views.NewTable(state, columns, rows, views.TableDisplayMini) + content := view.View().Content + if !strings.Contains(content, "build") { + t.Errorf("expected 'build' row, got %q", content) + } + if !strings.Contains(content, "deploy") { + t.Errorf("expected 'deploy' row, got %q", content) } +} - container.Send(tea.Quit(), 100*time.Millisecond) - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) - out, err := io.ReadAll(tm.FinalOutput(t)) - if err != nil { - t.Error(err) +func TestTableKeyNavigation(t *testing.T) { + state := testRenderState() + columns := []views.TableColumn{{Title: "Item", Percentage: 100}} + rows := []views.TableRow{ + {Data: []string{"First"}}, + {Data: []string{"Second"}}, + } + table := views.NewTable(state, columns, rows, views.TableDisplayFull) + + selected := table.GetSelectedRow() + if selected == nil || selected.Data()[0] != "First" { + t.Fatal("expected initial selection on first row") + } + + table.Update(tea.KeyPressMsg{Text: "j"}) + selected = table.GetSelectedRow() + if selected == nil || selected.Data()[0] != "Second" { + t.Errorf("expected selection to move down, got %v", selected) + } + + table.Update(tea.KeyPressMsg{Text: "k"}) + selected = table.GetSelectedRow() + if selected == nil || selected.Data()[0] != "First" { + t.Errorf("expected selection to move up, got %v", selected) } - teatest.RequireEqualOutput(t, out) } -func TestFormOutput(t *testing.T) { - ctx := context.Background() +func TestTableExpansion(t *testing.T) { + state := testRenderState() + columns := []views.TableColumn{{Title: "Item", Percentage: 100}} + rows := []views.TableRow{ + {Data: []string{"Parent"}, Children: []views.TableRow{ + {Data: []string{"Child"}}, + }}, + } + table := views.NewTable(state, columns, rows, views.TableDisplayFull) + + // Children should not be visible initially + content := table.View().Content + if strings.Contains(content, "Child") { + t.Error("children should be collapsed initially") + } + + // Toggle expansion with space + table.Update(tea.KeyPressMsg{Code: tea.KeySpace}) + content = table.View().Content + if !strings.Contains(content, "Child") { + t.Error("children should be visible after expansion") + } +} + +// --- Integration test --- +// The form test needs the full bubbletea lifecycle to verify +// interactive input handling and view transitions. + +func TestFormInteraction(t *testing.T) { app := &tuikit.Application{Name: "tuikit-test"} - container, err := tuikit.NewContainer(ctx, app) + container, err := tuikit.NewContainer(t.Context(), app) if err != nil { - t.Errorf("Failed to create container: %v", err) + t.Fatal(err) } - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) + tm := teatest.NewTestModel(t, container, + teatest.WithInitialTermSize(80, 80), + teatest.WithProgramOptions( + tea.WithColorProfile(colorprofile.Ascii), + tea.WithEnvironment([]string{"NO_COLOR=1", "TERM=dumb"}), + ), + ) container.SetSendFunc(tm.Send) view, err := views.NewFormView( container.RenderState(), @@ -296,7 +260,7 @@ func TestFormOutput(t *testing.T) { }, ) if err != nil { - t.Errorf("Failed to create form view: %v", err) + t.Fatal(err) } view.Callback = func(v map[string]any) error { inner := &sampleTypes.Echo{Content: "Thank you for confirming!"} @@ -305,7 +269,7 @@ func TestFormOutput(t *testing.T) { return nil } if err := container.SetView(view); err != nil { - t.Errorf("Failed to set view: %v", err) + t.Fatal(err) } teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { diff --git a/testdata/TestCollectionOutput.golden b/testdata/TestCollectionOutput.golden deleted file mode 100644 index 982354d..0000000 --- a/testdata/TestCollectionOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - 3 Colors - -║ Blue Violet (Tertiary Color) -Green (Secondary Color) -Red (Primary Color) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2;[?2026$p[?2027$p \ No newline at end of file diff --git a/testdata/TestDetailOutput.golden b/testdata/TestDetailOutput.golden deleted file mode 100644 index 7001356..0000000 --- a/testdata/TestDetailOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -╭──────────────────────────────────────────────────────────╮ -│ Name │ Test Item │ -│ Status │ Active │ -╰──────────────────────────────────────────────────────────╯ - -╭──────────────────────────────────────────────────────────────────────────╮ -│ │ -│ This is the body content of the detail view. │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯ -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestEntityOutput.golden b/testdata/TestEntityOutput.golden deleted file mode 100644 index 2dab911..0000000 --- a/testdata/TestEntityOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - Green - - I am a(n) Color. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestErrorOutput.golden b/testdata/TestErrorOutput.golden deleted file mode 100644 index a13c7db..0000000 --- a/testdata/TestErrorOutput.golden +++ /dev/null @@ -1,10 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -!! encountered error !! - -something went wrong -please try again - -──────────────────────────────────────────────────────────────────────────────── -[>4m[=0;1u[?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestFormOutput.golden b/testdata/TestFormOutput.golden deleted file mode 100644 index 4ea0d8a..0000000 --- a/testdata/TestFormOutput.golden +++ /dev/null @@ -1,50 +0,0 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - - ●∙∙ loading... - - - ╭─────────────╮ - ∙●∙ loading... -╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - Are you sure? - - Yes No - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -←/→ toggle • enter submit • y Yes • n No - Thank you for confirming! [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file diff --git a/testdata/TestFrameOutput.golden b/testdata/TestFrameOutput.golden deleted file mode 100644 index 7bf58be..0000000 --- a/testdata/TestFrameOutput.golden +++ /dev/null @@ -1 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h[>4;2m[=1;1u[?u Hello, world![>4m[=0;1u [?25h[?2004l \ No newline at end of file diff --git a/testdata/TestLoadingOutput.golden b/testdata/TestLoadingOutput.golden deleted file mode 100644 index 720ec92..0000000 --- a/testdata/TestLoadingOutput.golden +++ /dev/null @@ -1,9 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - - ●∙∙ thinking... - - -[>4m[=0;1u[?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestMarkdownOutput.golden b/testdata/TestMarkdownOutput.golden deleted file mode 100644 index ada68ac..0000000 --- a/testdata/TestMarkdownOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - Hello! - -I am a Markdown document. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ ↑/↓: navigate ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestTableMiniOutput.golden b/testdata/TestTableMiniOutput.golden deleted file mode 100644 index 053c1a9..0000000 --- a/testdata/TestTableMiniOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - -╭─────────────────────────────────────────────────────╮ -│ │ -│ Command │ -│ ─────────────────────────────────────────────────── │ -│ ◌ build │ -│ ◌ test │ -│ ◌ deploy │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -╰─────────────────────────────────────────────────────╯ - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/testdata/TestTableOutput.golden b/testdata/TestTableOutput.golden deleted file mode 100644 index eaa81ee..0000000 --- a/testdata/TestTableOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?2026$p[?2027$p[?25l[?2004h]2;tuikit-test[>4;2m[=1;1u[?u ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ -Name Status -────────────────────────────────────────────────────────────────────────────── -◌ Alpha Active -● Beta Inactive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -──────────────────────────────────────────────────────────────────────────────── -[ q: quit ] [ h: show help ][>4m[=0;1u [?25h[?2004l]2; \ No newline at end of file diff --git a/themes/base.go b/themes/base.go index 61e30a2..d422df3 100644 --- a/themes/base.go +++ b/themes/base.go @@ -252,9 +252,7 @@ func (t baseTheme) GlamourMarkdownStyleJSON() (string, error) { } func (t baseTheme) HuhTheme() huh.Theme { - return huh.ThemeFunc(func(isDark bool) *huh.Styles { - return t.huhStyles(isDark) - }) + return huh.ThemeFunc(t.huhStyles) } func (t baseTheme) huhStyles(isDark bool) *huh.Styles { diff --git a/types/msgs.go b/types/msgs.go index 980d70a..04837c1 100644 --- a/types/msgs.go +++ b/types/msgs.go @@ -10,6 +10,13 @@ import ( var TickTime = time.Millisecond * 250 +// Key name strings returned by KeyPressMsg.String(). +const ( + KeyUp = "up" + KeyDown = "down" + KeyEnter = "enter" +) + type TickMsg time.Time type SubmitMsg struct{} type ReplaceViewMsg struct{} diff --git a/views/archive.go b/views/archive.go index 2b831a8..5f2fd45 100644 --- a/views/archive.go +++ b/views/archive.go @@ -125,7 +125,7 @@ func (v *LogArchiveView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } v.model.SetItems(v.items) - case "enter": + case types.KeyEnter: if v.activeEntry != nil { return v, nil } diff --git a/views/collection.go b/views/collection.go index de48c88..de21372 100644 --- a/views/collection.go +++ b/views/collection.go @@ -107,7 +107,7 @@ func (v *CollectionView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return v, nil } v.format = types.CollectionFormatJSON - case "enter": + case types.KeyEnter: if v.selectedFunc == nil { return v, nil } diff --git a/views/entity.go b/views/entity.go index 3501499..2e92fed 100644 --- a/views/entity.go +++ b/views/entity.go @@ -92,9 +92,9 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } v.format = types.CollectionFormatJSON v.viewport.GotoTop() - case "up": + case types.KeyUp: v.viewport.ScrollUp(1) - case "down": + case types.KeyDown: v.viewport.ScrollDown(1) default: for _, cb := range v.callbacks { diff --git a/views/markdown.go b/views/markdown.go index 66a05f8..717df55 100644 --- a/views/markdown.go +++ b/views/markdown.go @@ -52,9 +52,9 @@ func (v *MarkdownView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v.viewport.SetHeight(v.height) case tea.KeyPressMsg: switch msg.String() { - case "up": + case types.KeyUp: v.viewport.ScrollUp(1) - case "down": + case types.KeyDown: v.viewport.ScrollDown(1) } } diff --git a/views/table.go b/views/table.go index d10c445..0bb2070 100644 --- a/views/table.go +++ b/views/table.go @@ -86,11 +86,11 @@ func (t *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (t *Table) handleKeyMsg(msg tea.KeyPressMsg) tea.Cmd { switch msg.String() { - case "up", "k": + case types.KeyUp, "k": t.moveCursor(-1) - case "down", "j": + case types.KeyDown, "j": t.moveCursor(1) - case "enter": + case types.KeyEnter: return t.selectRow() case "space", "tab": t.toggleExpansion()