checkpoint

mine
derailed 2019-12-29 11:39:22 -07:00
parent 0cb291326a
commit 365fc01f17
71 changed files with 902 additions and 757 deletions

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -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"))
}

View File

@ -21,4 +21,5 @@ const (
KeySubjectKind ContextKey = "subjectKind"
KeySubjectName ContextKey = "subjectName"
KeyNamespace ContextKey = "namespace"
KeyCluster ContextKey = "cluster"
)

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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])
}

View File

@ -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)

View File

@ -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.

View File

@ -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"}

View File

@ -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 '🐩'
}
}

View File

@ -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)
}
}

108
internal/ui/command.go Normal file
View File

@ -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 '🐩'
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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"))

View File

@ -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
}

View File

@ -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))

View File

@ -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)

View File

@ -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] <Blee> \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] <Blee> \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] <Blee> \n", i.GetText(false))

View File

@ -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")
}
}
}

View File

@ -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))
})
}

View File

@ -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())
}
// ----------------------------------------------------------------------------

View File

@ -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},

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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()))

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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!")

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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()))
}

28
internal/view/event.go Normal file
View File

@ -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)
}

View File

@ -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 {

View File

@ -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, "<backspace>", v.GetCell(1, 0).Text)
assert.Equal(t, "Erase", v.GetCell(1, 1).Text)
assert.Equal(t, "<ctrl-k>", v.GetCell(1, 0).Text)
assert.Equal(t, "Kill", v.GetCell(1, 1).Text)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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{}))
}

View File

@ -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()))
}

View File

@ -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...

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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,
}

View File

@ -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()))
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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()))
}

View File

@ -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),
})

View File

@ -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))
}

View File

@ -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...)

View File

@ -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()

51
skins/snazzy.yml Normal file
View File

@ -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"