summaryrefslogtreecommitdiff
path: root/internal/config/load.go
blob: b243a389458239ba1c11e2f469186283487d1af0 (plain) (blame)
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
// SPDX-License-Identifier: GPL-3.0-or-later

package config

import (
	"errors"
	"fmt"
	"io/fs"
	"maps"
	"os"
	"path/filepath"
	"slices"

	"git.labunix.xyz/krino/internal/sexp"
	"git.labunix.xyz/krino/internal/xdg"
)

// Config is the whole configuration: the main file and the directories it
// includes.
type Config struct {
	Main *Main
	Dirs []*Dir
}

// unusedOverrides reports text for a file no part of this configuration
// names: it would otherwise pass as checked while the file on disk was read
// instead. Text for an included directory this call did not load - an
// editor holding buffers for several while checking one - is not reported.
func unusedOverrides(over map[string][]byte, used map[string]bool, mainFile string, include []string) []*Diag {
	known := map[string]bool{filepath.Clean(mainFile): true}
	for _, name := range include {
		known[filepath.Clean(DirFile(mainFile, name))] = true
	}
	var out []*Diag
	for _, file := range slices.Sorted(maps.Keys(over)) {
		if !used[file] && !known[file] {
			out = append(out, &Diag{File: mainFile, Msg: fmt.Sprintf("unsaved text for %s, which this configuration does not read", file)})
		}
	}
	return out
}

// readSource is the text of file: the caller's override when it has one,
// else the file's own bytes. It records which overrides were used, so
// LoadWith can report one it never reached.
func readSource(overrides map[string][]byte, used map[string]bool, file string) ([]byte, error) {
	key := filepath.Clean(file)
	if src, ok := overrides[key]; ok {
		used[key] = true
		return src, nil
	}
	return os.ReadFile(file)
}

// cleanOverrides keys the caller's overrides by cleaned path, so a file
// named with a "." or ".." segment is still recognised as itself. Two keys
// that name one file are refused rather than resolved by map order.
func cleanOverrides(overrides map[string][]byte) (map[string][]byte, []*Diag) {
	if len(overrides) == 0 {
		return nil, nil
	}
	out := make(map[string][]byte, len(overrides))
	var errs []*Diag
	for _, file := range slices.Sorted(maps.Keys(overrides)) {
		key := filepath.Clean(file)
		if _, seen := out[key]; seen {
			errs = append(errs, &Diag{File: key, Msg: "unsaved text given twice for this file"})
			continue
		}
		out[key] = overrides[file]
	}
	return out, errs
}

// DefaultFile is krino.conf in the XDG config directory.
func DefaultFile() string {
	return filepath.Join(xdg.ConfigHome(), "krino", "krino.conf")
}

// DirFile is where directory name's config lives, beside the main file.
func DirFile(mainFile, name string) string {
	return filepath.Join(filepath.Dir(mainFile), "dirs", name+".conf")
}

// Load reads the main file and the files of the directories it includes.
// With names, only those directories are read, and each must be included.
// The Config is nil only when the main file itself cannot be read.
func Load(mainFile string, names ...string) (*Config, []*Diag) {
	return LoadWith(mainFile, nil, names...)
}

// LoadWith is Load with some files' text supplied by the caller: overrides
// maps a file path - mainFile, or DirFile(mainFile, name) - to the text to
// read instead of that file's own, so an editor can have unsaved text
// checked exactly as a run would read it (GUI design §1.3). A nil map is
// Load.
func LoadWith(mainFile string, overrides map[string][]byte, names ...string) (*Config, []*Diag) {
	over, collisions := cleanOverrides(overrides)
	used := map[string]bool{}
	src, err := readSource(over, used, mainFile)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, []*Diag{{File: mainFile, Msg: "not found; create it with: krino init"}}
	}
	if err != nil {
		return nil, []*Diag{{File: mainFile, Msg: err.Error()}}
	}
	m, errs := ParseMain(mainFile, src)
	cfg := &Config{Main: m}
	if _, perr := sexp.Parse(mainFile, src); perr != nil {
		// krino.conf itself is unreadable: report just that, and load none
		// of the directories, so a syntax error never also produces
		// "NAME is not in include" for names the caller asked for.
		return cfg, errs
	}
	want := m.Include
	if len(names) > 0 {
		want = nil
		for _, n := range names {
			if _, ok := m.IncludePos[n]; !ok {
				errs = append(errs, &Diag{File: mainFile, Msg: fmt.Sprintf("%q is not in include", n)})
				continue
			}
			want = append(want, n)
		}
	}
	for _, name := range want {
		file := DirFile(mainFile, name)
		src, err := readSource(over, used, file)
		if err != nil {
			msg := err.Error()
			if errors.Is(err, fs.ErrNotExist) {
				msg = fmt.Sprintf("included %q, but %s does not exist; create it with: krino new %s PATH", name, file, name)
			}
			errs = append(errs, &Diag{File: mainFile, Pos: m.IncludePos[name], Msg: msg})
			continue
		}
		dir, derrs := ParseDir(name, file, src)
		errs = append(errs, derrs...)
		cfg.Dirs = append(cfg.Dirs, dir)
	}
	errs = append(errs, collisions...)
	return cfg, append(errs, unusedOverrides(over, used, mainFile, m.Include)...)
}

// Resolved is the settings that apply in dir: built-in, then the main
// file's defaults, then the directory's own.
func (c *Config) Resolved(dir *Dir) Resolved {
	return dir.Settings.Over(c.Main.Defaults.Over(Builtin()))
}

// LogFile is the main file's (log ...), or the default under XDG_STATE_HOME.
func (c *Config) LogFile() string {
	if c.Main.Log != "" {
		return c.Main.Log
	}
	return filepath.Join(xdg.StateHome(), "krino", "krino.log")
}

// LockFile is where a directory's lock lives while a run is active:
// $XDG_STATE_HOME/krino/<name>.lock, beside the log (spec §3).
func (c *Config) LockFile(name string) string {
	return filepath.Join(xdg.StateHome(), "krino", name+".lock")
}