agy-powerline 1.3.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.
package/src/lib/git.js CHANGED
@@ -13,6 +13,15 @@ const SYNC_CACHE_NAME = ".git-sync-cache.json";
13
13
  const SYNC_CACHE_MAX = 20;
14
14
  const COMMIT = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
15
15
 
16
+ // A git call that runs past its time is skipped until a later refresh. Tests
17
+ // give git more time: on a busy CI machine a plain call can take longer.
18
+ const timeouts = { sync: 150, detail: 300 };
19
+ function setGitTimeouts(next) {
20
+ const previous = { ...timeouts };
21
+ Object.assign(timeouts, next);
22
+ return previous;
23
+ }
24
+
16
25
  // Only an absolute folder that exists can belong to the user's session. A
17
26
  // relative path (a Windows path read on Linux, for one) resolves against
18
27
  // wherever this process runs, and the upward walk would then report that
@@ -392,7 +401,7 @@ function getGitSync(gitDir, branch) {
392
401
  ["rev-list", "--left-right", "--count", `${branch}...${branch}@{upstream}`, "--"],
393
402
  {
394
403
  cwd: path.dirname(gitDir),
395
- timeout: 150,
404
+ timeout: timeouts.sync,
396
405
  stdio: ["ignore", "pipe", "ignore"],
397
406
  encoding: "utf8",
398
407
  // Antigravity may run the statusline without a console; without this,
@@ -414,7 +423,7 @@ function getGitSync(gitDir, branch) {
414
423
  // A timeout or missing git is worth knowing about when ahead/behind
415
424
  // counts never show.
416
425
  if (err.code === "ETIMEDOUT" || err.signal === "SIGTERM") {
417
- logWarning("GIT_TIMEOUT", { timeoutMs: 150 });
426
+ logWarning("GIT_TIMEOUT", { timeoutMs: timeouts.sync });
418
427
  } else if (err.code === "ENOENT") {
419
428
  logWarning("GIT_NOT_FOUND");
420
429
  }
@@ -437,7 +446,6 @@ const DETAIL_NEEDS = {
437
446
  const STATUS_CACHE_NAME = ".git-status-cache.json";
438
447
  const STATUS_CACHE_MAX = 20;
439
448
  const STATUS_TTL_MS = 5000;
440
- const DETAIL_TIMEOUT_MS = 300;
441
449
 
442
450
  // No optional locks: the bar must never hold the index lock and make the
443
451
  // user's own git commands fail. The -c flags and the submodule skip keep git
@@ -559,7 +567,7 @@ function runGitDetail(cwd, args) {
559
567
  const { execFileSync } = require("child_process");
560
568
  return execFileSync("git", args, {
561
569
  cwd,
562
- timeout: DETAIL_TIMEOUT_MS,
570
+ timeout: timeouts.detail,
563
571
  stdio: ["ignore", "pipe", "ignore"],
564
572
  encoding: "utf8",
565
573
  maxBuffer: 16 * 1024 * 1024,
@@ -567,7 +575,7 @@ function runGitDetail(cwd, args) {
567
575
  });
568
576
  } catch (err) {
569
577
  if (err.code === "ETIMEDOUT" || err.signal === "SIGTERM") {
570
- logWarning("GIT_TIMEOUT", { timeoutMs: DETAIL_TIMEOUT_MS });
578
+ logWarning("GIT_TIMEOUT", { timeoutMs: timeouts.detail });
571
579
  } else if (err.code === "ENOENT") {
572
580
  logWarning("GIT_NOT_FOUND");
573
581
  }
@@ -767,4 +775,5 @@ module.exports = {
767
775
  getGitDetails,
768
776
  resolveGitDetails,
769
777
  clearGitDetailsCache,
778
+ setGitTimeouts,
770
779
  };
@@ -3,6 +3,7 @@
3
3
  const {
4
4
  rgb,
5
5
  fg,
6
+ mix,
6
7
  RESET,
7
8
  DIM,
8
9
  BOLD,
@@ -16,6 +17,7 @@ const {
16
17
  basenameOf,
17
18
  homePath,
18
19
  formatResetText,
20
+ formatClockTime,
19
21
  paceIndex,
20
22
  paceGlyph,
21
23
  formatGitSync,
@@ -24,7 +26,8 @@ const {
24
26
  gitFileParts,
25
27
  formatTasks,
26
28
  formatMode,
27
- clipText,
29
+ limitWidth,
30
+ shortenPath,
28
31
  formatTokens,
29
32
  formatContextTokens,
30
33
  shortModelName,
@@ -41,10 +44,12 @@ const {
41
44
  resolveColor,
42
45
  widgetColors,
43
46
  widgetTag,
47
+ OS_ICONS,
44
48
  isRaw,
45
49
  isPaced,
46
50
  METER_STYLES,
47
51
  meterStyle,
52
+ hiddenByThreshold,
48
53
  hasNoData,
49
54
  RAM_CAUTION,
50
55
  RAM_DANGER,
@@ -143,27 +148,41 @@ function renderDotMeter(
143
148
  : colorForUsedPct(pct, palette, options));
144
149
 
145
150
  const marker = paceIndex(options.elapsedPct, width);
146
- const [full, blank] = METER_STYLES[options.style] || METER_STYLES.dots;
151
+ const [full, blank, pace] = METER_STYLES[options.style] || METER_STYLES.dots;
152
+ // A Classic meter is one color: the empty glyph's own texture (░) sets it
153
+ // apart, and the pace marker is a thin line.
154
+ const classic = options.classic === true;
155
+ const track = palette.track || rgb(60, 65, 75);
156
+ // ramp shades each filled cell by its place, from the theme's ok color to
157
+ // caution to danger, over the track; rings keeps the fill's color, faded.
158
+ const ramp = options.style === "ramp";
159
+ const trackColor = ramp
160
+ ? track
161
+ : options.style === "rings"
162
+ ? mix(color, track, 0.5)
163
+ : classic
164
+ ? color
165
+ : track;
166
+ const fillColor = (i) => (ramp ? rampColor(width > 1 ? i / (width - 1) : 0, palette) : color);
167
+ const markerColor = palette.text || LINES.white;
147
168
 
148
- if (marker < 0) {
169
+ if (marker < 0 && !ramp) {
149
170
  if (ascii) {
150
171
  return fg(color) + "#".repeat(filled) + DIM + "-".repeat(empty) + RESET;
151
172
  }
152
- const trackColor = palette.track || rgb(60, 65, 75);
153
173
  return fg(color) + full.repeat(filled) + fg(trackColor) + blank.repeat(empty) + RESET;
154
174
  }
155
175
 
156
176
  // The marker replaces the dot where the quota window currently is.
157
- const trackColor = palette.track || rgb(60, 65, 75);
158
- const markerColor = palette.text || LINES.white;
177
+ const markGlyph = ascii ? paceGlyph(true) : classic ? "│" : pace || paceGlyph(false);
159
178
  // ASCII empties are dim; NORMAL ends that without dropping a blink.
160
179
  const undim = ascii ? NORMAL : "";
161
180
  let out = "";
162
181
  for (let i = 0; i < width; i++) {
163
182
  if (i === marker) {
164
- out += undim + fg(markerColor) + paceGlyph(ascii);
183
+ out += undim + fg(markerColor) + markGlyph;
165
184
  } else if (i < filled) {
166
- out += undim + fg(color) + (ascii ? "#" : full);
185
+ out += undim + fg(fillColor(i)) + (ascii ? "#" : full);
167
186
  } else {
168
187
  out += ascii ? DIM + "-" : fg(trackColor) + blank;
169
188
  }
@@ -171,16 +190,27 @@ function renderDotMeter(
171
190
  return out + RESET;
172
191
  }
173
192
 
193
+ // A point along the meter, 0 to 1, as a color between ok, caution and danger.
194
+ function rampColor(at, palette) {
195
+ const ok = palette.quotaLow || LINES.green;
196
+ const caution = palette.quotaHigh || LINES.yellow;
197
+ const danger = palette.quotaCrit || LINES.red;
198
+ return at <= 0.5 ? mix(ok, caution, at * 2) : mix(caution, danger, (at - 0.5) * 2);
199
+ }
200
+
174
201
  function buildLinesSegment(id, session, palette, emojis, ascii = false, widget = {}, config = {}) {
175
202
  // Raw widgets keep only their value: no icon, no label.
176
203
  const raw = isRaw(widget, config);
177
- const tag = (icon) => widgetTag(id, { emojis, raw, icon });
178
- // Meters: 25 dots, or 12 when the bar is fitted to a narrow terminal.
204
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
205
+ // Meters: 25 cells, or 12 when the bar is fitted to a narrow terminal;
206
+ // Classic draws the same cells as blocks.
207
+ const classic = config.layout === "classic";
179
208
  const dots = widget?.compact ? 12 : 25;
180
- const style = meterStyle(widget, config, "lines");
209
+ const style = meterStyle(widget, config, classic ? "classic" : "lines");
181
210
  const pace = (quota, thresh) => ({
182
211
  ...thresh,
183
212
  style,
213
+ classic,
184
214
  elapsedPct: isPaced(widget, config) ? quota.elapsedPct : null,
185
215
  });
186
216
  const over200k = session.exceeds200k ? " >200k" : "";
@@ -240,11 +270,14 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
240
270
  return { thresh, blinkSeq, color };
241
271
  }
242
272
 
243
- if (hasNoData(id, session)) {
273
+ if (hasNoData(id, session) || hiddenByThreshold(id, widget, session)) {
244
274
  return null;
245
275
  }
246
276
 
247
277
  switch (id) {
278
+ case "clock":
279
+ return fg(palette.text) + tag() + formatClockTime(new Date(), config.clock) + RESET;
280
+
248
281
  case "model":
249
282
  if (!session.model) {
250
283
  return null;
@@ -252,7 +285,11 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
252
285
  return (
253
286
  fg(palette.model) +
254
287
  tag() +
255
- (widget?.short ? shortModelName(session.model, ascii) : session.model) +
288
+ limitWidth(
289
+ widget?.short ? shortModelName(session.model, ascii) : session.model,
290
+ widget,
291
+ ascii
292
+ ) +
256
293
  RESET
257
294
  );
258
295
 
@@ -328,6 +365,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
328
365
  ...thresh,
329
366
  color,
330
367
  style,
368
+ classic,
331
369
  }) +
332
370
  " " +
333
371
  fg(color) +
@@ -362,7 +400,11 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
362
400
  }
363
401
 
364
402
  case "cwd": {
365
- const dirname = homePath(session.cwd);
403
+ const dirname = limitWidth(
404
+ shortenPath(homePath(session.cwd), widget?.depth, ascii),
405
+ widget,
406
+ ascii
407
+ );
366
408
  if (!dirname) {
367
409
  return null;
368
410
  }
@@ -370,13 +412,28 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
370
412
  }
371
413
 
372
414
  case "project": {
373
- const proj = basenameOf(session.project);
415
+ const proj = limitWidth(basenameOf(session.project), widget, ascii);
374
416
  if (!proj) {
375
417
  return null;
376
418
  }
377
419
  return fg(palette.location) + tag() + proj + RESET;
378
420
  }
379
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
+
380
437
  case "git": {
381
438
  if (!session.git || !session.git.branch) {
382
439
  return null;
@@ -384,11 +441,12 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
384
441
  const opStr = widget.operation === false ? "" : formatGitOperation(session.git.operation);
385
442
  const syncStr = formatGitSync(session.git.ahead, session.git.behind, ascii);
386
443
  const syncPart = syncStr ? ` ${syncStr}` : "";
444
+ const branch = limitWidth(session.git.branch, widget, ascii);
387
445
  return (
388
446
  fg(palette.git) +
389
447
  tag() +
390
448
  "(" +
391
- session.git.branch +
449
+ branch +
392
450
  (session.git.dirty ? `${fg(palette.gitDirty)}*` : "") +
393
451
  (opStr ? `${fg(palette.gitDirty)} ${opStr}` : "") +
394
452
  (syncPart ? `${fg(palette.git)}${syncPart}` : "") +
@@ -442,7 +500,9 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
442
500
  if (!session.gitDetails?.worktree) {
443
501
  return null;
444
502
  }
445
- return fg(palette.git) + tag() + session.gitDetails.worktree + RESET;
503
+ return (
504
+ fg(palette.git) + tag() + limitWidth(session.gitDetails.worktree, widget, ascii) + RESET
505
+ );
446
506
 
447
507
  case "plan":
448
508
  if (!session.plan) {
@@ -479,7 +539,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
479
539
  if (!session.title) {
480
540
  return null;
481
541
  }
482
- 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;
483
543
 
484
544
  case "sandbox": {
485
545
  if (!session.sandbox) {
@@ -576,11 +636,15 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
576
636
  if (!widget?.text) {
577
637
  return null;
578
638
  }
579
- return fg(palette.text) + tag() + widget.text + RESET;
639
+ return fg(palette.text) + tag() + limitWidth(widget.text, widget, ascii) + RESET;
580
640
  }
581
641
 
582
642
  case "exec": {
583
- 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);
584
648
  if (!output) {
585
649
  return null;
586
650
  }
@@ -707,7 +771,7 @@ function renderLinesLayout(session, emojis, ascii = false, colors, config) {
707
771
  return RESET;
708
772
  }
709
773
 
710
- const pipe = ` ${DIM}${ascii ? "|" : "│"}${RESET} `;
774
+ const pipe = ` ${DIM}${ascii || config.layout === "classic" ? "|" : "│"}${RESET} `;
711
775
 
712
776
  return (
713
777
  rows
@@ -715,7 +779,8 @@ function renderLinesLayout(session, emojis, ascii = false, colors, config) {
715
779
  const parts = groups.map((group) => group.join(pipe));
716
780
  return joinFlex(parts, session.terminalWidth);
717
781
  })
718
- .join("\n") + RESET
782
+ // rowGap: a line holding a space, as in Pills, so a host keeps it.
783
+ .join(config.rowGap === true ? "\n \n" : "\n") + RESET
719
784
  );
720
785
  }
721
786
 
@@ -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,
@@ -67,7 +71,12 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
67
71
  const ratio = Math.max(0, Math.min(100, Number(pct))) / 100;
68
72
  const used = Math.round(ratio * width);
69
73
  const marker = paceIndex(pace.elapsedPct, width);
70
- if (METER_STYLES[pace.style]) {
74
+ // pips, the default, needs a Unicode font: ASCII mode keeps the cells.
75
+ const pips = pace.style === "pips";
76
+ if (pips && !pace.ascii && getActiveDepth() !== "none") {
77
+ return pipMeter(used, marker, width, fillColor, emptyColor) + bg(segmentColor);
78
+ }
79
+ if (METER_STYLES[pace.style] && !(pips && pace.ascii)) {
71
80
  return glyphMeter(used, marker, width, METER_STYLES[pace.style], pace);
72
81
  }
73
82
  // The fill is a background color; without color it is drawn with glyphs.
@@ -87,10 +96,30 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
87
96
  return meter + bg(segmentColor);
88
97
  }
89
98
 
99
+ // Small dots on the pill's own color: the filled ones in the light fill color,
100
+ // the rest in the dark track color, the same pair cells use. The pace marker
101
+ // is a larger dot in the other color, so nothing in the meter is a line.
102
+ function pipMeter(used, marker, width, fillColor, emptyColor) {
103
+ let meter = "";
104
+ let color = null;
105
+
106
+ for (let i = 0; i < width; i++) {
107
+ const filled = i < used;
108
+ const next = i === marker ? (filled ? emptyColor : fillColor) : filled ? fillColor : emptyColor;
109
+ if (next !== color) {
110
+ meter += fg(next);
111
+ color = next;
112
+ }
113
+ meter += i === marker ? METER_STYLES.pips[2] : METER_STYLES.pips[0];
114
+ }
115
+
116
+ return meter;
117
+ }
118
+
90
119
  // A glyph style draws on the pill's own color, with no cells: filled glyphs in
91
120
  // the pill's text color (readable on the pill, gradients included), empty
92
121
  // ones in the same color dimmed. ASCII mode keeps # and -.
93
- function glyphMeter(used, marker, width, [full, empty], { ascii, textColor }) {
122
+ function glyphMeter(used, marker, width, [full, empty, pace], { ascii, textColor }) {
94
123
  let meter = fg(textColor);
95
124
  let dim = false;
96
125
 
@@ -101,7 +130,7 @@ function glyphMeter(used, marker, width, [full, empty], { ascii, textColor }) {
101
130
  dim = blank;
102
131
  }
103
132
  if (i === marker) {
104
- meter += paceGlyph(ascii);
133
+ meter += (!ascii && pace) || paceGlyph(ascii);
105
134
  } else if (ascii) {
106
135
  meter += blank ? "-" : "#";
107
136
  } else {
@@ -144,7 +173,7 @@ function pill(color, text, blink = false) {
144
173
  function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, config = {}) {
145
174
  // Raw widgets keep only their value: no icon, no label.
146
175
  const raw = isRaw(widget, config);
147
- const tag = (icon) => widgetTag(id, { emojis, raw, icon });
176
+ const tag = (icon) => widgetTag(id, { emojis, raw, icon, widget, ascii });
148
177
  const lbl = (value, icon) => ` ${tag(icon)}${value} `;
149
178
  // The pill frames a colored meter; plain-text meters (ASCII) get brackets.
150
179
  const open = ascii ? "[" : "";
@@ -153,18 +182,27 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
153
182
  const pace = (quota) => (isPaced(widget, config) ? quota.elapsedPct : null);
154
183
  const over200k = session.exceeds200k ? " >200k" : "";
155
184
 
156
- if (hasNoData(id, session)) {
185
+ if (hasNoData(id, session) || hiddenByThreshold(id, widget, session)) {
157
186
  return null;
158
187
  }
159
188
 
160
189
  switch (id) {
190
+ case "clock":
191
+ return pill(colors.meta, lbl(formatClockTime(new Date(), config.clock)));
192
+
161
193
  case "model":
162
194
  if (!session.model) {
163
195
  return null;
164
196
  }
165
197
  return pill(
166
198
  colors.model,
167
- 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
+ )
168
206
  );
169
207
 
170
208
  case "quota": {
@@ -455,7 +493,11 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
455
493
  }
456
494
 
457
495
  case "cwd": {
458
- const name = homePath(session.cwd);
496
+ const name = limitWidth(
497
+ shortenPath(homePath(session.cwd), widget?.depth, ascii),
498
+ widget,
499
+ ascii
500
+ );
459
501
  if (!name) {
460
502
  return null;
461
503
  }
@@ -463,13 +505,28 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
463
505
  }
464
506
 
465
507
  case "project": {
466
- const name = basenameOf(session.project);
508
+ const name = limitWidth(basenameOf(session.project), widget, ascii);
467
509
  if (!name) {
468
510
  return null;
469
511
  }
470
512
  return pill(colors.meta, lbl(name));
471
513
  }
472
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
+
473
530
  case "git": {
474
531
  // Antigravity can send vcs.type without a branch; an empty pill says nothing.
475
532
  if (!session.git?.branch) {
@@ -480,9 +537,10 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
480
537
  const opPart = opStr ? `|${opStr}` : "";
481
538
  const syncStr = formatGitSync(session.git.ahead, session.git.behind, ascii);
482
539
  const syncPart = syncStr ? ` ${syncStr}` : "";
540
+ const branch = limitWidth(session.git.branch, widget, ascii);
483
541
  return pill(
484
542
  session.git.dirty ? colors.gitDirty : colors.git,
485
- lbl(`${mark}${session.git.branch}${opPart}${syncPart}`)
543
+ lbl(`${mark}${branch}${opPart}${syncPart}`)
486
544
  );
487
545
  }
488
546
 
@@ -520,7 +578,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
520
578
  if (!session.gitDetails?.worktree) {
521
579
  return null;
522
580
  }
523
- return pill(colors.git, lbl(session.gitDetails.worktree));
581
+ return pill(colors.git, lbl(limitWidth(session.gitDetails.worktree, widget, ascii)));
524
582
 
525
583
  case "plan":
526
584
  if (!session.plan) {
@@ -565,7 +623,7 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
565
623
  if (!session.title) {
566
624
  return null;
567
625
  }
568
- return pill(colors.meta, lbl(clipText(session.title, TITLE_MAX, ascii)));
626
+ return pill(colors.meta, lbl(limitWidth(session.title, widget, ascii, TITLE_MAX)));
569
627
 
570
628
  case "ram": {
571
629
  const pct = session.ramPct ?? 0;
@@ -654,11 +712,15 @@ function buildSegment(id, session, colors, emojis, ascii = false, widget = {}, c
654
712
  if (!widget?.text) {
655
713
  return null;
656
714
  }
657
- return pill(colors.meta, lbl(widget.text));
715
+ return pill(colors.meta, lbl(limitWidth(widget.text, widget, ascii)));
658
716
  }
659
717
 
660
718
  case "exec": {
661
- 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);
662
724
  if (!output) {
663
725
  return null;
664
726
  }
@@ -686,7 +748,6 @@ function buildRows(session, config, colors) {
686
748
  // The widgets shown in the current row, so a gradient can be spread over
687
749
  // exactly those once the row is complete.
688
750
  let placed = [];
689
- const splitEveryWidget = !config.powerline.enabled;
690
751
  const gradient = gradientStops(config.gradient);
691
752
 
692
753
  function styledSegment(widget, ownColors) {
@@ -767,13 +828,8 @@ function buildRows(session, config, colors) {
767
828
  continue;
768
829
  }
769
830
 
770
- const group = splitEveryWidget ? [] : current;
771
- placed.push({ widget, group, index: group.length });
772
- group.push(segment);
773
-
774
- if (splitEveryWidget) {
775
- groups.push(group);
776
- }
831
+ placed.push({ widget, group: current, index: current.length });
832
+ current.push(segment);
777
833
  }
778
834
 
779
835
  flushRow();
@@ -794,20 +850,36 @@ function rightCap(color, terminal, glyph) {
794
850
  return glyph ? fg(color) + capBackground(terminal) + glyph : "";
795
851
  }
796
852
 
853
+ // Compared as the terminal will draw them: at 256 colors two different colors
854
+ // can land on the same code, and without color everything is the same.
797
855
  function sameColor(a, b) {
798
- return String(a) === String(b);
856
+ return bg(a) === bg(b);
857
+ }
858
+
859
+ // Each solid Powerline glyph (U+E0B0 to U+E0BE, even code points) has a thin
860
+ // outline twin at the next code point. The round twins are drawn shorter than
861
+ // the cell in many Nerd Fonts, so round joins use a box-drawing line, which
862
+ // spans the full height.
863
+ function thinGlyph(glyph) {
864
+ const code = glyph.codePointAt(0);
865
+ if (glyph.length !== 1 || code < 0xe0b0 || code > 0xe0be || code % 2 !== 0) {
866
+ return glyph;
867
+ }
868
+ return code === 0xe0b4 || code === 0xe0b6 ? "│" : String.fromCodePoint(code + 1);
799
869
  }
800
870
 
801
871
  // A separator between two widgets of the same color would vanish, so it is
802
- // drawn in the text color instead. Inverted separators (meant for
872
+ // drawn as its thin outline in the text color instead, the way Powerline
873
+ // divides one color. ASCII joins are characters, not shapes, so they take the
874
+ // text color too, which reads on any pill. Inverted separators (meant for
803
875
  // left-pointing glyphs) take the next widget's color on the previous one's.
804
- function joinSegments(previous, next, glyph, invert, textColor) {
876
+ function joinSegments(previous, next, glyph, invert, textColor, ascii = false) {
805
877
  if (!glyph) {
806
878
  return "";
807
879
  }
808
880
 
809
- if (sameColor(previous.color, next.color)) {
810
- return fg(next.textColor || textColor) + bg(next.color) + glyph;
881
+ if (ascii || sameColor(previous.color, next.color)) {
882
+ return fg(next.textColor || textColor) + bg(next.color) + thinGlyph(glyph);
811
883
  }
812
884
 
813
885
  return invert
@@ -846,13 +918,19 @@ function renderMeterSegment(segment, colors, ascii = false) {
846
918
 
847
919
  // Caps cycle per pill and separators per join, counted across the whole
848
920
  // statusline, so lists with several glyphs alternate from left to right.
921
+ // Glyphs are drawn without bold, which some fonts draw wider or shifted, and
922
+ // the pill ends on the terminal's background so the padding after it stays
923
+ // clear even without an end cap. Without color, solid caps would be blobs of
924
+ // text color, so only ASCII brackets are kept.
849
925
  function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }) {
850
926
  const { powerline } = config;
851
927
  const ascii = config.ascii === true;
852
928
  const terminal = colors.terminal;
853
929
  const pillIndex = counters.pill++;
854
- const startCap = glyphChar(pickGlyph(powerline.startCaps, pillIndex), "startCap", ascii);
855
- const endCap = glyphChar(pickGlyph(powerline.endCaps, pillIndex), "endCap", ascii);
930
+ const caps = ascii || getActiveDepth() !== "none";
931
+ const cap = (list, slot) => (caps ? glyphChar(pickGlyph(list, pillIndex), slot, ascii) : "");
932
+ const startCap = cap(powerline.startCaps, "startCap");
933
+ const endCap = cap(powerline.endCaps, "endCap");
856
934
 
857
935
  let out = leftCap(group[0].color, terminal, startCap);
858
936
 
@@ -873,13 +951,22 @@ function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }
873
951
  const slot = counters.separator++;
874
952
  const glyph = glyphChar(pickGlyph(powerline.separators, slot), "separator", ascii);
875
953
  const invert = powerline.separatorInvert[slot % (powerline.separators.length || 1)];
876
- out += joinSegments(segment, group[i + 1], glyph, invert === true, colors.text);
954
+ out +=
955
+ NORMAL + joinSegments(segment, group[i + 1], glyph, invert === true, colors.text, ascii);
877
956
  }
878
957
  }
879
958
 
880
- out += rightCap(group[group.length - 1].color, terminal, endCap);
959
+ const last = group[group.length - 1].color;
960
+ return out + NORMAL + (endCap ? rightCap(last, terminal, endCap) : capBackground(terminal));
961
+ }
881
962
 
882
- return out;
963
+ // With joins off every widget is a pill of its own, one space from the next;
964
+ // the Flex Separator still decides where the row opens up.
965
+ function renderPills(group, colors, config, counters) {
966
+ if (config.powerline.enabled) {
967
+ return renderGroup(group, colors, config, counters);
968
+ }
969
+ return group.map((segment) => renderGroup([segment], colors, config, counters)).join(" ");
883
970
  }
884
971
 
885
972
  function joinFlex(parts, terminalWidth) {
@@ -917,7 +1004,7 @@ function renderPillRows(rows, session, colors, config) {
917
1004
  return rows
918
1005
  .map((row) =>
919
1006
  joinFlex(
920
- row.map((group) => renderGroup(group, colors, config, counters)),
1007
+ row.map((group) => renderPills(group, colors, config, counters)),
921
1008
  session.terminalWidth
922
1009
  )
923
1010
  )