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 := h/2 - totalHeight/2
if s < 0 {
s = 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)
}