From bad694e466705db83da2e864a3988255eb97055a Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 17 Oct 2022 15:16:11 -0500 Subject: [PATCH] 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 Acked-by: Robin Jarry --- CHANGELOG.md | 1 + commands/account/split.go | 63 ++++++++++++++++ doc/aerc.1.scd | 17 +++++ lib/ui/grid.go | 18 +++++ widgets/account.go | 153 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 commands/account/split.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6554f61..9cc5c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). defined in their environment. - Warn before sending emails that may need an attachment with `no-attachment-warning` in `aerc.conf`. +- 3 panel view via `:split` and `:vsplit` ### Changed diff --git a/commands/account/split.go b/commands/account/split.go new file mode 100644 index 0000000..2b80225 --- /dev/null +++ b/commands/account/split.go @@ -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 +} diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index d416e07..328576a 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -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 necessary). +*split* [+|-] [] + Creates a horizontal split, showing a 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] ]... Sorts the message list by the given criteria. *-r* sorts the 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 "auto-mark-read" config. +*vsplit* [+|-] [] + Creates a vertical split of the message list. The message list will be + 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 *close* diff --git a/lib/ui/grid.go b/lib/ui/grid.go index 76eba00..28640d0 100644 --- a/lib/ui/grid.go +++ b/lib/ui/grid.go @@ -234,6 +234,24 @@ func (grid *Grid) RemoveChild(content Drawable) { 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 { return func() int { return i } } diff --git a/widgets/account.go b/widgets/account.go index cafc29d..b6000f7 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -37,6 +37,12 @@ type AccountView struct { newConn bool // True if this is a first run after a new connection/reconnection uiConf *config.UIConfig + split *MessageViewer + splitSize int + splitDebounce *time.Timer + splitMsg *models.MessageInfo + splitDir string + // Check-mail ticker ticker *time.Ticker checkingMail bool @@ -151,6 +157,9 @@ func (acct *AccountView) Draw(ctx *ui.Context) { if acct.state.SetWidth(ctx.Width()) { acct.UpdateStatus() } + if acct.SplitSize() > 0 { + acct.UpdateSplitView() + } 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() +}