git-watchtower 2.3.23 → 2.3.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.23",
3
+ "version": "2.3.25",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
package/src/ui/ansi.js CHANGED
@@ -602,25 +602,79 @@ function style(text, ...styles) {
602
602
  }
603
603
 
604
604
  /**
605
- * Pad a string on the right, truncating if too long (uses raw string length)
605
+ * Walk graphemes up to a column budget, returning the longest prefix
606
+ * whose visible width does not exceed `maxCols`. Drops one grapheme
607
+ * past the budget (mirrors how truncate() consumes — never mid-cut
608
+ * a wide char or surrogate pair).
609
+ * @param {string} str
610
+ * @param {number} maxCols
611
+ * @returns {string}
612
+ * @private
613
+ */
614
+ function _sliceByColumns(str, maxCols) {
615
+ if (maxCols <= 0) return '';
616
+ let acc = '';
617
+ let used = 0;
618
+ if (_graphemeSegmenter) {
619
+ for (const { segment } of _graphemeSegmenter.segment(str)) {
620
+ const w = visibleLength(segment);
621
+ if (used + w > maxCols) break;
622
+ acc += segment;
623
+ used += w;
624
+ }
625
+ } else {
626
+ for (const ch of str) {
627
+ const w = _codePointWidth(ch.codePointAt(0));
628
+ if (used + w > maxCols) break;
629
+ acc += ch;
630
+ used += w;
631
+ }
632
+ }
633
+ return acc;
634
+ }
635
+
636
+ /**
637
+ * Pad a string on the right to a target visible-column width.
638
+ * Truncates (without adding an ellipsis) when the input already
639
+ * exceeds the budget.
640
+ *
641
+ * Width is measured in display columns via visibleLength, NOT UTF-16
642
+ * `.length`. ASCII-only inputs keep their previous behaviour because
643
+ * `.length === visibleLength` for them. CJK / wide emoji / ZWJ
644
+ * clusters now pad by the columns they actually render in, and the
645
+ * truncate path slices on grapheme boundaries so a wide character
646
+ * isn't cut mid-byte.
647
+ *
648
+ * SGR-styled strings (e.g. `\x1b[31mhi\x1b[0m`) previously had their
649
+ * raw-byte length compared against `len`, so the colour wrapper made
650
+ * the string look "too long" and `substring(0, len)` cut the escape
651
+ * sequence in half — leaving the terminal in a stuck colour state.
652
+ * Now the width comparison uses visibleLength which strips ANSI
653
+ * before measuring, and the slice helper preserves SGR runs because
654
+ * they have width 0.
655
+ *
606
656
  * @param {string} str - String to pad
607
- * @param {number} len - Target length
657
+ * @param {number} len - Target visible-column width
608
658
  * @returns {string}
609
659
  */
610
660
  function padRight(str, len) {
611
- if (str.length >= len) return str.substring(0, len);
612
- return str + ' '.repeat(len - str.length);
661
+ const cols = visibleLength(str);
662
+ if (cols >= len) return _sliceByColumns(str, len);
663
+ return str + ' '.repeat(len - cols);
613
664
  }
614
665
 
615
666
  /**
616
- * Pad a string on the left, truncating if too long (uses raw string length)
667
+ * Pad a string on the left to a target visible-column width.
668
+ * Same column-vs-codeunit semantics as {@link padRight}.
669
+ *
617
670
  * @param {string} str - String to pad
618
- * @param {number} len - Target length
671
+ * @param {number} len - Target visible-column width
619
672
  * @returns {string}
620
673
  */
621
674
  function padLeft(str, len) {
622
- if (str.length >= len) return str.substring(0, len);
623
- return ' '.repeat(len - str.length) + str;
675
+ const cols = visibleLength(str);
676
+ if (cols >= len) return _sliceByColumns(str, len);
677
+ return ' '.repeat(len - cols) + str;
624
678
  }
625
679
 
626
680
  /**
@@ -26,6 +26,32 @@ const { isBaseBranch } = require('../git/pr');
26
26
  const { detectInstallSource, getUpdateCommand } = require('../utils/install-source');
27
27
  const { version: PACKAGE_VERSION } = require('../../package.json');
28
28
 
29
+ // ---------------------------------------------------------------------------
30
+ // Branch-row layout helpers
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Spaces to write after the (possibly truncated) branch name so the
35
+ * sparkline column lands at a consistent offset regardless of the
36
+ * branch name's actual byte count.
37
+ *
38
+ * Must measure in display columns, not UTF-16 code units. truncate()
39
+ * appends ansi.reset (4 bytes) on its long-string path; CJK / wide
40
+ * emoji span 1 code unit per 2 columns; ZWJ clusters render as 2
41
+ * columns from up to 8 units. Using raw .length would drift the
42
+ * sparkline by N columns per non-ASCII branch and by 4 for any
43
+ * truncated branch.
44
+ *
45
+ * Always returns at least 1 so consecutive writes never abut.
46
+ *
47
+ * @param {string} displayName - Already truncate()d branch name.
48
+ * @param {number} maxNameLen - Column budget the name was truncated to.
49
+ * @returns {number} Number of space characters to write.
50
+ */
51
+ function computeNamePadding(displayName, maxNameLen) {
52
+ return Math.max(1, maxNameLen - visibleLength(displayName) + 2);
53
+ }
54
+
29
55
  // ---------------------------------------------------------------------------
30
56
  // Compact number formatting
31
57
  // ---------------------------------------------------------------------------
@@ -239,7 +265,7 @@ function renderBranchList(state, write) {
239
265
  const fixedWidth = 27 + DIFF_TAG_FIXED_LEN;
240
266
  const maxNameLen = contentWidth - fixedWidth;
241
267
  const displayName = truncate(branch.name, maxNameLen);
242
- const namePadding = Math.max(1, maxNameLen - displayName.length + 2);
268
+ const namePadding = computeNamePadding(displayName, maxNameLen);
243
269
 
244
270
  if (isSelected) write(ansi.inverse);
245
271
  write(cursor);
@@ -1600,4 +1626,6 @@ module.exports = {
1600
1626
  renderStashConfirm,
1601
1627
  renderCleanupConfirm,
1602
1628
  renderUpdateModal,
1629
+ // Layout helpers — exported for unit testing
1630
+ computeNamePadding,
1603
1631
  };