v0.3.0 cf370a0

scalestore.go RawHistoryFolder

1package config
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"math"
8	"os"
9	"path/filepath"
10	"strconv"
11	"strings"
12)
13
14// ScaleStore writes output.<name>.scale into the config file, preserving
15// unrelated lines and comments.
16type ScaleStore struct{ Path string }
17
18// SaveOutputScale updates output.<name>.scale in the config file. The file is
19// replaced atomically; a symlinked config is written through to its target.
20func (s ScaleStore) SaveOutputScale(output string, scale float64) error {
21	path := s.Path
22	if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
23		// Replacing a dangling link would lose it: report it instead.
24		if path, err = filepath.EvalSymlinks(path); err != nil {
25			return err
26		}
27	}
28	data, err := os.ReadFile(path)
29	// A new config may hold commands: only its owner reads it.
30	mode := os.FileMode(0o600)
31	switch {
32	case errors.Is(err, os.ErrNotExist):
33		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
34			return err
35		}
36	case err != nil:
37		return err
38	default:
39		if info, err := os.Stat(path); err == nil {
40			mode = info.Mode().Perm()
41		}
42	}
43	return writeAtomic(path, setKey(data, "output."+output+".scale", formatScale(scale)), mode)
44}
45
46// setKey replaces the last active assignment, or enables a matching commented
47// assignment. New keys go beside the closest related key, then at the end.
48func setKey(data []byte, key, value string) []byte {
49	lines := strings.SplitAfter(string(data), "\n")
50	commented := -1
51	for i, line := range lines {
52		body := strings.TrimSpace(line)
53		if !strings.HasPrefix(body, "#") {
54			continue
55		}
56		body = strings.TrimSpace(strings.TrimPrefix(body, "#"))
57		k, _, ok := strings.Cut(body, "=")
58		if ok && strings.TrimSpace(k) == key {
59			commented = i
60		}
61	}
62	for i := len(lines) - 1; i >= 0; i-- {
63		prefix, rest, ok := strings.Cut(lines[i], "=")
64		if !ok || strings.TrimSpace(prefix) != key {
65			continue
66		}
67		lines[i] = replaceValue(prefix+"=", rest, value)
68		return []byte(strings.Join(lines, ""))
69	}
70	if commented >= 0 {
71		line := lines[commented]
72		indent, body, _ := strings.Cut(line, "#")
73		prefix, rest, _ := strings.Cut(strings.TrimLeft(body, " \t"), "=")
74		lines[commented] = replaceValue(indent+prefix+"=", rest, value)
75		return []byte(strings.Join(lines, ""))
76	}
77	// Match the longest dotted parent first: output.DP-2.scale belongs beside
78	// output.DP-2.*, not merely beside any output.* key.
79	for parent := key; ; {
80		dot := strings.LastIndexByte(parent, '.')
81		if dot < 0 {
82			break
83		}
84		parent = parent[:dot]
85		index := -1
86		for i, line := range lines {
87			k, _, ok := strings.Cut(line, "=")
88			k = strings.TrimSpace(k)
89			if ok && (k == parent || strings.HasPrefix(k, parent+".")) {
90				index = i
91			}
92		}
93		if index < 0 {
94			continue
95		}
96		eol := "\n"
97		if strings.HasSuffix(lines[index], "\r\n") {
98			eol = "\r\n"
99		}
100		if !strings.HasSuffix(lines[index], "\n") {
101			lines[index] += eol
102		}
103		lines = append(lines, "")
104		copy(lines[index+2:], lines[index+1:])
105		lines[index+1] = key + " = " + value + eol
106		return []byte(strings.Join(lines, ""))
107	}
108	var b bytes.Buffer
109	b.Write(data)
110	if len(data) > 0 && data[len(data)-1] != '\n' {
111		b.WriteByte('\n')
112	}
113	fmt.Fprintf(&b, "%s = %s\n", key, value)
114	return b.Bytes()
115}
116
117// replaceValue keeps indentation, spacing and trailing comments in an assignment.
118func replaceValue(prefix, rest, value string) string {
119	plain := strings.TrimRight(rest, "\r\n")
120	eol := rest[len(plain):]
121	old := stripComment(strings.TrimSpace(plain))
122	start := strings.Index(plain, old)
123	return prefix + plain[:start] + value + plain[start+len(old):] + eol
124}
125
126// formatScale writes scales as the config expects them: a decimal when it is
127// exact (1.25), else a fraction (4/3).
128func formatScale(s float64) string {
129	n, d := int(math.Round(s*120)), 120
130	g := gcd(n, d)
131	n, d = n/g, d/g
132	den := d
133	for den%2 == 0 {
134		den /= 2
135	}
136	for den%5 == 0 {
137		den /= 5
138	}
139	if den == 1 {
140		return strconv.FormatFloat(float64(n)/float64(d), 'f', -1, 64)
141	}
142	return fmt.Sprintf("%d/%d", n, d)
143}
144
145func gcd(a, b int) int {
146	for b != 0 {
147		a, b = b, a%b
148	}
149	return a
150}
151
152// writeAtomic replaces path with data: readers and the watcher never see a
153// half-written file.
154func writeAtomic(path string, data []byte, mode os.FileMode) error {
155	tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*")
156	if err != nil {
157		return err
158	}
159	defer os.Remove(tmp.Name())
160	if _, err := tmp.Write(data); err != nil {
161		tmp.Close()
162		return err
163	}
164	if err := tmp.Chmod(mode); err != nil {
165		tmp.Close()
166		return err
167	}
168	if err := tmp.Sync(); err != nil {
169		tmp.Close()
170		return err
171	}
172	if err := tmp.Close(); err != nil {
173		return err
174	}
175	return os.Rename(tmp.Name(), path)
176}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close