k9s/internal/ui/menu.go

155 lines
3.6 KiB
Go

package ui
import (
"fmt"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
)
const (
menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s "
maxRows = 7
)
var menuRX = regexp.MustCompile(`\d`)
// Menu presents menu options.
type Menu struct {
*tview.Table
styles *config.Styles
}
// NewMenu returns a new menu.
func NewMenu(styles *config.Styles) *Menu {
v := Menu{Table: tview.NewTable(), styles: styles}
v.SetBackgroundColor(styles.BgColor())
return &v
}
// HintsChanged updates the menu based on hints changing.
func (v *Menu) HintsChanged(hh model.MenuHints) {
v.HydrateMenu(hh)
}
// HydrateMenu populate menu ui from hints.
func (v *Menu) HydrateMenu(hh model.MenuHints) {
v.Clear()
sort.Sort(hh)
t := v.buildMenuTable(hh)
for row := 0; row < len(t); row++ {
for col := 0; col < len(t[row]); col++ {
if len(t[row][col]) == 0 {
continue
}
c := tview.NewTableCell(t[row][col])
c.SetBackgroundColor(v.styles.BgColor())
v.SetCell(row, col, c)
}
}
}
func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string {
table := make([]model.MenuHints, maxRows+1)
colCount := (len(hh) / maxRows) + 1
for row := 0; row < maxRows; row++ {
table[row] = make(model.MenuHints, colCount+1)
}
var row, col int
firstCmd := true
maxKeys := make([]int, colCount+1)
for _, h := range hh {
if !h.Visible {
continue
}
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)
}
table[row][col] = h
row++
if row >= maxRows {
col++
row = 0
}
}
strTable := make([][]string, maxRows+1)
for r := 0; r < len(table); r++ {
strTable[r] = make([]string, len(table[r]))
}
for row := range strTable {
for col := range strTable[row] {
strTable[row][col] = keyConv(v.formatMenu(table[row][col], maxKeys[col]))
}
}
return strTable
}
// ----------------------------------------------------------------------------
// 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)
}
// Truncate a string to the given l and suffix ellipsis if needed.
func Truncate(str string, width int) string {
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
}
func toMnemonic(s string) string {
if len(s) == 0 {
return s
}
return "<" + keyConv(strings.ToLower(s)) + ">"
}
func (v *Menu) formatMenu(h model.MenuHint, size int) string {
i, err := strconv.Atoi(h.Mnemonic)
if err == nil {
return formatNSMenu(i, h.Description, v.styles.Frame())
}
return formatPlainMenu(h, size, v.styles.Frame())
}
func formatNSMenu(i int, name string, styles config.Frame) string {
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
return fmt.Sprintf(fmat, i, Truncate(name, 14))
}
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s "
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)
}