git-watchtower 2.3.10 → 2.3.12
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/bin/git-watchtower.js +25 -7
- package/package.json +1 -1
- package/src/casino/poll-churn.js +53 -0
- package/src/ui/ansi.js +128 -9
package/bin/git-watchtower.js
CHANGED
|
@@ -63,6 +63,7 @@ const readline = require('readline');
|
|
|
63
63
|
// Casino mode - Vegas-style feedback effects
|
|
64
64
|
const casino = require('../src/casino');
|
|
65
65
|
const casinoSounds = require('../src/casino/sounds');
|
|
66
|
+
const { sumPollChurn } = require('../src/casino/poll-churn');
|
|
66
67
|
|
|
67
68
|
// Gitignore utilities for file watcher
|
|
68
69
|
const { loadGitignorePatterns, shouldIgnoreFile } = require('../src/utils/gitignore');
|
|
@@ -1995,8 +1996,13 @@ async function pollGitChanges() {
|
|
|
1995
1996
|
// Keep deleted branches in the list (don't remove them)
|
|
1996
1997
|
const pollFilteredBranches = allBranches;
|
|
1997
1998
|
|
|
1998
|
-
// Detect updates on other branches (for flash notification)
|
|
1999
|
+
// Detect updates on other branches (for flash notification).
|
|
2000
|
+
// We also capture each updated branch's pre-update commit so the casino
|
|
2001
|
+
// win calculation can compute REAL line churn via git diff, instead of
|
|
2002
|
+
// the old `notifyBranches.length * 100` placeholder that surfaced as
|
|
2003
|
+
// inflated dashboard numbers.
|
|
1999
2004
|
const updatedBranches = [];
|
|
2005
|
+
const updatedBranchPrevCommits = new Map();
|
|
2000
2006
|
const currentBranchName = store.get('currentBranch');
|
|
2001
2007
|
const activeBranchNames = new Set();
|
|
2002
2008
|
for (const branch of pollFilteredBranches) {
|
|
@@ -2007,6 +2013,7 @@ async function pollGitChanges() {
|
|
|
2007
2013
|
const prevCommit = previousBranchStates.get(branch.name);
|
|
2008
2014
|
if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranchName) {
|
|
2009
2015
|
updatedBranches.push(branch);
|
|
2016
|
+
updatedBranchPrevCommits.set(branch.name, prevCommit);
|
|
2010
2017
|
branch.justUpdated = true;
|
|
2011
2018
|
}
|
|
2012
2019
|
previousBranchStates.set(branch.name, branch.commit);
|
|
@@ -2041,14 +2048,25 @@ async function pollGitChanges() {
|
|
|
2041
2048
|
showFlash(names);
|
|
2042
2049
|
playSound();
|
|
2043
2050
|
|
|
2044
|
-
// Casino mode: trigger win effect based on
|
|
2051
|
+
// Casino mode: trigger win effect based on REAL line churn from
|
|
2052
|
+
// each updated branch's prev → new commit. New branches (no prev
|
|
2053
|
+
// commit) contribute 0 — the win still fires from the slot animation
|
|
2054
|
+
// path, but no fake volume is added to totalLinesAdded.
|
|
2045
2055
|
if (casinoOn) {
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2056
|
+
const churn = await sumPollChurn(
|
|
2057
|
+
updatedBranches,
|
|
2058
|
+
updatedBranchPrevCommits,
|
|
2059
|
+
getDiffStats
|
|
2060
|
+
);
|
|
2061
|
+
const totalLines = churn.added + churn.deleted;
|
|
2062
|
+
// getWinLevel(0) returns null and would skip the slot result label
|
|
2063
|
+
// and the sound. Use max(1, totalLines) so a no-line update (e.g.
|
|
2064
|
+
// a tag-only commit, an empty merge) still registers as a "small"
|
|
2065
|
+
// win — the user did get a notification, after all.
|
|
2066
|
+
const winLines = Math.max(1, totalLines);
|
|
2067
|
+
const winLevel = casino.getWinLevel(winLines);
|
|
2050
2068
|
casino.stopSlotReels(true, render, winLevel); // Win - matching symbols + flash + label
|
|
2051
|
-
casino.triggerWin(
|
|
2069
|
+
casino.triggerWin(churn.added, churn.deleted, render);
|
|
2052
2070
|
if (winLevel) {
|
|
2053
2071
|
casinoSounds.playForWinLevel(winLevel.key);
|
|
2054
2072
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Casino mode "win" line-count aggregation.
|
|
3
|
+
*
|
|
4
|
+
* The polling loop used to fake the line-change total as
|
|
5
|
+
* `notifyBranches.length * 100` — a placeholder that surfaced as inflated
|
|
6
|
+
* (and incorrect) numbers in the dashboard's "+lines" / netWinnings
|
|
7
|
+
* displays. This module sums the REAL diff between each updated branch's
|
|
8
|
+
* previous commit and its new commit so the casino stats track actual
|
|
9
|
+
* code churn.
|
|
10
|
+
*
|
|
11
|
+
* @module casino/poll-churn
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sum the diff churn across a list of updated branches by running the
|
|
16
|
+
* caller-supplied diff function on each (prev → new) pair in parallel.
|
|
17
|
+
* Branches missing a prev commit (e.g. brand-new branches) contribute 0,
|
|
18
|
+
* so a notification still fires but no fake line count is invented.
|
|
19
|
+
*
|
|
20
|
+
* @param {Array<{name: string, commit: string}>} updatedBranches
|
|
21
|
+
* @param {Map<string, string>} prevCommits - map of branch.name → previous commit hash
|
|
22
|
+
* @param {(from: string, to: string) => Promise<{added: number, deleted: number}>} diffFn
|
|
23
|
+
* @returns {Promise<{added: number, deleted: number}>}
|
|
24
|
+
*/
|
|
25
|
+
async function sumPollChurn(updatedBranches, prevCommits, diffFn) {
|
|
26
|
+
if (!updatedBranches || updatedBranches.length === 0) {
|
|
27
|
+
return { added: 0, deleted: 0 };
|
|
28
|
+
}
|
|
29
|
+
const results = await Promise.all(
|
|
30
|
+
updatedBranches.map(async (branch) => {
|
|
31
|
+
const prev = prevCommits ? prevCommits.get(branch.name) : null;
|
|
32
|
+
if (!prev || !branch.commit) return { added: 0, deleted: 0 };
|
|
33
|
+
try {
|
|
34
|
+
const r = await diffFn(prev, branch.commit);
|
|
35
|
+
return {
|
|
36
|
+
added: Number.isFinite(r && r.added) ? r.added : 0,
|
|
37
|
+
deleted: Number.isFinite(r && r.deleted) ? r.deleted : 0,
|
|
38
|
+
};
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// diffFn already returns { added: 0, deleted: 0 } on git failure
|
|
41
|
+
// (see src/git/commands.js#getDiffStats), but guard against a
|
|
42
|
+
// throwing test stub or future caller passing a stricter fn.
|
|
43
|
+
return { added: 0, deleted: 0 };
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
return results.reduce(
|
|
48
|
+
(acc, d) => ({ added: acc.added + d.added, deleted: acc.deleted + d.deleted }),
|
|
49
|
+
{ added: 0, deleted: 0 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { sumPollChurn };
|
package/src/ui/ansi.js
CHANGED
|
@@ -358,12 +358,106 @@ function sanitizeForRender(str) {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
361
|
+
* Display width of a single Unicode code point in terminal columns.
|
|
362
|
+
* Returns 0 for combining marks / variation selectors / ZWJ, 2 for East
|
|
363
|
+
* Asian Wide / Fullwidth and emoji-presentation code points, 1 otherwise.
|
|
364
|
+
* Used by visibleLength's grapheme iterator.
|
|
365
|
+
* @param {number} cp
|
|
366
|
+
* @returns {number}
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
function _codePointWidth(cp) {
|
|
370
|
+
if (cp == null) return 0;
|
|
371
|
+
// C0 / DEL — defensively zero-width (stripAnsi already drops them).
|
|
372
|
+
if (cp < 0x20 || cp === 0x7f) return 0;
|
|
373
|
+
|
|
374
|
+
// Combining marks, variation selectors, ZWJ — zero-width.
|
|
375
|
+
if (
|
|
376
|
+
(cp >= 0x0300 && cp <= 0x036F) || // Combining Diacritical Marks
|
|
377
|
+
(cp >= 0x1AB0 && cp <= 0x1AFF) || // Combining Diacritical Marks Extended
|
|
378
|
+
(cp >= 0x1DC0 && cp <= 0x1DFF) || // Combining Diacritical Marks Supplement
|
|
379
|
+
(cp >= 0x20D0 && cp <= 0x20FF) || // Combining Diacritical Marks for Symbols
|
|
380
|
+
(cp >= 0xFE00 && cp <= 0xFE0F) || // Variation Selectors (incl. emoji presentation)
|
|
381
|
+
(cp >= 0xFE20 && cp <= 0xFE2F) || // Combining Half Marks
|
|
382
|
+
cp === 0x200D // ZWJ
|
|
383
|
+
) return 0;
|
|
384
|
+
|
|
385
|
+
// East Asian Wide / Fullwidth / emoji — two columns.
|
|
386
|
+
if (
|
|
387
|
+
(cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
|
|
388
|
+
(cp >= 0x2E80 && cp <= 0x303E) || // CJK Radicals etc
|
|
389
|
+
(cp >= 0x3041 && cp <= 0x33FF) || // CJK Symbols / Hiragana / Katakana / Bopomofo / Hangul / CJK
|
|
390
|
+
(cp >= 0x3400 && cp <= 0x4DBF) || // CJK Unified Ideographs Ext A
|
|
391
|
+
(cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs
|
|
392
|
+
(cp >= 0xA000 && cp <= 0xA4CF) || // Yi Syllables
|
|
393
|
+
(cp >= 0xAC00 && cp <= 0xD7A3) || // Hangul Syllables
|
|
394
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
395
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) || // CJK Compatibility Forms
|
|
396
|
+
(cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms
|
|
397
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) || // Fullwidth Signs
|
|
398
|
+
(cp >= 0x1F300 && cp <= 0x1F5FF) || // Misc Symbols and Pictographs
|
|
399
|
+
(cp >= 0x1F600 && cp <= 0x1F64F) || // Emoticons
|
|
400
|
+
(cp >= 0x1F680 && cp <= 0x1F6FF) || // Transport
|
|
401
|
+
(cp >= 0x1F900 && cp <= 0x1F9FF) || // Supplemental Symbols and Pictographs
|
|
402
|
+
(cp >= 0x1FA00 && cp <= 0x1FAFF) || // Symbols and Pictographs Extended-A
|
|
403
|
+
(cp >= 0x1F1E6 && cp <= 0x1F1FF) || // Regional indicators (flags)
|
|
404
|
+
(cp >= 0x1F3FB && cp <= 0x1F3FF) || // Emoji modifiers (skin tone)
|
|
405
|
+
(cp >= 0x20000 && cp <= 0x2FFFD) || // CJK Ideographs Extension B-F
|
|
406
|
+
(cp >= 0x30000 && cp <= 0x3FFFD) // CJK Ideographs Extension G-H
|
|
407
|
+
) return 2;
|
|
408
|
+
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Reused grapheme segmenter — instantiating per-call would be wasteful for
|
|
413
|
+
// strings hit on every render. Default locale is fine; we only care about
|
|
414
|
+
// boundaries, not script-aware ordering.
|
|
415
|
+
const _graphemeSegmenter = (typeof Intl !== 'undefined' && Intl.Segmenter)
|
|
416
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
417
|
+
: null;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get the visible width of a string in terminal columns, excluding ANSI
|
|
421
|
+
* codes and counting wide characters (CJK ideographs, emoji, fullwidth)
|
|
422
|
+
* as 2 columns. Combining marks, variation selectors, and ZWJ are 0-width.
|
|
423
|
+
* Grapheme clusters that contain at least one wide code point clamp to 2
|
|
424
|
+
* — so a ZWJ family (e.g. 👨👩👧, which is 5 code points = 13 UTF-16 units)
|
|
425
|
+
* counts as 2, matching how terminals render it.
|
|
426
|
+
*
|
|
427
|
+
* Without this, branch names with emoji/CJK misalign sparklines, badges
|
|
428
|
+
* and box borders because the renderer's `padRight`/truncation math drifts
|
|
429
|
+
* by one or more columns per non-ASCII character.
|
|
430
|
+
*
|
|
431
|
+
* @param {string} str - String potentially containing ANSI codes and unicode
|
|
432
|
+
* @returns {number} Visible width in terminal columns
|
|
364
433
|
*/
|
|
365
434
|
function visibleLength(str) {
|
|
366
|
-
|
|
435
|
+
const stripped = stripAnsi(str);
|
|
436
|
+
// ASCII fast path — every char is 1 column, regex avoids the Segmenter
|
|
437
|
+
// setup cost on the renderer's hot path.
|
|
438
|
+
if (/^[\x20-\x7e]*$/.test(stripped)) return stripped.length;
|
|
439
|
+
if (!_graphemeSegmenter) {
|
|
440
|
+
// Pre-Intl.Segmenter fallback (shouldn't hit on Node 16+).
|
|
441
|
+
let width = 0;
|
|
442
|
+
for (const ch of stripped) width += _codePointWidth(ch.codePointAt(0));
|
|
443
|
+
return width;
|
|
444
|
+
}
|
|
445
|
+
let total = 0;
|
|
446
|
+
for (const { segment } of _graphemeSegmenter.segment(stripped)) {
|
|
447
|
+
let clusterTotal = 0;
|
|
448
|
+
let clusterHasWide = false;
|
|
449
|
+
for (const ch of segment) {
|
|
450
|
+
const w = _codePointWidth(ch.codePointAt(0));
|
|
451
|
+
if (w === 2) clusterHasWide = true;
|
|
452
|
+
clusterTotal += w;
|
|
453
|
+
}
|
|
454
|
+
// ZWJ-joined emoji clusters (e.g. family / flag / skin-tone) sum to
|
|
455
|
+
// more than 2 by code point, but render as a single 2-column glyph.
|
|
456
|
+
// Cap them at 2; non-wide clusters (combining marks on a base char)
|
|
457
|
+
// still take at least 1 column.
|
|
458
|
+
total += clusterHasWide ? 2 : Math.max(1, clusterTotal);
|
|
459
|
+
}
|
|
460
|
+
return total;
|
|
367
461
|
}
|
|
368
462
|
|
|
369
463
|
/**
|
|
@@ -382,14 +476,39 @@ function truncate(str, maxLen, suffix = '…') {
|
|
|
382
476
|
// raw input with cursor/screen-control sequences in it. SGR survives.
|
|
383
477
|
const safeStr = sanitizeForRender(str);
|
|
384
478
|
const visible = stripAnsi(safeStr);
|
|
385
|
-
|
|
479
|
+
|
|
480
|
+
// Compare in display columns, not UTF-16 code units, so a string of CJK
|
|
481
|
+
// ideographs (e.g. "中文测试" — 4 code units, 8 columns) is correctly
|
|
482
|
+
// recognised as needing truncation in a 4-column slot.
|
|
483
|
+
if (visibleLength(visible) <= maxLen) {
|
|
386
484
|
return safeStr;
|
|
387
485
|
}
|
|
388
486
|
|
|
389
|
-
// Long-string path: drop ALL escapes (including SGR),
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
487
|
+
// Long-string path: drop ALL escapes (including SGR), accumulate
|
|
488
|
+
// graphemes up to the column budget, append ellipsis + reset. Walking
|
|
489
|
+
// graphemes (not code units) keeps wide characters from being split
|
|
490
|
+
// mid-glyph and keeps the truncated width <= maxLen even for emoji
|
|
491
|
+
// and CJK input.
|
|
492
|
+
const suffixWidth = visibleLength(suffix);
|
|
493
|
+
const budget = Math.max(0, maxLen - suffixWidth);
|
|
494
|
+
let acc = '';
|
|
495
|
+
let used = 0;
|
|
496
|
+
if (_graphemeSegmenter) {
|
|
497
|
+
for (const { segment } of _graphemeSegmenter.segment(visible)) {
|
|
498
|
+
const w = visibleLength(segment);
|
|
499
|
+
if (used + w > budget) break;
|
|
500
|
+
acc += segment;
|
|
501
|
+
used += w;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
for (const ch of visible) {
|
|
505
|
+
const w = _codePointWidth(ch.codePointAt(0));
|
|
506
|
+
if (used + w > budget) break;
|
|
507
|
+
acc += ch;
|
|
508
|
+
used += w;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return acc + suffix + ansi.reset;
|
|
393
512
|
}
|
|
394
513
|
|
|
395
514
|
/**
|