agy-powerline 1.2.0 → 1.4.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 +72 -0
- package/README.fr.md +243 -0
- package/README.ja.md +144 -49
- package/README.md +160 -52
- package/README.zh-CN.md +144 -49
- package/package.json +5 -2
- package/src/cli/caps.js +2 -0
- package/src/cli/configure.js +70 -172
- 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 +53 -1
- package/src/lib/catalog.js +54 -2
- package/src/lib/colors.js +144 -14
- package/src/lib/config-check.js +74 -0
- package/src/lib/config.js +89 -21
- 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 +385 -4
- package/src/lib/layouts/lines.js +175 -28
- package/src/lib/layouts/pills.js +232 -33
- 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 +13 -2
- 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 +45 -0
- package/src/runtime/statusline.js +15 -1
package/src/lib/layouts/pills.js
CHANGED
|
@@ -7,11 +7,15 @@ const {
|
|
|
7
7
|
bgDefault,
|
|
8
8
|
fg,
|
|
9
9
|
BOLD,
|
|
10
|
+
DIM,
|
|
10
11
|
NORMAL,
|
|
11
12
|
BLINK,
|
|
12
13
|
BLINK_OFF,
|
|
13
14
|
getActiveDepth,
|
|
15
|
+
gradientColors,
|
|
16
|
+
readableText,
|
|
14
17
|
} = require("../colors");
|
|
18
|
+
const { gradientStops } = require("../themes");
|
|
15
19
|
const { pickGlyph, glyphChar } = require("../powerline");
|
|
16
20
|
const {
|
|
17
21
|
quotaColor,
|
|
@@ -23,7 +27,11 @@ const {
|
|
|
23
27
|
formatContextTokens,
|
|
24
28
|
shortModelName,
|
|
25
29
|
formatTokenSplit,
|
|
30
|
+
formatUsage,
|
|
26
31
|
formatGitSync,
|
|
32
|
+
formatGitOperation,
|
|
33
|
+
gitChangeParts,
|
|
34
|
+
gitFileParts,
|
|
27
35
|
formatTasks,
|
|
28
36
|
formatMode,
|
|
29
37
|
clipText,
|
|
@@ -35,12 +43,15 @@ const {
|
|
|
35
43
|
paceIndex,
|
|
36
44
|
paceGlyph,
|
|
37
45
|
} = require("../formatters");
|
|
38
|
-
const { execWidgetOutput } = require("../exec");
|
|
46
|
+
const { execWidgetOutput, restoreAfterReset } = require("../exec");
|
|
39
47
|
const {
|
|
48
|
+
resolveColor,
|
|
40
49
|
widgetColors,
|
|
41
50
|
widgetTag,
|
|
42
51
|
isRaw,
|
|
43
52
|
isPaced,
|
|
53
|
+
METER_STYLES,
|
|
54
|
+
meterStyle,
|
|
44
55
|
hasNoData,
|
|
45
56
|
RAM_CAUTION,
|
|
46
57
|
RAM_DANGER,
|
|
@@ -56,6 +67,14 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
|
|
|
56
67
|
const ratio = Math.max(0, Math.min(100, Number(pct))) / 100;
|
|
57
68
|
const used = Math.round(ratio * width);
|
|
58
69
|
const marker = paceIndex(pace.elapsedPct, width);
|
|
70
|
+
// pips, the default, needs a Unicode font: ASCII mode keeps the cells.
|
|
71
|
+
const pips = pace.style === "pips";
|
|
72
|
+
if (pips && !pace.ascii && getActiveDepth() !== "none") {
|
|
73
|
+
return pipMeter(used, marker, width, fillColor, emptyColor) + bg(segmentColor);
|
|
74
|
+
}
|
|
75
|
+
if (METER_STYLES[pace.style] && !(pips && pace.ascii)) {
|
|
76
|
+
return glyphMeter(used, marker, width, METER_STYLES[pace.style], pace);
|
|
77
|
+
}
|
|
59
78
|
// The fill is a background color; without color it is drawn with glyphs.
|
|
60
79
|
const plain = getActiveDepth() === "none";
|
|
61
80
|
const glyph = (filled) => (!plain ? " " : pace.ascii ? (filled ? "#" : "-") : filled ? "█" : "░");
|
|
@@ -73,6 +92,51 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
|
|
|
73
92
|
return meter + bg(segmentColor);
|
|
74
93
|
}
|
|
75
94
|
|
|
95
|
+
// Small dots on the pill's own color: the filled ones in the light fill color,
|
|
96
|
+
// the rest in the dark track color, the same pair cells use. The pace marker
|
|
97
|
+
// is a larger dot in the other color, so nothing in the meter is a line.
|
|
98
|
+
function pipMeter(used, marker, width, fillColor, emptyColor) {
|
|
99
|
+
let meter = "";
|
|
100
|
+
let color = null;
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < width; i++) {
|
|
103
|
+
const filled = i < used;
|
|
104
|
+
const next = i === marker ? (filled ? emptyColor : fillColor) : filled ? fillColor : emptyColor;
|
|
105
|
+
if (next !== color) {
|
|
106
|
+
meter += fg(next);
|
|
107
|
+
color = next;
|
|
108
|
+
}
|
|
109
|
+
meter += i === marker ? METER_STYLES.pips[2] : METER_STYLES.pips[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return meter;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A glyph style draws on the pill's own color, with no cells: filled glyphs in
|
|
116
|
+
// the pill's text color (readable on the pill, gradients included), empty
|
|
117
|
+
// ones in the same color dimmed. ASCII mode keeps # and -.
|
|
118
|
+
function glyphMeter(used, marker, width, [full, empty, pace], { ascii, textColor }) {
|
|
119
|
+
let meter = fg(textColor);
|
|
120
|
+
let dim = false;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < width; i++) {
|
|
123
|
+
const blank = i !== marker && i >= used;
|
|
124
|
+
if (blank !== dim) {
|
|
125
|
+
meter += blank ? DIM : NORMAL;
|
|
126
|
+
dim = blank;
|
|
127
|
+
}
|
|
128
|
+
if (i === marker) {
|
|
129
|
+
meter += (!ascii && pace) || paceGlyph(ascii);
|
|
130
|
+
} else if (ascii) {
|
|
131
|
+
meter += blank ? "-" : "#";
|
|
132
|
+
} else {
|
|
133
|
+
meter += blank ? empty : full;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return dim ? meter + NORMAL : meter;
|
|
138
|
+
}
|
|
139
|
+
|
|
76
140
|
function meterSegment(
|
|
77
141
|
color,
|
|
78
142
|
pct,
|
|
@@ -437,14 +501,52 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
|
|
|
437
501
|
return null;
|
|
438
502
|
}
|
|
439
503
|
const mark = session.git.dirty ? `${ascii ? "* " : "● "}` : "";
|
|
504
|
+
const opStr = widget.operation === false ? "" : formatGitOperation(session.git.operation);
|
|
505
|
+
const opPart = opStr ? `|${opStr}` : "";
|
|
440
506
|
const syncStr = formatGitSync(session.git.ahead, session.git.behind, ascii);
|
|
441
507
|
const syncPart = syncStr ? ` ${syncStr}` : "";
|
|
442
508
|
return pill(
|
|
443
509
|
session.git.dirty ? colors.gitDirty : colors.git,
|
|
444
|
-
lbl(`${mark}${session.git.branch}${syncPart}`)
|
|
510
|
+
lbl(`${mark}${session.git.branch}${opPart}${syncPart}`)
|
|
445
511
|
);
|
|
446
512
|
}
|
|
447
513
|
|
|
514
|
+
case "gitChanges": {
|
|
515
|
+
const parts = gitChangeParts(session.gitDetails?.changes, ascii);
|
|
516
|
+
if (!parts.length) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
return pill(colors.git, lbl(parts.map((part) => part.text).join(" ")));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
case "gitFiles": {
|
|
523
|
+
const files = session.gitDetails?.files;
|
|
524
|
+
const parts = gitFileParts(files, ascii);
|
|
525
|
+
if (!parts.length) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const color = files.conflicts > 0 ? colors.gitDirty : colors.git;
|
|
529
|
+
return pill(color, lbl(parts.map((part) => part.text).join(" ")));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
case "gitSha":
|
|
533
|
+
if (!session.gitDetails?.sha) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
return pill(colors.git, lbl(session.gitDetails.sha));
|
|
537
|
+
|
|
538
|
+
case "gitStash":
|
|
539
|
+
if (!(session.gitDetails?.stash > 0)) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
return pill(colors.git, lbl(String(session.gitDetails.stash)));
|
|
543
|
+
|
|
544
|
+
case "gitWorktree":
|
|
545
|
+
if (!session.gitDetails?.worktree) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
return pill(colors.git, lbl(session.gitDetails.worktree));
|
|
549
|
+
|
|
448
550
|
case "plan":
|
|
449
551
|
if (!session.plan) {
|
|
450
552
|
return null;
|
|
@@ -547,6 +649,32 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
|
|
|
547
649
|
)
|
|
548
650
|
);
|
|
549
651
|
|
|
652
|
+
case "tokensToday":
|
|
653
|
+
case "tokensWeek": {
|
|
654
|
+
const usage = formatUsage(
|
|
655
|
+
session.usage?.[id === "tokensToday" ? "today" : "week"],
|
|
656
|
+
widget,
|
|
657
|
+
ascii
|
|
658
|
+
);
|
|
659
|
+
if (!usage) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
if (usage.pct === null) {
|
|
663
|
+
return pill(colors.meta, lbl(usage.text));
|
|
664
|
+
}
|
|
665
|
+
// A budget alerts like the context widgets: caution, then danger.
|
|
666
|
+
const thresh = resolveContextThresholds(widget, config);
|
|
667
|
+
const level =
|
|
668
|
+
usage.pct >= thresh.crit ? "crit" : usage.pct >= thresh.warn ? "warn" : "normal";
|
|
669
|
+
const color = contextColor(usage.pct, colors.meta, false, {
|
|
670
|
+
crit: thresh.crit,
|
|
671
|
+
warn: thresh.warn,
|
|
672
|
+
critColor: colors.warn,
|
|
673
|
+
warnColor: colors.caution,
|
|
674
|
+
});
|
|
675
|
+
return pill(color, lbl(usage.text), !ascii && shouldBlink(level, thresh.blink));
|
|
676
|
+
}
|
|
677
|
+
|
|
550
678
|
case "customText": {
|
|
551
679
|
if (!widget?.text) {
|
|
552
680
|
return null;
|
|
@@ -573,11 +701,57 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
|
|
|
573
701
|
}
|
|
574
702
|
}
|
|
575
703
|
|
|
704
|
+
// Pill text on a gradient color that the theme's text doesn't read on.
|
|
705
|
+
const READABLE_TEXT = [[22, 22, 28], WHITE, [0, 0, 0]];
|
|
706
|
+
|
|
576
707
|
function buildRows(session, config, colors) {
|
|
577
708
|
const rows = [];
|
|
578
709
|
let groups = [];
|
|
579
710
|
let current = [];
|
|
580
|
-
|
|
711
|
+
// The widgets shown in the current row, so a gradient can be spread over
|
|
712
|
+
// exactly those once the row is complete.
|
|
713
|
+
let placed = [];
|
|
714
|
+
const gradient = gradientStops(config.gradient);
|
|
715
|
+
|
|
716
|
+
function styledSegment(widget, ownColors) {
|
|
717
|
+
const segment = buildSegment(
|
|
718
|
+
widget.id,
|
|
719
|
+
session,
|
|
720
|
+
ownColors,
|
|
721
|
+
config.emojis,
|
|
722
|
+
config.ascii === true,
|
|
723
|
+
widget,
|
|
724
|
+
config
|
|
725
|
+
);
|
|
726
|
+
if (segment) {
|
|
727
|
+
segment.textColor = ownColors.text;
|
|
728
|
+
segment.bold = widget.bold !== false;
|
|
729
|
+
if (segment.meter) {
|
|
730
|
+
segment.style = meterStyle(widget, config, "pills");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return segment;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Each widget takes its step of the gradient in place of its identity
|
|
737
|
+
// color; alert colors and a widget's own color stay as they are.
|
|
738
|
+
function applyGradient() {
|
|
739
|
+
const shades = gradientColors(gradient, placed.length);
|
|
740
|
+
placed.forEach(({ widget, group, index }, i) => {
|
|
741
|
+
if (resolveColor(widget.color, colors)) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const ownColors = widgetColors({ ...widget, color: shades[i] }, colors);
|
|
745
|
+
const segment = styledSegment(widget, ownColors);
|
|
746
|
+
if (!segment) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (!resolveColor(widget.textColor, colors)) {
|
|
750
|
+
segment.textColor = readableText(segment.color, ownColors.text, READABLE_TEXT);
|
|
751
|
+
}
|
|
752
|
+
group[index] = segment;
|
|
753
|
+
});
|
|
754
|
+
}
|
|
581
755
|
|
|
582
756
|
function flushGroup() {
|
|
583
757
|
if (current.length) {
|
|
@@ -589,6 +763,11 @@ function buildRows(session, config, colors) {
|
|
|
589
763
|
function flushRow() {
|
|
590
764
|
flushGroup();
|
|
591
765
|
|
|
766
|
+
if (gradient && placed.length) {
|
|
767
|
+
applyGradient();
|
|
768
|
+
}
|
|
769
|
+
placed = [];
|
|
770
|
+
|
|
592
771
|
if (groups.length) {
|
|
593
772
|
rows.push(groups);
|
|
594
773
|
groups = [];
|
|
@@ -606,29 +785,14 @@ function buildRows(session, config, colors) {
|
|
|
606
785
|
continue;
|
|
607
786
|
}
|
|
608
787
|
|
|
609
|
-
const
|
|
610
|
-
const segment = buildSegment(
|
|
611
|
-
widget.id,
|
|
612
|
-
session,
|
|
613
|
-
ownColors,
|
|
614
|
-
config.emojis,
|
|
615
|
-
config.ascii === true,
|
|
616
|
-
widget,
|
|
617
|
-
config
|
|
618
|
-
);
|
|
788
|
+
const segment = styledSegment(widget, widgetColors(widget, colors));
|
|
619
789
|
|
|
620
790
|
if (!segment) {
|
|
621
791
|
continue;
|
|
622
792
|
}
|
|
623
793
|
|
|
624
|
-
|
|
625
|
-
segment
|
|
626
|
-
|
|
627
|
-
if (splitEveryWidget) {
|
|
628
|
-
groups.push([segment]);
|
|
629
|
-
} else {
|
|
630
|
-
current.push(segment);
|
|
631
|
-
}
|
|
794
|
+
placed.push({ widget, group: current, index: current.length });
|
|
795
|
+
current.push(segment);
|
|
632
796
|
}
|
|
633
797
|
|
|
634
798
|
flushRow();
|
|
@@ -649,20 +813,36 @@ function rightCap(color, terminal, glyph) {
|
|
|
649
813
|
return glyph ? fg(color) + capBackground(terminal) + glyph : "";
|
|
650
814
|
}
|
|
651
815
|
|
|
816
|
+
// Compared as the terminal will draw them: at 256 colors two different colors
|
|
817
|
+
// can land on the same code, and without color everything is the same.
|
|
652
818
|
function sameColor(a, b) {
|
|
653
|
-
return
|
|
819
|
+
return bg(a) === bg(b);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Each solid Powerline glyph (U+E0B0 to U+E0BE, even code points) has a thin
|
|
823
|
+
// outline twin at the next code point. The round twins are drawn shorter than
|
|
824
|
+
// the cell in many Nerd Fonts, so round joins use a box-drawing line, which
|
|
825
|
+
// spans the full height.
|
|
826
|
+
function thinGlyph(glyph) {
|
|
827
|
+
const code = glyph.codePointAt(0);
|
|
828
|
+
if (glyph.length !== 1 || code < 0xe0b0 || code > 0xe0be || code % 2 !== 0) {
|
|
829
|
+
return glyph;
|
|
830
|
+
}
|
|
831
|
+
return code === 0xe0b4 || code === 0xe0b6 ? "│" : String.fromCodePoint(code + 1);
|
|
654
832
|
}
|
|
655
833
|
|
|
656
834
|
// A separator between two widgets of the same color would vanish, so it is
|
|
657
|
-
// drawn in the text color instead
|
|
835
|
+
// drawn as its thin outline in the text color instead, the way Powerline
|
|
836
|
+
// divides one color. ASCII joins are characters, not shapes, so they take the
|
|
837
|
+
// text color too, which reads on any pill. Inverted separators (meant for
|
|
658
838
|
// left-pointing glyphs) take the next widget's color on the previous one's.
|
|
659
|
-
function joinSegments(previous, next, glyph, invert, textColor) {
|
|
839
|
+
function joinSegments(previous, next, glyph, invert, textColor, ascii = false) {
|
|
660
840
|
if (!glyph) {
|
|
661
841
|
return "";
|
|
662
842
|
}
|
|
663
843
|
|
|
664
|
-
if (sameColor(previous.color, next.color)) {
|
|
665
|
-
return fg(next.textColor || textColor) + bg(next.color) + glyph;
|
|
844
|
+
if (ascii || sameColor(previous.color, next.color)) {
|
|
845
|
+
return fg(next.textColor || textColor) + bg(next.color) + thinGlyph(glyph);
|
|
666
846
|
}
|
|
667
847
|
|
|
668
848
|
return invert
|
|
@@ -674,7 +854,9 @@ function segmentContent(text, color, textColor, blink = false, bold = true) {
|
|
|
674
854
|
const blinkSeq = blink ? BLINK : "";
|
|
675
855
|
const blinkOff = blink ? BLINK_OFF : "";
|
|
676
856
|
// NORMAL clears bold left over from a previous widget.
|
|
677
|
-
|
|
857
|
+
const style = bg(color) + fg(textColor) + blinkSeq + (bold ? BOLD : NORMAL);
|
|
858
|
+
// Only exec output with "colors": true still holds escapes here.
|
|
859
|
+
return style + (text.includes("\x1b") ? restoreAfterReset(text, style) : text) + blinkOff;
|
|
678
860
|
}
|
|
679
861
|
|
|
680
862
|
function renderMeterSegment(segment, colors, ascii = false) {
|
|
@@ -685,6 +867,8 @@ function renderMeterSegment(segment, colors, ascii = false) {
|
|
|
685
867
|
const meter = fillMeter(displayPct, 12, segment.fill, segment.empty, segment.color, {
|
|
686
868
|
elapsedPct: segment.elapsedPct,
|
|
687
869
|
ascii,
|
|
870
|
+
style: segment.style,
|
|
871
|
+
textColor,
|
|
688
872
|
});
|
|
689
873
|
const bold = segment.bold !== false;
|
|
690
874
|
|
|
@@ -697,13 +881,19 @@ function renderMeterSegment(segment, colors, ascii = false) {
|
|
|
697
881
|
|
|
698
882
|
// Caps cycle per pill and separators per join, counted across the whole
|
|
699
883
|
// statusline, so lists with several glyphs alternate from left to right.
|
|
884
|
+
// Glyphs are drawn without bold, which some fonts draw wider or shifted, and
|
|
885
|
+
// the pill ends on the terminal's background so the padding after it stays
|
|
886
|
+
// clear even without an end cap. Without color, solid caps would be blobs of
|
|
887
|
+
// text color, so only ASCII brackets are kept.
|
|
700
888
|
function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }) {
|
|
701
889
|
const { powerline } = config;
|
|
702
890
|
const ascii = config.ascii === true;
|
|
703
891
|
const terminal = colors.terminal;
|
|
704
892
|
const pillIndex = counters.pill++;
|
|
705
|
-
const
|
|
706
|
-
const
|
|
893
|
+
const caps = ascii || getActiveDepth() !== "none";
|
|
894
|
+
const cap = (list, slot) => (caps ? glyphChar(pickGlyph(list, pillIndex), slot, ascii) : "");
|
|
895
|
+
const startCap = cap(powerline.startCaps, "startCap");
|
|
896
|
+
const endCap = cap(powerline.endCaps, "endCap");
|
|
707
897
|
|
|
708
898
|
let out = leftCap(group[0].color, terminal, startCap);
|
|
709
899
|
|
|
@@ -724,13 +914,22 @@ function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }
|
|
|
724
914
|
const slot = counters.separator++;
|
|
725
915
|
const glyph = glyphChar(pickGlyph(powerline.separators, slot), "separator", ascii);
|
|
726
916
|
const invert = powerline.separatorInvert[slot % (powerline.separators.length || 1)];
|
|
727
|
-
out +=
|
|
917
|
+
out +=
|
|
918
|
+
NORMAL + joinSegments(segment, group[i + 1], glyph, invert === true, colors.text, ascii);
|
|
728
919
|
}
|
|
729
920
|
}
|
|
730
921
|
|
|
731
|
-
|
|
922
|
+
const last = group[group.length - 1].color;
|
|
923
|
+
return out + NORMAL + (endCap ? rightCap(last, terminal, endCap) : capBackground(terminal));
|
|
924
|
+
}
|
|
732
925
|
|
|
733
|
-
|
|
926
|
+
// With joins off every widget is a pill of its own, one space from the next;
|
|
927
|
+
// the Flex Separator still decides where the row opens up.
|
|
928
|
+
function renderPills(group, colors, config, counters) {
|
|
929
|
+
if (config.powerline.enabled) {
|
|
930
|
+
return renderGroup(group, colors, config, counters);
|
|
931
|
+
}
|
|
932
|
+
return group.map((segment) => renderGroup([segment], colors, config, counters)).join(" ");
|
|
734
933
|
}
|
|
735
934
|
|
|
736
935
|
function joinFlex(parts, terminalWidth) {
|
|
@@ -768,7 +967,7 @@ function renderPillRows(rows, session, colors, config) {
|
|
|
768
967
|
return rows
|
|
769
968
|
.map((row) =>
|
|
770
969
|
joinFlex(
|
|
771
|
-
row.map((group) =>
|
|
970
|
+
row.map((group) => renderPills(group, colors, config, counters)),
|
|
772
971
|
session.terminalWidth
|
|
773
972
|
)
|
|
774
973
|
)
|
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
|
}
|