~sircmpwn/aerc

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH] Add UI options to save/pipe messages with unsupported mimetypes

Details
Message ID
<20191115202833.77006-1-greg@gpanders.com>
DKIM signature
missing
Download raw message
Patch: +163 -123
Adds a message indicating the user's ability to :save or :pipe a message
with an unsupported mimetype and also adds a selector widget (similar to
the tutorial).

The selector widget was previously defined in the account wizard module,
so this commit breaks it out into its own module to allow for re-use.

Further, modify the BeginExLine() function to take an argument that
pre-populates the command line, allowing functions to initiate an ex
command without executing it.

Closes #95.
---
 lib/ui/textinput.go       |   3 +-
 widgets/account-wizard.go | 110 +++-----------------------------------
 widgets/aerc.go           |   6 +--
 widgets/exline.go         |   4 +-
 widgets/msgviewer.go      |  58 +++++++++++++++-----
 widgets/selecter.go       | 103 +++++++++++++++++++++++++++++++++++
 widgets/tabhost.go        |   2 +-
 7 files changed, 163 insertions(+), 123 deletions(-)
 create mode 100644 widgets/selecter.go

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 7cd5553..b704832 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()
-- 
2.24.0
Details
Message ID
<BYIDRGZJ9QEO.1FPI76LWPFPJZ@homura>
In-Reply-To
<20191115202833.77006-1-greg@gpanders.com> (view parent)
DKIM signature
pass
Download raw message
Thanks!

To git.sr.ht:~sircmpwn/aerc
   8a84830..3338dce  master -> master