diff options
| -rw-r--r-- | aerc.go | 5 | ||||
| -rw-r--r-- | commands/account/compose.go | 28 | ||||
| -rw-r--r-- | config/binds.conf | 8 | ||||
| -rw-r--r-- | lib/ui/textinput.go | 5 | ||||
| -rw-r--r-- | widgets/aerc.go | 2 | ||||
| -rw-r--r-- | widgets/compose.go | 122 | ||||
| -rw-r--r-- | widgets/exline.go | 2 | 
7 files changed, 169 insertions, 3 deletions
| @@ -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<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 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 <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 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, | 
