summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKoni Marti <koni.marti@gmail.com>2022-04-18 16:06:27 +0200
committerRobin Jarry <robin@jarry.cc>2022-04-25 11:21:07 +0200
commitce18e928813526e59462e391c09e868c62facb42 (patch)
tree9898097ca19e9aea7792f08ec8694a25432b83b1
parenteb7e45d43be883c4be0e635875846f0d7ddca485 (diff)
downloadaerc-ce18e928813526e59462e391c09e868c62facb42.tar.gz
aerc-ce18e928813526e59462e391c09e868c62facb42.zip
statusline: refactor to make it more customizable
Refactor statusline by clearly separating the rendering part from the text display. Use printf-like format string for statusline customization. Document printf-like format string to customize the statusline. Allow to completely mute the statusline (except for push notifications) with a format specifier. Provide a display mode with unicode icons for the status elements. Implements: https://todo.sr.ht/~rjarry/aerc/34 Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
-rw-r--r--config/aerc.conf17
-rw-r--r--config/config.go28
-rw-r--r--doc/aerc-config.5.scd46
-rw-r--r--lib/statusline/folderstate.go32
-rw-r--r--lib/statusline/renderer.go194
-rw-r--r--lib/statusline/state.go105
-rw-r--r--lib/statusline/texter.go73
-rw-r--r--widgets/account.go5
8 files changed, 407 insertions, 93 deletions
diff --git a/config/aerc.conf b/config/aerc.conf
index 1ad7ce5a..458f6359 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -139,6 +139,23 @@ styleset-name=default
# Default: false
#threading-enabled=false
+[statusline]
+# Describes the format string for the statusline.
+#
+# Default: [%a] %S %>%T
+render-format=[%a] %S %>%T
+
+# Specifies the separator between grouped statusline elements.
+#
+# Default: " | "
+# separator=
+
+# Defines the mode for displaying the status elements.
+# Options: text, icon
+#
+# Default: text
+# display-mode=
+
[viewer]
#
# Specifies the pager to use when displaying emails. Note that some filters
diff --git a/config/config.go b/config/config.go
index f8b2f650..8eeea100 100644
--- a/config/config.go
+++ b/config/config.go
@@ -147,6 +147,12 @@ type ViewerConfig struct {
KeyPassthrough bool `ini:"-"`
}
+type StatuslineConfig struct {
+ RenderFormat string `ini:"render-format"`
+ Separator string
+ DisplayMode string `ini:"display-mode"`
+}
+
type TriggersConfig struct {
NewEmail string `ini:"new-email"`
ExecuteCommand func(command []string) error
@@ -163,11 +169,12 @@ type AercConfig struct {
Bindings BindingConfig
ContextualBinds []BindingConfigContext
Compose ComposeConfig
- Ini *ini.File `ini:"-"`
- Accounts []AccountConfig `ini:"-"`
- Filters []FilterConfig `ini:"-"`
- Viewer ViewerConfig `ini:"-"`
- Triggers TriggersConfig `ini:"-"`
+ Ini *ini.File `ini:"-"`
+ Accounts []AccountConfig `ini:"-"`
+ Filters []FilterConfig `ini:"-"`
+ Viewer ViewerConfig `ini:"-"`
+ Statusline StatuslineConfig `ini:"-"`
+ Triggers TriggersConfig `ini:"-"`
Ui UIConfig
ContextualUis []UIConfigContext
General GeneralConfig
@@ -410,6 +417,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
}
}
}
+ if statusline, err := file.GetSection("statusline"); err == nil {
+ if err := statusline.MapTo(&config.Statusline); err != nil {
+ return err
+ }
+ }
if compose, err := file.GetSection("compose"); err == nil {
if err := compose.MapTo(&config.Compose); err != nil {
return err
@@ -654,6 +666,12 @@ func LoadConfigFromFile(root *string, logger *log.Logger) (*AercConfig, error) {
},
},
+ Statusline: StatuslineConfig{
+ RenderFormat: "[%a] %S %>%T",
+ Separator: " | ",
+ DisplayMode: "",
+ },
+
Compose: ComposeConfig{
HeaderLayout: [][]string{
{"To", "From"},
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 493fd715..b615629b 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -295,6 +295,52 @@ index-format=...
index-format=...
```
+## STATUSLINE
+
+These options are configured in the *[statusline]* section of aerc.conf.
+
+*render-format*
+ Describes the format string for the statusline format.
+
+ 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 notficiations), use
+ %m only.
+
+ Default: [%a] %S %>%T
+
+[- *Format specifier*
+:[ *Description*
+| %%
+: literal %
+| %a
+: active account name
+| %d
+: active directory name
+| %c
+: connection state
+| %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.
+
+*separator*
+ Specifies the separator between grouped statusline elements (e.g. for
+ the %S and %T specifiers in *render-format*).
+
+ Default: " | "
+
+*display-mode*
+ Defines the mode for displaying the status elements.
+ Options: text, icon
+
+ Default: text
+
## VIEWER
diff --git a/lib/statusline/folderstate.go b/lib/statusline/folderstate.go
deleted file mode 100644
index ff470b72..00000000
--- a/lib/statusline/folderstate.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package statusline
-
-type folderState struct {
- Search string
- Filter string
- FilterActivity string
- Sorting string
-
- Threading string
-}
-
-func (fs *folderState) State() []string {
- var line []string
-
- if fs.FilterActivity != "" {
- line = append(line, fs.FilterActivity)
- } else {
- if fs.Filter != "" {
- line = append(line, fs.Filter)
- }
- }
- if fs.Search != "" {
- line = append(line, fs.Search)
- }
- if fs.Sorting != "" {
- line = append(line, fs.Sorting)
- }
- if fs.Threading != "" {
- line = append(line, fs.Threading)
- }
- return line
-}
diff --git a/lib/statusline/renderer.go b/lib/statusline/renderer.go
new file mode 100644
index 00000000..2ab05dd9
--- /dev/null
+++ b/lib/statusline/renderer.go
@@ -0,0 +1,194 @@
+package statusline
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "unicode"
+
+ "github.com/mattn/go-runewidth"
+)
+
+type renderParams struct {
+ width int
+ sep string
+ acct *accountState
+ fldr *folderState
+}
+
+type renderFunc func(r renderParams) string
+
+func newRenderer(renderFormat, textMode string) renderFunc {
+ var texter Texter
+ switch strings.ToLower(textMode) {
+ case "icon":
+ texter = &icon{}
+ default:
+ texter = &text{}
+ }
+
+ return renderer(texter, renderFormat)
+}
+
+func renderer(texter Texter, renderFormat string) renderFunc {
+ var leftFmt, rightFmt string
+ if idx := strings.Index(renderFormat, "%>"); idx < 0 {
+ leftFmt = renderFormat
+ } else {
+ leftFmt, rightFmt = renderFormat[:idx], strings.Replace(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))
+ 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/statusline/state.go b/lib/statusline/state.go
index 895bb2c4..3fecd0fe 100644
--- a/lib/statusline/state.go
+++ b/lib/statusline/state.go
@@ -2,76 +2,80 @@ package statusline
import (
"fmt"
- "strings"
+
+ "git.sr.ht/~rjarry/aerc/config"
)
type State struct {
- Name string
- Multiple bool
- Separator string
+ separator string
+ renderer renderFunc
+ acct *accountState
+ fldr map[string]*folderState
+ width int
+}
- Connection string
+type accountState struct {
+ Name string
+ Multiple bool
ConnActivity string
Connected bool
+ Passthrough bool
+}
- Passthrough string
-
- fs map[string]*folderState
+type folderState struct {
+ Name string
+ Search string
+ Filter string
+ FilterActivity string
+ Sorting bool
+ Threading bool
}
-func NewState(name string, multipleAccts bool, sep string) *State {
- return &State{Name: name, Multiple: multipleAccts, Separator: sep,
- fs: make(map[string]*folderState)}
+func NewState(name string, multipleAccts bool, conf config.StatuslineConfig) *State {
+ return &State{separator: conf.Separator,
+ renderer: newRenderer(conf.RenderFormat, conf.DisplayMode),
+ acct: &accountState{Name: name, Multiple: multipleAccts},
+ fldr: make(map[string]*folderState),
+ }
}
func (s *State) StatusLine(folder string) string {
- var line []string
- if s.Connection != "" || s.ConnActivity != "" {
- conn := s.Connection
- if s.ConnActivity != "" {
- conn = s.ConnActivity
- }
- if s.Multiple {
- line = append(line, fmt.Sprintf("[%s] %s", s.Name, conn))
- } else {
- line = append(line, conn)
- }
- }
- if s.Connected {
- if s.Passthrough != "" {
- line = append(line, s.Passthrough)
- }
- if folder != "" {
- line = append(line, s.folderState(folder).State()...)
- }
- }
- return strings.Join(line, s.Separator)
+ return s.renderer(renderParams{
+ width: s.width,
+ sep: s.separator,
+ acct: s.acct,
+ fldr: s.folderState(folder),
+ })
}
func (s *State) folderState(folder string) *folderState {
- if _, ok := s.fs[folder]; !ok {
- s.fs[folder] = &folderState{}
+ if _, ok := s.fldr[folder]; !ok {
+ s.fldr[folder] = &folderState{Name: folder}
+ }
+ return s.fldr[folder]
+}
+
+func (s *State) SetWidth(w int) bool {
+ changeState := false
+ if s.width != w {
+ s.width = w
+ changeState = true
}
- return s.fs[folder]
+ return changeState
}
type SetStateFunc func(s *State, folder string)
func Connected(state bool) SetStateFunc {
return func(s *State, folder string) {
- s.ConnActivity = ""
- s.Connected = state
- if state {
- s.Connection = "Connected"
- } else {
- s.Connection = "Disconnected"
- }
+ s.acct.ConnActivity = ""
+ s.acct.Connected = state
}
}
func ConnectionActivity(desc string) SetStateFunc {
return func(s *State, folder string) {
- s.ConnActivity = desc
+ s.acct.ConnActivity = desc
}
}
@@ -111,27 +115,18 @@ func Search(desc string) SetStateFunc {
func Sorting(on bool) SetStateFunc {
return func(s *State, folder string) {
- s.folderState(folder).Sorting = ""
- if on {
- s.folderState(folder).Sorting = "sorting"
- }
+ s.folderState(folder).Sorting = on
}
}
func Threading(on bool) SetStateFunc {
return func(s *State, folder string) {
- s.folderState(folder).Threading = ""
- if on {
- s.folderState(folder).Threading = "threading"
- }
+ s.folderState(folder).Threading = on
}
}
func Passthrough(on bool) SetStateFunc {
return func(s *State, folder string) {
- s.Passthrough = ""
- if on {
- s.Passthrough = "passthrough"
- }
+ s.acct.Passthrough = on
}
}
diff --git a/lib/statusline/texter.go b/lib/statusline/texter.go
new file mode 100644
index 00000000..d06b1982
--- /dev/null
+++ b/lib/statusline/texter.go
@@ -0,0 +1,73 @@
+package statusline
+
+import "strings"
+
+type Texter interface {
+ Connected() string
+ Disconnected() string
+ Passthrough() string
+ Sorting() string
+ Threading() string
+ FormatFilter(string) string
+ FormatSearch(string) string
+}
+
+type text struct{}
+
+func (t text) Connected() string {
+ return "Connected"
+}
+
+func (t text) Disconnected() string {
+ return "Disconnected"
+}
+
+func (t text) Passthrough() string {
+ return "passthrough"
+}
+
+func (t text) Sorting() string {
+ return "sorting"
+}
+
+func (t text) Threading() string {
+ return "threading"
+}
+
+func (t text) FormatFilter(s string) string {
+ return s
+}
+
+func (t text) FormatSearch(s string) string {
+ return s
+}
+
+type icon struct{}
+
+func (i icon) Connected() string {
+ return "โœ“"
+}
+
+func (i icon) Disconnected() string {
+ return "โœ˜"
+}
+
+func (i icon) Passthrough() string {
+ return "โž”"
+}
+
+func (i icon) Sorting() string {
+ return "โš™"
+}
+
+func (i icon) Threading() string {
+ return "๐Ÿงต"
+}
+
+func (i icon) FormatFilter(s string) string {
+ return strings.ReplaceAll(s, "filter", "๐Ÿ”ฆ")
+}
+
+func (i icon) FormatSearch(s string) string {
+ return strings.ReplaceAll(s, "search", "๐Ÿ”Ž")
+}
diff --git a/widgets/account.go b/widgets/account.go
index 994bba6e..b34396bd 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -59,7 +59,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
conf: conf,
host: host,
logger: logger,
- state: statusline.NewState(acct.Name, len(conf.Accounts) > 1, " | "),
+ state: statusline.NewState(acct.Name, len(conf.Accounts) > 1, conf.Statusline),
}
view.grid = ui.NewGrid().Rows([]ui.GridSpec{
@@ -170,6 +170,9 @@ func (acct *AccountView) Invalidate() {
}
func (acct *AccountView) Draw(ctx *ui.Context) {
+ if acct.state.SetWidth(ctx.Width()) {
+ acct.UpdateStatus()
+ }
acct.grid.Draw(ctx)
}