mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-03-02 18:23:33 +01:00
Go has evolved significantly over the years and has introduced some handy helper functions that make the code easier to read. Use helper functions like slices.Contains, map.Copy, and strings.CutPrefix, when appropriate. Signed-off-by: Moritz Poldrack <git@moritz.sh> Acked-by: Robin Jarry <robin@jarry.cc>
273 lines
5.8 KiB
Go
273 lines
5.8 KiB
Go
package app
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/mattn/go-runewidth"
|
||
|
||
"git.sr.ht/~rjarry/aerc/config"
|
||
"git.sr.ht/~rjarry/aerc/lib/ui"
|
||
"git.sr.ht/~rockorager/vaxis"
|
||
)
|
||
|
||
type Selector struct {
|
||
chooser bool
|
||
focused bool
|
||
focus int
|
||
options []string
|
||
uiConfig *config.UIConfig
|
||
|
||
onChoose func(option string)
|
||
onSelect func(option string)
|
||
}
|
||
|
||
func NewSelector(options []string, focus int, uiConfig *config.UIConfig) *Selector {
|
||
return &Selector{
|
||
focus: focus,
|
||
options: options,
|
||
uiConfig: uiConfig,
|
||
}
|
||
}
|
||
|
||
func (sel *Selector) Chooser(chooser bool) *Selector {
|
||
sel.chooser = chooser
|
||
return sel
|
||
}
|
||
|
||
func (sel *Selector) Invalidate() {
|
||
ui.Invalidate()
|
||
}
|
||
|
||
func (sel *Selector) Draw(ctx *ui.Context) {
|
||
defaultSelectorStyle := sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT)
|
||
w, h := ctx.Width(), ctx.Height()
|
||
ctx.Fill(0, 0, w, h, ' ', defaultSelectorStyle)
|
||
|
||
if w < 5 || h < 1 {
|
||
// if width and height are that small, don't even try to draw
|
||
// something
|
||
return
|
||
}
|
||
|
||
y := 1
|
||
if h == 1 {
|
||
y = 0
|
||
}
|
||
|
||
format := "[%s]"
|
||
|
||
calculateWidth := func(space int) int {
|
||
neededWidth := 2
|
||
for i, option := range sel.options {
|
||
neededWidth += runewidth.StringWidth(fmt.Sprintf(format, option))
|
||
if i < len(sel.options)-1 {
|
||
neededWidth += space
|
||
}
|
||
}
|
||
return neededWidth - space
|
||
}
|
||
|
||
space := 5
|
||
for ; space > 0; space-- {
|
||
if w > calculateWidth(space) {
|
||
break
|
||
}
|
||
}
|
||
|
||
x := 2
|
||
for i, option := range sel.options {
|
||
style := defaultSelectorStyle
|
||
if sel.focus == i {
|
||
if sel.focused {
|
||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_FOCUSED)
|
||
} else if sel.chooser {
|
||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_CHOOSER)
|
||
}
|
||
}
|
||
|
||
if space == 0 {
|
||
if sel.focus == i {
|
||
leftArrow, rightArrow := ' ', ' '
|
||
if i > 0 {
|
||
leftArrow = '❮'
|
||
}
|
||
if i < len(sel.options)-1 {
|
||
rightArrow = '❯'
|
||
}
|
||
|
||
s := runewidth.Truncate(option,
|
||
w-runewidth.RuneWidth(leftArrow)-runewidth.RuneWidth(rightArrow)-runewidth.StringWidth(fmt.Sprintf(format, "")),
|
||
"…")
|
||
|
||
nextPos := 0
|
||
nextPos += ctx.Printf(nextPos, y, defaultSelectorStyle, "%c", leftArrow)
|
||
nextPos += ctx.Printf(nextPos, y, style, format, s)
|
||
ctx.Printf(nextPos, y, defaultSelectorStyle, "%c", rightArrow)
|
||
}
|
||
} else {
|
||
x += ctx.Printf(x, y, style, format, option)
|
||
x += space
|
||
}
|
||
}
|
||
}
|
||
|
||
func (sel *Selector) OnChoose(fn func(option string)) *Selector {
|
||
sel.onChoose = fn
|
||
return sel
|
||
}
|
||
|
||
func (sel *Selector) OnSelect(fn func(option string)) *Selector {
|
||
sel.onSelect = fn
|
||
return sel
|
||
}
|
||
|
||
func (sel *Selector) Select(option string) {
|
||
for i, opt := range sel.options {
|
||
if option == opt {
|
||
sel.focus = i
|
||
if sel.onSelect != nil {
|
||
sel.onSelect(opt)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
func (sel *Selector) Selected() string {
|
||
return sel.options[sel.focus]
|
||
}
|
||
|
||
func (sel *Selector) Focus(focus bool) {
|
||
sel.focused = focus
|
||
sel.Invalidate()
|
||
}
|
||
|
||
func (sel *Selector) Event(event vaxis.Event) bool {
|
||
if key, ok := event.(vaxis.Key); ok {
|
||
switch {
|
||
case key.Matches('h', vaxis.ModCtrl):
|
||
fallthrough
|
||
case key.Matches(vaxis.KeyLeft):
|
||
if sel.focus > 0 {
|
||
sel.focus--
|
||
sel.Invalidate()
|
||
}
|
||
if sel.onSelect != nil {
|
||
sel.onSelect(sel.Selected())
|
||
}
|
||
case key.Matches('l', vaxis.ModCtrl):
|
||
fallthrough
|
||
case key.Matches(vaxis.KeyRight):
|
||
if sel.focus < len(sel.options)-1 {
|
||
sel.focus++
|
||
sel.Invalidate()
|
||
}
|
||
if sel.onSelect != nil {
|
||
sel.onSelect(sel.Selected())
|
||
}
|
||
case key.Matches(vaxis.KeyEnter):
|
||
if sel.onChoose != nil {
|
||
sel.onChoose(sel.Selected())
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
var ErrNoOptionSelected = fmt.Errorf("no option selected")
|
||
|
||
type SelectorDialog struct {
|
||
callback func(string, error)
|
||
title string
|
||
prompt string
|
||
uiConfig *config.UIConfig
|
||
selector *Selector
|
||
}
|
||
|
||
func NewSelectorDialog(title string, prompt string, options []string, focus int,
|
||
uiConfig *config.UIConfig, cb func(string, error),
|
||
) *SelectorDialog {
|
||
sd := &SelectorDialog{
|
||
callback: cb,
|
||
title: title,
|
||
prompt: strings.TrimSpace(prompt),
|
||
uiConfig: uiConfig,
|
||
selector: NewSelector(options, focus, uiConfig).Chooser(true),
|
||
}
|
||
sd.selector.Focus(true)
|
||
return sd
|
||
}
|
||
|
||
func (gp *SelectorDialog) Draw(ctx *ui.Context) {
|
||
defaultStyle := gp.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||
titleStyle := gp.uiConfig.GetStyle(config.STYLE_TITLE)
|
||
|
||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle)
|
||
ctx.Printf(1, 0, titleStyle, "%s", gp.title)
|
||
var i int
|
||
lines := strings.Split(gp.prompt, "\n")
|
||
for i = 0; i < len(lines); i++ {
|
||
ctx.Printf(1, 2+i, defaultStyle, "%s", lines[i])
|
||
}
|
||
gp.selector.Draw(ctx.Subcontext(1, ctx.Height()-1, ctx.Width()-2, 1))
|
||
}
|
||
|
||
func (gp *SelectorDialog) ContextWidth() (func(int) int, func(int) int) {
|
||
// horizontal starting position in columns from the left
|
||
start := func(int) int {
|
||
return 4
|
||
}
|
||
// dialog width from the starting column
|
||
width := func(w int) int {
|
||
return w - 8
|
||
}
|
||
return start, width
|
||
}
|
||
|
||
func (gp *SelectorDialog) ContextHeight() (func(int) int, func(int) int) {
|
||
totalHeight := 2 // title + empty line
|
||
totalHeight += strings.Count(gp.prompt, "\n") + 1
|
||
totalHeight += 2 // empty line + selector
|
||
start := func(h int) int {
|
||
s := max(h/2-totalHeight/2, 0)
|
||
return s
|
||
}
|
||
height := func(h int) int {
|
||
if totalHeight > h {
|
||
return h
|
||
} else {
|
||
return totalHeight
|
||
}
|
||
}
|
||
return start, height
|
||
}
|
||
|
||
func (gp *SelectorDialog) Invalidate() {
|
||
ui.Invalidate()
|
||
}
|
||
|
||
func (gp *SelectorDialog) Event(event vaxis.Event) bool {
|
||
switch event := event.(type) {
|
||
case vaxis.Key:
|
||
switch {
|
||
case event.Matches(vaxis.KeyEnter):
|
||
gp.selector.Focus(false)
|
||
gp.callback(gp.selector.Selected(), nil)
|
||
case event.Matches(vaxis.KeyEsc):
|
||
gp.selector.Focus(false)
|
||
gp.callback("", ErrNoOptionSelected)
|
||
default:
|
||
gp.selector.Event(event)
|
||
}
|
||
default:
|
||
gp.selector.Event(event)
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (gp *SelectorDialog) Focus(f bool) {
|
||
gp.selector.Focus(f)
|
||
}
|