aerc

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

commit 577248f5e15d98a9a6522a605acd434059582bfc
parent c05e5f73f29566812b7623311db8c6196c7be063
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun, 12 May 2019 00:06:09 -0400

Add initial compose widget

Diffstat:
Maerc.go | 5+++++
Acommands/account/compose.go | 28++++++++++++++++++++++++++++
Mconfig/binds.conf | 8++++++++
Mlib/ui/textinput.go | 5+++--
Mwidgets/aerc.go | 2++
Awidgets/compose.go | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwidgets/exline.go | 2+-
7 files changed, 169 insertions(+), 3 deletions(-)

diff --git a/aerc.go b/aerc.go @@ -25,6 +25,11 @@ func getCommands(selected libui.Drawable) []*commands.Commands { account.AccountCommands, commands.GlobalCommands, } + case *widgets.Composer: + return []*commands.Commands{ + // TODO: compose-specific commands + commands.GlobalCommands, + } case *widgets.MessageViewer: return []*commands.Commands{ msgview.MessageViewCommands, diff --git a/commands/account/compose.go b/commands/account/compose.go @@ -0,0 +1,28 @@ +package account + +import ( + "errors" + + "github.com/mattn/go-runewidth" + + "git.sr.ht/~sircmpwn/aerc2/widgets" +) + +func init() { + register("compose", Compose) +} + +// TODO: Accept arguments for default headers, message body +func Compose(aerc *widgets.Aerc, args []string) error { + if len(args) != 1 { + return errors.New("Usage: compose") + } + // TODO: Pass along the sender info + composer := widgets.NewComposer() + // TODO: Change tab name when message subject changes + aerc.NewTab(composer, runewidth.Truncate( + "New email", 32, "…")) + return nil +} + + diff --git a/config/binds.conf b/config/binds.conf @@ -39,6 +39,14 @@ r = :reply<Enter> a = :reply -a<Enter> f = :forward<Enter> +[compose] +$noinherit = true +$ex = <semicolon> +<C-k> = :prev-field<Enter> +<C-j> = :next-field<Enter> +<C-p> = :prev-tab<Enter> +<C-n> = :next-tab<Enter> + [terminal] $noinherit = true $ex = <semicolon> diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go @@ -22,10 +22,11 @@ type TextInput struct { // Creates a new TextInput. TextInputs will render a "textbox" in the entire // context they're given, and process keypresses to build a string from user // input. -func NewTextInput() *TextInput { +func NewTextInput(text string) *TextInput { return &TextInput{ cells: -1, - text: []rune{}, + text: []rune(text), + index: len([]rune(text)), } } diff --git a/widgets/aerc.go b/widgets/aerc.go @@ -91,6 +91,8 @@ func (aerc *Aerc) getBindings() *config.KeyBindings { switch aerc.SelectedTab().(type) { case *AccountView: return aerc.conf.Bindings.MessageList + case *Composer: + return aerc.conf.Bindings.Compose case *MessageViewer: return aerc.conf.Bindings.MessageView case *Terminal: diff --git a/widgets/compose.go b/widgets/compose.go @@ -0,0 +1,122 @@ +package widgets + +import ( + "os/exec" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + + "git.sr.ht/~sircmpwn/aerc2/lib/ui" +) + +type headerEditor struct { + ui.Invalidatable + name string + input *ui.TextInput +} + +type Composer struct { + headers struct { + from *headerEditor + subject *headerEditor + to *headerEditor + } + + editor *Terminal + grid *ui.Grid + + focusable []ui.DrawableInteractive + focused int +} + +// TODO: Let caller configure headers, initial body (for replies), etc +func NewComposer() *Composer { + grid := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 3}, + {ui.SIZE_WEIGHT, 1}, + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + }) + + // TODO: let user specify extra headers to edit by default + headers := ui.NewGrid().Rows([]ui.GridSpec{ + {ui.SIZE_EXACT, 1}, // To/From + {ui.SIZE_EXACT, 1}, // Subject + {ui.SIZE_EXACT, 1}, // [spacer] + }).Columns([]ui.GridSpec{ + {ui.SIZE_WEIGHT, 1}, + {ui.SIZE_WEIGHT, 1}, + }) + + headers.AddChild(newHeaderEditor("To", "Simon Ser <contact@emersion.fr>")).At(0, 0) + headers.AddChild(newHeaderEditor("From", "Drew DeVault <sir@cmpwn.com>")).At(0, 1) + headers.AddChild(newHeaderEditor("Subject", "Re: [PATCH RFC aerc2] widgets: fix StatusLine race")).At(1, 0).Span(1, 2) + headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2) + + // TODO: built-in config option, $EDITOR, then vi, in that order + // TODO: temp file + editor := exec.Command("vim") + term, _ := NewTerminal(editor) + + grid.AddChild(headers).At(0, 0) + grid.AddChild(term).At(1, 0) + + return &Composer{ + grid: grid, + editor: term, + focused: 0, + focusable: []ui.DrawableInteractive{ + term, + }, + } +} + +func (c *Composer) Draw(ctx *ui.Context) { + c.grid.Draw(ctx) +} + +func (c *Composer) Invalidate() { + c.grid.Invalidate() +} + +func (c *Composer) OnInvalidate(fn func(d ui.Drawable)) { + c.grid.OnInvalidate(func(_ ui.Drawable) { + fn(c) + }) +} + +// TODO: Focus various fields separately +// TODO: Consider having a different set of keybindings for a focused and +// unfocused terminal? +func (c *Composer) Event(event tcell.Event) bool { + if c.editor != nil { + return c.editor.Event(event) + } + return false +} + +func (c *Composer) Focus(focus bool) { + if c.editor != nil { + c.editor.Focus(focus) + } +} + +func newHeaderEditor(name string, value string) *headerEditor { + // TODO: Set default vaule to something sane, I guess + return &headerEditor{ + input: ui.NewTextInput(value), + name: name, + } +} + +func (he *headerEditor) Draw(ctx *ui.Context) { + name := he.name + " " + size := runewidth.StringWidth(name) + ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault) + ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name) + he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1)) +} + +func (he *headerEditor) Invalidate() { + he.DoInvalidate(he) +} diff --git a/widgets/exline.go b/widgets/exline.go @@ -14,7 +14,7 @@ type ExLine struct { } func NewExLine(commit func(cmd string), cancel func()) *ExLine { - input := ui.NewTextInput().Prompt(":") + input := ui.NewTextInput("").Prompt(":") exline := &ExLine{ cancel: cancel, commit: commit,