From 1ff687ca2b0821c2cacc1fa725abb3302d2af9da Mon Sep 17 00:00:00 2001 From: Kalyan Sriram Date: Mon, 27 Jul 2020 01:03:55 -0700 Subject: [PATCH] Implement style configuration. Introduce the ability to configure stylesets, allowing customization of aerc's look (color scheme, font weight, etc). Default styleset is installed to /path/to/aerc/stylesets/default. --- Makefile | 7 +- commands/compose/attach.go | 5 +- commands/compose/detach.go | 5 +- commands/exec.go | 15 +- commands/msg/pipe.go | 14 +- config/aerc.conf.in | 11 + config/config.go | 67 ++++- config/default_styleset | 33 +++ config/style.go | 379 +++++++++++++++++++++++++++ doc/aerc-config.5.scd | 14 + doc/aerc-stylesets.7.scd | 189 +++++++++++++ lib/ui/borders.go | 13 +- lib/ui/stack.go | 10 +- lib/ui/tab.go | 11 +- lib/ui/text.go | 42 +-- lib/ui/textinput.go | 32 ++- widgets/account-wizard.go | 102 +++---- widgets/account.go | 5 +- widgets/aerc.go | 20 +- widgets/compose.go | 68 +++-- widgets/dirlist.go | 10 +- widgets/exline.go | 6 +- widgets/getpasswd.go | 19 +- widgets/msglist.go | 54 ++-- widgets/msgviewer.go | 63 +++-- widgets/pgpinfo.go | 34 +-- widgets/{selecter.go => selector.go} | 46 ++-- widgets/spinner.go | 6 +- widgets/status.go | 49 ++-- widgets/tabhost.go | 3 + 30 files changed, 1040 insertions(+), 292 deletions(-) create mode 100644 config/default_styleset create mode 100644 config/style.go create mode 100644 doc/aerc-stylesets.7.scd rename widgets/{selecter.go => selector.go} (53%) diff --git a/Makefile b/Makefile index 1e7fbd6..1c1de75 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,8 @@ DOCS := \ aerc-notmuch.5 \ aerc-smtp.5 \ aerc-tutorial.7 \ - aerc-templates.7 + aerc-templates.7 \ + aerc-stylesets.7 .1.scd.1: scdoc < $< > $@ @@ -58,7 +59,7 @@ clean: install: all mkdir -m755 -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(MANDIR)/man5 $(DESTDIR)$(MANDIR)/man7 \ - $(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates + $(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates $(DESTDIR)$(SHAREDIR)/stylesets install -m755 aerc $(DESTDIR)$(BINDIR)/aerc install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1 install -m644 aerc-search.1 $(DESTDIR)$(MANDIR)/man1/aerc-search.1 @@ -70,6 +71,7 @@ install: all install -m644 aerc-smtp.5 $(DESTDIR)$(MANDIR)/man5/aerc-smtp.5 install -m644 aerc-tutorial.7 $(DESTDIR)$(MANDIR)/man7/aerc-tutorial.7 install -m644 aerc-templates.7 $(DESTDIR)$(MANDIR)/man7/aerc-templates.7 + install -m644 aerc-stylesets.7 $(DESTDIR)$(MANDIR)/man7/aerc-stylesets.7 install -m644 config/accounts.conf $(DESTDIR)$(SHAREDIR)/accounts.conf install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf install -m644 config/binds.conf $(DESTDIR)$(SHAREDIR)/binds.conf @@ -78,6 +80,7 @@ install: all install -m755 filters/plaintext $(DESTDIR)$(SHAREDIR)/filters/plaintext install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply install -m644 templates/forward_as_body $(DESTDIR)$(SHAREDIR)/templates/forward_as_body + install -m644 config/default_styleset $(DESTDIR)$(SHAREDIR)/stylesets/default RMDIR_IF_EMPTY:=sh -c '\ if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \ diff --git a/commands/compose/attach.go b/commands/compose/attach.go index 2b633dc..148442b 100644 --- a/commands/compose/attach.go +++ b/commands/compose/attach.go @@ -4,11 +4,9 @@ import ( "fmt" "os" "strings" - "time" "git.sr.ht/~sircmpwn/aerc/commands" "git.sr.ht/~sircmpwn/aerc/widgets" - "github.com/gdamore/tcell" "github.com/mitchellh/go-homedir" ) @@ -52,8 +50,7 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error { composer, _ := aerc.SelectedTab().(*widgets.Composer) composer.AddAttachment(path) - aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second). - Color(tcell.ColorDefault, tcell.ColorGreen) + aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name())) return nil } diff --git a/commands/compose/detach.go b/commands/compose/detach.go index e8b07ed..b48159d 100644 --- a/commands/compose/detach.go +++ b/commands/compose/detach.go @@ -3,10 +3,8 @@ package compose import ( "fmt" "strings" - "time" "git.sr.ht/~sircmpwn/aerc/widgets" - "github.com/gdamore/tcell" ) type Detach struct{} @@ -44,8 +42,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error { return err } - aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second). - Color(tcell.ColorDefault, tcell.ColorGreen) + aerc.PushSuccess(fmt.Sprintf("Detached %s", path)) return nil } diff --git a/commands/exec.go b/commands/exec.go index e15afbe..7d24fdc 100644 --- a/commands/exec.go +++ b/commands/exec.go @@ -7,8 +7,6 @@ import ( "time" "git.sr.ht/~sircmpwn/aerc/widgets" - - "github.com/gdamore/tcell" ) type ExecCmd struct{} @@ -35,14 +33,15 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error { if err != nil { aerc.PushError(" " + err.Error()) } else { - color := tcell.ColorDefault if cmd.ProcessState.ExitCode() != 0 { - color = tcell.ColorRed + aerc.PushError(fmt.Sprintf( + "%s: completed with status %d", args[0], + cmd.ProcessState.ExitCode())) + } else { + aerc.PushStatus(fmt.Sprintf( + "%s: completed with status %d", args[0], + cmd.ProcessState.ExitCode()), 10*time.Second) } - aerc.PushStatus(fmt.Sprintf( - "%s: completed with status %d", args[0], - cmd.ProcessState.ExitCode()), 10*time.Second). - Color(tcell.ColorDefault, color) } }() return nil diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go index 20cb8b4..4e4ba67 100644 --- a/commands/msg/pipe.go +++ b/commands/msg/pipe.go @@ -12,7 +12,6 @@ import ( "git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/getopt" - "github.com/gdamore/tcell" ) type Pipe struct{} @@ -96,14 +95,15 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { if err != nil { aerc.PushError(" " + err.Error()) } else { - color := tcell.ColorDefault if ecmd.ProcessState.ExitCode() != 0 { - color = tcell.ColorRed + aerc.PushError(fmt.Sprintf( + "%s: completed with status %d", cmd[0], + ecmd.ProcessState.ExitCode())) + } else { + aerc.PushStatus(fmt.Sprintf( + "%s: completed with status %d", cmd[0], + ecmd.ProcessState.ExitCode()), 10*time.Second) } - aerc.PushStatus(fmt.Sprintf( - "%s: completed with status %d", cmd[0], - ecmd.ProcessState.ExitCode()), 10*time.Second). - Color(tcell.ColorDefault, color) } } diff --git a/config/aerc.conf.in b/config/aerc.conf.in index 3348efa..b9381a8 100644 --- a/config/aerc.conf.in +++ b/config/aerc.conf.in @@ -67,6 +67,17 @@ sort= # Default: true next-message-on-delete=true +# The directories where the stylesets are stored. It takes a colon-separated +# list of directories. +# +# default: @SHAREDIR@/stylesets/ +stylesets-dirs=@SHAREDIR@/stylesets/ + +# Sets the styleset to use for the aerc ui elements. +# +# Default: default +styleset-name=default + [viewer] # # Specifies the pager to use when displaying emails. Note that some filters diff --git a/config/config.go b/config/config.go index 00a52ce..9e78c86 100644 --- a/config/config.go +++ b/config/config.go @@ -45,6 +45,9 @@ type UIConfig struct { NextMessageOnDelete bool `ini:"next-message-on-delete"` CompletionDelay time.Duration `ini:"completion-delay"` CompletionPopovers bool `ini:"completion-popovers"` + StyleSetDirs []string `ini:"stylesets-dirs" delim:":"` + StyleSetName string `ini:"styleset-name"` + style StyleSet } type ContextType int @@ -411,6 +414,19 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { } } } + + if err := config.Ui.loadStyleSet( + config.Ui.StyleSetDirs); err != nil { + return err + } + + for idx, _ := range config.ContextualUis { + if err := config.ContextualUis[idx].UiConfig.loadStyleSet( + config.Ui.StyleSetDirs); err != nil { + return err + } + } + return nil } @@ -471,6 +487,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) { NextMessageOnDelete: true, CompletionDelay: 250 * time.Millisecond, CompletionPopovers: true, + StyleSetDirs: []string{path.Join(sharedir, "stylesets")}, + StyleSetName: "default", }, ContextualUis: []UIConfigContext{}, @@ -500,6 +518,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) { Forwards: "forward_as_body", }, } + // These bindings are not configurable config.Bindings.AccountWizard.ExKey = KeyStroke{ Key: tcell.KeyCtrlE, @@ -510,6 +529,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) { if err = config.LoadConfig(file); err != nil { return nil, err } + if ui, err := file.GetSection("general"); err == nil { if err := ui.MapTo(&config.General); err != nil { return nil, err @@ -617,8 +637,18 @@ func parseLayout(layout string) [][]string { return l } -func (config *AercConfig) mergeContextualUi(baseUi *UIConfig, - contextType ContextType, s string) { +func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error { + ui.style = NewStyleSet() + err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs) + if err != nil { + return fmt.Errorf("Unable to load default styleset: %s", err) + } + + return nil +} + +func (config AercConfig) mergeContextualUi(baseUi UIConfig, + contextType ContextType, s string) UIConfig { for _, contextualUi := range config.ContextualUis { if contextualUi.ContextType != contextType { continue @@ -628,17 +658,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig, continue } - mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig) - return - } -} - -func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig { - baseUi := config.Ui - - for k, v := range params { - config.mergeContextualUi(&baseUi, k, v) + mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride) + if contextualUi.UiConfig.StyleSetName != "" { + baseUi.style = contextualUi.UiConfig.style + } + return baseUi } return baseUi } + +func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig { + baseUi := config.Ui + + for k, v := range params { + baseUi = config.mergeContextualUi(baseUi, k, v) + } + + return baseUi +} + +func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style { + return uiConfig.style.Get(so) +} + +func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style { + return uiConfig.style.Selected(so) +} diff --git a/config/default_styleset b/config/default_styleset new file mode 100644 index 0000000..fa52f23 --- /dev/null +++ b/config/default_styleset @@ -0,0 +1,33 @@ +# +# aerc default styleset +# +# This styleset uses the terminal defaults as its base. +# More information on how to configure the styleset can be found in +# the *aerc-styleset.7* manpage. Please read the manual before +# modifying or creating a styleset. + +*.default=true +*.selected.reverse=toggle + +title.reverse=true +header.bold=true + +*error.bold=true +error.fg=red +warning.fg=yellow +success.fg=green + +statusline*.default=true +statusline_default.reverse=true +statusline_error.fg=red +statusline_error.reverse=true + +msglist_unread.bold=true + +completion_pill.reverse=true + +tab.reverse=true +border.reverse = true + +selector_focused.reverse=true +selector_chooser.bold=true diff --git a/config/style.go b/config/style.go new file mode 100644 index 0000000..f159be3 --- /dev/null +++ b/config/style.go @@ -0,0 +1,379 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/gdamore/tcell" + "github.com/go-ini/ini" + "github.com/mitchellh/go-homedir" +) + +type StyleObject int32 + +const ( + STYLE_DEFAULT StyleObject = iota + STYLE_ERROR + STYLE_WARNING + STYLE_SUCCESS + + STYLE_TITLE + STYLE_HEADER + + STYLE_STATUSLINE_DEFAULT + STYLE_STATUSLINE_ERROR + STYLE_STATUSLINE_SUCCESS + + STYLE_MSGLIST_DEFAULT + STYLE_MSGLIST_UNREAD + STYLE_MSGLIST_READ + STYLE_MSGLIST_DELETED + STYLE_MSGLIST_MARKED + STYLE_MSGLIST_FLAGGED + + STYLE_DIRLIST_DEFAULT + + STYLE_COMPLETION_DEFAULT + STYLE_COMPLETION_GUTTER + STYLE_COMPLETION_PILL + + STYLE_TAB + STYLE_STACK + STYLE_SPINNER + STYLE_BORDER + + STYLE_SELECTOR_DEFAULT + STYLE_SELECTOR_FOCUSED + STYLE_SELECTOR_CHOOSER +) + +var StyleNames = map[string]StyleObject{ + "default": STYLE_DEFAULT, + "error": STYLE_ERROR, + "warning": STYLE_WARNING, + "success": STYLE_SUCCESS, + + "title": STYLE_TITLE, + "header": STYLE_HEADER, + + "statusline_default": STYLE_STATUSLINE_DEFAULT, + "statusline_error": STYLE_STATUSLINE_ERROR, + "statusline_success": STYLE_STATUSLINE_SUCCESS, + + "msglist_default": STYLE_MSGLIST_DEFAULT, + "msglist_unread": STYLE_MSGLIST_UNREAD, + "msglist_read": STYLE_MSGLIST_READ, + "msglist_deleted": STYLE_MSGLIST_DELETED, + "msglist_marked": STYLE_MSGLIST_MARKED, + "msglist_flagged": STYLE_MSGLIST_FLAGGED, + + "dirlist_default": STYLE_DIRLIST_DEFAULT, + + "completion_default": STYLE_COMPLETION_DEFAULT, + "completion_gutter": STYLE_COMPLETION_GUTTER, + "completion_pill": STYLE_COMPLETION_PILL, + + "tab": STYLE_TAB, + "stack": STYLE_STACK, + "spinner": STYLE_SPINNER, + "border": STYLE_BORDER, + + "selector_default": STYLE_SELECTOR_DEFAULT, + "selector_focused": STYLE_SELECTOR_FOCUSED, + "selector_chooser": STYLE_SELECTOR_CHOOSER, +} + +type Style struct { + Fg tcell.Color + Bg tcell.Color + Bold bool + Blink bool + Underline bool + Reverse bool +} + +func (s Style) Get() tcell.Style { + return tcell.StyleDefault. + Foreground(s.Fg). + Background(s.Bg). + Bold(s.Bold). + Blink(s.Blink). + Underline(s.Blink). + Reverse(s.Reverse) +} + +func (s *Style) Normal() { + s.Bold = false + s.Blink = false + s.Underline = false + s.Reverse = false +} + +func (s *Style) Default() *Style { + s.Fg = tcell.ColorDefault + s.Bg = tcell.ColorDefault + return s +} + +func (s *Style) Reset() *Style { + s.Default() + s.Normal() + return s +} + +func boolSwitch(val string, cur_val bool) (bool, error) { + switch val { + case "true": + return true, nil + case "false": + return false, nil + case "toggle": + return !cur_val, nil + default: + return cur_val, errors.New( + "Bool Switch attribute must be true, false, or toggle") + } +} + +func (s *Style) Set(attr, val string) error { + switch attr { + case "fg": + s.Fg = tcell.GetColor(val) + case "bg": + s.Bg = tcell.GetColor(val) + case "bold": + if state, err := boolSwitch(val, s.Bold); err != nil { + return err + } else { + s.Bold = state + } + case "blink": + if state, err := boolSwitch(val, s.Blink); err != nil { + return err + } else { + s.Blink = state + } + case "underline": + if state, err := boolSwitch(val, s.Underline); err != nil { + return err + } else { + s.Underline = state + } + case "reverse": + if state, err := boolSwitch(val, s.Reverse); err != nil { + return err + } else { + s.Reverse = state + } + case "default": + s.Default() + case "normal": + s.Normal() + default: + return errors.New("Unknown style attribute: " + attr) + } + + return nil +} + +type StyleSet struct { + objects map[StyleObject]*Style + selected map[StyleObject]*Style +} + +func NewStyleSet() StyleSet { + ss := StyleSet{ + objects: make(map[StyleObject]*Style), + selected: make(map[StyleObject]*Style), + } + for _, so := range StyleNames { + ss.objects[so] = new(Style) + ss.selected[so] = new(Style) + } + + return ss +} + +func (ss StyleSet) reset() { + for _, so := range StyleNames { + ss.objects[so].Reset() + ss.selected[so].Reset() + } +} + +func (ss StyleSet) Get(so StyleObject) tcell.Style { + return ss.objects[so].Get() +} + +func (ss StyleSet) Selected(so StyleObject) tcell.Style { + return ss.selected[so].Get() +} + +func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) { + for _, dir := range stylesetsDir { + stylesetPath, err := homedir.Expand(path.Join(dir, stylesetName)) + if err != nil { + return "", err + } + + if _, err := os.Stat(stylesetPath); os.IsNotExist(err) { + continue + } + + return stylesetPath, nil + } + + return "", fmt.Errorf( + "Can't find styleset %q in any of %v", stylesetName, stylesetsDir) +} + +func (ss *StyleSet) ParseStyleSet(file *ini.File) error { + ss.reset() + + defaultSection, err := file.GetSection(ini.DefaultSection) + if err != nil { + return err + } + + selectedKeys := []string{} + + for _, key := range defaultSection.KeyStrings() { + tokens := strings.Split(key, ".") + var styleName, attr string + switch len(tokens) { + case 2: + styleName, attr = tokens[0], tokens[1] + case 3: + if tokens[1] != "selected" { + return errors.New("Unknown modifier: " + tokens[1]) + } + selectedKeys = append(selectedKeys, key) + continue + default: + return errors.New("Style parsing error: " + key) + } + val := defaultSection.KeysHash()[key] + + if strings.ContainsAny(styleName, "*?") { + regex := fnmatchToRegex(styleName) + for sn, so := range StyleNames { + matched, err := regexp.MatchString(regex, sn) + if err != nil { + return err + } + + if !matched { + continue + } + + if err := ss.objects[so].Set(attr, val); err != nil { + return err + } + if err := ss.selected[so].Set(attr, val); err != nil { + return err + } + } + } else { + so, ok := StyleNames[styleName] + if !ok { + return errors.New("Unknown style object: " + styleName) + } + if err := ss.objects[so].Set(attr, val); err != nil { + return err + } + if err := ss.selected[so].Set(attr, val); err != nil { + return err + } + } + } + + for _, key := range selectedKeys { + tokens := strings.Split(key, ".") + styleName, modifier, attr := tokens[0], tokens[1], tokens[2] + if modifier != "selected" { + return errors.New("Unknown modifier: " + modifier) + } + + val := defaultSection.KeysHash()[key] + + if strings.ContainsAny(styleName, "*?") { + regex := fnmatchToRegex(styleName) + for sn, so := range StyleNames { + matched, err := regexp.MatchString(regex, sn) + if err != nil { + return err + } + + if !matched { + continue + } + + if err := ss.selected[so].Set(attr, val); err != nil { + return err + } + } + } else { + so, ok := StyleNames[styleName] + if !ok { + return errors.New("Unknown style object: " + styleName) + } + if err := ss.selected[so].Set(attr, val); err != nil { + return err + } + } + } + + for _, key := range defaultSection.KeyStrings() { + tokens := strings.Split(key, ".") + styleName, attr := tokens[0], tokens[1] + val := defaultSection.KeysHash()[key] + + if styleName != "selected" { + continue + } + + for _, so := range StyleNames { + if err := ss.selected[so].Set(attr, val); err != nil { + return err + } + } + } + + return nil +} + +func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error { + filepath, err := findStyleSet(stylesetName, stylesetDirs) + if err != nil { + return err + } + + file, err := ini.Load(filepath) + if err != nil { + return err + } + + return ss.ParseStyleSet(file) +} + +func fnmatchToRegex(pattern string) string { + n := len(pattern) + var regex strings.Builder + + for i := 0; i < n; i++ { + switch pattern[i] { + case '*': + regex.WriteString(".*") + case '?': + regex.WriteByte('.') + default: + regex.WriteByte(pattern[i]) + } + } + + return regex.String() +} diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index af64ad6..fcd70ec 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -173,6 +173,20 @@ These options are configured in the *[ui]* section of aerc.conf. Default: 250ms +*stylesets-dirs* + The directories where the stylesets are stored. The config takes a + colon-seperated list of dirs. + + Default: "/usr/share/aerc/stylesets" + +*styleset-name* + The name of the styleset to be used to style the ui elements. The + stylesets are stored in the 'stylesets' directory in the config + directory. + + Default: default + + ## Contextual UI Configuration The UI configuration can be specialized for accounts, specific mail diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd new file mode 100644 index 0000000..829418e --- /dev/null +++ b/doc/aerc-stylesets.7.scd @@ -0,0 +1,189 @@ +aerc-stylesets(7) + +# Name + +aerc-stylesets - styleset file specification for *aerc*(1) + +# SYNOPSIS + +aerc uses a simple configuration syntax to configure the styleset for +its ui. + +# Styleset Configuration + +Aerc uses a simple configuration file to describe a styleset. The +styleset is described as key, value pairs. In each line, the key +represents the style object it signifies and the color/atrribute of +that is modified. + +For example, in the line below, the foreground color of the +style object "msglist_unread" is set to "cornflowerblue" +``` +msglist_unread.fg=cornflowerblue +``` + +The configuration also allows wildcard matching of the style_objects +to configure multiple style objects at a time. + +## Style +The following options are available to be modified for each of the +style objects. + +*fg* + The foreground color of the style object is set. + + Syntax: `.fg=` + +*bg* + The background color of the style object is set. + + Syntax: `.bg=` + +*bold* + The bold attribute of the style object is set/unset. + + Syntax: `.bold=` + +*blink* + The blink attribute of the style object is set/unset. + _The terminal needs to support blinking text_ + + Syntax: `.bold=` + +*underline* + The underline attribute of the style object is set/unset. + _The terminal needs to support underline text_ + + Syntax: `.underline=` + +*reverse* + Reverses the color of the style object. Exchanges the foreground + and background colors. + + Syntax: `.reverse=` + _If the value is false, it doesn't change anything_ + +*normal* + All the attributes of the style object are unset. + + Syntax: `.normal=` + _The value doesn't matter_ + +*default* + Set the style object to the default style of the context. Usually + based on the terminal. + + Syntax: `.default=` + _The value doesn't matter_ + +## Style Objects +The style objects represent the various ui elements or ui instances for +styling. + +[[ *Style Object* +:[ *Description* +| default +: The default style object used for normal ui elements while not + using specialized configuration. +| error +: The style used to show errors. +| warning +: The style used when showing warnings. +| success +: The style used for success messages. +| title +: The style object used to style titles in ui elements. +| header +: The style object used to style headers in ui elements. +| statusline_default +: The default style applied to the statusline. +| statusline_error +: The style used for error messages in statusline. +| statusline_success +: The style used for success messages in statusline. +| msglist_default +: The default style for messages in a message list. +| msglist_unread +: Unread messages in a message list. +| msglist_read +: Read messages in a message list. +| msglist_deleted +: The messages marked as deleted. +| msglist_marked +: The messages with the marked flag. +| msglist_flagged +: The messages with the flagged flag. +| dirlist_default +: The default style for directories in the directory list. +| completion_default +: The default style for the completion engine. +| completion_gutter +: The completion gutter. +| completion_pill +: The completion pill. +| tab +: The style for the tab bar. +| stack +: The style for ui stack element. +| spinner +: The style for the loading spinner. +| border +: The style used to draw borders. *Only the background color is used*. +| selecter_default +: The default style for the selecter ui element. +| selecter_focused +: The focused item in a selecter ui element. +| selecter_chooser +: The item chooser in a selecter ui element. + +## fnmatch style wildcard matching +The styleset configuration can be made simpler by using the fnmatch +style wildcard matching for the style object. + +The special characters used in the fnmatch wildcards are: +[[ *Pattern* +:[ *Meaning* +| \* +: Matches everything +| \? +: Matches any single character + +For example, the following wildcards can be made using this syntax. +[[ *Example* +:[ Description +| \*.fg=blue +: Set the foreground color of all style objects to blue. +| \*list.bg=hotpink +: Set the background color of all style objects that end in list + to hotpink. + +## Selected modifier +Selected modifier can be applied to any style object. The style provided for +the selected modifier are applied on top of the style object it corresponds to. + +If you would like to make sure message that are flagged as read in the msglist +appear in yellow foreground and black background. You can specify that with +this. + +\tmsglist_default.selected.fg=yellow +\tmsglist_default.selected.bg=black + +If we specify the global style selected modifer using fnmatch as below: + +\t\*.selected.reverse=toggle + +This toggles the reverse switch for selected version of all the style objects. + +## Colors +The color values are set using the values accepted by the tcell library. +The values can be one of the following. + + *default* + The color is set as per the system or terminal default. + + ** + Any w3c approved color name is used to set colors for the style. + + ** + Hexcode for a color can be used. The format must be "\#XXXXXX" + diff --git a/lib/ui/borders.go b/lib/ui/borders.go index 7a75759..99d6880 100644 --- a/lib/ui/borders.go +++ b/lib/ui/borders.go @@ -2,6 +2,8 @@ package ui import ( "github.com/gdamore/tcell" + + "git.sr.ht/~sircmpwn/aerc/config" ) const ( @@ -16,12 +18,15 @@ type Bordered struct { borders uint content Drawable onInvalidate func(d Drawable) + uiConfig config.UIConfig } -func NewBordered(content Drawable, borders uint) *Bordered { +func NewBordered( + content Drawable, borders uint, uiConfig config.UIConfig) *Bordered { b := &Bordered{ - borders: borders, - content: content, + borders: borders, + content: content, + uiConfig: uiConfig, } content.OnInvalidate(b.contentInvalidated) return b @@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) { y := 0 width := ctx.Width() height := ctx.Height() - style := tcell.StyleDefault.Reverse(true) + style := bordered.uiConfig.GetStyle(config.STYLE_BORDER) if bordered.borders&BORDER_LEFT != 0 { ctx.Fill(0, 0, 1, ctx.Height(), ' ', style) x += 1 diff --git a/lib/ui/stack.go b/lib/ui/stack.go index 690a869..c9004a0 100644 --- a/lib/ui/stack.go +++ b/lib/ui/stack.go @@ -3,16 +3,19 @@ package ui import ( "fmt" + "git.sr.ht/~sircmpwn/aerc/config" + "github.com/gdamore/tcell" ) type Stack struct { children []Drawable onInvalidate []func(d Drawable) + uiConfig config.UIConfig } -func NewStack() *Stack { - return &Stack{} +func NewStack(uiConfig config.UIConfig) *Stack { + return &Stack{uiConfig: uiConfig} } func (stack *Stack) Children() []Drawable { @@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) { if len(stack.children) > 0 { stack.Peek().Draw(ctx) } else { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', + stack.uiConfig.GetStyle(config.STYLE_STACK)) } } diff --git a/lib/ui/tab.go b/lib/ui/tab.go index 4b99e4b..cd5f448 100644 --- a/lib/ui/tab.go +++ b/lib/ui/tab.go @@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) { func (strip *TabStrip) Draw(ctx *Context) { x := 0 for i, tab := range strip.Tabs { - style := tcell.StyleDefault.Reverse(true) + style := strip.uiConfig.GetStyle(config.STYLE_TAB) if strip.Selected == i { - style = tcell.StyleDefault + style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB) } tabWidth := 32 if ctx.Width()-x < tabWidth { @@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) { break } } - style := tcell.StyleDefault.Reverse(true) - ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style) + ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', + strip.uiConfig.GetStyle(config.STYLE_TAB)) } func (strip *TabStrip) Invalidate() { @@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) { if content.Selected >= len(content.Tabs) { width := ctx.Width() height := ctx.Height() - ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault) + ctx.Fill(0, 0, width, height, ' ', + content.uiConfig.GetStyle(config.STYLE_TAB)) } tab := content.Tabs[content.Selected] diff --git a/lib/ui/text.go b/lib/ui/text.go index 2b82598..455c2eb 100644 --- a/lib/ui/text.go +++ b/lib/ui/text.go @@ -15,17 +15,13 @@ type Text struct { Invalidatable text string strategy uint - fg tcell.Color - bg tcell.Color - bold bool - reverse bool + style tcell.Style } -func NewText(text string) *Text { +func NewText(text string, style tcell.Style) *Text { return &Text{ - bg: tcell.ColorDefault, - fg: tcell.ColorDefault, - text: text, + text: text, + style: style, } } @@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text { return t } -func (t *Text) Bold(bold bool) *Text { - t.bold = bold - t.Invalidate() - return t -} - -func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text { - t.fg = fg - t.bg = bg - t.Invalidate() - return t -} - -func (t *Text) Reverse(reverse bool) *Text { - t.reverse = reverse - t.Invalidate() - return t -} - func (t *Text) Draw(ctx *Context) { size := runewidth.StringWidth(t.text) x := 0 @@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) { if t.strategy == TEXT_RIGHT { x = ctx.Width() - size } - style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg) - if t.bold { - style = style.Bold(true) - } - if t.reverse { - style = style.Reverse(true) - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) - ctx.Printf(x, 0, style, "%s", t.text) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style) + ctx.Printf(x, 0, t.style, "%s", t.text) } func (t *Text) Invalidate() { diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index f6b0c72..2445065 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -6,6 +6,8 @@ import ( "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" + + "git.sr.ht/~sircmpwn/aerc/config" ) // TODO: Attach history providers @@ -27,16 +29,18 @@ type TextInput struct { completeIndex int completeDelay time.Duration completeDebouncer *time.Timer + uiConfig config.UIConfig } // Creates a new TextInput. TextInputs will render a "textbox" in the entire // context they're given, and process keypresses to build a string from user // input. -func NewTextInput(text string) *TextInput { +func NewTextInput(text string, ui config.UIConfig) *TextInput { return &TextInput{ - cells: -1, - text: []rune(text), - index: len([]rune(text)), + cells: -1, + text: []rune(text), + index: len([]rune(text)), + uiConfig: ui, } } @@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) { ti.ensureScroll() } ti.ctx = ctx // gross - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + + defaultStyle := ti.uiConfig.GetStyle(config.STYLE_DEFAULT) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle) text := ti.text[scroll:] sindex := ti.index - scroll if ti.password { - x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt) + x := ctx.Printf(0, 0, defaultStyle, "%s", ti.prompt) cells := runewidth.StringWidth(string(text)) - ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault) + ctx.Fill(x, 0, cells, 1, '*', defaultStyle) } else { - ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(text)) + ctx.Printf(0, 0, defaultStyle, "%s%s", ti.prompt, string(text)) } cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt) if ti.focus { @@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) { ti.Set(stem + ti.StringRight()) ti.Invalidate() }, + uiConfig: ti.uiConfig, } width := maxLen(ti.completions) + 3 height := len(ti.completions) @@ -353,6 +360,7 @@ type completions struct { onSelect func(int) onExec func() onStem func(string) + uiConfig config.UIConfig } func maxLen(ss []string) int { @@ -367,10 +375,10 @@ func maxLen(ss []string) int { } func (c *completions) Draw(ctx *Context) { - bg := tcell.StyleDefault - sel := tcell.StyleDefault.Reverse(true) - gutter := tcell.StyleDefault - pill := tcell.StyleDefault.Reverse(true) + bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT) + gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER) + pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL) + sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg) diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go index 4e51926..ae45bb8 100644 --- a/widgets/account-wizard.go +++ b/widgets/account-wizard.go @@ -75,21 +75,21 @@ type AccountWizard struct { func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { wizard := &AccountWizard{ - accountName: ui.NewTextInput("").Prompt("> "), + accountName: ui.NewTextInput("", conf.Ui).Prompt("> "), aerc: aerc, conf: conf, temporary: false, copySent: true, - email: ui.NewTextInput("").Prompt("> "), - fullName: ui.NewTextInput("").Prompt("> "), - imapPassword: ui.NewTextInput("").Prompt("] ").Password(true), - imapServer: ui.NewTextInput("").Prompt("> "), - imapStr: ui.NewText("imaps://"), - imapUsername: ui.NewTextInput("").Prompt("> "), - smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true), - smtpServer: ui.NewTextInput("").Prompt("> "), - smtpStr: ui.NewText("smtps://"), - smtpUsername: ui.NewTextInput("").Prompt("> "), + email: ui.NewTextInput("", conf.Ui).Prompt("> "), + fullName: ui.NewTextInput("", conf.Ui).Prompt("> "), + imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true), + imapServer: ui.NewTextInput("", conf.Ui).Prompt("> "), + imapStr: ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)), + imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "), + smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true), + smtpServer: ui.NewTextInput("", conf.Ui).Prompt("> "), + smtpStr: ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)), + smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "), } // Autofill some stuff for the user @@ -150,33 +150,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { {ui.SIZE_WEIGHT, ui.Const(1)}, }) basics.AddChild( - ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" + - "This wizard supports basic IMAP & SMTP configuration.\n" + - "For other configurations, use to exit and read the " + - "aerc-config(5) man page.\n" + - "Press and to cycle between each field in this form, or and .")) + ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+ + "This wizard supports basic IMAP & SMTP configuration.\n"+ + "For other configurations, use to exit and read the "+ + "aerc-config(5) man page.\n"+ + "Press and to cycle between each field in this form, "+ + "or and .", + conf.Ui.GetStyle(config.STYLE_DEFAULT))) basics.AddChild( - ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')"). - Bold(true)). + ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(1, 0) basics.AddChild(wizard.accountName). At(2, 0) basics.AddChild(ui.NewFill(' ')). At(3, 0) basics.AddChild( - ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')"). - Bold(true)). + ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(4, 0) basics.AddChild(wizard.fullName). At(5, 0) basics.AddChild(ui.NewFill(' ')). At(6, 0) basics.AddChild( - ui.NewText("Your email address? (e.g. 'john@example.org')").Bold(true)). + ui.NewText("Your email address? (e.g. 'john@example.org')", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(7, 0) basics.AddChild(wizard.email). At(8, 0) - selecter := NewSelecter([]string{"Next"}, 0). + selecter := NewSelecter([]string{"Next"}, 0, conf.Ui). OnChoose(func(option string) { email := wizard.email.String() if strings.ContainsRune(email, '@') { @@ -227,16 +230,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, ui.Const(1)}, }) - incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)")) + incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)", + conf.Ui.GetStyle(config.STYLE_DEFAULT))) incoming.AddChild( - ui.NewText("Username").Bold(true)). + ui.NewText("Username", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(1, 0) incoming.AddChild(wizard.imapUsername). At(2, 0) incoming.AddChild(ui.NewFill(' ')). At(3, 0) incoming.AddChild( - ui.NewText("Password").Bold(true)). + ui.NewText("Password", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(4, 0) incoming.AddChild(wizard.imapPassword). At(5, 0) @@ -244,20 +250,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { At(6, 0) incoming.AddChild( ui.NewText("Server address "+ - "(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)). + "(e.g. 'mail.example.org' or 'mail.example.org:1313')", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(7, 0) incoming.AddChild(wizard.imapServer). At(8, 0) incoming.AddChild(ui.NewFill(' ')). At(9, 0) incoming.AddChild( - ui.NewText("Connection mode").Bold(true)). + ui.NewText("Connection mode", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(10, 0) imapMode := NewSelecter([]string{ "IMAP over SSL/TLS", "IMAP with STARTTLS", "Insecure IMAP", - }, 0).Chooser(true).OnSelect(func(option string) { + }, 0, conf.Ui).Chooser(true).OnSelect(func(option string) { switch option { case "IMAP over SSL/TLS": wizard.imapMode = IMAP_OVER_TLS @@ -269,7 +277,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { wizard.imapUri() }) incoming.AddChild(imapMode).At(11, 0) - selecter = NewSelecter([]string{"Previous", "Next"}, 1). + selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui). OnChoose(wizard.advance) incoming.AddChild(ui.NewFill(' ')).At(12, 0) incoming.AddChild(wizard.imapStr).At(13, 0) @@ -304,16 +312,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, ui.Const(1)}, }) - outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)")) + outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)", + conf.Ui.GetStyle(config.STYLE_DEFAULT))) outgoing.AddChild( - ui.NewText("Username").Bold(true)). + ui.NewText("Username", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(1, 0) outgoing.AddChild(wizard.smtpUsername). At(2, 0) outgoing.AddChild(ui.NewFill(' ')). At(3, 0) outgoing.AddChild( - ui.NewText("Password").Bold(true)). + ui.NewText("Password", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(4, 0) outgoing.AddChild(wizard.smtpPassword). At(5, 0) @@ -321,20 +332,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { At(6, 0) outgoing.AddChild( ui.NewText("Server address "+ - "(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)). + "(e.g. 'mail.example.org' or 'mail.example.org:1313')", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(7, 0) outgoing.AddChild(wizard.smtpServer). At(8, 0) outgoing.AddChild(ui.NewFill(' ')). At(9, 0) outgoing.AddChild( - ui.NewText("Connection mode").Bold(true)). + ui.NewText("Connection mode", + conf.Ui.GetStyle(config.STYLE_HEADER))). At(10, 0) smtpMode := NewSelecter([]string{ "SMTP over SSL/TLS", "SMTP with STARTTLS", "Insecure SMTP", - }, 0).Chooser(true).OnSelect(func(option string) { + }, 0, conf.Ui).Chooser(true).OnSelect(func(option string) { switch option { case "SMTP over SSL/TLS": wizard.smtpMode = SMTP_OVER_TLS @@ -346,15 +359,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { wizard.smtpUri() }) outgoing.AddChild(smtpMode).At(11, 0) - selecter = NewSelecter([]string{"Previous", "Next"}, 1). + selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui). OnChoose(wizard.advance) outgoing.AddChild(ui.NewFill(' ')).At(12, 0) outgoing.AddChild(wizard.smtpStr).At(13, 0) outgoing.AddChild(ui.NewFill(' ')).At(14, 0) outgoing.AddChild( - ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)). - At(15, 0) - copySent := NewSelecter([]string{"Yes", "No"}, 0). + ui.NewText("Copy sent messages to 'Sent' folder?", + conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0) + copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui). Chooser(true).OnChoose(func(option string) { switch option { case "Yes": @@ -380,15 +393,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { {ui.SIZE_WEIGHT, ui.Const(1)}, }) complete.AddChild(ui.NewText( - "\nConfiguration complete!\n\n" + - "You can go back and double check your settings, or choose 'Finish' to\n" + - "save your settings to accounts.conf.\n\n" + - "To add another account in the future, run ':new-account'.")) + "\nConfiguration complete!\n\n"+ + "You can go back and double check your settings, or choose 'Finish' to\n"+ + "save your settings to accounts.conf.\n\n"+ + "To add another account in the future, run ':new-account'.", + conf.Ui.GetStyle(config.STYLE_DEFAULT))) selecter = NewSelecter([]string{ "Previous", "Finish & open tutorial", "Finish", - }, 1).OnChoose(func(option string) { + }, 1, conf.Ui).OnChoose(func(option string) { switch option { case "Previous": wizard.advance("Previous") diff --git a/widgets/account.go b/widgets/account.go index 211f09d..53c65ba 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -64,15 +64,14 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon worker, err := worker.NewWorker(acct.Source, logger) if err != nil { - host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)). - Color(tcell.ColorDefault, tcell.ColorRed) + host.SetError(fmt.Sprintf("%s: %s", acct.Name, err)) return view } view.worker = worker view.dirlist = NewDirectoryList(conf, acct, logger, worker) if acctUiConf.SidebarWidth > 0 { - view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT)) + view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT, acctUiConf)) } view.msglist = NewMessageList(conf, logger, aerc) diff --git a/widgets/aerc.go b/widgets/aerc.go index 4913be3..692e00d 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, tabs := ui.NewTabs(&conf.Ui) - statusbar := ui.NewStack() - statusline := NewStatusLine() + statusbar := ui.NewStack(conf.Ui) + statusline := NewStatusLine(conf.Ui) statusbar.Push(statusline) grid := ui.NewGrid().Rows([]ui.GridSpec{ @@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, logger: logger, statusbar: statusbar, statusline: statusline, - prompts: ui.NewStack(), + prompts: ui.NewStack(conf.Ui), tabs: tabs, } @@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage { return aerc.statusline.Set(status) } +func (aerc *Aerc) SetError(status string) *StatusMessage { + return aerc.statusline.SetError(status) +} + func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage { return aerc.statusline.Push(text, expiry) } -func (aerc *Aerc) PushError(text string) { - aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed) +func (aerc *Aerc) PushError(text string) *StatusMessage { + return aerc.statusline.PushError(text) +} + +func (aerc *Aerc) PushSuccess(text string) *StatusMessage { + return aerc.statusline.PushSuccess(text) } func (aerc *Aerc) focus(item ui.Interactive) { @@ -555,7 +563,7 @@ func (aerc *Aerc) CloseDialog() { func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) { chText = make(chan string, 1) chErr = make(chan error, 1) - getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) { + getPasswd := NewGetPasswd(title, prompt, aerc.conf, func(pw string, err error) { defer func() { close(chErr) close(chText) diff --git a/widgets/compose.go b/widgets/compose.go index b68c406..03c9175 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig, templateData := templates.ParseTemplateData(defaults, original) cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) { - aerc.PushError(fmt.Sprintf("could not complete header: %v", err)) + aerc.PushError( + fmt.Sprintf("could not complete header: %v", err)) worker.Logger.Printf("could not complete header: %v", err) }, aerc.Logger()) - layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults) + layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults) email, err := ioutil.TempFile("", "aerc-compose-*.eml") if err != nil { @@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig, return c, nil } -func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer, +func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer, defaults map[string]string) ( newLayout HeaderLayout, editors map[string]*headerEditor, focusable []ui.MouseableDrawableInteractive, ) { - layout := conf.Compose.HeaderLayout + layout := aerc.conf.Compose.HeaderLayout editors = make(map[string]*headerEditor) focusable = make([]ui.MouseableDrawableInteractive, 0) for _, row := range layout { for _, h := range row { - e := newHeaderEditor(h, "") - if conf.Ui.CompletionPopovers { - e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay) + e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig()) + if aerc.conf.Ui.CompletionPopovers { + e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay) } editors[h] = e switch h { @@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer, for _, h := range []string{"Cc", "Bcc"} { if val, ok := defaults[h]; ok && val != "" { if _, ok := editors[h]; !ok { - e := newHeaderEditor(h, "") - if conf.Ui.CompletionPopovers { - e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay) + e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig()) + if aerc.conf.Ui.CompletionPopovers { + e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay) } editors[h] = e focusable = append(focusable, e) @@ -259,7 +260,8 @@ func (c *Composer) readSignatureFromFile() []byte { } signature, err := ioutil.ReadFile(sigFile) if err != nil { - c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile)) + c.aerc.PushError( + fmt.Sprintf(" Error loading signature from file: %v", sigFile)) return nil } return signature @@ -648,7 +650,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) { } return } - e := newHeaderEditor(header, value) + e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig()) if c.config.Ui.CompletionPopovers { e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay) } @@ -704,23 +706,27 @@ func (c *Composer) reloadEmail() error { } type headerEditor struct { - name string - focused bool - input *ui.TextInput + name string + focused bool + input *ui.TextInput + uiConfig config.UIConfig } -func newHeaderEditor(name string, value string) *headerEditor { +func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor { return &headerEditor{ - input: ui.NewTextInput(value), - name: name, + input: ui.NewTextInput(value, uiConfig), + name: name, + uiConfig: uiConfig, } } func (he *headerEditor) Draw(ctx *ui.Context) { name := he.name + " " size := runewidth.StringWidth(name) - ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name) + defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT) + headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER) + ctx.Fill(0, 0, size, ctx.Height(), ' ', defaultStyle) + ctx.Printf(0, 0, headerStyle, "%s", name) he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1)) } @@ -784,21 +790,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage { {ui.SIZE_WEIGHT, ui.Const(1)}, }) + uiConfig := composer.config.Ui + if err != nil { - grid.AddChild(ui.NewText(err.Error()). - Color(tcell.ColorRed, tcell.ColorDefault)) - grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0) + grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR))) + grid.AddChild(ui.NewText("Press [q] to close this tab.", + uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0) } else { // TODO: source this from actual keybindings? - grid.AddChild(ui.NewText( - "Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0) - grid.AddChild(ui.NewText("Attachments:"). - Reverse(true)).At(1, 0) + grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach", + uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0) + grid.AddChild(ui.NewText("Attachments:", + uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0) if len(composer.attachments) == 0 { - grid.AddChild(ui.NewText("(none)")).At(2, 0) + grid.AddChild(ui.NewText("(none)", + uiConfig.GetStyle(config.STYLE_DEFAULT))).At(2, 0) } else { for i, a := range composer.attachments { - grid.AddChild(ui.NewText(a)).At(i+2, 0) + grid.AddChild(ui.NewText(a, uiConfig.GetStyle(config.STYLE_DEFAULT))). + At(i+2, 0) } } } diff --git a/widgets/dirlist.go b/widgets/dirlist.go index 3711544..3ed79cc 100644 --- a/widgets/dirlist.go +++ b/widgets/dirlist.go @@ -196,7 +196,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string { } func (dirlist *DirectoryList) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', + dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)) if dirlist.spinner.IsRunning() { dirlist.spinner.Draw(ctx) @@ -204,7 +205,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { } if len(dirlist.dirs) == 0 { - style := tcell.StyleDefault + style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT) ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist) return } @@ -236,10 +237,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { style := tcell.StyleDefault if name == dirlist.selected { - style = style.Reverse(true) - } else if name == dirlist.selecting { - style = style.Reverse(true) - style = style.Foreground(tcell.ColorGray) + style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT) } ctx.Fill(0, row, textWidth, 1, ' ', style) diff --git a/widgets/exline.go b/widgets/exline.go index 6def938..692c8e2 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -15,13 +15,14 @@ type ExLine struct { tabcomplete func(cmd string) []string cmdHistory lib.History input *ui.TextInput + conf *config.AercConfig } func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(), tabcomplete func(cmd string) []string, cmdHistory lib.History) *ExLine { - input := ui.NewTextInput("").Prompt(":").Set(cmd) + input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd) if conf.Ui.CompletionPopovers { input.TabComplete(tabcomplete, conf.Ui.CompletionDelay) } @@ -31,6 +32,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin tabcomplete: tabcomplete, cmdHistory: cmdHistory, input: input, + conf: conf, } input.OnInvalidate(func(d ui.Drawable) { exline.Invalidate() @@ -41,7 +43,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string), tabcomplete func(cmd string) []string) *ExLine { - input := ui.NewTextInput("").Prompt(prompt) + input := ui.NewTextInput("", conf.Ui).Prompt(prompt) if conf.Ui.CompletionPopovers { input.TabComplete(tabcomplete, conf.Ui.CompletionDelay) } diff --git a/widgets/getpasswd.go b/widgets/getpasswd.go index 34f8b1f..3cdc5cf 100644 --- a/widgets/getpasswd.go +++ b/widgets/getpasswd.go @@ -5,6 +5,7 @@ import ( "github.com/gdamore/tcell" + "git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/lib/ui" ) @@ -14,14 +15,17 @@ type GetPasswd struct { title string prompt string input *ui.TextInput + conf *config.AercConfig } -func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPasswd { +func NewGetPasswd(title string, prompt string, conf *config.AercConfig, + cb func(string, error)) *GetPasswd { getpasswd := &GetPasswd{ callback: cb, title: title, prompt: prompt, - input: ui.NewTextInput("").Password(true).Prompt("Password: "), + conf: conf, + input: ui.NewTextInput("", conf.Ui).Password(true).Prompt("Password: "), } getpasswd.input.OnInvalidate(func(_ ui.Drawable) { getpasswd.Invalidate() @@ -31,10 +35,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw } func (gp *GetPasswd) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true)) - ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title) - ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt) + defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT) + titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE) + + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle) + ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle) + ctx.Printf(1, 0, titleStyle, "%s", gp.title) + ctx.Printf(1, 1, defaultStyle, gp.prompt) gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1)) } diff --git a/widgets/msglist.go b/widgets/msglist.go index 1ed6bb1..e38dd9e 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -50,7 +50,8 @@ func (ml *MessageList) Invalidate() { func (ml *MessageList) Draw(ctx *ui.Context) { ml.height = ctx.Height() - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', + ml.aerc.SelectedAccount().UiConfig().GetStyle(config.STYLE_MSGLIST_DEFAULT)) store := ml.Store() if store == nil { @@ -101,38 +102,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) { continue } - style := tcell.StyleDefault + uiConfig := ml.conf.GetUiConfig(map[config.ContextType]string{ + config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name, + config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(), + config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject, + }) + + so := config.STYLE_MSGLIST_DEFAULT - // current row - if row == ml.store.SelectedIndex()-ml.scroll { - style = style.Reverse(true) - } // deleted message if _, ok := store.Deleted[msg.Uid]; ok { - style = style.Foreground(tcell.ColorGray) + so = config.STYLE_MSGLIST_DELETED } // unread message seen := false + flagged := false for _, flag := range msg.Flags { - if flag == models.SeenFlag { + switch flag { + case models.SeenFlag: seen = true + case models.FlaggedFlag: + flagged = true } } if !seen { - style = style.Bold(true) + so = config.STYLE_MSGLIST_UNREAD } - ctx.Fill(0, row, textWidth, 1, ' ', style) - - confParams := map[config.ContextType]string{ - config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name, - config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(), + if flagged { + so = config.STYLE_MSGLIST_FLAGGED } - if msg.Envelope != nil { - confParams[config.UI_CONTEXT_SUBJECT] = msg.Envelope.Subject - } - uiConfig := ml.conf.GetUiConfig(confParams) + // marked message + if store.IsMarked(msg.Uid) { + so = config.STYLE_MSGLIST_MARKED + } + + style := uiConfig.GetStyle(so) + + // current row + if row == ml.store.SelectedIndex()-ml.scroll { + style = uiConfig.GetStyleSelected(so) + } + + ctx.Fill(0, row, ctx.Width(), 1, ' ', style) fmtStr, args, err := format.ParseMessageFormat( ml.aerc.SelectedAccount().acct.From, uiConfig.IndexFormat, @@ -342,7 +355,8 @@ func (ml *MessageList) ensureScroll() { } func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) { - msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage + uiConfig := ml.aerc.SelectedAccount().UiConfig() + msg := uiConfig.EmptyMessage ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0, - tcell.StyleDefault, "%s", msg) + uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg) } diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 107ff59..30c83f7 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -33,6 +33,7 @@ type MessageViewer struct { grid *ui.Grid switcher *PartSwitcher msg lib.MessageView + uiConfig config.UIConfig } type PartSwitcher struct { @@ -62,9 +63,11 @@ func NewMessageViewer(acct *AccountView, header, headerHeight := layout.grid( func(header string) ui.Drawable { return &HeaderView{ + conf: conf, Name: header, Value: fmtHeader(msg.MessageInfo(), header, acct.UiConfig().TimestampFormat), + uiConfig: acct.UiConfig(), } }, ) @@ -94,15 +97,16 @@ func NewMessageViewer(acct *AccountView, err := createSwitcher(acct, switcher, conf, msg) if err != nil { return &MessageViewer{ - err: err, - grid: grid, - msg: msg, + err: err, + grid: grid, + msg: msg, + uiConfig: acct.UiConfig(), } } grid.AddChild(header).At(0, 0) if msg.PGPDetails() != nil { - grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0) + grid.AddChild(NewPGPInfo(msg.PGPDetails(), acct.UiConfig())).At(1, 0) grid.AddChild(ui.NewFill(' ')).At(2, 0) grid.AddChild(switcher).At(3, 0) } else { @@ -116,6 +120,7 @@ func NewMessageViewer(acct *AccountView, grid: grid, msg: msg, switcher: switcher, + uiConfig: acct.UiConfig(), } switcher.mv = mv @@ -224,8 +229,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher, func (mv *MessageViewer) Draw(ctx *ui.Context) { if mv.err != nil { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error()) + style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Printf(0, 0, style, "%s", mv.err.Error()) return } mv.grid.Draw(ctx) @@ -347,7 +353,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) { ps.height = ctx.Height() y := ctx.Height() - height for i, part := range ps.parts { - style := tcell.StyleDefault.Reverse(ps.selected == i) + style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT) + if ps.selected == i { + style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT) + } ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style) name := fmt.Sprintf("%s/%s", strings.ToLower(part.part.MIMEType), @@ -436,6 +445,7 @@ func (mv *MessageViewer) Focus(focus bool) { type PartViewer struct { ui.Invalidatable + conf *config.AercConfig err error fetched bool filter *exec.Cmd @@ -450,6 +460,7 @@ type PartViewer struct { term *Terminal selecter *Selecter grid *ui.Grid + uiConfig config.UIConfig } func NewPartViewer(acct *AccountView, conf *config.AercConfig, @@ -519,7 +530,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, {ui.SIZE_WEIGHT, ui.Const(1)}, }) - selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0). + selecter := NewSelecter([]string{"Save message", "Pipe to command"}, + 0, acct.UiConfig()). OnChoose(func(option string) { switch option { case "Save message": @@ -532,6 +544,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, grid.AddChild(selecter).At(2, 0) pv := &PartViewer{ + conf: conf, filter: filter, index: index, msg: msg, @@ -543,6 +556,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, term: term, selecter: selecter, grid: grid, + uiConfig: acct.UiConfig(), } if term != nil { @@ -661,14 +675,16 @@ func (pv *PartViewer) Invalidate() { } func (pv *PartViewer) Draw(ctx *ui.Context) { + style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT) + styleError := pv.uiConfig.GetStyle(config.STYLE_ERROR) if pv.filter == nil { // TODO: Let them download it directly or something - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed), + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Printf(0, 0, styleError, "No filter configured for this mimetype ('%s/%s')", pv.part.MIMEType, pv.part.MIMESubType, ) - ctx.Printf(0, 2, tcell.StyleDefault, + ctx.Printf(0, 2, style, "You can still :save the message or :pipe it to an external command") pv.selecter.Focus(true) pv.grid.Draw(ctx) @@ -679,8 +695,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) { pv.fetched = true } if pv.err != nil { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error()) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Printf(0, 0, style, "%s", pv.err.Error()) return } pv.term.Draw(ctx) @@ -702,8 +718,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool { type HeaderView struct { ui.Invalidatable - Name string - Value string + conf *config.AercConfig + Name string + Value string + uiConfig config.UIConfig } func (hv *HeaderView) Draw(ctx *ui.Context) { @@ -711,18 +729,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) { size := runewidth.StringWidth(name) lim := ctx.Width() - size - 1 value := runewidth.Truncate(" "+hv.Value, lim, "…") - var ( - hstyle tcell.Style - vstyle tcell.Style - ) + + vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT) + hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER) + // TODO: Make this more robust and less dumb if hv.Name == "PGP" { - vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen) - hstyle = tcell.StyleDefault.Bold(true) - } else { - vstyle = tcell.StyleDefault - hstyle = tcell.StyleDefault.Bold(true) + vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS) } + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle) ctx.Printf(0, 0, hstyle, "%s", name) ctx.Printf(size, 0, vstyle, "%s", value) diff --git a/widgets/pgpinfo.go b/widgets/pgpinfo.go index 5da9141..94fb877 100644 --- a/widgets/pgpinfo.go +++ b/widgets/pgpinfo.go @@ -3,40 +3,40 @@ package widgets import ( "errors" + "git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/lib/ui" - "github.com/gdamore/tcell" "golang.org/x/crypto/openpgp" pgperrors "golang.org/x/crypto/openpgp/errors" ) type PGPInfo struct { ui.Invalidatable - details *openpgp.MessageDetails + details *openpgp.MessageDetails + uiConfig config.UIConfig } -func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo { - return &PGPInfo{details: details} +func NewPGPInfo(details *openpgp.MessageDetails, uiConfig config.UIConfig) *PGPInfo { + return &PGPInfo{details: details, uiConfig: uiConfig} } func (p *PGPInfo) DrawSignature(ctx *ui.Context) { - errorStyle := tcell.StyleDefault.Background(tcell.ColorRed). - Foreground(tcell.ColorWhite).Bold(true) - softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true) - validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true) + errorStyle := p.uiConfig.GetStyle(config.STYLE_ERROR) + warningStyle := p.uiConfig.GetStyle(config.STYLE_WARNING) + validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS) + defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT) // TODO: Nicer prompt for TOFU, fetch from keyserver, etc if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) || p.details.SignedBy == nil { - x := ctx.Printf(0, 0, softErrorStyle, "*") - x += ctx.Printf(x, 0, tcell.StyleDefault, + x := ctx.Printf(0, 0, warningStyle, "*") + x += ctx.Printf(x, 0, defaultStyle, " Signed with unknown key (%8X); authenticity unknown", p.details.SignedByKeyId) } else if p.details.SignatureError != nil { x := ctx.Printf(0, 0, errorStyle, "Invalid signature!") - x += ctx.Printf(x, 0, tcell.StyleDefault. - Foreground(tcell.ColorRed).Bold(true), + x += ctx.Printf(x, 0, errorStyle, " This message may have been tampered with! (%s)", p.details.SignatureError.Error()) } else { @@ -44,24 +44,26 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) { ident := entity.PrimaryIdentity() x := ctx.Printf(0, 0, validStyle, "✓ Authentic ") - x += ctx.Printf(x, 0, tcell.StyleDefault, + x += ctx.Printf(x, 0, defaultStyle, "Signature from %s (%8X)", ident.Name, p.details.SignedByKeyId) } } func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) { - validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true) + validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS) + defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT) entity := p.details.DecryptedWith.Entity ident := entity.PrimaryIdentity() x := ctx.Printf(0, y, validStyle, "✓ Encrypted ") - x += ctx.Printf(x, y, tcell.StyleDefault, + x += ctx.Printf(x, y, defaultStyle, "To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId) } func (p *PGPInfo) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle) if p.details.IsSigned && p.details.IsEncrypted { p.DrawSignature(ctx) p.DrawEncryption(ctx, 1) diff --git a/widgets/selecter.go b/widgets/selector.go similarity index 53% rename from widgets/selecter.go rename to widgets/selector.go index 7fae9cd..d19d38f 100644 --- a/widgets/selecter.go +++ b/widgets/selector.go @@ -3,46 +3,50 @@ package widgets import ( "github.com/gdamore/tcell" + "git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/lib/ui" ) -type Selecter struct { +type Selector struct { ui.Invalidatable - chooser bool - focused bool - focus int - options []string + chooser bool + focused bool + focus int + options []string + uiConfig config.UIConfig onChoose func(option string) onSelect func(option string) } -func NewSelecter(options []string, focus int) *Selecter { - return &Selecter{ - focus: focus, - options: options, +func NewSelector(options []string, focus int, uiConfig config.UIConfig) *Selector { + return &Selector{ + focus: focus, + options: options, + uiConfig: uiConfig, } } -func (sel *Selecter) Chooser(chooser bool) *Selecter { +func (sel *Selector) Chooser(chooser bool) *Selector { sel.chooser = chooser return sel } -func (sel *Selecter) Invalidate() { +func (sel *Selector) Invalidate() { sel.DoInvalidate(sel) } -func (sel *Selecter) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) +func (sel *Selector) Draw(ctx *ui.Context) { + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', + sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT)) x := 2 for i, option := range sel.options { - style := tcell.StyleDefault + style := sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT) if sel.focus == i { if sel.focused { - style = style.Reverse(true) + style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_FOCUSED) } else if sel.chooser { - style = style.Bold(true) + style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_CHOOSER) } } x += ctx.Printf(x, 1, style, "[%s]", option) @@ -50,26 +54,26 @@ func (sel *Selecter) Draw(ctx *ui.Context) { } } -func (sel *Selecter) OnChoose(fn func(option string)) *Selecter { +func (sel *Selector) OnChoose(fn func(option string)) *Selector { sel.onChoose = fn return sel } -func (sel *Selecter) OnSelect(fn func(option string)) *Selecter { +func (sel *Selector) OnSelect(fn func(option string)) *Selector { sel.onSelect = fn return sel } -func (sel *Selecter) Selected() string { +func (sel *Selector) Selected() string { return sel.options[sel.focus] } -func (sel *Selecter) Focus(focus bool) { +func (sel *Selector) Focus(focus bool) { sel.focused = focus sel.Invalidate() } -func (sel *Selecter) Event(event tcell.Event) bool { +func (sel *Selector) Event(event tcell.Event) bool { switch event := event.(type) { case *tcell.EventKey: switch event.Key() { diff --git a/widgets/spinner.go b/widgets/spinner.go index 51b8c1b..0c72422 100644 --- a/widgets/spinner.go +++ b/widgets/spinner.go @@ -16,6 +16,7 @@ type Spinner struct { frame int64 // access via atomic frames []string stop chan struct{} + style tcell.Style } func NewSpinner(uiConf *config.UIConfig) *Spinner { @@ -23,6 +24,7 @@ func NewSpinner(uiConf *config.UIConfig) *Spinner { stop: make(chan struct{}), frame: -1, frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter), + style: uiConf.GetStyle(config.STYLE_SPINNER), } return &spinner } @@ -70,9 +72,9 @@ func (s *Spinner) Draw(ctx *ui.Context) { cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames))) - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', s.style) col := ctx.Width()/2 - len(s.frames[0])/2 + 1 - ctx.Printf(col, 0, tcell.StyleDefault, "%s", s.frames[cur]) + ctx.Printf(col, 0, s.style, "%s", s.frames[cur]) } func (s *Spinner) Invalidate() { diff --git a/widgets/status.go b/widgets/status.go index 6bdeb4f..122ca5f 100644 --- a/widgets/status.go +++ b/widgets/status.go @@ -6,6 +6,7 @@ import ( "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" + "git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/lib/ui" ) @@ -14,21 +15,21 @@ type StatusLine struct { stack []*StatusMessage fallback StatusMessage aerc *Aerc + uiConfig config.UIConfig } type StatusMessage struct { - bg tcell.Color - fg tcell.Color + style tcell.Style message string } -func NewStatusLine() *StatusLine { +func NewStatusLine(uiConfig config.UIConfig) *StatusLine { return &StatusLine{ fallback: StatusMessage{ - bg: tcell.ColorDefault, - fg: tcell.ColorDefault, + style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT), message: "Idle", }, + uiConfig: uiConfig, } } @@ -41,9 +42,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) { if len(status.stack) != 0 { line = status.stack[len(status.stack)-1] } - style := tcell.StyleDefault. - Background(line.bg).Foreground(line.fg).Reverse(true) - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style) pendingKeys := "" if status.aerc != nil { for _, pendingKey := range status.aerc.pendingKeys { @@ -51,13 +50,21 @@ func (status *StatusLine) Draw(ctx *ui.Context) { } } message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5) - ctx.Printf(0, 0, style, "%s%s", message, pendingKeys) + ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys) } func (status *StatusLine) Set(text string) *StatusMessage { status.fallback = StatusMessage{ - bg: tcell.ColorDefault, - fg: tcell.ColorDefault, + style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT), + message: text, + } + 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, } status.Invalidate() @@ -66,8 +73,7 @@ func (status *StatusLine) Set(text string) *StatusMessage { func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage { msg := &StatusMessage{ - bg: tcell.ColorDefault, - fg: tcell.ColorDefault, + style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT), message: text, } status.stack = append(status.stack, msg) @@ -85,6 +91,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage return msg } +func (status *StatusLine) PushError(text string) *StatusMessage { + msg := status.Push(text, 10*time.Second) + msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR)) + return msg +} + +func (status *StatusLine) PushSuccess(text string) *StatusMessage { + msg := status.Push(text, 10*time.Second) + msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS)) + return msg +} + func (status *StatusLine) Expire() { status.stack = nil } @@ -93,7 +111,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) { status.aerc = aerc } -func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) { - msg.bg = bg - msg.fg = fg +func (msg *StatusMessage) Color(style tcell.Style) { + msg.style = style } diff --git a/widgets/tabhost.go b/widgets/tabhost.go index 0ac67e5..28c9be0 100644 --- a/widgets/tabhost.go +++ b/widgets/tabhost.go @@ -7,6 +7,9 @@ import ( type TabHost interface { BeginExCommand(cmd string) SetStatus(status string) *StatusMessage + SetError(err string) *StatusMessage PushStatus(text string, expiry time.Duration) *StatusMessage + PushError(text string) *StatusMessage + PushSuccess(text string) *StatusMessage Beep() }