commit 9688b2930f084035f77c14b12efcbe958a289c2f Author: Brian Buller Date: Wed Jun 18 07:13:07 2025 -0500 First Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/absolute_layout.go b/absolute_layout.go new file mode 100644 index 0000000..ea18cec --- /dev/null +++ b/absolute_layout.go @@ -0,0 +1,129 @@ +/* +Copyright © Brian Buller + +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) +} diff --git a/bordered_widget.go b/bordered_widget.go new file mode 100644 index 0000000..6c9eb6f --- /dev/null +++ b/bordered_widget.go @@ -0,0 +1,110 @@ +/* +Copyright © Brian Buller + +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 } diff --git a/button.go b/button.go new file mode 100644 index 0000000..822f690 --- /dev/null +++ b/button.go @@ -0,0 +1,130 @@ +/* +Copyright © Brian Buller + +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 } diff --git a/checkbox.go b/checkbox.go new file mode 100644 index 0000000..751b50b --- /dev/null +++ b/checkbox.go @@ -0,0 +1,128 @@ +/* +Copyright © Brian Buller + +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] + } +} diff --git a/field.go b/field.go new file mode 100644 index 0000000..7f0d2d8 --- /dev/null +++ b/field.go @@ -0,0 +1,214 @@ +/* +Copyright © Brian Buller + +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) + } +} diff --git a/menu.go b/menu.go new file mode 100644 index 0000000..6f6c304 --- /dev/null +++ b/menu.go @@ -0,0 +1,227 @@ +/* +Copyright © Brian Buller + +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 +} diff --git a/menu_item.go b/menu_item.go new file mode 100644 index 0000000..6e709d2 --- /dev/null +++ b/menu_item.go @@ -0,0 +1,148 @@ +/* +Copyright © Brian Buller + +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 } diff --git a/relative_layout.go b/relative_layout.go new file mode 100644 index 0000000..9c7f46f --- /dev/null +++ b/relative_layout.go @@ -0,0 +1,108 @@ +/* +Copyright © Brian Buller + +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, + } +} diff --git a/searcher.go b/searcher.go new file mode 100644 index 0000000..6d23605 --- /dev/null +++ b/searcher.go @@ -0,0 +1,297 @@ +/* +Copyright © Brian Buller + +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) + } +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..6a8584d --- /dev/null +++ b/table.go @@ -0,0 +1,201 @@ +/* +Copyright © Brian Buller + +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 } diff --git a/text.go b/text.go new file mode 100644 index 0000000..ea92857 --- /dev/null +++ b/text.go @@ -0,0 +1,78 @@ +/* +Copyright © Brian Buller + +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 } diff --git a/widget.go b/widget.go new file mode 100644 index 0000000..fc4e8c3 --- /dev/null +++ b/widget.go @@ -0,0 +1,80 @@ +/* +Copyright © Brian Buller + +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 - (*)(nil) +// where 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 +}