From 0f78f06610c0e8887aba2ae50e99b86477a384b3 Mon Sep 17 00:00:00 2001 From: Reto Brunner Date: Wed, 27 May 2020 07:37:02 +0200 Subject: [PATCH] Add Style configuration The following functionalities are added to configure aerc ui styles. - Read stylesets from file with very basic fnmatch wildcard matching - Add default styleset - Support different stylesets as part of UiConfig allowing contextual styles. - Move widgets/ui elements to use the stylesets. - Add configuration manual for the styleset --- Makefile | 7 +- commands/account/mkdir.go | 2 +- commands/account/view.go | 3 +- commands/compose/attach.go | 10 +- commands/compose/detach.go | 4 +- commands/compose/postpone.go | 6 +- commands/compose/send.go | 6 +- commands/exec.go | 17 +- commands/msg/archive.go | 2 +- commands/msg/copy.go | 2 +- commands/msg/delete.go | 4 +- commands/msg/forward.go | 3 +- commands/msg/modify-labels.go | 2 +- commands/msg/move.go | 2 +- commands/msg/pipe.go | 18 +- commands/msg/read.go | 4 +- commands/msg/recall.go | 3 +- commands/msg/reply.go | 3 +- commands/msgview/next.go | 4 +- commands/msgview/open.go | 6 +- commands/msgview/save.go | 2 +- commands/term.go | 3 +- commands/util.go | 4 +- config/aerc.conf.in | 11 + config/config.go | 75 +++++-- config/default_styleset | 33 +++ config/style.go | 372 ++++++++++++++++++++++++++++++++++ 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 | 109 +++++----- widgets/account.go | 11 +- widgets/aerc.go | 29 ++- widgets/compose.go | 69 ++++--- widgets/dirlist.go | 12 +- widgets/exline.go | 6 +- widgets/getpasswd.go | 18 +- widgets/msglist.go | 71 ++++--- widgets/msgviewer.go | 63 +++--- widgets/pgpinfo.go | 34 ++-- widgets/selecter.go | 26 ++- widgets/spinner.go | 6 +- widgets/status.go | 49 +++-- widgets/tabhost.go | 3 + 48 files changed, 1093 insertions(+), 332 deletions(-) create mode 100644 config/default_styleset create mode 100644 config/style.go create mode 100644 doc/aerc-stylesets.7.scd diff --git a/Makefile b/Makefile index 59e066d..aa60e95 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ DOCS := \ aerc-notmuch.5 \ aerc-smtp.5 \ aerc-tutorial.7 \ - aerc-templates.7 + aerc-templates.7 \ + aerc-stylesets.7 .1.scd.1: scdoc < $< > $@ @@ -59,7 +60,7 @@ clean: install: all mkdir -m755 -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \ - $(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates + $(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates $(SHAREDIR)/stylesets install -m755 aerc $(BINDIR)/aerc install -m644 aerc.1 $(MANDIR)/man1/aerc.1 install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.1 @@ -71,6 +72,7 @@ install: all install -m644 aerc-smtp.5 $(MANDIR)/man5/aerc-smtp.5 install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7 install -m644 aerc-templates.7 $(MANDIR)/man7/aerc-templates.7 + install -m644 aerc-stylesets.7 $(MANDIR)/man7/aerc-stylesets.7 install -m644 config/accounts.conf $(SHAREDIR)/accounts.conf install -m644 aerc.conf $(SHAREDIR)/aerc.conf install -m644 config/binds.conf $(SHAREDIR)/binds.conf @@ -79,6 +81,7 @@ install: all install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body + install -m644 config/default_styleset $(SHAREDIR)/stylesets/default RMDIR_IF_EMPTY:=sh -c '\ if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \ diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go index 4352a42..f99fc01 100644 --- a/commands/account/mkdir.go +++ b/commands/account/mkdir.go @@ -40,7 +40,7 @@ func (MakeDir) Execute(aerc *widgets.Aerc, args []string) error { aerc.PushStatus("Directory created.", 10*time.Second) acct.Directories().Select(name) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) return nil diff --git a/commands/account/view.go b/commands/account/view.go index b421666..d4653be 100644 --- a/commands/account/view.go +++ b/commands/account/view.go @@ -2,6 +2,7 @@ package account import ( "errors" + "time" "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/widgets" @@ -41,7 +42,7 @@ func (ViewMessage) Execute(aerc *widgets.Aerc, args []string) error { lib.NewMessageStoreView(msg, store, aerc.DecryptKeys, func(view lib.MessageView, err error) { if err != nil { - aerc.PushError(err.Error()) + aerc.PushError(err.Error(), 10*time.Second) return } viewer := widgets.NewMessageViewer(acct, aerc.Config(), view) diff --git a/commands/compose/attach.go b/commands/compose/attach.go index 2b633dc..6b8d72f 100644 --- a/commands/compose/attach.go +++ b/commands/compose/attach.go @@ -8,7 +8,6 @@ import ( "git.sr.ht/~sircmpwn/aerc/commands" "git.sr.ht/~sircmpwn/aerc/widgets" - "github.com/gdamore/tcell" "github.com/mitchellh/go-homedir" ) @@ -36,24 +35,23 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error { path, err := homedir.Expand(path) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) return err } pathinfo, err := os.Stat(path) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) return err } else if pathinfo.IsDir() { - aerc.PushError("Attachment must be a file, not a directory") + aerc.PushError("Attachment must be a file, not a directory", 10*time.Second) return nil } 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()), 10*time.Second) return nil } diff --git a/commands/compose/detach.go b/commands/compose/detach.go index e8b07ed..8bc0e88 100644 --- a/commands/compose/detach.go +++ b/commands/compose/detach.go @@ -6,7 +6,6 @@ import ( "time" "git.sr.ht/~sircmpwn/aerc/widgets" - "github.com/gdamore/tcell" ) type Detach struct{} @@ -44,8 +43,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), 10*time.Second) return nil } diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go index 60c9df1..90b6134 100644 --- a/commands/compose/postpone.go +++ b/commands/compose/postpone.go @@ -63,7 +63,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error { go func() { errStr := <-errChan if errStr != "" { - aerc.PushError(" " + errStr) + aerc.PushError(" "+errStr, 10*time.Second) return } @@ -71,7 +71,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error { ctr := datacounter.NewWriterCounter(ioutil.Discard) err = composer.WriteMessage(header, ctr) if err != nil { - aerc.PushError(errors.Wrap(err, "WriteMessage").Error()) + aerc.PushError(errors.Wrap(err, "WriteMessage").Error(), 10*time.Second) composer.Close() return } @@ -90,7 +90,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error { r.Close() composer.Close() case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) r.Close() composer.Close() } diff --git a/commands/compose/send.go b/commands/compose/send.go index 59ae5d0..a22be8f 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -12,7 +12,6 @@ import ( "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" - "github.com/gdamore/tcell" "github.com/google/shlex" "github.com/miolini/datacounter" "github.com/pkg/errors" @@ -225,8 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { aerc.PushStatus("Sending...", 10*time.Second) nbytes, err := sendAsync() if err != nil { - aerc.SetStatus(" "+err.Error()). - Color(tcell.ColorDefault, tcell.ColorRed) + aerc.SetError(" " + err.Error()) return } if config.CopyTo != "" { @@ -247,7 +245,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { composer.SetSent() composer.Close() case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) r.Close() composer.Close() } diff --git a/commands/exec.go b/commands/exec.go index e15afbe..0a5470d 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{} @@ -33,16 +31,17 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error { go func() { err := cmd.Run() if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } 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()), 10*time.Second) + } 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/archive.go b/commands/msg/archive.go index 5561674..ba7e1f7 100644 --- a/commands/msg/archive.go +++ b/commands/msg/archive.go @@ -86,7 +86,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: wg.Done() case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) success = false wg.Done() } diff --git a/commands/msg/copy.go b/commands/msg/copy.go index f3d4030..e822c5c 100644 --- a/commands/msg/copy.go +++ b/commands/msg/copy.go @@ -60,7 +60,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: aerc.PushStatus("Messages copied.", 10*time.Second) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) return nil diff --git a/commands/msg/delete.go b/commands/msg/delete.go index 4bda8b9..482b60c 100644 --- a/commands/msg/delete.go +++ b/commands/msg/delete.go @@ -47,7 +47,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: aerc.PushStatus("Messages deleted.", 10*time.Second) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) @@ -68,7 +68,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error { lib.NewMessageStoreView(next, store, aerc.DecryptKeys, func(view lib.MessageView, err error) { if err != nil { - aerc.PushError(err.Error()) + aerc.PushError(err.Error(), 10*time.Second) return } nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) diff --git a/commands/msg/forward.go b/commands/msg/forward.go index 28abbed..5dd51b2 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -9,6 +9,7 @@ import ( "os" "path" "strings" + "time" "git.sr.ht/~sircmpwn/aerc/models" "git.sr.ht/~sircmpwn/aerc/widgets" @@ -83,7 +84,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error { composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(), acct.Worker(), template, defaults, original) if err != nil { - aerc.PushError("Error: " + err.Error()) + aerc.PushError("Error: "+err.Error(), 10*time.Second) return nil, err } diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go index f91075a..d74aece 100644 --- a/commands/msg/modify-labels.go +++ b/commands/msg/modify-labels.go @@ -58,7 +58,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: aerc.PushStatus("labels updated", 10*time.Second) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) return nil diff --git a/commands/msg/move.go b/commands/msg/move.go index 830e752..a19194e 100644 --- a/commands/msg/move.go +++ b/commands/msg/move.go @@ -71,7 +71,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error { case *types.Done: aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) return nil diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go index c88d61f..15b8c52 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{} @@ -76,7 +75,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { doTerm := func(reader io.Reader, name string) { term, err := commands.QuickTerm(aerc, cmd, reader) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) return } aerc.NewTab(term, name) @@ -94,16 +93,17 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error { }() err = ecmd.Run() if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } 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()), 10*time.Second) + } 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/commands/msg/read.go b/commands/msg/read.go index 1e264c2..e27f743 100644 --- a/commands/msg/read.go +++ b/commands/msg/read.go @@ -93,7 +93,7 @@ func submitReadChange(aerc *widgets.Aerc, store *lib.MessageStore, case *types.Done: aerc.PushStatus(msg_success, 10*time.Second) case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) } }) } @@ -106,7 +106,7 @@ func submitReadChangeWg(aerc *widgets.Aerc, store *lib.MessageStore, case *types.Done: wg.Done() case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) *success = false wg.Done() } diff --git a/commands/msg/recall.go b/commands/msg/recall.go index 7c9ac19..6c5e973 100644 --- a/commands/msg/recall.go +++ b/commands/msg/recall.go @@ -2,6 +2,7 @@ package msg import ( "io" + "time" "github.com/emersion/go-message" _ "github.com/emersion/go-message/charset" @@ -91,7 +92,7 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error { }, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Error: - aerc.PushError(" " + msg.Error.Error()) + aerc.PushError(" "+msg.Error.Error(), 10*time.Second) composer.Close() } }) diff --git a/commands/msg/reply.go b/commands/msg/reply.go index 28ce245..455c7ca 100644 --- a/commands/msg/reply.go +++ b/commands/msg/reply.go @@ -7,6 +7,7 @@ import ( "io" gomail "net/mail" "strings" + "time" "git.sr.ht/~sircmpwn/getopt" @@ -139,7 +140,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error { composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(), acct.Worker(), template, defaults, original) if err != nil { - aerc.PushError("Error: " + err.Error()) + aerc.PushError("Error: "+err.Error(), 10*time.Second) return err } diff --git a/commands/msgview/next.go b/commands/msgview/next.go index 978cf10..f9fb3d7 100644 --- a/commands/msgview/next.go +++ b/commands/msgview/next.go @@ -1,6 +1,8 @@ package msgview import ( + "time" + "git.sr.ht/~sircmpwn/aerc/commands/account" "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/widgets" @@ -40,7 +42,7 @@ func (NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error { lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys, func(view lib.MessageView, err error) { if err != nil { - aerc.PushError(err.Error()) + aerc.PushError(err.Error(), 10*time.Second) return } nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) diff --git a/commands/msgview/open.go b/commands/msgview/open.go index f708b2d..7f26542 100644 --- a/commands/msgview/open.go +++ b/commands/msgview/open.go @@ -49,20 +49,20 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error { tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) return } defer tmpFile.Close() _, err = io.Copy(tmpFile, reader) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) return } err = lib.OpenFile(tmpFile.Name()) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } aerc.PushStatus("Opened", 10*time.Second) diff --git a/commands/msgview/save.go b/commands/msgview/save.go index ef6bba8..ea1b8f3 100644 --- a/commands/msgview/save.go +++ b/commands/msgview/save.go @@ -128,7 +128,7 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error { go func() { err := <-ch if err != nil { - aerc.PushError(fmt.Sprintf("Save failed: %v", err)) + aerc.PushError(fmt.Sprintf("Save failed: %v", err), 10*time.Second) return } aerc.PushStatus("Saved to "+path, 10*time.Second) diff --git a/commands/term.go b/commands/term.go index 00f6937..9023285 100644 --- a/commands/term.go +++ b/commands/term.go @@ -2,6 +2,7 @@ package commands import ( "os/exec" + "time" "github.com/riywo/loginshell" @@ -46,7 +47,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error { term.OnClose = func(err error) { aerc.RemoveTab(term) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } } return nil diff --git a/commands/util.go b/commands/util.go index fdf20bd..7c7b6ab 100644 --- a/commands/util.go +++ b/commands/util.go @@ -32,7 +32,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter term.OnClose = func(err error) { if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) // remove the tab on error, otherwise it gets stuck aerc.RemoveTab(term) } else { @@ -56,7 +56,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter err := <-status if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } } 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 8ebd69d..c430724 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 @@ -332,6 +335,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { if err := ui.MapTo(&config.Ui); err != nil { return err } + + stylesetsDirs := ui.Key("stylesets-dirs").String() + if stylesetsDirs != "" { + config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":") + } } for _, sectionName := range file.SectionStrings() { if !strings.Contains(sectionName, "ui:") { @@ -346,6 +354,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { if err := uiSection.MapTo(&uiSubConfig); err != nil { return err } + stylesetsDirs := uiSection.Key("stylesets-dirs").String() + if stylesetsDirs != "" { + uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":") + } contextualUi := UIConfigContext{ UiConfig: uiSubConfig, @@ -406,6 +418,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 } @@ -466,6 +491,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{}, @@ -495,6 +522,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, @@ -505,6 +533,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 @@ -612,8 +641,17 @@ 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() + if err := ui.style.ParseStyleSet(ui.StyleSetName, styleSetDirs); err != nil { + return fmt.Errorf("Error whie parsing styleset \"%s\": %s", ui.StyleSetName, err) + } + + return nil +} + +func (config AercConfig) mergeContextualUi(baseUi UIConfig, + contextType ContextType, s string) UIConfig { for _, contextualUi := range config.ContextualUis { if contextualUi.ContextType != contextType { continue @@ -623,17 +661,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..9e918ae --- /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.fg=red +warning.fg=yellow +*error.bold=true +success.fg=green + +statusline*.default=true +statusline_default.reverse=true +statusline_error.fg=red +statusline_success.fg=green + +msglist_unread.bold=true + +completion_pill.reverse=true + +tab.reverse=true +border.reverse = true + +selecter_focused.reverse=true +selecter_chooser.bold=true diff --git a/config/style.go b/config/style.go new file mode 100644 index 0000000..fb17d93 --- /dev/null +++ b/config/style.go @@ -0,0 +1,372 @@ +package config + +import ( + "errors" + "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_SELECTER_DEFAULT + STYLE_SELECTER_FOCUSED + STYLE_SELECTER_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, + + "selecter_default": STYLE_SELECTER_DEFAULT, + "selecter_focused": STYLE_SELECTER_FOCUSED, + "selecter_chooser": STYLE_SELECTER_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 "", errors.New("Can't find styleset - " + stylesetName) +} +func (ss *StyleSet) ParseStyleSet(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 + } + + 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 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 eb88849..3ff5177 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..5b4cd37 --- /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 correspons 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 6f93367..e247dd2 100644 --- a/widgets/account-wizard.go +++ b/widgets/account-wizard.go @@ -10,6 +10,7 @@ import ( "path" "strconv" "strings" + "time" "github.com/gdamore/tcell" "github.com/go-ini/ini" @@ -75,21 +76,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 +151,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { {ui.SIZE_WEIGHT, 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 +231,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, 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 +251,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 +278,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 +313,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, 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 +333,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 +360,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 +394,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { {ui.SIZE_WEIGHT, 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") @@ -414,7 +429,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) { func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) { if d == nil { - wizard.aerc.PushError(" " + err.Error()) + wizard.aerc.PushError(" "+err.Error(), 10*time.Second) wizard.Invalidate() return } @@ -429,7 +444,7 @@ func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) { wizard.step = step wizard.focus = focus wizard.Focus(true) - wizard.aerc.PushError(" " + err.Error()) + wizard.aerc.PushError(" "+err.Error(), 10*time.Second) wizard.Invalidate() return } @@ -540,7 +555,7 @@ func (wizard *AccountWizard) finish(tutorial bool) { term.OnClose = func(err error) { wizard.aerc.RemoveTab(term) if err != nil { - wizard.aerc.PushError(" " + err.Error()) + wizard.aerc.PushError(" "+err.Error(), 10*time.Second) } } } diff --git a/widgets/account.go b/widgets/account.go index 20ed345..564a95d 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/gdamore/tcell" @@ -54,8 +55,7 @@ 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 &AccountView{ acct: acct, aerc: aerc, @@ -67,7 +67,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon dirlist := NewDirectoryList(conf, acct, logger, worker) if acctUiConf.SidebarWidth > 0 { - grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT)) + grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT, acctUiConf)) } msglist := NewMessageList(conf, logger, aerc) @@ -280,8 +280,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { acct.labels = msg.Labels case *types.Error: acct.logger.Printf("%v", msg.Error) - acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)). - Color(tcell.ColorDefault, tcell.ColorRed) + acct.host.SetError(fmt.Sprintf("%v", msg.Error)) } } @@ -291,7 +290,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion { } criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort) if err != nil { - acct.aerc.PushError(" ui.sort: " + err.Error()) + acct.aerc.PushError(" ui.sort: "+err.Error(), 10*time.Second) return nil } return criteria diff --git a/widgets/aerc.go b/widgets/aerc.go index 829873a..57d6cef 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, expiry time.Duration) *StatusMessage { + return aerc.statusline.PushError(text, expiry) +} + +func (aerc *Aerc) PushSuccess(text string, expiry time.Duration) *StatusMessage { + return aerc.statusline.PushSuccess(text, expiry) } func (aerc *Aerc) focus(item ui.Interactive) { @@ -416,11 +424,11 @@ func (aerc *Aerc) BeginExCommand(cmd string) { exline := NewExLine(aerc.conf, cmd, func(cmd string) { parts, err := shlex.Split(cmd) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } err = aerc.cmd(parts) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } // only add to history if this is an unsimulated command, // ie one not executed from a keybinding @@ -444,7 +452,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) { } err := aerc.cmd(cmd) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } }, func(cmd string) []string { return nil // TODO: completions @@ -471,7 +479,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) { } err := aerc.cmd(cmd) if err != nil { - aerc.PushError(" " + err.Error()) + aerc.PushError(" "+err.Error(), 10*time.Second) } }, func(cmd string) []string { return nil // TODO: completions @@ -552,11 +560,10 @@ func (aerc *Aerc) CloseDialog() { return } - 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 01b8dd8..f85e1f3 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), 10*time.Second) 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,9 @@ 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), + 10*time.Second) return nil } return signature @@ -648,7 +651,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) } @@ -702,23 +705,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)) } @@ -779,21 +786,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage { {ui.SIZE_WEIGHT, 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 600b38c..18072fa 100644 --- a/widgets/dirlist.go +++ b/widgets/dirlist.go @@ -194,7 +194,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) @@ -202,7 +203,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 } @@ -212,12 +213,9 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { if row >= ctx.Height() { break } - style := tcell.StyleDefault + style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT) 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, ctx.Width(), 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..b3ea9e0 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,16 @@ 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 +34,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 5aedb44..5d12f8e 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -3,6 +3,7 @@ package widgets import ( "fmt" "log" + "time" "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" @@ -49,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 { @@ -84,34 +86,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) { continue } - style := tcell.StyleDefault - - // 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) - } - // unread message - seen := false - for _, flag := range msg.Flags { - if flag == models.SeenFlag { - seen = true - } - } - if !seen { - style = style.Bold(true) - } - - ctx.Fill(0, row, ctx.Width(), 1, ' ', style) 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 + + // deleted message + if _, ok := store.Deleted[msg.Uid]; ok { + so = config.STYLE_MSGLIST_DELETED + } + // unread message + seen := false + flaged := false + for _, flag := range msg.Flags { + switch flag { + case models.SeenFlag: + seen = true + case models.FlaggedFlag: + flaged = true + } + } + if !seen { + so = config.STYLE_MSGLIST_UNREAD + } + + if flaged { + so = config.STYLE_MSGLIST_FLAGGED + } + + // 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, @@ -168,7 +186,7 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) { lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys, func(view lib.MessageView, err error) { if err != nil { - ml.aerc.PushError(err.Error()) + ml.aerc.PushError(err.Error(), 10*time.Second) return } viewer := NewMessageViewer(acct, ml.aerc.Config(), view) @@ -288,7 +306,8 @@ func (ml *MessageList) Scroll() { } 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 ce85970..6544ddd 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -32,6 +32,7 @@ type MessageViewer struct { grid *ui.Grid switcher *PartSwitcher msg lib.MessageView + uiConfig config.UIConfig } type PartSwitcher struct { @@ -61,9 +62,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(), } }, ) @@ -93,15 +96,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 { @@ -115,6 +119,7 @@ func NewMessageViewer(acct *AccountView, grid: grid, msg: msg, switcher: switcher, + uiConfig: acct.UiConfig(), } switcher.mv = mv @@ -223,8 +228,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) @@ -346,7 +352,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), @@ -435,6 +444,7 @@ func (mv *MessageViewer) Focus(focus bool) { type PartViewer struct { ui.Invalidatable + conf *config.AercConfig err error fetched bool filter *exec.Cmd @@ -449,6 +459,7 @@ type PartViewer struct { term *Terminal selecter *Selecter grid *ui.Grid + uiConfig config.UIConfig } func NewPartViewer(acct *AccountView, conf *config.AercConfig, @@ -518,7 +529,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, {ui.SIZE_WEIGHT, 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": @@ -531,6 +543,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, grid.AddChild(selecter).At(2, 0) pv := &PartViewer{ + conf: conf, filter: filter, index: index, msg: msg, @@ -542,6 +555,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig, term: term, selecter: selecter, grid: grid, + uiConfig: acct.UiConfig(), } if term != nil { @@ -639,14 +653,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) @@ -657,8 +673,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) @@ -680,8 +696,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) { @@ -689,18 +707,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/selecter.go index 7fae9cd..0faf37e 100644 --- a/widgets/selecter.go +++ b/widgets/selecter.go @@ -3,24 +3,27 @@ package widgets import ( "github.com/gdamore/tcell" + "git.sr.ht/~sircmpwn/aerc/config" "git.sr.ht/~sircmpwn/aerc/lib/ui" ) type Selecter 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 { +func NewSelecter(options []string, focus int, uiConfig config.UIConfig) *Selecter { return &Selecter{ - focus: focus, - options: options, + focus: focus, + options: options, + uiConfig: uiConfig, } } @@ -34,15 +37,16 @@ func (sel *Selecter) Invalidate() { } func (sel *Selecter) Draw(ctx *ui.Context) { - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', + sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT)) x := 2 for i, option := range sel.options { - style := tcell.StyleDefault + style := sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT) if sel.focus == i { if sel.focused { - style = style.Reverse(true) + style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_FOCUSED) } else if sel.chooser { - style = style.Bold(true) + style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_CHOOSER) } } x += ctx.Printf(x, 1, style, "[%s]", option) 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 8d0a1ae..d6d7761 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) @@ -84,6 +90,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage return msg } +func (status *StatusLine) PushError(text string, expiry time.Duration) *StatusMessage { + msg := status.Push(text, expiry) + msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR)) + return msg +} + +func (status *StatusLine) PushSuccess(text string, expiry time.Duration) *StatusMessage { + msg := status.Push(text, expiry) + msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS)) + return msg +} + func (status *StatusLine) Expire() { status.stack = nil } @@ -92,7 +110,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..1322a0a 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, expiry time.Duration) *StatusMessage + PushSuccess(text string, expiry time.Duration) *StatusMessage Beep() }