aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--commands/new-account.go20
-rw-r--r--config/config.go9
-rw-r--r--doc/aerc.1.scd7
-rw-r--r--lib/ui/text.go2
-rw-r--r--lib/ui/textinput.go31
-rw-r--r--widgets/account-wizard.go625
-rw-r--r--widgets/aerc.go2
7 files changed, 683 insertions, 13 deletions
diff --git a/commands/new-account.go b/commands/new-account.go
new file mode 100644
index 0000000..6a64eb2
--- /dev/null
+++ 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
index d885402..c6136cf 100644
--- 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
index e96334d..459ec62 100644
--- 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
index 8aea8eb..2b82598 100644
--- 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
index 4a5308e..ce443c3 100644
--- 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
new file mode 100644
index 0000000..3ce207c
--- /dev/null
+++ 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
index 187eddb..eba76a2 100644
--- 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":