ui: add :split and :vsplit view options

Add :split and :vsplit commands, which split the message list view to
include a message viewer. Each command takes an int, or a delta value
("+1", "-1"). The int value is the resulting size of the message list,
and a new message viewer will be displayed below / to the right of the
message list. This viewer *does not* set seen flags.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Tim Culverhouse 2022-10-17 15:16:11 -05:00 committed by Robin Jarry
parent 7016c6f86a
commit bad694e466
5 changed files with 252 additions and 0 deletions

View File

@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
defined in their environment. defined in their environment.
- Warn before sending emails that may need an attachment with - Warn before sending emails that may need an attachment with
`no-attachment-warning` in `aerc.conf`. `no-attachment-warning` in `aerc.conf`.
- 3 panel view via `:split` and `:vsplit`
### Changed ### Changed

63
commands/account/split.go Normal file
View File

@ -0,0 +1,63 @@
package account
import (
"errors"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/widgets"
)
type Split struct{}
func init() {
register(Split{})
}
func (Split) Aliases() []string {
return []string{"split", "vsplit"}
}
func (Split) Complete(aerc *widgets.Aerc, args []string) []string {
return nil
}
func (Split) Execute(aerc *widgets.Aerc, args []string) error {
if len(args) > 2 {
return errors.New("Usage: [v]split n")
}
acct := aerc.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
n := 0
var err error
if len(args) > 1 {
delta := false
if strings.HasPrefix(args[1], "+") || strings.HasPrefix(args[1], "-") {
delta = true
}
n, err = strconv.Atoi(args[1])
if err != nil {
return errors.New("Usage: [v]split n")
}
if delta {
n = acct.SplitSize() + n
// Maintain split direction when using deltas
if acct.SplitSize() > 0 {
args[0] = acct.SplitDirection()
}
}
}
if n == acct.SplitSize() {
// Repeated commands of the same size have the effect of
// toggling the split
n = 0
}
if args[0] == "split" {
acct.Split(n)
return nil
}
acct.Vsplit(n)
return nil
}

View File

@ -355,6 +355,14 @@ message list, the message in the message viewer, etc).
Selects the nth message in the message list (and scrolls it into view if Selects the nth message in the message list (and scrolls it into view if
necessary). necessary).
*split* [+|-] [<n>]
Creates a horizontal split, showing a <n> messages and a message view
below the message list. If a + or - is prepended, the message list size
will grow or shrink accordingly. The split can be cleared by calling
:split 0, or just :split. The split can be toggled by calling split with
the same size repeatedly. For example, :split 10 will create a split.
Calling :split 10 again will remove the split. Also see *vsplit*
*sort* [[-r] <criterion>]... *sort* [[-r] <criterion>]...
Sorts the message list by the given criteria. *-r* sorts the Sorts the message list by the given criteria. *-r* sorts the
immediately following criterion in reverse order. immediately following criterion in reverse order.
@ -389,6 +397,15 @@ message list, the message in the message viewer, etc).
flag -p is set, the message will not be marked as "seen" and ignores the flag -p is set, the message will not be marked as "seen" and ignores the
"auto-mark-read" config. "auto-mark-read" config.
*vsplit* [+|-] [<n>]
Creates a vertical split of the message list. The message list will be
<n> columns wide, and a vertical message view will be shown to the right
of the message list. If a + or - is prepended, the message list size
will grow or shrink accordingly. The split can be cleared by calling
:vsplit 0, or just :vsplit. The split can be toggled by calling split
with the same size repeatedly. For example, :vsplit 10 will create a
split. Calling :vsplit 10 again will remove the split. Also see *split*
## MESSAGE VIEW COMMANDS ## MESSAGE VIEW COMMANDS
*close* *close*

View File

@ -234,6 +234,24 @@ func (grid *Grid) RemoveChild(content Drawable) {
grid.Invalidate() grid.Invalidate()
} }
func (grid *Grid) ReplaceChild(old Drawable, new Drawable) {
grid.mutex.Lock()
for i, cell := range grid.cells {
if cell.Content == old {
grid.cells[i] = &GridCell{
RowSpan: cell.RowSpan,
ColSpan: cell.ColSpan,
Row: cell.Row,
Column: cell.Column,
Content: new,
}
break
}
}
grid.mutex.Unlock()
grid.Invalidate()
}
func Const(i int) func() int { func Const(i int) func() int {
return func() int { return i } return func() int { return i }
} }

View File

@ -37,6 +37,12 @@ type AccountView struct {
newConn bool // True if this is a first run after a new connection/reconnection newConn bool // True if this is a first run after a new connection/reconnection
uiConf *config.UIConfig uiConf *config.UIConfig
split *MessageViewer
splitSize int
splitDebounce *time.Timer
splitMsg *models.MessageInfo
splitDir string
// Check-mail ticker // Check-mail ticker
ticker *time.Ticker ticker *time.Ticker
checkingMail bool checkingMail bool
@ -151,6 +157,9 @@ func (acct *AccountView) Draw(ctx *ui.Context) {
if acct.state.SetWidth(ctx.Width()) { if acct.state.SetWidth(ctx.Width()) {
acct.UpdateStatus() acct.UpdateStatus()
} }
if acct.SplitSize() > 0 {
acct.UpdateSplitView()
}
acct.grid.Draw(ctx) acct.grid.Draw(ctx)
} }
@ -466,3 +475,147 @@ func (acct *AccountView) CheckMailTimer(d time.Duration) {
} }
}() }()
} }
func (acct *AccountView) clearSplit() {
if acct.split != nil {
acct.split.Close()
}
acct.splitSize = 0
acct.splitDir = ""
acct.split = nil
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}).Columns([]ui.GridSpec{
{Strategy: ui.SIZE_EXACT, Size: func() int {
return acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
if acct.uiConf.SidebarWidth > 0 {
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.uiConf))
}
acct.grid.AddChild(acct.msglist).At(0, 1)
ui.Invalidate()
}
func (acct *AccountView) UpdateSplitView() {
if acct.Store() == nil {
return
}
if acct.splitMsg == acct.msglist.Selected() {
return
}
if acct.splitDebounce != nil {
acct.splitDebounce.Stop()
}
fn := func() {
if acct.split != nil {
acct.split.Close()
}
msg, err := acct.SelectedMessage()
if err != nil {
return
}
lib.NewMessageStoreView(msg, false, acct.Store(), acct.aerc.Crypto, acct.aerc.DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
acct.aerc.PushError(err.Error())
return
}
orig := acct.split
acct.split = NewMessageViewer(acct, acct.conf, view)
acct.grid.ReplaceChild(orig, acct.split)
})
acct.splitMsg = msg
ui.Invalidate()
}
acct.splitDebounce = time.AfterFunc(100*time.Millisecond, func() {
ui.QueueFunc(fn)
})
}
func (acct *AccountView) SplitSize() int {
return acct.splitSize
}
func (acct *AccountView) SplitDirection() string {
return acct.splitDir
}
// Split splits the message list view horizontally. The message list will be n
// rows high. If n is 0, any existing split is removed
func (acct *AccountView) Split(n int) {
if n == 0 {
acct.clearSplit()
return
}
acct.splitSize = n
acct.splitDir = "split"
if acct.split != nil {
acct.split.Close()
}
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
// Add 1 so that the splitSize is the number of visible messages
{Strategy: ui.SIZE_EXACT, Size: ui.Const(acct.splitSize + 1)},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}).Columns([]ui.GridSpec{
{Strategy: ui.SIZE_EXACT, Size: func() int {
return acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
if acct.uiConf.SidebarWidth > 0 {
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.uiConf)).Span(2, 1)
}
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_BOTTOM, acct.uiConf)).At(0, 1)
lib.NewMessageStoreView(acct.msglist.Selected(), false, acct.Store(), acct.aerc.Crypto, acct.aerc.DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
acct.aerc.PushError(err.Error())
return
}
acct.split = NewMessageViewer(acct, acct.conf, view)
acct.grid.AddChild(acct.split).At(1, 1)
})
ui.Invalidate()
}
// Vsplit splits the message list view vertically. The message list will be n
// rows wide. If n is 0, any existing split is removed
func (acct *AccountView) Vsplit(n int) {
if n == 0 {
acct.clearSplit()
return
}
acct.splitSize = n
acct.splitDir = "vsplit"
if acct.split != nil {
acct.split.Close()
}
acct.grid = ui.NewGrid().Rows([]ui.GridSpec{
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
}).Columns([]ui.GridSpec{
{Strategy: ui.SIZE_EXACT, Size: func() int {
return acct.UiConfig().SidebarWidth
}},
{Strategy: ui.SIZE_EXACT, Size: ui.Const(acct.splitSize)},
{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
})
if acct.uiConf.SidebarWidth > 0 {
acct.grid.AddChild(ui.NewBordered(acct.dirlist, ui.BORDER_RIGHT, acct.uiConf)).At(0, 0)
}
acct.grid.AddChild(ui.NewBordered(acct.msglist, ui.BORDER_RIGHT, acct.uiConf)).At(0, 1)
lib.NewMessageStoreView(acct.msglist.Selected(), false, acct.Store(), acct.aerc.Crypto, acct.aerc.DecryptKeys,
func(view lib.MessageView, err error) {
if err != nil {
acct.aerc.PushError(err.Error())
return
}
acct.split = NewMessageViewer(acct, acct.conf, view)
acct.grid.AddChild(acct.split).At(0, 2)
})
ui.Invalidate()
}