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.
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const readline = require("readline");
2
4
  const { spawn } = require("child_process");
3
5
  const {
@@ -19,11 +21,10 @@ const {
19
21
  PRESETS,
20
22
  PRESET_ORDER,
21
23
  applyPreset,
22
- resolveQuotaThresholds,
23
- resolveContextThresholds,
24
24
  } = require("../lib/shared");
25
25
  const { resolveInstallDir } = require("../lib/paths");
26
26
  const { WIDGET_TAGS } = require("../lib/widget-style");
27
+ const { editFields: getEditFields } = require("../lib/widget-options");
27
28
  const { loadEditableConfig } = require("./config-guard");
28
29
  const {
29
30
  parseColor,
@@ -48,7 +49,7 @@ const {
48
49
  glyphName,
49
50
  } = require("../lib/powerline");
50
51
 
51
- const REPO_URL = "https://github.com/wassim-azi/agy-powerline";
52
+ const { REPO_URL } = require("./repo-url");
52
53
 
53
54
  const CSI = "\x1b[";
54
55
  const RESET = `${CSI}0m`;
@@ -294,7 +295,15 @@ const TAB_OF_SCREEN = {
294
295
  glyphs: "powerline",
295
296
  };
296
297
 
297
- const PRESET_ICONS = { default: "🎯", compact: "⚡", workspace: "📂", limits: "🔋", full: "🚀" };
298
+ const PRESET_ICONS = {
299
+ default: "🎯",
300
+ compact: "⚡",
301
+ agent: "🧠",
302
+ git: "🔀",
303
+ tokens: "📊",
304
+ quotas: "🔋",
305
+ full: "🚀",
306
+ };
298
307
 
299
308
  // Set by renderFrame so tests can draw a screen at a fixed size.
300
309
  let columnsOverride = null;
@@ -408,6 +417,7 @@ function previewParts(width) {
408
417
  // The toggle keys work on the widget list, so only that screen names them.
409
418
  const key = (letter) => (screen === "main" ? letter : "");
410
419
  const chips = [
420
+ ui.chip("gaps", preview.rowGap, key("v")),
411
421
  ui.chip("emojis", preview.emojis, key("e")),
412
422
  ui.chip("minimal", preview.minimalist, key("m")),
413
423
  ui.chip("pace", preview.pacing, key("r")),
@@ -415,10 +425,7 @@ function previewParts(width) {
415
425
  ui.chip("ascii", preview.ascii, key("i")),
416
426
  ];
417
427
  if (preview.layout === "pills") {
418
- chips.unshift(
419
- ui.chip("joins", preview.powerline.enabled, key("j")),
420
- ui.chip("gaps", preview.rowGap, key("v"))
421
- );
428
+ chips.unshift(ui.chip("joins", preview.powerline.enabled, key("j")));
422
429
  }
423
430
 
424
431
  return { title, bar: String(rendered).split("\n"), chips: chips.join(" ") };
@@ -749,151 +756,14 @@ function drawMain() {
749
756
  });
750
757
  }
751
758
 
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
759
  // Longest custom badge text; the badge is meant to be a word or two.
774
760
  const TEXT_MAX = 40;
775
761
 
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
762
  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;
763
+ return getEditFields(widget, config, {
764
+ colorName: (value) => (value === undefined ? "theme default" : colorName(value)),
765
+ cmdHelp: `The command runs only in agy, from your install folder. Edit it in ${getConfigPath()}.`,
766
+ });
897
767
  }
898
768
 
899
769
  // A theme slot's color name, in that color.
@@ -1165,21 +1035,38 @@ const toHex = (rgb) => `#${rgb.map((part) => part.toString(16).padStart(2, "0"))
1165
1035
 
1166
1036
  // One color as a pill in that color with the theme's pill text, the way the
1167
1037
  // 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"],
1038
+ function colorCell(label, rgb, ink) {
1039
+ return `${ui.pill(` ${label.padEnd(9)}`, rgb, ink)} ${dim(toHex(rgb))}`;
1040
+ }
1041
+
1042
+ // Widget colors on the left, states on the right; a blank line between rows
1043
+ // keeps the pills from touching.
1044
+ const PALETTE_ROWS = [
1045
+ [
1046
+ ["model", "Model"],
1047
+ ["fiveHour", "OK"],
1048
+ ],
1049
+ [
1050
+ ["context", "Context"],
1051
+ ["caution", "Caution"],
1052
+ ],
1053
+ [
1054
+ ["git", "Git"],
1055
+ ["warn", "Danger"],
1056
+ ],
1057
+ [
1058
+ ["weekly", "7d quota"],
1059
+ ["meta", "Details"],
1060
+ ],
1181
1061
  ];
1182
1062
 
1063
+ function paletteLines(colors) {
1064
+ return PALETTE_ROWS.flatMap((row, index) => [
1065
+ ...(index ? [""] : []),
1066
+ { raw: row.map(([slot, label]) => colorCell(label, colors[slot], colors.text)).join(" ") },
1067
+ ]);
1068
+ }
1069
+
1183
1070
  function themeDetails(id) {
1184
1071
  if (id === "custom") {
1185
1072
  return {
@@ -1203,9 +1090,7 @@ function themeDetails(id) {
1203
1090
  ...(background ? [{ raw: `${dim("For")} ${ui.tag(background)}` }] : []),
1204
1091
  "",
1205
1092
  heading("Palette"),
1206
- ...PALETTE_SLOTS.map(([slot, label]) =>
1207
- colorLine(label, theme.colors[slot], theme.colors.text)
1208
- ),
1093
+ ...paletteLines(theme.colors),
1209
1094
  "",
1210
1095
  heading("Sample"),
1211
1096
  ...themeSample(theme.colors),
@@ -1340,7 +1225,7 @@ const POWERLINE_HELP = {
1340
1225
  enabled:
1341
1226
  "On: adjacent widgets share one pill, with a separator between them. Off: every widget gets its own pill.",
1342
1227
  rowGap:
1343
- "Puts a blank line between Pills rows, so stacked pills don't touch. The bar takes more lines (three rows become five).",
1228
+ "Puts a blank line between rows, in every layout, so stacked pills don't touch and text rows get more air. The bar takes more lines (three rows become five).",
1344
1229
  style: "Sets start caps, end caps and separators in one go. Enter cycles the styles.",
1345
1230
  separators: "Glyphs between widgets of a pill. With several, they cycle per join, left to right.",
1346
1231
  startCaps: "Glyphs that open each pill. With several, they cycle per pill, left to right.",
@@ -1369,7 +1254,7 @@ function powerlineDetails(row) {
1369
1254
  function drawPowerline() {
1370
1255
  const items = POWERLINE_ROWS.map((row) => ({ text: row.label, value: powerlineRowValue(row) }));
1371
1256
  const notes =
1372
- config.layout === "lines"
1257
+ config.layout !== "pills"
1373
1258
  ? ["Powerline shapes apply to Pills. Switch layout with l."]
1374
1259
  : [
1375
1260
  "A style sets caps and separators together; edit a list to fine-tune it.",
@@ -1454,10 +1339,24 @@ function drawGlyphs() {
1454
1339
  });
1455
1340
  }
1456
1341
 
1342
+ // Three short paragraphs per layout, each under 90 columns so a wide panel
1343
+ // shows them unwrapped.
1457
1344
  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.",
1345
+ pills: [
1346
+ "Widgets sit in colored pills, joined by powerline caps and separators.",
1347
+ "Meters draw as small dots on the pill's color, with a larger dot as the pace marker.",
1348
+ "The Powerline tab sets the caps, separators, joins and the space between rows.",
1349
+ ],
1350
+ lines: [
1351
+ "Colored text on the terminal background, with thin │ dividers between widgets.",
1352
+ "Meters draw as dots in the ok, caution or danger color, over a dim track.",
1353
+ "Powerline shapes don't apply; the theme, widgets and space between rows do.",
1354
+ ],
1355
+ classic: [
1356
+ "Colored text on the terminal background, with | dividers between widgets.",
1357
+ "Meters draw as blocks in one color: █ filled, ░ empty and │ as the pace marker.",
1358
+ "Powerline shapes don't apply; the theme, widgets and space between rows do.",
1359
+ ],
1461
1360
  };
1462
1361
 
1463
1362
  function drawLayout() {
@@ -1471,10 +1370,9 @@ function drawLayout() {
1471
1370
  return screenFrame({
1472
1371
  title: "Layout",
1473
1372
  items,
1474
- notes: ["Both layouts use your widgets and theme; Powerline shapes apply to Pills."],
1475
1373
  details: hovered && {
1476
1374
  title: hovered.name,
1477
- lines: [`${hovered.description}.`, "", LAYOUT_HELP[hovered.id] ?? ""],
1375
+ lines: LAYOUT_HELP[hovered.id].flatMap((line, i) => (i ? ["", line] : [line])),
1478
1376
  },
1479
1377
  keys: [
1480
1378
  [ui.glyph.upDown, "preview"],
package/src/cli/doctor.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");
@@ -23,6 +25,7 @@ const {
23
25
  const { runStatuslineCommand } = require("../lib/run");
24
26
  const { detectColorDepth } = require("../lib/colors");
25
27
  const { readLog, getLogPath } = require("../lib/log");
28
+ const { USAGE_PERIODS, usageAvailable } = require("../lib/usage");
26
29
  const { version } = require("../../package.json");
27
30
 
28
31
  const DIM = "\x1b[2m";
@@ -144,6 +147,15 @@ async function doctorCommand(options = {}) {
144
147
  console.log(" The bar shows the defaults until you fix the file or delete it.");
145
148
  }
146
149
 
150
+ // Not a failure: the widgets just hide.
151
+ if (configState.config.widgets.some((widget) => USAGE_PERIODS[widget.id]) && !usageAvailable()) {
152
+ console.log();
153
+ console.log(
154
+ `${YELLOW} usage widgets need Node 22.13 or newer${RESET} (this is ${process.version}).`
155
+ );
156
+ console.log(" tokensToday and tokensWeek stay hidden until Node is updated.");
157
+ }
158
+
147
159
  const breaking = cmdBreakingChars(installDir);
148
160
  if (breaking.length) {
149
161
  failed += 1;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { readConfigFile, mergeConfig, setConfigDir } = require("../lib/shared");
6
+ const { resolveInstallDir, writeFileAtomic, PRIVATE_FILE_MODE } = require("../lib/paths");
7
+ const { printInvalidConfig } = require("./config-guard");
8
+ const { version } = require("../../package.json");
9
+
10
+ const DIM = "\x1b[2m";
11
+ const GREEN = "\x1b[32m";
12
+ const RED = "\x1b[31m";
13
+ const YELLOW = "\x1b[33m";
14
+ const RESET = "\x1b[0m";
15
+
16
+ // The config as the bar reads it, with the release that wrote it first, so an
17
+ // import can tell a file from a newer release.
18
+ function exportDocument(config) {
19
+ return { agyPowerline: { version }, ...mergeConfig(config) };
20
+ }
21
+
22
+ function execCommandCount(config) {
23
+ return (config.widgets || []).filter(
24
+ (widget) => widget.id === "exec" && String(widget.cmd || "").trim()
25
+ ).length;
26
+ }
27
+
28
+ async function exportCommand(file, options = {}) {
29
+ const installDir = resolveInstallDir();
30
+ if (!installDir) {
31
+ return;
32
+ }
33
+
34
+ setConfigDir(installDir);
35
+ const { config, error } = readConfigFile();
36
+ if (error) {
37
+ // Exporting the defaults would look like a backup of the user's bar.
38
+ printInvalidConfig(error);
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+
43
+ const body = JSON.stringify(exportDocument(config), null, 2) + "\n";
44
+
45
+ if (file) {
46
+ const target = path.resolve(String(file));
47
+ let existing = null;
48
+ try {
49
+ existing = fs.statSync(target);
50
+ } catch {
51
+ // Nothing there yet.
52
+ }
53
+ if (existing?.isDirectory()) {
54
+ console.error(`${RED}error${RESET} ${target} is a folder. Name a file to write.`);
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ if (existing && !options.force) {
59
+ console.error(`${RED}error${RESET} ${target} already exists. Pass --force to replace it.`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ // Owner-only, like the config itself: it can hold your commands.
64
+ writeFileAtomic(target, body, { mode: PRIVATE_FILE_MODE });
65
+ console.error(`${GREEN}✔${RESET} Config exported to ${DIM}${target}${RESET}`);
66
+ } else {
67
+ process.stdout.write(body);
68
+ }
69
+
70
+ // Notes go to stderr, so `export > file` keeps the JSON clean.
71
+ const commands = execCommandCount(config);
72
+ if (commands) {
73
+ console.error(
74
+ `${YELLOW}note${RESET} The export holds ${commands} exec widget command${commands === 1 ? "" : "s"}; check ${commands === 1 ? "it" : "them"} before sharing the file.`
75
+ );
76
+ }
77
+ }
78
+
79
+ module.exports = {
80
+ exportCommand,
81
+ exportDocument,
82
+ };
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ const {
4
+ saveConfig,
5
+ setConfigDir,
6
+ getConfigPath,
7
+ renderStatusline,
8
+ sampleData,
9
+ resolveColorDepth,
10
+ } = require("../lib/shared");
11
+ const { GRADIENTS, GRADIENT_ORDER, normalizeGradient, gradientStops } = require("../lib/themes");
12
+ const { bg, gradientColors, setActiveDepth, getActiveDepth } = require("../lib/colors");
13
+ const { resolveInstallDir } = require("../lib/paths");
14
+ const { loadEditableConfig } = require("./config-guard");
15
+
16
+ const DIM = "\x1b[2m";
17
+ const BOLD = "\x1b[1m";
18
+ const GREEN = "\x1b[32m";
19
+ const RED = "\x1b[31m";
20
+ const CYAN = "\x1b[36m";
21
+ const RESET = "\x1b[0m";
22
+
23
+ const STRIP_WIDTH = 24;
24
+
25
+ // A row of colored cells showing the gradient; empty without color.
26
+ function gradientStrip(value, config) {
27
+ const stops = gradientStops(value);
28
+ if (!stops) {
29
+ return "";
30
+ }
31
+ const previous = getActiveDepth();
32
+ setActiveDepth(resolveColorDepth({ configColor: config.color, env: process.env }));
33
+ try {
34
+ if (getActiveDepth() === "none") {
35
+ return "";
36
+ }
37
+ const cells = gradientColors(stops, STRIP_WIDTH).map((color) => `${bg(color)} `);
38
+ return `${cells.join("")}${RESET}`;
39
+ } finally {
40
+ setActiveDepth(previous);
41
+ }
42
+ }
43
+
44
+ function listGradients(config) {
45
+ const current = config.gradient || "off";
46
+
47
+ console.log();
48
+ console.log(`${BOLD} agy-powerline gradients${RESET}`);
49
+ console.log(` current ${CYAN}${current}${RESET} ${gradientStrip(config.gradient, config)}`);
50
+ console.log(` config ${DIM}${getConfigPath()}${RESET}`);
51
+ console.log();
52
+
53
+ const maxIdLen = Math.max(...GRADIENT_ORDER.map((id) => id.length), "off".length);
54
+
55
+ for (const id of [...GRADIENT_ORDER, "off"]) {
56
+ const isCurrent = id === current;
57
+ const marker = isCurrent ? `${GREEN}*${RESET}` : " ";
58
+ const idFormatted = isCurrent
59
+ ? `${BOLD}${CYAN}${id.padEnd(maxIdLen)}${RESET}`
60
+ : id.padEnd(maxIdLen);
61
+ const preview = id === "off" ? `${DIM}theme colors only${RESET}` : gradientStrip(id, config);
62
+ console.log(` ${marker} ${idFormatted} ${preview}`);
63
+ }
64
+
65
+ console.log();
66
+ console.log(` Run ${CYAN}npx agy-powerline gradient <name>${RESET} to color each row.`);
67
+ console.log(` Your own colors: ${CYAN}npx agy-powerline gradient "#ff5f6d-#ffc371"${RESET}`);
68
+ console.log();
69
+ }
70
+
71
+ async function gradientCommand(name, options = {}) {
72
+ const installDir = resolveInstallDir();
73
+ if (!installDir) {
74
+ return;
75
+ }
76
+
77
+ setConfigDir(installDir);
78
+ const config = loadEditableConfig();
79
+ if (!config) {
80
+ return;
81
+ }
82
+
83
+ if (!name || options.list) {
84
+ listGradients(config);
85
+ return;
86
+ }
87
+
88
+ const raw = String(name).trim();
89
+ const off = raw.toLowerCase() === "off";
90
+ const value = off ? null : normalizeGradient(raw);
91
+
92
+ if (!off && !value) {
93
+ console.error(`${RED}error${RESET} Unknown gradient "${raw}".`);
94
+ console.error(` Available gradients: ${GRADIENT_ORDER.join(", ")}, off`);
95
+ console.error(" Or 2 to 6 hex colors joined by dashes, e.g. #ff5f6d-#ffc371");
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+
100
+ if (off) {
101
+ delete config.gradient;
102
+ } else {
103
+ config.gradient = value;
104
+ }
105
+ saveConfig(config);
106
+
107
+ const title = off ? "off" : GRADIENTS[value]?.name || value;
108
+
109
+ console.log();
110
+ console.log(`${GREEN}✔${RESET} Gradient set to ${BOLD}${title}${RESET}`);
111
+ console.log(` config ${DIM}${getConfigPath()}${RESET}`);
112
+ console.log();
113
+ console.log(" Preview:");
114
+ console.log(renderStatusline(sampleData(), config));
115
+ console.log();
116
+ }
117
+
118
+ module.exports = {
119
+ gradientCommand,
120
+ gradientStrip,
121
+ };