1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
// Package config loads glint's TOML configuration, falling back to defaults.
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/BurntSushi/toml"
)
// Config holds the user-tunable settings glint reads at startup.
type Config struct {
VaultDir string `toml:"vault_dir"` // the vault `glint -v` opens from anywhere
DailySubdir string `toml:"daily_subdir"`
DailyFormat string `toml:"daily_format"`
GlamourStyle string `toml:"glamour_style"`
Theme string `toml:"theme"`
InboxDir string `toml:"inbox_dir"`
Spellcheck string `toml:"spellcheck"` // auto | on | off (TASK-020)
// PDF/printable export fonts (TASK-021). CSS font-family stacks that
// override the house-style --font-* tokens. Defaults are portable
// open/system faces โ glint ships to people without the kit's licensed
// fonts, so nothing licensed is baked in.
FontDisplay string `toml:"font_display"` // headings / cover
FontBody string `toml:"font_body"` // body prose
FontMono string `toml:"font_mono"` // code
}
// Default returns the built-in configuration used when no file is present.
// VaultDir is empty by default: bare glint works in the current directory; set
// vault_dir (config) to give `glint -v` a fixed vault to open from anywhere.
func Default() Config {
return Config{
DailySubdir: "Daily",
DailyFormat: "2006-01-02",
Theme: "auto",
Spellcheck: "auto",
FontDisplay: "Georgia, \"Times New Roman\", serif",
FontBody: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
FontMono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
}
}
// WorkingDir is where bare `glint`, `glint -n`, and `-d` operate: the
// $GLINT_VAULT pin if set, otherwise the current working directory.
func (c Config) WorkingDir() string {
if v := os.Getenv("GLINT_VAULT"); v != "" {
return v
}
if wd, err := os.Getwd(); err == nil {
return wd
}
return "."
}
// Vault is the directory `glint -v` opens from anywhere: the configured
// vault_dir, or the working directory when none is set.
func (c Config) Vault() string {
if c.VaultDir != "" {
return c.VaultDir
}
return c.WorkingDir()
}
// Path is the config file location: ~/.config/glint/config.toml.
func Path() string {
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join(".config", "glint", "config.toml")
}
return filepath.Join(home, ".config", "glint", "config.toml")
}
// DictPath is the personal spellcheck dictionary location:
// ~/.config/glint/dict.txt (TASK-020).
func DictPath() string {
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join(".config", "glint", "dict.txt")
}
return filepath.Join(home, ".config", "glint", "dict.txt")
}
// Load reads the config file and overlays it onto the defaults.
func Load() (Config, error) {
return loadFromFile(Path())
}
// Save writes cfg to path as TOML, creating the parent directory.
func Save(cfg Config, path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
if err := toml.NewEncoder(f).Encode(cfg); err != nil {
_ = f.Close()
return err
}
return f.Close()
}
// loadFromFile is the testable core of Load. A missing file yields the
// defaults with no error; a malformed file yields the defaults plus an error.
func loadFromFile(path string) (Config, error) {
cfg := Default()
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return cfg, nil // absent config is fine
}
return cfg, fmt.Errorf("read %s: %w", path, err)
}
var fileCfg Config
if _, err := toml.Decode(string(data), &fileCfg); err != nil {
return cfg, fmt.Errorf("parse %s: %w", path, err)
}
if fileCfg.VaultDir != "" {
cfg.VaultDir = fileCfg.VaultDir
}
if fileCfg.DailySubdir != "" {
cfg.DailySubdir = fileCfg.DailySubdir
}
if fileCfg.DailyFormat != "" {
cfg.DailyFormat = fileCfg.DailyFormat
}
if fileCfg.GlamourStyle != "" {
cfg.GlamourStyle = fileCfg.GlamourStyle
}
if fileCfg.Theme != "" {
cfg.Theme = fileCfg.Theme
}
if fileCfg.InboxDir != "" {
cfg.InboxDir = fileCfg.InboxDir
}
if fileCfg.Spellcheck != "" {
cfg.Spellcheck = fileCfg.Spellcheck
}
if fileCfg.FontDisplay != "" {
cfg.FontDisplay = fileCfg.FontDisplay
}
if fileCfg.FontBody != "" {
cfg.FontBody = fileCfg.FontBody
}
if fileCfg.FontMono != "" {
cfg.FontMono = fileCfg.FontMono
}
return cfg, nil
}
// InboxRoot is the directory new notes default into: the working directory when
// InboxDir is empty, an absolute InboxDir as-is, or InboxDir resolved under the
// working directory when relative.
func (c Config) InboxRoot() string {
if c.InboxDir == "" {
return c.WorkingDir()
}
if filepath.IsAbs(c.InboxDir) {
return c.InboxDir
}
return filepath.Join(c.WorkingDir(), c.InboxDir)
}
// DailyDir is the folder daily notes live in (for the daily-folder picker): an
// absolute daily_subdir as-is, else daily_subdir under the vault.
func (c Config) DailyDir() string {
if filepath.IsAbs(c.DailySubdir) {
return c.DailySubdir
}
return filepath.Join(c.Vault(), c.DailySubdir)
}
// DailyPath builds the path to the daily note for time t. Daily notes belong to
// the vault (Vault()), not the working directory, so `glint --daily` lands in
// the same place from anywhere. An absolute daily_subdir is used as-is.
func (c Config) DailyPath(t time.Time) string {
name := t.Format(c.DailyFormat) + ".md"
if filepath.IsAbs(c.DailySubdir) {
return filepath.Join(c.DailySubdir, name)
}
return filepath.Join(c.Vault(), c.DailySubdir, name)
}
|