agy-powerline 1.2.0 → 1.3.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/CHANGELOG.md +34 -0
- package/README.fr.md +190 -0
- package/README.ja.md +68 -26
- package/README.md +84 -29
- package/README.zh-CN.md +80 -38
- package/package.json +3 -2
- package/src/cli/caps.js +2 -0
- package/src/cli/configure.js +8 -144
- package/src/cli/doctor.js +12 -0
- package/src/cli/export.js +82 -0
- package/src/cli/gradient.js +121 -0
- package/src/cli/import.js +273 -0
- package/src/cli/init.js +186 -7
- package/src/cli/layout.js +2 -0
- package/src/cli/preset.js +2 -0
- package/src/cli/repo-url.js +7 -0
- package/src/cli/theme.js +2 -0
- package/src/cli/uninstall.js +6 -2
- package/src/index.js +51 -0
- package/src/lib/catalog.js +54 -2
- package/src/lib/colors.js +127 -0
- package/src/lib/config-check.js +74 -0
- package/src/lib/config.js +20 -1
- package/src/lib/exec.js +140 -21
- package/src/lib/fit.js +7 -0
- package/src/lib/formatters.js +67 -0
- package/src/lib/git.js +374 -2
- package/src/lib/layouts/lines.js +139 -20
- package/src/lib/layouts/pills.js +167 -18
- package/src/lib/paths.js +73 -13
- package/src/lib/prompt.js +30 -0
- package/src/lib/run.js +2 -0
- package/src/lib/session.js +17 -3
- package/src/lib/shared.js +11 -1
- package/src/lib/themes.js +79 -5
- package/src/lib/usage.js +524 -0
- package/src/lib/widget-options.js +194 -0
- package/src/lib/widget-style.js +39 -0
- package/src/runtime/statusline.js +15 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
saveConfig,
|
|
7
|
+
setConfigDir,
|
|
8
|
+
getConfigPath,
|
|
9
|
+
mergeConfig,
|
|
10
|
+
renderStatusline,
|
|
11
|
+
sampleData,
|
|
12
|
+
detectStyle,
|
|
13
|
+
} = require("../lib/shared");
|
|
14
|
+
const { resolveInstallDir, writeFileAtomic, PRIVATE_FILE_MODE } = require("../lib/paths");
|
|
15
|
+
const { createRl, confirm } = require("../lib/prompt");
|
|
16
|
+
const { loadEditableConfig } = require("./config-guard");
|
|
17
|
+
const { version } = require("../../package.json");
|
|
18
|
+
|
|
19
|
+
const DIM = "\x1b[2m";
|
|
20
|
+
const BOLD = "\x1b[1m";
|
|
21
|
+
const GREEN = "\x1b[32m";
|
|
22
|
+
const RED = "\x1b[31m";
|
|
23
|
+
const YELLOW = "\x1b[33m";
|
|
24
|
+
const CYAN = "\x1b[36m";
|
|
25
|
+
const RESET = "\x1b[0m";
|
|
26
|
+
|
|
27
|
+
// A config is a few KB; anything this big isn't one.
|
|
28
|
+
const MAX_IMPORT_BYTES = 256 * 1024;
|
|
29
|
+
const BACKUP_SUFFIX = ".bak";
|
|
30
|
+
const { visible, droppedSettings } = require("../lib/config-check");
|
|
31
|
+
const SETTINGS = ["emojis", "ascii", "color", "pacing", "minimalist", "rowGap", "log"];
|
|
32
|
+
|
|
33
|
+
function readImportFile(file) {
|
|
34
|
+
const target = path.resolve(String(file));
|
|
35
|
+
let stat;
|
|
36
|
+
try {
|
|
37
|
+
stat = fs.statSync(target);
|
|
38
|
+
} catch {
|
|
39
|
+
return { target, error: "the file doesn't exist" };
|
|
40
|
+
}
|
|
41
|
+
if (!stat.isFile()) {
|
|
42
|
+
return { target, error: "it isn't a file" };
|
|
43
|
+
}
|
|
44
|
+
if (stat.size > MAX_IMPORT_BYTES) {
|
|
45
|
+
return {
|
|
46
|
+
target,
|
|
47
|
+
error: `it is larger than ${MAX_IMPORT_BYTES / 1024} KB, so it isn't a config`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(fs.readFileSync(target, "utf8").replace(/^\uFEFF/, ""));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// The parser quotes the text it failed on; keep only where.
|
|
56
|
+
const at = String(err.message).match(/position \d+|line \d+ column \d+/)?.[0];
|
|
57
|
+
return { target, error: `it isn't valid JSON${at ? ` (${at})` : ""}` };
|
|
58
|
+
}
|
|
59
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
60
|
+
return { target, error: "its top-level value isn't an object" };
|
|
61
|
+
}
|
|
62
|
+
return { target, data };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function majorOf(value) {
|
|
66
|
+
const major = parseInt(String(value ?? "").match(/^(\d+)\./)?.[1] ?? "", 10);
|
|
67
|
+
return Number.isFinite(major) ? major : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// "model context git / fiveHourBar fiveHourReset ...", one row per line break.
|
|
71
|
+
function widgetRows(widgets) {
|
|
72
|
+
const rows = [[]];
|
|
73
|
+
for (const widget of widgets) {
|
|
74
|
+
if (widget.id === "lineBreak") {
|
|
75
|
+
rows.push([]);
|
|
76
|
+
} else if (widget.id !== "separator") {
|
|
77
|
+
rows[rows.length - 1].push(widget.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return rows.map((row) => row.join(" ") || "(empty row)");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function changes(before, after) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
const show = (value) => (value === undefined || value === null ? "off" : JSON.stringify(value));
|
|
86
|
+
const compare = (name, a, b) => {
|
|
87
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
88
|
+
lines.push(` ${name.padEnd(11)} ${DIM}${visible(show(a))}${RESET} → ${visible(show(b))}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
compare("theme", before.theme, after.theme);
|
|
93
|
+
compare("layout", before.layout, after.layout);
|
|
94
|
+
compare(
|
|
95
|
+
"style",
|
|
96
|
+
detectStyle(before.powerline) ?? "custom",
|
|
97
|
+
detectStyle(after.powerline) ?? "custom"
|
|
98
|
+
);
|
|
99
|
+
compare("gradient", before.gradient, after.gradient);
|
|
100
|
+
compare("meterStyle", before.meterStyle, after.meterStyle);
|
|
101
|
+
for (const key of SETTINGS) {
|
|
102
|
+
compare(key, before[key], after[key]);
|
|
103
|
+
}
|
|
104
|
+
compare("thresholds", before.thresholds, after.thresholds);
|
|
105
|
+
compare("clock", before.clock, after.clock);
|
|
106
|
+
compare("customTheme", before.customTheme, after.customTheme);
|
|
107
|
+
|
|
108
|
+
const rowsBefore = widgetRows(before.widgets);
|
|
109
|
+
const rowsAfter = widgetRows(after.widgets);
|
|
110
|
+
if (JSON.stringify(before.widgets) !== JSON.stringify(after.widgets)) {
|
|
111
|
+
lines.push(" widgets");
|
|
112
|
+
rowsBefore.forEach((row) => lines.push(` ${DIM}- ${visible(row)}${RESET}`));
|
|
113
|
+
rowsAfter.forEach((row) => lines.push(` + ${visible(row)}`));
|
|
114
|
+
if (JSON.stringify(rowsBefore) === JSON.stringify(rowsAfter)) {
|
|
115
|
+
lines.push(` ${DIM}(same widgets, different options)${RESET}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Exec commands the import would add: these run on every refresh.
|
|
122
|
+
function newCommands(before, after) {
|
|
123
|
+
const known = new Set(
|
|
124
|
+
before.widgets.filter((widget) => widget.id === "exec").map((widget) => String(widget.cmd))
|
|
125
|
+
);
|
|
126
|
+
return after.widgets
|
|
127
|
+
.filter((widget) => widget.id === "exec" && String(widget.cmd || "").trim())
|
|
128
|
+
.map((widget) => String(widget.cmd))
|
|
129
|
+
.filter((cmd) => !known.has(cmd));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fail(message, hint) {
|
|
133
|
+
console.error(`${RED}error${RESET} ${message}`);
|
|
134
|
+
if (hint) {
|
|
135
|
+
console.error(` ${hint}`);
|
|
136
|
+
}
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function importCommand(file, options = {}) {
|
|
141
|
+
if (!file) {
|
|
142
|
+
fail("Name the file to import.", "Usage: npx agy-powerline import <file> [--merge] [--yes]");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const installDir = resolveInstallDir();
|
|
147
|
+
if (!installDir) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setConfigDir(installDir);
|
|
151
|
+
// A broken config is never replaced: the user can still fix it.
|
|
152
|
+
const current = loadEditableConfig();
|
|
153
|
+
if (!current) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { target, data, error } = readImportFile(file);
|
|
158
|
+
if (error) {
|
|
159
|
+
fail(`Can't import ${target}: ${error}. Nothing was changed.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { agyPowerline, ...raw } = data;
|
|
164
|
+
const fileMajor = majorOf(agyPowerline?.version);
|
|
165
|
+
if (fileMajor !== null && fileMajor > majorOf(version)) {
|
|
166
|
+
fail(
|
|
167
|
+
`${target} comes from agy-powerline ${visible(agyPowerline.version)}, newer than this one (${version}). Nothing was changed.`,
|
|
168
|
+
"Update first: npx agy-powerline@latest import <file>"
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const merge = Boolean(options.merge);
|
|
174
|
+
const next = mergeConfig(merge ? { ...current, ...raw } : raw);
|
|
175
|
+
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(`${BOLD} Import ${RESET}${DIM}${target}${RESET} ${merge ? "(merge)" : "(replace)"}`);
|
|
178
|
+
const dropped = droppedSettings(raw);
|
|
179
|
+
if (dropped.length) {
|
|
180
|
+
console.log(`${YELLOW} Left out${RESET} (this version can't use them):`);
|
|
181
|
+
dropped.forEach((item) => console.log(` ${item}`));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const diff = changes(current, next);
|
|
185
|
+
if (!diff.length) {
|
|
186
|
+
console.log(" Nothing to change: the file matches your config.");
|
|
187
|
+
console.log();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.log(" Changes:");
|
|
191
|
+
diff.forEach((line) => console.log(line));
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(" Preview:");
|
|
194
|
+
console.log(renderStatusline(sampleData(), next));
|
|
195
|
+
console.log();
|
|
196
|
+
|
|
197
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
198
|
+
const added = newCommands(current, next);
|
|
199
|
+
const rl =
|
|
200
|
+
interactive && (!options.yes || (added.length && !options["allow-exec"])) ? createRl() : null;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
if (added.length) {
|
|
204
|
+
console.log(
|
|
205
|
+
`${YELLOW} The file adds ${added.length} shell command${added.length === 1 ? "" : "s"}${RESET}, run on every refresh:`
|
|
206
|
+
);
|
|
207
|
+
added.forEach((cmd) => console.log(` ${CYAN}${visible(cmd)}${RESET}`));
|
|
208
|
+
console.log();
|
|
209
|
+
if (!options["allow-exec"]) {
|
|
210
|
+
if (!rl) {
|
|
211
|
+
fail(
|
|
212
|
+
"The file adds shell commands. Nothing was changed.",
|
|
213
|
+
"Check them above, then pass --allow-exec to import them."
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!(await confirm(rl, "Run these commands in your bar?", false))) {
|
|
218
|
+
console.log(" Nothing was changed.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!options.yes) {
|
|
225
|
+
if (!rl) {
|
|
226
|
+
fail("No TTY. Pass --yes to import non-interactively. Nothing was changed.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!(await confirm(rl, "Apply this config?", false))) {
|
|
230
|
+
console.log(" Nothing was changed.");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
rl?.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// One import can be undone by importing the backup.
|
|
239
|
+
const configPath = getConfigPath();
|
|
240
|
+
const backup = `${configPath}${BACKUP_SUFFIX}`;
|
|
241
|
+
let backedUp = false;
|
|
242
|
+
try {
|
|
243
|
+
const previous = fs.readFileSync(configPath, "utf8");
|
|
244
|
+
writeFileAtomic(backup, previous, { mode: PRIVATE_FILE_MODE });
|
|
245
|
+
backedUp = true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err.code !== "ENOENT") {
|
|
248
|
+
fail(`Can't back up ${configPath} (${err.code || "error"}). Nothing was changed.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
saveConfig(next);
|
|
254
|
+
|
|
255
|
+
console.log(`${GREEN}✔${RESET} Config imported into ${DIM}${configPath}${RESET}`);
|
|
256
|
+
if (backedUp) {
|
|
257
|
+
console.log(
|
|
258
|
+
` previous config ${DIM}${backup}${RESET} (undo: npx agy-powerline import "${backup}")`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
console.log(" The bar shows it on its next refresh.");
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
importCommand,
|
|
267
|
+
readImportFile,
|
|
268
|
+
droppedSettings,
|
|
269
|
+
newCommands,
|
|
270
|
+
visible,
|
|
271
|
+
MAX_IMPORT_BYTES,
|
|
272
|
+
BACKUP_SUFFIX,
|
|
273
|
+
};
|
package/src/cli/init.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const path = require("path");
|
|
3
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
defaultConfig,
|
|
7
|
+
saveConfig,
|
|
8
|
+
setConfigDir,
|
|
9
|
+
readConfigFile,
|
|
10
|
+
applyPreset,
|
|
11
|
+
renderStatusline,
|
|
12
|
+
sampleData,
|
|
13
|
+
resolveColorDepth,
|
|
14
|
+
PRESETS,
|
|
15
|
+
PRESET_ORDER,
|
|
16
|
+
LAYOUTS,
|
|
17
|
+
LAYOUT_ORDER,
|
|
18
|
+
THEMES,
|
|
19
|
+
THEME_ORDER,
|
|
20
|
+
canonicalThemeId,
|
|
21
|
+
} = require("../lib/shared");
|
|
4
22
|
const {
|
|
5
23
|
AGY_SETTINGS,
|
|
6
24
|
RUNTIME_FILES,
|
|
@@ -17,8 +35,10 @@ const {
|
|
|
17
35
|
cmdBreakingChars,
|
|
18
36
|
cmdBreakingMessage,
|
|
19
37
|
} = require("../lib/paths");
|
|
20
|
-
const { createRl, confirm } = require("../lib/prompt");
|
|
38
|
+
const { createRl, confirm, choose } = require("../lib/prompt");
|
|
39
|
+
const { printInvalidConfig } = require("./config-guard");
|
|
21
40
|
const { buildRuntimeBundle } = require("./bundle");
|
|
41
|
+
const { REPO_URL } = require("./repo-url");
|
|
22
42
|
|
|
23
43
|
const DIM = "\x1b[2m";
|
|
24
44
|
const BOLD = "\x1b[1m";
|
|
@@ -58,6 +78,117 @@ function assertUnixNode() {
|
|
|
58
78
|
process.exit(1);
|
|
59
79
|
}
|
|
60
80
|
|
|
81
|
+
function unknownChoice(kind, value, ids) {
|
|
82
|
+
console.error(`${RED}error${RESET} Unknown ${kind} "${value}".`);
|
|
83
|
+
console.error(` Available ${kind}s: ${ids.join(", ")}`);
|
|
84
|
+
console.error(` Run ${CYAN}npx agy-powerline ${kind} --list${RESET} to view details.`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --preset, --layout and --theme, checked before anything is written. Returns
|
|
90
|
+
// the ids given ({} for none), or null after printing what's wrong.
|
|
91
|
+
function readChoiceFlags(options) {
|
|
92
|
+
const choices = {};
|
|
93
|
+
if (options.preset !== undefined) {
|
|
94
|
+
if (!PRESETS[options.preset]) {
|
|
95
|
+
return unknownChoice("preset", options.preset, PRESET_ORDER);
|
|
96
|
+
}
|
|
97
|
+
choices.preset = options.preset;
|
|
98
|
+
}
|
|
99
|
+
if (options.layout !== undefined) {
|
|
100
|
+
if (!LAYOUTS[options.layout]) {
|
|
101
|
+
return unknownChoice("layout", options.layout, LAYOUT_ORDER);
|
|
102
|
+
}
|
|
103
|
+
choices.layout = options.layout;
|
|
104
|
+
}
|
|
105
|
+
if (options.theme !== undefined) {
|
|
106
|
+
// Aliases (dark, light, colorblind) name a theme too.
|
|
107
|
+
const theme = canonicalThemeId(options.theme);
|
|
108
|
+
if (!THEMES[theme]) {
|
|
109
|
+
return unknownChoice("theme", options.theme, THEME_ORDER);
|
|
110
|
+
}
|
|
111
|
+
choices.theme = theme;
|
|
112
|
+
}
|
|
113
|
+
return choices;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function applyChoices(config, choices) {
|
|
117
|
+
const next = choices.preset ? applyPreset(config, choices.preset) : { ...config };
|
|
118
|
+
if (choices.layout) {
|
|
119
|
+
next.layout = choices.layout;
|
|
120
|
+
}
|
|
121
|
+
if (choices.theme) {
|
|
122
|
+
next.theme = choices.theme;
|
|
123
|
+
}
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The bar a choice gives, fitted to the terminal's width (less the indent).
|
|
128
|
+
// exec widgets keep their [cmd] placeholder: nothing runs here.
|
|
129
|
+
function preview(config, rows = Infinity) {
|
|
130
|
+
const width = Math.max(40, (process.stdout.columns || 80) - 6);
|
|
131
|
+
const color = resolveColorDepth({ configColor: config.color, env: process.env });
|
|
132
|
+
return renderStatusline({ ...sampleData(), terminal_width: width }, config, { color })
|
|
133
|
+
.split("\n")
|
|
134
|
+
.slice(0, rows)
|
|
135
|
+
.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const BACKGROUNDS = [
|
|
139
|
+
["dark", "dark"],
|
|
140
|
+
["light", "light"],
|
|
141
|
+
["colorblind", "color-blind"],
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// The questions of a first install in a terminal, for what the flags left open.
|
|
145
|
+
async function askChoices(rl, given) {
|
|
146
|
+
const choices = { ...given };
|
|
147
|
+
const base = defaultConfig();
|
|
148
|
+
const option = (id, description, config, rows) =>
|
|
149
|
+
`${id}${description ? ` ${DIM}${description}${RESET}` : ""}\n${preview(config, rows)}`;
|
|
150
|
+
|
|
151
|
+
if (!choices.preset) {
|
|
152
|
+
const index = await choose(
|
|
153
|
+
rl,
|
|
154
|
+
`\n${BOLD} Which widgets?${RESET}`,
|
|
155
|
+
PRESET_ORDER.map((id) =>
|
|
156
|
+
option(id, PRESETS[id].description, applyChoices(base, { ...choices, preset: id }))
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
choices.preset = PRESET_ORDER[index];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!choices.layout) {
|
|
163
|
+
const index = await choose(
|
|
164
|
+
rl,
|
|
165
|
+
`\n${BOLD} Which layout?${RESET}`,
|
|
166
|
+
LAYOUT_ORDER.map((id) =>
|
|
167
|
+
option(id, LAYOUTS[id].description, applyChoices(base, { ...choices, layout: id }))
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
choices.layout = LAYOUT_ORDER[index];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!choices.theme) {
|
|
174
|
+
const background = await choose(
|
|
175
|
+
rl,
|
|
176
|
+
`\n${BOLD} Your terminal's background?${RESET}`,
|
|
177
|
+
BACKGROUNDS.map(([, label]) => label)
|
|
178
|
+
);
|
|
179
|
+
const kind = BACKGROUNDS[background][0];
|
|
180
|
+
const ids = THEME_ORDER.filter((id) => THEMES[id]?.kind === kind);
|
|
181
|
+
const index = await choose(
|
|
182
|
+
rl,
|
|
183
|
+
`\n${BOLD} Which theme?${RESET}`,
|
|
184
|
+
ids.map((id) => option(id, "", applyChoices(base, { ...choices, theme: id }), 1))
|
|
185
|
+
);
|
|
186
|
+
choices.theme = ids[index];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return choices;
|
|
190
|
+
}
|
|
191
|
+
|
|
61
192
|
function reportUnreadableSettings(error, command) {
|
|
62
193
|
console.error(`${RED}error${RESET} ${AGY_SETTINGS} is not valid JSON, so it was left untouched.`);
|
|
63
194
|
console.error(` ${error}`);
|
|
@@ -74,12 +205,33 @@ async function initCommand(options = {}) {
|
|
|
74
205
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
75
206
|
const installDir = resolveInstallDir();
|
|
76
207
|
const command = statuslineCommand(installDir);
|
|
208
|
+
const destConfig = path.join(installDir, "statusline.config.json");
|
|
77
209
|
|
|
78
210
|
console.log();
|
|
79
211
|
console.log(`${CYAN}${BOLD} agy-powerline${RESET}`);
|
|
80
212
|
console.log(`${DIM} Antigravity statusline installer${RESET}`);
|
|
81
213
|
console.log();
|
|
82
214
|
|
|
215
|
+
let choices = readChoiceFlags(options);
|
|
216
|
+
if (!choices) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Re-running init never replaces a config; flags change only their settings
|
|
221
|
+
// in it, and a config with a JSON error isn't touched at all.
|
|
222
|
+
const firstInstall = !fs.existsSync(destConfig);
|
|
223
|
+
let existingConfig = null;
|
|
224
|
+
if (!firstInstall && Object.keys(choices).length) {
|
|
225
|
+
setConfigDir(installDir);
|
|
226
|
+
const existing = readConfigFile();
|
|
227
|
+
if (existing.error) {
|
|
228
|
+
printInvalidConfig(existing.error);
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
existingConfig = existing.config;
|
|
233
|
+
}
|
|
234
|
+
|
|
83
235
|
// Validate settings.json before writing anything, so a file we cannot
|
|
84
236
|
// safely update never ends up half-installed or overwritten.
|
|
85
237
|
const current = readSettingsForUpdate();
|
|
@@ -101,11 +253,18 @@ async function initCommand(options = {}) {
|
|
|
101
253
|
const rl = interactive && !yes ? createRl() : null;
|
|
102
254
|
|
|
103
255
|
try {
|
|
256
|
+
if (firstInstall && rl) {
|
|
257
|
+
choices = await askChoices(rl, choices);
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(` ${BOLD}${choices.preset} · ${choices.layout} · ${choices.theme}${RESET}`);
|
|
260
|
+
console.log(preview(applyChoices(defaultConfig(), choices)));
|
|
261
|
+
console.log();
|
|
262
|
+
}
|
|
263
|
+
|
|
104
264
|
fs.mkdirSync(installDir, { recursive: true });
|
|
105
265
|
restrictPrivateFiles(installDir);
|
|
106
266
|
|
|
107
267
|
const destStatusline = path.join(installDir, "statusline.js");
|
|
108
|
-
const destConfig = path.join(installDir, "statusline.config.json");
|
|
109
268
|
|
|
110
269
|
let writeScripts = true;
|
|
111
270
|
|
|
@@ -144,9 +303,11 @@ async function initCommand(options = {}) {
|
|
|
144
303
|
|
|
145
304
|
writeInstallMeta(installDir);
|
|
146
305
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
saveConfig(defaultConfig());
|
|
306
|
+
setConfigDir(installDir);
|
|
307
|
+
if (firstInstall) {
|
|
308
|
+
saveConfig(applyChoices(defaultConfig(), choices));
|
|
309
|
+
} else if (existingConfig) {
|
|
310
|
+
saveConfig(applyChoices(existingConfig, choices));
|
|
150
311
|
}
|
|
151
312
|
|
|
152
313
|
const statusLine = await updateAgySettings(command, current, {
|
|
@@ -166,12 +327,24 @@ async function initCommand(options = {}) {
|
|
|
166
327
|
if (typeof statusLine?.padding === "number") {
|
|
167
328
|
console.log(` padding ${DIM}${statusLine.padding} line(s)${RESET}`);
|
|
168
329
|
}
|
|
330
|
+
if (existingConfig) {
|
|
331
|
+
const changed = Object.entries(choices).map(([key, value]) => `${key} ${value}`);
|
|
332
|
+
console.log(` config ${DIM}kept yours, with ${changed.join(", ")}${RESET}`);
|
|
333
|
+
} else if (!firstInstall) {
|
|
334
|
+
console.log(` config ${DIM}kept yours${RESET}`);
|
|
335
|
+
}
|
|
169
336
|
console.log();
|
|
170
337
|
console.log(" Next:");
|
|
171
338
|
console.log(`${CYAN} npx agy-powerline configure${RESET}`);
|
|
339
|
+
console.log(
|
|
340
|
+
`${CYAN} npx agy-powerline preset${RESET} / ${CYAN}layout${RESET} / ${CYAN}theme${RESET} ${DIM}change one of your choices${RESET}`
|
|
341
|
+
);
|
|
172
342
|
console.log(`${CYAN} npx agy-powerline doctor${RESET}`);
|
|
173
343
|
console.log(" then restart Antigravity / open a new session.");
|
|
174
344
|
console.log();
|
|
345
|
+
console.log(`${DIM} Like it? A star on GitHub helps others find it:${RESET}`);
|
|
346
|
+
console.log(`${DIM} ${REPO_URL}${RESET}`);
|
|
347
|
+
console.log();
|
|
175
348
|
} finally {
|
|
176
349
|
rl?.close();
|
|
177
350
|
}
|
|
@@ -222,7 +395,7 @@ async function updateAgySettings(command, { exists, settings }, { yes, rl, stack
|
|
|
222
395
|
statusLine.padding = settings.statusLine.padding;
|
|
223
396
|
}
|
|
224
397
|
|
|
225
|
-
writeJsonSafely(AGY_SETTINGS, { ...settings, statusLine });
|
|
398
|
+
const originalBackup = writeJsonSafely(AGY_SETTINGS, { ...settings, statusLine });
|
|
226
399
|
|
|
227
400
|
console.log(`${DIM} ${exists ? "updated" : "created"} ${AGY_SETTINGS}${RESET}`);
|
|
228
401
|
// init means "show the bar", so a statusLine switched off with enabled: false
|
|
@@ -233,10 +406,16 @@ async function updateAgySettings(command, { exists, settings }, { yes, rl, stack
|
|
|
233
406
|
if (exists) {
|
|
234
407
|
console.log(`${DIM} backup ${settingsBackupPath(AGY_SETTINGS)}${RESET}`);
|
|
235
408
|
}
|
|
409
|
+
if (originalBackup) {
|
|
410
|
+
console.log(
|
|
411
|
+
`${DIM} original ${originalBackup} (kept as it was before agy-powerline)${RESET}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
236
414
|
|
|
237
415
|
return statusLine;
|
|
238
416
|
}
|
|
239
417
|
|
|
240
418
|
module.exports = {
|
|
241
419
|
initCommand,
|
|
420
|
+
askChoices,
|
|
242
421
|
};
|
package/src/cli/layout.js
CHANGED
package/src/cli/preset.js
CHANGED
package/src/cli/theme.js
CHANGED
package/src/cli/uninstall.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const path = require("path");
|
|
3
5
|
const {
|
|
@@ -66,8 +68,10 @@ function removeStatusLine(settings) {
|
|
|
66
68
|
delete next.statusLine;
|
|
67
69
|
|
|
68
70
|
if (Object.keys(next).length === 0) {
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
+
// A symlinked settings.json is emptied rather than deleted (see paths.js).
|
|
72
|
+
return removeSettingsFile(AGY_SETTINGS) === "emptied"
|
|
73
|
+
? "settings file was empty, emptied (it is a symlink)"
|
|
74
|
+
: "settings file was empty, deleted";
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
writeJsonSafely(AGY_SETTINGS, next);
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
2
3
|
|
|
3
4
|
const fs = require("fs");
|
|
4
5
|
const path = require("path");
|
|
@@ -9,6 +10,9 @@ const { themeCommand } = require("./cli/theme");
|
|
|
9
10
|
const { layoutCommand } = require("./cli/layout");
|
|
10
11
|
const { presetCommand } = require("./cli/preset");
|
|
11
12
|
const { capsCommand } = require("./cli/caps");
|
|
13
|
+
const { gradientCommand } = require("./cli/gradient");
|
|
14
|
+
const { exportCommand } = require("./cli/export");
|
|
15
|
+
const { importCommand } = require("./cli/import");
|
|
12
16
|
const { doctorCommand } = require("./cli/doctor");
|
|
13
17
|
const { uninstallCommand } = require("./cli/uninstall");
|
|
14
18
|
const { reportCommand } = require("./cli/report");
|
|
@@ -46,6 +50,17 @@ const COMMANDS = [
|
|
|
46
50
|
value: "<lines>",
|
|
47
51
|
help: "set padding blank lines above statusline",
|
|
48
52
|
},
|
|
53
|
+
preset: {
|
|
54
|
+
type: "string",
|
|
55
|
+
value: "<id>",
|
|
56
|
+
help: "start from this widget preset (skips the question)",
|
|
57
|
+
},
|
|
58
|
+
layout: { type: "string", value: "<id>", help: "use this layout (skips the question)" },
|
|
59
|
+
theme: {
|
|
60
|
+
type: "string",
|
|
61
|
+
value: "<id>",
|
|
62
|
+
help: "use this theme, or dark, light, colorblind (skips the question)",
|
|
63
|
+
},
|
|
49
64
|
},
|
|
50
65
|
run: (_arg, opts) =>
|
|
51
66
|
initCommand({
|
|
@@ -103,6 +118,42 @@ const COMMANDS = [
|
|
|
103
118
|
},
|
|
104
119
|
run: (arg, opts) => capsCommand(arg, opts),
|
|
105
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: "gradient",
|
|
123
|
+
arg: "[name]",
|
|
124
|
+
description:
|
|
125
|
+
"View or set a gradient across each row (a preset, hex colors like #ff5f6d-#ffc371, or off)",
|
|
126
|
+
options: {
|
|
127
|
+
list: { type: "boolean", short: "l", help: "list all gradient presets with a preview" },
|
|
128
|
+
},
|
|
129
|
+
run: (arg, opts) => gradientCommand(arg, opts),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "export",
|
|
133
|
+
arg: "[file]",
|
|
134
|
+
description: "Write your config to a file (or to stdout without one) to back it up or share it",
|
|
135
|
+
options: {
|
|
136
|
+
force: { type: "boolean", help: "replace the file if it already exists" },
|
|
137
|
+
},
|
|
138
|
+
run: (arg, opts) => exportCommand(arg, opts),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "import",
|
|
142
|
+
arg: "<file>",
|
|
143
|
+
description: "Load a config from a file, after showing what changes",
|
|
144
|
+
options: {
|
|
145
|
+
merge: {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
help: "change only the settings the file has (default: replace the whole config)",
|
|
148
|
+
},
|
|
149
|
+
yes: { type: "boolean", short: "y", help: "apply without asking" },
|
|
150
|
+
"allow-exec": {
|
|
151
|
+
type: "boolean",
|
|
152
|
+
help: "accept the shell commands (exec widgets) the file adds; --yes alone doesn't",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
run: (arg, opts) => importCommand(arg, opts),
|
|
156
|
+
},
|
|
106
157
|
{
|
|
107
158
|
name: "doctor",
|
|
108
159
|
description:
|