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/.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/container.go b/container.go index 99220f3..05dd12e 100644 --- a/container.go +++ b/container.go @@ -9,8 +9,8 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/types" @@ -139,7 +139,6 @@ func (c *Container) Init() tea.Cmd { cmd := c.CurrentView().Init() cmds = append( cmds, - tea.SetWindowTitle(c.app.Name), c.doTick(), cmd, ) @@ -183,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) @@ -227,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: @@ -266,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 d2d81f2..e4cf6aa 100644 --- a/container_test.go +++ b/container_test.go @@ -2,187 +2,253 @@ package tuikit_test import ( "bytes" - "context" "errors" - "io" + "os" + "strings" "testing" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/exp/teatest" - "github.com/muesli/termenv" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/x/exp/teatest/v2" "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 init() { - lipgloss.SetColorProfile(termenv.Ascii) +func TestMain(m *testing.M) { + 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) +// 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(), } +} - 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) - } +// --- 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(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 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) +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.NewLoadingView("thinking...", 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(1*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 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 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) + } + if !strings.Contains(content, "Markdown") { + t.Errorf("expected bold text, 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 TestEntityView(t *testing.T) { + state := testRenderState() + e := &sampleTypes.Thing{Name: "Green", Type: "Color"} + view := views.NewEntityView(state, e, types.EntityFormatDocument) + content := view.View().Content + if !strings.Contains(content, "Green") { + t.Errorf("expected entity name, got %q", content) } + if !strings.Contains(content, "Color") { + t.Errorf("expected entity type, 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 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"}, + ) + 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) + } + if !strings.Contains(content, "Green") { + t.Errorf("expected 'Green' in collection, 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) +func TestDetailView(t *testing.T) { + state := testRenderState() + view := views.NewDetailView( + state, + "Body content here.", + views.DetailField{Key: "Name", Value: "Test Item"}, + views.DetailField{Key: "Status", Value: "Active"}, + ) + content := view.View().Content + if !strings.Contains(content, "Name") { + t.Errorf("expected metadata key, got %q", content) + } + if !strings.Contains(content, "Test Item") { + t.Errorf("expected metadata value, got %q", content) } + 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) - 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 TestTableView(t *testing.T) { + state := testRenderState() + 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(state, columns, rows, views.TableDisplayFull) + content := view.View().Content + if !strings.Contains(content, "Name") { + t.Errorf("expected column header, got %q", content) + } + if !strings.Contains(content, "Alpha") { + t.Errorf("expected row data, got %q", content) + } + if !strings.Contains(content, "Beta") { + t.Errorf("expected row data, 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 TestTableMiniView(t *testing.T) { + state := testRenderState() + columns := []views.TableColumn{ + {Title: "Command", Percentage: 100}, + } + rows := []views.TableRow{ + {Data: []string{"build"}}, + {Data: []string{"test"}}, + {Data: []string{"deploy"}}, + } + 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) } - 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) +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) - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - 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) + selected := table.GetSelectedRow() + if selected == nil || selected.Data()[0] != "First" { + t.Fatal("expected initial selection on first row") } - 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) + 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 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) +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) - tm := teatest.NewTestModel(t, container, teatest.WithInitialTermSize(80, 80)) - container.SetSendFunc(tm.Send) - 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) + // Children should not be visible initially + content := table.View().Content + if strings.Contains(content, "Child") { + t.Error("children should be collapsed initially") } - 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) + // 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") } - teatest.RequireEqualOutput(t, out) } -func TestFormOutput(t *testing.T) { - ctx := context.Background() +// --- 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(), @@ -194,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!"} @@ -203,16 +269,16 @@ 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 { 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(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)) - 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/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" diff --git a/go.mod b/go.mod index c83cd06..68cf8c4 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ module github.com/flowexec/tuikit -go 1.23.0 +go 1.25.8 require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.5 - github.com/charmbracelet/huh v0.7.0 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/log v0.4.2 - github.com/charmbracelet/x/exp/teatest v0.0.0-20250623112707-45752038d08d - 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 @@ -21,38 +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/colorprofile v0.3.1 // 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/term 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 4b8e6c1..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,44 +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/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= -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/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 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/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/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= @@ -56,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= @@ -102,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 f0e2a80..2fae1a5 100644 --- a/program.go +++ b/program.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" ) type Program struct { diff --git a/sample/types/views.go b/sample/types/views.go index 5cc8fc7..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" + 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 deleted file mode 100644 index a7a7ae8..0000000 --- a/testdata/TestCollectionOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?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 diff --git a/testdata/TestEntityOutput.golden b/testdata/TestEntityOutput.golden deleted file mode 100644 index afb8775..0000000 --- a/testdata/TestEntityOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?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 diff --git a/testdata/TestErrorOutput.golden b/testdata/TestErrorOutput.golden deleted file mode 100644 index 125c400..0000000 --- a/testdata/TestErrorOutput.golden +++ /dev/null @@ -1,10 +0,0 @@ -[?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 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 e328c5d..0000000 --- a/testdata/TestFrameOutput.golden +++ /dev/null @@ -1 +0,0 @@ -[?25l[?2004h]2;tuikit-test Hello, world! [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file diff --git a/testdata/TestLoadingOutput.golden b/testdata/TestLoadingOutput.golden deleted file mode 100644 index 9f393bd..0000000 --- a/testdata/TestLoadingOutput.golden +++ /dev/null @@ -1,9 +0,0 @@ -[?25l[?2004h]2;tuikit-test ╭─────────────╮ -│ tuikit-test │───────────────────────────────────────────────────────────────── -╰─────────────╯ - - - ●∙∙ thinking... - - -  [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file diff --git a/testdata/TestMarkdownOutput.golden b/testdata/TestMarkdownOutput.golden deleted file mode 100644 index 94d14f7..0000000 --- a/testdata/TestMarkdownOutput.golden +++ /dev/null @@ -1,80 +0,0 @@ -[?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 diff --git a/themes/base.go b/themes/base.go index 73cd16c..d422df3 100644 --- a/themes/base.go +++ b/themes/base.go @@ -6,11 +6,11 @@ import ( "strings" "text/template" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" - "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 ( @@ -25,6 +25,7 @@ type baseTheme struct { Name string `json:"-" yaml:"-"` SpinnerType spinner.Spinner `json:"-" yaml:"-"` Colors *ColorPalette + isDark bool } func (t baseTheme) String() string { @@ -192,7 +193,7 @@ func (t baseTheme) RenderInContainer(text string) string { } func (t baseTheme) ListStyles() list.Styles { - s := list.DefaultStyles() + s := list.DefaultStyles(t.isDark) s.StatusBar = s.StatusBar. Padding(0, 0, 1, 0). Italic(true). @@ -201,7 +202,7 @@ func (t baseTheme) ListStyles() list.Styles { } func (t baseTheme) ListItemStyles() list.DefaultItemStyles { - s := list.NewDefaultItemStyles() + 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)) @@ -250,8 +251,12 @@ func (t baseTheme) GlamourMarkdownStyleJSON() (string, error) { return builder.String(), nil } -func (t baseTheme) HuhTheme() *huh.Theme { - baseTheme := huh.ThemeBase() +func (t baseTheme) HuhTheme() huh.Theme { + return huh.ThemeFunc(t.huhStyles) +} + +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(lipgloss.HiddenBorder()) 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 0e27ede..615a05b 100644 --- a/themes/theme.go +++ b/themes/theme.go @@ -1,11 +1,11 @@ package themes import ( - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" - "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 cb5bf3c..233333f 100644 --- a/themes/themes.go +++ b/themes/themes.go @@ -2,8 +2,10 @@ package themes import ( _ "embed" + "os" - "github.com/charmbracelet/bubbles/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 37796bb..04837c1 100644 --- a/types/msgs.go +++ b/types/msgs.go @@ -3,13 +3,20 @@ package types import ( "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) 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 a1f46c1..5f2fd45 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" + "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 { @@ -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 types.KeyEnter: if v.activeEntry != nil { return v, 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 9d2b9e6..de21372 100644 --- a/views/collection.go +++ b/views/collection.go @@ -4,9 +4,9 @@ import ( "fmt" "sort" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "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 { @@ -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 types.KeyEnter: if v.selectedFunc == nil { return v, nil } @@ -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 ae23ed7..2e92fed 100644 --- a/views/entity.go +++ b/views/entity.go @@ -4,9 +4,9 @@ import ( "fmt" "math" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "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" @@ -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,10 +69,10 @@ 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.SetContent(v.renderedContent()) - case tea.KeyMsg: + v.viewport.SetWidth(msg.ContentWidth) + v.viewport.SetHeight(msg.ContentHeight) + v.viewport.SetContent(v.renderedView().Content) + case tea.KeyPressMsg: switch msg.String() { case "-", "d": if v.format == types.EntityFormatDocument { @@ -92,9 +92,9 @@ func (v *EntityView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } v.format = types.CollectionFormatJSON v.viewport.GotoTop() - case tea.KeyUp.String(): + case types.KeyUp: v.viewport.ScrollUp(1) - case tea.KeyDown.String(): + case types.KeyDown: v.viewport.ScrollDown(1) default: for _, cb := range v.callbacks { @@ -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 fd9f57b..28140af 100644 --- a/views/errors.go +++ b/views/errors.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - tea "github.com/charmbracelet/bubbletea" + 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 057749e..e9b517d 100644 --- a/views/form.go +++ b/views/form.go @@ -12,8 +12,8 @@ import ( "strings" "sync" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/huh" + tea "charm.land/bubbletea/v2" + "charm.land/huh/v2" "golang.org/x/term" "github.com/flowexec/tuikit/themes" @@ -266,7 +266,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return f, cmd } -func (f *Form) View() string { +func (f *Form) View() tea.View { f.mu.RLock() defer f.mu.RUnlock() @@ -274,7 +274,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 162d5c4..a6be28c 100644 --- a/views/frame.go +++ b/views/frame.go @@ -1,7 +1,7 @@ package views import ( - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" ) const FrameViewType = "frame" @@ -22,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 b32aeab..315b72d 100644 --- a/views/loading.go +++ b/views/loading.go @@ -2,9 +2,10 @@ package views import ( "fmt" + "sync" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" "github.com/flowexec/tuikit/themes" ) @@ -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: @@ -36,14 +42,16 @@ 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 { + 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)) } 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 9094cbf..717df55 100644 --- a/views/markdown.go +++ b/views/markdown.go @@ -4,9 +4,9 @@ import ( "math" "sync" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "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" @@ -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,13 +48,13 @@ 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 - case tea.KeyMsg: + v.viewport.SetWidth(v.width) + v.viewport.SetHeight(v.height) + case tea.KeyPressMsg: switch msg.String() { - case tea.KeyUp.String(): + case types.KeyUp: v.viewport.ScrollUp(1) - case tea.KeyDown.String(): + case types.KeyDown: v.viewport.ScrollDown(1) } } @@ -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..0bb2070 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,21 +78,21 @@ 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": + 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 " ", "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 {