aboutsummaryrefslogtreecommitdiffstats
path: root/lib/state
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-02-05 23:23:02 +0100
committerRobin Jarry <robin@jarry.cc>2023-02-20 14:48:42 +0100
commit6af06c9dfec03e923589d34187ba8358e3423d5c (patch)
tree3722f17464ca651ebd12d7d6d55a0e97ae72c8ec /lib/state
parent34db5942bd7b642107002b75de9d5d5c7fe90e4c (diff)
downloadaerc-6af06c9dfec03e923589d34187ba8358e3423d5c.tar.gz
statusline: move files to lib/state
These modules will not handle statusline rendering after next commit. Move them in lib/state to make next commit easier to review. Signed-off-by: Robin Jarry <robin@jarry.cc> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
Diffstat (limited to 'lib/state')
-rw-r--r--lib/state/renderer.go205
-rw-r--r--lib/state/state.go135
-rw-r--r--lib/state/texter.go73
3 files changed, 413 insertions, 0 deletions
diff --git a/lib/state/renderer.go b/lib/state/renderer.go
new file mode 100644
index 00000000..13e593fe
--- /dev/null
+++ b/lib/state/renderer.go
@@ -0,0 +1,205 @@
+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
new file mode 100644
index 00000000..b5f925ac
--- /dev/null
+++ b/lib/state/state.go
@@ -0,0 +1,135 @@
+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
+ Connected bool
+ Passthrough bool
+}
+
+type folderState struct {
+ Name string
+ Search string
+ Filter string
+ FilterActivity string
+ Sorting bool
+ 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}
+ }
+ return s.fldr[folder]
+}
+
+func (s *State) SetWidth(w int) bool {
+ changeState := false
+ if s.width != w {
+ s.width = w
+ changeState = true
+ }
+ return changeState
+}
+
+func (s *State) Connected() bool {
+ return s.acct.Connected
+}
+
+type SetStateFunc func(s *State, folder string)
+
+func SetConnected(state bool) SetStateFunc {
+ return func(s *State, folder string) {
+ s.acct.ConnActivity = ""
+ s.acct.Connected = state
+ }
+}
+
+func ConnectionActivity(desc string) SetStateFunc {
+ return func(s *State, folder string) {
+ s.acct.ConnActivity = desc
+ }
+}
+
+func SearchFilterClear() SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).Search = ""
+ s.folderState(folder).FilterActivity = ""
+ s.folderState(folder).Filter = ""
+ }
+}
+
+func FilterActivity(str string) SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).FilterActivity = str
+ }
+}
+
+func FilterResult(str string) SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).FilterActivity = ""
+ s.folderState(folder).Filter = concatFilters(s.folderState(folder).Filter, str)
+ }
+}
+
+func concatFilters(existing, next string) string {
+ if existing == "" {
+ return next
+ }
+ return fmt.Sprintf("%s && %s", existing, next)
+}
+
+func Search(desc string) SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).Search = desc
+ }
+}
+
+func Sorting(on bool) SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).Sorting = on
+ }
+}
+
+func Threading(on bool) SetStateFunc {
+ return func(s *State, folder string) {
+ s.folderState(folder).Threading = on
+ }
+}
+
+func Passthrough(on bool) SetStateFunc {
+ return func(s *State, folder string) {
+ s.acct.Passthrough = on
+ }
+}
diff --git a/lib/state/texter.go b/lib/state/texter.go
new file mode 100644
index 00000000..21cf3627
--- /dev/null
+++ b/lib/state/texter.go
@@ -0,0 +1,73 @@
+package state
+
+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", "๐Ÿ”Ž")
+}