diff --git a/go.mod b/go.mod index 8049378a..54ea0506 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/derailed/k9s go 1.13 -replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview - replace ( k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 @@ -30,7 +28,7 @@ replace ( require ( github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.2 + github.com/derailed/tview v0.3.3 github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect diff --git a/go.sum b/go.sum index e51bc4c1..f4153eee 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/derailed/tview v0.3.2 h1:By43yu6kbGvA+iL09VAhTKxKEd02BBOtUPIlrkeHxT4= github.com/derailed/tview v0.3.2/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= +github.com/derailed/tview v0.3.3 h1:tipPwxcDhx0zRBZuc8VKIrNgWL40FL5JeF/30XVieUE= +github.com/derailed/tview v0.3.3/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= diff --git a/internal/config/style.go b/internal/config/style.go index 14289964..b12bd586 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -14,10 +14,15 @@ var ( K9sStylesFile = filepath.Join(K9sHome, "skin.yml") ) +type StyleListener interface { + StylesChanged(*Styles) +} + type ( // Styles tracks K9s styling options. Styles struct { - K9s Style `yaml:"k9s"` + K9s Style `yaml:"k9s"` + listeners []StyleListener } // Body tracks body styles. @@ -257,9 +262,10 @@ func newMenu() Menu { } // NewStyles creates a new default config. -func NewStyles(path string) (*Styles, error) { - s := &Styles{K9s: newStyle()} - return s, s.load(path) +func NewStyles() *Styles { + return &Styles{ + K9s: newStyle(), + } } // FgColor returns the foreground color. @@ -272,6 +278,30 @@ func (s *Styles) BgColor() tcell.Color { return AsColor(s.Body().BgColor) } +func (s *Styles) AddListener(l StyleListener) { + s.listeners = append(s.listeners, l) +} + +func (s *Styles) RemoveListener(l StyleListener) { + victim := -1 + for i, lis := range s.listeners { + if lis == l { + victim = i + break + } + } + if victim == -1 { + return + } + s.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...) +} + +func (s *Styles) fireStylesChanged() { + for _, list := range s.listeners { + list.StylesChanged(s) + } +} + // Body returns body styles. func (s *Styles) Body() Body { return s.K9s.Body @@ -303,7 +333,7 @@ func (s *Styles) Views() Views { } // Load K9s configuration from file -func (s *Styles) load(path string) error { +func (s *Styles) Load(path string) error { f, err := ioutil.ReadFile(path) if err != nil { return err @@ -312,6 +342,7 @@ func (s *Styles) load(path string) error { if err := yaml.Unmarshal(f, s); err != nil { return err } + s.fireStylesChanged() return nil } @@ -330,6 +361,5 @@ func AsColor(c string) tcell.Color { if color, ok := tcell.ColorNames[c]; ok { return color } - - return tcell.ColorPink + return tcell.GetColor(c) } diff --git a/internal/config/style_test.go b/internal/config/style_test.go index fe1d9d95..56c2d78d 100644 --- a/internal/config/style_test.go +++ b/internal/config/style_test.go @@ -1,17 +1,33 @@ -package config +package config_test import ( "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/stretchr/testify/assert" ) -func TestSkinNone(t *testing.T) { - s, err := NewStyles("test_assets/empty_skin.yml") - assert.Nil(t, err) +func TestAsColor(t *testing.T) { + uu := map[string]tcell.Color{ + "blah": tcell.ColorDefault, + "blue": tcell.ColorBlue, + "#ffffff": tcell.NewHexColor(33554431), + "#ff0000": tcell.NewHexColor(33488896), + } + for k := range uu { + c, u := k, uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u, config.AsColor(c)) + }) + } +} + +func TestSkinNone(t *testing.T) { + s := config.NewStyles() + assert.Nil(t, s.Load("test_assets/empty_skin.yml")) s.Update() assert.Equal(t, "cadetblue", s.Body().FgColor) @@ -20,14 +36,11 @@ func TestSkinNone(t *testing.T) { assert.Equal(t, tcell.ColorCadetBlue, s.FgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) - assert.Equal(t, tcell.ColorPink, AsColor("blah")) - assert.Equal(t, tcell.ColorWhite, AsColor("white")) } func TestSkin(t *testing.T) { - s, err := NewStyles("test_assets/black_and_wtf.yml") - assert.Nil(t, err) - + s := config.NewStyles() + assert.Nil(t, s.Load("test_assets/black_and_wtf.yml")) s.Update() assert.Equal(t, "white", s.Body().FgColor) @@ -36,16 +49,14 @@ func TestSkin(t *testing.T) { assert.Equal(t, tcell.ColorWhite, s.FgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) - assert.Equal(t, tcell.ColorPink, AsColor("blah")) - assert.Equal(t, tcell.ColorWhite, AsColor("white")) } func TestSkinNotExits(t *testing.T) { - _, err := NewStyles("test_assets/blee.yml") - assert.NotNil(t, err) + s := config.NewStyles() + assert.NotNil(t, s.Load("test_assets/blee.yml")) } func TestSkinBoarked(t *testing.T) { - _, err := NewStyles("test_assets/skin_boarked.yml") - assert.NotNil(t, err) + s := config.NewStyles() + assert.NotNil(t, s.Load("test_assets/skin_boarked.yml")) } diff --git a/internal/keys.go b/internal/keys.go index d82167cb..5fc36bb5 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -21,4 +21,5 @@ const ( KeySubjectKind ContextKey = "subjectKind" KeySubjectName ContextKey = "subjectName" KeyNamespace ContextKey = "namespace" + KeyCluster ContextKey = "cluster" ) diff --git a/internal/model/generic.go b/internal/model/generic.go index 0bbc4d63..c049c038 100644 --- a/internal/model/generic.go +++ b/internal/model/generic.go @@ -27,10 +27,6 @@ type Generic struct { // List returns a collection of node resources. func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { - defer func(t time.Time) { - log.Debug().Msgf("LIST elapsed: %v", time.Since(t)) - }(time.Now()) - // Ensures the factory is tracking this resource _, err := g.factory.CanForResource(g.namespace, g.gvr) if err != nil { diff --git a/internal/model/policy.go b/internal/model/policy.go index 8cc167be..25b1c120 100644 --- a/internal/model/policy.go +++ b/internal/model/policy.go @@ -52,7 +52,6 @@ func (p *Policy) List(ctx context.Context) ([]runtime.Object, error) { return oo, nil } -// BOZO!! refactor! func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) { crbs, err := fetchClusterRoleBindings(p.factory) if err != nil { diff --git a/internal/model/rbac.go b/internal/model/rbac.go index 3f09da8b..9bf89b8c 100644 --- a/internal/model/rbac.go +++ b/internal/model/rbac.go @@ -51,7 +51,6 @@ func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { } } -// BOZO!!Refact gvr as const func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { o, err := r.factory.Get(crbGVR, path, labels.Everything()) if err != nil { diff --git a/internal/model/reconcile.go b/internal/model/reconcile.go deleted file mode 100644 index a1fe7e8c..00000000 --- a/internal/model/reconcile.go +++ /dev/null @@ -1,102 +0,0 @@ -package model - -import ( - "context" - "fmt" - "time" - - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/render" - "github.com/rs/zerolog/log" -) - -// Reconcile previous vs current state and emits delta events. -func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (render.TableData, error) { - defer func(t time.Time) { - log.Debug().Msgf("RECONCILE elapsed: %v", time.Since(t)) - }(time.Now()) - - path, ok := ctx.Value(internal.KeyPath).(string) - if !ok { - return table, fmt.Errorf("no path in context for %s", gvr) - } - log.Debug().Msgf("Reconcile %q in ns %q with path %q", gvr, table.Namespace, path) - factory, ok := ctx.Value(internal.KeyFactory).(Factory) - if !ok { - return table, fmt.Errorf("no Factory in context for %s", gvr) - } - m, ok := Registry[string(gvr)] - if !ok { - log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr) - m = ResourceMeta{ - Model: &Generic{}, - Renderer: &render.Generic{}, - } - } - if m.Model == nil { - m.Model = &Resource{} - } - m.Model.Init(table.Namespace, string(gvr), factory) - - oo, err := m.Model.List(ctx) - if err != nil { - return table, err - } - log.Debug().Msgf("Model returned [%d] items", len(oo)) - - rows := make(render.Rows, len(oo)) - if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil { - return table, err - } - update(&table, rows) - table.Header = m.Renderer.Header(table.Namespace) - - log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents)) - return table, nil -} - -func update(table *render.TableData, rows render.Rows) { - cacheEmpty := len(table.RowEvents) == 0 - kk := make([]string, 0, len(rows)) - var blankDelta render.DeltaRow - for _, row := range rows { - kk = append(kk, row.ID) - if cacheEmpty { - table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row)) - continue - } - if index, ok := table.RowEvents.FindIndex(row.ID); ok { - delta := render.NewDeltaRow(table.RowEvents[index].Row, row, table.Header.HasAge()) - if delta.IsBlank() { - table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta - } else { - table.RowEvents[index] = render.NewDeltaRowEvent(row, delta) - } - continue - } - table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row)) - } - - if cacheEmpty { - return - } - ensureDeletes(table, kk) -} - -// EnsureDeletes delete items in cache that are no longer valid. -func ensureDeletes(table *render.TableData, newKeys []string) { - for _, re := range table.RowEvents { - var found bool - for i, key := range newKeys { - if key == re.Row.ID { - found = true - newKeys = append(newKeys[:i], newKeys[i+1:]...) - break - } - } - if !found { - table.RowEvents = table.RowEvents.Delete(re.Row.ID) - } - } -} diff --git a/internal/model/resource.go b/internal/model/resource.go index f6701dc2..45fb6051 100644 --- a/internal/model/resource.go +++ b/internal/model/resource.go @@ -2,11 +2,9 @@ package model import ( "context" - "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" - "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -23,10 +21,6 @@ func (r *Resource) Init(ns, gvr string, f Factory) { // List returns a collection of nodes. func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { - defer func(t time.Time) { - log.Debug().Msgf("LIST elapsed: %v", time.Since(t)) - }(time.Now()) - strLabel, ok := ctx.Value(internal.KeyLabels).(string) lsel := labels.Everything() if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { @@ -37,10 +31,6 @@ func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { // Render returns a node as a row. func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { - defer func(t time.Time) { - log.Debug().Msgf("HYDRATE elapsed: %v", time.Since(t)) - }(time.Now()) - for i, o := range oo { if err := re.Render(o, r.namespace, &rr[i]); err != nil { return err diff --git a/internal/model/stack.go b/internal/model/stack.go index 0d91e137..c02b240d 100644 --- a/internal/model/stack.go +++ b/internal/model/stack.go @@ -117,7 +117,6 @@ func (s *Stack) Peek() []Component { // ClearHistory clear out the stack history up to most recent. func (s *Stack) ClearHistory() { - log.Debug().Msgf("STACK CLEARED!!") for range s.components { s.Pop() } diff --git a/internal/model/table.go b/internal/model/table.go index 8c67e1b9..169ac222 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -3,6 +3,7 @@ package model import ( "context" "fmt" + "runtime" "sync/atomic" "time" @@ -143,16 +144,8 @@ func (t *Table) fireTableLoadFailed(err error) { } func (t *Table) reconcile(ctx context.Context) error { - defer func(t time.Time) { - log.Debug().Msgf("RECONCILE elapsed: %v", time.Since(t)) - }(time.Now()) + log.Debug().Msgf("GOROUTINE %d", runtime.NumGoroutine()) - path, ok := ctx.Value(internal.KeyPath).(string) - if !ok { - return fmt.Errorf("no path in context for %s", t.gvr) - } - - log.Debug().Msgf("Reconcile %q in %q:%q", t.gvr, t.namespace, path) factory, ok := ctx.Value(internal.KeyFactory).(Factory) if !ok { return fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory)) @@ -182,6 +175,5 @@ func (t *Table) reconcile(ctx context.Context) error { t.data.Update(rows) t.data.Namespace, t.data.Header = t.namespace, m.Renderer.Header(t.namespace) - log.Debug().Msgf("Table returned [%d] events", len(t.data.RowEvents)) return nil } diff --git a/internal/render/ev.go b/internal/render/ev.go index a03aa9cb..5a87191d 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -70,7 +70,7 @@ func (e Event) Render(o interface{}, ns string, r *Row) error { r.Fields = append(r.Fields, ev.Namespace) } r.Fields = append(r.Fields, - ev.Name, + asRef(ev.InvolvedObject), ev.Reason, ev.Source.Component, strconv.Itoa(int(ev.Count)), @@ -79,3 +79,7 @@ func (e Event) Render(o interface{}, ns string, r *Row) error { return nil } + +func asRef(r v1.ObjectReference) string { + return strings.ToLower(r.Kind) + ":" + r.Name +} diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go index 81d8febc..766e5c7a 100644 --- a/internal/render/ev_test.go +++ b/internal/render/ev_test.go @@ -13,5 +13,5 @@ func TestEventRender(t *testing.T) { c.Render(load(t, "ev"), "", &r) assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) - assert.Equal(t, render.Fields{"default", "hello-1567197780-mn4mv.15bfce150bd764dd", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:6]) + assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:6]) } diff --git a/internal/ui/action.go b/internal/ui/action.go index 15041d80..f9f561a9 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -17,6 +17,7 @@ type ( Description string Action ActionHandler Visible bool + Shared bool } // KeyActions tracks mappings between keystrokes and actions. @@ -28,6 +29,10 @@ func NewKeyAction(d string, a ActionHandler, display bool) KeyAction { return KeyAction{Description: d, Action: a, Visible: display} } +func NewSharedKeyAction(d string, a ActionHandler, display bool) KeyAction { + return KeyAction{Description: d, Action: a, Visible: display, Shared: true} +} + // Add sets up keyboard action listener. func (a KeyActions) Add(aa KeyActions) { for k, v := range aa { @@ -60,7 +65,9 @@ func (a KeyActions) Delete(kk ...tcell.Key) { func (a KeyActions) Hints() model.MenuHints { kk := make([]int, 0, len(a)) for k := range a { - kk = append(kk, int(k)) + if !a[k].Shared { + kk = append(kk, int(k)) + } } sort.Ints(kk) diff --git a/internal/ui/app.go b/internal/ui/app.go index 92ace63b..97dd67ee 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -18,20 +18,20 @@ type App struct { } // NewApp returns a new app. -func NewApp() *App { +func NewApp(cluster string) *App { a := App{ Application: tview.NewApplication(), actions: make(KeyActions), Main: NewPages(), cmdBuff: NewCmdBuff(':', CommandBuff), } - a.RefreshStyles() + a.ReloadStyles(cluster) a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), - "logo": NewLogoView(a.Styles), - "cmd": NewCmdView(a.Styles), - "flash": NewFlashView(&a, "Initializing..."), + "logo": NewLogo(a.Styles), + "cmd": NewCommand(a.Styles), + "flash": NewFlash(&a, "Initializing..."), "crumbs": NewCrumbs(a.Styles), } @@ -46,6 +46,10 @@ func (a *App) Init() { a.SetRoot(a.Main, true) } +func (a *App) ReloadStyles(cluster string) { + a.RefreshStyles(cluster) +} + // Conn returns an api server connection. func (a *App) Conn() client.Connection { return a.Config.GetConnection() @@ -188,18 +192,18 @@ func (a *App) Crumbs() *Crumbs { } // Logo return the app logo. -func (a *App) Logo() *LogoView { - return a.views["logo"].(*LogoView) +func (a *App) Logo() *Logo { + return a.views["logo"].(*Logo) } // Flash returns app flash. -func (a *App) Flash() *FlashView { - return a.views["flash"].(*FlashView) +func (a *App) Flash() *Flash { + return a.views["flash"].(*Flash) } // Cmd returns app cmd. -func (a *App) Cmd() *CmdView { - return a.views["cmd"].(*CmdView) +func (a *App) Cmd() *Command { + return a.views["cmd"].(*Command) } // Menu returns app menu. diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 0aabd540..db0047f8 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -8,7 +8,7 @@ import ( ) func TestAppGetCmd(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() a.CmdBuff().Set("blee") @@ -16,7 +16,7 @@ func TestAppGetCmd(t *testing.T) { } func TestAppInCmdMode(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() a.CmdBuff().Set("blee") assert.False(t, a.InCmdMode()) @@ -26,7 +26,7 @@ func TestAppInCmdMode(t *testing.T) { } func TestAppResetCmd(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() a.CmdBuff().Set("blee") @@ -36,7 +36,7 @@ func TestAppResetCmd(t *testing.T) { } func TestAppHasCmd(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() a.ActivateCmd(true) @@ -47,7 +47,7 @@ func TestAppHasCmd(t *testing.T) { } func TestAppGetActions(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) @@ -56,7 +56,7 @@ func TestAppGetActions(t *testing.T) { } func TestAppViews(t *testing.T) { - a := ui.NewApp() + a := ui.NewApp("") a.Init() vv := []string{"crumbs", "logo", "cmd", "flash", "menu"} diff --git a/internal/ui/cmd.go b/internal/ui/cmd.go deleted file mode 100644 index 326890e0..00000000 --- a/internal/ui/cmd.go +++ /dev/null @@ -1,101 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) - -const defaultPrompt = "%c> %s" - -// CmdView captures users free from command input. -type CmdView struct { - *tview.TextView - - activated bool - icon rune - text string - styles *config.Styles -} - -// NewCmdView returns a new command view. -func NewCmdView(styles *config.Styles) *CmdView { - v := CmdView{styles: styles, TextView: tview.NewTextView()} - v.SetWordWrap(true) - v.SetWrap(true) - v.SetDynamicColors(true) - v.SetBorder(true) - v.SetBorderPadding(0, 0, 1, 1) - v.SetBackgroundColor(styles.BgColor()) - v.SetTextColor(styles.FgColor()) - - return &v -} - -// InCmdMode returns true if command is active, false otherwise. -func (v *CmdView) InCmdMode() bool { - return v.activated -} - -func (v *CmdView) activate() { - v.write(v.text) -} - -func (v *CmdView) update(s string) { - if v.text == s { - return - } - v.text = s - v.Clear() - v.write(v.text) -} - -func (v *CmdView) write(s string) { - fmt.Fprintf(v, defaultPrompt, v.icon, s) -} - -// ---------------------------------------------------------------------------- -// Event Listener protocol... - -// BufferChanged indicates the buffer was changed. -func (v *CmdView) BufferChanged(s string) { - v.update(s) -} - -// BufferActive indicates the buff activity changed. -func (v *CmdView) BufferActive(f bool, k BufferKind) { - if v.activated = f; f { - v.SetBorder(true) - v.SetTextColor(v.styles.FgColor()) - v.SetBorderColor(colorFor(k)) - v.icon = iconFor(k) - // v.reset() - v.activate() - } else { - v.SetBorder(false) - v.SetBackgroundColor(v.styles.BgColor()) - v.Clear() - } - log.Debug().Msgf("CmdView activated: %t", v.activated) -} - -func colorFor(k BufferKind) tcell.Color { - switch k { - case CommandBuff: - return tcell.ColorAqua - default: - return tcell.ColorSeaGreen - } -} - -func iconFor(k BufferKind) rune { - switch k { - case CommandBuff: - return '🐶' - default: - return '🐩' - } -} diff --git a/internal/ui/cmd_buff.go b/internal/ui/cmd_buff.go index a22dc96b..bbb57804 100644 --- a/internal/ui/cmd_buff.go +++ b/internal/ui/cmd_buff.go @@ -1,7 +1,5 @@ package ui -import "github.com/rs/zerolog/log" - const maxBuff = 10 const ( @@ -67,7 +65,6 @@ func (c *CmdBuff) IsActive() bool { // SetActive toggles cmd buffer active state. func (c *CmdBuff) SetActive(b bool) { - log.Debug().Msgf("CMDBUFF -- Active %t", b) c.active = b c.fireActive(c.active) } @@ -146,9 +143,7 @@ func (c *CmdBuff) fireChanged() { } func (c *CmdBuff) fireActive(b bool) { - log.Debug().Msgf("CMDBUFF LIST SIZE %d", len(c.listeners)) for _, l := range c.listeners { - log.Debug().Msgf("CMDBUFF LIST -- %T", l) l.BufferActive(b, c.kind) } } diff --git a/internal/ui/command.go b/internal/ui/command.go new file mode 100644 index 00000000..f50c4fc3 --- /dev/null +++ b/internal/ui/command.go @@ -0,0 +1,108 @@ +package ui + +import ( + "fmt" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const defaultPrompt = "%c> %s" + +// Command captures users free from command input. +type Command struct { + *tview.TextView + + activated bool + icon rune + text string + styles *config.Styles +} + +// NewCommand returns a new command view. +func NewCommand(styles *config.Styles) *Command { + c := Command{styles: styles, TextView: tview.NewTextView()} + c.SetWordWrap(true) + c.SetWrap(true) + c.SetDynamicColors(true) + c.SetBorder(true) + c.SetBorderPadding(0, 0, 1, 1) + c.SetBackgroundColor(styles.BgColor()) + c.SetTextColor(styles.FgColor()) + styles.AddListener(&c) + + return &c +} + +func (c *Command) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.SetTextColor(s.FgColor()) +} + +// InCmdMode returns true if command is active, false otherwise. +func (c *Command) InCmdMode() bool { + return c.activated +} + +func (c *Command) activate() { + c.write(c.text) +} + +func (c *Command) update(s string) { + if c.text == s { + return + } + c.text = s + c.Clear() + c.write(c.text) +} + +func (c *Command) write(s string) { + fmt.Fprintf(c, defaultPrompt, c.icon, s) +} + +// ---------------------------------------------------------------------------- +// Event Listener protocol... + +// BufferChanged indicates the buffer was changed. +func (c *Command) BufferChanged(s string) { + c.update(s) +} + +// BufferActive indicates the buff activity changed. +func (c *Command) BufferActive(f bool, k BufferKind) { + if c.activated = f; f { + c.SetBorder(true) + c.SetTextColor(c.styles.FgColor()) + c.SetBorderColor(colorFor(k)) + c.icon = iconFor(k) + // c.reset() + c.activate() + } else { + c.SetBorder(false) + c.SetBackgroundColor(c.styles.BgColor()) + c.Clear() + } + log.Debug().Msgf("Command activated: %t", c.activated) +} + +func colorFor(k BufferKind) tcell.Color { + switch k { + case CommandBuff: + return tcell.ColorAqua + default: + return tcell.ColorSeaGreen + } +} + +func iconFor(k BufferKind) rune { + switch k { + case CommandBuff: + return '🐶' + default: + return '🐩' + } +} diff --git a/internal/ui/cmd_test.go b/internal/ui/command_test.go similarity index 79% rename from internal/ui/cmd_test.go rename to internal/ui/command_test.go index f0434efb..bfe82f74 100644 --- a/internal/ui/cmd_test.go +++ b/internal/ui/command_test.go @@ -9,8 +9,7 @@ import ( ) func TestCmdNew(t *testing.T) { - defaults, _ := config.NewStyles("") - v := ui.NewCmdView(defaults) + v := ui.NewCommand(config.NewStyles()) buff := ui.NewCmdBuff(':', ui.CommandBuff) buff.AddListener(v) @@ -20,8 +19,7 @@ func TestCmdNew(t *testing.T) { } func TestCmdUpdate(t *testing.T) { - defaults, _ := config.NewStyles("") - v := ui.NewCmdView(defaults) + v := ui.NewCommand(config.NewStyles()) buff := ui.NewCmdBuff(':', ui.CommandBuff) buff.AddListener(v) @@ -34,8 +32,7 @@ func TestCmdUpdate(t *testing.T) { } func TestCmdMode(t *testing.T) { - defaults, _ := config.NewStyles("") - v := ui.NewCmdView(defaults) + v := ui.NewCommand(config.NewStyles()) buff := ui.NewCmdBuff(':', ui.CommandBuff) buff.AddListener(v) diff --git a/internal/ui/config.go b/internal/ui/config.go index b3432415..476dc79a 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -2,6 +2,7 @@ package ui import ( "context" + "fmt" "path/filepath" "github.com/derailed/k9s/internal/config" @@ -19,14 +20,22 @@ type synchronizer interface { // Configurator represents an application configurationa. type Configurator struct { - HasSkins bool + skinFile string Config *config.Config Styles *config.Styles Bench *config.Bench } +func (c *Configurator) HasSkins() bool { + return c.skinFile != "" +} + // StylesUpdater watches for skin file changes. func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error { + if !c.HasSkins() { + return nil + } + w, err := fsnotify.NewWatcher() if err != nil { return err @@ -38,12 +47,13 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error case evt := <-w.Events: _ = evt s.QueueUpdateDraw(func() { - c.RefreshStyles() + c.RefreshStyles(c.Config.K9s.CurrentCluster) }) case err := <-w.Errors: log.Info().Err(err).Msg("Skin watcher failed") return case <-ctx.Done(): + log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile) if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing watcher") } @@ -52,7 +62,8 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error } }() - return w.Add(config.K9sStylesFile) + log.Debug().Msgf("SkinWatcher watching `%s", c.skinFile) + return w.Add(c.skinFile) } // InitBench load benchmark configuration if any. @@ -69,14 +80,28 @@ func BenchConfig(cluster string) string { } // RefreshStyles load for skin configuration changes. -func (c *Configurator) RefreshStyles() { - var err error - if c.Styles, err = config.NewStyles(config.K9sStylesFile); err != nil { - log.Info().Msg("No skin file found. Loading stock skins.") +func (c *Configurator) RefreshStyles(cluster string) { + clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", cluster)) + if c.Styles == nil { + c.Styles = config.NewStyles() } - if err == nil { - c.HasSkins = true + if err := c.Styles.Load(clusterSkins); err != nil { + log.Info().Msgf("No cluster specific skin file found -- %s", clusterSkins) + } else { + log.Debug().Msgf("Found cluster skins %s", clusterSkins) + c.updateStyles(clusterSkins) + return } + + if err := c.Styles.Load(config.K9sStylesFile); err != nil { + log.Info().Msgf("No skin file found -- %s. Loading stock skins.", config.K9sStylesFile) + return + } + c.updateStyles(config.K9sStylesFile) +} + +func (c *Configurator) updateStyles(f string) { + c.skinFile = f c.Styles.Update() render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 484387fb..0c409721 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -20,9 +20,9 @@ func TestConfiguratorRefreshStyle(t *testing.T) { config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml") cfg := ui.Configurator{} - cfg.RefreshStyles() + cfg.RefreshStyles("") - assert.True(t, cfg.HasSkins) + assert.True(t, cfg.HasSkins()) assert.Equal(t, tcell.ColorGhostWhite, render.StdColor) assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor) } diff --git a/internal/ui/crumbs.go b/internal/ui/crumbs.go index bec5006c..37dd9293 100644 --- a/internal/ui/crumbs.go +++ b/internal/ui/crumbs.go @@ -19,45 +19,52 @@ type Crumbs struct { // NewCrumbs returns a new breadcrumb view. func NewCrumbs(styles *config.Styles) *Crumbs { - v := Crumbs{ + c := Crumbs{ stack: model.NewStack(), styles: styles, TextView: tview.NewTextView(), } - v.SetBackgroundColor(styles.BgColor()) - v.SetTextAlign(tview.AlignLeft) - v.SetBorderPadding(0, 0, 1, 1) - v.SetDynamicColors(true) + c.SetBackgroundColor(styles.BgColor()) + c.SetTextAlign(tview.AlignLeft) + c.SetBorderPadding(0, 0, 1, 1) + c.SetDynamicColors(true) + styles.AddListener(&c) - return &v + return &c +} + +func (c *Crumbs) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.refresh(c.stack.Flatten()) } // StackPushed indicates a new item was added. -func (v *Crumbs) StackPushed(c model.Component) { - v.stack.Push(c) - v.refresh(v.stack.Flatten()) +func (c *Crumbs) StackPushed(comp model.Component) { + c.stack.Push(comp) + c.refresh(c.stack.Flatten()) } // StackPopped indicates an item was deleted -func (v *Crumbs) StackPopped(_, _ model.Component) { - v.stack.Pop() - v.refresh(v.stack.Flatten()) +func (c *Crumbs) StackPopped(_, _ model.Component) { + c.stack.Pop() + c.refresh(c.stack.Flatten()) } // StackTop indicates the top of the stack -func (v *Crumbs) StackTop(top model.Component) {} +func (c *Crumbs) StackTop(top model.Component) {} // Refresh updates view with new crumbs. -func (v *Crumbs) refresh(crumbs []string) { - v.Clear() - last, bgColor := len(crumbs)-1, v.styles.Frame().Crumb.BgColor - for i, c := range crumbs { +func (c *Crumbs) refresh(crumbs []string) { + c.Clear() + last, bgColor := len(crumbs)-1, c.styles.Frame().Crumb.BgColor + for i, crumb := range crumbs { if i == last { - bgColor = v.styles.Frame().Crumb.ActiveColor + bgColor = c.styles.Frame().Crumb.ActiveColor } - fmt.Fprintf(v, "[%s:%s:b] <%s> [-:%s:-] ", - v.styles.Frame().Crumb.FgColor, - bgColor, strings.Replace(strings.ToLower(c), " ", "", -1), - v.styles.Body().BgColor) + fmt.Fprintf(c, "[%s:%s:b] <%s> [-:%s:-] ", + c.styles.Frame().Crumb.FgColor, + bgColor, strings.Replace(strings.ToLower(crumb), " ", "", -1), + c.styles.Body().BgColor) } } diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index abcd5596..3d3c807f 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -18,8 +18,7 @@ func init() { } func TestNewCrumbs(t *testing.T) { - defaults, _ := config.NewStyles("") - v := ui.NewCrumbs(defaults) + v := ui.NewCrumbs(config.NewStyles()) v.StackPushed(makeComponent("c1")) v.StackPushed(makeComponent("c2")) v.StackPushed(makeComponent("c3")) diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 1efc2a69..8c2c883d 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" @@ -34,8 +35,8 @@ type ( // FlashLevel represents flash message severity. FlashLevel int - // FlashView represents a flash message indicator. - FlashView struct { + // Flash represents a flash message indicator. + Flash struct { *tview.TextView cancel context.CancelFunc @@ -43,45 +44,51 @@ type ( } ) -// NewFlashView returns a new flash view. -func NewFlashView(app *App, m string) *FlashView { - f := FlashView{app: app, TextView: tview.NewTextView()} +// NewFlash returns a new flash view. +func NewFlash(app *App, m string) *Flash { + f := Flash{app: app, TextView: tview.NewTextView()} f.SetTextColor(tcell.ColorAqua) f.SetTextAlign(tview.AlignLeft) f.SetBorderPadding(0, 0, 1, 1) f.SetText("") + f.app.Styles.AddListener(&f) return &f } +func (f *Flash) StylesChanged(s *config.Styles) { + f.SetBackgroundColor(s.BgColor()) + f.SetTextColor(s.FgColor()) +} + // Info displays an info flash message. -func (v *FlashView) Info(msg string) { - v.setMessage(FlashInfo, msg) +func (f *Flash) Info(msg string) { + f.setMessage(FlashInfo, msg) } // Infof displays a formatted info flash message. -func (v *FlashView) Infof(fmat string, args ...interface{}) { - v.Info(fmt.Sprintf(fmat, args...)) +func (f *Flash) Infof(fmat string, args ...interface{}) { + f.Info(fmt.Sprintf(fmat, args...)) } // Warn displays a warning flash message. -func (v *FlashView) Warn(msg string) { - v.setMessage(FlashWarn, msg) +func (f *Flash) Warn(msg string) { + f.setMessage(FlashWarn, msg) } // Warnf displays a formatted warning flash message. -func (v *FlashView) Warnf(fmat string, args ...interface{}) { - v.Warn(fmt.Sprintf(fmat, args...)) +func (f *Flash) Warnf(fmat string, args ...interface{}) { + f.Warn(fmt.Sprintf(fmat, args...)) } // Err displays an error flash message. -func (v *FlashView) Err(err error) { +func (f *Flash) Err(err error) { log.Error().Err(err).Msgf("%v", err) - v.setMessage(FlashErr, err.Error()) + f.setMessage(FlashErr, err.Error()) } // Errf displays a formatted error flash message. -func (v *FlashView) Errf(fmat string, args ...interface{}) { +func (f *Flash) Errf(fmat string, args ...interface{}) { var err error for _, a := range args { switch e := a.(type) { @@ -90,30 +97,30 @@ func (v *FlashView) Errf(fmat string, args ...interface{}) { } } log.Error().Err(err).Msgf(fmat, args...) - v.setMessage(FlashErr, fmt.Sprintf(fmat, args...)) + f.setMessage(FlashErr, fmt.Sprintf(fmat, args...)) } -func (v *FlashView) setMessage(level FlashLevel, msg ...string) { - if v.cancel != nil { - v.cancel() +func (f *Flash) setMessage(level FlashLevel, msg ...string) { + if f.cancel != nil { + f.cancel() } var ctx1, ctx2 context.Context { var timerCancel context.CancelFunc - ctx1, v.cancel = context.WithCancel(context.TODO()) + ctx1, f.cancel = context.WithCancel(context.TODO()) ctx2, timerCancel = context.WithTimeout(context.TODO(), flashDelay*time.Second) - go v.refresh(ctx1, ctx2, timerCancel) + go f.refresh(ctx1, ctx2, timerCancel) } - _, _, width, _ := v.GetRect() + _, _, width, _ := f.GetRect() if width <= 15 { width = 100 } m := strings.Join(msg, " ") - v.SetTextColor(flashColor(level)) - v.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) + f.SetTextColor(flashColor(level)) + f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) } -func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) { +func (f *Flash) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) { defer cancel() for { select { @@ -122,8 +129,8 @@ func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFun return // Timed out clear and bail case <-ctx2.Done(): - v.app.QueueUpdateDraw(func() { - v.Clear() + f.app.QueueUpdateDraw(func() { + f.Clear() }) return } diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index 19032d0f..7f3f022e 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -9,7 +9,7 @@ import ( ) func TestFlashInfo(t *testing.T) { - f := ui.NewFlashView(ui.NewApp(), "YO!") + f := ui.NewFlash(ui.NewApp(""), "YO!") f.Info("Blee") assert.Equal(t, "😎 Blee\n", f.GetText(false)) @@ -19,7 +19,7 @@ func TestFlashInfo(t *testing.T) { } func TestFlashWarn(t *testing.T) { - f := ui.NewFlashView(ui.NewApp(), "YO!") + f := ui.NewFlash(ui.NewApp(""), "YO!") f.Warn("Blee") assert.Equal(t, "😗 Blee\n", f.GetText(false)) @@ -29,7 +29,7 @@ func TestFlashWarn(t *testing.T) { } func TestFlashErr(t *testing.T) { - f := ui.NewFlashView(ui.NewApp(), "YO!") + f := ui.NewFlash(ui.NewApp(""), "YO!") f.Err(errors.New("Blee")) assert.Equal(t, "😡 Blee\n", f.GetText(false)) diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index 8d5bea25..dd46756b 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -10,8 +10,8 @@ import ( "github.com/gdamore/tcell" ) -// IndicatorView represents a status indicator. -type IndicatorView struct { +// StatusIndicator represents a status indicator when main header is collapsed. +type StatusIndicator struct { *tview.TextView app *App @@ -20,67 +20,74 @@ type IndicatorView struct { cancel context.CancelFunc } -// NewIndicatorView returns a new status indicator. -func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView { - v := IndicatorView{ +// NewStatusIndicator returns a new status indicator. +func NewStatusIndicator(app *App, styles *config.Styles) *StatusIndicator { + s := StatusIndicator{ TextView: tview.NewTextView(), app: app, styles: styles, } - v.SetTextAlign(tview.AlignCenter) - v.SetTextColor(tcell.ColorWhite) - v.SetBackgroundColor(styles.BgColor()) - v.SetDynamicColors(true) + s.SetTextAlign(tview.AlignCenter) + s.SetTextColor(tcell.ColorWhite) + s.SetBackgroundColor(styles.BgColor()) + s.SetDynamicColors(true) + styles.AddListener(&s) - return &v + return &s +} + +func (s *StatusIndicator) StylesChanged(styles *config.Styles) { + s.styles = styles + s.SetBackgroundColor(styles.BgColor()) + s.SetTextColor(styles.FgColor()) } // SetPermanent sets permanent title to be reset to after updates -func (v *IndicatorView) SetPermanent(info string) { - v.permanent = info - v.SetText(info) +func (s *StatusIndicator) SetPermanent(info string) { + s.permanent = info + s.SetText(info) } // Reset clears out the logo view and resets colors. -func (v *IndicatorView) Reset() { - v.Clear() - v.SetPermanent(v.permanent) +func (s *StatusIndicator) Reset() { + s.Clear() + s.SetPermanent(s.permanent) } // Err displays a log error state. -func (v *IndicatorView) Err(msg string) { - v.update(msg, "orangered") +func (s *StatusIndicator) Err(msg string) { + s.update(msg, "orangered") } // Warn displays a log warning state. -func (v *IndicatorView) Warn(msg string) { - v.update(msg, "mediumvioletred") +func (s *StatusIndicator) Warn(msg string) { + s.update(msg, "mediumvioletred") } // Info displays a log info state. -func (v *IndicatorView) Info(msg string) { - v.update(msg, "lawngreen") +func (s *StatusIndicator) Info(msg string) { + s.update(msg, "lawngreen") } -func (v *IndicatorView) update(msg, c string) { - v.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg)) +func (s *StatusIndicator) update(msg, c string) { + s.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg)) } -func (v *IndicatorView) setText(msg string) { - if v.cancel != nil { - v.cancel() +func (s *StatusIndicator) setText(msg string) { + if s.cancel != nil { + s.cancel() } - v.SetText(msg) + s.SetText(msg) var ctx context.Context - ctx, v.cancel = context.WithCancel(context.Background()) + ctx, s.cancel = context.WithCancel(context.Background()) go func(ctx context.Context) { select { case <-ctx.Done(): return case <-time.After(5 * time.Second): - v.app.QueueUpdateDraw(func() { - v.Reset() + s.app.QueueUpdateDraw(func() { + s.Reset() }) } }(ctx) diff --git a/internal/ui/indicator_test.go b/internal/ui/indicator_test.go index 9ddea4c9..2e3c5a36 100644 --- a/internal/ui/indicator_test.go +++ b/internal/ui/indicator_test.go @@ -9,9 +9,7 @@ import ( ) func TestIndicatorReset(t *testing.T) { - s, _ := config.NewStyles("") - - i := ui.NewIndicatorView(ui.NewApp(), s) + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) i.SetPermanent("Blee") i.Info("duh") i.Reset() @@ -20,27 +18,21 @@ func TestIndicatorReset(t *testing.T) { } func TestIndicatorInfo(t *testing.T) { - s, _ := config.NewStyles("") - - i := ui.NewIndicatorView(ui.NewApp(), s) + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) i.Info("Blee") assert.Equal(t, "[lawngreen::b] \n", i.GetText(false)) } func TestIndicatorWarn(t *testing.T) { - s, _ := config.NewStyles("") - - i := ui.NewIndicatorView(ui.NewApp(), s) + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) i.Warn("Blee") assert.Equal(t, "[mediumvioletred::b] \n", i.GetText(false)) } func TestIndicatorErr(t *testing.T) { - s, _ := config.NewStyles("") - - i := ui.NewIndicatorView(ui.NewApp(), s) + i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) i.Err("Blee") assert.Equal(t, "[orangered::b] \n", i.GetText(false)) diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 6ac7fe48..e01ae33d 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -7,67 +7,84 @@ import ( "github.com/derailed/tview" ) -// LogoView represents a K9s logo. -type LogoView struct { +// Logo represents a K9s logo. +type Logo struct { *tview.Flex + logo, status *tview.TextView styles *config.Styles } -// NewLogoView returns a new logo. -func NewLogoView(styles *config.Styles) *LogoView { - v := LogoView{ +// NewLogo returns a new logo. +func NewLogo(styles *config.Styles) *Logo { + l := Logo{ Flex: tview.NewFlex(), logo: logo(), status: status(), styles: styles, } - v.SetDirection(tview.FlexRow) - v.AddItem(v.logo, 0, 6, false) - v.AddItem(v.status, 0, 1, false) - v.refreshLogo(styles.Body().LogoColor) + l.SetDirection(tview.FlexRow) + l.AddItem(l.logo, 0, 6, false) + l.AddItem(l.status, 0, 1, false) + l.refreshLogo(styles.Body().LogoColor) + styles.AddListener(&l) - return &v + return &l +} + +func (l *Logo) Logo() *tview.TextView { + return l.logo +} + +func (l *Logo) Status() *tview.TextView { + return l.status +} + +func (l *Logo) StylesChanged(s *config.Styles) { + l.styles = s + l.Reset() } // Reset clears out the logo view and resets colors. -func (v *LogoView) Reset() { - v.status.Clear() - v.status.SetBackgroundColor(v.styles.BgColor()) - v.refreshLogo(v.styles.Body().LogoColor) +func (l *Logo) Reset() { + l.status.Clear() + l.SetBackgroundColor(l.styles.BgColor()) + l.status.SetBackgroundColor(l.styles.BgColor()) + l.logo.SetBackgroundColor(l.styles.BgColor()) + l.refreshLogo(l.styles.Body().LogoColor) } // Err displays a log error state. -func (v *LogoView) Err(msg string) { - v.update(msg, "red") +func (l *Logo) Err(msg string) { + l.update(msg, "red") } // Warn displays a log warning state. -func (v *LogoView) Warn(msg string) { - v.update(msg, "mediumvioletred") +func (l *Logo) Warn(msg string) { + l.update(msg, "mediumvioletred") } // Info displays a log info state. -func (v *LogoView) Info(msg string) { - v.update(msg, "green") +func (l *Logo) Info(msg string) { + l.update(msg, "green") } -func (v *LogoView) update(msg, c string) { - v.refreshStatus(msg, c) - v.refreshLogo(c) +func (l *Logo) update(msg, c string) { + l.refreshStatus(msg, c) + l.refreshLogo(c) } -func (v *LogoView) refreshStatus(msg, c string) { - v.status.SetBackgroundColor(config.AsColor(c)) - v.status.SetText(fmt.Sprintf("[white::b]%s", msg)) +func (l *Logo) refreshStatus(msg, c string) { + l.status.SetBackgroundColor(config.AsColor(c)) + l.status.SetText(fmt.Sprintf("[white::b]%s", msg)) } -func (v *LogoView) refreshLogo(c string) { - v.logo.Clear() +func (l *Logo) refreshLogo(c string) { + l.logo.Clear() for i, s := range LogoSmall { - fmt.Fprintf(v.logo, "[%s::b]%s", c, s) + fmt.Fprintf(l.logo, "[%s::b]%s", c, s) if i+1 < len(LogoSmall) { - fmt.Fprintf(v.logo, "\n") + fmt.Fprintf(l.logo, "\n") } } } diff --git a/internal/ui/logo_test.go b/internal/ui/logo_test.go index 41c0f3a7..ebb67ad5 100644 --- a/internal/ui/logo_test.go +++ b/internal/ui/logo_test.go @@ -1,20 +1,20 @@ -package ui +package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestNewLogoView(t *testing.T) { - defaults, _ := config.NewStyles("") - v := NewLogoView(defaults) + v := ui.NewLogo(config.NewStyles()) v.Reset() const elogo = "[orange::b] ____ __.________ \n[orange::b]| |/ _/ __ \\______\n[orange::b]| < \\____ / ___/\n[orange::b]| | \\ / /\\___ \\ \n[orange::b]|____|__ \\ /____//____ >\n[orange::b] \\/ \\/ \n" - assert.Equal(t, elogo, v.logo.GetText(false)) - assert.Equal(t, "", v.status.GetText(false)) + assert.Equal(t, elogo, v.Logo().GetText(false)) + assert.Equal(t, "", v.Status().GetText(false)) } func TestLogoStatus(t *testing.T) { @@ -38,8 +38,7 @@ func TestLogoStatus(t *testing.T) { }, } - defaults, _ := config.NewStyles("") - v := NewLogoView(defaults) + v := ui.NewLogo(config.NewStyles()) for n := range uu { k, u := n, uu[n] t.Run(k, func(t *testing.T) { @@ -51,8 +50,8 @@ func TestLogoStatus(t *testing.T) { case "err": v.Err(u.msg) } - assert.Equal(t, u.logo, v.logo.GetText(false)) - assert.Equal(t, u.e, v.status.GetText(false)) + assert.Equal(t, u.logo, v.Logo().GetText(false)) + assert.Equal(t, u.e, v.Status().GetText(false)) }) } diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 5bf983b0..50cf47e7 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -31,56 +31,65 @@ type Menu struct { // NewMenu returns a new menu. func NewMenu(styles *config.Styles) *Menu { - v := Menu{Table: tview.NewTable(), styles: styles} - v.SetBackgroundColor(styles.BgColor()) + m := Menu{ + Table: tview.NewTable(), + styles: styles, + } + m.SetBackgroundColor(styles.BgColor()) + styles.AddListener(&m) - return &v + return &m } -func (v *Menu) StackPushed(c model.Component) { - v.HydrateMenu(c.Hints()) +func (m *Menu) StylesChanged(s *config.Styles) { + m.styles = s + m.SetBackgroundColor(s.BgColor()) } -func (v *Menu) StackPopped(o, top model.Component) { +func (m *Menu) StackPushed(c model.Component) { + m.HydrateMenu(c.Hints()) +} + +func (m *Menu) StackPopped(o, top model.Component) { if top != nil { - v.HydrateMenu(top.Hints()) + m.HydrateMenu(top.Hints()) } else { - v.Clear() + m.Clear() } } -func (v *Menu) StackTop(t model.Component) { - v.HydrateMenu(t.Hints()) +func (m *Menu) StackTop(t model.Component) { + m.HydrateMenu(t.Hints()) } // HydrateMenu populate menu ui from hints. -func (v *Menu) HydrateMenu(hh model.MenuHints) { - v.Clear() +func (m *Menu) HydrateMenu(hh model.MenuHints) { + m.Clear() sort.Sort(hh) table := make([]model.MenuHints, maxRows+1) colCount := (len(hh) / maxRows) + 1 - if v.hasDigits(hh) { + if m.hasDigits(hh) { colCount++ } for row := 0; row < maxRows; row++ { table[row] = make(model.MenuHints, colCount) } - t := v.buildMenuTable(hh, table, colCount) + t := m.buildMenuTable(hh, table, colCount) for row := 0; row < len(t); row++ { for col := 0; col < len(t[row]); col++ { - if len(t[row][col]) == 0 { - continue - } c := tview.NewTableCell(t[row][col]) - c.SetBackgroundColor(v.styles.BgColor()) - v.SetCell(row, col, c) + if len(t[row][col]) == 0 { + c = tview.NewTableCell("") + } + c.SetBackgroundColor(m.styles.BgColor()) + m.SetCell(row, col, c) } } } -func (v *Menu) hasDigits(hh model.MenuHints) bool { +func (m *Menu) hasDigits(hh model.MenuHints) bool { for _, h := range hh { if !h.Visible { continue @@ -92,7 +101,7 @@ func (v *Menu) hasDigits(hh model.MenuHints) bool { return false } -func (v *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCount int) [][]string { +func (m *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCount int) [][]string { var row, col int firstCmd := true maxKeys := make([]int, colCount) @@ -121,30 +130,30 @@ func (v *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCo for r := range out { out[r] = make([]string, len(table[r])) } - v.layout(table, maxKeys, out) + m.layout(table, maxKeys, out) return out } -func (v *Menu) layout(table []model.MenuHints, mm []int, out [][]string) { +func (m *Menu) layout(table []model.MenuHints, mm []int, out [][]string) { for r := range table { for c := range table[r] { - out[r][c] = keyConv(v.formatMenu(table[r][c], mm[c])) + out[r][c] = keyConv(m.formatMenu(table[r][c], mm[c])) } } } -func (v *Menu) formatMenu(h model.MenuHint, size int) string { +func (m *Menu) formatMenu(h model.MenuHint, size int) string { if h.Mnemonic == "" || h.Description == "" { return "" } i, err := strconv.Atoi(h.Mnemonic) if err == nil { - return formatNSMenu(i, h.Description, v.styles.Frame()) + return formatNSMenu(i, h.Description, m.styles.Frame()) } - return formatPlainMenu(h, size, v.styles.Frame()) + return formatPlainMenu(h, size, m.styles.Frame()) } // ---------------------------------------------------------------------------- diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index 5813ac03..ead592ca 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -11,8 +11,7 @@ import ( ) func TestNewMenu(t *testing.T) { - defaults, _ := config.NewStyles("") - v := ui.NewMenu(defaults) + v := ui.NewMenu(config.NewStyles()) v.HydrateMenu(model.MenuHints{ {Mnemonic: "a", Description: "bleeA", Visible: true}, {Mnemonic: "b", Description: "bleeB", Visible: true}, diff --git a/internal/ui/splash.go b/internal/ui/splash.go index 25419c03..724cd079 100644 --- a/internal/ui/splash.go +++ b/internal/ui/splash.go @@ -20,7 +20,7 @@ var LogoSmall = []string{ } // Logo K9s big logo for splash page. -var Logo = []string{ +var LogoBig = []string{ ` ____ __.________ _________ .____ .___ `, `| |/ _/ __ \_____\_ ___ \| | | |`, `| < \____ / ___/ \ \/| | | |`, @@ -29,42 +29,42 @@ var Logo = []string{ ` \/ \/ \/ \/ `, } -// SplashView represents a splash screen. -type SplashView struct { +// Splash represents a splash screen. +type Splash struct { *tview.Flex } // NewSplash instantiates a new splash screen with product and company info. -func NewSplash(styles *config.Styles, version string) *SplashView { - v := SplashView{Flex: tview.NewFlex()} +func NewSplash(styles *config.Styles, version string) *Splash { + s := Splash{Flex: tview.NewFlex()} logo := tview.NewTextView() logo.SetDynamicColors(true) logo.SetBackgroundColor(tcell.ColorDefault) logo.SetTextAlign(tview.AlignCenter) - v.layoutLogo(logo, styles) + s.layoutLogo(logo, styles) vers := tview.NewTextView() vers.SetDynamicColors(true) vers.SetBackgroundColor(tcell.ColorDefault) vers.SetTextAlign(tview.AlignCenter) - v.layoutRev(vers, version, styles) + s.layoutRev(vers, version, styles) - v.SetDirection(tview.FlexRow) - v.AddItem(logo, 10, 1, false) - v.AddItem(vers, 1, 1, false) + s.SetDirection(tview.FlexRow) + s.AddItem(logo, 10, 1, false) + s.AddItem(vers, 1, 1, false) - return &v + return &s } -func (v *SplashView) layoutLogo(t *tview.TextView, styles *config.Styles) { - logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor)) +func (s *Splash) layoutLogo(t *tview.TextView, styles *config.Styles) { + logo := strings.Join(LogoBig, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor)) fmt.Fprintf(t, "%s[%s::b]%s\n", strings.Repeat("\n", 2), styles.Body().LogoColor, logo) } -func (v *SplashView) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { +func (s *Splash) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", styles.Body().FgColor, rev) } diff --git a/internal/ui/splash_test.go b/internal/ui/splash_test.go index f297ece3..2113819c 100644 --- a/internal/ui/splash_test.go +++ b/internal/ui/splash_test.go @@ -1,15 +1,15 @@ -package ui +package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestNewSplash(t *testing.T) { - defaults, _ := config.NewStyles("") - s := NewSplash(defaults, "bozo") + s := ui.NewSplash(config.NewStyles(), "bozo") x, y, w, h := s.GetRect() assert.Equal(t, 0, x) diff --git a/internal/ui/table.go b/internal/ui/table.go index 3500ba85..1050810a 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -83,7 +83,6 @@ func (t *Table) SendKey(evt *tcell.EventKey) { } func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("KEY PRESS %#v", evt) key := evt.Key() if key == tcell.KeyUp || key == tcell.KeyDown { return evt diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index a8b815ad..7f636e65 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -14,8 +14,7 @@ import ( func TestTableNew(t *testing.T) { v := ui.NewTable("fred") - s, _ := config.NewStyles("") - ctx := context.WithValue(context.Background(), ui.KeyStyles, s) + ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles()) v.Init(ctx) assert.Equal(t, "fred", v.BaseTitle) @@ -23,8 +22,7 @@ func TestTableNew(t *testing.T) { func TestTableUpdate(t *testing.T) { v := ui.NewTable("fred") - s, _ := config.NewStyles("") - ctx := context.WithValue(context.Background(), ui.KeyStyles, s) + ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles()) v.Init(ctx) v.Update(makeTableData()) @@ -35,8 +33,7 @@ func TestTableUpdate(t *testing.T) { func TestTableSelection(t *testing.T) { v := ui.NewTable("fred") - s, _ := config.NewStyles("") - ctx := context.WithValue(context.Background(), ui.KeyStyles, s) + ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles()) v.Init(ctx) m := &testModel{} v.SetModel(m) diff --git a/internal/view/actions.go b/internal/view/actions.go index 95ca28fe..61e8790a 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -65,10 +65,10 @@ func hotKeyActions(r Runner, aa ui.KeyActions) { log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") continue } - aa[key] = ui.NewKeyAction( + aa[key] = ui.NewSharedKeyAction( hk.Description, gotoCmd(r, hk.Command), - true) + false) } } diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index a1b23edd..a857469d 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -21,10 +21,9 @@ func TestAliasNew(t *testing.T) { assert.Nil(t, v.Init(makeContext())) assert.Equal(t, "Aliases", v.Name()) - assert.Equal(t, 10, len(v.Hints())) + assert.Equal(t, 4, len(v.Hints())) } -// BOZO!! func TestAliasSearch(t *testing.T) { v := view.NewAlias(client.GVR("aliases")) assert.Nil(t, v.Init(makeContext())) diff --git a/internal/view/app.go b/internal/view/app.go index f33e0a1c..e94d1fdb 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -18,9 +18,9 @@ import ( ) const ( - splashTime = 1 - clusterRefresh = time.Duration(5 * time.Second) - indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" + splashTime = 1 + clusterRefresh = time.Duration(5 * time.Second) + statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" ) // App represents an application view. @@ -28,7 +28,7 @@ type App struct { *ui.App Content *PageStack - command *command + command *Command factory *watch.Factory version string showHeader bool @@ -38,14 +38,14 @@ type App struct { // NewApp returns a K9s app instance. func NewApp(cfg *config.Config) *App { a := App{ - App: ui.NewApp(), + App: ui.NewApp(cfg.K9s.CurrentCluster), Content: NewPageStack(), } a.Config = cfg a.InitBench(cfg.K9s.CurrentCluster) - a.Views()["indicator"] = ui.NewIndicatorView(a.App, a.Styles) - a.Views()["clusterInfo"] = newClusterInfoView(&a, client.NewMetricsServer(cfg.GetConnection())) + a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) + a.Views()["clusterInfo"] = NewClusterInfo(&a, client.NewMetricsServer(cfg.GetConnection())) return &a } @@ -86,7 +86,7 @@ func (a *App) Init(version string, rate int) error { a.factory = watch.NewFactory(a.Conn()) a.initFactory(ns) - a.command = newCommand(a) + a.command = NewCommand(a) if err := a.command.Init(); err != nil { return err } @@ -97,7 +97,7 @@ func (a *App) Init(version string, rate int) error { } main := tview.NewFlex().SetDirection(tview.FlexRow) - main.AddItem(a.indicator(), 1, 1, false) + main.AddItem(a.statusIndicator(), 1, 1, false) main.AddItem(a.Content, 0, 10, true) main.AddItem(a.Crumbs(), 2, 1, false) main.AddItem(a.Flash(), 2, 1, false) @@ -106,9 +106,25 @@ func (a *App) Init(version string, rate int) error { a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.toggleHeader(!a.Config.K9s.GetHeadless()) + a.Styles.AddListener(a) + return nil } +func (a *App) StylesChanged(s *config.Styles) { + a.Main.SetBackgroundColor(s.BgColor()) + if f, ok := a.Main.GetPrimitive("main").(*tview.Flex); ok { + f.SetBackgroundColor(s.BgColor()) + if h, ok := f.ItemAt(0).(*tview.Flex); ok { + h.SetBackgroundColor(s.BgColor()) + } else { + log.Error().Msgf("Header not found") + } + } else { + log.Error().Msgf("Main not found") + } +} + func (a *App) bindKeys() { a.AddActions(ui.KeyActions{ ui.KeyH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false), @@ -147,13 +163,14 @@ func (a *App) toggleHeader(flag bool) { flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) } else { flex.RemoveItemAtIndex(0) - flex.AddItemAtIndex(0, a.indicator(), 1, 1, false) + flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false) a.refreshIndicator() } } func (a *App) buildHeader() tview.Primitive { header := tview.NewFlex() + header.SetBackgroundColor(a.Styles.BgColor()) header.SetBorderPadding(0, 0, 1, 1) header.SetDirection(tview.FlexColumn) if !a.showHeader { @@ -176,6 +193,7 @@ func (a *App) Resume() { var ctx context.Context ctx, a.cancelFn = context.WithCancel(context.Background()) go a.clusterUpdater(ctx) + a.StylesUpdater(ctx, a) } func (a *App) clusterUpdater(ctx context.Context) { @@ -192,8 +210,8 @@ func (a *App) clusterUpdater(ctx context.Context) { } } +// BOZO!! Refact to use model/view strategy. func (a *App) refreshClusterInfo() { - log.Debug().Msgf("***** REFRESHING CLUSTER ******") if !a.showHeader { a.refreshIndicator() } else { @@ -207,12 +225,12 @@ func (a *App) refreshIndicator() { var cmx client.ClusterMetrics nos, nmx, err := fetchResources(a) if err != nil { - log.Error().Err(err).Msgf("unable to refresh cluster indicator") + log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") return } if err := cluster.Metrics(nos, nmx, &cmx); err != nil { - log.Error().Err(err).Msgf("unable to refresh cluster indicator") + log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") return } @@ -225,8 +243,8 @@ func (a *App) refreshIndicator() { mem = render.NAValue } - a.indicator().SetPermanent(fmt.Sprintf( - indicatorFmt, + a.statusIndicator().SetPermanent(fmt.Sprintf( + statusIndicatorFmt, a.version, cluster.ClusterName(), cluster.UserName(), @@ -273,6 +291,7 @@ func (a *App) switchCtx(name string, loadPods bool) error { a.Flash().Err(err) } a.refreshClusterInfo() + a.ReloadStyles(name) } return nil @@ -296,11 +315,8 @@ func (a *App) Run() { defer cancel() a.Halt() - // Only enable skin updater while in dev mode. - if a.HasSkins { - if err := a.StylesUpdater(ctx, a); err != nil { - log.Error().Err(err).Msg("Unable to track skin changes") - } + if err := a.StylesUpdater(ctx, a); err != nil { + log.Error().Err(err).Msg("Unable to track skin changes") } go func() { @@ -342,13 +358,13 @@ func (a *App) setLogo(l ui.FlashLevel, msg string) { func (a *App) setIndicator(l ui.FlashLevel, msg string) { switch l { case ui.FlashErr: - a.indicator().Err(msg) + a.statusIndicator().Err(msg) case ui.FlashWarn: - a.indicator().Warn(msg) + a.statusIndicator().Warn(msg) case ui.FlashInfo: - a.indicator().Info(msg) + a.statusIndicator().Info(msg) default: - a.indicator().Reset() + a.statusIndicator().Reset() } a.Draw() } @@ -427,10 +443,10 @@ func (a *App) inject(c model.Component) error { return nil } -func (a *App) clusterInfo() *clusterInfoView { - return a.Views()["clusterInfo"].(*clusterInfoView) +func (a *App) clusterInfo() *ClusterInfo { + return a.Views()["clusterInfo"].(*ClusterInfo) } -func (a *App) indicator() *ui.IndicatorView { - return a.Views()["indicator"].(*ui.IndicatorView) +func (a *App) statusIndicator() *ui.StatusIndicator { + return a.Views()["statusIndicator"].(*ui.StatusIndicator) } diff --git a/internal/view/app_test.go b/internal/view/app_test.go index 6e9fab90..4ff96086 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -13,5 +13,4 @@ func TestAppNew(t *testing.T) { a.Init("blee", 10) assert.Equal(t, 11, len(a.GetActions())) - assert.Equal(t, false, a.HasSkins) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 95d92242..4c98c6ff 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - rt "runtime" "strconv" "github.com/atotto/clipboard" @@ -77,7 +76,6 @@ func (b *Browser) Init(ctx context.Context) error { if err != nil { return err } - log.Debug().Msgf("ACCESSOR FOR %s -- %#v", b.gvr, b.accessor) b.envFn = b.defaultK9sEnv b.setNamespace(b.App().Config.ActiveNamespace()) @@ -93,8 +91,6 @@ func (b *Browser) Init(ctx context.Context) error { // Start initializes browser updates. func (b *Browser) Start() { b.Stop() - log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) - log.Debug().Msgf("BROWSER START %s", b.gvr) b.Table.Start() ctx := b.defaultContext() @@ -389,9 +385,9 @@ func (b *Browser) TableLoadFailed(err error) { // TableDataChanged notifies view new data is available. func (b *Browser) TableDataChanged(data render.TableData) { - b.Update(data) b.app.QueueUpdateDraw(func() { b.refreshActions() + b.Update(data) }) } @@ -410,7 +406,6 @@ func (b *Browser) defaultContext() context.Context { func (b *Browser) namespaceActions(aa ui.KeyActions) { if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" { - log.Warn().Msgf("NOT NAMESPACE RES %q -- %t -- %q", b.gvr, b.meta.Namespaced, b.GetTable().Path) return } b.namespaces = make(map[int]string, config.MaxFavoritesNS) diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index 3f75a1aa..95aaa3f8 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -16,104 +16,128 @@ import ( mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -type clusterInfoView struct { +// ClusterInfo represents a cluster info view. +type ClusterInfo struct { *tview.Table - app *App - mxs *client.MetricsServer + app *App + mxs *client.MetricsServer + styles *config.Styles } -func newClusterInfoView(app *App, mx *client.MetricsServer) *clusterInfoView { - return &clusterInfoView{ - app: app, - Table: tview.NewTable(), - mxs: mx, +// NewClusterInfo returns a new cluster info view. +func NewClusterInfo(app *App, mx *client.MetricsServer) *ClusterInfo { + return &ClusterInfo{ + app: app, + Table: tview.NewTable(), + mxs: mx, + styles: app.Styles, } } -func (v *clusterInfoView) init(version string) { - cluster := model.NewCluster(v.app.Conn(), v.mxs) +func (c *ClusterInfo) init(version string) { + cluster := model.NewCluster(c.app.Conn(), c.mxs) - row := v.initInfo(cluster) - row = v.initVersion(row, version, cluster) + c.app.Styles.AddListener(c) - v.SetCell(row, 0, v.sectionCell("CPU")) - v.SetCell(row, 1, v.infoCell(render.NAValue)) + row := c.initInfo(cluster) + row = c.initVersion(row, version, cluster) + + c.SetCell(row, 0, c.sectionCell("CPU")) + c.SetCell(row, 1, c.infoCell(render.NAValue)) row++ - v.SetCell(row, 0, v.sectionCell("MEM")) - v.SetCell(row, 1, v.infoCell(render.NAValue)) + c.SetCell(row, 0, c.sectionCell("MEM")) + c.SetCell(row, 1, c.infoCell(render.NAValue)) - v.refresh() + c.refresh() } -func (v *clusterInfoView) initInfo(cluster *model.Cluster) int { +// StylesChanges notifies skin changed. +func (c *ClusterInfo) StylesChanged(s *config.Styles) { + c.styles = s + c.SetBackgroundColor(s.BgColor()) + c.refresh() +} + +func (c *ClusterInfo) initInfo(cluster *model.Cluster) int { var row int - v.SetCell(row, 0, v.sectionCell("Context")) - v.SetCell(row, 1, v.infoCell(cluster.ContextName())) + c.SetCell(row, 0, c.sectionCell("Context")) + c.SetCell(row, 1, c.infoCell(cluster.ContextName())) row++ - v.SetCell(row, 0, v.sectionCell("Cluster")) - v.SetCell(row, 1, v.infoCell(cluster.ClusterName())) + c.SetCell(row, 0, c.sectionCell("Cluster")) + c.SetCell(row, 1, c.infoCell(cluster.ClusterName())) row++ - v.SetCell(row, 0, v.sectionCell("User")) - v.SetCell(row, 1, v.infoCell(cluster.UserName())) + c.SetCell(row, 0, c.sectionCell("User")) + c.SetCell(row, 1, c.infoCell(cluster.UserName())) row++ return row } -func (v *clusterInfoView) initVersion(row int, version string, cluster *model.Cluster) int { - v.SetCell(row, 0, v.sectionCell("K9s Rev")) - v.SetCell(row, 1, v.infoCell(version)) +func (c *ClusterInfo) initVersion(row int, version string, cluster *model.Cluster) int { + c.SetCell(row, 0, c.sectionCell("K9s Rev")) + c.SetCell(row, 1, c.infoCell(version)) row++ - v.SetCell(row, 0, v.sectionCell("K8s Rev")) - v.SetCell(row, 1, v.infoCell(cluster.Version())) + c.SetCell(row, 0, c.sectionCell("K8s Rev")) + c.SetCell(row, 1, c.infoCell(cluster.Version())) row++ return row } -func (v *clusterInfoView) sectionCell(t string) *tview.TableCell { - c := tview.NewTableCell(t + ":") - c.SetAlign(tview.AlignLeft) +func (c *ClusterInfo) sectionCell(t string) *tview.TableCell { + cell := tview.NewTableCell(t + ":") + cell.SetAlign(tview.AlignLeft) var s tcell.Style - c.SetStyle(s.Bold(true).Foreground(config.AsColor(v.app.Styles.K9s.Info.SectionColor))) - c.SetBackgroundColor(v.app.Styles.BgColor()) + cell.SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) + cell.SetBackgroundColor(c.app.Styles.BgColor()) - return c + return cell } -func (v *clusterInfoView) infoCell(t string) *tview.TableCell { - c := tview.NewTableCell(t) - c.SetExpansion(2) - c.SetTextColor(config.AsColor(v.app.Styles.K9s.Info.FgColor)) - c.SetBackgroundColor(v.app.Styles.BgColor()) +func (c *ClusterInfo) infoCell(t string) *tview.TableCell { + cell := tview.NewTableCell(t) + cell.SetExpansion(2) + cell.SetTextColor(config.AsColor(c.styles.K9s.Info.FgColor)) + cell.SetBackgroundColor(c.app.Styles.BgColor()) - return c + return cell } -func (v *clusterInfoView) refresh() { +func (c *ClusterInfo) refresh() { var ( - cluster = model.NewCluster(v.app.Conn(), v.mxs) + cluster = model.NewCluster(c.app.Conn(), c.mxs) row int ) - v.GetCell(row, 1).SetText(cluster.ContextName()) + + c.GetCell(row, 1).SetText(cluster.ContextName()) row++ - v.GetCell(row, 1).SetText(cluster.ClusterName()) + c.GetCell(row, 1).SetText(cluster.ClusterName()) row++ - v.GetCell(row, 1).SetText(cluster.UserName()) + c.GetCell(row, 1).SetText(cluster.UserName()) row += 2 - v.GetCell(row, 1).SetText(cluster.Version()) + c.GetCell(row, 1).SetText(cluster.Version()) row++ - c := v.GetCell(row, 1) - c.SetText(render.NAValue) - c = v.GetCell(row+1, 1) - c.SetText(render.NAValue) + cell := c.GetCell(row, 1) + cell.SetText(render.NAValue) + cell = c.GetCell(row+1, 1) + cell.SetText(render.NAValue) - v.refreshMetrics(cluster, row) + c.refreshMetrics(cluster, row) + c.updateStyle() +} + +func (c *ClusterInfo) updateStyle() { + for row := 0; row < c.GetRowCount(); row++ { + c.GetCell(row, 0).SetTextColor(config.AsColor(c.styles.K9s.Info.FgColor)) + c.GetCell(row, 0).SetBackgroundColor(c.styles.BgColor()) + var s tcell.Style + c.GetCell(row, 1).SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) + } } func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) { @@ -131,8 +155,8 @@ func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) { return nos, nmx, nil } -func (v *clusterInfoView) refreshMetrics(cluster *model.Cluster, row int) { - nos, nmx, err := fetchResources(v.app) +func (c *ClusterInfo) refreshMetrics(cluster *model.Cluster, row int) { + nos, nmx, err := fetchResources(c.app) if err != nil { log.Warn().Msgf("NodeMetrics %#v", err) return @@ -142,20 +166,20 @@ func (v *clusterInfoView) refreshMetrics(cluster *model.Cluster, row int) { if err := cluster.Metrics(nos, nmx, &cmx); err != nil { log.Error().Err(err).Msgf("failed to retrieve cluster metrics") } - c := v.GetCell(row, 1) + cell := c.GetCell(row, 1) cpu := render.AsPerc(cmx.PercCPU) if cpu == "0" { cpu = render.NAValue } - c.SetText(cpu + "%" + ui.Deltas(strip(c.Text), cpu)) + cell.SetText(cpu + "%" + ui.Deltas(strip(cell.Text), cpu)) row++ - c = v.GetCell(row, 1) + cell = c.GetCell(row, 1) mem := render.AsPerc(cmx.PercMEM) if mem == "0" { mem = render.NAValue } - c.SetText(mem + "%" + ui.Deltas(strip(c.Text), mem)) + cell.SetText(mem + "%" + ui.Deltas(strip(cell.Text), mem)) } func strip(s string) string { diff --git a/internal/view/command.go b/internal/view/command.go index e7047e10..15178bf0 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -13,19 +13,19 @@ import ( var customViewers MetaViewers -type command struct { +type Command struct { app *App alias *dao.Alias } -func newCommand(app *App) *command { - return &command{ +func NewCommand(app *App) *Command { + return &Command{ app: app, } } -func (c *command) Init() error { +func (c *Command) Init() error { c.alias = dao.NewAlias(c.app.factory) if _, err := c.alias.Ensure(); err != nil { return err @@ -35,8 +35,8 @@ func (c *command) Init() error { return nil } -// Reset resets command and reload aliases. -func (c *command) Reset() error { +// Reset resets Command and reload aliases. +func (c *Command) Reset() error { c.alias.Clear() if _, err := c.alias.Ensure(); err != nil { return err @@ -45,13 +45,13 @@ func (c *command) Reset() error { return nil } -func (c *command) defaultCmd() error { +func (c *Command) defaultCmd() error { return c.run(c.app.Config.ActiveView()) } var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) -func (c *command) specialCmd(cmd string) bool { +func (c *Command) specialCmd(cmd string) bool { cmds := strings.Split(cmd, " ") switch cmds[0] { case "q", "Q", "quit": @@ -79,10 +79,10 @@ func (c *command) specialCmd(cmd string) bool { return false } -func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) { +func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { gvr, ok := c.alias.Get(cmd) if !ok { - return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd) + return "", nil, fmt.Errorf("Huh? `%s` Command not found", cmd) } v, ok := customViewers[client.GVR(gvr)] @@ -93,8 +93,8 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) { return gvr, &v, nil } -// Exec the command by showing associated display. -func (c *command) run(cmd string) error { +// Exec the Command by showing associated display. +func (c *Command) run(cmd string) error { if c.specialCmd(cmd) { return nil } @@ -112,7 +112,7 @@ func (c *command) run(cmd string) error { view := c.componentFor(gvr, v) return c.exec(gvr, view) default: - // checks if command includes a namespace + // checks if Command includes a namespace ns := c.app.Config.ActiveNamespace() if len(cmds) == 2 { ns = cmds[1] @@ -124,7 +124,7 @@ func (c *command) run(cmd string) error { } } -func (c *command) componentFor(gvr string, v *MetaViewer) ResourceViewer { +func (c *Command) componentFor(gvr string, v *MetaViewer) ResourceViewer { var view ResourceViewer if v.viewerFn != nil { log.Debug().Msgf("Custom viewer for %s", gvr) @@ -142,14 +142,14 @@ func (c *command) componentFor(gvr string, v *MetaViewer) ResourceViewer { return view } -func (c *command) exec(gvr string, comp model.Component) error { +func (c *Command) exec(gvr string, comp model.Component) error { if comp == nil { return fmt.Errorf("No component given for %s", gvr) } g := client.GVR(gvr) c.app.Flash().Infof("Viewing %s resource...", g.ToR()) - log.Debug().Msgf("Running command %s", gvr) + log.Debug().Msgf("Running Command %s", gvr) c.app.Config.SetActiveView(g.ToR()) if err := c.app.Config.Save(); err != nil { log.Error().Err(err).Msg("Config save failed!") diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 11a37af1..1893a9b7 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) { assert.Nil(t, c.Init(makeCtx())) assert.Equal(t, "Containers", c.Name()) - assert.Equal(t, 18, len(c.Hints())) + assert.Equal(t, 10, len(c.Hints())) } diff --git a/internal/view/context_test.go b/internal/view/context_test.go index 21865ba4..8c7da491 100644 --- a/internal/view/context_test.go +++ b/internal/view/context_test.go @@ -13,5 +13,5 @@ func TestContext(t *testing.T) { assert.Nil(t, ctx.Init(makeCtx())) assert.Equal(t, "Contexts", ctx.Name()) - assert.Equal(t, 9, len(ctx.Hints())) + assert.Equal(t, 1, len(ctx.Hints())) } diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index d9c93664..0ece1e38 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Deployments", v.Name()) - assert.Equal(t, 17, len(v.Hints())) + assert.Equal(t, 7, len(v.Hints())) } diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index b9f6cd3d..df5b67c9 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "DaemonSets", v.Name()) - assert.Equal(t, 16, len(v.Hints())) + assert.Equal(t, 6, len(v.Hints())) } diff --git a/internal/view/event.go b/internal/view/event.go new file mode 100644 index 00000000..58ffa82b --- /dev/null +++ b/internal/view/event.go @@ -0,0 +1,28 @@ +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Event represents a command alias view. +type Event struct { + ResourceViewer +} + +// NewEvent returns a new alias view. +func NewEvent(gvr client.GVR) ResourceViewer { + e := Event{ + ResourceViewer: NewBrowser(gvr), + } + e.GetTable().SetColorerFn(render.Event{}.ColorerFunc()) + e.SetBindKeysFn(e.bindKeys) + + return &e +} + +func (e *Event) bindKeys(aa ui.KeyActions) { + aa.Delete(tcell.KeyCtrlD, ui.KeyE) +} diff --git a/internal/view/help.go b/internal/view/help.go index 10192203..cbb7c84c 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -51,7 +51,8 @@ func (v *Help) Init(ctx context.Context) error { func (v *Help) bindKeys() { v.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS) v.Actions().Set(ui.KeyActions{ - tcell.KeyEsc: ui.NewKeyAction("Back", v.app.PrevCmd, true), + tcell.KeyEsc: ui.NewKeyAction("Back", v.app.PrevCmd, false), + ui.KeyHelp: ui.NewKeyAction("Back", v.app.PrevCmd, false), tcell.KeyEnter: ui.NewKeyAction("Back", v.app.PrevCmd, false), }) } @@ -110,15 +111,20 @@ func (v *Help) showHotKeys() (model.MenuHints, error) { if err := hh.Load(); err != nil { return nil, fmt.Errorf("no hotkey configuration found") } - m := make(model.MenuHints, 0, len(hh.HotKey)) - for _, hk := range hh.HotKey { - m = append(m, model.MenuHint{ - Mnemonic: hk.ShortCut, - Description: hk.Description, + kk := make(sort.StringSlice, 0, len(hh.HotKey)) + for k := range hh.HotKey { + kk = append(kk, k) + } + kk.Sort() + mm := make(model.MenuHints, 0, len(hh.HotKey)) + for _, k := range kk { + mm = append(mm, model.MenuHint{ + Mnemonic: hh.HotKey[k].ShortCut, + Description: hh.HotKey[k].Description, }) } - return m, nil + return mm, nil } func (v *Help) showGeneral() model.MenuHints { diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 1e65c3e3..a89f5da8 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -20,8 +20,8 @@ func TestHelp(t *testing.T) { v := view.NewHelp() assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 26, v.GetRowCount()) + assert.Equal(t, 16, v.GetRowCount()) assert.Equal(t, 10, v.GetColumnCount()) - assert.Equal(t, "", v.GetCell(1, 0).Text) - assert.Equal(t, "Erase", v.GetCell(1, 1).Text) + assert.Equal(t, "", v.GetCell(1, 0).Text) + assert.Equal(t, "Kill", v.GetCell(1, 1).Text) } diff --git a/internal/view/log.go b/internal/view/log.go index 414684dd..f8a0d8dd 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -35,12 +35,13 @@ type Log struct { app *App logs *Details - scrollIndicator *AutoScrollIndicator + indicator *LogIndicator ansiWriter io.Writer path, container string cancelFn context.CancelFunc previous bool gvr client.GVR + fullScreen bool } var _ model.Component = &Log{} @@ -68,8 +69,8 @@ func (l *Log) Init(ctx context.Context) (err error) { l.SetBorderPadding(0, 0, 1, 1) l.SetDirection(tview.FlexRow) - l.scrollIndicator = NewAutoScrollIndicator(l.app.Styles) - l.AddItem(l.scrollIndicator, 1, 1, false) + l.indicator = NewLogIndicator(l.app.Styles) + l.AddItem(l.indicator, 1, 1, false) l.logs = NewDetails("") l.logs.SetBorder(false) @@ -89,22 +90,9 @@ func (l *Log) Init(ctx context.Context) (err error) { return nil } -// Refresh refreshes the viewer. -func (l *Log) Refresh() {} - -// App returns an app handle. -func (l *Log) App() *App { - return l.app -} - // Hints returns a collection of menu hints. func (l *Log) Hints() model.MenuHints { - return l.Actions().Hints() -} - -// Actions returns available actions. -func (l *Log) Actions() ui.KeyActions { - return l.logs.actions + return l.logs.Actions().Hints() } // Start runs the component. @@ -133,8 +121,10 @@ func (l *Log) bindKeys() { l.logs.Actions().Set(ui.KeyActions{ tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true), ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), - ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), + ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false), + ui.KeyShiftF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), + ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false), ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false), ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false), @@ -212,8 +202,8 @@ func (l *Log) updateLogs(ctx context.Context, c <-chan string, buffSize int) { } // ScrollIndicator returns the scroll mode viewer. -func (l *Log) ScrollIndicator() *AutoScrollIndicator { - return l.scrollIndicator +func (l *Log) Indicator() *LogIndicator { + return l.indicator } func (l *Log) setTitle(path, co string) { @@ -251,12 +241,12 @@ func (l *Log) log(lines string) { // Flush write logs to viewer. func (l *Log) Flush(index int, buff []string) { - if index == 0 || !l.scrollIndicator.AutoScroll() { + if index == 0 || !l.indicator.AutoScroll() { return } l.log(strings.Join(buff[:index], "\n")) l.app.QueueUpdateDraw(func() { - l.scrollIndicator.Refresh() + l.indicator.Refresh() l.logs.ScrollToEnd() }) } @@ -306,14 +296,6 @@ func saveData(cluster, name, data string) (string, error) { return path, nil } -// ToggleAutoScrollCmd toggles auto scrolling of logs. -func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { - l.scrollIndicator.ToggleAutoScroll() - l.scrollIndicator.Refresh() - - return nil -} - func (l *Log) topCmd(evt *tcell.EventKey) *tcell.EventKey { l.app.Flash().Info("Top of logs...") l.logs.ScrollToBeginning() @@ -346,3 +328,27 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { l.logs.ScrollTo(0, 0) return nil } + +func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleTextWrap() + l.logs.SetWrap(l.indicator.textWrap) + return nil +} + +func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleAutoScroll() + return nil +} + +func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey { + l.indicator.ToggleFullScreen() + sidePadding := 1 + if l.indicator.FullScreen() { + sidePadding = 0 + } + l.SetFullScreen(l.indicator.FullScreen()) + l.Box.SetBorder(!l.indicator.FullScreen()) + l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding) + + return nil +} diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go new file mode 100644 index 00000000..3c806dcb --- /dev/null +++ b/internal/view/log_indicator.go @@ -0,0 +1,83 @@ +package view + +import ( + "fmt" + "sync/atomic" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" +) + +// LogIndicator represents a log view indicator. +type LogIndicator struct { + *tview.TextView + + styles *config.Styles + scrollStatus int32 + fullScreen bool + textWrap bool +} + +// NewLogIndicator returns a new indicator. +func NewLogIndicator(styles *config.Styles) *LogIndicator { + l := LogIndicator{ + styles: styles, + TextView: tview.NewTextView(), + scrollStatus: 1, + } + l.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) + l.SetTextAlign(tview.AlignRight) + l.SetDynamicColors(true) + + return &l +} + +func (l *LogIndicator) AutoScroll() bool { + return atomic.LoadInt32(&l.scrollStatus) == 1 +} + +func (l *LogIndicator) TextWrap() bool { + return l.textWrap +} + +func (l *LogIndicator) FullScreen() bool { + return l.fullScreen +} + +func (l *LogIndicator) ToggleFullScreen() { + l.fullScreen = !l.fullScreen + l.Refresh() +} + +func (l *LogIndicator) ToggleTextWrap() { + l.textWrap = !l.textWrap + l.Refresh() +} + +func (l *LogIndicator) ToggleAutoScroll() { + var val int32 = 1 + if l.AutoScroll() { + val = 0 + } + atomic.StoreInt32(&l.scrollStatus, val) + l.Refresh() +} + +func (l *LogIndicator) Refresh() { + l.Clear() + l.update("Autoscroll: " + l.onOff(l.AutoScroll())) + l.update("FullScreen: " + l.onOff(l.fullScreen)) + l.update("Wrap: " + l.onOff(l.textWrap)) +} + +func (l *LogIndicator) onOff(b bool) string { + if b { + return "On" + } + return "Off" +} + +func (l *LogIndicator) update(status string) { + fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor + fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status) +} diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go new file mode 100644 index 00000000..05f1caa7 --- /dev/null +++ b/internal/view/log_indicator_test.go @@ -0,0 +1,17 @@ +package view_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/view" + "github.com/stretchr/testify/assert" +) + +func TestLogIndicatorRefresh(t *testing.T) { + defaults := config.NewStyles() + v := view.NewLogIndicator(defaults) + v.Refresh() + + assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false)) +} diff --git a/internal/view/log_test.go b/internal/view/log_test.go index 0e4fef57..a852a9ab 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -1,4 +1,4 @@ -package view_test +package view import ( "bytes" @@ -9,7 +9,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/view" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) @@ -29,20 +28,20 @@ func TestLogAnsi(t *testing.T) { } func TestLogFlush(t *testing.T) { - v := view.NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) v.Init(makeContext()) v.Flush(2, []string{"blee", "bozo"}) - v.ToggleAutoScrollCmd(nil) + v.toggleAutoScrollCmd(nil) assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) - assert.Equal(t, " Autoscroll: Off ", v.ScrollIndicator().GetText(true)) - v.ToggleAutoScrollCmd(nil) - assert.Equal(t, " Autoscroll: On ", v.ScrollIndicator().GetText(true)) - assert.Equal(t, 8, len(v.Hints())) + assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true)) + v.toggleAutoScrollCmd(nil) + assert.Equal(t, " Autoscroll: On FullScreen: Off Wrap: Off ", v.Indicator().GetText(true)) + assert.Equal(t, 10, len(v.Hints())) } func TestLogViewSave(t *testing.T) { - v := view.NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) v.Init(makeContext()) app := makeApp() @@ -56,7 +55,7 @@ func TestLogViewSave(t *testing.T) { } func TestLogViewNav(t *testing.T) { - v := view.NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) v.Init(makeContext()) var buff []string @@ -64,26 +63,27 @@ func TestLogViewNav(t *testing.T) { buff = append(buff, fmt.Sprintf("line-%d\n", i)) } v.Flush(100, buff) - v.ToggleAutoScrollCmd(nil) + v.toggleAutoScrollCmd(nil) r, _ := v.Logs().GetScrollOffset() assert.Equal(t, -1, r) } func TestLogViewClear(t *testing.T) { - v := view.NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) + v := NewLog(client.GVR("v1/pods"), "fred/p1", "blee", false) v.Init(makeContext()) v.Flush(2, []string{"blee", "bozo"}) - v.ToggleAutoScrollCmd(nil) + v.toggleAutoScrollCmd(nil) assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true)) v.Logs().Clear() assert.Equal(t, "", v.Logs().GetText(true)) } +// ---------------------------------------------------------------------------- // Helpers... -func makeApp() *view.App { - return view.NewApp(config.NewConfig(ks{})) +func makeApp() *App { + return NewApp(config.NewConfig(ks{})) } diff --git a/internal/view/ns_test.go b/internal/view/ns_test.go index cb36c8ac..5ab9d368 100644 --- a/internal/view/ns_test.go +++ b/internal/view/ns_test.go @@ -13,5 +13,5 @@ func TestNSCleanser(t *testing.T) { assert.Nil(t, ns.Init(makeCtx())) assert.Equal(t, "Namespaces", ns.Name()) - assert.Equal(t, 13, len(ns.Hints())) + assert.Equal(t, 3, len(ns.Hints())) } diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 78e53aae..313f22b8 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 25, len(po.Hints())) + assert.Equal(t, 15, len(po.Hints())) } // Helpers... diff --git a/internal/view/port_forward_test.go b/internal/view/port_forward_test.go index 71a86159..69a0648c 100644 --- a/internal/view/port_forward_test.go +++ b/internal/view/port_forward_test.go @@ -13,5 +13,5 @@ func TestPortForwardNew(t *testing.T) { assert.Nil(t, pf.Init(makeCtx())) assert.Equal(t, "PortForwards", pf.Name()) - assert.Equal(t, 16, len(pf.Hints())) + assert.Equal(t, 8, len(pf.Hints())) } diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 4193c82f..463cb340 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -13,5 +13,5 @@ func TestRbacNew(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Rbac", v.Name()) - assert.Equal(t, 10, len(v.Hints())) + assert.Equal(t, 2, len(v.Hints())) } diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 7684e0fc..b376b64c 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -23,6 +23,9 @@ func coreRes(vv MetaViewers) { vv["v1/namespaces"] = MetaViewer{ viewerFn: NewNamespace, } + vv["v1/events"] = MetaViewer{ + viewerFn: NewEvent, + } vv["v1/pods"] = MetaViewer{ viewerFn: NewPod, } diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index bb3962df..55770d41 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -13,5 +13,5 @@ func TestScreenDumpNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "ScreenDumps", po.Name()) - assert.Equal(t, 12, len(po.Hints())) + assert.Equal(t, 2, len(po.Hints())) } diff --git a/internal/view/scroll_indicator.go b/internal/view/scroll_indicator.go deleted file mode 100644 index f56003fc..00000000 --- a/internal/view/scroll_indicator.go +++ /dev/null @@ -1,57 +0,0 @@ -package view - -import ( - "fmt" - "sync/atomic" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" -) - -// AutoScrollIndicator represents a log autoscroll status indicator. -type AutoScrollIndicator struct { - *tview.TextView - - styles *config.Styles - scrollStatus int32 -} - -// NewAutoScrollIndicator returns a new indicator. -func NewAutoScrollIndicator(styles *config.Styles) *AutoScrollIndicator { - a := AutoScrollIndicator{ - styles: styles, - TextView: tview.NewTextView(), - scrollStatus: 1, - } - a.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor)) - a.SetTextAlign(tview.AlignRight) - a.SetDynamicColors(true) - - return &a -} - -func (a *AutoScrollIndicator) AutoScroll() bool { - return atomic.LoadInt32(&a.scrollStatus) == 1 -} - -func (a *AutoScrollIndicator) ToggleAutoScroll() { - var val int32 = 1 - if a.AutoScroll() { - val = 0 - } - atomic.StoreInt32(&a.scrollStatus, val) -} - -func (a *AutoScrollIndicator) Refresh() { - autoScroll := "Off" - if a.AutoScroll() { - autoScroll = "On" - } - a.update("Autoscroll: " + autoScroll) -} - -func (a *AutoScrollIndicator) update(status string) { - a.Clear() - fg, bg := a.styles.Frame().Crumb.FgColor, a.styles.Frame().Crumb.ActiveColor - fmt.Fprintf(a, "[%s:%s:b] %-15s ", fg, bg, status) -} diff --git a/internal/view/scroll_indicator_test.go b/internal/view/scroll_indicator_test.go deleted file mode 100644 index e47af271..00000000 --- a/internal/view/scroll_indicator_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package view_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/view" - "github.com/stretchr/testify/assert" -) - -func TestScrollIndicatorRefresg(t *testing.T) { - defaults, _ := config.NewStyles("") - v := view.NewAutoScrollIndicator(defaults) - v.Refresh() - - assert.Equal(t, "[black:orange:b] Autoscroll: On \n", v.GetText(false)) -} diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index 085e9c4f..d7effb2c 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Secrets", s.Name()) - assert.Equal(t, 13, len(s.Hints())) + assert.Equal(t, 3, len(s.Hints())) } diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index fbc5bc27..3ae4e794 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "StatefulSets", s.Name()) - assert.Equal(t, 17, len(s.Hints())) + assert.Equal(t, 7, len(s.Hints())) } diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index 9579eb00..f9ebc3ac 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Services", s.Name()) - assert.Equal(t, 17, len(s.Hints())) + assert.Equal(t, 7, len(s.Hints())) } diff --git a/internal/view/table.go b/internal/view/table.go index 4b242368..0595c100 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -86,15 +86,15 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { func (t *Table) bindKeys() { t.Actions().Add(ui.KeyActions{ - ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, false), - tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, false), - ui.KeySlash: ui.NewKeyAction("Filter Mode", t.activateCmd, false), - tcell.KeyEscape: ui.NewKeyAction("Filter Reset", t.resetCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Filter", t.filterCmd, false), - tcell.KeyBackspace2: ui.NewKeyAction("Erase", t.eraseCmd, false), - tcell.KeyBackspace: ui.NewKeyAction("Erase", t.eraseCmd, false), - tcell.KeyDelete: ui.NewKeyAction("Erase", t.eraseCmd, false), + ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), + tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), + tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), + tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", t.resetCmd, false), + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", t.filterCmd, false), + tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), + tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), + tcell.KeyDelete: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(0, true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(-1, true), false), }) diff --git a/internal/view/yaml_test.go b/internal/view/yaml_test.go index 96c71709..41451559 100644 --- a/internal/view/yaml_test.go +++ b/internal/view/yaml_test.go @@ -49,7 +49,7 @@ func TestYaml(t *testing.T) { }, } - s, _ := config.NewStyles("skins/stock.yml") + s := config.NewStyles() for _, u := range uu { assert.Equal(t, u.e, colorizeYAML(s.Views().Yaml, u.s)) } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index b0bbdc6b..08538969 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -147,11 +147,11 @@ func (f *Factory) isClusterWide() bool { } func (f *Factory) preload(ns string) { - verbs := []string{"get", "list", "watch"} - _, _ = f.CanForResource(ns, "v1/pods", verbs...) - _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...) - _, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...) - _, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...) + // verbs := []string{"get", "list", "watch"} + // _, _ = f.CanForResource(ns, "v1/pods", verbs...) + // _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...) + // _, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...) + // _, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...) } // CanForResource return an informer is user has access. @@ -201,7 +201,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { } func toGVR(gvr string) schema.GroupVersionResource { - log.Debug().Msgf(">>> Convert GVR %q", gvr) tokens := strings.Split(gvr, "/") if len(tokens) < 3 { tokens = append([]string{""}, tokens...) diff --git a/main.go b/main.go index 8e4f977c..2d854cff 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,9 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" _ "k8s.io/client-go/plugin/pkg/client/auth" + + "net/http" + _ "net/http/pprof" ) func init() { @@ -21,6 +24,10 @@ func main() { panic(err) } + go func() { + http.ListenAndServe("localhost:6060", nil) + }() + log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) cmd.Execute() diff --git a/skins/snazzy.yml b/skins/snazzy.yml new file mode 100644 index 00000000..3e1e285e --- /dev/null +++ b/skins/snazzy.yml @@ -0,0 +1,51 @@ +k9s: + body: + fgColor: "#97979b" + bgColor: "#282a36" + logoColor: "#5af78e" + info: + fgColor: white + sectionColor: "#5af78e" + frame: + border: + fgColor: "#5af78e" + focusColor: "#5af78e" + menu: + fgColor: white + keyColor: "#57c7ff" + numKeyColor: "#ff6ac1" + crumbs: + fgColor: "#282a36" + bgColor: white + activeColor: "#f3f99d" + status: + newColor: "#eff0eb" + modifyColor: "#5af78e" + addColor: "#57c7ff" + errorColor: "#ff5c57" + highlightcolor: "#f3f99d" + killColor: mediumpurple + completedColor: gray + title: + fgColor: "#5af78e" + bgColor: "#282a36" + highlightColor: white + counterColor: white + filterColor: "#57c7ff" + table: + fgColor: "#57c7ff" + bgColor: "#282a36" + cursorColor: "#5af78e" + markColor: darkgoldenrod + header: + fgColor: white + bgColor: "#282a36" + sorterColor: orange + views: + yaml: + keyColor: "#ff5c57" + colonColor: white + valueColor: "#f3f99d" + logs: + fgColor: white + bgColor: "#282a36"