4a4050ee0f
Fix panic when resizing the terminal by dynamically adjusting the width of the option selector. The selector does not check the width of the terminal before printing. This can lead to a panic in the account wizard when reducing the terminal width. If the terminal width is not large enough, the space between the options is reduced. If this is still not enough, then the selector will only show the focused option and arrows indicating the alternatives. Fixes: https://todo.sr.ht/~rjarry/aerc/41 Reported-by: Omar Polo <op@omarpolo.com> Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
165 lines
3.3 KiB
Go
165 lines
3.3 KiB
Go
package widgets
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/gdamore/tcell/v2"
|
||
"github.com/mattn/go-runewidth"
|
||
|
||
"git.sr.ht/~rjarry/aerc/config"
|
||
"git.sr.ht/~rjarry/aerc/lib/ui"
|
||
)
|
||
|
||
type Selector struct {
|
||
ui.Invalidatable
|
||
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() {
|
||
sel.DoInvalidate(sel)
|
||
}
|
||
|
||
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) Selected() string {
|
||
return sel.options[sel.focus]
|
||
}
|
||
|
||
func (sel *Selector) Focus(focus bool) {
|
||
sel.focused = focus
|
||
sel.Invalidate()
|
||
}
|
||
|
||
func (sel *Selector) Event(event tcell.Event) bool {
|
||
switch event := event.(type) {
|
||
case *tcell.EventKey:
|
||
switch event.Key() {
|
||
case tcell.KeyCtrlH:
|
||
fallthrough
|
||
case tcell.KeyLeft:
|
||
if sel.focus > 0 {
|
||
sel.focus--
|
||
sel.Invalidate()
|
||
}
|
||
if sel.onSelect != nil {
|
||
sel.onSelect(sel.Selected())
|
||
}
|
||
case tcell.KeyCtrlL:
|
||
fallthrough
|
||
case tcell.KeyRight:
|
||
if sel.focus < len(sel.options)-1 {
|
||
sel.focus++
|
||
sel.Invalidate()
|
||
}
|
||
if sel.onSelect != nil {
|
||
sel.onSelect(sel.Selected())
|
||
}
|
||
case tcell.KeyEnter:
|
||
if sel.onChoose != nil {
|
||
sel.onChoose(sel.Selected())
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|