Added a few more controls

* Menus!
* Progress bars!
* Ascii Art!
This commit is contained in:
Brian Buller 2015-12-01 10:08:38 -06:00
parent 5390cfa85a
commit 55e193ace2
4 changed files with 738 additions and 7 deletions

109
termbox_asciiart.go Normal file
View File

@ -0,0 +1,109 @@
package termboxUtil
import (
"github.com/nsf/termbox-go"
)
// ASCIIArt is a []string with more functions
type ASCIIArt struct {
contents []string
x, y int
bg, fg termbox.Attribute
}
// CreateASCIIArt Create an ASCII art object from a string slice
func CreateASCIIArt(c []string, x, y int, fg, bg termbox.Attribute) *ASCIIArt {
i := ASCIIArt{contents: c, x: x, y: y, fg: fg, bg: bg}
return &i
}
// GetX Return the x position of the modal
func (i *ASCIIArt) GetX() int { return i.x }
// SetX set the x position of the modal to x
func (i *ASCIIArt) SetX(x int) *ASCIIArt {
i.x = x
return i
}
// GetY Return the y position of the modal
func (i *ASCIIArt) GetY() int { return i.y }
// SetY Set the y position of the modal to y
func (i *ASCIIArt) SetY(y int) *ASCIIArt {
i.y = y
return i
}
// GetHeight Returns the number of strings in the contents slice
func (i *ASCIIArt) GetHeight() int {
return len(i.contents)
}
// SetContents Sets the contents of i to c
func (i *ASCIIArt) SetContents(c []string) *ASCIIArt {
i.contents = c
return i
}
// GetContents returns the ascii art
func (i *ASCIIArt) GetContents() []string {
return i.contents
}
// SetContentLine Sets a specific line of the contents to s
func (i *ASCIIArt) SetContentLine(s string, idx int) *ASCIIArt {
if idx >= 0 && idx < len(i.contents) {
i.contents[idx] = s
}
return i
}
// GetBackground Return the current background color of the modal
func (i *ASCIIArt) GetBackground() termbox.Attribute { return i.bg }
// SetBackground Set the current background color to bg
func (i *ASCIIArt) SetBackground(bg termbox.Attribute) *ASCIIArt {
i.bg = bg
return i
}
// GetForeground Return the current foreground color
func (i *ASCIIArt) GetForeground() termbox.Attribute { return i.fg }
// SetForeground Set the foreground color to fg
func (i *ASCIIArt) SetForeground(fg termbox.Attribute) *ASCIIArt {
i.fg = fg
return i
}
// Align Align the Ascii art over width width with alignment a
func (i *ASCIIArt) Align(a TextAlignment, width int) *ASCIIArt {
// First get the width of the longest string in the slice
var newContents []string
incomingLength := 0
for _, line := range i.contents {
if len(line) > incomingLength {
incomingLength = len(line)
}
}
for _, line := range i.contents {
newContents = append(newContents, AlignText(AlignText(line, incomingLength, AlignLeft), width, a))
}
i.contents = newContents
return i
}
// HandleKeyPress accepts the termbox event and returns whether it was consumed
func (i *ASCIIArt) HandleKeyPress(event termbox.Event) bool {
return false
}
// Draw outputs the input field on the screen
func (i *ASCIIArt) Draw() {
drawX, drawY := i.x, i.y
for _, line := range i.contents {
DrawStringAtPoint(line, drawX, drawY, i.fg, i.bg)
drawY++
}
}

395
termbox_menu.go Normal file
View File

@ -0,0 +1,395 @@
package termboxUtil
import "github.com/nsf/termbox-go"
// Menu is a menu with a list of options
type Menu struct {
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
isDone bool
bordered bool
vimMode 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 {
i := Menu{
title: title,
x: x, y: y, width: width, height: height,
fg: fg, bg: bg, selectedFg: bg, selectedBg: fg,
disabledFg: bg, disabledBg: bg,
}
for _, line := range options {
i.options = append(i.options, MenuOption{text: line})
}
if len(i.options) > 0 {
i.SetSelectedOption(&i.options[0])
}
return &i
}
// GetTitle returns the current title of the menu
func (i *Menu) GetTitle() string { return i.title }
// SetTitle sets the current title of the menu to s
func (i *Menu) SetTitle(s string) *Menu {
i.title = s
return i
}
// GetOptions returns the current options of the menu
func (i *Menu) GetOptions() []MenuOption {
return i.options
}
// SetOptions set the menu's options to opts
func (i *Menu) SetOptions(opts []MenuOption) *Menu {
i.options = opts
return i
}
// SetOptionsFromStrings sets the options of this menu from a slice of strings
func (i *Menu) SetOptionsFromStrings(opts []string) *Menu {
var newOpts []MenuOption
for _, v := range opts {
newOpts = append(newOpts, *CreateOptionFromText(v))
}
return i.SetOptions(newOpts)
}
// GetX returns the current x coordinate of the menu
func (i *Menu) GetX() int { return i.x }
// SetX sets the current x coordinate of the menu to x
func (i *Menu) SetX(x int) *Menu {
i.x = x
return i
}
// GetY returns the current y coordinate of the menu
func (i *Menu) GetY() int { return i.y }
// SetY sets the current y coordinate of the menu to y
func (i *Menu) SetY(y int) *Menu {
i.y = y
return i
}
// GetWidth returns the current width of the menu
func (i *Menu) GetWidth() int { return i.width }
// SetWidth sets the current menu width to width
func (i *Menu) SetWidth(width int) *Menu {
i.width = width
return i
}
// GetHeight returns the current height of the menu
func (i *Menu) GetHeight() int { return i.height }
// SetHeight set the height of the menu to height
func (i *Menu) SetHeight(height int) *Menu {
i.height = height
return i
}
// GetSelectedOption returns the current selected option
func (i *Menu) GetSelectedOption() *MenuOption {
idx := i.GetSelectedIndex()
if idx != -1 {
return &i.options[idx]
}
return nil
}
// GetOptionFromIndex Returns the
func (i *Menu) GetOptionFromIndex(idx int) *MenuOption {
if idx >= 0 && idx < len(i.options) {
return &i.options[idx]
}
return nil
}
// GetOptionFromText Returns the first option with the text v
func (i *Menu) GetOptionFromText(v string) *MenuOption {
for idx := range i.options {
testOption := &i.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 (i *Menu) GetSelectedIndex() int {
for idx := range i.options {
if i.options[idx].IsSelected() {
return idx
}
}
return -1
}
// SetSelectedOption sets the current selected option to v (if it's valid)
func (i *Menu) SetSelectedOption(v *MenuOption) *Menu {
for idx := range i.options {
if &i.options[idx] == v {
i.options[idx].Select()
} else {
i.options[idx].Unselect()
}
}
return i
}
// SelectPrevOption Decrements the selected option (if it can)
func (i *Menu) SelectPrevOption() *Menu {
idx := i.GetSelectedIndex()
for idx >= 0 {
idx--
testOption := i.GetOptionFromIndex(idx)
if testOption != nil && !testOption.IsDisabled() {
i.SetSelectedOption(testOption)
return i
}
}
return i
}
// SelectNextOption Increments the selected option (if it can)
func (i *Menu) SelectNextOption() *Menu {
idx := i.GetSelectedIndex()
for idx < len(i.options) {
idx++
testOption := i.GetOptionFromIndex(idx)
if testOption != nil && !testOption.IsDisabled() {
i.SetSelectedOption(testOption)
return i
}
}
return i
}
// SetOptionDisabled Disables the specified option
func (i *Menu) SetOptionDisabled(idx int) {
if len(i.options) > idx {
i.GetOptionFromIndex(idx).Disable()
}
}
// SetOptionEnabled Enables the specified option
func (i *Menu) SetOptionEnabled(idx int) {
if len(i.options) > idx {
i.GetOptionFromIndex(idx).Enable()
}
}
// HelpIsShown returns true or false if the help is displayed
func (i *Menu) HelpIsShown() bool { return i.showHelp }
// ShowHelp sets whether or not to display the help text
func (i *Menu) ShowHelp(b bool) *Menu {
i.showHelp = b
return i
}
// GetBackground returns the current background color
func (i *Menu) GetBackground() termbox.Attribute { return i.bg }
// SetBackground sets the background color to bg
func (i *Menu) SetBackground(bg termbox.Attribute) *Menu {
i.bg = bg
return i
}
// GetForeground returns the current foreground color
func (i *Menu) GetForeground() termbox.Attribute { return i.fg }
// SetForeground sets the current foreground color to fg
func (i *Menu) SetForeground(fg termbox.Attribute) *Menu {
i.fg = fg
return i
}
// IsDone returns whether the user has answered the modal
func (i *Menu) IsDone() bool { return i.isDone }
// SetDone sets whether the modal has completed it's purpose
func (i *Menu) SetDone(b bool) *Menu {
i.isDone = b
return i
}
// IsBordered returns true or false if this menu has a border
func (i *Menu) IsBordered() bool { return i.bordered }
// SetBordered sets whether we render a border around the menu
func (i *Menu) SetBordered(b bool) *Menu {
i.bordered = b
return i
}
// EnableVimMode Enables h,j,k,l navigation
func (i *Menu) EnableVimMode() *Menu {
i.vimMode = true
return i
}
// DisableVimMode Disables h,j,k,l navigation
func (i *Menu) DisableVimMode() *Menu {
i.vimMode = false
return i
}
// HandleKeyPress handles the termbox event and returns whether it was consumed
func (i *Menu) HandleKeyPress(event termbox.Event) bool {
if event.Key == termbox.KeyEnter {
i.isDone = true
return true
}
currentIdx := i.GetSelectedIndex()
switch event.Key {
case termbox.KeyArrowUp:
i.SelectPrevOption()
case termbox.KeyArrowDown:
i.SelectNextOption()
}
if i.vimMode {
switch event.Ch {
case 'j':
i.SelectNextOption()
case 'k':
i.SelectPrevOption()
}
}
if i.GetSelectedIndex() != currentIdx {
return true
}
return false
}
// Draw draws the modal
func (i *Menu) Draw() {
// First blank out the area we'll be putting the menu
FillWithChar(' ', i.x, i.y, i.x+i.width, i.y+i.height, i.fg, i.bg)
// Now draw the border
optionStartX := i.x
optionStartY := i.y
optionWidth := i.width
optionHeight := i.height
if optionHeight == -1 {
optionHeight = len(i.options)
}
if i.bordered {
if i.height == -1 {
DrawBorder(i.x, i.y, i.x+i.width, i.y+optionHeight+1, i.fg, i.bg)
} else {
DrawBorder(i.x, i.y, i.x+i.width, i.y+optionHeight, i.fg, i.bg)
}
optionStartX = i.x + 1
optionStartY = i.y + 1
optionWidth = i.width - 1
}
// The title
if i.title != "" {
DrawStringAtPoint(AlignText(i.title, optionWidth, AlignCenter), optionStartX, optionStartY, i.fg, i.bg)
optionStartY++
if i.bordered {
FillWithChar('-', optionStartX, optionStartY, optionWidth, optionStartY, i.fg, i.bg)
optionStartY++
}
}
// Print the options
if len(i.options) > 0 {
for idx := range i.options {
currOpt := &i.options[idx]
if currOpt.IsDisabled() {
DrawStringAtPoint(currOpt.GetText(), optionStartX, optionStartY, i.disabledFg, i.disabledBg)
} else if i.GetSelectedOption() == currOpt {
DrawStringAtPoint(AlignText(currOpt.GetText(), optionWidth, AlignLeft), optionStartX, optionStartY, i.selectedFg, i.selectedBg)
} else {
DrawStringAtPoint(currOpt.GetText(), optionStartX, optionStartY, i.fg, i.bg)
}
optionStartY++
}
}
}
/* MenuOption Struct & methods */
// MenuOption An option in the menu
type MenuOption struct {
text string
selected bool
disabled bool
helpText string
}
// 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 (i *MenuOption) SetText(s string) *MenuOption {
i.text = s
return i
}
// GetText Returns the text for this option
func (i *MenuOption) GetText() string { return i.text }
// Disable Sets this option to disabled
func (i *MenuOption) Disable() *MenuOption {
i.disabled = true
return i
}
// Enable Sets this option to enabled
func (i *MenuOption) Enable() *MenuOption {
i.disabled = false
return i
}
// IsDisabled returns whether this option is enabled
func (i *MenuOption) IsDisabled() bool {
return i.disabled
}
// IsSelected Returns whether this option is selected
func (i *MenuOption) IsSelected() bool {
return i.selected
}
// Select Sets this option to selected
func (i *MenuOption) Select() *MenuOption {
i.selected = true
return i
}
// Unselect Sets this option to not selected
func (i *MenuOption) Unselect() *MenuOption {
i.selected = false
return i
}
// SetHelpText Sets this option's help text to s
func (i *MenuOption) SetHelpText(s string) *MenuOption {
i.helpText = s
return i
}
// GetHelpText Returns the help text for this option
func (i *MenuOption) GetHelpText() string { return i.helpText }

224
termbox_progressbar.go Normal file
View File

@ -0,0 +1,224 @@
package termboxUtil
import "github.com/nsf/termbox-go"
// ProgressBar Just contains the data needed to display a progress bar
type ProgressBar struct {
total int
progress int
allowOverflow bool
allowUnderflow bool
fullChar rune
emptyChar rune
bordered bool
alignment TextAlignment
x, y int
width, height int
bg, fg termbox.Attribute
}
// CreateProgressBar Create a progress bar object
func CreateProgressBar(tot, x, y int, fg, bg termbox.Attribute) *ProgressBar {
i := ProgressBar{total: tot,
fullChar: '#', emptyChar: ' ',
x: x, y: y, height: 1, width: 10,
bordered: true, fg: fg, bg: bg,
alignment: AlignLeft,
}
return &i
}
// GetProgress returns the curret progress value
func (i *ProgressBar) GetProgress() int {
return i.progress
}
// SetProgress sets the current progress of the bar
func (i *ProgressBar) SetProgress(p int) *ProgressBar {
if (p <= i.total || i.allowOverflow) || (p >= 0 || i.allowUnderflow) {
i.progress = p
}
return i
}
// IncrProgress increments the current progress of the bar
func (i *ProgressBar) IncrProgress() *ProgressBar {
if i.progress < i.total || i.allowOverflow {
i.progress++
}
return i
}
// DecrProgress decrements the current progress of the bar
func (i *ProgressBar) DecrProgress() *ProgressBar {
if i.progress > 0 || i.allowUnderflow {
i.progress--
}
return i
}
// GetPercent returns the percent full of the bar
func (i *ProgressBar) GetPercent() int {
return int(float64(i.progress) / float64(i.total) * 100)
}
// EnableOverflow Tells the progress bar that it can go over the total
func (i *ProgressBar) EnableOverflow() *ProgressBar {
i.allowOverflow = true
return i
}
// DisableOverflow Tells the progress bar that it can NOT go over the total
func (i *ProgressBar) DisableOverflow() *ProgressBar {
i.allowOverflow = false
return i
}
// EnableUnderflow Tells the progress bar that it can go below zero
func (i *ProgressBar) EnableUnderflow() *ProgressBar {
i.allowUnderflow = true
return i
}
// DisableUnderflow Tells the progress bar that it can NOT go below zero
func (i *ProgressBar) DisableUnderflow() *ProgressBar {
i.allowUnderflow = false
return i
}
// GetFullChar returns the rune used for 'full'
func (i *ProgressBar) GetFullChar() rune {
return i.fullChar
}
// SetFullChar sets the rune used for 'full'
func (i *ProgressBar) SetFullChar(f rune) *ProgressBar {
i.fullChar = f
return i
}
// GetEmptyChar gets the rune used for 'empty'
func (i *ProgressBar) GetEmptyChar() rune {
return i.emptyChar
}
// SetEmptyChar sets the rune used for 'empty'
func (i *ProgressBar) SetEmptyChar(f rune) *ProgressBar {
i.emptyChar = f
return i
}
// GetX Return the x position of the Progress Bar
func (i *ProgressBar) GetX() int { return i.x }
// SetX set the x position of the ProgressBar to x
func (i *ProgressBar) SetX(x int) *ProgressBar {
i.x = x
return i
}
// GetY Return the y position of the ProgressBar
func (i *ProgressBar) GetY() int { return i.y }
// SetY Set the y position of the ProgressBar to y
func (i *ProgressBar) SetY(y int) *ProgressBar {
i.y = y
return i
}
// GetHeight returns the height of the progress bar
// Defaults to 1 (3 if bordered)
func (i *ProgressBar) GetHeight() int {
return i.height
}
// SetHeight Sets the height of the progress bar
func (i *ProgressBar) SetHeight(h int) *ProgressBar {
i.height = h
return i
}
// GetWidth returns the width of the progress bar
func (i *ProgressBar) GetWidth() int {
return i.width
}
// SetWidth Sets the width of the progress bar
func (i *ProgressBar) SetWidth(w int) *ProgressBar {
i.width = w
return i
}
// GetBackground Return the current background color of the modal
func (i *ProgressBar) GetBackground() termbox.Attribute { return i.bg }
// SetBackground Set the current background color to bg
func (i *ProgressBar) SetBackground(bg termbox.Attribute) *ProgressBar {
i.bg = bg
return i
}
// GetForeground Return the current foreground color
func (i *ProgressBar) GetForeground() termbox.Attribute { return i.fg }
// SetForeground Set the foreground color to fg
func (i *ProgressBar) SetForeground(fg termbox.Attribute) *ProgressBar {
i.fg = fg
return i
}
// Align Tells which direction the progress bar empties
func (i *ProgressBar) Align(a TextAlignment) *ProgressBar {
i.alignment = a
return i
}
// HandleKeyPress accepts the termbox event and returns whether it was consumed
func (i *ProgressBar) HandleKeyPress(event termbox.Event) bool {
return false
}
// Draw outputs the input field on the screen
func (i *ProgressBar) Draw() {
// For now, just draw a [#### ] bar
// TODO: make this more advanced
drawX, drawY := i.x, i.y
fillWidth, fillHeight := i.width-2, i.height
DrawStringAtPoint("[", drawX, drawY, i.fg, i.bg)
numFull := int(float64(fillWidth) * float64(i.progress) / float64(i.total))
FillWithChar(i.fullChar, drawX+1, drawY, drawX+1+numFull, drawY+(fillHeight-1), i.fg, i.bg)
DrawStringAtPoint("]", drawX+i.width-1, drawY, i.fg, i.bg)
/*
drawX, drawY := i.x, i.y
drawWidth, drawHeight := i.width, i.height
if i.bordered {
if i.height == 1 && i.width > 2 {
// Just using [ & ] for the border
DrawStringAtPoint("[", drawX, drawY, i.fg, i.bg)
DrawStringAtPoint("]", drawX+i.width-1, drawY, i.fg, i.bg)
drawX++
drawWidth -= 2
} else if i.height >= 3 {
DrawBorder(drawX, drawY, drawX+i.width, drawY+i.height, i.fg, i.bg)
drawX++
drawY++
drawWidth -= 2
drawHeight -= 2
}
}
// Figure out how many chars are full
numFull := drawWidth * (i.progress / i.total)
switch i.alignment {
case AlignRight: // TODO: Fill from right to left
case AlignCenter: // TODO: Fill from middle out
default: // Fill from left to right
FillWithChar(i.fullChar, drawX, drawY, drawX+numFull, drawY+(drawHeight-1), i.fg, i.bg)
if numFull < drawWidth {
FillWithChar(i.emptyChar, drawX+numFull, drawY, drawX+drawWidth-1, drawY+(drawHeight-1), i.fg, i.bg)
}
}
*/
}

View File

@ -42,16 +42,16 @@ func FillWithChar(r rune, x1, y1, x2, y2 int, fg termbox.Attribute, bg termbox.A
// DrawBorder Draw a border around the area inside x1,y1 -> x2, y2
func DrawBorder(x1, y1, x2, y2 int, fg termbox.Attribute, bg termbox.Attribute) {
termbox.SetCell(x1, y1, '', fg, bg)
FillWithChar('', x1+1, y1, x2-1, y1, fg, bg)
termbox.SetCell(x2, y1, '', fg, bg)
termbox.SetCell(x1, y1, '+', fg, bg)
FillWithChar('-', x1+1, y1, x2-1, y1, fg, bg)
termbox.SetCell(x2, y1, '+', fg, bg)
FillWithChar('|', x1, y1+1, x1, y2-1, fg, bg)
FillWithChar('|', x2, y1+1, x2, y2-1, fg, bg)
termbox.SetCell(x1, y2, '', fg, bg)
FillWithChar('', x1+1, y2, x2-1, y2, fg, bg)
termbox.SetCell(x2, y2, '', fg, bg)
termbox.SetCell(x1, y2, '+', fg, bg)
FillWithChar('-', x1+1, y2, x2-1, y2, fg, bg)
termbox.SetCell(x2, y2, '+', fg, bg)
}
// AlignText Aligns the text txt within width characters using the specified alignment
@ -69,7 +69,10 @@ func AlignText(txt string, width int, align TextAlignment) string {
case AlignRight:
return fmt.Sprintf("%s%s", strings.Repeat(" ", numSpaces), txt)
default:
return fmt.Sprintf("%s%s", txt, strings.Repeat(" ", numSpaces))
if numSpaces >= 0 {
return fmt.Sprintf("%s%s", txt, strings.Repeat(" ", numSpaces))
}
return txt
}
}