theamify-cli 1.0.1 β 2.0.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/README.md +67 -207
- package/package.json +2 -2
- package/src/cli.js +4 -4
- package/src/commands/manage.js +16 -11
- package/src/lib/tools.js +112 -1
- package/src/wizard/browse.js +113 -81
- package/src/lib/chafa.js +0 -143
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# π¨ theamify
|
|
2
2
|
|
|
3
3
|
> **GRUB Theme Manager & Interactive Browser Wizard**
|
|
4
|
-
> Browse, **preview (terminal thumbnails)**, download and apply GRUB boot themes β
|
|
4
|
+
> Browse, **preview (terminal thumbnails)**, download and apply GRUB boot themes β now on npm as **`theamify-cli`**.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -13,42 +13,49 @@
|
|
|
13
13
|
npm install -g theamify-cli
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The `-g`
|
|
16
|
+
The `-g` flag puts the `theamify` command on your system `PATH` (or provisions a user-local runtime under `~/.local/share/theamify`), so you can call it from **any folder and any terminal**:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
theamify # β¨ interactive
|
|
19
|
+
theamify # β¨ interactive wizard (detects tools β browse β preview β apply)
|
|
20
20
|
theamify list # list all themes with status
|
|
21
|
-
theamify info <name> # details + terminal preview
|
|
21
|
+
theamify info <name> # details + terminal preview
|
|
22
22
|
theamify get <name> # download & cache a theme
|
|
23
23
|
sudo theamify use <name> # apply to GRUB + rebuild
|
|
24
|
+
theamify update # update tools (chafa, grub-customizer) + re-download themes
|
|
24
25
|
theamify status # GRUB & dependency status
|
|
25
|
-
theamify doctor # diagnose install, GRUB & dependencies
|
|
26
|
-
theamify uninstall # remove theamify (
|
|
26
|
+
theamify doctor # diagnose install, GRUB, tools & dependencies
|
|
27
|
+
theamify uninstall # remove only theamify (keeps chafa & grub-customizer)
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
> π‘
|
|
30
|
-
>
|
|
31
|
-
> ```bash
|
|
32
|
-
> npm install -g theamify-cli@latest
|
|
33
|
-
> ```
|
|
34
|
-
|
|
35
|
-
> π§° Prefer a zero-install run (no permanent install)? Use npx directly:
|
|
36
|
-
>
|
|
37
|
-
> ```bash
|
|
38
|
-
> npx -y theamify-cli
|
|
39
|
-
> ```
|
|
30
|
+
> π‘ Upgrading is the same command β `npm install -g theamify-cli@latest`, and your downloaded themes & registry edits are preserved.
|
|
31
|
+
> π§° Zero-install run (no permanent install): `npx -y theamify-cli`
|
|
40
32
|
|
|
41
33
|
---
|
|
42
34
|
|
|
43
|
-
## β¨ What's New
|
|
35
|
+
## β¨ What's New (v1.1.0 β npm release)
|
|
44
36
|
|
|
45
37
|
| Feature | Details |
|
|
46
38
|
|---|---|
|
|
47
|
-
| π§ **Interactive
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
39
|
+
| π§ **Interactive wizard** | One command: detect companion tools β pick theme β **preview** β apply |
|
|
40
|
+
| π§© **Auto-detected companion tools** | **chafa** (thumbnails) + **grub-customizer** (GUI) are detected first; installed if missing, or **option to update** if present β then it continues on its own |
|
|
41
|
+
| πΌοΈ **Terminal thumbnail preview** | See the boot-screen image inline via `chafa` before you commit |
|
|
42
|
+
| π¦ **npm global install** | Engine provisions itself user-writable (no sudo for downloads) |
|
|
43
|
+
| π©Ί **`theamify doctor`** | Diagnose install, GRUB, active theme, registry, tools & dependencies |
|
|
44
|
+
| π **Safe uninstall** | Removes **only theamify** β leaves chafa & grub-customizer for later use |
|
|
45
|
+
| β¨οΈ **Ctrl+C / Ctrl+Z safe** | Signal safety net; interactive `sudo` is never frozen |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## π§ Wizard Sequence (every `theamify` run)
|
|
50
|
+
|
|
51
|
+
1. **Detect companion tools** β `chafa` then `grub-customizer`. For each:
|
|
52
|
+
- **Already installed?** β *βUpdate it now? (or just continue)β* β Continue moves on.
|
|
53
|
+
- **Missing?** β *βInstall `<name>`? Yes/Noβ* β installs via your package manager (`sudo`), then continues.
|
|
54
|
+
2. **Show the theme picker** β every registry theme with status (`[REMOTE]`/`[CACHED]`/`[ACTIVE]`).
|
|
55
|
+
3. **Pick a theme** β optional **terminal thumbnail preview** (needs `chafa`).
|
|
56
|
+
4. **Act** β download, apply to GRUB, or open in browser.
|
|
57
|
+
|
|
58
|
+
> Companion tools are installed strictly **before** the browser, so previews are ready when you need them.
|
|
52
59
|
|
|
53
60
|
---
|
|
54
61
|
|
|
@@ -60,14 +67,35 @@ theamify uninstall # remove theamify (asks before deleting anything)
|
|
|
60
67
|
| `git` | Downloading / cloning themes |
|
|
61
68
|
| `curl` **or** `wget` | Downloading |
|
|
62
69
|
| `chafa` | Terminal thumbnail previews β **auto-installed by the wizard** |
|
|
63
|
-
| `
|
|
70
|
+
| `grub-customizer` | GUI to tweak GRUB menu β **auto-installed by the wizard** |
|
|
71
|
+
| `sudo` | Tool installs + applying a GRUB theme (`theamify use`) |
|
|
64
72
|
|
|
65
73
|
---
|
|
66
74
|
|
|
67
|
-
##
|
|
75
|
+
## π Project Structure (npm package)
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
theamify/
|
|
79
|
+
βββ bin/theamify.js # npm bin entry β src/cli.js
|
|
80
|
+
βββ src/
|
|
81
|
+
β βββ cli.js # command router + signal safety net
|
|
82
|
+
β βββ core/engine.js # runtime provisioning & engine forwarding
|
|
83
|
+
β βββ commands/manage.js # doctor / status / uninstall
|
|
84
|
+
β βββ wizard/browse.js # interactive wizard (browse β preview β apply)
|
|
85
|
+
β βββ lib/
|
|
86
|
+
β βββ conf.js # themes.conf registry parser
|
|
87
|
+
β βββ tools.js # companion tools: chafa + grub-customizer
|
|
88
|
+
βββ vendor/ # bundled bash engine (theamify + lib/ + config/)
|
|
89
|
+
βββ test/ # unit tests (node:test)
|
|
90
|
+
βββ package.json
|
|
91
|
+
βββ .github/workflows/ # ci.yml + publish.yml (npm OIDC)
|
|
92
|
+
```
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Rosetta Stone β CLI vs classic tool
|
|
97
|
+
|
|
98
|
+
The npm CLI is a thin wrapper around the original Bash engine; every non-interactive command behaves identically:
|
|
71
99
|
|
|
72
100
|
| npm CLI | Original tool |
|
|
73
101
|
|---|---|
|
|
@@ -77,31 +105,24 @@ non-interactive command still behaves identically:
|
|
|
77
105
|
| `theamify info <n>` | `theamify info <n>` |
|
|
78
106
|
| `theamify add/del/remove/open/update` | same names |
|
|
79
107
|
|
|
80
|
-
`theamify <no args>`
|
|
81
|
-
browser/preview/apply flow instead of the flat TUI menu.
|
|
108
|
+
`theamify <no args>` differs β it runs the interactive wizard (tools-then-browser) instead of the flat TUI menu.
|
|
82
109
|
|
|
83
110
|
---
|
|
84
111
|
|
|
85
|
-
##
|
|
112
|
+
## π§© Companion Tools: chafa & grub-customizer
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
wizard run ensures it's present (installing it when missing), updates it on
|
|
89
|
-
`theamify update`, and removes it on `theamify uninstall`.
|
|
114
|
+
The wizard manages two companion tools as first-class:
|
|
90
115
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
| Tool | Purpose | Install command (apt) | Lifecycle |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `chafa` | Renders theme thumbnails in the terminal | `sudo apt install chafa` | auto-installed/updated by wizard |
|
|
119
|
+
| `grub-customizer` | GUI for GRUB menu & theme tweaks | `sudo apt install grub-customizer` | auto-installed/updated by wizard |
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
theamify # pick a theme β auto-preview
|
|
102
|
-
theamify info <name> # details + thumbnail preview
|
|
103
|
-
theamify update # also updates chafa
|
|
104
|
-
```
|
|
121
|
+
**Behavior:**
|
|
122
|
+
- **Detect** β if present, offer to update; if absent, install (with your consent).
|
|
123
|
+
- **Continue** regardless of Yes/No β the wizard proceeds to the theme picker.
|
|
124
|
+
- **`theamify update`** updates tools then re-downloads themes.
|
|
125
|
+
- **`theamify uninstall`** removes **only** theamify β chafa and grub-customizer stay on the system for later use.
|
|
105
126
|
|
|
106
127
|
---
|
|
107
128
|
|
|
@@ -120,167 +141,6 @@ theamify update # also updates chafa
|
|
|
120
141
|
|
|
121
142
|
---
|
|
122
143
|
|
|
123
|
-
## π Project Structure (npm package)
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
bin/theamify.js # npm bin β src/cli.js
|
|
127
|
-
src/cli.js # command router + signal safety net
|
|
128
|
-
src/core/engine.js # runtime provisioning & engine forwarding
|
|
129
|
-
src/lib/conf.js # themes.conf registry parser
|
|
130
|
-
src/commands/manage.js # doctor / status / uninstall
|
|
131
|
-
src/wizard/browse.js # interactive browser + chafa preview + apply
|
|
132
|
-
vendor/ # bundled bash engine (theamify + lib/ + config/)
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## π Uninstalling
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
theamify uninstall # interactive β removes the engine runtime
|
|
141
|
-
npm uninstall -g theamify-cli
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Your active GRUB theme and `/etc/default/grub` settings are always preserved.
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
144
|
## π License
|
|
149
145
|
|
|
150
146
|
MIT β see [LICENSE](LICENSE). Don Artkins 2026.
|
|
151
|
-
|
|
152
|
-
## Project Structure
|
|
153
|
-
```bash
|
|
154
|
-
theamify/
|
|
155
|
-
|
|
156
|
-
|-- theamify # Main CLI executable
|
|
157
|
-
|
|
158
|
-
|-- install.sh # System-wide installer
|
|
159
|
-
|
|
160
|
-
|-- uninstall.sh # Remover
|
|
161
|
-
|
|
162
|
-
|-- update.sh # Self-updater (git pull + reinstall)
|
|
163
|
-
|
|
164
|
-
|-- lib/
|
|
165
|
-
|
|
166
|
-
| |-- colors.sh # ANSI colors & UI components
|
|
167
|
-
|
|
168
|
-
| |-- utils.sh # Helpers, git clone, dep checks
|
|
169
|
-
|
|
170
|
-
| |-- grub.sh # GRUB detection & apply logic
|
|
171
|
-
|
|
172
|
-
| `-- themes.sh # Theme registry CRUD & download
|
|
173
|
-
|
|
174
|
-
|-- config/
|
|
175
|
-
|
|
176
|
-
| `-- themes.conf # Theme registry database
|
|
177
|
-
|
|
178
|
-
|-- themes/ # Downloaded theme cache (auto-populated)
|
|
179
|
-
|
|
180
|
-
|-- .repo_cache/ # Git clone cache (auto-populated)
|
|
181
|
-
|
|
182
|
-
|-- .gitignore
|
|
183
|
-
|
|
184
|
-
|-- LICENSE # MIT
|
|
185
|
-
|
|
186
|
-
`-- CONTRIBUTING.md
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## Install
|
|
190
|
-
|
|
191
|
-
```bash
|
|
192
|
-
git clone https://github.com/DonArtkins/theamify
|
|
193
|
-
cd theamify
|
|
194
|
-
sudo ./install.sh
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
After install, `theamify` is available system-wide - run it from any terminal.
|
|
198
|
-
Only `theamify use` requires `sudo`. All other commands run as a normal user.
|
|
199
|
-
|
|
200
|
-
### Updating an existing install
|
|
201
|
-
|
|
202
|
-
`install.sh` deliberately leaves an already-installed `config/themes.conf`
|
|
203
|
-
alone, so your own `add`/`del` edits via `theamify` survive a reinstall.
|
|
204
|
-
That also means a plain `sudo ./install.sh` will **not** pick up registry
|
|
205
|
-
changes from this checkout (new/removed/edited entries) - pass `--sync-conf`
|
|
206
|
-
when you want that:
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
sudo ./install.sh --sync-conf
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md#developing-against-an-existing-install)
|
|
213
|
-
for the full dev-vs-installed workflow.
|
|
214
|
-
|
|
215
|
-
## Usage
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
theamify # Interactive TUI menu
|
|
219
|
-
theamify list # List all themes with status
|
|
220
|
-
theamify info <name> # Show details + preview (needs chafa)
|
|
221
|
-
theamify get <name> # Download & cache a theme
|
|
222
|
-
theamify get --all # Download every theme
|
|
223
|
-
sudo theamify use <name> # Apply theme to GRUB + rebuild
|
|
224
|
-
theamify update <name> # Re-download one theme
|
|
225
|
-
theamify update # Re-download all cached themes
|
|
226
|
-
theamify open <name> # Open source page in browser
|
|
227
|
-
theamify add <github-url> # Add new theme to registry
|
|
228
|
-
theamify del <name> # Remove theme from registry
|
|
229
|
-
theamify remove <name> # Clear local cache (keeps registry entry)
|
|
230
|
-
theamify status # Show GRUB + dependency status
|
|
231
|
-
theamify clean # Clear repo clone cache
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Theme Previews in Terminal
|
|
235
|
-
|
|
236
|
-
Install `chafa` for inline image previews in `theamify info`:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
sudo apt install chafa
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Themes Included
|
|
243
|
-
|
|
244
|
-
| Name | Style | Source |
|
|
245
|
-
|------------------|-----------------|--------|
|
|
246
|
-
| CyberEXS | Cyberpunk dark | [gnome-look](https://www.gnome-look.org/p/1968990) |
|
|
247
|
-
| CyberSynchro | Cyberpunk teal | [gnome-look](https://www.gnome-look.org/p/1972621) |
|
|
248
|
-
| Space-Isolation | Sci-Fi | [gnome-look](https://www.gnome-look.org/p/2296342) |
|
|
249
|
-
| Kawaii-GRUB | Anime | [gnome-look](https://www.gnome-look.org/p/2218890) |
|
|
250
|
-
| Kayoko-Onikata | Anime / Cat | [gnome-look](https://www.gnome-look.org/p/2350900) |
|
|
251
|
-
| Matrices | Abstract tech | [gnome-look](https://www.gnome-look.org/p/2271298) |
|
|
252
|
-
| Particle | Abstract | [gnome-look](https://www.gnome-look.org/p/2269763) |
|
|
253
|
-
| Zzz-GRUB | Chill / Cat | [gnome-look](https://www.gnome-look.org/p/2354136) |
|
|
254
|
-
|
|
255
|
-
> **Matrices / Particle note:** both repos ship no pre-built theme folder -
|
|
256
|
-
> `backgrounds/`, `common/`, and `config/` are raw assets, and `theme.txt` is
|
|
257
|
-
> only produced at build time by their `generate.sh` (`-t [window|sidebar]
|
|
258
|
-
> -s [1080p|2k|4k]`). The registry entries use a `generate:<args>` SUBDIR so
|
|
259
|
-
> `theamify get` drives that script automatically - see CONTRIBUTING.md for
|
|
260
|
-
> how that mechanism works if you want to add another theme shaped like this.
|
|
261
|
-
|
|
262
|
-
## Add Your Own Theme
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
theamify add https://github.com/someone/their-grub-theme
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Follow the interactive wizard - it asks for name, subdir, description, and tags,
|
|
269
|
-
then appends the entry to `themes.conf`.
|
|
270
|
-
|
|
271
|
-
## Uninstall
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
sudo ./uninstall.sh
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Your active GRUB theme and `/etc/default/grub` settings are preserved.
|
|
278
|
-
|
|
279
|
-
## Contributing
|
|
280
|
-
|
|
281
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, coding conventions, and how
|
|
282
|
-
to submit a theme or a patch.
|
|
283
|
-
|
|
284
|
-
## License
|
|
285
|
-
|
|
286
|
-
MIT - see [LICENSE](LICENSE). Don Artkins 2026.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theamify-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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
|
@@ -3,7 +3,7 @@ import pc from 'picocolors';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { runThemeBrowser } from './wizard/browse.js';
|
|
5
5
|
import { runDoctor, runStatus, runUninstallWizard } from './commands/manage.js';
|
|
6
|
-
import {
|
|
6
|
+
import { updateManagedTools } from './lib/tools.js';
|
|
7
7
|
import { runEngine } from './core/engine.js';
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
@@ -85,9 +85,9 @@ const main = defineCommand({
|
|
|
85
85
|
case 'uninstall':
|
|
86
86
|
return runUninstallWizard();
|
|
87
87
|
case 'update':
|
|
88
|
-
// Self-update path: refresh
|
|
89
|
-
// re-downloads to the engine
|
|
90
|
-
await
|
|
88
|
+
// Self-update path: refresh companion tools (chafa, grub-customizer),
|
|
89
|
+
// then forward theme re-downloads to the engine.
|
|
90
|
+
await updateManagedTools();
|
|
91
91
|
return forwardEngine(['update', ...rest]);
|
|
92
92
|
case 'use':
|
|
93
93
|
case 'apply':
|
package/src/commands/manage.js
CHANGED
|
@@ -2,15 +2,15 @@ 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
|
USER_BIN_LINK,
|
|
10
10
|
resolveEngine,
|
|
11
11
|
} from '../core/engine.js';
|
|
12
|
-
import { parseThemes } from '../lib/conf.js';
|
|
13
|
-
import {
|
|
12
|
+
import { parseThemes, resolveConfPath } from '../lib/conf.js';
|
|
13
|
+
import { companionToolStatus } from '../lib/tools.js';
|
|
14
14
|
|
|
15
15
|
/** `theamify doctor` β installation, GRUB and dependency health. */
|
|
16
16
|
export async function runDoctor() {
|
|
@@ -23,29 +23,34 @@ export async function runDoctor() {
|
|
|
23
23
|
|
|
24
24
|
// Registry / theme count
|
|
25
25
|
try {
|
|
26
|
-
const themes = parseThemes();
|
|
26
|
+
const themes = parseThemes(resolveConfPath());
|
|
27
27
|
const cached = themes.filter((t) => fs.existsSync(path.join(USER_DIR, 'themes', t.name)));
|
|
28
28
|
console.log(` Registry : ${themes.length} theme(s), ${cached.length} cached`);
|
|
29
29
|
} catch (e) {
|
|
30
30
|
console.log(` Registry : ${pc.red(`unreadable (${e.message})`)}`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Base dependencies
|
|
34
|
+
const deps = ['git', 'wget', 'curl'].map((d) => [d, which(d)]);
|
|
35
|
+
console.log(' Deps : ' + deps.map(([d, ok]) => `${d}:${ok ? pc.green('β') : pc.dim('β')}`).join(' '));
|
|
36
|
+
|
|
37
|
+
// Companion tools (chafa, grub-customizer)
|
|
38
|
+
const tools = companionToolStatus();
|
|
39
|
+
console.log(' Tools : ' + tools.map((t) => `${t.name}:${t.present ? pc.green('β') : pc.dim('β')}`).join(' '));
|
|
40
|
+
|
|
33
41
|
// GRUB active theme + dir
|
|
34
42
|
const grubDir = grubDetectDir();
|
|
35
43
|
console.log(` GRUB dir : ${pc.cyan(grubDir)}`);
|
|
36
44
|
const active = activeThemeLabel();
|
|
37
45
|
console.log(` Active : ${active ? pc.green('β ' + active) : pc.dim('none')}`);
|
|
38
46
|
|
|
39
|
-
// Dependencies
|
|
40
|
-
const deps = ['git', 'chafa', 'wget', 'curl'].map((d) => [d, which(d)]);
|
|
41
|
-
console.log(' Deps : ' + deps.map(([d, ok]) => `${d}:${ok ? pc.green('β') : pc.dim('β')}`).join(' '));
|
|
42
47
|
const grub = ['update-grub', 'grub-mkconfig', 'grub2-mkconfig'].filter((c) => which(c));
|
|
43
48
|
console.log(` GRUB cmd : ${grub.length ? grub.join(', ') : pc.red('none found')}`);
|
|
44
49
|
console.log();
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function which(bin) {
|
|
48
|
-
try { return
|
|
53
|
+
try { return execaSync('which', [bin], { reject: false }).stdout.trim(); } catch { return ''; }
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function grubDetectDir() {
|
|
@@ -113,10 +118,10 @@ export async function runUninstallWizard() {
|
|
|
113
118
|
});
|
|
114
119
|
if (p.isCancel(keepGrub)) { p.cancel('Aborted.'); process.exit(0); }
|
|
115
120
|
|
|
116
|
-
//
|
|
117
|
-
|
|
121
|
+
// Companion tools (chafa, grub-customizer) are intentionally LEFT in place β
|
|
122
|
+
// the user may want them for later; uninstall only removes theamify itself.
|
|
118
123
|
|
|
119
124
|
p.outro(pc.green(
|
|
120
|
-
`Uninstalled.${removed ? ' Runtime removed.' : ''}${keepGrub ? ' Active GRUB theme left in place.' : ' To revert GRUB, remove GRUB_THEME= from /etc/default/grub and rebuild.'}
|
|
125
|
+
`Uninstalled.${removed ? ' Runtime removed.' : ''}${keepGrub ? ' Active GRUB theme left in place.' : ' To revert GRUB, remove GRUB_THEME= from /etc/default/grub and rebuild.'} Companion tools (chafa, grub-customizer) were kept for your use.`,
|
|
121
126
|
));
|
|
122
127
|
}
|
package/src/lib/tools.js
CHANGED
|
@@ -67,10 +67,121 @@ function packageNameFor(tool, pm) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** @returns {boolean} whether the tool binary is on PATH (sync) */
|
|
70
|
-
function hasToolSync(tool) {
|
|
70
|
+
export function hasToolSync(tool) {
|
|
71
71
|
try {
|
|
72
72
|
return Boolean(execaSync('bash', ['-c', `command -v ${tool.bin}`], { reject: false }).stdout.trim());
|
|
73
73
|
} catch {
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install a tool via sudo, prompting the user first. Returns true when the tool
|
|
80
|
+
* is usable afterwards.
|
|
81
|
+
* @param {{name:string,bin:string,purpose:string,install?:Object}} tool
|
|
82
|
+
* @param {{silent?:boolean}} [opts]
|
|
83
|
+
*/
|
|
84
|
+
export async function ensureTool(tool, { silent = false } = {}) {
|
|
85
|
+
if (await hasTool(tool)) return true;
|
|
86
|
+
|
|
87
|
+
const pm = detectPackageManager();
|
|
88
|
+
const pkg = packageNameFor(tool, pm);
|
|
89
|
+
if (!pm || !pkg) {
|
|
90
|
+
if (!silent) p.log.warn(`No supported package manager (or no package) found for ${tool.name} β install "${tool.bin}" manually.`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!silent) {
|
|
95
|
+
const want = await p.confirm({
|
|
96
|
+
message: `Install ${pc.cyan(tool.name)} (${pkg}) for ${tool.purpose}?`,
|
|
97
|
+
initialValue: true,
|
|
98
|
+
});
|
|
99
|
+
if (p.isCancel(want) || !want) {
|
|
100
|
+
p.log.message(pc.dim(`Skipped ${tool.name} β install later with your package manager.`));
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const s = p.spinner();
|
|
106
|
+
s.start(`Installing ${tool.name}β¦`);
|
|
107
|
+
try {
|
|
108
|
+
// Release the spinner so the sudo prompt is visible & interruptible.
|
|
109
|
+
s.stop('');
|
|
110
|
+
const args = pm === 'pacman' ? ['-S', '--noconfirm', pkg] : [pm, 'install', '-y', pkg];
|
|
111
|
+
await runPrivileged(args);
|
|
112
|
+
if (!(await hasTool(tool))) throw new Error(`Install appeared to complete, but "${tool.bin}" is not on PATH.`);
|
|
113
|
+
if (!silent) p.log.success(`${tool.name} installed.`);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (!silent) p.log.warn(`${tool.name} install failed: ${e.message}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* If a tool is present, offer to update it (default: just continue). If missing,
|
|
123
|
+
* install it. This is the wizard's "auto-detect β install or update β continue".
|
|
124
|
+
*/
|
|
125
|
+
export async function ensureManagedTool(tool) {
|
|
126
|
+
if (await hasTool(tool)) {
|
|
127
|
+
const decision = await p.confirm({
|
|
128
|
+
message: `${pc.cyan(tool.name)} is already installed. Update it now? (or just continue)`,
|
|
129
|
+
initialValue: false,
|
|
130
|
+
});
|
|
131
|
+
if (p.isCancel(decision)) return true;
|
|
132
|
+
if (decision) return updateTool(tool, { prompt: false });
|
|
133
|
+
p.log.message(pc.dim(`${tool.name} kept as-is β continuing.`));
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return ensureTool(tool, { silent: false });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Run the wizard through every companion tool in registry order. */
|
|
140
|
+
export async function ensureManagedTools() {
|
|
141
|
+
p.note('Companion tools are detected, installed or updated first.', 'Setup');
|
|
142
|
+
for (const tool of TOOLS) {
|
|
143
|
+
await ensureManagedTool(tool);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Upgrade all managed companion tools (used by `theamify update`). */
|
|
148
|
+
export async function updateManagedTools() {
|
|
149
|
+
for (const tool of TOOLS) {
|
|
150
|
+
if (await hasTool(tool)) await updateTool(tool, { prompt: false });
|
|
151
|
+
else await ensureTool(tool, { silent: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Update a single tool to its latest available version (best-effort). */
|
|
156
|
+
export async function updateTool(tool, { prompt = false } = {}) {
|
|
157
|
+
if (!(await hasTool(tool))) return ensureTool(tool, { silent: true });
|
|
158
|
+
|
|
159
|
+
const pm = detectPackageManager();
|
|
160
|
+
const pkg = packageNameFor(tool, pm);
|
|
161
|
+
if (!pm || !pkg) return false;
|
|
162
|
+
|
|
163
|
+
if (prompt) {
|
|
164
|
+
const want = await p.confirm({ message: `Update ${pc.cyan(tool.name)} now?`, initialValue: true });
|
|
165
|
+
if (p.isCancel(want) || !want) { p.log.message(pc.dim(`${tool.name} update skipped.`)); return true; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
p.log.step(`Updating ${tool.name}β¦`);
|
|
169
|
+
try {
|
|
170
|
+
const args = pm === 'pacman'
|
|
171
|
+
? ['-Syu', '--noconfirm', pkg]
|
|
172
|
+
: pm === 'apk'
|
|
173
|
+
? ['apk', 'upgrade', pkg]
|
|
174
|
+
: [pm, 'install', '-y', '--only-upgrade', pkg];
|
|
175
|
+
await runPrivileged(args);
|
|
176
|
+
p.log.success(`${tool.name} updated.`);
|
|
177
|
+
return true;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
p.log.warn(`${tool.name} update failed: ${e.message}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Report companion-tool presence (sync) for `doctor`. */
|
|
185
|
+
export function companionToolStatus() {
|
|
186
|
+
return TOOLS.map((tool) => ({ name: tool.name, present: hasToolSync(tool) }));
|
|
76
187
|
}
|
package/src/wizard/browse.js
CHANGED
|
@@ -2,8 +2,9 @@ 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
|
-
import {
|
|
7
|
+
import { ensureManagedTools } from '../lib/tools.js';
|
|
7
8
|
import { resolveEngine, USER_DIR } from '../core/engine.js';
|
|
8
9
|
|
|
9
10
|
const USER_THEMES = path.join(USER_DIR, 'themes');
|
|
@@ -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,103 +68,130 @@ 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
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 });
|
|
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;
|
|
97
91
|
}
|
|
92
|
+
}
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
if (p.isCancel(action) || action === 'back') { p.outro('Bye! π'); process.exit(0); }
|
|
94
|
+
/**
|
|
95
|
+
* Interactive GRUB-theme browser wizard (mirrors GitSwitch's look & feel).
|
|
96
|
+
* Stays in a menu loop: pick a theme β preview / download / apply / open β
|
|
97
|
+
* back to the theme list β it NEVER hard-exits until you choose Quit.
|
|
98
|
+
*/
|
|
99
|
+
export async function runThemeBrowser() {
|
|
100
|
+
// Step 1: ensure companion tools (chafa, grub-customizer) β detect, install
|
|
101
|
+
// or update, then continue BEFORE showing the theme picker.
|
|
102
|
+
await ensureManagedTools();
|
|
110
103
|
|
|
111
104
|
const engine = await resolveEngine();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
105
|
+
let quit = false;
|
|
106
|
+
|
|
107
|
+
while (!quit) {
|
|
108
|
+
const themes = parseThemes(resolveConfPath());
|
|
109
|
+
const active = activeTheme();
|
|
110
|
+
|
|
111
|
+
const picked = await p.select({
|
|
112
|
+
message: pc.bold('Pick a GRUB theme'),
|
|
113
|
+
options: themes.map((t) => {
|
|
114
|
+
const status = themeStatus(t.name);
|
|
115
|
+
const color = statusColor(status);
|
|
116
|
+
return {
|
|
117
|
+
value: t,
|
|
118
|
+
label: `${t.name} ${color(`[${status.toUpperCase()}]`)}${t.name === active ? ' β' : ''}`,
|
|
119
|
+
hint: (isCached(t.name) ? 'πΌ ' : '') + t.tags.slice(0, 3).join(', '),
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
if (p.isCancel(picked)) { quit = true; break; }
|
|
124
|
+
|
|
125
|
+
p.log.step(`${pc.bold(picked.name)} β ${picked.desc}`);
|
|
126
|
+
p.log.message(pc.dim(picked.url));
|
|
127
|
+
|
|
128
|
+
// Thumbnails render by default: show the preview whenever the theme is
|
|
129
|
+
// cached, otherwise point the user at Download to unlock it.
|
|
130
|
+
const cached = isCached(picked.name);
|
|
131
|
+
if (cached) {
|
|
132
|
+
await showThemePreview({ name: picked.name });
|
|
133
|
+
} else {
|
|
134
|
+
p.log.message(pc.dim('Not cached yet β choose βDownload / update itβ below to preview it.'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Per-theme action loop: runs until the user goes back to the list or quits.
|
|
138
|
+
let backToMenu = false;
|
|
139
|
+
while (!quit && !backToMenu) {
|
|
140
|
+
const action = await p.select({
|
|
141
|
+
message: `What do you want to do with ${pc.cyan(picked.name)}?`,
|
|
142
|
+
options: [
|
|
143
|
+
{ value: 'preview', label: 'Preview terminal thumbnail', hint: cached ? 'renders below' : 'downloads first' },
|
|
144
|
+
{ value: 'get', label: 'Download / update it', hint: 'git clone into local cache' },
|
|
145
|
+
{ value: 'use', label: 'Apply to GRUB', hint: 'needs sudo; rebuilds GRUB' },
|
|
146
|
+
{ value: 'open', label: 'Open source page in browser' },
|
|
147
|
+
{ value: 'menu', label: 'β Back to theme list' },
|
|
148
|
+
{ value: 'quit', label: 'Quit browser' },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (p.isCancel(action) || action === 'quit') { quit = true; break; }
|
|
153
|
+
if (action === 'menu') { backToMenu = true; break; }
|
|
154
|
+
|
|
155
|
+
if (action === 'preview') {
|
|
156
|
+
if (await ensureCached(engine, picked.name)) {
|
|
157
|
+
await showThemePreview({ name: picked.name, width: 80, height: 22 });
|
|
158
|
+
}
|
|
159
|
+
} else if (action === 'get') {
|
|
160
|
+
await ensureCached(engine, picked.name);
|
|
161
|
+
} else if (action === 'use') {
|
|
162
|
+
await runUse(engine, picked.name);
|
|
163
|
+
} else if (action === 'open') {
|
|
164
|
+
await execa('bash', [engine, 'open', picked.name], { stdio: 'inherit', reject: false });
|
|
165
|
+
}
|
|
125
166
|
}
|
|
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
167
|
}
|
|
168
|
+
|
|
169
|
+
p.outro(pc.cyan('Bye! π'));
|
|
131
170
|
}
|
|
132
171
|
|
|
133
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Apply a theme to GRUB, re-invoking with sudo, spinner released for the prompt.
|
|
174
|
+
* Returns true when the apply was attempted/succeeded (false if aborted).
|
|
175
|
+
*/
|
|
134
176
|
export async function runUse(engine, name) {
|
|
135
|
-
|
|
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
|
-
}
|
|
177
|
+
if (!(await ensureCached(engine, name))) { p.cancel('Aborted β theme not downloaded.'); return false; }
|
|
148
178
|
|
|
149
179
|
const confirmApply = await p.confirm({ message: `Apply ${pc.cyan(name)} to GRUB and rebuild? (sudo)`, initialValue: true });
|
|
150
|
-
if (p.isCancel(confirmApply) || !confirmApply) {
|
|
180
|
+
if (p.isCancel(confirmApply) || !confirmApply) {
|
|
181
|
+
p.log.message(pc.dim('Skipped β theme not applied.'));
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
151
184
|
|
|
152
|
-
const { execa } = await import('execa');
|
|
153
185
|
if (process.getuid() !== 0) {
|
|
154
186
|
// Release spinner, then prompt sudo on a clean terminal so Ctrl+C works.
|
|
155
187
|
console.log();
|
|
156
188
|
const res = await execa('sudo', ['bash', engine, 'use', name], { stdio: 'inherit', reject: false });
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
if (res.exitCode === 0) { p.log.success(`${name} is now your GRUB theme! π`); return true; }
|
|
190
|
+
p.cancel('GRUB apply failed.');
|
|
191
|
+
return false;
|
|
160
192
|
}
|
|
161
193
|
const res = await execa('bash', [engine, 'use', name], { stdio: 'inherit', reject: false });
|
|
162
|
-
res.exitCode === 0
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
194
|
+
if (res.exitCode === 0) { p.log.success(`${name} is now your GRUB theme! π`); return true; }
|
|
195
|
+
p.cancel('GRUB apply failed.');
|
|
196
|
+
return false;
|
|
197
|
+
}
|
package/src/lib/chafa.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|