agy-powerline 1.4.0 → 1.5.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.
@@ -374,7 +374,23 @@ function homePath(value) {
374
374
  return "";
375
375
  }
376
376
 
377
- const resolved = path.resolve(String(value));
377
+ const input = String(value);
378
+ if (input === "~" || /^~[\\/]/.test(input)) {
379
+ return input;
380
+ }
381
+ const windowsAbsolute = /^[a-z]:[\\/]|^\\\\/i.test(input);
382
+ const posixAbsolute = input.startsWith("/");
383
+
384
+ // Payload paths can come from another OS. Preserve absolute paths in that
385
+ // format instead of resolving them relative to this machine's cwd.
386
+ if (
387
+ (windowsAbsolute && process.platform !== "win32") ||
388
+ (posixAbsolute && process.platform === "win32")
389
+ ) {
390
+ return input;
391
+ }
392
+
393
+ const resolved = path.resolve(input);
378
394
  const home = path.resolve(os.homedir());
379
395
  const prefix =
380
396
  process.platform === "win32"
@@ -388,6 +404,60 @@ function homePath(value) {
388
404
  return resolved;
389
405
  }
390
406
 
407
+ function isValidDepth(value) {
408
+ return Number.isInteger(value) && value >= 1 && value <= 10;
409
+ }
410
+
411
+ // Keep the path's root and final `depth` folders, replacing any middle
412
+ // folders with one ellipsis. Accept either OS's path separators on every OS.
413
+ function shortenPath(value, depth, ascii = false) {
414
+ const text = String(value ?? "");
415
+ if (!text || !isValidDepth(depth)) {
416
+ return text;
417
+ }
418
+
419
+ let root = "";
420
+ let rest = text;
421
+ let separator = text.match(/[\\/]/)?.[0] || "/";
422
+ // A network share keeps \\server\share (or //server/share) as its root.
423
+ const unc = text.match(/^([\\/]{2}[^\\/]+[\\/][^\\/]+)([\\/])?(.*)$/);
424
+ const drive = text.match(/^([a-z]:)([\\/])(.*)$/i);
425
+
426
+ if (unc) {
427
+ root = unc[1];
428
+ separator = unc[2] || separator;
429
+ rest = unc[3];
430
+ } else if (drive) {
431
+ root = drive[1];
432
+ separator = drive[2];
433
+ rest = drive[3];
434
+ } else if (text === "~" || /^~[\\/]/.test(text)) {
435
+ root = "~";
436
+ if (text.length > 1) {
437
+ separator = text[1];
438
+ rest = text.slice(2);
439
+ } else {
440
+ rest = "";
441
+ }
442
+ } else if (text[0] === "/" || text[0] === "\\") {
443
+ root = text[0];
444
+ separator = text[0];
445
+ rest = text.slice(1);
446
+ }
447
+
448
+ const folders = rest.split(/[\\/]+/).filter(Boolean);
449
+ if (folders.length <= depth) {
450
+ return text;
451
+ }
452
+
453
+ const suffix = folders.slice(-depth).join(separator);
454
+ const ellipsis = ascii ? "..." : "…";
455
+ if (root === "/" || root === "\\") {
456
+ return `${root}${ellipsis}${separator}${suffix}`;
457
+ }
458
+ return `${root ? `${root}${separator}` : ""}${ellipsis}${separator}${suffix}`;
459
+ }
460
+
391
461
  function label(emojis, emoji, named, value) {
392
462
  if (emojis) {
393
463
  return named ? ` ${emoji} ${named}: ${value} ` : ` ${emoji} ${value} `;
@@ -560,15 +630,44 @@ function clipText(text, max, ascii = false) {
560
630
  const ellipsis = ascii ? "..." : "…";
561
631
  let out = "";
562
632
  let width = 0;
563
- for (const char of value) {
564
- const w = charWidth(char.codePointAt(0));
633
+ let index = 0;
634
+ const escape = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\]8;;[^\x1b\x07]*(?:\x07|\x1b\\))/y;
635
+ while (index < value.length) {
636
+ if (value[index] === "\x1b") {
637
+ escape.lastIndex = index;
638
+ const match = escape.exec(value);
639
+ if (match) {
640
+ out += match[0];
641
+ index += match[0].length;
642
+ continue;
643
+ }
644
+ }
645
+
646
+ const code = value.codePointAt(index);
647
+ const char = String.fromCodePoint(code);
648
+ const w = charWidth(code);
565
649
  if (width + w > max - ellipsis.length) {
566
650
  break;
567
651
  }
568
652
  out += char;
569
653
  width += w;
654
+ index += char.length;
570
655
  }
571
- return out.trimEnd() + ellipsis;
656
+ const clipped = out.trimEnd() + ellipsis;
657
+ return /\x1b\[[0-?]*[ -/]*m/.test(clipped) ? `${clipped}\x1b[0m` : clipped;
658
+ }
659
+
660
+ function isValidMaxWidth(value) {
661
+ return Number.isInteger(value) && value >= 4 && value <= 200;
662
+ }
663
+
664
+ function limitWidth(value, widget, ascii = false, fallback) {
665
+ const width = isValidMaxWidth(widget?.maxWidth)
666
+ ? widget.maxWidth
667
+ : isValidMaxWidth(fallback)
668
+ ? fallback
669
+ : null;
670
+ return width === null ? value : clipText(value, width, ascii);
572
671
  }
573
672
 
574
673
  // Removes escape sequences and control characters from text that did not
@@ -656,6 +755,10 @@ module.exports = {
656
755
  formatTasks,
657
756
  formatMode,
658
757
  clipText,
758
+ limitWidth,
759
+ isValidMaxWidth,
760
+ shortenPath,
761
+ isValidDepth,
659
762
  quotaColor,
660
763
  contextColor,
661
764
  resolveQuotaThresholds,
@@ -17,6 +17,7 @@ const {
17
17
  basenameOf,
18
18
  homePath,
19
19
  formatResetText,
20
+ formatClockTime,
20
21
  paceIndex,
21
22
  paceGlyph,
22
23
  formatGitSync,
@@ -25,7 +26,8 @@ const {
25
26
  gitFileParts,
26
27
  formatTasks,
27
28
  formatMode,
28
- clipText,
29
+ limitWidth,
30
+ shortenPath,
29
31
  formatTokens,
30
32
  formatContextTokens,
31
33
  shortModelName,
@@ -42,10 +44,12 @@ const {
42
44
  resolveColor,
43
45
  widgetColors,
44
46
  widgetTag,
47
+ OS_ICONS,
45
48
  isRaw,
46
49
  isPaced,
47
50
  METER_STYLES,
48
51
  meterStyle,
52
+ hiddenByThreshold,
49
53
  hasNoData,
50
54
  RAM_CAUTION,
51
55
  RAM_DANGER,
@@ -197,7 +201,7 @@ function rampColor(at, palette) {
197
201
  function buildLinesSegment(id, session, palette, emojis, ascii = false, widget = {}, config = {}) {
198
202
  // Raw widgets keep only their value: no icon, no label.
199
203
  const raw = isRaw(widget, config);
200
- const tag = (icon) => widgetTag(id, { emojis, raw, icon });
204
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
201
205
  // Meters: 25 cells, or 12 when the bar is fitted to a narrow terminal;
202
206
  // Classic draws the same cells as blocks.
203
207
  const classic = config.layout === "classic";
@@ -266,11 +270,14 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
266
270
  return { thresh, blinkSeq, color };
267
271
  }
268
272
 
269
- if (hasNoData(id, session)) {
273
+ if (hasNoData(id, session) || hiddenByThreshold(id, widget, session)) {
270
274
  return null;
271
275
  }
272
276
 
273
277
  switch (id) {
278
+ case "clock":
279
+ return fg(palette.text) + tag() + formatClockTime(new Date(), config.clock) + RESET;
280
+
274
281
  case "model":
275
282
  if (!session.model) {
276
283
  return null;
@@ -278,7 +285,11 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
278
285
  return (
279
286
  fg(palette.model) +
280
287
  tag() +
281
- (widget?.short ? shortModelName(session.model, ascii) : session.model) +
288
+ limitWidth(
289
+ widget?.short ? shortModelName(session.model, ascii) : session.model,
290
+ widget,
291
+ ascii
292
+ ) +
282
293
  RESET
283
294
  );
284
295
 
@@ -389,7 +400,11 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
389
400
  }
390
401
 
391
402
  case "cwd": {
392
- const dirname = homePath(session.cwd);
403
+ const dirname = limitWidth(
404
+ shortenPath(homePath(session.cwd), widget?.depth, ascii),
405
+ widget,
406
+ ascii
407
+ );
393
408
  if (!dirname) {
394
409
  return null;
395
410
  }
@@ -397,13 +412,28 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
397
412
  }
398
413
 
399
414
  case "project": {
400
- const proj = basenameOf(session.project);
415
+ const proj = limitWidth(basenameOf(session.project), widget, ascii);
401
416
  if (!proj) {
402
417
  return null;
403
418
  }
404
419
  return fg(palette.location) + tag() + proj + RESET;
405
420
  }
406
421
 
422
+ case "os": {
423
+ if (!session.os) {
424
+ return null;
425
+ }
426
+ return fg(palette.location) + tag(OS_ICONS[session.os]) + session.os + RESET;
427
+ }
428
+
429
+ case "host": {
430
+ if (!session.hostname) {
431
+ return null;
432
+ }
433
+ const name = session.username ? `${session.username}@${session.hostname}` : session.hostname;
434
+ return fg(palette.location) + tag() + name + RESET;
435
+ }
436
+
407
437
  case "git": {
408
438
  if (!session.git || !session.git.branch) {
409
439
  return null;
@@ -411,11 +441,12 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
411
441
  const opStr = widget.operation === false ? "" : formatGitOperation(session.git.operation);
412
442
  const syncStr = formatGitSync(session.git.ahead, session.git.behind, ascii);
413
443
  const syncPart = syncStr ? ` ${syncStr}` : "";
444
+ const branch = limitWidth(session.git.branch, widget, ascii);
414
445
  return (
415
446
  fg(palette.git) +
416
447
  tag() +
417
448
  "(" +
418
- session.git.branch +
449
+ branch +
419
450
  (session.git.dirty ? `${fg(palette.gitDirty)}*` : "") +
420
451
  (opStr ? `${fg(palette.gitDirty)} ${opStr}` : "") +
421
452
  (syncPart ? `${fg(palette.git)}${syncPart}` : "") +
@@ -469,7 +500,9 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
469
500
  if (!session.gitDetails?.worktree) {
470
501
  return null;
471
502
  }
472
- return fg(palette.git) + tag() + session.gitDetails.worktree + RESET;
503
+ return (
504
+ fg(palette.git) + tag() + limitWidth(session.gitDetails.worktree, widget, ascii) + RESET
505
+ );
473
506
 
474
507
  case "plan":
475
508
  if (!session.plan) {
@@ -506,7 +539,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
506
539
  if (!session.title) {
507
540
  return null;
508
541
  }
509
- return fg(palette.text) + tag() + clipText(session.title, TITLE_MAX, ascii) + RESET;
542
+ return fg(palette.text) + tag() + limitWidth(session.title, widget, ascii, TITLE_MAX) + RESET;
510
543
 
511
544
  case "sandbox": {
512
545
  if (!session.sandbox) {
@@ -603,11 +636,15 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
603
636
  if (!widget?.text) {
604
637
  return null;
605
638
  }
606
- return fg(palette.text) + tag() + widget.text + RESET;
639
+ return fg(palette.text) + tag() + limitWidth(widget.text, widget, ascii) + RESET;
607
640
  }
608
641
 
609
642
  case "exec": {
610
- const output = execWidgetOutput(widget, session);
643
+ // Only real output is cut: the [cmd] preview and blocked markers stay whole.
644
+ const output =
645
+ session.execMode === "run"
646
+ ? limitWidth(execWidgetOutput(widget, session), widget, ascii)
647
+ : execWidgetOutput(widget, session);
611
648
  if (!output) {
612
649
  return null;
613
650
  }
@@ -20,6 +20,7 @@ const { pickGlyph, glyphChar } = require("../powerline");
20
20
  const {
21
21
  quotaColor,
22
22
  contextColor,
23
+ formatClockTime,
23
24
  resolveQuotaThresholds,
24
25
  resolveContextThresholds,
25
26
  shouldBlink,
@@ -34,7 +35,8 @@ const {
34
35
  gitFileParts,
35
36
  formatTasks,
36
37
  formatMode,
37
- clipText,
38
+ limitWidth,
39
+ shortenPath,
38
40
  homePath,
39
41
  basenameOf,
40
42
  label,
@@ -48,10 +50,12 @@ const {
48
50
  resolveColor,
49
51
  widgetColors,
50
52
  widgetTag,
53
+ OS_ICONS,
51
54
  isRaw,
52
55
  isPaced,
53
56
  METER_STYLES,
54
57
  meterStyle,
58
+ hiddenByThreshold,
55
59
  hasNoData,
56
60
  RAM_CAUTION,
57
61
  RAM_DANGER,
@@ -169,7 +173,7 @@ function pill(color, text, blink = false) {
169
173
  function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, config = {}) {
170
174
  // Raw widgets keep only their value: no icon, no label.
171
175
  const raw = isRaw(widget, config);
172
- const tag = (icon) => widgetTag(id, { emojis, raw, icon });
176
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
173
177
  const lbl = (value, icon) => ` ${tag(icon)}${value} `;
174
178
  // The pill frames a colored meter; plain-text meters (ASCII) get brackets.
175
179
  const open = ascii ? "[" : "";
@@ -178,18 +182,27 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
178
182
  const pace = (quota) => (isPaced(widget, config) ? quota.elapsedPct : null);
179
183
  const over200k = session.exceeds200k ? " >200k" : "";
180
184
 
181
- if (hasNoData(id, session)) {
185
+ if (hasNoData(id, session) || hiddenByThreshold(id, widget, session)) {
182
186
  return null;
183
187
  }
184
188
 
185
189
  switch (id) {
190
+ case "clock":
191
+ return pill(colors.meta, lbl(formatClockTime(new Date(), config.clock)));
192
+
186
193
  case "model":
187
194
  if (!session.model) {
188
195
  return null;
189
196
  }
190
197
  return pill(
191
198
  colors.model,
192
- lbl(widget?.short ? shortModelName(session.model, ascii) : session.model)
199
+ lbl(
200
+ limitWidth(
201
+ widget?.short ? shortModelName(session.model, ascii) : session.model,
202
+ widget,
203
+ ascii
204
+ )
205
+ )
193
206
  );
194
207
 
195
208
  case "quota": {
@@ -480,7 +493,11 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
480
493
  }
481
494
 
482
495
  case "cwd": {
483
- const name = homePath(session.cwd);
496
+ const name = limitWidth(
497
+ shortenPath(homePath(session.cwd), widget?.depth, ascii),
498
+ widget,
499
+ ascii
500
+ );
484
501
  if (!name) {
485
502
  return null;
486
503
  }
@@ -488,13 +505,28 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
488
505
  }
489
506
 
490
507
  case "project": {
491
- const name = basenameOf(session.project);
508
+ const name = limitWidth(basenameOf(session.project), widget, ascii);
492
509
  if (!name) {
493
510
  return null;
494
511
  }
495
512
  return pill(colors.meta, lbl(name));
496
513
  }
497
514
 
515
+ case "os": {
516
+ if (!session.os) {
517
+ return null;
518
+ }
519
+ return pill(colors.meta, lbl(session.os, OS_ICONS[session.os]));
520
+ }
521
+
522
+ case "host": {
523
+ if (!session.hostname) {
524
+ return null;
525
+ }
526
+ const name = session.username ? `${session.username}@${session.hostname}` : session.hostname;
527
+ return pill(colors.meta, lbl(name));
528
+ }
529
+
498
530
  case "git": {
499
531
  // Antigravity can send vcs.type without a branch; an empty pill says nothing.
500
532
  if (!session.git?.branch) {
@@ -505,9 +537,10 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
505
537
  const opPart = opStr ? `|${opStr}` : "";
506
538
  const syncStr = formatGitSync(session.git.ahead, session.git.behind, ascii);
507
539
  const syncPart = syncStr ? ` ${syncStr}` : "";
540
+ const branch = limitWidth(session.git.branch, widget, ascii);
508
541
  return pill(
509
542
  session.git.dirty ? colors.gitDirty : colors.git,
510
- lbl(`${mark}${session.git.branch}${opPart}${syncPart}`)
543
+ lbl(`${mark}${branch}${opPart}${syncPart}`)
511
544
  );
512
545
  }
513
546
 
@@ -545,7 +578,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
545
578
  if (!session.gitDetails?.worktree) {
546
579
  return null;
547
580
  }
548
- return pill(colors.git, lbl(session.gitDetails.worktree));
581
+ return pill(colors.git, lbl(limitWidth(session.gitDetails.worktree, widget, ascii)));
549
582
 
550
583
  case "plan":
551
584
  if (!session.plan) {
@@ -590,7 +623,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
590
623
  if (!session.title) {
591
624
  return null;
592
625
  }
593
- return pill(colors.meta, lbl(clipText(session.title, TITLE_MAX, ascii)));
626
+ return pill(colors.meta, lbl(limitWidth(session.title, widget, ascii, TITLE_MAX)));
594
627
 
595
628
  case "ram": {
596
629
  const pct = session.ramPct ?? 0;
@@ -679,11 +712,15 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
679
712
  if (!widget?.text) {
680
713
  return null;
681
714
  }
682
- return pill(colors.meta, lbl(widget.text));
715
+ return pill(colors.meta, lbl(limitWidth(widget.text, widget, ascii)));
683
716
  }
684
717
 
685
718
  case "exec": {
686
- const output = execWidgetOutput(widget, session);
719
+ // Only real output is cut: the [cmd] preview and blocked markers stay whole.
720
+ const output =
721
+ session.execMode === "run"
722
+ ? limitWidth(execWidgetOutput(widget, session), widget, ascii)
723
+ : execWidgetOutput(widget, session);
687
724
  if (!output) {
688
725
  return null;
689
726
  }
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  const os = require("os");
4
- const { formatReset, resetInSeconds, firstText } = require("./formatters");
4
+ const { formatReset, resetInSeconds, firstText, stripControl } = require("./formatters");
5
5
  const { resolveGit, resolveGitDetails } = require("./git");
6
6
  const { resolveUsage } = require("./usage");
7
+ const { isWsl } = require("./paths");
7
8
 
8
9
  const FIVE_HOURS = 5 * 3600;
9
10
  const SEVEN_DAYS = 7 * 86400;
@@ -22,6 +23,46 @@ const QUOTA_BUCKETS = {
22
23
  // No quota window lasts a year: a reset further out is a broken value.
23
24
  const MAX_RESET_SECONDS = 366 * 86400;
24
25
 
26
+ // Node's platform ids, spelled the way people write them.
27
+ const OS_NAMES = {
28
+ win32: "Windows",
29
+ darwin: "macOS",
30
+ linux: "Linux",
31
+ android: "Android",
32
+ freebsd: "FreeBSD",
33
+ openbsd: "OpenBSD",
34
+ sunos: "SunOS",
35
+ aix: "AIX",
36
+ };
37
+
38
+ function currentOsName() {
39
+ if (process.platform === "linux" && isWsl()) {
40
+ return "WSL";
41
+ }
42
+ return OS_NAMES[process.platform] || process.platform;
43
+ }
44
+
45
+ function currentUserAndHost() {
46
+ let userInfoName = "";
47
+ try {
48
+ userInfoName = os.userInfo().username;
49
+ } catch {
50
+ // Some containers have no passwd entry for the current user.
51
+ }
52
+ const username =
53
+ [userInfoName, process.env.USER, process.env.USERNAME]
54
+ .map((name) => stripControl(name).trim())
55
+ .find(Boolean) || "";
56
+
57
+ let hostname = "";
58
+ try {
59
+ hostname = stripControl(os.hostname()).split(".")[0].trim();
60
+ } catch {
61
+ // The host widget hides when the OS cannot provide a name.
62
+ }
63
+ return { username, hostname };
64
+ }
65
+
25
66
  // Payload numbers can be strings, negative or absurd. The bar must never show
26
67
  // NaN, a negative count or more than 100%.
27
68
  function count(value) {
@@ -193,9 +234,15 @@ function extractSession(data, config, options = {}) {
193
234
  const cacheHitPct = cacheTotal > 0 ? Math.round((cacheReadTokens * 100) / cacheTotal) : 0;
194
235
 
195
236
  const modelName = firstText(data.model?.display_name, data.model?.id);
237
+ const uses = (id) => Boolean(config?.widgets?.some((widget) => widget?.id === id));
238
+ const osName = uses("os") ? currentOsName() : "";
239
+ const { username, hostname } = uses("host") ? currentUserAndHost() : {};
196
240
 
197
241
  return {
242
+ os: osName,
198
243
  ramPct,
244
+ username: username || "",
245
+ hostname: hostname || "",
199
246
  model: modelName,
200
247
  contextPct,
201
248
  contextSize,
@@ -254,5 +301,6 @@ module.exports = {
254
301
  readQuota,
255
302
  tightestQuota,
256
303
  contextUsedPercent,
304
+ currentUserAndHost,
257
305
  extractSession,
258
306
  };