aboutsummaryrefslogtreecommitdiffstats
path: root/lib/statusline
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 /lib/statusline
parenteb7e45d43be883c4be0e635875846f0d7ddca485 (diff)
downloadaerc-ce18e928813526e59462e391c09e868c62facb42.tar.gz
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>
Diffstat (limited to 'lib/statusline')
-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
4 files changed, 317 insertions, 87 deletions
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", "๐Ÿ”Ž")
+}