390 lines
9.6 KiB
Go
390 lines
9.6 KiB
Go
/*
|
|
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"
|
|
|
|
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
|
|
"github.com/gdamore/tcell"
|
|
)
|
|
|
|
type Searcher struct {
|
|
id string
|
|
style tcell.Style
|
|
|
|
x, y int
|
|
w, h int
|
|
buffer *Buffer
|
|
|
|
active bool
|
|
visible bool
|
|
focusable bool
|
|
|
|
title string
|
|
search *Field
|
|
data []string
|
|
filteredData []string
|
|
filteredToTrue map[int]int
|
|
cursor int
|
|
disableBorder bool
|
|
|
|
selectFunc func(idx int, s string) bool
|
|
onChange func(idx int, s string) bool
|
|
hideOnSelect bool
|
|
logger func(string, ...any)
|
|
|
|
keyMap *KeyMap
|
|
}
|
|
|
|
var _ Widget = (*Searcher)(nil)
|
|
|
|
func NewSearcher(id string, style tcell.Style) *Searcher {
|
|
ret := &Searcher{
|
|
search: NewField(fmt.Sprintf("%s-searcher-field", id), style),
|
|
}
|
|
ret.Init(id, style)
|
|
return ret
|
|
}
|
|
|
|
func (w *Searcher) Init(id string, style tcell.Style) {
|
|
w.id = id
|
|
w.style = style
|
|
w.visible = true
|
|
w.search.SetOnChange(func(prev, curr string) {
|
|
w.updateFilter()
|
|
})
|
|
w.keyMap = NewKeyMap(
|
|
NewKey(BuildEK(tcell.KeyUp), w.handleKeyUp),
|
|
NewKey(BuildEK(tcell.KeyDown), w.handleKeyDown),
|
|
NewKey(BuildEK(tcell.KeyHome), w.handleKeyHome),
|
|
NewKey(BuildEK(tcell.KeyEnd), w.handleKeyEnd),
|
|
NewKey(BuildEK(tcell.KeyPgUp), w.handleKeyPgUp),
|
|
NewKey(BuildEK(tcell.KeyPgDn), w.handleKeyPgDn),
|
|
NewKey(BuildEK(tcell.KeyEnter), w.handleKeyEnter),
|
|
)
|
|
w.focusable = true
|
|
w.filteredToTrue = make(map[int]int)
|
|
}
|
|
|
|
func (w *Searcher) Id() string { return w.id }
|
|
func (w *Searcher) HandleResize(ev *tcell.EventResize) {
|
|
w.w, w.h = ev.Size()
|
|
// Remove 2 from each for borders
|
|
aW, aH := w.w-2, w.h-2
|
|
if !w.disableBorder {
|
|
aW, aH = aW-2, aH-2
|
|
}
|
|
w.search.SetPos(Coord{X: 1, Y: 1})
|
|
w.search.HandleResize(Coord{X: aW, Y: aH}.ResizeEvent())
|
|
// w.buildBuffer()
|
|
}
|
|
|
|
func (w *Searcher) GetKeyMap() *KeyMap { return w.keyMap }
|
|
func (w *Searcher) SetKeyMap(km *KeyMap) { w.keyMap = km }
|
|
|
|
func (w *Searcher) HandleKey(ev *tcell.EventKey) bool {
|
|
if !w.active {
|
|
return false
|
|
}
|
|
sel := w.cursor
|
|
ret := w.keyMap.Handle(ev)
|
|
if !ret {
|
|
ret = w.search.HandleKey(ev)
|
|
}
|
|
if w.cursor != sel && w.onChange != nil {
|
|
w.onChange(w.cursor, w.filteredData[w.cursor])
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (w *Searcher) handleKeyUp(ev *tcell.EventKey) bool {
|
|
if len(w.filteredData) == 0 {
|
|
return false
|
|
}
|
|
w.cursor = ((w.cursor - 1) + len(w.filteredData)) % len(w.filteredData)
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyDown(ev *tcell.EventKey) bool {
|
|
if len(w.filteredData) == 0 {
|
|
return false
|
|
}
|
|
w.cursor = ((w.cursor + 1) + len(w.filteredData)) % len(w.filteredData)
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyHome(ev *tcell.EventKey) bool {
|
|
if w.cursor == 0 {
|
|
return false
|
|
}
|
|
w.cursor = 0
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyEnd(ev *tcell.EventKey) bool {
|
|
if w.cursor == len(w.filteredData)-1 {
|
|
return false
|
|
}
|
|
w.cursor = len(w.filteredData) - 1
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyPgUp(ev *tcell.EventKey) bool {
|
|
if w.cursor == 0 {
|
|
return false
|
|
}
|
|
w.cursor -= w.h
|
|
if w.cursor < 0 {
|
|
w.cursor = 0
|
|
}
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyPgDn(ev *tcell.EventKey) bool {
|
|
mx := len(w.filteredData) - 1
|
|
if w.cursor == mx {
|
|
return false
|
|
}
|
|
w.cursor += w.h
|
|
if w.cursor > mx {
|
|
w.cursor = mx
|
|
}
|
|
// w.buildBuffer()
|
|
return true
|
|
}
|
|
|
|
func (w *Searcher) handleKeyEnter(ev *tcell.EventKey) bool {
|
|
if w.hideOnSelect {
|
|
w.visible = false
|
|
}
|
|
if w.selectFunc == nil || len(w.filteredData) <= w.cursor {
|
|
return false
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
res := w.selectFunc(idx, selV)
|
|
// w.buildBuffer()
|
|
return res
|
|
}
|
|
|
|
func (w *Searcher) HandleTime(ev *tcell.EventTime) { w.search.HandleTime(ev) }
|
|
func (w *Searcher) Draw(screen tcell.Screen) {
|
|
if !w.visible {
|
|
return
|
|
}
|
|
w.oldDraw(screen)
|
|
// w.buffer.Draw(w.x, w.y, screen)
|
|
}
|
|
|
|
func (w *Searcher) oldDraw(screen tcell.Screen) {
|
|
dStyle := w.style.Dim(!w.active)
|
|
if !w.disableBorder {
|
|
if len(w.title) > 0 {
|
|
wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h-1, w.title, wh.BRD_CSIMPLE, dStyle, screen)
|
|
} else {
|
|
wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, wh.BRD_CSIMPLE, dStyle, screen)
|
|
}
|
|
}
|
|
// w.GetPos().DrawOffset(w.search, screen)
|
|
x, y := w.x+1, w.y+2
|
|
w.search.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
|
|
w.search.Draw(screen)
|
|
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 = wh.Max(stIdx, 0)
|
|
for i := stIdx; i < fD; i++ {
|
|
st := dStyle
|
|
if i == w.cursor {
|
|
st = st.Reverse(true)
|
|
}
|
|
wh.DrawText(x, y, w.filteredData[i], st, screen)
|
|
y++
|
|
if y >= w.y+w.h-1 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *Searcher) buildBuffer() {
|
|
b := NewBuffer()
|
|
dStyle := w.style.Dim(!w.active)
|
|
if !w.disableBorder {
|
|
if len(w.title) > 0 {
|
|
w.buffer.TitledBorderFilled(0, 0, w.w, w.y, w.title, wh.BRD_CSIMPLE, dStyle)
|
|
} else {
|
|
w.buffer.BorderFilled(0, 0, w.w, w.y, wh.BRD_CSIMPLE, dStyle)
|
|
}
|
|
}
|
|
// x, y := 1, 1
|
|
w.buffer = b
|
|
}
|
|
|
|
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 = wh.Max(maxData, len(w.filteredData[i]))
|
|
}
|
|
return ret + maxData
|
|
}
|
|
|
|
func (w *Searcher) WantH() int {
|
|
ret := w.search.WantH() + len(w.filteredData)
|
|
if !w.disableBorder {
|
|
return ret + 2
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (w *Searcher) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
|
|
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.h }
|
|
func (w *Searcher) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
|
|
func (w *Searcher) Focusable() bool { return w.focusable }
|
|
func (w *Searcher) SetFocusable(b bool) { w.focusable = b }
|
|
func (w *Searcher) MinW() int {
|
|
return 2 + w.search.MinW()
|
|
}
|
|
|
|
func (w *Searcher) MinH() int {
|
|
return 2 + w.search.MinH() + 5
|
|
}
|
|
|
|
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.filteredData = data
|
|
w.updateFilter()
|
|
}
|
|
|
|
func (w *Searcher) updateFilter() {
|
|
var selVal string
|
|
data := []string{}
|
|
w.filteredToTrue = make(map[int]int)
|
|
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.filteredToTrue[len(w.filteredData)] = i
|
|
w.filteredData = append(w.filteredData, w.data[i])
|
|
}
|
|
} else {
|
|
if strings.Contains(strings.ToLower(w.data[i]), filter) {
|
|
w.filteredToTrue[len(w.filteredData)] = i
|
|
w.filteredData = append(w.filteredData, w.data[i])
|
|
}
|
|
}
|
|
}
|
|
for i := range w.filteredData {
|
|
if w.filteredData[i] == selVal {
|
|
w.cursor = i
|
|
return
|
|
}
|
|
}
|
|
// If we're here, then the selected value changed
|
|
if w.cursor > len(w.filteredData) {
|
|
w.cursor = len(w.filteredData) - 1
|
|
w.doChange()
|
|
return
|
|
}
|
|
w.cursor = 0
|
|
w.doChange()
|
|
}
|
|
|
|
func (w *Searcher) SelectedValue() string { return w.filteredData[w.cursor] }
|
|
func (w *Searcher) SelectedIndex() int { return 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) SetOnChange(f func(idx int, s string) bool) { w.onChange = f }
|
|
func (w *Searcher) doChange() {
|
|
l := len(w.filteredData)
|
|
if w.onChange == nil || l == 0 {
|
|
return
|
|
}
|
|
if w.cursor < l {
|
|
tr, ok := w.filteredToTrue[w.cursor]
|
|
if !ok {
|
|
// How did we get here?
|
|
return
|
|
}
|
|
w.onChange(tr, w.data[tr])
|
|
}
|
|
}
|
|
|
|
func (w *Searcher) ClearSearch() {
|
|
w.cursor = 0
|
|
w.filteredData = w.data
|
|
w.search.SetValue("")
|
|
}
|
|
|
|
func (w *Searcher) SetLogger(l func(string, ...any)) { w.logger = l }
|
|
func (w *Searcher) Log(txt string, args ...any) {
|
|
if w.logger != nil {
|
|
w.logger(txt, args...)
|
|
}
|
|
}
|
|
|
|
func (w *Searcher) DisableBorder(b bool) { w.disableBorder = b }
|