diff --git a/internal/views/alias.go b/internal/views/alias.go index aa9bc3d0..5f193e94 100644 --- a/internal/views/alias.go +++ b/internal/views/alias.go @@ -21,13 +21,13 @@ type aliasView struct { cancel context.CancelFunc } -func newAliasView(app *appView) *aliasView { +func newAliasView(app *appView, current igniter) *aliasView { v := aliasView{tableView: newTableView(app, aliasTitle)} { v.SetBorderFocusColor(tcell.ColorFuchsia) v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone) v.colorerFn = aliasColorer - v.current = app.content.GetPrimitive("main").(igniter) + v.current = current v.currentNS = "" v.registerActions() } diff --git a/internal/views/alias_test.go b/internal/views/alias_test.go new file mode 100644 index 00000000..38124f62 --- /dev/null +++ b/internal/views/alias_test.go @@ -0,0 +1,18 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestAliasView(t *testing.T) { + v := newAliasView(NewApp(config.NewConfig(ks{})), nil) + td := v.hydrate() + v.init(nil, "") + + assert.Equal(t, 3, len(td.Header)) + assert.Equal(t, 31, len(td.Rows)) + assert.Equal(t, "Aliases", v.getTitle()) +} diff --git a/internal/views/app.go b/internal/views/app.go index 913d9fcb..e3d5816c 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -8,10 +8,8 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/watch" "github.com/derailed/tview" - "github.com/fsnotify/fsnotify" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/tools/portforward" ) @@ -41,12 +39,6 @@ type ( init(ctx context.Context, ns string) } - keyHandler interface { - keyboard(evt *tcell.EventKey) *tcell.EventKey - } - - actionsFn func(keyActions) - resourceViewer interface { igniter @@ -57,101 +49,80 @@ type ( } appView struct { - *tview.Application + *shellView - config *config.Config - styles *config.Styles - bench *config.Bench - version string - flags *genericclioptions.ConfigFlags - pages *tview.Pages - content *tview.Pages - flashView *flashView - crumbsView *crumbsView - logoView *logoView - menuView *menuView - clusterInfoView *clusterInfoView - cmdView *cmdView - command *command - focusGroup []tview.Primitive - focusCurrent int - focusChanged focusHandler - cancel context.CancelFunc - cancelSkin context.CancelFunc - cmdBuff *cmdBuff - actions keyActions - stopCh chan struct{} - informer *watch.Informer - forwarders []forwarder - hasSkins bool + cmdBuff *cmdBuff + command *command + cancel context.CancelFunc + informer *watch.Informer + stopCh chan struct{} + forwarders []forwarder } ) // NewApp returns a K9s app instance. func NewApp(cfg *config.Config) *appView { v := appView{ - Application: tview.NewApplication(), - config: cfg, - pages: tview.NewPages(), - actions: make(keyActions), - content: tview.NewPages(), - cmdBuff: newCmdBuff(':'), - } - { - v.initBench(cfg.K9s.CurrentCluster) - v.refreshStyles() - v.menuView = newMenuView(&v) - v.logoView = newLogoView(&v) - v.cmdView = newCmdView(&v, '🐶') - v.command = newCommand(&v) - v.flashView = newFlashView(&v, "Initializing...") - v.crumbsView = newCrumbsView(&v) - v.clusterInfoView = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection())) - v.focusChanged = v.changedFocus - v.SetInputCapture(v.keyboard) + shellView: newShellView(), + cmdBuff: newCmdBuff(':'), } - v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd, false) - v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd, false) - v.actions[tcell.KeyCtrlC] = newKeyAction("Quit", v.quitCmd, false) - v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd, false) - v.actions[tcell.KeyCtrlA] = newKeyAction("Aliases", v.aliasCmd, true) - v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd, false) - v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false) - v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false) - v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) - v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) + v.config = cfg + v.initBench(cfg.K9s.CurrentCluster) + v.refreshStyles() + + v.views["menu"] = newMenuView(v.styles) + v.views["logo"] = newLogoView(v.styles) + v.views["cmd"] = newCmdView(v.styles, '🐶') + v.command = newCommand(&v) + v.views["flash"] = newFlashView(&v, "Initializing...") + v.views["crumbs"] = newCrumbsView(v.styles) + v.views["clusterInfo"] = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection())) + + v.SetInputCapture(v.keyboard) + v.registerActions() return &v } -func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) { - a.version = v - a.flags = flags +func (a *appView) registerActions() { + a.actions[KeyColon] = newKeyAction("Cmd", a.activateCmd, false) + a.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", a.redrawCmd, false) + a.actions[tcell.KeyCtrlC] = newKeyAction("Quit", a.quitCmd, false) + a.actions[KeyHelp] = newKeyAction("Help", a.helpCmd, false) + a.actions[tcell.KeyCtrlA] = newKeyAction("Aliases", a.aliasCmd, true) + a.actions[tcell.KeyEscape] = newKeyAction("Escape", a.escapeCmd, false) + a.actions[tcell.KeyEnter] = newKeyAction("Goto", a.gotoCmd, false) + a.actions[tcell.KeyBackspace2] = newKeyAction("Erase", a.eraseCmd, false) + a.actions[tcell.KeyBackspace] = newKeyAction("Erase", a.eraseCmd, false) + a.actions[tcell.KeyDelete] = newKeyAction("Erase", a.eraseCmd, false) +} + +func (a *appView) Init(version string, rate int) { a.startInformer() - a.clusterInfoView.init() - a.cmdBuff.addListener(a.cmdView) + a.clusterInfo().init(version) + a.cmdBuff.addListener(a.cmd()) header := tview.NewFlex() { header.SetDirection(tview.FlexColumn) - header.AddItem(a.clusterInfoView, 35, 1, false) - header.AddItem(a.menuView, 0, 1, false) - header.AddItem(a.logoView, 26, 1, false) + header.AddItem(a.clusterInfo(), 35, 1, false) + header.AddItem(a.views["menu"], 0, 1, false) + header.AddItem(a.logo(), 26, 1, false) } main := tview.NewFlex() { main.SetDirection(tview.FlexRow) main.AddItem(header, 7, 1, false) - main.AddItem(a.cmdView, 3, 1, false) + main.AddItem(a.cmd(), 3, 1, false) main.AddItem(a.content, 0, 10, true) - main.AddItem(a.crumbsView, 2, 1, false) - main.AddItem(a.flashView, 1, 1, false) + main.AddItem(a.crumbs(), 2, 1, false) + main.AddItem(a.flash(), 1, 1, false) } a.pages.AddPage("main", main, true, false) - a.pages.AddPage("splash", newSplash(a), true, true) + a.pages.AddPage("splash", newSplash(a.styles, version), true, true) a.SetRoot(a.pages, true) } @@ -163,7 +134,7 @@ func (a *appView) clusterUpdater(ctx context.Context) { return case <-time.After(clusterRefresh): a.QueueUpdateDraw(func() { - a.clusterInfoView.refresh() + a.clusterInfo().refresh() }) } } @@ -175,9 +146,9 @@ func (a *appView) startInformer() { } a.stopCh = make(chan struct{}) - ns := "" - if a.flags.Namespace != nil { - ns = *a.flags.Namespace + ns, err := a.conn().Config().CurrentNamespaceName() + if err != nil { + log.Warn().Err(err).Msg("No namespace specified using all namespaces") } a.informer = watch.NewInformer(a.conn(), ns) a.informer.Run(a.stopCh) @@ -193,11 +164,6 @@ func (a *appView) BailOut() { if a.cancel != nil { a.cancel() - a.cancel = nil - } - if a.cancelSkin != nil { - a.cancelSkin() - a.cancelSkin = nil } a.stopForwarders() a.Stop() @@ -215,33 +181,6 @@ func (a *appView) conn() k8s.Connection { return a.config.GetConnection() } -func (a *appView) stylesUpdater(ctx context.Context) error { - w, err := fsnotify.NewWatcher() - if err != nil { - return err - } - - go func() { - for { - select { - case evt := <-w.Events: - _ = evt - a.QueueUpdateDraw(func() { - a.refreshStyles() - }) - case err := <-w.Errors: - log.Info().Err(err).Msg("Skin watcher failed") - return - case <-ctx.Done(): - w.Close() - return - } - } - }() - - return w.Add(config.K9sStylesFile) -} - // Run starts the application loop func (a *appView) Run() { ctx, cancel := context.WithCancel(context.Background()) @@ -249,10 +188,9 @@ func (a *appView) Run() { go a.clusterUpdater(ctx) // Only enable skin updater while in dev mode. - if a.version == devMode && a.hasSkins { + if a.hasSkins { var ctx context.Context - ctx, a.cancelSkin = context.WithCancel(context.Background()) - if err := a.stylesUpdater(ctx); err != nil { + if err := a.stylesUpdater(ctx, a); err != nil { log.Error().Err(err).Msg("Unable to track skin changes") } } @@ -270,8 +208,16 @@ func (a *appView) Run() { } } +func (a *appView) crumbs() *crumbsView { + return a.views["crumbs"].(*crumbsView) +} + +func (a *appView) logo() *logoView { + return a.views["logo"].(*logoView) +} + func (a *appView) statusReset() { - a.logoView.reset() + a.logo().reset() a.Draw() } @@ -280,13 +226,13 @@ func (a *appView) status(l flashLevel, msg string) { switch l { case flashErr: - a.logoView.err(msg) + a.logo().err(msg) case flashWarn: - a.logoView.warn(msg) + a.logo().warn(msg) case flashInfo: - a.logoView.info(msg) + a.logo().info(msg) default: - a.logoView.reset() + a.logo().reset() } a.Draw() } @@ -322,11 +268,6 @@ func (a *appView) redrawCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } -func (a *appView) focusCmd(evt *tcell.EventKey) *tcell.EventKey { - a.nextFocus() - return evt -} - func (a *appView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { if a.cmdBuff.isActive() { a.cmdBuff.del() @@ -335,7 +276,7 @@ func (a *appView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } -func (a *appView) deactivateCmd(evt *tcell.EventKey) *tcell.EventKey { +func (a *appView) escapeCmd(evt *tcell.EventKey) *tcell.EventKey { if a.cmdBuff.isActive() { a.cmdBuff.reset() } @@ -362,10 +303,10 @@ func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdView.inCmdMode() { + if a.inCmdMode() { return evt } - a.flashView.info("Command mode activated.") + a.flash().info("Command mode activated.") log.Debug().Msg("Entering command mode...") a.cmdBuff.setActive(true) a.cmdBuff.clear() @@ -373,7 +314,7 @@ func (a *appView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdMode() { + if a.inCmdMode() { return evt } a.BailOut() @@ -382,24 +323,27 @@ func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdView.inCmdMode() { + if a.inCmdMode() { return evt } - a.inject(newHelpView(a)) + a.inject(newHelpView(a, a.currentView())) return nil } +func (a *appView) currentView() igniter { + return a.content.GetPrimitive("main").(igniter) +} func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdView.inCmdMode() { + if a.inCmdMode() { return evt } - a.inject(newAliasView(a)) + a.inject(newAliasView(a, a.currentView())) return nil } func (a *appView) fwdCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdView.inCmdMode() { + if a.inCmdMode() { return evt } @@ -438,76 +382,29 @@ func (a *appView) inject(i igniter) { i.init(ctx, a.config.ActiveNamespace()) } a.content.AddPage("main", i, true, true) - - a.focusGroup = append([]tview.Primitive{}, i) - a.focusCurrent = 0 - a.fireFocusChanged(i) a.SetFocus(i) } -func (a *appView) cmdMode() bool { - return a.cmdView.inCmdMode() -} - func (a *appView) flash() *flashView { - return a.flashView + return a.views["flash"].(*flashView) } func (a *appView) setHints(h hints) { - a.menuView.populateMenu(h) + a.views["menu"].(*menuView).populateMenu(h) } -func (a *appView) fireFocusChanged(p tview.Primitive) { - if a.focusChanged != nil { - a.focusChanged(p) - } +func (a *appView) clusterInfo() *clusterInfoView { + return a.views["clusterInfo"].(*clusterInfoView) } -func (a *appView) changedFocus(p tview.Primitive) { - switch p.(type) { - case hinter: - a.setHints(p.(hinter).hints()) - } +func (a *appView) clusterInfoRefresh() { + a.clusterInfo().refresh() } -func (a *appView) nextFocus() { - for i := range a.focusGroup { - if i == a.focusCurrent { - a.focusCurrent = 0 - victim := a.focusGroup[a.focusCurrent] - if i+1 < len(a.focusGroup) { - a.focusCurrent++ - victim = a.focusGroup[a.focusCurrent] - } - a.fireFocusChanged(victim) - a.SetFocus(victim) - return - } - } - return +func (a *appView) cmd() *cmdView { + return a.views["cmd"].(*cmdView) } -func (a *appView) initBench(cluster string) { - var err error - if a.bench, err = config.NewBench(benchConfig(cluster)); err != nil { - log.Warn().Err(err).Msg("No benchmark config file found, using defaults.") - } -} - -func (a *appView) refreshStyles() { - var err error - if a.styles, err = config.NewStyles(config.K9sStylesFile); err != nil { - log.Warn().Err(err).Msg("No skin file found. Loading defaults.") - } - if err == nil { - a.hasSkins = true - } - a.styles.Update() - - stdColor = config.AsColor(a.styles.Style.Status.NewColor) - addColor = config.AsColor(a.styles.Style.Status.AddColor) - modColor = config.AsColor(a.styles.Style.Status.ModifyColor) - errColor = config.AsColor(a.styles.Style.Status.ErrorColor) - highlightColor = config.AsColor(a.styles.Style.Status.HighlightColor) - completedColor = config.AsColor(a.styles.Style.Status.CompletedColor) +func (a *appView) inCmdMode() bool { + return a.cmd().inCmdMode() } diff --git a/internal/views/app_test.go b/internal/views/app_test.go new file mode 100644 index 00000000..62bf8019 --- /dev/null +++ b/internal/views/app_test.go @@ -0,0 +1,18 @@ +package views + +// import ( +// "testing" + +// "github.com/derailed/k9s/internal/config" +// "github.com/stretchr/testify/assert" +// ) + +// func TestNewApp(t *testing.T) { +// mk := NewMockKubeSettings() +// cfg := config.NewConfig(mk) +// a := NewApp(cfg) +// a.Init("blee", 10) + +// assert.Equal(t, 10, len(a.actions)) +// assert.Equal(t, false, a.hasSkins) +// } diff --git a/internal/views/cluster_info.go b/internal/views/cluster_info.go index c0e06558..43440fc0 100644 --- a/internal/views/cluster_info.go +++ b/internal/views/cluster_info.go @@ -39,7 +39,7 @@ func newClusterInfoView(app *appView, mx resource.MetricsServer) *clusterInfoVie } } -func (v *clusterInfoView) init() { +func (v *clusterInfoView) init(version string) { cluster := resource.NewCluster(v.app.conn(), &log.Logger, v.mxs) var row int @@ -56,7 +56,7 @@ func (v *clusterInfoView) init() { row++ v.SetCell(row, 0, v.sectionCell("K9s Rev")) - v.SetCell(row, 1, v.infoCell(v.app.version)) + v.SetCell(row, 1, v.infoCell(version)) row++ rev := cluster.Version() diff --git a/internal/views/cmd.go b/internal/views/cmd.go index deb5f5d9..f3fc6705 100644 --- a/internal/views/cmd.go +++ b/internal/views/cmd.go @@ -15,20 +15,20 @@ type cmdView struct { activated bool icon rune text string - app *appView + styles *config.Styles } -func newCmdView(app *appView, ic rune) *cmdView { - v := cmdView{app: app, icon: ic, TextView: tview.NewTextView()} +func newCmdView(styles *config.Styles, ic rune) *cmdView { + v := cmdView{styles: styles, icon: ic, TextView: tview.NewTextView()} { v.SetWordWrap(true) v.SetWrap(true) v.SetDynamicColors(true) v.SetBorder(true) v.SetBorderPadding(0, 0, 1, 1) - v.SetBackgroundColor(app.styles.BgColor()) - v.SetBorderColor(config.AsColor(app.styles.Style.Border.FocusColor)) - v.SetTextColor(app.styles.FgColor()) + v.SetBackgroundColor(styles.BgColor()) + v.SetBorderColor(config.AsColor(styles.Style.Border.FocusColor)) + v.SetTextColor(styles.FgColor()) } return &v } @@ -66,11 +66,11 @@ func (v *cmdView) active(f bool) { v.activated = f if f { v.SetBorder(true) - v.SetTextColor(v.app.styles.FgColor()) + v.SetTextColor(v.styles.FgColor()) v.activate() } else { v.SetBorder(false) - v.SetBackgroundColor(v.app.styles.BgColor()) + v.SetBackgroundColor(v.styles.BgColor()) v.Clear() } } diff --git a/internal/views/cmd_test.go b/internal/views/cmd_test.go new file mode 100644 index 00000000..28778eec --- /dev/null +++ b/internal/views/cmd_test.go @@ -0,0 +1,28 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdUpdate(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newCmdView(defaults, 'T') + v.update("blee") + + assert.Equal(t, "T> blee\n", v.GetText(false)) +} + +func TestNewCmdActivate(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newCmdView(defaults, 'T') + v.update("blee") + v.append('!') + + assert.Equal(t, "T> blee!\n", v.GetText(false)) + assert.False(t, v.inCmdMode()) + v.active(true) + assert.True(t, v.inCmdMode()) +} diff --git a/internal/views/command.go b/internal/views/command.go index 65d7e712..350e9296 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -28,12 +28,12 @@ func (c *command) lastCmd() bool { func (c *command) pushCmd(cmd string) { c.history.push(cmd) - c.app.crumbsView.update(c.history.stack) + c.app.crumbs().update(c.history.stack) } func (c *command) previousCmd() (string, bool) { c.history.pop() - c.app.crumbsView.update(c.history.stack) + c.app.crumbs().update(c.history.stack) return c.history.top() } @@ -56,10 +56,10 @@ func (c *command) run(cmd string) bool { c.app.BailOut() return true case cmd == "?", cmd == "help": - c.app.inject(newHelpView(c.app)) + c.app.inject(newHelpView(c.app, c.app.currentView())) return true case cmd == "alias": - c.app.inject(newAliasView(c.app)) + c.app.inject(newAliasView(c.app, c.app.currentView())) return true case policyMatcher.MatchString(cmd): tokens := policyMatcher.FindAllStringSubmatch(cmd, -1) diff --git a/internal/views/config.go b/internal/views/config.go new file mode 100644 index 00000000..14815ed7 --- /dev/null +++ b/internal/views/config.go @@ -0,0 +1,74 @@ +package views + +import ( + "context" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog/log" +) + +type synchronizer interface { + QueueUpdateDraw(func()) *tview.Application + QueueUpdate(func()) *tview.Application +} + +type configurator struct { + hasSkins bool + config *config.Config + styles *config.Styles + bench *config.Bench +} + +func (c *configurator) stylesUpdater(ctx context.Context, s synchronizer) error { + w, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + go func() { + for { + select { + case evt := <-w.Events: + _ = evt + s.QueueUpdateDraw(func() { + c.refreshStyles() + }) + case err := <-w.Errors: + log.Info().Err(err).Msg("Skin watcher failed") + return + case <-ctx.Done(): + w.Close() + return + } + } + }() + + return w.Add(config.K9sStylesFile) +} + +func (c *configurator) initBench(cluster string) { + var err error + if c.bench, err = config.NewBench(benchConfig(cluster)); err != nil { + log.Warn().Err(err).Msg("No benchmark config file found, using defaults.") + } +} + +func (c *configurator) refreshStyles() { + var err error + if c.styles, err = config.NewStyles(config.K9sStylesFile); err != nil { + log.Warn().Err(err).Msg("No skin file found. Loading defaults.") + } + if err == nil { + c.hasSkins = true + } + c.styles.Update() + + stdColor = config.AsColor(c.styles.Style.Status.NewColor) + addColor = config.AsColor(c.styles.Style.Status.AddColor) + modColor = config.AsColor(c.styles.Style.Status.ModifyColor) + errColor = config.AsColor(c.styles.Style.Status.ErrorColor) + highlightColor = config.AsColor(c.styles.Style.Status.HighlightColor) + completedColor = config.AsColor(c.styles.Style.Status.CompletedColor) +} diff --git a/internal/views/crumbs.go b/internal/views/crumbs.go index 637aa878..24816818 100644 --- a/internal/views/crumbs.go +++ b/internal/views/crumbs.go @@ -3,19 +3,20 @@ package views import ( "fmt" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" ) type crumbsView struct { *tview.TextView - app *appView + styles *config.Styles } -func newCrumbsView(app *appView) *crumbsView { - v := crumbsView{app: app, TextView: tview.NewTextView()} +func newCrumbsView(styles *config.Styles) *crumbsView { + v := crumbsView{styles: styles, TextView: tview.NewTextView()} { - v.SetBackgroundColor(app.styles.BgColor()) + v.SetBackgroundColor(styles.BgColor()) v.SetTextAlign(tview.AlignLeft) v.SetBorderPadding(0, 0, 1, 1) v.SetDynamicColors(true) @@ -26,14 +27,14 @@ func newCrumbsView(app *appView) *crumbsView { func (v *crumbsView) update(crumbs []string) { v.Clear() - last, bgColor := len(crumbs)-1, v.app.styles.Style.Crumb.BgColor + last, bgColor := len(crumbs)-1, v.styles.Style.Crumb.BgColor for i, c := range crumbs { if i == last { - bgColor = v.app.styles.Style.Crumb.ActiveColor + bgColor = v.styles.Style.Crumb.ActiveColor } fmt.Fprintf(v, "[%s:%s:b] <%s> [-:%s:-] ", - v.app.styles.Style.Crumb.FgColor, + v.styles.Style.Crumb.FgColor, bgColor, c, - v.app.styles.Style.BgColor) + v.styles.Style.BgColor) } } diff --git a/internal/views/crumbs_test.go b/internal/views/crumbs_test.go new file mode 100644 index 00000000..388a9fd0 --- /dev/null +++ b/internal/views/crumbs_test.go @@ -0,0 +1,16 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewCrumbs(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newCrumbsView(defaults) + v.update([]string{"blee", "duh"}) + + assert.Equal(t, "[black:aqua:b] [-:black:-] [black:orange:b] [-:black:-] \n", v.GetText(false)) +} diff --git a/internal/views/details.go b/internal/views/details.go index 92f77caf..73b0ca69 100644 --- a/internal/views/details.go +++ b/internal/views/details.go @@ -42,7 +42,7 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView { v.SetInputCapture(v.keyboard) v.cmdBuff = newCmdBuff('/') { - v.cmdBuff.addListener(app.cmdView) + v.cmdBuff.addListener(app.cmd()) v.cmdBuff.reset() } v.SetChangedFunc(func() { @@ -104,7 +104,7 @@ func (v *detailsView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *detailsView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if !v.app.cmdView.inCmdMode() { + if !v.app.inCmdMode() { v.cmdBuff.setActive(true) v.cmdBuff.clear() return nil diff --git a/internal/views/help.go b/internal/views/help.go index 548d4840..3bab42b8 100644 --- a/internal/views/help.go +++ b/internal/views/help.go @@ -22,16 +22,15 @@ type helpView struct { actions keyActions } -func newHelpView(app *appView) *helpView { +func newHelpView(app *appView, current igniter) *helpView { v := helpView{TextView: tview.NewTextView(), app: app, actions: make(keyActions)} - { - v.SetTextColor(tcell.ColorAqua) - v.SetBorder(true) - v.SetBorderPadding(0, 0, 1, 1) - v.SetDynamicColors(true) - v.SetInputCapture(v.keyboard) - v.current = app.content.GetPrimitive("main").(igniter) - } + v.SetTextColor(tcell.ColorAqua) + v.SetBorder(true) + v.SetBorderPadding(0, 0, 1, 1) + v.SetDynamicColors(true) + v.SetInputCapture(v.keyboard) + v.current = current + v.actions[tcell.KeyEsc] = newKeyAction("Back", v.backCmd, true) v.actions[tcell.KeyEnter] = newKeyAction("Back", v.backCmd, false) diff --git a/internal/views/help_test.go b/internal/views/help_test.go new file mode 100644 index 00000000..eb04665c --- /dev/null +++ b/internal/views/help_test.go @@ -0,0 +1,49 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ks struct{} + +func (k ks) CurrentContextName() (string, error) { + return "test", nil +} + +func (k ks) CurrentClusterName() (string, error) { + return "test", nil +} + +func (k ks) CurrentNamespaceName() (string, error) { + return "test", nil +} + +func (k ks) ClusterNames() ([]string, error) { + return []string{"test"}, nil +} + +func (k ks) NamespaceNames(nn []v1.Namespace) []string { + return []string{"test"} +} + +func newNS(n string) v1.Namespace { + return v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: n, + }} +} + +func TestNewHelpView(t *testing.T) { + cfg := config.NewConfig(ks{}) + a := NewApp(cfg) + v := newHelpView(a, nil) + v.init(nil, "") + + const e = "🏠 General\n : Command mode\n / Filter mode\n esc Clear filter\n tab Next term match\n backtab Previous term match\n Ctrl-r Refresh\n Shift-i Invert Sort\n p Previous resource view\n q Quit\n\n🤖 View Navigation\n g Goto Top\n G Goto Bottom\n Ctrl-b Page Down\n Ctrl-f Page Up\n h Left\n l Right\n k Up\n j Down\n️️\n😱 Help\n ? Help\n Ctrl-a Aliases view\n" + assert.Equal(t, e, v.GetText(true)) + assert.Equal(t, "Help", v.getTitle()) +} diff --git a/internal/views/helpers.go b/internal/views/helpers.go index e06c86b0..37249136 100644 --- a/internal/views/helpers.go +++ b/internal/views/helpers.go @@ -1,6 +1,7 @@ package views import ( + "path" "regexp" "strconv" "strings" @@ -33,6 +34,12 @@ func stripPort(p string) string { return p } +// Namespaced converts an fqn resource name to ns and name. +func namespaced(n string) (string, string) { + ns, po := path.Split(n) + return strings.Trim(ns, "/"), po +} + // ContainerID computes container ID based on ns/po/co. func containerID(path, co string) string { ns, n := namespaced(path) diff --git a/internal/views/helpers_test.go b/internal/views/helpers_test.go index 6f5bd487..88182dfd 100644 --- a/internal/views/helpers_test.go +++ b/internal/views/helpers_test.go @@ -13,6 +13,37 @@ func init() { zerolog.SetGlobalLevel(zerolog.Disabled) } +func TestIsTCPPort(t *testing.T) { + uu := map[string]struct { + p string + e bool + }{ + "tcp": {"80╱TCP", true}, + "udp": {"80╱UDP", false}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, isTCPPort(u.p)) + }) + } +} + +func TestFQN(t *testing.T) { + uu := map[string]struct { + ns, n, e string + }{ + "fullFQN": {"blee", "fred", "blee/fred"}, + "allNS": {"", "fred", "fred"}, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, fqn(u.ns, u.n)) + }) + } +} + func TestDeltas(t *testing.T) { uu := []struct { s1, s2, e string diff --git a/internal/views/log.go b/internal/views/log.go index 865f717f..b52186c7 100644 --- a/internal/views/log.go +++ b/internal/views/log.go @@ -19,8 +19,8 @@ type logView struct { *tview.Flex app *appView - logs *detailsView backFn actionHandler + logs *detailsView status *statusView ansiWriter io.Writer autoScroll int32 @@ -46,7 +46,7 @@ func newLogView(title string, app *appView, backFn actionHandler) *logView { v.logs.SetMaxBuffer(app.config.K9s.LogBufferSize) } v.ansiWriter = tview.ANSIWriter(v.logs) - v.status = newStatusView(app) + v.status = newStatusView(app.styles) v.SetDirection(tview.FlexRow) v.AddItem(v.status, 1, 1, false) v.AddItem(v.logs, 0, 1, true) @@ -88,11 +88,6 @@ func (v *logView) logLine(line string) { fmt.Fprintln(v.ansiWriter, tview.Escape(line)) } -func (v *logView) log(lines fmt.Stringer) { - v.logs.Clear() - fmt.Fprintln(v.ansiWriter, lines.String()) -} - func (v *logView) flush(index int, buff []string) { if index == 0 { return diff --git a/internal/views/log_test.go b/internal/views/log_test.go index 36aea07a..2584d9ef 100644 --- a/internal/views/log_test.go +++ b/internal/views/log_test.go @@ -3,8 +3,11 @@ package views import ( "bytes" "fmt" + "io/ioutil" + "path/filepath" "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) @@ -22,3 +25,58 @@ func TestAnsi(t *testing.T) { fmt.Fprintf(aw, s) assert.Equal(t, s+"\n", v.GetText(false)) } + +func TestLogViewFlush(t *testing.T) { + v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) + v.flush(2, []string{"blee", "bozo"}) + + v.toggleScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) + assert.Equal(t, " Autoscroll: Off ", v.status.GetText(true)) + v.toggleScrollCmd(nil) + assert.Equal(t, " Autoscroll: On ", v.status.GetText(true)) +} + +func TestLogViewSave(t *testing.T) { + v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) + v.flush(2, []string{"blee", "bozo"}) + v.path = "k9s-test" + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + c1, _ := ioutil.ReadDir(dir) + v.saveCmd(nil) + c2, _ := ioutil.ReadDir(dir) + assert.Equal(t, len(c2), len(c1)+1) +} + +func TestLogViewNav(t *testing.T) { + v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) + var buff []string + v.autoScroll = 1 + for i := 0; i < 100; i++ { + buff = append(buff, fmt.Sprintf("line-%d\n", i)) + } + v.flush(100, buff) + + v.topCmd(nil) + r, _ := v.logs.GetScrollOffset() + assert.Equal(t, 0, r) + v.pageDownCmd(nil) + r, _ = v.logs.GetScrollOffset() + assert.Equal(t, 0, r) + v.pageUpCmd(nil) + r, _ = v.logs.GetScrollOffset() + assert.Equal(t, 0, r) + v.bottomCmd(nil) + r, _ = v.logs.GetScrollOffset() + assert.Equal(t, 0, r) +} + +func TestLogViewClear(t *testing.T) { + v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil) + v.flush(2, []string{"blee", "bozo"}) + + v.toggleScrollCmd(nil) + assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true)) + v.clearCmd(nil) + assert.Equal(t, "", v.logs.GetText(true)) +} diff --git a/internal/views/logo.go b/internal/views/logo.go index 8f2e5a5f..f037bdcb 100644 --- a/internal/views/logo.go +++ b/internal/views/logo.go @@ -10,28 +10,28 @@ import ( type logoView struct { *tview.Flex logo, status *tview.TextView - app *appView + styles *config.Styles } -func newLogoView(app *appView) *logoView { +func newLogoView(styles *config.Styles) *logoView { v := logoView{ Flex: tview.NewFlex(), logo: logo(), status: status(), - app: app, + styles: styles, } v.SetDirection(tview.FlexRow) v.AddItem(v.logo, 0, 6, false) v.AddItem(v.status, 0, 1, false) - v.refreshLogo(app.styles.Style.LogoColor) + v.refreshLogo(styles.Style.LogoColor) return &v } func (v *logoView) reset() { v.status.Clear() - v.status.SetBackgroundColor(v.app.styles.BgColor()) - v.refreshLogo(v.app.styles.Style.LogoColor) + v.status.SetBackgroundColor(v.styles.BgColor()) + v.refreshLogo(v.styles.Style.LogoColor) } func (v *logoView) err(msg string) { diff --git a/internal/views/logo_test.go b/internal/views/logo_test.go new file mode 100644 index 00000000..ef3d1c27 --- /dev/null +++ b/internal/views/logo_test.go @@ -0,0 +1,58 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewLogoView(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newLogoView(defaults) + 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)) +} + +func TestLogoStatus(t *testing.T) { + uu := map[string]struct { + logo, msg, e string + }{ + "info": { + "[green::b] ____ __.________ \n[green::b]| |/ _/ __ \\______\n[green::b]| < \\____ / ___/\n[green::b]| | \\ / /\\___ \\ \n[green::b]|____|__ \\ /____//____ >\n[green::b] \\/ \\/ \n", + "blee", + "[white::b]blee\n", + }, + "warn": { + "[mediumvioletred::b] ____ __.________ \n[mediumvioletred::b]| |/ _/ __ \\______\n[mediumvioletred::b]| < \\____ / ___/\n[mediumvioletred::b]| | \\ / /\\___ \\ \n[mediumvioletred::b]|____|__ \\ /____//____ >\n[mediumvioletred::b] \\/ \\/ \n", + "blee", + "[white::b]blee\n", + }, + "err": { + "[red::b] ____ __.________ \n[red::b]| |/ _/ __ \\______\n[red::b]| < \\____ / ___/\n[red::b]| | \\ / /\\___ \\ \n[red::b]|____|__ \\ /____//____ >\n[red::b] \\/ \\/ \n", + "blee", + "[white::b]blee\n", + }, + } + + defaults, _ := config.NewStyles("") + v := newLogoView(defaults) + for k, u := range uu { + t.Run(k, func(t *testing.T) { + switch k { + case "info": + v.info(u.msg) + case "warn": + v.warn(u.msg) + case "err": + v.err(u.msg) + } + assert.Equal(t, u.logo, v.logo.GetText(false)) + assert.Equal(t, u.e, v.status.GetText(false)) + }) + } + +} diff --git a/internal/views/menu.go b/internal/views/menu.go index 33d46c0f..d2add2ed 100644 --- a/internal/views/menu.go +++ b/internal/views/menu.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" "github.com/gdamore/tcell" @@ -100,12 +101,12 @@ func (a keyActions) toHints() hints { type menuView struct { *tview.Table - app *appView + styles *config.Styles } -func newMenuView(app *appView) *menuView { - v := menuView{Table: tview.NewTable(), app: app} - v.SetBackgroundColor(app.styles.BgColor()) +func newMenuView(styles *config.Styles) *menuView { + v := menuView{Table: tview.NewTable(), styles: styles} + v.SetBackgroundColor(styles.BgColor()) return &v } @@ -120,7 +121,7 @@ func (v *menuView) populateMenu(hh hints) { continue } c := tview.NewTableCell(t[row][col]) - c.SetBackgroundColor(v.app.styles.BgColor()) + c.SetBackgroundColor(v.styles.BgColor()) v.SetCell(row, col, c) } } @@ -177,16 +178,16 @@ func (*menuView) toMnemonic(s string) string { func (v *menuView) formatMenu(h hint, size int) string { i, err := strconv.Atoi(h.mnemonic) if err == nil { - fmat := strings.Replace(menuIndexFmt, "[key", "["+v.app.styles.Style.Menu.NumKeyColor, 1) - fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1) - fmat = strings.Replace(fmat, "[fg", "["+v.app.styles.Style.Menu.FgColor, 1) + fmat := strings.Replace(menuIndexFmt, "[key", "["+v.styles.Style.Menu.NumKeyColor, 1) + fmat = strings.Replace(fmat, ":bg:", ":"+v.styles.Style.Title.BgColor+":", -1) + fmat = strings.Replace(fmat, "[fg", "["+v.styles.Style.Menu.FgColor, 1) return fmt.Sprintf(fmat, i, resource.Truncate(h.description, 14)) } menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s " - fmat := strings.Replace(menuFmt, "[key", "["+v.app.styles.Style.Menu.KeyColor, 1) - fmat = strings.Replace(fmat, "[fg", "["+v.app.styles.Style.Menu.FgColor, 1) - fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1) + fmat := strings.Replace(menuFmt, "[key", "["+v.styles.Style.Menu.KeyColor, 1) + fmat = strings.Replace(fmat, "[fg", "["+v.styles.Style.Menu.FgColor, 1) + fmat = strings.Replace(fmat, ":bg:", ":"+v.styles.Style.Title.BgColor+":", -1) return fmt.Sprintf(fmat, v.toMnemonic(h.mnemonic), h.description) } diff --git a/internal/views/menu_test.go b/internal/views/menu_test.go new file mode 100644 index 00000000..8b64c257 --- /dev/null +++ b/internal/views/menu_test.go @@ -0,0 +1,50 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" +) + +func TestNewMenuView(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newMenuView(defaults) + v.populateMenu(hints{ + {"a", "bleeA"}, + {"b", "bleeB"}, + {"0", "zero"}, + }) + + assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text) + assert.Equal(t, " [dodgerblue:black:b] [white:black:d]bleeA ", v.GetCell(0, 1).Text) + assert.Equal(t, " [dodgerblue:black:b] [white:black:d]bleeB ", v.GetCell(1, 1).Text) +} + +func TestKeyActions(t *testing.T) { + uu := map[string]struct { + aa keyActions + e hints + }{ + "a": { + aa: keyActions{ + KeyB: newKeyAction("bleeB", nil, true), + KeyA: newKeyAction("bleeA", nil, true), + tcell.Key(Key0): newKeyAction("zero", nil, true), + tcell.Key(Key1): newKeyAction("one", nil, false), + }, + e: hints{ + {"0", "zero"}, + {"a", "bleeA"}, + {"b", "bleeB"}, + }, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.aa.toHints()) + }) + } +} diff --git a/internal/views/mock_connection.go b/internal/views/mock_connection.go index 2f12993f..66f8a948 100644 --- a/internal/views/mock_connection.go +++ b/internal/views/mock_connection.go @@ -1,5 +1,5 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/k8s (interfaces: Connection) +// Source: github.com/derailed/k9s/internal/config (interfaces: Connection) package views @@ -24,19 +24,23 @@ func NewMockConnection() *MockConnection { return &MockConnection{fail: pegomock.GlobalFailHandler} } -func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) bool { +func (mock *MockConnection) CanIAccess(_param0 string, _param1 string, _param2 string, _param3 []string) (bool, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockConnection().") } params := []pegomock.Param{_param0, _param1, _param2, _param3} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("CanIAccess", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 bool + var ret1 error if len(result) != 0 { if result[0] != nil { ret0 = result[0].(bool) } + if result[1] != nil { + ret1 = result[1].(error) + } } - return ret0 + return ret0, ret1 } func (mock *MockConnection) Config() *k8s.Config { @@ -239,14 +243,15 @@ func (mock *MockConnection) ServerVersion() (*version.Info, error) { return ret0, ret1 } -func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) { +func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockConnection().") } params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 string var ret1 bool + var ret2 error if len(result) != 0 { if result[0] != nil { ret0 = result[0].(string) @@ -254,8 +259,11 @@ func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (strin if result[1] != nil { ret1 = result[1].(bool) } + if result[2] != nil { + ret2 = result[2].(error) + } } - return ret0, ret1 + return ret0, ret1, ret2 } func (mock *MockConnection) SupportsResource(_param0 string) bool { diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 8810835c..9584077d 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -41,6 +41,9 @@ func helpCmds(c k8s.Connection) map[string]resCmd { func allCRDs(c k8s.Connection) map[string]k8s.APIGroup { m := map[string]k8s.APIGroup{} + if c == nil { + return m + } crds, _ := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces). Resource(). @@ -310,6 +313,9 @@ func resourceViews(c k8s.Connection) map[string]resCmd { }, } + if c == nil { + return cmds + } rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"}) if err != nil { log.Error().Err(err).Msg("Checking HPA") diff --git a/internal/views/resource.go b/internal/views/resource.go index 9f7400d4..fc12d269 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -118,7 +118,7 @@ func (v *resourceView) init(ctx context.Context, ns string) { } v.update(vctx) - v.app.clusterInfoView.refresh() + v.app.clusterInfoRefresh() v.refresh() if tv, ok := v.CurrentPage().Item.(*tableView); ok { r, _ := tv.GetSelection() @@ -275,7 +275,7 @@ func (v *resourceView) dismissModal() { } func (v *resourceView) defaultEnter(ns, resource, selection string) { - yaml, err := v.list.Resource().Describe(v.title, selection, v.app.flags) + yaml, err := v.list.Resource().Describe(v.title, selection) if err != nil { v.app.flash().errf("Describe command failed %s", err) log.Warn().Msgf("Describe %v", err.Error()) @@ -446,11 +446,6 @@ func (v *resourceView) rowSelected() bool { return v.selectedItem != noSelection } -func namespaced(n string) (string, string) { - ns, po := path.Split(n) - return strings.Trim(ns, "/"), po -} - func (v *resourceView) refreshActions() { if v.list.Access(resource.NamespaceAccess) { v.namespaces = make(map[int]string, config.MaxFavoritesNS) diff --git a/internal/views/resource_test.go b/internal/views/resource_test.go new file mode 100644 index 00000000..93acdf8a --- /dev/null +++ b/internal/views/resource_test.go @@ -0,0 +1,23 @@ +package views + +// import ( +// "context" +// "testing" + +// "github.com/derailed/k9s/internal/config" +// "github.com/derailed/k9s/internal/resource" +// ) + +// func TestNewResource(t *testing.T) { +// mc := NewMockConnection() +// mk := NewMockKubeSettings() + +// c := config.NewConfig(mk) +// c.SetConnection(mc) +// a := NewApp(c) +// l := resource.NewPodList(nil, "") +// v := newResourceView("fred", a, l) + +// ctx, _ := context.WithCancel(context.Background()) +// v.init(ctx, "") +// } diff --git a/internal/views/shell.go b/internal/views/shell.go new file mode 100644 index 00000000..f332e5f1 --- /dev/null +++ b/internal/views/shell.go @@ -0,0 +1,34 @@ +package views + +import ( + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +type ( + keyHandler interface { + keyboard(evt *tcell.EventKey) *tcell.EventKey + } + + actionsFn func(keyActions) + + shellView struct { + *tview.Application + configurator + + actions keyActions + pages *tview.Pages + content *tview.Pages + views map[string]tview.Primitive + } +) + +func newShellView() *shellView { + return &shellView{ + Application: tview.NewApplication(), + actions: make(keyActions), + pages: tview.NewPages(), + content: tview.NewPages(), + views: make(map[string]tview.Primitive), + } +} diff --git a/internal/views/sorter.go b/internal/views/sorter.go index cd9dd212..39f9d8e0 100644 --- a/internal/views/sorter.go +++ b/internal/views/sorter.go @@ -1,7 +1,6 @@ package views import ( - "regexp" "strconv" "time" @@ -58,6 +57,10 @@ func less(asc bool, c1, c2 string) bool { return true } + if o, ok := isIntegerSort(asc, c1, c2); ok { + return o + } + if o, ok := isMetricSort(asc, c1, c2); ok { return o } @@ -66,10 +69,6 @@ func less(asc bool, c1, c2 string) bool { return o } - if o, ok := isIntegerSort(asc, c1, c2); ok { - return o - } - b := sortorder.NaturalLess(c1, c2) if asc { return b @@ -104,26 +103,18 @@ func isMetricSort(asc bool, c1, c2 string) (bool, bool) { } func isIntegerSort(asc bool, c1, c2 string) (bool, bool) { - n1, err := strconv.Atoi(c1) - if err != nil { + n1, err1 := strconv.Atoi(c1) + n2, err2 := strconv.Atoi(c2) + if err1 != nil || err2 != nil { return false, false } - n2, _ := strconv.Atoi(c2) + if asc { return n1 <= n2, true } return n1 > n2, true } -var metricRX = regexp.MustCompile(`\A(\d+)(m|Mi)\z`) - -func isMetric(s string) (string, bool) { - if m := metricRX.FindStringSubmatch(s); len(m) == 3 { - return m[1], true - } - return s, false -} - func isDuration(s string) (time.Duration, bool) { d, err := time.ParseDuration(s) if err != nil { diff --git a/internal/views/splash.go b/internal/views/splash.go index b045bc90..5790f876 100644 --- a/internal/views/splash.go +++ b/internal/views/splash.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -36,44 +37,39 @@ var Logo = []string{ // Splash screen definition type splashView struct { *tview.Flex - - app *appView } // NewSplash instantiates a new splash screen with product and company info. -func newSplash(app *appView) *splashView { - v := splashView{Flex: tview.NewFlex(), app: app} +func newSplash(styles *config.Styles, version string) *splashView { + v := splashView{Flex: tview.NewFlex()} logo := tview.NewTextView() - { - logo.SetDynamicColors(true) - logo.SetBackgroundColor(tcell.ColorDefault) - logo.SetTextAlign(tview.AlignCenter) - } - v.layoutLogo(logo) + logo.SetDynamicColors(true) + logo.SetBackgroundColor(tcell.ColorDefault) + logo.SetTextAlign(tview.AlignCenter) + v.layoutLogo(logo, styles) vers := tview.NewTextView() - { - vers.SetDynamicColors(true) - vers.SetBackgroundColor(tcell.ColorDefault) - vers.SetTextAlign(tview.AlignCenter) - } - v.layoutRev(vers, app.version) + vers.SetDynamicColors(true) + vers.SetBackgroundColor(tcell.ColorDefault) + vers.SetTextAlign(tview.AlignCenter) + v.layoutRev(vers, version, styles) v.SetDirection(tview.FlexRow) v.AddItem(logo, 10, 1, false) v.AddItem(vers, 1, 1, false) + return &v } -func (v *splashView) layoutLogo(t *tview.TextView) { - logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", v.app.styles.Style.LogoColor)) +func (v *splashView) layoutLogo(t *tview.TextView, styles *config.Styles) { + logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", styles.Style.LogoColor)) fmt.Fprintf(t, "%s[%s::b]%s\n", strings.Repeat("\n", 2), - v.app.styles.Style.LogoColor, + styles.Style.LogoColor, logo) } -func (v *splashView) layoutRev(t *tview.TextView, rev string) { - fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", v.app.styles.Style.FgColor, rev) +func (v *splashView) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { + fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", styles.Style.FgColor, rev) } diff --git a/internal/views/splash_test.go b/internal/views/splash_test.go new file mode 100644 index 00000000..4049bfa8 --- /dev/null +++ b/internal/views/splash_test.go @@ -0,0 +1,19 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewSplash(t *testing.T) { + defaults, _ := config.NewStyles("") + s := newSplash(defaults, "bozo") + + x, y, w, h := s.GetRect() + assert.Equal(t, 0, x) + assert.Equal(t, 0, y) + assert.Equal(t, 15, w) + assert.Equal(t, 10, h) +} diff --git a/internal/views/status.go b/internal/views/status.go index 267a3339..c0d876ba 100644 --- a/internal/views/status.go +++ b/internal/views/status.go @@ -10,13 +10,13 @@ import ( type statusView struct { *tview.TextView - app *appView + styles *config.Styles } -func newStatusView(app *appView) *statusView { - v := statusView{app: app, TextView: tview.NewTextView()} +func newStatusView(styles *config.Styles) *statusView { + v := statusView{styles: styles, TextView: tview.NewTextView()} { - v.SetBackgroundColor(config.AsColor(app.styles.Style.Log.BgColor)) + v.SetBackgroundColor(config.AsColor(styles.Style.Log.BgColor)) v.SetTextAlign(tview.AlignRight) v.SetDynamicColors(true) } @@ -25,14 +25,14 @@ func newStatusView(app *appView) *statusView { func (v *statusView) update(status []string) { v.Clear() - last, bgColor := len(status)-1, v.app.styles.Style.Crumb.BgColor + last, bgColor := len(status)-1, v.styles.Style.Crumb.BgColor for i, c := range status { if i == last { - bgColor = v.app.styles.Style.Crumb.ActiveColor + bgColor = v.styles.Style.Crumb.ActiveColor } fmt.Fprintf(v, "[%s:%s:b] %s [-:%s:-] ", - v.app.styles.Style.Crumb.FgColor, + v.styles.Style.Crumb.FgColor, bgColor, c, - v.app.styles.Style.BgColor) + v.styles.Style.BgColor) } } diff --git a/internal/views/status_test.go b/internal/views/status_test.go new file mode 100644 index 00000000..4cd67d5c --- /dev/null +++ b/internal/views/status_test.go @@ -0,0 +1,16 @@ +package views + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewStatus(t *testing.T) { + defaults, _ := config.NewStyles("") + v := newStatusView(defaults) + v.update([]string{"blee", "duh"}) + + assert.Equal(t, "[black:aqua:b] blee [-:black:-] [black:orange:b] duh [-:black:-] \n", v.GetText(false)) +} diff --git a/internal/views/subject.go b/internal/views/subject.go index 9bb2a597..da3ec10c 100644 --- a/internal/views/subject.go +++ b/internal/views/subject.go @@ -81,12 +81,10 @@ func (v *subjectView) init(c context.Context, _ string) { v.app.SetFocus(v) } -func (v *subjectView) setExtraActionsFn(f actionsFn) { -} - -func (v *subjectView) setColorerFn(f colorerFn) {} -func (v *subjectView) setEnterFn(f enterFn) {} -func (v *subjectView) setDecorateFn(f decorateFn) {} +func (v *subjectView) setExtraActionsFn(f actionsFn) {} +func (v *subjectView) setColorerFn(f colorerFn) {} +func (v *subjectView) setEnterFn(f enterFn) {} +func (v *subjectView) setDecorateFn(f decorateFn) {} func (v *subjectView) bindKeys() { // No time data or ns @@ -97,7 +95,6 @@ func (v *subjectView) bindKeys() { v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false) v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false) v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) - v.actions[KeyShiftK] = newKeyAction("Sort Kind", v.sortColCmd(1), true) } diff --git a/internal/views/table.go b/internal/views/table.go index 7578622b..3e7b4c66 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -76,7 +76,7 @@ func newTableView(app *appView, title string) *tableView { v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor)) v.SetBorderAttributes(tcell.AttrBold) v.SetBorderPadding(0, 0, 1, 1) - v.cmdBuff.addListener(app.cmdView) + v.cmdBuff.addListener(app.cmd()) v.cmdBuff.reset() v.SetSelectable(true, false) v.SetSelectedStyle( @@ -265,7 +265,7 @@ func (v *tableView) sortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if v.app.cmdView.inCmdMode() { + if v.app.inCmdMode() { return evt } diff --git a/internal/views/utils.go b/internal/views/utils.go deleted file mode 100644 index a10a9225..00000000 --- a/internal/views/utils.go +++ /dev/null @@ -1,107 +0,0 @@ -package views - -import ( - "fmt" - "regexp" - "strings" -) - -const newLogColor = "greenyellow" - -type ( - logBuffer struct { - capacity int - decorate bool - modified bool - head *logEntry - current *logEntry - rx *regexp.Regexp - } - - logEntry struct { - line string - next *logEntry - } -) - -func newLogBuffer(c int, f bool) *logBuffer { - return &logBuffer{capacity: c, decorate: f, rx: regexp.MustCompile(`\[\w*\:\:\]`)} -} - -func (b *logBuffer) clear() { - b.head, b.current = nil, nil -} - -func (b *logBuffer) add(line string) { - b.modified = true - if b.decorate { - line = b.decorateLine(line) - } - n := logEntry{line: line} - if b.head == nil { - b.head = &n - b.current = b.head - return - } - - if b.full() { - b.head = b.head.next - } - b.current.next = &n - b.current = &n -} - -func (b *logBuffer) full() bool { - return b.length() == b.capacity -} - -func (b *logBuffer) length() int { - c, count := b.head, 0 - for c != nil { - c = c.next - count++ - } - return count -} - -func (*logBuffer) decorateLine(l string) string { - return l -} - -func (b *logBuffer) trimLine(l string) string { - return b.rx.ReplaceAllString(l, "") -} - -func (b *logBuffer) cleanse() { - if !b.modified { - return - } - c := b.head - for c != nil { - c.line = b.trimLine(c.line) - c = c.next - } - b.modified = true -} - -func (b *logBuffer) String() string { - return strings.Join(b.lines(), "\n") -} - -func (b *logBuffer) lines() []string { - out := make([]string, b.length()) - c := b.head - for i := 0; c != nil; i++ { - out[i] = c.line - c = c.next - } - return out -} - -func (b *logBuffer) dump() { - c := b.head - for c != nil { - fmt.Println(c.line) - c = c.next - } -} diff --git a/internal/views/utils_test.go b/internal/views/utils_test.go deleted file mode 100644 index 6618e68a..00000000 --- a/internal/views/utils_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package views - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLogBufferAdd(t *testing.T) { - uu := []struct { - lines []string - expected []string - }{ - {[]string{}, []string{}}, - {[]string{"l1"}, []string{"l1"}}, - {[]string{"l1", "l2"}, []string{"l1", "l2"}}, - {[]string{"l1", "l2", "l3"}, []string{"l2", "l3"}}, - {[]string{"l1", "l2", "l3", "l4"}, []string{"l3", "l4"}}, - } - - for _, u := range uu { - b := newLogBuffer(2, false) - for _, l := range u.lines { - b.add(l) - } - - assert.Equal(t, len(u.expected), b.length()) - assert.Equal(t, u.expected, b.lines()) - } -} - -func TestLogBufferCleanse(t *testing.T) { - b := newLogBuffer(2, true) - ll := []string{"l1", "l2"} - ee := []string{b.decorateLine("l1"), b.decorateLine("l2")} - for _, l := range ll { - b.add(l) - } - assert.Equal(t, ee, b.lines()) - b.cleanse() - assert.Equal(t, ll, b.lines()) -} - -func TestLogBufferDecorate(t *testing.T) { - l := "hello k9s" - var b *logBuffer - assert.Equal(t, l, b.decorateLine(l)) -} - -func TestLogBufferTrimLine(t *testing.T) { - l := "hello k9s" - dl := "[" + newLogColor + "::]" + l + "[::]" - b := newLogBuffer(1, true) - assert.Equal(t, l, b.trimLine(dl)) -} diff --git a/internal/views/yaml_test.go b/internal/views/yaml_test.go index 82c8e658..096211fb 100644 --- a/internal/views/yaml_test.go +++ b/internal/views/yaml_test.go @@ -41,7 +41,7 @@ func TestYaml(t *testing.T) { }, } - s, _ := config.NewStyles(config.K9sStylesFile) + s, _ := config.NewStyles("skins/stock.yml") for _, u := range uu { assert.Equal(t, u.e, colorizeYAML(s.Style, u.s)) }