aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--commands/account/import-mbox.go2
-rw-r--r--config/aerc.conf26
-rw-r--r--config/columns.go15
-rw-r--r--config/statusline.go138
-rw-r--r--config/templates.go6
-rw-r--r--config/ui.go2
-rw-r--r--doc/aerc-config.5.scd58
-rw-r--r--doc/aerc-templates.7.scd39
-rw-r--r--lib/state/renderer.go205
-rw-r--r--lib/state/state.go86
-rw-r--r--lib/state/templates.go78
-rw-r--r--lib/state/texter.go21
-rw-r--r--models/templates.go6
-rw-r--r--widgets/account.go12
-rw-r--r--widgets/aerc.go18
-rw-r--r--widgets/status.go95
-rw-r--r--widgets/tabhost.go4
18 files changed, 447 insertions, 366 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7bd13e6b..e32cafd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add a `trimSignature` function to the templating engine.
- Change local domain name for SMTP with `smtp-domain=example.com` in
`aerc.conf`
+- New column-based status line format with `status-columns`.
### Changed
@@ -29,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `[ui].index-format` setting has been replaced by `index-columns`.
- `[triggers].new-email` now needs to use `aerc-templates(7)` syntax instead
of the (now deprecated) `index-format` placeholders.
+- `[statusline].render-format` has been replaced by `status-columns`.
## [0.14.0](https://git.sr.ht/~rjarry/aerc/refs/0.14.0) - 2023-01-04
diff --git a/commands/account/import-mbox.go b/commands/account/import-mbox.go
index 5d6a0a0c..85e9a341 100644
--- a/commands/account/import-mbox.go
+++ b/commands/account/import-mbox.go
@@ -125,7 +125,7 @@ func (ImportMbox) Execute(aerc *widgets.Aerc, args []string) error {
}
infoStr := fmt.Sprintf("%s: imported %d of %d successfully.", args[0], appended, len(messages))
log.Debugf(infoStr)
- aerc.SetStatus(infoStr)
+ aerc.PushSuccess(infoStr)
}
if len(store.Uids()) > 0 {
diff --git a/config/aerc.conf b/config/aerc.conf
index 43d17727..38049b77 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -279,10 +279,30 @@
#client-threads-delay=50ms
[statusline]
-# Describes the format string for the statusline.
#
-# Default: [%a] %S %>%T
-#render-format=[%a] %S %>%T
+# Describes the format for the status line. This is a comma separated list of
+# column names with an optional align and width suffix. See [ui].index-columns
+# for more details. To completely mute the status line except for push
+# notifications, explicitly set status-columns to an empty string.
+#
+# Default: left<*,center:=,right>*
+#status-columns=left<*,center:=,right>*
+
+#
+# Each name in status-columns must have a corresponding column-$name setting.
+# All column-$name settings accept golang text/template syntax. See
+# aerc-templates(7) for available template attributes and functions.
+#
+# Default settings
+#column-left=[{{.Account}}] {{.StatusInfo}}
+#column-center={{.PendingKeys}}
+#column-right={{.TrayInfo}}
+
+#
+# String separator inserted between columns.
+# See [ui].column-separator for more details.
+#
+#column-separator=" "
# Specifies the separator between grouped statusline elements.
#
diff --git a/config/columns.go b/config/columns.go
index 55efa2a7..e1389bc0 100644
--- a/config/columns.go
+++ b/config/columns.go
@@ -2,6 +2,7 @@ package config
import (
"bytes"
+ "crypto/sha256"
"fmt"
"reflect"
"regexp"
@@ -113,7 +114,7 @@ func ParseColumnDefs(key *ini.Key, section *ini.Section) ([]*ColumnDef, error) {
columns = append(columns, c)
}
if len(columns) == 0 {
- return nil, fmt.Errorf("%s cannot be empty", key.Name())
+ return nil, nil
}
return columns, nil
}
@@ -156,3 +157,15 @@ func ColumnDefsToIni(defs []*ColumnDef, keyName string) string {
return s.String()
}
+
+var templateFieldNameRe = regexp.MustCompile(`\{\{\.?(\w+)\}\}`)
+
+func columnNameFromTemplate(s string) string {
+ match := templateFieldNameRe.FindStringSubmatch(s)
+ if match == nil {
+ h := sha256.New()
+ h.Write([]byte(s))
+ return fmt.Sprintf("%x", h.Sum(nil)[:3])
+ }
+ return strings.ReplaceAll(strings.ToLower(match[1]), "info", "")
+}
diff --git a/config/statusline.go b/config/statusline.go
index 483241c0..7cc2140c 100644
--- a/config/statusline.go
+++ b/config/statusline.go
@@ -1,21 +1,50 @@
package config
import (
+ "regexp"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/log"
"github.com/go-ini/ini"
)
type StatuslineConfig struct {
+ StatusColumns []*ColumnDef `ini:"-"`
+ ColumnSeparator string `ini:"column-separator"`
+ Separator string `ini:"separator"`
+ DisplayMode string `ini:"display-mode"`
+ // deprecated
RenderFormat string `ini:"render-format"`
- Separator string
- DisplayMode string `ini:"display-mode"`
}
func defaultStatuslineConfig() *StatuslineConfig {
+ left, _ := templates.ParseTemplate("column-left", `[{{.Account}}] {{.StatusInfo}}`)
+ center, _ := templates.ParseTemplate("column-center", `{{.PendingKeys}}`)
+ right, _ := templates.ParseTemplate("column-right", `{{.TrayInfo}}`)
return &StatuslineConfig{
- RenderFormat: "[%a] %S %>%T",
- Separator: " | ",
- DisplayMode: "",
+ StatusColumns: []*ColumnDef{
+ {
+ Name: "left",
+ Template: left,
+ Flags: ALIGN_LEFT | WIDTH_AUTO,
+ },
+ {
+ Name: "center",
+ Template: center,
+ Flags: ALIGN_CENTER | WIDTH_FIT,
+ },
+ {
+ Name: "right",
+ Template: right,
+ Flags: ALIGN_RIGHT | WIDTH_AUTO,
+ },
+ },
+ ColumnSeparator: " ",
+ Separator: " | ",
+ DisplayMode: "text",
+ // deprecated
+ RenderFormat: "",
}
}
@@ -29,7 +58,106 @@ func parseStatusline(file *ini.File) error {
if err := statusline.MapTo(&Statusline); err != nil {
return err
}
+
+ if key, err := statusline.GetKey("status-columns"); err == nil {
+ columns, err := ParseColumnDefs(key, statusline)
+ if err != nil {
+ return err
+ }
+ Statusline.StatusColumns = columns
+ } else if Statusline.RenderFormat != "" {
+ columns, err := convertRenderFormat()
+ if err != nil {
+ return err
+ }
+ Statusline.StatusColumns = columns
+ log.Warnf("%s %s",
+ "The [statusline] render-format setting has been replaced by status-columns.",
+ "render-format will be removed in aerc 0.17.")
+ Warnings = append(Warnings, Warning{
+ Title: "DEPRECATION WARNING: [statusline].render-format",
+ Body: `
+The render-format setting is deprecated. It has been replaced by status-columns.
+
+Your configuration in this instance was automatically converted to:
+
+[statusline]
+` + ColumnDefsToIni(columns, "status-columns") + `
+Your configuration file was not changed. To make this change permanent and to
+dismiss this deprecation warning on launch, copy the above lines into aerc.conf
+and remove index-format from it. See aerc-config(5) for more details.
+
+index-format will be removed in aerc 0.17.
+`,
+ })
+ }
+
out:
log.Debugf("aerc.conf: [statusline] %#v", Statusline)
return nil
}
+
+var (
+ renderFmtRe = regexp.MustCompile(`%(-?\d+)?(\.\d+)?[acdmSTp]`)
+ statuslineMute = false
+)
+
+func convertRenderFormat() ([]*ColumnDef, error) {
+ var columns []*ColumnDef
+
+ tokens := strings.Split(Statusline.RenderFormat, "%>")
+
+ left := renderFmtRe.ReplaceAllStringFunc(
+ tokens[0], renderVerbToTemplate)
+ left = strings.TrimSpace(left)
+ t, err := templates.ParseTemplate("column-left", left)
+ if err != nil {
+ return nil, err
+ }
+ columns = append(columns, &ColumnDef{
+ Name: "left",
+ Template: t,
+ Flags: ALIGN_LEFT | WIDTH_AUTO,
+ })
+
+ if len(tokens) == 2 {
+ right := renderFmtRe.ReplaceAllStringFunc(
+ tokens[1], renderVerbToTemplate)
+ right = strings.TrimSpace(right)
+ t, err := templates.ParseTemplate("column-right", right)
+ if err != nil {
+ return nil, err
+ }
+ columns = append(columns, &ColumnDef{
+ Name: "right",
+ Template: t,
+ Flags: ALIGN_RIGHT | WIDTH_AUTO,
+ })
+ }
+
+ if statuslineMute {
+ columns = nil
+ }
+
+ return columns, nil
+}
+
+func renderVerbToTemplate(verb string) (template string) {
+ switch verb[len(verb)-1] {
+ case 'a':
+ template = `{{.Account}}`
+ case 'c':
+ template = `{{.ConnectionInfo}}`
+ case 'd':
+ template = `{{.Folder}}`
+ case 'S':
+ template = `{{.StatusInfo}}`
+ case 'T':
+ template = `{{.TrayInfo}}`
+ case 'p':
+ template = `{{cwd}}`
+ case 'm':
+ statuslineMute = true
+ }
+ return template
+}
diff --git a/config/templates.go b/config/templates.go
index 3fce9857..0f3870c5 100644
--- a/config/templates.go
+++ b/config/templates.go
@@ -104,3 +104,9 @@ func (d *dummyData) OriginalHeader(string) string { return "" }
func (d *dummyData) Recent(...string) int { return 1 }
func (d *dummyData) Unread(...string) int { return 3 }
func (d *dummyData) Exists(...string) int { return 14 }
+func (d *dummyData) Connected() bool { return false }
+func (d *dummyData) ConnectionInfo() string { return "" }
+func (d *dummyData) ContentInfo() string { return "" }
+func (d *dummyData) StatusInfo() string { return "" }
+func (d *dummyData) TrayInfo() string { return "" }
+func (d *dummyData) PendingKeys() string { return "" }
diff --git a/config/ui.go b/config/ui.go
index b59c9b09..d4b36f34 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -494,7 +494,7 @@ func indexVerbToTemplate(verb rune) (f, name string) {
f = "%" + string(verb)
}
if name == "" {
- name = "wtf"
+ name = columnNameFromTemplate(f)
}
return
}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 6e20fd71..4d995d6b 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -422,41 +422,43 @@ index-format=...
These options are configured in the *[statusline]* section of _aerc.conf_.
-*render-format* = _<format>_
- Describes the format string for the statusline format.
+*status-columns* = _<column1,column2,column3...>_
+ Describes the format for the statusline. This is a comma separated list
+ of column names with an optional align and width suffix. See
+ *[ui].index-columns* for more details.
- For a minimal statusline that only shows the current account and
- the connection information, use _[%a] %c_.
+ To completely mute the statusline (except for push notifications),
+ explicitly set *status-columns* to an empty string:
- To completely mute the statusline (except for push notifications), use
- _%m_ only.
+ status-columns=
- Default: _[%a] %S %>%T_
+ Default: _left<\*,center>=,right>\*_
-[- *Format specifier*
-:[ *Description*
-| _%%_
-: literal %
-| _%a_
-: active account name
-| _%d_
-: active directory name
-| _%c_
-: connection state
-| _%p_
-: current path
-| _%m_
-: mute statusline and show only push notifications
-| _%S_
-: general status information (e.g. connection state, filter, search)
-| _%T_
-: general on/off information (e.g. passthrough, threading, sorting)
-| _%>_
-: does not print anything but all format specifier that follow will be right justified.
+*column-separator* = _"<separator>"_
+ String separator inserted between columns. See *[ui].column-separator*
+ for more details.
+
+ Default: _" "_
+
+*column-<name>* = _<go template>_
+ Each name in *status-columns* must have a corresponding *column-<name>*
+ setting. All *column-<name>* settings accept golang text/template
+ syntax.
+
+ By default, these columns are defined:
+
+ ```
+ column-left = [{{.Account}}] {{.StatusInfo}}
+ column-center = {{.PendingKeys}}
+ column-right = {{.TrayInfo}}
+ ```
+
+ See *aerc-templates*(7) for all available symbols and functions.
*separator* = _"<string>"_
Specifies the separator between grouped statusline elements (e.g. for
- the _%S_ and _%T_ specifiers in *render-format*).
+ the _{{.ContentInfo}}_, _{{.TrayInfo}}_ and _{{.StatusInfo}}_ in
+ *column-<name>*).
Default: _" | "_
diff --git a/doc/aerc-templates.7.scd b/doc/aerc-templates.7.scd
index 71042835..8e1bcf1b 100644
--- a/doc/aerc-templates.7.scd
+++ b/doc/aerc-templates.7.scd
@@ -162,6 +162,45 @@ available always.
{{.Exists "archive" "spam" "foo/baz" "foo/bar"}}
```
+*Status line*
+
+ The following data will only be available in the status line templates:
+
+ Connection state.
+
+ ```
+ {{.Connected}}
+ {{.ConnectionInfo}}
+ ```
+
+ General status information (e.g. filter, search) separated with
+ *[statusline].separator*.
+
+ ```
+ {{.ContentInfo}}
+ ```
+
+ Combination of *{{.ConnectionInfo}}* and *{{.StatusInfo}}* separated
+ with *[statusline].separator*.
+
+ ```
+ {{.StatusInfo}}
+ ```
+
+ General on/off information (e.g. passthrough, threading, sorting),
+ separated with *[statusline].separator*.
+
+ ```
+ {{.TrayInfo}}
+ ```
+
+ Currently pressed key sequence that does not match any key binding
+ and/or is incomplete.
+
+ ```
+ {{.PendingKeys}}
+ ```
+
# TEMPLATE FUNCTIONS
Besides the standard functions described in go's text/template documentation,
diff --git a/lib/state/renderer.go b/lib/state/renderer.go
deleted file mode 100644
index 13e593fe..00000000
--- a/lib/state/renderer.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package state
-
-import (
- "errors"
- "fmt"
- "os"
- "strings"
- "unicode"
-
- "git.sr.ht/~rjarry/aerc/config"
- "github.com/mattn/go-runewidth"
-)
-
-type renderParams struct {
- width int
- sep string
- acct *accountState
- fldr *folderState
-}
-
-type renderFunc func(r renderParams) string
-
-func newRenderer() renderFunc {
- var texter Texter
- switch strings.ToLower(config.Statusline.DisplayMode) {
- case "icon":
- texter = &icon{}
- default:
- texter = &text{}
- }
-
- return renderer(texter)
-}
-
-func renderer(texter Texter) renderFunc {
- var leftFmt, rightFmt string
- if idx := strings.Index(config.Statusline.RenderFormat, "%>"); idx < 0 {
- leftFmt = config.Statusline.RenderFormat
- } else {
- leftFmt = config.Statusline.RenderFormat[:idx]
- rightFmt = strings.Replace(config.Statusline.RenderFormat[idx:], "%>", "", 1)
- }
-
- return func(r renderParams) string {
- lfmtStr, largs, err := parseStatuslineFormat(leftFmt, texter, r)
- if err != nil {
- return err.Error()
- }
- rfmtStr, rargs, err := parseStatuslineFormat(rightFmt, texter, r)
- if err != nil {
- return err.Error()
- }
- leftText, rightText := fmt.Sprintf(lfmtStr, largs...), fmt.Sprintf(rfmtStr, rargs...)
- return runewidth.FillRight(leftText, r.width-len(rightText)-1) + rightText
- }
-}
-
-func connectionInfo(acct *accountState, texter Texter) (conn string) {
- if acct.ConnActivity != "" {
- conn += acct.ConnActivity
- } else {
- if acct.Connected {
- conn += texter.Connected()
- } else {
- conn += texter.Disconnected()
- }
- }
- return
-}
-
-func contentInfo(acct *accountState, fldr *folderState, texter Texter) []string {
- var status []string
- if fldr.FilterActivity != "" {
- status = append(status, fldr.FilterActivity)
- } else if fldr.Filter != "" {
- status = append(status, texter.FormatFilter(fldr.Filter))
- }
- if fldr.Search != "" {
- status = append(status, texter.FormatSearch(fldr.Search))
- }
- return status
-}
-
-func trayInfo(acct *accountState, fldr *folderState, texter Texter) []string {
- var tray []string
- if fldr.Sorting {
- tray = append(tray, texter.Sorting())
- }
- if fldr.Threading {
- tray = append(tray, texter.Threading())
- }
- if acct.Passthrough {
- tray = append(tray, texter.Passthrough())
- }
- return tray
-}
-
-func parseStatuslineFormat(format string, texter Texter, r renderParams) (string, []interface{}, error) {
- retval := make([]byte, 0, len(format))
- var args []interface{}
- mute := false
-
- var c rune
- for i, ni := 0, 0; i < len(format); {
- ni = strings.IndexByte(format[i:], '%')
- if ni < 0 {
- ni = len(format)
- retval = append(retval, []byte(format[i:ni])...)
- break
- }
- ni += i + 1
- // Check for fmt flags
- if ni == len(format) {
- goto handle_end_error
- }
- c = rune(format[ni])
- if c == '+' || c == '-' || c == '#' || c == ' ' || c == '0' {
- ni++
- }
-
- // Check for precision and width
- if ni == len(format) {
- goto handle_end_error
- }
- c = rune(format[ni])
- for unicode.IsDigit(c) {
- ni++
- c = rune(format[ni])
- }
- if c == '.' {
- ni++
- c = rune(format[ni])
- for unicode.IsDigit(c) {
- ni++
- c = rune(format[ni])
- }
- }
-
- retval = append(retval, []byte(format[i:ni])...)
- // Get final format verb
- if ni == len(format) {
- goto handle_end_error
- }
- c = rune(format[ni])
- switch c {
- case '%':
- retval = append(retval, '%')
- case 'a':
- retval = append(retval, 's')
- args = append(args, r.acct.Name)
- case 'c':
- retval = append(retval, 's')
- args = append(args, connectionInfo(r.acct, texter))
- case 'd':
- retval = append(retval, 's')
- args = append(args, r.fldr.Name)
- case 'm':
- mute = true
- case 'S':
- var status []string
- if conn := connectionInfo(r.acct, texter); conn != "" {
- status = append(status, conn)
- }
-
- if r.acct.Connected {
- status = append(status, contentInfo(r.acct, r.fldr, texter)...)
- }
- retval = append(retval, 's')
- args = append(args, strings.Join(status, r.sep))
- case 'T':
- var tray []string
- if r.acct.Connected {
- tray = trayInfo(r.acct, r.fldr, texter)
- }
- retval = append(retval, 's')
- args = append(args, strings.Join(tray, r.sep))
- case 'p':
- path, err := os.Getwd()
- if err == nil {
- home, _ := os.UserHomeDir()
- if strings.HasPrefix(path, home) {
- path = strings.Replace(path, home, "~", 1)
- }
- retval = append(retval, 's')
- args = append(args, path)
- }
- default:
- // Just ignore it and print as is
- // so %k in index format becomes %%k to Printf
- retval = append(retval, '%')
- retval = append(retval, byte(c))
- }
- i = ni + 1
- }
-
- if mute {
- return "", nil, nil
- }
-
- return string(retval), args, nil
-
-handle_end_error:
- return "", nil,
- errors.New("reached end of string while parsing statusline format")
-}
diff --git a/lib/state/state.go b/lib/state/state.go
index b5f925ac..49431029 100644
--- a/lib/state/state.go
+++ b/lib/state/state.go
@@ -2,27 +2,16 @@ package state
import (
"fmt"
-
- "git.sr.ht/~rjarry/aerc/config"
)
-type State struct {
- renderer renderFunc
- acct *accountState
- fldr map[string]*folderState
- width int
-}
-
-type accountState struct {
- Name string
- Multiple bool
- ConnActivity string
+type AccountState struct {
Connected bool
- Passthrough bool
+ connActivity string
+ passthrough bool
+ folders map[string]*folderState
}
type folderState struct {
- Name string
Search string
Filter string
FilterActivity string
@@ -30,60 +19,33 @@ type folderState struct {
Threading bool
}
-func NewState(name string, multipleAccts bool) *State {
- return &State{
- renderer: newRenderer(),
- acct: &accountState{Name: name, Multiple: multipleAccts},
- fldr: make(map[string]*folderState),
- }
-}
-
-func (s *State) StatusLine(folder string) string {
- return s.renderer(renderParams{
- width: s.width,
- sep: config.Statusline.Separator,
- acct: s.acct,
- fldr: s.folderState(folder),
- })
-}
-
-func (s *State) folderState(folder string) *folderState {
- if _, ok := s.fldr[folder]; !ok {
- s.fldr[folder] = &folderState{Name: folder}
+func (s *AccountState) folderState(folder string) *folderState {
+ if s.folders == nil {
+ s.folders = make(map[string]*folderState)
}
- return s.fldr[folder]
-}
-
-func (s *State) SetWidth(w int) bool {
- changeState := false
- if s.width != w {
- s.width = w
- changeState = true
+ if _, ok := s.folders[folder]; !ok {
+ s.folders[folder] = &folderState{}
}
- return changeState
-}
-
-func (s *State) Connected() bool {
- return s.acct.Connected
+ return s.folders[folder]
}
-type SetStateFunc func(s *State, folder string)
+type SetStateFunc func(s *AccountState, folder string)
func SetConnected(state bool) SetStateFunc {
- return func(s *State, folder string) {
- s.acct.ConnActivity = ""
- s.acct.Connected = state
+ return func(s *AccountState, folder string) {
+ s.connActivity = ""
+ s.Connected = state
}
}
func ConnectionActivity(desc string) SetStateFunc {
- return func(s *State, folder string) {
- s.acct.ConnActivity = desc
+ return func(s *AccountState, folder string) {
+ s.connActivity = desc
}
}
func SearchFilterClear() SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).Search = ""
s.folderState(folder).FilterActivity = ""
s.folderState(folder).Filter = ""
@@ -91,13 +53,13 @@ func SearchFilterClear() SetStateFunc {
}
func FilterActivity(str string) SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).FilterActivity = str
}
}
func FilterResult(str string) SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).FilterActivity = ""
s.folderState(folder).Filter = concatFilters(s.folderState(folder).Filter, str)
}
@@ -111,25 +73,25 @@ func concatFilters(existing, next string) string {
}
func Search(desc string) SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).Search = desc
}
}
func Sorting(on bool) SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).Sorting = on
}
}
func Threading(on bool) SetStateFunc {
- return func(s *State, folder string) {
+ return func(s *AccountState, folder string) {
s.folderState(folder).Threading = on
}
}
func Passthrough(on bool) SetStateFunc {
- return func(s *State, folder string) {
- s.acct.Passthrough = on
+ return func(s *AccountState, folder string) {
+ s.passthrough = on
}
}
diff --git a/lib/state/templates.go b/lib/state/templates.go
index 48106295..f37c4865 100644
--- a/lib/state/templates.go
+++ b/lib/state/templates.go
@@ -30,6 +30,9 @@ type TemplateData struct {
folder string // selected folder name
folders []string
getRUEcount func(string) (int, int, int)
+
+ state *AccountState
+ pendingKeys []config.KeyStroke
}
// only used for compose/reply/forward
@@ -65,6 +68,14 @@ func (d *TemplateData) SetRUE(folders []string, cb func(string) (int, int, int))
d.getRUEcount = cb
}
+func (d *TemplateData) SetState(state *AccountState) {
+ d.state = state
+}
+
+func (d *TemplateData) SetPendingKeys(keys []config.KeyStroke) {
+ d.pendingKeys = keys
+}
+
func (d *TemplateData) Account() string {
if d.account != nil {
return d.account.Name
@@ -357,3 +368,70 @@ func (d *TemplateData) Exists(folders ...string) int {
_, _, e := d.rue(folders...)
return e
}
+
+func (d *TemplateData) Connected() bool {
+ if d.state != nil {
+ return d.state.Connected
+ }
+ return false
+}
+
+func (d *TemplateData) ConnectionInfo() string {
+ switch {
+ case d.state == nil:
+ return ""
+ case d.state.connActivity != "":
+ return d.state.connActivity
+ case d.state.Connected:
+ return texter().Connected()
+ default:
+ return texter().Disconnected()
+ }
+}
+
+func (d *TemplateData) ContentInfo() string {
+ if d.state == nil {
+ return ""
+ }
+ var content []string
+ fldr := d.state.folderState(d.folder)
+ if fldr.FilterActivity != "" {
+ content = append(content, fldr.FilterActivity)
+ } else if fldr.Filter != "" {
+ content = append(content, texter().FormatFilter(fldr.Filter))
+ }
+ if fldr.Search != "" {
+ content = append(content, texter().FormatSearch(fldr.Search))
+ }
+ return strings.Join(content, config.Statusline.Separator)
+}
+
+func (d *TemplateData) StatusInfo() string {
+ stat := d.ConnectionInfo()
+ if content := d.ContentInfo(); content != "" {
+ stat += config.Statusline.Separator + content
+ }
+ return stat
+}
+
+func (d *TemplateData) TrayInfo() string {
+ if d.state == nil {
+ return ""
+ }
+ var tray []string
+ fldr := d.state.folderState(d.folder)
+ if fldr.Sorting {
+ tray = append(tray, texter().Sorting())
+ }
+ if fldr.Threading {
+ tray = append(tray, texter().Threading())
+ }
+ if d.state.passthrough {
+ tray = append(tray, texter().Passthrough())
+ }
+ return strings.Join(tray, config.Statusline.Separator)
+}
+
+func (d *TemplateData) PendingKeys() string {
+ return config.FormatKeyStrokes(d.pendingKeys)
+}
diff --git a/lib/state/texter.go b/lib/state/texter.go
index 21cf3627..9212108d 100644
--- a/lib/state/texter.go
+++ b/lib/state/texter.go
@@ -1,8 +1,12 @@
package state
-import "strings"
+import (
+ "strings"
-type Texter interface {
+ "git.sr.ht/~rjarry/aerc/config"
+)
+
+type texterInterface interface {
Connected() string
Disconnected() string
Passthrough() string
@@ -14,6 +18,8 @@ type Texter interface {
type text struct{}
+var txt text
+
func (t text) Connected() string {
return "Connected"
}
@@ -44,6 +50,8 @@ func (t text) FormatSearch(s string) string {
type icon struct{}
+var icn icon
+
func (i icon) Connected() string {
return "✓"
}
@@ -71,3 +79,12 @@ func (i icon) FormatFilter(s string) string {
func (i icon) FormatSearch(s string) string {
return strings.ReplaceAll(s, "search", "🔎")
}
+
+func texter() texterInterface {
+ switch strings.ToLower(config.Statusline.DisplayMode) {
+ case "icon":
+ return &icn
+ default:
+ return &txt
+ }
+}
diff --git a/models/templates.go b/models/templates.go
index bdc93b9c..c07f3dbd 100644
--- a/models/templates.go
+++ b/models/templates.go
@@ -33,4 +33,10 @@ type TemplateData interface {
Recent(folders ...string) int
Unread(folders ...string) int
Exists(folders ...string) int
+ Connected() bool
+ ConnectionInfo() string
+ ContentInfo() string
+ StatusInfo() string
+ TrayInfo() string
+ PendingKeys() string
}
diff --git a/widgets/account.go b/widgets/account.go
index 135afef2..5b5d4dcd 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -35,7 +35,7 @@ type AccountView struct {
tab *ui.Tab
msglist *MessageList
worker *types.Worker
- state *state.State
+ state state.AccountState
newConn bool // True if this is a first run after a new connection/reconnection
uiConf *config.UIConfig
@@ -66,7 +66,6 @@ func NewAccountView(
acct: acct,
aerc: aerc,
host: host,
- state: state.NewState(acct.Name, len(config.Accounts) > 1),
uiConf: acctUiConf,
}
@@ -117,14 +116,14 @@ func NewAccountView(
func (acct *AccountView) SetStatus(setters ...state.SetStateFunc) {
for _, fn := range setters {
- fn(acct.state, acct.SelectedDirectory())
+ fn(&acct.state, acct.SelectedDirectory())
}
acct.UpdateStatus()
}
func (acct *AccountView) UpdateStatus() {
if acct.isSelected() {
- acct.host.SetStatus(acct.state.StatusLine(acct.SelectedDirectory()))
+ acct.host.UpdateStatus()
}
}
@@ -157,9 +156,6 @@ func (acct *AccountView) Invalidate() {
}
func (acct *AccountView) Draw(ctx *ui.Context) {
- if acct.state.SetWidth(ctx.Width()) {
- acct.UpdateStatus()
- }
acct.grid.Draw(ctx)
}
@@ -480,7 +476,7 @@ func (acct *AccountView) CheckMailTimer(d time.Duration) {
go func() {
defer log.PanicHandler()
for range acct.ticker.C {
- if !acct.state.Connected() {
+ if !acct.state.Connected {
continue
}
acct.CheckMail()
diff --git a/widgets/aerc.go b/widgets/aerc.go
index b8be1100..572a123c 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -60,7 +60,7 @@ func NewAerc(
tabs := ui.NewTabs(config.Ui)
statusbar := ui.NewStack(config.Ui)
- statusline := NewStatusLine(config.Ui)
+ statusline := &StatusLine{}
statusbar.Push(statusline)
grid := ui.NewGrid().Rows([]ui.GridSpec{
@@ -538,24 +538,16 @@ func (aerc *Aerc) SelectPreviousTab() bool {
return aerc.tabs.SelectPrevious()
}
-func (aerc *Aerc) SetStatus(status string) *StatusMessage {
- return aerc.statusline.Set(status)
-}
-
func (aerc *Aerc) UpdateStatus() {
if acct := aerc.SelectedAccount(); acct != nil {
- acct.UpdateStatus()
+ aerc.statusline.Update(acct)
} else {
- aerc.ClearStatus()
+ aerc.statusline.Clear()
}
}
-func (aerc *Aerc) ClearStatus() {
- aerc.statusline.Set("")
-}
-
-func (aerc *Aerc) SetError(status string) *StatusMessage {
- return aerc.statusline.SetError(status)
+func (aerc *Aerc) SetError(err string) {
+ aerc.statusline.SetError(err)
}
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
diff --git a/widgets/status.go b/widgets/status.go
index 67667016..00877ee2 100644
--- a/widgets/status.go
+++ b/widgets/status.go
@@ -1,20 +1,24 @@
package widgets
import (
+ "bytes"
"time"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
"git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/state"
+ "git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
)
type StatusLine struct {
- stack []*StatusMessage
- fallback StatusMessage
- aerc *Aerc
+ stack []*StatusMessage
+ aerc *Aerc
+ acct *AccountView
+ err string
}
type StatusMessage struct {
@@ -22,51 +26,72 @@ type StatusMessage struct {
message string
}
-func NewStatusLine(uiConfig *config.UIConfig) *StatusLine {
- return &StatusLine{
- fallback: StatusMessage{
- style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
- message: "Idle",
- },
- }
-}
-
func (status *StatusLine) Invalidate() {
ui.Invalidate()
}
func (status *StatusLine) Draw(ctx *ui.Context) {
- line := &status.fallback
- if len(status.stack) != 0 {
- line = status.stack[len(status.stack)-1]
- }
- ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style)
- pendingKeys := ""
- if status.aerc != nil {
- for _, pendingKey := range status.aerc.pendingKeys {
- pendingKeys += string(pendingKey.Rune)
+ style := status.uiConfig().GetStyle(config.STYLE_STATUSLINE_DEFAULT)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
+ switch {
+ case len(status.stack) != 0:
+ line := status.stack[len(status.stack)-1]
+ msg := runewidth.Truncate(line.message, ctx.Width(), "")
+ msg = runewidth.FillRight(msg, ctx.Width())
+ ctx.Printf(0, 0, line.style, "%s", msg)
+ case status.err != "":
+ msg := runewidth.Truncate(status.err, ctx.Width(), "")
+ msg = runewidth.FillRight(msg, ctx.Width())
+ style := status.uiConfig().GetStyle(config.STYLE_STATUSLINE_ERROR)
+ ctx.Printf(0, 0, style, "%s", msg)
+ case status.aerc != nil && status.acct != nil:
+ var data state.TemplateData
+ data.SetPendingKeys(status.aerc.pendingKeys)
+ data.SetState(&status.acct.state)
+ data.SetAccount(status.acct.acct)
+ data.SetFolder(status.acct.Directories().Selected())
+ msg, _ := status.acct.SelectedMessage()
+ data.SetInfo(msg, 0, false)
+ table := ui.NewTable(
+ ctx.Height(),
+ config.Statusline.StatusColumns,
+ config.Statusline.ColumnSeparator,
+ nil,
+ func(*ui.Table, int) tcell.Style { return style },
+ )
+ var buf bytes.Buffer
+ cells := make([]string, len(table.Columns))
+ for c, col := range table.Columns {
+ err := templates.Render(col.Def.Template, &buf, &data)
+ if err != nil {
+ log.Errorf("%s", err)
+ cells[c] = err.Error()
+ } else {
+ cells[c] = buf.String()
+ }
+ buf.Reset()
}
+ table.AddRow(cells, nil)
+ table.Draw(ctx)
}
- message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
- ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys)
}
-func (status *StatusLine) Set(text string) *StatusMessage {
- status.fallback = StatusMessage{
- style: status.uiConfig().GetStyle(config.STYLE_STATUSLINE_DEFAULT),
- message: text,
- }
+func (status *StatusLine) Update(acct *AccountView) {
+ status.acct = acct
status.Invalidate()
- return &status.fallback
}
-func (status *StatusLine) SetError(text string) *StatusMessage {
- status.fallback = StatusMessage{
- style: status.uiConfig().GetStyle(config.STYLE_STATUSLINE_ERROR),
- message: text,
+func (status *StatusLine) SetError(err string) {
+ prev := status.err
+ status.err = err
+ if prev != status.err {
+ status.Invalidate()
}
- status.Invalidate()
- return &status.fallback
+}
+
+func (status *StatusLine) Clear() {
+ status.SetError("")
+ status.acct = nil
}
func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
diff --git a/widgets/tabhost.go b/widgets/tabhost.go
index 28c9be02..c0a9dd53 100644
--- a/widgets/tabhost.go
+++ b/widgets/tabhost.go
@@ -6,8 +6,8 @@ import (
type TabHost interface {
BeginExCommand(cmd string)
- SetStatus(status string) *StatusMessage
- SetError(err string) *StatusMessage
+ UpdateStatus()
+ SetError(err string)
PushStatus(text string, expiry time.Duration) *StatusMessage
PushError(text string) *StatusMessage
PushSuccess(text string) *StatusMessage