theamify-cli 1.1.0 → 2.1.0
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.
- package/bin/theamify.js +0 -0
- package/package.json +2 -2
- package/src/cli.js +15 -1
- package/src/commands/manage.js +43 -13
- package/src/core/engine.js +55 -30
- package/src/wizard/browse.js +136 -78
- package/vendor/theamify +1 -1
package/bin/theamify.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theamify-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "theamify — GRUB theme manager & interactive browser wizard. Browse, preview, download and apply GRUB boot themes from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"execa": "^8.0.1",
|
|
50
50
|
"picocolors": "^1.1.1"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const require = createRequire(import.meta.url);
|
|
|
10
10
|
const pkg = require('../package.json');
|
|
11
11
|
|
|
12
12
|
/** Commands forwarded straight to the bash engine. */
|
|
13
|
-
const ENGINE_COMMANDS = ['list', 'ls', 'info', 'show', 'get', 'fetch', 'download', 'remove', 'rm', 'uncache', 'add', 'del', 'delete', 'update', 'open', '
|
|
13
|
+
const ENGINE_COMMANDS = ['list', 'ls', 'info', 'show', 'get', 'fetch', 'download', 'remove', 'rm', 'uncache', 'add', 'del', 'delete', 'update', 'open', 'clean', 'purge-cache'];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Forward argv to the bash engine and mirror its exit status onto this process.
|
|
@@ -78,6 +78,20 @@ const main = defineCommand({
|
|
|
78
78
|
case 'wizard':
|
|
79
79
|
case 'browse':
|
|
80
80
|
return runThemeBrowser();
|
|
81
|
+
case 'install':
|
|
82
|
+
// Guided setup: companion tools → download all themes.
|
|
83
|
+
return runThemeBrowser();
|
|
84
|
+
case 'help':
|
|
85
|
+
case '-h':
|
|
86
|
+
case '--help':
|
|
87
|
+
printUsage();
|
|
88
|
+
return;
|
|
89
|
+
case 'version':
|
|
90
|
+
case '-v':
|
|
91
|
+
case '-V':
|
|
92
|
+
case '--version':
|
|
93
|
+
console.log(`theamify v${pkg.version}`);
|
|
94
|
+
return;
|
|
81
95
|
case 'status':
|
|
82
96
|
return runStatus();
|
|
83
97
|
case 'doctor':
|
package/src/commands/manage.js
CHANGED
|
@@ -2,11 +2,11 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { execa } from 'execa';
|
|
5
|
+
import { execa, execaSync } from 'execa';
|
|
6
6
|
import {
|
|
7
7
|
findInstalledRuntime,
|
|
8
8
|
USER_DIR,
|
|
9
|
-
|
|
9
|
+
removeShadowBin,
|
|
10
10
|
resolveEngine,
|
|
11
11
|
} from '../core/engine.js';
|
|
12
12
|
import { parseThemes, resolveConfPath } from '../lib/conf.js';
|
|
@@ -50,7 +50,7 @@ export async function runDoctor() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function which(bin) {
|
|
53
|
-
try { return
|
|
53
|
+
try { return execaSync('which', [bin], { reject: false }).stdout.trim(); } catch { return ''; }
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function grubDetectDir() {
|
|
@@ -80,7 +80,35 @@ export async function runStatus() {
|
|
|
80
80
|
if (res.exitCode !== 0) process.exit(res.exitCode);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Remove GRUB_THEME from /etc/default/grub and rebuild the boot menu so the
|
|
85
|
+
* system returns to the default theme. Requires sudo when not root.
|
|
86
|
+
* @returns {Promise<boolean>} true when GRUB was reset
|
|
87
|
+
*/
|
|
88
|
+
async function resetGrubTheme() {
|
|
89
|
+
const script = `
|
|
90
|
+
GRUB=/etc/default/grub
|
|
91
|
+
[ -f "$GRUB" ] || exit 0
|
|
92
|
+
sed -i '/^GRUB_THEME=/d' "$GRUB"
|
|
93
|
+
if command -v update-grub >/dev/null 2>&1; then
|
|
94
|
+
update-grub
|
|
95
|
+
elif command -v grub-mkconfig >/dev/null 2>&1; then
|
|
96
|
+
grub-mkconfig -o /boot/grub/grub.cfg
|
|
97
|
+
elif command -v grub2-mkconfig >/dev/null 2>&1; then
|
|
98
|
+
grub2-mkconfig -o /boot/grub2/grub.cfg
|
|
99
|
+
fi
|
|
100
|
+
`.trim();
|
|
101
|
+
if (process.getuid() === 0) {
|
|
102
|
+
const res = await execa('bash', ['-c', script], { stdio: 'inherit', reject: false });
|
|
103
|
+
return res.exitCode === 0;
|
|
104
|
+
}
|
|
105
|
+
// Release the terminal so the sudo password prompt is visible & interruptible.
|
|
106
|
+
console.log();
|
|
107
|
+
const res = await execa('sudo', ['bash', '-c', script], { stdio: 'inherit', reject: false });
|
|
108
|
+
return res.exitCode === 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `theamify uninstall` — removes theamify, ALL downloaded themes, and resets GRUB to default. */
|
|
84
112
|
export async function runUninstallWizard() {
|
|
85
113
|
p.intro(pc.bgRed(pc.black(' theamify Uninstaller ')));
|
|
86
114
|
const found = findInstalledRuntime();
|
|
@@ -89,7 +117,7 @@ export async function runUninstallWizard() {
|
|
|
89
117
|
let removed = false;
|
|
90
118
|
if (found) {
|
|
91
119
|
const confirm = await p.confirm({
|
|
92
|
-
message: `Remove theamify files at ${found.dir}
|
|
120
|
+
message: `Remove theamify files at ${found.dir}/? (this deletes ALL downloaded themes)`,
|
|
93
121
|
initialValue: true,
|
|
94
122
|
});
|
|
95
123
|
if (!p.isCancel(confirm) && confirm) {
|
|
@@ -102,26 +130,28 @@ export async function runUninstallWizard() {
|
|
|
102
130
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
131
|
}
|
|
104
132
|
removed = true;
|
|
105
|
-
|
|
106
|
-
if (USER_BIN_LINK.startsWith(USER_DIR)) {
|
|
107
|
-
fs.rmSync(USER_BIN_LINK, { force: true });
|
|
108
|
-
}
|
|
133
|
+
removeShadowBin();
|
|
109
134
|
} catch {
|
|
110
135
|
p.log.warn(`Could not remove ${found.dir}.`);
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
// Reset the boot menu back to the default (remove the applied GRUB theme).
|
|
141
|
+
const resetGrub = await p.confirm({
|
|
142
|
+
message: 'Remove your applied GRUB theme and reset to the default boot menu?',
|
|
117
143
|
initialValue: true,
|
|
118
144
|
});
|
|
119
|
-
if (p.isCancel(
|
|
145
|
+
if (p.isCancel(resetGrub)) { p.cancel('Aborted.'); process.exit(0); }
|
|
146
|
+
const grubReset = resetGrub ? await resetGrubTheme() : false;
|
|
120
147
|
|
|
121
148
|
// Companion tools (chafa, grub-customizer) are intentionally LEFT in place —
|
|
122
149
|
// the user may want them for later; uninstall only removes theamify itself.
|
|
150
|
+
const themeNote = grubReset
|
|
151
|
+
? 'Boot menu reset to the default theme.'
|
|
152
|
+
: (resetGrub ? 'GRUB reset could not be completed — remove GRUB_THEME= from /etc/default/grub and rebuild.' : 'Your applied GRUB theme was left in place.');
|
|
123
153
|
|
|
124
154
|
p.outro(pc.green(
|
|
125
|
-
`Uninstalled.${removed ? ' Runtime
|
|
155
|
+
`Uninstalled.${removed ? ' Runtime + all downloaded themes removed.' : ''} ${themeNote} Companion tools (chafa, grub-customizer) were kept for your use.`,
|
|
126
156
|
));
|
|
127
157
|
}
|
package/src/core/engine.js
CHANGED
|
@@ -14,6 +14,7 @@ export const ENGINE_SCRIPT = path.join(VENDOR_DIR, 'theamify');
|
|
|
14
14
|
export const SYSTEM_DIR = '/usr/local/share/theamify';
|
|
15
15
|
export const USER_DIR = path.join(os.homedir(), '.local', 'share', 'theamify');
|
|
16
16
|
export const BIN_NAME = 'theamify';
|
|
17
|
+
/** Legacy self-shadowing symlink that used to point at the bundled bash engine. */
|
|
17
18
|
export const USER_BIN_LINK = path.join(os.homedir(), '.local', 'bin', BIN_NAME);
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -27,53 +28,72 @@ export function findInstalledRuntime() {
|
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Remove the legacy `~/.local/bin/theamify` symlink if it exists. It was created
|
|
33
|
+
* by old installs to point at the bundled bash engine, which SHADOWS the npm CLI
|
|
34
|
+
* (because `~/.local/bin` precedes the npm global bin on PATH — so `theamify`,
|
|
35
|
+
* `theamify uninstall`, `theamify doctor`, etc. silently ran the old engine).
|
|
36
|
+
* The npm package already installs `theamify -> bin/theamify.js` in the npm
|
|
37
|
+
* global bin, which is on PATH, so the symlink is unnecessary and harmful.
|
|
38
|
+
* Only ever removes a symlink — never a real file the user owns.
|
|
39
|
+
*/
|
|
40
|
+
export function removeShadowBin() {
|
|
41
|
+
try {
|
|
42
|
+
const st = fs.lstatSync(USER_BIN_LINK);
|
|
43
|
+
if (st.isSymbolicLink()) fs.rmSync(USER_BIN_LINK, { force: true });
|
|
44
|
+
} catch { /* nothing to remove */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Copy the vendored engine script + shared libs into a runtime dir. */
|
|
48
|
+
function copyEngineTo(dir) {
|
|
49
|
+
fs.mkdirSync(path.join(dir, 'lib'), { recursive: true });
|
|
50
|
+
fs.copyFileSync(path.join(VENDOR_DIR, BIN_NAME), path.join(dir, BIN_NAME));
|
|
51
|
+
fs.chmodSync(path.join(dir, BIN_NAME), 0o755);
|
|
52
|
+
for (const lib of ['colors', 'utils', 'grub', 'themes']) {
|
|
53
|
+
fs.copyFileSync(path.join(VENDOR_DIR, 'lib', `${lib}.sh`), path.join(dir, 'lib', `${lib}.sh`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Refreshes an existing installed runtime's engine script + libs to match the
|
|
59
|
+
* bundled package (so the disk engine is never a stale older version). User
|
|
60
|
+
* registry edits in config/themes.conf are preserved.
|
|
61
|
+
*/
|
|
62
|
+
function syncEngineTo(dir) {
|
|
63
|
+
const installed = path.join(dir, BIN_NAME);
|
|
64
|
+
const vendored = path.join(VENDOR_DIR, BIN_NAME);
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(installed) && fs.readFileSync(installed, 'utf8') !== fs.readFileSync(vendored, 'utf8')) {
|
|
67
|
+
copyEngineTo(dir);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
copyEngineTo(dir);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
/**
|
|
31
75
|
* Provision a user-owned install of the engine under ~/.local/share/theamify so
|
|
32
76
|
* it is writable without root (downloads write into themes/ and .repo_cache/).
|
|
33
|
-
* Preserves an existing config/themes.conf (user registry edits).
|
|
77
|
+
* Preserves an existing config/themes.conf (user registry edits). Does NOT create
|
|
78
|
+
* a PATH shadow symlink — the npm global bin is already on PATH. Returns the
|
|
34
79
|
* runtime dir.
|
|
35
80
|
*/
|
|
36
81
|
export async function installUserRuntime() {
|
|
37
|
-
const { execaSync } = await import('execa');
|
|
38
|
-
fs.mkdirSync(path.join(USER_DIR, 'lib'), { recursive: true });
|
|
39
82
|
fs.mkdirSync(path.join(USER_DIR, 'config'), { recursive: true });
|
|
40
83
|
fs.mkdirSync(path.join(USER_DIR, 'themes'), { recursive: true });
|
|
41
84
|
fs.mkdirSync(path.join(USER_DIR, '.repo_cache'), { recursive: true });
|
|
42
85
|
|
|
43
|
-
|
|
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
|
-
}
|
|
86
|
+
copyEngineTo(USER_DIR);
|
|
48
87
|
const confSrc = path.join(VENDOR_DIR, 'config', 'themes.conf');
|
|
49
88
|
const confDst = path.join(USER_DIR, 'config', 'themes.conf');
|
|
50
89
|
if (!fs.existsSync(confDst)) {
|
|
51
90
|
fs.copyFileSync(confSrc, confDst);
|
|
52
91
|
}
|
|
53
92
|
|
|
54
|
-
|
|
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));
|
|
93
|
+
removeShadowBin();
|
|
61
94
|
return USER_DIR;
|
|
62
95
|
}
|
|
63
96
|
|
|
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
97
|
/**
|
|
78
98
|
* Resolve the path to the engine to run for a subcommand.
|
|
79
99
|
* Prefers an installed runtime; otherwise provisions the bundled one in the
|
|
@@ -82,8 +102,13 @@ export function ensureOnPath(binDir) {
|
|
|
82
102
|
*/
|
|
83
103
|
export async function resolveEngine() {
|
|
84
104
|
const found = findInstalledRuntime();
|
|
85
|
-
if (found)
|
|
105
|
+
if (found) {
|
|
106
|
+
syncEngineTo(found.dir);
|
|
107
|
+
removeShadowBin();
|
|
108
|
+
return path.join(found.dir, BIN_NAME);
|
|
109
|
+
}
|
|
86
110
|
const dir = await installUserRuntime();
|
|
111
|
+
removeShadowBin();
|
|
87
112
|
return path.join(dir, BIN_NAME);
|
|
88
113
|
}
|
|
89
114
|
|
|
@@ -97,4 +122,4 @@ export async function runEngine(args = []) {
|
|
|
97
122
|
const { execa } = await import('execa');
|
|
98
123
|
const engine = await resolveEngine();
|
|
99
124
|
return execa('bash', [engine, ...args], { stdio: 'inherit', reject: false });
|
|
100
|
-
}
|
|
125
|
+
}
|
package/src/wizard/browse.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { execa } from 'execa';
|
|
5
6
|
import { parseThemes, resolveConfPath } from '../lib/conf.js';
|
|
6
7
|
import { ensureManagedTools } from '../lib/tools.js';
|
|
7
8
|
import { resolveEngine, USER_DIR } from '../core/engine.js';
|
|
@@ -35,6 +36,11 @@ const statusColor = (status) => ({
|
|
|
35
36
|
remote: pc.dim,
|
|
36
37
|
}[status] || pc.dim);
|
|
37
38
|
|
|
39
|
+
/** True when a theme has been downloaded into the local user cache. */
|
|
40
|
+
function isCached(name) {
|
|
41
|
+
return fs.existsSync(path.join(USER_THEMES, name));
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
/**
|
|
39
45
|
* Render a terminal "thumbnail" preview of a cached theme via chafa (or any
|
|
40
46
|
* terminal image renderer the user has). Silently nops when unsatisfiable.
|
|
@@ -50,7 +56,6 @@ export async function showThemePreview({ name, width = 40, height = 12 }) {
|
|
|
50
56
|
.sort()[0];
|
|
51
57
|
if (!image) return false;
|
|
52
58
|
|
|
53
|
-
const { execa } = await import('execa');
|
|
54
59
|
for (const tool of ['chafa', 'timg', 'viu', 'jp2a']) {
|
|
55
60
|
const args = tool === 'chafa'
|
|
56
61
|
? ['--size', `${width}x${height}`, image]
|
|
@@ -63,104 +68,157 @@ export async function showThemePreview({ name, width = 40, height = 12 }) {
|
|
|
63
68
|
}
|
|
64
69
|
} catch { /* try next */ }
|
|
65
70
|
}
|
|
66
|
-
p.log.message(pc.dim('Install `chafa` (
|
|
71
|
+
p.log.message(pc.dim('Install `chafa` (sudo apt install chafa) for terminal theme previews.'));
|
|
67
72
|
return false;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Make sure a theme is present in the local cache, downloading on demand.
|
|
77
|
+
* @returns {Promise<boolean>} true when the theme is cached (ready to preview/apply)
|
|
78
|
+
*/
|
|
79
|
+
async function ensureCached(engine, name) {
|
|
80
|
+
if (isCached(name)) return true;
|
|
81
|
+
p.log.warn(`${name} isn't downloaded yet. Fetching first…`);
|
|
82
|
+
const s = p.spinner();
|
|
83
|
+
s.start(`Downloading ${name}…`);
|
|
84
|
+
try {
|
|
85
|
+
await execa('bash', [engine, 'get', name], { stdio: 'inherit', reject: true });
|
|
86
|
+
s.stop(pc.green(`Downloaded ${name}.`));
|
|
87
|
+
return true;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
s.stop(pc.red(`Download failed: ${e.message}`));
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Download every registry theme into the local cache during setup. Runs the
|
|
96
|
+
* engine's `get --all`, which skips anything already cached and never prompts
|
|
97
|
+
* per-theme. Returns the number cached afterwards.
|
|
98
|
+
*/
|
|
99
|
+
async function ensureAllThemes(engine, { prompt = true } = {}) {
|
|
100
|
+
if (prompt) {
|
|
101
|
+
const want = await p.confirm({
|
|
102
|
+
message: 'Download all themes now (so every theme has an instant preview)?',
|
|
103
|
+
initialValue: true,
|
|
104
|
+
});
|
|
105
|
+
if (p.isCancel(want) || !want) {
|
|
106
|
+
p.log.message(pc.dim('Skipped — you can download individual themes from the menu.'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const s = p.spinner();
|
|
112
|
+
s.start('Downloading all themes…');
|
|
113
|
+
try {
|
|
114
|
+
await execa('bash', [engine, 'get', '--all'], { stdio: 'inherit', reject: true });
|
|
115
|
+
s.stop(pc.green('All themes downloaded.'));
|
|
116
|
+
} catch (e) {
|
|
117
|
+
s.stop(pc.red(`Theme download incomplete: ${e.message}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Interactive GRUB-theme browser wizard (mirrors GitSwitch's look & feel).
|
|
123
|
+
* Stays in a menu loop: pick a theme → preview / download / apply / open →
|
|
124
|
+
* back to the theme list — it NEVER hard-exits until you choose Quit.
|
|
125
|
+
*/
|
|
71
126
|
export async function runThemeBrowser() {
|
|
72
127
|
// Step 1: ensure companion tools (chafa, grub-customizer) — detect, install
|
|
73
128
|
// or update, then continue BEFORE showing the theme picker.
|
|
74
129
|
await ensureManagedTools();
|
|
75
130
|
|
|
76
|
-
const
|
|
77
|
-
const active = activeTheme();
|
|
78
|
-
|
|
79
|
-
const selected = await p.select({
|
|
80
|
-
message: pc.bold('Pick a GRUB theme'),
|
|
81
|
-
options: themes.map((t) => {
|
|
82
|
-
const status = themeStatus(t.name);
|
|
83
|
-
const color = statusColor(status);
|
|
84
|
-
return {
|
|
85
|
-
value: t,
|
|
86
|
-
label: `${t.name} ${color(`[${status.toUpperCase()}]`)}${t.name === active ? ' ⭐' : ''}`,
|
|
87
|
-
hint: t.tags.slice(0, 3).join(', '),
|
|
88
|
-
};
|
|
89
|
-
}),
|
|
90
|
-
});
|
|
91
|
-
if (p.isCancel(selected)) { p.cancel('Bye! 👋'); process.exit(0); }
|
|
92
|
-
|
|
93
|
-
p.log.step(`${pc.bold(selected.name)} — ${selected.desc}`);
|
|
94
|
-
p.log.message(pc.dim(selected.url));
|
|
95
|
-
|
|
96
|
-
if (fs.existsSync(path.join(USER_THEMES, selected.name))) {
|
|
97
|
-
await showThemePreview({ name: selected.name });
|
|
98
|
-
}
|
|
131
|
+
const engine = await resolveEngine();
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
// Step 2: install-flow — make all themes downloadable/previewable up front so
|
|
134
|
+
// the user never has to fetch one just to see it.
|
|
135
|
+
await ensureAllThemes(engine, { prompt: true });
|
|
136
|
+
|
|
137
|
+
let quit = false;
|
|
138
|
+
|
|
139
|
+
while (!quit) {
|
|
140
|
+
const themes = parseThemes(resolveConfPath());
|
|
141
|
+
const active = activeTheme();
|
|
142
|
+
|
|
143
|
+
const picked = await p.select({
|
|
144
|
+
message: pc.bold('Pick a GRUB theme'),
|
|
145
|
+
options: themes.map((t) => {
|
|
146
|
+
const status = themeStatus(t.name);
|
|
147
|
+
const color = statusColor(status);
|
|
148
|
+
return {
|
|
149
|
+
value: t,
|
|
150
|
+
label: `${t.name} ${color(`[${status.toUpperCase()}]`)}${t.name === active ? ' ⭐' : ''}`,
|
|
151
|
+
hint: (isCached(t.name) ? '🖼 ' : '') + t.tags.slice(0, 3).join(', '),
|
|
152
|
+
};
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
if (p.isCancel(picked)) { quit = true; break; }
|
|
156
|
+
|
|
157
|
+
p.log.step(`${pc.bold(picked.name)} — ${picked.desc}`);
|
|
158
|
+
p.log.message(pc.dim(picked.url));
|
|
159
|
+
|
|
160
|
+
// Thumbnails render by default: show the preview whenever the theme is
|
|
161
|
+
// cached, otherwise point the user at Download to unlock it.
|
|
162
|
+
const cached = isCached(picked.name);
|
|
163
|
+
if (cached) {
|
|
164
|
+
await showThemePreview({ name: picked.name });
|
|
165
|
+
} else {
|
|
166
|
+
p.log.message(pc.dim('Not cached yet — choose “Download / update it” below to preview it.'));
|
|
167
|
+
}
|
|
111
168
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
// Per-theme action loop: runs until the user goes back to the list or quits.
|
|
170
|
+
let backToMenu = false;
|
|
171
|
+
while (!quit && !backToMenu) {
|
|
172
|
+
const action = await p.select({
|
|
173
|
+
message: `What do you want to do with ${pc.cyan(picked.name)}?`,
|
|
174
|
+
options: [
|
|
175
|
+
{ value: 'get', label: 'Download / update it', hint: 'git clone into local cache' },
|
|
176
|
+
{ value: 'use', label: 'Apply to GRUB', hint: 'needs sudo; rebuilds GRUB' },
|
|
177
|
+
{ value: 'open', label: 'Open source page in browser' },
|
|
178
|
+
{ value: 'menu', label: '← Back to theme list' },
|
|
179
|
+
{ value: 'quit', label: 'Quit browser' },
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (p.isCancel(action) || action === 'quit') { quit = true; break; }
|
|
184
|
+
if (action === 'menu') { backToMenu = true; break; }
|
|
185
|
+
|
|
186
|
+
if (action === 'get') {
|
|
187
|
+
await ensureCached(engine, picked.name);
|
|
188
|
+
} else if (action === 'use') {
|
|
189
|
+
await runUse(engine, picked.name);
|
|
190
|
+
} else if (action === 'open') {
|
|
191
|
+
await execa('bash', [engine, 'open', picked.name], { stdio: 'inherit', reject: false });
|
|
192
|
+
}
|
|
126
193
|
}
|
|
127
|
-
} else if (action === 'use') {
|
|
128
|
-
await runUse(engine, selected.name);
|
|
129
|
-
} else if (action === 'open') {
|
|
130
|
-
await execa('bash', [engine, 'open', selected.name], { stdio: 'inherit', reject: false });
|
|
131
194
|
}
|
|
195
|
+
|
|
196
|
+
p.outro(pc.cyan('Bye! 👋'));
|
|
132
197
|
}
|
|
133
198
|
|
|
134
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Apply a theme to GRUB, re-invoking with sudo, spinner released for the prompt.
|
|
201
|
+
* Returns true when the apply was attempted/succeeded (false if aborted).
|
|
202
|
+
*/
|
|
135
203
|
export async function runUse(engine, name) {
|
|
136
|
-
|
|
137
|
-
if (!fs.existsSync(cached)) {
|
|
138
|
-
p.log.warn(`${name} isn't downloaded yet. Fetching first…`);
|
|
139
|
-
const s = p.spinner();
|
|
140
|
-
s.start(`Downloading ${name}…`);
|
|
141
|
-
try {
|
|
142
|
-
await execa('bash', [engine, 'get', name], { stdio: 'inherit', reject: true });
|
|
143
|
-
s.stop();
|
|
144
|
-
} catch (e) {
|
|
145
|
-
s.stop(pc.red(`Download failed: ${e.message}`));
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
204
|
+
if (!(await ensureCached(engine, name))) { p.cancel('Aborted — theme not downloaded.'); return false; }
|
|
149
205
|
|
|
150
206
|
const confirmApply = await p.confirm({ message: `Apply ${pc.cyan(name)} to GRUB and rebuild? (sudo)`, initialValue: true });
|
|
151
|
-
if (p.isCancel(confirmApply) || !confirmApply) {
|
|
207
|
+
if (p.isCancel(confirmApply) || !confirmApply) {
|
|
208
|
+
p.log.message(pc.dim('Skipped — theme not applied.'));
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
152
211
|
|
|
153
|
-
const { execa } = await import('execa');
|
|
154
212
|
if (process.getuid() !== 0) {
|
|
155
213
|
// Release spinner, then prompt sudo on a clean terminal so Ctrl+C works.
|
|
156
214
|
console.log();
|
|
157
215
|
const res = await execa('sudo', ['bash', engine, 'use', name], { stdio: 'inherit', reject: false });
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
if (res.exitCode === 0) { p.log.success(`${name} is now your GRUB theme! 🎉`); return true; }
|
|
217
|
+
p.cancel('GRUB apply failed.');
|
|
218
|
+
return false;
|
|
161
219
|
}
|
|
162
220
|
const res = await execa('bash', [engine, 'use', name], { stdio: 'inherit', reject: false });
|
|
163
|
-
res.exitCode === 0
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
221
|
+
if (res.exitCode === 0) { p.log.success(`${name} is now your GRUB theme! 🎉`); return true; }
|
|
222
|
+
p.cancel('GRUB apply failed.');
|
|
223
|
+
return false;
|
|
224
|
+
}
|
package/vendor/theamify
CHANGED
|
@@ -10,7 +10,7 @@ set -euo pipefail
|
|
|
10
10
|
# -----------------------------------------------------------------------------
|
|
11
11
|
# VERSION & TOOL NAME
|
|
12
12
|
# -----------------------------------------------------------------------------
|
|
13
|
-
readonly VERSION="1.0
|
|
13
|
+
readonly VERSION="2.1.0"
|
|
14
14
|
readonly TOOL="theamify"
|
|
15
15
|
|
|
16
16
|
# -----------------------------------------------------------------------------
|