1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package config handles loading and saving lune configuration from TOML.
6package config
7
8import (
9 "errors"
10 "fmt"
11 "os"
12 "path/filepath"
13
14 "github.com/BurntSushi/toml"
15)
16
17// ErrNotFound indicates the config file doesn't exist.
18var ErrNotFound = errors.New("config file not found")
19
20// Config represents the lune configuration file structure.
21type Config struct {
22 UI UIConfig `toml:"ui"`
23 Defaults Defaults `toml:"defaults"`
24 Areas []Area `toml:"areas"`
25 Notebooks []Notebook `toml:"notebooks"`
26 Habits []Habit `toml:"habits"`
27}
28
29// UIConfig holds user interface preferences.
30type UIConfig struct {
31 Color string `toml:"color"` // "always", "never", "auto"
32}
33
34// Defaults holds default selections for commands.
35type Defaults struct {
36 Area string `toml:"area"`
37 Notebook string `toml:"notebook"`
38}
39
40// Area represents a Lunatask area of life with its goals.
41type Area struct {
42 ID string `toml:"id"`
43 Name string `toml:"name"`
44 Key string `toml:"key"`
45 Goals []Goal `toml:"goals"`
46}
47
48// Goal represents a goal within an area.
49type Goal struct {
50 ID string `toml:"id"`
51 Name string `toml:"name"`
52 Key string `toml:"key"`
53}
54
55// Notebook represents a Lunatask notebook for notes.
56type Notebook struct {
57 ID string `toml:"id"`
58 Name string `toml:"name"`
59 Key string `toml:"key"`
60}
61
62// Habit represents a trackable habit.
63type Habit struct {
64 ID string `toml:"id"`
65 Name string `toml:"name"`
66 Key string `toml:"key"`
67}
68
69// Path returns the path to the config file.
70func Path() (string, error) {
71 configDir, err := os.UserConfigDir()
72 if err != nil {
73 return "", fmt.Errorf("getting config dir: %w", err)
74 }
75
76 return filepath.Join(configDir, "lunatask", "config.toml"), nil
77}
78
79// Load reads the config file. Returns ErrNotFound if the file doesn't exist.
80func Load() (*Config, error) {
81 path, err := Path()
82 if err != nil {
83 return nil, err
84 }
85
86 var cfg Config
87 if _, err := toml.DecodeFile(path, &cfg); err != nil {
88 if errors.Is(err, os.ErrNotExist) {
89 return nil, ErrNotFound
90 }
91
92 return nil, fmt.Errorf("decoding config: %w", err)
93 }
94
95 return &cfg, nil
96}
97
98// Save writes the config to disk.
99func (c *Config) Save() error {
100 path, err := Path()
101 if err != nil {
102 return err
103 }
104
105 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
106 return fmt.Errorf("creating config dir: %w", err)
107 }
108
109 f, err := os.Create(path) //nolint:gosec // path is from user config dir
110 if err != nil {
111 return fmt.Errorf("creating config file: %w", err)
112 }
113
114 if err := toml.NewEncoder(f).Encode(c); err != nil {
115 _ = f.Close()
116
117 return fmt.Errorf("encoding config: %w", err)
118 }
119
120 if err := f.Close(); err != nil {
121 return fmt.Errorf("closing config file: %w", err)
122 }
123
124 return nil
125}
126
127// AreaByKey finds an area by its key.
128func (c *Config) AreaByKey(key string) *Area {
129 for i := range c.Areas {
130 if c.Areas[i].Key == key {
131 return &c.Areas[i]
132 }
133 }
134
135 return nil
136}
137
138// NotebookByKey finds a notebook by its key.
139func (c *Config) NotebookByKey(key string) *Notebook {
140 for i := range c.Notebooks {
141 if c.Notebooks[i].Key == key {
142 return &c.Notebooks[i]
143 }
144 }
145
146 return nil
147}
148
149// HabitByKey finds a habit by its key.
150func (c *Config) HabitByKey(key string) *Habit {
151 for i := range c.Habits {
152 if c.Habits[i].Key == key {
153 return &c.Habits[i]
154 }
155 }
156
157 return nil
158}
159
160// GoalByKey finds a goal within this area by its key.
161func (a *Area) GoalByKey(key string) *Goal {
162 for i := range a.Goals {
163 if a.Goals[i].Key == key {
164 return &a.Goals[i]
165 }
166 }
167
168 return nil
169}