aboutsummaryrefslogtreecommitdiffstats
path: root/app/listbox.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/listbox.go')
-rw-r--r--app/listbox.go299
1 files changed, 299 insertions, 0 deletions
diff --git a/app/listbox.go b/app/listbox.go
new file mode 100644
index 00000000..5a80261e
--- /dev/null
+++ b/app/listbox.go
@@ -0,0 +1,299 @@
+package app
+
+import (
+ "math"
+ "strings"
+ "sync"
+
+ "git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/ui"
+ "git.sr.ht/~rjarry/aerc/log"
+ "github.com/gdamore/tcell/v2"
+ "github.com/mattn/go-runewidth"
+)
+
+type ListBox struct {
+ Scrollable
+ title string
+ lines []string
+ selected string
+ cursorPos int
+ horizPos int
+ jump int
+ showCursor bool
+ showFilter bool
+ filterMutex sync.Mutex
+ filter *ui.TextInput
+ uiConfig *config.UIConfig
+ cb func(string)
+}
+
+func NewListBox(title string, lines []string, uiConfig *config.UIConfig, cb func(string)) *ListBox {
+ lb := &ListBox{
+ title: title,
+ lines: lines,
+ cursorPos: -1,
+ jump: -1,
+ uiConfig: uiConfig,
+ cb: cb,
+ filter: ui.NewTextInput("", uiConfig),
+ }
+ lb.filter.OnChange(func(ti *ui.TextInput) {
+ var show bool
+ if ti.String() == "" {
+ show = false
+ } else {
+ show = true
+ }
+ lb.setShowFilterField(show)
+ lb.filter.Focus(show)
+ lb.Invalidate()
+ })
+ lb.dedup()
+ return lb
+}
+
+func (lb *ListBox) dedup() {
+ dedupped := make([]string, 0, len(lb.lines))
+ dedup := make(map[string]struct{})
+ for _, line := range lb.lines {
+ if _, dup := dedup[line]; dup {
+ log.Warnf("ignore duplicate: %s", line)
+ continue
+ }
+ dedup[line] = struct{}{}
+ dedupped = append(dedupped, line)
+ }
+ lb.lines = dedupped
+}
+
+func (lb *ListBox) setShowFilterField(b bool) {
+ lb.filterMutex.Lock()
+ defer lb.filterMutex.Unlock()
+ lb.showFilter = b
+}
+
+func (lb *ListBox) showFilterField() bool {
+ lb.filterMutex.Lock()
+ defer lb.filterMutex.Unlock()
+ return lb.showFilter
+}
+
+func (lb *ListBox) Draw(ctx *ui.Context) {
+ defaultStyle := lb.uiConfig.GetStyle(config.STYLE_DEFAULT)
+ titleStyle := lb.uiConfig.GetStyle(config.STYLE_TITLE)
+ w, h := ctx.Width(), ctx.Height()
+ ctx.Fill(0, 0, w, h, ' ', defaultStyle)
+ ctx.Fill(0, 0, w, 1, ' ', titleStyle)
+ ctx.Printf(0, 0, titleStyle, "%s", lb.title)
+
+ y := 0
+ if lb.showFilterField() {
+ y = 1
+ x := ctx.Printf(0, y, defaultStyle, "Filter: ")
+ lb.filter.Draw(ctx.Subcontext(x, y, w-x, 1))
+ }
+
+ lb.drawBox(ctx.Subcontext(0, y+1, w, h-(y+1)))
+}
+
+func (lb *ListBox) moveCursor(delta int) {
+ list := lb.filtered()
+ if len(list) == 0 {
+ return
+ }
+ lb.cursorPos += delta
+ if lb.cursorPos < 0 {
+ lb.cursorPos = 0
+ }
+ if lb.cursorPos >= len(list) {
+ lb.cursorPos = len(list) - 1
+ }
+ lb.selected = list[lb.cursorPos]
+ lb.showCursor = true
+ lb.horizPos = 0
+}
+
+func (lb *ListBox) moveHorizontal(delta int) {
+ lb.horizPos += delta
+ if lb.horizPos > len(lb.selected) {
+ lb.horizPos = len(lb.selected)
+ }
+ if lb.horizPos < 0 {
+ lb.horizPos = 0
+ }
+}
+
+func (lb *ListBox) filtered() []string {
+ list := []string{}
+ filterTerm := lb.filter.String()
+ for _, line := range lb.lines {
+ if strings.Contains(line, filterTerm) {
+ list = append(list, line)
+ }
+ }
+ return list
+}
+
+func (lb *ListBox) drawBox(ctx *ui.Context) {
+ defaultStyle := lb.uiConfig.GetStyle(config.STYLE_DEFAULT)
+ selectedStyle := lb.uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, nil)
+
+ w, h := ctx.Width(), ctx.Height()
+ lb.jump = h
+ list := lb.filtered()
+
+ lb.UpdateScroller(ctx.Height(), len(list))
+ scroll := 0
+ lb.cursorPos = -1
+ for i := 0; i < len(list); i++ {
+ if lb.selected == list[i] {
+ scroll = i
+ lb.cursorPos = i
+ break
+ }
+ }
+ lb.EnsureScroll(scroll)
+
+ needScrollbar := lb.NeedScrollbar()
+ if needScrollbar {
+ w -= 1
+ if w < 0 {
+ w = 0
+ }
+ }
+
+ if lb.lines == nil || len(list) == 0 {
+ return
+ }
+
+ y := 0
+ for i := lb.Scroll(); i < len(list) && y < h; i++ {
+ style := defaultStyle
+ line := runewidth.Truncate(list[i], w-1, "❯")
+ if lb.selected == list[i] && lb.showCursor {
+ style = selectedStyle
+ if len(list[i]) > w {
+ if len(list[i])-lb.horizPos < w {
+ lb.horizPos = len(list[i]) - w + 1
+ }
+ rest := list[i][lb.horizPos:]
+ line = runewidth.Truncate(rest,
+ w-1, "❯")
+ if lb.horizPos > 0 && len(line) > 0 {
+ line = "❮" + line[1:]
+ }
+ }
+ }
+ ctx.Printf(1, y, style, line)
+ y += 1
+ }
+
+ if needScrollbar {
+ scrollBarCtx := ctx.Subcontext(w, 0, 1, ctx.Height())
+ lb.drawScrollbar(scrollBarCtx)
+ }
+}
+
+func (lb *ListBox) drawScrollbar(ctx *ui.Context) {
+ gutterStyle := tcell.StyleDefault
+ pillStyle := tcell.StyleDefault.Reverse(true)
+
+ // gutter
+ h := ctx.Height()
+ ctx.Fill(0, 0, 1, h, ' ', gutterStyle)
+
+ // pill
+ pillSize := int(math.Ceil(float64(h) * lb.PercentVisible()))
+ pillOffset := int(math.Floor(float64(h) * lb.PercentScrolled()))
+ ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
+}
+
+func (lb *ListBox) Invalidate() {
+ ui.Invalidate()
+}
+
+func (lb *ListBox) Event(event tcell.Event) bool {
+ if event, ok := event.(*tcell.EventKey); ok {
+ switch event.Key() {
+ case tcell.KeyLeft:
+ lb.moveHorizontal(-1)
+ lb.Invalidate()
+ return true
+ case tcell.KeyRight:
+ lb.moveHorizontal(+1)
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlB:
+ line := lb.selected[:lb.horizPos]
+ fds := strings.Fields(line)
+ if len(fds) > 1 {
+ lb.moveHorizontal(
+ strings.LastIndex(line,
+ fds[len(fds)-1]) - lb.horizPos - 1)
+ } else {
+ lb.horizPos = 0
+ }
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlW:
+ line := lb.selected[lb.horizPos+1:]
+ fds := strings.Fields(line)
+ if len(fds) > 1 {
+ lb.moveHorizontal(strings.Index(line, fds[1]))
+ }
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlA, tcell.KeyHome:
+ lb.horizPos = 0
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlE, tcell.KeyEnd:
+ lb.horizPos = len(lb.selected)
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlP, tcell.KeyUp:
+ lb.moveCursor(-1)
+ lb.Invalidate()
+ return true
+ case tcell.KeyCtrlN, tcell.KeyDown:
+ lb.moveCursor(+1)
+ lb.Invalidate()
+ return true
+ case tcell.KeyPgUp:
+ if lb.jump >= 0 {
+ lb.moveCursor(-lb.jump)
+ lb.Invalidate()
+ }
+ return true
+ case tcell.KeyPgDn:
+ if lb.jump >= 0 {
+ lb.moveCursor(+lb.jump)
+ lb.Invalidate()
+ }
+ return true
+ case tcell.KeyEnter:
+ return lb.quit(lb.selected)
+ case tcell.KeyEsc:
+ return lb.quit("")
+ }
+ }
+ if lb.filter != nil {
+ handled := lb.filter.Event(event)
+ lb.Invalidate()
+ return handled
+ }
+ return false
+}
+
+func (lb *ListBox) quit(s string) bool {
+ lb.filter.Focus(false)
+ if lb.cb != nil {
+ lb.cb(s)
+ }
+ return true
+}
+
+func (lb *ListBox) Focus(f bool) {
+ lb.filter.Focus(f)
+}