aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--commands/account/split.go63
-rw-r--r--doc/aerc.1.scd17
-rw-r--r--lib/ui/grid.go18
-rw-r--r--widgets/account.go153
5 files changed, 252 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6554f61f..9cc5c0f5 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 00000000..2b802256
--- /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 d416e072..328576ad 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* [+|-] [<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>]...
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* [+|-] [<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
*close*
diff --git a/lib/ui/grid.go b/lib/ui/grid.go
index 76eba000..28640d03 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 cafc29de..b6000f7c 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()
+}