From 96541148d82540d45fb538fc3f829ca79a4ec365 Mon Sep 17 00:00:00 2001 From: everettraven Date: Fri, 19 Apr 2024 13:32:27 -0400 Subject: [PATCH] large refactor to decouple model and streaming logic Signed-off-by: everettraven --- go.mod | 10 +- go.sum | 19 ++- internal/cli/root.go | 22 ++- pkg/charm/models/{ => dashboard}/dashboard.go | 48 +++--- .../models/{ => dashboard}/dashboard_test.go | 11 +- .../models/{ => helper}/composite_help.go | 2 +- pkg/charm/models/panels/item.go | 71 -------- pkg/charm/models/panels/item/item.go | 106 ++++++++++++ .../models/panels/{ => item}/item_test.go | 9 +- pkg/charm/models/panels/{ => logs}/logs.go | 76 +++++---- .../models/panels/{ => logs}/logs_test.go | 19 +-- pkg/charm/models/panels/{ => table}/table.go | 158 +++++++++--------- .../models/panels/{ => table}/table_test.go | 15 +- pkg/charm/models/{tabber.go => tabs/tabs.go} | 70 ++++---- .../{tabber_test.go => tabs/tabs_test.go} | 5 +- pkg/charm/styles/styles.go | 15 ++ pkg/factories/datastream/informerfactory.go | 2 +- pkg/factories/datastream/item.go | 76 +++------ pkg/factories/datastream/logs.go | 52 +++--- pkg/factories/datastream/table.go | 65 ++++--- pkg/factories/panel/item.go | 9 +- pkg/factories/panel/logs.go | 7 +- pkg/factories/panel/panelfactory.go | 21 ++- pkg/factories/panel/panelfactory_test.go | 18 +- pkg/factories/panel/table.go | 7 +- 25 files changed, 512 insertions(+), 401 deletions(-) rename pkg/charm/models/{ => dashboard}/dashboard.go (64%) rename pkg/charm/models/{ => dashboard}/dashboard_test.go (73%) rename pkg/charm/models/{ => helper}/composite_help.go (98%) delete mode 100644 pkg/charm/models/panels/item.go create mode 100644 pkg/charm/models/panels/item/item.go rename pkg/charm/models/panels/{ => item}/item_test.go (70%) rename pkg/charm/models/panels/{ => logs}/logs.go (77%) rename pkg/charm/models/panels/{ => logs}/logs_test.go (81%) rename pkg/charm/models/panels/{ => table}/table.go (60%) rename pkg/charm/models/panels/{ => table}/table_test.go (87%) rename pkg/charm/models/{tabber.go => tabs/tabs.go} (70%) rename pkg/charm/models/{tabber_test.go => tabs/tabs_test.go} (79%) diff --git a/go.mod b/go.mod index 0388323..9940a7c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/calyptia/go-bubble-table v0.2.1 github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/lipgloss v0.10.0 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v1.7.0 @@ -53,20 +53,20 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.15.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.13.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index aaffdad..655c3f5 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5 github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= @@ -93,8 +93,8 @@ github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D 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.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -106,8 +106,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= @@ -119,8 +119,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -179,8 +180,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/cli/root.go b/internal/cli/root.go index 4c1e1ee..394afe2 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -11,7 +11,8 @@ import ( "path/filepath" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models" + "github.com/everettraven/buoy/pkg/charm/models/dashboard" + "github.com/everettraven/buoy/pkg/charm/models/tabs" "github.com/everettraven/buoy/pkg/charm/styles" "github.com/everettraven/buoy/pkg/factories/datastream" "github.com/everettraven/buoy/pkg/factories/panel" @@ -103,14 +104,31 @@ func run(path string, themePath string) error { if err != nil { if errSetter, ok := panel.(ErrorSetter); ok { errSetter.SetError(err) + continue } else { log.Fatalf("getting datastream for model: %s", err) } } + if dataStream == nil { + log.Printf("nil datastream returned for panel (%T)", panel) + continue + } + go dataStream.Run(make(<-chan struct{})) } - m := models.NewDashboard(models.DefaultDashboardKeys, theme, panelModels...) + dashboardStyles := dashboard.DashboardStyleOptions{ + TabModelStyle: tabs.TabModelStyleOptions{ + GapStyle: theme.TabGap(), + ContentStyle: theme.ContentStyle(), + SelectedStyle: theme.SelectedTabStyle(), + TabStyle: theme.TabStyle(), + LeftArrow: theme.TabLeftArrow, + RightArrow: theme.TabRightArrow, + }, + DividerStyle: theme.TabGap(), + } + m := dashboard.New(dashboard.DefaultDashboardKeys, dashboardStyles, panelModels...) if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) diff --git a/pkg/charm/models/dashboard.go b/pkg/charm/models/dashboard/dashboard.go similarity index 64% rename from pkg/charm/models/dashboard.go rename to pkg/charm/models/dashboard/dashboard.go index a2ecfc7..cf2e9c0 100644 --- a/pkg/charm/models/dashboard.go +++ b/pkg/charm/models/dashboard/dashboard.go @@ -1,4 +1,4 @@ -package models +package dashboard import ( "strings" @@ -8,7 +8,8 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/helper" + "github.com/everettraven/buoy/pkg/charm/models/tabs" ) type DashboardKeyMap struct { @@ -45,29 +46,36 @@ type Namer interface { Name() string } +// DashboardStyleOptions is the set of style options that can be +// used to configure the styles used by the Dashboard model +type DashboardStyleOptions struct { + TabModelStyle tabs.TabModelStyleOptions + DividerStyle lipgloss.Style +} + // Dashboard is a tea.Model implementation // for viewing Kubernetes information based // on a declarative dashboard description type Dashboard struct { - tabber *Tabber - width int - help help.Model - keys DashboardKeyMap - theme styles.Theme + tabber *tabs.TabModel + width int + help help.Model + keys DashboardKeyMap + dividerStyle lipgloss.Style } -func NewDashboard(keys DashboardKeyMap, theme styles.Theme, panels ...tea.Model) *Dashboard { - tabs := []Tab{} +func New(keys DashboardKeyMap, style DashboardStyleOptions, panels ...tea.Model) *Dashboard { + tabset := []tabs.Tab{} for _, panel := range panels { if namer, ok := panel.(Namer); ok { - tabs = append(tabs, Tab{Name: namer.Name(), Model: panel}) + tabset = append(tabset, tabs.Tab{Name: namer.Name(), Model: panel}) } } return &Dashboard{ - tabber: NewTabber(DefaultTabberKeys, theme, tabs...), - help: help.New(), - keys: keys, - theme: theme, + tabber: tabs.New(tabs.DefaultTabberKeys, style.TabModelStyle, tabset...), + help: help.New(), + keys: keys, + dividerStyle: style.DividerStyle, } } @@ -98,15 +106,15 @@ func (d *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (d *Dashboard) View() string { - div := d.theme.TabGap().Render(strings.Repeat(" ", max(0, d.width-2))) - return lipgloss.JoinVertical(0, d.tabber.View(), div, d.help.View(d.Help())) + divider := d.dividerStyle.Render(strings.Repeat(" ", max(0, d.width-2))) + return lipgloss.JoinVertical(0, d.tabber.View(), divider, d.help.View(d.Help())) } func (d *Dashboard) Help() help.KeyMap { - return CompositeHelpKeyMap{ - helps: []help.KeyMap{ + return helper.NewCompositeHelpKeyMap( + []help.KeyMap{ d.tabber.Help(), d.keys, - }, - } + }..., + ) } diff --git a/pkg/charm/models/dashboard_test.go b/pkg/charm/models/dashboard/dashboard_test.go similarity index 73% rename from pkg/charm/models/dashboard_test.go rename to pkg/charm/models/dashboard/dashboard_test.go index 1e04f74..96a604f 100644 --- a/pkg/charm/models/dashboard_test.go +++ b/pkg/charm/models/dashboard/dashboard_test.go @@ -1,26 +1,25 @@ -package models +package dashboard import ( "testing" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/panels/item" "github.com/everettraven/buoy/pkg/types" "github.com/stretchr/testify/assert" ) func TestDashboardUpdate(t *testing.T) { panels := []tea.Model{ - panels.NewItem(types.Item{ + item.New(types.Item{ PanelBase: types.PanelBase{ Name: "test", }, - }, viewport.New(10, 10), styles.Theme{}), + }, viewport.New(10, 10), item.Styles{}), } - d := NewDashboard(DefaultDashboardKeys, styles.Theme{}, panels...) + d := New(DefaultDashboardKeys, DashboardStyleOptions{}, panels...) t.Log("WindowSizeUpdate") d.Update(tea.WindowSizeMsg{Width: 50, Height: 50}) diff --git a/pkg/charm/models/composite_help.go b/pkg/charm/models/helper/composite_help.go similarity index 98% rename from pkg/charm/models/composite_help.go rename to pkg/charm/models/helper/composite_help.go index 12a6e6c..e97bc80 100644 --- a/pkg/charm/models/composite_help.go +++ b/pkg/charm/models/helper/composite_help.go @@ -1,4 +1,4 @@ -package models +package helper import ( "github.com/charmbracelet/bubbles/help" diff --git a/pkg/charm/models/panels/item.go b/pkg/charm/models/panels/item.go deleted file mode 100644 index b253b52..0000000 --- a/pkg/charm/models/panels/item.go +++ /dev/null @@ -1,71 +0,0 @@ -package panels - -import ( - "sync" - - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/styles" - "github.com/everettraven/buoy/pkg/types" -) - -// Item is a tea.Model implementation -// that represents an item panel -type Item struct { - viewport viewport.Model - mutex *sync.Mutex - item types.Item - theme styles.Theme - err error -} - -func NewItem(item types.Item, viewport viewport.Model, theme styles.Theme) *Item { - return &Item{ - viewport: viewport, - mutex: &sync.Mutex{}, - item: item, - theme: theme, - } -} - -func (m *Item) Init() tea.Cmd { return nil } - -func (m *Item) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.viewport.Width = msg.Width - m.viewport.Height = msg.Height / 2 - } - m.viewport, cmd = m.viewport.Update(msg) - return m, cmd -} - -func (m *Item) View() string { - if m.err != nil { - return m.err.Error() - } - return m.viewport.View() -} - -func (m *Item) SetContent(content string) { - m.mutex.Lock() - defer m.mutex.Unlock() - m.viewport.SetContent(content) -} - -func (m *Item) Name() string { - return m.item.Name -} - -func (m *Item) ItemDefinition() types.Item { - return m.item -} - -func (m *Item) Theme() styles.Theme { - return m.theme -} - -func (m *Item) SetError(err error) { - m.err = err -} diff --git a/pkg/charm/models/panels/item/item.go b/pkg/charm/models/panels/item/item.go new file mode 100644 index 0000000..1237463 --- /dev/null +++ b/pkg/charm/models/panels/item/item.go @@ -0,0 +1,106 @@ +package item + +import ( + "bytes" + "io" + "sync" + + "github.com/alecthomas/chroma/quick" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/everettraven/buoy/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" + apimachtypes "k8s.io/apimachinery/pkg/types" +) + +type Styles struct { + SyntaxHighlightDark string + SyntaxHighlightLight string +} + +// Model is a tea.Model implementation +// that represents an item panel +type Model struct { + viewport viewport.Model + mutex *sync.Mutex + item types.Item + theme Styles + err error +} + +func New(item types.Item, viewport viewport.Model, theme Styles) *Model { + return &Model{ + viewport: viewport, + mutex: &sync.Mutex{}, + item: item, + theme: theme, + } +} + +func (m *Model) Init() tea.Cmd { return nil } + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height / 2 + } + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd +} + +func (m *Model) View() string { + if m.err != nil { + return m.err.Error() + } + return m.viewport.View() +} + +func (m *Model) SetContent(content string) { + m.mutex.Lock() + defer m.mutex.Unlock() + // by default set the content as the plain string passed in + m.viewport.SetContent(content) + + // attempt to perform syntax highlighting + theme := m.theme.SyntaxHighlightDark + if !lipgloss.HasDarkBackground() { + theme = m.theme.SyntaxHighlightLight + } + rw := &bytes.Buffer{} + err := quick.Highlight(rw, content, "yaml", "terminal16m", theme) + if err != nil { + return + } + highlighted, err := io.ReadAll(rw) + if err != nil { + return + } + m.viewport.SetContent(string(highlighted)) +} + +func (m *Model) Name() string { + return m.item.Name +} + +func (m *Model) Key() apimachtypes.NamespacedName { + return m.item.Key +} + +func (m *Model) GVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: m.item.Group, + Version: m.item.Version, + Kind: m.item.Kind, + } +} + +func (m *Model) Theme() Styles { + return m.theme +} + +func (m *Model) SetError(err error) { + m.err = err +} diff --git a/pkg/charm/models/panels/item_test.go b/pkg/charm/models/panels/item/item_test.go similarity index 70% rename from pkg/charm/models/panels/item_test.go rename to pkg/charm/models/panels/item/item_test.go index dad0c2c..1bfc726 100644 --- a/pkg/charm/models/panels/item_test.go +++ b/pkg/charm/models/panels/item/item_test.go @@ -1,4 +1,4 @@ -package panels +package item import ( "errors" @@ -6,27 +6,26 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/styles" "github.com/everettraven/buoy/pkg/types" "github.com/stretchr/testify/assert" ) func TestItemUpdate(t *testing.T) { - item := NewItem(types.Item{}, viewport.New(10, 10), styles.Theme{}) + item := New(types.Item{}, viewport.New(10, 10), Styles{}) item.Update(tea.WindowSizeMsg{Width: 50, Height: 50}) assert.Equal(t, 50, item.viewport.Width) assert.Equal(t, 25, item.viewport.Height) } func TestItemViewWithError(t *testing.T) { - item := NewItem(types.Item{}, viewport.New(10, 10), styles.Theme{}) + item := New(types.Item{}, viewport.New(10, 10), Styles{}) err := errors.New("some error") item.SetError(err) assert.Equal(t, err.Error(), item.View()) } func TestViewWithContent(t *testing.T) { - item := NewItem(types.Item{}, viewport.New(50, 50), styles.Theme{}) + item := New(types.Item{}, viewport.New(50, 50), Styles{}) item.SetContent("some content") assert.Contains(t, item.View(), "some content") } diff --git a/pkg/charm/models/panels/logs.go b/pkg/charm/models/panels/logs/logs.go similarity index 77% rename from pkg/charm/models/panels/logs.go rename to pkg/charm/models/panels/logs/logs.go index bb396ce..488541a 100644 --- a/pkg/charm/models/panels/logs.go +++ b/pkg/charm/models/panels/logs/logs.go @@ -1,4 +1,4 @@ -package panels +package logs import ( "fmt" @@ -11,13 +11,14 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/everettraven/buoy/pkg/charm/styles" "github.com/everettraven/buoy/pkg/types" "github.com/muesli/reflow/wrap" "github.com/sahilm/fuzzy" + "k8s.io/apimachinery/pkg/runtime/schema" + apimachtypes "k8s.io/apimachinery/pkg/types" ) -type LogsKeyMap struct { +type KeyMap struct { Search key.Binding SubmitSearch key.Binding QuitSearch key.Binding @@ -26,19 +27,19 @@ type LogsKeyMap struct { // ShortHelp returns keybindings to be shown in the mini help view. It's part // of the key.Map interface. -func (k LogsKeyMap) ShortHelp() []key.Binding { +func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{} } // FullHelp returns keybindings for the expanded help view. It's part of the // key.Map interface. -func (k LogsKeyMap) FullHelp() [][]key.Binding { +func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Search, k.SubmitSearch, k.QuitSearch, k.ToggleStrict}, } } -var DefaultLogsKeys = LogsKeyMap{ +var DefaultKeys = KeyMap{ Search: key.NewBinding( key.WithKeys("/"), key.WithHelp("/", "open a prompt to search logs"), @@ -61,28 +62,35 @@ const modeLogs = "logs" const modeSearching = "searching" const modeSearched = "searched" -// Logs is a tea.Model implementation -// that represents an item panel -type Logs struct { +type Styles struct { + SearchPrompt string + SearchPlaceholder string + SearchModeStyle lipgloss.Style + SearchMatchHighlightStyle lipgloss.Style +} + +// Model is a tea.Model implementation +// that can be used to view logs +type Model struct { viewport viewport.Model searchbar textinput.Model mutex *sync.Mutex content string contentUpdated bool mode string - keys LogsKeyMap + keys KeyMap strictSearch bool - theme styles.Theme + theme Styles log *types.Logs err error } -func NewLogs(keys LogsKeyMap, log *types.Logs, theme styles.Theme) *Logs { +func New(keys KeyMap, log *types.Logs, theme Styles) *Model { searchbar := textinput.New() - searchbar.Prompt = "> " - searchbar.Placeholder = "search term" + searchbar.Prompt = theme.SearchPrompt + searchbar.Placeholder = theme.SearchPlaceholder vp := viewport.New(10, 10) - return &Logs{ + return &Model{ viewport: vp, searchbar: searchbar, log: log, @@ -94,9 +102,9 @@ func NewLogs(keys LogsKeyMap, log *types.Logs, theme styles.Theme) *Logs { } } -func (m *Logs) Init() tea.Cmd { return nil } +func (m *Model) Init() tea.Cmd { return nil } -func (m *Logs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.mutex.Lock() defer m.mutex.Unlock() var cmd tea.Cmd @@ -150,7 +158,7 @@ func (m *Logs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m *Logs) View() string { +func (m *Model) View() string { if m.err != nil { return m.err.Error() } @@ -159,7 +167,7 @@ func (m *Logs) View() string { if m.strictSearch { searchMode = "strict" } - searchModeOutput := m.theme.LogSearchModeStyle().Render(fmt.Sprintf("search mode: %s", searchMode)) + searchModeOutput := m.theme.SearchModeStyle.Render(fmt.Sprintf("search mode: %s", searchMode)) if m.mode == modeSearching { return lipgloss.JoinVertical(lipgloss.Top, @@ -178,26 +186,38 @@ func (m *Logs) View() string { return m.viewport.View() } -func (m *Logs) Help() help.KeyMap { +func (m *Model) Help() help.KeyMap { return m.keys } -func (m *Logs) AddContent(content string) { +func (m *Model) AddContent(content string) { m.mutex.Lock() defer m.mutex.Unlock() m.content = strings.Join([]string{m.content, content}, "\n") m.contentUpdated = true } -func (m *Logs) Name() string { +func (m *Model) Name() string { return m.log.Name } -func (m *Logs) LogDefinition() *types.Logs { - return m.log +func (m *Model) Key() apimachtypes.NamespacedName { + return m.log.Key +} + +func (m *Model) GVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: m.log.Group, + Version: m.log.Version, + Kind: m.log.Kind, + } +} + +func (m *Model) Container() string { + return m.log.Container } -func (m *Logs) SetError(err error) { +func (m *Model) SetError(err error) { m.err = err } @@ -205,15 +225,15 @@ func (m *Logs) SetError(err error) { // and returns a string with the matching log lines // and the matched term highlighted. Uses fuzzy search // if strict search is not enabled. Wraps logs to the width of the viewport. -func (m *Logs) searchLogs() string { +func (m *Model) searchLogs() string { term := m.searchbar.Value() wrap := m.viewport.Width strict := m.strictSearch splitLogs := strings.Split(m.content, "\n") if strict { - return strictMatchLogs(term, splitLogs, m.viewport.Width, m.theme.LogSearchHighlightStyle()) + return strictMatchLogs(term, splitLogs, m.viewport.Width, m.theme.SearchMatchHighlightStyle) } - return fuzzyMatchLogs(term, splitLogs, wrap, m.theme.LogSearchHighlightStyle()) + return fuzzyMatchLogs(term, splitLogs, wrap, m.theme.SearchMatchHighlightStyle) } func strictMatchLogs(searchTerm string, logLines []string, wrap int, style lipgloss.Style) string { diff --git a/pkg/charm/models/panels/logs_test.go b/pkg/charm/models/panels/logs/logs_test.go similarity index 81% rename from pkg/charm/models/panels/logs_test.go rename to pkg/charm/models/panels/logs/logs_test.go index 721f221..ec9d4b9 100644 --- a/pkg/charm/models/panels/logs_test.go +++ b/pkg/charm/models/panels/logs/logs_test.go @@ -1,29 +1,28 @@ -package panels +package logs import ( "errors" "testing" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/styles" "github.com/stretchr/testify/assert" ) func TestEnterSearchMode(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) assert.Equal(t, logs.mode, modeSearching) } func TestExecuteSearch(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) logs.Update(tea.KeyMsg{Type: tea.KeyEnter}) assert.Equal(t, logs.mode, modeSearched) } func TestExitSearchMode(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.mode = modeSearching logs.searchbar.Focus() logs.Update(tea.KeyMsg{Type: tea.KeyEsc}) @@ -38,7 +37,7 @@ func TestExitSearchMode(t *testing.T) { } func TestSearchModeToggle(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) logs.Update(tea.KeyMsg{Type: tea.KeyCtrlS}) assert.True(t, logs.strictSearch) @@ -48,7 +47,7 @@ func TestSearchModeToggle(t *testing.T) { func TestSearchLogs(t *testing.T) { t.Log("strict search") - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.strictSearch = true logs.content = "some log line\nlog line with a search term\n" logs.viewport.Width = 50 @@ -64,14 +63,14 @@ func TestSearchLogs(t *testing.T) { } func TestLogsWindowSizeUpdate(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.Update(tea.WindowSizeMsg{Width: 100, Height: 100}) assert.Equal(t, logs.viewport.Width, 100) assert.Equal(t, logs.viewport.Height, 50) } func TestLogsAddContent(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) logs.AddContent("some log line\n") assert.Equal(t, "\nsome log line\n", logs.content) assert.True(t, logs.contentUpdated) @@ -81,7 +80,7 @@ func TestLogsAddContent(t *testing.T) { } func TestLogsViewWithError(t *testing.T) { - logs := NewLogs(DefaultLogsKeys, nil, styles.Theme{}) + logs := New(DefaultKeys, nil, Styles{}) err := errors.New("some error") logs.SetError(err) assert.Equal(t, err, logs.err) diff --git a/pkg/charm/models/panels/table.go b/pkg/charm/models/panels/table/table.go similarity index 60% rename from pkg/charm/models/panels/table.go rename to pkg/charm/models/panels/table/table.go index f0e7163..c1e3998 100644 --- a/pkg/charm/models/panels/table.go +++ b/pkg/charm/models/panels/table/table.go @@ -1,4 +1,4 @@ -package panels +package table import ( "bytes" @@ -14,57 +14,64 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/everettraven/buoy/pkg/charm/styles" buoytypes "github.com/everettraven/buoy/pkg/types" "github.com/tidwall/gjson" - "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/cache" - "sigs.k8s.io/yaml" ) -type TableKeyMap struct { +type KeyMap struct { ViewModeToggle key.Binding } // ShortHelp returns keybindings to be shown in the mini help view. It's part // of the key.Map interface. -func (k TableKeyMap) ShortHelp() []key.Binding { +func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{} } // FullHelp returns keybindings for the expanded help view. It's part of the // key.Map interface. -func (k TableKeyMap) FullHelp() [][]key.Binding { +func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.ViewModeToggle}, } } -var DefaultTableKeys = TableKeyMap{ +var DefaultKeys = KeyMap{ ViewModeToggle: key.NewBinding( key.WithKeys("v"), key.WithHelp("v", "toggle viewing contents of selected resource"), ), } -const modeView = "view" -const modeTable = "table" +const ( + modeView = "view" + modeTable = "table" +) type RowInfo struct { Row tbl.Row Identifier *types.NamespacedName - // Is this necessary? Can the index change on different iterations? - Index int + Index int +} + +type Styles struct { + SelectedRow lipgloss.Style + SyntaxHighlightDark string + SyntaxHighlightLight string } -// Table is a tea.Model implementation +type ViewActionFunc func(row *RowInfo) (string, error) + +// TODO: Can some of the action logic be decoupled from this model? + +// Model is a tea.Model implementation // that represents a table panel -type Table struct { +type Model struct { tableModel tbl.Model - lister cache.GenericLister - scope meta.RESTScopeName viewport viewport.Model mode string mutex *sync.Mutex @@ -72,12 +79,13 @@ type Table struct { columns []buoytypes.Column err error tempRows []tbl.Row - keys TableKeyMap - theme styles.Theme + keys KeyMap table *buoytypes.Table + styles Styles + viewAction ViewActionFunc } -func NewTable(keys TableKeyMap, table *buoytypes.Table, theme styles.Theme) *Table { +func New(keys KeyMap, table *buoytypes.Table, styles Styles) *Model { tblColumns := []string{} width := 0 for _, column := range table.Columns { @@ -86,9 +94,9 @@ func NewTable(keys TableKeyMap, table *buoytypes.Table, theme styles.Theme) *Tab } tab := tbl.New(tblColumns, 100, 10) - tab.Styles.SelectedRow = theme.TableSelectedRowStyle() + tab.Styles.SelectedRow = styles.SelectedRow - return &Table{ + return &Model{ tableModel: tab, viewport: viewport.New(0, 0), mode: modeTable, @@ -96,16 +104,16 @@ func NewTable(keys TableKeyMap, table *buoytypes.Table, theme styles.Theme) *Tab rows: map[types.UID]*RowInfo{}, columns: table.Columns, keys: keys, - theme: theme, table: table, + styles: styles, } } -func (m *Table) Init() tea.Cmd { +func (m *Model) Init() tea.Cmd { return nil } -func (m *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -114,15 +122,16 @@ func (m *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.Height = msg.Height / 2 case tea.KeyMsg: switch { - case key.Matches(msg, DefaultTableKeys.ViewModeToggle): + case key.Matches(msg, DefaultKeys.ViewModeToggle): switch m.mode { case modeTable: m.mode = modeView - vpContent, err := m.FetchContentForIndex(m.tableModel.Cursor()) + row := m.FetchRowForIndex(m.tableModel.Cursor()) + vpContent, err := m.viewAction(row) if err != nil { m.viewport.SetContent(err.Error()) } else { - m.viewport.SetContent(vpContent) + m.viewport.SetContent(highlight(vpContent, m.styles)) } case modeView: m.mode = modeTable @@ -145,7 +154,7 @@ func (m *Table) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m *Table) View() string { +func (m *Model) View() string { if m.err != nil { return m.err.Error() } @@ -159,7 +168,7 @@ func (m *Table) View() string { } } -func (m *Table) AddOrUpdate(u *unstructured.Unstructured) { +func (m *Model) AddOrUpdate(u *unstructured.Unstructured) { m.mutex.Lock() defer m.mutex.Unlock() uid := u.GetUID() @@ -181,14 +190,14 @@ func (m *Table) AddOrUpdate(u *unstructured.Unstructured) { m.updateRows() } -func (m *Table) DeleteRow(uid types.UID) { +func (m *Model) DeleteRow(uid types.UID) { m.mutex.Lock() defer m.mutex.Unlock() delete(m.rows, uid) m.updateRows() } -func (m *Table) updateRows() { +func (m *Model) updateRows() { rows := []tbl.Row{} indice := 0 for _, rowInfo := range m.rows { @@ -199,31 +208,35 @@ func (m *Table) updateRows() { m.tempRows = rows } -func (m *Table) Columns() []buoytypes.Column { +func (m *Model) Columns() []buoytypes.Column { return m.columns } -func (m *Table) Name() string { +func (m *Model) Name() string { return m.table.Name } -func (m *Table) TableDefinition() *buoytypes.Table { - return m.table +func (m *Model) GVK() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: m.table.Group, + Version: m.table.Version, + Kind: m.table.Kind, + } } -func (m *Table) SetLister(lister cache.GenericLister) { - m.lister = lister +func (m *Model) Namespace() string { + return m.table.Namespace } -func (m *Table) SetScope(scope meta.RESTScopeName) { - m.scope = scope +func (m *Model) LabelSelector() labels.Set { + return m.table.LabelSelector } -func (m *Table) SetError(err error) { +func (m *Model) SetError(err error) { m.err = err } -func (m *Table) FetchContentForIndex(index int) (string, error) { +func (m *Model) FetchRowForIndex(index int) *RowInfo { m.mutex.Lock() defer m.mutex.Unlock() var rowInfo *RowInfo @@ -234,47 +247,14 @@ func (m *Table) FetchContentForIndex(index int) (string, error) { } } - if rowInfo == nil { - return "", fmt.Errorf("no row data found for selected row %d", index) - } - - name := rowInfo.Identifier.String() - if m.scope == meta.RESTScopeNameRoot { - name = rowInfo.Identifier.Name - } - - obj, err := m.lister.Get(name) - if err != nil { - return "", fmt.Errorf("fetching definition for %q: %w", name, err) - } - - itemJSON, err := obj.(*unstructured.Unstructured).MarshalJSON() - if err != nil { - return "", fmt.Errorf("error marshalling item %q: %w", name, err) - } - - itemYAML, err := yaml.JSONToYAML(itemJSON) - if err != nil { - return "", fmt.Errorf("converting JSON to YAML for item %q: %w", name, err) - } + return rowInfo +} - theme := m.theme.SyntaxHighlightDarkTheme - if !lipgloss.HasDarkBackground() { - theme = m.theme.SyntaxHighlightLightTheme - } - rw := &bytes.Buffer{} - err = quick.Highlight(rw, string(itemYAML), "yaml", "terminal16m", theme) - if err != nil { - return "", fmt.Errorf("highlighting YAML for item %q: %w", name, err) - } - highlighted, err := io.ReadAll(rw) - if err != nil { - return "", fmt.Errorf("reading highlighted YAML for item %q: %w", name, err) - } - return string(highlighted), nil +func (m *Model) SetViewActionFunc(vaf ViewActionFunc) { + m.viewAction = vaf } -func (m *Table) Help() help.KeyMap { +func (m *Model) Help() help.KeyMap { return m.keys } @@ -289,3 +269,21 @@ func getDotNotationValue(item map[string]interface{}, dotPath string) (interface } return res.Value(), nil } + +func highlight(s string, styles Styles) string { + // attempt to perform syntax highlighting + theme := styles.SyntaxHighlightDark + if !lipgloss.HasDarkBackground() { + theme = styles.SyntaxHighlightLight + } + rw := &bytes.Buffer{} + err := quick.Highlight(rw, s, "yaml", "terminal16m", theme) + if err != nil { + return s + } + highlighted, err := io.ReadAll(rw) + if err != nil { + return s + } + return string(highlighted) +} diff --git a/pkg/charm/models/panels/table_test.go b/pkg/charm/models/panels/table/table_test.go similarity index 87% rename from pkg/charm/models/panels/table_test.go rename to pkg/charm/models/panels/table/table_test.go index 74d27dd..b1394bf 100644 --- a/pkg/charm/models/panels/table_test.go +++ b/pkg/charm/models/panels/table/table_test.go @@ -1,11 +1,10 @@ -package panels +package table import ( "errors" "testing" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/styles" buoytypes "github.com/everettraven/buoy/pkg/types" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -14,7 +13,7 @@ import ( func TestTableUpdate(t *testing.T) { t.Log("WindowSizeUpdate") - table := NewTable(DefaultTableKeys, &buoytypes.Table{}, styles.Theme{}) + table := New(DefaultKeys, &buoytypes.Table{}, Styles{}) table.Update(tea.WindowSizeMsg{Width: 50, Height: 50}) assert.Equal(t, 50, table.viewport.Width) assert.Equal(t, 25, table.viewport.Height) @@ -29,11 +28,11 @@ func TestTableUpdate(t *testing.T) { } func TestAddOrUpdate(t *testing.T) { - table := NewTable(DefaultTableKeys, &buoytypes.Table{ + table := New(DefaultKeys, &buoytypes.Table{ Columns: []buoytypes.Column{ {Header: "Name", Width: 10, Path: "metadata.name"}, }, - }, styles.Theme{}) + }, Styles{}) t.Log("add a row") u := &unstructured.Unstructured{} @@ -54,11 +53,11 @@ func TestAddOrUpdate(t *testing.T) { } func TestDeleteRow(t *testing.T) { - table := NewTable(DefaultTableKeys, &buoytypes.Table{ + table := New(DefaultKeys, &buoytypes.Table{ Columns: []buoytypes.Column{ {Header: "Name", Width: 10, Path: "metadata.name"}, }, - }, styles.Theme{}) + }, Styles{}) t.Log("add a row") u := &unstructured.Unstructured{} @@ -74,7 +73,7 @@ func TestDeleteRow(t *testing.T) { } func TestTableView(t *testing.T) { - table := NewTable(DefaultTableKeys, &buoytypes.Table{}, styles.Theme{}) + table := New(DefaultKeys, &buoytypes.Table{}, Styles{}) t.Log("view with error state") err := errors.New("some error") diff --git a/pkg/charm/models/tabber.go b/pkg/charm/models/tabs/tabs.go similarity index 70% rename from pkg/charm/models/tabber.go rename to pkg/charm/models/tabs/tabs.go index 0d3311d..3970268 100644 --- a/pkg/charm/models/tabber.go +++ b/pkg/charm/models/tabs/tabs.go @@ -1,4 +1,4 @@ -package models +package tabs import ( "strings" @@ -7,7 +7,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/helper" ) type Helper interface { @@ -19,27 +19,46 @@ type Tab struct { Model tea.Model } -type Tabber struct { +// TabModelStyleOptions is the set of style options that can be +// used to configure the styles used by the TabModel +type TabModelStyleOptions struct { + GapStyle lipgloss.Style + ContentStyle lipgloss.Style + SelectedStyle lipgloss.Style + TabStyle lipgloss.Style + LeftArrow string + RightArrow string +} + +type TabModel struct { tabs []Tab selected int keyMap TabberKeyMap width int - theme styles.Theme + styles TabModelStyleOptions + pager *pager } -func NewTabber(keyMap TabberKeyMap, theme styles.Theme, tabs ...Tab) *Tabber { - return &Tabber{ +func New(keyMap TabberKeyMap, styles TabModelStyleOptions, tabs ...Tab) *TabModel { + return &TabModel{ tabs: tabs, keyMap: keyMap, - theme: theme, + styles: styles, + pager: &pager{ + tabRightArrow: styles.GapStyle.Render(styles.RightArrow), + tabLeftArrow: styles.GapStyle.Render(styles.LeftArrow), + pages: []page{}, + selectedStyle: styles.SelectedStyle, + tabStyle: styles.TabStyle, + }, } } -func (t *Tabber) Init() tea.Cmd { +func (t *TabModel) Init() tea.Cmd { return nil } -func (t *Tabber) Update(msg tea.Msg) (*Tabber, tea.Cmd) { +func (t *TabModel) Update(msg tea.Msg) (*TabModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { @@ -72,36 +91,24 @@ func (t *Tabber) Update(msg tea.Msg) (*Tabber, tea.Cmd) { return t, cmd } -func (t *Tabber) View() string { - tabRightArrow := t.theme.TabGap().Render(" ▶ ") - tabLeftArrow := t.theme.TabGap().Render(" ◀ ") - - pager := &pager{ - tabRightArrow: tabRightArrow, - tabLeftArrow: tabLeftArrow, - pages: []page{}, - theme: t.theme, - } - pager.setPages(t.tabs, t.selected, t.width) - - tabBlock := pager.renderForSelectedTab(t.selected) +func (t *TabModel) View() string { + t.pager.setPages(t.tabs, t.selected, t.width) + tabBlock := t.pager.renderForSelectedTab(t.selected) // gap is a repeating of the spaces so that the bottom border continues the entire width // of the terminal. This allows it to look like a proper set of tabs - gap := t.theme.TabGap().Render(strings.Repeat(" ", max(0, t.width-lipgloss.Width(tabBlock)-2))) + gap := t.styles.GapStyle.Render(strings.Repeat(" ", max(0, t.width-lipgloss.Width(tabBlock)-2))) tabsWithBorder := lipgloss.JoinHorizontal(lipgloss.Bottom, tabBlock, gap) - content := t.theme.ContentStyle().Render(t.tabs[t.selected].Model.View()) + content := t.styles.ContentStyle.Render(t.tabs[t.selected].Model.View()) return lipgloss.JoinVertical(0, tabsWithBorder, content) } -func (t *Tabber) Help() help.KeyMap { +func (t *TabModel) Help() help.KeyMap { helps := []help.KeyMap{} if helper, ok := t.tabs[t.selected].Model.(Helper); ok { helps = append(helps, helper.Help()) } - return CompositeHelpKeyMap{ - helps: append(helps, t.keyMap), - } + return helper.NewCompositeHelpKeyMap(helps...) } type TabberKeyMap struct { @@ -144,7 +151,8 @@ type pager struct { pages []page tabRightArrow string tabLeftArrow string - theme styles.Theme + selectedStyle lipgloss.Style + tabStyle lipgloss.Style } func (p *pager) renderForSelectedTab(selected int) string { @@ -168,9 +176,9 @@ func (p *pager) setPages(tabs []Tab, selected int, width int) { tempTab := "" tempPage := page{start: 0, tabs: []string{}} for i, tab := range tabs { - renderedTab := p.theme.TabStyle().Render(tab.Name) + renderedTab := p.tabStyle.Render(tab.Name) if i == selected { - renderedTab = p.theme.SelectedTabStyle().Render(tab.Name) + renderedTab = p.selectedStyle.Render(tab.Name) } tempTab = lipgloss.JoinHorizontal(lipgloss.Top, tempTab, renderedTab) joined := lipgloss.JoinHorizontal(lipgloss.Bottom, p.tabLeftArrow, tempTab, p.tabRightArrow) diff --git a/pkg/charm/models/tabber_test.go b/pkg/charm/models/tabs/tabs_test.go similarity index 79% rename from pkg/charm/models/tabber_test.go rename to pkg/charm/models/tabs/tabs_test.go index 2b0f01e..4b94d20 100644 --- a/pkg/charm/models/tabber_test.go +++ b/pkg/charm/models/tabs/tabs_test.go @@ -1,15 +1,14 @@ -package models +package tabs import ( "testing" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/styles" "github.com/stretchr/testify/assert" ) func TestTabberUpdate(t *testing.T) { - tabber := NewTabber(DefaultTabberKeys, styles.Theme{}, Tab{Name: "test", Model: nil}, Tab{Name: "test2", Model: nil}) + tabber := New(DefaultTabberKeys, TabModelStyleOptions{}, Tab{Name: "test", Model: nil}, Tab{Name: "test2", Model: nil}) t.Log("navigate to next tab") tabber.Update(tea.KeyMsg{Type: tea.KeyTab}) diff --git a/pkg/charm/styles/styles.go b/pkg/charm/styles/styles.go index cf98d49..ded4bb6 100644 --- a/pkg/charm/styles/styles.go +++ b/pkg/charm/styles/styles.go @@ -25,6 +25,11 @@ type Theme struct { // terminal has a light background. Available themes can be found at // https://github.com/alecthomas/chroma/tree/master/styles SyntaxHighlightLightTheme string + + // TabLeftArrow is the string to render to indicate there are more tabs to the left + TabLeftArrow string + // TabRightArrow is the string to render to indicate there are more tabs to the right + TabRightArrow string } const DefaultThemePath = "~/.config/buoy/themes/default.json" @@ -38,6 +43,8 @@ func LoadTheme(themePath string) (Theme, error) { LogSearchHighlightColor: DefaultColor, SyntaxHighlightDarkTheme: "nord", SyntaxHighlightLightTheme: "monokailight", + TabRightArrow: " > ", + TabLeftArrow: " < ", } // If the specified theme file doesn't exist, use the default theme if _, err := os.Stat(themePath); err != nil { @@ -93,3 +100,11 @@ func (t *Theme) LogSearchHighlightStyle() lipgloss.Style { func (t *Theme) LogSearchModeStyle() lipgloss.Style { return lipgloss.NewStyle().Italic(true).Faint(true) } + +func (t *Theme) TabArrowRight() string { + return t.TabArrowRight() +} + +func (t *Theme) TabArrowLeft() string { + return t.TabArrowLeft() +} diff --git a/pkg/factories/datastream/informerfactory.go b/pkg/factories/datastream/informerfactory.go index 590e9bb..be39936 100644 --- a/pkg/factories/datastream/informerfactory.go +++ b/pkg/factories/datastream/informerfactory.go @@ -24,7 +24,7 @@ type InvalidPanelType struct { error } -type DatastreamFactoryFunc func(tea.Model) (Datastream, error) +type DatastreamFactoryFunc func(interface{}) (Datastream, error) type datastreamFactory struct { // informerFactoryFuncs is a list of functions that return informers diff --git a/pkg/factories/datastream/item.go b/pkg/factories/datastream/item.go index 759c926..5cb1853 100644 --- a/pkg/factories/datastream/item.go +++ b/pkg/factories/datastream/item.go @@ -1,46 +1,39 @@ package datastream import ( - "bytes" "fmt" - "io" "time" - "github.com/alecthomas/chroma/quick" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/everettraven/buoy/pkg/charm/models/panels" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/tools/cache" "sigs.k8s.io/yaml" ) +type ItemPanel interface { + Key() types.NamespacedName + GVK() schema.GroupVersionKind + SetContent(string) +} + func ItemDatastreamFunc(dynamicClient *dynamic.DynamicClient, restMapper meta.RESTMapper) DatastreamFactoryFunc { - return func(m tea.Model) (Datastream, error) { - if _, ok := m.(*panels.Item); !ok { - return nil, &InvalidPanelType{fmt.Errorf("model is not of type *panels.Item")} - } - panel := m.(*panels.Item) - item := panel.ItemDefinition() - theme := panel.Theme().SyntaxHighlightDarkTheme - if !lipgloss.HasDarkBackground() { - theme = panel.Theme().SyntaxHighlightLightTheme + return func(obj interface{}) (Datastream, error) { + item, ok := obj.(ItemPanel) + if !ok { + return nil, &InvalidPanelType{fmt.Errorf("provided object doesn't implement the Item interface. Unable to determine namespace/name of item")} } + // create informer and event handler - infFact := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, 1*time.Minute, item.Key.Namespace, func(lo *v1.ListOptions) { - lo.FieldSelector = fmt.Sprintf("metadata.name=%s", item.Key.Name) + infFact := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, 1*time.Minute, item.Key().Namespace, func(lo *v1.ListOptions) { + lo.FieldSelector = fmt.Sprintf("metadata.name=%s", item.Key().Name) }) - gvk := schema.GroupVersionKind{ - Group: item.Group, - Version: item.Version, - Kind: item.Kind, - } - mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + + mapping, err := restMapper.RESTMapping(item.GVK().GroupKind(), item.GVK().Version) if err != nil { return nil, fmt.Errorf("error creating resource mapping: %w", err) } @@ -51,56 +44,35 @@ func ItemDatastreamFunc(dynamicClient *dynamic.DynamicClient, restMapper meta.RE u := obj.(*unstructured.Unstructured) itemJSON, err := u.MarshalJSON() if err != nil { - panel.SetContent(fmt.Sprintf("error marshalling item %q", item.Key.String())) + item.SetContent(fmt.Sprintf("error marshalling item %q", item.Key().String())) return } itemYAML, err := yaml.JSONToYAML(itemJSON) if err != nil { - panel.SetContent(fmt.Sprintf("converting JSON to YAML for item %q", item.Key.String())) + item.SetContent(fmt.Sprintf("converting JSON to YAML for item %q", item.Key().String())) return } - rw := &bytes.Buffer{} - err = quick.Highlight(rw, string(itemYAML), "yaml", "terminal16m", theme) - if err != nil { - panel.SetContent(fmt.Sprintf("highlighting YAML for item %q", item.Key.String())) - return - } - highlighted, err := io.ReadAll(rw) - if err != nil { - panel.SetContent(fmt.Sprintf("reading highlighted YAML for item %q", item.Key.String())) - return - } - panel.SetContent(string(highlighted)) + + item.SetContent(string(itemYAML)) }, UpdateFunc: func(oldObj, newObj interface{}) { u := newObj.(*unstructured.Unstructured) itemJSON, err := u.MarshalJSON() if err != nil { - panel.SetContent(fmt.Sprintf("error marshalling item %q", item.Key.String())) + item.SetContent(fmt.Sprintf("error marshalling item %q", item.Key().String())) return } itemYAML, err := yaml.JSONToYAML(itemJSON) if err != nil { - panel.SetContent(fmt.Sprintf("converting JSON to YAML for item %q", item.Key.String())) - return - } - rw := &bytes.Buffer{} - err = quick.Highlight(rw, string(itemYAML), "yaml", "terminal16m", theme) - if err != nil { - panel.SetContent(fmt.Sprintf("highlighting YAML for item %q", item.Key.String())) - return - } - highlighted, err := io.ReadAll(rw) - if err != nil { - panel.SetContent(fmt.Sprintf("reading highlighted YAML for item %q", item.Key.String())) + item.SetContent(fmt.Sprintf("converting JSON to YAML for item %q", item.Key().String())) return } - panel.SetContent(string(highlighted)) + item.SetContent(string(itemYAML)) }, DeleteFunc: func(obj interface{}) { - panel.SetContent("") + item.SetContent("") }, }) diff --git a/pkg/factories/datastream/logs.go b/pkg/factories/datastream/logs.go index 16def0e..8a8d00a 100644 --- a/pkg/factories/datastream/logs.go +++ b/pkg/factories/datastream/logs.go @@ -7,14 +7,13 @@ import ( "fmt" "io" - tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) @@ -23,46 +22,47 @@ var _ Datastream = &logDatastream{} type logDatastream struct { logReadCloser io.ReadCloser - logPanel *panels.Logs + contentAdder ContentAdder } func (l *logDatastream) Run(stopCh <-chan struct{}) { - go streamLogs(l.logReadCloser, l.logPanel) + go streamLogs(l.logReadCloser, l.contentAdder) +} + +type Log interface { + Key() types.NamespacedName + GVK() schema.GroupVersionKind + Container() string + ContentAdder } func LogsDatastreamFunc(typedClient *kubernetes.Clientset, dynamicClient *dynamic.DynamicClient, restMapper meta.RESTMapper) DatastreamFactoryFunc { - return func(m tea.Model) (Datastream, error) { - if _, ok := m.(*panels.Logs); !ok { - return nil, &InvalidPanelType{fmt.Errorf("model is not of type *panels.Logs")} - } - logs := m.(*panels.Logs) - logsPanel := logs.LogDefinition() - gvk := schema.GroupVersionKind{ - Group: logsPanel.Group, - Version: logsPanel.Version, - Kind: logsPanel.Kind, + return func(obj interface{}) (Datastream, error) { + log, ok := obj.(Log) + if !ok { + return nil, &InvalidPanelType{fmt.Errorf("object does not implement Log interface. Unable to determine how to fetch logs")} } - if gvk == v1.SchemeGroupVersion.WithKind("Pod") { - pod, err := typedClient.CoreV1().Pods(logsPanel.Key.Namespace).Get(context.Background(), logsPanel.Key.Name, metav1.GetOptions{}) + if log.GVK() == v1.SchemeGroupVersion.WithKind("Pod") { + pod, err := typedClient.CoreV1().Pods(log.Key().Namespace).Get(context.Background(), log.Key().Name, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("error getting pod: %w", err) } - rc, err := logsForPod(typedClient, pod, logsPanel.Container) + rc, err := logsForPod(typedClient, pod, log.Container()) if err != nil { return nil, fmt.Errorf("error getting logs for pod: %w", err) } return &logDatastream{ logReadCloser: rc, - logPanel: logs, + contentAdder: log, }, nil } - mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + mapping, err := restMapper.RESTMapping(log.GVK().GroupKind(), log.GVK().Version) if err != nil { return nil, fmt.Errorf("error creating resource mapping: %w", err) } - u, err := dynamicClient.Resource(mapping.Resource).Namespace(logsPanel.Key.Namespace).Get(context.Background(), logsPanel.Key.Name, metav1.GetOptions{}) + u, err := dynamicClient.Resource(mapping.Resource).Namespace(log.Key().Namespace).Get(context.Background(), log.Key().Name, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("error getting object: %w", err) } @@ -79,13 +79,13 @@ func LogsDatastreamFunc(typedClient *kubernetes.Clientset, dynamicClient *dynami return nil, fmt.Errorf("no pods found for object") } pod := &pods.Items[0] - rc, err := logsForPod(typedClient, pod, logsPanel.Container) + rc, err := logsForPod(typedClient, pod, log.Container()) if err != nil { return nil, fmt.Errorf("error getting logs for pod: %w", err) } return &logDatastream{ logReadCloser: rc, - logPanel: logs, + contentAdder: log, }, nil } } @@ -110,10 +110,14 @@ func getPodSelectorForUnstructured(u *unstructured.Unstructured) (labels.Selecto return metav1.LabelSelectorAsSelector(sel) } -func streamLogs(rc io.ReadCloser, logPanel *panels.Logs) { +type ContentAdder interface { + AddContent(string) +} + +func streamLogs(rc io.ReadCloser, ca ContentAdder) { scanner := bufio.NewScanner(rc) for scanner.Scan() { - logPanel.AddContent(scanner.Text()) + ca.AddContent(scanner.Text()) } } diff --git a/pkg/factories/datastream/table.go b/pkg/factories/datastream/table.go index 9bbf599..6025b19 100644 --- a/pkg/factories/datastream/table.go +++ b/pkg/factories/datastream/table.go @@ -4,37 +4,41 @@ import ( "fmt" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" + "github.com/everettraven/buoy/pkg/charm/models/panels/table" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/yaml" ) +type Table interface { + GVK() schema.GroupVersionKind + AddOrUpdate(*unstructured.Unstructured) + DeleteRow(types.UID) + Namespace() string + LabelSelector() labels.Set + SetViewActionFunc(table.ViewActionFunc) +} + func TableDatastreamFunc(dynamicClient *dynamic.DynamicClient, restMapper meta.RESTMapper) DatastreamFactoryFunc { - return func(m tea.Model) (Datastream, error) { - if _, ok := m.(*panels.Table); !ok { + return func(obj interface{}) (Datastream, error) { + tbl, ok := obj.(Table) + if !ok { return nil, &InvalidPanelType{fmt.Errorf("model is not of type *panels.Table")} } - table := m.(*panels.Table) - tableDef := table.TableDefinition() - // create informer and event handler - gvk := schema.GroupVersionKind{ - Group: tableDef.Group, - Version: tableDef.Version, - Kind: tableDef.Kind, - } - mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + + mapping, err := restMapper.RESTMapping(tbl.GVK().GroupKind(), tbl.GVK().Version) if err != nil { return nil, fmt.Errorf("error creating resource mapping: %w", err) } - ns := tableDef.Namespace + ns := tbl.Namespace() if mapping.Scope.Name() == meta.RESTScopeNameRoot { ns = "" } @@ -43,7 +47,7 @@ func TableDatastreamFunc(dynamicClient *dynamic.DynamicClient, restMapper meta.R 1*time.Minute, ns, dynamicinformer.TweakListOptionsFunc(func(options *v1.ListOptions) { - ls := labels.SelectorFromSet(tableDef.LabelSelector) + ls := labels.SelectorFromSet(tbl.LabelSelector()) options.LabelSelector = ls.String() }), ) @@ -52,24 +56,43 @@ func TableDatastreamFunc(dynamicClient *dynamic.DynamicClient, restMapper meta.R _, err = inf.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { u := obj.(*unstructured.Unstructured) - table.AddOrUpdate(u) + tbl.AddOrUpdate(u) }, UpdateFunc: func(oldObj, newObj interface{}) { u := newObj.(*unstructured.Unstructured) - table.AddOrUpdate(u) + tbl.AddOrUpdate(u) }, DeleteFunc: func(obj interface{}) { u := obj.(*unstructured.Unstructured) - table.DeleteRow(u.GetUID()) + tbl.DeleteRow(u.GetUID()) }, }) - if err != nil { return nil, err } + tbl.SetViewActionFunc(func(row *table.RowInfo) (string, error) { + name := row.Identifier.String() + if mapping.Scope.Name() == meta.RESTScopeNameRoot { + name = row.Identifier.Name + } - table.SetLister(inf.Lister()) - table.SetScope(mapping.Scope.Name()) + obj, err := inf.Lister().Get(name) + if err != nil { + return "", fmt.Errorf("fetching definition for %q: %w", name, err) + } + + itemJSON, err := obj.(*unstructured.Unstructured).MarshalJSON() + if err != nil { + return "", fmt.Errorf("error marshalling item %q: %w", name, err) + } + + itemYAML, err := yaml.JSONToYAML(itemJSON) + if err != nil { + return "", fmt.Errorf("converting JSON to YAML for item %q: %w", name, err) + } + + return string(itemYAML), nil + }) return inf.Informer(), nil } } diff --git a/pkg/factories/panel/item.go b/pkg/factories/panel/item.go index d8da189..9500deb 100644 --- a/pkg/factories/panel/item.go +++ b/pkg/factories/panel/item.go @@ -6,15 +6,14 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/panels/item" "github.com/everettraven/buoy/pkg/types" ) var _ PanelFactory = &Item{} type Item struct { - theme styles.Theme + theme item.Styles } func (t *Item) ModelForPanel(panel types.Panel) (tea.Model, error) { @@ -27,7 +26,7 @@ func (t *Item) ModelForPanel(panel types.Panel) (tea.Model, error) { return iw, nil } -func (t *Item) modelWrapperForItemPanel(itemPanel types.Item) *panels.Item { +func (t *Item) modelWrapperForItemPanel(itemPanel types.Item) *item.Model { vp := viewport.New(100, 20) - return panels.NewItem(itemPanel, vp, t.theme) + return item.New(itemPanel, vp, t.theme) } diff --git a/pkg/factories/panel/logs.go b/pkg/factories/panel/logs.go index d2a4bb1..ac71cc8 100644 --- a/pkg/factories/panel/logs.go +++ b/pkg/factories/panel/logs.go @@ -5,15 +5,14 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/panels/logs" "github.com/everettraven/buoy/pkg/types" ) var _ PanelFactory = &Log{} type Log struct { - theme styles.Theme + theme logs.Styles } func (t *Log) ModelForPanel(panel types.Panel) (tea.Model, error) { @@ -22,6 +21,6 @@ func (t *Log) ModelForPanel(panel types.Panel) (tea.Model, error) { if err != nil { return nil, fmt.Errorf("unmarshalling panel to table type: %s", err) } - logPanel := panels.NewLogs(panels.DefaultLogsKeys, log, t.theme) + logPanel := logs.New(logs.DefaultKeys, log, t.theme) return logPanel, nil } diff --git a/pkg/factories/panel/panelfactory.go b/pkg/factories/panel/panelfactory.go index b67738a..15c6014 100644 --- a/pkg/factories/panel/panelfactory.go +++ b/pkg/factories/panel/panelfactory.go @@ -4,6 +4,9 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" + "github.com/everettraven/buoy/pkg/charm/models/panels/item" + "github.com/everettraven/buoy/pkg/charm/models/panels/logs" + "github.com/everettraven/buoy/pkg/charm/models/panels/table" "github.com/everettraven/buoy/pkg/charm/styles" "github.com/everettraven/buoy/pkg/types" ) @@ -28,9 +31,21 @@ func (p *paneler) ModelForPanel(panel types.Panel) (tea.Model, error) { func NewPanelFactory(theme styles.Theme) PanelFactory { return &paneler{ panelerRegistry: map[string]PanelFactory{ - types.PanelTypeTable: &Table{theme: theme}, - types.PanelTypeItem: &Item{theme: theme}, - types.PanelTypeLogs: &Log{theme: theme}, + types.PanelTypeTable: &Table{theme: table.Styles{ + SelectedRow: theme.TableSelectedRowStyle(), + SyntaxHighlightDark: theme.SyntaxHighlightDarkTheme, + SyntaxHighlightLight: theme.SyntaxHighlightLightTheme, + }}, + types.PanelTypeItem: &Item{theme: item.Styles{ + SyntaxHighlightDark: theme.SyntaxHighlightDarkTheme, + SyntaxHighlightLight: theme.SyntaxHighlightLightTheme, + }}, + types.PanelTypeLogs: &Log{theme: logs.Styles{ + SearchPrompt: "> ", + SearchPlaceholder: "query", + SearchModeStyle: theme.LogSearchModeStyle(), + SearchMatchHighlightStyle: theme.LogSearchHighlightStyle(), + }}, }, } } diff --git a/pkg/factories/panel/panelfactory_test.go b/pkg/factories/panel/panelfactory_test.go index 923699d..069511a 100644 --- a/pkg/factories/panel/panelfactory_test.go +++ b/pkg/factories/panel/panelfactory_test.go @@ -3,7 +3,9 @@ package panel import ( "testing" - "github.com/everettraven/buoy/pkg/charm/models/panels" + "github.com/everettraven/buoy/pkg/charm/models/panels/item" + "github.com/everettraven/buoy/pkg/charm/models/panels/logs" + "github.com/everettraven/buoy/pkg/charm/models/panels/table" "github.com/everettraven/buoy/pkg/charm/styles" "github.com/everettraven/buoy/pkg/types" "github.com/stretchr/testify/assert" @@ -51,7 +53,7 @@ func TestTablePanel(t *testing.T) { tbl, err := panelFactory.ModelForPanel(*panel) assert.NoError(t, err) assert.NotNil(t, tbl) - assert.IsType(t, &panels.Table{}, tbl) + assert.IsType(t, &table.Model{}, tbl) } func TestItemPanel(t *testing.T) { @@ -64,7 +66,7 @@ func TestItemPanel(t *testing.T) { "key": { "namespace": "kube-system", "name": "kube-apiserver-kind-control-plane" - } + } }` panel := &types.Panel{} @@ -72,10 +74,10 @@ func TestItemPanel(t *testing.T) { assert.NoError(t, err) panelFactory := NewPanelFactory(styles.Theme{}) - item, err := panelFactory.ModelForPanel(*panel) + itemModel, err := panelFactory.ModelForPanel(*panel) assert.NoError(t, err) - assert.NotNil(t, item) - assert.IsType(t, &panels.Item{}, item) + assert.NotNil(t, itemModel) + assert.IsType(t, &item.Model{}, itemModel) } func TestLogPanel(t *testing.T) { @@ -88,7 +90,7 @@ func TestLogPanel(t *testing.T) { "key": { "namespace": "kube-system", "name": "kube-apiserver-kind-control-plane" - } + } }` panel := &types.Panel{} @@ -99,5 +101,5 @@ func TestLogPanel(t *testing.T) { log, err := panelFactory.ModelForPanel(*panel) assert.NoError(t, err) assert.NotNil(t, log) - assert.IsType(t, &panels.Logs{}, log) + assert.IsType(t, &logs.Model{}, log) } diff --git a/pkg/factories/panel/table.go b/pkg/factories/panel/table.go index d955072..e9b15d7 100644 --- a/pkg/factories/panel/table.go +++ b/pkg/factories/panel/table.go @@ -5,15 +5,14 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/everettraven/buoy/pkg/charm/models/panels" - "github.com/everettraven/buoy/pkg/charm/styles" + "github.com/everettraven/buoy/pkg/charm/models/panels/table" buoytypes "github.com/everettraven/buoy/pkg/types" ) var _ PanelFactory = &Table{} type Table struct { - theme styles.Theme + theme table.Styles } func (t *Table) ModelForPanel(panel buoytypes.Panel) (tea.Model, error) { @@ -22,6 +21,6 @@ func (t *Table) ModelForPanel(panel buoytypes.Panel) (tea.Model, error) { if err != nil { return nil, fmt.Errorf("unmarshalling panel to table type: %s", err) } - table := panels.NewTable(panels.DefaultTableKeys, tab, t.theme) + table := table.New(table.DefaultKeys, tab, t.theme) return table, nil }