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