checkpoint
parent
0cb291326a
commit
365fc01f17
4
go.mod
4
go.mod
|
|
@ -2,8 +2,6 @@ module github.com/derailed/k9s
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
|
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783
|
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783
|
||||||
|
|
@ -30,7 +28,7 @@ replace (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.2
|
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/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
|
||||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
|
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
|
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
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/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 h1:By43yu6kbGvA+iL09VAhTKxKEd02BBOtUPIlrkeHxT4=
|
||||||
github.com/derailed/tview v0.3.2/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI=
|
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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
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=
|
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,15 @@ var (
|
||||||
K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
|
K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type StyleListener interface {
|
||||||
|
StylesChanged(*Styles)
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Styles tracks K9s styling options.
|
// Styles tracks K9s styling options.
|
||||||
Styles struct {
|
Styles struct {
|
||||||
K9s Style `yaml:"k9s"`
|
K9s Style `yaml:"k9s"`
|
||||||
|
listeners []StyleListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body tracks body styles.
|
// Body tracks body styles.
|
||||||
|
|
@ -257,9 +262,10 @@ func newMenu() Menu {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStyles creates a new default config.
|
// NewStyles creates a new default config.
|
||||||
func NewStyles(path string) (*Styles, error) {
|
func NewStyles() *Styles {
|
||||||
s := &Styles{K9s: newStyle()}
|
return &Styles{
|
||||||
return s, s.load(path)
|
K9s: newStyle(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FgColor returns the foreground color.
|
// FgColor returns the foreground color.
|
||||||
|
|
@ -272,6 +278,30 @@ func (s *Styles) BgColor() tcell.Color {
|
||||||
return AsColor(s.Body().BgColor)
|
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.
|
// Body returns body styles.
|
||||||
func (s *Styles) Body() Body {
|
func (s *Styles) Body() Body {
|
||||||
return s.K9s.Body
|
return s.K9s.Body
|
||||||
|
|
@ -303,7 +333,7 @@ func (s *Styles) Views() Views {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load K9s configuration from file
|
// Load K9s configuration from file
|
||||||
func (s *Styles) load(path string) error {
|
func (s *Styles) Load(path string) error {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -312,6 +342,7 @@ func (s *Styles) load(path string) error {
|
||||||
if err := yaml.Unmarshal(f, s); err != nil {
|
if err := yaml.Unmarshal(f, s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.fireStylesChanged()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +361,5 @@ func AsColor(c string) tcell.Color {
|
||||||
if color, ok := tcell.ColorNames[c]; ok {
|
if color, ok := tcell.ColorNames[c]; ok {
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
return tcell.GetColor(c)
|
||||||
return tcell.ColorPink
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,33 @@
|
||||||
package config
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSkinNone(t *testing.T) {
|
func TestAsColor(t *testing.T) {
|
||||||
s, err := NewStyles("test_assets/empty_skin.yml")
|
uu := map[string]tcell.Color{
|
||||||
assert.Nil(t, err)
|
"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()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
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.ColorCadetBlue, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
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) {
|
func TestSkin(t *testing.T) {
|
||||||
s, err := NewStyles("test_assets/black_and_wtf.yml")
|
s := config.NewStyles()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, s.Load("test_assets/black_and_wtf.yml"))
|
||||||
|
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "white", s.Body().FgColor)
|
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.ColorWhite, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
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) {
|
func TestSkinNotExits(t *testing.T) {
|
||||||
_, err := NewStyles("test_assets/blee.yml")
|
s := config.NewStyles()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, s.Load("test_assets/blee.yml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkinBoarked(t *testing.T) {
|
func TestSkinBoarked(t *testing.T) {
|
||||||
_, err := NewStyles("test_assets/skin_boarked.yml")
|
s := config.NewStyles()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, s.Load("test_assets/skin_boarked.yml"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,5 @@ const (
|
||||||
KeySubjectKind ContextKey = "subjectKind"
|
KeySubjectKind ContextKey = "subjectKind"
|
||||||
KeySubjectName ContextKey = "subjectName"
|
KeySubjectName ContextKey = "subjectName"
|
||||||
KeyNamespace ContextKey = "namespace"
|
KeyNamespace ContextKey = "namespace"
|
||||||
|
KeyCluster ContextKey = "cluster"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,6 @@ type Generic struct {
|
||||||
|
|
||||||
// List returns a collection of node resources.
|
// List returns a collection of node resources.
|
||||||
func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
|
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
|
// Ensures the factory is tracking this resource
|
||||||
_, err := g.factory.CanForResource(g.namespace, g.gvr)
|
_, err := g.factory.CanForResource(g.namespace, g.gvr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ func (p *Policy) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
return oo, nil
|
return oo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOZO!! refactor!
|
|
||||||
func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) {
|
func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) {
|
||||||
crbs, err := fetchClusterRoleBindings(p.factory)
|
crbs, err := fetchClusterRoleBindings(p.factory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
|
||||||
o, err := r.factory.Get(crbGVR, path, labels.Everything())
|
o, err := r.factory.Get(crbGVR, path, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,11 +2,9 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -23,10 +21,6 @@ func (r *Resource) Init(ns, gvr string, f Factory) {
|
||||||
|
|
||||||
// List returns a collection of nodes.
|
// List returns a collection of nodes.
|
||||||
func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) {
|
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)
|
strLabel, ok := ctx.Value(internal.KeyLabels).(string)
|
||||||
lsel := labels.Everything()
|
lsel := labels.Everything()
|
||||||
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
|
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.
|
// Render returns a node as a row.
|
||||||
func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
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 {
|
for i, o := range oo {
|
||||||
if err := re.Render(o, r.namespace, &rr[i]); err != nil {
|
if err := re.Render(o, r.namespace, &rr[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,6 @@ func (s *Stack) Peek() []Component {
|
||||||
|
|
||||||
// ClearHistory clear out the stack history up to most recent.
|
// ClearHistory clear out the stack history up to most recent.
|
||||||
func (s *Stack) ClearHistory() {
|
func (s *Stack) ClearHistory() {
|
||||||
log.Debug().Msgf("STACK CLEARED!!")
|
|
||||||
for range s.components {
|
for range s.components {
|
||||||
s.Pop()
|
s.Pop()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package model
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -143,16 +144,8 @@ func (t *Table) fireTableLoadFailed(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) reconcile(ctx context.Context) error {
|
func (t *Table) reconcile(ctx context.Context) error {
|
||||||
defer func(t time.Time) {
|
log.Debug().Msgf("GOROUTINE %d", runtime.NumGoroutine())
|
||||||
log.Debug().Msgf("RECONCILE elapsed: %v", time.Since(t))
|
|
||||||
}(time.Now())
|
|
||||||
|
|
||||||
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)
|
factory, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
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.Update(rows)
|
||||||
t.data.Namespace, t.data.Header = t.namespace, m.Renderer.Header(t.namespace)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.Namespace)
|
||||||
}
|
}
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
ev.Name,
|
asRef(ev.InvolvedObject),
|
||||||
ev.Reason,
|
ev.Reason,
|
||||||
ev.Source.Component,
|
ev.Source.Component,
|
||||||
strconv.Itoa(int(ev.Count)),
|
strconv.Itoa(int(ev.Count)),
|
||||||
|
|
@ -79,3 +79,7 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func asRef(r v1.ObjectReference) string {
|
||||||
|
return strings.ToLower(r.Kind) + ":" + r.Name
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestEventRender(t *testing.T) {
|
||||||
c.Render(load(t, "ev"), "", &r)
|
c.Render(load(t, "ev"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
|
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])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type (
|
||||||
Description string
|
Description string
|
||||||
Action ActionHandler
|
Action ActionHandler
|
||||||
Visible bool
|
Visible bool
|
||||||
|
Shared bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyActions tracks mappings between keystrokes and actions.
|
// 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}
|
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.
|
// Add sets up keyboard action listener.
|
||||||
func (a KeyActions) Add(aa KeyActions) {
|
func (a KeyActions) Add(aa KeyActions) {
|
||||||
for k, v := range aa {
|
for k, v := range aa {
|
||||||
|
|
@ -60,7 +65,9 @@ func (a KeyActions) Delete(kk ...tcell.Key) {
|
||||||
func (a KeyActions) Hints() model.MenuHints {
|
func (a KeyActions) Hints() model.MenuHints {
|
||||||
kk := make([]int, 0, len(a))
|
kk := make([]int, 0, len(a))
|
||||||
for k := range a {
|
for k := range a {
|
||||||
kk = append(kk, int(k))
|
if !a[k].Shared {
|
||||||
|
kk = append(kk, int(k))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sort.Ints(kk)
|
sort.Ints(kk)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,20 @@ type App struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp returns a new app.
|
// NewApp returns a new app.
|
||||||
func NewApp() *App {
|
func NewApp(cluster string) *App {
|
||||||
a := App{
|
a := App{
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
actions: make(KeyActions),
|
actions: make(KeyActions),
|
||||||
Main: NewPages(),
|
Main: NewPages(),
|
||||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||||
}
|
}
|
||||||
a.RefreshStyles()
|
a.ReloadStyles(cluster)
|
||||||
|
|
||||||
a.views = map[string]tview.Primitive{
|
a.views = map[string]tview.Primitive{
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
"logo": NewLogoView(a.Styles),
|
"logo": NewLogo(a.Styles),
|
||||||
"cmd": NewCmdView(a.Styles),
|
"cmd": NewCommand(a.Styles),
|
||||||
"flash": NewFlashView(&a, "Initializing..."),
|
"flash": NewFlash(&a, "Initializing..."),
|
||||||
"crumbs": NewCrumbs(a.Styles),
|
"crumbs": NewCrumbs(a.Styles),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,6 +46,10 @@ func (a *App) Init() {
|
||||||
a.SetRoot(a.Main, true)
|
a.SetRoot(a.Main, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) ReloadStyles(cluster string) {
|
||||||
|
a.RefreshStyles(cluster)
|
||||||
|
}
|
||||||
|
|
||||||
// Conn returns an api server connection.
|
// Conn returns an api server connection.
|
||||||
func (a *App) Conn() client.Connection {
|
func (a *App) Conn() client.Connection {
|
||||||
return a.Config.GetConnection()
|
return a.Config.GetConnection()
|
||||||
|
|
@ -188,18 +192,18 @@ func (a *App) Crumbs() *Crumbs {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo return the app logo.
|
// Logo return the app logo.
|
||||||
func (a *App) Logo() *LogoView {
|
func (a *App) Logo() *Logo {
|
||||||
return a.views["logo"].(*LogoView)
|
return a.views["logo"].(*Logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flash returns app flash.
|
// Flash returns app flash.
|
||||||
func (a *App) Flash() *FlashView {
|
func (a *App) Flash() *Flash {
|
||||||
return a.views["flash"].(*FlashView)
|
return a.views["flash"].(*Flash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cmd returns app cmd.
|
// Cmd returns app cmd.
|
||||||
func (a *App) Cmd() *CmdView {
|
func (a *App) Cmd() *Command {
|
||||||
return a.views["cmd"].(*CmdView)
|
return a.views["cmd"].(*Command)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu returns app menu.
|
// Menu returns app menu.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppGetCmd(t *testing.T) {
|
func TestAppGetCmd(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().Set("blee")
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ func TestAppGetCmd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppInCmdMode(t *testing.T) {
|
func TestAppInCmdMode(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().Set("blee")
|
||||||
assert.False(t, a.InCmdMode())
|
assert.False(t, a.InCmdMode())
|
||||||
|
|
@ -26,7 +26,7 @@ func TestAppInCmdMode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppResetCmd(t *testing.T) {
|
func TestAppResetCmd(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().Set("blee")
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ func TestAppResetCmd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppHasCmd(t *testing.T) {
|
func TestAppHasCmd(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
a.ActivateCmd(true)
|
a.ActivateCmd(true)
|
||||||
|
|
@ -47,7 +47,7 @@ func TestAppHasCmd(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppGetActions(t *testing.T) {
|
func TestAppGetActions(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
|
a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
|
||||||
|
|
@ -56,7 +56,7 @@ func TestAppGetActions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppViews(t *testing.T) {
|
func TestAppViews(t *testing.T) {
|
||||||
a := ui.NewApp()
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
||||||
|
|
|
||||||
|
|
@ -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 '🐩'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
const maxBuff = 10
|
const maxBuff = 10
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -67,7 +65,6 @@ func (c *CmdBuff) IsActive() bool {
|
||||||
|
|
||||||
// SetActive toggles cmd buffer active state.
|
// SetActive toggles cmd buffer active state.
|
||||||
func (c *CmdBuff) SetActive(b bool) {
|
func (c *CmdBuff) SetActive(b bool) {
|
||||||
log.Debug().Msgf("CMDBUFF -- Active %t", b)
|
|
||||||
c.active = b
|
c.active = b
|
||||||
c.fireActive(c.active)
|
c.fireActive(c.active)
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +143,7 @@ func (c *CmdBuff) fireChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CmdBuff) fireActive(b bool) {
|
func (c *CmdBuff) fireActive(b bool) {
|
||||||
log.Debug().Msgf("CMDBUFF LIST SIZE %d", len(c.listeners))
|
|
||||||
for _, l := range c.listeners {
|
for _, l := range c.listeners {
|
||||||
log.Debug().Msgf("CMDBUFF LIST -- %T", l)
|
|
||||||
l.BufferActive(b, c.kind)
|
l.BufferActive(b, c.kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 '🐩'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCmdNew(t *testing.T) {
|
func TestCmdNew(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewCommand(config.NewStyles())
|
||||||
v := ui.NewCmdView(defaults)
|
|
||||||
|
|
||||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||||
buff.AddListener(v)
|
buff.AddListener(v)
|
||||||
|
|
@ -20,8 +19,7 @@ func TestCmdNew(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdUpdate(t *testing.T) {
|
func TestCmdUpdate(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewCommand(config.NewStyles())
|
||||||
v := ui.NewCmdView(defaults)
|
|
||||||
|
|
||||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||||
buff.AddListener(v)
|
buff.AddListener(v)
|
||||||
|
|
@ -34,8 +32,7 @@ func TestCmdUpdate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdMode(t *testing.T) {
|
func TestCmdMode(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewCommand(config.NewStyles())
|
||||||
v := ui.NewCmdView(defaults)
|
|
||||||
|
|
||||||
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
buff := ui.NewCmdBuff(':', ui.CommandBuff)
|
||||||
buff.AddListener(v)
|
buff.AddListener(v)
|
||||||
|
|
@ -2,6 +2,7 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
|
@ -19,14 +20,22 @@ type synchronizer interface {
|
||||||
|
|
||||||
// Configurator represents an application configurationa.
|
// Configurator represents an application configurationa.
|
||||||
type Configurator struct {
|
type Configurator struct {
|
||||||
HasSkins bool
|
skinFile string
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Styles *config.Styles
|
Styles *config.Styles
|
||||||
Bench *config.Bench
|
Bench *config.Bench
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Configurator) HasSkins() bool {
|
||||||
|
return c.skinFile != ""
|
||||||
|
}
|
||||||
|
|
||||||
// StylesUpdater watches for skin file changes.
|
// StylesUpdater watches for skin file changes.
|
||||||
func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error {
|
func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error {
|
||||||
|
if !c.HasSkins() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
w, err := fsnotify.NewWatcher()
|
w, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -38,12 +47,13 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error
|
||||||
case evt := <-w.Events:
|
case evt := <-w.Events:
|
||||||
_ = evt
|
_ = evt
|
||||||
s.QueueUpdateDraw(func() {
|
s.QueueUpdateDraw(func() {
|
||||||
c.RefreshStyles()
|
c.RefreshStyles(c.Config.K9s.CurrentCluster)
|
||||||
})
|
})
|
||||||
case err := <-w.Errors:
|
case err := <-w.Errors:
|
||||||
log.Info().Err(err).Msg("Skin watcher failed")
|
log.Info().Err(err).Msg("Skin watcher failed")
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile)
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
log.Error().Err(err).Msg("Closing watcher")
|
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.
|
// InitBench load benchmark configuration if any.
|
||||||
|
|
@ -69,14 +80,28 @@ func BenchConfig(cluster string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshStyles load for skin configuration changes.
|
// RefreshStyles load for skin configuration changes.
|
||||||
func (c *Configurator) RefreshStyles() {
|
func (c *Configurator) RefreshStyles(cluster string) {
|
||||||
var err error
|
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", cluster))
|
||||||
if c.Styles, err = config.NewStyles(config.K9sStylesFile); err != nil {
|
if c.Styles == nil {
|
||||||
log.Info().Msg("No skin file found. Loading stock skins.")
|
c.Styles = config.NewStyles()
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||||
c.HasSkins = true
|
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()
|
c.Styles.Update()
|
||||||
|
|
||||||
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ func TestConfiguratorRefreshStyle(t *testing.T) {
|
||||||
config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml")
|
config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml")
|
||||||
|
|
||||||
cfg := ui.Configurator{}
|
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.ColorGhostWhite, render.StdColor)
|
||||||
assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor)
|
assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,45 +19,52 @@ type Crumbs struct {
|
||||||
|
|
||||||
// NewCrumbs returns a new breadcrumb view.
|
// NewCrumbs returns a new breadcrumb view.
|
||||||
func NewCrumbs(styles *config.Styles) *Crumbs {
|
func NewCrumbs(styles *config.Styles) *Crumbs {
|
||||||
v := Crumbs{
|
c := Crumbs{
|
||||||
stack: model.NewStack(),
|
stack: model.NewStack(),
|
||||||
styles: styles,
|
styles: styles,
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
}
|
}
|
||||||
v.SetBackgroundColor(styles.BgColor())
|
c.SetBackgroundColor(styles.BgColor())
|
||||||
v.SetTextAlign(tview.AlignLeft)
|
c.SetTextAlign(tview.AlignLeft)
|
||||||
v.SetBorderPadding(0, 0, 1, 1)
|
c.SetBorderPadding(0, 0, 1, 1)
|
||||||
v.SetDynamicColors(true)
|
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.
|
// StackPushed indicates a new item was added.
|
||||||
func (v *Crumbs) StackPushed(c model.Component) {
|
func (c *Crumbs) StackPushed(comp model.Component) {
|
||||||
v.stack.Push(c)
|
c.stack.Push(comp)
|
||||||
v.refresh(v.stack.Flatten())
|
c.refresh(c.stack.Flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackPopped indicates an item was deleted
|
// StackPopped indicates an item was deleted
|
||||||
func (v *Crumbs) StackPopped(_, _ model.Component) {
|
func (c *Crumbs) StackPopped(_, _ model.Component) {
|
||||||
v.stack.Pop()
|
c.stack.Pop()
|
||||||
v.refresh(v.stack.Flatten())
|
c.refresh(c.stack.Flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackTop indicates the top of the stack
|
// 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.
|
// Refresh updates view with new crumbs.
|
||||||
func (v *Crumbs) refresh(crumbs []string) {
|
func (c *Crumbs) refresh(crumbs []string) {
|
||||||
v.Clear()
|
c.Clear()
|
||||||
last, bgColor := len(crumbs)-1, v.styles.Frame().Crumb.BgColor
|
last, bgColor := len(crumbs)-1, c.styles.Frame().Crumb.BgColor
|
||||||
for i, c := range crumbs {
|
for i, crumb := range crumbs {
|
||||||
if i == last {
|
if i == last {
|
||||||
bgColor = v.styles.Frame().Crumb.ActiveColor
|
bgColor = c.styles.Frame().Crumb.ActiveColor
|
||||||
}
|
}
|
||||||
fmt.Fprintf(v, "[%s:%s:b] <%s> [-:%s:-] ",
|
fmt.Fprintf(c, "[%s:%s:b] <%s> [-:%s:-] ",
|
||||||
v.styles.Frame().Crumb.FgColor,
|
c.styles.Frame().Crumb.FgColor,
|
||||||
bgColor, strings.Replace(strings.ToLower(c), " ", "", -1),
|
bgColor, strings.Replace(strings.ToLower(crumb), " ", "", -1),
|
||||||
v.styles.Body().BgColor)
|
c.styles.Body().BgColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCrumbs(t *testing.T) {
|
func TestNewCrumbs(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewCrumbs(config.NewStyles())
|
||||||
v := ui.NewCrumbs(defaults)
|
|
||||||
v.StackPushed(makeComponent("c1"))
|
v.StackPushed(makeComponent("c1"))
|
||||||
v.StackPushed(makeComponent("c2"))
|
v.StackPushed(makeComponent("c2"))
|
||||||
v.StackPushed(makeComponent("c3"))
|
v.StackPushed(makeComponent("c3"))
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -34,8 +35,8 @@ type (
|
||||||
// FlashLevel represents flash message severity.
|
// FlashLevel represents flash message severity.
|
||||||
FlashLevel int
|
FlashLevel int
|
||||||
|
|
||||||
// FlashView represents a flash message indicator.
|
// Flash represents a flash message indicator.
|
||||||
FlashView struct {
|
Flash struct {
|
||||||
*tview.TextView
|
*tview.TextView
|
||||||
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
@ -43,45 +44,51 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewFlashView returns a new flash view.
|
// NewFlash returns a new flash view.
|
||||||
func NewFlashView(app *App, m string) *FlashView {
|
func NewFlash(app *App, m string) *Flash {
|
||||||
f := FlashView{app: app, TextView: tview.NewTextView()}
|
f := Flash{app: app, TextView: tview.NewTextView()}
|
||||||
f.SetTextColor(tcell.ColorAqua)
|
f.SetTextColor(tcell.ColorAqua)
|
||||||
f.SetTextAlign(tview.AlignLeft)
|
f.SetTextAlign(tview.AlignLeft)
|
||||||
f.SetBorderPadding(0, 0, 1, 1)
|
f.SetBorderPadding(0, 0, 1, 1)
|
||||||
f.SetText("")
|
f.SetText("")
|
||||||
|
f.app.Styles.AddListener(&f)
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Flash) StylesChanged(s *config.Styles) {
|
||||||
|
f.SetBackgroundColor(s.BgColor())
|
||||||
|
f.SetTextColor(s.FgColor())
|
||||||
|
}
|
||||||
|
|
||||||
// Info displays an info flash message.
|
// Info displays an info flash message.
|
||||||
func (v *FlashView) Info(msg string) {
|
func (f *Flash) Info(msg string) {
|
||||||
v.setMessage(FlashInfo, msg)
|
f.setMessage(FlashInfo, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof displays a formatted info flash message.
|
// Infof displays a formatted info flash message.
|
||||||
func (v *FlashView) Infof(fmat string, args ...interface{}) {
|
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
||||||
v.Info(fmt.Sprintf(fmat, args...))
|
f.Info(fmt.Sprintf(fmat, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn displays a warning flash message.
|
// Warn displays a warning flash message.
|
||||||
func (v *FlashView) Warn(msg string) {
|
func (f *Flash) Warn(msg string) {
|
||||||
v.setMessage(FlashWarn, msg)
|
f.setMessage(FlashWarn, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf displays a formatted warning flash message.
|
// Warnf displays a formatted warning flash message.
|
||||||
func (v *FlashView) Warnf(fmat string, args ...interface{}) {
|
func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
||||||
v.Warn(fmt.Sprintf(fmat, args...))
|
f.Warn(fmt.Sprintf(fmat, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err displays an error flash message.
|
// 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)
|
log.Error().Err(err).Msgf("%v", err)
|
||||||
v.setMessage(FlashErr, err.Error())
|
f.setMessage(FlashErr, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errf displays a formatted error flash message.
|
// 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
|
var err error
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
switch e := a.(type) {
|
switch e := a.(type) {
|
||||||
|
|
@ -90,30 +97,30 @@ func (v *FlashView) Errf(fmat string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Error().Err(err).Msgf(fmat, args...)
|
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) {
|
func (f *Flash) setMessage(level FlashLevel, msg ...string) {
|
||||||
if v.cancel != nil {
|
if f.cancel != nil {
|
||||||
v.cancel()
|
f.cancel()
|
||||||
}
|
}
|
||||||
var ctx1, ctx2 context.Context
|
var ctx1, ctx2 context.Context
|
||||||
{
|
{
|
||||||
var timerCancel context.CancelFunc
|
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)
|
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 {
|
if width <= 15 {
|
||||||
width = 100
|
width = 100
|
||||||
}
|
}
|
||||||
m := strings.Join(msg, " ")
|
m := strings.Join(msg, " ")
|
||||||
v.SetTextColor(flashColor(level))
|
f.SetTextColor(flashColor(level))
|
||||||
v.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
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()
|
defer cancel()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -122,8 +129,8 @@ func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFun
|
||||||
return
|
return
|
||||||
// Timed out clear and bail
|
// Timed out clear and bail
|
||||||
case <-ctx2.Done():
|
case <-ctx2.Done():
|
||||||
v.app.QueueUpdateDraw(func() {
|
f.app.QueueUpdateDraw(func() {
|
||||||
v.Clear()
|
f.Clear()
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlashInfo(t *testing.T) {
|
func TestFlashInfo(t *testing.T) {
|
||||||
f := ui.NewFlashView(ui.NewApp(), "YO!")
|
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||||
|
|
||||||
f.Info("Blee")
|
f.Info("Blee")
|
||||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
||||||
|
|
@ -19,7 +19,7 @@ func TestFlashInfo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlashWarn(t *testing.T) {
|
func TestFlashWarn(t *testing.T) {
|
||||||
f := ui.NewFlashView(ui.NewApp(), "YO!")
|
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||||
|
|
||||||
f.Warn("Blee")
|
f.Warn("Blee")
|
||||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||||
|
|
@ -29,7 +29,7 @@ func TestFlashWarn(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlashErr(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"))
|
f.Err(errors.New("Blee"))
|
||||||
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IndicatorView represents a status indicator.
|
// StatusIndicator represents a status indicator when main header is collapsed.
|
||||||
type IndicatorView struct {
|
type StatusIndicator struct {
|
||||||
*tview.TextView
|
*tview.TextView
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
|
|
@ -20,67 +20,74 @@ type IndicatorView struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIndicatorView returns a new status indicator.
|
// NewStatusIndicator returns a new status indicator.
|
||||||
func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView {
|
func NewStatusIndicator(app *App, styles *config.Styles) *StatusIndicator {
|
||||||
v := IndicatorView{
|
s := StatusIndicator{
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
app: app,
|
app: app,
|
||||||
styles: styles,
|
styles: styles,
|
||||||
}
|
}
|
||||||
v.SetTextAlign(tview.AlignCenter)
|
s.SetTextAlign(tview.AlignCenter)
|
||||||
v.SetTextColor(tcell.ColorWhite)
|
s.SetTextColor(tcell.ColorWhite)
|
||||||
v.SetBackgroundColor(styles.BgColor())
|
s.SetBackgroundColor(styles.BgColor())
|
||||||
v.SetDynamicColors(true)
|
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
|
// SetPermanent sets permanent title to be reset to after updates
|
||||||
func (v *IndicatorView) SetPermanent(info string) {
|
func (s *StatusIndicator) SetPermanent(info string) {
|
||||||
v.permanent = info
|
s.permanent = info
|
||||||
v.SetText(info)
|
s.SetText(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset clears out the logo view and resets colors.
|
// Reset clears out the logo view and resets colors.
|
||||||
func (v *IndicatorView) Reset() {
|
func (s *StatusIndicator) Reset() {
|
||||||
v.Clear()
|
s.Clear()
|
||||||
v.SetPermanent(v.permanent)
|
s.SetPermanent(s.permanent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err displays a log error state.
|
// Err displays a log error state.
|
||||||
func (v *IndicatorView) Err(msg string) {
|
func (s *StatusIndicator) Err(msg string) {
|
||||||
v.update(msg, "orangered")
|
s.update(msg, "orangered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn displays a log warning state.
|
// Warn displays a log warning state.
|
||||||
func (v *IndicatorView) Warn(msg string) {
|
func (s *StatusIndicator) Warn(msg string) {
|
||||||
v.update(msg, "mediumvioletred")
|
s.update(msg, "mediumvioletred")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays a log info state.
|
// Info displays a log info state.
|
||||||
func (v *IndicatorView) Info(msg string) {
|
func (s *StatusIndicator) Info(msg string) {
|
||||||
v.update(msg, "lawngreen")
|
s.update(msg, "lawngreen")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *IndicatorView) update(msg, c string) {
|
func (s *StatusIndicator) update(msg, c string) {
|
||||||
v.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg))
|
s.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *IndicatorView) setText(msg string) {
|
func (s *StatusIndicator) setText(msg string) {
|
||||||
if v.cancel != nil {
|
if s.cancel != nil {
|
||||||
v.cancel()
|
s.cancel()
|
||||||
}
|
}
|
||||||
v.SetText(msg)
|
s.SetText(msg)
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
ctx, v.cancel = context.WithCancel(context.Background())
|
ctx, s.cancel = context.WithCancel(context.Background())
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
v.app.QueueUpdateDraw(func() {
|
s.app.QueueUpdateDraw(func() {
|
||||||
v.Reset()
|
s.Reset()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}(ctx)
|
}(ctx)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIndicatorReset(t *testing.T) {
|
func TestIndicatorReset(t *testing.T) {
|
||||||
s, _ := config.NewStyles("")
|
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
|
||||||
|
|
||||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
|
||||||
i.SetPermanent("Blee")
|
i.SetPermanent("Blee")
|
||||||
i.Info("duh")
|
i.Info("duh")
|
||||||
i.Reset()
|
i.Reset()
|
||||||
|
|
@ -20,27 +18,21 @@ func TestIndicatorReset(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndicatorInfo(t *testing.T) {
|
func TestIndicatorInfo(t *testing.T) {
|
||||||
s, _ := config.NewStyles("")
|
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
|
||||||
|
|
||||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
|
||||||
i.Info("Blee")
|
i.Info("Blee")
|
||||||
|
|
||||||
assert.Equal(t, "[lawngreen::b] <Blee> \n", i.GetText(false))
|
assert.Equal(t, "[lawngreen::b] <Blee> \n", i.GetText(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndicatorWarn(t *testing.T) {
|
func TestIndicatorWarn(t *testing.T) {
|
||||||
s, _ := config.NewStyles("")
|
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
|
||||||
|
|
||||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
|
||||||
i.Warn("Blee")
|
i.Warn("Blee")
|
||||||
|
|
||||||
assert.Equal(t, "[mediumvioletred::b] <Blee> \n", i.GetText(false))
|
assert.Equal(t, "[mediumvioletred::b] <Blee> \n", i.GetText(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndicatorErr(t *testing.T) {
|
func TestIndicatorErr(t *testing.T) {
|
||||||
s, _ := config.NewStyles("")
|
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
|
||||||
|
|
||||||
i := ui.NewIndicatorView(ui.NewApp(), s)
|
|
||||||
i.Err("Blee")
|
i.Err("Blee")
|
||||||
|
|
||||||
assert.Equal(t, "[orangered::b] <Blee> \n", i.GetText(false))
|
assert.Equal(t, "[orangered::b] <Blee> \n", i.GetText(false))
|
||||||
|
|
|
||||||
|
|
@ -7,67 +7,84 @@ import (
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogoView represents a K9s logo.
|
// Logo represents a K9s logo.
|
||||||
type LogoView struct {
|
type Logo struct {
|
||||||
*tview.Flex
|
*tview.Flex
|
||||||
|
|
||||||
logo, status *tview.TextView
|
logo, status *tview.TextView
|
||||||
styles *config.Styles
|
styles *config.Styles
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogoView returns a new logo.
|
// NewLogo returns a new logo.
|
||||||
func NewLogoView(styles *config.Styles) *LogoView {
|
func NewLogo(styles *config.Styles) *Logo {
|
||||||
v := LogoView{
|
l := Logo{
|
||||||
Flex: tview.NewFlex(),
|
Flex: tview.NewFlex(),
|
||||||
logo: logo(),
|
logo: logo(),
|
||||||
status: status(),
|
status: status(),
|
||||||
styles: styles,
|
styles: styles,
|
||||||
}
|
}
|
||||||
v.SetDirection(tview.FlexRow)
|
l.SetDirection(tview.FlexRow)
|
||||||
v.AddItem(v.logo, 0, 6, false)
|
l.AddItem(l.logo, 0, 6, false)
|
||||||
v.AddItem(v.status, 0, 1, false)
|
l.AddItem(l.status, 0, 1, false)
|
||||||
v.refreshLogo(styles.Body().LogoColor)
|
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.
|
// Reset clears out the logo view and resets colors.
|
||||||
func (v *LogoView) Reset() {
|
func (l *Logo) Reset() {
|
||||||
v.status.Clear()
|
l.status.Clear()
|
||||||
v.status.SetBackgroundColor(v.styles.BgColor())
|
l.SetBackgroundColor(l.styles.BgColor())
|
||||||
v.refreshLogo(v.styles.Body().LogoColor)
|
l.status.SetBackgroundColor(l.styles.BgColor())
|
||||||
|
l.logo.SetBackgroundColor(l.styles.BgColor())
|
||||||
|
l.refreshLogo(l.styles.Body().LogoColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err displays a log error state.
|
// Err displays a log error state.
|
||||||
func (v *LogoView) Err(msg string) {
|
func (l *Logo) Err(msg string) {
|
||||||
v.update(msg, "red")
|
l.update(msg, "red")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn displays a log warning state.
|
// Warn displays a log warning state.
|
||||||
func (v *LogoView) Warn(msg string) {
|
func (l *Logo) Warn(msg string) {
|
||||||
v.update(msg, "mediumvioletred")
|
l.update(msg, "mediumvioletred")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays a log info state.
|
// Info displays a log info state.
|
||||||
func (v *LogoView) Info(msg string) {
|
func (l *Logo) Info(msg string) {
|
||||||
v.update(msg, "green")
|
l.update(msg, "green")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *LogoView) update(msg, c string) {
|
func (l *Logo) update(msg, c string) {
|
||||||
v.refreshStatus(msg, c)
|
l.refreshStatus(msg, c)
|
||||||
v.refreshLogo(c)
|
l.refreshLogo(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *LogoView) refreshStatus(msg, c string) {
|
func (l *Logo) refreshStatus(msg, c string) {
|
||||||
v.status.SetBackgroundColor(config.AsColor(c))
|
l.status.SetBackgroundColor(config.AsColor(c))
|
||||||
v.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
l.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *LogoView) refreshLogo(c string) {
|
func (l *Logo) refreshLogo(c string) {
|
||||||
v.logo.Clear()
|
l.logo.Clear()
|
||||||
for i, s := range LogoSmall {
|
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) {
|
if i+1 < len(LogoSmall) {
|
||||||
fmt.Fprintf(v.logo, "\n")
|
fmt.Fprintf(l.logo, "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
package ui
|
package ui_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewLogoView(t *testing.T) {
|
func TestNewLogoView(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewLogo(config.NewStyles())
|
||||||
v := NewLogoView(defaults)
|
|
||||||
v.Reset()
|
v.Reset()
|
||||||
|
|
||||||
const elogo = "[orange::b] ____ __.________ \n[orange::b]| |/ _/ __ \\______\n[orange::b]| < \\____ / ___/\n[orange::b]| | \\ / /\\___ \\ \n[orange::b]|____|__ \\ /____//____ >\n[orange::b] \\/ \\/ \n"
|
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, elogo, v.Logo().GetText(false))
|
||||||
assert.Equal(t, "", v.status.GetText(false))
|
assert.Equal(t, "", v.Status().GetText(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogoStatus(t *testing.T) {
|
func TestLogoStatus(t *testing.T) {
|
||||||
|
|
@ -38,8 +38,7 @@ func TestLogoStatus(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewLogo(config.NewStyles())
|
||||||
v := NewLogoView(defaults)
|
|
||||||
for n := range uu {
|
for n := range uu {
|
||||||
k, u := n, uu[n]
|
k, u := n, uu[n]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
|
|
@ -51,8 +50,8 @@ func TestLogoStatus(t *testing.T) {
|
||||||
case "err":
|
case "err":
|
||||||
v.Err(u.msg)
|
v.Err(u.msg)
|
||||||
}
|
}
|
||||||
assert.Equal(t, u.logo, v.logo.GetText(false))
|
assert.Equal(t, u.logo, v.Logo().GetText(false))
|
||||||
assert.Equal(t, u.e, v.status.GetText(false))
|
assert.Equal(t, u.e, v.Status().GetText(false))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,56 +31,65 @@ type Menu struct {
|
||||||
|
|
||||||
// NewMenu returns a new menu.
|
// NewMenu returns a new menu.
|
||||||
func NewMenu(styles *config.Styles) *Menu {
|
func NewMenu(styles *config.Styles) *Menu {
|
||||||
v := Menu{Table: tview.NewTable(), styles: styles}
|
m := Menu{
|
||||||
v.SetBackgroundColor(styles.BgColor())
|
Table: tview.NewTable(),
|
||||||
|
styles: styles,
|
||||||
|
}
|
||||||
|
m.SetBackgroundColor(styles.BgColor())
|
||||||
|
styles.AddListener(&m)
|
||||||
|
|
||||||
return &v
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Menu) StackPushed(c model.Component) {
|
func (m *Menu) StylesChanged(s *config.Styles) {
|
||||||
v.HydrateMenu(c.Hints())
|
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 {
|
if top != nil {
|
||||||
v.HydrateMenu(top.Hints())
|
m.HydrateMenu(top.Hints())
|
||||||
} else {
|
} else {
|
||||||
v.Clear()
|
m.Clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Menu) StackTop(t model.Component) {
|
func (m *Menu) StackTop(t model.Component) {
|
||||||
v.HydrateMenu(t.Hints())
|
m.HydrateMenu(t.Hints())
|
||||||
}
|
}
|
||||||
|
|
||||||
// HydrateMenu populate menu ui from hints.
|
// HydrateMenu populate menu ui from hints.
|
||||||
func (v *Menu) HydrateMenu(hh model.MenuHints) {
|
func (m *Menu) HydrateMenu(hh model.MenuHints) {
|
||||||
v.Clear()
|
m.Clear()
|
||||||
sort.Sort(hh)
|
sort.Sort(hh)
|
||||||
|
|
||||||
table := make([]model.MenuHints, maxRows+1)
|
table := make([]model.MenuHints, maxRows+1)
|
||||||
colCount := (len(hh) / maxRows) + 1
|
colCount := (len(hh) / maxRows) + 1
|
||||||
if v.hasDigits(hh) {
|
if m.hasDigits(hh) {
|
||||||
colCount++
|
colCount++
|
||||||
}
|
}
|
||||||
for row := 0; row < maxRows; row++ {
|
for row := 0; row < maxRows; row++ {
|
||||||
table[row] = make(model.MenuHints, colCount)
|
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 row := 0; row < len(t); row++ {
|
||||||
for col := 0; col < len(t[row]); col++ {
|
for col := 0; col < len(t[row]); col++ {
|
||||||
if len(t[row][col]) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
c := tview.NewTableCell(t[row][col])
|
c := tview.NewTableCell(t[row][col])
|
||||||
c.SetBackgroundColor(v.styles.BgColor())
|
if len(t[row][col]) == 0 {
|
||||||
v.SetCell(row, col, c)
|
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 {
|
for _, h := range hh {
|
||||||
if !h.Visible {
|
if !h.Visible {
|
||||||
continue
|
continue
|
||||||
|
|
@ -92,7 +101,7 @@ func (v *Menu) hasDigits(hh model.MenuHints) bool {
|
||||||
return false
|
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
|
var row, col int
|
||||||
firstCmd := true
|
firstCmd := true
|
||||||
maxKeys := make([]int, colCount)
|
maxKeys := make([]int, colCount)
|
||||||
|
|
@ -121,30 +130,30 @@ func (v *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCo
|
||||||
for r := range out {
|
for r := range out {
|
||||||
out[r] = make([]string, len(table[r]))
|
out[r] = make([]string, len(table[r]))
|
||||||
}
|
}
|
||||||
v.layout(table, maxKeys, out)
|
m.layout(table, maxKeys, out)
|
||||||
|
|
||||||
return 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 r := range table {
|
||||||
for c := range table[r] {
|
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 == "" {
|
if h.Mnemonic == "" || h.Description == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
i, err := strconv.Atoi(h.Mnemonic)
|
i, err := strconv.Atoi(h.Mnemonic)
|
||||||
if err == nil {
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewMenu(t *testing.T) {
|
func TestNewMenu(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
v := ui.NewMenu(config.NewStyles())
|
||||||
v := ui.NewMenu(defaults)
|
|
||||||
v.HydrateMenu(model.MenuHints{
|
v.HydrateMenu(model.MenuHints{
|
||||||
{Mnemonic: "a", Description: "bleeA", Visible: true},
|
{Mnemonic: "a", Description: "bleeA", Visible: true},
|
||||||
{Mnemonic: "b", Description: "bleeB", Visible: true},
|
{Mnemonic: "b", Description: "bleeB", Visible: true},
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ var LogoSmall = []string{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo K9s big logo for splash page.
|
// 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.
|
// Splash represents a splash screen.
|
||||||
type SplashView struct {
|
type Splash struct {
|
||||||
*tview.Flex
|
*tview.Flex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSplash instantiates a new splash screen with product and company info.
|
// NewSplash instantiates a new splash screen with product and company info.
|
||||||
func NewSplash(styles *config.Styles, version string) *SplashView {
|
func NewSplash(styles *config.Styles, version string) *Splash {
|
||||||
v := SplashView{Flex: tview.NewFlex()}
|
s := Splash{Flex: tview.NewFlex()}
|
||||||
|
|
||||||
logo := tview.NewTextView()
|
logo := tview.NewTextView()
|
||||||
logo.SetDynamicColors(true)
|
logo.SetDynamicColors(true)
|
||||||
logo.SetBackgroundColor(tcell.ColorDefault)
|
logo.SetBackgroundColor(tcell.ColorDefault)
|
||||||
logo.SetTextAlign(tview.AlignCenter)
|
logo.SetTextAlign(tview.AlignCenter)
|
||||||
v.layoutLogo(logo, styles)
|
s.layoutLogo(logo, styles)
|
||||||
|
|
||||||
vers := tview.NewTextView()
|
vers := tview.NewTextView()
|
||||||
vers.SetDynamicColors(true)
|
vers.SetDynamicColors(true)
|
||||||
vers.SetBackgroundColor(tcell.ColorDefault)
|
vers.SetBackgroundColor(tcell.ColorDefault)
|
||||||
vers.SetTextAlign(tview.AlignCenter)
|
vers.SetTextAlign(tview.AlignCenter)
|
||||||
v.layoutRev(vers, version, styles)
|
s.layoutRev(vers, version, styles)
|
||||||
|
|
||||||
v.SetDirection(tview.FlexRow)
|
s.SetDirection(tview.FlexRow)
|
||||||
v.AddItem(logo, 10, 1, false)
|
s.AddItem(logo, 10, 1, false)
|
||||||
v.AddItem(vers, 1, 1, false)
|
s.AddItem(vers, 1, 1, false)
|
||||||
|
|
||||||
return &v
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *SplashView) layoutLogo(t *tview.TextView, styles *config.Styles) {
|
func (s *Splash) layoutLogo(t *tview.TextView, styles *config.Styles) {
|
||||||
logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor))
|
logo := strings.Join(LogoBig, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor))
|
||||||
fmt.Fprintf(t, "%s[%s::b]%s\n",
|
fmt.Fprintf(t, "%s[%s::b]%s\n",
|
||||||
strings.Repeat("\n", 2),
|
strings.Repeat("\n", 2),
|
||||||
styles.Body().LogoColor,
|
styles.Body().LogoColor,
|
||||||
logo)
|
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)
|
fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", styles.Body().FgColor, rev)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package ui
|
package ui_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewSplash(t *testing.T) {
|
func TestNewSplash(t *testing.T) {
|
||||||
defaults, _ := config.NewStyles("")
|
s := ui.NewSplash(config.NewStyles(), "bozo")
|
||||||
s := NewSplash(defaults, "bozo")
|
|
||||||
|
|
||||||
x, y, w, h := s.GetRect()
|
x, y, w, h := s.GetRect()
|
||||||
assert.Equal(t, 0, x)
|
assert.Equal(t, 0, x)
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
log.Debug().Msgf("KEY PRESS %#v", evt)
|
|
||||||
key := evt.Key()
|
key := evt.Key()
|
||||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||||
return evt
|
return evt
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ import (
|
||||||
|
|
||||||
func TestTableNew(t *testing.T) {
|
func TestTableNew(t *testing.T) {
|
||||||
v := ui.NewTable("fred")
|
v := ui.NewTable("fred")
|
||||||
s, _ := config.NewStyles("")
|
ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles())
|
||||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
|
||||||
v.Init(ctx)
|
v.Init(ctx)
|
||||||
|
|
||||||
assert.Equal(t, "fred", v.BaseTitle)
|
assert.Equal(t, "fred", v.BaseTitle)
|
||||||
|
|
@ -23,8 +22,7 @@ func TestTableNew(t *testing.T) {
|
||||||
|
|
||||||
func TestTableUpdate(t *testing.T) {
|
func TestTableUpdate(t *testing.T) {
|
||||||
v := ui.NewTable("fred")
|
v := ui.NewTable("fred")
|
||||||
s, _ := config.NewStyles("")
|
ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles())
|
||||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
|
||||||
v.Init(ctx)
|
v.Init(ctx)
|
||||||
|
|
||||||
v.Update(makeTableData())
|
v.Update(makeTableData())
|
||||||
|
|
@ -35,8 +33,7 @@ func TestTableUpdate(t *testing.T) {
|
||||||
|
|
||||||
func TestTableSelection(t *testing.T) {
|
func TestTableSelection(t *testing.T) {
|
||||||
v := ui.NewTable("fred")
|
v := ui.NewTable("fred")
|
||||||
s, _ := config.NewStyles("")
|
ctx := context.WithValue(context.Background(), ui.KeyStyles, config.NewStyles())
|
||||||
ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
|
|
||||||
v.Init(ctx)
|
v.Init(ctx)
|
||||||
m := &testModel{}
|
m := &testModel{}
|
||||||
v.SetModel(m)
|
v.SetModel(m)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
aa[key] = ui.NewKeyAction(
|
aa[key] = ui.NewSharedKeyAction(
|
||||||
hk.Description,
|
hk.Description,
|
||||||
gotoCmd(r, hk.Command),
|
gotoCmd(r, hk.Command),
|
||||||
true)
|
false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,9 @@ func TestAliasNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, v.Init(makeContext()))
|
assert.Nil(t, v.Init(makeContext()))
|
||||||
assert.Equal(t, "Aliases", v.Name())
|
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) {
|
func TestAliasSearch(t *testing.T) {
|
||||||
v := view.NewAlias(client.GVR("aliases"))
|
v := view.NewAlias(client.GVR("aliases"))
|
||||||
assert.Nil(t, v.Init(makeContext()))
|
assert.Nil(t, v.Init(makeContext()))
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
splashTime = 1
|
splashTime = 1
|
||||||
clusterRefresh = time.Duration(5 * time.Second)
|
clusterRefresh = time.Duration(5 * time.Second)
|
||||||
indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App represents an application view.
|
// App represents an application view.
|
||||||
|
|
@ -28,7 +28,7 @@ type App struct {
|
||||||
*ui.App
|
*ui.App
|
||||||
|
|
||||||
Content *PageStack
|
Content *PageStack
|
||||||
command *command
|
command *Command
|
||||||
factory *watch.Factory
|
factory *watch.Factory
|
||||||
version string
|
version string
|
||||||
showHeader bool
|
showHeader bool
|
||||||
|
|
@ -38,14 +38,14 @@ type App struct {
|
||||||
// NewApp returns a K9s app instance.
|
// NewApp returns a K9s app instance.
|
||||||
func NewApp(cfg *config.Config) *App {
|
func NewApp(cfg *config.Config) *App {
|
||||||
a := App{
|
a := App{
|
||||||
App: ui.NewApp(),
|
App: ui.NewApp(cfg.K9s.CurrentCluster),
|
||||||
Content: NewPageStack(),
|
Content: NewPageStack(),
|
||||||
}
|
}
|
||||||
a.Config = cfg
|
a.Config = cfg
|
||||||
a.InitBench(cfg.K9s.CurrentCluster)
|
a.InitBench(cfg.K9s.CurrentCluster)
|
||||||
|
|
||||||
a.Views()["indicator"] = ui.NewIndicatorView(a.App, a.Styles)
|
a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles)
|
||||||
a.Views()["clusterInfo"] = newClusterInfoView(&a, client.NewMetricsServer(cfg.GetConnection()))
|
a.Views()["clusterInfo"] = NewClusterInfo(&a, client.NewMetricsServer(cfg.GetConnection()))
|
||||||
|
|
||||||
return &a
|
return &a
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ func (a *App) Init(version string, rate int) error {
|
||||||
a.factory = watch.NewFactory(a.Conn())
|
a.factory = watch.NewFactory(a.Conn())
|
||||||
a.initFactory(ns)
|
a.initFactory(ns)
|
||||||
|
|
||||||
a.command = newCommand(a)
|
a.command = NewCommand(a)
|
||||||
if err := a.command.Init(); err != nil {
|
if err := a.command.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +97,7 @@ func (a *App) Init(version string, rate int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
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.Content, 0, 10, true)
|
||||||
main.AddItem(a.Crumbs(), 2, 1, false)
|
main.AddItem(a.Crumbs(), 2, 1, false)
|
||||||
main.AddItem(a.Flash(), 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.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
|
||||||
a.toggleHeader(!a.Config.K9s.GetHeadless())
|
a.toggleHeader(!a.Config.K9s.GetHeadless())
|
||||||
|
|
||||||
|
a.Styles.AddListener(a)
|
||||||
|
|
||||||
return nil
|
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() {
|
func (a *App) bindKeys() {
|
||||||
a.AddActions(ui.KeyActions{
|
a.AddActions(ui.KeyActions{
|
||||||
ui.KeyH: ui.NewKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
|
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)
|
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
|
||||||
} else {
|
} else {
|
||||||
flex.RemoveItemAtIndex(0)
|
flex.RemoveItemAtIndex(0)
|
||||||
flex.AddItemAtIndex(0, a.indicator(), 1, 1, false)
|
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
|
||||||
a.refreshIndicator()
|
a.refreshIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) buildHeader() tview.Primitive {
|
func (a *App) buildHeader() tview.Primitive {
|
||||||
header := tview.NewFlex()
|
header := tview.NewFlex()
|
||||||
|
header.SetBackgroundColor(a.Styles.BgColor())
|
||||||
header.SetBorderPadding(0, 0, 1, 1)
|
header.SetBorderPadding(0, 0, 1, 1)
|
||||||
header.SetDirection(tview.FlexColumn)
|
header.SetDirection(tview.FlexColumn)
|
||||||
if !a.showHeader {
|
if !a.showHeader {
|
||||||
|
|
@ -176,6 +193,7 @@ func (a *App) Resume() {
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
ctx, a.cancelFn = context.WithCancel(context.Background())
|
ctx, a.cancelFn = context.WithCancel(context.Background())
|
||||||
go a.clusterUpdater(ctx)
|
go a.clusterUpdater(ctx)
|
||||||
|
a.StylesUpdater(ctx, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) clusterUpdater(ctx context.Context) {
|
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() {
|
func (a *App) refreshClusterInfo() {
|
||||||
log.Debug().Msgf("***** REFRESHING CLUSTER ******")
|
|
||||||
if !a.showHeader {
|
if !a.showHeader {
|
||||||
a.refreshIndicator()
|
a.refreshIndicator()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,12 +225,12 @@ func (a *App) refreshIndicator() {
|
||||||
var cmx client.ClusterMetrics
|
var cmx client.ClusterMetrics
|
||||||
nos, nmx, err := fetchResources(a)
|
nos, nmx, err := fetchResources(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("unable to refresh cluster indicator")
|
log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cluster.Metrics(nos, nmx, &cmx); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,8 +243,8 @@ func (a *App) refreshIndicator() {
|
||||||
mem = render.NAValue
|
mem = render.NAValue
|
||||||
}
|
}
|
||||||
|
|
||||||
a.indicator().SetPermanent(fmt.Sprintf(
|
a.statusIndicator().SetPermanent(fmt.Sprintf(
|
||||||
indicatorFmt,
|
statusIndicatorFmt,
|
||||||
a.version,
|
a.version,
|
||||||
cluster.ClusterName(),
|
cluster.ClusterName(),
|
||||||
cluster.UserName(),
|
cluster.UserName(),
|
||||||
|
|
@ -273,6 +291,7 @@ func (a *App) switchCtx(name string, loadPods bool) error {
|
||||||
a.Flash().Err(err)
|
a.Flash().Err(err)
|
||||||
}
|
}
|
||||||
a.refreshClusterInfo()
|
a.refreshClusterInfo()
|
||||||
|
a.ReloadStyles(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -296,11 +315,8 @@ func (a *App) Run() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
a.Halt()
|
a.Halt()
|
||||||
|
|
||||||
// Only enable skin updater while in dev mode.
|
if err := a.StylesUpdater(ctx, a); err != nil {
|
||||||
if a.HasSkins {
|
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() {
|
go func() {
|
||||||
|
|
@ -342,13 +358,13 @@ func (a *App) setLogo(l ui.FlashLevel, msg string) {
|
||||||
func (a *App) setIndicator(l ui.FlashLevel, msg string) {
|
func (a *App) setIndicator(l ui.FlashLevel, msg string) {
|
||||||
switch l {
|
switch l {
|
||||||
case ui.FlashErr:
|
case ui.FlashErr:
|
||||||
a.indicator().Err(msg)
|
a.statusIndicator().Err(msg)
|
||||||
case ui.FlashWarn:
|
case ui.FlashWarn:
|
||||||
a.indicator().Warn(msg)
|
a.statusIndicator().Warn(msg)
|
||||||
case ui.FlashInfo:
|
case ui.FlashInfo:
|
||||||
a.indicator().Info(msg)
|
a.statusIndicator().Info(msg)
|
||||||
default:
|
default:
|
||||||
a.indicator().Reset()
|
a.statusIndicator().Reset()
|
||||||
}
|
}
|
||||||
a.Draw()
|
a.Draw()
|
||||||
}
|
}
|
||||||
|
|
@ -427,10 +443,10 @@ func (a *App) inject(c model.Component) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) clusterInfo() *clusterInfoView {
|
func (a *App) clusterInfo() *ClusterInfo {
|
||||||
return a.Views()["clusterInfo"].(*clusterInfoView)
|
return a.Views()["clusterInfo"].(*ClusterInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) indicator() *ui.IndicatorView {
|
func (a *App) statusIndicator() *ui.StatusIndicator {
|
||||||
return a.Views()["indicator"].(*ui.IndicatorView)
|
return a.Views()["statusIndicator"].(*ui.StatusIndicator)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,4 @@ func TestAppNew(t *testing.T) {
|
||||||
a.Init("blee", 10)
|
a.Init("blee", 10)
|
||||||
|
|
||||||
assert.Equal(t, 11, len(a.GetActions()))
|
assert.Equal(t, 11, len(a.GetActions()))
|
||||||
assert.Equal(t, false, a.HasSkins)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
rt "runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
|
|
@ -77,7 +76,6 @@ func (b *Browser) Init(ctx context.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("ACCESSOR FOR %s -- %#v", b.gvr, b.accessor)
|
|
||||||
|
|
||||||
b.envFn = b.defaultK9sEnv
|
b.envFn = b.defaultK9sEnv
|
||||||
b.setNamespace(b.App().Config.ActiveNamespace())
|
b.setNamespace(b.App().Config.ActiveNamespace())
|
||||||
|
|
@ -93,8 +91,6 @@ func (b *Browser) Init(ctx context.Context) error {
|
||||||
// Start initializes browser updates.
|
// Start initializes browser updates.
|
||||||
func (b *Browser) Start() {
|
func (b *Browser) Start() {
|
||||||
b.Stop()
|
b.Stop()
|
||||||
log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine())
|
|
||||||
log.Debug().Msgf("BROWSER START %s", b.gvr)
|
|
||||||
|
|
||||||
b.Table.Start()
|
b.Table.Start()
|
||||||
ctx := b.defaultContext()
|
ctx := b.defaultContext()
|
||||||
|
|
@ -389,9 +385,9 @@ func (b *Browser) TableLoadFailed(err error) {
|
||||||
|
|
||||||
// TableDataChanged notifies view new data is available.
|
// TableDataChanged notifies view new data is available.
|
||||||
func (b *Browser) TableDataChanged(data render.TableData) {
|
func (b *Browser) TableDataChanged(data render.TableData) {
|
||||||
b.Update(data)
|
|
||||||
b.app.QueueUpdateDraw(func() {
|
b.app.QueueUpdateDraw(func() {
|
||||||
b.refreshActions()
|
b.refreshActions()
|
||||||
|
b.Update(data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,7 +406,6 @@ func (b *Browser) defaultContext() context.Context {
|
||||||
|
|
||||||
func (b *Browser) namespaceActions(aa ui.KeyActions) {
|
func (b *Browser) namespaceActions(aa ui.KeyActions) {
|
||||||
if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
b.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
b.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||||
|
|
|
||||||
|
|
@ -16,104 +16,128 @@ import (
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type clusterInfoView struct {
|
// ClusterInfo represents a cluster info view.
|
||||||
|
type ClusterInfo struct {
|
||||||
*tview.Table
|
*tview.Table
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
mxs *client.MetricsServer
|
mxs *client.MetricsServer
|
||||||
|
styles *config.Styles
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClusterInfoView(app *App, mx *client.MetricsServer) *clusterInfoView {
|
// NewClusterInfo returns a new cluster info view.
|
||||||
return &clusterInfoView{
|
func NewClusterInfo(app *App, mx *client.MetricsServer) *ClusterInfo {
|
||||||
app: app,
|
return &ClusterInfo{
|
||||||
Table: tview.NewTable(),
|
app: app,
|
||||||
mxs: mx,
|
Table: tview.NewTable(),
|
||||||
|
mxs: mx,
|
||||||
|
styles: app.Styles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) init(version string) {
|
func (c *ClusterInfo) init(version string) {
|
||||||
cluster := model.NewCluster(v.app.Conn(), v.mxs)
|
cluster := model.NewCluster(c.app.Conn(), c.mxs)
|
||||||
|
|
||||||
row := v.initInfo(cluster)
|
c.app.Styles.AddListener(c)
|
||||||
row = v.initVersion(row, version, cluster)
|
|
||||||
|
|
||||||
v.SetCell(row, 0, v.sectionCell("CPU"))
|
row := c.initInfo(cluster)
|
||||||
v.SetCell(row, 1, v.infoCell(render.NAValue))
|
row = c.initVersion(row, version, cluster)
|
||||||
|
|
||||||
|
c.SetCell(row, 0, c.sectionCell("CPU"))
|
||||||
|
c.SetCell(row, 1, c.infoCell(render.NAValue))
|
||||||
row++
|
row++
|
||||||
v.SetCell(row, 0, v.sectionCell("MEM"))
|
c.SetCell(row, 0, c.sectionCell("MEM"))
|
||||||
v.SetCell(row, 1, v.infoCell(render.NAValue))
|
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
|
var row int
|
||||||
v.SetCell(row, 0, v.sectionCell("Context"))
|
c.SetCell(row, 0, c.sectionCell("Context"))
|
||||||
v.SetCell(row, 1, v.infoCell(cluster.ContextName()))
|
c.SetCell(row, 1, c.infoCell(cluster.ContextName()))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
v.SetCell(row, 0, v.sectionCell("Cluster"))
|
c.SetCell(row, 0, c.sectionCell("Cluster"))
|
||||||
v.SetCell(row, 1, v.infoCell(cluster.ClusterName()))
|
c.SetCell(row, 1, c.infoCell(cluster.ClusterName()))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
v.SetCell(row, 0, v.sectionCell("User"))
|
c.SetCell(row, 0, c.sectionCell("User"))
|
||||||
v.SetCell(row, 1, v.infoCell(cluster.UserName()))
|
c.SetCell(row, 1, c.infoCell(cluster.UserName()))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) initVersion(row int, version string, cluster *model.Cluster) int {
|
func (c *ClusterInfo) initVersion(row int, version string, cluster *model.Cluster) int {
|
||||||
v.SetCell(row, 0, v.sectionCell("K9s Rev"))
|
c.SetCell(row, 0, c.sectionCell("K9s Rev"))
|
||||||
v.SetCell(row, 1, v.infoCell(version))
|
c.SetCell(row, 1, c.infoCell(version))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
v.SetCell(row, 0, v.sectionCell("K8s Rev"))
|
c.SetCell(row, 0, c.sectionCell("K8s Rev"))
|
||||||
v.SetCell(row, 1, v.infoCell(cluster.Version()))
|
c.SetCell(row, 1, c.infoCell(cluster.Version()))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) sectionCell(t string) *tview.TableCell {
|
func (c *ClusterInfo) sectionCell(t string) *tview.TableCell {
|
||||||
c := tview.NewTableCell(t + ":")
|
cell := tview.NewTableCell(t + ":")
|
||||||
c.SetAlign(tview.AlignLeft)
|
cell.SetAlign(tview.AlignLeft)
|
||||||
var s tcell.Style
|
var s tcell.Style
|
||||||
c.SetStyle(s.Bold(true).Foreground(config.AsColor(v.app.Styles.K9s.Info.SectionColor)))
|
cell.SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor)))
|
||||||
c.SetBackgroundColor(v.app.Styles.BgColor())
|
cell.SetBackgroundColor(c.app.Styles.BgColor())
|
||||||
|
|
||||||
return c
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) infoCell(t string) *tview.TableCell {
|
func (c *ClusterInfo) infoCell(t string) *tview.TableCell {
|
||||||
c := tview.NewTableCell(t)
|
cell := tview.NewTableCell(t)
|
||||||
c.SetExpansion(2)
|
cell.SetExpansion(2)
|
||||||
c.SetTextColor(config.AsColor(v.app.Styles.K9s.Info.FgColor))
|
cell.SetTextColor(config.AsColor(c.styles.K9s.Info.FgColor))
|
||||||
c.SetBackgroundColor(v.app.Styles.BgColor())
|
cell.SetBackgroundColor(c.app.Styles.BgColor())
|
||||||
|
|
||||||
return c
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) refresh() {
|
func (c *ClusterInfo) refresh() {
|
||||||
var (
|
var (
|
||||||
cluster = model.NewCluster(v.app.Conn(), v.mxs)
|
cluster = model.NewCluster(c.app.Conn(), c.mxs)
|
||||||
row int
|
row int
|
||||||
)
|
)
|
||||||
v.GetCell(row, 1).SetText(cluster.ContextName())
|
|
||||||
|
c.GetCell(row, 1).SetText(cluster.ContextName())
|
||||||
row++
|
row++
|
||||||
v.GetCell(row, 1).SetText(cluster.ClusterName())
|
c.GetCell(row, 1).SetText(cluster.ClusterName())
|
||||||
row++
|
row++
|
||||||
v.GetCell(row, 1).SetText(cluster.UserName())
|
c.GetCell(row, 1).SetText(cluster.UserName())
|
||||||
row += 2
|
row += 2
|
||||||
v.GetCell(row, 1).SetText(cluster.Version())
|
c.GetCell(row, 1).SetText(cluster.Version())
|
||||||
row++
|
row++
|
||||||
|
|
||||||
c := v.GetCell(row, 1)
|
cell := c.GetCell(row, 1)
|
||||||
c.SetText(render.NAValue)
|
cell.SetText(render.NAValue)
|
||||||
c = v.GetCell(row+1, 1)
|
cell = c.GetCell(row+1, 1)
|
||||||
c.SetText(render.NAValue)
|
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) {
|
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
|
return nos, nmx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *clusterInfoView) refreshMetrics(cluster *model.Cluster, row int) {
|
func (c *ClusterInfo) refreshMetrics(cluster *model.Cluster, row int) {
|
||||||
nos, nmx, err := fetchResources(v.app)
|
nos, nmx, err := fetchResources(c.app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Msgf("NodeMetrics %#v", err)
|
log.Warn().Msgf("NodeMetrics %#v", err)
|
||||||
return
|
return
|
||||||
|
|
@ -142,20 +166,20 @@ func (v *clusterInfoView) refreshMetrics(cluster *model.Cluster, row int) {
|
||||||
if err := cluster.Metrics(nos, nmx, &cmx); err != nil {
|
if err := cluster.Metrics(nos, nmx, &cmx); err != nil {
|
||||||
log.Error().Err(err).Msgf("failed to retrieve cluster metrics")
|
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)
|
cpu := render.AsPerc(cmx.PercCPU)
|
||||||
if cpu == "0" {
|
if cpu == "0" {
|
||||||
cpu = render.NAValue
|
cpu = render.NAValue
|
||||||
}
|
}
|
||||||
c.SetText(cpu + "%" + ui.Deltas(strip(c.Text), cpu))
|
cell.SetText(cpu + "%" + ui.Deltas(strip(cell.Text), cpu))
|
||||||
row++
|
row++
|
||||||
|
|
||||||
c = v.GetCell(row, 1)
|
cell = c.GetCell(row, 1)
|
||||||
mem := render.AsPerc(cmx.PercMEM)
|
mem := render.AsPerc(cmx.PercMEM)
|
||||||
if mem == "0" {
|
if mem == "0" {
|
||||||
mem = render.NAValue
|
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 {
|
func strip(s string) string {
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,19 @@ import (
|
||||||
|
|
||||||
var customViewers MetaViewers
|
var customViewers MetaViewers
|
||||||
|
|
||||||
type command struct {
|
type Command struct {
|
||||||
app *App
|
app *App
|
||||||
|
|
||||||
alias *dao.Alias
|
alias *dao.Alias
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommand(app *App) *command {
|
func NewCommand(app *App) *Command {
|
||||||
return &command{
|
return &Command{
|
||||||
app: app,
|
app: app,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) Init() error {
|
func (c *Command) Init() error {
|
||||||
c.alias = dao.NewAlias(c.app.factory)
|
c.alias = dao.NewAlias(c.app.factory)
|
||||||
if _, err := c.alias.Ensure(); err != nil {
|
if _, err := c.alias.Ensure(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -35,8 +35,8 @@ func (c *command) Init() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset resets command and reload aliases.
|
// Reset resets Command and reload aliases.
|
||||||
func (c *command) Reset() error {
|
func (c *Command) Reset() error {
|
||||||
c.alias.Clear()
|
c.alias.Clear()
|
||||||
if _, err := c.alias.Ensure(); err != nil {
|
if _, err := c.alias.Ensure(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -45,13 +45,13 @@ func (c *command) Reset() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) defaultCmd() error {
|
func (c *Command) defaultCmd() error {
|
||||||
return c.run(c.app.Config.ActiveView())
|
return c.run(c.app.Config.ActiveView())
|
||||||
}
|
}
|
||||||
|
|
||||||
var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`)
|
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, " ")
|
cmds := strings.Split(cmd, " ")
|
||||||
switch cmds[0] {
|
switch cmds[0] {
|
||||||
case "q", "Q", "quit":
|
case "q", "Q", "quit":
|
||||||
|
|
@ -79,10 +79,10 @@ func (c *command) specialCmd(cmd string) bool {
|
||||||
return false
|
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)
|
gvr, ok := c.alias.Get(cmd)
|
||||||
if !ok {
|
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)]
|
v, ok := customViewers[client.GVR(gvr)]
|
||||||
|
|
@ -93,8 +93,8 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
|
||||||
return gvr, &v, nil
|
return gvr, &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec the command by showing associated display.
|
// Exec the Command by showing associated display.
|
||||||
func (c *command) run(cmd string) error {
|
func (c *Command) run(cmd string) error {
|
||||||
if c.specialCmd(cmd) {
|
if c.specialCmd(cmd) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ func (c *command) run(cmd string) error {
|
||||||
view := c.componentFor(gvr, v)
|
view := c.componentFor(gvr, v)
|
||||||
return c.exec(gvr, view)
|
return c.exec(gvr, view)
|
||||||
default:
|
default:
|
||||||
// checks if command includes a namespace
|
// checks if Command includes a namespace
|
||||||
ns := c.app.Config.ActiveNamespace()
|
ns := c.app.Config.ActiveNamespace()
|
||||||
if len(cmds) == 2 {
|
if len(cmds) == 2 {
|
||||||
ns = cmds[1]
|
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
|
var view ResourceViewer
|
||||||
if v.viewerFn != nil {
|
if v.viewerFn != nil {
|
||||||
log.Debug().Msgf("Custom viewer for %s", gvr)
|
log.Debug().Msgf("Custom viewer for %s", gvr)
|
||||||
|
|
@ -142,14 +142,14 @@ func (c *command) componentFor(gvr string, v *MetaViewer) ResourceViewer {
|
||||||
return view
|
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 {
|
if comp == nil {
|
||||||
return fmt.Errorf("No component given for %s", gvr)
|
return fmt.Errorf("No component given for %s", gvr)
|
||||||
}
|
}
|
||||||
|
|
||||||
g := client.GVR(gvr)
|
g := client.GVR(gvr)
|
||||||
c.app.Flash().Infof("Viewing %s resource...", g.ToR())
|
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())
|
c.app.Config.SetActiveView(g.ToR())
|
||||||
if err := c.app.Config.Save(); err != nil {
|
if err := c.app.Config.Save(); err != nil {
|
||||||
log.Error().Err(err).Msg("Config save failed!")
|
log.Error().Err(err).Msg("Config save failed!")
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, c.Init(makeCtx()))
|
assert.Nil(t, c.Init(makeCtx()))
|
||||||
assert.Equal(t, "Containers", c.Name())
|
assert.Equal(t, "Containers", c.Name())
|
||||||
assert.Equal(t, 18, len(c.Hints()))
|
assert.Equal(t, 10, len(c.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestContext(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, ctx.Init(makeCtx()))
|
assert.Nil(t, ctx.Init(makeCtx()))
|
||||||
assert.Equal(t, "Contexts", ctx.Name())
|
assert.Equal(t, "Contexts", ctx.Name())
|
||||||
assert.Equal(t, 9, len(ctx.Hints()))
|
assert.Equal(t, 1, len(ctx.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, v.Init(makeCtx()))
|
assert.Nil(t, v.Init(makeCtx()))
|
||||||
assert.Equal(t, "Deployments", v.Name())
|
assert.Equal(t, "Deployments", v.Name())
|
||||||
assert.Equal(t, 17, len(v.Hints()))
|
assert.Equal(t, 7, len(v.Hints()))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, v.Init(makeCtx()))
|
assert.Nil(t, v.Init(makeCtx()))
|
||||||
assert.Equal(t, "DaemonSets", v.Name())
|
assert.Equal(t, "DaemonSets", v.Name())
|
||||||
assert.Equal(t, 16, len(v.Hints()))
|
assert.Equal(t, 6, len(v.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,8 @@ func (v *Help) Init(ctx context.Context) error {
|
||||||
func (v *Help) bindKeys() {
|
func (v *Help) bindKeys() {
|
||||||
v.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS)
|
v.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS)
|
||||||
v.Actions().Set(ui.KeyActions{
|
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),
|
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 {
|
if err := hh.Load(); err != nil {
|
||||||
return nil, fmt.Errorf("no hotkey configuration found")
|
return nil, fmt.Errorf("no hotkey configuration found")
|
||||||
}
|
}
|
||||||
m := make(model.MenuHints, 0, len(hh.HotKey))
|
kk := make(sort.StringSlice, 0, len(hh.HotKey))
|
||||||
for _, hk := range hh.HotKey {
|
for k := range hh.HotKey {
|
||||||
m = append(m, model.MenuHint{
|
kk = append(kk, k)
|
||||||
Mnemonic: hk.ShortCut,
|
}
|
||||||
Description: hk.Description,
|
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 {
|
func (v *Help) showGeneral() model.MenuHints {
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ func TestHelp(t *testing.T) {
|
||||||
v := view.NewHelp()
|
v := view.NewHelp()
|
||||||
|
|
||||||
assert.Nil(t, v.Init(ctx))
|
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, 10, v.GetColumnCount())
|
||||||
assert.Equal(t, "<backspace>", v.GetCell(1, 0).Text)
|
assert.Equal(t, "<ctrl-k>", v.GetCell(1, 0).Text)
|
||||||
assert.Equal(t, "Erase", v.GetCell(1, 1).Text)
|
assert.Equal(t, "Kill", v.GetCell(1, 1).Text)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,13 @@ type Log struct {
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
logs *Details
|
logs *Details
|
||||||
scrollIndicator *AutoScrollIndicator
|
indicator *LogIndicator
|
||||||
ansiWriter io.Writer
|
ansiWriter io.Writer
|
||||||
path, container string
|
path, container string
|
||||||
cancelFn context.CancelFunc
|
cancelFn context.CancelFunc
|
||||||
previous bool
|
previous bool
|
||||||
gvr client.GVR
|
gvr client.GVR
|
||||||
|
fullScreen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ model.Component = &Log{}
|
var _ model.Component = &Log{}
|
||||||
|
|
@ -68,8 +69,8 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
||||||
l.SetBorderPadding(0, 0, 1, 1)
|
l.SetBorderPadding(0, 0, 1, 1)
|
||||||
l.SetDirection(tview.FlexRow)
|
l.SetDirection(tview.FlexRow)
|
||||||
|
|
||||||
l.scrollIndicator = NewAutoScrollIndicator(l.app.Styles)
|
l.indicator = NewLogIndicator(l.app.Styles)
|
||||||
l.AddItem(l.scrollIndicator, 1, 1, false)
|
l.AddItem(l.indicator, 1, 1, false)
|
||||||
|
|
||||||
l.logs = NewDetails("")
|
l.logs = NewDetails("")
|
||||||
l.logs.SetBorder(false)
|
l.logs.SetBorder(false)
|
||||||
|
|
@ -89,22 +90,9 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
||||||
return nil
|
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.
|
// Hints returns a collection of menu hints.
|
||||||
func (l *Log) Hints() model.MenuHints {
|
func (l *Log) Hints() model.MenuHints {
|
||||||
return l.Actions().Hints()
|
return l.logs.Actions().Hints()
|
||||||
}
|
|
||||||
|
|
||||||
// Actions returns available actions.
|
|
||||||
func (l *Log) Actions() ui.KeyActions {
|
|
||||||
return l.logs.actions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start runs the component.
|
// Start runs the component.
|
||||||
|
|
@ -133,8 +121,10 @@ func (l *Log) bindKeys() {
|
||||||
l.logs.Actions().Set(ui.KeyActions{
|
l.logs.Actions().Set(ui.KeyActions{
|
||||||
tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true),
|
tcell.KeyEscape: ui.NewKeyAction("Back", l.app.PrevCmd, true),
|
||||||
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, 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.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.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false),
|
||||||
ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false),
|
ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false),
|
||||||
ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, 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.
|
// ScrollIndicator returns the scroll mode viewer.
|
||||||
func (l *Log) ScrollIndicator() *AutoScrollIndicator {
|
func (l *Log) Indicator() *LogIndicator {
|
||||||
return l.scrollIndicator
|
return l.indicator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) setTitle(path, co string) {
|
func (l *Log) setTitle(path, co string) {
|
||||||
|
|
@ -251,12 +241,12 @@ func (l *Log) log(lines string) {
|
||||||
|
|
||||||
// Flush write logs to viewer.
|
// Flush write logs to viewer.
|
||||||
func (l *Log) Flush(index int, buff []string) {
|
func (l *Log) Flush(index int, buff []string) {
|
||||||
if index == 0 || !l.scrollIndicator.AutoScroll() {
|
if index == 0 || !l.indicator.AutoScroll() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.log(strings.Join(buff[:index], "\n"))
|
l.log(strings.Join(buff[:index], "\n"))
|
||||||
l.app.QueueUpdateDraw(func() {
|
l.app.QueueUpdateDraw(func() {
|
||||||
l.scrollIndicator.Refresh()
|
l.indicator.Refresh()
|
||||||
l.logs.ScrollToEnd()
|
l.logs.ScrollToEnd()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -306,14 +296,6 @@ func saveData(cluster, name, data string) (string, error) {
|
||||||
return path, nil
|
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 {
|
func (l *Log) topCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
l.app.Flash().Info("Top of logs...")
|
l.app.Flash().Info("Top of logs...")
|
||||||
l.logs.ScrollToBeginning()
|
l.logs.ScrollToBeginning()
|
||||||
|
|
@ -346,3 +328,27 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
l.logs.ScrollTo(0, 0)
|
l.logs.ScrollTo(0, 0)
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package view_test
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/view"
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
@ -29,20 +28,20 @@ func TestLogAnsi(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogFlush(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.Init(makeContext())
|
||||||
v.Flush(2, []string{"blee", "bozo"})
|
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, "blee\nbozo\n", v.Logs().GetText(true))
|
||||||
assert.Equal(t, " Autoscroll: Off ", v.ScrollIndicator().GetText(true))
|
assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||||
v.ToggleAutoScrollCmd(nil)
|
v.toggleAutoScrollCmd(nil)
|
||||||
assert.Equal(t, " Autoscroll: On ", v.ScrollIndicator().GetText(true))
|
assert.Equal(t, " Autoscroll: On FullScreen: Off Wrap: Off ", v.Indicator().GetText(true))
|
||||||
assert.Equal(t, 8, len(v.Hints()))
|
assert.Equal(t, 10, len(v.Hints()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogViewSave(t *testing.T) {
|
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())
|
v.Init(makeContext())
|
||||||
|
|
||||||
app := makeApp()
|
app := makeApp()
|
||||||
|
|
@ -56,7 +55,7 @@ func TestLogViewSave(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogViewNav(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())
|
v.Init(makeContext())
|
||||||
|
|
||||||
var buff []string
|
var buff []string
|
||||||
|
|
@ -64,26 +63,27 @@ func TestLogViewNav(t *testing.T) {
|
||||||
buff = append(buff, fmt.Sprintf("line-%d\n", i))
|
buff = append(buff, fmt.Sprintf("line-%d\n", i))
|
||||||
}
|
}
|
||||||
v.Flush(100, buff)
|
v.Flush(100, buff)
|
||||||
v.ToggleAutoScrollCmd(nil)
|
v.toggleAutoScrollCmd(nil)
|
||||||
|
|
||||||
r, _ := v.Logs().GetScrollOffset()
|
r, _ := v.Logs().GetScrollOffset()
|
||||||
assert.Equal(t, -1, r)
|
assert.Equal(t, -1, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogViewClear(t *testing.T) {
|
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.Init(makeContext())
|
||||||
|
|
||||||
v.Flush(2, []string{"blee", "bozo"})
|
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, "blee\nbozo\n", v.Logs().GetText(true))
|
||||||
v.Logs().Clear()
|
v.Logs().Clear()
|
||||||
assert.Equal(t, "", v.Logs().GetText(true))
|
assert.Equal(t, "", v.Logs().GetText(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func makeApp() *view.App {
|
func makeApp() *App {
|
||||||
return view.NewApp(config.NewConfig(ks{}))
|
return NewApp(config.NewConfig(ks{}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestNSCleanser(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, ns.Init(makeCtx()))
|
assert.Nil(t, ns.Init(makeCtx()))
|
||||||
assert.Equal(t, "Namespaces", ns.Name())
|
assert.Equal(t, "Namespaces", ns.Name())
|
||||||
assert.Equal(t, 13, len(ns.Hints()))
|
assert.Equal(t, 3, len(ns.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, po.Init(makeCtx()))
|
assert.Nil(t, po.Init(makeCtx()))
|
||||||
assert.Equal(t, "Pods", po.Name())
|
assert.Equal(t, "Pods", po.Name())
|
||||||
assert.Equal(t, 25, len(po.Hints()))
|
assert.Equal(t, 15, len(po.Hints()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestPortForwardNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, pf.Init(makeCtx()))
|
assert.Nil(t, pf.Init(makeCtx()))
|
||||||
assert.Equal(t, "PortForwards", pf.Name())
|
assert.Equal(t, "PortForwards", pf.Name())
|
||||||
assert.Equal(t, 16, len(pf.Hints()))
|
assert.Equal(t, 8, len(pf.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestRbacNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, v.Init(makeCtx()))
|
assert.Nil(t, v.Init(makeCtx()))
|
||||||
assert.Equal(t, "Rbac", v.Name())
|
assert.Equal(t, "Rbac", v.Name())
|
||||||
assert.Equal(t, 10, len(v.Hints()))
|
assert.Equal(t, 2, len(v.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ func coreRes(vv MetaViewers) {
|
||||||
vv["v1/namespaces"] = MetaViewer{
|
vv["v1/namespaces"] = MetaViewer{
|
||||||
viewerFn: NewNamespace,
|
viewerFn: NewNamespace,
|
||||||
}
|
}
|
||||||
|
vv["v1/events"] = MetaViewer{
|
||||||
|
viewerFn: NewEvent,
|
||||||
|
}
|
||||||
vv["v1/pods"] = MetaViewer{
|
vv["v1/pods"] = MetaViewer{
|
||||||
viewerFn: NewPod,
|
viewerFn: NewPod,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestScreenDumpNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, po.Init(makeCtx()))
|
assert.Nil(t, po.Init(makeCtx()))
|
||||||
assert.Equal(t, "ScreenDumps", po.Name())
|
assert.Equal(t, "ScreenDumps", po.Name())
|
||||||
assert.Equal(t, 12, len(po.Hints()))
|
assert.Equal(t, 2, len(po.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, s.Init(makeCtx()))
|
assert.Nil(t, s.Init(makeCtx()))
|
||||||
assert.Equal(t, "Secrets", s.Name())
|
assert.Equal(t, "Secrets", s.Name())
|
||||||
assert.Equal(t, 13, len(s.Hints()))
|
assert.Equal(t, 3, len(s.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, s.Init(makeCtx()))
|
assert.Nil(t, s.Init(makeCtx()))
|
||||||
assert.Equal(t, "StatefulSets", s.Name())
|
assert.Equal(t, "StatefulSets", s.Name())
|
||||||
assert.Equal(t, 17, len(s.Hints()))
|
assert.Equal(t, 7, len(s.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, s.Init(makeCtx()))
|
assert.Nil(t, s.Init(makeCtx()))
|
||||||
assert.Equal(t, "Services", s.Name())
|
assert.Equal(t, "Services", s.Name())
|
||||||
assert.Equal(t, 17, len(s.Hints()))
|
assert.Equal(t, 7, len(s.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,15 +86,15 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (t *Table) bindKeys() {
|
func (t *Table) bindKeys() {
|
||||||
t.Actions().Add(ui.KeyActions{
|
t.Actions().Add(ui.KeyActions{
|
||||||
ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, false),
|
ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false),
|
||||||
tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, false),
|
tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false),
|
||||||
tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, false),
|
tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false),
|
||||||
ui.KeySlash: ui.NewKeyAction("Filter Mode", t.activateCmd, false),
|
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false),
|
||||||
tcell.KeyEscape: ui.NewKeyAction("Filter Reset", t.resetCmd, false),
|
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", t.resetCmd, false),
|
||||||
tcell.KeyEnter: ui.NewKeyAction("Filter", t.filterCmd, false),
|
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", t.filterCmd, false),
|
||||||
tcell.KeyBackspace2: ui.NewKeyAction("Erase", t.eraseCmd, false),
|
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", t.eraseCmd, false),
|
||||||
tcell.KeyBackspace: ui.NewKeyAction("Erase", t.eraseCmd, false),
|
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", t.eraseCmd, false),
|
||||||
tcell.KeyDelete: ui.NewKeyAction("Erase", t.eraseCmd, false),
|
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", t.eraseCmd, false),
|
||||||
ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(0, true), false),
|
ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(0, true), false),
|
||||||
ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(-1, true), false),
|
ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(-1, true), false),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func TestYaml(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _ := config.NewStyles("skins/stock.yml")
|
s := config.NewStyles()
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
assert.Equal(t, u.e, colorizeYAML(s.Views().Yaml, u.s))
|
assert.Equal(t, u.e, colorizeYAML(s.Views().Yaml, u.s))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,11 +147,11 @@ func (f *Factory) isClusterWide() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Factory) preload(ns string) {
|
func (f *Factory) preload(ns string) {
|
||||||
verbs := []string{"get", "list", "watch"}
|
// verbs := []string{"get", "list", "watch"}
|
||||||
_, _ = f.CanForResource(ns, "v1/pods", verbs...)
|
// _, _ = f.CanForResource(ns, "v1/pods", verbs...)
|
||||||
_, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...)
|
// _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...)
|
||||||
_, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...)
|
// _, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...)
|
||||||
_, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...)
|
// _, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanForResource return an informer is user has access.
|
// 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 {
|
func toGVR(gvr string) schema.GroupVersionResource {
|
||||||
log.Debug().Msgf(">>> Convert GVR %q", gvr)
|
|
||||||
tokens := strings.Split(gvr, "/")
|
tokens := strings.Split(gvr, "/")
|
||||||
if len(tokens) < 3 {
|
if len(tokens) < 3 {
|
||||||
tokens = append([]string{""}, tokens...)
|
tokens = append([]string{""}, tokens...)
|
||||||
|
|
|
||||||
7
main.go
7
main.go
|
|
@ -8,6 +8,9 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -21,6 +24,10 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe("localhost:6060", nil)
|
||||||
|
}()
|
||||||
|
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
|
||||||
|
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue