test pass

mine
derailed 2019-06-18 14:11:26 -06:00
parent 4601d95e0f
commit a7e4d890f3
37 changed files with 704 additions and 481 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

74
internal/views/config.go Normal file
View File

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

View File

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

View File

@ -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] <blee> [-:black:-] [black:orange:b] <duh> [-:black:-] \n", v.GetText(false))
}

View File

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

View File

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

View File

@ -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 :<cmd> Command mode\n /<term> 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())
}

View File

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

View File

@ -13,6 +13,37 @@ func init() {
zerolog.SetGlobalLevel(zerolog.Disabled)
}
func TestIsTCPPort(t *testing.T) {
uu := map[string]struct {
p string
e bool
}{
"tcp": {"80TCP", true},
"udp": {"80UDP", 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]<a> [white:black:d]bleeA ", v.GetCell(0, 1).Text)
assert.Equal(t, " [dodgerblue:black:b]<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())
})
}
}

View File

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

View File

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

View File

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

View File

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

34
internal/views/shell.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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