aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2022-12-27 22:59:10 +0100
committerRobin Jarry <robin@jarry.cc>2023-02-20 14:48:42 +0100
commitd2e74cdb91e140b50d14c3a8b315cde272f32587 (patch)
tree4f4a20c02f60b67d7a8eb967bb45d789a057453b
parent6af06c9dfec03e923589d34187ba8358e3423d5c (diff)
downloadaerc-d2e74cdb91e140b50d14c3a8b315cde272f32587.tar.gz
aerc-d2e74cdb91e140b50d14c3a8b315cde272f32587.zip
statusline: add column based render format
In the spirit of commit 535300cfdbfc ("config: add columns based index format"), reuse the column definitions and table widget. Add automatic translation of render-format to column definitions. Allow empty columns to be compatible with the %m (mute) flag. Rename the State object to AccountState to be more precise. Reuse that object in state.TempateData to expose account state info. Move actual status line rendering in StatusLine.Draw(). Add new template fields for status specific data: {{.ConnectionInfo}} Connection state. {{.ContentInfo}} General status information (e.g. filter, search) {{.StatusInfo}} Combination of {{.ConnectionInfo}} and {{.StatusInfo}} {{.TrayInfo}} General on/off information (e.g. passthrough, threading, sorting) {{.PendingKeys}} Currently pressed key sequence that does not match any key binding and/or is incomplete. Display a warning on startup if render-format has been converted to status-columns. Signed-off-by: Robin Jarry <robin@jarry.cc> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
-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