package view import ( "context" "fmt" "sort" "strconv" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/derailed/tview" ) const ( helpTitle = "Help" helpTitleFmt = " [aqua::b]%s " ) // HelpFunc processes menu hints. type HelpFunc func() model.MenuHints // Help presents a help viewer. type Help struct { *Table styles *config.Styles hints HelpFunc maxKey, maxDesc, maxRows int } // NewHelp returns a new help viewer. func NewHelp(app *App) *Help { return &Help{ Table: NewTable(client.NewGVR("help")), hints: app.Content.Top().Hints, } } // Init initializes the component. func (h *Help) Init(ctx context.Context) error { if err := h.Table.Init(ctx); err != nil { return err } h.SetSelectable(false, false) h.resetTitle() h.SetBorder(true) h.SetBorderPadding(0, 0, 1, 1) h.bindKeys() h.build() h.app.Styles.AddListener(h) h.StylesChanged(h.app.Styles) return nil } // InCmdMode checks if prompt is active. func (*Help) InCmdMode() bool { return false } // StylesChanged notifies skin changed. func (h *Help) StylesChanged(s *config.Styles) { h.styles = s h.SetBackgroundColor(s.BgColor()) h.updateStyle() } func (h *Help) bindKeys() { h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS, ui.KeySlash) h.Actions().Set(ui.KeyActions{ tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, true), ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false), tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false), }) } func (h *Help) computeMaxes(hh model.MenuHints) { h.maxKey, h.maxDesc = 0, 0 for _, hint := range hh { if len(hint.Mnemonic) > h.maxKey { h.maxKey = len(hint.Mnemonic) } if len(hint.Description) > h.maxDesc { h.maxDesc = len(hint.Description) } } h.maxKey += 2 } func (h *Help) computeExtraMaxes(ee map[string]string) { h.maxDesc = 0 for k := range ee { if len(k) > h.maxDesc { h.maxDesc = len(k) } } } func (h *Help) build() { h.Clear() sections := []string{"RESOURCE", "GENERAL", "NAVIGATION"} h.maxRows = len(h.showGeneral()) ff := []HelpFunc{ h.hints, h.showGeneral, h.showNav, } var col int extras := h.app.Content.Top().ExtraHints() for i, section := range sections { hh := ff[i]() sort.Sort(hh) h.computeMaxes(hh) if extras != nil { h.computeExtraMaxes(extras) } h.addSection(col, section, hh) if i == 0 && extras != nil { h.addExtras(extras, col, len(hh)) } col += 2 } if hh, err := h.showHotKeys(); err == nil { h.computeMaxes(hh) h.addSection(col, "HOTKEYS", hh) } } func (h *Help) addExtras(extras map[string]string, col, size int) { kk := make([]string, 0, len(extras)) for k := range extras { kk = append(kk, k) } sort.StringSlice(kk).Sort() row := size + 1 for _, k := range kk { h.SetCell(row, col, padCell(extras[k], h.maxKey)) h.SetCell(row, col+1, padCell(k, h.maxDesc)) row++ } } func (h *Help) showNav() model.MenuHints { return model.MenuHints{ { Mnemonic: "g", Description: "Goto Top", }, { Mnemonic: "Shift-g", Description: "Goto Bottom", }, { Mnemonic: "Ctrl-b", Description: "Page Up", }, { Mnemonic: "Ctrl-f", Description: "Page Down", }, { Mnemonic: "h", Description: "Left", }, { Mnemonic: "l", Description: "Right", }, { Mnemonic: "k", Description: "Up", }, { Mnemonic: "j", Description: "Down", }, } } func (h *Help) showHotKeys() (model.MenuHints, error) { hh := config.NewHotKeys() if err := hh.Load(); err != nil { return nil, fmt.Errorf("no hotkey configuration found") } kk := make(sort.StringSlice, 0, len(hh.HotKey)) for k := range hh.HotKey { kk = append(kk, k) } kk.Sort() mm := make(model.MenuHints, 0, len(hh.HotKey)) for _, k := range kk { mm = append(mm, model.MenuHint{ Mnemonic: hh.HotKey[k].ShortCut, Description: hh.HotKey[k].Description, }) } return mm, nil } func (h *Help) showGeneral() model.MenuHints { return model.MenuHints{ { Mnemonic: "?", Description: "Help", }, { Mnemonic: "Ctrl-a", Description: "Aliases", }, { Mnemonic: ":cmd", Description: "Command mode", }, { Mnemonic: "/term", Description: "Filter mode", }, { Mnemonic: "esc", Description: "Back/Clear", }, { Mnemonic: "tab", Description: "Field Next", }, { Mnemonic: "backtab", Description: "Field Previous", }, { Mnemonic: "Ctrl-r", Description: "Reload", }, { Mnemonic: "Ctrl-u", Description: "Command Clear", }, { Mnemonic: "Ctrl-e", Description: "Toggle Header", }, { Mnemonic: "Ctrl-g", Description: "Toggle Crumbs", }, { Mnemonic: ":q", Description: "Quit", }, { Mnemonic: "space", Description: "Mark", }, { Mnemonic: "Ctrl-space", Description: "Mark Range", }, { Mnemonic: "Ctrl-\\", Description: "Mark Clear", }, { Mnemonic: "Ctrl-s", Description: "Save", }, } } func (h *Help) resetTitle() { h.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle)) } func (h *Help) addSpacer(c int) { cell := tview.NewTableCell(render.Pad("", h.maxKey)) cell.SetExpansion(1) h.SetCell(0, c, cell) } func (h *Help) addSection(c int, title string, hh model.MenuHints) { if len(hh) > h.maxRows { h.maxRows = len(hh) } row := 0 h.SetCell(row, c, h.titleCell(title)) h.addSpacer(c + 1) row++ for _, hint := range hh { col := c h.SetCell(row, col, padCellWithRef(ui.ToMnemonic(hint.Mnemonic), h.maxKey, hint.Mnemonic)) col++ h.SetCell(row, col, padCell(hint.Description, h.maxDesc)) row++ } if len(hh) >= h.maxRows { return } for i := h.maxRows - len(hh); i > 0; i-- { col := c h.SetCell(row, col, padCell("", h.maxKey)) col++ h.SetCell(row, col, padCell("", h.maxDesc)) row++ } } func (h *Help) updateStyle() { var ( style = tcell.StyleDefault.Background(h.styles.K9s.Help.BgColor.Color()) key = style.Foreground(h.styles.K9s.Help.KeyColor.Color()).Bold(true) numKey = style.Foreground(h.app.Styles.K9s.Help.NumKeyColor.Color()).Bold(true) info = style.Foreground(h.app.Styles.K9s.Help.FgColor.Color()) heading = style.Foreground(h.app.Styles.K9s.Help.SectionColor.Color()) ) for col := 0; col < h.GetColumnCount(); col++ { for row := 0; row < h.GetRowCount(); row++ { c := h.GetCell(row, col) if c == nil { continue } switch { case row == 0: c.SetStyle(heading) case col%2 != 0: c.SetStyle(info) default: if _, err := strconv.Atoi(extractRef(c)); err == nil { c.SetStyle(numKey) continue } c.SetStyle(key) } } } } // ---------------------------------------------------------------------------- // Helpers... func extractRef(c *tview.TableCell) string { if ref, ok := c.GetReference().(string); ok { return ref } return c.Text } func (h *Help) titleCell(title string) *tview.TableCell { c := tview.NewTableCell(title) c.SetTextColor(h.Styles().K9s.Help.SectionColor.Color()) c.SetAttributes(tcell.AttrBold) c.SetExpansion(1) c.SetAlign(tview.AlignLeft) return c } func padCellWithRef(s string, width int, ref interface{}) *tview.TableCell { return padCell(s, width).SetReference(ref) } func padCell(s string, width int) *tview.TableCell { return tview.NewTableCell(render.Pad(s, width)) }