init: pristine aerc 0.20.0 source
This commit is contained in:
+275
@@ -0,0 +1,275 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user