main 6af0a61

config.go RawHistoryFolder

1package system
2
3import (
4	"context"
5	"errors"
6	"strings"
7
8	"github.com/bnema/archup/internal/domain/ports"
9)
10
11// DistributionType represents the type of Arch-based distribution
12type DistributionType int
13
14const (
15	// DistributionArch represents vanilla Arch Linux
16	DistributionArch DistributionType = iota
17	// DistributionCachyOS represents CachyOS
18	DistributionCachyOS
19	// DistributionEndeavourOS represents EndeavourOS
20	DistributionEndeavourOS
21	// DistributionGaruda represents Garuda Linux
22	DistributionGaruda
23	// DistributionManjaro represents Manjaro Linux
24	DistributionManjaro
25	// DistributionUnknown represents an unknown or unsupported distribution
26	DistributionUnknown
27)
28
29// String returns the string representation of the distribution type
30func (d DistributionType) String() string {
31	switch d {
32	case DistributionArch:
33		return "Arch Linux"
34	case DistributionCachyOS:
35		return "CachyOS"
36	case DistributionEndeavourOS:
37		return "EndeavourOS"
38	case DistributionGaruda:
39		return "Garuda Linux"
40	case DistributionManjaro:
41		return "Manjaro Linux"
42	case DistributionUnknown:
43		return "Unknown"
44	default:
45		return "Unknown"
46	}
47}
48
49// DetectDistribution detects which Arch-based distribution is running
50func DetectDistribution(ctx context.Context, fs ports.FileSystem) (DistributionType, error) {
51	// Check for vanilla Arch first
52	exists, err := fs.Exists("/etc/arch-release")
53	if err != nil {
54		return DistributionUnknown, errors.New("failed to detect distribution: " + err.Error())
55	}
56	if !exists {
57		return DistributionUnknown, errors.New("not running on Arch Linux or Arch ISO")
58	}
59
60	// Check for derivatives
61	if exists, _ := fs.Exists("/etc/cachyos-release"); exists {
62		return DistributionCachyOS, nil
63	}
64	if exists, _ := fs.Exists("/etc/eos-release"); exists {
65		return DistributionEndeavourOS, nil
66	}
67	if exists, _ := fs.Exists("/etc/garuda-release"); exists {
68		return DistributionGaruda, nil
69	}
70	if exists, _ := fs.Exists("/etc/manjaro-release"); exists {
71		return DistributionManjaro, nil
72	}
73
74	// If no derivative markers found, it's vanilla Arch
75	return DistributionArch, nil
76}
77
78// SystemConfig is an immutable value object representing system configuration
79// It encapsulates hostname, locale, timezone, and keymap settings
80type SystemConfig struct {
81	hostname string
82	timezone string
83	locale   string
84	keymap   string
85}
86
87// NewSystemConfig creates a new SystemConfig with validation
88func NewSystemConfig(hostname, timezone, locale, keymap string) (*SystemConfig, error) {
89	// Validate hostname
90	if err := ValidateHostname(hostname); err != nil {
91		return nil, err
92	}
93
94	// Validate timezone
95	if err := ValidateTimezone(timezone); err != nil {
96		return nil, err
97	}
98
99	// Validate locale
100	if err := ValidateLocale(locale); err != nil {
101		return nil, err
102	}
103
104	// Validate keymap
105	if err := ValidateKeymap(keymap); err != nil {
106		return nil, err
107	}
108
109	return &SystemConfig{
110		hostname: hostname,
111		timezone: timezone,
112		locale:   locale,
113		keymap:   keymap,
114	}, nil
115}
116
117// Hostname returns the configured hostname
118func (c *SystemConfig) Hostname() string {
119	return c.hostname
120}
121
122// Timezone returns the configured timezone
123func (c *SystemConfig) Timezone() string {
124	return c.timezone
125}
126
127// Locale returns the configured locale
128func (c *SystemConfig) Locale() string {
129	return c.locale
130}
131
132// Keymap returns the configured keymap
133func (c *SystemConfig) Keymap() string {
134	return c.keymap
135}
136
137// String returns a human-readable representation
138func (c *SystemConfig) String() string {
139	return "SystemConfig(" +
140		"hostname=" + c.hostname +
141		", timezone=" + c.timezone +
142		", locale=" + c.locale +
143		", keymap=" + c.keymap +
144		")"
145}
146
147// Equals checks if two SystemConfig objects are equal
148// Value objects should be compared by their fields
149func (c *SystemConfig) Equals(other *SystemConfig) bool {
150	if other == nil {
151		return false
152	}
153	return c.hostname == other.hostname &&
154		c.timezone == other.timezone &&
155		c.locale == other.locale &&
156		c.keymap == other.keymap
157}
158
159// Errors specific to SystemConfig
160var (
161	// ErrInvalidHostname is returned when hostname validation fails
162	ErrInvalidHostname = errors.New("invalid hostname: must be 1-63 chars, alphanumeric and hyphens, no leading/trailing hyphen")
163
164	// ErrInvalidTimezone is returned when timezone validation fails
165	ErrInvalidTimezone = errors.New("invalid timezone")
166
167	// ErrInvalidLocale is returned when locale validation fails
168	ErrInvalidLocale = errors.New("invalid locale")
169
170	// ErrInvalidKeymap is returned when keymap validation fails
171	ErrInvalidKeymap = errors.New("invalid keymap")
172
173	// ErrInvalidUsername is returned when username validation fails
174	ErrInvalidUsername = errors.New("invalid username: must be 1-32 chars, lowercase, start with letter")
175
176	// ErrInvalidPassword is returned when password validation fails
177	ErrInvalidPassword = errors.New("invalid password: must be at least 4 characters")
178)
179
180// ValidateHostname validates hostname according to RFC 1123
181// Valid hostnames: letters, digits, hyphens
182// Cannot start/end with hyphen, length 1-63 characters
183func ValidateHostname(hostname string) error {
184	if hostname == "" {
185		return ErrInvalidHostname
186	}
187
188	if len(hostname) > 63 {
189		return ErrInvalidHostname
190	}
191
192	if hostname[0] == '-' || hostname[len(hostname)-1] == '-' {
193		return ErrInvalidHostname
194	}
195
196	for _, ch := range hostname {
197		if !isHostnameChar(ch) {
198			return ErrInvalidHostname
199		}
200	}
201
202	return nil
203}
204
205// ValidateTimezone validates timezone string
206// Common zones: "UTC", "US/Eastern", "Europe/London", etc.
207// Empty timezone is allowed (means system default)
208func ValidateTimezone(timezone string) error {
209	// Empty timezone is allowed (system default)
210	if timezone == "" {
211		return nil
212	}
213
214	// Check for obviously invalid patterns
215	if strings.Contains(timezone, "..") || strings.HasPrefix(timezone, "/") {
216		return ErrInvalidTimezone
217	}
218
219	// Length check (reasonable limit)
220	if len(timezone) > 64 {
221		return ErrInvalidTimezone
222	}
223
224	return nil
225}
226
227// ValidateLocale validates locale string
228// Valid formats: "en_US.UTF-8", "en_US", "C.UTF-8", etc.
229// Empty locale is allowed (means system default)
230func ValidateLocale(locale string) error {
231	// Empty locale is allowed (system default)
232	if locale == "" {
233		return nil
234	}
235
236	// Basic format validation
237	if len(locale) > 64 {
238		return ErrInvalidLocale
239	}
240
241	// Should contain language code
242	if !strings.Contains(locale, "_") && !strings.Contains(locale, ".") && locale != "C" {
243		return ErrInvalidLocale
244	}
245
246	return nil
247}
248
249// ValidateKeymap validates keyboard layout
250// Common layouts: us, de, fr, dvorak, etc.
251// Can include variations like de-nodeadkeys
252func ValidateKeymap(keymap string) error {
253	if keymap == "" {
254		return ErrInvalidKeymap
255	}
256
257	if len(keymap) > 64 {
258		return ErrInvalidKeymap
259	}
260
261	// Allow letters, digits, hyphens
262	for _, ch := range keymap {
263		if !isKeymapChar(ch) {
264			return ErrInvalidKeymap
265		}
266	}
267
268	return nil
269}
270
271func isHostnameChar(ch rune) bool {
272	return (ch >= 'a' && ch <= 'z') ||
273		(ch >= 'A' && ch <= 'Z') ||
274		(ch >= '0' && ch <= '9') ||
275		ch == '-'
276}
277
278func isKeymapChar(ch rune) bool {
279	return (ch >= 'a' && ch <= 'z') ||
280		(ch >= '0' && ch <= '9') ||
281		ch == '-'
282}
283
284// ValidateUsername validates Linux username
285// Must be 1-32 chars, lowercase, start with letter, alphanumeric and underscores
286func ValidateUsername(username string) error {
287	if username == "" || len(username) > 32 {
288		return ErrInvalidUsername
289	}
290
291	// Must start with lowercase letter
292	if username[0] < 'a' || username[0] > 'z' {
293		return ErrInvalidUsername
294	}
295
296	// Must be lowercase alphanumeric or underscore
297	for _, ch := range username {
298		if !isUsernameChar(ch) {
299			return ErrInvalidUsername
300		}
301	}
302
303	return nil
304}
305
306func isUsernameChar(ch rune) bool {
307	return (ch >= 'a' && ch <= 'z') ||
308		(ch >= '0' && ch <= '9') ||
309		ch == '_'
310}
311
312// ValidatePassword validates password using user domain validation
313// Must be at least 4 characters
314func ValidatePassword(password string) error {
315	if password == "" {
316		return ErrInvalidPassword
317	}
318	if len(password) < 4 {
319		return ErrInvalidPassword
320	}
321	if len(password) > 128 {
322		return ErrInvalidPassword
323	}
324	return nil
325}

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 1

Tags 0

    ↑↓ moveenter openesc close