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