add rx filter + headless
parent
7d19faf55a
commit
c34994bcbf
38
cmd/root.go
38
cmd/root.go
|
|
@ -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
1
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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()])
|
||||
|
|
|
|||
|
|
@ -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:])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue