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
548a5fff68
commit
905cb9dfd3
30 changed files with 1051 additions and 292 deletions
7
Makefile
7
Makefile
|
@ -35,7 +35,8 @@ DOCS := \
|
||||||
aerc-notmuch.5 \
|
aerc-notmuch.5 \
|
||||||
aerc-smtp.5 \
|
aerc-smtp.5 \
|
||||||
aerc-tutorial.7 \
|
aerc-tutorial.7 \
|
||||||
aerc-templates.7
|
aerc-templates.7 \
|
||||||
|
aerc-stylesets.7
|
||||||
|
|
||||||
.1.scd.1:
|
.1.scd.1:
|
||||||
scdoc < $< > $@
|
scdoc < $< > $@
|
||||||
|
@ -58,7 +59,7 @@ clean:
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
mkdir -m755 -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(MANDIR)/man5 $(DESTDIR)$(MANDIR)/man7 \
|
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 -m755 aerc $(DESTDIR)$(BINDIR)/aerc
|
||||||
install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1
|
install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1
|
||||||
install -m644 aerc-search.1 $(DESTDIR)$(MANDIR)/man1/aerc-search.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-smtp.5 $(DESTDIR)$(MANDIR)/man5/aerc-smtp.5
|
||||||
install -m644 aerc-tutorial.7 $(DESTDIR)$(MANDIR)/man7/aerc-tutorial.7
|
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-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 config/accounts.conf $(DESTDIR)$(SHAREDIR)/accounts.conf
|
||||||
install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf
|
install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf
|
||||||
install -m644 config/binds.conf $(DESTDIR)$(SHAREDIR)/binds.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 -m755 filters/plaintext $(DESTDIR)$(SHAREDIR)/filters/plaintext
|
||||||
install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply
|
install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply
|
||||||
install -m644 templates/forward_as_body $(DESTDIR)$(SHAREDIR)/templates/forward_as_body
|
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 '\
|
RMDIR_IF_EMPTY:=sh -c '\
|
||||||
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
|
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
|
||||||
|
|
|
@ -4,11 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/commands"
|
"git.sr.ht/~sircmpwn/aerc/commands"
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,8 +50,7 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||||
composer.AddAttachment(path)
|
composer.AddAttachment(path)
|
||||||
|
|
||||||
aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second).
|
aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()))
|
||||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,8 @@ package compose
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Detach struct{}
|
type Detach struct{}
|
||||||
|
@ -44,8 +42,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second).
|
aerc.PushSuccess(fmt.Sprintf("Detached %s", path))
|
||||||
Color(tcell.ColorDefault, tcell.ColorGreen)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExecCmd struct{}
|
type ExecCmd struct{}
|
||||||
|
@ -51,14 +49,15 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerc.PushError(" " + err.Error())
|
aerc.PushError(" " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
color := tcell.ColorDefault
|
|
||||||
if cmd.ProcessState.ExitCode() != 0 {
|
if cmd.ProcessState.ExitCode() != 0 {
|
||||||
color = tcell.ColorRed
|
aerc.PushError(fmt.Sprintf(
|
||||||
|
"%s: completed with status %d", args[0],
|
||||||
|
cmd.ProcessState.ExitCode()))
|
||||||
|
} else {
|
||||||
|
aerc.PushStatus(fmt.Sprintf(
|
||||||
|
"%s: completed with status %d", args[0],
|
||||||
|
cmd.ProcessState.ExitCode()), 10*time.Second)
|
||||||
}
|
}
|
||||||
aerc.PushStatus(fmt.Sprintf(
|
|
||||||
"%s: completed with status %d", args[0],
|
|
||||||
cmd.ProcessState.ExitCode()), 10*time.Second).
|
|
||||||
Color(tcell.ColorDefault, color)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/getopt"
|
"git.sr.ht/~sircmpwn/getopt"
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pipe struct{}
|
type Pipe struct{}
|
||||||
|
@ -96,14 +95,15 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerc.PushError(" " + err.Error())
|
aerc.PushError(" " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
color := tcell.ColorDefault
|
|
||||||
if ecmd.ProcessState.ExitCode() != 0 {
|
if ecmd.ProcessState.ExitCode() != 0 {
|
||||||
color = tcell.ColorRed
|
aerc.PushError(fmt.Sprintf(
|
||||||
|
"%s: completed with status %d", cmd[0],
|
||||||
|
ecmd.ProcessState.ExitCode()))
|
||||||
|
} 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
|
# Default: true
|
||||||
next-message-on-delete=true
|
next-message-on-delete=true
|
||||||
|
|
||||||
|
# The directories where the stylesets are stored. It takes a colon-separated
|
||||||
|
# list of directories.
|
||||||
|
#
|
||||||
|
# default: @SHAREDIR@/stylesets/
|
||||||
|
stylesets-dirs=@SHAREDIR@/stylesets/
|
||||||
|
|
||||||
|
# Sets the styleset to use for the aerc ui elements.
|
||||||
|
#
|
||||||
|
# Default: default
|
||||||
|
styleset-name=default
|
||||||
|
|
||||||
[viewer]
|
[viewer]
|
||||||
#
|
#
|
||||||
# Specifies the pager to use when displaying emails. Note that some filters
|
# Specifies the pager to use when displaying emails. Note that some filters
|
||||||
|
|
|
@ -45,6 +45,9 @@ type UIConfig struct {
|
||||||
NextMessageOnDelete bool `ini:"next-message-on-delete"`
|
NextMessageOnDelete bool `ini:"next-message-on-delete"`
|
||||||
CompletionDelay time.Duration `ini:"completion-delay"`
|
CompletionDelay time.Duration `ini:"completion-delay"`
|
||||||
CompletionPopovers bool `ini:"completion-popovers"`
|
CompletionPopovers bool `ini:"completion-popovers"`
|
||||||
|
StyleSetDirs []string `ini:"stylesets-dirs" delim:":"`
|
||||||
|
StyleSetName string `ini:"styleset-name"`
|
||||||
|
style StyleSet
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextType int
|
type ContextType int
|
||||||
|
@ -411,6 +414,30 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := config.Ui.loadStyleSet(
|
||||||
|
config.Ui.StyleSetDirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, contextualUi := range config.ContextualUis {
|
||||||
|
if contextualUi.UiConfig.StyleSetName == "" &&
|
||||||
|
len(contextualUi.UiConfig.StyleSetDirs) == 0 {
|
||||||
|
continue // no need to do anything if nothing is overridden
|
||||||
|
}
|
||||||
|
// fill in the missing part from the base
|
||||||
|
if contextualUi.UiConfig.StyleSetName == "" {
|
||||||
|
config.ContextualUis[idx].UiConfig.StyleSetName = config.Ui.StyleSetName
|
||||||
|
} else if len(contextualUi.UiConfig.StyleSetDirs) == 0 {
|
||||||
|
config.ContextualUis[idx].UiConfig.StyleSetDirs = config.Ui.StyleSetDirs
|
||||||
|
}
|
||||||
|
// since at least one of them has changed, load the styleset
|
||||||
|
if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
|
||||||
|
config.ContextualUis[idx].UiConfig.StyleSetDirs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,6 +498,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
NextMessageOnDelete: true,
|
NextMessageOnDelete: true,
|
||||||
CompletionDelay: 250 * time.Millisecond,
|
CompletionDelay: 250 * time.Millisecond,
|
||||||
CompletionPopovers: true,
|
CompletionPopovers: true,
|
||||||
|
StyleSetDirs: []string{path.Join(sharedir, "stylesets")},
|
||||||
|
StyleSetName: "default",
|
||||||
},
|
},
|
||||||
|
|
||||||
ContextualUis: []UIConfigContext{},
|
ContextualUis: []UIConfigContext{},
|
||||||
|
@ -500,6 +529,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
Forwards: "forward_as_body",
|
Forwards: "forward_as_body",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// These bindings are not configurable
|
// These bindings are not configurable
|
||||||
config.Bindings.AccountWizard.ExKey = KeyStroke{
|
config.Bindings.AccountWizard.ExKey = KeyStroke{
|
||||||
Key: tcell.KeyCtrlE,
|
Key: tcell.KeyCtrlE,
|
||||||
|
@ -510,6 +540,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
|
||||||
if err = config.LoadConfig(file); err != nil {
|
if err = config.LoadConfig(file); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui, err := file.GetSection("general"); err == nil {
|
if ui, err := file.GetSection("general"); err == nil {
|
||||||
if err := ui.MapTo(&config.General); err != nil {
|
if err := ui.MapTo(&config.General); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -617,8 +648,18 @@ func parseLayout(layout string) [][]string {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
|
||||||
contextType ContextType, s string) {
|
ui.style = NewStyleSet()
|
||||||
|
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 {
|
for _, contextualUi := range config.ContextualUis {
|
||||||
if contextualUi.ContextType != contextType {
|
if contextualUi.ContextType != contextType {
|
||||||
continue
|
continue
|
||||||
|
@ -628,17 +669,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig)
|
mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
|
||||||
return
|
if contextualUi.UiConfig.StyleSetName != "" {
|
||||||
}
|
baseUi.style = contextualUi.UiConfig.style
|
||||||
}
|
}
|
||||||
|
return baseUi
|
||||||
func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
|
||||||
baseUi := config.Ui
|
|
||||||
|
|
||||||
for k, v := range params {
|
|
||||||
config.mergeContextualUi(&baseUi, k, v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseUi
|
return baseUi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
|
||||||
|
baseUi := config.Ui
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
baseUi = config.mergeContextualUi(baseUi, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
|
||||||
|
return uiConfig.style.Get(so)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
|
||||||
|
return uiConfig.style.Selected(so)
|
||||||
|
}
|
||||||
|
|
33
config/default_styleset
Normal file
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
|
Default: 250ms
|
||||||
|
|
||||||
|
*stylesets-dirs*
|
||||||
|
The directories where the stylesets are stored. The config takes a
|
||||||
|
colon-seperated list of dirs.
|
||||||
|
|
||||||
|
Default: "/usr/share/aerc/stylesets"
|
||||||
|
|
||||||
|
*styleset-name*
|
||||||
|
The name of the styleset to be used to style the ui elements. The
|
||||||
|
stylesets are stored in the 'stylesets' directory in the config
|
||||||
|
directory.
|
||||||
|
|
||||||
|
Default: default
|
||||||
|
|
||||||
|
|
||||||
## Contextual UI Configuration
|
## Contextual UI Configuration
|
||||||
|
|
||||||
The UI configuration can be specialized for accounts, specific mail
|
The UI configuration can be specialized for accounts, specific mail
|
||||||
|
|
189
doc/aerc-stylesets.7.scd
Normal file
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 (
|
import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -16,12 +18,15 @@ type Bordered struct {
|
||||||
borders uint
|
borders uint
|
||||||
content Drawable
|
content Drawable
|
||||||
onInvalidate func(d Drawable)
|
onInvalidate func(d Drawable)
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBordered(content Drawable, borders uint) *Bordered {
|
func NewBordered(
|
||||||
|
content Drawable, borders uint, uiConfig config.UIConfig) *Bordered {
|
||||||
b := &Bordered{
|
b := &Bordered{
|
||||||
borders: borders,
|
borders: borders,
|
||||||
content: content,
|
content: content,
|
||||||
|
uiConfig: uiConfig,
|
||||||
}
|
}
|
||||||
content.OnInvalidate(b.contentInvalidated)
|
content.OnInvalidate(b.contentInvalidated)
|
||||||
return b
|
return b
|
||||||
|
@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) {
|
||||||
y := 0
|
y := 0
|
||||||
width := ctx.Width()
|
width := ctx.Width()
|
||||||
height := ctx.Height()
|
height := ctx.Height()
|
||||||
style := tcell.StyleDefault.Reverse(true)
|
style := bordered.uiConfig.GetStyle(config.STYLE_BORDER)
|
||||||
if bordered.borders&BORDER_LEFT != 0 {
|
if bordered.borders&BORDER_LEFT != 0 {
|
||||||
ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
|
ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
|
||||||
x += 1
|
x += 1
|
||||||
|
|
|
@ -3,16 +3,19 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Stack struct {
|
type Stack struct {
|
||||||
children []Drawable
|
children []Drawable
|
||||||
onInvalidate []func(d Drawable)
|
onInvalidate []func(d Drawable)
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStack() *Stack {
|
func NewStack(uiConfig config.UIConfig) *Stack {
|
||||||
return &Stack{}
|
return &Stack{uiConfig: uiConfig}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (stack *Stack) Children() []Drawable {
|
func (stack *Stack) Children() []Drawable {
|
||||||
|
@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) {
|
||||||
if len(stack.children) > 0 {
|
if len(stack.children) > 0 {
|
||||||
stack.Peek().Draw(ctx)
|
stack.Peek().Draw(ctx)
|
||||||
} else {
|
} else {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||||
|
stack.uiConfig.GetStyle(config.STYLE_STACK))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) {
|
||||||
func (strip *TabStrip) Draw(ctx *Context) {
|
func (strip *TabStrip) Draw(ctx *Context) {
|
||||||
x := 0
|
x := 0
|
||||||
for i, tab := range strip.Tabs {
|
for i, tab := range strip.Tabs {
|
||||||
style := tcell.StyleDefault.Reverse(true)
|
style := strip.uiConfig.GetStyle(config.STYLE_TAB)
|
||||||
if strip.Selected == i {
|
if strip.Selected == i {
|
||||||
style = tcell.StyleDefault
|
style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB)
|
||||||
}
|
}
|
||||||
tabWidth := 32
|
tabWidth := 32
|
||||||
if ctx.Width()-x < tabWidth {
|
if ctx.Width()-x < tabWidth {
|
||||||
|
@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style := tcell.StyleDefault.Reverse(true)
|
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
|
||||||
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
|
strip.uiConfig.GetStyle(config.STYLE_TAB))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (strip *TabStrip) Invalidate() {
|
func (strip *TabStrip) Invalidate() {
|
||||||
|
@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) {
|
||||||
if content.Selected >= len(content.Tabs) {
|
if content.Selected >= len(content.Tabs) {
|
||||||
width := ctx.Width()
|
width := ctx.Width()
|
||||||
height := ctx.Height()
|
height := ctx.Height()
|
||||||
ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, width, height, ' ',
|
||||||
|
content.uiConfig.GetStyle(config.STYLE_TAB))
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := content.Tabs[content.Selected]
|
tab := content.Tabs[content.Selected]
|
||||||
|
|
|
@ -15,17 +15,13 @@ type Text struct {
|
||||||
Invalidatable
|
Invalidatable
|
||||||
text string
|
text string
|
||||||
strategy uint
|
strategy uint
|
||||||
fg tcell.Color
|
style tcell.Style
|
||||||
bg tcell.Color
|
|
||||||
bold bool
|
|
||||||
reverse bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewText(text string) *Text {
|
func NewText(text string, style tcell.Style) *Text {
|
||||||
return &Text{
|
return &Text{
|
||||||
bg: tcell.ColorDefault,
|
text: text,
|
||||||
fg: tcell.ColorDefault,
|
style: style,
|
||||||
text: text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Text) Bold(bold bool) *Text {
|
|
||||||
t.bold = bold
|
|
||||||
t.Invalidate()
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text {
|
|
||||||
t.fg = fg
|
|
||||||
t.bg = bg
|
|
||||||
t.Invalidate()
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Text) Reverse(reverse bool) *Text {
|
|
||||||
t.reverse = reverse
|
|
||||||
t.Invalidate()
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Text) Draw(ctx *Context) {
|
func (t *Text) Draw(ctx *Context) {
|
||||||
size := runewidth.StringWidth(t.text)
|
size := runewidth.StringWidth(t.text)
|
||||||
x := 0
|
x := 0
|
||||||
|
@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) {
|
||||||
if t.strategy == TEXT_RIGHT {
|
if t.strategy == TEXT_RIGHT {
|
||||||
x = ctx.Width() - size
|
x = ctx.Width() - size
|
||||||
}
|
}
|
||||||
style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style)
|
||||||
if t.bold {
|
ctx.Printf(x, 0, t.style, "%s", t.text)
|
||||||
style = style.Bold(true)
|
|
||||||
}
|
|
||||||
if t.reverse {
|
|
||||||
style = style.Reverse(true)
|
|
||||||
}
|
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
|
||||||
ctx.Printf(x, 0, style, "%s", t.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Text) Invalidate() {
|
func (t *Text) Invalidate() {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Attach history providers
|
// TODO: Attach history providers
|
||||||
|
@ -27,16 +29,18 @@ type TextInput struct {
|
||||||
completeIndex int
|
completeIndex int
|
||||||
completeDelay time.Duration
|
completeDelay time.Duration
|
||||||
completeDebouncer *time.Timer
|
completeDebouncer *time.Timer
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new TextInput. TextInputs will render a "textbox" in the entire
|
// Creates a new TextInput. TextInputs will render a "textbox" in the entire
|
||||||
// context they're given, and process keypresses to build a string from user
|
// context they're given, and process keypresses to build a string from user
|
||||||
// input.
|
// input.
|
||||||
func NewTextInput(text string) *TextInput {
|
func NewTextInput(text string, ui config.UIConfig) *TextInput {
|
||||||
return &TextInput{
|
return &TextInput{
|
||||||
cells: -1,
|
cells: -1,
|
||||||
text: []rune(text),
|
text: []rune(text),
|
||||||
index: len([]rune(text)),
|
index: len([]rune(text)),
|
||||||
|
uiConfig: ui,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) {
|
||||||
ti.ensureScroll()
|
ti.ensureScroll()
|
||||||
}
|
}
|
||||||
ti.ctx = ctx // gross
|
ti.ctx = ctx // gross
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
|
||||||
|
defaultStyle := ti.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||||
|
|
||||||
text := ti.text[scroll:]
|
text := ti.text[scroll:]
|
||||||
sindex := ti.index - scroll
|
sindex := ti.index - scroll
|
||||||
if ti.password {
|
if ti.password {
|
||||||
x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt)
|
x := ctx.Printf(0, 0, defaultStyle, "%s", ti.prompt)
|
||||||
cells := runewidth.StringWidth(string(text))
|
cells := runewidth.StringWidth(string(text))
|
||||||
ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault)
|
ctx.Fill(x, 0, cells, 1, '*', defaultStyle)
|
||||||
} else {
|
} else {
|
||||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(text))
|
ctx.Printf(0, 0, defaultStyle, "%s%s", ti.prompt, string(text))
|
||||||
}
|
}
|
||||||
cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
|
cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
|
||||||
if ti.focus {
|
if ti.focus {
|
||||||
|
@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
|
||||||
ti.Set(stem + ti.StringRight())
|
ti.Set(stem + ti.StringRight())
|
||||||
ti.Invalidate()
|
ti.Invalidate()
|
||||||
},
|
},
|
||||||
|
uiConfig: ti.uiConfig,
|
||||||
}
|
}
|
||||||
width := maxLen(ti.completions) + 3
|
width := maxLen(ti.completions) + 3
|
||||||
height := len(ti.completions)
|
height := len(ti.completions)
|
||||||
|
@ -353,6 +360,7 @@ type completions struct {
|
||||||
onSelect func(int)
|
onSelect func(int)
|
||||||
onExec func()
|
onExec func()
|
||||||
onStem func(string)
|
onStem func(string)
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func maxLen(ss []string) int {
|
func maxLen(ss []string) int {
|
||||||
|
@ -367,10 +375,10 @@ func maxLen(ss []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *completions) Draw(ctx *Context) {
|
func (c *completions) Draw(ctx *Context) {
|
||||||
bg := tcell.StyleDefault
|
bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT)
|
||||||
sel := tcell.StyleDefault.Reverse(true)
|
gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
|
||||||
gutter := tcell.StyleDefault
|
pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL)
|
||||||
pill := tcell.StyleDefault.Reverse(true)
|
sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT)
|
||||||
|
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)
|
||||||
|
|
||||||
|
|
|
@ -75,21 +75,21 @@ type AccountWizard struct {
|
||||||
|
|
||||||
func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
wizard := &AccountWizard{
|
wizard := &AccountWizard{
|
||||||
accountName: ui.NewTextInput("").Prompt("> "),
|
accountName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
aerc: aerc,
|
aerc: aerc,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
temporary: false,
|
temporary: false,
|
||||||
copySent: true,
|
copySent: true,
|
||||||
email: ui.NewTextInput("").Prompt("> "),
|
email: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
fullName: ui.NewTextInput("").Prompt("> "),
|
fullName: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
imapPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||||
imapServer: ui.NewTextInput("").Prompt("> "),
|
imapServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
imapStr: ui.NewText("imaps://"),
|
imapStr: ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||||
imapUsername: ui.NewTextInput("").Prompt("> "),
|
imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true),
|
smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
|
||||||
smtpServer: ui.NewTextInput("").Prompt("> "),
|
smtpServer: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
smtpStr: ui.NewText("smtps://"),
|
smtpStr: ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
|
||||||
smtpUsername: ui.NewTextInput("").Prompt("> "),
|
smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Autofill some stuff for the user
|
// Autofill some stuff for the user
|
||||||
|
@ -150,33 +150,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||||
})
|
})
|
||||||
basics.AddChild(
|
basics.AddChild(
|
||||||
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" +
|
ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+
|
||||||
"This wizard supports basic IMAP & SMTP configuration.\n" +
|
"This wizard supports basic IMAP & SMTP configuration.\n"+
|
||||||
"For other configurations, use <Ctrl+q> to exit and read the " +
|
"For other configurations, use <Ctrl+q> to exit and read the "+
|
||||||
"aerc-config(5) man page.\n" +
|
"aerc-config(5) man page.\n"+
|
||||||
"Press <Tab> and <Shift+Tab> to cycle between each field in this form, or <Ctrl+j> and <Ctrl+k>."))
|
"Press <Tab> and <Shift+Tab> to cycle between each field in this form, "+
|
||||||
|
"or <Ctrl+j> and <Ctrl+k>.",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||||
basics.AddChild(
|
basics.AddChild(
|
||||||
ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')").
|
ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')",
|
||||||
Bold(true)).
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(1, 0)
|
At(1, 0)
|
||||||
basics.AddChild(wizard.accountName).
|
basics.AddChild(wizard.accountName).
|
||||||
At(2, 0)
|
At(2, 0)
|
||||||
basics.AddChild(ui.NewFill(' ')).
|
basics.AddChild(ui.NewFill(' ')).
|
||||||
At(3, 0)
|
At(3, 0)
|
||||||
basics.AddChild(
|
basics.AddChild(
|
||||||
ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')").
|
ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')",
|
||||||
Bold(true)).
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(4, 0)
|
At(4, 0)
|
||||||
basics.AddChild(wizard.fullName).
|
basics.AddChild(wizard.fullName).
|
||||||
At(5, 0)
|
At(5, 0)
|
||||||
basics.AddChild(ui.NewFill(' ')).
|
basics.AddChild(ui.NewFill(' ')).
|
||||||
At(6, 0)
|
At(6, 0)
|
||||||
basics.AddChild(
|
basics.AddChild(
|
||||||
ui.NewText("Your email address? (e.g. 'john@example.org')").Bold(true)).
|
ui.NewText("Your email address? (e.g. 'john@example.org')",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(7, 0)
|
At(7, 0)
|
||||||
basics.AddChild(wizard.email).
|
basics.AddChild(wizard.email).
|
||||||
At(8, 0)
|
At(8, 0)
|
||||||
selecter := NewSelecter([]string{"Next"}, 0).
|
selecter := NewSelecter([]string{"Next"}, 0, conf.Ui).
|
||||||
OnChoose(func(option string) {
|
OnChoose(func(option string) {
|
||||||
email := wizard.email.String()
|
email := wizard.email.String()
|
||||||
if strings.ContainsRune(email, '@') {
|
if strings.ContainsRune(email, '@') {
|
||||||
|
@ -227,16 +230,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
}).Columns([]ui.GridSpec{
|
}).Columns([]ui.GridSpec{
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{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(
|
incoming.AddChild(
|
||||||
ui.NewText("Username").Bold(true)).
|
ui.NewText("Username",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(1, 0)
|
At(1, 0)
|
||||||
incoming.AddChild(wizard.imapUsername).
|
incoming.AddChild(wizard.imapUsername).
|
||||||
At(2, 0)
|
At(2, 0)
|
||||||
incoming.AddChild(ui.NewFill(' ')).
|
incoming.AddChild(ui.NewFill(' ')).
|
||||||
At(3, 0)
|
At(3, 0)
|
||||||
incoming.AddChild(
|
incoming.AddChild(
|
||||||
ui.NewText("Password").Bold(true)).
|
ui.NewText("Password",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(4, 0)
|
At(4, 0)
|
||||||
incoming.AddChild(wizard.imapPassword).
|
incoming.AddChild(wizard.imapPassword).
|
||||||
At(5, 0)
|
At(5, 0)
|
||||||
|
@ -244,20 +250,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
At(6, 0)
|
At(6, 0)
|
||||||
incoming.AddChild(
|
incoming.AddChild(
|
||||||
ui.NewText("Server address "+
|
ui.NewText("Server address "+
|
||||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(7, 0)
|
At(7, 0)
|
||||||
incoming.AddChild(wizard.imapServer).
|
incoming.AddChild(wizard.imapServer).
|
||||||
At(8, 0)
|
At(8, 0)
|
||||||
incoming.AddChild(ui.NewFill(' ')).
|
incoming.AddChild(ui.NewFill(' ')).
|
||||||
At(9, 0)
|
At(9, 0)
|
||||||
incoming.AddChild(
|
incoming.AddChild(
|
||||||
ui.NewText("Connection mode").Bold(true)).
|
ui.NewText("Connection mode",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(10, 0)
|
At(10, 0)
|
||||||
imapMode := NewSelecter([]string{
|
imapMode := NewSelecter([]string{
|
||||||
"IMAP over SSL/TLS",
|
"IMAP over SSL/TLS",
|
||||||
"IMAP with STARTTLS",
|
"IMAP with STARTTLS",
|
||||||
"Insecure IMAP",
|
"Insecure IMAP",
|
||||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||||
switch option {
|
switch option {
|
||||||
case "IMAP over SSL/TLS":
|
case "IMAP over SSL/TLS":
|
||||||
wizard.imapMode = IMAP_OVER_TLS
|
wizard.imapMode = IMAP_OVER_TLS
|
||||||
|
@ -269,7 +277,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
wizard.imapUri()
|
wizard.imapUri()
|
||||||
})
|
})
|
||||||
incoming.AddChild(imapMode).At(11, 0)
|
incoming.AddChild(imapMode).At(11, 0)
|
||||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||||
OnChoose(wizard.advance)
|
OnChoose(wizard.advance)
|
||||||
incoming.AddChild(ui.NewFill(' ')).At(12, 0)
|
incoming.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||||
incoming.AddChild(wizard.imapStr).At(13, 0)
|
incoming.AddChild(wizard.imapStr).At(13, 0)
|
||||||
|
@ -304,16 +312,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
}).Columns([]ui.GridSpec{
|
}).Columns([]ui.GridSpec{
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{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(
|
outgoing.AddChild(
|
||||||
ui.NewText("Username").Bold(true)).
|
ui.NewText("Username",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(1, 0)
|
At(1, 0)
|
||||||
outgoing.AddChild(wizard.smtpUsername).
|
outgoing.AddChild(wizard.smtpUsername).
|
||||||
At(2, 0)
|
At(2, 0)
|
||||||
outgoing.AddChild(ui.NewFill(' ')).
|
outgoing.AddChild(ui.NewFill(' ')).
|
||||||
At(3, 0)
|
At(3, 0)
|
||||||
outgoing.AddChild(
|
outgoing.AddChild(
|
||||||
ui.NewText("Password").Bold(true)).
|
ui.NewText("Password",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(4, 0)
|
At(4, 0)
|
||||||
outgoing.AddChild(wizard.smtpPassword).
|
outgoing.AddChild(wizard.smtpPassword).
|
||||||
At(5, 0)
|
At(5, 0)
|
||||||
|
@ -321,20 +332,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
At(6, 0)
|
At(6, 0)
|
||||||
outgoing.AddChild(
|
outgoing.AddChild(
|
||||||
ui.NewText("Server address "+
|
ui.NewText("Server address "+
|
||||||
"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
|
"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(7, 0)
|
At(7, 0)
|
||||||
outgoing.AddChild(wizard.smtpServer).
|
outgoing.AddChild(wizard.smtpServer).
|
||||||
At(8, 0)
|
At(8, 0)
|
||||||
outgoing.AddChild(ui.NewFill(' ')).
|
outgoing.AddChild(ui.NewFill(' ')).
|
||||||
At(9, 0)
|
At(9, 0)
|
||||||
outgoing.AddChild(
|
outgoing.AddChild(
|
||||||
ui.NewText("Connection mode").Bold(true)).
|
ui.NewText("Connection mode",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_HEADER))).
|
||||||
At(10, 0)
|
At(10, 0)
|
||||||
smtpMode := NewSelecter([]string{
|
smtpMode := NewSelecter([]string{
|
||||||
"SMTP over SSL/TLS",
|
"SMTP over SSL/TLS",
|
||||||
"SMTP with STARTTLS",
|
"SMTP with STARTTLS",
|
||||||
"Insecure SMTP",
|
"Insecure SMTP",
|
||||||
}, 0).Chooser(true).OnSelect(func(option string) {
|
}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
|
||||||
switch option {
|
switch option {
|
||||||
case "SMTP over SSL/TLS":
|
case "SMTP over SSL/TLS":
|
||||||
wizard.smtpMode = SMTP_OVER_TLS
|
wizard.smtpMode = SMTP_OVER_TLS
|
||||||
|
@ -346,15 +359,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
wizard.smtpUri()
|
wizard.smtpUri()
|
||||||
})
|
})
|
||||||
outgoing.AddChild(smtpMode).At(11, 0)
|
outgoing.AddChild(smtpMode).At(11, 0)
|
||||||
selecter = NewSelecter([]string{"Previous", "Next"}, 1).
|
selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
|
||||||
OnChoose(wizard.advance)
|
OnChoose(wizard.advance)
|
||||||
outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
|
outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
|
||||||
outgoing.AddChild(wizard.smtpStr).At(13, 0)
|
outgoing.AddChild(wizard.smtpStr).At(13, 0)
|
||||||
outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
|
outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
|
||||||
outgoing.AddChild(
|
outgoing.AddChild(
|
||||||
ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)).
|
ui.NewText("Copy sent messages to 'Sent' folder?",
|
||||||
At(15, 0)
|
conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
|
||||||
copySent := NewSelecter([]string{"Yes", "No"}, 0).
|
copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
|
||||||
Chooser(true).OnChoose(func(option string) {
|
Chooser(true).OnChoose(func(option string) {
|
||||||
switch option {
|
switch option {
|
||||||
case "Yes":
|
case "Yes":
|
||||||
|
@ -380,15 +393,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||||
})
|
})
|
||||||
complete.AddChild(ui.NewText(
|
complete.AddChild(ui.NewText(
|
||||||
"\nConfiguration complete!\n\n" +
|
"\nConfiguration complete!\n\n"+
|
||||||
"You can go back and double check your settings, or choose 'Finish' to\n" +
|
"You can go back and double check your settings, or choose 'Finish' to\n"+
|
||||||
"save your settings to accounts.conf.\n\n" +
|
"save your settings to accounts.conf.\n\n"+
|
||||||
"To add another account in the future, run ':new-account'."))
|
"To add another account in the future, run ':new-account'.",
|
||||||
|
conf.Ui.GetStyle(config.STYLE_DEFAULT)))
|
||||||
selecter = NewSelecter([]string{
|
selecter = NewSelecter([]string{
|
||||||
"Previous",
|
"Previous",
|
||||||
"Finish & open tutorial",
|
"Finish & open tutorial",
|
||||||
"Finish",
|
"Finish",
|
||||||
}, 1).OnChoose(func(option string) {
|
}, 1, conf.Ui).OnChoose(func(option string) {
|
||||||
switch option {
|
switch option {
|
||||||
case "Previous":
|
case "Previous":
|
||||||
wizard.advance("Previous")
|
wizard.advance("Previous")
|
||||||
|
|
|
@ -64,15 +64,14 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon
|
||||||
|
|
||||||
worker, err := worker.NewWorker(acct.Source, logger)
|
worker, err := worker.NewWorker(acct.Source, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)).
|
host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
|
||||||
Color(tcell.ColorDefault, tcell.ColorRed)
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
view.worker = worker
|
view.worker = worker
|
||||||
|
|
||||||
view.dirlist = NewDirectoryList(conf, acct, logger, worker)
|
view.dirlist = NewDirectoryList(conf, acct, logger, worker)
|
||||||
if acctUiConf.SidebarWidth > 0 {
|
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)
|
view.msglist = NewMessageList(conf, logger, aerc)
|
||||||
|
|
|
@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
||||||
|
|
||||||
tabs := ui.NewTabs(&conf.Ui)
|
tabs := ui.NewTabs(&conf.Ui)
|
||||||
|
|
||||||
statusbar := ui.NewStack()
|
statusbar := ui.NewStack(conf.Ui)
|
||||||
statusline := NewStatusLine()
|
statusline := NewStatusLine(conf.Ui)
|
||||||
statusbar.Push(statusline)
|
statusbar.Push(statusline)
|
||||||
|
|
||||||
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
||||||
|
@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
statusbar: statusbar,
|
statusbar: statusbar,
|
||||||
statusline: statusline,
|
statusline: statusline,
|
||||||
prompts: ui.NewStack(),
|
prompts: ui.NewStack(conf.Ui),
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage {
|
||||||
return aerc.statusline.Set(status)
|
return aerc.statusline.Set(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aerc *Aerc) SetError(status string) *StatusMessage {
|
||||||
|
return aerc.statusline.SetError(status)
|
||||||
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
|
func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
|
||||||
return aerc.statusline.Push(text, expiry)
|
return aerc.statusline.Push(text, expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) PushError(text string) {
|
func (aerc *Aerc) PushError(text string) *StatusMessage {
|
||||||
aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed)
|
return aerc.statusline.PushError(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (aerc *Aerc) PushSuccess(text string) *StatusMessage {
|
||||||
|
return aerc.statusline.PushSuccess(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (aerc *Aerc) focus(item ui.Interactive) {
|
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) {
|
func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
|
||||||
chText = make(chan string, 1)
|
chText = make(chan string, 1)
|
||||||
chErr = make(chan error, 1)
|
chErr = make(chan error, 1)
|
||||||
getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) {
|
getPasswd := NewGetPasswd(title, prompt, aerc.conf, func(pw string, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
close(chErr)
|
close(chErr)
|
||||||
close(chText)
|
close(chText)
|
||||||
|
|
|
@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
||||||
|
|
||||||
templateData := templates.ParseTemplateData(defaults, original)
|
templateData := templates.ParseTemplateData(defaults, original)
|
||||||
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
|
||||||
aerc.PushError(fmt.Sprintf("could not complete header: %v", err))
|
aerc.PushError(
|
||||||
|
fmt.Sprintf("could not complete header: %v", err))
|
||||||
worker.Logger.Printf("could not complete header: %v", err)
|
worker.Logger.Printf("could not complete header: %v", err)
|
||||||
}, aerc.Logger())
|
}, aerc.Logger())
|
||||||
layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults)
|
layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
|
||||||
|
|
||||||
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
email, err := ioutil.TempFile("", "aerc-compose-*.eml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
|
||||||
defaults map[string]string) (
|
defaults map[string]string) (
|
||||||
newLayout HeaderLayout,
|
newLayout HeaderLayout,
|
||||||
editors map[string]*headerEditor,
|
editors map[string]*headerEditor,
|
||||||
focusable []ui.MouseableDrawableInteractive,
|
focusable []ui.MouseableDrawableInteractive,
|
||||||
) {
|
) {
|
||||||
layout := conf.Compose.HeaderLayout
|
layout := aerc.conf.Compose.HeaderLayout
|
||||||
editors = make(map[string]*headerEditor)
|
editors = make(map[string]*headerEditor)
|
||||||
focusable = make([]ui.MouseableDrawableInteractive, 0)
|
focusable = make([]ui.MouseableDrawableInteractive, 0)
|
||||||
|
|
||||||
for _, row := range layout {
|
for _, row := range layout {
|
||||||
for _, h := range row {
|
for _, h := range row {
|
||||||
e := newHeaderEditor(h, "")
|
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||||
if conf.Ui.CompletionPopovers {
|
if aerc.conf.Ui.CompletionPopovers {
|
||||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||||
}
|
}
|
||||||
editors[h] = e
|
editors[h] = e
|
||||||
switch h {
|
switch h {
|
||||||
|
@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
|
||||||
for _, h := range []string{"Cc", "Bcc"} {
|
for _, h := range []string{"Cc", "Bcc"} {
|
||||||
if val, ok := defaults[h]; ok && val != "" {
|
if val, ok := defaults[h]; ok && val != "" {
|
||||||
if _, ok := editors[h]; !ok {
|
if _, ok := editors[h]; !ok {
|
||||||
e := newHeaderEditor(h, "")
|
e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
|
||||||
if conf.Ui.CompletionPopovers {
|
if aerc.conf.Ui.CompletionPopovers {
|
||||||
e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
|
e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
|
||||||
}
|
}
|
||||||
editors[h] = e
|
editors[h] = e
|
||||||
focusable = append(focusable, e)
|
focusable = append(focusable, e)
|
||||||
|
@ -259,7 +260,8 @@ func (c *Composer) readSignatureFromFile() []byte {
|
||||||
}
|
}
|
||||||
signature, err := ioutil.ReadFile(sigFile)
|
signature, err := ioutil.ReadFile(sigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile))
|
c.aerc.PushError(
|
||||||
|
fmt.Sprintf(" Error loading signature from file: %v", sigFile))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return signature
|
return signature
|
||||||
|
@ -648,7 +650,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e := newHeaderEditor(header, value)
|
e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
|
||||||
if c.config.Ui.CompletionPopovers {
|
if c.config.Ui.CompletionPopovers {
|
||||||
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
|
e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
|
||||||
}
|
}
|
||||||
|
@ -704,23 +706,27 @@ func (c *Composer) reloadEmail() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type headerEditor struct {
|
type headerEditor struct {
|
||||||
name string
|
name string
|
||||||
focused bool
|
focused bool
|
||||||
input *ui.TextInput
|
input *ui.TextInput
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHeaderEditor(name string, value string) *headerEditor {
|
func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
|
||||||
return &headerEditor{
|
return &headerEditor{
|
||||||
input: ui.NewTextInput(value),
|
input: ui.NewTextInput(value, uiConfig),
|
||||||
name: name,
|
name: name,
|
||||||
|
uiConfig: uiConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (he *headerEditor) Draw(ctx *ui.Context) {
|
func (he *headerEditor) Draw(ctx *ui.Context) {
|
||||||
name := he.name + " "
|
name := he.name + " "
|
||||||
size := runewidth.StringWidth(name)
|
size := runewidth.StringWidth(name)
|
||||||
ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault)
|
defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name)
|
headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||||
|
ctx.Fill(0, 0, size, ctx.Height(), ' ', defaultStyle)
|
||||||
|
ctx.Printf(0, 0, headerStyle, "%s", name)
|
||||||
he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
|
he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,21 +790,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{ui.SIZE_WEIGHT, ui.Const(1)},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
uiConfig := composer.config.Ui
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
grid.AddChild(ui.NewText(err.Error()).
|
grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR)))
|
||||||
Color(tcell.ColorRed, tcell.ColorDefault))
|
grid.AddChild(ui.NewText("Press [q] to close this tab.",
|
||||||
grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0)
|
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0)
|
||||||
} else {
|
} else {
|
||||||
// TODO: source this from actual keybindings?
|
// TODO: source this from actual keybindings?
|
||||||
grid.AddChild(ui.NewText(
|
grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach",
|
||||||
"Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0)
|
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0)
|
||||||
grid.AddChild(ui.NewText("Attachments:").
|
grid.AddChild(ui.NewText("Attachments:",
|
||||||
Reverse(true)).At(1, 0)
|
uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0)
|
||||||
if len(composer.attachments) == 0 {
|
if len(composer.attachments) == 0 {
|
||||||
grid.AddChild(ui.NewText("(none)")).At(2, 0)
|
grid.AddChild(ui.NewText("(none)",
|
||||||
|
uiConfig.GetStyle(config.STYLE_DEFAULT))).At(2, 0)
|
||||||
} else {
|
} else {
|
||||||
for i, a := range composer.attachments {
|
for i, a := range composer.attachments {
|
||||||
grid.AddChild(ui.NewText(a)).At(i+2, 0)
|
grid.AddChild(ui.NewText(a, uiConfig.GetStyle(config.STYLE_DEFAULT))).
|
||||||
|
At(i+2, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,7 +196,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||||
|
dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT))
|
||||||
|
|
||||||
if dirlist.spinner.IsRunning() {
|
if dirlist.spinner.IsRunning() {
|
||||||
dirlist.spinner.Draw(ctx)
|
dirlist.spinner.Draw(ctx)
|
||||||
|
@ -204,7 +205,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(dirlist.dirs) == 0 {
|
if len(dirlist.dirs) == 0 {
|
||||||
style := tcell.StyleDefault
|
style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
|
||||||
ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
|
ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -236,10 +237,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
|
||||||
|
|
||||||
style := tcell.StyleDefault
|
style := tcell.StyleDefault
|
||||||
if name == dirlist.selected {
|
if name == dirlist.selected {
|
||||||
style = style.Reverse(true)
|
style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT)
|
||||||
} else if name == dirlist.selecting {
|
|
||||||
style = style.Reverse(true)
|
|
||||||
style = style.Foreground(tcell.ColorGray)
|
|
||||||
}
|
}
|
||||||
ctx.Fill(0, row, textWidth, 1, ' ', style)
|
ctx.Fill(0, row, textWidth, 1, ' ', style)
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,14 @@ type ExLine struct {
|
||||||
tabcomplete func(cmd string) []string
|
tabcomplete func(cmd string) []string
|
||||||
cmdHistory lib.History
|
cmdHistory lib.History
|
||||||
input *ui.TextInput
|
input *ui.TextInput
|
||||||
|
conf *config.AercConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(),
|
func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(),
|
||||||
tabcomplete func(cmd string) []string,
|
tabcomplete func(cmd string) []string,
|
||||||
cmdHistory lib.History) *ExLine {
|
cmdHistory lib.History) *ExLine {
|
||||||
|
|
||||||
input := ui.NewTextInput("").Prompt(":").Set(cmd)
|
input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd)
|
||||||
if conf.Ui.CompletionPopovers {
|
if conf.Ui.CompletionPopovers {
|
||||||
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +32,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
|
||||||
tabcomplete: tabcomplete,
|
tabcomplete: tabcomplete,
|
||||||
cmdHistory: cmdHistory,
|
cmdHistory: cmdHistory,
|
||||||
input: input,
|
input: input,
|
||||||
|
conf: conf,
|
||||||
}
|
}
|
||||||
input.OnInvalidate(func(d ui.Drawable) {
|
input.OnInvalidate(func(d ui.Drawable) {
|
||||||
exline.Invalidate()
|
exline.Invalidate()
|
||||||
|
@ -41,7 +43,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
|
||||||
func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string),
|
func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string),
|
||||||
tabcomplete func(cmd string) []string) *ExLine {
|
tabcomplete func(cmd string) []string) *ExLine {
|
||||||
|
|
||||||
input := ui.NewTextInput("").Prompt(prompt)
|
input := ui.NewTextInput("", conf.Ui).Prompt(prompt)
|
||||||
if conf.Ui.CompletionPopovers {
|
if conf.Ui.CompletionPopovers {
|
||||||
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,14 +15,17 @@ type GetPasswd struct {
|
||||||
title string
|
title string
|
||||||
prompt string
|
prompt string
|
||||||
input *ui.TextInput
|
input *ui.TextInput
|
||||||
|
conf *config.AercConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPasswd {
|
func NewGetPasswd(title string, prompt string, conf *config.AercConfig,
|
||||||
|
cb func(string, error)) *GetPasswd {
|
||||||
getpasswd := &GetPasswd{
|
getpasswd := &GetPasswd{
|
||||||
callback: cb,
|
callback: cb,
|
||||||
title: title,
|
title: title,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
input: ui.NewTextInput("").Password(true).Prompt("Password: "),
|
conf: conf,
|
||||||
|
input: ui.NewTextInput("", conf.Ui).Password(true).Prompt("Password: "),
|
||||||
}
|
}
|
||||||
getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
|
getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
|
||||||
getpasswd.Invalidate()
|
getpasswd.Invalidate()
|
||||||
|
@ -31,10 +35,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gp *GetPasswd) Draw(ctx *ui.Context) {
|
func (gp *GetPasswd) Draw(ctx *ui.Context) {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT)
|
||||||
ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true))
|
titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE)
|
||||||
ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
|
|
||||||
ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||||
|
ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle)
|
||||||
|
ctx.Printf(1, 0, titleStyle, "%s", gp.title)
|
||||||
|
ctx.Printf(1, 1, defaultStyle, gp.prompt)
|
||||||
gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
|
gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,8 @@ func (ml *MessageList) Invalidate() {
|
||||||
|
|
||||||
func (ml *MessageList) Draw(ctx *ui.Context) {
|
func (ml *MessageList) Draw(ctx *ui.Context) {
|
||||||
ml.height = ctx.Height()
|
ml.height = ctx.Height()
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
|
||||||
|
ml.aerc.SelectedAccount().UiConfig().GetStyle(config.STYLE_MSGLIST_DEFAULT))
|
||||||
|
|
||||||
store := ml.Store()
|
store := ml.Store()
|
||||||
if store == nil {
|
if store == nil {
|
||||||
|
@ -101,38 +102,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
|
||||||
continue
|
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
|
// deleted message
|
||||||
if _, ok := store.Deleted[msg.Uid]; ok {
|
if _, ok := store.Deleted[msg.Uid]; ok {
|
||||||
style = style.Foreground(tcell.ColorGray)
|
so = config.STYLE_MSGLIST_DELETED
|
||||||
}
|
}
|
||||||
// unread message
|
// unread message
|
||||||
seen := false
|
seen := false
|
||||||
|
flagged := false
|
||||||
for _, flag := range msg.Flags {
|
for _, flag := range msg.Flags {
|
||||||
if flag == models.SeenFlag {
|
switch flag {
|
||||||
|
case models.SeenFlag:
|
||||||
seen = true
|
seen = true
|
||||||
|
case models.FlaggedFlag:
|
||||||
|
flagged = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !seen {
|
if !seen {
|
||||||
style = style.Bold(true)
|
so = config.STYLE_MSGLIST_UNREAD
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Fill(0, row, textWidth, 1, ' ', style)
|
if flagged {
|
||||||
|
so = config.STYLE_MSGLIST_FLAGGED
|
||||||
confParams := map[config.ContextType]string{
|
|
||||||
config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
|
|
||||||
config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(),
|
|
||||||
}
|
}
|
||||||
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(
|
fmtStr, args, err := format.ParseMessageFormat(
|
||||||
ml.aerc.SelectedAccount().acct.From,
|
ml.aerc.SelectedAccount().acct.From,
|
||||||
uiConfig.IndexFormat,
|
uiConfig.IndexFormat,
|
||||||
|
@ -342,7 +355,8 @@ func (ml *MessageList) ensureScroll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
|
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
|
||||||
msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage
|
uiConfig := ml.aerc.SelectedAccount().UiConfig()
|
||||||
|
msg := uiConfig.EmptyMessage
|
||||||
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
|
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
|
||||||
tcell.StyleDefault, "%s", msg)
|
uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ type MessageViewer struct {
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
switcher *PartSwitcher
|
switcher *PartSwitcher
|
||||||
msg lib.MessageView
|
msg lib.MessageView
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartSwitcher struct {
|
type PartSwitcher struct {
|
||||||
|
@ -62,9 +63,11 @@ func NewMessageViewer(acct *AccountView,
|
||||||
header, headerHeight := layout.grid(
|
header, headerHeight := layout.grid(
|
||||||
func(header string) ui.Drawable {
|
func(header string) ui.Drawable {
|
||||||
return &HeaderView{
|
return &HeaderView{
|
||||||
|
conf: conf,
|
||||||
Name: header,
|
Name: header,
|
||||||
Value: fmtHeader(msg.MessageInfo(), header,
|
Value: fmtHeader(msg.MessageInfo(), header,
|
||||||
acct.UiConfig().TimestampFormat),
|
acct.UiConfig().TimestampFormat),
|
||||||
|
uiConfig: acct.UiConfig(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -94,15 +97,16 @@ func NewMessageViewer(acct *AccountView,
|
||||||
err := createSwitcher(acct, switcher, conf, msg)
|
err := createSwitcher(acct, switcher, conf, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &MessageViewer{
|
return &MessageViewer{
|
||||||
err: err,
|
err: err,
|
||||||
grid: grid,
|
grid: grid,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
|
uiConfig: acct.UiConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.AddChild(header).At(0, 0)
|
grid.AddChild(header).At(0, 0)
|
||||||
if msg.PGPDetails() != nil {
|
if msg.PGPDetails() != nil {
|
||||||
grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
|
grid.AddChild(NewPGPInfo(msg.PGPDetails(), acct.UiConfig())).At(1, 0)
|
||||||
grid.AddChild(ui.NewFill(' ')).At(2, 0)
|
grid.AddChild(ui.NewFill(' ')).At(2, 0)
|
||||||
grid.AddChild(switcher).At(3, 0)
|
grid.AddChild(switcher).At(3, 0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,6 +120,7 @@ func NewMessageViewer(acct *AccountView,
|
||||||
grid: grid,
|
grid: grid,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
switcher: switcher,
|
switcher: switcher,
|
||||||
|
uiConfig: acct.UiConfig(),
|
||||||
}
|
}
|
||||||
switcher.mv = mv
|
switcher.mv = mv
|
||||||
|
|
||||||
|
@ -224,8 +229,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher,
|
||||||
|
|
||||||
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
||||||
if mv.err != nil {
|
if mv.err != nil {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
|
||||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||||
|
ctx.Printf(0, 0, style, "%s", mv.err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mv.grid.Draw(ctx)
|
mv.grid.Draw(ctx)
|
||||||
|
@ -347,7 +353,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
|
||||||
ps.height = ctx.Height()
|
ps.height = ctx.Height()
|
||||||
y := ctx.Height() - height
|
y := ctx.Height() - height
|
||||||
for i, part := range ps.parts {
|
for i, part := range ps.parts {
|
||||||
style := tcell.StyleDefault.Reverse(ps.selected == i)
|
style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
|
if ps.selected == i {
|
||||||
|
style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
|
||||||
|
}
|
||||||
ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
|
ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
|
||||||
name := fmt.Sprintf("%s/%s",
|
name := fmt.Sprintf("%s/%s",
|
||||||
strings.ToLower(part.part.MIMEType),
|
strings.ToLower(part.part.MIMEType),
|
||||||
|
@ -436,6 +445,7 @@ func (mv *MessageViewer) Focus(focus bool) {
|
||||||
|
|
||||||
type PartViewer struct {
|
type PartViewer struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
|
conf *config.AercConfig
|
||||||
err error
|
err error
|
||||||
fetched bool
|
fetched bool
|
||||||
filter *exec.Cmd
|
filter *exec.Cmd
|
||||||
|
@ -450,6 +460,7 @@ type PartViewer struct {
|
||||||
term *Terminal
|
term *Terminal
|
||||||
selecter *Selecter
|
selecter *Selecter
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
|
@ -519,7 +530,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
{ui.SIZE_WEIGHT, ui.Const(1)},
|
{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) {
|
OnChoose(func(option string) {
|
||||||
switch option {
|
switch option {
|
||||||
case "Save message":
|
case "Save message":
|
||||||
|
@ -532,6 +544,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
grid.AddChild(selecter).At(2, 0)
|
grid.AddChild(selecter).At(2, 0)
|
||||||
|
|
||||||
pv := &PartViewer{
|
pv := &PartViewer{
|
||||||
|
conf: conf,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
index: index,
|
index: index,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
|
@ -543,6 +556,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
term: term,
|
term: term,
|
||||||
selecter: selecter,
|
selecter: selecter,
|
||||||
grid: grid,
|
grid: grid,
|
||||||
|
uiConfig: acct.UiConfig(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if term != nil {
|
if term != nil {
|
||||||
|
@ -661,14 +675,16 @@ func (pv *PartViewer) Invalidate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pv *PartViewer) Draw(ctx *ui.Context) {
|
func (pv *PartViewer) Draw(ctx *ui.Context) {
|
||||||
|
style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
|
styleError := pv.uiConfig.GetStyle(config.STYLE_ERROR)
|
||||||
if pv.filter == nil {
|
if pv.filter == nil {
|
||||||
// TODO: Let them download it directly or something
|
// TODO: Let them download it directly or something
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||||
ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
|
ctx.Printf(0, 0, styleError,
|
||||||
"No filter configured for this mimetype ('%s/%s')",
|
"No filter configured for this mimetype ('%s/%s')",
|
||||||
pv.part.MIMEType, pv.part.MIMESubType,
|
pv.part.MIMEType, pv.part.MIMESubType,
|
||||||
)
|
)
|
||||||
ctx.Printf(0, 2, tcell.StyleDefault,
|
ctx.Printf(0, 2, style,
|
||||||
"You can still :save the message or :pipe it to an external command")
|
"You can still :save the message or :pipe it to an external command")
|
||||||
pv.selecter.Focus(true)
|
pv.selecter.Focus(true)
|
||||||
pv.grid.Draw(ctx)
|
pv.grid.Draw(ctx)
|
||||||
|
@ -679,8 +695,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
|
||||||
pv.fetched = true
|
pv.fetched = true
|
||||||
}
|
}
|
||||||
if pv.err != nil {
|
if pv.err != nil {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
||||||
ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
|
ctx.Printf(0, 0, style, "%s", pv.err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pv.term.Draw(ctx)
|
pv.term.Draw(ctx)
|
||||||
|
@ -702,8 +718,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
|
||||||
|
|
||||||
type HeaderView struct {
|
type HeaderView struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
Name string
|
conf *config.AercConfig
|
||||||
Value string
|
Name string
|
||||||
|
Value string
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
func (hv *HeaderView) Draw(ctx *ui.Context) {
|
||||||
|
@ -711,18 +729,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
|
||||||
size := runewidth.StringWidth(name)
|
size := runewidth.StringWidth(name)
|
||||||
lim := ctx.Width() - size - 1
|
lim := ctx.Width() - size - 1
|
||||||
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
value := runewidth.Truncate(" "+hv.Value, lim, "…")
|
||||||
var (
|
|
||||||
hstyle tcell.Style
|
vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
vstyle tcell.Style
|
hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)
|
||||||
)
|
|
||||||
// TODO: Make this more robust and less dumb
|
// TODO: Make this more robust and less dumb
|
||||||
if hv.Name == "PGP" {
|
if hv.Name == "PGP" {
|
||||||
vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
|
vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||||
hstyle = tcell.StyleDefault.Bold(true)
|
|
||||||
} else {
|
|
||||||
vstyle = tcell.StyleDefault
|
|
||||||
hstyle = tcell.StyleDefault.Bold(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
|
||||||
ctx.Printf(0, 0, hstyle, "%s", name)
|
ctx.Printf(0, 0, hstyle, "%s", name)
|
||||||
ctx.Printf(size, 0, vstyle, "%s", value)
|
ctx.Printf(size, 0, vstyle, "%s", value)
|
||||||
|
|
|
@ -3,40 +3,40 @@ package widgets
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
|
||||||
"golang.org/x/crypto/openpgp"
|
"golang.org/x/crypto/openpgp"
|
||||||
pgperrors "golang.org/x/crypto/openpgp/errors"
|
pgperrors "golang.org/x/crypto/openpgp/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PGPInfo struct {
|
type PGPInfo struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
details *openpgp.MessageDetails
|
details *openpgp.MessageDetails
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo {
|
func NewPGPInfo(details *openpgp.MessageDetails, uiConfig config.UIConfig) *PGPInfo {
|
||||||
return &PGPInfo{details: details}
|
return &PGPInfo{details: details, uiConfig: uiConfig}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
|
func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
|
||||||
errorStyle := tcell.StyleDefault.Background(tcell.ColorRed).
|
errorStyle := p.uiConfig.GetStyle(config.STYLE_ERROR)
|
||||||
Foreground(tcell.ColorWhite).Bold(true)
|
warningStyle := p.uiConfig.GetStyle(config.STYLE_WARNING)
|
||||||
softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true)
|
validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||||
validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
|
|
||||||
// TODO: Nicer prompt for TOFU, fetch from keyserver, etc
|
// TODO: Nicer prompt for TOFU, fetch from keyserver, etc
|
||||||
if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
|
if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
|
||||||
p.details.SignedBy == nil {
|
p.details.SignedBy == nil {
|
||||||
|
|
||||||
x := ctx.Printf(0, 0, softErrorStyle, "*")
|
x := ctx.Printf(0, 0, warningStyle, "*")
|
||||||
x += ctx.Printf(x, 0, tcell.StyleDefault,
|
x += ctx.Printf(x, 0, defaultStyle,
|
||||||
" Signed with unknown key (%8X); authenticity unknown",
|
" Signed with unknown key (%8X); authenticity unknown",
|
||||||
p.details.SignedByKeyId)
|
p.details.SignedByKeyId)
|
||||||
} else if p.details.SignatureError != nil {
|
} else if p.details.SignatureError != nil {
|
||||||
x := ctx.Printf(0, 0, errorStyle, "Invalid signature!")
|
x := ctx.Printf(0, 0, errorStyle, "Invalid signature!")
|
||||||
x += ctx.Printf(x, 0, tcell.StyleDefault.
|
x += ctx.Printf(x, 0, errorStyle,
|
||||||
Foreground(tcell.ColorRed).Bold(true),
|
|
||||||
" This message may have been tampered with! (%s)",
|
" This message may have been tampered with! (%s)",
|
||||||
p.details.SignatureError.Error())
|
p.details.SignatureError.Error())
|
||||||
} else {
|
} else {
|
||||||
|
@ -44,24 +44,26 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
|
||||||
ident := entity.PrimaryIdentity()
|
ident := entity.PrimaryIdentity()
|
||||||
|
|
||||||
x := ctx.Printf(0, 0, validStyle, "✓ Authentic ")
|
x := ctx.Printf(0, 0, validStyle, "✓ Authentic ")
|
||||||
x += ctx.Printf(x, 0, tcell.StyleDefault,
|
x += ctx.Printf(x, 0, defaultStyle,
|
||||||
"Signature from %s (%8X)",
|
"Signature from %s (%8X)",
|
||||||
ident.Name, p.details.SignedByKeyId)
|
ident.Name, p.details.SignedByKeyId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
|
func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
|
||||||
validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
|
||||||
|
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
entity := p.details.DecryptedWith.Entity
|
entity := p.details.DecryptedWith.Entity
|
||||||
ident := entity.PrimaryIdentity()
|
ident := entity.PrimaryIdentity()
|
||||||
|
|
||||||
x := ctx.Printf(0, y, validStyle, "✓ Encrypted ")
|
x := ctx.Printf(0, y, validStyle, "✓ Encrypted ")
|
||||||
x += ctx.Printf(x, y, tcell.StyleDefault,
|
x += ctx.Printf(x, y, defaultStyle,
|
||||||
"To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
|
"To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGPInfo) Draw(ctx *ui.Context) {
|
func (p *PGPInfo) Draw(ctx *ui.Context) {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
|
||||||
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
|
||||||
if p.details.IsSigned && p.details.IsEncrypted {
|
if p.details.IsSigned && p.details.IsEncrypted {
|
||||||
p.DrawSignature(ctx)
|
p.DrawSignature(ctx)
|
||||||
p.DrawEncryption(ctx, 1)
|
p.DrawEncryption(ctx, 1)
|
||||||
|
|
|
@ -3,46 +3,50 @@ package widgets
|
||||||
import (
|
import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Selecter struct {
|
type Selector struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
chooser bool
|
chooser bool
|
||||||
focused bool
|
focused bool
|
||||||
focus int
|
focus int
|
||||||
options []string
|
options []string
|
||||||
|
uiConfig config.UIConfig
|
||||||
|
|
||||||
onChoose func(option string)
|
onChoose func(option string)
|
||||||
onSelect func(option string)
|
onSelect func(option string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSelecter(options []string, focus int) *Selecter {
|
func NewSelector(options []string, focus int, uiConfig config.UIConfig) *Selector {
|
||||||
return &Selecter{
|
return &Selector{
|
||||||
focus: focus,
|
focus: focus,
|
||||||
options: options,
|
options: options,
|
||||||
|
uiConfig: uiConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Chooser(chooser bool) *Selecter {
|
func (sel *Selector) Chooser(chooser bool) *Selector {
|
||||||
sel.chooser = chooser
|
sel.chooser = chooser
|
||||||
return sel
|
return sel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Invalidate() {
|
func (sel *Selector) Invalidate() {
|
||||||
sel.DoInvalidate(sel)
|
sel.DoInvalidate(sel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Draw(ctx *ui.Context) {
|
func (sel *Selector) 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_SELECTOR_DEFAULT))
|
||||||
x := 2
|
x := 2
|
||||||
for i, option := range sel.options {
|
for i, option := range sel.options {
|
||||||
style := tcell.StyleDefault
|
style := sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT)
|
||||||
if sel.focus == i {
|
if sel.focus == i {
|
||||||
if sel.focused {
|
if sel.focused {
|
||||||
style = style.Reverse(true)
|
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_FOCUSED)
|
||||||
} else if sel.chooser {
|
} else if sel.chooser {
|
||||||
style = style.Bold(true)
|
style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_CHOOSER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x += ctx.Printf(x, 1, style, "[%s]", option)
|
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
|
sel.onChoose = fn
|
||||||
return sel
|
return sel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) OnSelect(fn func(option string)) *Selecter {
|
func (sel *Selector) OnSelect(fn func(option string)) *Selector {
|
||||||
sel.onSelect = fn
|
sel.onSelect = fn
|
||||||
return sel
|
return sel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Selected() string {
|
func (sel *Selector) Selected() string {
|
||||||
return sel.options[sel.focus]
|
return sel.options[sel.focus]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Focus(focus bool) {
|
func (sel *Selector) Focus(focus bool) {
|
||||||
sel.focused = focus
|
sel.focused = focus
|
||||||
sel.Invalidate()
|
sel.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sel *Selecter) Event(event tcell.Event) bool {
|
func (sel *Selector) Event(event tcell.Event) bool {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
switch event.Key() {
|
switch event.Key() {
|
|
@ -16,6 +16,7 @@ type Spinner struct {
|
||||||
frame int64 // access via atomic
|
frame int64 // access via atomic
|
||||||
frames []string
|
frames []string
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
|
style tcell.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSpinner(uiConf *config.UIConfig) *Spinner {
|
func NewSpinner(uiConf *config.UIConfig) *Spinner {
|
||||||
|
@ -23,6 +24,7 @@ func NewSpinner(uiConf *config.UIConfig) *Spinner {
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
frame: -1,
|
frame: -1,
|
||||||
frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter),
|
frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter),
|
||||||
|
style: uiConf.GetStyle(config.STYLE_SPINNER),
|
||||||
}
|
}
|
||||||
return &spinner
|
return &spinner
|
||||||
}
|
}
|
||||||
|
@ -70,9 +72,9 @@ func (s *Spinner) Draw(ctx *ui.Context) {
|
||||||
|
|
||||||
cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames)))
|
cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames)))
|
||||||
|
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', s.style)
|
||||||
col := ctx.Width()/2 - len(s.frames[0])/2 + 1
|
col := ctx.Width()/2 - len(s.frames[0])/2 + 1
|
||||||
ctx.Printf(col, 0, tcell.StyleDefault, "%s", s.frames[cur])
|
ctx.Printf(col, 0, s.style, "%s", s.frames[cur])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spinner) Invalidate() {
|
func (s *Spinner) Invalidate() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
"git.sr.ht/~sircmpwn/aerc/lib/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,21 +15,21 @@ type StatusLine struct {
|
||||||
stack []*StatusMessage
|
stack []*StatusMessage
|
||||||
fallback StatusMessage
|
fallback StatusMessage
|
||||||
aerc *Aerc
|
aerc *Aerc
|
||||||
|
uiConfig config.UIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusMessage struct {
|
type StatusMessage struct {
|
||||||
bg tcell.Color
|
style tcell.Style
|
||||||
fg tcell.Color
|
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatusLine() *StatusLine {
|
func NewStatusLine(uiConfig config.UIConfig) *StatusLine {
|
||||||
return &StatusLine{
|
return &StatusLine{
|
||||||
fallback: StatusMessage{
|
fallback: StatusMessage{
|
||||||
bg: tcell.ColorDefault,
|
style: uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||||
fg: tcell.ColorDefault,
|
|
||||||
message: "Idle",
|
message: "Idle",
|
||||||
},
|
},
|
||||||
|
uiConfig: uiConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,9 +42,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
|
||||||
if len(status.stack) != 0 {
|
if len(status.stack) != 0 {
|
||||||
line = status.stack[len(status.stack)-1]
|
line = status.stack[len(status.stack)-1]
|
||||||
}
|
}
|
||||||
style := tcell.StyleDefault.
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style)
|
||||||
Background(line.bg).Foreground(line.fg).Reverse(true)
|
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
|
|
||||||
pendingKeys := ""
|
pendingKeys := ""
|
||||||
if status.aerc != nil {
|
if status.aerc != nil {
|
||||||
for _, pendingKey := range status.aerc.pendingKeys {
|
for _, pendingKey := range status.aerc.pendingKeys {
|
||||||
|
@ -51,13 +50,21 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
|
message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
|
||||||
ctx.Printf(0, 0, style, "%s%s", message, pendingKeys)
|
ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (status *StatusLine) Set(text string) *StatusMessage {
|
func (status *StatusLine) Set(text string) *StatusMessage {
|
||||||
status.fallback = StatusMessage{
|
status.fallback = StatusMessage{
|
||||||
bg: tcell.ColorDefault,
|
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||||
fg: tcell.ColorDefault,
|
message: text,
|
||||||
|
}
|
||||||
|
status.Invalidate()
|
||||||
|
return &status.fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (status *StatusLine) SetError(text string) *StatusMessage {
|
||||||
|
status.fallback = StatusMessage{
|
||||||
|
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR),
|
||||||
message: text,
|
message: text,
|
||||||
}
|
}
|
||||||
status.Invalidate()
|
status.Invalidate()
|
||||||
|
@ -66,8 +73,7 @@ func (status *StatusLine) Set(text string) *StatusMessage {
|
||||||
|
|
||||||
func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
|
func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
|
||||||
msg := &StatusMessage{
|
msg := &StatusMessage{
|
||||||
bg: tcell.ColorDefault,
|
style: status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
|
||||||
fg: tcell.ColorDefault,
|
|
||||||
message: text,
|
message: text,
|
||||||
}
|
}
|
||||||
status.stack = append(status.stack, msg)
|
status.stack = append(status.stack, msg)
|
||||||
|
@ -85,6 +91,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
|
||||||
return msg
|
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() {
|
func (status *StatusLine) Expire() {
|
||||||
status.stack = nil
|
status.stack = nil
|
||||||
}
|
}
|
||||||
|
@ -93,7 +111,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) {
|
||||||
status.aerc = aerc
|
status.aerc = aerc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) {
|
func (msg *StatusMessage) Color(style tcell.Style) {
|
||||||
msg.bg = bg
|
msg.style = style
|
||||||
msg.fg = fg
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@ import (
|
||||||
type TabHost interface {
|
type TabHost interface {
|
||||||
BeginExCommand(cmd string)
|
BeginExCommand(cmd string)
|
||||||
SetStatus(status string) *StatusMessage
|
SetStatus(status string) *StatusMessage
|
||||||
|
SetError(err string) *StatusMessage
|
||||||
PushStatus(text string, expiry time.Duration) *StatusMessage
|
PushStatus(text string, expiry time.Duration) *StatusMessage
|
||||||
|
PushError(text string) *StatusMessage
|
||||||
|
PushSuccess(text string) *StatusMessage
|
||||||
Beep()
|
Beep()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue