config.go

  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}