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
This commit is contained in:
Reto Brunner 2020-05-27 07:37:02 +02:00
parent 6c4ed3cfe2
commit 0f78f06610
48 changed files with 1093 additions and 332 deletions

View File

@ -36,7 +36,8 @@ DOCS := \
aerc-notmuch.5 \ aerc-notmuch.5 \
aerc-smtp.5 \ aerc-smtp.5 \
aerc-tutorial.7 \ aerc-tutorial.7 \
aerc-templates.7 aerc-templates.7 \
aerc-stylesets.7
.1.scd.1: .1.scd.1:
scdoc < $< > $@ scdoc < $< > $@
@ -59,7 +60,7 @@ clean:
install: all install: all
mkdir -m755 -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \ 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 -m755 aerc $(BINDIR)/aerc
install -m644 aerc.1 $(MANDIR)/man1/aerc.1 install -m644 aerc.1 $(MANDIR)/man1/aerc.1
install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.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-smtp.5 $(MANDIR)/man5/aerc-smtp.5
install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7 install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7
install -m644 aerc-templates.7 $(MANDIR)/man7/aerc-templates.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 config/accounts.conf $(SHAREDIR)/accounts.conf
install -m644 aerc.conf $(SHAREDIR)/aerc.conf install -m644 aerc.conf $(SHAREDIR)/aerc.conf
install -m644 config/binds.conf $(SHAREDIR)/binds.conf install -m644 config/binds.conf $(SHAREDIR)/binds.conf
@ -79,6 +81,7 @@ install: all
install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext
install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply
install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body
install -m644 config/default_styleset $(SHAREDIR)/stylesets/default
RMDIR_IF_EMPTY:=sh -c '\ RMDIR_IF_EMPTY:=sh -c '\
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \ if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \

View File

@ -40,7 +40,7 @@ func (MakeDir) Execute(aerc *widgets.Aerc, args []string) error {
aerc.PushStatus("Directory created.", 10*time.Second) aerc.PushStatus("Directory created.", 10*time.Second)
acct.Directories().Select(name) acct.Directories().Select(name)
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
} }
}) })
return nil return nil

View File

@ -2,6 +2,7 @@ package account
import ( import (
"errors" "errors"
"time"
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/widgets" "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, lib.NewMessageStoreView(msg, store, aerc.DecryptKeys,
func(view lib.MessageView, err error) { func(view lib.MessageView, err error) {
if err != nil { if err != nil {
aerc.PushError(err.Error()) aerc.PushError(err.Error(), 10*time.Second)
return return
} }
viewer := widgets.NewMessageViewer(acct, aerc.Config(), view) viewer := widgets.NewMessageViewer(acct, aerc.Config(), view)

View File

@ -8,7 +8,6 @@ import (
"git.sr.ht/~sircmpwn/aerc/commands" "git.sr.ht/~sircmpwn/aerc/commands"
"git.sr.ht/~sircmpwn/aerc/widgets" "git.sr.ht/~sircmpwn/aerc/widgets"
"github.com/gdamore/tcell"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
@ -36,24 +35,23 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {
path, err := homedir.Expand(path) path, err := homedir.Expand(path)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
return err return err
} }
pathinfo, err := os.Stat(path) pathinfo, err := os.Stat(path)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
return err return err
} else if pathinfo.IsDir() { } 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 return nil
} }
composer, _ := aerc.SelectedTab().(*widgets.Composer) composer, _ := aerc.SelectedTab().(*widgets.Composer)
composer.AddAttachment(path) composer.AddAttachment(path)
aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second). aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second)
Color(tcell.ColorDefault, tcell.ColorGreen)
return nil return nil
} }

View File

@ -6,7 +6,6 @@ import (
"time" "time"
"git.sr.ht/~sircmpwn/aerc/widgets" "git.sr.ht/~sircmpwn/aerc/widgets"
"github.com/gdamore/tcell"
) )
type Detach struct{} type Detach struct{}
@ -44,8 +43,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
return err return err
} }
aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second). aerc.PushSuccess(fmt.Sprintf("Detached %s", path), 10*time.Second)
Color(tcell.ColorDefault, tcell.ColorGreen)
return nil return nil
} }

View File

@ -63,7 +63,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
go func() { go func() {
errStr := <-errChan errStr := <-errChan
if errStr != "" { if errStr != "" {
aerc.PushError(" " + errStr) aerc.PushError(" "+errStr, 10*time.Second)
return return
} }
@ -71,7 +71,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
ctr := datacounter.NewWriterCounter(ioutil.Discard) ctr := datacounter.NewWriterCounter(ioutil.Discard)
err = composer.WriteMessage(header, ctr) err = composer.WriteMessage(header, ctr)
if err != nil { if err != nil {
aerc.PushError(errors.Wrap(err, "WriteMessage").Error()) aerc.PushError(errors.Wrap(err, "WriteMessage").Error(), 10*time.Second)
composer.Close() composer.Close()
return return
} }
@ -90,7 +90,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
r.Close() r.Close()
composer.Close() composer.Close()
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
r.Close() r.Close()
composer.Close() composer.Close()
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/gdamore/tcell"
"github.com/google/shlex" "github.com/google/shlex"
"github.com/miolini/datacounter" "github.com/miolini/datacounter"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -225,8 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
aerc.PushStatus("Sending...", 10*time.Second) aerc.PushStatus("Sending...", 10*time.Second)
nbytes, err := sendAsync() nbytes, err := sendAsync()
if err != nil { if err != nil {
aerc.SetStatus(" "+err.Error()). aerc.SetError(" " + err.Error())
Color(tcell.ColorDefault, tcell.ColorRed)
return return
} }
if config.CopyTo != "" { if config.CopyTo != "" {
@ -247,7 +245,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
composer.SetSent() composer.SetSent()
composer.Close() composer.Close()
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
r.Close() r.Close()
composer.Close() composer.Close()
} }

View File

@ -7,8 +7,6 @@ import (
"time" "time"
"git.sr.ht/~sircmpwn/aerc/widgets" "git.sr.ht/~sircmpwn/aerc/widgets"
"github.com/gdamore/tcell"
) )
type ExecCmd struct{} type ExecCmd struct{}
@ -33,16 +31,17 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
go func() { go func() {
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} else { } else {
color := tcell.ColorDefault
if cmd.ProcessState.ExitCode() != 0 { 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 return nil

View File

@ -86,7 +86,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
case *types.Done: case *types.Done:
wg.Done() wg.Done()
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
success = false success = false
wg.Done() wg.Done()
} }

View File

@ -60,7 +60,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error {
case *types.Done: case *types.Done:
aerc.PushStatus("Messages copied.", 10*time.Second) aerc.PushStatus("Messages copied.", 10*time.Second)
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
} }
}) })
return nil return nil

View File

@ -47,7 +47,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
case *types.Done: case *types.Done:
aerc.PushStatus("Messages deleted.", 10*time.Second) aerc.PushStatus("Messages deleted.", 10*time.Second)
case *types.Error: 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, lib.NewMessageStoreView(next, store, aerc.DecryptKeys,
func(view lib.MessageView, err error) { func(view lib.MessageView, err error) {
if err != nil { if err != nil {
aerc.PushError(err.Error()) aerc.PushError(err.Error(), 10*time.Second)
return return
} }
nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)

View File

@ -9,6 +9,7 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"time"
"git.sr.ht/~sircmpwn/aerc/models" "git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/widgets" "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(), composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
acct.Worker(), template, defaults, original) acct.Worker(), template, defaults, original)
if err != nil { if err != nil {
aerc.PushError("Error: " + err.Error()) aerc.PushError("Error: "+err.Error(), 10*time.Second)
return nil, err return nil, err
} }

View File

@ -58,7 +58,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error {
case *types.Done: case *types.Done:
aerc.PushStatus("labels updated", 10*time.Second) aerc.PushStatus("labels updated", 10*time.Second)
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
} }
}) })
return nil return nil

View File

@ -71,7 +71,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error {
case *types.Done: case *types.Done:
aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second) aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second)
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
} }
}) })
return nil return nil

View File

@ -12,7 +12,6 @@ import (
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
"git.sr.ht/~sircmpwn/getopt" "git.sr.ht/~sircmpwn/getopt"
"github.com/gdamore/tcell"
) )
type Pipe struct{} type Pipe struct{}
@ -76,7 +75,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
doTerm := func(reader io.Reader, name string) { doTerm := func(reader io.Reader, name string) {
term, err := commands.QuickTerm(aerc, cmd, reader) term, err := commands.QuickTerm(aerc, cmd, reader)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
return return
} }
aerc.NewTab(term, name) aerc.NewTab(term, name)
@ -94,16 +93,17 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
}() }()
err = ecmd.Run() err = ecmd.Run()
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} else { } else {
color := tcell.ColorDefault
if ecmd.ProcessState.ExitCode() != 0 { 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)
} }
} }

View File

@ -93,7 +93,7 @@ func submitReadChange(aerc *widgets.Aerc, store *lib.MessageStore,
case *types.Done: case *types.Done:
aerc.PushStatus(msg_success, 10*time.Second) aerc.PushStatus(msg_success, 10*time.Second)
case *types.Error: 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: case *types.Done:
wg.Done() wg.Done()
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
*success = false *success = false
wg.Done() wg.Done()
} }

View File

@ -2,6 +2,7 @@ package msg
import ( import (
"io" "io"
"time"
"github.com/emersion/go-message" "github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset" _ "github.com/emersion/go-message/charset"
@ -91,7 +92,7 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
}, func(msg types.WorkerMessage) { }, func(msg types.WorkerMessage) {
switch msg := msg.(type) { switch msg := msg.(type) {
case *types.Error: case *types.Error:
aerc.PushError(" " + msg.Error.Error()) aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
composer.Close() composer.Close()
} }
}) })

View File

@ -7,6 +7,7 @@ import (
"io" "io"
gomail "net/mail" gomail "net/mail"
"strings" "strings"
"time"
"git.sr.ht/~sircmpwn/getopt" "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(), composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
acct.AccountConfig(), acct.Worker(), template, defaults, original) acct.AccountConfig(), acct.Worker(), template, defaults, original)
if err != nil { if err != nil {
aerc.PushError("Error: " + err.Error()) aerc.PushError("Error: "+err.Error(), 10*time.Second)
return err return err
} }

View File

@ -1,6 +1,8 @@
package msgview package msgview
import ( import (
"time"
"git.sr.ht/~sircmpwn/aerc/commands/account" "git.sr.ht/~sircmpwn/aerc/commands/account"
"git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/widgets" "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, lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys,
func(view lib.MessageView, err error) { func(view lib.MessageView, err error) {
if err != nil { if err != nil {
aerc.PushError(err.Error()) aerc.PushError(err.Error(), 10*time.Second)
return return
} }
nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)

View File

@ -49,20 +49,20 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension) tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
return return
} }
defer tmpFile.Close() defer tmpFile.Close()
_, err = io.Copy(tmpFile, reader) _, err = io.Copy(tmpFile, reader)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
return return
} }
err = lib.OpenFile(tmpFile.Name()) err = lib.OpenFile(tmpFile.Name())
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
aerc.PushStatus("Opened", 10*time.Second) aerc.PushStatus("Opened", 10*time.Second)

View File

@ -128,7 +128,7 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error {
go func() { go func() {
err := <-ch err := <-ch
if err != nil { if err != nil {
aerc.PushError(fmt.Sprintf("Save failed: %v", err)) aerc.PushError(fmt.Sprintf("Save failed: %v", err), 10*time.Second)
return return
} }
aerc.PushStatus("Saved to "+path, 10*time.Second) aerc.PushStatus("Saved to "+path, 10*time.Second)

View File

@ -2,6 +2,7 @@ package commands
import ( import (
"os/exec" "os/exec"
"time"
"github.com/riywo/loginshell" "github.com/riywo/loginshell"
@ -46,7 +47,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error {
term.OnClose = func(err error) { term.OnClose = func(err error) {
aerc.RemoveTab(term) aerc.RemoveTab(term)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
} }
return nil return nil

View File

@ -32,7 +32,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter
term.OnClose = func(err error) { term.OnClose = func(err error) {
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
// remove the tab on error, otherwise it gets stuck // remove the tab on error, otherwise it gets stuck
aerc.RemoveTab(term) aerc.RemoveTab(term)
} else { } else {
@ -56,7 +56,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter
err := <-status err := <-status
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
} }

View File

@ -67,6 +67,17 @@ sort=
# Default: true # Default: true
next-message-on-delete=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] [viewer]
# #
# Specifies the pager to use when displaying emails. Note that some filters # Specifies the pager to use when displaying emails. Note that some filters

View File

@ -45,6 +45,9 @@ type UIConfig struct {
NextMessageOnDelete bool `ini:"next-message-on-delete"` NextMessageOnDelete bool `ini:"next-message-on-delete"`
CompletionDelay time.Duration `ini:"completion-delay"` CompletionDelay time.Duration `ini:"completion-delay"`
CompletionPopovers bool `ini:"completion-popovers"` CompletionPopovers bool `ini:"completion-popovers"`
StyleSetDirs []string `ini:"stylesets-dirs", delim:":"`
StyleSetName string `ini:"styleset-name"`
style StyleSet
} }
type ContextType int type ContextType int
@ -332,6 +335,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
if err := ui.MapTo(&config.Ui); err != nil { if err := ui.MapTo(&config.Ui); err != nil {
return err return err
} }
stylesetsDirs := ui.Key("stylesets-dirs").String()
if stylesetsDirs != "" {
config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":")
}
} }
for _, sectionName := range file.SectionStrings() { for _, sectionName := range file.SectionStrings() {
if !strings.Contains(sectionName, "ui:") { if !strings.Contains(sectionName, "ui:") {
@ -346,6 +354,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
if err := uiSection.MapTo(&uiSubConfig); err != nil { if err := uiSection.MapTo(&uiSubConfig); err != nil {
return err return err
} }
stylesetsDirs := uiSection.Key("stylesets-dirs").String()
if stylesetsDirs != "" {
uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":")
}
contextualUi := contextualUi :=
UIConfigContext{ UIConfigContext{
UiConfig: uiSubConfig, 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 return nil
} }
@ -466,6 +491,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
NextMessageOnDelete: true, NextMessageOnDelete: true,
CompletionDelay: 250 * time.Millisecond, CompletionDelay: 250 * time.Millisecond,
CompletionPopovers: true, CompletionPopovers: true,
StyleSetDirs: []string{path.Join(sharedir, "stylesets")},
StyleSetName: "default",
}, },
ContextualUis: []UIConfigContext{}, ContextualUis: []UIConfigContext{},
@ -495,6 +522,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
Forwards: "forward_as_body", Forwards: "forward_as_body",
}, },
} }
// These bindings are not configurable // These bindings are not configurable
config.Bindings.AccountWizard.ExKey = KeyStroke{ config.Bindings.AccountWizard.ExKey = KeyStroke{
Key: tcell.KeyCtrlE, Key: tcell.KeyCtrlE,
@ -505,6 +533,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
if err = config.LoadConfig(file); err != nil { if err = config.LoadConfig(file); err != nil {
return nil, err return nil, err
} }
if ui, err := file.GetSection("general"); err == nil { if ui, err := file.GetSection("general"); err == nil {
if err := ui.MapTo(&config.General); err != nil { if err := ui.MapTo(&config.General); err != nil {
return nil, err return nil, err
@ -612,8 +641,17 @@ func parseLayout(layout string) [][]string {
return l return l
} }
func (config *AercConfig) mergeContextualUi(baseUi *UIConfig, func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
contextType ContextType, s string) { 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 { for _, contextualUi := range config.ContextualUis {
if contextualUi.ContextType != contextType { if contextualUi.ContextType != contextType {
continue continue
@ -623,17 +661,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
continue continue
} }
mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig) mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
return if contextualUi.UiConfig.StyleSetName != "" {
} baseUi.style = contextualUi.UiConfig.style
} }
return baseUi
func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
baseUi := config.Ui
for k, v := range params {
config.mergeContextualUi(&baseUi, k, v)
} }
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)
}

33
config/default_styleset Normal file
View File

@ -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

372
config/style.go Normal file
View File

@ -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()
}

View File

@ -173,6 +173,20 @@ These options are configured in the *[ui]* section of aerc.conf.
Default: 250ms 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 ## Contextual UI Configuration
The UI configuration can be specialized for accounts, specific mail The UI configuration can be specialized for accounts, specific mail

189
doc/aerc-stylesets.7.scd Normal file
View File

@ -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: `<style_object>.fg=<color>`
*bg*
The background color of the style object is set.
Syntax: `<style_object>.bg=<color>`
*bold*
The bold attribute of the style object is set/unset.
Syntax: `<style_object>.bold=<true|false|toggle>`
*blink*
The blink attribute of the style object is set/unset.
_The terminal needs to support blinking text_
Syntax: `<style_object>.bold=<true|false|toggle>`
*underline*
The underline attribute of the style object is set/unset.
_The terminal needs to support underline text_
Syntax: `<style_object>.underline=<true|false|toggle>`
*reverse*
Reverses the color of the style object. Exchanges the foreground
and background colors.
Syntax: `<style_object>.reverse=<true|false|toggle>`
_If the value is false, it doesn't change anything_
*normal*
All the attributes of the style object are unset.
Syntax: `<style_object>.normal=<true>`
_The value doesn't matter_
*default*
Set the style object to the default style of the context. Usually
based on the terminal.
Syntax: `<style_object>.default=<true>`
_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.
*<Color name>*
Any w3c approved color name is used to set colors for the style.
*<Hex code>*
Hexcode for a color can be used. The format must be "\#XXXXXX"

View File

@ -2,6 +2,8 @@ package ui
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"git.sr.ht/~sircmpwn/aerc/config"
) )
const ( const (
@ -16,12 +18,15 @@ type Bordered struct {
borders uint borders uint
content Drawable content Drawable
onInvalidate func(d 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{ b := &Bordered{
borders: borders, borders: borders,
content: content, content: content,
uiConfig: uiConfig,
} }
content.OnInvalidate(b.contentInvalidated) content.OnInvalidate(b.contentInvalidated)
return b return b
@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) {
y := 0 y := 0
width := ctx.Width() width := ctx.Width()
height := ctx.Height() height := ctx.Height()
style := tcell.StyleDefault.Reverse(true) style := bordered.uiConfig.GetStyle(config.STYLE_BORDER)
if bordered.borders&BORDER_LEFT != 0 { if bordered.borders&BORDER_LEFT != 0 {
ctx.Fill(0, 0, 1, ctx.Height(), ' ', style) ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
x += 1 x += 1

View File

@ -3,16 +3,19 @@ package ui
import ( import (
"fmt" "fmt"
"git.sr.ht/~sircmpwn/aerc/config"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
type Stack struct { type Stack struct {
children []Drawable children []Drawable
onInvalidate []func(d Drawable) onInvalidate []func(d Drawable)
uiConfig config.UIConfig
} }
func NewStack() *Stack { func NewStack(uiConfig config.UIConfig) *Stack {
return &Stack{} return &Stack{uiConfig: uiConfig}
} }
func (stack *Stack) Children() []Drawable { func (stack *Stack) Children() []Drawable {
@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) {
if len(stack.children) > 0 { if len(stack.children) > 0 {
stack.Peek().Draw(ctx) stack.Peek().Draw(ctx)
} else { } 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))
} }
} }

View File

@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) {
func (strip *TabStrip) Draw(ctx *Context) { func (strip *TabStrip) Draw(ctx *Context) {
x := 0 x := 0
for i, tab := range strip.Tabs { for i, tab := range strip.Tabs {
style := tcell.StyleDefault.Reverse(true) style := strip.uiConfig.GetStyle(config.STYLE_TAB)
if strip.Selected == i { if strip.Selected == i {
style = tcell.StyleDefault style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB)
} }
tabWidth := 32 tabWidth := 32
if ctx.Width()-x < tabWidth { if ctx.Width()-x < tabWidth {
@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) {
break break
} }
} }
style := tcell.StyleDefault.Reverse(true) ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style) strip.uiConfig.GetStyle(config.STYLE_TAB))
} }
func (strip *TabStrip) Invalidate() { func (strip *TabStrip) Invalidate() {
@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) {
if content.Selected >= len(content.Tabs) { if content.Selected >= len(content.Tabs) {
width := ctx.Width() width := ctx.Width()
height := ctx.Height() 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] tab := content.Tabs[content.Selected]

View File

@ -15,17 +15,13 @@ type Text struct {
Invalidatable Invalidatable
text string text string
strategy uint strategy uint
fg tcell.Color style tcell.Style
bg tcell.Color
bold bool
reverse bool
} }
func NewText(text string) *Text { func NewText(text string, style tcell.Style) *Text {
return &Text{ return &Text{
bg: tcell.ColorDefault, text: text,
fg: tcell.ColorDefault, style: style,
text: text,
} }
} }
@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text {
return t 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) { func (t *Text) Draw(ctx *Context) {
size := runewidth.StringWidth(t.text) size := runewidth.StringWidth(t.text)
x := 0 x := 0
@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) {
if t.strategy == TEXT_RIGHT { if t.strategy == TEXT_RIGHT {
x = ctx.Width() - size x = ctx.Width() - size
} }
style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style)
if t.bold { ctx.Printf(x, 0, t.style, "%s", t.text)
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)
} }
func (t *Text) Invalidate() { func (t *Text) Invalidate() {

View File

@ -6,6 +6,8 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc/config"
) )
// TODO: Attach history providers // TODO: Attach history providers
@ -27,16 +29,18 @@ type TextInput struct {
completeIndex int completeIndex int
completeDelay time.Duration completeDelay time.Duration
completeDebouncer *time.Timer completeDebouncer *time.Timer
uiConfig config.UIConfig
} }
// Creates a new TextInput. TextInputs will render a "textbox" in the entire // 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 // context they're given, and process keypresses to build a string from user
// input. // input.
func NewTextInput(text string) *TextInput { func NewTextInput(text string, ui config.UIConfig) *TextInput {
return &TextInput{ return &TextInput{
cells: -1, cells: -1,
text: []rune(text), text: []rune(text),
index: len([]rune(text)), index: len([]rune(text)),
uiConfig: ui,
} }
} }
@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) {
ti.ensureScroll() ti.ensureScroll()
} }
ti.ctx = ctx // gross 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:] text := ti.text[scroll:]
sindex := ti.index - scroll sindex := ti.index - scroll
if ti.password { 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)) cells := runewidth.StringWidth(string(text))
ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault) ctx.Fill(x, 0, cells, 1, '*', defaultStyle)
} else { } 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) cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
if ti.focus { if ti.focus {
@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
ti.Set(stem + ti.StringRight()) ti.Set(stem + ti.StringRight())
ti.Invalidate() ti.Invalidate()
}, },
uiConfig: ti.uiConfig,
} }
width := maxLen(ti.completions) + 3 width := maxLen(ti.completions) + 3
height := len(ti.completions) height := len(ti.completions)
@ -353,6 +360,7 @@ type completions struct {
onSelect func(int) onSelect func(int)
onExec func() onExec func()
onStem func(string) onStem func(string)
uiConfig config.UIConfig
} }
func maxLen(ss []string) int { func maxLen(ss []string) int {
@ -367,10 +375,10 @@ func maxLen(ss []string) int {
} }
func (c *completions) Draw(ctx *Context) { func (c *completions) Draw(ctx *Context) {
bg := tcell.StyleDefault bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT)
sel := tcell.StyleDefault.Reverse(true) gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
gutter := tcell.StyleDefault pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL)
pill := tcell.StyleDefault.Reverse(true) sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT)
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)

View File

@ -10,6 +10,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/go-ini/ini" "github.com/go-ini/ini"
@ -75,21 +76,21 @@ type AccountWizard struct {
func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard { func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
wizard := &AccountWizard{ wizard := &AccountWizard{
accountName: ui.NewTextInput("").Prompt("> "), accountName: ui.NewTextInput("", conf.Ui).Prompt("> "),
aerc: aerc, aerc: aerc,
conf: conf, conf: conf,
temporary: false, temporary: false,
copySent: true, copySent: true,
email: ui.NewTextInput("").Prompt("> "), email: ui.NewTextInput("", conf.Ui).Prompt("> "),
fullName: ui.NewTextInput("").Prompt("> "), fullName: ui.NewTextInput("", conf.Ui).Prompt("> "),
imapPassword: ui.NewTextInput("").Prompt("] ").Password(true), imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
imapServer: ui.NewTextInput("").Prompt("> "), imapServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
imapStr: ui.NewText("imaps://"), imapStr: ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
imapUsername: ui.NewTextInput("").Prompt("> "), imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true), smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
smtpServer: ui.NewTextInput("").Prompt("> "), smtpServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
smtpStr: ui.NewText("smtps://"), smtpStr: ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
smtpUsername: ui.NewTextInput("").Prompt("> "), smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
} }
// Autofill some stuff for the user // Autofill some stuff for the user
@ -150,33 +151,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}) })
basics.AddChild( basics.AddChild(
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" + ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+
"This wizard supports basic IMAP & SMTP configuration.\n" + "This wizard supports basic IMAP & SMTP configuration.\n"+
"For other configurations, use <Ctrl+q> to exit and read the " + "For other configurations, use <Ctrl+q> to exit and read the "+
"aerc-config(5) man page.\n" + "aerc-config(5) man page.\n"+
"Press <Tab> and <Shift+Tab> to cycle between each field in this form, or <Ctrl+j> and <Ctrl+k>.")) "Press <Tab> and <Shift+Tab> to cycle between each field in this form, "+
"or <Ctrl+j> and <Ctrl+k>.",
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
basics.AddChild( basics.AddChild(
ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')"). ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')",
Bold(true)). conf.Ui.GetStyle(config.STYLE_HEADER))).
At(1, 0) At(1, 0)
basics.AddChild(wizard.accountName). basics.AddChild(wizard.accountName).
At(2, 0) At(2, 0)
basics.AddChild(ui.NewFill(' ')). basics.AddChild(ui.NewFill(' ')).
At(3, 0) At(3, 0)
basics.AddChild( basics.AddChild(
ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')"). ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')",
Bold(true)). conf.Ui.GetStyle(config.STYLE_HEADER))).
At(4, 0) At(4, 0)
basics.AddChild(wizard.fullName). basics.AddChild(wizard.fullName).
At(5, 0) At(5, 0)
basics.AddChild(ui.NewFill(' ')). basics.AddChild(ui.NewFill(' ')).
At(6, 0) At(6, 0)
basics.AddChild( 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) At(7, 0)
basics.AddChild(wizard.email). basics.AddChild(wizard.email).
At(8, 0) At(8, 0)
selecter := NewSelecter([]string{"Next"}, 0). selecter := NewSelecter([]string{"Next"}, 0, conf.Ui).
OnChoose(func(option string) { OnChoose(func(option string) {
email := wizard.email.String() email := wizard.email.String()
if strings.ContainsRune(email, '@') { if strings.ContainsRune(email, '@') {
@ -227,16 +231,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {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( incoming.AddChild(
ui.NewText("Username").Bold(true)). ui.NewText("Username",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(1, 0) At(1, 0)
incoming.AddChild(wizard.imapUsername). incoming.AddChild(wizard.imapUsername).
At(2, 0) At(2, 0)
incoming.AddChild(ui.NewFill(' ')). incoming.AddChild(ui.NewFill(' ')).
At(3, 0) At(3, 0)
incoming.AddChild( incoming.AddChild(
ui.NewText("Password").Bold(true)). ui.NewText("Password",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(4, 0) At(4, 0)
incoming.AddChild(wizard.imapPassword). incoming.AddChild(wizard.imapPassword).
At(5, 0) At(5, 0)
@ -244,20 +251,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
At(6, 0) At(6, 0)
incoming.AddChild( incoming.AddChild(
ui.NewText("Server address "+ 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) At(7, 0)
incoming.AddChild(wizard.imapServer). incoming.AddChild(wizard.imapServer).
At(8, 0) At(8, 0)
incoming.AddChild(ui.NewFill(' ')). incoming.AddChild(ui.NewFill(' ')).
At(9, 0) At(9, 0)
incoming.AddChild( incoming.AddChild(
ui.NewText("Connection mode").Bold(true)). ui.NewText("Connection mode",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(10, 0) At(10, 0)
imapMode := NewSelecter([]string{ imapMode := NewSelecter([]string{
"IMAP over SSL/TLS", "IMAP over SSL/TLS",
"IMAP with STARTTLS", "IMAP with STARTTLS",
"Insecure IMAP", "Insecure IMAP",
}, 0).Chooser(true).OnSelect(func(option string) { }, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
switch option { switch option {
case "IMAP over SSL/TLS": case "IMAP over SSL/TLS":
wizard.imapMode = IMAP_OVER_TLS wizard.imapMode = IMAP_OVER_TLS
@ -269,7 +278,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
wizard.imapUri() wizard.imapUri()
}) })
incoming.AddChild(imapMode).At(11, 0) incoming.AddChild(imapMode).At(11, 0)
selecter = NewSelecter([]string{"Previous", "Next"}, 1). selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
OnChoose(wizard.advance) OnChoose(wizard.advance)
incoming.AddChild(ui.NewFill(' ')).At(12, 0) incoming.AddChild(ui.NewFill(' ')).At(12, 0)
incoming.AddChild(wizard.imapStr).At(13, 0) incoming.AddChild(wizard.imapStr).At(13, 0)
@ -304,16 +313,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {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( outgoing.AddChild(
ui.NewText("Username").Bold(true)). ui.NewText("Username",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(1, 0) At(1, 0)
outgoing.AddChild(wizard.smtpUsername). outgoing.AddChild(wizard.smtpUsername).
At(2, 0) At(2, 0)
outgoing.AddChild(ui.NewFill(' ')). outgoing.AddChild(ui.NewFill(' ')).
At(3, 0) At(3, 0)
outgoing.AddChild( outgoing.AddChild(
ui.NewText("Password").Bold(true)). ui.NewText("Password",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(4, 0) At(4, 0)
outgoing.AddChild(wizard.smtpPassword). outgoing.AddChild(wizard.smtpPassword).
At(5, 0) At(5, 0)
@ -321,20 +333,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
At(6, 0) At(6, 0)
outgoing.AddChild( outgoing.AddChild(
ui.NewText("Server address "+ 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) At(7, 0)
outgoing.AddChild(wizard.smtpServer). outgoing.AddChild(wizard.smtpServer).
At(8, 0) At(8, 0)
outgoing.AddChild(ui.NewFill(' ')). outgoing.AddChild(ui.NewFill(' ')).
At(9, 0) At(9, 0)
outgoing.AddChild( outgoing.AddChild(
ui.NewText("Connection mode").Bold(true)). ui.NewText("Connection mode",
conf.Ui.GetStyle(config.STYLE_HEADER))).
At(10, 0) At(10, 0)
smtpMode := NewSelecter([]string{ smtpMode := NewSelecter([]string{
"SMTP over SSL/TLS", "SMTP over SSL/TLS",
"SMTP with STARTTLS", "SMTP with STARTTLS",
"Insecure SMTP", "Insecure SMTP",
}, 0).Chooser(true).OnSelect(func(option string) { }, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
switch option { switch option {
case "SMTP over SSL/TLS": case "SMTP over SSL/TLS":
wizard.smtpMode = SMTP_OVER_TLS wizard.smtpMode = SMTP_OVER_TLS
@ -346,15 +360,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
wizard.smtpUri() wizard.smtpUri()
}) })
outgoing.AddChild(smtpMode).At(11, 0) outgoing.AddChild(smtpMode).At(11, 0)
selecter = NewSelecter([]string{"Previous", "Next"}, 1). selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
OnChoose(wizard.advance) OnChoose(wizard.advance)
outgoing.AddChild(ui.NewFill(' ')).At(12, 0) outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
outgoing.AddChild(wizard.smtpStr).At(13, 0) outgoing.AddChild(wizard.smtpStr).At(13, 0)
outgoing.AddChild(ui.NewFill(' ')).At(14, 0) outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
outgoing.AddChild( outgoing.AddChild(
ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)). ui.NewText("Copy sent messages to 'Sent' folder?",
At(15, 0) conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
copySent := NewSelecter([]string{"Yes", "No"}, 0). copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
Chooser(true).OnChoose(func(option string) { Chooser(true).OnChoose(func(option string) {
switch option { switch option {
case "Yes": case "Yes":
@ -380,15 +394,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}) })
complete.AddChild(ui.NewText( complete.AddChild(ui.NewText(
"\nConfiguration complete!\n\n" + "\nConfiguration complete!\n\n"+
"You can go back and double check your settings, or choose 'Finish' to\n" + "You can go back and double check your settings, or choose 'Finish' to\n"+
"save your settings to accounts.conf.\n\n" + "save your settings to accounts.conf.\n\n"+
"To add another account in the future, run ':new-account'.")) "To add another account in the future, run ':new-account'.",
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
selecter = NewSelecter([]string{ selecter = NewSelecter([]string{
"Previous", "Previous",
"Finish & open tutorial", "Finish & open tutorial",
"Finish", "Finish",
}, 1).OnChoose(func(option string) { }, 1, conf.Ui).OnChoose(func(option string) {
switch option { switch option {
case "Previous": case "Previous":
wizard.advance("Previous") wizard.advance("Previous")
@ -414,7 +429,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {
func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) { func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
if d == nil { if d == nil {
wizard.aerc.PushError(" " + err.Error()) wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
wizard.Invalidate() wizard.Invalidate()
return return
} }
@ -429,7 +444,7 @@ func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
wizard.step = step wizard.step = step
wizard.focus = focus wizard.focus = focus
wizard.Focus(true) wizard.Focus(true)
wizard.aerc.PushError(" " + err.Error()) wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
wizard.Invalidate() wizard.Invalidate()
return return
} }
@ -540,7 +555,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
term.OnClose = func(err error) { term.OnClose = func(err error) {
wizard.aerc.RemoveTab(term) wizard.aerc.RemoveTab(term)
if err != nil { if err != nil {
wizard.aerc.PushError(" " + err.Error()) wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"time"
"github.com/gdamore/tcell" "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) worker, err := worker.NewWorker(acct.Source, logger)
if err != nil { if err != nil {
host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)). host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
Color(tcell.ColorDefault, tcell.ColorRed)
return &AccountView{ return &AccountView{
acct: acct, acct: acct,
aerc: aerc, aerc: aerc,
@ -67,7 +67,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
dirlist := NewDirectoryList(conf, acct, logger, worker) dirlist := NewDirectoryList(conf, acct, logger, worker)
if acctUiConf.SidebarWidth > 0 { 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) msglist := NewMessageList(conf, logger, aerc)
@ -280,8 +280,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
acct.labels = msg.Labels acct.labels = msg.Labels
case *types.Error: case *types.Error:
acct.logger.Printf("%v", msg.Error) acct.logger.Printf("%v", msg.Error)
acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)). acct.host.SetError(fmt.Sprintf("%v", msg.Error))
Color(tcell.ColorDefault, tcell.ColorRed)
} }
} }
@ -291,7 +290,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
} }
criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort) criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
if err != nil { if err != nil {
acct.aerc.PushError(" ui.sort: " + err.Error()) acct.aerc.PushError(" ui.sort: "+err.Error(), 10*time.Second)
return nil return nil
} }
return criteria return criteria

View File

@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
tabs := ui.NewTabs(&conf.Ui) tabs := ui.NewTabs(&conf.Ui)
statusbar := ui.NewStack() statusbar := ui.NewStack(conf.Ui)
statusline := NewStatusLine() statusline := NewStatusLine(conf.Ui)
statusbar.Push(statusline) statusbar.Push(statusline)
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
logger: logger, logger: logger,
statusbar: statusbar, statusbar: statusbar,
statusline: statusline, statusline: statusline,
prompts: ui.NewStack(), prompts: ui.NewStack(conf.Ui),
tabs: tabs, tabs: tabs,
} }
@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage {
return aerc.statusline.Set(status) 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 { func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
return aerc.statusline.Push(text, expiry) return aerc.statusline.Push(text, expiry)
} }
func (aerc *Aerc) PushError(text string) { func (aerc *Aerc) PushError(text string, expiry time.Duration) *StatusMessage {
aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed) 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) { 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) { exline := NewExLine(aerc.conf, cmd, func(cmd string) {
parts, err := shlex.Split(cmd) parts, err := shlex.Split(cmd)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
err = aerc.cmd(parts) err = aerc.cmd(parts)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
// only add to history if this is an unsimulated command, // only add to history if this is an unsimulated command,
// ie one not executed from a keybinding // ie one not executed from a keybinding
@ -444,7 +452,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
} }
err := aerc.cmd(cmd) err := aerc.cmd(cmd)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
}, func(cmd string) []string { }, func(cmd string) []string {
return nil // TODO: completions return nil // TODO: completions
@ -471,7 +479,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
} }
err := aerc.cmd(cmd) err := aerc.cmd(cmd)
if err != nil { if err != nil {
aerc.PushError(" " + err.Error()) aerc.PushError(" "+err.Error(), 10*time.Second)
} }
}, func(cmd string) []string { }, func(cmd string) []string {
return nil // TODO: completions return nil // TODO: completions
@ -552,11 +560,10 @@ func (aerc *Aerc) CloseDialog() {
return return
} }
func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) { func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
chText = make(chan string, 1) chText = make(chan string, 1)
chErr = make(chan error, 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() { defer func() {
close(chErr) close(chErr)
close(chText) close(chText)

View File

@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
templateData := templates.ParseTemplateData(defaults, original) templateData := templates.ParseTemplateData(defaults, original)
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) { 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) worker.Logger.Printf("could not complete header: %v", err)
}, aerc.Logger()) }, aerc.Logger())
layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults) layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
email, err := ioutil.TempFile("", "aerc-compose-*.eml") email, err := ioutil.TempFile("", "aerc-compose-*.eml")
if err != nil { if err != nil {
@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
return c, nil return c, nil
} }
func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer, func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
defaults map[string]string) ( defaults map[string]string) (
newLayout HeaderLayout, newLayout HeaderLayout,
editors map[string]*headerEditor, editors map[string]*headerEditor,
focusable []ui.MouseableDrawableInteractive, focusable []ui.MouseableDrawableInteractive,
) { ) {
layout := conf.Compose.HeaderLayout layout := aerc.conf.Compose.HeaderLayout
editors = make(map[string]*headerEditor) editors = make(map[string]*headerEditor)
focusable = make([]ui.MouseableDrawableInteractive, 0) focusable = make([]ui.MouseableDrawableInteractive, 0)
for _, row := range layout { for _, row := range layout {
for _, h := range row { for _, h := range row {
e := newHeaderEditor(h, "") e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
if conf.Ui.CompletionPopovers { if aerc.conf.Ui.CompletionPopovers {
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay) e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
} }
editors[h] = e editors[h] = e
switch h { switch h {
@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
for _, h := range []string{"Cc", "Bcc"} { for _, h := range []string{"Cc", "Bcc"} {
if val, ok := defaults[h]; ok && val != "" { if val, ok := defaults[h]; ok && val != "" {
if _, ok := editors[h]; !ok { if _, ok := editors[h]; !ok {
e := newHeaderEditor(h, "") e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
if conf.Ui.CompletionPopovers { if aerc.conf.Ui.CompletionPopovers {
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay) e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
} }
editors[h] = e editors[h] = e
focusable = append(focusable, e) focusable = append(focusable, e)
@ -259,7 +260,9 @@ func (c *Composer) readSignatureFromFile() []byte {
} }
signature, err := ioutil.ReadFile(sigFile) signature, err := ioutil.ReadFile(sigFile)
if err != nil { 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 nil
} }
return signature return signature
@ -648,7 +651,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
} }
return return
} }
e := newHeaderEditor(header, value) e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
if c.config.Ui.CompletionPopovers { if c.config.Ui.CompletionPopovers {
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay) e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
} }
@ -702,23 +705,27 @@ func (c *Composer) reloadEmail() error {
} }
type headerEditor struct { type headerEditor struct {
name string name string
focused bool focused bool
input *ui.TextInput 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{ return &headerEditor{
input: ui.NewTextInput(value), input: ui.NewTextInput(value, uiConfig),
name: name, name: name,
uiConfig: uiConfig,
} }
} }
func (he *headerEditor) Draw(ctx *ui.Context) { func (he *headerEditor) Draw(ctx *ui.Context) {
name := he.name + " " name := he.name + " "
size := runewidth.StringWidth(name) size := runewidth.StringWidth(name)
ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault) defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name) 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)) 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}, {ui.SIZE_WEIGHT, 1},
}) })
uiConfig := composer.config.Ui
if err != nil { if err != nil {
grid.AddChild(ui.NewText(err.Error()). grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR)))
Color(tcell.ColorRed, tcell.ColorDefault)) grid.AddChild(ui.NewText("Press [q] to close this tab.",
grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0) uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0)
} else { } else {
// TODO: source this from actual keybindings? // TODO: source this from actual keybindings?
grid.AddChild(ui.NewText( grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach",
"Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0) uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0)
grid.AddChild(ui.NewText("Attachments:"). grid.AddChild(ui.NewText("Attachments:",
Reverse(true)).At(1, 0) uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0)
if len(composer.attachments) == 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 { } else {
for i, a := range composer.attachments { 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)
} }
} }
} }

View File

@ -194,7 +194,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string {
} }
func (dirlist *DirectoryList) Draw(ctx *ui.Context) { 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() { if dirlist.spinner.IsRunning() {
dirlist.spinner.Draw(ctx) dirlist.spinner.Draw(ctx)
@ -202,7 +203,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
} }
if len(dirlist.dirs) == 0 { if len(dirlist.dirs) == 0 {
style := tcell.StyleDefault style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist) ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
return return
} }
@ -212,12 +213,9 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
if row >= ctx.Height() { if row >= ctx.Height() {
break break
} }
style := tcell.StyleDefault style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
if name == dirlist.selected { if name == dirlist.selected {
style = style.Reverse(true) style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT)
} else if name == dirlist.selecting {
style = style.Reverse(true)
style = style.Foreground(tcell.ColorGray)
} }
ctx.Fill(0, row, ctx.Width(), 1, ' ', style) ctx.Fill(0, row, ctx.Width(), 1, ' ', style)

View File

@ -15,13 +15,14 @@ type ExLine struct {
tabcomplete func(cmd string) []string tabcomplete func(cmd string) []string
cmdHistory lib.History cmdHistory lib.History
input *ui.TextInput input *ui.TextInput
conf *config.AercConfig
} }
func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(), func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(),
tabcomplete func(cmd string) []string, tabcomplete func(cmd string) []string,
cmdHistory lib.History) *ExLine { cmdHistory lib.History) *ExLine {
input := ui.NewTextInput("").Prompt(":").Set(cmd) input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd)
if conf.Ui.CompletionPopovers { if conf.Ui.CompletionPopovers {
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay) input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
} }
@ -31,6 +32,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
tabcomplete: tabcomplete, tabcomplete: tabcomplete,
cmdHistory: cmdHistory, cmdHistory: cmdHistory,
input: input, input: input,
conf: conf,
} }
input.OnInvalidate(func(d ui.Drawable) { input.OnInvalidate(func(d ui.Drawable) {
exline.Invalidate() 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), func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string),
tabcomplete func(cmd string) []string) *ExLine { tabcomplete func(cmd string) []string) *ExLine {
input := ui.NewTextInput("").Prompt(prompt) input := ui.NewTextInput("", conf.Ui).Prompt(prompt)
if conf.Ui.CompletionPopovers { if conf.Ui.CompletionPopovers {
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay) input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
) )
@ -14,14 +15,16 @@ type GetPasswd struct {
title string title string
prompt string prompt string
input *ui.TextInput 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{ getpasswd := &GetPasswd{
callback: cb, callback: cb,
title: title, title: title,
prompt: prompt, 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.input.OnInvalidate(func(_ ui.Drawable) {
getpasswd.Invalidate() getpasswd.Invalidate()
@ -31,10 +34,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw
} }
func (gp *GetPasswd) Draw(ctx *ui.Context) { func (gp *GetPasswd) Draw(ctx *ui.Context) {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT)
ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true)) titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE)
ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt) 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)) gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
} }

View File

@ -3,6 +3,7 @@ package widgets
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
@ -49,7 +50,8 @@ func (ml *MessageList) Invalidate() {
func (ml *MessageList) Draw(ctx *ui.Context) { func (ml *MessageList) Draw(ctx *ui.Context) {
ml.height = ctx.Height() 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() store := ml.Store()
if store == nil { if store == nil {
@ -84,34 +86,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
continue 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{ uiConfig := ml.conf.GetUiConfig(map[config.ContextType]string{
config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name, config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(), config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject, 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( fmtStr, args, err := format.ParseMessageFormat(
ml.aerc.SelectedAccount().acct.From, ml.aerc.SelectedAccount().acct.From,
uiConfig.IndexFormat, uiConfig.IndexFormat,
@ -168,7 +186,7 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys, lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys,
func(view lib.MessageView, err error) { func(view lib.MessageView, err error) {
if err != nil { if err != nil {
ml.aerc.PushError(err.Error()) ml.aerc.PushError(err.Error(), 10*time.Second)
return return
} }
viewer := NewMessageViewer(acct, ml.aerc.Config(), view) viewer := NewMessageViewer(acct, ml.aerc.Config(), view)
@ -288,7 +306,8 @@ func (ml *MessageList) Scroll() {
} }
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) { 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, ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
tcell.StyleDefault, "%s", msg) uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
} }

View File

@ -32,6 +32,7 @@ type MessageViewer struct {
grid *ui.Grid grid *ui.Grid
switcher *PartSwitcher switcher *PartSwitcher
msg lib.MessageView msg lib.MessageView
uiConfig config.UIConfig
} }
type PartSwitcher struct { type PartSwitcher struct {
@ -61,9 +62,11 @@ func NewMessageViewer(acct *AccountView,
header, headerHeight := layout.grid( header, headerHeight := layout.grid(
func(header string) ui.Drawable { func(header string) ui.Drawable {
return &HeaderView{ return &HeaderView{
conf: conf,
Name: header, Name: header,
Value: fmtHeader(msg.MessageInfo(), header, Value: fmtHeader(msg.MessageInfo(), header,
acct.UiConfig().TimestampFormat), acct.UiConfig().TimestampFormat),
uiConfig: acct.UiConfig(),
} }
}, },
) )
@ -93,15 +96,16 @@ func NewMessageViewer(acct *AccountView,
err := createSwitcher(acct, switcher, conf, msg) err := createSwitcher(acct, switcher, conf, msg)
if err != nil { if err != nil {
return &MessageViewer{ return &MessageViewer{
err: err, err: err,
grid: grid, grid: grid,
msg: msg, msg: msg,
uiConfig: acct.UiConfig(),
} }
} }
grid.AddChild(header).At(0, 0) grid.AddChild(header).At(0, 0)
if msg.PGPDetails() != nil { 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(ui.NewFill(' ')).At(2, 0)
grid.AddChild(switcher).At(3, 0) grid.AddChild(switcher).At(3, 0)
} else { } else {
@ -115,6 +119,7 @@ func NewMessageViewer(acct *AccountView,
grid: grid, grid: grid,
msg: msg, msg: msg,
switcher: switcher, switcher: switcher,
uiConfig: acct.UiConfig(),
} }
switcher.mv = mv switcher.mv = mv
@ -223,8 +228,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher,
func (mv *MessageViewer) Draw(ctx *ui.Context) { func (mv *MessageViewer) Draw(ctx *ui.Context) {
if mv.err != nil { if mv.err != nil {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error()) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
ctx.Printf(0, 0, style, "%s", mv.err.Error())
return return
} }
mv.grid.Draw(ctx) mv.grid.Draw(ctx)
@ -346,7 +352,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
ps.height = ctx.Height() ps.height = ctx.Height()
y := ctx.Height() - height y := ctx.Height() - height
for i, part := range ps.parts { 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) ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
name := fmt.Sprintf("%s/%s", name := fmt.Sprintf("%s/%s",
strings.ToLower(part.part.MIMEType), strings.ToLower(part.part.MIMEType),
@ -435,6 +444,7 @@ func (mv *MessageViewer) Focus(focus bool) {
type PartViewer struct { type PartViewer struct {
ui.Invalidatable ui.Invalidatable
conf *config.AercConfig
err error err error
fetched bool fetched bool
filter *exec.Cmd filter *exec.Cmd
@ -449,6 +459,7 @@ type PartViewer struct {
term *Terminal term *Terminal
selecter *Selecter selecter *Selecter
grid *ui.Grid grid *ui.Grid
uiConfig config.UIConfig
} }
func NewPartViewer(acct *AccountView, conf *config.AercConfig, func NewPartViewer(acct *AccountView, conf *config.AercConfig,
@ -518,7 +529,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
{ui.SIZE_WEIGHT, 1}, {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) { OnChoose(func(option string) {
switch option { switch option {
case "Save message": case "Save message":
@ -531,6 +543,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
grid.AddChild(selecter).At(2, 0) grid.AddChild(selecter).At(2, 0)
pv := &PartViewer{ pv := &PartViewer{
conf: conf,
filter: filter, filter: filter,
index: index, index: index,
msg: msg, msg: msg,
@ -542,6 +555,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
term: term, term: term,
selecter: selecter, selecter: selecter,
grid: grid, grid: grid,
uiConfig: acct.UiConfig(),
} }
if term != nil { if term != nil {
@ -639,14 +653,16 @@ func (pv *PartViewer) Invalidate() {
} }
func (pv *PartViewer) Draw(ctx *ui.Context) { 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 { if pv.filter == nil {
// TODO: Let them download it directly or something // TODO: Let them download it directly or something
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed), ctx.Printf(0, 0, styleError,
"No filter configured for this mimetype ('%s/%s')", "No filter configured for this mimetype ('%s/%s')",
pv.part.MIMEType, pv.part.MIMESubType, 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") "You can still :save the message or :pipe it to an external command")
pv.selecter.Focus(true) pv.selecter.Focus(true)
pv.grid.Draw(ctx) pv.grid.Draw(ctx)
@ -657,8 +673,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
pv.fetched = true pv.fetched = true
} }
if pv.err != nil { if pv.err != nil {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error()) ctx.Printf(0, 0, style, "%s", pv.err.Error())
return return
} }
pv.term.Draw(ctx) pv.term.Draw(ctx)
@ -680,8 +696,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
type HeaderView struct { type HeaderView struct {
ui.Invalidatable ui.Invalidatable
Name string conf *config.AercConfig
Value string Name string
Value string
uiConfig config.UIConfig
} }
func (hv *HeaderView) Draw(ctx *ui.Context) { func (hv *HeaderView) Draw(ctx *ui.Context) {
@ -689,18 +707,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
size := runewidth.StringWidth(name) size := runewidth.StringWidth(name)
lim := ctx.Width() - size - 1 lim := ctx.Width() - size - 1
value := runewidth.Truncate(" "+hv.Value, lim, "…") value := runewidth.Truncate(" "+hv.Value, lim, "…")
var (
hstyle tcell.Style vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
vstyle tcell.Style hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)
)
// TODO: Make this more robust and less dumb // TODO: Make this more robust and less dumb
if hv.Name == "PGP" { if hv.Name == "PGP" {
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen) vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
hstyle = tcell.StyleDefault.Bold(true)
} else {
vstyle = tcell.StyleDefault
hstyle = tcell.StyleDefault.Bold(true)
} }
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle) ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
ctx.Printf(0, 0, hstyle, "%s", name) ctx.Printf(0, 0, hstyle, "%s", name)
ctx.Printf(size, 0, vstyle, "%s", value) ctx.Printf(size, 0, vstyle, "%s", value)

View File

@ -3,40 +3,40 @@ package widgets
import ( import (
"errors" "errors"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
"github.com/gdamore/tcell"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
pgperrors "golang.org/x/crypto/openpgp/errors" pgperrors "golang.org/x/crypto/openpgp/errors"
) )
type PGPInfo struct { type PGPInfo struct {
ui.Invalidatable ui.Invalidatable
details *openpgp.MessageDetails details *openpgp.MessageDetails
uiConfig config.UIConfig
} }
func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo { func NewPGPInfo(details *openpgp.MessageDetails, uiConfig config.UIConfig) *PGPInfo {
return &PGPInfo{details: details} return &PGPInfo{details: details, uiConfig: uiConfig}
} }
func (p *PGPInfo) DrawSignature(ctx *ui.Context) { func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
errorStyle := tcell.StyleDefault.Background(tcell.ColorRed). errorStyle := p.uiConfig.GetStyle(config.STYLE_ERROR)
Foreground(tcell.ColorWhite).Bold(true) warningStyle := p.uiConfig.GetStyle(config.STYLE_WARNING)
softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true) validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true) defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
// TODO: Nicer prompt for TOFU, fetch from keyserver, etc // TODO: Nicer prompt for TOFU, fetch from keyserver, etc
if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) || if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
p.details.SignedBy == nil { p.details.SignedBy == nil {
x := ctx.Printf(0, 0, softErrorStyle, "*") x := ctx.Printf(0, 0, warningStyle, "*")
x += ctx.Printf(x, 0, tcell.StyleDefault, x += ctx.Printf(x, 0, defaultStyle,
" Signed with unknown key (%8X); authenticity unknown", " Signed with unknown key (%8X); authenticity unknown",
p.details.SignedByKeyId) p.details.SignedByKeyId)
} else if p.details.SignatureError != nil { } else if p.details.SignatureError != nil {
x := ctx.Printf(0, 0, errorStyle, "Invalid signature!") x := ctx.Printf(0, 0, errorStyle, "Invalid signature!")
x += ctx.Printf(x, 0, tcell.StyleDefault. x += ctx.Printf(x, 0, errorStyle,
Foreground(tcell.ColorRed).Bold(true),
" This message may have been tampered with! (%s)", " This message may have been tampered with! (%s)",
p.details.SignatureError.Error()) p.details.SignatureError.Error())
} else { } else {
@ -44,24 +44,26 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
ident := entity.PrimaryIdentity() ident := entity.PrimaryIdentity()
x := ctx.Printf(0, 0, validStyle, "✓ Authentic ") x := ctx.Printf(0, 0, validStyle, "✓ Authentic ")
x += ctx.Printf(x, 0, tcell.StyleDefault, x += ctx.Printf(x, 0, defaultStyle,
"Signature from %s (%8X)", "Signature from %s (%8X)",
ident.Name, p.details.SignedByKeyId) ident.Name, p.details.SignedByKeyId)
} }
} }
func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) { 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 entity := p.details.DecryptedWith.Entity
ident := entity.PrimaryIdentity() ident := entity.PrimaryIdentity()
x := ctx.Printf(0, y, validStyle, "✓ Encrypted ") 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) "To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
} }
func (p *PGPInfo) Draw(ctx *ui.Context) { 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 { if p.details.IsSigned && p.details.IsEncrypted {
p.DrawSignature(ctx) p.DrawSignature(ctx)
p.DrawEncryption(ctx, 1) p.DrawEncryption(ctx, 1)

View File

@ -3,24 +3,27 @@ package widgets
import ( import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
) )
type Selecter struct { type Selecter struct {
ui.Invalidatable ui.Invalidatable
chooser bool chooser bool
focused bool focused bool
focus int focus int
options []string options []string
uiConfig config.UIConfig
onChoose func(option string) onChoose func(option string)
onSelect 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{ return &Selecter{
focus: focus, focus: focus,
options: options, options: options,
uiConfig: uiConfig,
} }
} }
@ -34,15 +37,16 @@ func (sel *Selecter) Invalidate() {
} }
func (sel *Selecter) Draw(ctx *ui.Context) { 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 x := 2
for i, option := range sel.options { for i, option := range sel.options {
style := tcell.StyleDefault style := sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT)
if sel.focus == i { if sel.focus == i {
if sel.focused { if sel.focused {
style = style.Reverse(true) style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_FOCUSED)
} else if sel.chooser { } else if sel.chooser {
style = style.Bold(true) style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_CHOOSER)
} }
} }
x += ctx.Printf(x, 1, style, "[%s]", option) x += ctx.Printf(x, 1, style, "[%s]", option)

View File

@ -16,6 +16,7 @@ type Spinner struct {
frame int64 // access via atomic frame int64 // access via atomic
frames []string frames []string
stop chan struct{} stop chan struct{}
style tcell.Style
} }
func NewSpinner(uiConf *config.UIConfig) *Spinner { func NewSpinner(uiConf *config.UIConfig) *Spinner {
@ -23,6 +24,7 @@ func NewSpinner(uiConf *config.UIConfig) *Spinner {
stop: make(chan struct{}), stop: make(chan struct{}),
frame: -1, frame: -1,
frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter), frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter),
style: uiConf.GetStyle(config.STYLE_SPINNER),
} }
return &spinner return &spinner
} }
@ -70,9 +72,9 @@ func (s *Spinner) Draw(ctx *ui.Context) {
cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames))) 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 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() { func (s *Spinner) Invalidate() {

View File

@ -6,6 +6,7 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib/ui" "git.sr.ht/~sircmpwn/aerc/lib/ui"
) )
@ -14,21 +15,21 @@ type StatusLine struct {
stack []*StatusMessage stack []*StatusMessage
fallback StatusMessage fallback StatusMessage
aerc *Aerc aerc *Aerc
uiConfig config.UIConfig
} }
type StatusMessage struct { type StatusMessage struct {
bg tcell.Color style tcell.Style
fg tcell.Color
message string message string
} }
func NewStatusLine() *StatusLine { func NewStatusLine(uiConfig config.UIConfig) *StatusLine {
return &StatusLine{ return &StatusLine{
fallback: StatusMessage{ fallback: StatusMessage{
bg: tcell.ColorDefault, style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
fg: tcell.ColorDefault,
message: "Idle", message: "Idle",
}, },
uiConfig: uiConfig,
} }
} }
@ -41,9 +42,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
if len(status.stack) != 0 { if len(status.stack) != 0 {
line = status.stack[len(status.stack)-1] line = status.stack[len(status.stack)-1]
} }
style := tcell.StyleDefault. ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style)
Background(line.bg).Foreground(line.fg).Reverse(true)
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
pendingKeys := "" pendingKeys := ""
if status.aerc != nil { if status.aerc != nil {
for _, pendingKey := range status.aerc.pendingKeys { 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) 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 { func (status *StatusLine) Set(text string) *StatusMessage {
status.fallback = StatusMessage{ status.fallback = StatusMessage{
bg: tcell.ColorDefault, style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
fg: tcell.ColorDefault, 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, message: text,
} }
status.Invalidate() status.Invalidate()
@ -66,8 +73,7 @@ func (status *StatusLine) Set(text string) *StatusMessage {
func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage { func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
msg := &StatusMessage{ msg := &StatusMessage{
bg: tcell.ColorDefault, style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
fg: tcell.ColorDefault,
message: text, message: text,
} }
status.stack = append(status.stack, msg) status.stack = append(status.stack, msg)
@ -84,6 +90,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
return msg 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() { func (status *StatusLine) Expire() {
status.stack = nil status.stack = nil
} }
@ -92,7 +110,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) {
status.aerc = aerc status.aerc = aerc
} }
func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) { func (msg *StatusMessage) Color(style tcell.Style) {
msg.bg = bg msg.style = style
msg.fg = fg
} }

View File

@ -7,6 +7,9 @@ import (
type TabHost interface { type TabHost interface {
BeginExCommand(cmd string) BeginExCommand(cmd string)
SetStatus(status string) *StatusMessage SetStatus(status string) *StatusMessage
SetError(err string) *StatusMessage
PushStatus(text string, expiry time.Duration) *StatusMessage PushStatus(text string, expiry time.Duration) *StatusMessage
PushError(text string, expiry time.Duration) *StatusMessage
PushSuccess(text string, expiry time.Duration) *StatusMessage
Beep() Beep()
} }