Add initial compose widget

This commit is contained in:
Drew DeVault 2019-05-12 00:06:09 -04:00
parent c05e5f73f2
commit 577248f5e1
7 changed files with 169 additions and 3 deletions

View file

@ -25,6 +25,11 @@ func getCommands(selected libui.Drawable) []*commands.Commands {
account.AccountCommands, account.AccountCommands,
commands.GlobalCommands, commands.GlobalCommands,
} }
case *widgets.Composer:
return []*commands.Commands{
// TODO: compose-specific commands
commands.GlobalCommands,
}
case *widgets.MessageViewer: case *widgets.MessageViewer:
return []*commands.Commands{ return []*commands.Commands{
msgview.MessageViewCommands, msgview.MessageViewCommands,

View file

@ -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
}

View file

@ -39,6 +39,14 @@ r = :reply<Enter>
a = :reply -a<Enter> a = :reply -a<Enter>
f = :forward<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] [terminal]
$noinherit = true $noinherit = true
$ex = <semicolon> $ex = <semicolon>

View file

@ -22,10 +22,11 @@ type TextInput struct {
// Creates a new TextInput. TextInputs will render a "textbox" in the entire // 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 // context they're given, and process keypresses to build a string from user
// input. // input.
func NewTextInput() *TextInput { func NewTextInput(text string) *TextInput {
return &TextInput{ return &TextInput{
cells: -1, cells: -1,
text: []rune{}, text: []rune(text),
index: len([]rune(text)),
} }
} }

View file

@ -91,6 +91,8 @@ func (aerc *Aerc) getBindings() *config.KeyBindings {
switch aerc.SelectedTab().(type) { switch aerc.SelectedTab().(type) {
case *AccountView: case *AccountView:
return aerc.conf.Bindings.MessageList return aerc.conf.Bindings.MessageList
case *Composer:
return aerc.conf.Bindings.Compose
case *MessageViewer: case *MessageViewer:
return aerc.conf.Bindings.MessageView return aerc.conf.Bindings.MessageView
case *Terminal: case *Terminal:

122
widgets/compose.go Normal file
View file

@ -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)
}

View file

@ -14,7 +14,7 @@ type ExLine struct {
} }
func NewExLine(commit func(cmd string), cancel func()) *ExLine { func NewExLine(commit func(cmd string), cancel func()) *ExLine {
input := ui.NewTextInput().Prompt(":") input := ui.NewTextInput("").Prompt(":")
exline := &ExLine{ exline := &ExLine{
cancel: cancel, cancel: cancel,
commit: commit, commit: commit,