aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

commit 6811143925384ba1cfda8b3e1b338b0cfb9ac6e3
parent 176245208d40a9ca2ec324be7863a22819de29bc
Author: Drew DeVault <sir@cmpwn.com>
Date:   Tue, 21 May 2019 16:31:04 -0400

New account wizard, part one

Diffstat:
Acommands/new-account.go | 20++++++++++++++++++++
Mconfig/config.go | 9+++++++++
Mdoc/aerc.1.scd | 7++++---
Mlib/ui/text.go | 2+-
Mlib/ui/textinput.go | 31++++++++++++++++++++++---------
Awidgets/account-wizard.go | 625+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwidgets/aerc.go | 2++
7 files changed, 683 insertions(+), 13 deletions(-)

diff --git a/commands/new-account.go b/commands/new-account.go @@ -0,0 +1,20 @@ +package commands + +import ( + "errors" + + "git.sr.ht/~sircmpwn/aerc/widgets" +) + +func init() { + register("new-account", CommandNewAccount) +} + +func CommandNewAccount(aerc *widgets.Aerc, args []string) error { + if len(args) != 1 { + return errors.New("Usage: new-account") + } + wizard := widgets.NewAccountWizard() + aerc.NewTab(wizard, "New account") + return nil +} diff --git a/config/config.go b/config/config.go @@ -11,6 +11,7 @@ import ( "strings" "unicode" + "github.com/gdamore/tcell" "github.com/go-ini/ini" "github.com/kyoh86/xdg" ) @@ -45,6 +46,7 @@ type AccountConfig struct { type BindingConfig struct { Global *KeyBindings + AccountWizard *KeyBindings Compose *KeyBindings ComposeEditor *KeyBindings ComposeReview *KeyBindings @@ -208,6 +210,7 @@ func LoadConfig(root *string) (*AercConfig, error) { config := &AercConfig{ Bindings: BindingConfig{ Global: NewKeyBindings(), + AccountWizard: NewKeyBindings(), Compose: NewKeyBindings(), ComposeEditor: NewKeyBindings(), ComposeReview: NewKeyBindings(), @@ -229,6 +232,12 @@ func LoadConfig(root *string) (*AercConfig, error) { EmptyMessage: "(no messages)", }, } + // These bindings are not configurable + config.Bindings.AccountWizard.ExKey = KeyStroke{ + Key: tcell.KeyCtrlE, + } + quit, _ := ParseBinding("<C-q>", ":quit<Enter>") + config.Bindings.AccountWizard.Add(quit) if filters, err := file.GetSection("filters"); err == nil { // TODO: Parse the filter more finely, e.g. parse the regex for _, match := range filters.KeyStrings() { diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd @@ -8,12 +8,13 @@ aerc - the world's best email client _aerc_ -Starts the interactive aerc mail client on /dev/tty. +For a guided tutorial, use *:help tutorial*. # RUNTIME COMMANDS -To execute a command, press : to summon the command interface. Commands may also -be bound to keys, see *aerc-config*(5) for details. +To execute a command, press ':' to bring up the command interface. Commands may +also be bound to keys, see *aerc-config*(5) for details. In some contexts, such +as the terminal emulator, ';' is used to bring up the command interface. Different commands work in different contexts, depending on the kind of tab you have selected. diff --git a/lib/ui/text.go b/lib/ui/text.go @@ -77,7 +77,7 @@ func (t *Text) Draw(ctx *Context) { style = style.Reverse(true) } ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) - ctx.Printf(x, 0, style, t.text) + ctx.Printf(x, 0, style, "%s", t.text) } func (t *Text) Invalidate() { diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go @@ -10,14 +10,15 @@ import ( type TextInput struct { Invalidatable - cells int - ctx *Context - focus bool - index int - prompt string - scroll int - text []rune - change []func(ti *TextInput) + cells int + ctx *Context + focus bool + index int + password bool + prompt string + scroll int + text []rune + change []func(ti *TextInput) } // Creates a new TextInput. TextInputs will render a "textbox" in the entire @@ -31,6 +32,11 @@ func NewTextInput(text string) *TextInput { } } +func (ti *TextInput) Password(password bool) *TextInput { + ti.password = password + return ti +} + func (ti *TextInput) Prompt(prompt string) *TextInput { ti.prompt = prompt return ti @@ -42,6 +48,7 @@ func (ti *TextInput) String() string { func (ti *TextInput) Set(value string) { ti.text = []rune(value) + ti.index = len(ti.text) } func (ti *TextInput) Invalidate() { @@ -51,7 +58,13 @@ func (ti *TextInput) Invalidate() { func (ti *TextInput) Draw(ctx *Context) { ti.ctx = ctx // gross ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(ti.text)) + if ti.password { + x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt) + cells := runewidth.StringWidth(string(ti.text)) + ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault) + } else { + ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(ti.text)) + } cells := runewidth.StringWidth(string(ti.text[:ti.index]) + ti.prompt) if cells != ti.cells && ti.focus { ctx.SetCursor(cells, 0) diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go @@ -0,0 +1,625 @@ +package widgets + +import ( + "net/url" + "strings" + + "github.com/gdamore/tcell" + + "git.sr.ht/~sircmpwn/aerc/lib/ui" +) + +const ( + CONFIGURE_BASICS = iota + CONFIGURE_INCOMING = iota + CONFIGURE_OUTGOING = iota + CONFIGURE_COMPLETE = iota +) + +const ( + IMAP_OVER_TLS = iota + IMAP_STARTTLS = iota + IMAP_INSECURE = iota +) + +const ( + SMTP_OVER_TLS = iota + SMTP_STARTTLS = iota + SMTP_INSECURE = iota +) + +type AccountWizard struct { + ui.Invalidatable + step int + steps []*ui.Grid + focus int + testing bool + // CONFIGURE_BASICS + accountName *ui.TextInput + email *ui.TextInput + fullName *ui.TextInput + basics []ui.Interactive + // CONFIGURE_INCOMING + imapUsername *ui.TextInput + imapPassword *ui.TextInput + imapServer *ui.TextInput + imapMode int + imapStr *ui.Text + imapUrl url.URL + incoming []ui.Interactive + // CONFIGURE_OUTGOING + smtpUsername *ui.TextInput + smtpPassword *ui.TextInput + smtpServer *ui.TextInput + smtpMode int + smtpStr *ui.Text + smtpUrl url.URL + copySent bool + outgoing []ui.Interactive + // CONFIGURE_COMPLETE + complete []ui.Interactive +} + +func NewAccountWizard() *AccountWizard { + wizard := &AccountWizard{ + accountName: ui.NewTextInput("").Prompt("> "), + email: ui.NewTextInput("").Prompt("> "), + fullName: ui.NewTextInput("").Prompt("> "), + imapUsername: ui.NewTextInput("").Prompt("> "), + imapPassword: ui.NewTextInput("").Prompt("] ").Password(true), + imapServer: ui.NewTextInput("").Prompt("> "), + imapStr: ui.NewText("imaps://"), + smtpUsername: ui.NewTextInput("").Prompt("> "), + smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true), + smtpServer: ui.NewTextInput("").Prompt("> "), + smtpStr: ui.NewText("smtps://"), + copySent: true, + } + + // Autofill some stuff for the user + wizard.email.OnChange(func(_ *ui.TextInput) { + value := wizard.email.String() + wizard.imapUsername.Set(value) + wizard.smtpUsername.Set(value) + if strings.ContainsRune(value, '@') { + server := value[strings.IndexRune(value, '@')+1:] + wizard.imapServer.Set(server) + wizard.smtpServer.Set(server) + } + wizard.imapUri() + wizard.smtpUri() + }) + wizard.imapServer.OnChange(func(_ *ui.TextInput) { + wizard.smtpServer.Set(wizard.imapServer.String()) + wizard.imapUri() + wizard.smtpUri() + }) + wizard.imapUsername.OnChange(func(_ *ui.TextInput) { + wizard.smtpUsername.Set(wizard.imapUsername.String()) + wizard.imapUri() + wizard.smtpUri() + }) + wizard.imapPassword.OnChange(func(_ *ui.TextInput) { + wizard.smtpPassword.Set(wizard.imapPassword.String()) + wizard.imapUri() + wizard.smtpUri() + }) + wizard.smtpServer.OnChange(func(_ *ui.TextInput) { + wizard.smtpUri() + }) + wizard.smtpUsername.OnChange(func(_ *ui.TextInput) { + wizard.smtpUri() + }) + wizard.smtpPassword.OnChange(func(_ *ui.TextInput) { + wizard.smtpUri() + }) + + basics := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 8}, // Introduction + {ui.SIZE_EXACT, 1}, // Account name (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Full name (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Email address (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + basics.AddChild( + ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" + + "This wizard supports basic IMAP & SMTP configuration.\n" + + "For other configurations, use <Ctrl+q> to exit and read the " + + "aerc-config(5) man page.\n" + + "Press <Tab> to cycle between each field in this form, or <Ctrl+k> and <Ctrl+j>.")) + basics.AddChild( + ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')"). + Bold(true)). + At(1, 0) + basics.AddChild(wizard.accountName). + At(2, 0) + basics.AddChild(ui.NewFill(' ')). + At(3, 0) + basics.AddChild( + ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')"). + Bold(true)). + At(4, 0) + basics.AddChild(wizard.fullName). + At(5, 0) + basics.AddChild(ui.NewFill(' ')). + At(6, 0) + basics.AddChild( + ui.NewText("Your email address? (e.g. 'john@example.org')").Bold(true)). + At(7, 0) + basics.AddChild(wizard.email). + At(8, 0) + selecter := newSelecter([]string{"Next"}, 0). + OnChoose(wizard.advance) + basics.AddChild(selecter).At(9, 0) + wizard.basics = []ui.Interactive{ + wizard.accountName, wizard.fullName, wizard.email, selecter, + } + basics.OnInvalidate(func(_ ui.Drawable) { + wizard.Invalidate() + }) + + incoming := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 3}, // Introduction + {ui.SIZE_EXACT, 1}, // Username (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Password (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Server (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Connection mode (label) + {ui.SIZE_EXACT, 2}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 2}, // Connection string + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)")) + incoming.AddChild( + ui.NewText("Username").Bold(true)). + At(1, 0) + incoming.AddChild(wizard.imapUsername). + At(2, 0) + incoming.AddChild(ui.NewFill(' ')). + At(3, 0) + incoming.AddChild( + ui.NewText("Password").Bold(true)). + At(4, 0) + incoming.AddChild(wizard.imapPassword). + At(5, 0) + incoming.AddChild(ui.NewFill(' ')). + At(6, 0) + incoming.AddChild( + ui.NewText("Server address "+ + "(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)). + At(7, 0) + incoming.AddChild(wizard.imapServer). + At(8, 0) + incoming.AddChild(ui.NewFill(' ')). + At(9, 0) + incoming.AddChild( + ui.NewText("Connection mode").Bold(true)). + At(10, 0) + imapMode := newSelecter([]string{ + "IMAP over SSL/TLS", + "IMAP with STARTTLS", + "Insecure IMAP", + }, 0).Chooser(true).OnSelect(func(option string) { + switch option { + case "IMAP over SSL/TLS": + wizard.imapMode = IMAP_OVER_TLS + case "IMAP with STARTTLS": + wizard.imapMode = IMAP_STARTTLS + case "Insecure IMAP": + wizard.imapMode = IMAP_INSECURE + } + wizard.imapUri() + }) + incoming.AddChild(imapMode).At(11, 0) + selecter = newSelecter([]string{"Previous", "Next"}, 1). + OnChoose(wizard.advance) + incoming.AddChild(ui.NewFill(' ')).At(12, 0) + incoming.AddChild(wizard.imapStr).At(13, 0) + incoming.AddChild(selecter).At(14, 0) + wizard.incoming = []ui.Interactive{ + wizard.imapUsername, wizard.imapPassword, wizard.imapServer, + imapMode, selecter, + } + incoming.OnInvalidate(func(_ ui.Drawable) { + wizard.Invalidate() + }) + + outgoing := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 3}, // Introduction + {ui.SIZE_EXACT, 1}, // Username (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Password (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Server (label) + {ui.SIZE_EXACT, 1}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Connection mode (label) + {ui.SIZE_EXACT, 2}, // (input) + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Connection string + {ui.SIZE_EXACT, 1}, // Padding + {ui.SIZE_EXACT, 1}, // Copy to sent (label) + {ui.SIZE_EXACT, 2}, // (input) + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)")) + outgoing.AddChild( + ui.NewText("Username").Bold(true)). + At(1, 0) + outgoing.AddChild(wizard.smtpUsername). + At(2, 0) + outgoing.AddChild(ui.NewFill(' ')). + At(3, 0) + outgoing.AddChild( + ui.NewText("Password").Bold(true)). + At(4, 0) + outgoing.AddChild(wizard.smtpPassword). + At(5, 0) + outgoing.AddChild(ui.NewFill(' ')). + At(6, 0) + outgoing.AddChild( + ui.NewText("Server address "+ + "(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)). + At(7, 0) + outgoing.AddChild(wizard.smtpServer). + At(8, 0) + outgoing.AddChild(ui.NewFill(' ')). + At(9, 0) + outgoing.AddChild( + ui.NewText("Connection mode").Bold(true)). + At(10, 0) + smtpMode := newSelecter([]string{ + "SMTP over SSL/TLS", + "SMTP with STARTTLS", + "Insecure SMTP", + }, 0).Chooser(true).OnSelect(func(option string) { + switch option { + case "SMTP over SSL/TLS": + wizard.smtpMode = SMTP_OVER_TLS + case "SMTP with STARTTLS": + wizard.smtpMode = SMTP_STARTTLS + case "Insecure SMTP": + wizard.smtpMode = SMTP_INSECURE + } + wizard.smtpUri() + }) + outgoing.AddChild(smtpMode).At(11, 0) + selecter = newSelecter([]string{"Previous", "Next"}, 1). + OnChoose(wizard.advance) + outgoing.AddChild(ui.NewFill(' ')).At(12, 0) + outgoing.AddChild(wizard.smtpStr).At(13, 0) + outgoing.AddChild(ui.NewFill(' ')).At(14, 0) + outgoing.AddChild( + ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)). + At(15, 0) + copySent := newSelecter([]string{"Yes", "No"}, 0). + Chooser(true).OnChoose(func(option string) { + switch option { + case "Yes": + wizard.copySent = true + case "No": + wizard.copySent = false + } + }) + outgoing.AddChild(copySent).At(16, 0) + outgoing.AddChild(selecter).At(17, 0) + wizard.outgoing = []ui.Interactive{ + wizard.smtpUsername, wizard.smtpPassword, wizard.smtpServer, + smtpMode, copySent, selecter, + } + outgoing.OnInvalidate(func(_ ui.Drawable) { + wizard.Invalidate() + }) + + complete := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 7}, // Introduction + {ui.SIZE_WEIGHT, 1}, // Previous / Finish / Finish & open tutorial + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + complete.AddChild(ui.NewText( + "\nConfiguration complete!\n\n" + + "You can go back and double check your settings, or choose 'Finish' to\n" + + "save your settings to accounts.conf.\n\n" + + "To add another account in the future, run ':new-account'.")) + selecter = newSelecter([]string{ + "Previous", + "Finish", + "Finish & open tutorial", + }, 1).OnChoose(func(option string) { + switch option { + case "Previous": + wizard.advance("Previous") + case "Finish & open tutorial": + // TODO + fallthrough + case "Finish": + // TODO + } + }) + complete.AddChild(selecter).At(1, 0) + wizard.complete = []ui.Interactive{selecter} + complete.OnInvalidate(func(_ ui.Drawable) { + wizard.Invalidate() + }) + + wizard.steps = []*ui.Grid{basics, incoming, outgoing, complete} + return wizard +} + +func (wizard *AccountWizard) imapUri() url.URL { + host := wizard.imapServer.String() + user := wizard.imapUsername.String() + pass := wizard.imapPassword.String() + var scheme string + switch wizard.imapMode { + case IMAP_OVER_TLS: + scheme = "imaps" + case IMAP_STARTTLS: + scheme = "imap" + case IMAP_INSECURE: + scheme = "imap+insecure" + } + var ( + userpass *url.Userinfo + userwopass *url.Userinfo + ) + if pass == "" { + userpass = url.User(user) + userwopass = userpass + } else { + userpass = url.UserPassword(user, pass) + userwopass = url.UserPassword(user, strings.Repeat("*", len(pass))) + } + uri := url.URL{ + Scheme: scheme, + Host: host, + User: userpass, + } + clean := url.URL{ + Scheme: scheme, + Host: host, + User: userwopass, + } + wizard.imapStr.Text("Connection URL: " + + strings.ReplaceAll(clean.String(), "%2A", "*")) + wizard.imapUrl = uri + return uri +} + +func (wizard *AccountWizard) smtpUri() url.URL { + host := wizard.smtpServer.String() + user := wizard.smtpUsername.String() + pass := wizard.smtpPassword.String() + var scheme string + switch wizard.smtpMode { + case SMTP_OVER_TLS: + scheme = "smtps+plain" + case SMTP_STARTTLS: + scheme = "smtp+plain" + case SMTP_INSECURE: + scheme = "smtp+plain" + } + var ( + userpass *url.Userinfo + userwopass *url.Userinfo + ) + if pass == "" { + userpass = url.User(user) + userwopass = userpass + } else { + userpass = url.UserPassword(user, pass) + userwopass = url.UserPassword(user, strings.Repeat("*", len(pass))) + } + uri := url.URL{ + Scheme: scheme, + Host: host, + User: userpass, + } + clean := url.URL{ + Scheme: scheme, + Host: host, + User: userwopass, + } + wizard.smtpStr.Text("Connection URL: " + + strings.ReplaceAll(clean.String(), "%2A", "*")) + wizard.smtpUrl = uri + return uri +} + +func (wizard *AccountWizard) Invalidate() { + wizard.DoInvalidate(wizard) +} + +func (wizard *AccountWizard) Draw(ctx *ui.Context) { + wizard.steps[wizard.step].Draw(ctx) +} + +func (wizard *AccountWizard) getInteractive() []ui.Interactive { + switch wizard.step { + case CONFIGURE_BASICS: + return wizard.basics + case CONFIGURE_INCOMING: + return wizard.incoming + case CONFIGURE_OUTGOING: + return wizard.outgoing + case CONFIGURE_COMPLETE: + return wizard.complete + } + return nil +} + +func (wizard *AccountWizard) advance(direction string) { + wizard.Focus(false) + if direction == "Next" && wizard.step < len(wizard.steps)-1 { + wizard.step++ + } + if direction == "Previous" && wizard.step > 0 { + wizard.step-- + } + wizard.focus = 0 + wizard.Focus(true) + wizard.Invalidate() +} + +func (wizard *AccountWizard) Focus(focus bool) { + if interactive := wizard.getInteractive(); interactive != nil { + interactive[wizard.focus].Focus(focus) + } +} + +func (wizard *AccountWizard) Event(event tcell.Event) bool { + interactive := wizard.getInteractive() + switch event := event.(type) { + case *tcell.EventKey: + switch event.Key() { + case tcell.KeyUp: + fallthrough + case tcell.KeyCtrlK: + if interactive != nil { + interactive[wizard.focus].Focus(false) + wizard.focus-- + if wizard.focus < 0 { + wizard.focus = len(interactive) - 1 + } + interactive[wizard.focus].Focus(true) + } + wizard.Invalidate() + return true + case tcell.KeyDown: + fallthrough + case tcell.KeyTab: + fallthrough + case tcell.KeyCtrlJ: + if interactive != nil { + interactive[wizard.focus].Focus(false) + wizard.focus++ + if wizard.focus >= len(interactive) { + wizard.focus = 0 + } + interactive[wizard.focus].Focus(true) + } + wizard.Invalidate() + return true + } + } + if interactive != nil { + return interactive[wizard.focus].Event(event) + } + return false +} + +type selecter struct { + ui.Invalidatable + chooser bool + focused bool + focus int + options []string + + onChoose func(option string) + onSelect func(option string) +} + +func newSelecter(options []string, focus int) *selecter { + return &selecter{ + focus: focus, + options: options, + } +} + +func (sel *selecter) Chooser(chooser bool) *selecter { + sel.chooser = chooser + return sel +} + +func (sel *selecter) Invalidate() { + sel.DoInvalidate(sel) +} + +func (sel *selecter) Draw(ctx *ui.Context) { + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + x := 2 + for i, option := range sel.options { + style := tcell.StyleDefault + if sel.focus == i { + if sel.focused { + style = style.Reverse(true) + } else if sel.chooser { + style = style.Bold(true) + } + } + x += ctx.Printf(x, 1, style, "[%s]", option) + x += 5 + } +} + +func (sel *selecter) OnChoose(fn func(option string)) *selecter { + sel.onChoose = fn + return sel +} + +func (sel *selecter) OnSelect(fn func(option string)) *selecter { + sel.onSelect = fn + return sel +} + +func (sel *selecter) Selected() string { + return sel.options[sel.focus] +} + +func (sel *selecter) Focus(focus bool) { + sel.focused = focus + sel.Invalidate() +} + +func (sel *selecter) 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 +} diff --git a/widgets/aerc.go b/widgets/aerc.go @@ -99,6 +99,8 @@ func (aerc *Aerc) getBindings() *config.KeyBindings { switch view := aerc.SelectedTab().(type) { case *AccountView: return aerc.conf.Bindings.MessageList + case *AccountWizard: + return aerc.conf.Bindings.AccountWizard case *Composer: switch view.Bindings() { case "compose::editor":