bug fixes + cleanup

mine
derailed 2019-10-02 14:52:15 -06:00
parent 8778305bf2
commit ffd1c61c8c
29 changed files with 253 additions and 140 deletions

View File

@ -8,7 +8,7 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// K9sAlias stores K9s command aliases. // K9sAlias manages K9s aliases.
var K9sAlias = filepath.Join(K9sHome, "alias.yml") var K9sAlias = filepath.Join(K9sHome, "alias.yml")
type Alias map[string]string type Alias map[string]string
@ -33,7 +33,7 @@ func (a Aliases) loadDefaults() {
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings" a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles" a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
a.Alias["rob"] = "rbac.authorization.k8s.io/v1/rolebindings" a.Alias["rob"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1beta1/rolebindings" a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
{ {
a.Alias["ctx"] = "contexts" a.Alias["ctx"] = "contexts"
a.Alias["contexts"] = "contexts" a.Alias["contexts"] = "contexts"
@ -87,7 +87,7 @@ func (a Aliases) Define(args ...string) {
} }
} }
// LoadAliases K9s alias from a given file. // LoadAliases loads alias from a given file.
func (a Aliases) LoadAliases(path string) error { func (a Aliases) LoadAliases(path string) error {
f, err := ioutil.ReadFile(path) f, err := ioutil.ReadFile(path)
if err != nil { if err != nil {

View File

@ -7,20 +7,42 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAliasesLoad(t *testing.T) { func TestAliasDefine(t *testing.T) {
aa := config.NewAliases() uu := map[string]struct {
assert.Nil(t, aa.LoadAliases("test_assets/alias.yml")) aa []string
}{
"one": {[]string{"blee", "duh"}},
"multi": {[]string{"blee", "duh", "fred", "zorg"}},
}
assert.Equal(t, 27, len(aa.Alias)) for k, u := range uu {
t.Run(k, func(t *testing.T) {
a := config.NewAliases()
a.Define(u.aa...)
for i := 0; i < len(u.aa); i += 2 {
v, ok := a.Get(u.aa[i])
assert.True(t, ok)
assert.Equal(t, u.aa[i+1], v)
}
})
}
}
func TestAliasesLoad(t *testing.T) {
a := config.NewAliases()
assert.Nil(t, a.LoadAliases("test_assets/alias.yml"))
assert.Equal(t, 27, len(a.Alias))
} }
func TestAliasesSave(t *testing.T) { func TestAliasesSave(t *testing.T) {
aa := config.NewAliases() a := config.NewAliases()
aa.Alias["test"] = "fred" a.Alias["test"] = "fred"
aa.Alias["blee"] = "duh" a.Alias["blee"] = "duh"
aa.SaveAliases("/tmp/a.yml") a.SaveAliases("/tmp/a.yml")
assert.Nil(t, aa.LoadAliases("/tmp/a.yml")) assert.Nil(t, a.LoadAliases("/tmp/a.yml"))
assert.Equal(t, 28, len(aa.Alias)) assert.Equal(t, 28, len(a.Alias))
} }

View File

@ -100,7 +100,6 @@ func TestConfigLoad(t *testing.T) {
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster) assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
assert.NotNil(t, cfg.K9s.Clusters) assert.NotNil(t, cfg.K9s.Clusters)
assert.Equal(t, 2, len(cfg.K9s.Clusters)) assert.Equal(t, 2, len(cfg.K9s.Clusters))
assert.Equal(t, 1, len(cfg.K9s.Plugins))
nn := []string{ nn := []string{
"default", "default",
@ -294,19 +293,6 @@ var expectedConfig = `k9s:
- kube-system - kube-system
view: view:
active: ctx active: ctx
plugins:
blah:
shortCut: shift-s
scopes:
- po
- dp
description: blee
command: duh
background: false
args:
- -n
- $NAMESPACE
- -boolean
` `
var resetConfig = `k9s: var resetConfig = `k9s:
@ -324,17 +310,4 @@ var resetConfig = `k9s:
- default - default
view: view:
active: po active: po
plugins:
blah:
shortCut: shift-s
scopes:
- po
- dp
description: blee
command: duh
background: false
args:
- -n
- $NAMESPACE
- -boolean
` `

View File

@ -19,7 +19,6 @@ type K9s struct {
CurrentContext string `yaml:"currentContext"` CurrentContext string `yaml:"currentContext"`
CurrentCluster string `yaml:"currentCluster"` CurrentCluster string `yaml:"currentCluster"`
Clusters map[string]*Cluster `yaml:"clusters,omitempty"` Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
Plugins map[string]*Plugin `yaml:"plugins,omitempty"`
manualRefreshRate int manualRefreshRate int
manualHeadless *bool manualHeadless *bool
manualCommand *string manualCommand *string
@ -32,7 +31,6 @@ func NewK9s() *K9s {
LogBufferSize: defaultLogBufferSize, LogBufferSize: defaultLogBufferSize,
LogRequestSize: defaultLogRequestSize, LogRequestSize: defaultLogRequestSize,
Clusters: make(map[string]*Cluster), Clusters: make(map[string]*Cluster),
Plugins: make(map[string]*Plugin),
} }
} }

View File

@ -1,5 +1,20 @@
package config package config
import (
"io/ioutil"
"path/filepath"
"gopkg.in/yaml.v2"
)
// K9sPlugins manages K9s plugins.
var K9sPlugins = filepath.Join(K9sHome, "plugin.yml")
// Plugins represents a collection of plugins.
type Plugins struct {
Plugin map[string]Plugin `yaml:"plugin"`
}
// Plugin describes a K9s plugin // Plugin describes a K9s plugin
type Plugin struct { type Plugin struct {
ShortCut string `yaml:"shortCut"` ShortCut string `yaml:"shortCut"`
@ -9,3 +24,33 @@ type Plugin struct {
Background bool `yaml:"background"` Background bool `yaml:"background"`
Args []string `yaml:"args"` Args []string `yaml:"args"`
} }
// NewPlugins returns a new plugin.
func NewPlugins() Plugins {
return Plugins{
Plugin: make(map[string]Plugin),
}
}
// Load K9s plugins.
func (p Plugins) Load() error {
return p.LoadPlugins(K9sPlugins)
}
// LoadPlugins loads plugins from a given file.
func (p Plugins) LoadPlugins(path string) error {
f, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var pp Plugins
if err := yaml.Unmarshal(f, &pp); err != nil {
return err
}
for k, v := range pp.Plugin {
p.Plugin[k] = v
}
return nil
}

View File

@ -0,0 +1,15 @@
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestPluginLoad(t *testing.T) {
p := config.NewPlugins()
assert.Nil(t, p.LoadPlugins("test_assets/plugin.yml"))
assert.Equal(t, 1, len(p.Plugin))
}

View File

@ -197,7 +197,7 @@ func newTitle() Title {
BgColor: "black", BgColor: "black",
HighlightColor: "fuchsia", HighlightColor: "fuchsia",
CounterColor: "papayawhip", CounterColor: "papayawhip",
FilterColor: "orange", FilterColor: "seagreen",
} }
} }

View File

@ -27,15 +27,3 @@ k9s:
- kube-system - kube-system
view: view:
active: po active: po
plugins:
blah:
shortCut: shift-s
description: blee
scopes:
- po
- dp
command: duh
args:
- -n
- $NAMESPACE
- -boolean

View File

@ -0,0 +1,12 @@
plugin:
blah:
shortCut: shift-s
description: blee
scopes:
- po
- dp
command: duh
args:
- -n
- $NAMESPACE
- -boolean

View File

@ -16,7 +16,6 @@ import (
var ( var (
// RestMapping holds k8s resource mapping // RestMapping holds k8s resource mapping
// BOZO!! Has to be a better way...
RestMapping = &RestMapper{} RestMapping = &RestMapper{}
toFileName = regexp.MustCompile(`[^(\w/\.)]`) toFileName = regexp.MustCompile(`[^(\w/\.)]`)
) )

View File

@ -54,7 +54,7 @@ func NewApp() *App {
actions: make(KeyActions), actions: make(KeyActions),
pages: tview.NewPages(), pages: tview.NewPages(),
content: tview.NewPages(), content: tview.NewPages(),
cmdBuff: NewCmdBuff(':'), cmdBuff: NewCmdBuff(':', CommandBuff),
} }
s.RefreshStyles() s.RefreshStyles()
@ -62,7 +62,7 @@ func NewApp() *App {
s.views = map[string]tview.Primitive{ s.views = map[string]tview.Primitive{
"menu": NewMenuView(s.Styles), "menu": NewMenuView(s.Styles),
"logo": NewLogoView(s.Styles), "logo": NewLogoView(s.Styles),
"cmd": NewCmdView(s.Styles, '🐶'), "cmd": NewCmdView(s.Styles),
"crumbs": NewCrumbsView(s.Styles), "crumbs": NewCrumbsView(s.Styles),
} }
@ -186,7 +186,6 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.InCmdMode() { if a.InCmdMode() {
return evt return evt
} }
a.Flash().Info("Command mode activated.")
a.cmdBuff.SetActive(true) a.cmdBuff.SetActive(true)
a.cmdBuff.Clear() a.cmdBuff.Clear()

View File

@ -5,6 +5,7 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell"
) )
const defaultPrompt = "%c> %s" const defaultPrompt = "%c> %s"
@ -20,8 +21,8 @@ type CmdView struct {
} }
// NewCmdView returns a new command view. // NewCmdView returns a new command view.
func NewCmdView(styles *config.Styles, ic rune) *CmdView { func NewCmdView(styles *config.Styles) *CmdView {
v := CmdView{styles: styles, icon: ic, TextView: tview.NewTextView()} v := CmdView{styles: styles, TextView: tview.NewTextView()}
{ {
v.SetWordWrap(true) v.SetWordWrap(true)
v.SetWrap(true) v.SetWrap(true)
@ -29,7 +30,7 @@ func NewCmdView(styles *config.Styles, ic rune) *CmdView {
v.SetBorder(true) v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.SetBackgroundColor(styles.BgColor()) v.SetBackgroundColor(styles.BgColor())
v.SetBorderColor(config.AsColor(styles.Frame().Border.FocusColor)) // v.SetBorderColor(config.AsColor(styles.Frame().Border.FocusColor))
v.SetTextColor(styles.FgColor()) v.SetTextColor(styles.FgColor())
} }
return &v return &v
@ -67,11 +68,13 @@ func (v *CmdView) BufferChanged(s string) {
} }
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (v *CmdView) BufferActive(f bool) { func (v *CmdView) BufferActive(f bool, k BufferKind) {
v.activated = f v.activated = f
if f { if f {
v.SetBorder(true) v.SetBorder(true)
v.icon = iconFor(k)
v.SetTextColor(v.styles.FgColor()) v.SetTextColor(v.styles.FgColor())
v.SetBorderColor(colorFor(k))
v.activate() v.activate()
} else { } else {
v.SetBorder(false) v.SetBorder(false)
@ -79,3 +82,20 @@ func (v *CmdView) BufferActive(f bool) {
v.Clear() v.Clear()
} }
} }
func colorFor(k BufferKind) tcell.Color {
switch k {
case CommandBuff:
return tcell.ColorAqua
default:
return tcell.ColorSeaGreen
}
}
func iconFor(k BufferKind) rune {
switch k {
case CommandBuff:
return '🐶'
default:
return '🤓'
}
}

View File

@ -2,19 +2,30 @@ package ui
const maxBuff = 10 const maxBuff = 10
const (
// CommandBuff indicates a command buffer.
CommandBuff BufferKind = 1 << iota
// FilterBuff indicates a search buffer.
FilterBuff
)
type ( type (
// BufferKind indicates a buffer type
BufferKind int8
// BuffWatcher represents a command buffer listener. // BuffWatcher represents a command buffer listener.
BuffWatcher interface { BuffWatcher interface {
// Changed indicates the buffer was changed. // Changed indicates the buffer was changed.
BufferChanged(s string) BufferChanged(s string)
// Active indicates the buff activity changed. // Active indicates the buff activity changed.
BufferActive(state bool) BufferActive(state bool, kind BufferKind)
} }
// CmdBuff represents user command input. // CmdBuff represents user command input.
CmdBuff struct { CmdBuff struct {
buff []rune buff []rune
kind BufferKind
hotKey rune hotKey rune
active bool active bool
listeners []BuffWatcher listeners []BuffWatcher
@ -22,9 +33,10 @@ type (
) )
// NewCmdBuff returns a new command buffer. // NewCmdBuff returns a new command buffer.
func NewCmdBuff(key rune) *CmdBuff { func NewCmdBuff(key rune, kind BufferKind) *CmdBuff {
return &CmdBuff{ return &CmdBuff{
hotKey: key, hotKey: key,
kind: kind,
buff: make([]rune, 0, maxBuff), buff: make([]rune, 0, maxBuff),
listeners: []BuffWatcher{}, listeners: []BuffWatcher{},
} }
@ -47,8 +59,9 @@ func (c *CmdBuff) String() string {
} }
// Set initializes the buffer with a command. // Set initializes the buffer with a command.
func (c *CmdBuff) Set(rr []rune) { func (c *CmdBuff) Set(cmd string) {
c.buff = rr c.buff = []rune(cmd)
c.fireChanged()
} }
// Add adds a new charater to the buffer. // Add adds a new charater to the buffer.
@ -104,6 +117,6 @@ func (c *CmdBuff) fireChanged() {
func (c *CmdBuff) fireActive(b bool) { func (c *CmdBuff) fireActive(b bool) {
for _, l := range c.listeners { for _, l := range c.listeners {
l.BufferActive(b) l.BufferActive(b, c.kind)
} }
} }

View File

@ -16,7 +16,7 @@ func (l *testListener) BufferChanged(s string) {
l.text = s l.text = s
} }
func (l *testListener) BufferActive(s bool) { func (l *testListener) BufferActive(s bool, _ BufferKind) {
if s { if s {
l.act++ l.act++
return return
@ -25,7 +25,7 @@ func (l *testListener) BufferActive(s bool) {
} }
func TestCmdBuffActivate(t *testing.T) { func TestCmdBuffActivate(t *testing.T) {
b, l := NewCmdBuff('>'), testListener{} b, l := NewCmdBuff('>', CommandBuff), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.SetActive(true) b.SetActive(true)
@ -35,7 +35,7 @@ func TestCmdBuffActivate(t *testing.T) {
} }
func TestCmdBuffDeactivate(t *testing.T) { func TestCmdBuffDeactivate(t *testing.T) {
b, l := NewCmdBuff('>'), testListener{} b, l := NewCmdBuff('>', CommandBuff), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.SetActive(false) b.SetActive(false)
@ -45,7 +45,7 @@ func TestCmdBuffDeactivate(t *testing.T) {
} }
func TestCmdBuffChanged(t *testing.T) { func TestCmdBuffChanged(t *testing.T) {
b, l := NewCmdBuff('>'), testListener{} b, l := NewCmdBuff('>', CommandBuff), testListener{}
b.AddListener(&l) b.AddListener(&l)
b.Add('b') b.Add('b')
@ -77,7 +77,7 @@ func TestCmdBuffChanged(t *testing.T) {
} }
func TestCmdBuffAdd(t *testing.T) { func TestCmdBuffAdd(t *testing.T) {
b := NewCmdBuff('>') b := NewCmdBuff('>', CommandBuff)
uu := []struct { uu := []struct {
runes []rune runes []rune
@ -98,7 +98,7 @@ func TestCmdBuffAdd(t *testing.T) {
} }
func TestCmdBuffDel(t *testing.T) { func TestCmdBuffDel(t *testing.T) {
b := NewCmdBuff('>') b := NewCmdBuff('>', CommandBuff)
uu := []struct { uu := []struct {
runes []rune runes []rune
@ -120,7 +120,7 @@ func TestCmdBuffDel(t *testing.T) {
} }
func TestCmdBuffEmpty(t *testing.T) { func TestCmdBuffEmpty(t *testing.T) {
b := NewCmdBuff('>') b := NewCmdBuff('>', CommandBuff)
uu := []struct { uu := []struct {
runes []rune runes []rune

View File

@ -9,20 +9,20 @@ import (
func TestNewCmdUpdate(t *testing.T) { func TestNewCmdUpdate(t *testing.T) {
defaults, _ := config.NewStyles("") defaults, _ := config.NewStyles("")
v := NewCmdView(defaults, 'T') v := NewCmdView(defaults)
v.update("blee") v.update("blee")
assert.Equal(t, "T> blee\n", v.GetText(false)) assert.Equal(t, "\x00> blee\n", v.GetText(false))
} }
func TestCmdInCmdMode(t *testing.T) { func TestCmdInCmdMode(t *testing.T) {
defaults, _ := config.NewStyles("") defaults, _ := config.NewStyles("")
v := NewCmdView(defaults, 'T') v := NewCmdView(defaults)
v.update("blee") v.update("blee")
v.append('!') v.append('!')
assert.Equal(t, "T> blee!\n", v.GetText(false)) assert.Equal(t, "\x00> blee!\n", v.GetText(false))
assert.False(t, v.InCmdMode()) assert.False(t, v.InCmdMode())
v.BufferActive(true) v.BufferActive(true, CommandBuff)
assert.True(t, v.InCmdMode()) assert.True(t, v.InCmdMode())
} }

View File

@ -47,7 +47,7 @@ func (v *MenuView) HydrateMenu(hh Hints) {
t := v.buildMenuTable(hh) t := v.buildMenuTable(hh)
for row := 0; row < len(t); row++ { for row := 0; row < len(t); row++ {
for col := 0; col < len(t[row]); col++ { for col := 0; col < len(t[row]); col++ {
if len(t[row][col]) == 0 { if t[row][col] == "" {
continue continue
} }
c := tview.NewTableCell(t[row][col]) c := tview.NewTableCell(t[row][col])
@ -57,33 +57,42 @@ func (v *MenuView) HydrateMenu(hh Hints) {
} }
} }
func (v *MenuView) buildMenuTable(hh Hints) [][]string { func isDigit(s string) bool {
table := make([][]Hint, maxRows+1) return menuRX.MatchString(s)
colCount := (len(hh) / maxRows) + 1
for row := 0; row < maxRows; row++ {
table[row] = make([]Hint, colCount+1)
} }
var row, col int func (v *MenuView) buildMenuTable(hh Hints) [][]string {
table := make([][]Hint, maxRows)
colCount := len(hh) / maxRows
if colCount == 0 {
colCount = 1
}
if isDigit(hh[0].Mnemonic) {
colCount++
}
for row := 0; row < maxRows; row++ {
table[row] = make([]Hint, colCount)
}
var row, col, added int
firstCmd := true firstCmd := true
maxKeys := make([]int, colCount+1) maxKeys := make([]int, colCount+1)
for _, h := range hh { for _, h := range hh {
if !h.Visible { if !h.Visible {
continue continue
} }
isDigit := menuRX.MatchString(h.Mnemonic) if !isDigit(h.Mnemonic) && firstCmd {
if !isDigit && firstCmd {
row, col, firstCmd = 0, col+1, false row, col, firstCmd = 0, col+1, false
if added == 0 {
col = 0
}
} }
if maxKeys[col] < len(h.Mnemonic) { if maxKeys[col] < len(h.Mnemonic) {
maxKeys[col] = len(h.Mnemonic) maxKeys[col] = len(h.Mnemonic)
} }
table[row][col] = h table[row][col] = h
row++ added, row = added+1, row+1
if row >= maxRows { if row >= maxRows {
col++ row, col = 0, col+1
row = 0
} }
} }
@ -248,6 +257,7 @@ func initKeys() {
initNumbKeys() initNumbKeys()
initStdKeys() initStdKeys()
initShiftKeys() initShiftKeys()
initCtrlKeys()
} }
func initNumbKeys() { func initNumbKeys() {
@ -292,6 +302,12 @@ func initStdKeys() {
tcell.KeyNames[tcell.Key(KeyZ)] = "z" tcell.KeyNames[tcell.Key(KeyZ)] = "z"
} }
// BOZO!! No sure why these aren't mapped??
func initCtrlKeys() {
tcell.KeyNames[tcell.KeyCtrlI] = "Ctrl-I"
tcell.KeyNames[tcell.KeyCtrlM] = "Ctrl-M"
}
func initShiftKeys() { func initShiftKeys() {
tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A" tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A"
tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B" tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B"

View File

@ -12,9 +12,9 @@ func TestNewMenuView(t *testing.T) {
defaults, _ := config.NewStyles("") defaults, _ := config.NewStyles("")
v := NewMenuView(defaults) v := NewMenuView(defaults)
v.HydrateMenu(Hints{ v.HydrateMenu(Hints{
{"0", "zero", true},
{"a", "bleeA", true}, {"a", "bleeA", true},
{"b", "bleeB", true}, {"b", "bleeB", true},
{"0", "zero", true},
}) })
assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text) assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text)

View File

@ -12,8 +12,6 @@ import (
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
// "github.com/ktr0731/go-fuzzyfinder/matching"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
"k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/duration"
@ -52,7 +50,7 @@ func NewTable(title string, styles *config.Styles) *Table {
Table: tview.NewTable(), Table: tview.NewTable(),
styles: styles, styles: styles,
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/'), cmdBuff: NewCmdBuff('/', FilterBuff),
baseTitle: title, baseTitle: title,
sortCol: SortColumn{0, 0, true}, sortCol: SortColumn{0, 0, true},
} }
@ -178,6 +176,7 @@ func (v *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
v.SearchBuff().Add(evt.Rune()) v.SearchBuff().Add(evt.Rune())
v.ClearSelection() v.ClearSelection()
v.doUpdate(v.filtered()) v.doUpdate(v.filtered())
v.UpdateTitle()
v.SelectFirstRow() v.SelectFirstRow()
return nil return nil
} }
@ -383,7 +382,7 @@ func (v *Table) filtered() resource.TableData {
q := v.cmdBuff.String() q := v.cmdBuff.String()
if isFuzzySelector(q) { if isFuzzySelector(q) {
return v.fuzzFilter(q[2:]) return v.fuzzyFilter(q[2:])
} }
return v.rxFilter(q) return v.rxFilter(q)
@ -412,7 +411,7 @@ func (v *Table) rxFilter(q string) resource.TableData {
return filtered return filtered
} }
func (v *Table) fuzzFilter(q string) resource.TableData { func (v *Table) fuzzyFilter(q string) resource.TableData {
var ss, kk []string var ss, kk []string
for k, row := range v.data.Rows { for k, row := range v.data.Rows {
ss = append(ss, row.Fields[v.NameColIndex()]) ss = append(ss, row.Fields[v.NameColIndex()])
@ -506,7 +505,7 @@ func (v *Table) UpdateTitle() {
title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.styles.Frame()) title = skinTitle(fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc), v.styles.Frame())
} }
if !v.cmdBuff.IsActive() && !v.cmdBuff.Empty() { if !v.cmdBuff.Empty() {
cmd := v.cmdBuff.String() cmd := v.cmdBuff.String()
if isLabelSelector(cmd) { if isLabelSelector(cmd) {
cmd = trimLabelSelector(cmd) cmd = trimLabelSelector(cmd)

View File

@ -49,6 +49,7 @@ func (v *aliasView) Init(context.Context, string) {
func (v *aliasView) registerActions() { func (v *aliasView) registerActions() {
v.RmAction(ui.KeyShiftA) v.RmAction(ui.KeyShiftA)
v.RmAction(tcell.KeyCtrlS)
v.SetActions(ui.KeyActions{ v.SetActions(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoCmd, true), tcell.KeyEnter: ui.NewKeyAction("Goto", v.gotoCmd, true),

View File

@ -20,6 +20,7 @@ const (
splashTime = 1 splashTime = 1
devMode = "dev" devMode = "dev"
clusterRefresh = time.Duration(5 * time.Second) clusterRefresh = time.Duration(5 * time.Second)
indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
) )
type ( type (
@ -55,6 +56,7 @@ type (
forwarders map[string]forwarder forwarders map[string]forwarder
version string version string
showHeader bool showHeader bool
filter string
} }
) )
@ -105,7 +107,6 @@ func (a *appView) Init(version string, rate int) {
a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
main.AddItem(a.indicator(), 1, 1, false) main.AddItem(a.indicator(), 1, 1, false)
// main.AddItem(a.Cmd(), 3, 1, false)
main.AddItem(a.Frame(), 0, 10, true) main.AddItem(a.Frame(), 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false) main.AddItem(a.Crumbs(), 2, 1, false)
main.AddItem(a.Flash(), 1, 1, false) main.AddItem(a.Flash(), 1, 1, false)
@ -116,7 +117,7 @@ func (a *appView) Init(version string, rate int) {
func (a *appView) BufferChanged(s string) {} func (a *appView) BufferChanged(s string) {}
// Active indicates the buff activity changed. // Active indicates the buff activity changed.
func (a *appView) BufferActive(state bool) { func (a *appView) BufferActive(state bool, _ ui.BufferKind) {
flex, ok := a.Main().GetPrimitive("main").(*tview.Flex) flex, ok := a.Main().GetPrimitive("main").(*tview.Flex)
if !ok { if !ok {
return return
@ -144,6 +145,7 @@ func (a *appView) toggleHeader(flag bool) {
func (a *appView) buildHeader() tview.Primitive { func (a *appView) buildHeader() tview.Primitive {
header := tview.NewFlex() header := tview.NewFlex()
header.SetBorderPadding(0, 0, 1, 1)
header.SetDirection(tview.FlexColumn) header.SetDirection(tview.FlexColumn)
if !a.showHeader { if !a.showHeader {
return header return header
@ -192,7 +194,7 @@ func (a *appView) refreshIndicator() {
} }
info := fmt.Sprintf( info := fmt.Sprintf(
"[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%", indicatorFmt,
a.version, a.version,
cluster.ClusterName(), cluster.ClusterName(),
cluster.UserName(), cluster.UserName(),

View File

@ -54,7 +54,7 @@ func (c *command) defaultCmd() {
// Helpers... // Helpers...
var policyMatcher = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`) var authRX = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`)
func (c *command) isK9sCmd(cmd string) bool { func (c *command) isK9sCmd(cmd string) bool {
cmds := strings.Split(cmd, " ") cmds := strings.Split(cmd, " ")
@ -69,10 +69,10 @@ func (c *command) isK9sCmd(cmd string) bool {
c.app.aliasCmd(nil) c.app.aliasCmd(nil)
return true return true
default: default:
if !policyMatcher.MatchString(cmd) { if !authRX.MatchString(cmd) {
return false return false
} }
tokens := policyMatcher.FindAllStringSubmatch(cmd, -1) tokens := authRX.FindAllStringSubmatch(cmd, -1)
if len(tokens) == 1 && len(tokens[0]) == 3 { if len(tokens) == 1 && len(tokens[0]) == 3 {
c.app.inject(newPolicyView(c.app, tokens[0][1], tokens[0][2])) c.app.inject(newPolicyView(c.app, tokens[0][1], tokens[0][2]))
return true return true
@ -100,8 +100,8 @@ func (c *command) viewMetaFor(cmd string) (string, *viewer) {
} }
v, ok := vv[gvr] v, ok := vv[gvr]
if !ok { if !ok {
log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", cmd)).Msg("Viewer Failed") log.Error().Err(fmt.Errorf("Huh? `%s` viewer not found", gvr)).Msg("Viewer Failed")
c.app.Flash().Warnf("Huh? `%s` viewer not found", gvr) c.app.Flash().Warnf("Huh? viewer for %s not found", cmd)
return "", nil return "", nil
} }

View File

@ -60,7 +60,6 @@ func (v *containerView) k9sEnv() K9sEnv {
ns, n := namespaced(*v.path) ns, n := namespaced(*v.path)
env["POD"] = n env["POD"] = n
env["NAMESPACE"] = ns env["NAMESPACE"] = ns
log.Debug().Msgf("OVER ENV %#v", env)
return env return env
} }

View File

@ -55,7 +55,7 @@ func newDetailsView(app *appView, backFn ui.ActionHandler) *detailsView {
v.SetTitleColor(tcell.ColorAqua) v.SetTitleColor(tcell.ColorAqua)
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
v.cmdBuff = ui.NewCmdBuff('/') v.cmdBuff = ui.NewCmdBuff('/', ui.FilterBuff)
v.cmdBuff.AddListener(app.Cmd()) v.cmdBuff.AddListener(app.Cmd())
v.cmdBuff.Reset() v.cmdBuff.Reset()

View File

@ -10,7 +10,7 @@ import (
type K9sEnv map[string]string type K9sEnv map[string]string
// EnvRX match $XXX custom arg. // EnvRX match $XXX custom arg.
var envRX = regexp.MustCompile(`\A\$([\w|-]+)`) var envRX = regexp.MustCompile(`\A\$([\w]+)`)
func (e K9sEnv) envFor(n string) (string, error) { func (e K9sEnv) envFor(n string) (string, error) {
envs := envRX.FindStringSubmatch(n) envs := envRX.FindStringSubmatch(n)
@ -22,5 +22,5 @@ func (e K9sEnv) envFor(n string) (string, error) {
return "", fmt.Errorf("No matching for %s", n) return "", fmt.Errorf("No matching for %s", n)
} }
return env, nil return envRX.ReplaceAllString(n, env), nil
} }

View File

@ -14,16 +14,17 @@ func TestK9sEnv(t *testing.T) {
err error err error
e string e string
}{ }{
"match": {q: "$A", err: nil, e: "10"}, "match": {q: "$A", e: "10"},
"noMatch": {q: "$BLEE", err: errors.New("No matching for $BLEE"), e: ""}, "noMatch": {q: "$BLEE", err: errors.New("No matching for $BLEE"), e: ""},
"lower": {q: "$b", err: nil, e: "blee"}, "lower": {q: "$b", e: "blee"},
"dash": {q: "$col-0", err: nil, e: "fred"}, "dash": {q: "$col0", e: "fred"},
"mix": {q: "$col0-blee", e: "fred-blee"},
} }
e := K9sEnv{ e := K9sEnv{
"A": "10", "A": "10",
"B": "blee", "B": "blee",
"COL-0": "fred", "COL0": "fred",
} }
for k, u := range uu { for k, u := range uu {

View File

@ -130,16 +130,16 @@ func (v *logView) flush(index int, buff []string) {
return return
} }
v.log(strings.Join(buff[:index], "\n"))
if atomic.LoadInt32(&v.autoScroll) == 1 { if atomic.LoadInt32(&v.autoScroll) == 1 {
v.log(strings.Join(buff[:index], "\n"))
v.app.QueueUpdateDraw(func() { v.app.QueueUpdateDraw(func() {
v.update() v.updateIndicator()
v.logs.ScrollToEnd() v.logs.ScrollToEnd()
}) })
} }
} }
func (v *logView) update() { func (v *logView) updateIndicator() {
status := "Off" status := "Off"
if v.autoScroll == 1 { if v.autoScroll == 1 {
status = "On" status = "On"
@ -205,7 +205,7 @@ func (v *logView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
v.logs.LineUp() v.logs.LineUp()
v.app.Flash().Info("Autoscroll is off.") v.app.Flash().Info("Autoscroll is off.")
} }
v.update() v.updateIndicator()
return nil return nil
} }

View File

@ -334,7 +334,6 @@ func (v *resourceView) refresh() {
if v.list.Namespaced() { if v.list.Namespaced() {
v.list.SetNamespace(v.currentNS) v.list.SetNamespace(v.currentNS)
} }
log.Debug().Msgf("Reconcile with NS %q", v.currentNS)
if err := v.list.Reconcile(v.app.informer, v.path); err != nil { if err := v.list.Reconcile(v.app.informer, v.path); err != nil {
v.app.Flash().Err(err) v.app.Flash().Err(err)
} }
@ -347,7 +346,6 @@ func (v *resourceView) refresh() {
func (v *resourceView) namespaceActions(aa ui.KeyActions) { func (v *resourceView) namespaceActions(aa ui.KeyActions) {
ns, err := v.app.Conn().Config().CurrentNamespaceName() ns, err := v.app.Conn().Config().CurrentNamespaceName()
log.Debug().Msgf("NAMESPACE %q -- %v", ns, err)
if err == nil && ns != resource.AllNamespace { if err == nil && ns != resource.AllNamespace {
return return
} }
@ -401,7 +399,13 @@ func (v *resourceView) refreshActions() {
} }
func (v *resourceView) customActions(aa ui.KeyActions) { func (v *resourceView) customActions(aa ui.KeyActions) {
for k, plugin := range v.app.Config.K9s.Plugins { pp := config.NewPlugins()
if err := pp.Load(); err != nil {
log.Warn().Msgf("No plugin configuration found")
return
}
for k, plugin := range pp.Plugin {
if !in(plugin.Scopes, v.list.GetName()) { if !in(plugin.Scopes, v.list.GetName()) {
continue continue
} }
@ -428,10 +432,12 @@ func (v *resourceView) execCmd(bin string, bg bool, args ...string) ui.ActionHan
return evt return evt
} }
env := v.envFn() var (
aa := make([]string, len(args)) env = v.envFn()
aa = make([]string, len(args))
err error
)
for i, a := range args { for i, a := range args {
var err error
aa[i], err = env.envFor(a) aa[i], err = env.envFor(a)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Args match failed") log.Error().Err(err).Msg("Args match failed")
@ -454,12 +460,10 @@ func (v *resourceView) defaultK9sEnv() K9sEnv {
"NAMESPACE": ns, "NAMESPACE": ns,
"NAME": n, "NAME": n,
} }
row := v.masterPage().GetRow() row := v.masterPage().GetRow()
for i, r := range row { for i, r := range row {
env["COL-"+strconv.Itoa(i)] = r env["COL"+strconv.Itoa(i)] = r
} }
log.Debug().Msgf("ENVs %#v", env)
return env return env
} }

View File

@ -19,8 +19,7 @@ func newTableView(app *appView, title string) *tableView {
} }
v.SearchBuff().AddListener(app.Cmd()) v.SearchBuff().AddListener(app.Cmd())
v.SearchBuff().AddListener(&v) v.SearchBuff().AddListener(&v)
v.SearchBuff().Reset() v.SearchBuff().Set(app.filter)
v.bindKeys() v.bindKeys()
return &v return &v
@ -30,8 +29,8 @@ func newTableView(app *appView, title string) *tableView {
func (v *tableView) BufferChanged(s string) {} func (v *tableView) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed. // BufferActive indicates the buff activity changed.
func (v *tableView) BufferActive(state bool) { func (v *tableView) BufferActive(state bool, k ui.BufferKind) {
v.app.BufferActive(state) v.app.BufferActive(state, k)
} }
func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
@ -46,6 +45,11 @@ func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *tableView) setFilterFn(fn func(string)) { func (v *tableView) setFilterFn(fn func(string)) {
v.filterFn = fn v.filterFn = fn
cmd := v.SearchBuff().String()
if isLabelSelector(cmd) && v.filterFn != nil {
v.filterFn(trimLabelSelector(cmd))
}
} }
func (v *tableView) bindKeys() { func (v *tableView) bindKeys() {
@ -70,6 +74,7 @@ func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
v.SearchBuff().SetActive(false) v.SearchBuff().SetActive(false)
cmd := v.SearchBuff().String() cmd := v.SearchBuff().String()
v.app.filter = cmd
if isLabelSelector(cmd) && v.filterFn != nil { if isLabelSelector(cmd) && v.filterFn != nil {
v.filterFn(trimLabelSelector(cmd)) v.filterFn(trimLabelSelector(cmd))
return nil return nil
@ -91,6 +96,7 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.SearchBuff().Empty() { if !v.SearchBuff().Empty() {
v.app.Flash().Info("Clearing filter...") v.app.Flash().Info("Clearing filter...")
} }
v.app.filter = ""
if isLabelSelector(v.SearchBuff().String()) { if isLabelSelector(v.SearchBuff().String()) {
v.filterFn("") v.filterFn("")
} }
@ -106,11 +112,12 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
v.app.Flash().Info("Filter mode activated.") v.app.Flash().Info("Filter mode activated.")
if isLabelSelector(v.SearchBuff().String()) { // if isLabelSelector(v.SearchBuff().String()) {
return nil // return nil
} // }
v.SearchBuff().Reset() // v.SearchBuff().Reset()
v.SearchBuff().SetActive(true) v.SearchBuff().SetActive(true)
v.SearchBuff().Set(v.app.filter)
return nil return nil
} }

View File

@ -73,7 +73,7 @@ func TestTableViewFilter(t *testing.T) {
} }
v.Update(data) v.Update(data)
v.SearchBuff().SetActive(true) v.SearchBuff().SetActive(true)
v.SearchBuff().Set([]rune("blee")) v.SearchBuff().Set("blee")
v.filterCmd(nil) v.filterCmd(nil)
assert.Equal(t, 2, v.GetRowCount()) assert.Equal(t, 2, v.GetRowCount())
v.resetCmd(nil) v.resetCmd(nil)