1// Package config loads the bar's TOML configuration.
2package config
3
4import (
5 "fmt"
6 "os"
7 "path/filepath"
8
9 "github.com/BurntSushi/toml"
10)
11
12// Bar holds the global bar settings.
13type Bar struct {
14 Font string `toml:"font"`
15 Size float64 `toml:"size"` // font size in logical pixels
16 // Scale multiplies the monitor scale (1.0 follows the monitor).
17 Scale float64 `toml:"scale"`
18 // Output is the wl_output name; empty lets the compositor choose.
19 Output string `toml:"output"`
20 // Theme is "auto" (the colors of the terminal in use) or the path of a
21 // terminal theme or config file: kitty, foot, ghostty or alacritty.
22 Theme string `toml:"theme"`
23 // Accent is the number (0-15) of the theme's ANSI color the bundled
24 // scripts use for highlights. Scripts read it as $NEFERBAR_ACCENT.
25 Accent int `toml:"accent"`
26 // Background and Foreground, as #rrggbb, override the theme's.
27 Background string `toml:"background"`
28 Foreground string `toml:"foreground"`
29}
30
31// Module is one script.
32type Module struct {
33 Name string `toml:"name"`
34 Zone string `toml:"zone"` // left | center | right
35 Exec string `toml:"exec"`
36}
37
38// Config is the whole file.
39type Config struct {
40 Bar Bar `toml:"bar"`
41 Module []Module `toml:"module"`
42}
43
44// Default returns the built-in settings.
45func Default() Config {
46 return Config{Bar: Bar{
47 Font: "JetBrainsMono Nerd Font Mono", Size: 14, Scale: 1,
48 Theme: "auto", Accent: 4,
49 }}
50}
51
52// DefaultPath is $XDG_CONFIG_HOME/neferbar/config.toml.
53func DefaultPath() string {
54 dir := os.Getenv("XDG_CONFIG_HOME")
55 if dir == "" {
56 home, _ := os.UserHomeDir()
57 dir = filepath.Join(home, ".config")
58 }
59 return filepath.Join(dir, "neferbar", "config.toml")
60}
61
62// Load reads path over the defaults. A missing file at the default path is
63// not an error.
64func Load(path string, explicit bool) (Config, error) {
65 c := Default()
66 md, err := toml.DecodeFile(path, &c)
67 if err != nil {
68 if !explicit && os.IsNotExist(err) {
69 return c, nil
70 }
71 return c, fmt.Errorf("config: %w", err)
72 }
73 if u := md.Undecoded(); len(u) > 0 {
74 return c, fmt.Errorf("config: unknown key %s", u[0])
75 }
76 return c, c.Validate()
77}
78
79// Validate checks every field.
80func (c *Config) Validate() error {
81 b := &c.Bar
82 if b.Font == "" {
83 return fmt.Errorf("config: bar.font is empty")
84 }
85 if b.Size < 6 || b.Size > 128 {
86 return fmt.Errorf("config: bar.size %v out of range 6..128", b.Size)
87 }
88 if b.Scale < 0.5 || b.Scale > 8 {
89 return fmt.Errorf("config: bar.scale %v out of range 0.5..8", b.Scale)
90 }
91 for _, col := range []string{b.Background, b.Foreground} {
92 if col == "" {
93 continue // taken from the theme
94 }
95 if _, err := ParseColor(col); err != nil {
96 return err
97 }
98 }
99 if b.Accent < 0 || b.Accent > 15 {
100 return fmt.Errorf("config: bar.accent %d must be a color number from 0 to 15", b.Accent)
101 }
102 if b.Theme == "" {
103 return fmt.Errorf("config: bar.theme is empty; use \"auto\" or the path of a theme file")
104 }
105 seen := map[string]bool{}
106 for i := range c.Module {
107 m := &c.Module[i]
108 if m.Exec == "" {
109 return fmt.Errorf("config: module %d has no exec", i+1)
110 }
111 if m.Name == "" {
112 m.Name = fmt.Sprintf("module%d", i+1)
113 }
114 if seen[m.Name] {
115 return fmt.Errorf("config: two modules are named %q", m.Name)
116 }
117 seen[m.Name] = true
118 switch m.Zone {
119 case "left", "center", "right":
120 default:
121 return fmt.Errorf("config: module %q: zone must be left, center or right", m.Name)
122 }
123 }
124 return nil
125}
126
127// ParseColor parses #rrggbb.
128func ParseColor(s string) ([3]uint8, error) {
129 var r, g, b uint8
130 if len(s) != 7 || s[0] != '#' {
131 return [3]uint8{}, fmt.Errorf("config: color %q must be #rrggbb", s)
132 }
133 if _, err := fmt.Sscanf(s[1:], "%02x%02x%02x", &r, &g, &b); err != nil {
134 return [3]uint8{}, fmt.Errorf("config: color %q must be #rrggbb", s)
135 }
136 return [3]uint8{r, g, b}, nil
137}