agy-powerline 1.1.0 → 1.2.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 +25 -0
- package/README.ja.md +148 -0
- package/README.md +6 -3
- package/README.zh-CN.md +148 -0
- package/package.json +11 -3
- package/src/cli/bundle.js +82 -0
- package/src/cli/configure.js +896 -65
- package/src/cli/init.js +24 -9
- package/src/cli/tui-style.js +154 -31
- package/src/lib/catalog.js +73 -4
- package/src/lib/config.js +6 -1
- package/src/lib/fit.js +2 -0
- package/src/lib/git.js +70 -2
- package/src/lib/layouts/lines.js +6 -0
- package/src/lib/layouts/pills.js +42 -0
- package/src/lib/log.js +24 -1
- package/src/lib/mock-data.js +6 -1
- package/src/lib/paths.js +6 -2
- package/src/lib/session.js +44 -8
- package/src/lib/widget-style.js +2 -0
package/src/cli/configure.js
CHANGED
|
@@ -19,8 +19,11 @@ const {
|
|
|
19
19
|
PRESETS,
|
|
20
20
|
PRESET_ORDER,
|
|
21
21
|
applyPreset,
|
|
22
|
+
resolveQuotaThresholds,
|
|
23
|
+
resolveContextThresholds,
|
|
22
24
|
} = require("../lib/shared");
|
|
23
25
|
const { resolveInstallDir } = require("../lib/paths");
|
|
26
|
+
const { WIDGET_TAGS } = require("../lib/widget-style");
|
|
24
27
|
const { loadEditableConfig } = require("./config-guard");
|
|
25
28
|
const {
|
|
26
29
|
parseColor,
|
|
@@ -29,7 +32,8 @@ const {
|
|
|
29
32
|
getActiveDepth,
|
|
30
33
|
resolveColorDepth,
|
|
31
34
|
} = require("../lib/colors");
|
|
32
|
-
const { makeUi } = require("./tui-style");
|
|
35
|
+
const { makeUi, wrapText } = require("./tui-style");
|
|
36
|
+
const { visibleWidth, stripControl } = require("../lib/formatters");
|
|
33
37
|
const {
|
|
34
38
|
STYLES,
|
|
35
39
|
STYLE_ORDER,
|
|
@@ -115,6 +119,10 @@ let searchQuery = "";
|
|
|
115
119
|
let glyphSlot = "separator";
|
|
116
120
|
// null while browsing the glyph list; a string while typing a code point.
|
|
117
121
|
let hexInput = null;
|
|
122
|
+
// The widget being edited (its index in the list), and the text being typed
|
|
123
|
+
// into one of its fields (null when not typing).
|
|
124
|
+
let editIndex = 0;
|
|
125
|
+
let textInput = null;
|
|
118
126
|
|
|
119
127
|
function openUrl(url) {
|
|
120
128
|
const target = String(url || "").trim();
|
|
@@ -277,6 +285,7 @@ const TABS = [
|
|
|
277
285
|
const TAB_OF_SCREEN = {
|
|
278
286
|
main: "main",
|
|
279
287
|
add: "main",
|
|
288
|
+
edit: "main",
|
|
280
289
|
presets: "presets",
|
|
281
290
|
themes: "themes",
|
|
282
291
|
customTheme: "themes",
|
|
@@ -287,13 +296,42 @@ const TAB_OF_SCREEN = {
|
|
|
287
296
|
|
|
288
297
|
const PRESET_ICONS = { default: "🎯", compact: "⚡", workspace: "📂", limits: "🔋", full: "🚀" };
|
|
289
298
|
|
|
290
|
-
// Set by renderFrame so tests can draw a screen at a fixed
|
|
299
|
+
// Set by renderFrame so tests can draw a screen at a fixed size.
|
|
291
300
|
let columnsOverride = null;
|
|
301
|
+
let rowsOverride = null;
|
|
292
302
|
let ui;
|
|
293
303
|
|
|
304
|
+
// Window sizes where the editor changes shape: from MEDIUM columns the
|
|
305
|
+
// descriptions line up in a column; from WIDE the list gets a details panel
|
|
306
|
+
// next to it.
|
|
307
|
+
const MEDIUM = 80;
|
|
308
|
+
const WIDE = 140;
|
|
309
|
+
// A list taller than the window scrolls, but keeps at least this many rows
|
|
310
|
+
// before the editor gives up its spacing and the preview's toggles.
|
|
311
|
+
const MIN_LIST_ROWS = 6;
|
|
312
|
+
// What a short window gives up, one step at a time, so the list keeps rows.
|
|
313
|
+
const COMPACT_STEPS = [
|
|
314
|
+
{ tagline: true, gaps: true, notes: true, chips: true },
|
|
315
|
+
{ tagline: false, gaps: true, notes: true, chips: true },
|
|
316
|
+
{ tagline: false, gaps: false, notes: true, chips: true },
|
|
317
|
+
{ tagline: false, gaps: false, notes: false, chips: true },
|
|
318
|
+
{ tagline: false, gaps: false, notes: false, chips: false },
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// First visible row of a scrolled list, kept between frames so the list only
|
|
322
|
+
// moves when the cursor leaves the window.
|
|
323
|
+
let scrollTop = 0;
|
|
324
|
+
let scrollScreen = null;
|
|
325
|
+
|
|
326
|
+
// The editor fills the window, so the live preview has the room the real bar
|
|
327
|
+
// gets in agy.
|
|
294
328
|
function frameWidth() {
|
|
295
329
|
const columns = columnsOverride || process.stdout.columns || 100;
|
|
296
|
-
return Math.max(50,
|
|
330
|
+
return Math.max(50, columns - 1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function frameHeight() {
|
|
334
|
+
return Math.max(12, rowsOverride || process.stdout.rows || 40);
|
|
297
335
|
}
|
|
298
336
|
|
|
299
337
|
function previewConfig() {
|
|
@@ -324,7 +362,32 @@ function dim(text) {
|
|
|
324
362
|
return ui.muted(text);
|
|
325
363
|
}
|
|
326
364
|
|
|
327
|
-
|
|
365
|
+
// What the preview shows that isn't applied yet: the hovered theme, preset
|
|
366
|
+
// or layout. Empty when the preview is the saved setup.
|
|
367
|
+
function previewPending() {
|
|
368
|
+
if (screen === "themes") {
|
|
369
|
+
const hovered = THEME_ORDER[cursor];
|
|
370
|
+
return hovered && hovered !== config.theme ? "theme" : "";
|
|
371
|
+
}
|
|
372
|
+
if (screen === "presets") {
|
|
373
|
+
const hovered = PRESET_ORDER[cursor];
|
|
374
|
+
return hovered && hovered !== currentPreset() ? "preset" : "";
|
|
375
|
+
}
|
|
376
|
+
if (screen === "layout") {
|
|
377
|
+
const hovered = LAYOUT_ORDER[cursor];
|
|
378
|
+
return hovered && hovered !== config.layout ? "layout" : "";
|
|
379
|
+
}
|
|
380
|
+
return "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// The preset whose widgets are exactly the current ones, if any.
|
|
384
|
+
function currentPreset() {
|
|
385
|
+
const mine = JSON.stringify(config.widgets);
|
|
386
|
+
return PRESET_ORDER.find((id) => JSON.stringify(applyPreset(config, id).widgets) === mine);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The bar's rows and the toggle chips under them.
|
|
390
|
+
function previewParts(width) {
|
|
328
391
|
const preview = previewConfig();
|
|
329
392
|
const { dot } = ui.glyph;
|
|
330
393
|
previewData.terminal_width = width - 4;
|
|
@@ -339,7 +402,9 @@ function previewPanel(width) {
|
|
|
339
402
|
const layout = LAYOUTS[preview.layout]?.name ?? preview.layout;
|
|
340
403
|
const style =
|
|
341
404
|
preview.layout === "pills" ? ` ${dot} ${detectStyle(preview.powerline) ?? "custom"}` : "";
|
|
342
|
-
const
|
|
405
|
+
const pending = previewPending();
|
|
406
|
+
const note = pending ? ` ${ui.accentText(`${dot} ${pending} preview, enter to apply`)}` : "";
|
|
407
|
+
const title = `Live preview ${dim(`${dot} ${layout} ${dot} ${preview.theme}${style}`)}${note}`;
|
|
343
408
|
// The toggle keys work on the widget list, so only that screen names them.
|
|
344
409
|
const key = (letter) => (screen === "main" ? letter : "");
|
|
345
410
|
const chips = [
|
|
@@ -356,31 +421,271 @@ function previewPanel(width) {
|
|
|
356
421
|
);
|
|
357
422
|
}
|
|
358
423
|
|
|
359
|
-
return
|
|
424
|
+
return { title, bar: String(rendered).split("\n"), chips: chips.join(" ") };
|
|
360
425
|
}
|
|
361
426
|
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
function
|
|
427
|
+
// Keeps the cursor's row inside a window of `space` rows; returns the slice
|
|
428
|
+
// of the list to show.
|
|
429
|
+
function windowRows(count, selected, space) {
|
|
430
|
+
if (scrollScreen !== screen) {
|
|
431
|
+
scrollScreen = screen;
|
|
432
|
+
scrollTop = 0;
|
|
433
|
+
}
|
|
434
|
+
if (count <= space) {
|
|
435
|
+
scrollTop = 0;
|
|
436
|
+
return { start: 0, end: count };
|
|
437
|
+
}
|
|
438
|
+
if (selected >= 0) {
|
|
439
|
+
if (selected < scrollTop) {
|
|
440
|
+
scrollTop = selected;
|
|
441
|
+
} else if (selected >= scrollTop + space) {
|
|
442
|
+
scrollTop = selected - space + 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
scrollTop = Math.max(0, Math.min(scrollTop, count - space));
|
|
446
|
+
return { start: scrollTop, end: scrollTop + space };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// List items are { text, extra, value, number } or a plain line: extra is a
|
|
450
|
+
// description, value a setting's current value. From MEDIUM columns they
|
|
451
|
+
// line up in one column.
|
|
452
|
+
function listRows(items, selected, rowWidth) {
|
|
453
|
+
const shown = (item) => item.value ?? item.extra ?? "";
|
|
454
|
+
const entries = items.filter((item) => typeof item !== "string");
|
|
455
|
+
const labels = entries.filter((item) => shown(item));
|
|
456
|
+
// A badge follows its label, so it counts toward the label's width.
|
|
457
|
+
const labelWidth = (item) =>
|
|
458
|
+
visibleWidth(item.text) + (item.badge ? 1 + visibleWidth(item.badge) : 0);
|
|
459
|
+
const widest = Math.max(0, ...labels.map(labelWidth));
|
|
460
|
+
const pad = frameWidth() >= MEDIUM ? Math.min(widest, Math.floor(rowWidth * 0.45)) : 0;
|
|
461
|
+
const leadWidth = Math.max(0, ...entries.map((item) => visibleWidth(item.lead ?? "")));
|
|
462
|
+
const lead = (item) =>
|
|
463
|
+
leadWidth ? (item.lead ?? "") + " ".repeat(leadWidth - visibleWidth(item.lead ?? "")) : "";
|
|
464
|
+
return items.map((item, index) =>
|
|
465
|
+
typeof item === "string"
|
|
466
|
+
? item
|
|
467
|
+
: ui.row(index === selected, item.text, shown(item), item.number ?? null, {
|
|
468
|
+
pad,
|
|
469
|
+
lead: lead(item),
|
|
470
|
+
badge: item.badge,
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// A widget's icon from the bar, when the bar shows icons.
|
|
476
|
+
function widgetIcon(id) {
|
|
477
|
+
return config.emojis !== false && !config.ascii ? (WIDGET_TAGS[id]?.icon ?? "") : "";
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// On a wide window the list and the details panel share the width evenly.
|
|
481
|
+
function splitWidths(width) {
|
|
482
|
+
const list = Math.floor((width - 1) / 2);
|
|
483
|
+
return { list, details: width - 1 - list };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Details entries are plain strings (wrapped to the panel), { dim: text }
|
|
487
|
+
// (wrapped, in the muted color) or { raw: line } (as is: a rendered widget,
|
|
488
|
+
// a heading, a row of tags).
|
|
489
|
+
function detailsBody(lines, panelWidth) {
|
|
490
|
+
const wrapWidth = Math.min(panelWidth - 4, 88);
|
|
491
|
+
return lines.flatMap((entry) => {
|
|
492
|
+
if (typeof entry === "string") {
|
|
493
|
+
return wrapText(entry, wrapWidth);
|
|
494
|
+
}
|
|
495
|
+
if ("raw" in entry) {
|
|
496
|
+
return [entry.raw];
|
|
497
|
+
}
|
|
498
|
+
return wrapText(entry.dim, wrapWidth).map((line) => dim(line));
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Top lines, then blank lines, then bottom lines, in exactly `tall` rows;
|
|
503
|
+
// when both don't fit, the top wins.
|
|
504
|
+
function anchor(top, bottom, tall) {
|
|
505
|
+
const free = tall - top.length - bottom.length;
|
|
506
|
+
if (free < 0) {
|
|
507
|
+
return [...top, ...bottom].slice(0, tall);
|
|
508
|
+
}
|
|
509
|
+
return [...top, ...Array(free).fill(""), ...bottom];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Every screen: brand line and tabs, the screen's list (with a details panel
|
|
513
|
+
// beside it on a wide window, both as tall as the longer one, notes at the
|
|
514
|
+
// bottom), the live preview and the keys. Panels fit their content rather
|
|
515
|
+
// than the window, so a tall window doesn't get rows of empty boxes. A short
|
|
516
|
+
// window gives up blank lines, the tagline, the notes and the preview's
|
|
517
|
+
// toggles before the list starts to scroll.
|
|
518
|
+
function screenFrame({ title, head = [], items, selected = cursor, notes = [], keys, details }) {
|
|
365
519
|
const width = frameWidth();
|
|
366
|
-
const
|
|
367
|
-
const
|
|
520
|
+
const height = frameHeight();
|
|
521
|
+
const wide = Boolean(details) && width >= WIDE;
|
|
522
|
+
const widths = wide ? splitWidths(width) : { list: width, details: 0 };
|
|
523
|
+
const preview = previewParts(width);
|
|
524
|
+
const hints = ui.hints(keys, width - 1);
|
|
525
|
+
const notesWidth = wide ? widths.details : widths.list;
|
|
526
|
+
const noteLines = (step) =>
|
|
527
|
+
step.notes && notes.length
|
|
528
|
+
? [
|
|
529
|
+
"",
|
|
530
|
+
...detailsBody(
|
|
531
|
+
notes.map((n) => ({ dim: n })),
|
|
532
|
+
notesWidth
|
|
533
|
+
),
|
|
534
|
+
]
|
|
535
|
+
: [];
|
|
536
|
+
const detailLines = wide ? detailsBody(details.lines, widths.details) : [];
|
|
537
|
+
|
|
538
|
+
// Rows left for the list panel's body once everything else is drawn: the
|
|
539
|
+
// brand and tabs, both panels' borders, the preview and the key hints.
|
|
540
|
+
const bodySpace = (step) =>
|
|
541
|
+
height -
|
|
542
|
+
1 -
|
|
543
|
+
(step.gaps ? 3 : 0) -
|
|
544
|
+
(step.tagline ? 1 : 0) -
|
|
545
|
+
2 -
|
|
546
|
+
2 -
|
|
547
|
+
(2 + preview.bar.length + (step.chips ? 2 : 0)) -
|
|
548
|
+
hints.length;
|
|
549
|
+
const listSpace = (step) => bodySpace(step) - head.length - (wide ? 0 : noteLines(step).length);
|
|
550
|
+
const fits = (step) =>
|
|
551
|
+
listSpace(step) >= items.length &&
|
|
552
|
+
(!wide || detailLines.length + noteLines(step).length <= bodySpace(step));
|
|
553
|
+
const step =
|
|
554
|
+
COMPACT_STEPS.find(fits) ||
|
|
555
|
+
COMPACT_STEPS.find((s) => listSpace(s) >= MIN_LIST_ROWS) ||
|
|
556
|
+
COMPACT_STEPS[COMPACT_STEPS.length - 1];
|
|
557
|
+
|
|
558
|
+
const content = Math.max(
|
|
559
|
+
head.length + items.length + (wide ? 0 : noteLines(step).length),
|
|
560
|
+
wide ? detailLines.length + noteLines(step).length : 0
|
|
561
|
+
);
|
|
562
|
+
const tall = Math.max(1, Math.min(content, bodySpace(step)));
|
|
563
|
+
const { start, end } = windowRows(items.length, selected, Math.max(1, listSpace(step)));
|
|
564
|
+
const rows = listRows(items, selected, widths.list - 4).slice(start, end);
|
|
565
|
+
const { up, down } = ui.glyph;
|
|
566
|
+
const counts = {
|
|
567
|
+
above: start > 0 ? `${up} ${start} more` : "",
|
|
568
|
+
below: end < items.length ? `${down} ${items.length - end} more` : "",
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const body = wide
|
|
572
|
+
? ui.beside(
|
|
573
|
+
ui.panel(title, anchor([...head, ...rows], [], tall), widths.list, counts),
|
|
574
|
+
ui.panel(details.title, anchor(detailLines, noteLines(step), tall), widths.details)
|
|
575
|
+
)
|
|
576
|
+
: ui.panel(title, anchor([...head, ...rows], noteLines(step), tall), width, counts);
|
|
577
|
+
|
|
578
|
+
// The star link sits at the right end of the brand line, shortened to the
|
|
579
|
+
// star alone when the line is tight.
|
|
580
|
+
const link = (text) => `\x1b]8;;${REPO_URL}\x07${text}\x1b]8;;\x07`;
|
|
581
|
+
const star = link(`${ui.glyph.star} ${dim("Star on GitHub")}`);
|
|
582
|
+
const starShort = link(ui.glyph.star);
|
|
368
583
|
const toast = flashMessage ? ` ${ui.toast(flashMessage)}` : "";
|
|
369
|
-
const
|
|
584
|
+
const brand = ` ${ui.wordmark("agy-powerline")} ${ui.tag(`v${VERSION}`)}${toast}`;
|
|
585
|
+
const gap = step.gaps ? [""] : [];
|
|
370
586
|
|
|
371
587
|
return [
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
` ${dim(`${ui.glyph.sparkle}
|
|
375
|
-
|
|
588
|
+
...gap,
|
|
589
|
+
ui.spread(brand, star) ?? ui.spread(brand, starShort) ?? `${brand} ${starShort}`,
|
|
590
|
+
...(step.tagline ? [` ${dim(`${ui.glyph.sparkle} Make agy feel like yours`)}`] : []),
|
|
591
|
+
...(step.gaps ? [ui.rule()] : []),
|
|
376
592
|
ui.tabs(TABS, TAB_OF_SCREEN[screen]),
|
|
377
|
-
|
|
378
|
-
...
|
|
379
|
-
...
|
|
380
|
-
...
|
|
593
|
+
...gap,
|
|
594
|
+
...body,
|
|
595
|
+
...ui.panel(preview.title, [...preview.bar, ...(step.chips ? ["", preview.chips] : [])], width),
|
|
596
|
+
...hints,
|
|
597
|
+
];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// One widget drawn on its own with the previewed theme and layout, for the
|
|
601
|
+
// details panel; a widget with nothing to show in the sample data says so.
|
|
602
|
+
function widgetPreview(widget) {
|
|
603
|
+
const width = splitWidths(frameWidth()).details - 4;
|
|
604
|
+
let rendered = "";
|
|
605
|
+
try {
|
|
606
|
+
rendered = renderStatusline(
|
|
607
|
+
{ ...previewData, terminal_width: width },
|
|
608
|
+
{ ...previewConfig(), widgets: [widget] },
|
|
609
|
+
{ color: getActiveDepth() }
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
// The full preview below reports render errors.
|
|
613
|
+
}
|
|
614
|
+
const lines = String(rendered).split("\n");
|
|
615
|
+
if (lines.every((line) => !line.replace(/\x1b\[[0-9;]*m/g, "").trim())) {
|
|
616
|
+
return [{ dim: "Hidden in this preview: the sample data has nothing for it." }];
|
|
617
|
+
}
|
|
618
|
+
return lines.map((line) => ({ raw: line }));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
622
|
+
|
|
623
|
+
const EXEC_NOTE = {
|
|
624
|
+
dim: "Runs only in agy, from your install folder; previews here show [cmd].",
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const heading = (text) => ({ raw: ui.heading(text) });
|
|
628
|
+
|
|
629
|
+
function presetsUsing(id) {
|
|
630
|
+
const names = PRESET_ORDER.filter((name) =>
|
|
631
|
+
PRESETS[name].widgets.some((widget) => widget.id === id)
|
|
632
|
+
).map((name) => PRESETS[name].name);
|
|
633
|
+
return [
|
|
634
|
+
heading("In presets"),
|
|
635
|
+
names.length ? { raw: names.map(ui.tag).join(" ") } : { dim: "none" },
|
|
381
636
|
];
|
|
382
637
|
}
|
|
383
638
|
|
|
639
|
+
// A widget's own settings from the config file, cleaned of control
|
|
640
|
+
// characters before they reach the terminal.
|
|
641
|
+
function widgetOptions(widget) {
|
|
642
|
+
return Object.entries(widget)
|
|
643
|
+
.filter(([key]) => key !== "id")
|
|
644
|
+
.map(([key, value]) => ({
|
|
645
|
+
raw: `${dim(key)} ${stripControl(typeof value === "string" ? value : JSON.stringify(value))}`,
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function widgetDetails(widget) {
|
|
650
|
+
if (!widget) {
|
|
651
|
+
return { title: "No widgets", lines: ["Press a to add your first widget."] };
|
|
652
|
+
}
|
|
653
|
+
if (widget.id === "separator") {
|
|
654
|
+
return {
|
|
655
|
+
title: "Flex space",
|
|
656
|
+
lines: [
|
|
657
|
+
"Grows to fill the free space in its row, pushing the widgets after it to the right. In Pills it also starts a new pill.",
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (widget.id === "lineBreak") {
|
|
662
|
+
return {
|
|
663
|
+
title: "New row",
|
|
664
|
+
lines: ["Starts a new row: the widgets after it go on the next line of the bar."],
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const meta = WIDGET_CATALOG.find((item) => item.id === widget.id);
|
|
669
|
+
const options = widgetOptions(widget);
|
|
670
|
+
return {
|
|
671
|
+
title: [widgetIcon(widget.id), widgetLabel(widget.id)].filter(Boolean).join(" "),
|
|
672
|
+
lines: [
|
|
673
|
+
`${capitalize(meta?.description ?? widget.id)}.`,
|
|
674
|
+
"",
|
|
675
|
+
heading("Preview"),
|
|
676
|
+
...widgetPreview(widget),
|
|
677
|
+
"",
|
|
678
|
+
heading("Options"),
|
|
679
|
+
...(options.length
|
|
680
|
+
? options
|
|
681
|
+
: [{ dim: "Defaults. Colors, labels and thresholds per widget: docs/configuration.md" }]),
|
|
682
|
+
"",
|
|
683
|
+
...presetsUsing(widget.id),
|
|
684
|
+
...(widget.id === "exec" ? ["", EXEC_NOTE] : []),
|
|
685
|
+
],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
384
689
|
function widgetCount() {
|
|
385
690
|
const count = config.widgets.filter((w) => w.id !== "separator" && w.id !== "lineBreak").length;
|
|
386
691
|
return `${count} widget${count === 1 ? "" : "s"}`;
|
|
@@ -388,10 +693,13 @@ function widgetCount() {
|
|
|
388
693
|
|
|
389
694
|
function drawMain() {
|
|
390
695
|
const { upDown, shift } = ui.glyph;
|
|
391
|
-
|
|
392
|
-
|
|
696
|
+
// Flex spaces and new rows are layout, not widgets: they get no number, so
|
|
697
|
+
// the numbers match the widget count in the title.
|
|
698
|
+
let number = 0;
|
|
699
|
+
const items = config.widgets.length
|
|
700
|
+
? config.widgets.map((widget) => {
|
|
393
701
|
const meta = WIDGET_CATALOG.find((item) => item.id === widget.id);
|
|
394
|
-
let extra = meta?.
|
|
702
|
+
let extra = meta?.summary ?? "";
|
|
395
703
|
if (widget.id === "exec" && widget.cmd) {
|
|
396
704
|
extra = `cmd: ${widget.cmd}`;
|
|
397
705
|
} else if (widget.text) {
|
|
@@ -409,18 +717,27 @@ function drawMain() {
|
|
|
409
717
|
widget.id === "separator"
|
|
410
718
|
? `${config.ascii ? "..." : "╌╌"} flex space`
|
|
411
719
|
: `${config.ascii ? "--" : "↵"} new row`;
|
|
412
|
-
return
|
|
720
|
+
return { text: dim(divider), number: "" };
|
|
413
721
|
}
|
|
414
|
-
|
|
722
|
+
number += 1;
|
|
723
|
+
return {
|
|
724
|
+
lead: widgetIcon(widget.id),
|
|
725
|
+
text: widgetLabel(widget.id),
|
|
726
|
+
extra: stripControl(extra),
|
|
727
|
+
number,
|
|
728
|
+
};
|
|
415
729
|
})
|
|
416
730
|
: [dim(`${ui.glyph.sparkle} nothing here yet. Press a to add your first widget.`)];
|
|
417
731
|
|
|
418
732
|
return screenFrame({
|
|
419
733
|
title: `Your widgets ${dim(`${ui.glyph.dot} ${widgetCount()}`)}`,
|
|
420
|
-
|
|
734
|
+
items,
|
|
735
|
+
selected: config.widgets.length ? cursor : -1,
|
|
736
|
+
details: widgetDetails(config.widgets[cursor]),
|
|
421
737
|
keys: [
|
|
422
738
|
[upDown, "move"],
|
|
423
739
|
[`${shift}${upDown}`, "reorder"],
|
|
740
|
+
["enter", "edit"],
|
|
424
741
|
["a", "add"],
|
|
425
742
|
["s", "flex space"],
|
|
426
743
|
["n", "new row"],
|
|
@@ -432,25 +749,344 @@ function drawMain() {
|
|
|
432
749
|
});
|
|
433
750
|
}
|
|
434
751
|
|
|
752
|
+
// Widgets that take each option, as the layouts read them.
|
|
753
|
+
const QUOTA_WIDGETS = new Set([
|
|
754
|
+
"quota",
|
|
755
|
+
"fiveHourBar",
|
|
756
|
+
"fiveHourPct",
|
|
757
|
+
"weeklyBar",
|
|
758
|
+
"weeklyPct",
|
|
759
|
+
"thirdPartyFiveHour",
|
|
760
|
+
"thirdPartyWeekly",
|
|
761
|
+
"argonWeekly",
|
|
762
|
+
]);
|
|
763
|
+
const PACED_WIDGETS = new Set([
|
|
764
|
+
"fiveHourBar",
|
|
765
|
+
"weeklyBar",
|
|
766
|
+
"thirdPartyFiveHour",
|
|
767
|
+
"thirdPartyWeekly",
|
|
768
|
+
"argonWeekly",
|
|
769
|
+
]);
|
|
770
|
+
const CONTEXT_WIDGETS = new Set(["context", "contextPct", "contextTokens"]);
|
|
771
|
+
const RESET_WIDGETS = new Set(["fiveHourReset", "weeklyReset", "argonReset"]);
|
|
772
|
+
const COLOR_SLOTS = ["model", "context", "git", "weekly", "thirdParty", "fiveHour", "meta"];
|
|
773
|
+
// Longest custom badge text; the badge is meant to be a word or two.
|
|
774
|
+
const TEXT_MAX = 40;
|
|
775
|
+
|
|
776
|
+
const onOff = (value) => (value ? "on" : "off");
|
|
777
|
+
|
|
778
|
+
// The options of one widget: cycle fields step through values, number fields
|
|
779
|
+
// step by 5 with the arrows, text fields take typing, and fixed ones only show
|
|
780
|
+
// their value. undefined means "not set": the widget follows the global
|
|
781
|
+
// setting or its default.
|
|
782
|
+
function editFields(widget) {
|
|
783
|
+
const quota = QUOTA_WIDGETS.has(widget.id);
|
|
784
|
+
const context = CONTEXT_WIDGETS.has(widget.id);
|
|
785
|
+
const thresholds = quota
|
|
786
|
+
? resolveQuotaThresholds({}, config)
|
|
787
|
+
: context
|
|
788
|
+
? resolveContextThresholds({}, config)
|
|
789
|
+
: null;
|
|
790
|
+
const fields = [
|
|
791
|
+
{
|
|
792
|
+
key: "color",
|
|
793
|
+
label: "Color",
|
|
794
|
+
values: [undefined, ...COLOR_SLOTS],
|
|
795
|
+
show: (value) => (value === undefined ? "theme default" : colorName(value)),
|
|
796
|
+
help: "Draws this widget in another of the theme's colors. Caution and danger colors still take over when a quota or the context runs low.",
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
key: "raw",
|
|
800
|
+
label: "Label",
|
|
801
|
+
values: [undefined, false, true],
|
|
802
|
+
show: (value) =>
|
|
803
|
+
value === undefined
|
|
804
|
+
? `default (${config.minimalist ? "hidden" : "shown"})`
|
|
805
|
+
: value
|
|
806
|
+
? "hidden, value only"
|
|
807
|
+
: "shown",
|
|
808
|
+
help: "Shows or hides the icon and label before the value. Default follows minimalist mode (m).",
|
|
809
|
+
},
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
if (widget.id === "model") {
|
|
813
|
+
fields.push({
|
|
814
|
+
key: "short",
|
|
815
|
+
label: "Short name",
|
|
816
|
+
values: [undefined, true],
|
|
817
|
+
show: (value) => onOff(value),
|
|
818
|
+
help: "3.8 Flash·High instead of Gemini 3.8 Flash (High).",
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
if (widget.id === "totalTokens") {
|
|
822
|
+
fields.push({
|
|
823
|
+
key: "split",
|
|
824
|
+
label: "Split",
|
|
825
|
+
values: [undefined, true],
|
|
826
|
+
show: (value) => onOff(value),
|
|
827
|
+
help: "Input and output tokens apart (123.1K↑ 355.0K↓) instead of one total.",
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (RESET_WIDGETS.has(widget.id)) {
|
|
831
|
+
fields.push({
|
|
832
|
+
key: "format",
|
|
833
|
+
label: "Format",
|
|
834
|
+
values: [undefined, "time", "both"],
|
|
835
|
+
show: (value) => value ?? "countdown",
|
|
836
|
+
help: "countdown (2hr 47m), time (6:33pm) or both (6:33pm (2hr 47m)).",
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (widget.id === "customText") {
|
|
840
|
+
fields.push({
|
|
841
|
+
key: "text",
|
|
842
|
+
label: "Text",
|
|
843
|
+
text: true,
|
|
844
|
+
show: (value) => value || "(empty: hidden)",
|
|
845
|
+
help: "The badge's text. Enter to type, Enter again to keep it, Esc to cancel.",
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (widget.id === "exec") {
|
|
849
|
+
fields.push({
|
|
850
|
+
key: "cmd",
|
|
851
|
+
label: "Command",
|
|
852
|
+
fixed: true,
|
|
853
|
+
show: (value) => value || "(none)",
|
|
854
|
+
help: `The command runs only in agy, from your install folder. Edit it in ${getConfigPath()}.`,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
if (thresholds) {
|
|
858
|
+
const what = quota ? "left" : "used";
|
|
859
|
+
fields.push(
|
|
860
|
+
{
|
|
861
|
+
key: "warn",
|
|
862
|
+
label: "Caution at",
|
|
863
|
+
number: thresholds.warn,
|
|
864
|
+
show: (value) => (value === undefined ? `default (${thresholds.warn}%)` : `${value}%`),
|
|
865
|
+
help: `Turns to the caution color at this share ${what}.`,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
key: "crit",
|
|
869
|
+
label: "Danger at",
|
|
870
|
+
number: thresholds.crit,
|
|
871
|
+
show: (value) => (value === undefined ? `default (${thresholds.crit}%)` : `${value}%`),
|
|
872
|
+
help: `Turns to the danger color at this share ${what}.`,
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
key: "blink",
|
|
876
|
+
label: "Blink",
|
|
877
|
+
values: [undefined, true, "warn", false],
|
|
878
|
+
show: (value) =>
|
|
879
|
+
value === undefined
|
|
880
|
+
? `default (${thresholds.blink ? "on" : "off"})`
|
|
881
|
+
: { true: "at danger", warn: "at caution and danger", false: "off" }[String(value)],
|
|
882
|
+
help: "Makes the widget blink when it reaches the danger (or caution) level. ASCII mode never blinks.",
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (PACED_WIDGETS.has(widget.id)) {
|
|
887
|
+
fields.push({
|
|
888
|
+
key: "pace",
|
|
889
|
+
label: "Pace marker",
|
|
890
|
+
values: [undefined, false, true],
|
|
891
|
+
show: (value) =>
|
|
892
|
+
value === undefined ? `default (${onOff(config.pacing !== false)})` : onOff(value),
|
|
893
|
+
help: "Marks where usage would be if it were spread evenly over the window. Default follows r.",
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
return fields;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// A theme slot's color name, in that color.
|
|
900
|
+
function colorName(slot) {
|
|
901
|
+
const colors = themeColors(config.theme, config.customTheme);
|
|
902
|
+
const block = config.ascii ? "#" : "■";
|
|
903
|
+
return colors[slot] ? `${fg(colors[slot])}${block}${RESET} ${slot}` : String(slot);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function editedWidget() {
|
|
907
|
+
return config.widgets[editIndex];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function drawEdit() {
|
|
911
|
+
const widget = editedWidget();
|
|
912
|
+
const fields = editFields(widget);
|
|
913
|
+
const field = fields[cursor];
|
|
914
|
+
const typing = textInput !== null;
|
|
915
|
+
const caret = config.ascii ? "_" : "▏";
|
|
916
|
+
const items = fields.map((item) => ({
|
|
917
|
+
text: item.label,
|
|
918
|
+
value:
|
|
919
|
+
typing && item === field
|
|
920
|
+
? `${BOLD}${textInput}${RESET}${caret}`
|
|
921
|
+
: stripControl(item.show(widget[item.key])),
|
|
922
|
+
}));
|
|
923
|
+
const label = [widgetIcon(widget.id), widgetLabel(widget.id)].filter(Boolean).join(" ");
|
|
924
|
+
const changeKeys = field?.text
|
|
925
|
+
? [["enter", "type"]]
|
|
926
|
+
: field?.number !== undefined
|
|
927
|
+
? [[ui.glyph.leftRight, "change"]]
|
|
928
|
+
: field?.fixed
|
|
929
|
+
? []
|
|
930
|
+
: [[`enter ${ui.glyph.leftRight}`, "change"]];
|
|
931
|
+
|
|
932
|
+
return screenFrame({
|
|
933
|
+
title: `Edit ${dim(`${ui.glyph.dot} ${label}`)}`,
|
|
934
|
+
items,
|
|
935
|
+
// A wide window shows the help in the details panel instead.
|
|
936
|
+
notes: field && frameWidth() < WIDE ? [field.help] : [],
|
|
937
|
+
details: field && {
|
|
938
|
+
title: field.label,
|
|
939
|
+
lines: [field.help, "", heading("Preview"), ...widgetPreview(widget)],
|
|
940
|
+
},
|
|
941
|
+
keys: typing
|
|
942
|
+
? [
|
|
943
|
+
["type", "text"],
|
|
944
|
+
["enter", "keep"],
|
|
945
|
+
["esc", "cancel"],
|
|
946
|
+
]
|
|
947
|
+
: [
|
|
948
|
+
[ui.glyph.upDown, "option"],
|
|
949
|
+
...changeKeys,
|
|
950
|
+
...(field?.fixed ? [] : [["x", "default"]]),
|
|
951
|
+
["esc", "back"],
|
|
952
|
+
],
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function openEdit() {
|
|
957
|
+
const widget = config.widgets[cursor];
|
|
958
|
+
if (!widget || widget.id === "separator" || widget.id === "lineBreak") {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
editIndex = cursor;
|
|
962
|
+
textInput = null;
|
|
963
|
+
screen = "edit";
|
|
964
|
+
cursor = 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function closeEdit() {
|
|
968
|
+
textInput = null;
|
|
969
|
+
screen = "main";
|
|
970
|
+
cursor = editIndex;
|
|
971
|
+
clampCursor(config.widgets.length);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Sets one option, or removes it when the value is undefined, then saves.
|
|
975
|
+
function setOption(key, value) {
|
|
976
|
+
const widget = editedWidget();
|
|
977
|
+
if (value === undefined) {
|
|
978
|
+
delete widget[key];
|
|
979
|
+
} else {
|
|
980
|
+
widget[key] = value;
|
|
981
|
+
}
|
|
982
|
+
persist();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function stepOption(field, direction) {
|
|
986
|
+
const current = editedWidget()[field.key];
|
|
987
|
+
if (field.number !== undefined) {
|
|
988
|
+
const from = Number.isFinite(current) ? current : field.number;
|
|
989
|
+
setOption(field.key, Math.max(0, Math.min(100, from + direction * 5)));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const index = field.values.findIndex((value) => value === current);
|
|
993
|
+
const next = (Math.max(0, index) + direction + field.values.length) % field.values.length;
|
|
994
|
+
setOption(field.key, field.values[next]);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function onEdit(key) {
|
|
998
|
+
const fields = editFields(editedWidget());
|
|
999
|
+
const field = fields[cursor];
|
|
1000
|
+
|
|
1001
|
+
if (textInput !== null) {
|
|
1002
|
+
if (key.name === "escape") {
|
|
1003
|
+
textInput = null;
|
|
1004
|
+
} else if (key.name === "return") {
|
|
1005
|
+
const text = stripControl(textInput).trim();
|
|
1006
|
+
setOption(field.key, text || undefined);
|
|
1007
|
+
textInput = null;
|
|
1008
|
+
} else if (key.name === "backspace" || key.name === "delete") {
|
|
1009
|
+
textInput = textInput.slice(0, -1);
|
|
1010
|
+
} else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
1011
|
+
const char = key.sequence;
|
|
1012
|
+
if (char >= " " && char !== "\x7f" && textInput.length < TEXT_MAX) {
|
|
1013
|
+
textInput += char;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (key.name === "escape") {
|
|
1020
|
+
closeEdit();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (key.name === "up") {
|
|
1024
|
+
moveCursor(-1, fields.length);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (key.name === "down") {
|
|
1028
|
+
moveCursor(1, fields.length);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (!field || field.fixed) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (key.name === "x" || key.name === "delete" || key.name === "backspace") {
|
|
1035
|
+
setOption(field.key, undefined);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (field.text) {
|
|
1039
|
+
if (key.name === "return") {
|
|
1040
|
+
textInput = editedWidget()[field.key] ?? "";
|
|
1041
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (key.name === "left" || key.name === "right") {
|
|
1045
|
+
stepOption(field, key.name === "left" ? -1 : 1);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (key.name === "return" && field.values) {
|
|
1049
|
+
stepOption(field, 1);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
435
1053
|
function drawAdd() {
|
|
436
|
-
const
|
|
1054
|
+
const widgets = filteredAddableWidgets();
|
|
437
1055
|
const caret = config.ascii ? "_" : "▏";
|
|
438
1056
|
const search = searchQuery
|
|
439
1057
|
? `${BOLD}${searchQuery}${RESET}${caret}`
|
|
440
1058
|
: `${caret}${dim("type to filter")}`;
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
449
|
-
|
|
1059
|
+
const head = [`${config.ascii ? "search:" : "🔎"} ${search}`, ""];
|
|
1060
|
+
const items = widgets.length
|
|
1061
|
+
? widgets.map((widget) => ({
|
|
1062
|
+
lead: widgetIcon(widget.id),
|
|
1063
|
+
text: widget.label,
|
|
1064
|
+
extra: widget.summary,
|
|
1065
|
+
}))
|
|
1066
|
+
: [dim(`No widget matches "${searchQuery}". Backspace to edit, Esc to go back.`)];
|
|
1067
|
+
const hovered = widgets[cursor];
|
|
450
1068
|
|
|
451
1069
|
return screenFrame({
|
|
452
1070
|
title: "Add a widget",
|
|
453
|
-
|
|
1071
|
+
head,
|
|
1072
|
+
items,
|
|
1073
|
+
selected: widgets.length ? cursor : -1,
|
|
1074
|
+
details: hovered
|
|
1075
|
+
? {
|
|
1076
|
+
title: [widgetIcon(hovered.id), hovered.label].filter(Boolean).join(" "),
|
|
1077
|
+
lines: [
|
|
1078
|
+
`${capitalize(hovered.description)}.`,
|
|
1079
|
+
"",
|
|
1080
|
+
heading("Preview"),
|
|
1081
|
+
...widgetPreview({ id: hovered.id }),
|
|
1082
|
+
"",
|
|
1083
|
+
...presetsUsing(hovered.id),
|
|
1084
|
+
"",
|
|
1085
|
+
{ dim: hovered.unique ? "One per bar." : "Can be added more than once." },
|
|
1086
|
+
...(hovered.id === "exec" ? [EXEC_NOTE] : []),
|
|
1087
|
+
],
|
|
1088
|
+
}
|
|
1089
|
+
: { title: "No match", lines: ["Backspace edits the search, Esc clears it."] },
|
|
454
1090
|
keys: [
|
|
455
1091
|
["type", "filter"],
|
|
456
1092
|
[ui.glyph.upDown, "move"],
|
|
@@ -461,17 +1097,51 @@ function drawAdd() {
|
|
|
461
1097
|
});
|
|
462
1098
|
}
|
|
463
1099
|
|
|
1100
|
+
function presetDetails(id) {
|
|
1101
|
+
const preset = PRESETS[id];
|
|
1102
|
+
if (!preset) {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
const rows = [[]];
|
|
1106
|
+
for (const widget of preset.widgets) {
|
|
1107
|
+
if (widget.id === "lineBreak") {
|
|
1108
|
+
rows.push([]);
|
|
1109
|
+
} else if (widget.id !== "separator") {
|
|
1110
|
+
const icon = widgetIcon(widget.id);
|
|
1111
|
+
rows[rows.length - 1].push(
|
|
1112
|
+
icon ? `${icon} ${widgetLabel(widget.id)}` : widgetLabel(widget.id)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const count = rows.flat().length;
|
|
1117
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1118
|
+
return {
|
|
1119
|
+
title: preset.name,
|
|
1120
|
+
lines: [
|
|
1121
|
+
`${preset.description}.`,
|
|
1122
|
+
"",
|
|
1123
|
+
heading(`${plural(count, "widget")} on ${plural(rows.length, "row")}`),
|
|
1124
|
+
...rows.map((labels, index) => ({
|
|
1125
|
+
raw: `${ui.accentText(String(index + 1))} ${labels.join(dim(" · "))}`,
|
|
1126
|
+
})),
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
464
1131
|
function drawPresets() {
|
|
465
|
-
const
|
|
1132
|
+
const current = currentPreset();
|
|
1133
|
+
const items = PRESET_ORDER.map((id) => {
|
|
466
1134
|
const preset = PRESETS[id];
|
|
467
|
-
const
|
|
468
|
-
|
|
1135
|
+
const lead = config.ascii ? "" : (PRESET_ICONS[id] ?? "");
|
|
1136
|
+
const badge = id === current ? ui.chip("current", true) : "";
|
|
1137
|
+
return { lead, text: preset.name, badge, extra: preset.description };
|
|
469
1138
|
});
|
|
470
1139
|
|
|
471
1140
|
return screenFrame({
|
|
472
1141
|
title: "Presets",
|
|
473
|
-
|
|
1142
|
+
items,
|
|
474
1143
|
notes: ["A preset swaps your widgets; your layout and theme stay."],
|
|
1144
|
+
details: presetDetails(PRESET_ORDER[cursor]),
|
|
475
1145
|
keys: [
|
|
476
1146
|
[ui.glyph.upDown, "preview"],
|
|
477
1147
|
["enter", "apply"],
|
|
@@ -491,23 +1161,89 @@ function themeSwatch(colors) {
|
|
|
491
1161
|
);
|
|
492
1162
|
}
|
|
493
1163
|
|
|
1164
|
+
const toHex = (rgb) => `#${rgb.map((part) => part.toString(16).padStart(2, "0")).join("")}`;
|
|
1165
|
+
|
|
1166
|
+
// One color as a pill in that color with the theme's pill text, the way the
|
|
1167
|
+
// bar draws it, then its hex value.
|
|
1168
|
+
function colorLine(label, rgb, ink) {
|
|
1169
|
+
return { raw: `${ui.pill(` ${label.padEnd(9)}`, rgb, ink)} ${dim(toHex(rgb))}` };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const PALETTE_SLOTS = [
|
|
1173
|
+
["model", "Model"],
|
|
1174
|
+
["context", "Context"],
|
|
1175
|
+
["git", "Git"],
|
|
1176
|
+
["weekly", "7d quota"],
|
|
1177
|
+
["meta", "Details"],
|
|
1178
|
+
["fiveHour", "OK"],
|
|
1179
|
+
["caution", "Caution"],
|
|
1180
|
+
["warn", "Danger"],
|
|
1181
|
+
];
|
|
1182
|
+
|
|
1183
|
+
function themeDetails(id) {
|
|
1184
|
+
if (id === "custom") {
|
|
1185
|
+
return {
|
|
1186
|
+
title: "Custom",
|
|
1187
|
+
lines: [
|
|
1188
|
+
"Your own colors, on top of a base theme.",
|
|
1189
|
+
"",
|
|
1190
|
+
"Enter opens the color editor; the preview follows every change.",
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
const theme = THEMES[id];
|
|
1195
|
+
if (!theme) {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const background = { dark: "dark terminals", light: "light terminals" }[theme.kind];
|
|
1199
|
+
return {
|
|
1200
|
+
title: theme.name,
|
|
1201
|
+
lines: [
|
|
1202
|
+
`${theme.description}.`,
|
|
1203
|
+
...(background ? [{ raw: `${dim("For")} ${ui.tag(background)}` }] : []),
|
|
1204
|
+
"",
|
|
1205
|
+
heading("Palette"),
|
|
1206
|
+
...PALETTE_SLOTS.map(([slot, label]) =>
|
|
1207
|
+
colorLine(label, theme.colors[slot], theme.colors.text)
|
|
1208
|
+
),
|
|
1209
|
+
"",
|
|
1210
|
+
heading("Sample"),
|
|
1211
|
+
...themeSample(theme.colors),
|
|
1212
|
+
],
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// The editor drawn in the hovered theme: a selected and a normal row, and the
|
|
1217
|
+
// three states quotas and the context go through.
|
|
1218
|
+
function themeSample(colors) {
|
|
1219
|
+
const { on } = ui.glyph;
|
|
1220
|
+
const state = (slot, label) => `${fg(colors[slot])}${on} ${label}${RESET}`;
|
|
1221
|
+
return [
|
|
1222
|
+
{ raw: ui.row(true, "Selected item", "its description") },
|
|
1223
|
+
{ raw: ui.row(false, "Normal item", "its description") },
|
|
1224
|
+
{
|
|
1225
|
+
raw: ` ${state("fiveHour", "ok")} ${state("caution", "caution")} ${state("warn", "danger")}`,
|
|
1226
|
+
},
|
|
1227
|
+
];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
494
1230
|
function drawThemes() {
|
|
495
|
-
const
|
|
1231
|
+
const items = THEME_ORDER.map((id) => {
|
|
496
1232
|
const theme =
|
|
497
1233
|
THEMES[id] || (id === "custom" ? { name: "Custom", description: "Your own colors" } : null);
|
|
498
1234
|
if (!theme) {
|
|
499
1235
|
return "";
|
|
500
1236
|
}
|
|
501
1237
|
const swatch = themeSwatch(id === "custom" ? null : theme.colors);
|
|
502
|
-
const
|
|
1238
|
+
const badge = id === config.theme ? ui.chip("current", true) : "";
|
|
503
1239
|
const extra = id === "custom" ? `${theme.description}, Enter to edit` : theme.description;
|
|
504
|
-
|
|
505
|
-
return ui.row(index === cursor, name + current, extra);
|
|
1240
|
+
return { lead: swatch, text: theme.name, badge, extra };
|
|
506
1241
|
});
|
|
507
1242
|
|
|
508
1243
|
return screenFrame({
|
|
509
1244
|
title: "Themes",
|
|
510
|
-
|
|
1245
|
+
items,
|
|
1246
|
+
details: themeDetails(THEME_ORDER[cursor]),
|
|
511
1247
|
keys: [
|
|
512
1248
|
[ui.glyph.upDown, "preview"],
|
|
513
1249
|
["enter", "apply"],
|
|
@@ -525,16 +1261,38 @@ function hexSwatch(hex) {
|
|
|
525
1261
|
}
|
|
526
1262
|
}
|
|
527
1263
|
|
|
1264
|
+
// The curated colors in a row, the slot's current one in brackets.
|
|
1265
|
+
function curatedPalette(current) {
|
|
1266
|
+
const block = config.ascii ? "#" : "■";
|
|
1267
|
+
return CURATED_HEX_PALETTE.map((hex) => {
|
|
1268
|
+
const swatch = `${fg(parseColor(hex))}${block}${RESET}`;
|
|
1269
|
+
return hex === current ? `[${swatch}]` : ` ${swatch} `;
|
|
1270
|
+
}).join("");
|
|
1271
|
+
}
|
|
1272
|
+
|
|
528
1273
|
function drawCustomTheme() {
|
|
529
1274
|
const customColors = config.customTheme?.colors || {};
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
1275
|
+
const items = CUSTOM_COLOR_SLOTS.map((slot) => ({
|
|
1276
|
+
text: slot.label,
|
|
1277
|
+
value: hexSwatch(customColors[slot.key] || "(base)"),
|
|
1278
|
+
}));
|
|
1279
|
+
const slot = CUSTOM_COLOR_SLOTS[cursor];
|
|
1280
|
+
const current = customColors[slot?.key];
|
|
533
1281
|
|
|
534
1282
|
return screenFrame({
|
|
535
1283
|
title: "Custom theme",
|
|
536
|
-
|
|
1284
|
+
items,
|
|
537
1285
|
notes: ["Cycle a slot through curated colors; the preview updates as you go."],
|
|
1286
|
+
details: slot && {
|
|
1287
|
+
title: slot.label,
|
|
1288
|
+
lines: [
|
|
1289
|
+
current ? `Now ${current}.` : "Now the base theme's color.",
|
|
1290
|
+
"",
|
|
1291
|
+
curatedPalette(current),
|
|
1292
|
+
"",
|
|
1293
|
+
{ dim: "← → and Enter step through these colors." },
|
|
1294
|
+
],
|
|
1295
|
+
},
|
|
538
1296
|
keys: [
|
|
539
1297
|
[ui.glyph.upDown, "slot"],
|
|
540
1298
|
[ui.glyph.leftRight, "cycle color"],
|
|
@@ -578,10 +1336,38 @@ function powerlineRowValue(row) {
|
|
|
578
1336
|
return THEMES[config.theme]?.name ?? config.theme;
|
|
579
1337
|
}
|
|
580
1338
|
|
|
1339
|
+
const POWERLINE_HELP = {
|
|
1340
|
+
enabled:
|
|
1341
|
+
"On: adjacent widgets share one pill, with a separator between them. Off: every widget gets its own pill.",
|
|
1342
|
+
rowGap:
|
|
1343
|
+
"Puts a blank line between Pills rows, so stacked pills don't touch. The bar takes more lines (three rows become five).",
|
|
1344
|
+
style: "Sets start caps, end caps and separators in one go. Enter cycles the styles.",
|
|
1345
|
+
separators: "Glyphs between widgets of a pill. With several, they cycle per join, left to right.",
|
|
1346
|
+
startCaps: "Glyphs that open each pill. With several, they cycle per pill, left to right.",
|
|
1347
|
+
endCaps: "Glyphs that close each pill. With several, they cycle per pill, left to right.",
|
|
1348
|
+
theme: "Enter opens the Themes tab.",
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
function powerlineDetails(row) {
|
|
1352
|
+
if (!row) {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
const lines = [POWERLINE_HELP[row.id] ?? ""];
|
|
1356
|
+
if (row.id === "style") {
|
|
1357
|
+
const width = Math.max(...STYLE_ORDER.map((id) => STYLES[id].name.length));
|
|
1358
|
+
lines.push(
|
|
1359
|
+
"",
|
|
1360
|
+
...STYLE_ORDER.map((id) => `${STYLES[id].name.padEnd(width)} ${dim(STYLES[id].description)}`)
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (row.slot) {
|
|
1364
|
+
lines.push("", "Enter edits the list.");
|
|
1365
|
+
}
|
|
1366
|
+
return { title: row.label, lines };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
581
1369
|
function drawPowerline() {
|
|
582
|
-
const
|
|
583
|
-
ui.row(index === cursor, row.label, powerlineRowValue(row))
|
|
584
|
-
);
|
|
1370
|
+
const items = POWERLINE_ROWS.map((row) => ({ text: row.label, value: powerlineRowValue(row) }));
|
|
585
1371
|
const notes =
|
|
586
1372
|
config.layout === "lines"
|
|
587
1373
|
? ["Powerline shapes apply to Pills. Switch layout with l."]
|
|
@@ -592,8 +1378,9 @@ function drawPowerline() {
|
|
|
592
1378
|
|
|
593
1379
|
return screenFrame({
|
|
594
1380
|
title: "Powerline",
|
|
595
|
-
|
|
1381
|
+
items,
|
|
596
1382
|
notes,
|
|
1383
|
+
details: powerlineDetails(POWERLINE_ROWS[cursor]),
|
|
597
1384
|
keys: [
|
|
598
1385
|
[ui.glyph.upDown, "move"],
|
|
599
1386
|
["enter", "toggle / cycle / edit"],
|
|
@@ -610,11 +1397,12 @@ function drawGlyphs() {
|
|
|
610
1397
|
if (hexInput !== null) {
|
|
611
1398
|
return screenFrame({
|
|
612
1399
|
title: `Powerline ${ui.glyph.dot} ${title}`,
|
|
613
|
-
|
|
1400
|
+
items: [
|
|
614
1401
|
`Code point: ${BOLD}U+${hexInput}${RESET}${dim("_".repeat(6 - hexInput.length))}`,
|
|
615
1402
|
"",
|
|
616
1403
|
dim("Type 4-6 hex digits, e.g. E0B0 (Powerline), E0C0 (flame), 2588 (block)."),
|
|
617
1404
|
],
|
|
1405
|
+
selected: -1,
|
|
618
1406
|
keys: [
|
|
619
1407
|
["enter", "apply"],
|
|
620
1408
|
["esc", "cancel"],
|
|
@@ -622,23 +1410,36 @@ function drawGlyphs() {
|
|
|
622
1410
|
});
|
|
623
1411
|
}
|
|
624
1412
|
|
|
625
|
-
const
|
|
1413
|
+
const items = list.length
|
|
626
1414
|
? list.map((hex, index) => {
|
|
627
1415
|
const inverted = isSeparator && config.powerline.separatorInvert[index] ? "inverted" : "";
|
|
628
|
-
return
|
|
1416
|
+
return { text: glyphLabel(hex, glyphSlot), value: inverted };
|
|
629
1417
|
})
|
|
630
1418
|
: [
|
|
631
1419
|
dim(
|
|
632
1420
|
`None: ${isSeparator ? "widgets in a pill touch directly" : "pills have no cap on this side"}. Press a to add one.`
|
|
633
1421
|
),
|
|
634
1422
|
];
|
|
1423
|
+
const hovered = list[cursor];
|
|
635
1424
|
|
|
636
1425
|
return screenFrame({
|
|
637
1426
|
title: `Powerline ${ui.glyph.dot} ${title}`,
|
|
638
|
-
|
|
1427
|
+
items,
|
|
1428
|
+
selected: list.length ? cursor : -1,
|
|
639
1429
|
notes: isSeparator
|
|
640
1430
|
? ["Invert swaps the separator colors; left-pointing glyphs get it automatically."]
|
|
641
1431
|
: [],
|
|
1432
|
+
details: {
|
|
1433
|
+
title,
|
|
1434
|
+
lines: hovered
|
|
1435
|
+
? [
|
|
1436
|
+
`${glyphChar(hovered, glyphSlot, config.ascii)} ${glyphName(hovered)}`,
|
|
1437
|
+
{ dim: `U+${hovered}, ${cursor + 1} of ${list.length}` },
|
|
1438
|
+
"",
|
|
1439
|
+
POWERLINE_HELP[key],
|
|
1440
|
+
]
|
|
1441
|
+
: [POWERLINE_HELP[key]],
|
|
1442
|
+
},
|
|
642
1443
|
keys: [
|
|
643
1444
|
[ui.glyph.upDown, "select"],
|
|
644
1445
|
[ui.glyph.leftRight, "cycle preset"],
|
|
@@ -653,17 +1454,28 @@ function drawGlyphs() {
|
|
|
653
1454
|
});
|
|
654
1455
|
}
|
|
655
1456
|
|
|
1457
|
+
const LAYOUT_HELP = {
|
|
1458
|
+
pills: "Widgets sit in colored pills. The Powerline tab sets their caps, separators and joins.",
|
|
1459
|
+
lines:
|
|
1460
|
+
"Colored text on the terminal background, with dividers and dot meters. Powerline shapes don't apply.",
|
|
1461
|
+
};
|
|
1462
|
+
|
|
656
1463
|
function drawLayout() {
|
|
657
|
-
const
|
|
1464
|
+
const items = LAYOUT_ORDER.map((id) => {
|
|
658
1465
|
const layout = LAYOUTS[id];
|
|
659
|
-
const
|
|
660
|
-
return
|
|
1466
|
+
const badge = id === config.layout ? ui.chip("current", true) : "";
|
|
1467
|
+
return { text: layout.name, badge, extra: layout.description };
|
|
661
1468
|
});
|
|
1469
|
+
const hovered = LAYOUTS[LAYOUT_ORDER[cursor]];
|
|
662
1470
|
|
|
663
1471
|
return screenFrame({
|
|
664
1472
|
title: "Layout",
|
|
665
|
-
|
|
1473
|
+
items,
|
|
666
1474
|
notes: ["Both layouts use your widgets and theme; Powerline shapes apply to Pills."],
|
|
1475
|
+
details: hovered && {
|
|
1476
|
+
title: hovered.name,
|
|
1477
|
+
lines: [`${hovered.description}.`, "", LAYOUT_HELP[hovered.id] ?? ""],
|
|
1478
|
+
},
|
|
667
1479
|
keys: [
|
|
668
1480
|
[ui.glyph.upDown, "preview"],
|
|
669
1481
|
["enter", "apply"],
|
|
@@ -680,6 +1492,7 @@ function frameLines() {
|
|
|
680
1492
|
});
|
|
681
1493
|
|
|
682
1494
|
if (screen === "add") return drawAdd();
|
|
1495
|
+
if (screen === "edit") return drawEdit();
|
|
683
1496
|
if (screen === "themes") return drawThemes();
|
|
684
1497
|
if (screen === "customTheme") return drawCustomTheme();
|
|
685
1498
|
if (screen === "presets") return drawPresets();
|
|
@@ -694,7 +1507,9 @@ function draw() {
|
|
|
694
1507
|
}
|
|
695
1508
|
|
|
696
1509
|
// Draws one screen without a terminal, for tests. Options: config, screen,
|
|
697
|
-
// cursor, columns,
|
|
1510
|
+
// cursor, columns, rows, scroll (the first row shown of a long list), color
|
|
1511
|
+
// ("truecolor", "256", "none"), search, flash, editIndex and textInput (the
|
|
1512
|
+
// widget editor), and
|
|
698
1513
|
// widgets (a list used as is, even empty, which a saved config never is).
|
|
699
1514
|
function renderFrame(options = {}) {
|
|
700
1515
|
config = mergeConfig(options.config || {});
|
|
@@ -707,14 +1522,20 @@ function renderFrame(options = {}) {
|
|
|
707
1522
|
flashMessage = options.flash || "";
|
|
708
1523
|
glyphSlot = options.glyphSlot || "separator";
|
|
709
1524
|
hexInput = options.hexInput ?? null;
|
|
1525
|
+
editIndex = options.editIndex ?? 0;
|
|
1526
|
+
textInput = options.textInput ?? null;
|
|
710
1527
|
previewData = options.data || sampleData();
|
|
711
1528
|
columnsOverride = options.columns || 100;
|
|
1529
|
+
rowsOverride = options.rows || 50;
|
|
1530
|
+
scrollTop = options.scroll || 0;
|
|
1531
|
+
scrollScreen = screen;
|
|
712
1532
|
const previousDepth = getActiveDepth();
|
|
713
1533
|
setActiveDepth(options.color || "truecolor");
|
|
714
1534
|
try {
|
|
715
1535
|
return frameLines().join("\n");
|
|
716
1536
|
} finally {
|
|
717
1537
|
columnsOverride = null;
|
|
1538
|
+
rowsOverride = null;
|
|
718
1539
|
setActiveDepth(previousDepth);
|
|
719
1540
|
}
|
|
720
1541
|
}
|
|
@@ -838,6 +1659,11 @@ function onMain(key) {
|
|
|
838
1659
|
return;
|
|
839
1660
|
}
|
|
840
1661
|
|
|
1662
|
+
if (key.name === "return") {
|
|
1663
|
+
openEdit();
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
841
1667
|
if (key.name === "a") {
|
|
842
1668
|
openAdd();
|
|
843
1669
|
return;
|
|
@@ -1279,7 +2105,8 @@ function onKey(_str, key) {
|
|
|
1279
2105
|
return;
|
|
1280
2106
|
}
|
|
1281
2107
|
|
|
1282
|
-
|
|
2108
|
+
const typing = screen === "add" || hexInput !== null || textInput !== null;
|
|
2109
|
+
if (isQuitKey(key, typing)) {
|
|
1283
2110
|
quit();
|
|
1284
2111
|
return;
|
|
1285
2112
|
}
|
|
@@ -1290,7 +2117,7 @@ function onKey(_str, key) {
|
|
|
1290
2117
|
return;
|
|
1291
2118
|
}
|
|
1292
2119
|
|
|
1293
|
-
if (
|
|
2120
|
+
if (!typing && key.name === "g") {
|
|
1294
2121
|
openRepo();
|
|
1295
2122
|
draw();
|
|
1296
2123
|
return;
|
|
@@ -1305,6 +2132,8 @@ function onKey(_str, key) {
|
|
|
1305
2132
|
|
|
1306
2133
|
if (screen === "add") {
|
|
1307
2134
|
onAdd(key);
|
|
2135
|
+
} else if (screen === "edit") {
|
|
2136
|
+
onEdit(key);
|
|
1308
2137
|
} else if (screen === "themes") {
|
|
1309
2138
|
onThemes(key);
|
|
1310
2139
|
} else if (screen === "customTheme") {
|
|
@@ -1359,6 +2188,8 @@ function start() {
|
|
|
1359
2188
|
process.stdin.on("keypress", onKey);
|
|
1360
2189
|
process.on("SIGINT", quit);
|
|
1361
2190
|
process.on("SIGTERM", quit);
|
|
2191
|
+
// The frame follows the window width, so redraw before a resize wraps it.
|
|
2192
|
+
process.stdout.on("resize", draw);
|
|
1362
2193
|
process.stdout.write(ALT_ON + HIDE);
|
|
1363
2194
|
draw();
|
|
1364
2195
|
}
|