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