diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | commands/account/import-mbox.go | 2 | ||||
-rw-r--r-- | config/aerc.conf | 26 | ||||
-rw-r--r-- | config/columns.go | 15 | ||||
-rw-r--r-- | config/statusline.go | 138 | ||||
-rw-r--r-- | config/templates.go | 6 | ||||
-rw-r--r-- | config/ui.go | 2 | ||||
-rw-r--r-- | doc/aerc-config.5.scd | 58 | ||||
-rw-r--r-- | doc/aerc-templates.7.scd | 39 | ||||
-rw-r--r-- | lib/state/renderer.go | 205 | ||||
-rw-r--r-- | lib/state/state.go | 86 | ||||
-rw-r--r-- | lib/state/templates.go | 78 | ||||
-rw-r--r-- | lib/state/texter.go | 21 | ||||
-rw-r--r-- | models/templates.go | 6 | ||||
-rw-r--r-- | widgets/account.go | 12 | ||||
-rw-r--r-- | widgets/aerc.go | 18 | ||||
-rw-r--r-- | widgets/status.go | 95 | ||||
-rw-r--r-- | widgets/tabhost.go | 4 |
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 |