Browse Source

config: config is now accessible throughout the program

Benton Edmondson 1 year ago
parent
commit
c433ebb8ac
5 changed files with 127 additions and 51 deletions
  1. 97 19
      config/config.go
  2. 2 2
      jtp/jtp.go
  3. 4 9
      main.go
  4. 9 9
      style/style.go
  5. 15 12
      ui/ui.go

+ 97 - 19
config/config.go

@@ -5,33 +5,62 @@ import (
 	"fmt"
 	"github.com/BurntSushi/toml"
 	"os"
+	"strings"
+	"strconv"
+	"time"
 )
 
 type Config struct {
-	Context   int      `toml:"context"`
-	Timeout   int      `toml:"timeout"`
-	Feeds     feeds    `toml:"feeds"`
-	Algos     algos    `toml:"algos"`
-	MediaHook []string `toml:"media_hook"`
+	Feeds     map[string][]string    `toml:"feeds"`
+	Media	  struct {
+		Hook []string `toml:"hook"`
+	}	`toml:"media"`
+	Style	  struct {
+		Context int `toml:"context"`
+		Colors struct {
+			Primary string `toml:"primary"`
+			Error string `toml:"error"`
+			Highlight string `toml:"highlight"`
+			Code string `toml:"code"`
+		} `toml:"colors"`
+	} `toml:"style"`
+	Network   struct {
+		Timeout time.Duration `toml:"timeout_seconds"`
+	} `toml:"network"`
 }
 
-type feeds = map[string][]string
-type algos = map[string]struct {
-	Server string `toml:"server"`
-	Query  string `toml:"query"`
+var Parsed *Config = nil
+
+/* I use the init function here because everyone who imports config needs
+   the config to be parsed before starting, and the config should only be parsed once.
+   It seems like a good use case. It is slightly ugly to have printing/exiting
+   code this deep in the program, and for it to not be referenced at the top level,
+   but ultimately it's not a big deal. */
+func init() {
+	var err error
+	location := location()
+	if Parsed, err = parse(location); err != nil {
+		os.Stderr.WriteString(fmt.Errorf("failed to parse %s: %w", location, err).Error() + "\n")
+		os.Exit(1)
+	}
+	if err = normalize(Parsed); err != nil {
+		os.Stderr.WriteString(fmt.Errorf("failed to parse %s: %w", location, err).Error() + "\n")
+		os.Exit(1)
+	}
 }
 
-func Parse() (*Config, error) {
+func parse(location string) (*Config, error) {
 	/* Default values */
-	config := &Config{
-		Context:   5,
-		Timeout:   10,
-		Feeds:     feeds{},
-		Algos:     algos{},
-		MediaHook: []string{"xdg-open", "%url"},
-	}
+	config := &Config{}
+	config.Feeds = map[string][]string{}
+	config.Media.Hook = []string{"xdg-open", "%url"}
+	config.Style.Context = 5
+	config.Style.Colors.Primary = "#A4f59b"
+	config.Style.Colors.Error = "#9c3535"
+	config.Style.Colors.Highlight = "#0d7d00"
+	config.Style.Colors.Code = "#4b4b4b"
+	config.Network.Timeout = 10
 
-	location := location()
 	if location == "" {
 		return config, nil
 	}
@@ -45,7 +74,7 @@ func Parse() (*Config, error) {
 	}
 
 	if undecoded := metadata.Undecoded(); len(undecoded) != 0 {
-		return nil, fmt.Errorf("config file %s contained unrecognized keys: %v", location, undecoded)
+		return nil, fmt.Errorf("contains unrecognized key(s): %v", undecoded)
 	}
 
 	return config, nil
@@ -62,3 +91,52 @@ func location() string {
 
 	return ""
 }
+
+func hexToAnsi(text string) (string, error) {
+	errNotAHexCode := errors.New("must be a hex code of the form '#fcba03'")
+
+	if !strings.HasPrefix(text, "#") {
+		return "", errNotAHexCode
+	}
+
+	if len(text) != 7 {
+		return "", errNotAHexCode
+	}
+
+	r, err := strconv.ParseUint(text[1:3], 16, 0)
+	if err != nil {
+		return "", errNotAHexCode
+	}
+	g, err := strconv.ParseUint(text[3:5], 16, 0)
+	if err != nil {
+		return "", errNotAHexCode
+	}
+	b, err := strconv.ParseUint(text[5:7], 16, 0)
+	if err != nil {
+		return "", errNotAHexCode
+	}
+
+	return strconv.Itoa(int(r)) + ";" + strconv.Itoa(int(g)) + ";" + strconv.Itoa(int(b)), nil
+}
+
+func normalize(config *Config) error {
+	var err error
+	config.Style.Colors.Primary, err = hexToAnsi(config.Style.Colors.Primary)
+	if err != nil {
+		return fmt.Errorf("key style.colors.primary is invalid: %w", err)
+	}
+	config.Style.Colors.Error, err = hexToAnsi(config.Style.Colors.Error)
+	if err != nil {
+		return fmt.Errorf("key style.colors.error is invalid: %w", err)
+	}
+	config.Style.Colors.Highlight, err = hexToAnsi(config.Style.Colors.Highlight)
+	if err != nil {
+		return fmt.Errorf("key style.colors.highlight is invalid: %w", err)
+	}
+	config.Style.Colors.Code, err = hexToAnsi(config.Style.Colors.Code)
+	if err != nil {
+		return fmt.Errorf("key style.colors.code is invalid: %w", err)
+	}
+	config.Network.Timeout *= time.Second
+	return nil
+}

+ 2 - 2
jtp/jtp.go

@@ -12,11 +12,11 @@ import (
 	"net/url"
 	"regexp"
 	"strings"
-	"time"
+	"servitor/config"
 )
 
 var dialer = &net.Dialer{
-	Timeout: 5 * time.Second,
+	Timeout: config.Parsed.Network.Timeout,
 }
 
 type bundle struct {

+ 4 - 9
main.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"servitor/config"
 	"servitor/ui"
 	"os"
 	"strings"
@@ -13,12 +12,7 @@ import (
 func main() {
 	if len(os.Args) < 3 {
 		help()
-	}
-
-	config, err := config.Parse()
-	if err != nil {
-		os.Stderr.WriteString(err.Error() + "\n")
-		return
+		os.Exit(1)
 	}
 
 	oldTerminal, err := term.MakeRaw(int(os.Stdin.Fd()))
@@ -31,7 +25,7 @@ func main() {
 		panic(err)
 	}
 
-	state := ui.NewState(config, width, height, printRaw)
+	state := ui.NewState(width, height, printRaw)
 	go func() {
 		for {
 			time.Sleep(500 * time.Millisecond)
@@ -49,6 +43,8 @@ func main() {
 		if err != nil {
 			term.Restore(int(os.Stdin.Fd()), oldTerminal)
 			help()
+			os.Stdout.WriteString("\n" + err.Error() + "\n")
+			os.Exit(1)
 		}
 	}()
 
@@ -106,5 +102,4 @@ Keybindings:
   :open <url or @>
   :feed <feed name>
 `)
-	os.Exit(0)
 }

+ 9 - 9
style/style.go

@@ -1,19 +1,19 @@
 package style
 
 import (
-	"fmt"
 	"servitor/ansi"
 	"strconv"
 	"strings"
+	"servitor/config"
 )
 
-func background(text string, r uint8, g uint8, b uint8) string {
-	prefix := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
+func background(text string, rgb string) string {
+	prefix := "48;2;" + rgb
 	return ansi.Apply(text, prefix)
 }
 
-func foreground(text string, r uint8, g uint8, b uint8) string {
-	prefix := fmt.Sprintf("38;2;%d;%d;%d", r, g, b)
+func foreground(text string, rgb string) string {
+	prefix := "38;2;" + rgb
 	return ansi.Apply(text, prefix)
 }
 
@@ -34,15 +34,15 @@ func Italic(text string) string {
 }
 
 func Code(text string) string {
-	return background(text, 75, 75, 75)
+	return background(text, config.Parsed.Style.Colors.Code)
 }
 
 func Highlight(text string) string {
-	return background(text, 13, 125, 0)
+	return background(text, config.Parsed.Style.Colors.Highlight)
 }
 
 func Color(text string) string {
-	return foreground(text, 164, 245, 155)
+	return foreground(text, config.Parsed.Style.Colors.Primary)
 }
 
 func Problem(issue error) string {
@@ -50,7 +50,7 @@ func Problem(issue error) string {
 }
 
 func Red(text string) string {
-	return foreground(text, 156, 53, 53)
+	return foreground(text, config.Parsed.Style.Colors.Error)
 }
 
 func Link(text string, number int) string {

+ 15 - 12
ui/ui.go

@@ -14,6 +14,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"errors"
 )
 
 /*
@@ -57,8 +58,6 @@ type State struct {
 	height int
 	output func(string)
 
-	config *config.Config
-
 	mode   int
 	buffer string
 }
@@ -74,7 +73,7 @@ func (s *State) view() string {
 	}
 
 	var top, center, bottom string
-	for i := -s.config.Context; i <= s.config.Context; i++ {
+	for i := -config.Parsed.Style.Context; i <= config.Parsed.Style.Context; i++ {
 		if !s.h.Current().feed.Contains(i) {
 			continue
 		}
@@ -108,10 +107,10 @@ func (s *State) view() string {
 			bottom += serialized
 		}
 	}
-	if s.h.Current().loadingUp && !s.h.Current().feed.Contains(-s.config.Context-1) {
+	if s.h.Current().loadingUp && !s.h.Current().feed.Contains(-config.Parsed.Style.Context-1) {
 		top = "\n  " + style.Color("Loading…") + "\n\n" + top
 	}
-	if s.h.Current().loadingDown && !s.h.Current().feed.Contains(s.config.Context+1) {
+	if s.h.Current().loadingDown && !s.h.Current().feed.Contains(config.Parsed.Style.Context+1) {
 		bottom += "  " + style.Color("Loading…") + "\n"
 	}
 
@@ -339,7 +338,7 @@ func (s *State) switchTo(item any) {
 			s.buffer = ""
 			s.output(s.view())
 		}
-		children, nextCollection, newBasepoint := narrowed.Harvest(uint(s.config.Context), 0)
+		children, nextCollection, newBasepoint := narrowed.Harvest(uint(config.Parsed.Style.Context), 0)
 		s.h.Add(&Page{
 			basepoint: newBasepoint,
 			children:  nextCollection,
@@ -366,7 +365,7 @@ func (s *State) SetWidthHeight(width int, height int) {
 
 func (s *State) loadSurroundings() {
 	page := s.h.Current()
-	context := s.config.Context
+	context := config.Parsed.Style.Context
 	if !page.loadingUp && !page.feed.Contains(-context) && page.frontier != nil {
 		page.loadingUp = true
 		go func() {
@@ -422,7 +421,7 @@ func (s *State) openInternally(input string) {
 }
 
 func (s *State) openFeed(input string) {
-	inputs, present := s.config.Feeds[input]
+	inputs, present := config.Parsed.Feeds[input]
 	if !present {
 		s.mode = problem
 		s.buffer = "Failed to open feed: " + input + " is not a known feed"
@@ -441,10 +440,9 @@ func (s *State) openFeed(input string) {
 	}()
 }
 
-func NewState(config *config.Config, width int, height int, output func(string)) *State {
+func NewState(width int, height int, output func(string)) *State {
 	s := &State{
 		h:      history.History[*Page]{},
-		config: config,
 		width:  width,
 		height: height,
 		output: output,
@@ -456,6 +454,11 @@ func NewState(config *config.Config, width int, height int, output func(string))
 
 func (s *State) Subcommand(name, argument string) error {
 	s.m.Lock()
+	if name == "feed" {
+		if _, present := config.Parsed.Feeds[argument]; !present {
+			return errors.New("failed to open feed: " + argument + " is not a known feed")
+		}
+	}
 	err := s.subcommand(name, argument)
 	if err != nil {
 		/* Here I hold the lock indefinitely intentionally, to stop the ui thread and allow main.go to do cleanup */
@@ -482,8 +485,8 @@ func (s *State) openExternally(link string, mediaType *mime.MediaType) {
 	s.buffer = link
 	s.output(s.view())
 
-	command := make([]string, len(s.config.MediaHook))
-	copy(command, s.config.MediaHook)
+	command := make([]string, len(config.Parsed.Media.Hook))
+	copy(command, config.Parsed.Media.Hook)
 
 	foundPercentU := false
 	for i, field := range command {