theamify-cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * Absolute path to the bundled bash engine + app tree shipped inside the npm
8
+ * package. The whole engine (script + lib/ + config/) lives under vendor/.
9
+ */
10
+ export const VENDOR_DIR = fileURLToPath(new URL('../../vendor', import.meta.url));
11
+ export const ENGINE_SCRIPT = path.join(VENDOR_DIR, 'theamify');
12
+
13
+ /** Install/run-time locations. */
14
+ export const SYSTEM_DIR = '/usr/local/share/theamify';
15
+ export const USER_DIR = path.join(os.homedir(), '.local', 'share', 'theamify');
16
+ export const BIN_NAME = 'theamify';
17
+ export const USER_BIN_LINK = path.join(os.homedir(), '.local', 'bin', BIN_NAME);
18
+
19
+ /**
20
+ * Locate an installed theamify runtime.
21
+ * @returns {{dir: string}|null}
22
+ */
23
+ export function findInstalledRuntime() {
24
+ for (const dir of [SYSTEM_DIR, USER_DIR]) {
25
+ if (fs.existsSync(path.join(dir, BIN_NAME))) return { dir };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Provision a user-owned install of the engine under ~/.local/share/theamify so
32
+ * it is writable without root (downloads write into themes/ and .repo_cache/).
33
+ * Preserves an existing config/themes.conf (user registry edits). Returns the
34
+ * runtime dir.
35
+ */
36
+ export async function installUserRuntime() {
37
+ const { execaSync } = await import('execa');
38
+ fs.mkdirSync(path.join(USER_DIR, 'lib'), { recursive: true });
39
+ fs.mkdirSync(path.join(USER_DIR, 'config'), { recursive: true });
40
+ fs.mkdirSync(path.join(USER_DIR, 'themes'), { recursive: true });
41
+ fs.mkdirSync(path.join(USER_DIR, '.repo_cache'), { recursive: true });
42
+
43
+ fs.copyFileSync(path.join(VENDOR_DIR, BIN_NAME), path.join(USER_DIR, BIN_NAME));
44
+ fs.chmodSync(path.join(USER_DIR, BIN_NAME), 0o755);
45
+ for (const lib of ['colors', 'utils', 'grub', 'themes']) {
46
+ fs.copyFileSync(path.join(VENDOR_DIR, 'lib', `${lib}.sh`), path.join(USER_DIR, 'lib', `${lib}.sh`));
47
+ }
48
+ const confSrc = path.join(VENDOR_DIR, 'config', 'themes.conf');
49
+ const confDst = path.join(USER_DIR, 'config', 'themes.conf');
50
+ if (!fs.existsSync(confDst)) {
51
+ fs.copyFileSync(confSrc, confDst);
52
+ }
53
+
54
+ // The installed engine expects a wrapping bin symlink so `sudo theamify` works.
55
+ fs.mkdirSync(path.dirname(USER_BIN_LINK), { recursive: true });
56
+ fs.rmSync(USER_BIN_LINK, { force: true });
57
+ fs.symlinkSync(path.join(USER_DIR, BIN_NAME), USER_BIN_LINK);
58
+
59
+ // Ensure a PATH marker so the command is reachable from a fresh shell.
60
+ await ensureOnPath(path.dirname(USER_BIN_LINK));
61
+ return USER_DIR;
62
+ }
63
+
64
+ /** Add a PATH export marker to ~/.bashrc and ~/.zshrc (idempotent). */
65
+ export function ensureOnPath(binDir) {
66
+ const marker = '# theamify CLI PATH';
67
+ const rcs = ['.bashrc', '.zshrc'].map((f) => path.join(os.homedir(), f));
68
+ for (const rc of rcs) {
69
+ try {
70
+ const content = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
71
+ if (content.includes(marker)) continue;
72
+ fs.appendFileSync(rc, `\n${marker}\nexport PATH="${binDir}:$PATH"\n`);
73
+ } catch { /* skip */ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Resolve the path to the engine to run for a subcommand.
79
+ * Prefers an installed runtime; otherwise provisions the bundled one in the
80
+ * user share dir (so downloads are writable) and returns that.
81
+ * @returns {string} absolute path to the theamify engine
82
+ */
83
+ export async function resolveEngine() {
84
+ const found = findInstalledRuntime();
85
+ if (found) return path.join(found.dir, BIN_NAME);
86
+ const dir = await installUserRuntime();
87
+ return path.join(dir, BIN_NAME);
88
+ }
89
+
90
+ /**
91
+ * Run the engine (installed or bundled) with inherited stdio so the rich TUI
92
+ * and all subcommands behave exactly like the native tool.
93
+ * @param {string[]} args
94
+ * @returns {Promise<{exitCode: number|null}>}
95
+ */
96
+ export async function runEngine(args = []) {
97
+ const { execa } = await import('execa');
98
+ const engine = await resolveEngine();
99
+ return execa('bash', [engine, ...args], { stdio: 'inherit', reject: false });
100
+ }
@@ -0,0 +1,143 @@
1
+ import { execa, execaSync } from 'execa';
2
+ import pc from 'picocolors';
3
+ import * as p from '@clack/prompts';
4
+
5
+ /**
6
+ * chafa β€” the terminal image renderer behind theamify's thumbnail previews.
7
+ * This module manages chafa as a first-class wizard dependency:
8
+ * - install when the wizard runs and chafa is missing
9
+ * - update when the wizard updates itself
10
+ * - uninstall when the wizard is uninstalled (opt-in)
11
+ */
12
+
13
+ export const CHAFA_BIN = 'chafa';
14
+
15
+ /** @returns {Promise<boolean>} true if `chafa` is on PATH. */
16
+ export async function hasChafa() {
17
+ try {
18
+ const res = await execa('bash', ['-c', `command -v ${CHAFA_BIN}`], { reject: false });
19
+ return Boolean(res.stdout.trim());
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /** Detect which package manager is available. @returns {string|null} */
26
+ export function detectPackageManager() {
27
+ const order = ['apt-get', 'dnf', 'yum', 'pacman', 'zypper', 'apk'];
28
+ const pm = order.find((name) => {
29
+ try {
30
+ return Boolean(execaSync('bash', ['-c', `command -v ${name}`], { reject: false }).stdout.trim());
31
+ } catch {
32
+ return false;
33
+ }
34
+ });
35
+ return pm || null;
36
+ }
37
+
38
+ /**
39
+ * Run a privileged package-manager command, releasing the terminal first so the
40
+ * sudo password prompt is visible and Ctrl+C actually works. Mirrors the
41
+ * interactive-sudo pattern used by gitswitch and warp-wizard.
42
+ * @param {string[]} cmd sudo + args
43
+ */
44
+ async function runPrivileged(cmd) {
45
+ const res = await execa('sudo', cmd, { stdio: 'inherit', reject: false });
46
+ if (res.exitCode !== 0) throw new Error(`Command failed: sudo ${cmd.join(' ')} (exit ${res.exitCode})`);
47
+ return res;
48
+ }
49
+
50
+ /**
51
+ * Install chafa, prompting the user first. Safe no-op when a package manager is
52
+ * unavailable or chafa is already installed.
53
+ * @param {{silent?: boolean}} [opts]
54
+ * @returns {Promise<boolean>} true when chafa is available afterwards
55
+ */
56
+ export async function ensureChafa({ silent = false } = {}) {
57
+ if (await hasChafa()) return true;
58
+
59
+ const pm = detectPackageManager();
60
+ if (!pm) {
61
+ if (!silent) p.log.warn('No supported package manager found β€” install chafa manually to enable previews.');
62
+ return false;
63
+ }
64
+
65
+ const want = silent ? true : await p.confirm({
66
+ message: `Install ${pc.cyan('chafa')} to enable terminal theme previews?`,
67
+ initialValue: true,
68
+ });
69
+ if (p.isCancel(want) || !want) {
70
+ if (!silent) p.log.message(pc.dim('Skipped β€” theme thumbnails will be disabled. Install later with your package manager.'));
71
+ return false;
72
+ }
73
+
74
+ if (!silent) p.log.step(`Installing chafa via ${pm}…`);
75
+ const s = p.spinner();
76
+ s.start('Installing chafa…');
77
+ try {
78
+ // Release the spinner so the sudo prompt is visible & interruptible.
79
+ s.stop('');
80
+ const pkgArgs = pm === 'pacman' ? ['-S', '--noconfirm', 'chafa'] : [pm, 'install', '-y', 'chafa'];
81
+ await runPrivileged(pkgArgs);
82
+ if (!(await hasChafa())) throw new Error('chafa install appeared to complete, but chafa is not on PATH.');
83
+ if (!silent) p.log.success('chafa installed β€” terminal thumbnails enabled!');
84
+ return true;
85
+ } catch (e) {
86
+ if (!silent) p.log.warn(`chafa install failed: ${e.message}\n Try manually: sudo apt install chafa`);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /** Update chafa to its latest available version (best-effort). */
92
+ export async function updateChafa() {
93
+ if (!(await hasChafa())) return ensureChafa({ silent: true });
94
+
95
+ const pm = detectPackageManager();
96
+ if (!pm) return false;
97
+ p.log.step('Updating chafa…');
98
+ try {
99
+ const args = pm === 'pacman'
100
+ ? ['-Syu', '--noconfirm', 'chafa']
101
+ : pm === 'apk'
102
+ ? ['apk', 'upgrade', 'chafa']
103
+ : [pm, 'install', '-y', '--only-upgrade', 'chafa'];
104
+ await runPrivileged(args);
105
+ p.log.success('chafa updated.');
106
+ return true;
107
+ } catch (e) {
108
+ p.log.warn(`chafa update failed: ${e.message}`);
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Remove chafa after confirming. Only called from the uninstall wizard.
115
+ * @returns {Promise<boolean>} true when chafa was removed
116
+ */
117
+ export async function uninstallChafa() {
118
+ if (!(await hasChafa())) { p.log.message(pc.dim('chafa not installed β€” nothing to remove.')); return false; }
119
+
120
+ const pm = detectPackageManager();
121
+ if (!pm) { p.log.warn('No package manager found β€” remove chafa yourself.'); return false; }
122
+
123
+ const want = await p.confirm({
124
+ message: 'Also remove chafa (thumbnail renderer)? You can re-enable previews later by reinstalling it.',
125
+ initialValue: false,
126
+ });
127
+ if (p.isCancel(want) || !want) { p.log.message(pc.dim('Kept chafa.')); return false; }
128
+
129
+ p.log.step(`Removing chafa via ${pm}…`);
130
+ try {
131
+ const args = pm === 'pacman'
132
+ ? ['pacman', '-Rns', '--noconfirm', 'chafa']
133
+ : pm === 'apk'
134
+ ? ['apk', 'del', 'chafa']
135
+ : [pm, 'remove', '-y', 'chafa'];
136
+ await runPrivileged(args);
137
+ p.log.success('chafa removed.');
138
+ return true;
139
+ } catch (e) {
140
+ p.log.warn(`chafa removal failed: ${e.message}`);
141
+ return false;
142
+ }
143
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { USER_DIR } from '../core/engine.js';
5
+
6
+ export const VENDOR_CONF = fileURLToPath(new URL('../../vendor/config/themes.conf', import.meta.url));
7
+
8
+ /**
9
+ * Parse the `themes.conf` registry (NAME|URL|SUBDIR|DESC|SOURCE|TAGS).
10
+ * Skips blank lines and `#` comments.
11
+ * @param {string} [file] path to themes.conf (defaults to vendored copy)
12
+ * @returns {Array<{name:string,url:string,sub:string,desc:string,source:string,tags:string[]}>}
13
+ */
14
+ export function parseThemes(file = VENDOR_CONF) {
15
+ const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
16
+ const out = [];
17
+ for (const raw of content.split('\n')) {
18
+ const line = raw.trim();
19
+ if (!line || line.startsWith('#')) continue;
20
+ const parts = line.split('|');
21
+ const [name, url, sub, desc, source, tags] = parts;
22
+ if (!name || !url) continue;
23
+ out.push({
24
+ name: name.trim(),
25
+ url: url.trim(),
26
+ sub: (sub || '.').trim(),
27
+ desc: (desc || '').trim(),
28
+ source: (source || '').trim(),
29
+ tags: (tags || '').split(',').map((t) => t.trim()).filter(Boolean),
30
+ });
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Find a theme by name (case-insensitive).
37
+ * @param {string} name
38
+ * @param {string} [file]
39
+ * @returns {object|null}
40
+ */
41
+ export function findTheme(name, file = VENDOR_CONF) {
42
+ if (!name) return null;
43
+ return parseThemes(file).find((t) => t.name.toLowerCase() === name.toLowerCase()) || null;
44
+ }
45
+
46
+ /**
47
+ * Resolve the registry file actually in use by the installed runtime when one
48
+ * exists (so user's `theamify add`/`del` edits are honored), else the vendored
49
+ * default shipped in this package.
50
+ * @returns {string} path to themes.conf (runtime or vendored)
51
+ */
52
+ export function resolveConfPath() {
53
+ const runtimeConf = path.join(USER_DIR, 'config', 'themes.conf');
54
+ return fs.existsSync(runtimeConf) ? runtimeConf : VENDOR_CONF;
55
+ }
@@ -0,0 +1,76 @@
1
+ import { execa, execaSync } from 'execa';
2
+ import pc from 'picocolors';
3
+ import * as p from '@clack/prompts';
4
+
5
+ /**
6
+ * theamify companion tools β€” installed / updated by the wizard, but NEVER
7
+ * removed on uninstall (they're useful to the user on their own).
8
+ *
9
+ * Registry:
10
+ * - chafa terminal image renderer β†’ inline theme thumbnails
11
+ * - grub-customizer GUI to tweak the GRUB menu & themes
12
+ */
13
+
14
+ export const TOOLS = [
15
+ {
16
+ name: 'chafa',
17
+ bin: 'chafa',
18
+ purpose: 'terminal thumbnail previews',
19
+ install: { 'apt-get': 'chafa', 'dnf': 'chafa', 'yum': 'chafa', 'pacman': 'chafa', 'zypper': 'chafa', 'apk': 'chafa' },
20
+ },
21
+ {
22
+ name: 'grub-customizer',
23
+ bin: 'grub-customizer',
24
+ purpose: 'GUI for tweaking the GRUB menu & themes',
25
+ install: { 'apt-get': 'grub-customizer', 'dnf': 'grub-customizer', 'yum': 'grub-customizer', 'pacman': 'grub-customizer', 'zypper': 'grub-customizer', 'apk': null },
26
+ },
27
+ ];
28
+
29
+ /** @param {{bin:string}} tool */
30
+ export async function hasTool(tool) {
31
+ try {
32
+ const res = await execa('bash', ['-c', `command -v ${tool.bin}`], { reject: false });
33
+ return Boolean(res.stdout.trim());
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /** Detect which package manager is available. @returns {string|null} */
40
+ export function detectPackageManager() {
41
+ const order = ['apt-get', 'dnf', 'yum', 'pacman', 'zypper', 'apk'];
42
+ const pm = order.find((name) => {
43
+ try {
44
+ return Boolean(execaSync('bash', ['-c', `command -v ${name}`], { reject: false }).stdout.trim());
45
+ } catch {
46
+ return false;
47
+ }
48
+ });
49
+ return pm || null;
50
+ }
51
+
52
+ /**
53
+ * Run a privileged package-manager command, releasing the terminal first so the
54
+ * sudo password prompt is visible and Ctrl+C actually works. Mirrors the
55
+ * interactive-sudo pattern used by gitswitch and warp-wizard.
56
+ * @param {string[]} cmd sudo + args
57
+ */
58
+ async function runPrivileged(cmd) {
59
+ const res = await execa('sudo', cmd, { stdio: 'inherit', reject: false });
60
+ if (res.exitCode !== 0) throw new Error(`Command failed: sudo ${cmd.join(' ')} (exit ${res.exitCode})`);
61
+ return res;
62
+ }
63
+
64
+ /** @returns {string|null} package-manager-specific package name for a tool */
65
+ function packageNameFor(tool, pm) {
66
+ return tool.install ? tool.install[pm] : (tool.name || null);
67
+ }
68
+
69
+ /** @returns {boolean} whether the tool binary is on PATH (sync) */
70
+ function hasToolSync(tool) {
71
+ try {
72
+ return Boolean(execaSync('bash', ['-c', `command -v ${tool.bin}`], { reject: false }).stdout.trim());
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
@@ -0,0 +1,165 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { parseThemes, resolveConfPath } from '../lib/conf.js';
6
+ import { ensureChafa } from '../lib/chafa.js';
7
+ import { resolveEngine, USER_DIR } from '../core/engine.js';
8
+
9
+ const USER_THEMES = path.join(USER_DIR, 'themes');
10
+ const GRUB_CFG = '/etc/default/grub';
11
+
12
+ /** Extract the currently-active theme folder name from /etc/default/grub. */
13
+ function activeTheme() {
14
+ try {
15
+ const cfg = fs.readFileSync(GRUB_CFG, 'utf8');
16
+ const m = cfg.match(/^GRUB_THEME="?([^"\n]+)"?/m);
17
+ if (!m) return null;
18
+ return (path.basename(path.dirname(m[1])) || m[1]).trim() || null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function themeStatus(name) {
25
+ const active = activeTheme();
26
+ let status = 'remote';
27
+ if (fs.existsSync(path.join(USER_THEMES, name))) status = 'cached';
28
+ if (name === active) status = 'active';
29
+ return status;
30
+ }
31
+
32
+ const statusColor = (status) => ({
33
+ active: pc.green,
34
+ cached: pc.cyan,
35
+ remote: pc.dim,
36
+ }[status] || pc.dim);
37
+
38
+ /**
39
+ * Render a terminal "thumbnail" preview of a cached theme via chafa (or any
40
+ * terminal image renderer the user has). Silently nops when unsatisfiable.
41
+ * @returns {Promise<boolean>} true if a preview was rendered
42
+ */
43
+ export async function showThemePreview({ name, width = 40, height = 12 }) {
44
+ const dir = path.join(USER_THEMES, name);
45
+ if (!fs.existsSync(dir)) return false;
46
+
47
+ const image = fs.readdirSync(dir, { recursive: true })
48
+ .map((f) => path.join(dir, f))
49
+ .filter((f) => /\.(png|jpe?g|webp)$/i.test(f) && !f.includes('/.git/'))
50
+ .sort()[0];
51
+ if (!image) return false;
52
+
53
+ const { execa } = await import('execa');
54
+ for (const tool of ['chafa', 'timg', 'viu', 'jp2a']) {
55
+ const args = tool === 'chafa'
56
+ ? ['--size', `${width}x${height}`, image]
57
+ : [image];
58
+ try {
59
+ const { stdout } = await execa(tool, args, { reject: false });
60
+ if (stdout && stdout.trim()) {
61
+ p.log.message(`\n${stdout}\n`);
62
+ return true;
63
+ }
64
+ } catch { /* try next */ }
65
+ }
66
+ p.log.message(pc.dim('Install `chafa` (: sudo apt install chafa) for terminal theme previews.'));
67
+ return false;
68
+ }
69
+
70
+ /** Interactive GRUB-theme browser wizard (mirrors GitSwitch's look & feel). */
71
+ export async function runThemeBrowser() {
72
+ // Step 1: ensure the thumbnail renderer is present before anything else.
73
+ await ensureChafa();
74
+
75
+ const themes = parseThemes(resolveConfPath());
76
+ const active = activeTheme();
77
+
78
+ const selected = await p.select({
79
+ message: pc.bold('Pick a GRUB theme'),
80
+ options: themes.map((t) => {
81
+ const status = themeStatus(t.name);
82
+ const color = statusColor(status);
83
+ return {
84
+ value: t,
85
+ label: `${t.name} ${color(`[${status.toUpperCase()}]`)}${t.name === active ? ' ⭐' : ''}`,
86
+ hint: t.tags.slice(0, 3).join(', '),
87
+ };
88
+ }),
89
+ });
90
+ if (p.isCancel(selected)) { p.cancel('Bye! πŸ‘‹'); process.exit(0); }
91
+
92
+ p.log.step(`${pc.bold(selected.name)} β€” ${selected.desc}`);
93
+ p.log.message(pc.dim(selected.url));
94
+
95
+ if (fs.existsSync(path.join(USER_THEMES, selected.name))) {
96
+ await showThemePreview({ name: selected.name });
97
+ }
98
+
99
+ const action = await p.select({
100
+ message: `What do you want to do with ${pc.cyan(selected.name)}?`,
101
+ options: [
102
+ { value: 'preview', label: 'Show terminal thumbnail preview', hint: 'requires chafa + cached theme' },
103
+ { value: 'get', label: 'Download / update it', hint: 'git clone into local cache' },
104
+ { value: 'use', label: 'Apply to GRUB', hint: 'needs sudo; rebuilds GRUB' },
105
+ { value: 'open', label: 'Open source page in browser' },
106
+ { value: 'back', label: 'Cancel' },
107
+ ],
108
+ });
109
+ if (p.isCancel(action) || action === 'back') { p.outro('Bye! πŸ‘‹'); process.exit(0); }
110
+
111
+ const engine = await resolveEngine();
112
+ const { execa } = await import('execa');
113
+
114
+ if (action === 'preview') {
115
+ await showThemePreview({ name: selected.name, width: 80, height: 20 });
116
+ p.outro(pc.green('Preview above.'));
117
+ } else if (action === 'get') {
118
+ const s = p.spinner();
119
+ s.start(`Downloading ${selected.name}…`);
120
+ try {
121
+ await execa('bash', [engine, 'get', selected.name], { stdio: 'inherit', reject: true });
122
+ s.stop(pc.green(`Downloaded ${selected.name}.`));
123
+ } catch (e) {
124
+ s.stop(pc.red(`Download failed: ${e.message}`));
125
+ }
126
+ } else if (action === 'use') {
127
+ await runUse(engine, selected.name);
128
+ } else if (action === 'open') {
129
+ await execa('bash', [engine, 'open', selected.name], { stdio: 'inherit', reject: false });
130
+ }
131
+ }
132
+
133
+ /** Apply a theme to GRUB, re-invoking with sudo, spinner released for the prompt. */
134
+ export async function runUse(engine, name) {
135
+ const cached = path.join(USER_THEMES, name);
136
+ if (!fs.existsSync(cached)) {
137
+ p.log.warn(`${name} isn't downloaded yet. Fetching first…`);
138
+ const s = p.spinner();
139
+ s.start(`Downloading ${name}…`);
140
+ try {
141
+ await execa('bash', [engine, 'get', name], { stdio: 'inherit', reject: true });
142
+ s.stop();
143
+ } catch (e) {
144
+ s.stop(pc.red(`Download failed: ${e.message}`));
145
+ return;
146
+ }
147
+ }
148
+
149
+ const confirmApply = await p.confirm({ message: `Apply ${pc.cyan(name)} to GRUB and rebuild? (sudo)`, initialValue: true });
150
+ if (p.isCancel(confirmApply) || !confirmApply) { p.outro('Skipped.'); return; }
151
+
152
+ const { execa } = await import('execa');
153
+ if (process.getuid() !== 0) {
154
+ // Release spinner, then prompt sudo on a clean terminal so Ctrl+C works.
155
+ console.log();
156
+ const res = await execa('sudo', ['bash', engine, 'use', name], { stdio: 'inherit', reject: false });
157
+ return res.exitCode === 0
158
+ ? p.outro(pc.green(`${name} is now your GRUB theme! πŸŽ‰`))
159
+ : p.cancel('GRUB apply failed.');
160
+ }
161
+ const res = await execa('bash', [engine, 'use', name], { stdio: 'inherit', reject: false });
162
+ res.exitCode === 0
163
+ ? p.outro(pc.green(`${name} is now your GRUB theme! πŸŽ‰`))
164
+ : p.cancel('GRUB apply failed.');
165
+ }
@@ -0,0 +1,36 @@
1
+ # +==============================================================================+
2
+ # | theamify Theme Registry |
3
+ # | Format : NAME|GITHUB_URL|SUBDIR|DESCRIPTION|SOURCE_URL|TAGS |
4
+ # | SUBDIR : "." = repo root | "path/to/dir" = subdir within repo |
5
+ # | "generate:<args>" = repo has no static theme folder, build it |
6
+ # | via its own generate.sh first (see CONTRIBUTING.md) |
7
+ # | Add : theamify add <url> |
8
+ # | Remove : theamify del <name> |
9
+ # +==============================================================================+
10
+
11
+ # -- HenriqueLopes42 Cyberpunk Series ------------------------------------------
12
+ CyberEXS|https://github.com/HenriqueLopes42/themeGrub.CyberEXS|.|Dark cyberpunk hooded-figure neon GRUB theme|https://www.gnome-look.org/p/1968990|cyberpunk,dark,neon,hooded,scifi
13
+ CyberSynchro|https://github.com/HenriqueLopes42/ThemeGrub.CyberSynchro|.|Teal & neon cyberpunk matrix GRUB theme|https://www.gnome-look.org/p/1972621|cyberpunk,teal,neon,matrix,dark
14
+
15
+ # -- Space & Sci-Fi -------------------------------------------------------------
16
+ Space-Isolation|https://github.com/callmenoodles/space-isolation|.|Alien: Isolation-inspired dark space GRUB theme|https://www.gnome-look.org/p/2296342|space,alien,scifi,dark,atmospheric,horror
17
+
18
+ # -- Anime / Kawaii ------------------------------------------------------------
19
+ # NOTE: upstream repo is "KawaiiGRUB" (double i) - a single-i URL 404s and
20
+ # makes git fall back to an interactive credential prompt over HTTPS, which
21
+ # hangs a non-interactive `get --all` run. Fixed here; see CONTRIBUTING.md.
22
+ Kawaii-GRUB|https://github.com/Gabbar-v7/KawaiiGRUB|.|Kawaii anime vibrant bold GRUB theme|https://www.gnome-look.org/p/2218890|anime,kawaii,cute,colorful,bold,vibrant
23
+ Kayoko-Onikata|https://github.com/MTFTau-5/Kayoko-Onikata-GRUB|.|Cat cafe anime cozy pastel GRUB theme|https://www.gnome-look.org/p/2350900|anime,cat,cute,cozy,pastel,kawaii
24
+
25
+ # -- Abstract / Tech -----------------------------------------------------------
26
+ # NOTE: yeyushengfan258's Matrix/Particle repos use the same "no static
27
+ # theme folder, build via generate.sh" pattern that the old Elegant entries
28
+ # used (and originally failed at). Verified working with the args below -
29
+ # generate.sh accepts -t [window|sidebar] and -s [1080p|2k|4k]; change either
30
+ # to taste. See CONTRIBUTING.md for how `generate:<args>` is driven.
31
+ Matrices|https://github.com/yeyushengfan258/Matrix-grub-theme|generate:-t window -s 1080p|Abstract matrix blue tech orb GRUB theme|https://www.gnome-look.org/p/2271298|matrix,abstract,blue,tech,modern,3d
32
+ Particle|https://github.com/yeyushengfan258/Particle-grub-theme|generate:-t window -s 1080p|Particle effect dynamic blue GRUB theme|https://www.gnome-look.org/p/2269763|particle,abstract,blue,dynamic,modern
33
+
34
+ # -- Chill / Minimal -----------------------------------------------------------
35
+ Zzz-GRUB|https://github.com/jsthope/Zzz-GRUB-Theme|.|Cute sleeping cat chill minimal GRUB theme|https://www.gnome-look.org/p/2354136|cat,chill,minimal,cute,anime,night,cozy
36
+