diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index 3935173..e81e836 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -63,9 +63,10 @@ func (ti *TextInput) StringRight() string { return string(ti.text[ti.index:]) } -func (ti *TextInput) Set(value string) { +func (ti *TextInput) Set(value string) *TextInput { ti.text = []rune(value) ti.index = len(ti.text) + return ti } func (ti *TextInput) Invalidate() { diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go index 904013f..d7b46b9 100644 --- a/widgets/account-wizard.go +++ b/widgets/account-wizard.go @@ -177,7 +177,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { At(7, 0) basics.AddChild(wizard.email). At(8, 0) - selecter := newSelecter([]string{"Next"}, 0). + selecter := NewSelecter([]string{"Next"}, 0). OnChoose(func(option string) { email := wizard.email.String() if strings.ContainsRune(email, '@') { @@ -254,7 +254,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { incoming.AddChild( ui.NewText("Connection mode").Bold(true)). At(10, 0) - imapMode := newSelecter([]string{ + imapMode := NewSelecter([]string{ "IMAP over SSL/TLS", "IMAP with STARTTLS", "Insecure IMAP", @@ -270,7 +270,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { wizard.imapUri() }) incoming.AddChild(imapMode).At(11, 0) - selecter = newSelecter([]string{"Previous", "Next"}, 1). + selecter = NewSelecter([]string{"Previous", "Next"}, 1). OnChoose(wizard.advance) incoming.AddChild(ui.NewFill(' ')).At(12, 0) incoming.AddChild(wizard.imapStr).At(13, 0) @@ -331,7 +331,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { outgoing.AddChild( ui.NewText("Connection mode").Bold(true)). At(10, 0) - smtpMode := newSelecter([]string{ + smtpMode := NewSelecter([]string{ "SMTP over SSL/TLS", "SMTP with STARTTLS", "Insecure SMTP", @@ -347,7 +347,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { wizard.smtpUri() }) outgoing.AddChild(smtpMode).At(11, 0) - selecter = newSelecter([]string{"Previous", "Next"}, 1). + selecter = NewSelecter([]string{"Previous", "Next"}, 1). OnChoose(wizard.advance) outgoing.AddChild(ui.NewFill(' ')).At(12, 0) outgoing.AddChild(wizard.smtpStr).At(13, 0) @@ -355,7 +355,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { outgoing.AddChild( ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)). At(15, 0) - copySent := newSelecter([]string{"Yes", "No"}, 0). + copySent := NewSelecter([]string{"Yes", "No"}, 0). Chooser(true).OnChoose(func(option string) { switch option { case "Yes": @@ -385,7 +385,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { "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{ + selecter = NewSelecter([]string{ "Previous", "Finish & open tutorial", "Finish", @@ -716,102 +716,6 @@ func (wizard *AccountWizard) Event(event tcell.Event) bool { 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 -} - func getSRV(host string, services []string) (string, string) { var hostport, srv string for _, srv = range services { diff --git a/widgets/aerc.go b/widgets/aerc.go index d324908..9d955e1 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -240,7 +240,7 @@ func (aerc *Aerc) Event(event tcell.Event) bool { exKey = aerc.conf.Bindings.Global.ExKey } if event.Key() == exKey.Key && event.Rune() == exKey.Rune { - aerc.BeginExCommand() + aerc.BeginExCommand("") return true } interactive, ok := aerc.tabs.Tabs[aerc.tabs.Selected].Content.(ui.Interactive) @@ -370,9 +370,9 @@ func (aerc *Aerc) focus(item ui.Interactive) { } } -func (aerc *Aerc) BeginExCommand() { +func (aerc *Aerc) BeginExCommand(cmd string) { previous := aerc.focused - exline := NewExLine(func(cmd string) { + exline := NewExLine(cmd, func(cmd string) { parts, err := shlex.Split(cmd) if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). diff --git a/widgets/exline.go b/widgets/exline.go index 1482f0e..f2c7249 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -16,11 +16,11 @@ type ExLine struct { input *ui.TextInput } -func NewExLine(commit func(cmd string), finish func(), +func NewExLine(cmd string, commit func(cmd string), finish func(), tabcomplete func(cmd string) []string, cmdHistory lib.History) *ExLine { - input := ui.NewTextInput("").Prompt(":").TabComplete(tabcomplete) + input := ui.NewTextInput("").Prompt(":").TabComplete(tabcomplete).Set(cmd) exline := &ExLine{ commit: commit, finish: finish, diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 4d41923..aca7dd4 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -68,7 +68,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig, }) switcher := &PartSwitcher{} - err := createSwitcher(switcher, conf, store, msg) + err := createSwitcher(acct, switcher, conf, store, msg) if err != nil { return &MessageViewer{ err: err, @@ -112,7 +112,7 @@ func fmtHeader(msg *models.MessageInfo, header string) string { } } -func enumerateParts(conf *config.AercConfig, store *lib.MessageStore, +func enumerateParts(acct *AccountView, conf *config.AercConfig, store *lib.MessageStore, msg *models.MessageInfo, body *models.BodyStructure, index []int) ([]*PartViewer, error) { @@ -124,14 +124,14 @@ func enumerateParts(conf *config.AercConfig, store *lib.MessageStore, pv := &PartViewer{part: part} parts = append(parts, pv) subParts, err := enumerateParts( - conf, store, msg, part, curindex) + acct, conf, store, msg, part, curindex) if err != nil { return nil, err } parts = append(parts, subParts...) continue } - pv, err := NewPartViewer(conf, store, msg, part, curindex) + pv, err := NewPartViewer(acct, conf, store, msg, part, curindex) if err != nil { return nil, err } @@ -140,7 +140,7 @@ func enumerateParts(conf *config.AercConfig, store *lib.MessageStore, return parts, nil } -func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig, +func createSwitcher(acct *AccountView, switcher *PartSwitcher, conf *config.AercConfig, store *lib.MessageStore, msg *models.MessageInfo) error { var err error @@ -150,7 +150,7 @@ func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig, if len(msg.BodyStructure.Parts) == 0 { switcher.selected = 0 - pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure, []int{1}) + pv, err := NewPartViewer(acct, conf, store, msg, msg.BodyStructure, []int{1}) if err != nil { return err } @@ -159,7 +159,7 @@ func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig, switcher.Invalidate() }) } else { - switcher.parts, err = enumerateParts(conf, store, + switcher.parts, err = enumerateParts(acct, conf, store, msg, msg.BodyStructure, []int{}) if err != nil { return err @@ -236,7 +236,7 @@ func (mv *MessageViewer) ToggleHeaders() { switcher := mv.switcher mv.conf.Viewer.ShowHeaders = !mv.conf.Viewer.ShowHeaders err := createSwitcher( - switcher, mv.conf, mv.store, mv.msg) + mv.acct, switcher, mv.conf, mv.store, mv.msg) if err != nil { mv.acct.Logger().Printf( "warning: error during create switcher - %v", err) @@ -299,10 +299,7 @@ func (ps *PartSwitcher) Focus(focus bool) { } func (ps *PartSwitcher) Event(event tcell.Event) bool { - if ps.parts[ps.selected].term != nil { - return ps.parts[ps.selected].term.Event(event) - } - return false + return ps.parts[ps.selected].Event(event) } func (ps *PartSwitcher) Draw(ctx *ui.Context) { @@ -414,9 +411,11 @@ type PartViewer struct { source io.Reader store *lib.MessageStore term *Terminal + selecter *Selecter + grid *ui.Grid } -func NewPartViewer(conf *config.AercConfig, +func NewPartViewer(acct *AccountView, conf *config.AercConfig, store *lib.MessageStore, msg *models.MessageInfo, part *models.BodyStructure, index []int) (*PartViewer, error) { @@ -475,6 +474,26 @@ func NewPartViewer(conf *config.AercConfig, } } + grid := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 3}, // Message + {ui.SIZE_EXACT, 1}, // Selector + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + + selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0). + OnChoose(func(option string) { + switch option { + case "Save message": + acct.aerc.BeginExCommand("save ") + case "Pipe to command": + acct.aerc.BeginExCommand("pipe ") + } + }) + + grid.AddChild(selecter).At(2, 0) + pv := &PartViewer{ filter: filter, index: index, @@ -486,6 +505,8 @@ func NewPartViewer(conf *config.AercConfig, sink: pipe, store: store, term: term, + selecter: selecter, + grid: grid, } if term != nil { @@ -590,6 +611,10 @@ func (pv *PartViewer) Draw(ctx *ui.Context) { ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed), "No filter configured for this mimetype") + ctx.Printf(0, 2, tcell.StyleDefault, + "You can still :save the message or :pipe it to an external command") + pv.selecter.Focus(true) + pv.grid.Draw(ctx) return } if !pv.fetched { @@ -611,6 +636,13 @@ func (pv *PartViewer) Cleanup() { } } +func (pv *PartViewer) Event(event tcell.Event) bool { + if pv.term != nil { + return pv.term.Event(event) + } + return pv.selecter.Event(event) +} + type HeaderView struct { ui.Invalidatable Name string diff --git a/widgets/selecter.go b/widgets/selecter.go new file mode 100644 index 0000000..7fae9cd --- /dev/null +++ b/widgets/selecter.go @@ -0,0 +1,103 @@ +package widgets + +import ( + "github.com/gdamore/tcell" + + "git.sr.ht/~sircmpwn/aerc/lib/ui" +) + +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/tabhost.go b/widgets/tabhost.go index 2c33cf8..0ac67e5 100644 --- a/widgets/tabhost.go +++ b/widgets/tabhost.go @@ -5,7 +5,7 @@ import ( ) type TabHost interface { - BeginExCommand() + BeginExCommand(cmd string) SetStatus(status string) *StatusMessage PushStatus(text string, expiry time.Duration) *StatusMessage Beep()