add rx filter + headless

mine
derailed 2019-08-12 22:14:04 -06:00
parent 7d19faf55a
commit c34994bcbf
19 changed files with 414 additions and 81 deletions

View File

@ -17,17 +17,14 @@ import (
)
const (
appName = "k9s"
defaultRefreshRate = 2 // secs
defaultLogLevel = "info"
shortAppDesc = "A graphical CLI for your Kubernetes cluster management."
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
appName = "k9s"
shortAppDesc = "A graphical CLI for your Kubernetes cluster management."
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
)
var (
version, commit, date = "dev", "dev", "n/a"
refreshRate int
logLevel string
k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags
rootCmd = &cobra.Command{
@ -71,12 +68,12 @@ func run(cmd *cobra.Command, args []string) {
}
}()
zerolog.SetGlobalLevel(parseLevel(logLevel))
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
cfg := loadConfiguration()
app := views.NewApp(cfg)
{
defer app.BailOut()
app.Init(version, refreshRate)
app.Init(version, *k9sFlags.RefreshRate)
app.Run()
}
}
@ -91,8 +88,12 @@ func loadConfiguration() *config.Config {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
}
if refreshRate != defaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(refreshRate)
if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
}
if k9sFlags.Headless != nil {
k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless)
}
if err := k9sCfg.Refine(k8sFlags); err != nil {
@ -126,18 +127,25 @@ func parseLevel(level string) zerolog.Level {
}
func initK9sFlags() {
k9sFlags = config.NewFlags()
rootCmd.Flags().IntVarP(
&refreshRate,
k9sFlags.RefreshRate,
"refresh", "r",
defaultRefreshRate,
config.DefaultRefreshRate,
"Specifies the default refresh rate as an integer (sec)",
)
rootCmd.Flags().StringVarP(
&logLevel,
k9sFlags.LogLevel,
"logLevel", "l",
defaultLogLevel,
config.DefaultLogLevel,
"Specify a log level (info, warn, debug, error, fatal, panic, trace)",
)
rootCmd.Flags().BoolVar(
k9sFlags.Headless,
"headless",
false,
"Turn K9s header off",
)
}
func initK8sFlags() {

1
go.sum
View File

@ -380,6 +380,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc=
google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

View File

@ -174,6 +174,7 @@ func (c *Config) Load(path string) error {
if cfg.K9s != nil {
c.K9s = cfg.K9s
}
log.Debug().Msgf("Headless ? %t", c.K9s.Headless)
return nil
}

View File

@ -259,6 +259,7 @@ func TestSetup(t *testing.T) {
var expectedConfig = `k9s:
refreshRate: 100
headless: false
logBufferSize: 500
logRequestSize: 100
currentContext: blee
@ -310,6 +311,7 @@ var expectedConfig = `k9s:
var resetConfig = `k9s:
refreshRate: 2
headless: false
logBufferSize: 200
logRequestSize: 200
currentContext: blee

36
internal/config/flags.go Normal file
View File

@ -0,0 +1,36 @@
package config
const (
// DefaultRefreshRate represents the refresh interval.
DefaultRefreshRate = 2 // secs
// DefaultLogLevel represents the default log level.
DefaultLogLevel = "info"
)
// Flags represents K9s configuration flags.
type Flags struct {
RefreshRate *int
LogLevel *string
Headless *bool
}
// NewFlags returns new configuration flags.
func NewFlags() *Flags {
return &Flags{
RefreshRate: intPtr(DefaultRefreshRate),
LogLevel: strPtr(DefaultLogLevel),
Headless: boolPtr(false),
}
}
func boolPtr(b bool) *bool {
return &b
}
func intPtr(i int) *int {
return &i
}
func strPtr(s string) *string {
return &s
}

View File

@ -12,14 +12,16 @@ const (
// K9s tracks K9s configuration options.
type K9s struct {
RefreshRate int `yaml:"refreshRate"`
manualRefreshRate int
RefreshRate int `yaml:"refreshRate"`
Headless bool `yaml:"headless"`
LogBufferSize int `yaml:"logBufferSize"`
LogRequestSize int `yaml:"logRequestSize"`
CurrentContext string `yaml:"currentContext"`
CurrentCluster string `yaml:"currentCluster"`
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
Plugins map[string]*Plugin `yaml:"plugins,omitempty"`
manualRefreshRate int
manualHeadless *bool
}
// NewK9s create a new K9s configuration.
@ -38,6 +40,21 @@ func (k *K9s) OverrideRefreshRate(r int) {
k.manualRefreshRate = r
}
// OverrideHeadless set the headlessness manually.
func (k *K9s) OverrideHeadless(b bool) {
k.manualHeadless = &b
}
// GetHeadless returns headless setting.
func (k *K9s) GetHeadless() bool {
h := k.Headless
if k.manualHeadless != nil && *k.manualHeadless {
h = *k.manualHeadless
}
return h
}
// GetRefreshRate returns the current refresh rate.
func (k *K9s) GetRefreshRate() int {
rate := k.RefreshRate

View File

@ -45,6 +45,7 @@ func (b *Benchmark) init(base string) error {
if err != nil {
return err
}
log.Debug().Msgf("Benchmarking Request %s", req.URL.String())
if b.config.Auth.User != "" || b.config.Auth.Password != "" {
req.SetBasicAuth(b.config.Auth.User, b.config.Auth.Password)

View File

@ -41,8 +41,8 @@ func (a KeyActions) Hints() Hints {
for _, k := range kk {
if name, ok := tcell.KeyNames[tcell.Key(k)]; ok {
hh = append(hh, Hint{
mnemonic: name,
description: a[tcell.Key(k)].Description})
Mnemonic: name,
Description: a[tcell.Key(k)].Description})
} else {
log.Error().Msgf("Unable to locate KeyName for %#v", string(k))
}

View File

@ -44,6 +44,7 @@ type (
content *tview.Pages
views map[string]tview.Primitive
cmdBuff *CmdBuff
hints Hints
}
)
@ -224,9 +225,15 @@ func (a *App) ActiveView() Igniter {
// SetHints updates menu hints.
func (a *App) SetHints(h Hints) {
a.hints = h
a.views["menu"].(*MenuView).HydrateMenu(h)
}
// GetHints retrieves the currently active hints.
func (a *App) GetHints() Hints {
return a.hints
}
// StatusReset reset log back to normal.
func (a *App) StatusReset() {
a.Logo().Reset()

View File

@ -8,7 +8,7 @@ import (
type (
// Hint represents keyboard mnemonic.
Hint struct {
mnemonic, description string
Mnemonic, Description string
}
// Hints a collection of keyboard mnemonics.
Hints []Hint
@ -28,8 +28,8 @@ func (h Hints) Swap(i, j int) {
}
func (h Hints) Less(i, j int) bool {
n, err1 := strconv.Atoi(h[i].mnemonic)
m, err2 := strconv.Atoi(h[j].mnemonic)
n, err1 := strconv.Atoi(h[i].Mnemonic)
m, err2 := strconv.Atoi(h[j].Mnemonic)
if err1 == nil && err2 == nil {
return n < m
}
@ -39,5 +39,5 @@ func (h Hints) Less(i, j int) bool {
if err1 != nil && err2 == nil {
return false
}
return strings.Compare(h[i].description, h[j].description) < 0
return strings.Compare(h[i].Description, h[j].Description) < 0
}

87
internal/ui/indicator.go Normal file
View File

@ -0,0 +1,87 @@
package ui
import (
"context"
"fmt"
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
// IndicatorView represents a status indicator.
type IndicatorView struct {
*tview.TextView
app *App
styles *config.Styles
permanent string
cancel context.CancelFunc
}
// NewIndicatorView returns a new logo.
func NewIndicatorView(app *App, styles *config.Styles) *IndicatorView {
v := IndicatorView{
TextView: tview.NewTextView(),
app: app,
styles: styles,
}
v.SetTextAlign(tview.AlignCenter)
v.SetTextColor(tcell.ColorWhite)
v.SetBackgroundColor(styles.BgColor())
v.SetDynamicColors(true)
return &v
}
// SetPermanent sets permanent title to be reset to after updates
func (v *IndicatorView) SetPermanent(info string) {
v.permanent = info
v.SetText(info)
}
// Reset clears out the logo view and resets colors.
func (v *IndicatorView) Reset() {
v.Clear()
v.SetPermanent(v.permanent)
}
// Err displays a log error state.
func (v *IndicatorView) Err(msg string) {
v.update(msg, "orangered")
}
// Warn displays a log warning state.
func (v *IndicatorView) Warn(msg string) {
v.update(msg, "mediumvioletred")
}
// Info displays a log info state.
func (v *IndicatorView) Info(msg string) {
v.update(msg, "lawngreen")
}
func (v *IndicatorView) update(msg, c string) {
v.setText(fmt.Sprintf("[%s::b] <%s> ", c, msg))
}
func (v *IndicatorView) setText(msg string) {
if v.cancel != nil {
v.cancel()
}
v.SetText(msg)
var ctx context.Context
ctx, v.cancel = context.WithCancel(context.Background())
go func(ctx context.Context) {
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
v.app.QueueUpdateDraw(func() {
v.Reset()
})
}
}(ctx)
}

View File

@ -3,6 +3,7 @@ package ui
import (
"fmt"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
@ -68,12 +69,12 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string {
firstCmd := true
maxKeys := make([]int, colCount+1)
for _, h := range hh {
isDigit := menuRX.MatchString(h.mnemonic)
isDigit := menuRX.MatchString(h.Mnemonic)
if !isDigit && firstCmd {
row, col, firstCmd = 0, col+1, false
}
if maxKeys[col] < len(h.mnemonic) {
maxKeys[col] = len(h.mnemonic)
if maxKeys[col] < len(h.Mnemonic) {
maxKeys[col] = len(h.Mnemonic)
}
table[row][col] = h
row++
@ -89,7 +90,7 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string {
}
for row := range strTable {
for col := range strTable[row] {
strTable[row][col] = v.formatMenu(table[row][col], maxKeys[col])
strTable[row][col] = keyConv(v.formatMenu(table[row][col], maxKeys[col]))
}
}
@ -99,6 +100,18 @@ func (v *MenuView) buildMenuTable(hh Hints) [][]string {
// ----------------------------------------------------------------------------
// Helpers...
func keyConv(s string) string {
if !strings.Contains(s, "alt") {
return s
}
if runtime.GOOS != "darwin" {
return s
}
return strings.Replace(s, "alt", "opt", 1)
}
func toMnemonic(s string) string {
if len(s) == 0 {
return s
@ -108,9 +121,9 @@ func toMnemonic(s string) string {
}
func (v *MenuView) formatMenu(h Hint, size int) string {
i, err := strconv.Atoi(h.mnemonic)
i, err := strconv.Atoi(h.Mnemonic)
if err == nil {
return formatNSMenu(i, h.description, v.styles.Frame())
return formatNSMenu(i, h.Description, v.styles.Frame())
}
return formatPlainMenu(h, size, v.styles.Frame())
@ -128,7 +141,7 @@ func formatPlainMenu(h Hint, size int, styles config.Frame) string {
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1)
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
return fmt.Sprintf(fmat, toMnemonic(h.mnemonic), h.description)
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
}
// -----------------------------------------------------------------------------

View File

@ -1,8 +1,10 @@
package ui
import (
"errors"
"fmt"
"path"
"regexp"
"strings"
"time"
@ -381,6 +383,37 @@ func (v *Table) filtered() resource.TableData {
}
q := v.cmdBuff.String()
if isFuzzySelector(q) {
return v.fuzzFilter(q[2:])
}
return v.rxFilter(q)
}
func (v *Table) rxFilter(q string) resource.TableData {
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
if err != nil {
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
v.cmdBuff.Clear()
return v.data
}
filtered := resource.TableData{
Header: v.data.Header,
Rows: resource.RowEvents{},
Namespace: v.data.Namespace,
}
for k, row := range v.data.Rows {
f := strings.Join(row.Fields, " ")
if rx.MatchString(f) {
filtered.Rows[k] = row
}
}
return filtered
}
func (v *Table) fuzzFilter(q string) resource.TableData {
var ss, kk []string
for k, row := range v.data.Rows {
ss = append(ss, row.Fields[v.NameColIndex()])

View File

@ -26,6 +26,7 @@ var (
cpuRX = regexp.MustCompile(`\A.{0,1}CPU`)
memRX = regexp.MustCompile(`\A.{0,1}MEM`)
labelCmd = regexp.MustCompile(`\A\-l`)
fuzzyCmd = regexp.MustCompile(`\A\-f`)
)
type cleanseFn func(string) string
@ -47,6 +48,13 @@ func isLabelSelector(s string) bool {
return labelCmd.MatchString(s)
}
func isFuzzySelector(s string) bool {
if s == "" {
return false
}
return fuzzyCmd.MatchString(s)
}
func trimLabelSelector(s string) string {
return strings.TrimSpace(s[2:])
}

View File

@ -2,10 +2,12 @@ package views
import (
"context"
"fmt"
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/watch"
"github.com/derailed/tview"
@ -17,7 +19,7 @@ import (
const (
splashTime = 1
devMode = "dev"
clusterRefresh = time.Duration(15 * time.Second)
clusterRefresh = time.Duration(5 * time.Second)
)
type (
@ -50,6 +52,7 @@ type (
informer *watch.Informer
stopCh chan struct{}
forwarders map[string]forwarder
version string
}
)
@ -63,6 +66,7 @@ func NewApp(cfg *config.Config) *appView {
v.InitBench(cfg.K9s.CurrentCluster)
v.command = newCommand(&v)
v.Views()["indicator"] = ui.NewIndicatorView(v.App, v.Styles)
v.Views()["flash"] = ui.NewFlashView(v.Application, "Initializing...")
v.Views()["clusterInfo"] = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection()))
@ -70,6 +74,7 @@ func NewApp(cfg *config.Config) *appView {
}
func (a *appView) Init(version string, rate int) {
a.version = version
a.App.Init()
a.AddActions(ui.KeyActions{
@ -85,6 +90,9 @@ func (a *appView) Init(version string, rate int) {
}
a.startInformer(ns)
a.clusterInfo().init(version)
if a.Config.K9s.GetHeadless() {
a.refreshIndicator()
}
}
header := tview.NewFlex()
@ -96,14 +104,17 @@ func (a *appView) Init(version string, rate int) {
}
main := tview.NewFlex()
{
main.SetDirection(tview.FlexRow)
main.SetDirection(tview.FlexRow)
if !a.Config.K9s.GetHeadless() {
main.AddItem(header, 7, 1, false)
main.AddItem(a.Cmd(), 3, 1, false)
main.AddItem(a.Frame(), 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false)
main.AddItem(a.Flash(), 1, 1, false)
} else {
main.AddItem(a.indicator(), 1, 1, false)
}
main.AddItem(a.Cmd(), 3, 1, false)
main.AddItem(a.Frame(), 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false)
main.AddItem(a.Flash(), 1, 1, false)
a.Main().AddPage("main", main, true, false)
a.Main().AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
@ -117,12 +128,45 @@ func (a *appView) clusterUpdater(ctx context.Context) {
return
case <-time.After(clusterRefresh):
a.QueueUpdateDraw(func() {
if a.Config.K9s.GetHeadless() {
a.refreshIndicator()
}
a.clusterInfo().refresh()
})
}
}
}
func (a *appView) refreshIndicator() {
mx := k8s.NewMetricsServer(a.Conn())
cluster := resource.NewCluster(a.Conn(), &log.Logger, mx)
var cmx k8s.ClusterMetrics
nos, nmx, err := fetchResources(a)
cpu, mem := "0", "0"
if err == nil {
cluster.Metrics(nos, nmx, &cmx)
cpu = resource.AsPerc(cmx.PercCPU)
if cpu == "0" {
cpu = resource.NAValue
}
mem = resource.AsPerc(cmx.PercMEM)
if mem == "0" {
mem = resource.NAValue
}
}
info := fmt.Sprintf(
"[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%",
a.version,
cluster.ClusterName(),
cluster.UserName(),
cluster.Version(),
cpu,
mem,
)
a.indicator().SetPermanent(info)
}
func (a *appView) startInformer(ns string) {
if a.stopCh != nil {
close(a.stopCh)
@ -135,6 +179,10 @@ func (a *appView) startInformer(ns string) {
log.Panic().Err(err).Msgf("%v", err)
}
a.informer.Run(a.stopCh)
if a.Config.K9s.GetHeadless() {
a.refreshIndicator()
}
}
// BailOut exists the application.
@ -188,6 +236,15 @@ func (a *appView) Run() {
func (a *appView) status(l ui.FlashLevel, msg string) {
a.Flash().Info(msg)
if a.Config.K9s.GetHeadless() {
a.setIndicator(l, msg)
} else {
a.setLogo(l, msg)
}
a.Draw()
}
func (a *appView) setLogo(l ui.FlashLevel, msg string) {
switch l {
case ui.FlashErr:
a.Logo().Err(msg)
@ -201,6 +258,20 @@ func (a *appView) status(l ui.FlashLevel, msg string) {
a.Draw()
}
func (a *appView) setIndicator(l ui.FlashLevel, msg string) {
switch l {
case ui.FlashErr:
a.indicator().Err(msg)
case ui.FlashWarn:
a.indicator().Warn(msg)
case ui.FlashInfo:
a.indicator().Info(msg)
default:
a.indicator().Reset()
}
a.Draw()
}
func (a *appView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
if top, ok := a.command.previousCmd(); ok {
log.Debug().Msgf("Previous command %s", top)
@ -225,7 +296,12 @@ func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.InCmdMode() {
return evt
}
a.inject(newHelpView(a, a.ActiveView()))
if _, ok := a.Frame().GetPrimitive("main").(*helpView); ok {
return evt
}
h := newHelpView(a, a.ActiveView(), a.GetHints())
a.inject(h)
return nil
}
@ -233,6 +309,10 @@ func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.InCmdMode() {
return evt
}
if _, ok := a.Frame().GetPrimitive("main").(*aliasView); ok {
return evt
}
a.inject(newAliasView(a, a.ActiveView()))
return nil
@ -267,3 +347,7 @@ func (a *appView) inject(i ui.Igniter) {
func (a *appView) clusterInfo() *clusterInfoView {
return a.Views()["clusterInfo"].(*clusterInfoView)
}
func (a *appView) indicator() *ui.IndicatorView {
return a.Views()["indicator"].(*ui.IndicatorView)
}

View File

@ -125,13 +125,13 @@ func (v *clusterInfoView) refresh() {
v.refreshMetrics(cluster, row)
}
func (v *clusterInfoView) fetchResources() (k8s.Collection, k8s.Collection, error) {
nos, err := v.app.informer.List(watch.NodeIndex, "", metav1.ListOptions{})
func fetchResources(app *appView) (k8s.Collection, k8s.Collection, error) {
nos, err := app.informer.List(watch.NodeIndex, "", metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
nmx, err := v.app.informer.List(watch.NodeMXIndex, "", metav1.ListOptions{})
nmx, err := app.informer.List(watch.NodeMXIndex, "", metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
@ -140,7 +140,7 @@ func (v *clusterInfoView) fetchResources() (k8s.Collection, k8s.Collection, erro
}
func (v *clusterInfoView) refreshMetrics(cluster *resource.Cluster, row int) {
nos, nmx, err := v.fetchResources()
nos, nmx, err := fetchResources(v.app)
if err != nil {
log.Warn().Err(err).Msg("NodeMetrics")
return

View File

@ -55,10 +55,10 @@ func (c *command) isStdCmd(cmd string) bool {
c.app.BailOut()
return true
case cmd == "?", cmd == "help":
c.app.inject(newHelpView(c.app, c.app.ActiveView()))
c.app.helpCmd(nil)
return true
case cmd == "alias":
c.app.inject(newAliasView(c.app, c.app.ActiveView()))
c.app.aliasCmd(nil)
return true
case policyMatcher.MatchString(cmd):
tokens := policyMatcher.FindAllStringSubmatch(cmd, -1)

View File

@ -3,6 +3,9 @@ package views
import (
"context"
"fmt"
"runtime"
"sort"
"strings"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
@ -21,7 +24,7 @@ type (
}
helpView struct {
*tview.TextView
*tview.Table
app *appView
current ui.Igniter
@ -29,19 +32,18 @@ type (
}
)
func newHelpView(app *appView, current ui.Igniter) *helpView {
func newHelpView(app *appView, current ui.Igniter, hh ui.Hints) *helpView {
v := helpView{
TextView: tview.NewTextView(),
app: app,
actions: make(ui.KeyActions),
Table: tview.NewTable(),
app: app,
actions: make(ui.KeyActions),
}
v.SetTextColor(tcell.ColorAqua)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
v.SetDynamicColors(true)
v.SetInputCapture(v.keyboard)
v.current = current
v.bindKeys()
v.build(hh)
return &v
}
@ -73,26 +75,18 @@ func (v *helpView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *helpView) Init(_ context.Context, _ string) {
v.resetTitle()
v.showGeneral()
v.showNav()
v.showHelp()
v.app.SetHints(v.Hints())
}
func (v *helpView) showHelp() {
views := []helpItem{
func (v *helpView) showHelp() ui.Hints {
return ui.Hints{
{"?", "Help"},
{"Ctrl-a", "Aliases view"},
}
fmt.Fprintf(v, "\n😱 [aqua::b]%s\n", "Help")
for _, h := range views {
v.printHelp(h.key, h.description)
}
}
func (v *helpView) showNav() {
navigation := []helpItem{
func (v *helpView) showNav() ui.Hints {
return ui.Hints{
{"g", "Goto Top"},
{"G", "Goto Bottom"},
{"Ctrl-b", "Page Down"},
@ -102,14 +96,10 @@ func (v *helpView) showNav() {
{"k", "Up"},
{"j", "Down"},
}
fmt.Fprintf(v, "\n🤖 [aqua::b]%s\n", "View Navigation")
for _, h := range navigation {
v.printHelp(h.key, h.description)
}
}
func (v *helpView) showGeneral() {
general := []helpItem{
func (v *helpView) showGeneral() ui.Hints {
return ui.Hints{
{":<cmd>", "Command mode"},
{"/<term>", "Filter mode"},
{"esc", "Clear filter"},
@ -120,14 +110,6 @@ func (v *helpView) showGeneral() {
{"p", "Previous resource view"},
{":q", "Quit"},
}
fmt.Fprintf(v, "🏠 [aqua::b]%s\n", "General")
for _, h := range general {
v.printHelp(h.key, h.description)
}
}
func (v *helpView) printHelp(key, desc string) {
fmt.Fprintf(v, "[dodgerblue::b]%9s [white::]%s\n", key, desc)
}
func (v *helpView) Hints() ui.Hints {
@ -141,3 +123,55 @@ func (v *helpView) getTitle() string {
func (v *helpView) resetTitle() {
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
}
func (v *helpView) build(hh ui.Hints) {
v.Clear()
sort.Sort(hh)
v.addSection(0, 0, "Resource", hh)
v.addSection(0, 4, "General", v.showGeneral())
v.addSection(0, 6, "Navigation", v.showNav())
v.addSection(0, 8, "Help", v.showHelp())
}
func (v *helpView) addSection(r, c int, title string, hh ui.Hints) {
row := r
cell := tview.NewTableCell(title)
cell.SetTextColor(tcell.ColorWhite)
cell.SetAttributes(tcell.AttrBold)
v.SetCell(r, c, cell)
row++
for _, h := range hh {
col := c
cell := tview.NewTableCell(toMnemonic(h.Mnemonic))
cell.SetTextColor(tcell.ColorDodgerBlue)
cell.SetAttributes(tcell.AttrBold)
cell.SetAlign(tview.AlignRight)
v.SetCell(row, col, cell)
col++
cell = tview.NewTableCell(h.Description)
cell.SetTextColor(tcell.ColorWhite)
v.SetCell(row, col, cell)
row++
}
}
func toMnemonic(s string) string {
if len(s) == 0 {
return s
}
return "<" + keyConv(strings.ToLower(s)) + ">"
}
func keyConv(s string) string {
if !strings.Contains(s, "alt") {
return s
}
if runtime.GOOS != "darwin" {
return s
}
return strings.Replace(s, "alt", "opt", 1)
}

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -40,10 +41,10 @@ func newNS(n string) v1.Namespace {
func TestNewHelpView(t *testing.T) {
cfg := config.NewConfig(ks{})
a := NewApp(cfg)
v := newHelpView(a, nil)
v := newHelpView(a, nil, ui.Hints{{"blee", "duh"}})
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())
assert.Equal(t, "<blee>", v.GetCell(1, 0).Text)
assert.Equal(t, "duh", v.GetCell(1, 1).Text)
}