Implement style configuration.
Introduce the ability to configure stylesets, allowing customization of aerc's look (color scheme, font weight, etc). Default styleset is installed to /path/to/aerc/stylesets/default.
This commit is contained in:
parent
1bab1754f0
commit
1ff687ca2b
30 changed files with 1040 additions and 292 deletions
7
Makefile
7
Makefile
|
@ -35,7 +35,8 @@ DOCS := \
|
|||
aerc-notmuch.5 \
|
||||
aerc-smtp.5 \
|
||||
aerc-tutorial.7 \
|
||||
aerc-templates.7
|
||||
aerc-templates.7 \
|
||||
aerc-stylesets.7
|
||||
|
||||
.1.scd.1:
|
||||
scdoc < $< > $@
|
||||
|
@ -58,7 +59,7 @@ clean:
|
|||
|
||||
install: all
|
||||
mkdir -m755 -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(MANDIR)/man5 $(DESTDIR)$(MANDIR)/man7 \
|
||||
$(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates
|
||||
$(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates $(DESTDIR)$(SHAREDIR)/stylesets
|
||||
install -m755 aerc $(DESTDIR)$(BINDIR)/aerc
|
||||
install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1
|
||||
install -m644 aerc-search.1 $(DESTDIR)$(MANDIR)/man1/aerc-search.1
|
||||
|
@ -70,6 +71,7 @@ install: all
|
|||
install -m644 aerc-smtp.5 $(DESTDIR)$(MANDIR)/man5/aerc-smtp.5
|
||||
install -m644 aerc-tutorial.7 $(DESTDIR)$(MANDIR)/man7/aerc-tutorial.7
|
||||
install -m644 aerc-templates.7 $(DESTDIR)$(MANDIR)/man7/aerc-templates.7
|
||||
install -m644 aerc-stylesets.7 $(DESTDIR)$(MANDIR)/man7/aerc-stylesets.7
|
||||
install -m644 config/accounts.conf $(DESTDIR)$(SHAREDIR)/accounts.conf
|
||||
install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf
|
||||
install -m644 config/binds.conf $(DESTDIR)$(SHAREDIR)/binds.conf
|
||||
|
@ -78,6 +80,7 @@ install: all
|
|||
install -m755 filters/plaintext $(DESTDIR)$(SHAREDIR)/filters/plaintext
|
||||
install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply
|
||||
install -m644 templates/forward_as_body $(DESTDIR)$(SHAREDIR)/templates/forward_as_body
|
||||
install -m644 config/default_styleset $(DESTDIR)$(SHAREDIR)/stylesets/default
|
||||
|
||||
RMDIR_IF_EMPTY:=sh -c '\
|
||||
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
|
||||
|
|
|
@ -4,11 +4,9 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/commands"
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
|
@ -52,8 +50,7 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||
composer.AddAttachment(path)
|
||||
|
||||
aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
||||
aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,10 +3,8 @@ package compose
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Detach struct{}
|
||||
|
@ -44,8 +42,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second).
|
||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
||||
aerc.PushSuccess(fmt.Sprintf("Detached %s", path))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import (
|
|||
"time"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type ExecCmd struct{}
|
||||
|
@ -35,14 +33,15 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
} else {
|
||||
color := tcell.ColorDefault
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
color = tcell.ColorRed
|
||||
aerc.PushError(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()))
|
||||
} else {
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
}
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", args[0],
|
||||
cmd.ProcessState.ExitCode()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, color)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||
|
||||
"git.sr.ht/~sircmpwn/getopt"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Pipe struct{}
|
||||
|
@ -96,14 +95,15 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
|
|||
if err != nil {
|
||||
aerc.PushError(" " + err.Error())
|
||||
} else {
|
||||
color := tcell.ColorDefault
|
||||
if ecmd.ProcessState.ExitCode() != 0 {
|
||||
color = tcell.ColorRed
|
||||
aerc.PushError(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()))
|
||||
} else {
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()), 10*time.Second)
|
||||
}
|
||||
aerc.PushStatus(fmt.Sprintf(
|
||||
"%s: completed with status %d", cmd[0],
|
||||
ecmd.ProcessState.ExitCode()), 10*time.Second).
|
||||
Color(tcell.ColorDefault, color)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -411,6 +414,19 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Ui.loadStyleSet(
|
||||
config.Ui.StyleSetDirs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx, _ := range config.ContextualUis {
|
||||
if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
|
||||
config.Ui.StyleSetDirs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -471,6 +487,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
NextMessageOnDelete: true,
|
||||
CompletionDelay: 250 * time.Millisecond,
|
||||
CompletionPopovers: true,
|
||||
StyleSetDirs: []string{path.Join(sharedir, "stylesets")},
|
||||
StyleSetName: "default",
|
||||
},
|
||||
|
||||
ContextualUis: []UIConfigContext{},
|
||||
|
@ -500,6 +518,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
Forwards: "forward_as_body",
|
||||
},
|
||||
}
|
||||
|
||||
// These bindings are not configurable
|
||||
config.Bindings.AccountWizard.ExKey = KeyStroke{
|
||||
Key: tcell.KeyCtrlE,
|
||||
|
@ -510,6 +529,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
|||
if err = config.LoadConfig(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ui, err := file.GetSection("general"); err == nil {
|
||||
if err := ui.MapTo(&config.General); err != nil {
|
||||
return nil, err
|
||||
|
@ -617,8 +637,18 @@ func parseLayout(layout string) [][]string {
|
|||
return l
|
||||
}
|
||||
|
||||
func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
||||
contextType ContextType, s string) {
|
||||
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
|
||||
ui.style = NewStyleSet()
|
||||
err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to load default styleset: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config AercConfig) mergeContextualUi(baseUi UIConfig,
|
||||
contextType ContextType, s string) UIConfig {
|
||||
for _, contextualUi := range config.ContextualUis {
|
||||
if contextualUi.ContextType != contextType {
|
||||
continue
|
||||
|
@ -628,17 +658,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
|||
continue
|
||||
}
|
||||
|
||||
mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
||||
baseUi := config.Ui
|
||||
|
||||
for k, v := range params {
|
||||
config.mergeContextualUi(&baseUi, k, v)
|
||||
mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
|
||||
if contextualUi.UiConfig.StyleSetName != "" {
|
||||
baseUi.style = contextualUi.UiConfig.style
|
||||
}
|
||||
return baseUi
|
||||
}
|
||||
|
||||
return baseUi
|
||||
}
|
||||
|
||||
func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
||||
baseUi := config.Ui
|
||||
|
||||
for k, v := range params {
|
||||
baseUi = config.mergeContextualUi(baseUi, k, v)
|
||||
}
|
||||
|
||||
return baseUi
|
||||
}
|
||||
|
||||
func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
|
||||
return uiConfig.style.Get(so)
|
||||
}
|
||||
|
||||
func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
|
||||
return uiConfig.style.Selected(so)
|
||||
}
|
||||
|
|
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.bold=true
|
||||
error.fg=red
|
||||
warning.fg=yellow
|
||||
success.fg=green
|
||||
|
||||
statusline*.default=true
|
||||
statusline_default.reverse=true
|
||||
statusline_error.fg=red
|
||||
statusline_error.reverse=true
|
||||
|
||||
msglist_unread.bold=true
|
||||
|
||||
completion_pill.reverse=true
|
||||
|
||||
tab.reverse=true
|
||||
border.reverse = true
|
||||
|
||||
selector_focused.reverse=true
|
||||
selector_chooser.bold=true
|
379
config/style.go
Normal file
379
config/style.go
Normal file
|
@ -0,0 +1,379 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/go-ini/ini"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
type StyleObject int32
|
||||
|
||||
const (
|
||||
STYLE_DEFAULT StyleObject = iota
|
||||
STYLE_ERROR
|
||||
STYLE_WARNING
|
||||
STYLE_SUCCESS
|
||||
|
||||
STYLE_TITLE
|
||||
STYLE_HEADER
|
||||
|
||||
STYLE_STATUSLINE_DEFAULT
|
||||
STYLE_STATUSLINE_ERROR
|
||||
STYLE_STATUSLINE_SUCCESS
|
||||
|
||||
STYLE_MSGLIST_DEFAULT
|
||||
STYLE_MSGLIST_UNREAD
|
||||
STYLE_MSGLIST_READ
|
||||
STYLE_MSGLIST_DELETED
|
||||
STYLE_MSGLIST_MARKED
|
||||
STYLE_MSGLIST_FLAGGED
|
||||
|
||||
STYLE_DIRLIST_DEFAULT
|
||||
|
||||
STYLE_COMPLETION_DEFAULT
|
||||
STYLE_COMPLETION_GUTTER
|
||||
STYLE_COMPLETION_PILL
|
||||
|
||||
STYLE_TAB
|
||||
STYLE_STACK
|
||||
STYLE_SPINNER
|
||||
STYLE_BORDER
|
||||
|
||||
STYLE_SELECTOR_DEFAULT
|
||||
STYLE_SELECTOR_FOCUSED
|
||||
STYLE_SELECTOR_CHOOSER
|
||||
)
|
||||
|
||||
var StyleNames = map[string]StyleObject{
|
||||
"default": STYLE_DEFAULT,
|
||||
"error": STYLE_ERROR,
|
||||
"warning": STYLE_WARNING,
|
||||
"success": STYLE_SUCCESS,
|
||||
|
||||
"title": STYLE_TITLE,
|
||||
"header": STYLE_HEADER,
|
||||
|
||||
"statusline_default": STYLE_STATUSLINE_DEFAULT,
|
||||
"statusline_error": STYLE_STATUSLINE_ERROR,
|
||||
"statusline_success": STYLE_STATUSLINE_SUCCESS,
|
||||
|
||||
"msglist_default": STYLE_MSGLIST_DEFAULT,
|
||||
"msglist_unread": STYLE_MSGLIST_UNREAD,
|
||||
"msglist_read": STYLE_MSGLIST_READ,
|
||||
"msglist_deleted": STYLE_MSGLIST_DELETED,
|
||||
"msglist_marked": STYLE_MSGLIST_MARKED,
|
||||
"msglist_flagged": STYLE_MSGLIST_FLAGGED,
|
||||
|
||||
"dirlist_default": STYLE_DIRLIST_DEFAULT,
|
||||
|
||||
"completion_default": STYLE_COMPLETION_DEFAULT,
|
||||
"completion_gutter": STYLE_COMPLETION_GUTTER,
|
||||
"completion_pill": STYLE_COMPLETION_PILL,
|
||||
|
||||
"tab": STYLE_TAB,
|
||||
"stack": STYLE_STACK,
|
||||
"spinner": STYLE_SPINNER,
|
||||
"border": STYLE_BORDER,
|
||||
|
||||
"selector_default": STYLE_SELECTOR_DEFAULT,
|
||||
"selector_focused": STYLE_SELECTOR_FOCUSED,
|
||||
"selector_chooser": STYLE_SELECTOR_CHOOSER,
|
||||
}
|
||||
|
||||
type Style struct {
|
||||
Fg tcell.Color
|
||||
Bg tcell.Color
|
||||
Bold bool
|
||||
Blink bool
|
||||
Underline bool
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
func (s Style) Get() tcell.Style {
|
||||
return tcell.StyleDefault.
|
||||
Foreground(s.Fg).
|
||||
Background(s.Bg).
|
||||
Bold(s.Bold).
|
||||
Blink(s.Blink).
|
||||
Underline(s.Blink).
|
||||
Reverse(s.Reverse)
|
||||
}
|
||||
|
||||
func (s *Style) Normal() {
|
||||
s.Bold = false
|
||||
s.Blink = false
|
||||
s.Underline = false
|
||||
s.Reverse = false
|
||||
}
|
||||
|
||||
func (s *Style) Default() *Style {
|
||||
s.Fg = tcell.ColorDefault
|
||||
s.Bg = tcell.ColorDefault
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Style) Reset() *Style {
|
||||
s.Default()
|
||||
s.Normal()
|
||||
return s
|
||||
}
|
||||
|
||||
func boolSwitch(val string, cur_val bool) (bool, error) {
|
||||
switch val {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
case "toggle":
|
||||
return !cur_val, nil
|
||||
default:
|
||||
return cur_val, errors.New(
|
||||
"Bool Switch attribute must be true, false, or toggle")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Style) Set(attr, val string) error {
|
||||
switch attr {
|
||||
case "fg":
|
||||
s.Fg = tcell.GetColor(val)
|
||||
case "bg":
|
||||
s.Bg = tcell.GetColor(val)
|
||||
case "bold":
|
||||
if state, err := boolSwitch(val, s.Bold); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.Bold = state
|
||||
}
|
||||
case "blink":
|
||||
if state, err := boolSwitch(val, s.Blink); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.Blink = state
|
||||
}
|
||||
case "underline":
|
||||
if state, err := boolSwitch(val, s.Underline); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.Underline = state
|
||||
}
|
||||
case "reverse":
|
||||
if state, err := boolSwitch(val, s.Reverse); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.Reverse = state
|
||||
}
|
||||
case "default":
|
||||
s.Default()
|
||||
case "normal":
|
||||
s.Normal()
|
||||
default:
|
||||
return errors.New("Unknown style attribute: " + attr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type StyleSet struct {
|
||||
objects map[StyleObject]*Style
|
||||
selected map[StyleObject]*Style
|
||||
}
|
||||
|
||||
func NewStyleSet() StyleSet {
|
||||
ss := StyleSet{
|
||||
objects: make(map[StyleObject]*Style),
|
||||
selected: make(map[StyleObject]*Style),
|
||||
}
|
||||
for _, so := range StyleNames {
|
||||
ss.objects[so] = new(Style)
|
||||
ss.selected[so] = new(Style)
|
||||
}
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
func (ss StyleSet) reset() {
|
||||
for _, so := range StyleNames {
|
||||
ss.objects[so].Reset()
|
||||
ss.selected[so].Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (ss StyleSet) Get(so StyleObject) tcell.Style {
|
||||
return ss.objects[so].Get()
|
||||
}
|
||||
|
||||
func (ss StyleSet) Selected(so StyleObject) tcell.Style {
|
||||
return ss.selected[so].Get()
|
||||
}
|
||||
|
||||
func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) {
|
||||
for _, dir := range stylesetsDir {
|
||||
stylesetPath, err := homedir.Expand(path.Join(dir, stylesetName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(stylesetPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return stylesetPath, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"Can't find styleset %q in any of %v", stylesetName, stylesetsDir)
|
||||
}
|
||||
|
||||
func (ss *StyleSet) ParseStyleSet(file *ini.File) error {
|
||||
ss.reset()
|
||||
|
||||
defaultSection, err := file.GetSection(ini.DefaultSection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedKeys := []string{}
|
||||
|
||||
for _, key := range defaultSection.KeyStrings() {
|
||||
tokens := strings.Split(key, ".")
|
||||
var styleName, attr string
|
||||
switch len(tokens) {
|
||||
case 2:
|
||||
styleName, attr = tokens[0], tokens[1]
|
||||
case 3:
|
||||
if tokens[1] != "selected" {
|
||||
return errors.New("Unknown modifier: " + tokens[1])
|
||||
}
|
||||
selectedKeys = append(selectedKeys, key)
|
||||
continue
|
||||
default:
|
||||
return errors.New("Style parsing error: " + key)
|
||||
}
|
||||
val := defaultSection.KeysHash()[key]
|
||||
|
||||
if strings.ContainsAny(styleName, "*?") {
|
||||
regex := fnmatchToRegex(styleName)
|
||||
for sn, so := range StyleNames {
|
||||
matched, err := regexp.MatchString(regex, sn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ss.objects[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ss.selected[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
so, ok := StyleNames[styleName]
|
||||
if !ok {
|
||||
return errors.New("Unknown style object: " + styleName)
|
||||
}
|
||||
if err := ss.objects[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ss.selected[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range selectedKeys {
|
||||
tokens := strings.Split(key, ".")
|
||||
styleName, modifier, attr := tokens[0], tokens[1], tokens[2]
|
||||
if modifier != "selected" {
|
||||
return errors.New("Unknown modifier: " + modifier)
|
||||
}
|
||||
|
||||
val := defaultSection.KeysHash()[key]
|
||||
|
||||
if strings.ContainsAny(styleName, "*?") {
|
||||
regex := fnmatchToRegex(styleName)
|
||||
for sn, so := range StyleNames {
|
||||
matched, err := regexp.MatchString(regex, sn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ss.selected[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
so, ok := StyleNames[styleName]
|
||||
if !ok {
|
||||
return errors.New("Unknown style object: " + styleName)
|
||||
}
|
||||
if err := ss.selected[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range defaultSection.KeyStrings() {
|
||||
tokens := strings.Split(key, ".")
|
||||
styleName, attr := tokens[0], tokens[1]
|
||||
val := defaultSection.KeysHash()[key]
|
||||
|
||||
if styleName != "selected" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, so := range StyleNames {
|
||||
if err := ss.selected[so].Set(attr, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error {
|
||||
filepath, err := findStyleSet(stylesetName, stylesetDirs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := ini.Load(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ss.ParseStyleSet(file)
|
||||
}
|
||||
|
||||
func fnmatchToRegex(pattern string) string {
|
||||
n := len(pattern)
|
||||
var regex strings.Builder
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
switch pattern[i] {
|
||||
case '*':
|
||||
regex.WriteString(".*")
|
||||
case '?':
|
||||
regex.WriteByte('.')
|
||||
default:
|
||||
regex.WriteByte(pattern[i])
|
||||
}
|
||||
}
|
||||
|
||||
return regex.String()
|
||||
}
|
|
@ -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 corresponds to.
|
||||
|
||||
If you would like to make sure message that are flagged as read in the msglist
|
||||
appear in yellow foreground and black background. You can specify that with
|
||||
this.
|
||||
|
||||
\tmsglist_default.selected.fg=yellow
|
||||
\tmsglist_default.selected.bg=black
|
||||
|
||||
If we specify the global style selected modifer using fnmatch as below:
|
||||
|
||||
\t\*.selected.reverse=toggle
|
||||
|
||||
This toggles the reverse switch for selected version of all the style objects.
|
||||
|
||||
## Colors
|
||||
The color values are set using the values accepted by the tcell library.
|
||||
The values can be one of the following.
|
||||
|
||||
*default*
|
||||
The color is set as per the system or terminal default.
|
||||
|
||||
*<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)
|
||||
|
||||
|
|
|
@ -75,21 +75,21 @@ type AccountWizard struct {
|
|||
|
||||
func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||
wizard := &AccountWizard{
|
||||
accountName: ui.NewTextInput("").Prompt("> "),
|
||||
accountName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
aerc: aerc,
|
||||
conf: conf,
|
||||
temporary: false,
|
||||
copySent: true,
|
||||
email: ui.NewTextInput("").Prompt("> "),
|
||||
fullName: ui.NewTextInput("").Prompt("> "),
|
||||
imapPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
||||
imapServer: ui.NewTextInput("").Prompt("> "),
|
||||
imapStr: ui.NewText("imaps://"),
|
||||
imapUsername: ui.NewTextInput("").Prompt("> "),
|
||||
smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
||||
smtpServer: ui.NewTextInput("").Prompt("> "),
|
||||
smtpStr: ui.NewText("smtps://"),
|
||||
smtpUsername: ui.NewTextInput("").Prompt("> "),
|
||||
email: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
fullName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||
imapServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
imapStr: ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||
imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||
smtpServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
smtpStr: ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||
smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||
}
|
||||
|
||||
// Autofill some stuff for the user
|
||||
|
@ -150,33 +150,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
basics.AddChild(
|
||||
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" +
|
||||
"This wizard supports basic IMAP & SMTP configuration.\n" +
|
||||
"For other configurations, use <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 +230,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
}).Columns([]ui.GridSpec{
|
||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)"))
|
||||
incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
incoming.AddChild(
|
||||
ui.NewText("Username").Bold(true)).
|
||||
ui.NewText("Username",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(1, 0)
|
||||
incoming.AddChild(wizard.imapUsername).
|
||||
At(2, 0)
|
||||
incoming.AddChild(ui.NewFill(' ')).
|
||||
At(3, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Password").Bold(true)).
|
||||
ui.NewText("Password",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(4, 0)
|
||||
incoming.AddChild(wizard.imapPassword).
|
||||
At(5, 0)
|
||||
|
@ -244,20 +250,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
At(6, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Server address "+
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(7, 0)
|
||||
incoming.AddChild(wizard.imapServer).
|
||||
At(8, 0)
|
||||
incoming.AddChild(ui.NewFill(' ')).
|
||||
At(9, 0)
|
||||
incoming.AddChild(
|
||||
ui.NewText("Connection mode").Bold(true)).
|
||||
ui.NewText("Connection mode",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(10, 0)
|
||||
imapMode := NewSelecter([]string{
|
||||
"IMAP over SSL/TLS",
|
||||
"IMAP with STARTTLS",
|
||||
"Insecure IMAP",
|
||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
||||
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||
switch option {
|
||||
case "IMAP over SSL/TLS":
|
||||
wizard.imapMode = IMAP_OVER_TLS
|
||||
|
@ -269,7 +277,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
wizard.imapUri()
|
||||
})
|
||||
incoming.AddChild(imapMode).At(11, 0)
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||
OnChoose(wizard.advance)
|
||||
incoming.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||
incoming.AddChild(wizard.imapStr).At(13, 0)
|
||||
|
@ -304,16 +312,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
}).Columns([]ui.GridSpec{
|
||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)"))
|
||||
outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Username").Bold(true)).
|
||||
ui.NewText("Username",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(1, 0)
|
||||
outgoing.AddChild(wizard.smtpUsername).
|
||||
At(2, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).
|
||||
At(3, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Password").Bold(true)).
|
||||
ui.NewText("Password",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(4, 0)
|
||||
outgoing.AddChild(wizard.smtpPassword).
|
||||
At(5, 0)
|
||||
|
@ -321,20 +332,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
At(6, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Server address "+
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(7, 0)
|
||||
outgoing.AddChild(wizard.smtpServer).
|
||||
At(8, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).
|
||||
At(9, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Connection mode").Bold(true)).
|
||||
ui.NewText("Connection mode",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||
At(10, 0)
|
||||
smtpMode := NewSelecter([]string{
|
||||
"SMTP over SSL/TLS",
|
||||
"SMTP with STARTTLS",
|
||||
"Insecure SMTP",
|
||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
||||
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||
switch option {
|
||||
case "SMTP over SSL/TLS":
|
||||
wizard.smtpMode = SMTP_OVER_TLS
|
||||
|
@ -346,15 +359,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
wizard.smtpUri()
|
||||
})
|
||||
outgoing.AddChild(smtpMode).At(11, 0)
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||
OnChoose(wizard.advance)
|
||||
outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||
outgoing.AddChild(wizard.smtpStr).At(13, 0)
|
||||
outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
|
||||
outgoing.AddChild(
|
||||
ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)).
|
||||
At(15, 0)
|
||||
copySent := NewSelecter([]string{"Yes", "No"}, 0).
|
||||
ui.NewText("Copy sent messages to 'Sent' folder?",
|
||||
conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
|
||||
copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
|
||||
Chooser(true).OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Yes":
|
||||
|
@ -380,15 +393,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
|||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
complete.AddChild(ui.NewText(
|
||||
"\nConfiguration complete!\n\n" +
|
||||
"You can go back and double check your settings, or choose 'Finish' to\n" +
|
||||
"save your settings to accounts.conf.\n\n" +
|
||||
"To add another account in the future, run ':new-account'."))
|
||||
"\nConfiguration complete!\n\n"+
|
||||
"You can go back and double check your settings, or choose 'Finish' to\n"+
|
||||
"save your settings to accounts.conf.\n\n"+
|
||||
"To add another account in the future, run ':new-account'.",
|
||||
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||
selecter = NewSelecter([]string{
|
||||
"Previous",
|
||||
"Finish & open tutorial",
|
||||
"Finish",
|
||||
}, 1).OnChoose(func(option string) {
|
||||
}, 1, conf.Ui).OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Previous":
|
||||
wizard.advance("Previous")
|
||||
|
|
|
@ -64,15 +64,14 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
|
|||
|
||||
worker, err := worker.NewWorker(acct.Source, logger)
|
||||
if err != nil {
|
||||
host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)).
|
||||
Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
|
||||
return view
|
||||
}
|
||||
view.worker = worker
|
||||
|
||||
view.dirlist = NewDirectoryList(conf, acct, logger, worker)
|
||||
if acctUiConf.SidebarWidth > 0 {
|
||||
view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT))
|
||||
view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT, acctUiConf))
|
||||
}
|
||||
|
||||
view.msglist = NewMessageList(conf, logger, aerc)
|
||||
|
|
|
@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
|||
|
||||
tabs := ui.NewTabs(&conf.Ui)
|
||||
|
||||
statusbar := ui.NewStack()
|
||||
statusline := NewStatusLine()
|
||||
statusbar := ui.NewStack(conf.Ui)
|
||||
statusline := NewStatusLine(conf.Ui)
|
||||
statusbar.Push(statusline)
|
||||
|
||||
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
||||
|
@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
|||
logger: logger,
|
||||
statusbar: statusbar,
|
||||
statusline: statusline,
|
||||
prompts: ui.NewStack(),
|
||||
prompts: ui.NewStack(conf.Ui),
|
||||
tabs: tabs,
|
||||
}
|
||||
|
||||
|
@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage {
|
|||
return aerc.statusline.Set(status)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) SetError(status string) *StatusMessage {
|
||||
return aerc.statusline.SetError(status)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
|
||||
return aerc.statusline.Push(text, expiry)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushError(text string) {
|
||||
aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed)
|
||||
func (aerc *Aerc) PushError(text string) *StatusMessage {
|
||||
return aerc.statusline.PushError(text)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) PushSuccess(text string) *StatusMessage {
|
||||
return aerc.statusline.PushSuccess(text)
|
||||
}
|
||||
|
||||
func (aerc *Aerc) focus(item ui.Interactive) {
|
||||
|
@ -555,7 +563,7 @@ func (aerc *Aerc) CloseDialog() {
|
|||
func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
|
||||
chText = make(chan string, 1)
|
||||
chErr = make(chan error, 1)
|
||||
getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) {
|
||||
getPasswd := NewGetPasswd(title, prompt, aerc.conf, func(pw string, err error) {
|
||||
defer func() {
|
||||
close(chErr)
|
||||
close(chText)
|
||||
|
|
|
@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
|||
|
||||
templateData := templates.ParseTemplateData(defaults, original)
|
||||
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
||||
aerc.PushError(fmt.Sprintf("could not complete header: %v", err))
|
||||
aerc.PushError(
|
||||
fmt.Sprintf("could not complete header: %v", err))
|
||||
worker.Logger.Printf("could not complete header: %v", err)
|
||||
}, aerc.Logger())
|
||||
layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults)
|
||||
layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
|
||||
|
||||
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
||||
if err != nil {
|
||||
|
@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
|||
return c, nil
|
||||
}
|
||||
|
||||
func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
||||
func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
|
||||
defaults map[string]string) (
|
||||
newLayout HeaderLayout,
|
||||
editors map[string]*headerEditor,
|
||||
focusable []ui.MouseableDrawableInteractive,
|
||||
) {
|
||||
layout := conf.Compose.HeaderLayout
|
||||
layout := aerc.conf.Compose.HeaderLayout
|
||||
editors = make(map[string]*headerEditor)
|
||||
focusable = make([]ui.MouseableDrawableInteractive, 0)
|
||||
|
||||
for _, row := range layout {
|
||||
for _, h := range row {
|
||||
e := newHeaderEditor(h, "")
|
||||
if conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||
if aerc.conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||
}
|
||||
editors[h] = e
|
||||
switch h {
|
||||
|
@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
|||
for _, h := range []string{"Cc", "Bcc"} {
|
||||
if val, ok := defaults[h]; ok && val != "" {
|
||||
if _, ok := editors[h]; !ok {
|
||||
e := newHeaderEditor(h, "")
|
||||
if conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
||||
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||
if aerc.conf.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||
}
|
||||
editors[h] = e
|
||||
focusable = append(focusable, e)
|
||||
|
@ -259,7 +260,8 @@ func (c *Composer) readSignatureFromFile() []byte {
|
|||
}
|
||||
signature, err := ioutil.ReadFile(sigFile)
|
||||
if err != nil {
|
||||
c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile))
|
||||
c.aerc.PushError(
|
||||
fmt.Sprintf(" Error loading signature from file: %v", sigFile))
|
||||
return nil
|
||||
}
|
||||
return signature
|
||||
|
@ -648,7 +650,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
|||
}
|
||||
return
|
||||
}
|
||||
e := newHeaderEditor(header, value)
|
||||
e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
|
||||
if c.config.Ui.CompletionPopovers {
|
||||
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
|
||||
}
|
||||
|
@ -704,23 +706,27 @@ func (c *Composer) reloadEmail() error {
|
|||
}
|
||||
|
||||
type headerEditor struct {
|
||||
name string
|
||||
focused bool
|
||||
input *ui.TextInput
|
||||
name string
|
||||
focused bool
|
||||
input *ui.TextInput
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func newHeaderEditor(name string, value string) *headerEditor {
|
||||
func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
|
||||
return &headerEditor{
|
||||
input: ui.NewTextInput(value),
|
||||
name: name,
|
||||
input: ui.NewTextInput(value, uiConfig),
|
||||
name: name,
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (he *headerEditor) Draw(ctx *ui.Context) {
|
||||
name := he.name + " "
|
||||
size := runewidth.StringWidth(name)
|
||||
ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name)
|
||||
defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||
ctx.Fill(0, 0, size, ctx.Height(), ' ', defaultStyle)
|
||||
ctx.Printf(0, 0, headerStyle, "%s", name)
|
||||
he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
|
||||
}
|
||||
|
||||
|
@ -784,21 +790,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
|||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
|
||||
uiConfig := composer.config.Ui
|
||||
|
||||
if err != nil {
|
||||
grid.AddChild(ui.NewText(err.Error()).
|
||||
Color(tcell.ColorRed, tcell.ColorDefault))
|
||||
grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0)
|
||||
grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR)))
|
||||
grid.AddChild(ui.NewText("Press [q] to close this tab.",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0)
|
||||
} else {
|
||||
// TODO: source this from actual keybindings?
|
||||
grid.AddChild(ui.NewText(
|
||||
"Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0)
|
||||
grid.AddChild(ui.NewText("Attachments:").
|
||||
Reverse(true)).At(1, 0)
|
||||
grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0)
|
||||
grid.AddChild(ui.NewText("Attachments:",
|
||||
uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0)
|
||||
if len(composer.attachments) == 0 {
|
||||
grid.AddChild(ui.NewText("(none)")).At(2, 0)
|
||||
grid.AddChild(ui.NewText("(none)",
|
||||
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(2, 0)
|
||||
} else {
|
||||
for i, a := range composer.attachments {
|
||||
grid.AddChild(ui.NewText(a)).At(i+2, 0)
|
||||
grid.AddChild(ui.NewText(a, uiConfig.GetStyle(config.STYLE_DEFAULT))).
|
||||
At(i+2, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,7 +196,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string {
|
|||
}
|
||||
|
||||
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT))
|
||||
|
||||
if dirlist.spinner.IsRunning() {
|
||||
dirlist.spinner.Draw(ctx)
|
||||
|
@ -204,7 +205,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
|||
}
|
||||
|
||||
if len(dirlist.dirs) == 0 {
|
||||
style := tcell.StyleDefault
|
||||
style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
|
||||
ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
|
||||
return
|
||||
}
|
||||
|
@ -236,10 +237,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
|||
|
||||
style := tcell.StyleDefault
|
||||
if name == dirlist.selected {
|
||||
style = style.Reverse(true)
|
||||
} else if name == dirlist.selecting {
|
||||
style = style.Reverse(true)
|
||||
style = style.Foreground(tcell.ColorGray)
|
||||
style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT)
|
||||
}
|
||||
ctx.Fill(0, row, textWidth, 1, ' ', style)
|
||||
|
||||
|
|
|
@ -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,17 @@ type GetPasswd struct {
|
|||
title string
|
||||
prompt string
|
||||
input *ui.TextInput
|
||||
conf *config.AercConfig
|
||||
}
|
||||
|
||||
func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPasswd {
|
||||
func NewGetPasswd(title string, prompt string, conf *config.AercConfig,
|
||||
cb func(string, error)) *GetPasswd {
|
||||
getpasswd := &GetPasswd{
|
||||
callback: cb,
|
||||
title: title,
|
||||
prompt: prompt,
|
||||
input: ui.NewTextInput("").Password(true).Prompt("Password: "),
|
||||
conf: conf,
|
||||
input: ui.NewTextInput("", conf.Ui).Password(true).Prompt("Password: "),
|
||||
}
|
||||
getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
|
||||
getpasswd.Invalidate()
|
||||
|
@ -31,10 +35,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw
|
|||
}
|
||||
|
||||
func (gp *GetPasswd) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true))
|
||||
ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
|
||||
ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt)
|
||||
defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT)
|
||||
titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE)
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle)
|
||||
ctx.Printf(1, 0, titleStyle, "%s", gp.title)
|
||||
ctx.Printf(1, 1, defaultStyle, gp.prompt)
|
||||
gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,8 @@ func (ml *MessageList) Invalidate() {
|
|||
|
||||
func (ml *MessageList) Draw(ctx *ui.Context) {
|
||||
ml.height = ctx.Height()
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
ml.aerc.SelectedAccount().UiConfig().GetStyle(config.STYLE_MSGLIST_DEFAULT))
|
||||
|
||||
store := ml.Store()
|
||||
if store == nil {
|
||||
|
@ -101,38 +102,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
|
|||
continue
|
||||
}
|
||||
|
||||
style := tcell.StyleDefault
|
||||
uiConfig := ml.conf.GetUiConfig(map[config.ContextType]string{
|
||||
config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
|
||||
config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
|
||||
config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject,
|
||||
})
|
||||
|
||||
so := config.STYLE_MSGLIST_DEFAULT
|
||||
|
||||
// current row
|
||||
if row == ml.store.SelectedIndex()-ml.scroll {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
// deleted message
|
||||
if _, ok := store.Deleted[msg.Uid]; ok {
|
||||
style = style.Foreground(tcell.ColorGray)
|
||||
so = config.STYLE_MSGLIST_DELETED
|
||||
}
|
||||
// unread message
|
||||
seen := false
|
||||
flagged := false
|
||||
for _, flag := range msg.Flags {
|
||||
if flag == models.SeenFlag {
|
||||
switch flag {
|
||||
case models.SeenFlag:
|
||||
seen = true
|
||||
case models.FlaggedFlag:
|
||||
flagged = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
style = style.Bold(true)
|
||||
so = config.STYLE_MSGLIST_UNREAD
|
||||
}
|
||||
|
||||
ctx.Fill(0, row, textWidth, 1, ' ', style)
|
||||
|
||||
confParams := map[config.ContextType]string{
|
||||
config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
|
||||
config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
|
||||
if flagged {
|
||||
so = config.STYLE_MSGLIST_FLAGGED
|
||||
}
|
||||
if msg.Envelope != nil {
|
||||
confParams[config.UI_CONTEXT_SUBJECT] = msg.Envelope.Subject
|
||||
}
|
||||
uiConfig := ml.conf.GetUiConfig(confParams)
|
||||
|
||||
// marked message
|
||||
if store.IsMarked(msg.Uid) {
|
||||
so = config.STYLE_MSGLIST_MARKED
|
||||
}
|
||||
|
||||
style := uiConfig.GetStyle(so)
|
||||
|
||||
// current row
|
||||
if row == ml.store.SelectedIndex()-ml.scroll {
|
||||
style = uiConfig.GetStyleSelected(so)
|
||||
}
|
||||
|
||||
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
||||
fmtStr, args, err := format.ParseMessageFormat(
|
||||
ml.aerc.SelectedAccount().acct.From,
|
||||
uiConfig.IndexFormat,
|
||||
|
@ -342,7 +355,8 @@ func (ml *MessageList) ensureScroll() {
|
|||
}
|
||||
|
||||
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
|
||||
msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage
|
||||
uiConfig := ml.aerc.SelectedAccount().UiConfig()
|
||||
msg := uiConfig.EmptyMessage
|
||||
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
|
||||
tcell.StyleDefault, "%s", msg)
|
||||
uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ type MessageViewer struct {
|
|||
grid *ui.Grid
|
||||
switcher *PartSwitcher
|
||||
msg lib.MessageView
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
type PartSwitcher struct {
|
||||
|
@ -62,9 +63,11 @@ func NewMessageViewer(acct *AccountView,
|
|||
header, headerHeight := layout.grid(
|
||||
func(header string) ui.Drawable {
|
||||
return &HeaderView{
|
||||
conf: conf,
|
||||
Name: header,
|
||||
Value: fmtHeader(msg.MessageInfo(), header,
|
||||
acct.UiConfig().TimestampFormat),
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -94,15 +97,16 @@ func NewMessageViewer(acct *AccountView,
|
|||
err := createSwitcher(acct, switcher, conf, msg)
|
||||
if err != nil {
|
||||
return &MessageViewer{
|
||||
err: err,
|
||||
grid: grid,
|
||||
msg: msg,
|
||||
err: err,
|
||||
grid: grid,
|
||||
msg: msg,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
grid.AddChild(header).At(0, 0)
|
||||
if msg.PGPDetails() != nil {
|
||||
grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
|
||||
grid.AddChild(NewPGPInfo(msg.PGPDetails(), acct.UiConfig())).At(1, 0)
|
||||
grid.AddChild(ui.NewFill(' ')).At(2, 0)
|
||||
grid.AddChild(switcher).At(3, 0)
|
||||
} else {
|
||||
|
@ -116,6 +120,7 @@ func NewMessageViewer(acct *AccountView,
|
|||
grid: grid,
|
||||
msg: msg,
|
||||
switcher: switcher,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
switcher.mv = mv
|
||||
|
||||
|
@ -224,8 +229,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher,
|
|||
|
||||
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
||||
if mv.err != nil {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
|
||||
style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, style, "%s", mv.err.Error())
|
||||
return
|
||||
}
|
||||
mv.grid.Draw(ctx)
|
||||
|
@ -347,7 +353,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
|
|||
ps.height = ctx.Height()
|
||||
y := ctx.Height() - height
|
||||
for i, part := range ps.parts {
|
||||
style := tcell.StyleDefault.Reverse(ps.selected == i)
|
||||
style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
if ps.selected == i {
|
||||
style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
|
||||
}
|
||||
ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
|
||||
name := fmt.Sprintf("%s/%s",
|
||||
strings.ToLower(part.part.MIMEType),
|
||||
|
@ -436,6 +445,7 @@ func (mv *MessageViewer) Focus(focus bool) {
|
|||
|
||||
type PartViewer struct {
|
||||
ui.Invalidatable
|
||||
conf *config.AercConfig
|
||||
err error
|
||||
fetched bool
|
||||
filter *exec.Cmd
|
||||
|
@ -450,6 +460,7 @@ type PartViewer struct {
|
|||
term *Terminal
|
||||
selecter *Selecter
|
||||
grid *ui.Grid
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||
|
@ -519,7 +530,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||
})
|
||||
|
||||
selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0).
|
||||
selecter := NewSelecter([]string{"Save message", "Pipe to command"},
|
||||
0, acct.UiConfig()).
|
||||
OnChoose(func(option string) {
|
||||
switch option {
|
||||
case "Save message":
|
||||
|
@ -532,6 +544,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
grid.AddChild(selecter).At(2, 0)
|
||||
|
||||
pv := &PartViewer{
|
||||
conf: conf,
|
||||
filter: filter,
|
||||
index: index,
|
||||
msg: msg,
|
||||
|
@ -543,6 +556,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
|||
term: term,
|
||||
selecter: selecter,
|
||||
grid: grid,
|
||||
uiConfig: acct.UiConfig(),
|
||||
}
|
||||
|
||||
if term != nil {
|
||||
|
@ -661,14 +675,16 @@ func (pv *PartViewer) Invalidate() {
|
|||
}
|
||||
|
||||
func (pv *PartViewer) Draw(ctx *ui.Context) {
|
||||
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
styleError := pv.uiConfig.GetStyle(config.STYLE_ERROR)
|
||||
if pv.filter == nil {
|
||||
// TODO: Let them download it directly or something
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, styleError,
|
||||
"No filter configured for this mimetype ('%s/%s')",
|
||||
pv.part.MIMEType, pv.part.MIMESubType,
|
||||
)
|
||||
ctx.Printf(0, 2, tcell.StyleDefault,
|
||||
ctx.Printf(0, 2, style,
|
||||
"You can still :save the message or :pipe it to an external command")
|
||||
pv.selecter.Focus(true)
|
||||
pv.grid.Draw(ctx)
|
||||
|
@ -679,8 +695,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
|
|||
pv.fetched = true
|
||||
}
|
||||
if pv.err != nil {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||
ctx.Printf(0, 0, style, "%s", pv.err.Error())
|
||||
return
|
||||
}
|
||||
pv.term.Draw(ctx)
|
||||
|
@ -702,8 +718,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
|
|||
|
||||
type HeaderView struct {
|
||||
ui.Invalidatable
|
||||
Name string
|
||||
Value string
|
||||
conf *config.AercConfig
|
||||
Name string
|
||||
Value string
|
||||
uiConfig config.UIConfig
|
||||
}
|
||||
|
||||
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
||||
|
@ -711,18 +729,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
|
|||
size := runewidth.StringWidth(name)
|
||||
lim := ctx.Width() - size - 1
|
||||
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
||||
var (
|
||||
hstyle tcell.Style
|
||||
vstyle tcell.Style
|
||||
)
|
||||
|
||||
vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||
hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||
|
||||
// TODO: Make this more robust and less dumb
|
||||
if hv.Name == "PGP" {
|
||||
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
|
||||
hstyle = tcell.StyleDefault.Bold(true)
|
||||
} else {
|
||||
vstyle = tcell.StyleDefault
|
||||
hstyle = tcell.StyleDefault.Bold(true)
|
||||
vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||
}
|
||||
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
||||
ctx.Printf(0, 0, hstyle, "%s", name)
|
||||
ctx.Printf(size, 0, vstyle, "%s", value)
|
||||
|
|
|
@ -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,46 +3,50 @@ package widgets
|
|||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
|
||||
"git.sr.ht/~sircmpwn/aerc/config"
|
||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||
)
|
||||
|
||||
type Selecter struct {
|
||||
type Selector struct {
|
||||
ui.Invalidatable
|
||||
chooser bool
|
||||
focused bool
|
||||
focus int
|
||||
options []string
|
||||
chooser bool
|
||||
focused bool
|
||||
focus int
|
||||
options []string
|
||||
uiConfig config.UIConfig
|
||||
|
||||
onChoose func(option string)
|
||||
onSelect func(option string)
|
||||
}
|
||||
|
||||
func NewSelecter(options []string, focus int) *Selecter {
|
||||
return &Selecter{
|
||||
focus: focus,
|
||||
options: options,
|
||||
func NewSelector(options []string, focus int, uiConfig config.UIConfig) *Selector {
|
||||
return &Selector{
|
||||
focus: focus,
|
||||
options: options,
|
||||
uiConfig: uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (sel *Selecter) Chooser(chooser bool) *Selecter {
|
||||
func (sel *Selector) Chooser(chooser bool) *Selector {
|
||||
sel.chooser = chooser
|
||||
return sel
|
||||
}
|
||||
|
||||
func (sel *Selecter) Invalidate() {
|
||||
func (sel *Selector) Invalidate() {
|
||||
sel.DoInvalidate(sel)
|
||||
}
|
||||
|
||||
func (sel *Selecter) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||
func (sel *Selector) Draw(ctx *ui.Context) {
|
||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||
sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT))
|
||||
x := 2
|
||||
for i, option := range sel.options {
|
||||
style := tcell.StyleDefault
|
||||
style := sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT)
|
||||
if sel.focus == i {
|
||||
if sel.focused {
|
||||
style = style.Reverse(true)
|
||||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_FOCUSED)
|
||||
} else if sel.chooser {
|
||||
style = style.Bold(true)
|
||||
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_CHOOSER)
|
||||
}
|
||||
}
|
||||
x += ctx.Printf(x, 1, style, "[%s]", option)
|
||||
|
@ -50,26 +54,26 @@ func (sel *Selecter) Draw(ctx *ui.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (sel *Selecter) OnChoose(fn func(option string)) *Selecter {
|
||||
func (sel *Selector) OnChoose(fn func(option string)) *Selector {
|
||||
sel.onChoose = fn
|
||||
return sel
|
||||
}
|
||||
|
||||
func (sel *Selecter) OnSelect(fn func(option string)) *Selecter {
|
||||
func (sel *Selector) OnSelect(fn func(option string)) *Selector {
|
||||
sel.onSelect = fn
|
||||
return sel
|
||||
}
|
||||
|
||||
func (sel *Selecter) Selected() string {
|
||||
func (sel *Selector) Selected() string {
|
||||
return sel.options[sel.focus]
|
||||
}
|
||||
|
||||
func (sel *Selecter) Focus(focus bool) {
|
||||
func (sel *Selector) Focus(focus bool) {
|
||||
sel.focused = focus
|
||||
sel.Invalidate()
|
||||
}
|
||||
|
||||
func (sel *Selecter) Event(event tcell.Event) bool {
|
||||
func (sel *Selector) Event(event tcell.Event) bool {
|
||||
switch event := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch event.Key() {
|
|
@ -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)
|
||||
|
@ -85,6 +91,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
|
|||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) PushError(text string) *StatusMessage {
|
||||
msg := status.Push(text, 10*time.Second)
|
||||
msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR))
|
||||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) PushSuccess(text string) *StatusMessage {
|
||||
msg := status.Push(text, 10*time.Second)
|
||||
msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS))
|
||||
return msg
|
||||
}
|
||||
|
||||
func (status *StatusLine) Expire() {
|
||||
status.stack = nil
|
||||
}
|
||||
|
@ -93,7 +111,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) {
|
|||
status.aerc = aerc
|
||||
}
|
||||
|
||||
func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) {
|
||||
msg.bg = bg
|
||||
msg.fg = fg
|
||||
func (msg *StatusMessage) Color(style tcell.Style) {
|
||||
msg.style = style
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ import (
|
|||
type TabHost interface {
|
||||
BeginExCommand(cmd string)
|
||||
SetStatus(status string) *StatusMessage
|
||||
SetError(err string) *StatusMessage
|
||||
PushStatus(text string, expiry time.Duration) *StatusMessage
|
||||
PushError(text string) *StatusMessage
|
||||
PushSuccess(text string) *StatusMessage
|
||||
Beep()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue