From 55e193ace2e0c00f98907e723a0834d9354254f1 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Tue, 1 Dec 2015 10:08:38 -0600 Subject: [PATCH] Added a few more controls * Menus! * Progress bars! * Ascii Art! --- termbox_asciiart.go | 109 ++++++++++++ termbox_menu.go | 395 +++++++++++++++++++++++++++++++++++++++++ termbox_progressbar.go | 224 +++++++++++++++++++++++ termbox_util.go | 17 +- 4 files changed, 738 insertions(+), 7 deletions(-) create mode 100644 termbox_asciiart.go create mode 100644 termbox_menu.go create mode 100644 termbox_progressbar.go diff --git a/termbox_asciiart.go b/termbox_asciiart.go new file mode 100644 index 0000000..d86855b --- /dev/null +++ b/termbox_asciiart.go @@ -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++ + } +} diff --git a/termbox_menu.go b/termbox_menu.go new file mode 100644 index 0000000..44e7485 --- /dev/null +++ b/termbox_menu.go @@ -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 } diff --git a/termbox_progressbar.go b/termbox_progressbar.go new file mode 100644 index 0000000..afb0ed8 --- /dev/null +++ b/termbox_progressbar.go @@ -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) + } + } + */ +} diff --git a/termbox_util.go b/termbox_util.go index fd69c31..db05a84 100755 --- a/termbox_util.go +++ b/termbox_util.go @@ -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 } }