@@ 22,82 22,26 @@ import (
"io/ioutil"
"os"
"path/filepath"
- "strconv"
"strings"
"time"
"git.sr.ht/~emersion/go-scfg"
- "github.com/gdamore/tcell/v2"
- "gitlab.com/tslocum/cbind"
)
type params int
const (
- one params = iota
+ none params = iota
+ one
multiple
)
-const (
- // config names
- autoUpdateInterval = "auto-update-interval"
-
- // config key command names
- keyCommands = "key-commands"
- cmdMode = "cmd-mode"
- enter = "enter"
- escape = "escape"
- left = "left"
- down = "down"
- up = "up"
- right = "right"
- pageDown = "page-down"
- pageUp = "page-up"
- download = "download"
- markFavorite = "mark-favorite"
- markRead = "mark-read"
- markUnfavorite = "unmark-favorite"
- markUnread = "mark-unread"
- openURL = "open-url"
- play = "play"
- listPage = "list-page"
- subscriptionPage = "subscription-page"
- favoritePage = "favorite-page"
- helpPage = "help-page"
- searchFeed = "search-feed"
- searchItem = "search-item"
-
- // config browser names
- browser = "browser"
- navigate = "navigate"
-
- // config podcast names
- podcast = "podcast"
- autoDownload = "auto-download"
- externalPlayer = "external-player"
-
- // config theme names
- theme = "theme"
- audioIcon = "audio-icon"
- errorColor = "error-color"
- readColor = "read-color"
- separatorColor = "separator-color"
- backgroundColor = "background-color"
- foregroundColor = "foreground-color"
- commandLine = "command-line"
- content = "content"
- tree = "tree"
- list = "list"
- titleBar = "title-bar"
- statusLine = "status-line"
-)
-
// Config contains all relevant configuration data for the application
type Config struct {
AutoUpdateInterval time.Duration
Browser *Browser
Theme Theme
- Podcast Podcast
+ Podcast *Podcast
CMDMode *KeyCMD
Enter *KeyCMD
Escape *KeyCMD
@@ 122,52 66,12 @@ type Config struct {
HelpPage *KeyCMD
}
-// Theme config
-type Theme struct {
- AudioIcon string
- ErrColor tcell.Color
- SeparatorColor tcell.Color
- ReadColor tcell.Color
- CMDBGColor tcell.Color
- CMDFGColor tcell.Color
- StatusBGColor tcell.Color
- StatusFGColor tcell.Color
- TitleBGColor tcell.Color
- TitleFGColor tcell.Color
- ListBGColor tcell.Color
- ListFGColor tcell.Color
- TreeBGColor tcell.Color
- TreeFGColor tcell.Color
- ContentBGColor tcell.Color
- ContentFGColor tcell.Color
-}
-
-// KeyCMD contains everything needs to set the KeyBinding
-type KeyCMD struct {
- Text string
- Rune rune
- Key tcell.Key
- Mod tcell.ModMask
-}
-
// Command config
type Command struct {
Bin string
Args []string
}
-// Podcast config
-type Podcast struct {
- AutoDownload bool
- ExternalPlayer *Command
-}
-
-// Browser config
-type Browser struct {
- HTTP *Command
- Gemini *Command
-}
-
// Load will load the config into the application, if it does not exist it will
// create a default one. If an error occurs reading the config, it will use
// the default one.
@@ 185,586 89,105 @@ func Load(configDir string) (*Config, error) {
return nil, err
}
- errors := make(map[error]bool)
- c := &Config{}
-
- if val, err := getValue(autoUpdateInterval, cfg, false, one); err != nil {
- errors[err] = false
- } else if val == nil || val[0] == "" {
- c.AutoUpdateInterval = 0
- } else {
- t, err := time.ParseDuration(val[0])
- if err != nil {
- errors[err] = false
- } else {
- c.AutoUpdateInterval = t
- }
- }
-
- if val, err := processPodcast(cfg); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- c.Podcast = *val
- }
-
- if val, err := processBrowser(cfg); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- c.Browser = val
+ c := &Config{
+ AutoUpdateInterval: 0,
}
- if val, err := processKeyCommands(cfg); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- c.CMDMode = val[cmdMode]
- c.Enter = val[enter]
- c.Escape = val[escape]
- c.HelpPage = val[helpPage]
- c.ListPage = val[listPage]
- c.SubscriptionPage = val[subscriptionPage]
- c.FavoritePage = val[favoritePage]
- c.NavLeft = val[left]
- c.NavUp = val[up]
- c.NavDown = val[down]
- c.NavRight = val[right]
- c.NavPageUp = val[pageUp]
- c.NavPageDown = val[pageDown]
- c.Play = val[play]
- c.Download = val[download]
- c.OpenURL = val[openURL]
- c.MarkRead = val[markRead]
- c.MarkUnread = val[markUnread]
- c.MarkFavorite = val[markFavorite]
- c.UnmarkFavorite = val[markUnfavorite]
- c.SearchFeed = val[searchFeed]
- c.SearchItem = val[searchItem]
- }
-
- if val, err := processTheme(cfg); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- c.Theme = *val
- }
-
- var eb strings.Builder
- for err := range errors {
- eb.WriteString(fmt.Sprintf("%s\n", err.Error()))
- }
-
- if eb.String() != "" {
- return nil, fmt.Errorf("errors found in the config:\n%s", eb.String())
- }
-
- return c, nil
-}
-
-func createKeyCMD(key string) (*KeyCMD, error) {
- m, k, c, err := cbind.Decode(key)
- if err != nil {
+ if err := parse(c, cfg); err != nil {
return nil, err
}
- return &KeyCMD{
- Text: key,
- Key: k,
- Rune: c,
- Mod: m,
- }, nil
+ return c, nil
}
-func processBrowser(cfg scfg.Block) (*Browser, map[error]bool) {
+func parse(cfg *Config, block scfg.Block) error {
+ c := Config{}
errors := make(map[error]bool)
-
- d := cfg.GetAll(browser)
- if d == nil {
- return nil, nil
- }
-
- b := &Browser{}
- strs := make(map[string]bool)
- for _, a := range d {
- if len(a.Params) < 1 {
- errors[fmt.Errorf("value is required for %s", browser)] = false
- continue
- }
-
- p := a.Params[0]
-
- if _, ok := strs[p]; ok {
- errors[fmt.Errorf("%s has a duplicate %s", browser, p)] = false
- continue
- }
-
- ac := a.Children
-
- v, err := getValue(navigate, ac, true, multiple)
- if err != nil {
- errors[err] = false
- continue
- } else if len(v) < 2 {
- errors[fmt.Errorf("at least 2 elements are required for %s", browser)] = false
- continue
- }
-
- var found bool
- var bin string
- args := make([]string, len(v)-1)
-
- for i, a := range v {
- if i == 0 {
- bin = a
- continue
- }
-
- args[i-1] = a
- if a == "[URL]" {
- found = true
+ var fkeys, fTheme bool
+
+ for _, dir := range block {
+ switch n := dir.Name; n {
+ case "auto-update-interval":
+ if err := validateDirective(n, dir, one); err != nil {
+ errors[err] = false
+ } else if dir.Params != nil && dir.Params[0] != "" {
+ t, err := time.ParseDuration(dir.Params[0])
+ if err != nil {
+ errors[err] = false
+ } else {
+ c.AutoUpdateInterval = t
+ }
}
- }
-
- switch p {
- case "http":
- if !found {
- errors[fmt.Errorf(`[URL] must be one of the args provided for %s "http"`, browser)] = false
- } else {
- b.HTTP = &Command{
- Bin: bin,
- Args: args,
+ case "browser":
+ if err := parseBrowser(cfg, dir); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
}
}
- case "gemini":
- if !found {
- errors[fmt.Errorf(`[URL] must be one of the args provided for %s "gemini"`, browser)] = false
- } else {
- b.Gemini = &Command{
- Bin: bin,
- Args: args,
+ case "key-commands":
+ fkeys = true
+ if err := parseKeyCommands(cfg, dir); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
}
}
-
- default:
- errors[fmt.Errorf("unsupported protocol provided for %s", browser)] = false
- }
- }
-
- return b, errors
-}
-
-func processPodcast(cfg scfg.Block) (*Podcast, map[error]bool) {
- errors := make(map[error]bool)
-
- d := cfg.Get(podcast)
- if d == nil {
- return &Podcast{AutoDownload: false}, nil
- }
-
- dc := d.Children
-
- p := &Podcast{}
-
- if v, err := getValue(autoDownload, dc, false, one); err != nil {
- errors[err] = false
- } else if v == nil || v[0] == "" {
- p.AutoDownload = false
- } else {
- if b, err := strconv.ParseBool(v[0]); err != nil {
- errors[err] = false
- } else {
- p.AutoDownload = b
- }
- }
-
- if v, err := getValue(externalPlayer, dc, false, multiple); err != nil {
- errors[err] = false
- } else if len(v) < 2 {
- errors[fmt.Errorf("at least 2 elements are required for %s", externalPlayer)] = false
- } else {
- e := &Command{
- Args: make([]string, len(v)-1),
- }
-
- found := false
- for i, a := range v {
- if i == 0 {
- e.Bin = a
- continue
+ case "podcast":
+ if err := parsePodcast(cfg, dir); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
}
-
- e.Args[i-1] = a
- if a == "[FILE]" {
- found = true
+ case "theme":
+ fTheme = true
+ if err := parseTheme(cfg, dir); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
}
+ default:
+ errors[fmt.Errorf("unknown directive %q", n)] = false
}
-
- if !found {
- errors[fmt.Errorf("[FILE] must be one of the args provided for %s", externalPlayer)] = false
- } else {
- p.ExternalPlayer = e
- }
- }
-
- return p, errors
-}
-
-func processKeyCommands(cfg scfg.Block) (map[string]*KeyCMD, map[error]bool) {
- keys := make(map[string]*KeyCMD)
- errors := make(map[error]bool)
-
- d := cfg.Get(keyCommands)
- if d == nil {
- errors[fmt.Errorf("%s is a required", keyCommands)] = false
- return nil, errors
- }
-
- dc := d.Children
-
- if k, err := createKeyCMD(":"); err != nil {
- errors[err] = false
- } else {
- keys[cmdMode] = k
- }
-
- if k, err := createKeyCMD("Enter"); err != nil {
- errors[err] = false
- } else {
- keys[enter] = k
- }
-
- if k, err := createKeyCMD("Esc"); err != nil {
- errors[err] = false
- } else {
- keys[escape] = k
- }
-
- if k, err := createKeyCMD("?"); err != nil {
- errors[err] = false
- } else {
- keys[searchFeed] = k
- }
-
- if k, err := createKeyCMD("/"); err != nil {
- errors[err] = false
- } else {
- keys[searchItem] = k
- }
-
- if v, err := getValue(helpPage, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[helpPage] = k
- }
-
- if v, err := getValue(down, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[down] = k
- }
-
- if v, err := getValue(download, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[download] = k
- }
-
- if v, err := getValue(favoritePage, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[favoritePage] = k
- }
-
- if v, err := getValue(left, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[left] = k
- }
-
- if v, err := getValue(listPage, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[listPage] = k
- }
-
- if v, err := getValue(markFavorite, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[markFavorite] = k
- }
-
- if v, err := getValue(markRead, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[markRead] = k
- }
-
- if v, err := getValue(markUnfavorite, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[markUnfavorite] = k
- }
-
- if v, err := getValue(markUnread, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[markUnread] = k
- }
-
- if v, err := getValue(openURL, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[openURL] = k
- }
-
- if v, err := getValue(pageDown, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[pageDown] = k
- }
-
- if v, err := getValue(pageUp, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[pageUp] = k
- }
-
- if v, err := getValue(play, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[play] = k
- }
-
- if v, err := getValue(right, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[right] = k
- }
-
- if v, err := getValue(subscriptionPage, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[subscriptionPage] = k
- }
-
- if v, err := getValue(up, dc, true, one); err != nil {
- errors[err] = false
- } else if k, err := createKeyCMD(v[0]); err != nil {
- errors[err] = false
- } else {
- keys[up] = k
- }
-
- return keys, errors
-}
-
-func processTheme(cfg scfg.Block) (*Theme, map[error]bool) {
- errors := make(map[error]bool)
-
- d := cfg.Get(theme)
- if d == nil {
- errors[fmt.Errorf("%s is a required", theme)] = false
- return nil, errors
- }
-
- dc := d.Children
-
- t := &Theme{}
-
- if v, err := getValue(audioIcon, dc, true, one); err != nil {
- errors[err] = false
- } else {
- t.AudioIcon = v[0]
- }
-
- bg := ""
- if v, err := getValue(backgroundColor, dc, true, one); err != nil {
- errors[err] = false
- } else {
- bg = v[0]
- }
-
- fg := ""
- if v, err := getValue(foregroundColor, dc, true, one); err != nil {
- errors[err] = false
- } else {
- fg = v[0]
- }
-
- if v, err := getValue(errorColor, dc, true, one); err != nil {
- errors[err] = false
- } else {
- t.ErrColor = tcell.GetColor(v[0])
- }
-
- if v, err := getValue(readColor, dc, true, one); err != nil {
- errors[err] = false
- } else {
- t.ReadColor = tcell.GetColor(v[0])
- }
-
- if v, err := getValue(separatorColor, dc, true, one); err != nil {
- errors[err] = false
- } else {
- t.SeparatorColor = tcell.GetColor(v[0])
- }
-
- if bga, fga, err := getChildTheme(titleBar, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.TitleBGColor = tcell.GetColor(bga)
- t.TitleFGColor = tcell.GetColor(fga)
- }
-
- if bga, fga, err := getChildTheme(statusLine, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.StatusBGColor = tcell.GetColor(bga)
- t.StatusFGColor = tcell.GetColor(fga)
- }
-
- if bga, fga, err := getChildTheme(commandLine, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.CMDBGColor = tcell.GetColor(bga)
- t.CMDFGColor = tcell.GetColor(fga)
- }
-
- if bga, fga, err := getChildTheme(content, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.ContentBGColor = tcell.GetColor(bga)
- t.ContentFGColor = tcell.GetColor(fga)
}
- if bga, fga, err := getChildTheme(list, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.ListBGColor = tcell.GetColor(bga)
- t.ListFGColor = tcell.GetColor(fga)
+ if !fkeys {
+ errors[fmt.Errorf("key-commands is a required directive")] = false
}
-
- if bga, fga, err := getChildTheme(tree, bg, fg, dc, false, one); len(err) > 0 {
- for k, v := range err {
- errors[k] = v
- }
- } else {
- t.TreeBGColor = tcell.GetColor(bga)
- t.TreeFGColor = tcell.GetColor(fga)
+ if !fTheme {
+ errors[fmt.Errorf("theme is a required directive")] = false
}
- return t, errors
-}
-
-func getChildTheme(name string, bg string, fg string, c scfg.Block, required bool, p params) (string, string, map[error]bool) {
- errors := make(map[error]bool)
-
- d := c.GetAll(name)
- if d == nil || len(d) < 1 {
- if required {
- errors[fmt.Errorf("%s is a required", name)] = false
- return "", "", errors
- }
- return bg, fg, nil
- } else if p == one && len(d) > 1 {
- errors[fmt.Errorf("more than one %s exists", name)] = false
- return "", "", errors
- }
-
- dc := d[0].Children
-
- bga := ""
- if v, err := getValue(backgroundColor, dc, true, one); err != nil {
- errors[fmt.Errorf("%s: %s", name, err.Error())] = false
- } else {
- bga = v[0]
+ var eb strings.Builder
+ for err := range errors {
+ eb.WriteString(fmt.Sprintf("%s\n", err.Error()))
}
- fga := ""
- if v, err := getValue(foregroundColor, dc, true, one); err != nil {
- errors[fmt.Errorf("%s: %s", name, err.Error())] = false
- } else {
- fga = v[0]
+ if eb.String() != "" {
+ return fmt.Errorf("errors found in the config:\n%s", eb.String())
}
- return bga, fga, errors
+ return nil
}
-func getValue(name string, b scfg.Block, required bool, p params) ([]string, error) {
- d := b.GetAll(name)
- if d == nil || len(d) < 1 {
- if !required {
- return nil, nil
- }
- return nil, fmt.Errorf("%s is required", name)
- } else if len(d) > 1 {
- return nil, fmt.Errorf("more than one %s exists", name)
- }
-
- o := d[0]
-
+func validateDirective(name string, dir *scfg.Directive, p params) error {
switch p {
+ case none:
+ if dir.Params != nil || len(dir.Params) > 0 {
+ return fmt.Errorf("no value should be present for %s", name)
+ }
case one:
- if o.Params == nil || len(o.Params) == 0 {
- return nil, fmt.Errorf("a value must be present for %s", name)
+ if dir.Params == nil || len(dir.Params) == 0 {
+ return fmt.Errorf("a value must be present for %s", name)
}
- if len(o.Params) > 1 {
- return nil, fmt.Errorf("only one value is allowed for %s", name)
+ if len(dir.Params) > 1 {
+ return fmt.Errorf("only one value is allowed for %s", name)
}
case multiple:
- if o.Params == nil || len(o.Params) == 0 {
- return nil, fmt.Errorf("value(s) must be present for %s", name)
+ if dir.Params == nil || len(dir.Params) == 0 {
+ return fmt.Errorf("value(s) must be present for %s", name)
}
}
- return o.Params, nil
+ return nil
}
func defaultConfig() []byte {
@@ 0,0 1,279 @@
+// This file is part of beagles.
+//
+// Copyright © 2020-2021 Chris Palmer <chris@red-oxide.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package config
+
+import (
+ "fmt"
+
+ "git.sr.ht/~emersion/go-scfg"
+ "github.com/gdamore/tcell/v2"
+ "gitlab.com/tslocum/cbind"
+)
+
+// KeyCMD contains everything needs to set the KeyBinding
+type KeyCMD struct {
+ Text string
+ Rune rune
+ Key tcell.Key
+ Mod tcell.ModMask
+}
+
+func createKeyCMD(key string) (*KeyCMD, error) {
+ m, k, c, err := cbind.Decode(key)
+ if err != nil {
+ return nil, err
+ }
+
+ return &KeyCMD{
+ Text: key,
+ Key: k,
+ Rune: c,
+ Mod: m,
+ }, nil
+}
+
+func parseKeyCommands(cfg *Config, dir *scfg.Directive) map[error]bool {
+ errors := make(map[error]bool)
+
+ if k, err := createKeyCMD(":"); err != nil {
+ errors[err] = false
+ } else {
+ cfg.CMDMode = k
+ }
+
+ if k, err := createKeyCMD("Enter"); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Enter = k
+ }
+
+ if k, err := createKeyCMD("Esc"); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Escape = k
+ }
+
+ if k, err := createKeyCMD("?"); err != nil {
+ errors[err] = false
+ } else {
+ cfg.SearchFeed = k
+ }
+
+ if k, err := createKeyCMD("/"); err != nil {
+ errors[err] = false
+ } else {
+ cfg.SearchItem = k
+ }
+
+ for _, d := range dir.Children {
+ switch n := d.Name; n {
+ case "down":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavDown = k
+ }
+ case "download":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Download = k
+ }
+ case "favorite-page":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.FavoritePage = k
+ }
+ case "help-page":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.HelpPage = k
+ }
+ case "left":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavLeft = k
+ }
+ case "list-page":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.ListPage = k
+ }
+ case "mark-favorite":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.MarkFavorite = k
+ }
+ case "mark-read":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.MarkRead = k
+ }
+ case "mark-unread":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.MarkUnread = k
+ }
+ case "open-url":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.OpenURL = k
+ }
+ case "page-down":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavPageDown = k
+ }
+ case "page-up":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavPageUp = k
+ }
+ case "play":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Play = k
+ }
+ case "right":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavRight = k
+ }
+ case "subscription-page":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.SubscriptionPage = k
+ }
+ case "unmark-favorite":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.UnmarkFavorite = k
+ }
+ case "up":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else if k, err := createKeyCMD(d.Params[0]); err != nil {
+ errors[err] = false
+ } else {
+ cfg.NavUp = k
+ }
+ default:
+ errors[fmt.Errorf("unknown directive within keycommands %q", n)] = false
+ }
+ }
+
+ if cfg.NavDown == nil {
+ errors[fmt.Errorf("down is a required directive for key-commands")] = false
+ }
+ if cfg.Download == nil {
+ errors[fmt.Errorf("download is a required directive for key-commands")] = false
+ }
+ if cfg.FavoritePage == nil {
+ errors[fmt.Errorf("favorite-page is a required directive for key-commands")] = false
+ }
+ if cfg.HelpPage == nil {
+ errors[fmt.Errorf("help-page is a required directive for key-commands")] = false
+ }
+ if cfg.NavLeft == nil {
+ errors[fmt.Errorf("left is a required directive for key-commands")] = false
+ }
+ if cfg.ListPage == nil {
+ errors[fmt.Errorf("list-page is a required directive for key-commands")] = false
+ }
+ if cfg.MarkFavorite == nil {
+ errors[fmt.Errorf("mark-favorite is a required directive for key-commands")] = false
+ }
+ if cfg.MarkRead == nil {
+ errors[fmt.Errorf("mark-read is a required directive for key-commands")] = false
+ }
+ if cfg.MarkUnread == nil {
+ errors[fmt.Errorf("mark-unread is a required directive for key-commands")] = false
+ }
+ if cfg.OpenURL == nil {
+ errors[fmt.Errorf("open-url is a required directive for key-commands")] = false
+ }
+ if cfg.NavPageDown == nil {
+ errors[fmt.Errorf("page-down is a required directive for key-commands")] = false
+ }
+ if cfg.NavPageUp == nil {
+ errors[fmt.Errorf("page-up is a required directive for key-commands")] = false
+ }
+ if cfg.Play == nil {
+ errors[fmt.Errorf("play is a required directive for key-commands")] = false
+ }
+ if cfg.NavRight == nil {
+ errors[fmt.Errorf("right is a required directive for key-commands")] = false
+ }
+ if cfg.SubscriptionPage == nil {
+ errors[fmt.Errorf("subscription-page is a required directive for key-commands")] = false
+ }
+ if cfg.UnmarkFavorite == nil {
+ errors[fmt.Errorf("unmark-favorite is a required directive for key-commands")] = false
+ }
+ if cfg.NavUp == nil {
+ errors[fmt.Errorf("up is a required directive for key-commands")] = false
+ }
+
+ return errors
+}
@@ 0,0 1,239 @@
+// This file is part of beagles.
+//
+// Copyright © 2020-2021 Chris Palmer <chris@red-oxide.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package config
+
+import (
+ "fmt"
+
+ "git.sr.ht/~emersion/go-scfg"
+ "github.com/gdamore/tcell/v2"
+)
+
+// Theme config
+type Theme struct {
+ AudioIcon string
+ ErrColor tcell.Color
+ SeparatorColor tcell.Color
+ ReadColor tcell.Color
+ CMDBGColor tcell.Color
+ CMDFGColor tcell.Color
+ StatusBGColor tcell.Color
+ StatusFGColor tcell.Color
+ TitleBGColor tcell.Color
+ TitleFGColor tcell.Color
+ ListBGColor tcell.Color
+ ListFGColor tcell.Color
+ TreeBGColor tcell.Color
+ TreeFGColor tcell.Color
+ ContentBGColor tcell.Color
+ ContentFGColor tcell.Color
+}
+
+func parseTheme(cfg *Config, dir *scfg.Directive) map[error]bool {
+ errors := make(map[error]bool)
+ cfg.Theme = Theme{}
+
+ var bg, fg string
+ var faudio, fread, ferr, fsep, ftitle, fstat, fcmd, fcnt, flist, ftree bool
+ for _, d := range dir.Children {
+ switch n := d.Name; n {
+ case "audio-icon":
+ faudio = true
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Theme.AudioIcon = d.Params[0]
+ }
+ case "background-color":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ bg = d.Params[0]
+ }
+ case "command-line":
+ fcmd = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.CMDBGColor = tcell.GetColor(bga)
+ cfg.Theme.CMDFGColor = tcell.GetColor(fga)
+ }
+ case "content":
+ fcnt = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.ContentBGColor = tcell.GetColor(bga)
+ cfg.Theme.ContentFGColor = tcell.GetColor(fga)
+ }
+ case "error-color":
+ ferr = true
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Theme.ErrColor = tcell.GetColor(d.Params[0])
+ }
+ case "foreground-color":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ fg = d.Params[0]
+ }
+ case "list":
+ flist = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.ListBGColor = tcell.GetColor(bga)
+ cfg.Theme.ListFGColor = tcell.GetColor(fga)
+ }
+ case "read-color":
+ fread = true
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Theme.ReadColor = tcell.GetColor(d.Params[0])
+ }
+ case "separator-color":
+ fsep = true
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ cfg.Theme.SeparatorColor = tcell.GetColor(d.Params[0])
+ }
+ case "status-bar":
+ fstat = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.StatusBGColor = tcell.GetColor(bga)
+ cfg.Theme.StatusFGColor = tcell.GetColor(fga)
+ }
+ case "title-bar":
+ ftitle = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.TitleBGColor = tcell.GetColor(bga)
+ cfg.Theme.TitleFGColor = tcell.GetColor(fga)
+ }
+ case "tree":
+ ftree = true
+ if bga, fga, err := parseChildTheme(n, d); len(err) > 0 {
+ for k, v := range err {
+ errors[k] = v
+ }
+ } else {
+ cfg.Theme.TreeBGColor = tcell.GetColor(bga)
+ cfg.Theme.TreeFGColor = tcell.GetColor(fga)
+ }
+ default:
+ errors[fmt.Errorf("unknown directive within theme %q", n)] = false
+ }
+ }
+
+ if bg == "" {
+ errors[fmt.Errorf("background-color is a required directive for theme")] = false
+ }
+ if fg == "" {
+ errors[fmt.Errorf("foreground-color is a required directive for theme")] = false
+ }
+ if !faudio {
+ errors[fmt.Errorf("audio-icon is a required directive for theme")] = false
+ }
+ if !ferr {
+ errors[fmt.Errorf("error-color is a required directive for theme")] = false
+ }
+ if !fread {
+ errors[fmt.Errorf("read-color is a required directive for theme")] = false
+ }
+ if !fsep {
+ errors[fmt.Errorf("separator-color is a required directive for theme")] = false
+ }
+
+ if !fcmd && bg != "" && fg != "" {
+ cfg.Theme.CMDBGColor = tcell.GetColor(bg)
+ cfg.Theme.CMDFGColor = tcell.GetColor(fg)
+ }
+ if !fcnt && bg != "" && fg != "" {
+ cfg.Theme.ContentBGColor = tcell.GetColor(bg)
+ cfg.Theme.ContentFGColor = tcell.GetColor(fg)
+ }
+ if !flist && bg != "" && fg != "" {
+ cfg.Theme.ListBGColor = tcell.GetColor(bg)
+ cfg.Theme.ListFGColor = tcell.GetColor(fg)
+ }
+ if !fstat && bg != "" && fg != "" {
+ cfg.Theme.StatusBGColor = tcell.GetColor(bg)
+ cfg.Theme.StatusFGColor = tcell.GetColor(fg)
+ }
+ if !ftitle && bg != "" && fg != "" {
+ cfg.Theme.TitleBGColor = tcell.GetColor(bg)
+ cfg.Theme.TitleFGColor = tcell.GetColor(fg)
+ }
+ if !ftree && bg != "" && fg != "" {
+ cfg.Theme.TreeBGColor = tcell.GetColor(bg)
+ cfg.Theme.TreeFGColor = tcell.GetColor(fg)
+ }
+
+ return errors
+}
+
+func parseChildTheme(name string, dir *scfg.Directive) (string, string, map[error]bool) {
+ errors := make(map[error]bool)
+
+ var bg, fg string
+ for _, d := range dir.Children {
+ switch n := d.Name; n {
+ case "background-color":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ bg = d.Params[0]
+ }
+ case "foreground-color":
+ if err := validateDirective(n, d, one); err != nil {
+ errors[err] = false
+ } else {
+ fg = d.Params[0]
+ }
+ default:
+ errors[fmt.Errorf("unknown directive within %s %q", name, n)] = false
+ }
+ }
+
+ if bg == "" {
+ errors[fmt.Errorf("background-color is a required directive for %s", name)] = false
+ }
+
+ if fg == "" {
+ errors[fmt.Errorf("foreground-color is a required directive for %s", name)] = false
+ }
+
+ return bg, fg, errors
+}