feat(tui,install): embed limine logo, CachyOS host setup, flat repo nav, unified hints, nav fixes
- commit
- ea4c67306836c2871eb09a547c7f54c1f0c4664f
- author
- brice · · 2026-02-27 17:49 UTC
- parent
- fad4841
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
18 files · +369 -107
A assets/arch-logo.png +0 -0 binary file: no text diff
binary file: no text diff
A assets/gen-limine-logo.py +113 -0
| @@ -0,0 +1,113 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | """ | |
| 3 | + | Generate arch-logo.png for Limine bootloader wallpaper. | |
| 4 | + | ||
| 5 | + | Renders the Arch ASCII logo with the same cyan→slate-blue gradient used in | |
| 6 | + | hyprlock.conf, on a deep navy background. Output is sized for 1920x1080. | |
| 7 | + | ||
| 8 | + | Usage: python3 gen-limine-logo.py [--out path/to/arch-logo.png] | |
| 9 | + | """ | |
| 10 | + | ||
| 11 | + | import sys | |
| 12 | + | import os | |
| 13 | + | import argparse | |
| 14 | + | from PIL import Image, ImageDraw, ImageFont | |
| 15 | + | ||
| 16 | + | LOGO_LINES = [ | |
| 17 | + | " -`", | |
| 18 | + | " .o+`", | |
| 19 | + | " `ooo/", | |
| 20 | + | " `+oooo:", | |
| 21 | + | " `+oooooo:", | |
| 22 | + | " -+oooooo+:", | |
| 23 | + | " `/:-:++oooo+:", | |
| 24 | + | " `/++++/+++++++:", | |
| 25 | + | " `/++++++++++++++:", | |
| 26 | + | " `/+++ooooooooooooo/`", | |
| 27 | + | " ./ooosssso++osssssso+`", | |
| 28 | + | " .oossssso-````/ossssss+`", | |
| 29 | + | " -osssssso. :ssssssso.", | |
| 30 | + | " :osssssss/ osssso+++.", | |
| 31 | + | " /ossssssss/ +ssssooo/-", | |
| 32 | + | " `/ossssso+/:- -:/+osssso+-", | |
| 33 | + | " `+sso+:-` `.-/+oso:", | |
| 34 | + | " `++:. `-/+/", | |
| 35 | + | " .` `", | |
| 36 | + | ] | |
| 37 | + | ||
| 38 | + | # Gradient: cyan at top → slate-blue at bottom (matching hyprlock.conf) | |
| 39 | + | GRADIENT_COLORS = [ | |
| 40 | + | (0, 212, 255), # line 0 - bright cyan | |
| 41 | + | (0, 212, 255), # line 1 | |
| 42 | + | (0, 212, 255), # line 2 | |
| 43 | + | (0, 212, 255), # line 3 | |
| 44 | + | (0, 196, 239), # line 4 | |
| 45 | + | (0, 180, 223), # line 5 | |
| 46 | + | (34, 164, 207), # line 6 | |
| 47 | + | (68, 148, 191), # line 7 | |
| 48 | + | (85, 136, 204), # line 8 | |
| 49 | + | (85, 128, 192), # line 9 | |
| 50 | + | (80, 120, 180), # line 10 | |
| 51 | + | (74, 123, 167), # line 11 | |
| 52 | + | (74, 123, 167), # line 12 | |
| 53 | + | (90, 122, 154), # line 13 | |
| 54 | + | (96, 122, 148), # line 14 | |
| 55 | + | (101, 122, 144), # line 15 | |
| 56 | + | (106, 122, 140), # line 16 | |
| 57 | + | (110, 122, 136), # line 17 | |
| 58 | + | (112, 128, 144), # line 18 | |
| 59 | + | ] | |
| 60 | + | ||
| 61 | + | # Background: deep navy from hyprlock | |
| 62 | + | BG_COLOR = (5, 10, 20) | |
| 63 | + | ||
| 64 | + | FONT_SIZE = 36 | |
| 65 | + | LINE_SPACING = 42 # px between lines | |
| 66 | + | PADDING = 40 # px padding around the logo content | |
| 67 | + | ||
| 68 | + | FONT_PATH = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Thin.ttf" | |
| 69 | + | FONT_PATH_FALLBACK = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf" | |
| 70 | + | ||
| 71 | + | ||
| 72 | + | def main(): | |
| 73 | + | parser = argparse.ArgumentParser() | |
| 74 | + | parser.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "arch-logo.png")) | |
| 75 | + | args = parser.parse_args() | |
| 76 | + | ||
| 77 | + | # Load font | |
| 78 | + | font_path = FONT_PATH if os.path.exists(FONT_PATH) else FONT_PATH_FALLBACK | |
| 79 | + | try: | |
| 80 | + | font = ImageFont.truetype(font_path, FONT_SIZE) | |
| 81 | + | except Exception as e: | |
| 82 | + | print(f"Failed to load font {font_path}: {e}", file=sys.stderr) | |
| 83 | + | sys.exit(1) | |
| 84 | + | ||
| 85 | + | # Measure block dimensions using a throw-away draw surface | |
| 86 | + | tmp = Image.new("RGB", (4000, 2000), BG_COLOR) | |
| 87 | + | tmp_draw = ImageDraw.Draw(tmp) | |
| 88 | + | widths = [tmp_draw.textbbox((0, 0), line, font=font)[2] for line in LOGO_LINES] | |
| 89 | + | max_w = max(widths) | |
| 90 | + | total_h = len(LOGO_LINES) * LINE_SPACING | |
| 91 | + | ||
| 92 | + | # Canvas is just the logo + padding — Limine centers it on screen | |
| 93 | + | img_w = int(max_w) + PADDING * 2 | |
| 94 | + | img_h = int(total_h) + PADDING * 2 | |
| 95 | + | ||
| 96 | + | img = Image.new("RGB", (img_w, img_h), BG_COLOR) | |
| 97 | + | draw = ImageDraw.Draw(img) | |
| 98 | + | ||
| 99 | + | for i, line in enumerate(LOGO_LINES): | |
| 100 | + | color = GRADIENT_COLORS[i] if i < len(GRADIENT_COLORS) else GRADIENT_COLORS[-1] | |
| 101 | + | y = PADDING + i * LINE_SPACING | |
| 102 | + | draw.text((PADDING, y), line, font=font, fill=color) | |
| 103 | + | ||
| 104 | + | # Quantize to a small palette (no dithering) — cuts file size dramatically | |
| 105 | + | # while preserving the gradient colors faithfully at boot resolution. | |
| 106 | + | img = img.quantize(colors=32, dither=Image.Dither.NONE) | |
| 107 | + | img.save(args.out, "PNG", optimize=True) | |
| 108 | + | size_kb = os.path.getsize(args.out) / 1024 | |
| 109 | + | print(f"Generated: {args.out} ({img_w}x{img_h}, {size_kb:.1f}K)") | |
| 110 | + | ||
| 111 | + | ||
| 112 | + | if __name__ == "__main__": | |
| 113 | + | main() | |
| @@ -0,0 +1,113 @@ | |||||
| 1 | + | #!/usr/bin/env python3 | |||
| 2 | + | """ | |||
| 3 | + | Generate arch-logo.png for Limine bootloader wallpaper. | |||
| 4 | + | ||||
| 5 | + | Renders the Arch ASCII logo with the same cyan→slate-blue gradient used in | |||
| 6 | + | hyprlock.conf, on a deep navy background. Output is sized for 1920x1080. | |||
| 7 | + | ||||
| 8 | + | Usage: python3 gen-limine-logo.py [--out path/to/arch-logo.png] | |||
| 9 | + | """ | |||
| 10 | + | ||||
| 11 | + | import sys | |||
| 12 | + | import os | |||
| 13 | + | import argparse | |||
| 14 | + | from PIL import Image, ImageDraw, ImageFont | |||
| 15 | + | ||||
| 16 | + | LOGO_LINES = [ | |||
| 17 | + | " -`", | |||
| 18 | + | " .o+`", | |||
| 19 | + | " `ooo/", | |||
| 20 | + | " `+oooo:", | |||
| 21 | + | " `+oooooo:", | |||
| 22 | + | " -+oooooo+:", | |||
| 23 | + | " `/:-:++oooo+:", | |||
| 24 | + | " `/++++/+++++++:", | |||
| 25 | + | " `/++++++++++++++:", | |||
| 26 | + | " `/+++ooooooooooooo/`", | |||
| 27 | + | " ./ooosssso++osssssso+`", | |||
| 28 | + | " .oossssso-````/ossssss+`", | |||
| 29 | + | " -osssssso. :ssssssso.", | |||
| 30 | + | " :osssssss/ osssso+++.", | |||
| 31 | + | " /ossssssss/ +ssssooo/-", | |||
| 32 | + | " `/ossssso+/:- -:/+osssso+-", | |||
| 33 | + | " `+sso+:-` `.-/+oso:", | |||
| 34 | + | " `++:. `-/+/", | |||
| 35 | + | " .` `", | |||
| 36 | + | ] | |||
| 37 | + | ||||
| 38 | + | # Gradient: cyan at top → slate-blue at bottom (matching hyprlock.conf) | |||
| 39 | + | GRADIENT_COLORS = [ | |||
| 40 | + | (0, 212, 255), # line 0 - bright cyan | |||
| 41 | + | (0, 212, 255), # line 1 | |||
| 42 | + | (0, 212, 255), # line 2 | |||
| 43 | + | (0, 212, 255), # line 3 | |||
| 44 | + | (0, 196, 239), # line 4 | |||
| 45 | + | (0, 180, 223), # line 5 | |||
| 46 | + | (34, 164, 207), # line 6 | |||
| 47 | + | (68, 148, 191), # line 7 | |||
| 48 | + | (85, 136, 204), # line 8 | |||
| 49 | + | (85, 128, 192), # line 9 | |||
| 50 | + | (80, 120, 180), # line 10 | |||
| 51 | + | (74, 123, 167), # line 11 | |||
| 52 | + | (74, 123, 167), # line 12 | |||
| 53 | + | (90, 122, 154), # line 13 | |||
| 54 | + | (96, 122, 148), # line 14 | |||
| 55 | + | (101, 122, 144), # line 15 | |||
| 56 | + | (106, 122, 140), # line 16 | |||
| 57 | + | (110, 122, 136), # line 17 | |||
| 58 | + | (112, 128, 144), # line 18 | |||
| 59 | + | ] | |||
| 60 | + | ||||
| 61 | + | # Background: deep navy from hyprlock | |||
| 62 | + | BG_COLOR = (5, 10, 20) | |||
| 63 | + | ||||
| 64 | + | FONT_SIZE = 36 | |||
| 65 | + | LINE_SPACING = 42 # px between lines | |||
| 66 | + | PADDING = 40 # px padding around the logo content | |||
| 67 | + | ||||
| 68 | + | FONT_PATH = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Thin.ttf" | |||
| 69 | + | FONT_PATH_FALLBACK = "/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf" | |||
| 70 | + | ||||
| 71 | + | ||||
| 72 | + | def main(): | |||
| 73 | + | parser = argparse.ArgumentParser() | |||
| 74 | + | parser.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "arch-logo.png")) | |||
| 75 | + | args = parser.parse_args() | |||
| 76 | + | ||||
| 77 | + | # Load font | |||
| 78 | + | font_path = FONT_PATH if os.path.exists(FONT_PATH) else FONT_PATH_FALLBACK | |||
| 79 | + | try: | |||
| 80 | + | font = ImageFont.truetype(font_path, FONT_SIZE) | |||
| 81 | + | except Exception as e: | |||
| 82 | + | print(f"Failed to load font {font_path}: {e}", file=sys.stderr) | |||
| 83 | + | sys.exit(1) | |||
| 84 | + | ||||
| 85 | + | # Measure block dimensions using a throw-away draw surface | |||
| 86 | + | tmp = Image.new("RGB", (4000, 2000), BG_COLOR) | |||
| 87 | + | tmp_draw = ImageDraw.Draw(tmp) | |||
| 88 | + | widths = [tmp_draw.textbbox((0, 0), line, font=font)[2] for line in LOGO_LINES] | |||
| 89 | + | max_w = max(widths) | |||
| 90 | + | total_h = len(LOGO_LINES) * LINE_SPACING | |||
| 91 | + | ||||
| 92 | + | # Canvas is just the logo + padding — Limine centers it on screen | |||
| 93 | + | img_w = int(max_w) + PADDING * 2 | |||
| 94 | + | img_h = int(total_h) + PADDING * 2 | |||
| 95 | + | ||||
| 96 | + | img = Image.new("RGB", (img_w, img_h), BG_COLOR) | |||
| 97 | + | draw = ImageDraw.Draw(img) | |||
| 98 | + | ||||
| 99 | + | for i, line in enumerate(LOGO_LINES): | |||
| 100 | + | color = GRADIENT_COLORS[i] if i < len(GRADIENT_COLORS) else GRADIENT_COLORS[-1] | |||
| 101 | + | y = PADDING + i * LINE_SPACING | |||
| 102 | + | draw.text((PADDING, y), line, font=font, fill=color) | |||
| 103 | + | ||||
| 104 | + | # Quantize to a small palette (no dithering) — cuts file size dramatically | |||
| 105 | + | # while preserving the gradient colors faithfully at boot resolution. | |||
| 106 | + | img = img.quantize(colors=32, dither=Image.Dither.NONE) | |||
| 107 | + | img.save(args.out, "PNG", optimize=True) | |||
| 108 | + | size_kb = os.path.getsize(args.out) / 1024 | |||
| 109 | + | print(f"Generated: {args.out} ({img_w}x{img_h}, {size_kb:.1f}K)") | |||
| 110 | + | ||||
| 111 | + | ||||
| 112 | + | if __name__ == "__main__": | |||
| 113 | + | main() | |||
M internal/application/handlers/install_base_handler.go +91 -0
| @@ -80,6 +80,15 @@ func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBas | |||
| 80 | 80 | h.logger.Info("Adding additional packages", "count", len(cmd.Packages)) | |
| 81 | 81 | } | |
| 82 | 82 | ||
| 83 | + | // For CachyOS kernel: add the repo to the live host before pacstrap | |
| 84 | + | if cmd.KernelVariant == packages.KernelCachyOS { | |
| 85 | + | if err := h.setupCachyOSOnHost(ctx); err != nil { | |
| 86 | + | h.logger.Error("Failed to setup CachyOS repo on host", "error", err) | |
| 87 | + | result.ErrorDetail = fmt.Sprintf("Failed to setup CachyOS repo on host: %v", err) | |
| 88 | + | return result, err | |
| 89 | + | } | |
| 90 | + | } | |
| 91 | + | ||
| 83 | 92 | h.logger.Info("Installing base packages", "count", len(basePackages)) | |
| 84 | 93 | args := append([]string{cmd.MountPoint}, basePackages...) | |
| 85 | 94 | if _, err := h.cmdExec.Execute(ctx, "pacstrap", args...); err != nil { | |
| @@ -109,6 +118,88 @@ func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBas | |||
| 109 | 118 | return result, nil | |
| 110 | 119 | } | |
| 111 | 120 | ||
| 121 | + | // setupCachyOSOnHost configures the CachyOS repo on the live host so that | |
| 122 | + | // pacstrap can resolve linux-cachyos. Mirrors the working bash approach: | |
| 123 | + | // import+sign key, write mirrorlist manually (no versioned package URLs), | |
| 124 | + | // patch pacman.conf, then sync. | |
| 125 | + | func (h *InstallBaseHandler) setupCachyOSOnHost(ctx context.Context) error { | |
| 126 | + | h.logger.Info("Setting up CachyOS repo on host for pacstrap") | |
| 127 | + | ||
| 128 | + | const ( | |
| 129 | + | cachyOSKeyID = "F3B607488DB35A47" | |
| 130 | + | keyserver = "keyserver.ubuntu.com" | |
| 131 | + | hostPacmanConf = "/etc/pacman.conf" | |
| 132 | + | cachyOSMirrorlistPath = "/etc/pacman.d/cachyos-mirrorlist" | |
| 133 | + | cachyOSMirrorlist = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n" | |
| 134 | + | ) | |
| 135 | + | ||
| 136 | + | // Only fetch from keyserver if key is not already present. | |
| 137 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--list-keys", cachyOSKeyID); err != nil { | |
| 138 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--recv-keys", | |
| 139 | + | cachyOSKeyID, "--keyserver", keyserver); err != nil { | |
| 140 | + | return fmt.Errorf("failed to receive CachyOS key on host: %w", err) | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // Local-sign the key so pacman trusts packages from this unofficial repo. | |
| 145 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil { | |
| 146 | + | return fmt.Errorf("failed to sign CachyOS key on host: %w", err) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // Write static mirrorlist — no versioned package URLs that can go stale. | |
| 150 | + | if err := h.fs.WriteFile(cachyOSMirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil { | |
| 151 | + | return fmt.Errorf("failed to write host CachyOS mirrorlist: %w", err) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | confBytes, err := h.fs.ReadFile(hostPacmanConf) | |
| 155 | + | if err != nil { | |
| 156 | + | return fmt.Errorf("failed to read host pacman.conf: %w", err) | |
| 157 | + | } | |
| 158 | + | conf := ensureCachyOSHostRepo(string(confBytes)) | |
| 159 | + | if err := h.fs.WriteFile(hostPacmanConf, []byte(conf), 0644); err != nil { | |
| 160 | + | return fmt.Errorf("failed to write host pacman.conf: %w", err) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | if _, err := h.cmdExec.Execute(ctx, "pacman", "-Sy", "--noconfirm"); err != nil { | |
| 164 | + | return fmt.Errorf("failed to sync host pacman repos: %w", err) | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | h.logger.Info("CachyOS repo ready on host") | |
| 168 | + | return nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func ensureCachyOSHostRepo(conf string) string { | |
| 172 | + | if strings.Contains(conf, "[cachyos]") { | |
| 173 | + | return conf | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | conf = strings.TrimRight(conf, "\n") | |
| 177 | + | if conf == "" { | |
| 178 | + | return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | block := []string{ | |
| 182 | + | "# CachyOS repositories", | |
| 183 | + | "[cachyos]", | |
| 184 | + | "Include = /etc/pacman.d/cachyos-mirrorlist", | |
| 185 | + | "", | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | lines := strings.Split(conf, "\n") | |
| 189 | + | for i, line := range lines { | |
| 190 | + | if strings.TrimSpace(line) == "[core]" { | |
| 191 | + | out := make([]string, 0, len(lines)+len(block)) | |
| 192 | + | out = append(out, lines[:i]...) | |
| 193 | + | out = append(out, block...) | |
| 194 | + | out = append(out, lines[i:]...) | |
| 195 | + | return strings.Join(out, "\n") + "\n" | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // Fallback: append if [core] section is not found. | |
| 200 | + | return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |
| 201 | + | } | |
| 202 | + | ||
| 112 | 203 | // loadBasePackages reads the base package list from file | |
| 113 | 204 | func (h *InstallBaseHandler) loadBasePackages() ([]string, error) { | |
| 114 | 205 | // Read from install directory (downloaded during bootstrap) | |
| @@ -80,6 +80,15 @@ func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBas | |||||
| 80 | h.logger.Info("Adding additional packages", "count", len(cmd.Packages)) | 80 | h.logger.Info("Adding additional packages", "count", len(cmd.Packages)) | ||
| 81 | } | 81 | } | ||
| 82 | 82 | ||||
| 83 | + | // For CachyOS kernel: add the repo to the live host before pacstrap | |||
| 84 | + | if cmd.KernelVariant == packages.KernelCachyOS { | |||
| 85 | + | if err := h.setupCachyOSOnHost(ctx); err != nil { | |||
| 86 | + | h.logger.Error("Failed to setup CachyOS repo on host", "error", err) | |||
| 87 | + | result.ErrorDetail = fmt.Sprintf("Failed to setup CachyOS repo on host: %v", err) | |||
| 88 | + | return result, err | |||
| 89 | + | } | |||
| 90 | + | } | |||
| 91 | + | ||||
| 83 | h.logger.Info("Installing base packages", "count", len(basePackages)) | 92 | h.logger.Info("Installing base packages", "count", len(basePackages)) | ||
| 84 | args := append([]string{cmd.MountPoint}, basePackages...) | 93 | args := append([]string{cmd.MountPoint}, basePackages...) | ||
| 85 | if _, err := h.cmdExec.Execute(ctx, "pacstrap", args...); err != nil { | 94 | if _, err := h.cmdExec.Execute(ctx, "pacstrap", args...); err != nil { | ||
| @@ -109,6 +118,88 @@ func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBas | |||||
| 109 | return result, nil | 118 | return result, nil | ||
| 110 | } | 119 | } | ||
| 111 | 120 | ||||
| 121 | + | // setupCachyOSOnHost configures the CachyOS repo on the live host so that | |||
| 122 | + | // pacstrap can resolve linux-cachyos. Mirrors the working bash approach: | |||
| 123 | + | // import+sign key, write mirrorlist manually (no versioned package URLs), | |||
| 124 | + | // patch pacman.conf, then sync. | |||
| 125 | + | func (h *InstallBaseHandler) setupCachyOSOnHost(ctx context.Context) error { | |||
| 126 | + | h.logger.Info("Setting up CachyOS repo on host for pacstrap") | |||
| 127 | + | ||||
| 128 | + | const ( | |||
| 129 | + | cachyOSKeyID = "F3B607488DB35A47" | |||
| 130 | + | keyserver = "keyserver.ubuntu.com" | |||
| 131 | + | hostPacmanConf = "/etc/pacman.conf" | |||
| 132 | + | cachyOSMirrorlistPath = "/etc/pacman.d/cachyos-mirrorlist" | |||
| 133 | + | cachyOSMirrorlist = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n" | |||
| 134 | + | ) | |||
| 135 | + | ||||
| 136 | + | // Only fetch from keyserver if key is not already present. | |||
| 137 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--list-keys", cachyOSKeyID); err != nil { | |||
| 138 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--recv-keys", | |||
| 139 | + | cachyOSKeyID, "--keyserver", keyserver); err != nil { | |||
| 140 | + | return fmt.Errorf("failed to receive CachyOS key on host: %w", err) | |||
| 141 | + | } | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // Local-sign the key so pacman trusts packages from this unofficial repo. | |||
| 145 | + | if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil { | |||
| 146 | + | return fmt.Errorf("failed to sign CachyOS key on host: %w", err) | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // Write static mirrorlist — no versioned package URLs that can go stale. | |||
| 150 | + | if err := h.fs.WriteFile(cachyOSMirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil { | |||
| 151 | + | return fmt.Errorf("failed to write host CachyOS mirrorlist: %w", err) | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | confBytes, err := h.fs.ReadFile(hostPacmanConf) | |||
| 155 | + | if err != nil { | |||
| 156 | + | return fmt.Errorf("failed to read host pacman.conf: %w", err) | |||
| 157 | + | } | |||
| 158 | + | conf := ensureCachyOSHostRepo(string(confBytes)) | |||
| 159 | + | if err := h.fs.WriteFile(hostPacmanConf, []byte(conf), 0644); err != nil { | |||
| 160 | + | return fmt.Errorf("failed to write host pacman.conf: %w", err) | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | if _, err := h.cmdExec.Execute(ctx, "pacman", "-Sy", "--noconfirm"); err != nil { | |||
| 164 | + | return fmt.Errorf("failed to sync host pacman repos: %w", err) | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | h.logger.Info("CachyOS repo ready on host") | |||
| 168 | + | return nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func ensureCachyOSHostRepo(conf string) string { | |||
| 172 | + | if strings.Contains(conf, "[cachyos]") { | |||
| 173 | + | return conf | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | conf = strings.TrimRight(conf, "\n") | |||
| 177 | + | if conf == "" { | |||
| 178 | + | return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | block := []string{ | |||
| 182 | + | "# CachyOS repositories", | |||
| 183 | + | "[cachyos]", | |||
| 184 | + | "Include = /etc/pacman.d/cachyos-mirrorlist", | |||
| 185 | + | "", | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | lines := strings.Split(conf, "\n") | |||
| 189 | + | for i, line := range lines { | |||
| 190 | + | if strings.TrimSpace(line) == "[core]" { | |||
| 191 | + | out := make([]string, 0, len(lines)+len(block)) | |||
| 192 | + | out = append(out, lines[:i]...) | |||
| 193 | + | out = append(out, block...) | |||
| 194 | + | out = append(out, lines[i:]...) | |||
| 195 | + | return strings.Join(out, "\n") + "\n" | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // Fallback: append if [core] section is not found. | |||
| 200 | + | return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |||
| 201 | + | } | |||
| 202 | + | ||||
| 112 | // loadBasePackages reads the base package list from file | 203 | // loadBasePackages reads the base package list from file | ||
| 113 | func (h *InstallBaseHandler) loadBasePackages() ([]string, error) { | 204 | func (h *InstallBaseHandler) loadBasePackages() ([]string, error) { | ||
| 114 | // Read from install directory (downloaded during bootstrap) | 205 | // Read from install directory (downloaded during bootstrap) | ||
M internal/application/handlers/post_install_handler.go +2 -6
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "github.com/bnema/archup/internal/application/dto" | |
| 12 | 12 | "github.com/bnema/archup/internal/config" | |
| 13 | 13 | "github.com/bnema/archup/internal/domain/ports" | |
| 14 | + | "github.com/bnema/archup/internal/ui/assets" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| 16 | 17 | // PostInstallHandler handles post-installation tasks | |
| @@ -159,13 +160,8 @@ func (h *PostInstallHandler) setupPostBoot(ctx context.Context, mountPoint, user | |||
| 159 | 160 | } | |
| 160 | 161 | ||
| 161 | 162 | func (h *PostInstallHandler) installLimineLogo(mountPoint string) error { | |
| 162 | - | logoContent, err := h.downloadTemplate(config.ArchLogoURL) | |
| 163 | - | if err != nil { | |
| 164 | - | return err | |
| 165 | - | } | |
| 166 | - | ||
| 167 | 163 | logoPath := filepath.Join(mountPoint, "boot", "arch-logo.png") | |
| 168 | - | if err := h.fs.WriteFile(logoPath, logoContent, 0644); err != nil { | |
| 164 | + | if err := h.fs.WriteFile(logoPath, assets.LimineLogo, 0644); err != nil { | |
| 169 | 165 | return fmt.Errorf("failed to write Limine logo: %w", err) | |
| 170 | 166 | } | |
| 171 | 167 | ||
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "github.com/bnema/archup/internal/application/dto" | 11 | "github.com/bnema/archup/internal/application/dto" | ||
| 12 | "github.com/bnema/archup/internal/config" | 12 | "github.com/bnema/archup/internal/config" | ||
| 13 | "github.com/bnema/archup/internal/domain/ports" | 13 | "github.com/bnema/archup/internal/domain/ports" | ||
| 14 | + | "github.com/bnema/archup/internal/ui/assets" | |||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| 16 | // PostInstallHandler handles post-installation tasks | 17 | // PostInstallHandler handles post-installation tasks | ||
| @@ -159,13 +160,8 @@ func (h *PostInstallHandler) setupPostBoot(ctx context.Context, mountPoint, user | |||||
| 159 | } | 160 | } | ||
| 160 | 161 | ||||
| 161 | func (h *PostInstallHandler) installLimineLogo(mountPoint string) error { | 162 | func (h *PostInstallHandler) installLimineLogo(mountPoint string) error { | ||
| 162 | - | logoContent, err := h.downloadTemplate(config.ArchLogoURL) | |||
| 163 | - | if err != nil { | |||
| 164 | - | return err | |||
| 165 | - | } | |||
| 166 | - | ||||
| 167 | logoPath := filepath.Join(mountPoint, "boot", "arch-logo.png") | 163 | logoPath := filepath.Join(mountPoint, "boot", "arch-logo.png") | ||
| 168 | - | if err := h.fs.WriteFile(logoPath, logoContent, 0644); err != nil { | 164 | + | if err := h.fs.WriteFile(logoPath, assets.LimineLogo, 0644); err != nil { |
| 169 | return fmt.Errorf("failed to write Limine logo: %w", err) | 165 | return fmt.Errorf("failed to write Limine logo: %w", err) | ||
| 170 | } | 166 | } | ||
| 171 | 167 | ||||
M internal/application/handlers/repos_handler.go +47 -11
| @@ -203,22 +203,34 @@ func ensureChaoticRepo(conf string) string { | |||
| 203 | 203 | return conf + chaotic | |
| 204 | 204 | } | |
| 205 | 205 | ||
| 206 | + | // setupCachyOSRepo configures the CachyOS repo inside the chroot. Mirrors the | |
| 207 | + | // bash approach: import+sign key, write mirrorlist manually (no versioned | |
| 208 | + | // package URLs), patch pacman.conf, then sync. | |
| 206 | 209 | func (h *ReposHandler) setupCachyOSRepo(ctx context.Context, mountPoint string) error { | |
| 207 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--recv-key", | |
| 208 | - | "F3B607488DB35A47", "--keyserver", "keyserver.ubuntu.com"); err != nil { | |
| 209 | - | return fmt.Errorf("failed to receive CachyOS key: %w", err) | |
| 210 | + | const ( | |
| 211 | + | cachyOSKeyID = "F3B607488DB35A47" | |
| 212 | + | keyserver = "keyserver.ubuntu.com" | |
| 213 | + | cachyOSMirrorlist = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n" | |
| 214 | + | ) | |
| 215 | + | ||
| 216 | + | // Only fetch from keyserver if key is not already present in chroot. | |
| 217 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--list-keys", cachyOSKeyID); err != nil { | |
| 218 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--recv-keys", | |
| 219 | + | cachyOSKeyID, "--keyserver", keyserver); err != nil { | |
| 220 | + | return fmt.Errorf("failed to receive CachyOS key: %w", err) | |
| 221 | + | } | |
| 210 | 222 | } | |
| 211 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--lsign-key", | |
| 212 | - | "F3B607488DB35A47"); err != nil { | |
| 223 | + | ||
| 224 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil { | |
| 213 | 225 | return fmt.Errorf("failed to sign CachyOS key: %w", err) | |
| 214 | 226 | } | |
| 215 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman", "-U", "--noconfirm", | |
| 216 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-keyring-20240331-1-any.pkg.tar.zst", | |
| 217 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-mirrorlist-18-1-any.pkg.tar.zst", | |
| 218 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-v3-mirrorlist-18-1-any.pkg.tar.zst", | |
| 219 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-v4-mirrorlist-6-1-any.pkg.tar.zst"); err != nil { | |
| 220 | - | return fmt.Errorf("failed to install CachyOS packages: %w", err) | |
| 227 | + | ||
| 228 | + | // Write static mirrorlist — no versioned package URLs that can go stale. | |
| 229 | + | mirrorlistPath := filepath.Join(mountPoint, "etc", "pacman.d", "cachyos-mirrorlist") | |
| 230 | + | if err := h.fs.WriteFile(mirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil { | |
| 231 | + | return fmt.Errorf("failed to write CachyOS mirrorlist: %w", err) | |
| 221 | 232 | } | |
| 233 | + | ||
| 222 | 234 | pacmanConfPath := filepath.Join(mountPoint, "etc", "pacman.conf") | |
| 223 | 235 | confBytes, err := h.fs.ReadFile(pacmanConfPath) | |
| 224 | 236 | if err != nil { | |
| @@ -232,6 +244,30 @@ func ensureCachyOSRepo(conf string) string { | |||
| 232 | 244 | if strings.Contains(conf, "[cachyos]") { | |
| 233 | 245 | return conf | |
| 234 | 246 | } | |
| 247 | + | ||
| 235 | 248 | conf = strings.TrimRight(conf, "\n") | |
| 249 | + | if conf == "" { | |
| 250 | + | return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | block := []string{ | |
| 254 | + | "# CachyOS repositories", | |
| 255 | + | "[cachyos]", | |
| 256 | + | "Include = /etc/pacman.d/cachyos-mirrorlist", | |
| 257 | + | "", | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | lines := strings.Split(conf, "\n") | |
| 261 | + | for i, line := range lines { | |
| 262 | + | if strings.TrimSpace(line) == "[core]" { | |
| 263 | + | out := make([]string, 0, len(lines)+len(block)) | |
| 264 | + | out = append(out, lines[:i]...) | |
| 265 | + | out = append(out, block...) | |
| 266 | + | out = append(out, lines[i:]...) | |
| 267 | + | return strings.Join(out, "\n") + "\n" | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // Fallback: append if [core] section is not found. | |
| 236 | 272 | return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |
| 237 | 273 | } | |
| @@ -203,22 +203,34 @@ func ensureChaoticRepo(conf string) string { | |||||
| 203 | return conf + chaotic | 203 | return conf + chaotic | ||
| 204 | } | 204 | } | ||
| 205 | 205 | ||||
| 206 | + | // setupCachyOSRepo configures the CachyOS repo inside the chroot. Mirrors the | |||
| 207 | + | // bash approach: import+sign key, write mirrorlist manually (no versioned | |||
| 208 | + | // package URLs), patch pacman.conf, then sync. | |||
| 206 | func (h *ReposHandler) setupCachyOSRepo(ctx context.Context, mountPoint string) error { | 209 | func (h *ReposHandler) setupCachyOSRepo(ctx context.Context, mountPoint string) error { | ||
| 207 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--recv-key", | 210 | + | const ( |
| 208 | - | "F3B607488DB35A47", "--keyserver", "keyserver.ubuntu.com"); err != nil { | 211 | + | cachyOSKeyID = "F3B607488DB35A47" |
| 209 | - | return fmt.Errorf("failed to receive CachyOS key: %w", err) | 212 | + | keyserver = "keyserver.ubuntu.com" |
| 213 | + | cachyOSMirrorlist = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n" | |||
| 214 | + | ) | |||
| 215 | + | ||||
| 216 | + | // Only fetch from keyserver if key is not already present in chroot. | |||
| 217 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--list-keys", cachyOSKeyID); err != nil { | |||
| 218 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--recv-keys", | |||
| 219 | + | cachyOSKeyID, "--keyserver", keyserver); err != nil { | |||
| 220 | + | return fmt.Errorf("failed to receive CachyOS key: %w", err) | |||
| 221 | + | } | |||
| 210 | } | 222 | } | ||
| 211 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--lsign-key", | 223 | + | |
| 212 | - | "F3B607488DB35A47"); err != nil { | 224 | + | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil { |
| 213 | return fmt.Errorf("failed to sign CachyOS key: %w", err) | 225 | return fmt.Errorf("failed to sign CachyOS key: %w", err) | ||
| 214 | } | 226 | } | ||
| 215 | - | if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman", "-U", "--noconfirm", | 227 | + | |
| 216 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-keyring-20240331-1-any.pkg.tar.zst", | 228 | + | // Write static mirrorlist — no versioned package URLs that can go stale. |
| 217 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-mirrorlist-18-1-any.pkg.tar.zst", | 229 | + | mirrorlistPath := filepath.Join(mountPoint, "etc", "pacman.d", "cachyos-mirrorlist") |
| 218 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-v3-mirrorlist-18-1-any.pkg.tar.zst", | 230 | + | if err := h.fs.WriteFile(mirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil { |
| 219 | - | "https://mirror.cachyos.org/repo/x86_64/cachyos/cachyos-v4-mirrorlist-6-1-any.pkg.tar.zst"); err != nil { | 231 | + | return fmt.Errorf("failed to write CachyOS mirrorlist: %w", err) |
| 220 | - | return fmt.Errorf("failed to install CachyOS packages: %w", err) | |||
| 221 | } | 232 | } | ||
| 233 | + | ||||
| 222 | pacmanConfPath := filepath.Join(mountPoint, "etc", "pacman.conf") | 234 | pacmanConfPath := filepath.Join(mountPoint, "etc", "pacman.conf") | ||
| 223 | confBytes, err := h.fs.ReadFile(pacmanConfPath) | 235 | confBytes, err := h.fs.ReadFile(pacmanConfPath) | ||
| 224 | if err != nil { | 236 | if err != nil { | ||
| @@ -232,6 +244,30 @@ func ensureCachyOSRepo(conf string) string { | |||||
| 232 | if strings.Contains(conf, "[cachyos]") { | 244 | if strings.Contains(conf, "[cachyos]") { | ||
| 233 | return conf | 245 | return conf | ||
| 234 | } | 246 | } | ||
| 247 | + | ||||
| 235 | conf = strings.TrimRight(conf, "\n") | 248 | conf = strings.TrimRight(conf, "\n") | ||
| 249 | + | if conf == "" { | |||
| 250 | + | return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | block := []string{ | |||
| 254 | + | "# CachyOS repositories", | |||
| 255 | + | "[cachyos]", | |||
| 256 | + | "Include = /etc/pacman.d/cachyos-mirrorlist", | |||
| 257 | + | "", | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | lines := strings.Split(conf, "\n") | |||
| 261 | + | for i, line := range lines { | |||
| 262 | + | if strings.TrimSpace(line) == "[core]" { | |||
| 263 | + | out := make([]string, 0, len(lines)+len(block)) | |||
| 264 | + | out = append(out, lines[:i]...) | |||
| 265 | + | out = append(out, block...) | |||
| 266 | + | out = append(out, lines[i:]...) | |||
| 267 | + | return strings.Join(out, "\n") + "\n" | |||
| 268 | + | } | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // Fallback: append if [core] section is not found. | |||
| 236 | return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | 272 | return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n" | ||
| 237 | } | 273 | } | ||
M internal/config/config.go +0 -1
| @@ -194,7 +194,6 @@ var PostBootScripts = []string{ | |||
| 194 | 194 | "blesh.sh", | |
| 195 | 195 | "cli-tools.sh", | |
| 196 | 196 | "dms-opt-in.sh", | |
| 197 | - | "logo.txt", | |
| 198 | 197 | } | |
| 199 | 198 | ||
| 200 | 199 | // Post-boot service template URL path | |
| @@ -194,7 +194,6 @@ var PostBootScripts = []string{ | |||||
| 194 | "blesh.sh", | 194 | "blesh.sh", | ||
| 195 | "cli-tools.sh", | 195 | "cli-tools.sh", | ||
| 196 | "dms-opt-in.sh", | 196 | "dms-opt-in.sh", | ||
| 197 | - | "logo.txt", | |||
| 198 | } | 197 | } | ||
| 199 | 198 | ||||
| 200 | // Post-boot service template URL path | 199 | // Post-boot service template URL path | ||
M internal/interfaces/tui/app.go +8 -5
| @@ -320,6 +320,8 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 320 | 320 | switch msg.String() { | |
| 321 | 321 | case "ctrl+c": | |
| 322 | 322 | return a, tea.Quit | |
| 323 | + | case "esc", "backspace": | |
| 324 | + | return a.startAMDPStateSelection() | |
| 323 | 325 | case "up", "shift+tab": | |
| 324 | 326 | a.gpuModel.MoveUp() | |
| 325 | 327 | return a, nil | |
| @@ -373,11 +375,9 @@ func (a *App) handleReposInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 373 | 375 | case "down": | |
| 374 | 376 | a.reposModel.MoveDown() | |
| 375 | 377 | return a, nil | |
| 376 | - | case "tab": | |
| 377 | - | a.reposModel.NextSection() | |
| 378 | - | return a, nil | |
| 379 | - | case "shift+tab": | |
| 380 | - | a.reposModel.PrevSection() | |
| 378 | + | case "left", "right", " ": | |
| 379 | + | // Select the item under the cursor (radio within its section) | |
| 380 | + | a.reposModel.Select() | |
| 381 | 381 | return a, nil | |
| 382 | 382 | case "enter": | |
| 383 | 383 | a.formData.AURHelper = a.reposModel.SelectedAURHelper() | |
| @@ -567,6 +567,9 @@ func (a *App) handleDiskInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 567 | 567 | switch msg.String() { | |
| 568 | 568 | case "ctrl+c": | |
| 569 | 569 | return a, tea.Quit | |
| 570 | + | case "esc", "backspace": | |
| 571 | + | a.currentScreen = ScreenForm | |
| 572 | + | return a, nil | |
| 570 | 573 | case "up", "shift+tab": | |
| 571 | 574 | a.diskModel.MoveUp() | |
| 572 | 575 | return a, nil | |
| @@ -320,6 +320,8 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||||
| 320 | switch msg.String() { | 320 | switch msg.String() { | ||
| 321 | case "ctrl+c": | 321 | case "ctrl+c": | ||
| 322 | return a, tea.Quit | 322 | return a, tea.Quit | ||
| 323 | + | case "esc", "backspace": | |||
| 324 | + | return a.startAMDPStateSelection() | |||
| 323 | case "up", "shift+tab": | 325 | case "up", "shift+tab": | ||
| 324 | a.gpuModel.MoveUp() | 326 | a.gpuModel.MoveUp() | ||
| 325 | return a, nil | 327 | return a, nil | ||
| @@ -373,11 +375,9 @@ func (a *App) handleReposInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||||
| 373 | case "down": | 375 | case "down": | ||
| 374 | a.reposModel.MoveDown() | 376 | a.reposModel.MoveDown() | ||
| 375 | return a, nil | 377 | return a, nil | ||
| 376 | - | case "tab": | 378 | + | case "left", "right", " ": |
| 377 | - | a.reposModel.NextSection() | 379 | + | // Select the item under the cursor (radio within its section) |
| 378 | - | return a, nil | 380 | + | a.reposModel.Select() |
| 379 | - | case "shift+tab": | |||
| 380 | - | a.reposModel.PrevSection() | |||
| 381 | return a, nil | 381 | return a, nil | ||
| 382 | case "enter": | 382 | case "enter": | ||
| 383 | a.formData.AURHelper = a.reposModel.SelectedAURHelper() | 383 | a.formData.AURHelper = a.reposModel.SelectedAURHelper() | ||
| @@ -567,6 +567,9 @@ func (a *App) handleDiskInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||||
| 567 | switch msg.String() { | 567 | switch msg.String() { | ||
| 568 | case "ctrl+c": | 568 | case "ctrl+c": | ||
| 569 | return a, tea.Quit | 569 | return a, tea.Quit | ||
| 570 | + | case "esc", "backspace": | |||
| 571 | + | a.currentScreen = ScreenForm | |||
| 572 | + | return a, nil | |||
| 570 | case "up", "shift+tab": | 573 | case "up", "shift+tab": | ||
| 571 | a.diskModel.MoveUp() | 574 | a.diskModel.MoveUp() | ||
| 572 | return a, nil | 575 | return a, nil | ||
M internal/interfaces/tui/models/repos.go +53 -42
| @@ -17,13 +17,14 @@ type ChaoticOption struct { | |||
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | 19 | // ReposModelImpl holds AUR helper and Chaotic-AUR selection state. | |
| 20 | + | // Navigation: ↑/↓ moves a single cursor across all items (crossing sections). | |
| 21 | + | // ←/→ selects the item under the cursor (radio within its section). | |
| 20 | 22 | type ReposModelImpl struct { | |
| 21 | 23 | aurOptions []AURHelperOption | |
| 22 | - | aurSelected int | |
| 24 | + | aurSelected int // which AUR item is selected (confirmed) | |
| 23 | 25 | chaoticOptions []ChaoticOption | |
| 24 | - | chaoticSelected int | |
| 25 | - | // focusSection: 0 = AUR helper, 1 = Chaotic-AUR | |
| 26 | - | focusSection int | |
| 26 | + | chaoticSelected int // which Chaotic item is selected (confirmed) | |
| 27 | + | cursor int // flat index across all items (0..totalItems-1) | |
| 27 | 28 | } | |
| 28 | 29 | ||
| 29 | 30 | // NewReposModel creates a new repos selection model. | |
| @@ -57,24 +58,38 @@ func NewReposModel() *ReposModelImpl { | |||
| 57 | 58 | aurSelected: aurSelected, | |
| 58 | 59 | chaoticOptions: chaoticOptions, | |
| 59 | 60 | chaoticSelected: chaoticSelected, | |
| 60 | - | focusSection: 0, | |
| 61 | + | cursor: 0, | |
| 61 | 62 | } | |
| 62 | 63 | } | |
| 63 | 64 | ||
| 65 | + | // totalItems returns the total number of items across all sections. | |
| 66 | + | func (rm *ReposModelImpl) totalItems() int { | |
| 67 | + | return len(rm.aurOptions) + len(rm.chaoticOptions) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // CursorIndex returns the current flat cursor position. | |
| 71 | + | func (rm *ReposModelImpl) CursorIndex() int { return rm.cursor } | |
| 72 | + | ||
| 64 | 73 | // AUROptions returns the selectable AUR helper options. | |
| 65 | 74 | func (rm *ReposModelImpl) AUROptions() []AURHelperOption { return rm.aurOptions } | |
| 66 | 75 | ||
| 67 | - | // AURSelectedIndex returns the current AUR selection index. | |
| 76 | + | // AURSelectedIndex returns the confirmed AUR selection index. | |
| 68 | 77 | func (rm *ReposModelImpl) AURSelectedIndex() int { return rm.aurSelected } | |
| 69 | 78 | ||
| 70 | 79 | // ChaoticOptions returns the selectable Chaotic-AUR options. | |
| 71 | 80 | func (rm *ReposModelImpl) ChaoticOptions() []ChaoticOption { return rm.chaoticOptions } | |
| 72 | 81 | ||
| 73 | - | // ChaoticSelectedIndex returns the current Chaotic-AUR selection index. | |
| 82 | + | // ChaoticSelectedIndex returns the confirmed Chaotic-AUR selection index. | |
| 74 | 83 | func (rm *ReposModelImpl) ChaoticSelectedIndex() int { return rm.chaoticSelected } | |
| 75 | 84 | ||
| 76 | - | // FocusSection returns the currently focused section (0=AUR, 1=Chaotic). | |
| 77 | - | func (rm *ReposModelImpl) FocusSection() int { return rm.focusSection } | |
| 85 | + | // FocusSection returns which section the cursor is in (0=AUR, 1=Chaotic). | |
| 86 | + | // Kept for view compatibility. | |
| 87 | + | func (rm *ReposModelImpl) FocusSection() int { | |
| 88 | + | if rm.cursor < len(rm.aurOptions) { | |
| 89 | + | return 0 | |
| 90 | + | } | |
| 91 | + | return 1 | |
| 92 | + | } | |
| 78 | 93 | ||
| 79 | 94 | // SelectedAURHelper returns the selected AUR helper value string. | |
| 80 | 95 | func (rm *ReposModelImpl) SelectedAURHelper() string { | |
| @@ -98,50 +113,46 @@ func (rm *ReposModelImpl) SelectedChaoticEnabled() bool { | |||
| 98 | 113 | return rm.chaoticOptions[rm.chaoticSelected].Enabled | |
| 99 | 114 | } | |
| 100 | 115 | ||
| 101 | - | // MoveUp moves selection up in the current section (wraps). | |
| 116 | + | // MoveUp moves the cursor up (crosses section boundaries, no wrap). | |
| 102 | 117 | func (rm *ReposModelImpl) MoveUp() { | |
| 103 | - | if rm.focusSection == 0 { | |
| 104 | - | if len(rm.aurOptions) == 0 { | |
| 105 | - | return | |
| 106 | - | } | |
| 107 | - | if rm.aurSelected == 0 { | |
| 108 | - | rm.aurSelected = len(rm.aurOptions) - 1 | |
| 109 | - | } else { | |
| 110 | - | rm.aurSelected-- | |
| 111 | - | } | |
| 112 | - | } else { | |
| 113 | - | if len(rm.chaoticOptions) == 0 { | |
| 114 | - | return | |
| 115 | - | } | |
| 116 | - | if rm.chaoticSelected == 0 { | |
| 117 | - | rm.chaoticSelected = len(rm.chaoticOptions) - 1 | |
| 118 | - | } else { | |
| 119 | - | rm.chaoticSelected-- | |
| 120 | - | } | |
| 118 | + | if rm.cursor > 0 { | |
| 119 | + | rm.cursor-- | |
| 121 | 120 | } | |
| 122 | 121 | } | |
| 123 | 122 | ||
| 124 | - | // MoveDown moves selection down in the current section (wraps). | |
| 123 | + | // MoveDown moves the cursor down (crosses section boundaries, no wrap). | |
| 125 | 124 | func (rm *ReposModelImpl) MoveDown() { | |
| 126 | - | if rm.focusSection == 0 { | |
| 127 | - | if len(rm.aurOptions) == 0 { | |
| 128 | - | return | |
| 129 | - | } | |
| 130 | - | rm.aurSelected = (rm.aurSelected + 1) % len(rm.aurOptions) | |
| 125 | + | if rm.cursor < rm.totalItems()-1 { | |
| 126 | + | rm.cursor++ | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // Select selects (radio) the item currently under the cursor within its section. | |
| 131 | + | func (rm *ReposModelImpl) Select() { | |
| 132 | + | if rm.cursor < len(rm.aurOptions) { | |
| 133 | + | rm.aurSelected = rm.cursor | |
| 131 | 134 | } else { | |
| 132 | - | if len(rm.chaoticOptions) == 0 { | |
| 133 | - | return | |
| 134 | - | } | |
| 135 | - | rm.chaoticSelected = (rm.chaoticSelected + 1) % len(rm.chaoticOptions) | |
| 135 | + | rm.chaoticSelected = rm.cursor - len(rm.aurOptions) | |
| 136 | 136 | } | |
| 137 | 137 | } | |
| 138 | 138 | ||
| 139 | - | // NextSection moves focus to the next section. | |
| 139 | + | // NextSection moves focus to the next section (kept for compatibility). | |
| 140 | 140 | func (rm *ReposModelImpl) NextSection() { | |
| 141 | - | rm.focusSection = (rm.focusSection + 1) % 2 | |
| 141 | + | rm.focusToSection(rm.FocusSection() + 1) | |
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | - | // PrevSection moves focus to the previous section. | |
| 144 | + | // PrevSection moves focus to the previous section (kept for compatibility). | |
| 145 | 145 | func (rm *ReposModelImpl) PrevSection() { | |
| 146 | - | rm.focusSection = (rm.focusSection - 1 + 2) % 2 | |
| 146 | + | rm.focusToSection(rm.FocusSection() - 1) | |
| 147 | 147 | } | |
| 148 | + | ||
| 149 | + | func (rm *ReposModelImpl) focusToSection(s int) { | |
| 150 | + | if s <= 0 { | |
| 151 | + | rm.cursor = 0 | |
| 152 | + | } else { | |
| 153 | + | rm.cursor = len(rm.aurOptions) | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // SectionCount returns the total number of sections. | |
| 158 | + | func (rm *ReposModelImpl) SectionCount() int { return 2 } | |
| @@ -17,13 +17,14 @@ type ChaoticOption struct { | |||||
| 17 | } | 17 | } | ||
| 18 | 18 | ||||
| 19 | // ReposModelImpl holds AUR helper and Chaotic-AUR selection state. | 19 | // ReposModelImpl holds AUR helper and Chaotic-AUR selection state. | ||
| 20 | + | // Navigation: ↑/↓ moves a single cursor across all items (crossing sections). | |||
| 21 | + | // ←/→ selects the item under the cursor (radio within its section). | |||
| 20 | type ReposModelImpl struct { | 22 | type ReposModelImpl struct { | ||
| 21 | aurOptions []AURHelperOption | 23 | aurOptions []AURHelperOption | ||
| 22 | - | aurSelected int | 24 | + | aurSelected int // which AUR item is selected (confirmed) |
| 23 | chaoticOptions []ChaoticOption | 25 | chaoticOptions []ChaoticOption | ||
| 24 | - | chaoticSelected int | 26 | + | chaoticSelected int // which Chaotic item is selected (confirmed) |
| 25 | - | // focusSection: 0 = AUR helper, 1 = Chaotic-AUR | 27 | + | cursor int // flat index across all items (0..totalItems-1) |
| 26 | - | focusSection int | |||
| 27 | } | 28 | } | ||
| 28 | 29 | ||||
| 29 | // NewReposModel creates a new repos selection model. | 30 | // NewReposModel creates a new repos selection model. | ||
| @@ -57,24 +58,38 @@ func NewReposModel() *ReposModelImpl { | |||||
| 57 | aurSelected: aurSelected, | 58 | aurSelected: aurSelected, | ||
| 58 | chaoticOptions: chaoticOptions, | 59 | chaoticOptions: chaoticOptions, | ||
| 59 | chaoticSelected: chaoticSelected, | 60 | chaoticSelected: chaoticSelected, | ||
| 60 | - | focusSection: 0, | 61 | + | cursor: 0, |
| 61 | } | 62 | } | ||
| 62 | } | 63 | } | ||
| 63 | 64 | ||||
| 65 | + | // totalItems returns the total number of items across all sections. | |||
| 66 | + | func (rm *ReposModelImpl) totalItems() int { | |||
| 67 | + | return len(rm.aurOptions) + len(rm.chaoticOptions) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // CursorIndex returns the current flat cursor position. | |||
| 71 | + | func (rm *ReposModelImpl) CursorIndex() int { return rm.cursor } | |||
| 72 | + | ||||
| 64 | // AUROptions returns the selectable AUR helper options. | 73 | // AUROptions returns the selectable AUR helper options. | ||
| 65 | func (rm *ReposModelImpl) AUROptions() []AURHelperOption { return rm.aurOptions } | 74 | func (rm *ReposModelImpl) AUROptions() []AURHelperOption { return rm.aurOptions } | ||
| 66 | 75 | ||||
| 67 | - | // AURSelectedIndex returns the current AUR selection index. | 76 | + | // AURSelectedIndex returns the confirmed AUR selection index. |
| 68 | func (rm *ReposModelImpl) AURSelectedIndex() int { return rm.aurSelected } | 77 | func (rm *ReposModelImpl) AURSelectedIndex() int { return rm.aurSelected } | ||
| 69 | 78 | ||||
| 70 | // ChaoticOptions returns the selectable Chaotic-AUR options. | 79 | // ChaoticOptions returns the selectable Chaotic-AUR options. | ||
| 71 | func (rm *ReposModelImpl) ChaoticOptions() []ChaoticOption { return rm.chaoticOptions } | 80 | func (rm *ReposModelImpl) ChaoticOptions() []ChaoticOption { return rm.chaoticOptions } | ||
| 72 | 81 | ||||
| 73 | - | // ChaoticSelectedIndex returns the current Chaotic-AUR selection index. | 82 | + | // ChaoticSelectedIndex returns the confirmed Chaotic-AUR selection index. |
| 74 | func (rm *ReposModelImpl) ChaoticSelectedIndex() int { return rm.chaoticSelected } | 83 | func (rm *ReposModelImpl) ChaoticSelectedIndex() int { return rm.chaoticSelected } | ||
| 75 | 84 | ||||
| 76 | - | // FocusSection returns the currently focused section (0=AUR, 1=Chaotic). | 85 | + | // FocusSection returns which section the cursor is in (0=AUR, 1=Chaotic). |
| 77 | - | func (rm *ReposModelImpl) FocusSection() int { return rm.focusSection } | 86 | + | // Kept for view compatibility. |
| 87 | + | func (rm *ReposModelImpl) FocusSection() int { | |||
| 88 | + | if rm.cursor < len(rm.aurOptions) { | |||
| 89 | + | return 0 | |||
| 90 | + | } | |||
| 91 | + | return 1 | |||
| 92 | + | } | |||
| 78 | 93 | ||||
| 79 | // SelectedAURHelper returns the selected AUR helper value string. | 94 | // SelectedAURHelper returns the selected AUR helper value string. | ||
| 80 | func (rm *ReposModelImpl) SelectedAURHelper() string { | 95 | func (rm *ReposModelImpl) SelectedAURHelper() string { | ||
| @@ -98,50 +113,46 @@ func (rm *ReposModelImpl) SelectedChaoticEnabled() bool { | |||||
| 98 | return rm.chaoticOptions[rm.chaoticSelected].Enabled | 113 | return rm.chaoticOptions[rm.chaoticSelected].Enabled | ||
| 99 | } | 114 | } | ||
| 100 | 115 | ||||
| 101 | - | // MoveUp moves selection up in the current section (wraps). | 116 | + | // MoveUp moves the cursor up (crosses section boundaries, no wrap). |
| 102 | func (rm *ReposModelImpl) MoveUp() { | 117 | func (rm *ReposModelImpl) MoveUp() { | ||
| 103 | - | if rm.focusSection == 0 { | 118 | + | if rm.cursor > 0 { |
| 104 | - | if len(rm.aurOptions) == 0 { | 119 | + | rm.cursor-- |
| 105 | - | return | |||
| 106 | - | } | |||
| 107 | - | if rm.aurSelected == 0 { | |||
| 108 | - | rm.aurSelected = len(rm.aurOptions) - 1 | |||
| 109 | - | } else { | |||
| 110 | - | rm.aurSelected-- | |||
| 111 | - | } | |||
| 112 | - | } else { | |||
| 113 | - | if len(rm.chaoticOptions) == 0 { | |||
| 114 | - | return | |||
| 115 | - | } | |||
| 116 | - | if rm.chaoticSelected == 0 { | |||
| 117 | - | rm.chaoticSelected = len(rm.chaoticOptions) - 1 | |||
| 118 | - | } else { | |||
| 119 | - | rm.chaoticSelected-- | |||
| 120 | - | } | |||
| 121 | } | 120 | } | ||
| 122 | } | 121 | } | ||
| 123 | 122 | ||||
| 124 | - | // MoveDown moves selection down in the current section (wraps). | 123 | + | // MoveDown moves the cursor down (crosses section boundaries, no wrap). |
| 125 | func (rm *ReposModelImpl) MoveDown() { | 124 | func (rm *ReposModelImpl) MoveDown() { | ||
| 126 | - | if rm.focusSection == 0 { | 125 | + | if rm.cursor < rm.totalItems()-1 { |
| 127 | - | if len(rm.aurOptions) == 0 { | 126 | + | rm.cursor++ |
| 128 | - | return | 127 | + | } |
| 129 | - | } | 128 | + | } |
| 130 | - | rm.aurSelected = (rm.aurSelected + 1) % len(rm.aurOptions) | 129 | + | |
| 130 | + | // Select selects (radio) the item currently under the cursor within its section. | |||
| 131 | + | func (rm *ReposModelImpl) Select() { | |||
| 132 | + | if rm.cursor < len(rm.aurOptions) { | |||
| 133 | + | rm.aurSelected = rm.cursor | |||
| 131 | } else { | 134 | } else { | ||
| 132 | - | if len(rm.chaoticOptions) == 0 { | 135 | + | rm.chaoticSelected = rm.cursor - len(rm.aurOptions) |
| 133 | - | return | |||
| 134 | - | } | |||
| 135 | - | rm.chaoticSelected = (rm.chaoticSelected + 1) % len(rm.chaoticOptions) | |||
| 136 | } | 136 | } | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
| 139 | - | // NextSection moves focus to the next section. | 139 | + | // NextSection moves focus to the next section (kept for compatibility). |
| 140 | func (rm *ReposModelImpl) NextSection() { | 140 | func (rm *ReposModelImpl) NextSection() { | ||
| 141 | - | rm.focusSection = (rm.focusSection + 1) % 2 | 141 | + | rm.focusToSection(rm.FocusSection() + 1) |
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | - | // PrevSection moves focus to the previous section. | 144 | + | // PrevSection moves focus to the previous section (kept for compatibility). |
| 145 | func (rm *ReposModelImpl) PrevSection() { | 145 | func (rm *ReposModelImpl) PrevSection() { | ||
| 146 | - | rm.focusSection = (rm.focusSection - 1 + 2) % 2 | 146 | + | rm.focusToSection(rm.FocusSection() - 1) |
| 147 | } | 147 | } | ||
| 148 | + | ||||
| 149 | + | func (rm *ReposModelImpl) focusToSection(s int) { | |||
| 150 | + | if s <= 0 { | |||
| 151 | + | rm.cursor = 0 | |||
| 152 | + | } else { | |||
| 153 | + | rm.cursor = len(rm.aurOptions) | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // SectionCount returns the total number of sections. | |||
| 158 | + | func (rm *ReposModelImpl) SectionCount() int { return 2 } | |||
M internal/interfaces/tui/views/amd_pstate.go +1 -1
| @@ -70,7 +70,7 @@ func RenderAMDPStateSelection(am *models.AMDPStateModelImpl) string { | |||
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | 72 | b.WriteString("\n") | |
| 73 | - | b.WriteString(info.Render("Up/Down Move | Enter Confirm | Esc Back | Ctrl+C Quit")) | |
| 73 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) | |
| 74 | 74 | ||
| 75 | 75 | return b.String() | |
| 76 | 76 | } | |
| @@ -70,7 +70,7 @@ func RenderAMDPStateSelection(am *models.AMDPStateModelImpl) string { | |||||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | b.WriteString("\n") | 72 | b.WriteString("\n") | ||
| 73 | - | b.WriteString(info.Render("Up/Down Move | Enter Confirm | Esc Back | Ctrl+C Quit")) | 73 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) |
| 74 | 74 | ||||
| 75 | return b.String() | 75 | return b.String() | ||
| 76 | } | 76 | } | ||
M internal/interfaces/tui/views/dank_linux.go +1 -1
| @@ -44,7 +44,7 @@ func RenderDankLinuxSelection(dm *models.DankLinuxModelImpl) string { | |||
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | 46 | b.WriteString("\n") | |
| 47 | - | b.WriteString(info.Render("↑/↓ to select • enter to confirm")) | |
| 47 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) | |
| 48 | 48 | ||
| 49 | 49 | return b.String() | |
| 50 | 50 | } | |
| @@ -44,7 +44,7 @@ func RenderDankLinuxSelection(dm *models.DankLinuxModelImpl) string { | |||||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | b.WriteString("\n") | 46 | b.WriteString("\n") | ||
| 47 | - | b.WriteString(info.Render("↑/↓ to select • enter to confirm")) | 47 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) |
| 48 | 48 | ||||
| 49 | return b.String() | 49 | return b.String() | ||
| 50 | } | 50 | } | ||
M internal/interfaces/tui/views/disk.go +1 -1
| @@ -57,7 +57,7 @@ func RenderDiskSelection(dm *models.DiskModelImpl) string { | |||
| 57 | 57 | b.WriteString("\n") | |
| 58 | 58 | b.WriteString(warningStyle.Render("⚠ WARNING: Selected disk will be completely erased!")) | |
| 59 | 59 | b.WriteString("\n\n") | |
| 60 | - | b.WriteString(dimStyle.Render("↑↓ Navigate • Enter Select • Ctrl+C Quit")) | |
| 60 | + | b.WriteString(dimStyle.Render("↑/↓ navigate • enter select • esc back • ctrl+c quit")) | |
| 61 | 61 | ||
| 62 | 62 | return b.String() | |
| 63 | 63 | } | |
| @@ -57,7 +57,7 @@ func RenderDiskSelection(dm *models.DiskModelImpl) string { | |||||
| 57 | b.WriteString("\n") | 57 | b.WriteString("\n") | ||
| 58 | b.WriteString(warningStyle.Render("⚠ WARNING: Selected disk will be completely erased!")) | 58 | b.WriteString(warningStyle.Render("⚠ WARNING: Selected disk will be completely erased!")) | ||
| 59 | b.WriteString("\n\n") | 59 | b.WriteString("\n\n") | ||
| 60 | - | b.WriteString(dimStyle.Render("↑↓ Navigate • Enter Select • Ctrl+C Quit")) | 60 | + | b.WriteString(dimStyle.Render("↑/↓ navigate • enter select • esc back • ctrl+c quit")) |
| 61 | 61 | ||||
| 62 | return b.String() | 62 | return b.String() | ||
| 63 | } | 63 | } | ||
M internal/interfaces/tui/views/encryption.go +1 -1
| @@ -42,7 +42,7 @@ func RenderEncryptionSelection(em *models.EncryptionModelImpl) string { | |||
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | 44 | b.WriteString("\n") | |
| 45 | - | b.WriteString(info.Render("↑/↓ to select • enter to confirm • esc to go back")) | |
| 45 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) | |
| 46 | 46 | ||
| 47 | 47 | return b.String() | |
| 48 | 48 | } | |
| @@ -42,7 +42,7 @@ func RenderEncryptionSelection(em *models.EncryptionModelImpl) string { | |||||
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| 44 | b.WriteString("\n") | 44 | b.WriteString("\n") | ||
| 45 | - | b.WriteString(info.Render("↑/↓ to select • enter to confirm • esc to go back")) | 45 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) |
| 46 | 46 | ||||
| 47 | return b.String() | 47 | return b.String() | ||
| 48 | } | 48 | } | ||
M internal/interfaces/tui/views/gpu.go +1 -1
| @@ -43,7 +43,7 @@ func RenderGPUSelection(gm *models.GPUModelImpl) string { | |||
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | b.WriteString("\n") | |
| 46 | - | b.WriteString(info.Render("↑↓ Move • Enter Confirm • Ctrl+C Quit")) | |
| 46 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) | |
| 47 | 47 | ||
| 48 | 48 | return b.String() | |
| 49 | 49 | } | |
| @@ -43,7 +43,7 @@ func RenderGPUSelection(gm *models.GPUModelImpl) string { | |||||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | b.WriteString("\n") | 45 | b.WriteString("\n") | ||
| 46 | - | b.WriteString(info.Render("↑↓ Move • Enter Confirm • Ctrl+C Quit")) | 46 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) |
| 47 | 47 | ||||
| 48 | return b.String() | 48 | return b.String() | ||
| 49 | } | 49 | } | ||
M internal/interfaces/tui/views/kernel.go +1 -1
| @@ -44,7 +44,7 @@ func RenderKernelSelection(km *models.KernelModelImpl) string { | |||
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | 46 | b.WriteString("\n") | |
| 47 | - | b.WriteString(info.Render("Up/Down Move | Enter Confirm | Esc Back | Ctrl+C Quit")) | |
| 47 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) | |
| 48 | 48 | ||
| 49 | 49 | return b.String() | |
| 50 | 50 | } | |
| @@ -44,7 +44,7 @@ func RenderKernelSelection(km *models.KernelModelImpl) string { | |||||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | b.WriteString("\n") | 46 | b.WriteString("\n") | ||
| 47 | - | b.WriteString(info.Render("Up/Down Move | Enter Confirm | Esc Back | Ctrl+C Quit")) | 47 | + | b.WriteString(info.Render("↑/↓ navigate • enter confirm • esc back • ctrl+c quit")) |
| 48 | 48 | ||||
| 49 | return b.String() | 49 | return b.String() | ||
| 50 | } | 50 | } | ||
M internal/interfaces/tui/views/repos.go +43 -35
| @@ -13,71 +13,79 @@ func RenderReposSelection(rm *models.ReposModelImpl) string { | |||
| 13 | 13 | ||
| 14 | 14 | title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) | |
| 15 | 15 | info := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) | |
| 16 | - | active := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |
| 17 | 16 | section := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14")) | |
| 18 | - | activeSection := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10")) | |
| 19 | 17 | desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true) | |
| 20 | 18 | ||
| 19 | + | // cursor = highlighted row; selected = confirmed choice (shown with [x]) | |
| 20 | + | cursorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |
| 21 | + | selectedMark := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |
| 22 | + | normalMark := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) | |
| 23 | + | ||
| 21 | 24 | b.WriteString("\n") | |
| 22 | 25 | b.WriteString(title.Render("Repository Configuration")) | |
| 23 | 26 | b.WriteString("\n\n") | |
| 24 | 27 | ||
| 25 | 28 | // AUR Helper section | |
| 26 | - | aurHeaderStyle := section | |
| 27 | - | if rm.FocusSection() == 0 { | |
| 28 | - | aurHeaderStyle = activeSection | |
| 29 | - | } | |
| 30 | - | b.WriteString(aurHeaderStyle.Render("AUR Helper")) | |
| 29 | + | b.WriteString(section.Render("AUR Helper")) | |
| 31 | 30 | b.WriteString("\n") | |
| 32 | 31 | ||
| 33 | 32 | for i, option := range rm.AUROptions() { | |
| 34 | - | prefix := " " | |
| 35 | - | style := lipgloss.NewStyle() | |
| 36 | - | labelStyle := lipgloss.NewStyle() | |
| 37 | - | ||
| 38 | - | if rm.FocusSection() == 0 && i == rm.AURSelectedIndex() { | |
| 39 | - | prefix = "> " | |
| 40 | - | style = active | |
| 41 | - | labelStyle = active | |
| 33 | + | flatIdx := i | |
| 34 | + | isCursor := rm.CursorIndex() == flatIdx | |
| 35 | + | isSelected := rm.AURSelectedIndex() == i | |
| 36 | + | ||
| 37 | + | // Selection marker | |
| 38 | + | if isSelected { | |
| 39 | + | b.WriteString(selectedMark.Render(" [x] ")) | |
| 40 | + | } else { | |
| 41 | + | b.WriteString(normalMark.Render(" [ ] ")) | |
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | - | b.WriteString(style.Render(prefix)) | |
| 45 | - | b.WriteString(labelStyle.Render(option.Label)) | |
| 44 | + | // Label | |
| 45 | + | label := option.Label | |
| 46 | + | if isCursor { | |
| 47 | + | b.WriteString(cursorStyle.Render("> " + label)) | |
| 48 | + | } else { | |
| 49 | + | b.WriteString(label) | |
| 50 | + | } | |
| 46 | 51 | b.WriteString("\n") | |
| 47 | - | b.WriteString(desc.Render(" " + option.Description)) | |
| 52 | + | b.WriteString(desc.Render(" " + option.Description)) | |
| 48 | 53 | b.WriteString("\n") | |
| 49 | 54 | } | |
| 50 | 55 | ||
| 51 | 56 | b.WriteString("\n") | |
| 52 | 57 | ||
| 53 | 58 | // Chaotic-AUR section | |
| 54 | - | chaoticHeaderStyle := section | |
| 55 | - | if rm.FocusSection() == 1 { | |
| 56 | - | chaoticHeaderStyle = activeSection | |
| 57 | - | } | |
| 58 | - | b.WriteString(chaoticHeaderStyle.Render("Chaotic-AUR")) | |
| 59 | + | b.WriteString(section.Render("Chaotic-AUR")) | |
| 59 | 60 | b.WriteString("\n") | |
| 60 | 61 | ||
| 62 | + | aurLen := len(rm.AUROptions()) | |
| 61 | 63 | for i, option := range rm.ChaoticOptions() { | |
| 62 | - | prefix := " " | |
| 63 | - | style := lipgloss.NewStyle() | |
| 64 | - | labelStyle := lipgloss.NewStyle() | |
| 65 | - | ||
| 66 | - | if rm.FocusSection() == 1 && i == rm.ChaoticSelectedIndex() { | |
| 67 | - | prefix = "> " | |
| 68 | - | style = active | |
| 69 | - | labelStyle = active | |
| 64 | + | flatIdx := aurLen + i | |
| 65 | + | isCursor := rm.CursorIndex() == flatIdx | |
| 66 | + | isSelected := rm.ChaoticSelectedIndex() == i | |
| 67 | + | ||
| 68 | + | // Selection marker | |
| 69 | + | if isSelected { | |
| 70 | + | b.WriteString(selectedMark.Render(" [x] ")) | |
| 71 | + | } else { | |
| 72 | + | b.WriteString(normalMark.Render(" [ ] ")) | |
| 70 | 73 | } | |
| 71 | 74 | ||
| 72 | - | b.WriteString(style.Render(prefix)) | |
| 73 | - | b.WriteString(labelStyle.Render(option.Label)) | |
| 75 | + | // Label | |
| 76 | + | label := option.Label | |
| 77 | + | if isCursor { | |
| 78 | + | b.WriteString(cursorStyle.Render("> " + label)) | |
| 79 | + | } else { | |
| 80 | + | b.WriteString(label) | |
| 81 | + | } | |
| 74 | 82 | b.WriteString("\n") | |
| 75 | - | b.WriteString(desc.Render(" " + option.Description)) | |
| 83 | + | b.WriteString(desc.Render(" " + option.Description)) | |
| 76 | 84 | b.WriteString("\n") | |
| 77 | 85 | } | |
| 78 | 86 | ||
| 79 | 87 | b.WriteString("\n") | |
| 80 | - | b.WriteString(info.Render("↑/↓ to select • tab to switch section • enter to confirm • esc to go back")) | |
| 88 | + | b.WriteString(info.Render("↑/↓ move • ←/→ or space select • enter confirm • esc back")) | |
| 81 | 89 | ||
| 82 | 90 | return b.String() | |
| 83 | 91 | } | |
| @@ -13,71 +13,79 @@ func RenderReposSelection(rm *models.ReposModelImpl) string { | |||||
| 13 | 13 | ||||
| 14 | title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) | 14 | title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) | ||
| 15 | info := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) | 15 | info := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) | ||
| 16 | - | active := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |||
| 17 | section := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14")) | 16 | section := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14")) | ||
| 18 | - | activeSection := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10")) | |||
| 19 | desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true) | 17 | desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true) | ||
| 20 | 18 | ||||
| 19 | + | // cursor = highlighted row; selected = confirmed choice (shown with [x]) | |||
| 20 | + | cursorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |||
| 21 | + | selectedMark := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) | |||
| 22 | + | normalMark := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) | |||
| 23 | + | ||||
| 21 | b.WriteString("\n") | 24 | b.WriteString("\n") | ||
| 22 | b.WriteString(title.Render("Repository Configuration")) | 25 | b.WriteString(title.Render("Repository Configuration")) | ||
| 23 | b.WriteString("\n\n") | 26 | b.WriteString("\n\n") | ||
| 24 | 27 | ||||
| 25 | // AUR Helper section | 28 | // AUR Helper section | ||
| 26 | - | aurHeaderStyle := section | 29 | + | b.WriteString(section.Render("AUR Helper")) |
| 27 | - | if rm.FocusSection() == 0 { | |||
| 28 | - | aurHeaderStyle = activeSection | |||
| 29 | - | } | |||
| 30 | - | b.WriteString(aurHeaderStyle.Render("AUR Helper")) | |||
| 31 | b.WriteString("\n") | 30 | b.WriteString("\n") | ||
| 32 | 31 | ||||
| 33 | for i, option := range rm.AUROptions() { | 32 | for i, option := range rm.AUROptions() { | ||
| 34 | - | prefix := " " | 33 | + | flatIdx := i |
| 35 | - | style := lipgloss.NewStyle() | 34 | + | isCursor := rm.CursorIndex() == flatIdx |
| 36 | - | labelStyle := lipgloss.NewStyle() | 35 | + | isSelected := rm.AURSelectedIndex() == i |
| 37 | - | 36 | + | ||
| 38 | - | if rm.FocusSection() == 0 && i == rm.AURSelectedIndex() { | 37 | + | // Selection marker |
| 39 | - | prefix = "> " | 38 | + | if isSelected { |
| 40 | - | style = active | 39 | + | b.WriteString(selectedMark.Render(" [x] ")) |
| 41 | - | labelStyle = active | 40 | + | } else { |
| 41 | + | b.WriteString(normalMark.Render(" [ ] ")) | |||
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| 44 | - | b.WriteString(style.Render(prefix)) | 44 | + | // Label |
| 45 | - | b.WriteString(labelStyle.Render(option.Label)) | 45 | + | label := option.Label |
| 46 | + | if isCursor { | |||
| 47 | + | b.WriteString(cursorStyle.Render("> " + label)) | |||
| 48 | + | } else { | |||
| 49 | + | b.WriteString(label) | |||
| 50 | + | } | |||
| 46 | b.WriteString("\n") | 51 | b.WriteString("\n") | ||
| 47 | - | b.WriteString(desc.Render(" " + option.Description)) | 52 | + | b.WriteString(desc.Render(" " + option.Description)) |
| 48 | b.WriteString("\n") | 53 | b.WriteString("\n") | ||
| 49 | } | 54 | } | ||
| 50 | 55 | ||||
| 51 | b.WriteString("\n") | 56 | b.WriteString("\n") | ||
| 52 | 57 | ||||
| 53 | // Chaotic-AUR section | 58 | // Chaotic-AUR section | ||
| 54 | - | chaoticHeaderStyle := section | 59 | + | b.WriteString(section.Render("Chaotic-AUR")) |
| 55 | - | if rm.FocusSection() == 1 { | |||
| 56 | - | chaoticHeaderStyle = activeSection | |||
| 57 | - | } | |||
| 58 | - | b.WriteString(chaoticHeaderStyle.Render("Chaotic-AUR")) | |||
| 59 | b.WriteString("\n") | 60 | b.WriteString("\n") | ||
| 60 | 61 | ||||
| 62 | + | aurLen := len(rm.AUROptions()) | |||
| 61 | for i, option := range rm.ChaoticOptions() { | 63 | for i, option := range rm.ChaoticOptions() { | ||
| 62 | - | prefix := " " | 64 | + | flatIdx := aurLen + i |
| 63 | - | style := lipgloss.NewStyle() | 65 | + | isCursor := rm.CursorIndex() == flatIdx |
| 64 | - | labelStyle := lipgloss.NewStyle() | 66 | + | isSelected := rm.ChaoticSelectedIndex() == i |
| 65 | - | 67 | + | ||
| 66 | - | if rm.FocusSection() == 1 && i == rm.ChaoticSelectedIndex() { | 68 | + | // Selection marker |
| 67 | - | prefix = "> " | 69 | + | if isSelected { |
| 68 | - | style = active | 70 | + | b.WriteString(selectedMark.Render(" [x] ")) |
| 69 | - | labelStyle = active | 71 | + | } else { |
| 72 | + | b.WriteString(normalMark.Render(" [ ] ")) | |||
| 70 | } | 73 | } | ||
| 71 | 74 | ||||
| 72 | - | b.WriteString(style.Render(prefix)) | 75 | + | // Label |
| 73 | - | b.WriteString(labelStyle.Render(option.Label)) | 76 | + | label := option.Label |
| 77 | + | if isCursor { | |||
| 78 | + | b.WriteString(cursorStyle.Render("> " + label)) | |||
| 79 | + | } else { | |||
| 80 | + | b.WriteString(label) | |||
| 81 | + | } | |||
| 74 | b.WriteString("\n") | 82 | b.WriteString("\n") | ||
| 75 | - | b.WriteString(desc.Render(" " + option.Description)) | 83 | + | b.WriteString(desc.Render(" " + option.Description)) |
| 76 | b.WriteString("\n") | 84 | b.WriteString("\n") | ||
| 77 | } | 85 | } | ||
| 78 | 86 | ||||
| 79 | b.WriteString("\n") | 87 | b.WriteString("\n") | ||
| 80 | - | b.WriteString(info.Render("↑/↓ to select • tab to switch section • enter to confirm • esc to go back")) | 88 | + | b.WriteString(info.Render("↑/↓ move • ←/→ or space select • enter confirm • esc back")) |
| 81 | 89 | ||||
| 82 | return b.String() | 90 | return b.String() | ||
| 83 | } | 91 | } | ||
A internal/ui/assets/arch-logo.png +0 -0 binary file: no text diff
binary file: no text diff
M internal/ui/assets/assets.go +3 -0
| @@ -4,3 +4,6 @@ import _ "embed" | |||
| 4 | 4 | ||
| 5 | 5 | //go:embed logo.txt | |
| 6 | 6 | var LogoASCII string | |
| 7 | + | ||
| 8 | + | //go:embed arch-logo.png | |
| 9 | + | var LimineLogo []byte | |
| @@ -4,3 +4,6 @@ import _ "embed" | |||||
| 4 | 4 | ||||
| 5 | //go:embed logo.txt | 5 | //go:embed logo.txt | ||
| 6 | var LogoASCII string | 6 | var LogoASCII string | ||
| 7 | + | ||||
| 8 | + | //go:embed arch-logo.png | |||
| 9 | + | var LimineLogo []byte | |||
M test/vm/lib/qemu-profile.sh +3 -1
| @@ -100,6 +100,8 @@ build_qemu_args() { | |||
| 100 | 100 | QEMU_ARGS+=(-device hda-duplex,audiodev=audio0) | |
| 101 | 101 | ||
| 102 | 102 | # ── GPU / Display ───────────────────────────────────────────────────────────── | |
| 103 | - | QEMU_ARGS+=(-device "virtio-vga,xres=1920,yres=1080") | |
| 103 | + | # Use std VGA: works regardless of guest GPU drivers (virtio-vga needs | |
| 104 | + | # virtio-gpu module in the guest initramfs which may not be present). | |
| 105 | + | QEMU_ARGS+=(-device "VGA,vgamem_mb=64") | |
| 104 | 106 | QEMU_ARGS+=(-display "gtk,zoom-to-fit=on") | |
| 105 | 107 | } | |
| @@ -100,6 +100,8 @@ build_qemu_args() { | |||||
| 100 | QEMU_ARGS+=(-device hda-duplex,audiodev=audio0) | 100 | QEMU_ARGS+=(-device hda-duplex,audiodev=audio0) | ||
| 101 | 101 | ||||
| 102 | # ── GPU / Display ───────────────────────────────────────────────────────────── | 102 | # ── GPU / Display ───────────────────────────────────────────────────────────── | ||
| 103 | - | QEMU_ARGS+=(-device "virtio-vga,xres=1920,yres=1080") | 103 | + | # Use std VGA: works regardless of guest GPU drivers (virtio-vga needs |
| 104 | + | # virtio-gpu module in the guest initramfs which may not be present). | |||
| 105 | + | QEMU_ARGS+=(-device "VGA,vgamem_mb=64") | |||
| 104 | QEMU_ARGS+=(-display "gtk,zoom-to-fit=on") | 106 | QEMU_ARGS+=(-display "gtk,zoom-to-fit=on") | ||
| 105 | } | 107 | } | ||