From 577248f5e15d98a9a6522a605acd434059582bfc Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sun, 12 May 2019 00:06:09 -0400 Subject: [PATCH] Add initial compose widget --- aerc.go | 5 ++ commands/account/compose.go | 28 +++++++++ config/binds.conf | 8 +++ lib/ui/textinput.go | 5 +- widgets/aerc.go | 2 + widgets/compose.go | 122 ++++++++++++++++++++++++++++++++++++ widgets/exline.go | 2 +- 7 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 commands/account/compose.go create mode 100644 widgets/compose.go diff --git a/aerc.go b/aerc.go index 5b6f9d7..978d448 100644 --- 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 new file mode 100644 index 0000000..15fc354 --- /dev/null +++ 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 index 520c731..1102c21 100644 --- a/config/binds.conf +++ b/config/binds.conf @@ -39,6 +39,14 @@ r = :reply a = :reply -a f = :forward +[compose] +$noinherit = true +$ex = + = :prev-field + = :next-field + = :prev-tab + = :next-tab + [terminal] $noinherit = true $ex = diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index 542a1f8..3e1f68a 100644 --- 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 index 773848d..fb109d4 100644 --- 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 new file mode 100644 index 0000000..cf3dac9 --- /dev/null +++ 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 ")).At(0, 0) + headers.AddChild(newHeaderEditor("From", "Drew DeVault ")).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 index a5b896f..7b7530d 100644 --- 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,