1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Move all components responsible for module composition into /view

This commit is contained in:
Chris Cummer
2019-08-04 21:42:40 -07:00
parent 94d63306d4
commit dbc047516d
54 changed files with 226 additions and 196 deletions

132
view/keyboard_widget.go Normal file
View File

@@ -0,0 +1,132 @@
package view
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/wtf"
)
type helpItem struct {
Key string
Text string
}
// KeyboardWidget manages keyboard control for a widget
type KeyboardWidget struct {
app *tview.Application
pages *tview.Pages
view *tview.TextView
settings *cfg.Common
charMap map[string]func()
keyMap map[tcell.Key]func()
charHelp []helpItem
keyHelp []helpItem
maxKey int
}
// NewKeyboardWidget creates and returns a new instance of KeyboardWidget
func NewKeyboardWidget(app *tview.Application, pages *tview.Pages, settings *cfg.Common) KeyboardWidget {
return KeyboardWidget{
app: app,
pages: pages,
settings: settings,
charMap: make(map[string]func()),
keyMap: make(map[tcell.Key]func()),
charHelp: []helpItem{},
keyHelp: []helpItem{},
}
}
// SetKeyboardChar sets a character/function combination that responds to key presses
// Example:
//
// widget.SetKeyboardChar("d", widget.deleteSelectedItem)
//
func (widget *KeyboardWidget) SetKeyboardChar(char string, fn func(), helpText string) {
if char == "" {
return
}
widget.charMap[char] = fn
widget.charHelp = append(widget.charHelp, helpItem{char, helpText})
}
// SetKeyboardKey sets a tcell.Key/function combination that responds to key presses
// Example:
//
// widget.SetKeyboardKey(tcell.KeyCtrlD, widget.deleteSelectedItem)
//
func (widget *KeyboardWidget) SetKeyboardKey(key tcell.Key, fn func(), helpText string) {
widget.keyMap[key] = fn
widget.keyHelp = append(widget.keyHelp, helpItem{tcell.KeyNames[key], helpText})
if len(tcell.KeyNames[key]) > widget.maxKey {
widget.maxKey = len(tcell.KeyNames[key])
}
}
// InputCapture is the function passed to tview's SetInputCapture() function
// This is done during the main widget's creation process using the following code:
//
// widget.View.SetInputCapture(widget.InputCapture)
//
func (widget *KeyboardWidget) InputCapture(event *tcell.EventKey) *tcell.EventKey {
if event == nil {
return nil
}
fn := widget.charMap[string(event.Rune())]
if fn != nil {
fn()
return nil
}
fn = widget.keyMap[event.Key()]
if fn != nil {
fn()
return nil
}
return event
}
// HelpText returns the help text and keyboard command info for this widget
func (widget *KeyboardWidget) HelpText() string {
str := " [green::b]Keyboard commands for " + strings.Title(widget.settings.Module.Type) + "[white]\n\n"
for _, item := range widget.charHelp {
str += fmt.Sprintf(" %s\t%s\n", item.Key, item.Text)
}
str += "\n\n"
for _, item := range widget.keyHelp {
str += fmt.Sprintf(" %-*s\t%s\n", widget.maxKey, item.Key, item.Text)
}
return str
}
func (widget *KeyboardWidget) SetView(view *tview.TextView) {
widget.view = view
}
func (widget *KeyboardWidget) ShowHelp() {
closeFunc := func() {
widget.pages.RemovePage("help")
widget.app.SetFocus(widget.view)
}
modal := wtf.NewBillboardModal(widget.HelpText(), closeFunc)
widget.pages.AddPage("help", modal, false, true)
widget.app.SetFocus(modal)
widget.app.QueueUpdate(func() {
widget.app.Draw()
})
}

View File

@@ -0,0 +1,159 @@
package view
import (
"testing"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
func test() {}
func testKeyboardWidget() KeyboardWidget {
keyWid := NewKeyboardWidget(
tview.NewApplication(),
tview.NewPages(),
nil,
)
return keyWid
}
func Test_SetKeyboardChar(t *testing.T) {
tests := []struct {
name string
char string
fn func()
helpText string
mapChar string
expected bool
}{
{
name: "with blank char",
char: "",
fn: test,
helpText: "help",
mapChar: "",
expected: false,
},
{
name: "with undefined char",
char: "d",
fn: test,
helpText: "help",
mapChar: "m",
expected: false,
},
{
name: "with defined char",
char: "d",
fn: test,
helpText: "help",
mapChar: "d",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keyWid := testKeyboardWidget()
keyWid.SetKeyboardChar(tt.char, tt.fn, tt.helpText)
actual := keyWid.charMap[tt.mapChar]
if tt.expected != (actual != nil) {
t.Errorf("\nexpected: %s\n got: %T", "actual != nil", actual)
}
})
}
}
func Test_SetKeyboardKey(t *testing.T) {
tests := []struct {
name string
key tcell.Key
fn func()
helpText string
mapKey tcell.Key
expected bool
}{
{
name: "with undefined key",
key: tcell.KeyCtrlA,
fn: test,
helpText: "help",
mapKey: tcell.KeyCtrlZ,
expected: false,
},
{
name: "with defined key",
key: tcell.KeyCtrlA,
fn: test,
helpText: "help",
mapKey: tcell.KeyCtrlA,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keyWid := testKeyboardWidget()
keyWid.SetKeyboardKey(tt.key, tt.fn, tt.helpText)
actual := keyWid.keyMap[tt.mapKey]
if tt.expected != (actual != nil) {
t.Errorf("\nexpected: %s\n got: %T", "actual != nil", actual)
}
})
}
}
func Test_InputCapture(t *testing.T) {
tests := []struct {
name string
before func(keyWid KeyboardWidget) KeyboardWidget
event *tcell.EventKey
expected *tcell.EventKey
}{
{
name: "with nil event",
before: func(keyWid KeyboardWidget) KeyboardWidget { return keyWid },
event: nil,
expected: nil,
},
{
name: "with undefined event",
before: func(keyWid KeyboardWidget) KeyboardWidget { return keyWid },
event: tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),
expected: tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),
},
{
name: "with defined event",
before: func(keyWid KeyboardWidget) KeyboardWidget {
keyWid.SetKeyboardChar("a", test, "help")
return keyWid
},
event: tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone),
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keyWid := testKeyboardWidget()
keyWid = tt.before(keyWid)
actual := keyWid.InputCapture(tt.event)
if tt.expected == nil {
if actual != nil {
t.Errorf("\nexpected: %v\n got: %v", tt.expected, actual.Rune())
}
return
}
if tt.expected.Rune() != actual.Rune() {
t.Errorf("\nexpected: %v\n got: %v", tt.expected.Rune(), actual.Rune())
}
})
}
}

100
view/multisource_widget.go Normal file
View File

@@ -0,0 +1,100 @@
package view
import (
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/wtf"
)
// MultiSourceWidget is a widget that supports displaying data from multiple sources
type MultiSourceWidget struct {
moduleConfig *cfg.Common
singular string
plural string
DisplayFunction func()
Idx int
Sources []string
}
// NewMultiSourceWidget creates and returns an instance of MultiSourceWidget
func NewMultiSourceWidget(moduleConfig *cfg.Common, singular, plural string) MultiSourceWidget {
widget := MultiSourceWidget{
moduleConfig: moduleConfig,
singular: singular,
plural: plural,
}
widget.loadSources()
return widget
}
/* -------------------- Exported Functions -------------------- */
// CurrentSource returns the string representations of the currently-displayed source
func (widget *MultiSourceWidget) CurrentSource() string {
if widget.Idx >= len(widget.Sources) {
return ""
}
return widget.Sources[widget.Idx]
}
// NextSource displays the next source in the source list. If the current source is the last
// source it wraps around to the first source
func (widget *MultiSourceWidget) NextSource() {
widget.Idx++
if widget.Idx == len(widget.Sources) {
widget.Idx = 0
}
if widget.DisplayFunction != nil {
widget.DisplayFunction()
}
}
// PrevSource displays the previous source in the source list. If the current source is the first
// source, it wraps around to the last source
func (widget *MultiSourceWidget) PrevSource() {
widget.Idx--
if widget.Idx < 0 {
widget.Idx = len(widget.Sources) - 1
}
if widget.DisplayFunction != nil {
widget.DisplayFunction()
}
}
// SetDisplayFunction stores the function that should be called when the source is
// changed. This is typically called from within the initializer for the struct that
// embeds MultiSourceWidget
//
// Example:
//
// widget := Widget{
// MultiSourceWidget: wtf.NewMultiSourceWidget(settings.common, "person", "people")
// }
//
// widget.SetDisplayFunction(widget.display)
//
func (widget *MultiSourceWidget) SetDisplayFunction(displayFunc func()) {
widget.DisplayFunction = displayFunc
}
/* -------------------- Unexported Functions -------------------- */
func (widget *MultiSourceWidget) loadSources() {
var empty []interface{}
single := widget.moduleConfig.Config.UString(widget.singular, "")
multiple := widget.moduleConfig.Config.UList(widget.plural, empty)
asStrs := wtf.ToStrs(multiple)
if single != "" {
asStrs = append(asStrs, single)
}
widget.Sources = asStrs
}

78
view/scrollable.go Normal file
View File

@@ -0,0 +1,78 @@
package view
import (
"strconv"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/cfg"
)
type ScrollableWidget struct {
TextWidget
Selected int
maxItems int
RenderFunction func()
}
func NewScrollableWidget(app *tview.Application, commonSettings *cfg.Common, focusable bool) ScrollableWidget {
widget := ScrollableWidget{
TextWidget: NewTextWidget(app, commonSettings, focusable),
}
widget.Unselect()
widget.View.SetScrollable(true)
widget.View.SetRegions(true)
return widget
}
func (widget *ScrollableWidget) SetRenderFunction(displayFunc func()) {
widget.RenderFunction = displayFunc
}
func (widget *ScrollableWidget) SetItemCount(items int) {
widget.maxItems = items
}
func (widget *ScrollableWidget) GetSelected() int {
return widget.Selected
}
func (widget *ScrollableWidget) RowColor(idx int) string {
if widget.View.HasFocus() && (idx == widget.Selected) {
return widget.CommonSettings().DefaultFocusedRowColor()
}
return widget.CommonSettings().RowColor(idx)
}
func (widget *ScrollableWidget) Next() {
widget.Selected++
if widget.Selected >= widget.maxItems {
widget.Selected = 0
}
widget.RenderFunction()
}
func (widget *ScrollableWidget) Prev() {
widget.Selected--
if widget.Selected < 0 {
widget.Selected = widget.maxItems - 1
}
widget.RenderFunction()
}
func (widget *ScrollableWidget) Unselect() {
widget.Selected = -1
if widget.RenderFunction != nil {
widget.RenderFunction()
}
}
func (widget *ScrollableWidget) Redraw(title, content string, wrap bool) {
widget.TextWidget.Redraw(title, content, wrap)
widget.app.QueueUpdateDraw(func() {
widget.View.Highlight(strconv.Itoa(widget.Selected)).ScrollToHighlight()
})
}

162
view/text_widget.go Normal file
View File

@@ -0,0 +1,162 @@
package view
import (
"fmt"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
"github.com/wtfutil/wtf/wtf"
)
type TextWidget struct {
bordered bool
commonSettings *cfg.Common
enabled bool
focusable bool
focusChar string
name string
quitChan chan bool
refreshing bool
refreshInterval int
app *tview.Application
View *tview.TextView
}
func NewTextWidget(app *tview.Application, commonSettings *cfg.Common, focusable bool) TextWidget {
widget := TextWidget{
commonSettings: commonSettings,
app: app,
bordered: commonSettings.Bordered,
enabled: commonSettings.Enabled,
focusable: focusable,
focusChar: commonSettings.FocusChar(),
name: commonSettings.Name,
quitChan: make(chan bool),
refreshing: false,
refreshInterval: commonSettings.RefreshInterval,
}
widget.View = widget.addView()
widget.View.SetBorder(widget.bordered)
return widget
}
/* -------------------- Exported Functions -------------------- */
// Bordered returns whether or not this widget should be drawn with a border
func (widget *TextWidget) Bordered() bool {
return widget.bordered
}
func (widget *TextWidget) BorderColor() string {
if widget.Focusable() {
return widget.commonSettings.Colors.BorderFocusable
}
return widget.commonSettings.Colors.BorderNormal
}
func (widget *TextWidget) CommonSettings() *cfg.Common {
return widget.commonSettings
}
func (widget *TextWidget) ConfigText() string {
return utils.HelpFromInterface(cfg.Common{})
}
func (widget *TextWidget) ContextualTitle(defaultStr string) string {
if widget.FocusChar() == "" {
return fmt.Sprintf(" %s ", defaultStr)
}
return fmt.Sprintf(" %s [darkgray::u]%s[::-][green] ", defaultStr, widget.FocusChar())
}
func (widget *TextWidget) Disable() {
widget.enabled = false
}
func (widget *TextWidget) Disabled() bool {
return !widget.Enabled()
}
func (widget *TextWidget) Enabled() bool {
return widget.enabled
}
func (widget *TextWidget) Focusable() bool {
return widget.enabled && widget.focusable
}
func (widget *TextWidget) FocusChar() string {
return widget.focusChar
}
func (widget *TextWidget) HelpText() string {
return fmt.Sprintf("\n There is no help available for widget %s", widget.commonSettings.Module.Type)
}
func (widget *TextWidget) QuitChan() chan bool {
return widget.quitChan
}
func (widget *TextWidget) Name() string {
return widget.name
}
// Refreshing returns TRUE if the widget is currently refreshing its data, FALSE if it is not
func (widget *TextWidget) Refreshing() bool {
return widget.refreshing
}
// RefreshInterval returns how often, in seconds, the widget will return its data
func (widget *TextWidget) RefreshInterval() int {
return widget.refreshInterval
}
func (widget *TextWidget) SetFocusChar(char string) {
widget.focusChar = char
}
func (widget *TextWidget) Stop() {
widget.enabled = false
widget.quitChan <- true
}
func (widget *TextWidget) String() string {
return widget.name
}
func (widget *TextWidget) TextView() *tview.TextView {
return widget.View
}
func (widget *TextWidget) Redraw(title, text string, wrap bool) {
widget.app.QueueUpdateDraw(func() {
widget.View.Clear()
widget.View.SetWrap(wrap)
widget.View.SetTitle(widget.ContextualTitle(title))
widget.View.SetText(text)
})
}
/* -------------------- Unexported Functions -------------------- */
func (widget *TextWidget) addView() *tview.TextView {
view := tview.NewTextView()
view.SetBackgroundColor(wtf.ColorFor(widget.commonSettings.Colors.Background))
view.SetBorderColor(wtf.ColorFor(widget.BorderColor()))
view.SetTextColor(wtf.ColorFor(widget.commonSettings.Colors.Text))
view.SetTitleColor(wtf.ColorFor(widget.commonSettings.Colors.Title))
view.SetBorder(true)
view.SetDynamicColors(true)
view.SetWrap(false)
return view
}

209
view/text_widget_test.go Normal file
View File

@@ -0,0 +1,209 @@
package view
import (
"testing"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/cfg"
)
func testTextWidget() TextWidget {
txtWid := NewTextWidget(
tview.NewApplication(),
&cfg.Common{
Module: cfg.Module{
Name: "test widget",
},
},
true,
)
return txtWid
}
func Test_Bordered(t *testing.T) {
tests := []struct {
name string
before func(txtWid TextWidget) TextWidget
expected bool
}{
{
name: "without border",
before: func(txtWid TextWidget) TextWidget {
txtWid.bordered = false
return txtWid
},
expected: false,
},
{
name: "with border",
before: func(txtWid TextWidget) TextWidget {
txtWid.bordered = true
return txtWid
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
txtWid := testTextWidget()
txtWid = tt.before(txtWid)
actual := txtWid.Bordered()
if tt.expected != actual {
t.Errorf("\nexpected: %t\n got: %t", tt.expected, actual)
}
})
}
}
func Test_Disabled(t *testing.T) {
tests := []struct {
name string
before func(txtWid TextWidget) TextWidget
expected bool
}{
{
name: "when not enabled",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = false
return txtWid
},
expected: true,
},
{
name: "when enabled",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = true
return txtWid
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
txtWid := testTextWidget()
txtWid = tt.before(txtWid)
actual := txtWid.Disabled()
if tt.expected != actual {
t.Errorf("\nexpected: %t\n got: %t", tt.expected, actual)
}
})
}
}
func Test_Enabled(t *testing.T) {
tests := []struct {
name string
before func(txtWid TextWidget) TextWidget
expected bool
}{
{
name: "when not enabled",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = false
return txtWid
},
expected: false,
},
{
name: "when enabled",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = true
return txtWid
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
txtWid := testTextWidget()
txtWid = tt.before(txtWid)
actual := txtWid.Enabled()
if tt.expected != actual {
t.Errorf("\nexpected: %t\n got: %t", tt.expected, actual)
}
})
}
}
func Test_Focusable(t *testing.T) {
tests := []struct {
name string
before func(txtWid TextWidget) TextWidget
expected bool
}{
{
name: "when not focusable",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = false
txtWid.focusable = false
return txtWid
},
expected: false,
},
{
name: "when not focusable",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = false
txtWid.focusable = true
return txtWid
},
expected: false,
},
{
name: "when not focusable",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = true
txtWid.focusable = false
return txtWid
},
expected: false,
},
{
name: "when focusable",
before: func(txtWid TextWidget) TextWidget {
txtWid.enabled = true
txtWid.focusable = true
return txtWid
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
txtWid := testTextWidget()
txtWid = tt.before(txtWid)
actual := txtWid.Focusable()
if tt.expected != actual {
t.Errorf("\nexpected: %t\n got: %t", tt.expected, actual)
}
})
}
}
func Test_Name(t *testing.T) {
txtWid := testTextWidget()
actual := txtWid.Name()
expected := "test widget"
if expected != actual {
t.Errorf("\nexpected: %s\n got: %s", expected, actual)
}
}
func Test_String(t *testing.T) {
txtWid := testTextWidget()
actual := txtWid.String()
expected := "test widget"
if expected != actual {
t.Errorf("\nexpected: %s\n got: %s", expected, actual)
}
}