agy-powerline 1.3.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.
@@ -3,6 +3,7 @@
3
3
  const {
4
4
  rgb,
5
5
  fg,
6
+ mix,
6
7
  RESET,
7
8
  DIM,
8
9
  BOLD,
@@ -143,27 +144,41 @@ function renderDotMeter(
143
144
  : colorForUsedPct(pct, palette, options));
144
145
 
145
146
  const marker = paceIndex(options.elapsedPct, width);
146
- const [full, blank] = METER_STYLES[options.style] || METER_STYLES.dots;
147
+ const [full, blank, pace] = METER_STYLES[options.style] || METER_STYLES.dots;
148
+ // A Classic meter is one color: the empty glyph's own texture (░) sets it
149
+ // apart, and the pace marker is a thin line.
150
+ const classic = options.classic === true;
151
+ const track = palette.track || rgb(60, 65, 75);
152
+ // ramp shades each filled cell by its place, from the theme's ok color to
153
+ // caution to danger, over the track; rings keeps the fill's color, faded.
154
+ const ramp = options.style === "ramp";
155
+ const trackColor = ramp
156
+ ? track
157
+ : options.style === "rings"
158
+ ? mix(color, track, 0.5)
159
+ : classic
160
+ ? color
161
+ : track;
162
+ const fillColor = (i) => (ramp ? rampColor(width > 1 ? i / (width - 1) : 0, palette) : color);
163
+ const markerColor = palette.text || LINES.white;
147
164
 
148
- if (marker < 0) {
165
+ if (marker < 0 && !ramp) {
149
166
  if (ascii) {
150
167
  return fg(color) + "#".repeat(filled) + DIM + "-".repeat(empty) + RESET;
151
168
  }
152
- const trackColor = palette.track || rgb(60, 65, 75);
153
169
  return fg(color) + full.repeat(filled) + fg(trackColor) + blank.repeat(empty) + RESET;
154
170
  }
155
171
 
156
172
  // 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;
173
+ const markGlyph = ascii ? paceGlyph(true) : classic ? "│" : pace || paceGlyph(false);
159
174
  // ASCII empties are dim; NORMAL ends that without dropping a blink.
160
175
  const undim = ascii ? NORMAL : "";
161
176
  let out = "";
162
177
  for (let i = 0; i < width; i++) {
163
178
  if (i === marker) {
164
- out += undim + fg(markerColor) + paceGlyph(ascii);
179
+ out += undim + fg(markerColor) + markGlyph;
165
180
  } else if (i < filled) {
166
- out += undim + fg(color) + (ascii ? "#" : full);
181
+ out += undim + fg(fillColor(i)) + (ascii ? "#" : full);
167
182
  } else {
168
183
  out += ascii ? DIM + "-" : fg(trackColor) + blank;
169
184
  }
@@ -171,16 +186,27 @@ function renderDotMeter(
171
186
  return out + RESET;
172
187
  }
173
188
 
189
+ // A point along the meter, 0 to 1, as a color between ok, caution and danger.
190
+ function rampColor(at, palette) {
191
+ const ok = palette.quotaLow || LINES.green;
192
+ const caution = palette.quotaHigh || LINES.yellow;
193
+ const danger = palette.quotaCrit || LINES.red;
194
+ return at <= 0.5 ? mix(ok, caution, at * 2) : mix(caution, danger, (at - 0.5) * 2);
195
+ }
196
+
174
197
  function buildLinesSegment(id, session, palette, emojis, ascii = false, widget = {}, config = {}) {
175
198
  // Raw widgets keep only their value: no icon, no label.
176
199
  const raw = isRaw(widget, config);
177
200
  const tag = (icon) => widgetTag(id, { emojis, raw, icon });
178
- // Meters: 25 dots, or 12 when the bar is fitted to a narrow terminal.
201
+ // Meters: 25 cells, or 12 when the bar is fitted to a narrow terminal;
202
+ // Classic draws the same cells as blocks.
203
+ const classic = config.layout === "classic";
179
204
  const dots = widget?.compact ? 12 : 25;
180
- const style = meterStyle(widget, config, "lines");
205
+ const style = meterStyle(widget, config, classic ? "classic" : "lines");
181
206
  const pace = (quota, thresh) => ({
182
207
  ...thresh,
183
208
  style,
209
+ classic,
184
210
  elapsedPct: isPaced(widget, config) ? quota.elapsedPct : null,
185
211
  });
186
212
  const over200k = session.exceeds200k ? " >200k" : "";
@@ -328,6 +354,7 @@ function buildLinesSegment(id, session, palette, emojis, ascii = false, widget =
328
354
  ...thresh,
329
355
  color,
330
356
  style,
357
+ classic,
331
358
  }) +
332
359
  " " +
333
360
  fg(color) +
@@ -707,7 +734,7 @@ function renderLinesLayout(session, emojis, ascii = false, colors, config) {
707
734
  return RESET;
708
735
  }
709
736
 
710
- const pipe = ` ${DIM}${ascii ? "|" : "│"}${RESET} `;
737
+ const pipe = ` ${DIM}${ascii || config.layout === "classic" ? "|" : "│"}${RESET} `;
711
738
 
712
739
  return (
713
740
  rows
@@ -715,7 +742,8 @@ function renderLinesLayout(session, emojis, ascii = false, colors, config) {
715
742
  const parts = groups.map((group) => group.join(pipe));
716
743
  return joinFlex(parts, session.terminalWidth);
717
744
  })
718
- .join("\n") + RESET
745
+ // rowGap: a line holding a space, as in Pills, so a host keeps it.
746
+ .join(config.rowGap === true ? "\n \n" : "\n") + RESET
719
747
  );
720
748
  }
721
749
 
@@ -67,7 +67,12 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
67
67
  const ratio = Math.max(0, Math.min(100, Number(pct))) / 100;
68
68
  const used = Math.round(ratio * width);
69
69
  const marker = paceIndex(pace.elapsedPct, width);
70
- if (METER_STYLES[pace.style]) {
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)) {
71
76
  return glyphMeter(used, marker, width, METER_STYLES[pace.style], pace);
72
77
  }
73
78
  // The fill is a background color; without color it is drawn with glyphs.
@@ -87,10 +92,30 @@ function fillMeter(pct, width, fillColor, emptyColor, segmentColor, pace = {}) {
87
92
  return meter + bg(segmentColor);
88
93
  }
89
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
+
90
115
  // A glyph style draws on the pill's own color, with no cells: filled glyphs in
91
116
  // the pill's text color (readable on the pill, gradients included), empty
92
117
  // ones in the same color dimmed. ASCII mode keeps # and -.
93
- function glyphMeter(used, marker, width, [full, empty], { ascii, textColor }) {
118
+ function glyphMeter(used, marker, width, [full, empty, pace], { ascii, textColor }) {
94
119
  let meter = fg(textColor);
95
120
  let dim = false;
96
121
 
@@ -101,7 +126,7 @@ function glyphMeter(used, marker, width, [full, empty], { ascii, textColor }) {
101
126
  dim = blank;
102
127
  }
103
128
  if (i === marker) {
104
- meter += paceGlyph(ascii);
129
+ meter += (!ascii && pace) || paceGlyph(ascii);
105
130
  } else if (ascii) {
106
131
  meter += blank ? "-" : "#";
107
132
  } else {
@@ -686,7 +711,6 @@ function buildRows(session, config, colors) {
686
711
  // The widgets shown in the current row, so a gradient can be spread over
687
712
  // exactly those once the row is complete.
688
713
  let placed = [];
689
- const splitEveryWidget = !config.powerline.enabled;
690
714
  const gradient = gradientStops(config.gradient);
691
715
 
692
716
  function styledSegment(widget, ownColors) {
@@ -767,13 +791,8 @@ function buildRows(session, config, colors) {
767
791
  continue;
768
792
  }
769
793
 
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
- }
794
+ placed.push({ widget, group: current, index: current.length });
795
+ current.push(segment);
777
796
  }
778
797
 
779
798
  flushRow();
@@ -794,20 +813,36 @@ function rightCap(color, terminal, glyph) {
794
813
  return glyph ? fg(color) + capBackground(terminal) + glyph : "";
795
814
  }
796
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.
797
818
  function sameColor(a, b) {
798
- return String(a) === String(b);
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);
799
832
  }
800
833
 
801
834
  // 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
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
803
838
  // left-pointing glyphs) take the next widget's color on the previous one's.
804
- function joinSegments(previous, next, glyph, invert, textColor) {
839
+ function joinSegments(previous, next, glyph, invert, textColor, ascii = false) {
805
840
  if (!glyph) {
806
841
  return "";
807
842
  }
808
843
 
809
- if (sameColor(previous.color, next.color)) {
810
- 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);
811
846
  }
812
847
 
813
848
  return invert
@@ -846,13 +881,19 @@ function renderMeterSegment(segment, colors, ascii = false) {
846
881
 
847
882
  // Caps cycle per pill and separators per join, counted across the whole
848
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.
849
888
  function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }) {
850
889
  const { powerline } = config;
851
890
  const ascii = config.ascii === true;
852
891
  const terminal = colors.terminal;
853
892
  const pillIndex = counters.pill++;
854
- const startCap = glyphChar(pickGlyph(powerline.startCaps, pillIndex), "startCap", ascii);
855
- const endCap = glyphChar(pickGlyph(powerline.endCaps, pillIndex), "endCap", ascii);
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");
856
897
 
857
898
  let out = leftCap(group[0].color, terminal, startCap);
858
899
 
@@ -873,13 +914,22 @@ function renderGroup(group, colors, config, counters = { pill: 0, separator: 0 }
873
914
  const slot = counters.separator++;
874
915
  const glyph = glyphChar(pickGlyph(powerline.separators, slot), "separator", ascii);
875
916
  const invert = powerline.separatorInvert[slot % (powerline.separators.length || 1)];
876
- out += joinSegments(segment, group[i + 1], glyph, invert === true, colors.text);
917
+ out +=
918
+ NORMAL + joinSegments(segment, group[i + 1], glyph, invert === true, colors.text, ascii);
877
919
  }
878
920
  }
879
921
 
880
- out += rightCap(group[group.length - 1].color, terminal, endCap);
922
+ const last = group[group.length - 1].color;
923
+ return out + NORMAL + (endCap ? rightCap(last, terminal, endCap) : capBackground(terminal));
924
+ }
881
925
 
882
- return out;
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(" ");
883
933
  }
884
934
 
885
935
  function joinFlex(parts, terminalWidth) {
@@ -917,7 +967,7 @@ function renderPillRows(rows, session, colors, config) {
917
967
  return rows
918
968
  .map((row) =>
919
969
  joinFlex(
920
- row.map((group) => renderGroup(group, colors, config, counters)),
970
+ row.map((group) => renderPills(group, colors, config, counters)),
921
971
  session.terminalWidth
922
972
  )
923
973
  )
package/src/lib/shared.js CHANGED
@@ -139,7 +139,8 @@ function renderBar(data, config, overrides) {
139
139
  }
140
140
 
141
141
  function renderLayout(session, config, emojis, ascii, colors) {
142
- if (config.layout === "lines") {
142
+ // Classic is Lines with block meters and | dividers.
143
+ if (config.layout === "lines" || config.layout === "classic") {
143
144
  return renderLinesLayout(session, emojis, ascii, colors, config);
144
145
  }
145
146
 
@@ -139,10 +139,12 @@ function widgetTag(id, { emojis = true, raw = false, icon } = {}) {
139
139
  return `${icon ?? tag.icon} ${tag.iconOnly ? "" : named}`;
140
140
  }
141
141
 
142
- // Meter styles: [filled, empty] glyphs. cells has none: Pills draws it with
143
- // background colors. Every glyph is one column wide for visibleWidth(); a
144
- // two-column glyph would overflow the meter and throw fitting off.
142
+ // Meter styles: [filled, empty] glyphs, and a pace marker for a style that
143
+ // has its own. cells has none: Pills draws it with background colors. Every
144
+ // glyph is one column wide for visibleWidth(); a two-column glyph would
145
+ // overflow the meter and throw fitting off.
145
146
  const METER_STYLES = {
147
+ pips: ["•", "•", "●"],
146
148
  cells: null,
147
149
  dots: ["●", "●"],
148
150
  blocks: ["█", "░"],
@@ -152,6 +154,8 @@ const METER_STYLES = {
152
154
  squares: ["■", "□"],
153
155
  braille: ["⣿", "⣀"],
154
156
  bars: ["▮", "▯"],
157
+ rings: ["●", "○"],
158
+ ramp: ["█", "░"],
155
159
  };
156
160
  const METER_STYLE_IDS = Object.keys(METER_STYLES);
157
161
 
@@ -160,11 +164,13 @@ function isMeterStyle(value) {
160
164
  }
161
165
 
162
166
  // The widget's "meter", else the global "meterStyle", else the layout's own;
163
- // cells exists only in Pills, so Lines draws dots instead.
167
+ // cells exists only in Pills, so the text layouts draw their own instead.
168
+ const OWN_METER = { pills: "pips", lines: "dots", classic: "blocks" };
169
+
164
170
  function meterStyle(widget, config, layout) {
165
- const own = layout === "lines" ? "dots" : "cells";
171
+ const own = OWN_METER[layout] ?? OWN_METER.pills;
166
172
  const id = [widget?.meter, config?.meterStyle].find(isMeterStyle) ?? own;
167
- return id === "cells" ? own : id;
173
+ return id === "cells" && layout !== "pills" ? own : id;
168
174
  }
169
175
 
170
176
  // Longest conversation title shown, in terminal columns.