First Commit

This commit is contained in:
2025-06-18 07:13:07 -05:00
commit 9688b2930f
13 changed files with 1850 additions and 0 deletions

0
README.md Normal file
View File

129
absolute_layout.go Normal file
View File

@@ -0,0 +1,129 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
"github.com/gdamore/tcell"
)
type AbsoluteLayout struct {
id string
style tcell.Style
x, y int
w, h int
widgets []Widget
wCoords map[Widget]Coord
active bool
visible bool
logger func(string)
}
func (w *AbsoluteLayout) SetLogger(l func(string)) { w.logger = l }
func (w *AbsoluteLayout) Log(txt string) {
if w.logger != nil {
w.logger(txt)
}
}
func NewAbsoluteLayout(id string, s tcell.Style) *AbsoluteLayout {
ret := &AbsoluteLayout{style: s}
ret.Init(id)
return ret
}
func (w *AbsoluteLayout) Init(id string) {
w.id = id
w.visible = true
w.wCoords = make(map[Widget]Coord)
}
func (w *AbsoluteLayout) Id() string { return w.id }
func (w *AbsoluteLayout) HandleResize(ev *tcell.EventResize) {
for _, wi := range w.widgets {
wi.HandleResize(ev)
}
}
func (w *AbsoluteLayout) HandleKey(ev *tcell.EventKey) bool {
for _, wi := range w.widgets {
w.Log(fmt.Sprintf("Passing key (%s) to %s", ev.Name(), wi.Id()))
if wi.HandleKey(ev) {
return true
}
}
return false
}
func (w *AbsoluteLayout) HandleTime(ev *tcell.EventTime) {
for _, wi := range w.widgets {
wi.HandleTime(ev)
}
}
func (w *AbsoluteLayout) Draw(screen tcell.Screen) {
if !w.visible {
return
}
for i := len(w.widgets) - 1; i >= 0; i-- {
var p Coord
var ok bool
if p, ok = w.wCoords[w.widgets[i]]; !ok {
// Don't know where to put this widget
continue
}
w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y}))
w.widgets[i].Draw(screen)
}
}
func (w *AbsoluteLayout) Active() bool { return w.active }
func (w *AbsoluteLayout) SetActive(a bool) { w.active = a }
func (w *AbsoluteLayout) Visible() bool { return w.visible }
func (w *AbsoluteLayout) SetVisible(a bool) { w.visible = a }
func (w *AbsoluteLayout) Focusable() bool { return true }
func (w *AbsoluteLayout) SetX(x int) { w.x = x }
func (w *AbsoluteLayout) SetY(y int) { w.y = y }
func (w *AbsoluteLayout) GetX() int { return w.x }
func (w *AbsoluteLayout) GetY() int { return w.y }
func (w *AbsoluteLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *AbsoluteLayout) GetW() int { return w.w }
func (w *AbsoluteLayout) GetH() int { return w.h }
func (w *AbsoluteLayout) SetW(wd int) { w.w = wd }
func (w *AbsoluteLayout) SetH(h int) { w.h = h }
func (w *AbsoluteLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *AbsoluteLayout) WantW() int { return w.w }
func (w *AbsoluteLayout) WantH() int { return w.h }
// Add a widget at x/y
func (w *AbsoluteLayout) Add(n Widget, pos Coord) {
w.widgets = append(w.widgets, n)
w.wCoords[n] = pos
}
func (w *AbsoluteLayout) Clear() {
w.widgets = []Widget{}
w.wCoords = make(map[Widget]Coord)
}

110
bordered_widget.go Normal file
View File

@@ -0,0 +1,110 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type BorderedWidget struct {
id string
style tcell.Style
x, y int
w, h int
widget Widget
border []rune
title string
active bool
visible bool
logger func(string)
}
func (w *BorderedWidget) SetLogger(l func(string)) { w.logger = l }
func (w *BorderedWidget) Log(txt string) {
if w.logger != nil {
w.logger(txt)
}
}
func NewBorderedWidget(id string, wd Widget, s tcell.Style) *BorderedWidget {
ret := &BorderedWidget{
style: s,
widget: wd,
}
ret.Init(id)
return ret
}
func (w *BorderedWidget) Init(id string) {
w.id = id
w.visible = true
w.border = h.BRD_CSIMPLE
}
func (w *BorderedWidget) Id() string { return w.id }
func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) {
w.widget.HandleResize(ev)
}
func (w *BorderedWidget) HandleKey(ev *tcell.EventKey) bool {
return w.HandleKey(ev)
}
func (w *BorderedWidget) HandleTime(ev *tcell.EventTime) {
w.HandleTime(ev)
}
func (w *BorderedWidget) Draw(screen tcell.Screen) {
if !w.visible {
return
}
if len(w.title) > 0 {
h.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, w.border, w.style, screen)
} else {
h.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.border, w.style, screen)
}
w.widget.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
w.widget.Draw(screen)
}
func (w *BorderedWidget) Active() bool { return w.active }
func (w *BorderedWidget) SetActive(a bool) { w.active = a }
func (w *BorderedWidget) Visible() bool { return w.visible }
func (w *BorderedWidget) SetVisible(a bool) { w.visible = a }
func (w *BorderedWidget) Focusable() bool { return true }
func (w *BorderedWidget) SetX(x int) { w.x = x }
func (w *BorderedWidget) SetY(y int) { w.y = y }
func (w *BorderedWidget) GetX() int { return w.x }
func (w *BorderedWidget) GetY() int { return w.y }
func (w *BorderedWidget) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *BorderedWidget) GetW() int { return w.w }
func (w *BorderedWidget) GetH() int { return w.h }
func (w *BorderedWidget) SetW(wd int) { w.w = wd }
func (w *BorderedWidget) SetH(h int) { w.h = h }
func (w *BorderedWidget) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *BorderedWidget) WantW() int { return w.w }
func (w *BorderedWidget) WantH() int { return w.h }
func (w *BorderedWidget) SetBorder(r []rune) { w.border = r }

130
button.go Normal file
View File

@@ -0,0 +1,130 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
"strings"
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Button struct {
id string
label string
style tcell.Style
x, y int
w, h int
active bool
visible bool
onPressed func() bool
}
var _ Widget = (*Button)(nil)
func NewButton(id string, style tcell.Style) *Button {
b := &Button{style: style}
b.Init(id)
return b
}
func (w *Button) Init(id string) {
w.id = id
w.visible = true
w.onPressed = func() bool { return false }
}
func (w *Button) Id() string { return w.id }
func (w *Button) HandleResize(ev *tcell.EventResize) {}
func (w *Button) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
if ev.Key() == tcell.KeyEnter {
return w.onPressed()
}
return false
}
func (w *Button) HandleTime(ev *tcell.EventTime) {}
func (w *Button) Draw(screen tcell.Screen) {
if !w.visible {
return
}
dStyle := w.style
if w.active {
dStyle = w.style.Bold(true)
}
if w.h == 1 {
lbl := w.label
if w.w < len(lbl) {
lbl = lbl[:w.w]
} else if w.w == len(w.label)+2 {
lbl = fmt.Sprintf("[%s]", lbl)
} else if w.w > len(w.label)+2 {
lbl = fmt.Sprintf("[%s]", h.Center(lbl, w.w-2))
}
h.DrawText(w.x, w.y, lbl, dStyle, screen)
} else if w.h == 2 {
lbl := w.label
if w.w < len(lbl) {
lbl = lbl[:w.w]
} else if w.w == len(lbl)+2 {
lbl = fmt.Sprintf("╭%s╮", lbl)
}
h.DrawText(w.x, w.y, lbl, dStyle, screen)
h.DrawText(w.x, w.y+1, fmt.Sprintf("╰%s╯", strings.Repeat("─", len(lbl)-2)), dStyle, screen)
return
}
if w.w < 2 {
h.DrawText(w.x, w.y, "╬", dStyle, screen)
return
} else if w.w == 2 {
h.DrawText(w.x, w.y, "[]", dStyle, screen)
return
}
lbl := h.Center(w.label, w.w-2)
h.DrawText(w.x, w.y, fmt.Sprintf("╭%s╮", strings.Repeat("─", w.w-2)), dStyle, screen)
h.DrawText(w.x, w.y, fmt.Sprintf("│%s│", h.Center(lbl, w.w-2)), dStyle, screen)
h.DrawText(w.x, w.y, fmt.Sprintf("╰%s╯", strings.Repeat("─", w.w-2)), dStyle, screen)
}
func (w *Button) Active() bool { return w.active }
func (w *Button) SetActive(a bool) { w.active = a }
func (w *Button) Visible() bool { return w.visible }
func (w *Button) SetVisible(a bool) { w.visible = a }
func (w *Button) SetX(x int) { w.x = x }
func (w *Button) SetY(y int) { w.y = y }
func (w *Button) GetX() int { return w.x }
func (w *Button) GetY() int { return w.y }
func (w *Button) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Button) SetW(x int) { w.w = x }
func (w *Button) SetH(y int) { w.h = y }
func (w *Button) GetW() int { return w.w }
func (w *Button) GetH() int { return w.y }
func (w *Button) WantW() int { return 2 + len(w.label) }
func (w *Button) WantH() int { return 3 }
func (w *Button) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Button) Focusable() bool { return true }
func (w *Button) SetLabel(l string) { w.label = l }
func (w *Button) SetOnPressed(p func() bool) { w.onPressed = p }

128
checkbox.go Normal file
View File

@@ -0,0 +1,128 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
const (
CHECKBOX_ON = iota
CHECKBOX_OFF
CHECKBOX_MAYBE
)
type Checkbox struct {
id string
label string
style tcell.Style
active bool
visible bool
state int
x, y int
w, h int
stateRunes []rune
}
var _ Widget = (*Checkbox)(nil)
func NewCheckbox(id string, style tcell.Style) *Checkbox {
ret := &Checkbox{style: style}
ret.Init(id)
return ret
}
func (w *Checkbox) Init(id string) {
w.id = id
w.visible = true
w.stateRunes = []rune{'X', ' ', '-'}
}
func (w *Checkbox) Id() string { return w.id }
func (w *Checkbox) HandleResize(ev *tcell.EventResize) {}
func (w *Checkbox) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
if ev.Key() == tcell.KeyEnter {
if w.state == CHECKBOX_ON {
w.state = CHECKBOX_OFF
} else {
w.state = CHECKBOX_ON
}
return true
}
return false
}
func (w *Checkbox) HandleTime(ev *tcell.EventTime) {}
func (w *Checkbox) Draw(screen tcell.Screen) {
if !w.visible {
return
}
dStyle := w.style
if w.active {
dStyle = w.style.Bold(true)
}
h.DrawText(w.x, w.y, fmt.Sprintf("[%s] %s", string(w.state), w.label), dStyle, screen)
}
func (w *Checkbox) Active() bool { return w.active }
func (w *Checkbox) SetActive(a bool) { w.active = a }
func (w *Checkbox) Visible() bool { return w.visible }
func (w *Checkbox) SetVisible(a bool) { w.visible = a }
func (w *Checkbox) SetX(x int) { w.x = x }
func (w *Checkbox) SetY(y int) { w.y = y }
func (w *Checkbox) GetX() int { return w.x }
func (w *Checkbox) GetY() int { return w.y }
func (w *Checkbox) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Checkbox) SetW(x int) { w.w = x }
func (w *Checkbox) SetH(y int) { w.h = y }
func (w *Checkbox) GetW() int { return w.w }
func (w *Checkbox) GetH() int { return w.y }
func (w *Checkbox) WantW() int {
return len(fmt.Sprintf("[%s] %s", string(w.state), w.label))
}
func (w *Checkbox) WantH() int { return 1 }
func (w *Checkbox) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Checkbox) Focusable() bool { return true }
func (w *Checkbox) SetLabel(l string) { w.label = l }
func (w *Checkbox) SetChecked(v bool) {
if v {
w.state = CHECKBOX_ON
} else {
w.state = CHECKBOX_OFF
}
}
func (w *Checkbox) IsChecked() bool { return w.state == CHECKBOX_ON }
func (w *Checkbox) SetToMaybe() { w.state = CHECKBOX_MAYBE }
func (w *Checkbox) SetStateRunes(runes []rune) {
for i := range runes {
if i > 2 {
return
}
w.stateRunes[i] = runes[i]
}
}

214
field.go Normal file
View File

@@ -0,0 +1,214 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Field struct {
id string
style tcell.Style
label string
value string
cursor int
visible bool
active bool
x, y int
w, h int
filter func(*tcell.EventKey) bool
onChange func(prev, curr string)
}
var _ Widget = (*Field)(nil)
func NewField(id string, style tcell.Style) *Field {
f := &Field{style: style}
f.Init(id)
return f
}
func (w *Field) Init(id string) {
w.id = id
w.visible = true
w.filter = func(ev *tcell.EventKey) bool {
return h.IsBS(*ev) ||
h.KeyIsDisplayable(*ev)
}
}
func (w *Field) Id() string { return w.id }
func (w *Field) HandleResize(ev *tcell.EventResize) {}
func (w *Field) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
if h.IsBS(*ev) {
return w.handleBackspace()
}
if h.HandleKeys(*ev, map[tcell.Key]func() bool{
tcell.KeyLeft: w.handleLeft,
tcell.KeyRight: w.handleRight,
tcell.KeyHome: w.handleHome,
tcell.KeyEnd: w.handleEnd,
tcell.KeyCtrlU: w.clearValueBeforeCursor,
}) {
return true
}
if w.filter != nil && !w.filter(ev) {
return false
}
if ev.Key() == tcell.KeyRune {
w.SetValue(fmt.Sprintf("%s%s%s", w.value[:w.cursor], string(ev.Rune()), w.value[w.cursor:]))
w.cursor++
return true
}
return false
}
func (w *Field) HandleTime(ev *tcell.EventTime) {}
func (w *Field) Draw(screen tcell.Screen) {
if !w.visible {
return
}
useStyle := w.style
if w.active {
useStyle = w.style.Bold(true)
}
x := w.x
labelW := len(w.label)
if labelW > 0 {
h.DrawText(w.x, w.y, w.label+": ", useStyle, screen)
x = x + labelW + 2
}
cursor := " "
var pre, post string
if len(w.value) > 0 {
pre = w.value[:w.cursor]
if w.cursor < len(w.value) {
cursor = string(w.value[w.cursor])
post = w.value[w.cursor+1:]
}
}
h.DrawText(x, w.y, pre, useStyle, screen)
x += len(pre)
if w.active {
h.DrawText(x, w.y, cursor, useStyle.Reverse(true).Blink(true), screen)
} else {
h.DrawText(x, w.y, cursor, useStyle, screen)
}
x += 1
h.DrawText(x, w.y, post, useStyle, screen)
}
func (w *Field) Active() bool { return w.active }
func (w *Field) SetActive(a bool) { w.active = a }
func (w *Field) Visible() bool { return w.visible }
func (w *Field) SetVisible(a bool) { w.visible = a }
func (w *Field) SetX(x int) { w.x = x }
func (w *Field) SetY(y int) { w.y = y }
func (w *Field) GetX() int { return w.x }
func (w *Field) GetY() int { return w.y }
func (w *Field) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Field) SetW(wd int) { w.w = wd }
func (w *Field) SetH(h int) { w.h = h }
func (w *Field) GetW() int { return w.w }
func (w *Field) GetH() int { return w.h }
func (w *Field) WantW() int {
return len(w.label) + 2 + len(w.value) + 1
}
func (w *Field) WantH() int {
return 1
}
func (w *Field) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Field) Focusable() bool { return true }
/* Non-Widget-Interface Functions */
func (w *Field) handleBackspace() bool {
st := w.cursor
if w.cursor > 0 {
w.cursor--
}
if w.cursor < len(w.value) {
w.SetValue(w.value[:w.cursor] + w.value[w.cursor+1:])
}
if st != w.cursor {
return true
}
return false
}
func (w *Field) handleLeft() bool {
if w.cursor > 0 {
w.cursor--
return true
}
return false
}
func (w *Field) handleRight() bool {
if w.cursor < len(w.value) {
w.cursor++
return true
}
return false
}
func (w *Field) clearValueBeforeCursor() bool {
w.SetValue(w.value[w.cursor:])
w.cursor = 0
return true
}
func (w *Field) handleHome() bool {
w.cursor = 0
return true
}
func (w *Field) handleEnd() bool {
w.cursor = len(w.value)
return true
}
func (w *Field) SetLabel(l string) { w.label = l }
func (w *Field) Label() string { return w.label }
func (w *Field) SetValue(v string) {
prev := w.value
w.value = v
w.doOnChange(prev, v)
}
func (w *Field) Value() string { return w.value }
func (w *Field) SetOnChange(v func(prev, curr string)) { w.onChange = v }
func (w *Field) doOnChange(prev, curr string) {
if w.onChange != nil {
w.onChange(prev, curr)
}
}

227
menu.go Normal file
View File

@@ -0,0 +1,227 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Menu struct {
id string
label string
style tcell.Style
active bool
visible bool
x, y int
w, h int
menuType MenuType
cursor int
items []Widget
onPressed func() bool
manualExpand bool
expanded bool
}
type MenuType int
const (
MenuTypeH = MenuType(iota)
MenuTypeV
)
func NewMenu(id string, style tcell.Style) *Menu {
ret := &Menu{style: style}
ret.Init(id)
return ret
}
func (w *Menu) Id() string { return w.id }
func (w *Menu) Init(id string) {
w.id = id
w.visible = true
}
func (w *Menu) HandleResize(ev *tcell.EventResize) {}
func (w *Menu) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
// See if the active menuitem consumes this event
if w.items[w.cursor].HandleKey(ev) {
return true
}
// Otherwise see if we handle it
if h.HandleKeys(*ev, map[tcell.Key]func() bool{
tcell.KeyRight: w.MoveRight,
tcell.KeyLeft: w.MoveLeft,
tcell.KeyUp: w.MoveUp,
tcell.KeyDown: w.MoveDown,
tcell.KeyEnter: func() bool {
if w.onPressed != nil {
return w.onPressed()
}
return false
},
}) {
return true
}
// See if we can find an item that matches the key pressed
return false
}
func (w *Menu) HandleTime(ev *tcell.EventTime) {}
func (w *Menu) Draw(screen tcell.Screen) {
switch w.menuType {
case MenuTypeH:
w.drawHMenu(screen)
case MenuTypeV:
w.drawVMenu(screen)
}
}
func (w *Menu) drawHMenu(screen tcell.Screen) {
st := w.style
if w.active {
st = w.style.Reverse(true)
}
x, y := w.x, w.y
if len(w.label) > 0 {
h.DrawText(x, y, w.label, st, screen)
x = x + len(w.label) + 1
}
h.DrawText(x, y, "-", w.style, screen)
x += 2
for i := range w.items {
w.items[i].SetActive(w.active && w.cursor == i)
w.items[i].SetPos(Coord{X: x, Y: y})
w.items[i].Draw(screen)
x += w.items[i].WantW() + 1
}
}
func (w *Menu) drawVMenu(screen tcell.Screen) {
if !w.visible {
return
}
x, y := w.x, w.y
wW, wH := w.WantW(), w.WantH()
st := w.style
if w.active {
st = w.style.Reverse(true)
h.TitledBorderFilled(x-1, y, x+w.WantW(), y+w.WantH(), w.label, h.BRD_CSIMPLE, w.style, screen)
}
h.DrawText(w.x, w.y, w.label, st, screen)
y++
if w.expanded || (w.active && !w.manualExpand) {
for i := range w.items {
w.items[i].SetActive(w.active && w.cursor == i)
w.items[i].SetSize(Coord{X: wW, Y: wH})
w.items[i].SetPos(Coord{X: x, Y: y})
w.items[i].Draw(screen)
y++
}
y++
}
}
func (w *Menu) Active() bool { return w.active }
func (w *Menu) SetActive(a bool) { w.active = a }
func (w *Menu) Visible() bool { return w.visible }
func (w *Menu) SetVisible(a bool) { w.visible = a }
func (w *Menu) SetX(x int) { w.x = x }
func (w *Menu) SetY(y int) { w.y = y }
func (w *Menu) GetX() int { return w.x }
func (w *Menu) GetY() int { return w.y }
func (w *Menu) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Menu) SetW(x int) { w.w = x }
func (w *Menu) SetH(y int) { w.h = y }
func (w *Menu) GetW() int { return w.w }
func (w *Menu) GetH() int { return w.y }
func (w *Menu) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Menu) Focusable() bool { return true }
func (w *Menu) WantW() int {
var maxW int
for i := range w.items {
if w.items[i].WantW() > maxW {
maxW = w.items[i].WantW()
}
}
return maxW
}
func (w *Menu) WantH() int {
if w.menuType == MenuTypeH {
return 1
} else {
var ret int
if len(w.label) > 0 {
ret = 1
}
return ret + len(w.items)
}
}
func (w *Menu) SetType(tp MenuType) { w.menuType = tp }
func (w *Menu) SetLabel(lbl string) { w.label = lbl }
func (w *Menu) SetFocusable(f bool) {}
func (w *Menu) SetOnPressed(p func() bool) { w.onPressed = p }
func (w *Menu) AddItems(iL ...Widget) {
var maxW int
for i := range iL {
if iL[i].WantW() > maxW {
maxW = iL[i].WantW()
}
w.items = append(w.items, iL[i])
}
w.SetW(maxW)
}
func (w *Menu) MoveRight() bool {
if w.menuType != MenuTypeH {
return false
}
w.cursor = (w.cursor + 1) % len(w.items)
return true
}
func (w *Menu) MoveLeft() bool {
if w.menuType != MenuTypeH {
return false
}
w.cursor = (w.cursor - 1 + len(w.items)) % len(w.items)
return true
}
func (w *Menu) MoveUp() bool {
if w.menuType != MenuTypeV {
return false
}
w.cursor = (w.cursor - 1 + len(w.items)) % len(w.items)
return true
}
func (w *Menu) MoveDown() bool {
if w.menuType != MenuTypeV {
return false
}
w.cursor = (w.cursor + 1) % len(w.items)
return true
}

148
menu_item.go Normal file
View File

@@ -0,0 +1,148 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type MenuItem struct {
id string
label string
style tcell.Style
active bool
visible bool
x, y int
w, h int
menuType MenuType
items []*MenuItem
onPressed func() bool
manualExpand bool
expanded bool
disabled bool
}
func NewMenuItem(id string, style tcell.Style) *MenuItem {
ret := &MenuItem{style: style}
ret.Init(id)
return ret
}
func (w *MenuItem) Init(id string) {
w.id = id
w.visible = true
}
func (w *MenuItem) Id() string { return w.id }
func (w *MenuItem) HandleResize(ev *tcell.EventResize) {}
func (w *MenuItem) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
if ev.Key() == tcell.KeyEnter {
if w.onPressed != nil {
return w.onPressed()
}
}
return false
}
func (w *MenuItem) HandleTime(ev *tcell.EventTime) {}
func (w *MenuItem) Draw(screen tcell.Screen) {
if !w.visible {
return
}
st := w.style
if w.active {
st = w.style.Reverse(true)
}
x, y := w.x, w.y
wd := w.w
h.DrawText(x, y, h.PadR(w.label, wd), st, screen)
if w.expanded {
x += 2
for i := range w.items {
w.items[i].SetPos(Coord{X: x, Y: y})
w.items[i].Draw(screen)
y++
}
}
}
func (w *MenuItem) Active() bool { return w.active }
func (w *MenuItem) SetActive(a bool) {
w.active = a
if !w.manualExpand {
w.expanded = a
}
}
func (w *MenuItem) Visible() bool { return w.visible }
func (w *MenuItem) SetVisible(a bool) { w.visible = a }
func (w *MenuItem) SetX(x int) { w.x = x }
func (w *MenuItem) SetY(y int) { w.y = y }
func (w *MenuItem) GetX() int { return w.x }
func (w *MenuItem) GetY() int { return w.y }
func (w *MenuItem) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *MenuItem) SetW(x int) { w.w = x }
func (w *MenuItem) SetH(y int) { w.h = y }
func (w *MenuItem) GetW() int { return w.w }
func (w *MenuItem) GetH() int { return w.y }
func (w *MenuItem) WantW() int {
ret := len(w.label) + 2
if len(w.items) > 0 {
for i := range w.items {
if w.items[i].WantW() > ret {
ret = w.items[i].WantW()
// TODO: Figure offset of subitems
}
}
}
return ret
}
func (w *MenuItem) WantH() int {
ret := 1
if len(w.items) > 0 {
for i := range len(w.items) {
ret += w.items[i].WantH()
}
}
return ret
}
func (w *MenuItem) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *MenuItem) Focusable() bool { return !w.disabled }
// How much width this item wants
func (w *MenuItem) Expand(e bool) {
if w.manualExpand {
w.expanded = e
}
}
func (w *MenuItem) SetLabel(lbl string) { w.label = lbl }
func (w *MenuItem) SetDisabled(d bool) { w.disabled = d }
func (w *MenuItem) AddItems(iL ...*MenuItem) {
w.items = append(w.items, iL...)
}
func (w *MenuItem) SetOnPressed(p func() bool) { w.onPressed = p }

108
relative_layout.go Normal file
View File

@@ -0,0 +1,108 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import "github.com/gdamore/tcell"
type RelativeLayout struct {
id string
style tcell.Style
widgetRelations map[Widget]widgetRelation
active bool
visible bool
x, y int
w, h int
}
var _ Widget = (*RelativeLayout)(nil)
type widgetRelation struct {
relation RelativeRelation
relTo Widget
}
type RelativeRelation int
const (
RRAbove = iota // Above Widget
RRToRightOf // To Right of Widget
RRBelow // Below Widget
RRToLeftOf // To Left of Widget
RATop // Anchored to parent Top
RARight // Anchored to parent Right
RABottom // Anchored to parent Bottom
RALeft // Anchored to parent Left
)
func NewRelativeLayout(id string, s tcell.Style) *RelativeLayout {
ret := &RelativeLayout{style: s}
ret.Init(id)
return ret
}
func (w *RelativeLayout) Init(id string) {
w.id = id
w.visible = true
}
func (w *RelativeLayout) Id() string { return w.id }
func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) {}
func (w *RelativeLayout) HandleKey(ev *tcell.EventKey) bool { return false }
func (w *RelativeLayout) HandleTime(ev *tcell.EventTime) {}
func (w *RelativeLayout) Draw(screen tcell.Screen) {
if !w.visible {
return
}
done := make(map[Widget]widgetRelation)
rem := make(map[Widget]widgetRelation)
for k, v := range w.widgetRelations {
if v.relTo == w {
done[k] = v
} else {
rem[k] = v
}
}
}
func (w *RelativeLayout) Active() bool { return w.active }
func (w *RelativeLayout) SetActive(a bool) { w.active = a }
func (w *RelativeLayout) Visible() bool { return w.visible }
func (w *RelativeLayout) SetVisible(a bool) { w.visible = a }
func (w *RelativeLayout) Focusable() bool { return true }
func (w *RelativeLayout) SetX(x int) { w.x = x }
func (w *RelativeLayout) SetY(y int) { w.y = y }
func (w *RelativeLayout) GetX() int { return w.x }
func (w *RelativeLayout) GetY() int { return w.y }
func (w *RelativeLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *RelativeLayout) SetW(wd int) { w.w = wd }
func (w *RelativeLayout) SetH(h int) { w.h = h }
func (w *RelativeLayout) GetW() int { return w.w }
func (w *RelativeLayout) GetH() int { return w.h }
func (w *RelativeLayout) WantW() int { return 1 }
func (w *RelativeLayout) WantH() int { return 1 }
func (w *RelativeLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *RelativeLayout) Add(n Widget, relTo Widget, relation RelativeRelation) {
w.widgetRelations[n] = widgetRelation{
relation: relation,
relTo: relTo,
}
}

297
searcher.go Normal file
View File

@@ -0,0 +1,297 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
"strings"
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Searcher struct {
id string
style tcell.Style
x, y int
w, h int
active bool
visible bool
title string
search *Field
data []string
filteredData []string
cursor int
selectFunc func(idx int, s string) bool
hideOnSelect bool
logger func(string)
}
var _ Widget = (*Searcher)(nil)
func NewSearcher(id string, style tcell.Style) *Searcher {
ret := &Searcher{
style: style,
search: NewField(fmt.Sprintf("%s-searcher-field", id), style),
}
ret.Init(id)
return ret
}
func (w *Searcher) Init(id string) {
w.id = id
w.visible = true
w.search.SetOnChange(func(prev, curr string) {
w.updateFilter()
w.Log("Updated Search:" + curr)
})
}
func (w *Searcher) Id() string { return w.id }
func (w *Searcher) HandleResize(ev *tcell.EventResize) {}
func (w *Searcher) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
if h.HandleKeys(*ev, map[tcell.Key]func() bool{
tcell.KeyUp: w.handleKeyUp,
tcell.KeyDown: w.handleKeyDown,
tcell.KeyHome: w.handleKeyHome,
tcell.KeyEnd: w.handleKeyEnd,
tcell.KeyPgUp: w.handleKeyPgUp,
tcell.KeyPgDn: w.handleKeyPgDn,
tcell.KeyEnter: w.handleKeyEnter,
}) {
return true
}
return w.search.HandleKey(ev)
}
func (w *Searcher) handleKeyUp() bool {
w.cursor = ((w.cursor - 1) + len(w.filteredData)) % len(w.filteredData)
return true
}
func (w *Searcher) handleKeyDown() bool {
w.cursor = ((w.cursor + 1) + len(w.filteredData)) % len(w.filteredData)
return true
}
func (w *Searcher) handleKeyHome() bool {
if w.cursor == 0 {
return false
}
w.cursor = 0
return true
}
func (w *Searcher) handleKeyEnd() bool {
if w.cursor == len(w.filteredData)-1 {
return false
}
w.cursor = len(w.filteredData) - 1
return true
}
func (w *Searcher) handleKeyPgUp() bool {
if w.cursor == 0 {
return false
}
w.cursor -= w.h
if w.cursor < 0 {
w.cursor = 0
}
return false
}
func (w *Searcher) handleKeyPgDn() bool {
mx := len(w.filteredData) - 1
if w.cursor == mx {
return false
}
w.cursor += w.h
if w.cursor > mx {
w.cursor = mx
}
return false
}
func (w *Searcher) handleKeyEnter() bool {
if w.hideOnSelect {
w.visible = false
}
if w.selectFunc != nil {
// Figure out our true index
var idx int
selV := w.filteredData[w.cursor]
for i := range w.data {
if w.data[i] == selV {
idx = i
}
}
return w.selectFunc(idx, selV)
}
return false
}
func (w *Searcher) HandleTime(ev *tcell.EventTime) {}
func (w *Searcher) Draw(screen tcell.Screen) {
if !w.visible {
return
}
w.search.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
w.search.SetSize(Coord{X: w.w - 2, Y: 1})
dStyle := w.style
if !w.active {
dStyle = dStyle.Dim(true)
}
if len(w.title) > 0 {
h.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, h.BRD_CSIMPLE, dStyle, screen)
} else {
h.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, h.BRD_CSIMPLE, dStyle, screen)
}
w.search.Draw(screen)
x, y := w.x+1, w.y+2
var stIdx int
if w.cursor > w.h/2 {
stIdx = w.cursor - (w.h / 2)
}
fD := len(w.filteredData)
if w.cursor+w.h/2 > fD {
stIdx = fD - w.h/2
}
stIdx = h.Max(stIdx, 0)
for i := stIdx; i < fD; i++ {
st := dStyle
if i == w.cursor {
st = st.Reverse(true)
}
h.DrawText(x, y, w.filteredData[i], st, screen)
y++
if y >= w.y+w.h {
break
}
}
}
func (w *Searcher) Active() bool { return w.active }
func (w *Searcher) SetActive(a bool) {
w.active = a
w.search.SetActive(a)
}
func (w *Searcher) Visible() bool { return w.visible }
func (w *Searcher) SetVisible(a bool) { w.visible = a }
func (w *Searcher) SetX(x int) { w.x = x }
func (w *Searcher) SetY(y int) { w.y = y }
func (w *Searcher) GetX() int { return w.x }
func (w *Searcher) GetY() int { return w.y }
func (w *Searcher) WantW() int {
ret := 2 + w.search.WantW()
var maxData int
for i := range w.filteredData {
maxData = h.Max(maxData, len(w.filteredData[i]))
}
return ret + maxData
}
func (w *Searcher) WantH() int {
return 2 + w.search.WantH() + len(w.filteredData) // Border + Field + Data
}
func (w *Searcher) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Searcher) SetW(x int) { w.w = x }
func (w *Searcher) SetH(y int) { w.h = y }
func (w *Searcher) GetW() int { return w.w }
func (w *Searcher) GetH() int { return w.y }
func (w *Searcher) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Searcher) Focusable() bool { return true }
func (w *Searcher) SetHideOnSelect(t bool) { w.hideOnSelect = t }
func (w *Searcher) SetTitle(ttl string) { w.title = ttl }
func (w *Searcher) SetData(data []string) {
w.data = data
w.updateFilter()
}
func (w *Searcher) updateFilter() {
var selVal string
var data []string
copy(data, w.filteredData)
if len(data) > 0 && len(data) > w.cursor {
selVal = data[w.cursor]
}
filter := w.search.Value()
cS := filter != strings.ToLower(filter)
w.filteredData = []string{}
for i := range w.data {
if cS {
if strings.Contains(w.data[i], filter) {
w.filteredData = append(w.filteredData, w.data[i])
}
} else {
if strings.Contains(strings.ToLower(w.data[i]), filter) {
w.filteredData = append(w.filteredData, w.data[i])
}
}
}
for i := range w.filteredData {
if w.filteredData[i] == selVal {
w.cursor = i
return
}
}
if w.cursor > len(w.filteredData) {
w.cursor = len(w.filteredData) - 1
return
}
w.cursor = 0
}
func (w *Searcher) SelectedValue() string {
return w.filteredData[w.cursor]
}
func (w *Searcher) SetSearchValue(val string) {
w.search.SetValue(val)
}
func (w *Searcher) SetSelectFunc(f func(idx int, s string) bool) {
w.selectFunc = f
}
func (w *Searcher) ClearSearch() {
w.cursor = 0
w.filteredData = w.data
w.search.SetValue("")
}
func (w *Searcher) SetLogger(l func(string)) { w.logger = l }
func (w *Searcher) Log(txt string) {
if w.logger != nil {
w.logger(txt)
}
}

201
table.go Normal file
View File

@@ -0,0 +1,201 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
"fmt"
"strings"
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Table struct {
id string
title string
style tcell.Style
active bool
visible bool
focusable bool
header []string
footer []string
data [][]string
minimized bool
cursorX, cursorY int
wrapColumns bool
x, y int
w, h int
wantW, wantH int
columnWidths []int
}
var _ Widget = (*Table)(nil)
type TableSelectMode int
const (
TableSelectCell = iota
TableSelectRow
TableSelectColumn
)
type TableSortDirection int
const (
TableSortAsc = iota
TableSortDesc
)
func NewTable(id string, style tcell.Style) *Table {
ret := &Table{style: style}
ret.Init(id)
return ret
}
func (w *Table) Init(id string) {
w.id = id
w.visible = true
}
func (w *Table) Id() string { return w.id }
func (w *Table) HandleResize(ev *tcell.EventResize) {}
func (w *Table) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
return false
}
func (w *Table) HandleTime(ev *tcell.EventTime) {}
func (w *Table) Draw(screen tcell.Screen) {
if !w.visible {
return
}
dStyle := w.style
if w.active {
dStyle = w.style.Bold(true)
}
var dat [][]string
for i := range w.data {
dat = append(dat, w.data[i])
}
x, y := w.x, w.y
width, height := w.w, w.h
if width <= 0 || height <= 0 {
return
}
if !w.active {
dStyle = dStyle.Dim(true)
}
if w.minimized {
h.DrawText(x, y, fmt.Sprintf("├%s (%d rows)┤", w.title, len(dat)), dStyle, screen)
return
}
if w.title != "" {
h.TitledBorder(x, y, w.x+width, w.y+height, w.title, h.BRD_CSIMPLE, dStyle, screen)
} else {
h.Border(x, y, w.x+width, w.y+height, h.BRD_CSIMPLE, dStyle, screen)
}
if len(w.header) > 0 {
h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen)
y++
}
if len(dat) == 0 {
h.DrawText(x, y, "No data in table", dStyle, screen)
y++
} else {
for i := range dat {
h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(dat[i], "│")), dStyle, screen)
y++
}
}
if len(w.footer) > 0 {
h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen)
y++
}
}
func (w *Table) Active() bool { return w.active }
func (w *Table) SetActive(a bool) { w.active = a }
func (w *Table) Visible() bool { return w.visible }
func (w *Table) SetVisible(a bool) { w.visible = a }
func (w *Table) SetX(x int) { w.x = x }
func (w *Table) SetY(y int) { w.y = y }
func (w *Table) GetX() int { return w.x }
func (w *Table) GetY() int { return w.y }
func (w *Table) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Table) SetW(x int) { w.w = x }
func (w *Table) SetH(y int) { w.h = y }
func (w *Table) GetW() int { return w.w }
func (w *Table) GetH() int { return w.y }
func (w *Table) WantW() int {
if w.minimized {
return len(fmt.Sprintf("├%s (%d rows)┤", w.title, len(w.data)))
}
// For each column, find the longest (in header, data, and footer)
var totalW int
colCnt := h.Max(len(w.header), len(w.footer))
for i := range w.data {
colCnt = h.Max(colCnt, len(w.data[i]))
}
for i := 0; i < colCnt; i++ {
var cols []int
if len(w.header) > i {
cols = append(cols, len(w.header[i]))
}
for j := range w.data {
if len(w.data[j]) > i {
cols = append(cols, len(w.data[j][i]))
}
}
if len(w.footer) > i {
cols = append(cols, len(w.footer[i]))
}
totalW += h.Max(cols...)
}
return totalW
}
func (w *Table) WantH() int {
if w.minimized {
return 1
}
datLen := len(w.data) + 2 // Data length + Border
if len(w.header) > 0 {
datLen += len(w.header) + 1 // Header length + separator
}
if datLen == 0 {
datLen = 1
}
if len(w.footer) > 0 {
datLen += len(w.footer) + 1 // Footer length + separator
}
return datLen
}
func (w *Table) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Table) Focusable() bool { return w.focusable }
func (w *Table) SetTitle(ttl string) { w.title = ttl }
func (w *Table) SetFocusable(f bool) { w.focusable = f }

78
text.go Normal file
View File

@@ -0,0 +1,78 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import (
h "git.bullercodeworks.com/brian/dhcli/helpers"
"github.com/gdamore/tcell"
)
type Text struct {
id string
text string
style tcell.Style
x, y int
w, h int
visible bool
}
var _ Widget = (*Text)(nil)
func NewText(id string, style tcell.Style) *Text {
ret := &Text{style: style}
ret.Init(id)
return ret
}
func (w *Text) Init(id string) {
w.id = id
w.visible = true
}
func (w *Text) Id() string { return w.id }
func (w *Text) HandleResize(ev *tcell.EventResize) {}
func (w *Text) HandleKey(ev *tcell.EventKey) bool { return false }
func (w *Text) HandleTime(ev *tcell.EventTime) {}
func (w *Text) Draw(screen tcell.Screen) {
if !w.visible {
return
}
h.DrawText(w.x, w.y, w.text, w.style, screen)
}
func (w *Text) Active() bool { return false }
func (w *Text) SetActive(a bool) {}
func (w *Text) Visible() bool { return w.visible }
func (w *Text) SetVisible(a bool) { w.visible = a }
func (w *Text) SetX(x int) { w.x = x }
func (w *Text) SetY(y int) { w.y = y }
func (w *Text) GetX() int { return w.x }
func (w *Text) GetY() int { return w.y }
func (w *Text) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Text) SetW(x int) { w.w = x }
func (w *Text) SetH(y int) { w.h = y }
func (w *Text) GetW() int { return w.w }
func (w *Text) GetH() int { return w.y }
func (w *Text) WantW() int { return h.Max(w.w, len(w.text)) }
func (w *Text) WantH() int { return h.Max(w.h, 1) }
func (w *Text) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Text) Focusable() bool { return false }
func (w *Text) SetText(txt string) { w.text = txt }

80
widget.go Normal file
View File

@@ -0,0 +1,80 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package widgets
import "github.com/gdamore/tcell"
type Widget interface {
Init(id string)
Id() string
HandleResize(*tcell.EventResize)
HandleKey(*tcell.EventKey) bool
HandleTime(*tcell.EventTime)
Draw(tcell.Screen)
Active() bool
SetActive(bool)
Visible() bool
SetVisible(bool)
Focusable() bool
SetX(int)
SetY(int)
GetX() int
GetY() int
SetPos(Coord)
SetW(int)
SetH(int)
GetW() int
GetH() int
WantW() int
WantH() int
SetSize(Coord)
}
type Coord struct {
X, Y int
}
func (p *Coord) Add(o Coord) Coord {
return Coord{
X: p.X + o.X,
Y: p.Y + o.Y,
}
}
// To validate that a struct satisfies this interface, you can do:
// var _ Widget - (*<struct>)(nil)
// where <struct> is the actual struct that you're validating.
type WidgetList []Widget
func (w *WidgetList) Contains(id string) bool {
return w.Find(id) != nil
}
func (w *WidgetList) Find(id string) Widget {
for _, wi := range *w {
if wi.Id() == id {
return wi
}
}
return nil
}