From 9e5918c853b46abce167e3ff429e80cdd002fcb6 Mon Sep 17 00:00:00 2001 From: Anatoly Rugalev Date: Tue, 8 Sep 2020 02:24:22 +0300 Subject: [PATCH] feat(themes): add themes support --- README.md | 6 +- app/app.go | 16 +- app/client/default.go | 36 +- app/ui/border/border.go | 39 +- app/ui/border/line.go | 9 +- app/ui/err/error.go | 34 -- app/ui/help/help.go | 17 +- app/ui/resourceMenu/resource_menu.go | 6 +- app/ui/resourceMenu/resource_picker.go | 2 +- app/ui/resources/pod/container_picker.go | 6 +- app/ui/resources/pod/port_picker.go | 6 +- app/ui/screen.go | 52 +-- app/ui/status/status.go | 53 +-- app/ui/theme/manager.go | 311 ++++++++-------- app/ui/theme/theme.go | 14 - app/ui/theme/themes/base16.go | 38 ++ app/ui/theme/themes/monokai.go | 36 ++ app/ui/theme/themes/paraiso.go | 36 ++ app/ui/theme/themes/solarized.go | 36 ++ app/ui/theme/themes/themes.go | 134 +++++++ app/ui/theme/themes/twilight.go | 36 ++ app/ui/widgets/listTable/listTable.go | 122 +++---- app/ui/widgets/listTable/preloader.go | 14 +- app/ui/widgets/listTable/resource.go | 2 +- app/ui/widgets/listTable/static.go | 4 +- app/ui/widgets/logo/logo.go | 46 +++ app/ui/widgets/popup/popup.go | 5 +- app/ui/workspace/workspace.go | 26 +- commander/listView.go | 2 - commander/resource.go | 2 +- commander/screen.go | 7 +- commander/theme.go | 31 +- commander/widget.go | 3 - commander/workspace.go | 1 + config/config.go | 4 + pb/config.pb.go | 444 +++++++++++++++++++++-- pb/config.proto | 39 ++ 37 files changed, 1175 insertions(+), 500 deletions(-) delete mode 100644 app/ui/err/error.go delete mode 100644 app/ui/theme/theme.go create mode 100644 app/ui/theme/themes/base16.go create mode 100644 app/ui/theme/themes/monokai.go create mode 100644 app/ui/theme/themes/paraiso.go create mode 100644 app/ui/theme/themes/solarized.go create mode 100644 app/ui/theme/themes/themes.go create mode 100644 app/ui/theme/themes/twilight.go create mode 100644 app/ui/widgets/logo/logo.go diff --git a/README.md b/README.md index d88f1e0..26f1e82 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,9 @@ kubecom --log-pager="jq -c | some_other_command" ### Configuration file -You can edit configuration file at `~/.kubecom.yaml` which currently stores menu configuration. Themes support is -coming soon. Configuration schema defined as protobuf in [pb](./pb) directory. +You can edit configuration file at `~/.kubecom.yaml` to modify resource menu titles and themes. Usually you don't need +to edit config manually: it updates automatically when you change resource menu items or switch theme. You can get +familiar with configuration capabilities inspecting [pb/config.proto](pb/config.proto) protobuf file. ### Hotkeys @@ -175,6 +176,7 @@ The most of hotkeys you can find on help dialog. Here they are: | S | Enter to container `/bin/sh` shell | | + (plus) | Add resource type to the menu | | F6, F7 | Move resource type up/down in menu | +| F10, F11 | Cycle through themes | ## Contribution diff --git a/app/app.go b/app/app.go index de1f27b..22e553c 100644 --- a/app/app.go +++ b/app/app.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/AnatolyRugalev/kube-commander/app/ui" "github.com/AnatolyRugalev/kube-commander/app/ui/status" + "github.com/AnatolyRugalev/kube-commander/app/ui/theme" "github.com/AnatolyRugalev/kube-commander/app/ui/workspace" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/AnatolyRugalev/kube-commander/config" @@ -123,13 +124,17 @@ func (a *app) Run() error { return fmt.Errorf("could not initialize screen: %w", err) } a.workspace = workspace.NewWorkspace(a, a.defaultNamespace) + a.screen.SetWorkspace(a.workspace) + + a.status = status.NewStatus(a.screen) + themeManager := theme.NewManager(a.screen, a.status, a) + a.Register(themeManager) + + a.screen.Init(a.status, themeManager) err = a.workspace.Init() if err != nil { return err } - a.status = status.NewStatus(a.screen) - a.screen.SetWorkspace(a.workspace) - a.screen.SetStatus(a.status) a.tApp.Start() go a.watchConfig() @@ -141,16 +146,11 @@ func (a *app) Run() error { } func (a *app) watchConfig() { - firstTime := true for event := range a.configCh { if event.Err != nil { a.status.Error(fmt.Errorf("config: %w", event.Err)) continue } - if !firstTime { - a.status.Info("Configuration was updated!") - } - firstTime = false for _, c := range a.configurables { c.ConfigUpdated(event.Config) } diff --git a/app/client/default.go b/app/client/default.go index 1f4742a..3a0c473 100644 --- a/app/client/default.go +++ b/app/client/default.go @@ -1,27 +1,20 @@ package client -import ( - "fmt" - "k8s.io/client-go/rest" - cmd "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" -) - type defaultConfig struct { kubeconfig string namespace string context string } -func (d defaultConfig) Context() string { +func (d *defaultConfig) Context() string { return d.context } -func (d defaultConfig) Kubeconfig() string { +func (d *defaultConfig) Kubeconfig() string { return d.kubeconfig } -func (d defaultConfig) Namespace() string { +func (d *defaultConfig) Namespace() string { return d.namespace } @@ -32,26 +25,3 @@ func NewDefaultConfig(kubeconfig string, context string, namespace string) *defa namespace: namespace, } } - -func (d *defaultConfig) ClientConfig() (*rest.Config, error) { - rules := cmd.NewDefaultClientConfigLoadingRules() - config, err := rules.Load() - if err != nil { - return nil, fmt.Errorf("error loading config: %w", err) - } - if d.context == "" { - d.context = config.CurrentContext - } - if ctx, ok := config.Contexts[config.CurrentContext]; ok && d.namespace == "" { - d.namespace = ctx.Namespace - } - if d.namespace == "" { - d.namespace = "default" - } - clientConfig := cmd.NewNonInteractiveClientConfig(*config, d.Context(), &cmd.ConfigOverrides{ - Context: clientcmdapi.Context{ - Namespace: d.namespace, - }, - }, rules) - return clientConfig.ClientConfig() -} diff --git a/app/ui/border/border.go b/app/ui/border/border.go index 0937894..c8f7dac 100644 --- a/app/ui/border/border.go +++ b/app/ui/border/border.go @@ -2,7 +2,6 @@ package border import ( "github.com/AnatolyRugalev/kube-commander/commander" - "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" ) @@ -31,8 +30,9 @@ type BorderedWidget struct { commander.MaxSizeWidget title string view views.View - style tcell.Style - titleStyle tcell.Style + theme commander.ThemeManager + style string + titleStyle string borders Borders } @@ -40,20 +40,21 @@ func (b Borders) Has(flag Borders) bool { return b&flag == flag } -func NewBorderedWidget(widget commander.MaxSizeWidget, title string, style tcell.Style, titleStyle tcell.Style, borders Borders) *BorderedWidget { +func NewBorderedWidget(widget commander.MaxSizeWidget, title string, theme commander.ThemeManager, style string, titleStyle string, borders Borders) *BorderedWidget { if borders == 0 { borders = All } return &BorderedWidget{ MaxSizeWidget: widget, title: title, + theme: theme, style: style, titleStyle: titleStyle, borders: borders, } } -func (b BorderedWidget) Draw() { +func (b *BorderedWidget) Draw() { w, h := b.view.Size() x0 := 0 @@ -65,47 +66,49 @@ func (b BorderedWidget) Draw() { y0++ } + style := b.theme.GetStyle(b.style) + titleStyle := b.theme.GetStyle(b.titleStyle) for y := 1; y < h-1; y++ { if b.borders.Has(Left) { - b.view.SetContent(0, y, vertical, nil, b.style) + b.view.SetContent(0, y, vertical, nil, style) } if b.borders.Has(Right) { - b.view.SetContent(w-1, y, vertical, nil, b.style) + b.view.SetContent(w-1, y, vertical, nil, style) } } for x := 1; x < w-1; x++ { if b.borders.Has(Top) { - b.view.SetContent(x, 0, horizontal, nil, b.style) + b.view.SetContent(x, 0, horizontal, nil, style) } if b.borders.Has(Bottom) { - b.view.SetContent(x, h-1, horizontal, nil, b.style) + b.view.SetContent(x, h-1, horizontal, nil, style) } } if b.title != "" && b.borders.Has(Top) { - b.view.SetContent(x0, 0, titleLeft, nil, b.style) + b.view.SetContent(x0, 0, titleLeft, nil, style) for i, r := range b.title { - b.view.SetContent(i+x0+1, 0, r, nil, b.titleStyle) + b.view.SetContent(i+x0+1, 0, r, nil, titleStyle) } - b.view.SetContent(x0+len(b.title)+1, 0, titleRight, nil, b.style) + b.view.SetContent(x0+len(b.title)+1, 0, titleRight, nil, style) } if b.borders.Has(Top | Left) { - b.view.SetContent(0, 0, cornerTopLeft, nil, b.style) + b.view.SetContent(0, 0, cornerTopLeft, nil, style) } if b.borders.Has(Top | Right) { - b.view.SetContent(w-1, 0, cornerTopRight, nil, b.style) + b.view.SetContent(w-1, 0, cornerTopRight, nil, style) } if b.borders.Has(Bottom | Left) { - b.view.SetContent(0, h-1, cornerBottomLeft, nil, b.style) + b.view.SetContent(0, h-1, cornerBottomLeft, nil, style) } if b.borders.Has(Bottom | Right) { - b.view.SetContent(w-1, h-1, cornerBottomRight, nil, b.style) + b.view.SetContent(w-1, h-1, cornerBottomRight, nil, style) } b.MaxSizeWidget.Draw() } -func (b BorderedWidget) offsets() (int, int) { +func (b *BorderedWidget) offsets() (int, int) { offsetH := 0 offsetW := 0 if b.borders.Has(Top) { @@ -146,7 +149,7 @@ func (b *BorderedWidget) SetView(view views.View) { b.MaxSizeWidget.SetView(viewport) } -func (b BorderedWidget) MaxSize() (int, int) { +func (b *BorderedWidget) MaxSize() (int, int) { w, h := b.MaxSizeWidget.MaxSize() offsetW, offsetH := b.offsets() if b.title != "" && len(b.title)+2 > w { diff --git a/app/ui/border/line.go b/app/ui/border/line.go index 78bf7c7..6bd3288 100644 --- a/app/ui/border/line.go +++ b/app/ui/border/line.go @@ -2,6 +2,7 @@ package border import ( "github.com/AnatolyRugalev/kube-commander/app/focus" + "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" ) @@ -11,18 +12,18 @@ type VerticalLine struct { views.WidgetWatchers view views.View - style tcell.Style + theme commander.ThemeManager } -func NewVerticalLine(style tcell.Style) *VerticalLine { +func NewVerticalLine(theme commander.ThemeManager) *VerticalLine { return &VerticalLine{ Focusable: focus.NewFocusable(), - style: style, + theme: theme, } } func (l VerticalLine) Draw() { - l.view.Fill(vertical, l.style) + l.view.Fill(vertical, l.theme.GetStyle("screen")) } func (l VerticalLine) Resize() { diff --git a/app/ui/err/error.go b/app/ui/err/error.go deleted file mode 100644 index 09b598d..0000000 --- a/app/ui/err/error.go +++ /dev/null @@ -1,34 +0,0 @@ -package err - -import ( - "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" - "github.com/AnatolyRugalev/kube-commander/commander" - "github.com/gdamore/tcell/views" - "github.com/kr/text" -) - -type widget struct { - *views.Text - *focus.Focusable -} - -func (w widget) MaxSize() (int, int) { - return w.Text.Size() -} - -func NewErrorWidget(err error) *widget { - widget := widget{ - Text: views.NewText(), - Focusable: focus.NewFocusable(), - } - var txt string - if execErr, ok := err.(*commander.ExecErr); ok { - txt = text.Wrap(err.Error()+"\n"+string(execErr.Output), 50) - } else { - txt = text.Wrap(err.Error(), 50) - } - widget.Text.SetText(txt) - widget.Text.SetStyle(theme.Default) - return &widget -} diff --git a/app/ui/help/help.go b/app/ui/help/help.go index 29c9866..398b1db 100644 --- a/app/ui/help/help.go +++ b/app/ui/help/help.go @@ -2,7 +2,6 @@ package help import ( "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell/views" ) @@ -10,6 +9,7 @@ import ( type widget struct { *views.Text *focus.Focusable + theme commander.ThemeManager } func (w widget) MaxSize() (int, int) { @@ -21,13 +21,13 @@ func (w widget) Size() (int, int) { return width, 1 } -var text = `kube-commander - browse your Kubernetes cluster in a casual way! +var text = `kubecom - browse your Kubernetes cluster in a casual way! Global: D: Describe selected resource ?: Shows help dialog E: Edit selected resource Q: Quit C: Copy resource name to the clipboard Ctrl+N or F2: Switch namespace - Del: Delete resource (with confirmation) + Del: Delete resource (with confirmation) F10, F11: Cycle through themes Navigation: ↑↓→←: List navigation /: Filter resources @@ -43,17 +43,22 @@ Pods: S: Shell into selected pod ` -func NewHelpWidget() *widget { +func NewHelpWidget(theme commander.ThemeManager) *widget { widget := widget{ Text: views.NewText(), Focusable: focus.NewFocusable(), + theme: theme, } widget.Text.SetText(text) - widget.Text.SetStyle(theme.Default) return &widget } +func (w *widget) Draw() { + w.Text.SetStyle(w.theme.GetStyle("screen")) + w.Text.Draw() +} + func ShowHelpPopup(workspace commander.Workspace) { - help := NewHelpWidget() + help := NewHelpWidget(workspace.Theme()) workspace.ShowPopup("Help", help) } diff --git a/app/ui/resourceMenu/resource_menu.go b/app/ui/resourceMenu/resource_menu.go index efa3271..582789a 100644 --- a/app/ui/resourceMenu/resource_menu.go +++ b/app/ui/resourceMenu/resource_menu.go @@ -135,7 +135,7 @@ func newItemFromPb(res *pb.Resource) *resourceItem { func NewResourcesMenu(workspace commander.Workspace, onSelect SelectFunc, selectNamespace func(), resourceProvider commander.ResourceProvider) (*ResourceMenu, error) { prov := make(commander.RowProvider) - lt := listTable.NewListTable(prov, listTable.NoHorizontalScroll|listTable.WithFilter, workspace.ScreenUpdater()) + lt := listTable.NewListTable(prov, listTable.NoHorizontalScroll|listTable.WithFilter, workspace.ScreenHandler()) r := &ResourceMenu{ ListTable: lt, onSelect: onSelect, @@ -237,7 +237,7 @@ func (r *ResourceMenu) OnKeyPress(row commander.Row, event *tcell.EventKey) bool return true } else if event.Key() == tcell.KeyDelete { go func() { - if r.workspace.Status().Confirm("Do you want to hide this resource? (y/n)") { + if r.workspace.Status().Confirm("Do you want to hide this resource? (y/N)") { for i, item := range r.items { if item.Id() == row.Id() { r.items = append(r.items[:i], r.items[i+1:]...) @@ -245,6 +245,8 @@ func (r *ResourceMenu) OnKeyPress(row commander.Row, event *tcell.EventKey) bool } } r.saveItems() + } else { + r.workspace.Status().Info("Cancelled.") } }() return true diff --git a/app/ui/resourceMenu/resource_picker.go b/app/ui/resourceMenu/resource_picker.go index c175ced..98925ea 100644 --- a/app/ui/resourceMenu/resource_picker.go +++ b/app/ui/resourceMenu/resource_picker.go @@ -38,7 +38,7 @@ func newResourcePicker(container commander.ResourceContainer, f GroupKindFunc) ( }, true)) resMap[res.Gk.String()] = res } - lt := listTable.NewStaticListTable(columns, rows, listTable.NoHorizontalScroll|listTable.WithFilter|listTable.WithHeaders) + lt := listTable.NewStaticListTable(columns, rows, listTable.NoHorizontalScroll|listTable.WithFilter|listTable.WithHeaders, container.ScreenHandler()) lt.BindOnKeyPress(func(row commander.Row, event *tcell.EventKey) bool { if row == nil { return false diff --git a/app/ui/resources/pod/container_picker.go b/app/ui/resources/pod/container_picker.go index 54a1a10..97ae153 100644 --- a/app/ui/resources/pod/container_picker.go +++ b/app/ui/resources/pod/container_picker.go @@ -19,7 +19,7 @@ func pickPodContainer(workspace commander.Workspace, pod v1.Pod, f ContainerFunc f(pod, pod.Spec.Containers[0], pod.Status.ContainerStatuses[0]) return } - picker := newContainerPicker(pod, func(pod v1.Pod, c v1.Container, status v1.ContainerStatus) { + picker := newContainerPicker(workspace.ScreenHandler(), pod, func(pod v1.Pod, c v1.Container, status v1.ContainerStatus) { workspace.FocusManager().Blur() f(pod, c, status) }) @@ -49,7 +49,7 @@ type picker struct { f ContainerFunc } -func newContainerPicker(pod v1.Pod, f ContainerFunc) *picker { +func newContainerPicker(screen commander.ScreenHandler, pod v1.Pod, f ContainerFunc) *picker { var items []commander.Row for i, status := range pod.Status.InitContainerStatuses { items = append(items, &item{ @@ -64,7 +64,7 @@ func newContainerPicker(pod v1.Pod, f ContainerFunc) *picker { }) } picker := &picker{ - ListTable: listTable.NewStaticListTable([]string{"Container", "Status"}, items, listTable.WithHeaders), + ListTable: listTable.NewStaticListTable([]string{"Container", "Status"}, items, listTable.WithHeaders, screen), pod: pod, f: f, } diff --git a/app/ui/resources/pod/port_picker.go b/app/ui/resources/pod/port_picker.go index 243a365..229c1c5 100644 --- a/app/ui/resources/pod/port_picker.go +++ b/app/ui/resources/pod/port_picker.go @@ -13,7 +13,7 @@ import ( type PortFunc func(pod v1.Pod, container v1.Container, port v1.ContainerPort) func pickPodPort(workspace commander.Workspace, pod v1.Pod, f PortFunc) { - picker, err := newPortPicker(pod, func(pod v1.Pod, c v1.Container, port v1.ContainerPort) { + picker, err := newPortPicker(workspace.ScreenHandler(), pod, func(pod v1.Pod, c v1.Container, port v1.ContainerPort) { workspace.FocusManager().Blur() f(pod, c, port) }) @@ -48,7 +48,7 @@ type portPicker struct { f PortFunc } -func newPortPicker(pod v1.Pod, f PortFunc) (*portPicker, error) { +func newPortPicker(screen commander.ScreenHandler, pod v1.Pod, f PortFunc) (*portPicker, error) { var items []commander.Row for i, status := range pod.Status.ContainerStatuses { container := pod.Spec.Containers[i] @@ -64,7 +64,7 @@ func newPortPicker(pod v1.Pod, f PortFunc) (*portPicker, error) { return nil, errors.New("this pod doesn't have any defined or active ports") } picker := &portPicker{ - ListTable: listTable.NewStaticListTable([]string{"Container", "Status", "Port"}, items, listTable.WithHeaders), + ListTable: listTable.NewStaticListTable([]string{"Container", "Status", "Port"}, items, listTable.WithHeaders, screen), pod: pod, f: f, } diff --git a/app/ui/screen.go b/app/ui/screen.go index c85031d..46e9ffd 100644 --- a/app/ui/screen.go +++ b/app/ui/screen.go @@ -2,7 +2,7 @@ package ui import ( "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" + "github.com/AnatolyRugalev/kube-commander/app/ui/widgets/logo" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" @@ -17,6 +17,8 @@ type Screen struct { status commander.StatusReporter view commander.View theme commander.ThemeManager + title *views.BoxLayout + titleBar *views.TextBar } func (s *Screen) View() commander.View { @@ -28,11 +30,13 @@ func (s *Screen) SetView(view views.View) { s.Panel.SetView(view) } -func (s *Screen) SetStatus(stat commander.StatusReporter) { - s.status = stat +func (s *Screen) Init(status commander.StatusReporter, theme commander.ThemeManager) { + s.status = status s.Panel.SetStatus(s.status) - - s.theme = theme.NewManager(s.workspace.FocusManager(), s, s.status) + s.theme = theme + s.title.AddWidget(logo.NewLogo(s.theme), 0) + s.title.AddWidget(s.titleBar, 1.0) + s.SetTitle(s.title) } func (s *Screen) UpdateScreen() { @@ -46,7 +50,7 @@ func (s *Screen) SetWorkspace(workspace commander.Workspace) { s.SetContent(s.workspace) } -func (s Screen) Workspace() commander.Workspace { +func (s *Screen) Workspace() commander.Workspace { return s.workspace } @@ -55,24 +59,23 @@ func NewScreen(app commander.App) *Screen { Panel: views.NewPanel(), Focusable: focus.NewFocusable(), app: app, + title: views.NewBoxLayout(views.Horizontal), + titleBar: views.NewTextBar(), } + return &s +} - title := views.NewTextBar() - title.SetStyle(tcell.StyleDefault. - Background(tcell.ColorTeal). - Foreground(tcell.ColorWhite)) - title.SetCenter("kube-commander", theme.Default) - title.SetRight(app.Config().Context(), theme.Default) - - s.SetTitle(title) +func (s *Screen) Draw() { + s.titleBar.SetStyle(s.theme.GetStyle("title-bar")) + s.Panel.SetStyle(s.theme.GetStyle("screen")) + s.Panel.Draw() +} - return &s +func (s *Screen) Theme() commander.ThemeManager { + return s.theme } -func (s Screen) HandleEvent(e tcell.Event) bool { - if s.theme.HandleEvent(e) { - return true - } +func (s *Screen) HandleEvent(e tcell.Event) bool { if s.BoxLayout.HandleEvent(e) { return true } @@ -83,13 +86,12 @@ func (s Screen) HandleEvent(e tcell.Event) bool { return true } switch ev.Key() { - case tcell.KeyF10: - err := s.theme.Init() - if err != nil { - s.status.Error(err) - } case tcell.KeyF11: - s.theme.DeInit() + s.theme.NextTheme() + return true + case tcell.KeyF10: + s.theme.PrevTheme() + return true } } return false diff --git a/app/ui/status/status.go b/app/ui/status/status.go index 17ef0d5..df964c7 100644 --- a/app/ui/status/status.go +++ b/app/ui/status/status.go @@ -2,7 +2,6 @@ package status import ( "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" @@ -13,20 +12,12 @@ import ( type Status struct { *focus.Focusable *views.Text - updater commander.ScreenUpdater + screen commander.ScreenHandler events chan *tcell.EventKey - style tcell.Style clearIn time.Time mu sync.Mutex } -func (s *Status) Error(err error) { - s.SetText(err.Error()) - s.SetStyle(s.style.Foreground(tcell.ColorDarkRed)) - s.updater.UpdateScreen() - s.ClearIn(time.Second * 10) -} - func (s *Status) watch() { ticker := time.NewTicker(time.Millisecond * 100) for { @@ -34,16 +25,16 @@ func (s *Status) watch() { s.mu.Lock() if !s.clearIn.IsZero() && s.clearIn.Before(t) { s.Clear() - s.clearIn = time.Time{} } s.mu.Unlock() } } func (s *Status) Clear() { + s.clearIn = time.Time{} s.SetText("") - s.SetStyle(s.style) - s.updater.UpdateScreen() + s.SetStyle(s.screen.Theme().GetStyle("status-bar")) + s.screen.UpdateScreen() } func (s *Status) ClearIn(duration time.Duration) { @@ -53,19 +44,29 @@ func (s *Status) ClearIn(duration time.Duration) { } func (s *Status) Warning(msg string) { + s.Clear() s.SetText(msg) - s.SetStyle(s.style.Foreground(tcell.ColorOrange)) - s.updater.UpdateScreen() + s.SetStyle(s.screen.Theme().GetStyle("status-warning")) + s.screen.UpdateScreen() s.ClearIn(time.Second * 5) } func (s *Status) Info(msg string) { + s.Clear() s.SetText(msg) - s.SetStyle(s.style.Foreground(tcell.ColorYellow)) - s.updater.UpdateScreen() + s.SetStyle(s.screen.Theme().GetStyle("status-info")) + s.screen.UpdateScreen() s.ClearIn(time.Second * 2) } +func (s *Status) Error(err error) { + s.Clear() + s.SetText(err.Error()) + s.SetStyle(s.screen.Theme().GetStyle("status-error")) + s.screen.UpdateScreen() + s.ClearIn(time.Second * 10) +} + func (s *Status) HandleEvent(ev tcell.Event) bool { if e, ok := ev.(*tcell.EventKey); ok { select { @@ -78,9 +79,10 @@ func (s *Status) HandleEvent(ev tcell.Event) bool { } func (s *Status) Confirm(msg string) bool { + s.Clear() s.SetText(msg) - s.SetStyle(s.style.Foreground(tcell.ColorYellow)) - s.updater.UpdateScreen() + s.SetStyle(s.screen.Theme().GetStyle("status-confirm")) + s.screen.UpdateScreen() ev := <-s.events switch ev.Rune() { case 'y', 'Y': @@ -89,19 +91,24 @@ func (s *Status) Confirm(msg string) bool { return false } +func (s *Status) Draw() { + if s.Style() == tcell.StyleDefault { + s.SetStyle(s.screen.Theme().GetStyle("status-bar")) + } + s.Text.Draw() +} + func (s *Status) Size() (int, int) { return 1, 1 } -func NewStatus(updater commander.ScreenUpdater) *Status { +func NewStatus(screen commander.ScreenHandler) *Status { s := &Status{ Focusable: focus.NewFocusable(), Text: views.NewText(), - updater: updater, + screen: screen, events: make(chan *tcell.EventKey), - style: theme.Default.Background(theme.ColorDisabledForeground), } - s.SetStyle(s.style) go s.watch() return s } diff --git a/app/ui/theme/manager.go b/app/ui/theme/manager.go index 6b96790..075e2b1 100644 --- a/app/ui/theme/manager.go +++ b/app/ui/theme/manager.go @@ -1,192 +1,187 @@ package theme import ( - "errors" "fmt" + "github.com/AnatolyRugalev/kube-commander/app/ui/theme/themes" "github.com/AnatolyRugalev/kube-commander/commander" + "github.com/AnatolyRugalev/kube-commander/pb" "github.com/gdamore/tcell" + "google.golang.org/protobuf/proto" + "sync" ) type manager struct { - focus commander.FocusManager - updater commander.ScreenUpdater - reporter commander.StatusReporter + focus commander.FocusManager + screen commander.ScreenHandler + status commander.StatusReporter + config commander.ConfigUpdater - initialized bool - stylable commander.Stylable - component commander.StyleComponent - componentIndex int -} + initialized bool -func NewManager(focus commander.FocusManager, updater commander.ScreenUpdater, reporter commander.StatusReporter) *manager { - return &manager{ - focus: focus, - updater: updater, - reporter: reporter, - } + themeMu sync.RWMutex + themes []*pb.Theme + currentThemeId int + currentTheme *pb.Theme + colors map[string]*pb.Color + styles map[string]*pb.Style } -func (m *manager) Init() error { - widget := m.focus.Current() - stylable, ok := widget.(commander.Stylable) - if !ok { - return errors.New("this widget cannot be stylized") +func ColorToProto(c *commander.Color) *pb.Color { + pbColor := &pb.Color{ + Name: c.Name, } - components := stylable.GetComponents() - if len(components) == 0 { - return errors.New("this widget does not contain components to stylize") + if c.Color&tcell.ColorIsRGB != 0 { + pbColor.Value = &pb.Color_Rgb{ + Rgb: fmt.Sprintf("%06x", c.Color.Hex()), + } + } else { + pbColor.Value = &pb.Color_Xterm{ + Xterm: int32(c.Color), + } } - m.stylable = stylable - m.initialized = true - m.switchComponent(0) - return nil + return pbColor } -func (m *manager) DeInit() { - m.stylable = nil - m.component = nil - m.componentIndex = 0 - m.initialized = false +func ProtoToColor(pbColor *pb.Color) *commander.Color { + c := &commander.Color{ + Name: pbColor.Name, + } + switch t := pbColor.Value.(type) { + case *pb.Color_Rgb: + c.Color = tcell.GetColor("#" + t.Rgb) + case *pb.Color_Xterm: + c.Color = tcell.Color(t.Xterm) + default: + c.Color = tcell.ColorDefault + } + return c } -func (m *manager) HandleEvent(e tcell.Event) bool { - if !m.initialized { - return false +func (m *manager) ConfigUpdated(config *pb.Config) { + current := config.CurrentTheme + if current == "" { + current = "base16" + } + allThemes := themes.DefaultThemes + themeIndex := make(map[string]int) + for i, t := range allThemes { + themeIndex[t.Name] = i + } + for _, t := range config.Themes { + if i, ok := themeIndex[t.Name]; ok { + allThemes[i] = t + } else { + allThemes = append(allThemes, t) + themeIndex[t.Name] = len(allThemes) - 1 + } } - ev, ok := e.(*tcell.EventKey) + i, ok := themeIndex[current] if !ok { - return false - } - switch ev.Key() { - case tcell.KeyPgUp: - m.PrevComponent() - return true - case tcell.KeyPgDn: - m.NextComponent() - return true - case tcell.KeyUp: - m.PrevBg() - return true - case tcell.KeyDown: - m.NextBg() - return true - case tcell.KeyLeft: - m.PrevFg() - return true - case tcell.KeyRight: - m.NextFg() - return true + i = 0 } - - switch ev.Rune() { - case 'b': - m.SwitchAttr(tcell.AttrBold) - return true - case 'l': - m.SwitchAttr(tcell.AttrBlink) - return true - case 'r': - m.SwitchAttr(tcell.AttrReverse) - return true - case 'u': - m.SwitchAttr(tcell.AttrUnderline) - return true - case 'd': - m.SwitchAttr(tcell.AttrDim) - return true + theme := allThemes[i] + m.themeMu.Lock() + m.themes = allThemes + m.currentThemeId = i + changed := m.currentTheme == nil || !proto.Equal(m.currentTheme, theme) + m.currentTheme = theme + m.themeMu.Unlock() + if changed { + m.ApplyTheme(theme) } - - return false -} - -func (m *manager) NextComponent() { - m.switchComponent(m.componentIndex + 1) -} - -func (m *manager) switchComponent(index int) { - components := m.stylable.GetComponents() - if index >= len(components) { - index = 0 - } else if index < 0 { - index = len(components) - 1 - } - m.componentIndex = index - m.component = components[index] - m.reporter.Info(fmt.Sprintf("Editing component: %s", m.component.Name())) - m.updater.UpdateScreen() -} - -func (m manager) PrevComponent() { - m.switchComponent(m.componentIndex - 1) -} - -func (m *manager) NextBg() { - m.setBg(1) } -func (m *manager) setBg(delta int32) { - style := m.component.Style() - _, bg, _ := style.Decompose() - bg = m.shiftColor(bg, delta) - m.component.SetStyle(style.Background(bg)) - m.reporter.Info(fmt.Sprintf("bg color: %d", bg)) - m.updater.UpdateScreen() -} - -func (m *manager) setFg(delta int32) { - style := m.component.Style() - fg, _, _ := style.Decompose() - fg = m.shiftColor(fg, delta) - m.component.SetStyle(style.Foreground(fg)) - m.reporter.Info(fmt.Sprintf("fg color: %d", fg)) - m.updater.UpdateScreen() -} - -func (m *manager) shiftColor(c tcell.Color, delta int32) tcell.Color { - c = c + tcell.Color(delta) - if c > tcell.ColorYellowGreen { - c = tcell.ColorBlack - } else if c < 0 { - c = tcell.ColorYellowGreen +func (m *manager) ApplyTheme(theme *pb.Theme) { + colors := make(map[string]*pb.Color) + styles := make(map[string]*pb.Style) + if len(theme.Styles) == 0 { + theme.Styles = themes.BaseStyles() } - return c -} - -func (m *manager) PrevBg() { - m.setBg(-1) + for _, c := range theme.Colors { + colors[c.Name] = c + } + for _, s := range theme.Styles { + styles[s.Name] = s + } + m.themeMu.Lock() + m.colors = colors + m.styles = styles + m.themeMu.Unlock() + m.screen.UpdateScreen() + m.status.Info(fmt.Sprintf("Applied theme: %s", theme.Name)) +} + +func (m *manager) NextTheme() { + m.themeMu.Lock() + themeId := m.currentThemeId + 1 + if themeId >= len(m.themes) { + themeId = 0 + } + err := m.config.UpdateConfig(func(config *pb.Config) { + config.CurrentTheme = m.themes[themeId].Name + }) + if err != nil { + m.status.Error(err) + } + m.themeMu.Unlock() } -func (m *manager) NextFg() { - m.setFg(1) +func (m *manager) PrevTheme() { + m.themeMu.Lock() + themeId := m.currentThemeId - 1 + if themeId < 0 { + themeId = len(m.themes) - 1 + } + err := m.config.UpdateConfig(func(config *pb.Config) { + config.CurrentTheme = m.themes[themeId].Name + }) + if err != nil { + m.status.Error(err) + } + m.themeMu.Unlock() } -func (m *manager) PrevFg() { - m.setFg(-1) +func NewManager(screen commander.ScreenHandler, status commander.StatusReporter, config commander.ConfigUpdater) *manager { + return &manager{ + screen: screen, + status: status, + config: config, + } } -func (m *manager) SwitchAttr(a tcell.AttrMask) { - style := m.component.Style() - _, _, attr := style.Decompose() - on := attr&a == 0 - switch a { - case tcell.AttrBold: - style = style.Bold(on) - m.reporter.Info(fmt.Sprintf("bold: %v", on)) - case tcell.AttrBlink: - style = style.Blink(on) - m.reporter.Info(fmt.Sprintf("blink: %v", on)) - case tcell.AttrReverse: - style = style.Reverse(on) - m.reporter.Info(fmt.Sprintf("reverse: %v", on)) - case tcell.AttrUnderline: - style = style.Underline(on) - m.reporter.Info(fmt.Sprintf("underline: %v", on)) - case tcell.AttrDim: - style = style.Dim(on) - m.reporter.Info(fmt.Sprintf("dim: %v", on)) - default: - return +func (m *manager) GetStyle(name string) commander.Style { + style := tcell.StyleDefault + m.themeMu.RLock() + pbStyle, ok := m.styles[name] + m.themeMu.RUnlock() + if !ok { + return style + } + fg := tcell.ColorDefault + bg := tcell.ColorDefault + fgColor, ok := m.colors[pbStyle.Fg] + if ok { + fg = ProtoToColor(fgColor).Color + } + style = style.Foreground(fg) + bgColor, ok := m.colors[pbStyle.Bg] + if ok { + bg = ProtoToColor(bgColor).Color + } + style = style.Background(bg) + for _, attr := range pbStyle.Attrs { + switch attr { + case pb.StyleAttribute_BOLD: + style = style.Bold(true) + case pb.StyleAttribute_BLINK: + style = style.Blink(true) + case pb.StyleAttribute_REVERSE: + style = style.Reverse(true) + case pb.StyleAttribute_UNDERLINE: + style = style.Underline(true) + case pb.StyleAttribute_DIM: + style = style.Dim(true) + } } - m.component.SetStyle(style) - m.updater.UpdateScreen() + return style } diff --git a/app/ui/theme/theme.go b/app/ui/theme/theme.go deleted file mode 100644 index e38ca2f..0000000 --- a/app/ui/theme/theme.go +++ /dev/null @@ -1,14 +0,0 @@ -package theme - -import "github.com/gdamore/tcell" - -var ( - Default = tcell. - StyleDefault. - Background(tcell.ColorTeal). - Foreground(tcell.ColorBlack) - - ColorSelectedFocusedBackground = tcell.ColorLightCyan - ColorSelectedUnfocusedBackground = tcell.ColorDarkGray - ColorDisabledForeground = tcell.ColorGray -) diff --git a/app/ui/theme/themes/base16.go b/app/ui/theme/themes/base16.go new file mode 100644 index 0000000..19b1ac4 --- /dev/null +++ b/app/ui/theme/themes/base16.go @@ -0,0 +1,38 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" + "github.com/gdamore/tcell" +) + +// Base16 uses base 16 colors of the terminal +var Base16 = &pb.Theme{ + Name: "base16", + Colors: []*pb.Color{ + XTermColor("bg", int32(tcell.ColorDefault)), + XTermColor("fg", int32(tcell.ColorWhite)), + XTermColor("title-bg", int32(tcell.ColorBlue)), + XTermColor("title-fg", int32(tcell.ColorWhite)), + XTermColor("loader-fg", int32(tcell.ColorBlue)), + XTermColor("selection-fg", int32(tcell.ColorBlack)), + XTermColor("selection-bg", int32(tcell.ColorWhite)), + XTermColor("unfocused-fg", int32(tcell.ColorWhite)), + XTermColor("unfocused-bg", int32(tcell.ColorGray)), + XTermColor("disabled-fg", int32(tcell.ColorGray)), + XTermColor("disabled-bg", int32(tcell.ColorDefault)), + XTermColor("status-bar", int32(tcell.ColorGray)), + XTermColor("error-fg", int32(tcell.ColorWhite)), + XTermColor("error-bg", int32(tcell.ColorRed)), + XTermColor("warning-fg", int32(tcell.ColorBlack)), + XTermColor("warning-bg", int32(tcell.ColorYellow)), + XTermColor("info-fg", int32(tcell.ColorBlack)), + XTermColor("info-bg", int32(tcell.ColorYellow)), + XTermColor("confirm-fg", int32(tcell.ColorBlack)), + XTermColor("confirm-bg", int32(tcell.ColorYellow)), + }, + Styles: BaseStyles(), +} + +func init() { + RegisterTheme(Base16) +} diff --git a/app/ui/theme/themes/monokai.go b/app/ui/theme/themes/monokai.go new file mode 100644 index 0000000..b99f289 --- /dev/null +++ b/app/ui/theme/themes/monokai.go @@ -0,0 +1,36 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" +) + +var Monokai = &pb.Theme{ + Name: "monokai", + Colors: []*pb.Color{ + RGBColor("bg", "272822"), + RGBColor("fg", "f8f8f2"), + RGBColor("title-bg", "66d9ef"), + RGBColor("title-fg", "272822"), + RGBColor("loader-fg", "66d9ef"), + RGBColor("selection-fg", "272822"), + RGBColor("selection-bg", "a6e22e"), + RGBColor("unfocused-fg", "75715e"), + RGBColor("unfocused-bg", "a1efe4"), + RGBColor("disabled-fg", "75715e"), + RGBColor("disabled-bg", "272822"), + RGBColor("status-bar", "a1efe4"), + RGBColor("error-fg", "f8f8f2"), + RGBColor("error-bg", "fe1d6e"), + RGBColor("warning-fg", "272822"), + RGBColor("warning-bg", "f4bf75"), + RGBColor("info-fg", "272822"), + RGBColor("info-bg", "f4bf75"), + RGBColor("confirm-fg", "272822"), + RGBColor("confirm-bg", "f4bf75"), + }, + Styles: BaseStyles(), +} + +func init() { + RegisterTheme(Monokai) +} diff --git a/app/ui/theme/themes/paraiso.go b/app/ui/theme/themes/paraiso.go new file mode 100644 index 0000000..1374d9f --- /dev/null +++ b/app/ui/theme/themes/paraiso.go @@ -0,0 +1,36 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" +) + +var Paraiso = &pb.Theme{ + Name: "paraiso", + Colors: []*pb.Color{ + RGBColor("bg", "2f1e2e"), + RGBColor("fg", "a39e9b"), + RGBColor("title-bg", "5bc4bf"), + RGBColor("title-fg", "2f1e2e"), + RGBColor("loader-fg", "7587a6"), + RGBColor("selection-fg", "2f1e2e"), + RGBColor("selection-bg", "fec418"), + RGBColor("unfocused-fg", "2f1e2e"), + RGBColor("unfocused-bg", "815ba4"), + RGBColor("disabled-fg", "776e71"), + RGBColor("disabled-bg", "2f1e2e"), + RGBColor("status-bar", "776e71"), + RGBColor("error-fg", "2f1e2e"), + RGBColor("error-bg", "ef6155"), + RGBColor("warning-fg", "2f1e2e"), + RGBColor("warning-bg", "fec418"), + RGBColor("info-fg", "2f1e2e"), + RGBColor("info-bg", "fec418"), + RGBColor("confirm-fg", "2f1e2e"), + RGBColor("confirm-bg", "fec418"), + }, + Styles: BaseStyles(), +} + +func init() { + RegisterTheme(Paraiso) +} diff --git a/app/ui/theme/themes/solarized.go b/app/ui/theme/themes/solarized.go new file mode 100644 index 0000000..dd47a1e --- /dev/null +++ b/app/ui/theme/themes/solarized.go @@ -0,0 +1,36 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" +) + +var Solarized = &pb.Theme{ + Name: "solarized", + Colors: []*pb.Color{ + RGBColor("bg", "002b36"), + RGBColor("fg", "93a1a1"), + RGBColor("title-bg", "2aa198"), + RGBColor("title-fg", "fdf6e3"), + RGBColor("loader-fg", "2aa198"), + RGBColor("selection-fg", "002b36"), + RGBColor("selection-bg", "fdf6e3"), + RGBColor("unfocused-fg", "002b36"), + RGBColor("unfocused-bg", "93a1a1"), + RGBColor("disabled-fg", "657b83"), + RGBColor("disabled-bg", "002b36"), + RGBColor("status-bar", "657b83"), + RGBColor("error-fg", "002b36"), + RGBColor("error-bg", "dc322f"), + RGBColor("warning-fg", "002b36"), + RGBColor("warning-bg", "b58900"), + RGBColor("info-fg", "002b36"), + RGBColor("info-bg", "b58900"), + RGBColor("confirm-fg", "002b36"), + RGBColor("confirm-bg", "b58900"), + }, + Styles: BaseStyles(), +} + +func init() { + RegisterTheme(Solarized) +} diff --git a/app/ui/theme/themes/themes.go b/app/ui/theme/themes/themes.go new file mode 100644 index 0000000..fd28aab --- /dev/null +++ b/app/ui/theme/themes/themes.go @@ -0,0 +1,134 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" +) + +var DefaultThemes []*pb.Theme + +func RegisterTheme(t *pb.Theme) { + DefaultThemes = append(DefaultThemes, t) +} + +func RGBColor(name string, color string) *pb.Color { + return &pb.Color{ + Name: name, + Value: &pb.Color_Rgb{ + Rgb: color, + }, + } +} + +func XTermColor(name string, color int32) *pb.Color { + return &pb.Color{ + Name: name, + Value: &pb.Color_Xterm{ + Xterm: color, + }, + } +} + +func Attributes(attrs ...pb.StyleAttribute) []pb.StyleAttribute { + return append([]pb.StyleAttribute{}, attrs...) +} + +func BaseStyles() []*pb.Style { + return []*pb.Style{ + { + Name: "screen", + Fg: "fg", + Bg: "bg", + }, + { + Name: "title-bar", + Fg: "title-fg", + Bg: "title-bg", + }, + { + Name: "logo-icon", + Fg: "title-fg", + Bg: "title-bg", + Attrs: Attributes(pb.StyleAttribute_REVERSE), + }, + { + Name: "logo-text", + Fg: "title-fg", + Bg: "title-bg", + }, + { + Name: "popup", + Bg: "bg", + }, + { + Name: "loader", + Fg: "loader-fg", + Bg: "bg", + }, + { + Name: "popup-title", + Fg: "fg", + Bg: "bg", + Attrs: Attributes(pb.StyleAttribute_UNDERLINE), + }, + { + Name: "row", + Fg: "fg", + Bg: "bg", + }, + { + Name: "row-header", + Fg: "fg", + Bg: "bg", + Attrs: Attributes(pb.StyleAttribute_UNDERLINE), + }, + { + Name: "row-selected-focused", + Fg: "selection-fg", + Bg: "selection-bg", + }, + { + Name: "row-selected-unfocused", + Fg: "unfocused-fg", + Bg: "unfocused-bg", + }, + { + Name: "row-disabled", + Fg: "disabled-fg", + Bg: "disabled-bg", + }, + { + Name: "filter-active", + Fg: "selection-fg", + Bg: "selection-bg", + }, + { + Name: "filter-inactive", + Fg: "unfocused-fg", + Bg: "unfocused-bg", + }, + { + Name: "status-bar", + Bg: "status-bar", + }, + { + Name: "status-error", + Fg: "error-fg", + Bg: "error-bg", + }, + { + Name: "status-warning", + Fg: "warning-fg", + Bg: "warning-bg", + }, + { + Name: "status-info", + Fg: "info-fg", + Bg: "info-bg", + }, + { + Name: "status-confirm", + Fg: "confirm-fg", + Bg: "confirm-bg", + }, + } +} diff --git a/app/ui/theme/themes/twilight.go b/app/ui/theme/themes/twilight.go new file mode 100644 index 0000000..76736c2 --- /dev/null +++ b/app/ui/theme/themes/twilight.go @@ -0,0 +1,36 @@ +package themes + +import ( + "github.com/AnatolyRugalev/kube-commander/pb" +) + +var Twilight = &pb.Theme{ + Name: "twilight", + Colors: []*pb.Color{ + RGBColor("bg", "1e1e1e"), + RGBColor("fg", "a7a7a7"), + RGBColor("title-bg", "7587a6"), + RGBColor("title-fg", "ffffff"), + RGBColor("loader-fg", "7587a6"), + RGBColor("selection-fg", "1e1e1e"), + RGBColor("selection-bg", "f9ee98"), + RGBColor("unfocused-fg", "5f5a60"), + RGBColor("unfocused-bg", "8f9d6a"), + RGBColor("disabled-fg", "5f5a60"), + RGBColor("disabled-bg", "1e1e1e"), + RGBColor("status-bar", "5f5a60"), + RGBColor("error-fg", "1e1e1e"), + RGBColor("error-bg", "cf6a4c"), + RGBColor("warning-fg", "1e1e1e"), + RGBColor("warning-bg", "9b859d"), + RGBColor("info-fg", "1e1e1e"), + RGBColor("info-bg", "9b859d"), + RGBColor("confirm-fg", "1e1e1e"), + RGBColor("confirm-bg", "9b859d"), + }, + Styles: BaseStyles(), +} + +func init() { + RegisterTheme(Twilight) +} diff --git a/app/ui/widgets/listTable/listTable.go b/app/ui/widgets/listTable/listTable.go index 9e3746d..a6aeea8 100644 --- a/app/ui/widgets/listTable/listTable.go +++ b/app/ui/widgets/listTable/listTable.go @@ -4,12 +4,12 @@ import ( "errors" "fmt" "github.com/AnatolyRugalev/kube-commander/app/focus" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" "github.com/mattn/go-runewidth" "strings" + "sync" "time" ) @@ -51,32 +51,18 @@ func (tf TableFormat) Has(flag TableFormat) bool { return tf&flag != 0 } -var DefaultStyler commander.ListViewStyler = func(list commander.ListView, row commander.Row) commander.Style { - style := theme.Default - if row == nil { - style = style.Underline(true) - } else if row.Id() == list.SelectedRowId() { - if list.IsFocused() { - style = style.Background(theme.ColorSelectedFocusedBackground) - } else { - style = style.Background(theme.ColorSelectedUnfocusedBackground) - } - } - if row != nil && !row.Enabled() { - style = style.Foreground(theme.ColorDisabledForeground) - } - return style -} - type ListTable struct { views.WidgetWatchers *focus.Focusable - view views.View - columns []string - ageCol int - rows []commander.Row - rowIndex map[string]int + view views.View + ageCol int + + rowsMu sync.RWMutex + rows []commander.Row + rowIndex map[string]int + columns []string + selectedId string format TableFormat // internal representation of table values @@ -93,40 +79,16 @@ type ListTable struct { onInitStart InitFunc onInitFinish InitFunc - styler commander.ListViewStyler preloader *preloader rowProvider commander.RowProvider - updater commander.ScreenUpdater + screen commander.ScreenHandler stopCh chan struct{} filter string filterMode bool - - stRow commander.StyleComponent - stHeader commander.StyleComponent - stSelectedFocused commander.StyleComponent - stSelectedUnfocused commander.StyleComponent - stDisabled commander.StyleComponent - stFilter commander.StyleComponent - stFilterActive commander.StyleComponent } -func (lt *ListTable) GetComponents() []commander.StyleComponent { - return []commander.StyleComponent{ - lt.stRow, - lt.stHeader, - lt.stSelectedFocused, - lt.stSelectedUnfocused, - lt.stFilter, - lt.stFilterActive, - } -} - -func (lt *ListTable) Rows() []commander.Row { - return lt.rows -} - -func NewListTable(prov commander.RowProvider, format TableFormat, updater commander.ScreenUpdater) *ListTable { +func NewListTable(prov commander.RowProvider, format TableFormat, screen commander.ScreenHandler) *ListTable { lt := &ListTable{ Focusable: focus.NewFocusable(), format: format, @@ -137,18 +99,9 @@ func NewListTable(prov commander.RowProvider, format TableFormat, updater comman onChange: DefaultRowFunc, onInitStart: DefaultInit, onInitFinish: DefaultInit, - styler: DefaultStyler, - preloader: NewPreloader(updater), + preloader: NewPreloader(screen), rowProvider: prov, - updater: updater, - - stRow: theme.NewComponent("row", theme.Default), - stHeader: theme.NewComponent("header", theme.Default.Underline(true)), - stSelectedFocused: theme.NewComponent("selected-focused", theme.Default.Background(theme.ColorSelectedFocusedBackground)), - stSelectedUnfocused: theme.NewComponent("selected-unfocused", theme.Default.Background(theme.ColorSelectedUnfocusedBackground)), - stDisabled: theme.NewComponent("disabled", theme.Default.Foreground(theme.ColorDisabledForeground)), - stFilter: theme.NewComponent("filter", theme.Default.Background(theme.ColorSelectedUnfocusedBackground)), - stFilterActive: theme.NewComponent("filter-active", theme.Default.Background(theme.ColorSelectedFocusedBackground)), + screen: screen, } lt.Render() return lt @@ -181,18 +134,23 @@ func (lt *ListTable) watch() { for _, operation := range ops { switch op := operation.(type) { case *commander.OpClear: + lt.rowsMu.Lock() lt.rows = []commander.Row{} lt.rowIndex = make(map[string]int) lt.columns = []string{} + lt.rowsMu.Unlock() changed = true case *commander.OpSetColumns: + lt.rowsMu.Lock() // Compare columns content if strings.Join(lt.columns, "|") != strings.Join(op.Columns, "|") { lt.columns = op.Columns changed = true } lt.setAgeCol() + lt.rowsMu.Unlock() case *commander.OpAdded: + lt.rowsMu.Lock() index, ok := lt.rowIndex[op.Row.Id()] if !ok { if op.Index == nil { @@ -217,7 +175,9 @@ func (lt *ListTable) watch() { lt.rows[index] = op.Row // TODO: move row if new index provided? } + lt.rowsMu.Unlock() case *commander.OpDeleted: + lt.rowsMu.Lock() index, ok := lt.rowIndex[op.RowId] if ok { lt.rows = append(lt.rows[:index], lt.rows[index+1:]...) @@ -227,7 +187,9 @@ func (lt *ListTable) watch() { } changed = true } + lt.rowsMu.Unlock() case *commander.OpModified: + lt.rowsMu.Lock() index, ok := lt.rowIndex[op.Row.Id()] if ok { // Compare cells content @@ -240,9 +202,12 @@ func (lt *ListTable) watch() { changed = true } } else { + lt.rowsMu.Lock() lt.rows = append(lt.rows, op.Row) + lt.rowsMu.Unlock() changed = true } + lt.rowsMu.Unlock() case *commander.OpInitStart: lt.preloader.Start() lt.onInitStart() @@ -254,18 +219,18 @@ func (lt *ListTable) watch() { if changed { lt.Render() lt.reindexSelection() - if lt.updater != nil { - lt.updater.Resize() - lt.updater.UpdateScreen() + if lt.screen != nil { + lt.screen.Resize() + lt.screen.UpdateScreen() } } case <-ticker.C: // Periodically update list to ensure that age is somewhat relevant if lt.ageCol != -1 { lt.Render() - if lt.updater != nil { - lt.updater.Resize() - lt.updater.UpdateScreen() + if lt.screen != nil { + lt.screen.Resize() + lt.screen.UpdateScreen() } } } @@ -301,23 +266,18 @@ func (lt *ListTable) SelectedRow() commander.Row { return nil } -// deprecated -func (lt *ListTable) SetStyler(styler commander.ListViewStyler) { - lt.styler = styler -} - func (lt *ListTable) rowStyle(row commander.Row) commander.Style { if row.Id() == lt.SelectedRowId() { if lt.IsFocused() { - return lt.stSelectedFocused.Style() + return lt.screen.Theme().GetStyle("row-selected-focused") } else { - return lt.stSelectedUnfocused.Style() + return lt.screen.Theme().GetStyle("row-selected-unfocused") } } if row != nil && !row.Enabled() { - return lt.stDisabled.Style() + return lt.screen.Theme().GetStyle("row-disabled") } - return lt.stRow.Style() + return lt.screen.Theme().GetStyle("row") } @@ -355,6 +315,8 @@ func (lt *ListTable) BindOnInitStart(initFunc InitFunc) { } func (lt *ListTable) RowById(id string) commander.Row { + lt.rowsMu.RLock() + defer lt.rowsMu.RUnlock() if index, ok := lt.rowIndex[id]; ok { return lt.rows[index] } @@ -423,6 +385,8 @@ func (lt *ListTable) renderTable() table { t := table{ rowIndex: make(map[string]int), } + lt.rowsMu.RLock() + defer lt.rowsMu.RUnlock() t.dataHeight = len(lt.rows) t.columnDataWidths = []int{} if lt.format.Has(WithHeaders) { @@ -511,7 +475,7 @@ func (lt *ListTable) Draw() { } sizes := lt.getColumnSizes() if lt.format.Has(WithHeaders) { - lt.drawRow(index, lt.table.headers, sizes, lt.stHeader.Style()) + lt.drawRow(index, lt.table.headers, sizes, lt.screen.Theme().GetStyle("row-header")) index++ } rowIndex := 0 @@ -538,9 +502,9 @@ func (lt *ListTable) drawFilter(y int) { x := 0 var st commander.Style if lt.filterMode { - st = lt.stFilterActive.Style() + st = lt.screen.Theme().GetStyle("filter-active") } else { - st = lt.stFilter.Style() + st = lt.screen.Theme().GetStyle("filter-inactive") } for _, ch := range str { lt.view.SetContent(x, y, ch, nil, st) @@ -549,7 +513,7 @@ func (lt *ListTable) drawFilter(y int) { } func (lt *ListTable) defaultStyle() tcell.Style { - return tcell.StyleDefault.Background(tcell.ColorTeal) + return lt.screen.Theme().GetStyle("screen") } func (lt *ListTable) drawRow(y int, row []string, sizes []int, style tcell.Style) { diff --git a/app/ui/widgets/listTable/preloader.go b/app/ui/widgets/listTable/preloader.go index 6695a30..a8a2db1 100644 --- a/app/ui/widgets/listTable/preloader.go +++ b/app/ui/widgets/listTable/preloader.go @@ -24,14 +24,14 @@ type preloader struct { ticker *time.Ticker view views.View style tcell.Style - updater commander.ScreenUpdater + screen commander.ScreenHandler preloader *preloader } -func NewPreloader(updater commander.ScreenUpdater) *preloader { +func NewPreloader(screen commander.ScreenHandler) *preloader { return &preloader{ - updater: updater, - phase: -1, + screen: screen, + phase: -1, } } @@ -46,7 +46,7 @@ func (p *preloader) Start() { if p.phase >= len(phases) { p.phase = 0 } - p.updater.UpdateScreen() + p.screen.UpdateScreen() } }() } @@ -58,14 +58,14 @@ func (p *preloader) Stop() { } p.Unlock() p.phase = -1 - p.updater.UpdateScreen() + p.screen.UpdateScreen() } func (p *preloader) Draw() { if p.phase == -1 { return } - p.view.SetContent(0, 0, phases[p.phase], nil, tcell.StyleDefault.Background(tcell.ColorTeal).Foreground(tcell.ColorBlack)) + p.view.SetContent(0, 0, phases[p.phase], nil, p.screen.Theme().GetStyle("loader")) } func (p *preloader) Resize() { diff --git a/app/ui/widgets/listTable/resource.go b/app/ui/widgets/listTable/resource.go index 671a8b8..df396ab 100644 --- a/app/ui/widgets/listTable/resource.go +++ b/app/ui/widgets/listTable/resource.go @@ -30,7 +30,7 @@ func NewResourceListTable(container commander.ResourceContainer, resource *comma rowProvider: make(commander.RowProvider), format: format, } - resourceLt.ListTable = NewListTable(resourceLt.rowProvider, format, container.ScreenUpdater()) + resourceLt.ListTable = NewListTable(resourceLt.rowProvider, format, container.ScreenHandler()) if !format.Has(NoActions) { resourceLt.BindOnKeyPress(resourceLt.OnKeyPress) } diff --git a/app/ui/widgets/listTable/static.go b/app/ui/widgets/listTable/static.go index 9870a91..d2692d7 100644 --- a/app/ui/widgets/listTable/static.go +++ b/app/ui/widgets/listTable/static.go @@ -2,8 +2,8 @@ package listTable import "github.com/AnatolyRugalev/kube-commander/commander" -func NewStaticListTable(columns []string, rows []commander.Row, format TableFormat) *ListTable { - lt := NewListTable(NewStaticRowProvider(columns, rows), format, nil) +func NewStaticListTable(columns []string, rows []commander.Row, format TableFormat, screen commander.ScreenHandler) *ListTable { + lt := NewListTable(NewStaticRowProvider(columns, rows), format, screen) lt.watch() return lt } diff --git a/app/ui/widgets/logo/logo.go b/app/ui/widgets/logo/logo.go new file mode 100644 index 0000000..9168187 --- /dev/null +++ b/app/ui/widgets/logo/logo.go @@ -0,0 +1,46 @@ +package logo + +import ( + "github.com/AnatolyRugalev/kube-commander/commander" + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/views" +) + +type Logo struct { + views.WidgetWatchers + text *views.Text + theme commander.ThemeManager + view views.View +} + +func (l *Logo) Draw() { + l.text.SetText("☸ ️kubecom") + l.text.SetStyle(l.theme.GetStyle("logo-text")) + iconStyle := l.theme.GetStyle("logo-icon") + l.text.SetStyleAt(0, iconStyle) + l.text.SetStyleAt(1, iconStyle) + l.text.Draw() +} + +func (l *Logo) Resize() { +} + +func (l *Logo) HandleEvent(_ tcell.Event) bool { + return false +} + +func (l *Logo) SetView(view views.View) { + l.view = view + l.text.SetView(view) +} + +func (l *Logo) Size() (int, int) { + return 12, 1 +} + +func NewLogo(theme commander.ThemeManager) *Logo { + return &Logo{ + theme: theme, + text: views.NewText(), + } +} diff --git a/app/ui/widgets/popup/popup.go b/app/ui/widgets/popup/popup.go index 2648a46..a131a82 100644 --- a/app/ui/widgets/popup/popup.go +++ b/app/ui/widgets/popup/popup.go @@ -2,7 +2,6 @@ package popup import ( "github.com/AnatolyRugalev/kube-commander/app/ui/border" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell/views" ) @@ -43,9 +42,9 @@ func (p *popup) Reposition(view commander.View) { p.BorderedWidget.SetView(popupView) } -func NewPopup(view commander.View, title string, widget commander.MaxSizeWidget, onBlur func()) *popup { +func NewPopup(view commander.View, theme commander.ThemeManager, title string, widget commander.MaxSizeWidget, onBlur func()) *popup { popup := popup{ - BorderedWidget: border.NewBorderedWidget(widget, title, theme.Default, theme.Default.Underline(true), border.All), + BorderedWidget: border.NewBorderedWidget(widget, title, theme, "popup", "popup-title", border.All), onBlur: onBlur, } popup.Reposition(view) diff --git a/app/ui/workspace/workspace.go b/app/ui/workspace/workspace.go index 4ec41fc..2b52c16 100644 --- a/app/ui/workspace/workspace.go +++ b/app/ui/workspace/workspace.go @@ -7,8 +7,6 @@ import ( "github.com/AnatolyRugalev/kube-commander/app/ui/help" "github.com/AnatolyRugalev/kube-commander/app/ui/resourceMenu" "github.com/AnatolyRugalev/kube-commander/app/ui/resources/namespace" - "github.com/AnatolyRugalev/kube-commander/app/ui/theme" - "github.com/AnatolyRugalev/kube-commander/app/ui/widgets/listTable" "github.com/AnatolyRugalev/kube-commander/app/ui/widgets/popup" "github.com/AnatolyRugalev/kube-commander/commander" "github.com/gdamore/tcell" @@ -95,7 +93,7 @@ func (w *workspace) FocusManager() commander.FocusManager { } func (w *workspace) ShowPopup(title string, widget commander.MaxSizeWidget) { - w.popup = popup.NewPopup(w.view, title, widget, func() { + w.popup = popup.NewPopup(w.view, w.Theme(), title, widget, func() { w.popup.OnHide() w.popup = nil w.UpdateScreen() @@ -115,7 +113,7 @@ func (w *workspace) UpdateScreen() { } } -func (w *workspace) ScreenUpdater() commander.ScreenUpdater { +func (w *workspace) ScreenHandler() commander.ScreenHandler { return w } @@ -123,7 +121,7 @@ func (w *workspace) UpdateConfig(f commander.ConfigUpdateFunc) error { return w.container.ConfigUpdater().UpdateConfig(f) } -func (w *workspace) ConfigUpdater() commander.ScreenUpdater { +func (w *workspace) ConfigUpdater() commander.ScreenHandler { return w } @@ -131,7 +129,7 @@ func (w *workspace) Status() commander.StatusReporter { return w.container.StatusReporter() } -func (w workspace) Draw() { +func (w *workspace) Draw() { w.BoxLayout.Draw() if w.popup != nil { w.popup.Draw() @@ -184,27 +182,19 @@ func (w *workspace) Init() error { } w.container.Register(resMenu) - resMenu.SetStyler(w.styler) w.menu = resMenu w.menu.OnShow() - w.widget = help.NewHelpWidget() + w.widget = help.NewHelpWidget(w.Theme()) w.BoxLayout.AddWidget(w.menu, 0.0) - w.BoxLayout.AddWidget(border.NewVerticalLine(theme.Default), 0.0) + w.BoxLayout.AddWidget(border.NewVerticalLine(w.Theme()), 0.0) w.BoxLayout.AddWidget(w.widget, 1.0) w.focus = focus.NewFocusManager(w.menu) return nil } -func (w *workspace) styler(list commander.ListView, row commander.Row) tcell.Style { - style := listTable.DefaultStyler(list, row) - - if row != nil && row.Id() == w.selectedWidgetId && (row.Id() != w.menu.SelectedRowId() || !list.IsFocused()) { - _, bg, _ := theme.Default.Decompose() - return style.Background(bg).Bold(true).Underline(true) - } - - return style +func (w *workspace) Theme() commander.ThemeManager { + return w.container.Screen().Theme() } func (w *workspace) onMenuSelect(_ string, widget commander.Widget) bool { diff --git a/commander/listView.go b/commander/listView.go index 2876bdd..291f037 100644 --- a/commander/listView.go +++ b/commander/listView.go @@ -42,10 +42,8 @@ func NewSimpleRow(id string, cells []string, enabled bool) *simpleRow { type ListView interface { MaxSizeWidget - Rows() []Row SelectedRow() Row SelectedRowId() string - SetStyler(styler ListViewStyler) SelectId(id string) } diff --git a/commander/resource.go b/commander/resource.go index b176205..7a373b2 100644 --- a/commander/resource.go +++ b/commander/resource.go @@ -46,5 +46,5 @@ type ResourceContainer interface { ResourceProvider() ResourceProvider CommandBuilder() CommandBuilder CommandExecutor() CommandExecutor - ScreenUpdater() ScreenUpdater + ScreenHandler() ScreenHandler } diff --git a/commander/screen.go b/commander/screen.go index 8c8eaef..7a8d45f 100644 --- a/commander/screen.go +++ b/commander/screen.go @@ -1,10 +1,10 @@ package commander type Screen interface { - ScreenUpdater + ScreenHandler Widget + Init(status StatusReporter, theme ThemeManager) SetWorkspace(workspace Workspace) - SetStatus(status StatusReporter) Workspace() Workspace View() View } @@ -17,7 +17,8 @@ type StatusReporter interface { Confirm(msg string) bool } -type ScreenUpdater interface { +type ScreenHandler interface { UpdateScreen() Resize() + Theme() ThemeManager } diff --git a/commander/theme.go b/commander/theme.go index b7a2230..682d188 100644 --- a/commander/theme.go +++ b/commander/theme.go @@ -2,29 +2,22 @@ package commander import "github.com/gdamore/tcell" -type Stylable interface { - GetComponents() []StyleComponent +type Style = tcell.Style + +type Color struct { + Name string + Color tcell.Color } -type StyleComponent interface { +type ThemeComponent interface { Name() string - Style() Style - SetStyle(style Style) + Style(name string) Style + SetStyle(name string, style Style) } type ThemeManager interface { - tcell.EventHandler - Init() error - DeInit() - - NextComponent() - PrevComponent() - - NextBg() - PrevBg() - - NextFg() - PrevFg() - - SwitchAttr(attr tcell.AttrMask) + Configurable + GetStyle(name string) Style + NextTheme() + PrevTheme() } diff --git a/commander/widget.go b/commander/widget.go index ca865a2..95ca15a 100644 --- a/commander/widget.go +++ b/commander/widget.go @@ -1,7 +1,6 @@ package commander import ( - "github.com/gdamore/tcell" "github.com/gdamore/tcell/views" ) @@ -20,8 +19,6 @@ type MaxSizeWidget interface { MaxSize() (int, int) } -type Style = tcell.Style - type StylableWidget interface { SetStyle(style Style) Style() Style diff --git a/commander/workspace.go b/commander/workspace.go index 7b17d47..2e20616 100644 --- a/commander/workspace.go +++ b/commander/workspace.go @@ -7,6 +7,7 @@ type Workspace interface { Init() error ShowPopup(title string, widget MaxSizeWidget) FocusManager() FocusManager + Theme() ThemeManager } type NamespaceAccessor interface { diff --git a/config/config.go b/config/config.go index df28f84..1e495e7 100644 --- a/config/config.go +++ b/config/config.go @@ -86,6 +86,10 @@ func Watch(ctx context.Context, path string, ch chan<- Event) error { } func Save(path string, config *pb.Config) error { + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + return fmt.Errorf("could not create configuration file directory") + } jsonB, err := protojson.Marshal(config) if err != nil { return fmt.Errorf("error marshalling config: %w", err) diff --git a/pb/config.pb.go b/pb/config.pb.go index 79d4488..3d49e5b 100644 --- a/pb/config.pb.go +++ b/pb/config.pb.go @@ -25,12 +25,72 @@ const ( // of the legacy proto package is being used. const _ = proto.ProtoPackageIsVersion4 +type StyleAttribute int32 + +const ( + StyleAttribute_NONE StyleAttribute = 0 + StyleAttribute_BOLD StyleAttribute = 1 + StyleAttribute_BLINK StyleAttribute = 2 + StyleAttribute_REVERSE StyleAttribute = 3 + StyleAttribute_UNDERLINE StyleAttribute = 4 + StyleAttribute_DIM StyleAttribute = 5 +) + +// Enum value maps for StyleAttribute. +var ( + StyleAttribute_name = map[int32]string{ + 0: "NONE", + 1: "BOLD", + 2: "BLINK", + 3: "REVERSE", + 4: "UNDERLINE", + 5: "DIM", + } + StyleAttribute_value = map[string]int32{ + "NONE": 0, + "BOLD": 1, + "BLINK": 2, + "REVERSE": 3, + "UNDERLINE": 4, + "DIM": 5, + } +) + +func (x StyleAttribute) Enum() *StyleAttribute { + p := new(StyleAttribute) + *p = x + return p +} + +func (x StyleAttribute) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StyleAttribute) Descriptor() protoreflect.EnumDescriptor { + return file_config_proto_enumTypes[0].Descriptor() +} + +func (StyleAttribute) Type() protoreflect.EnumType { + return &file_config_proto_enumTypes[0] +} + +func (x StyleAttribute) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StyleAttribute.Descriptor instead. +func (StyleAttribute) EnumDescriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + type Config struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Menu []*Resource `protobuf:"bytes,1,rep,name=menu,proto3" json:"menu,omitempty"` + Menu []*Resource `protobuf:"bytes,1,rep,name=menu,proto3" json:"menu,omitempty"` + CurrentTheme string `protobuf:"bytes,2,opt,name=currentTheme,proto3" json:"currentTheme,omitempty"` + Themes []*Theme `protobuf:"bytes,3,rep,name=themes,proto3" json:"themes,omitempty"` } func (x *Config) Reset() { @@ -72,6 +132,20 @@ func (x *Config) GetMenu() []*Resource { return nil } +func (x *Config) GetCurrentTheme() string { + if x != nil { + return x.CurrentTheme + } + return "" +} + +func (x *Config) GetThemes() []*Theme { + if x != nil { + return x.Themes + } + return nil +} + type Resource struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -143,27 +217,291 @@ func (x *Resource) GetTitle() string { return "" } +// TODO: add Register method to theme manager +// TODO: register components with their styles into manager +// TODO: cycle through components and styles and edit themes through manager +type Theme struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Theme unique name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Color palette + Colors []*Color `protobuf:"bytes,2,rep,name=colors,proto3" json:"colors,omitempty"` + // Components color bindings + Styles []*Style `protobuf:"bytes,3,rep,name=styles,proto3" json:"styles,omitempty"` +} + +func (x *Theme) Reset() { + *x = Theme{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Theme) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Theme) ProtoMessage() {} + +func (x *Theme) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Theme.ProtoReflect.Descriptor instead. +func (*Theme) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Theme) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Theme) GetColors() []*Color { + if x != nil { + return x.Colors + } + return nil +} + +func (x *Theme) GetStyles() []*Style { + if x != nil { + return x.Styles + } + return nil +} + +// Color palette allows to reuse colors in theme +type Color struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Types that are assignable to Value: + // *Color_Rgb + // *Color_Xterm + Value isColor_Value `protobuf_oneof:"value"` +} + +func (x *Color) Reset() { + *x = Color{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Color) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Color) ProtoMessage() {} + +func (x *Color) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Color.ProtoReflect.Descriptor instead. +func (*Color) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Color) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (m *Color) GetValue() isColor_Value { + if m != nil { + return m.Value + } + return nil +} + +func (x *Color) GetRgb() string { + if x, ok := x.GetValue().(*Color_Rgb); ok { + return x.Rgb + } + return "" +} + +func (x *Color) GetXterm() int32 { + if x, ok := x.GetValue().(*Color_Xterm); ok { + return x.Xterm + } + return 0 +} + +type isColor_Value interface { + isColor_Value() +} + +type Color_Rgb struct { + Rgb string `protobuf:"bytes,2,opt,name=rgb,proto3,oneof"` +} + +type Color_Xterm struct { + Xterm int32 `protobuf:"varint,3,opt,name=xterm,proto3,oneof"` +} + +func (*Color_Rgb) isColor_Value() {} + +func (*Color_Xterm) isColor_Value() {} + +type Style struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Bg string `protobuf:"bytes,2,opt,name=bg,proto3" json:"bg,omitempty"` + Fg string `protobuf:"bytes,3,opt,name=fg,proto3" json:"fg,omitempty"` + Attrs []StyleAttribute `protobuf:"varint,4,rep,packed,name=attrs,proto3,enum=AnatolyRugalev.kubecom.config.StyleAttribute" json:"attrs,omitempty"` +} + +func (x *Style) Reset() { + *x = Style{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Style) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Style) ProtoMessage() {} + +func (x *Style) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Style.ProtoReflect.Descriptor instead. +func (*Style) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{4} +} + +func (x *Style) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Style) GetBg() string { + if x != nil { + return x.Bg + } + return "" +} + +func (x *Style) GetFg() string { + if x != nil { + return x.Fg + } + return "" +} + +func (x *Style) GetAttrs() []StyleAttribute { + if x != nil { + return x.Attrs + } + return nil +} + var File_config_proto protoreflect.FileDescriptor var file_config_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2e, 0x6b, - 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x45, 0x0a, - 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x04, 0x6d, 0x65, 0x6e, 0x75, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, - 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x04, - 0x6d, 0x65, 0x6e, 0x75, 0x22, 0x6a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, - 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x41, - 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2f, 0x6b, 0x75, - 0x62, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x3b, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xa7, 0x01, + 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x04, 0x6d, 0x65, 0x6e, 0x75, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, + 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x04, 0x6d, 0x65, 0x6e, 0x75, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, + 0x54, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x06, 0x74, 0x68, 0x65, + 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x41, 0x6e, 0x61, 0x74, + 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x63, + 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x52, + 0x06, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x73, 0x22, 0x6a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x22, 0x97, 0x01, 0x0a, 0x05, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x3c, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x24, 0x2e, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, + 0x65, 0x76, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, + 0x3c, 0x0a, 0x06, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, + 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x53, 0x74, 0x79, 0x6c, 0x65, 0x52, 0x06, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x73, 0x22, 0x50, 0x0a, + 0x05, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x03, 0x72, 0x67, + 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x03, 0x72, 0x67, 0x62, 0x12, 0x16, + 0x0a, 0x05, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, + 0x05, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x80, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x79, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x62, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x62, 0x67, 0x12, 0x0e, 0x0a, + 0x02, 0x66, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x66, 0x67, 0x12, 0x43, 0x0a, + 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x41, + 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2e, 0x6b, 0x75, + 0x62, 0x65, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x74, 0x79, + 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x05, 0x61, 0x74, 0x74, + 0x72, 0x73, 0x2a, 0x54, 0x0a, 0x0e, 0x53, 0x74, 0x79, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, + 0x0a, 0x04, 0x42, 0x4f, 0x4c, 0x44, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x42, 0x4c, 0x49, 0x4e, + 0x4b, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x45, 0x52, 0x53, 0x45, 0x10, 0x03, + 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x52, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12, + 0x07, 0x0a, 0x03, 0x44, 0x49, 0x4d, 0x10, 0x05, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x41, 0x6e, 0x61, 0x74, 0x6f, 0x6c, 0x79, 0x52, 0x75, + 0x67, 0x61, 0x6c, 0x65, 0x76, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -178,18 +516,27 @@ func file_config_proto_rawDescGZIP() []byte { return file_config_proto_rawDescData } -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_config_proto_goTypes = []interface{}{ - (*Config)(nil), // 0: AnatolyRugalev.kubecom.config.Config - (*Resource)(nil), // 1: AnatolyRugalev.kubecom.config.Resource + (StyleAttribute)(0), // 0: AnatolyRugalev.kubecom.config.StyleAttribute + (*Config)(nil), // 1: AnatolyRugalev.kubecom.config.Config + (*Resource)(nil), // 2: AnatolyRugalev.kubecom.config.Resource + (*Theme)(nil), // 3: AnatolyRugalev.kubecom.config.Theme + (*Color)(nil), // 4: AnatolyRugalev.kubecom.config.Color + (*Style)(nil), // 5: AnatolyRugalev.kubecom.config.Style } var file_config_proto_depIdxs = []int32{ - 1, // 0: AnatolyRugalev.kubecom.config.Config.menu:type_name -> AnatolyRugalev.kubecom.config.Resource - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 0: AnatolyRugalev.kubecom.config.Config.menu:type_name -> AnatolyRugalev.kubecom.config.Resource + 3, // 1: AnatolyRugalev.kubecom.config.Config.themes:type_name -> AnatolyRugalev.kubecom.config.Theme + 4, // 2: AnatolyRugalev.kubecom.config.Theme.colors:type_name -> AnatolyRugalev.kubecom.config.Color + 5, // 3: AnatolyRugalev.kubecom.config.Theme.styles:type_name -> AnatolyRugalev.kubecom.config.Style + 0, // 4: AnatolyRugalev.kubecom.config.Style.attrs:type_name -> AnatolyRugalev.kubecom.config.StyleAttribute + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_config_proto_init() } @@ -222,19 +569,60 @@ func file_config_proto_init() { return nil } } + file_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Theme); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Color); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Style); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_config_proto_msgTypes[3].OneofWrappers = []interface{}{ + (*Color_Rgb)(nil), + (*Color_Xterm)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, + NumEnums: 1, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, GoTypes: file_config_proto_goTypes, DependencyIndexes: file_config_proto_depIdxs, + EnumInfos: file_config_proto_enumTypes, MessageInfos: file_config_proto_msgTypes, }.Build() File_config_proto = out.File diff --git a/pb/config.proto b/pb/config.proto index d011370..d994f32 100644 --- a/pb/config.proto +++ b/pb/config.proto @@ -6,6 +6,8 @@ option go_package = "github.com/AnatolyRugalev/kube-commander/pb;pb"; message Config { repeated Resource menu = 1; + string currentTheme = 2; + repeated Theme themes = 3; } message Resource { @@ -14,3 +16,40 @@ message Resource { string kind = 3; string title = 4; } + +// TODO: add Register method to theme manager +// TODO: register components with their styles into manager +// TODO: cycle through components and styles and edit themes through manager +message Theme { + // Theme unique name + string name = 1; + // Color palette + repeated Color colors = 2; + // Components color bindings + repeated Style styles = 3; +} + +// Color palette allows to reuse colors in theme +message Color { + string name = 1; + oneof value { + string rgb = 2; + int32 xterm = 3; + } +} + +enum StyleAttribute { + NONE = 0; + BOLD = 1; + BLINK = 2; + REVERSE = 3; + UNDERLINE = 4; + DIM = 5; +} + +message Style { + string name = 1; + string bg = 2; + string fg = 3; + repeated StyleAttribute attrs = 4; +}