535 lines
14 KiB
Go
535 lines
14 KiB
Go
package termboxUtil
|
|
|
|
import "github.com/nsf/termbox-go"
|
|
|
|
// Menu is a menu with a list of options
|
|
type Menu struct {
|
|
id string
|
|
title string
|
|
options []MenuOption
|
|
// If height is -1, then it is adaptive to the menu
|
|
x, y, width, height int
|
|
showHelp bool
|
|
cursor int
|
|
bg, fg termbox.Attribute
|
|
selectedBg, selectedFg termbox.Attribute
|
|
disabledBg, disabledFg termbox.Attribute
|
|
selectedDisabledBg termbox.Attribute
|
|
selectedDisabledFg termbox.Attribute
|
|
activeFg, activeBg termbox.Attribute
|
|
isDone bool
|
|
bordered bool
|
|
vimMode bool
|
|
tabSkip bool
|
|
active bool
|
|
canSelectDisabled bool
|
|
}
|
|
|
|
// CreateMenu Creates a menu with the specified attributes
|
|
func CreateMenu(title string, options []string, x, y, width, height int, fg, bg termbox.Attribute) *Menu {
|
|
c := Menu{
|
|
title: title,
|
|
x: x, y: y, width: width, height: height,
|
|
fg: fg, bg: bg, selectedFg: bg, selectedBg: fg,
|
|
disabledFg: bg, disabledBg: bg,
|
|
activeFg: fg, activeBg: bg,
|
|
bordered: true,
|
|
}
|
|
for _, line := range options {
|
|
c.options = append(c.options, MenuOption{text: line})
|
|
}
|
|
if len(c.options) > 0 {
|
|
c.SetSelectedOption(&c.options[0])
|
|
}
|
|
return &c
|
|
}
|
|
|
|
func (c *Menu) SetActiveFgColor(fg termbox.Attribute) { c.activeFg = fg }
|
|
|
|
func (c *Menu) SetActiveBgColor(bg termbox.Attribute) { c.activeBg = bg }
|
|
|
|
func (c *Menu) SetActive(a bool) { c.active = a }
|
|
|
|
func (c *Menu) IsActive() bool { return c.active }
|
|
|
|
// GetID returns this control's ID
|
|
func (c *Menu) GetID() string { return c.id }
|
|
|
|
// SetID sets this control's ID
|
|
func (c *Menu) SetID(newID string) { c.id = newID }
|
|
|
|
// IsTabSkipped is always true for a label
|
|
func (c *Menu) IsTabSkipped() bool { return true }
|
|
|
|
// This doesn't do anything for a label
|
|
func (c *Menu) SetTabSkip(b bool) {}
|
|
|
|
// GetTitle returns the current title of the menu
|
|
func (c *Menu) GetTitle() string { return c.title }
|
|
|
|
// SetTitle sets the current title of the menu to s
|
|
func (c *Menu) SetTitle(s string) {
|
|
c.title = s
|
|
}
|
|
|
|
// GetOptions returns the current options of the menu
|
|
func (c *Menu) GetOptions() []MenuOption {
|
|
return c.options
|
|
}
|
|
|
|
// SetOptions set the menu's options to opts
|
|
func (c *Menu) SetOptions(opts []MenuOption) {
|
|
c.options = opts
|
|
}
|
|
|
|
// SetOptionsFromStrings sets the options of this menu from a slice of strings
|
|
func (c *Menu) SetOptionsFromStrings(opts []string) {
|
|
var newOpts []MenuOption
|
|
for _, v := range opts {
|
|
newOpts = append(newOpts, *CreateOptionFromText(v))
|
|
}
|
|
c.SetOptions(newOpts)
|
|
c.SetSelectedOption(c.GetOptionFromIndex(0))
|
|
}
|
|
|
|
// GetX returns the current x coordinate of the menu
|
|
func (c *Menu) GetX() int { return c.x }
|
|
|
|
// SetX sets the current x coordinate of the menu to x
|
|
func (c *Menu) SetX(x int) {
|
|
c.x = x
|
|
}
|
|
|
|
// GetY returns the current y coordinate of the menu
|
|
func (c *Menu) GetY() int { return c.y }
|
|
|
|
// SetY sets the current y coordinate of the menu to y
|
|
func (c *Menu) SetY(y int) {
|
|
c.y = y
|
|
}
|
|
|
|
// GetWidth returns the current width of the menu
|
|
func (c *Menu) GetWidth() int { return c.width }
|
|
|
|
// SetWidth sets the current menu width to width
|
|
func (c *Menu) SetWidth(width int) {
|
|
c.width = width
|
|
}
|
|
|
|
// GetHeight returns the current height of the menu
|
|
func (c *Menu) GetHeight() int { return c.height }
|
|
|
|
// SetHeight set the height of the menu to height
|
|
func (c *Menu) SetHeight(height int) {
|
|
c.height = height
|
|
}
|
|
|
|
// GetSelectedOption returns the current selected option
|
|
func (c *Menu) GetSelectedOption() *MenuOption {
|
|
idx := c.GetSelectedIndex()
|
|
if idx != -1 {
|
|
return &c.options[idx]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetOptionFromIndex Returns the
|
|
func (c *Menu) GetOptionFromIndex(idx int) *MenuOption {
|
|
if idx >= 0 && idx < len(c.options) {
|
|
return &c.options[idx]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetOptionFromText Returns the first option with the text v
|
|
func (c *Menu) GetOptionFromText(v string) *MenuOption {
|
|
for idx := range c.options {
|
|
testOption := &c.options[idx]
|
|
if testOption.GetText() == v {
|
|
return testOption
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSelectedIndex returns the index of the selected option
|
|
// Returns -1 if nothing is selected
|
|
func (c *Menu) GetSelectedIndex() int {
|
|
for idx := range c.options {
|
|
if c.options[idx].IsSelected() {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// SetSelectedIndex sets the selection to setIdx
|
|
func (c *Menu) SetSelectedIndex(idx int) {
|
|
if len(c.options) > 0 {
|
|
if idx < 0 {
|
|
idx = 0
|
|
} else if idx >= len(c.options) {
|
|
idx = len(c.options) - 1
|
|
}
|
|
c.SetSelectedOption(&c.options[idx])
|
|
}
|
|
}
|
|
|
|
// SetSelectedOption sets the current selected option to v (if it's valid)
|
|
func (c *Menu) SetSelectedOption(v *MenuOption) {
|
|
for idx := range c.options {
|
|
if &c.options[idx] == v {
|
|
c.options[idx].Select()
|
|
} else {
|
|
c.options[idx].Unselect()
|
|
}
|
|
}
|
|
}
|
|
|
|
// SelectPrevOption Decrements the selected option (if it can)
|
|
func (c *Menu) SelectPrevOption() {
|
|
idx := c.GetSelectedIndex()
|
|
for idx >= 0 {
|
|
idx--
|
|
testOption := c.GetOptionFromIndex(idx)
|
|
if testOption != nil {
|
|
if c.canSelectDisabled || !testOption.IsDisabled() {
|
|
c.SetSelectedOption(testOption)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SelectNextOption Increments the selected option (if it can)
|
|
func (c *Menu) SelectNextOption() {
|
|
idx := c.GetSelectedIndex()
|
|
for idx < len(c.options) {
|
|
idx++
|
|
testOption := c.GetOptionFromIndex(idx)
|
|
if testOption != nil {
|
|
if c.canSelectDisabled || !testOption.IsDisabled() {
|
|
c.SetSelectedOption(testOption)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SelectPageUpOption Goes up 'menu height' options
|
|
func (c *Menu) SelectPageUpOption() {
|
|
idx := c.GetSelectedIndex()
|
|
idx -= c.height
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
c.SetSelectedIndex(idx)
|
|
return
|
|
}
|
|
|
|
// SelectPageDownOption Goes down 'menu height' options
|
|
func (c *Menu) SelectPageDownOption() {
|
|
idx := c.GetSelectedIndex()
|
|
idx += c.height
|
|
if idx >= len(c.options) {
|
|
idx = len(c.options) - 1
|
|
}
|
|
c.SetSelectedIndex(idx)
|
|
return
|
|
}
|
|
|
|
// SelectFirstOption Goes to the top
|
|
func (c *Menu) SelectFirstOption() {
|
|
c.SetSelectedIndex(0)
|
|
return
|
|
}
|
|
|
|
// SelectLastOption Goes to the bottom
|
|
func (c *Menu) SelectLastOption() {
|
|
c.SetSelectedIndex(len(c.options) - 1)
|
|
return
|
|
}
|
|
|
|
// SetOptionDisabled Disables the specified option
|
|
func (c *Menu) SetOptionDisabled(idx int) {
|
|
if len(c.options) > idx {
|
|
c.GetOptionFromIndex(idx).Disable()
|
|
}
|
|
}
|
|
|
|
// SetOptionEnabled Enables the specified option
|
|
func (c *Menu) SetOptionEnabled(idx int) {
|
|
if len(c.options) > idx {
|
|
c.GetOptionFromIndex(idx).Enable()
|
|
}
|
|
}
|
|
|
|
// HelpIsShown returns true or false if the help is displayed
|
|
func (c *Menu) HelpIsShown() bool { return c.showHelp }
|
|
|
|
// ShowHelp sets whether or not to display the help text
|
|
func (c *Menu) ShowHelp(b bool) {
|
|
c.showHelp = b
|
|
}
|
|
|
|
func (c *Menu) GetFgColor() termbox.Attribute { return c.fg }
|
|
func (c *Menu) SetFgColor(fg termbox.Attribute) { c.fg = fg }
|
|
func (c *Menu) GetBgColor() termbox.Attribute { return c.bg }
|
|
func (c *Menu) SetBgColor(bg termbox.Attribute) { c.bg = bg }
|
|
|
|
func (c *Menu) GetSelectedFgColor() termbox.Attribute { return c.selectedFg }
|
|
func (c *Menu) SetSelectedFgColor(fg termbox.Attribute) { c.selectedFg = fg }
|
|
func (c *Menu) GetSelectedBgColor() termbox.Attribute { return c.selectedBg }
|
|
func (c *Menu) SetSelectedBgColor(bg termbox.Attribute) { c.selectedBg = bg }
|
|
|
|
func (c *Menu) GetSelectedDisabledFgColor() termbox.Attribute { return c.selectedDisabledFg }
|
|
func (c *Menu) SetSelectedDisabledFgColor(fg termbox.Attribute) { c.selectedDisabledFg = fg }
|
|
func (c *Menu) GetSelectedDisabledBgColor() termbox.Attribute { return c.selectedDisabledBg }
|
|
func (c *Menu) SetSelectedDisabledBgColor(bg termbox.Attribute) { c.selectedDisabledBg = bg }
|
|
|
|
func (c *Menu) GetDisabledFgColor() termbox.Attribute { return c.disabledFg }
|
|
func (c *Menu) SetDisabledFgColor(fg termbox.Attribute) { c.disabledFg = fg }
|
|
func (c *Menu) GetDisabledBgColor() termbox.Attribute { return c.disabledBg }
|
|
func (c *Menu) SetDisabledBgColor(bg termbox.Attribute) { c.disabledBg = bg }
|
|
|
|
// IsDone returns whether the user has answered the modal
|
|
func (c *Menu) IsDone() bool { return c.isDone }
|
|
|
|
// SetDone sets whether the modal has completed it's purpose
|
|
func (c *Menu) SetDone(b bool) {
|
|
c.isDone = b
|
|
}
|
|
|
|
// IsBordered returns true or false if this menu has a border
|
|
func (c *Menu) IsBordered() bool { return c.bordered }
|
|
|
|
// SetBordered sets whether we render a border around the menu
|
|
func (c *Menu) SetBordered(b bool) {
|
|
c.bordered = b
|
|
}
|
|
|
|
// EnableVimMode Enables h,j,k,l navigation
|
|
func (c *Menu) EnableVimMode() {
|
|
c.vimMode = true
|
|
}
|
|
|
|
// DisableVimMode Disables h,j,k,l navigation
|
|
func (c *Menu) DisableVimMode() {
|
|
c.vimMode = false
|
|
}
|
|
|
|
func (c *Menu) SetCanSelectDisabled(b bool) {
|
|
c.canSelectDisabled = b
|
|
}
|
|
|
|
// HandleEvent handles the termbox event and returns whether it was consumed
|
|
func (c *Menu) HandleEvent(event termbox.Event) bool {
|
|
if event.Key == termbox.KeyEnter || event.Key == termbox.KeySpace {
|
|
c.isDone = true
|
|
return true
|
|
}
|
|
currentIdx := c.GetSelectedIndex()
|
|
switch event.Key {
|
|
case termbox.KeyArrowUp:
|
|
c.SelectPrevOption()
|
|
case termbox.KeyArrowDown:
|
|
c.SelectNextOption()
|
|
case termbox.KeyArrowLeft:
|
|
c.SelectPageUpOption()
|
|
case termbox.KeyArrowRight:
|
|
c.SelectPageDownOption()
|
|
}
|
|
if c.vimMode {
|
|
switch event.Ch {
|
|
case 'j':
|
|
c.SelectNextOption()
|
|
case 'k':
|
|
c.SelectPrevOption()
|
|
}
|
|
if event.Key == termbox.KeyCtrlF {
|
|
c.SelectPageDownOption()
|
|
} else if event.Key == termbox.KeyCtrlB {
|
|
c.SelectPageUpOption()
|
|
}
|
|
}
|
|
if c.GetSelectedIndex() != currentIdx {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Draw draws the modal
|
|
func (c *Menu) Draw() {
|
|
useFg, useBg := c.fg, c.bg
|
|
if c.active {
|
|
useFg, useBg = c.activeFg, c.activeBg
|
|
}
|
|
// First blank out the area we'll be putting the menu
|
|
FillWithChar(' ', c.x, c.y, c.x+c.width, c.y+c.height, useFg, useBg)
|
|
// Now draw the border
|
|
optionStartX := c.x
|
|
optionStartY := c.y
|
|
optionWidth := c.width
|
|
_ = optionWidth
|
|
optionHeight := c.height
|
|
if optionHeight == -1 {
|
|
optionHeight = len(c.options)
|
|
}
|
|
if c.bordered {
|
|
pct := float64(c.GetSelectedIndex()) / float64(len(c.options))
|
|
if c.title == "" {
|
|
if len(c.options) > c.height-2 {
|
|
DrawBorderWithPct(c.x, c.y, c.x+c.width, c.y+c.height, pct, useFg, useBg)
|
|
} else {
|
|
DrawBorder(c.x, c.y, c.x+c.width, c.y+c.height, useFg, useBg)
|
|
}
|
|
} else {
|
|
if len(c.options) > c.height-2 {
|
|
DrawBorderWithTitleAndPct(c.x, c.y, c.x+c.width, c.y+c.height, " "+c.title+" ", pct, useFg, useBg)
|
|
} else {
|
|
DrawBorderWithTitle(c.x, c.y, c.x+c.width, c.y+c.height, " "+c.title+" ", useFg, useBg)
|
|
}
|
|
}
|
|
optionStartX = c.x + 1
|
|
optionStartY = c.y + 1
|
|
optionWidth = c.width - 1
|
|
optionHeight -= 2
|
|
}
|
|
|
|
if len(c.options) > 0 {
|
|
firstDispIdx := 0
|
|
lastDispIdx := len(c.options) - 1
|
|
if len(c.options) > c.height-2 {
|
|
lastDispIdx = c.height - 2
|
|
}
|
|
if c.GetSelectedIndex() > c.height-2 {
|
|
firstDispIdx = c.GetSelectedIndex() - (c.height - 2)
|
|
lastDispIdx = c.GetSelectedIndex()
|
|
}
|
|
for idx := firstDispIdx; idx < lastDispIdx+1; idx++ {
|
|
currOpt := &c.options[idx]
|
|
outTxt := currOpt.GetText()
|
|
if currOpt.IsDisabled() {
|
|
if c.GetSelectedOption() == currOpt {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, c.selectedDisabledFg, c.selectedDisabledBg)
|
|
} else {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, c.disabledFg, c.disabledBg)
|
|
}
|
|
} else if c.GetSelectedOption() == currOpt {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, c.selectedFg, c.selectedBg)
|
|
} else {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, useFg, useBg)
|
|
}
|
|
optionStartY++
|
|
}
|
|
}
|
|
/*
|
|
// Print the options
|
|
bldHeight := (optionHeight / 2)
|
|
startIdx := c.GetSelectedIndex()
|
|
endIdx := c.GetSelectedIndex()
|
|
for bldHeight > 0 && startIdx >= 1 {
|
|
startIdx--
|
|
bldHeight--
|
|
}
|
|
bldHeight += (optionHeight / 2)
|
|
for bldHeight > 0 && endIdx < len(c.options) {
|
|
endIdx++
|
|
bldHeight--
|
|
}
|
|
|
|
for idx := startIdx; idx < endIdx; idx++ {
|
|
if c.GetSelectedIndex()-idx >= optionHeight-1 {
|
|
// Skip this one
|
|
continue
|
|
}
|
|
currOpt := &c.options[idx]
|
|
outTxt := currOpt.GetText()
|
|
if len(outTxt) >= c.width {
|
|
outTxt = outTxt[:c.width]
|
|
}
|
|
if currOpt.IsDisabled() {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, c.disabledFg, c.disabledBg)
|
|
} else if c.GetSelectedOption() == currOpt {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, c.selectedFg, c.selectedBg)
|
|
} else {
|
|
DrawStringAtPoint(outTxt, optionStartX, optionStartY, useFg, useBg)
|
|
}
|
|
optionStartY++
|
|
if optionStartY > c.y+optionHeight-1 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
/* MenuOption Struct & methods */
|
|
|
|
// MenuOption An option in the menu
|
|
type MenuOption struct {
|
|
id string
|
|
text string
|
|
selected bool
|
|
disabled bool
|
|
helpText string
|
|
subMenu []MenuOption
|
|
}
|
|
|
|
// CreateOptionFromText just returns a MenuOption object
|
|
// That only has it's text value set.
|
|
func CreateOptionFromText(s string) *MenuOption {
|
|
return &MenuOption{text: s}
|
|
}
|
|
|
|
// SetText Sets the text for this option
|
|
func (c *MenuOption) SetText(s string) {
|
|
c.text = s
|
|
}
|
|
|
|
// GetText Returns the text for this option
|
|
func (c *MenuOption) GetText() string { return c.text }
|
|
|
|
// Disable Sets this option to disabled
|
|
func (c *MenuOption) Disable() {
|
|
c.disabled = true
|
|
}
|
|
|
|
// Enable Sets this option to enabled
|
|
func (c *MenuOption) Enable() {
|
|
c.disabled = false
|
|
}
|
|
|
|
// IsDisabled returns whether this option is enabled
|
|
func (c *MenuOption) IsDisabled() bool {
|
|
return c.disabled
|
|
}
|
|
|
|
// IsSelected Returns whether this option is selected
|
|
func (c *MenuOption) IsSelected() bool {
|
|
return c.selected
|
|
}
|
|
|
|
// Select Sets this option to selected
|
|
func (c *MenuOption) Select() {
|
|
c.selected = true
|
|
}
|
|
|
|
// Unselect Sets this option to not selected
|
|
func (c *MenuOption) Unselect() {
|
|
c.selected = false
|
|
}
|
|
|
|
// SetHelpText Sets this option's help text to s
|
|
func (c *MenuOption) SetHelpText(s string) {
|
|
c.helpText = s
|
|
}
|
|
|
|
// GetHelpText Returns the help text for this option
|
|
func (c *MenuOption) GetHelpText() string { return c.helpText }
|
|
|
|
// AddToSubMenu adds a slice of MenuOptions to this option
|
|
func (c *MenuOption) AddToSubMenu(sub *MenuOption) {
|
|
c.subMenu = append(c.subMenu, *sub)
|
|
}
|