// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s 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:-:b]<%d> [fg:-:fgstyle]%s " maxRows = 6 ) 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 { m := Menu{ Table: tview.NewTable(), styles: styles, } m.SetBackgroundColor(styles.BgColor()) styles.AddListener(&m) return &m } // StylesChanged notifies skin changed. func (m *Menu) StylesChanged(s *config.Styles) { m.styles = s m.SetBackgroundColor(s.BgColor()) for row := range m.GetRowCount() { for col := range m.GetColumnCount() { if c := m.GetCell(row, col); c != nil { c.BackgroundColor = s.BgColor() } } } } // StackPushed notifies a component was added. func (m *Menu) StackPushed(c model.Component) { m.HydrateMenu(c.Hints()) } // StackPopped notifies a component was removed. func (m *Menu) StackPopped(_, top model.Component) { if top != nil { m.HydrateMenu(top.Hints()) } else { m.Clear() } } // StackTop notifies the top component. func (m *Menu) StackTop(t model.Component) { m.HydrateMenu(t.Hints()) } // HydrateMenu populate menu ui from hints. func (m *Menu) HydrateMenu(hh model.MenuHints) { m.Clear() sort.Sort(hh) table := make([]model.MenuHints, maxRows+1) colCount := (len(hh) / maxRows) + 1 if m.hasDigits(hh) { colCount++ } for row := range maxRows { table[row] = make(model.MenuHints, colCount) } t := m.buildMenuTable(hh, table, colCount) for row := range t { for col := range len(t[row]) { c := tview.NewTableCell(t[row][col]) if t[row][col] == "" { c = tview.NewTableCell("") } c.SetBackgroundColor(m.styles.BgColor()) m.SetCell(row, col, c) } } } func (*Menu) hasDigits(hh model.MenuHints) bool { for _, h := range hh { if !h.Visible { continue } if menuRX.MatchString(h.Mnemonic) { return true } } return false } func (m *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCount int) [][]string { var row, col int firstCmd := true maxKeys := make([]int, colCount) for _, h := range hh { if !h.Visible { continue } if !menuRX.MatchString(h.Mnemonic) && firstCmd { row, col, firstCmd = 0, col+1, false if table[0][0].IsBlank() { col = 0 } } if maxKeys[col] < len(h.Mnemonic) { maxKeys[col] = len(h.Mnemonic) } table[row][col] = h row++ if row >= maxRows { row, col = 0, col+1 } } out := make([][]string, len(table)) for r := range out { out[r] = make([]string, len(table[r])) } m.layout(table, maxKeys, out) return out } func (m *Menu) layout(table []model.MenuHints, mm []int, out [][]string) { for r := range table { for c := range table[r] { out[r][c] = m.formatMenu(table[r][c], mm[c]) } } } func (m *Menu) formatMenu(h model.MenuHint, size int) string { if h.Mnemonic == "" || h.Description == "" { return "" } styles := m.styles.Frame() i, err := strconv.Atoi(h.Mnemonic) if err == nil { return formatNSMenu(i, h.Description, &styles) } return formatPlainMenu(h, size, &styles) } // ---------------------------------------------------------------------------- // Helpers... func keyConv(s string) string { if s == "" || !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 s == "" { return s } return "<" + keyConv(strings.ToLower(s)) + ">" } func formatNSMenu(i int, name string, styles *config.Frame) string { fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor.String(), 1) fmat = strings.ReplaceAll(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":") fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1) fmat = strings.Replace(fmat, "fgstyle]", styles.Menu.FgStyle.ToShortString()+"]", 1) return fmt.Sprintf(fmat, i, name) } func formatPlainMenu(h model.MenuHint, size int, styles *config.Frame) string { menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:fgstyle]%s " fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor.String(), 1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1) fmat = strings.ReplaceAll(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":") fmat = strings.Replace(fmat, "fgstyle]", styles.Menu.FgStyle.ToShortString()+"]", 1) return fmt.Sprintf(fmat, ToMnemonic(h.Mnemonic), h.Description) }