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
package/src/lib/paths.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const os = require("os");
|
|
3
5
|
const path = require("path");
|
|
@@ -25,6 +27,7 @@ const RUNTIME_FILES = [
|
|
|
25
27
|
"fit.js",
|
|
26
28
|
"log.js",
|
|
27
29
|
"payload-dump.js",
|
|
30
|
+
"usage.js",
|
|
28
31
|
path.join("layouts", "lines.js"),
|
|
29
32
|
path.join("layouts", "pills.js"),
|
|
30
33
|
];
|
|
@@ -34,10 +37,17 @@ const RUNTIME_FILES = [
|
|
|
34
37
|
// machine can't read them (Windows profile folders are private already).
|
|
35
38
|
const PRIVATE_FILES = [
|
|
36
39
|
"statusline.config.json",
|
|
40
|
+
// The config before the last import (BACKUP_SUFFIX in cli/import.js).
|
|
41
|
+
"statusline.config.json.bak",
|
|
37
42
|
".exec-cache.json",
|
|
38
43
|
"last-payload.json",
|
|
39
44
|
// SYNC_CACHE_NAME in git.js: commit IDs of the repos you work in.
|
|
40
45
|
".git-sync-cache.json",
|
|
46
|
+
// STATUS_CACHE_NAME in git.js: repo paths with their file and line counts.
|
|
47
|
+
".git-status-cache.json",
|
|
48
|
+
// USAGE_CACHE_NAME in usage.js: tokens per day, keyed by a hash of each
|
|
49
|
+
// conversation database's path.
|
|
50
|
+
".usage-cache.json",
|
|
41
51
|
// LOG_NAME in log.js, spelled out because log.js requires this file.
|
|
42
52
|
"agy-powerline.log",
|
|
43
53
|
];
|
|
@@ -238,15 +248,54 @@ function settingsBackupPath(file = AGY_SETTINGS) {
|
|
|
238
248
|
return `${file}.agy-powerline.bak`;
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
function settingsOriginalBackupPath(file = AGY_SETTINGS) {
|
|
252
|
+
return `${file}.agy-powerline.original.bak`;
|
|
253
|
+
}
|
|
244
254
|
|
|
245
|
-
|
|
246
|
-
|
|
255
|
+
// Two copies before every change: the latest file, replaced each time, and
|
|
256
|
+
// the file as it was before agy-powerline first changed it, written once and
|
|
257
|
+
// never replaced, so running init again can't lose the user's own version.
|
|
258
|
+
// Returns the original backup's path when this call created it.
|
|
259
|
+
function backUpSettings(file) {
|
|
260
|
+
if (!fs.existsSync(file)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const original = settingsOriginalBackupPath(file);
|
|
264
|
+
const created = !fs.existsSync(original);
|
|
265
|
+
if (created) {
|
|
266
|
+
fs.copyFileSync(file, original);
|
|
247
267
|
}
|
|
268
|
+
fs.copyFileSync(file, settingsBackupPath(file));
|
|
269
|
+
return created ? original : null;
|
|
270
|
+
}
|
|
248
271
|
|
|
272
|
+
// Backs up the current file, then writes atomically. Returns what
|
|
273
|
+
// backUpSettings() returns.
|
|
274
|
+
function writeJsonSafely(file, data) {
|
|
275
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
276
|
+
const originalBackup = backUpSettings(file);
|
|
249
277
|
writeFileAtomic(file, JSON.stringify(data, null, 2) + "\n");
|
|
278
|
+
return originalBackup;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Dotfile managers (stow, chezmoi, yadm) link files such as settings.json
|
|
282
|
+
// into place. A write goes through the link to the file it points to, so the
|
|
283
|
+
// link stays a link and the user's real file gets the change; a rename onto
|
|
284
|
+
// the link itself would replace it with a plain file.
|
|
285
|
+
function writeTarget(file) {
|
|
286
|
+
try {
|
|
287
|
+
return fs.realpathSync(file);
|
|
288
|
+
} catch {
|
|
289
|
+
// Missing file, or a link whose target doesn't exist yet: follow it once.
|
|
290
|
+
try {
|
|
291
|
+
if (fs.lstatSync(file).isSymbolicLink()) {
|
|
292
|
+
return path.resolve(path.dirname(file), fs.readlinkSync(file));
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Nothing at that path.
|
|
296
|
+
}
|
|
297
|
+
return file;
|
|
298
|
+
}
|
|
250
299
|
}
|
|
251
300
|
|
|
252
301
|
// Writes via temp file + rename so an interrupted write cannot leave a
|
|
@@ -254,9 +303,10 @@ function writeJsonSafely(file, data) {
|
|
|
254
303
|
// mode the existing file had, so rewriting a file never widens who can read
|
|
255
304
|
// it (the rename would otherwise reset it to the default).
|
|
256
305
|
function writeFileAtomic(file, body, { mode } = {}) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
306
|
+
const dest = writeTarget(file);
|
|
307
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
308
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
309
|
+
const target = process.platform === "win32" ? undefined : (mode ?? existingMode(dest));
|
|
260
310
|
fs.writeFileSync(tmp, body, { encoding: "utf8", mode: target ?? 0o666 });
|
|
261
311
|
if (target !== undefined) {
|
|
262
312
|
// The mode passed to writeFileSync is reduced by the umask; set it exactly.
|
|
@@ -270,10 +320,10 @@ function writeFileAtomic(file, body, { mode } = {}) {
|
|
|
270
320
|
}
|
|
271
321
|
|
|
272
322
|
try {
|
|
273
|
-
fs.renameSync(tmp,
|
|
323
|
+
fs.renameSync(tmp, dest);
|
|
274
324
|
} catch {
|
|
275
325
|
// Windows can refuse to replace a file another process holds open.
|
|
276
|
-
fs.writeFileSync(
|
|
326
|
+
fs.writeFileSync(dest, body, "utf8");
|
|
277
327
|
fs.rmSync(tmp, { force: true });
|
|
278
328
|
}
|
|
279
329
|
}
|
|
@@ -301,11 +351,20 @@ function restrictPrivateFiles(installDir) {
|
|
|
301
351
|
}
|
|
302
352
|
}
|
|
303
353
|
|
|
354
|
+
// Removes a settings.json that would be left empty. A symlinked one is emptied
|
|
355
|
+
// instead: deleting the link would break the user's dotfiles and leave the
|
|
356
|
+
// real file unchanged. Returns "deleted", "emptied" or null (nothing there).
|
|
304
357
|
function removeSettingsFile(file = AGY_SETTINGS) {
|
|
305
|
-
if (fs.existsSync(file)) {
|
|
306
|
-
|
|
307
|
-
|
|
358
|
+
if (!fs.existsSync(file)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
backUpSettings(file);
|
|
362
|
+
if (fs.lstatSync(file).isSymbolicLink()) {
|
|
363
|
+
writeFileAtomic(file, "{}\n");
|
|
364
|
+
return "emptied";
|
|
308
365
|
}
|
|
366
|
+
fs.rmSync(file, { force: true });
|
|
367
|
+
return "deleted";
|
|
309
368
|
}
|
|
310
369
|
|
|
311
370
|
// Antigravity runs the Windows launcher through `cmd /c`, which splits the
|
|
@@ -368,6 +427,7 @@ module.exports = {
|
|
|
368
427
|
readAgySettings,
|
|
369
428
|
readSettingsForUpdate,
|
|
370
429
|
settingsBackupPath,
|
|
430
|
+
settingsOriginalBackupPath,
|
|
371
431
|
writeJsonSafely,
|
|
372
432
|
writeFileAtomic,
|
|
373
433
|
removeSettingsFile,
|
package/src/lib/prompt.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const readline = require("readline");
|
|
2
4
|
|
|
3
5
|
function createRl() {
|
|
@@ -24,8 +26,36 @@ async function confirm(rl, message, defaultYes) {
|
|
|
24
26
|
return answer === "y" || answer === "yes";
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// Numbered options, answered with a number; Enter keeps the default. Anything
|
|
30
|
+
// else asks again, at most 3 times, then the default stands. No raw mode or
|
|
31
|
+
// arrow keys, so it works in every terminal and in cmd. An option can span
|
|
32
|
+
// several lines (a label and its preview). Returns the chosen index.
|
|
33
|
+
async function choose(rl, message, options, defaultIndex = 0, print = console.log) {
|
|
34
|
+
print(message);
|
|
35
|
+
options.forEach((option, i) => {
|
|
36
|
+
const [first, ...rest] = String(option).split("\n");
|
|
37
|
+
print(` ${String(i + 1).padStart(2)}) ${first}`);
|
|
38
|
+
rest.forEach((line) => print(` ${line}`));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
42
|
+
const answer = (
|
|
43
|
+
await question(rl, ` Choose 1-${options.length} (Enter: ${defaultIndex + 1}) `)
|
|
44
|
+
).trim();
|
|
45
|
+
if (!answer) {
|
|
46
|
+
return defaultIndex;
|
|
47
|
+
}
|
|
48
|
+
const index = /^\d{1,3}$/.test(answer) ? Number(answer) - 1 : -1;
|
|
49
|
+
if (index >= 0 && index < options.length) {
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return defaultIndex;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
module.exports = {
|
|
28
57
|
createRl,
|
|
29
58
|
question,
|
|
30
59
|
confirm,
|
|
60
|
+
choose,
|
|
31
61
|
};
|
package/src/lib/run.js
CHANGED
package/src/lib/session.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const { formatReset, resetInSeconds, firstText } = require("./formatters");
|
|
5
|
-
const { resolveGit } = require("./git");
|
|
5
|
+
const { resolveGit, resolveGitDetails } = require("./git");
|
|
6
|
+
const { resolveUsage } = require("./usage");
|
|
6
7
|
|
|
7
8
|
const FIVE_HOURS = 5 * 3600;
|
|
8
9
|
const SEVEN_DAYS = 7 * 86400;
|
|
@@ -144,7 +145,17 @@ function contextUsedPercent(data) {
|
|
|
144
145
|
return (current * 100) / size;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
// options.readUsage: the statusline itself may read agy's conversation
|
|
149
|
+
// databases; previews only use the usage cache.
|
|
150
|
+
function extractSession(data, config, options = {}) {
|
|
151
|
+
const git = resolveGit(data, config);
|
|
152
|
+
const gitDetails = resolveGitDetails(data, config);
|
|
153
|
+
// Once git status has been read for gitFiles, the branch's dirty marker can
|
|
154
|
+
// count uncommitted files too, not only an unfinished merge or rebase.
|
|
155
|
+
const files = gitDetails?.files;
|
|
156
|
+
if (git && files && gitDetails.branch === git.branch) {
|
|
157
|
+
git.dirty = git.dirty || files.staged + files.modified + files.conflicts > 0;
|
|
158
|
+
}
|
|
148
159
|
const contextPct = percent(contextUsedPercent(data));
|
|
149
160
|
const contextSize = count(data.context_window?.context_window_size);
|
|
150
161
|
const current = data.context_window?.current_usage;
|
|
@@ -224,7 +235,10 @@ function extractSession(data, config) {
|
|
|
224
235
|
version: firstText(data.version),
|
|
225
236
|
sandbox: data.sandbox?.enabled === true,
|
|
226
237
|
sandboxNet: data.sandbox?.allow_network === true,
|
|
227
|
-
git
|
|
238
|
+
git,
|
|
239
|
+
gitDetails,
|
|
240
|
+
// Only read when tokensToday or tokensWeek is on the bar.
|
|
241
|
+
usage: resolveUsage(config, { read: options.readUsage === true }),
|
|
228
242
|
terminalWidth: terminalWidth(data.terminal_width ?? process.stdout.columns ?? 0),
|
|
229
243
|
};
|
|
230
244
|
}
|
package/src/lib/shared.js
CHANGED
|
@@ -54,6 +54,7 @@ const {
|
|
|
54
54
|
} = require("./formatters");
|
|
55
55
|
|
|
56
56
|
const { extractSession } = require("./session");
|
|
57
|
+
const { execPayloadJson } = require("./exec");
|
|
57
58
|
const { fitWidgets, truncateLines, widestLine } = require("./fit");
|
|
58
59
|
|
|
59
60
|
const {
|
|
@@ -92,12 +93,21 @@ function renderBar(data, config, overrides) {
|
|
|
92
93
|
// sequences from every string before any layout prints it.
|
|
93
94
|
// Valid JSON can still be null, a number or an array; read those as empty.
|
|
94
95
|
const payload = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
95
|
-
const session = sanitizeDeep(
|
|
96
|
+
const session = sanitizeDeep(
|
|
97
|
+
extractSession(payload, resolved, { readUsage: overrides.usage === "read" })
|
|
98
|
+
);
|
|
96
99
|
// Exec widgets only run when the caller opts in ("run"); CLI previews keep
|
|
97
100
|
// the default placeholder so rendering a config never executes it.
|
|
98
101
|
session.execMode =
|
|
99
102
|
overrides.exec === "run" || overrides.exec === "blocked" ? overrides.exec : "preview";
|
|
100
103
|
session.execMemo = new Map();
|
|
104
|
+
// Built only when a command will run and asked for it.
|
|
105
|
+
if (
|
|
106
|
+
session.execMode === "run" &&
|
|
107
|
+
resolved.widgets.some((widget) => widget.id === "exec" && widget.payload === true)
|
|
108
|
+
) {
|
|
109
|
+
session.execPayload = execPayloadJson(payload);
|
|
110
|
+
}
|
|
101
111
|
|
|
102
112
|
const prevDepth = getActiveDepth();
|
|
103
113
|
|
package/src/lib/themes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { parseColor } = require("./colors");
|
|
3
|
+
const { parseColor, parseGradientStops, luminance } = require("./colors");
|
|
4
4
|
|
|
5
5
|
// Every theme targets one terminal background:
|
|
6
6
|
// dark-* bright colors, dark pill text, for dark terminals
|
|
@@ -9,10 +9,10 @@ const { parseColor } = require("./colors");
|
|
|
9
9
|
// protanopia, deuteranopia and tritanopia
|
|
10
10
|
// Most dark-* and light-* colors come from VS Code's default themes (Dark+
|
|
11
11
|
// and Light+ syntax, git decorations, warning/error and terminal colors);
|
|
12
|
-
// nocturne, onyx, metro and
|
|
13
|
-
// (credited below). A few colors are adjusted where the
|
|
14
|
-
// it: darkened or lightened for contrast, and gitDirty
|
|
15
|
-
// a dirty branch never looks like the caution state.
|
|
12
|
+
// nocturne, onyx, metro, cream and macchiato take their palettes from
|
|
13
|
+
// community themes (credited below). A few colors are adjusted where the
|
|
14
|
+
// checks below need it: darkened or lightened for contrast, and gitDirty
|
|
15
|
+
// moved toward orange so a dirty branch never looks like the caution state.
|
|
16
16
|
// Palettes are checked by tests/shared/themes.test.js: pill text and lines
|
|
17
17
|
// text reach WCAG AA (4.5:1), the ok / caution / danger states are clearly
|
|
18
18
|
// different from each other, and so are gitDirty and caution.
|
|
@@ -175,6 +175,22 @@ const THEMES = {
|
|
|
175
175
|
warn: "#f7768e",
|
|
176
176
|
}),
|
|
177
177
|
|
|
178
|
+
// Palette from Catppuccin Macchiato (catppuccin.com, MIT).
|
|
179
|
+
"dark-macchiato": theme("dark", "Dark Macchiato", "Pastel mauve, teal, peach and green", {
|
|
180
|
+
text: DARK_TEXT,
|
|
181
|
+
track: "#494d64",
|
|
182
|
+
model: "#c6a0f6",
|
|
183
|
+
fiveHour: "#a6da95",
|
|
184
|
+
weekly: "#7dc4e4",
|
|
185
|
+
thirdParty: "#f5a97f",
|
|
186
|
+
context: "#8aadf4",
|
|
187
|
+
git: "#8bd5ca",
|
|
188
|
+
gitDirty: "#f0c6c6",
|
|
189
|
+
meta: "#b8c0e0",
|
|
190
|
+
caution: "#eed49f",
|
|
191
|
+
warn: "#ed8796",
|
|
192
|
+
}),
|
|
193
|
+
|
|
178
194
|
"light-paper": theme("light", "Light Paper", "VS Code Light+ colors", {
|
|
179
195
|
text: LIGHT_TEXT,
|
|
180
196
|
track: "#c8c8c8",
|
|
@@ -311,6 +327,7 @@ const THEME_ORDER = [
|
|
|
311
327
|
"dark-nocturne",
|
|
312
328
|
"dark-onyx",
|
|
313
329
|
"dark-metro",
|
|
330
|
+
"dark-macchiato",
|
|
314
331
|
"light-paper",
|
|
315
332
|
"light-sky",
|
|
316
333
|
"light-sunset",
|
|
@@ -398,6 +415,57 @@ function themeColors(themeId, customThemeConfig) {
|
|
|
398
415
|
return resolveTheme(themeId, customThemeConfig).colors;
|
|
399
416
|
}
|
|
400
417
|
|
|
418
|
+
// Row gradients. Pill text and Lines text are adjusted per color to stay
|
|
419
|
+
// readable, so a preset works on dark and light terminals alike.
|
|
420
|
+
const GRADIENTS = {
|
|
421
|
+
aurora: { name: "Aurora", stops: ["#00c9a7", "#4facfe", "#a18cd1"] },
|
|
422
|
+
sunset: { name: "Sunset", stops: ["#ffd166", "#ff8c61", "#e8457c"] },
|
|
423
|
+
ocean: { name: "Ocean", stops: ["#2af598", "#009efd"] },
|
|
424
|
+
forest: { name: "Forest", stops: ["#d4fc79", "#3fb68b", "#1d7a5f"] },
|
|
425
|
+
neon: { name: "Neon", stops: ["#f72585", "#7209b7", "#4cc9f0"] },
|
|
426
|
+
candy: { name: "Candy", stops: ["#ff9a9e", "#fbc2eb", "#a1c4fd"] },
|
|
427
|
+
rainbow: { name: "Rainbow", stops: ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"] },
|
|
428
|
+
mono: { name: "Mono", stops: ["#e0e0e0", "#6e6e6e"] },
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const GRADIENT_ORDER = Object.keys(GRADIENTS);
|
|
432
|
+
|
|
433
|
+
const toHex = (rgb) => `#${rgb.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
434
|
+
|
|
435
|
+
// A preset name, or hex stops written as "#rrggbb-#rrggbb"; null when the
|
|
436
|
+
// value is anything else (or "off").
|
|
437
|
+
function normalizeGradient(value) {
|
|
438
|
+
if (typeof value !== "string") {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const key = value.trim().toLowerCase();
|
|
442
|
+
if (GRADIENTS[key]) {
|
|
443
|
+
return key;
|
|
444
|
+
}
|
|
445
|
+
const stops = parseGradientStops(key);
|
|
446
|
+
return stops ? stops.map(toHex).join("-") : null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function gradientStops(value) {
|
|
450
|
+
const key = normalizeGradient(value);
|
|
451
|
+
if (!key) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return GRADIENTS[key]
|
|
455
|
+
? GRADIENTS[key].stops.map((stop) => parseColor(stop))
|
|
456
|
+
: parseGradientStops(key);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Typical terminal backgrounds, a little toward the hard side. Themes for
|
|
460
|
+
// light terminals have light pill text on deep colors; dark themes the reverse.
|
|
461
|
+
const TERMINAL_BACKGROUNDS = { dark: [40, 42, 54], light: [245, 245, 240] };
|
|
462
|
+
|
|
463
|
+
function terminalBackground(colors) {
|
|
464
|
+
return luminance(colors?.text ?? [0, 0, 0]) > 0.5
|
|
465
|
+
? TERMINAL_BACKGROUNDS.light
|
|
466
|
+
: TERMINAL_BACKGROUNDS.dark;
|
|
467
|
+
}
|
|
468
|
+
|
|
401
469
|
module.exports = {
|
|
402
470
|
THEMES,
|
|
403
471
|
THEME_ORDER,
|
|
@@ -411,4 +479,10 @@ module.exports = {
|
|
|
411
479
|
resolveThemeId,
|
|
412
480
|
resolveTheme,
|
|
413
481
|
themeColors,
|
|
482
|
+
GRADIENTS,
|
|
483
|
+
GRADIENT_ORDER,
|
|
484
|
+
normalizeGradient,
|
|
485
|
+
gradientStops,
|
|
486
|
+
TERMINAL_BACKGROUNDS,
|
|
487
|
+
terminalBackground,
|
|
414
488
|
};
|