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:
parent
6c4ed3cfe2
commit
0f78f06610
48 changed files with 1093 additions and 332 deletions
7
Makefile
7
Makefile
|
@ -36,7 +36,8 @@ DOCS := \
|
|||
aerc-notmuch.5 \
|
||||
aerc-smtp.5 \
|
||||
aerc-tutorial.7 \
|
||||
aerc-templates.7
|
||||
aerc-templates.7 \
|
||||
aerc-stylesets.7
|
||||
|
||||
.1.scd.1:
|
||||
scdoc < $< > $@
|
||||
|
@ -59,7 +60,7 @@ clean:
|
|||
|
||||
install: all
|
||||
mkdir -m755 -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \
|
||||
$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates
|
||||
$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates $(SHAREDIR)/stylesets
|
||||
install -m755 aerc $(BINDIR)/aerc
|
||||
install -m644 aerc.1 $(MANDIR)/man1/aerc.1
|
||||
install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.1
|
||||
|
@ -71,6 +72,7 @@ install: all
|
|||
install -m644 aerc-smtp.5 $(MANDIR)/man5/aerc-smtp.5
|
||||
install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7
|
||||
install -m644 aerc-templates.7 $(MANDIR)/man7/aerc-templates.7
|
||||
install -m644 aerc-stylesets.7 $(MANDIR)/man7/aerc-stylesets.7
|
||||
install -m644 config/accounts.conf $(SHAREDIR)/accounts.conf
|
||||
install -m644 aerc.conf $(SHAREDIR)/aerc.conf
|
||||
install -m644 config/binds.conf $(SHAREDIR)/binds.conf
|
||||
|
@ -79,6 +81,7 @@ install: all
|
|||
install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext
|
||||
install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply
|
||||
install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body
|
||||
install -m644 config/default_styleset $(SHAREDIR)/stylesets/default
|
||||
|
||||
RMDIR_IF_EMPTY:=sh -c '\
|
||||
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
|
||||
|
|
|
@ -40,7 +40,7 @@ func (MakeDir) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
aerc.PushStatus("Directory created.", 10*time.Second)
|
||||
acct.Directories().Select(name)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
|
|
|
@ -2,6 +2,7 @@ package account
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
|
@ -41,7 +42,7 @@ func (ViewMessage) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
lib.NewMessageStoreView(msg, store, aerc.DecryptKeys,
|
||||
func(view lib.MessageView, err error) {
|
||||
if err != nil {
|
||||
aerc.PushError(err.Error())
|
||||
aerc.PushError(err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
viewer := widgets.NewMessageViewer(acct, aerc.Config(), view)
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"git.sr.ht/~sircmpwn/aerc/commands"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
|
@ -36,24 +35,23 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
|
||||
path, err := homedir.Expand(path)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
return err
|
||||
}
|
||||
|
||||
pathinfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
return err
|
||||
} else if pathinfo.IsDir() {
|
||||
aerc.PushError("Attachment must be a file, not a directory")
|
||||
aerc.PushError("Attachment must be a file, not a directory", 10*time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||
composer.AddAttachment(path)
|
||||
|
||||
aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
||||
aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Detach struct{}
|
||||
|
@ -44,8 +43,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second).
|
||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
||||
aerc.PushSuccess(fmt.Sprintf("Detached %s", path), 10*time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
go func() {
|
||||
errStr := <-errChan
|
||||
if errStr != "" {
|
||||
aerc.PushError(" " + errStr)
|
||||
aerc.PushError(" "+errStr, 10*time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
ctr := datacounter.NewWriterCounter(ioutil.Discard)
|
||||
err = composer.WriteMessage(header, ctr)
|
||||
if err != nil {
|
||||
aerc.PushError(errors.Wrap(err, "WriteMessage").Error())
|
||||
aerc.PushError(errors.Wrap(err, "WriteMessage").Error(), 10*time.Second)
|
||||
composer.Close()
|
||||
return
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
r.Close()
|
||||
composer.Close()
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
r.Close()
|
||||
composer.Close()
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/google/shlex"
|
||||
"github.com/miolini/datacounter"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -225,8 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
aerc.PushStatus("Sending...", 10*time.Second)
|
||||
nbytes, err := sendAsync()
|
||||
if err != nil {
|
||||
aerc.SetStatus(" "+err.Error()).
|
||||
Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
aerc.SetError(" " + err.Error())
|
||||
return
|
||||
}
|
||||
if config.CopyTo != "" {
|
||||
|
@ -247,7 +245,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
composer.SetSent()
|
||||
composer.Close()
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
r.Close()
|
||||
composer.Close()
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import (
|
|||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type ExecCmd struct{}
|
||||
|
@ -33,16 +31,17 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
go func() {
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
} else {
|
||||
color := tcell.ColorDefault
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
color = tcell.ColorRed
|
||||
aerc.PushError(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
} else {
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
}
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, color)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
|
|
|
@ -86,7 +86,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
case *types.Done:
|
||||
wg.Done()
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
success = false
|
||||
wg.Done()
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
case *types.Done:
|
||||
aerc.PushStatus("Messages copied.", 10*time.Second)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
|
|
|
@ -47,7 +47,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
case *types.Done:
|
||||
aerc.PushStatus("Messages deleted.", 10*time.Second)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -68,7 +68,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
lib.NewMessageStoreView(next, store, aerc.DecryptKeys,
|
||||
func(view lib.MessageView, err error) {
|
||||
if err != nil {
|
||||
aerc.PushError(err.Error())
|
||||
aerc.PushError(err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/models"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
|
@ -83,7 +84,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
|
||||
acct.Worker(), template, defaults, original)
|
||||
if err != nil {
|
||||
aerc.PushError("Error: " + err.Error())
|
||||
aerc.PushError("Error: "+err.Error(), 10*time.Second)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
case *types.Done:
|
||||
aerc.PushStatus("labels updated", 10*time.Second)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
|
|
|
@ -71,7 +71,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
case *types.Done:
|
||||
aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||
|
||||
"git.sr.ht/~sircmpwn/getopt"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Pipe struct{}
|
||||
|
@ -76,7 +75,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
doTerm := func(reader io.Reader, name string) {
|
||||
term, err := commands.QuickTerm(aerc, cmd, reader)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
aerc.NewTab(term, name)
|
||||
|
@ -94,16 +93,17 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
}()
|
||||
err = ecmd.Run()
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
} else {
|
||||
color := tcell.ColorDefault
|
||||
if ecmd.ProcessState.ExitCode() != 0 {
|
||||
color = tcell.ColorRed
|
||||
aerc.PushError(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
} else {
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
}
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, color)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ func submitReadChange(aerc *widgets.Aerc, store *lib.MessageStore,
|
|||
case *types.Done:
|
||||
aerc.PushStatus(msg_success, 10*time.Second)
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ func submitReadChangeWg(aerc *widgets.Aerc, store *lib.MessageStore,
|
|||
case *types.Done:
|
||||
wg.Done()
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
*success = false
|
||||
wg.Done()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package msg
|
|||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
_ "github.com/emersion/go-message/charset"
|
||||
|
@ -91,7 +92,7 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
}, func(msg types.WorkerMessage) {
|
||||
switch msg := msg.(type) {
|
||||
case *types.Error:
|
||||
aerc.PushError(" " + msg.Error.Error())
|
||||
aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
|
||||
composer.Close()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
gomail "net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/getopt"
|
||||
|
||||
|
@ -139,7 +140,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
|
||||
acct.AccountConfig(), acct.Worker(), template, defaults, original)
|
||||
if err != nil {
|
||||
aerc.PushError("Error: " + err.Error())
|
||||
aerc.PushError("Error: "+err.Error(), 10*time.Second)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package msgview
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/commands/account"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
|
@ -40,7 +42,7 @@ func (NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys,
|
||||
func(view lib.MessageView, err error) {
|
||||
if err != nil {
|
||||
aerc.PushError(err.Error())
|
||||
aerc.PushError(err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
|
||||
|
|
|
@ -49,20 +49,20 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
|
||||
tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
_, err = io.Copy(tmpFile, reader)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
err = lib.OpenFile(tmpFile.Name())
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
|
||||
aerc.PushStatus("Opened", 10*time.Second)
|
||||
|
|
|
@ -128,7 +128,7 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
go func() {
|
||||
err := <-ch
|
||||
if err != nil {
|
||||
aerc.PushError(fmt.Sprintf("Save failed: %v", err))
|
||||
aerc.PushError(fmt.Sprintf("Save failed: %v", err), 10*time.Second)
|
||||
return
|
||||
}
|
||||
aerc.PushStatus("Saved to "+path, 10*time.Second)
|
||||
|
|
|
@ -2,6 +2,7 @@ package commands
|
|||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/riywo/loginshell"
|
||||
|
||||
|
@ -46,7 +47,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error {
|
|||
term.OnClose = func(err error) {
|
||||
aerc.RemoveTab(term)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -32,7 +32,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter
|
|||
|
||||
term.OnClose = func(err error) {
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
// remove the tab on error, otherwise it gets stuck
|
||||
aerc.RemoveTab(term)
|
||||
} else {
|
||||
|
@ -56,7 +56,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter
|
|||
|
||||
err := <-status
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,17 @@ sort=
|
|||
# Default: true
|
||||
next-message-on-delete=true
|
||||
|
||||
# The directories where the stylesets are stored. It takes a colon-separated
|
||||
# list of directories.
|
||||
#
|
||||
# default: @SHAREDIR@/stylesets/
|
||||
stylesets-dirs=@SHAREDIR@/stylesets/
|
||||
|
||||
# Sets the styleset to use for the aerc ui elements.
|
||||
#
|
||||
# Default: default
|
||||
styleset-name=default
|
||||
|
||||
[viewer]
|
||||
#
|
||||
# Specifies the pager to use when displaying emails. Note that some filters
|
||||
|
|
|
@ -45,6 +45,9 @@ type UIConfig struct {
|
|||
NextMessageOnDelete bool `ini:"next-message-on-delete"`
|
||||
CompletionDelay time.Duration `ini:"completion-delay"`
|
||||
CompletionPopovers bool `ini:"completion-popovers"`
|
||||
StyleSetDirs []string `ini:"stylesets-dirs", delim:":"`
|
||||
StyleSetName string `ini:"styleset-name"`
|
||||
style StyleSet
|
||||
}
|
||||
|
||||
type ContextType int
|
||||
|
@ -332,6 +335,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
|||
if err := ui.MapTo(&config.Ui); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stylesetsDirs := ui.Key("stylesets-dirs").String()
|
||||
if stylesetsDirs != "" {
|
||||
config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":")
|
||||
}
|
||||
}
|
||||
for _, sectionName := range file.SectionStrings() {
|
||||
if !strings.Contains(sectionName, "ui:") {
|
||||
|
@ -346,6 +354,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
|||
if err := uiSection.MapTo(&uiSubConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
stylesetsDirs := uiSection.Key("stylesets-dirs").String()
|
||||
if stylesetsDirs != "" {
|
||||
uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":")
|
||||
}
|
||||
contextualUi :=
|
||||
UIConfigContext{
|
||||
UiConfig: uiSubConfig,
|
||||
|
@ -406,6 +418,19 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Ui.loadStyleSet(
|
||||
config.Ui.StyleSetDirs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx, _ := range config.ContextualUis {
|
||||
if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
|
||||
config.Ui.StyleSetDirs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -466,6 +491,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
NextMessageOnDelete: true,
|
||||
CompletionDelay: 250 * time.Millisecond,
|
||||
CompletionPopovers: true,
|
||||
StyleSetDirs: []string{path.Join(sharedir, "stylesets")},
|
||||
StyleSetName: "default",
|
||||
},
|
||||
|
||||
ContextualUis: []UIConfigContext{},
|
||||
|
@ -495,6 +522,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
Forwards: "forward_as_body",
|
||||
},
|
||||
}
|
||||
|
||||
// These bindings are not configurable
|
||||
config.Bindings.AccountWizard.ExKey = KeyStroke{
|
||||
Key: tcell.KeyCtrlE,
|
||||
|
@ -505,6 +533,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
if err = config.LoadConfig(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ui, err := file.GetSection("general"); err == nil {
|
||||
if err := ui.MapTo(&config.General); err != nil {
|
||||
return nil, err
|
||||
|
@ -612,8 +641,17 @@ func parseLayout(layout string) [][]string {
|
|||
return l
|
||||
}
|
||||
|
||||
func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
||||
contextType ContextType, s string) {
|
||||
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
|
||||
ui.style = NewStyleSet()
|
||||
if err := ui.style.ParseStyleSet(ui.StyleSetName, styleSetDirs); err != nil {
|
||||
return fmt.Errorf("Error whie parsing styleset \"%s\": %s", ui.StyleSetName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config AercConfig) mergeContextualUi(baseUi UIConfig,
|
||||
contextType ContextType, s string) UIConfig {
|
||||
for _, contextualUi := range config.ContextualUis {
|
||||
if contextualUi.ContextType != contextType {
|
||||
continue
|
||||
|
@ -623,17 +661,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
|||
continue
|
||||
}
|
||||
|
||||
mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
||||
baseUi := config.Ui
|
||||
|
||||
for k, v := range params {
|
||||
config.mergeContextualUi(&baseUi, k, v)
|
||||
mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
|
||||
if contextualUi.UiConfig.StyleSetName != "" {
|
||||
baseUi.style = contextualUi.UiConfig.style
|
||||
}
|
||||
return baseUi
|
||||
}
|
||||
|
||||
return baseUi
|
||||
}
|
||||
|
||||
func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
||||
baseUi := config.Ui
|
||||
|
||||
for k, v := range params {
|
||||
baseUi = config.mergeContextualUi(baseUi, k, v)
|
||||
}
|
||||
|
||||
return baseUi
|
||||
}
|
||||
|
||||
func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
|
||||
return uiConfig.style.Get(so)
|
||||
}
|
||||
|
||||
func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
|
||||
return uiConfig.style.Selected(so)
|
||||
}
|
||||
|
|
33
config/default_styleset
Normal file
33
config/default_styleset
Normal 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
372
config/style.go
Normal 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()
|
||||
}
|
|
@ -173,6 +173,20 @@ These options are configured in the *[ui]* section of aerc.conf.
|
|||
|
||||
Default: 250ms
|
||||
|
||||
*stylesets-dirs*
|
||||
The directories where the stylesets are stored. The config takes a
|
||||
colon-seperated list of dirs.
|
||||
|
||||
Default: "/usr/share/aerc/stylesets"
|
||||
|
||||
*styleset-name*
|
||||
The name of the styleset to be used to style the ui elements. The
|
||||
stylesets are stored in the 'stylesets' directory in the config
|
||||
directory.
|
||||
|
||||
Default: default
|
||||
|
||||
|
||||
## Contextual UI Configuration
|
||||
|
||||
The UI configuration can be specialized for accounts, specific mail
|
||||
|
|
189
doc/aerc-stylesets.7.scd
Normal file
189
doc/aerc-stylesets.7.scd
Normal 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"
|
||||
|
|
@ -2,6 +2,8 @@ package ui
|
|||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -16,12 +18,15 @@ type Bordered struct {
|
|||
borders uint
|
||||
content Drawable
|
||||
onInvalidate func(d Drawable)
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func NewBordered(content Drawable, borders uint) *Bordered {
|
||||
func NewBordered(
|
||||
content Drawable, borders uint, uiConfig config.UIConfig) *Bordered {
|
||||
b := &Bordered{
|
||||
borders: borders,
|
||||
content: content,
|
||||
borders: borders,
|
||||
content: content,
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
content.OnInvalidate(b.contentInvalidated)
|
||||
return b
|
||||
|
@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) {
|
|||
y := 0
|
||||
width := ctx.Width()
|
||||
height := ctx.Height()
|
||||
style := tcell.StyleDefault.Reverse(true)
|
||||
style := bordered.uiConfig.GetStyle(config.STYLE_BORDER)
|
||||
if bordered.borders&BORDER_LEFT != 0 {
|
||||
ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
|
||||
x += 1
|
||||
|
|
|
@ -3,16 +3,19 @@ package ui
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Stack struct {
|
||||
children []Drawable
|
||||
onInvalidate []func(d Drawable)
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func NewStack() *Stack {
|
||||
return &Stack{}
|
||||
func NewStack(uiConfig config.UIConfig) *Stack {
|
||||
return &Stack{uiConfig: uiConfig}
|
||||
}
|
||||
|
||||
func (stack *Stack) Children() []Drawable {
|
||||
|
@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) {
|
|||
if len(stack.children) > 0 {
|
||||
stack.Peek().Draw(ctx)
|
||||
} else {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
stack.uiConfig.GetStyle(config.STYLE_STACK))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) {
|
|||
func (strip *TabStrip) Draw(ctx *Context) {
|
||||
x := 0
|
||||
for i, tab := range strip.Tabs {
|
||||
style := tcell.StyleDefault.Reverse(true)
|
||||
style := strip.uiConfig.GetStyle(config.STYLE_TAB)
|
||||
if strip.Selected == i {
|
||||
style = tcell.StyleDefault
|
||||
style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB)
|
||||
}
|
||||
tabWidth := 32
|
||||
if ctx.Width()-x < tabWidth {
|
||||
|
@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) {
|
|||
break
|
||||
}
|
||||
}
|
||||
style := tcell.StyleDefault.Reverse(true)
|
||||
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
|
||||
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
|
||||
strip.uiConfig.GetStyle(config.STYLE_TAB))
|
||||
}
|
||||
|
||||
func (strip *TabStrip) Invalidate() {
|
||||
|
@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) {
|
|||
if content.Selected >= len(content.Tabs) {
|
||||
width := ctx.Width()
|
||||
height := ctx.Height()
|
||||
ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, width, height, ' ',
|
||||
content.uiConfig.GetStyle(config.STYLE_TAB))
|
||||
}
|
||||
|
||||
tab := content.Tabs[content.Selected]
|
||||
|
|
|
@ -15,17 +15,13 @@ type Text struct {
|
|||
Invalidatable
|
||||
text string
|
||||
strategy uint
|
||||
fg tcell.Color
|
||||
bg tcell.Color
|
||||
bold bool
|
||||
reverse bool
|
||||
style tcell.Style
|
||||
}
|
||||
|
||||
func NewText(text string) *Text {
|
||||
func NewText(text string, style tcell.Style) *Text {
|
||||
return &Text{
|
||||
bg: tcell.ColorDefault,
|
||||
fg: tcell.ColorDefault,
|
||||
text: text,
|
||||
text: text,
|
||||
style: style,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text {
|
|||
return t
|
||||
}
|
||||
|
||||
func (t *Text) Bold(bold bool) *Text {
|
||||
t.bold = bold
|
||||
t.Invalidate()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text {
|
||||
t.fg = fg
|
||||
t.bg = bg
|
||||
t.Invalidate()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Text) Reverse(reverse bool) *Text {
|
||||
t.reverse = reverse
|
||||
t.Invalidate()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Text) Draw(ctx *Context) {
|
||||
size := runewidth.StringWidth(t.text)
|
||||
x := 0
|
||||
|
@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) {
|
|||
if t.strategy == TEXT_RIGHT {
|
||||
x = ctx.Width() - size
|
||||
}
|
||||
style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg)
|
||||
if t.bold {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
if t.reverse {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(x, 0, style, "%s", t.text)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style)
|
||||
ctx.Printf(x, 0, t.style, "%s", t.text)
|
||||
}
|
||||
|
||||
func (t *Text) Invalidate() {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
)
|
||||
|
||||
// TODO: Attach history providers
|
||||
|
@ -27,16 +29,18 @@ type TextInput struct {
|
|||
completeIndex int
|
||||
completeDelay time.Duration
|
||||
completeDebouncer *time.Timer
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
// Creates a new TextInput. TextInputs will render a "textbox" in the entire
|
||||
// context they're given, and process keypresses to build a string from user
|
||||
// input.
|
||||
func NewTextInput(text string) *TextInput {
|
||||
func NewTextInput(text string, ui config.UIConfig) *TextInput {
|
||||
return &TextInput{
|
||||
cells: -1,
|
||||
text: []rune(text),
|
||||
index: len([]rune(text)),
|
||||
cells: -1,
|
||||
text: []rune(text),
|
||||
index: len([]rune(text)),
|
||||
uiConfig: ui,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) {
|
|||
ti.ensureScroll()
|
||||
}
|
||||
ti.ctx = ctx // gross
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
|
||||
defaultStyle := ti.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||
|
||||
text := ti.text[scroll:]
|
||||
sindex := ti.index - scroll
|
||||
if ti.password {
|
||||
x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt)
|
||||
x := ctx.Printf(0, 0, defaultStyle, "%s", ti.prompt)
|
||||
cells := runewidth.StringWidth(string(text))
|
||||
ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault)
|
||||
ctx.Fill(x, 0, cells, 1, '*', defaultStyle)
|
||||
} else {
|
||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(text))
|
||||
ctx.Printf(0, 0, defaultStyle, "%s%s", ti.prompt, string(text))
|
||||
}
|
||||
cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
|
||||
if ti.focus {
|
||||
|
@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
|
|||
ti.Set(stem + ti.StringRight())
|
||||
ti.Invalidate()
|
||||
},
|
||||
uiConfig: ti.uiConfig,
|
||||
}
|
||||
width := maxLen(ti.completions) + 3
|
||||
height := len(ti.completions)
|
||||
|
@ -353,6 +360,7 @@ type completions struct {
|
|||
onSelect func(int)
|
||||
onExec func()
|
||||
onStem func(string)
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func maxLen(ss []string) int {
|
||||
|
@ -367,10 +375,10 @@ func maxLen(ss []string) int {
|
|||
}
|
||||
|
||||
func (c *completions) Draw(ctx *Context) {
|
||||
bg := tcell.StyleDefault
|
||||
sel := tcell.StyleDefault.Reverse(true)
|
||||
gutter := tcell.StyleDefault
|
||||
pill := tcell.StyleDefault.Reverse(true)
|
||||
bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT)
|
||||
gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
|
||||
pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL)
|
||||
sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT)
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/go-ini/ini"
|
||||
|
@ -75,21 +76,21 @@ type AccountWizard struct {
|
|||
|
||||
func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||
wizard := &AccountWizard{
|
||||
accountName: ui.NewTextInput("").Prompt("> "),
|
||||
accountName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
aerc: aerc,
|
||||
conf: conf,
|
||||
temporary: false,
|
||||
copySent: true,
|
||||
email: ui.NewTextInput("").Prompt("> "),
|
||||
fullName: ui.NewTextInput("").Prompt("> "),
|
||||
imapPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
||||
imapServer: ui.NewTextInput("").Prompt("> "),
|
||||
imapStr: ui.NewText("imaps://"),
|
||||
imapUsername: ui.NewTextInput("").Prompt("> "),
|
||||
smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
||||
smtpServer: ui.NewTextInput("").Prompt("> "),
|
||||
smtpStr: ui.NewText("smtps://"),
|
||||
smtpUsername: ui.NewTextInput("").Prompt("> "),
|
||||
email: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
fullName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||
imapServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
imapStr: ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||
imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||
smtpServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
smtpStr: ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||
smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
}
|
||||
|
||||
// Autofill some stuff for the user
|
||||
|
@ -150,33 +151,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
basics.AddChild(
|
||||
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" +
|
||||
"This wizard supports basic IMAP & SMTP configuration.\n" +
|
||||
"For other configurations, use <Ctrl+q> to exit and read the " +
|
||||
"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>."))
|
||||
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+
|
||||
"This wizard supports basic IMAP & SMTP configuration.\n"+
|
||||
"For other configurations, use <Ctrl+q> to exit and read the "+
|
||||
"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>.",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
basics.AddChild(
|
||||
ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')").
|
||||
Bold(true)).
|
||||
ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(1, 0)
|
||||
basics.AddChild(wizard.accountName).
|
||||
At(2, 0)
|
||||
basics.AddChild(ui.NewFill(' ')).
|
||||
At(3, 0)
|
||||
basics.AddChild(
|
||||
ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')").
|
||||
Bold(true)).
|
||||
ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(4, 0)
|
||||
basics.AddChild(wizard.fullName).
|
||||
At(5, 0)
|
||||
basics.AddChild(ui.NewFill(' ')).
|
||||
At(6, 0)
|
||||
basics.AddChild(
|
||||
ui.NewText("Your email address? (e.g. 'john@example.org')").Bold(true)).
|
||||
ui.NewText("Your email address? (e.g. 'john@example.org')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(7, 0)
|
||||
basics.AddChild(wizard.email).
|
||||
At(8, 0)
|
||||
selecter := NewSelecter([]string{"Next"}, 0).
|
||||
selecter := NewSelecter([]string{"Next"}, 0, conf.Ui).
|
||||
OnChoose(func(option string) {
|
||||
email := wizard.email.String()
|
||||
if strings.ContainsRune(email, '@') {
|
||||
|
@ -227,16 +231,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
}).Columns([]ui.GridSpec{
|
||||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)"))
|
||||
incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
incoming.AddChild(
|
||||
ui.NewText("Username").Bold(true)).
|
||||
ui.NewText("Username",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(1, 0)
|
||||
incoming.AddChild(wizard.imapUsername).
|
||||
At(2, 0)
|
||||
incoming.AddChild(ui.NewFill(' ')).
|
||||
At(3, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Password").Bold(true)).
|
||||
ui.NewText("Password",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(4, 0)
|
||||
incoming.AddChild(wizard.imapPassword).
|
||||
At(5, 0)
|
||||
|
@ -244,20 +251,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
At(6, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Server address "+
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(7, 0)
|
||||
incoming.AddChild(wizard.imapServer).
|
||||
At(8, 0)
|
||||
incoming.AddChild(ui.NewFill(' ')).
|
||||
At(9, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Connection mode").Bold(true)).
|
||||
ui.NewText("Connection mode",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(10, 0)
|
||||
imapMode := NewSelecter([]string{
|
||||
"IMAP over SSL/TLS",
|
||||
"IMAP with STARTTLS",
|
||||
"Insecure IMAP",
|
||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
||||
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||
switch option {
|
||||
case "IMAP over SSL/TLS":
|
||||
wizard.imapMode = IMAP_OVER_TLS
|
||||
|
@ -269,7 +278,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
wizard.imapUri()
|
||||
})
|
||||
incoming.AddChild(imapMode).At(11, 0)
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||
OnChoose(wizard.advance)
|
||||
incoming.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||
incoming.AddChild(wizard.imapStr).At(13, 0)
|
||||
|
@ -304,16 +313,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
}).Columns([]ui.GridSpec{
|
||||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)"))
|
||||
outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Username").Bold(true)).
|
||||
ui.NewText("Username",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(1, 0)
|
||||
outgoing.AddChild(wizard.smtpUsername).
|
||||
At(2, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).
|
||||
At(3, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Password").Bold(true)).
|
||||
ui.NewText("Password",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(4, 0)
|
||||
outgoing.AddChild(wizard.smtpPassword).
|
||||
At(5, 0)
|
||||
|
@ -321,20 +333,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
At(6, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Server address "+
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(7, 0)
|
||||
outgoing.AddChild(wizard.smtpServer).
|
||||
At(8, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).
|
||||
At(9, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Connection mode").Bold(true)).
|
||||
ui.NewText("Connection mode",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(10, 0)
|
||||
smtpMode := NewSelecter([]string{
|
||||
"SMTP over SSL/TLS",
|
||||
"SMTP with STARTTLS",
|
||||
"Insecure SMTP",
|
||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
||||
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||
switch option {
|
||||
case "SMTP over SSL/TLS":
|
||||
wizard.smtpMode = SMTP_OVER_TLS
|
||||
|
@ -346,15 +360,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
wizard.smtpUri()
|
||||
})
|
||||
outgoing.AddChild(smtpMode).At(11, 0)
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||
OnChoose(wizard.advance)
|
||||
outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||
outgoing.AddChild(wizard.smtpStr).At(13, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)).
|
||||
At(15, 0)
|
||||
copySent := NewSelecter([]string{"Yes", "No"}, 0).
|
||||
ui.NewText("Copy sent messages to 'Sent' folder?",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
|
||||
copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
|
||||
Chooser(true).OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Yes":
|
||||
|
@ -380,15 +394,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
complete.AddChild(ui.NewText(
|
||||
"\nConfiguration complete!\n\n" +
|
||||
"You can go back and double check your settings, or choose 'Finish' to\n" +
|
||||
"save your settings to accounts.conf.\n\n" +
|
||||
"To add another account in the future, run ':new-account'."))
|
||||
"\nConfiguration complete!\n\n"+
|
||||
"You can go back and double check your settings, or choose 'Finish' to\n"+
|
||||
"save your settings to accounts.conf.\n\n"+
|
||||
"To add another account in the future, run ':new-account'.",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
selecter = NewSelecter([]string{
|
||||
"Previous",
|
||||
"Finish & open tutorial",
|
||||
"Finish",
|
||||
}, 1).OnChoose(func(option string) {
|
||||
}, 1, conf.Ui).OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Previous":
|
||||
wizard.advance("Previous")
|
||||
|
@ -414,7 +429,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {
|
|||
|
||||
func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
|
||||
if d == nil {
|
||||
wizard.aerc.PushError(" " + err.Error())
|
||||
wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
wizard.Invalidate()
|
||||
return
|
||||
}
|
||||
|
@ -429,7 +444,7 @@ func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
|
|||
wizard.step = step
|
||||
wizard.focus = focus
|
||||
wizard.Focus(true)
|
||||
wizard.aerc.PushError(" " + err.Error())
|
||||
wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
wizard.Invalidate()
|
||||
return
|
||||
}
|
||||
|
@ -540,7 +555,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
|
|||
term.OnClose = func(err error) {
|
||||
wizard.aerc.RemoveTab(term)
|
||||
if err != nil {
|
||||
wizard.aerc.PushError(" " + err.Error())
|
||||
wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
|
||||
|
@ -54,8 +55,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
|
|||
|
||||
worker, err := worker.NewWorker(acct.Source, logger)
|
||||
if err != nil {
|
||||
host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)).
|
||||
Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
|
||||
return &AccountView{
|
||||
acct: acct,
|
||||
aerc: aerc,
|
||||
|
@ -67,7 +67,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
|
|||
|
||||
dirlist := NewDirectoryList(conf, acct, logger, worker)
|
||||
if acctUiConf.SidebarWidth > 0 {
|
||||
grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT))
|
||||
grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT, acctUiConf))
|
||||
}
|
||||
|
||||
msglist := NewMessageList(conf, logger, aerc)
|
||||
|
@ -280,8 +280,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
|
|||
acct.labels = msg.Labels
|
||||
case *types.Error:
|
||||
acct.logger.Printf("%v", msg.Error)
|
||||
acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)).
|
||||
Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
acct.host.SetError(fmt.Sprintf("%v", msg.Error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,7 +290,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
|
|||
}
|
||||
criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
|
||||
if err != nil {
|
||||
acct.aerc.PushError(" ui.sort: " + err.Error())
|
||||
acct.aerc.PushError(" ui.sort: "+err.Error(), 10*time.Second)
|
||||
return nil
|
||||
}
|
||||
return criteria
|
||||
|
|
|
@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
|||
|
||||
tabs := ui.NewTabs(&conf.Ui)
|
||||
|
||||
statusbar := ui.NewStack()
|
||||
statusline := NewStatusLine()
|
||||
statusbar := ui.NewStack(conf.Ui)
|
||||
statusline := NewStatusLine(conf.Ui)
|
||||
statusbar.Push(statusline)
|
||||
|
||||
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
||||
|
@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
|||
logger: logger,
|
||||
statusbar: statusbar,
|
||||
statusline: statusline,
|
||||
prompts: ui.NewStack(),
|
||||
prompts: ui.NewStack(conf.Ui),
|
||||
tabs: tabs,
|
||||
}
|
||||
|
||||
|
@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage {
|
|||
return aerc.statusline.Set(status)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) SetError(status string) *StatusMessage {
|
||||
return aerc.statusline.SetError(status)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
|
||||
return aerc.statusline.Push(text, expiry)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushError(text string) {
|
||||
aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
func (aerc *Aerc) PushError(text string, expiry time.Duration) *StatusMessage {
|
||||
return aerc.statusline.PushError(text, expiry)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushSuccess(text string, expiry time.Duration) *StatusMessage {
|
||||
return aerc.statusline.PushSuccess(text, expiry)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) focus(item ui.Interactive) {
|
||||
|
@ -416,11 +424,11 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
|
|||
exline := NewExLine(aerc.conf, cmd, func(cmd string) {
|
||||
parts, err := shlex.Split(cmd)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
err = aerc.cmd(parts)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
// only add to history if this is an unsimulated command,
|
||||
// ie one not executed from a keybinding
|
||||
|
@ -444,7 +452,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
|
|||
}
|
||||
err := aerc.cmd(cmd)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
}, func(cmd string) []string {
|
||||
return nil // TODO: completions
|
||||
|
@ -471,7 +479,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
|
|||
}
|
||||
err := aerc.cmd(cmd)
|
||||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
aerc.PushError(" "+err.Error(), 10*time.Second)
|
||||
}
|
||||
}, func(cmd string) []string {
|
||||
return nil // TODO: completions
|
||||
|
@ -552,11 +560,10 @@ func (aerc *Aerc) CloseDialog() {
|
|||
return
|
||||
}
|
||||
|
||||
|
||||
func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
|
||||
chText = make(chan string, 1)
|
||||
chErr = make(chan error, 1)
|
||||
getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) {
|
||||
getPasswd := NewGetPasswd(title, prompt, aerc.conf, func(pw string, err error) {
|
||||
defer func() {
|
||||
close(chErr)
|
||||
close(chText)
|
||||
|
|
|
@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
|||
|
||||
templateData := templates.ParseTemplateData(defaults, original)
|
||||
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
||||
aerc.PushError(fmt.Sprintf("could not complete header: %v", err))
|
||||
aerc.PushError(
|
||||
fmt.Sprintf("could not complete header: %v", err), 10*time.Second)
|
||||
worker.Logger.Printf("could not complete header: %v", err)
|
||||
}, aerc.Logger())
|
||||
layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults)
|
||||
layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
|
||||
|
||||
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
||||
if err != nil {
|
||||
|
@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
|||
return c, nil
|
||||
}
|
||||
|
||||
func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
||||
func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
|
||||
defaults map[string]string) (
|
||||
newLayout HeaderLayout,
|
||||
editors map[string]*headerEditor,
|
||||
focusable []ui.MouseableDrawableInteractive,
|
||||
) {
|
||||
layout := conf.Compose.HeaderLayout
|
||||
layout := aerc.conf.Compose.HeaderLayout
|
||||
editors = make(map[string]*headerEditor)
|
||||
focusable = make([]ui.MouseableDrawableInteractive, 0)
|
||||
|
||||
for _, row := range layout {
|
||||
for _, h := range row {
|
||||
e := newHeaderEditor(h, "")
|
||||
if conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||
if aerc.conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||
}
|
||||
editors[h] = e
|
||||
switch h {
|
||||
|
@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
|||
for _, h := range []string{"Cc", "Bcc"} {
|
||||
if val, ok := defaults[h]; ok && val != "" {
|
||||
if _, ok := editors[h]; !ok {
|
||||
e := newHeaderEditor(h, "")
|
||||
if conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||
if aerc.conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||
}
|
||||
editors[h] = e
|
||||
focusable = append(focusable, e)
|
||||
|
@ -259,7 +260,9 @@ func (c *Composer) readSignatureFromFile() []byte {
|
|||
}
|
||||
signature, err := ioutil.ReadFile(sigFile)
|
||||
if err != nil {
|
||||
c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile))
|
||||
c.aerc.PushError(
|
||||
fmt.Sprintf(" Error loading signature from file: %v", sigFile),
|
||||
10*time.Second)
|
||||
return nil
|
||||
}
|
||||
return signature
|
||||
|
@ -648,7 +651,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
|||
}
|
||||
return
|
||||
}
|
||||
e := newHeaderEditor(header, value)
|
||||
e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
|
||||
if c.config.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
|
||||
}
|
||||
|
@ -702,23 +705,27 @@ func (c *Composer) reloadEmail() error {
|
|||
}
|
||||
|
||||
type headerEditor struct {
|
||||
name string
|
||||
focused bool
|
||||
input *ui.TextInput
|
||||
name string
|
||||
focused bool
|
||||
input *ui.TextInput
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func newHeaderEditor(name string, value string) *headerEditor {
|
||||
func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
|
||||
return &headerEditor{
|
||||
input: ui.NewTextInput(value),
|
||||
name: name,
|
||||
input: ui.NewTextInput(value, uiConfig),
|
||||
name: name,
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (he *headerEditor) Draw(ctx *ui.Context) {
|
||||
name := he.name + " "
|
||||
size := runewidth.StringWidth(name)
|
||||
ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name)
|
||||
defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||
ctx.Fill(0, 0, size, ctx.Height(), ' ', defaultStyle)
|
||||
ctx.Printf(0, 0, headerStyle, "%s", name)
|
||||
he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
|
||||
}
|
||||
|
||||
|
@ -779,21 +786,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
|||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
|
||||
uiConfig := composer.config.Ui
|
||||
|
||||
if err != nil {
|
||||
grid.AddChild(ui.NewText(err.Error()).
|
||||
Color(tcell.ColorRed, tcell.ColorDefault))
|
||||
grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0)
|
||||
grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR)))
|
||||
grid.AddChild(ui.NewText("Press [q] to close this tab.",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0)
|
||||
} else {
|
||||
// TODO: source this from actual keybindings?
|
||||
grid.AddChild(ui.NewText(
|
||||
"Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0)
|
||||
grid.AddChild(ui.NewText("Attachments:").
|
||||
Reverse(true)).At(1, 0)
|
||||
grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0)
|
||||
grid.AddChild(ui.NewText("Attachments:",
|
||||
uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0)
|
||||
if len(composer.attachments) == 0 {
|
||||
grid.AddChild(ui.NewText("(none)")).At(2, 0)
|
||||
grid.AddChild(ui.NewText("(none)",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(2, 0)
|
||||
} else {
|
||||
for i, a := range composer.attachments {
|
||||
grid.AddChild(ui.NewText(a)).At(i+2, 0)
|
||||
grid.AddChild(ui.NewText(a, uiConfig.GetStyle(config.STYLE_DEFAULT))).
|
||||
At(i+2, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -194,7 +194,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string {
|
|||
}
|
||||
|
||||
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT))
|
||||
|
||||
if dirlist.spinner.IsRunning() {
|
||||
dirlist.spinner.Draw(ctx)
|
||||
|
@ -202,7 +203,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
|||
}
|
||||
|
||||
if len(dirlist.dirs) == 0 {
|
||||
style := tcell.StyleDefault
|
||||
style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
|
||||
ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
|
||||
return
|
||||
}
|
||||
|
@ -212,12 +213,9 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
|||
if row >= ctx.Height() {
|
||||
break
|
||||
}
|
||||
style := tcell.StyleDefault
|
||||
style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
|
||||
if name == dirlist.selected {
|
||||
style = style.Reverse(true)
|
||||
} else if name == dirlist.selecting {
|
||||
style = style.Reverse(true)
|
||||
style = style.Foreground(tcell.ColorGray)
|
||||
style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT)
|
||||
}
|
||||
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
||||
|
||||
|
|
|
@ -15,13 +15,14 @@ type ExLine struct {
|
|||
tabcomplete func(cmd string) []string
|
||||
cmdHistory lib.History
|
||||
input *ui.TextInput
|
||||
conf *config.AercConfig
|
||||
}
|
||||
|
||||
func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(),
|
||||
tabcomplete func(cmd string) []string,
|
||||
cmdHistory lib.History) *ExLine {
|
||||
|
||||
input := ui.NewTextInput("").Prompt(":").Set(cmd)
|
||||
input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd)
|
||||
if conf.Ui.CompletionPopovers {
|
||||
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
||||
}
|
||||
|
@ -31,6 +32,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
|
|||
tabcomplete: tabcomplete,
|
||||
cmdHistory: cmdHistory,
|
||||
input: input,
|
||||
conf: conf,
|
||||
}
|
||||
input.OnInvalidate(func(d ui.Drawable) {
|
||||
exline.Invalidate()
|
||||
|
@ -41,7 +43,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
|
|||
func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string),
|
||||
tabcomplete func(cmd string) []string) *ExLine {
|
||||
|
||||
input := ui.NewTextInput("").Prompt(prompt)
|
||||
input := ui.NewTextInput("", conf.Ui).Prompt(prompt)
|
||||
if conf.Ui.CompletionPopovers {
|
||||
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/gdamore/tcell"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||
)
|
||||
|
||||
|
@ -14,14 +15,16 @@ type GetPasswd struct {
|
|||
title string
|
||||
prompt string
|
||||
input *ui.TextInput
|
||||
conf *config.AercConfig
|
||||
}
|
||||
|
||||
func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPasswd {
|
||||
func NewGetPasswd(title string, prompt string, conf *config.AercConfig, cb func(string, error)) *GetPasswd {
|
||||
getpasswd := &GetPasswd{
|
||||
callback: cb,
|
||||
title: title,
|
||||
prompt: prompt,
|
||||
input: ui.NewTextInput("").Password(true).Prompt("Password: "),
|
||||
conf: conf,
|
||||
input: ui.NewTextInput("", conf.Ui).Password(true).Prompt("Password: "),
|
||||
}
|
||||
getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
|
||||
getpasswd.Invalidate()
|
||||
|
@ -31,10 +34,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw
|
|||
}
|
||||
|
||||
func (gp *GetPasswd) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true))
|
||||
ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
|
||||
ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt)
|
||||
defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT)
|
||||
titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE)
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle)
|
||||
ctx.Printf(1, 0, titleStyle, "%s", gp.title)
|
||||
ctx.Printf(1, 1, defaultStyle, gp.prompt)
|
||||
gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package widgets
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
@ -49,7 +50,8 @@ func (ml *MessageList) Invalidate() {
|
|||
|
||||
func (ml *MessageList) Draw(ctx *ui.Context) {
|
||||
ml.height = ctx.Height()
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
ml.aerc.SelectedAccount().UiConfig().GetStyle(config.STYLE_MSGLIST_DEFAULT))
|
||||
|
||||
store := ml.Store()
|
||||
if store == nil {
|
||||
|
@ -84,34 +86,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
|
|||
continue
|
||||
}
|
||||
|
||||
style := tcell.StyleDefault
|
||||
|
||||
// current row
|
||||
if row == ml.store.SelectedIndex()-ml.scroll {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
// deleted message
|
||||
if _, ok := store.Deleted[msg.Uid]; ok {
|
||||
style = style.Foreground(tcell.ColorGray)
|
||||
}
|
||||
// unread message
|
||||
seen := false
|
||||
for _, flag := range msg.Flags {
|
||||
if flag == models.SeenFlag {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
|
||||
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
||||
uiConfig := ml.conf.GetUiConfig(map[config.ContextType]string{
|
||||
config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
|
||||
config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
|
||||
config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject,
|
||||
})
|
||||
|
||||
so := config.STYLE_MSGLIST_DEFAULT
|
||||
|
||||
// deleted message
|
||||
if _, ok := store.Deleted[msg.Uid]; ok {
|
||||
so = config.STYLE_MSGLIST_DELETED
|
||||
}
|
||||
// unread message
|
||||
seen := false
|
||||
flaged := false
|
||||
for _, flag := range msg.Flags {
|
||||
switch flag {
|
||||
case models.SeenFlag:
|
||||
seen = true
|
||||
case models.FlaggedFlag:
|
||||
flaged = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
so = config.STYLE_MSGLIST_UNREAD
|
||||
}
|
||||
|
||||
if flaged {
|
||||
so = config.STYLE_MSGLIST_FLAGGED
|
||||
}
|
||||
|
||||
// marked message
|
||||
if store.IsMarked(msg.Uid) {
|
||||
so = config.STYLE_MSGLIST_MARKED
|
||||
}
|
||||
|
||||
style := uiConfig.GetStyle(so)
|
||||
|
||||
// current row
|
||||
if row == ml.store.SelectedIndex()-ml.scroll {
|
||||
style = uiConfig.GetStyleSelected(so)
|
||||
}
|
||||
|
||||
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
||||
fmtStr, args, err := format.ParseMessageFormat(
|
||||
ml.aerc.SelectedAccount().acct.From,
|
||||
uiConfig.IndexFormat,
|
||||
|
@ -168,7 +186,7 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
|
|||
lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys,
|
||||
func(view lib.MessageView, err error) {
|
||||
if err != nil {
|
||||
ml.aerc.PushError(err.Error())
|
||||
ml.aerc.PushError(err.Error(), 10*time.Second)
|
||||
return
|
||||
}
|
||||
viewer := NewMessageViewer(acct, ml.aerc.Config(), view)
|
||||
|
@ -288,7 +306,8 @@ func (ml *MessageList) Scroll() {
|
|||
}
|
||||
|
||||
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
|
||||
msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage
|
||||
uiConfig := ml.aerc.SelectedAccount().UiConfig()
|
||||
msg := uiConfig.EmptyMessage
|
||||
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
|
||||
tcell.StyleDefault, "%s", msg)
|
||||
uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ type MessageViewer struct {
|
|||
grid *ui.Grid
|
||||
switcher *PartSwitcher
|
||||
msg lib.MessageView
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
type PartSwitcher struct {
|
||||
|
@ -61,9 +62,11 @@ func NewMessageViewer(acct *AccountView,
|
|||
header, headerHeight := layout.grid(
|
||||
func(header string) ui.Drawable {
|
||||
return &HeaderView{
|
||||
conf: conf,
|
||||
Name: header,
|
||||
Value: fmtHeader(msg.MessageInfo(), header,
|
||||
acct.UiConfig().TimestampFormat),
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -93,15 +96,16 @@ func NewMessageViewer(acct *AccountView,
|
|||
err := createSwitcher(acct, switcher, conf, msg)
|
||||
if err != nil {
|
||||
return &MessageViewer{
|
||||
err: err,
|
||||
grid: grid,
|
||||
msg: msg,
|
||||
err: err,
|
||||
grid: grid,
|
||||
msg: msg,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
grid.AddChild(header).At(0, 0)
|
||||
if msg.PGPDetails() != nil {
|
||||
grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
|
||||
grid.AddChild(NewPGPInfo(msg.PGPDetails(), acct.UiConfig())).At(1, 0)
|
||||
grid.AddChild(ui.NewFill(' ')).At(2, 0)
|
||||
grid.AddChild(switcher).At(3, 0)
|
||||
} else {
|
||||
|
@ -115,6 +119,7 @@ func NewMessageViewer(acct *AccountView,
|
|||
grid: grid,
|
||||
msg: msg,
|
||||
switcher: switcher,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
switcher.mv = mv
|
||||
|
||||
|
@ -223,8 +228,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher,
|
|||
|
||||
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
||||
if mv.err != nil {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
|
||||
style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, style, "%s", mv.err.Error())
|
||||
return
|
||||
}
|
||||
mv.grid.Draw(ctx)
|
||||
|
@ -346,7 +352,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
|
|||
ps.height = ctx.Height()
|
||||
y := ctx.Height() - height
|
||||
for i, part := range ps.parts {
|
||||
style := tcell.StyleDefault.Reverse(ps.selected == i)
|
||||
style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
if ps.selected == i {
|
||||
style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
|
||||
}
|
||||
ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
|
||||
name := fmt.Sprintf("%s/%s",
|
||||
strings.ToLower(part.part.MIMEType),
|
||||
|
@ -435,6 +444,7 @@ func (mv *MessageViewer) Focus(focus bool) {
|
|||
|
||||
type PartViewer struct {
|
||||
ui.Invalidatable
|
||||
conf *config.AercConfig
|
||||
err error
|
||||
fetched bool
|
||||
filter *exec.Cmd
|
||||
|
@ -449,6 +459,7 @@ type PartViewer struct {
|
|||
term *Terminal
|
||||
selecter *Selecter
|
||||
grid *ui.Grid
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||
|
@ -518,7 +529,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
{ui.SIZE_WEIGHT, 1},
|
||||
})
|
||||
|
||||
selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0).
|
||||
selecter := NewSelecter([]string{"Save message", "Pipe to command"},
|
||||
0, acct.UiConfig()).
|
||||
OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Save message":
|
||||
|
@ -531,6 +543,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
grid.AddChild(selecter).At(2, 0)
|
||||
|
||||
pv := &PartViewer{
|
||||
conf: conf,
|
||||
filter: filter,
|
||||
index: index,
|
||||
msg: msg,
|
||||
|
@ -542,6 +555,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
term: term,
|
||||
selecter: selecter,
|
||||
grid: grid,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
|
||||
if term != nil {
|
||||
|
@ -639,14 +653,16 @@ func (pv *PartViewer) Invalidate() {
|
|||
}
|
||||
|
||||
func (pv *PartViewer) Draw(ctx *ui.Context) {
|
||||
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
styleError := pv.uiConfig.GetStyle(config.STYLE_ERROR)
|
||||
if pv.filter == nil {
|
||||
// TODO: Let them download it directly or something
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, styleError,
|
||||
"No filter configured for this mimetype ('%s/%s')",
|
||||
pv.part.MIMEType, pv.part.MIMESubType,
|
||||
)
|
||||
ctx.Printf(0, 2, tcell.StyleDefault,
|
||||
ctx.Printf(0, 2, style,
|
||||
"You can still :save the message or :pipe it to an external command")
|
||||
pv.selecter.Focus(true)
|
||||
pv.grid.Draw(ctx)
|
||||
|
@ -657,8 +673,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
|
|||
pv.fetched = true
|
||||
}
|
||||
if pv.err != nil {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, style, "%s", pv.err.Error())
|
||||
return
|
||||
}
|
||||
pv.term.Draw(ctx)
|
||||
|
@ -680,8 +696,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
|
|||
|
||||
type HeaderView struct {
|
||||
ui.Invalidatable
|
||||
Name string
|
||||
Value string
|
||||
conf *config.AercConfig
|
||||
Name string
|
||||
Value string
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
||||
|
@ -689,18 +707,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
|
|||
size := runewidth.StringWidth(name)
|
||||
lim := ctx.Width() - size - 1
|
||||
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
||||
var (
|
||||
hstyle tcell.Style
|
||||
vstyle tcell.Style
|
||||
)
|
||||
|
||||
vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||
|
||||
// TODO: Make this more robust and less dumb
|
||||
if hv.Name == "PGP" {
|
||||
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
|
||||
hstyle = tcell.StyleDefault.Bold(true)
|
||||
} else {
|
||||
vstyle = tcell.StyleDefault
|
||||
hstyle = tcell.StyleDefault.Bold(true)
|
||||
vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||
}
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
||||
ctx.Printf(0, 0, hstyle, "%s", name)
|
||||
ctx.Printf(size, 0, vstyle, "%s", value)
|
||||
|
|
|
@ -3,40 +3,40 @@ package widgets
|
|||
import (
|
||||
"errors"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
pgperrors "golang.org/x/crypto/openpgp/errors"
|
||||
)
|
||||
|
||||
type PGPInfo struct {
|
||||
ui.Invalidatable
|
||||
details *openpgp.MessageDetails
|
||||
details *openpgp.MessageDetails
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo {
|
||||
return &PGPInfo{details: details}
|
||||
func NewPGPInfo(details *openpgp.MessageDetails, uiConfig config.UIConfig) *PGPInfo {
|
||||
return &PGPInfo{details: details, uiConfig: uiConfig}
|
||||
}
|
||||
|
||||
func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
|
||||
errorStyle := tcell.StyleDefault.Background(tcell.ColorRed).
|
||||
Foreground(tcell.ColorWhite).Bold(true)
|
||||
softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true)
|
||||
validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
||||
errorStyle := p.uiConfig.GetStyle(config.STYLE_ERROR)
|
||||
warningStyle := p.uiConfig.GetStyle(config.STYLE_WARNING)
|
||||
validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
|
||||
// TODO: Nicer prompt for TOFU, fetch from keyserver, etc
|
||||
if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
|
||||
p.details.SignedBy == nil {
|
||||
|
||||
x := ctx.Printf(0, 0, softErrorStyle, "*")
|
||||
x += ctx.Printf(x, 0, tcell.StyleDefault,
|
||||
x := ctx.Printf(0, 0, warningStyle, "*")
|
||||
x += ctx.Printf(x, 0, defaultStyle,
|
||||
" Signed with unknown key (%8X); authenticity unknown",
|
||||
p.details.SignedByKeyId)
|
||||
} else if p.details.SignatureError != nil {
|
||||
x := ctx.Printf(0, 0, errorStyle, "Invalid signature!")
|
||||
x += ctx.Printf(x, 0, tcell.StyleDefault.
|
||||
Foreground(tcell.ColorRed).Bold(true),
|
||||
x += ctx.Printf(x, 0, errorStyle,
|
||||
" This message may have been tampered with! (%s)",
|
||||
p.details.SignatureError.Error())
|
||||
} else {
|
||||
|
@ -44,24 +44,26 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
|
|||
ident := entity.PrimaryIdentity()
|
||||
|
||||
x := ctx.Printf(0, 0, validStyle, "✓ Authentic ")
|
||||
x += ctx.Printf(x, 0, tcell.StyleDefault,
|
||||
x += ctx.Printf(x, 0, defaultStyle,
|
||||
"Signature from %s (%8X)",
|
||||
ident.Name, p.details.SignedByKeyId)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
|
||||
validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
||||
validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
entity := p.details.DecryptedWith.Entity
|
||||
ident := entity.PrimaryIdentity()
|
||||
|
||||
x := ctx.Printf(0, y, validStyle, "✓ Encrypted ")
|
||||
x += ctx.Printf(x, y, tcell.StyleDefault,
|
||||
x += ctx.Printf(x, y, defaultStyle,
|
||||
"To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
|
||||
}
|
||||
|
||||
func (p *PGPInfo) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||
if p.details.IsSigned && p.details.IsEncrypted {
|
||||
p.DrawSignature(ctx)
|
||||
p.DrawEncryption(ctx, 1)
|
||||
|
|
|
@ -3,24 +3,27 @@ package widgets
|
|||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||
)
|
||||
|
||||
type Selecter struct {
|
||||
ui.Invalidatable
|
||||
chooser bool
|
||||
focused bool
|
||||
focus int
|
||||
options []string
|
||||
chooser bool
|
||||
focused bool
|
||||
focus int
|
||||
options []string
|
||||
uiConfig config.UIConfig
|
||||
|
||||
onChoose func(option string)
|
||||
onSelect func(option string)
|
||||
}
|
||||
|
||||
func NewSelecter(options []string, focus int) *Selecter {
|
||||
func NewSelecter(options []string, focus int, uiConfig config.UIConfig) *Selecter {
|
||||
return &Selecter{
|
||||
focus: focus,
|
||||
options: options,
|
||||
focus: focus,
|
||||
options: options,
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,15 +37,16 @@ func (sel *Selecter) Invalidate() {
|
|||
}
|
||||
|
||||
func (sel *Selecter) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT))
|
||||
x := 2
|
||||
for i, option := range sel.options {
|
||||
style := tcell.StyleDefault
|
||||
style := sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT)
|
||||
if sel.focus == i {
|
||||
if sel.focused {
|
||||
style = style.Reverse(true)
|
||||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_FOCUSED)
|
||||
} else if sel.chooser {
|
||||
style = style.Bold(true)
|
||||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_CHOOSER)
|
||||
}
|
||||
}
|
||||
x += ctx.Printf(x, 1, style, "[%s]", option)
|
||||
|
|
|
@ -16,6 +16,7 @@ type Spinner struct {
|
|||
frame int64 // access via atomic
|
||||
frames []string
|
||||
stop chan struct{}
|
||||
style tcell.Style
|
||||
}
|
||||
|
||||
func NewSpinner(uiConf *config.UIConfig) *Spinner {
|
||||
|
@ -23,6 +24,7 @@ func NewSpinner(uiConf *config.UIConfig) *Spinner {
|
|||
stop: make(chan struct{}),
|
||||
frame: -1,
|
||||
frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter),
|
||||
style: uiConf.GetStyle(config.STYLE_SPINNER),
|
||||
}
|
||||
return &spinner
|
||||
}
|
||||
|
@ -70,9 +72,9 @@ func (s *Spinner) Draw(ctx *ui.Context) {
|
|||
|
||||
cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames)))
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', s.style)
|
||||
col := ctx.Width()/2 - len(s.frames[0])/2 + 1
|
||||
ctx.Printf(col, 0, tcell.StyleDefault, "%s", s.frames[cur])
|
||||
ctx.Printf(col, 0, s.style, "%s", s.frames[cur])
|
||||
}
|
||||
|
||||
func (s *Spinner) Invalidate() {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/gdamore/tcell"
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||
)
|
||||
|
||||
|
@ -14,21 +15,21 @@ type StatusLine struct {
|
|||
stack []*StatusMessage
|
||||
fallback StatusMessage
|
||||
aerc *Aerc
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
type StatusMessage struct {
|
||||
bg tcell.Color
|
||||
fg tcell.Color
|
||||
style tcell.Style
|
||||
message string
|
||||
}
|
||||
|
||||
func NewStatusLine() *StatusLine {
|
||||
func NewStatusLine(uiConfig config.UIConfig) *StatusLine {
|
||||
return &StatusLine{
|
||||
fallback: StatusMessage{
|
||||
bg: tcell.ColorDefault,
|
||||
fg: tcell.ColorDefault,
|
||||
style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||
message: "Idle",
|
||||
},
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,9 +42,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
|
|||
if len(status.stack) != 0 {
|
||||
line = status.stack[len(status.stack)-1]
|
||||
}
|
||||
style := tcell.StyleDefault.
|
||||
Background(line.bg).Foreground(line.fg).Reverse(true)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style)
|
||||
pendingKeys := ""
|
||||
if status.aerc != nil {
|
||||
for _, pendingKey := range status.aerc.pendingKeys {
|
||||
|
@ -51,13 +50,21 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
|
|||
}
|
||||
}
|
||||
message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
|
||||
ctx.Printf(0, 0, style, "%s%s", message, pendingKeys)
|
||||
ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys)
|
||||
}
|
||||
|
||||
func (status *StatusLine) Set(text string) *StatusMessage {
|
||||
status.fallback = StatusMessage{
|
||||
bg: tcell.ColorDefault,
|
||||
fg: tcell.ColorDefault,
|
||||
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||
message: text,
|
||||
}
|
||||
status.Invalidate()
|
||||
return &status.fallback
|
||||
}
|
||||
|
||||
func (status *StatusLine) SetError(text string) *StatusMessage {
|
||||
status.fallback = StatusMessage{
|
||||
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR),
|
||||
message: text,
|
||||
}
|
||||
status.Invalidate()
|
||||
|
@ -66,8 +73,7 @@ func (status *StatusLine) Set(text string) *StatusMessage {
|
|||
|
||||
func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
|
||||
msg := &StatusMessage{
|
||||
bg: tcell.ColorDefault,
|
||||
fg: tcell.ColorDefault,
|
||||
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||
message: text,
|
||||
}
|
||||
status.stack = append(status.stack, msg)
|
||||
|
@ -84,6 +90,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
|
|||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) PushError(text string, expiry time.Duration) *StatusMessage {
|
||||
msg := status.Push(text, expiry)
|
||||
msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR))
|
||||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) PushSuccess(text string, expiry time.Duration) *StatusMessage {
|
||||
msg := status.Push(text, expiry)
|
||||
msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS))
|
||||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) Expire() {
|
||||
status.stack = nil
|
||||
}
|
||||
|
@ -92,7 +110,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) {
|
|||
status.aerc = aerc
|
||||
}
|
||||
|
||||
func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) {
|
||||
msg.bg = bg
|
||||
msg.fg = fg
|
||||
func (msg *StatusMessage) Color(style tcell.Style) {
|
||||
msg.style = style
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ import (
|
|||
type TabHost interface {
|
||||
BeginExCommand(cmd string)
|
||||
SetStatus(status string) *StatusMessage
|
||||
SetError(err string) *StatusMessage
|
||||
PushStatus(text string, expiry time.Duration) *StatusMessage
|
||||
PushError(text string, expiry time.Duration) *StatusMessage
|
||||
PushSuccess(text string, expiry time.Duration) *StatusMessage
|
||||
Beep()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue