hyper-windowtint 0.3.3 → 0.3.5

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/index.js +39 -31
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to `hyper-windowtint` will be documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.5] - 2026-05-16
9
+
10
+ ### Fixed
11
+ - Color collisions across projects. Previously every project root got an
12
+ independent random seed, and two random seeds could hash to the same
13
+ palette index — so two unrelated projects regularly came out the same
14
+ color. `seedForProjectRoot` now tracks which palette indices are in use
15
+ by other active projects and rolls candidate seeds until it finds one
16
+ that hashes to an unused slot. The first 12 distinct project roots in
17
+ a Hyper session are now guaranteed to get 12 distinct colors; only
18
+ projects 13+ have to collide.
19
+
20
+ ## [0.3.4] - 2026-05-16
21
+
22
+ ### Changed
23
+ - README now references real screenshots in `docs/` instead of a placeholder.
24
+
25
+ ### Removed
26
+ - Dead code left over from 0.3.3's `decorateTab` removal: the `uidToColor`
27
+ map, the `tintVersion` counter and its `WINDOWTINT_COLOR_CHANGE` dispatch,
28
+ and the `reduceUI` export. None affected runtime; they only existed to
29
+ feed the now-removed per-tab decorations. `index.js` drops from 647 to 622
30
+ lines.
31
+
8
32
  ## [0.3.3] - 2026-05-16
9
33
 
10
34
  ### Changed
package/index.js CHANGED
@@ -200,16 +200,49 @@ async function resolveProjectRootAsync(cwd) {
200
200
  }
201
201
  }
202
202
 
203
- function createRandomSeed() {
204
- return `windowtint:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
203
+ function createRandomSeed(salt) {
204
+ return `windowtint:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}${salt != null ? ':' + salt : ''}`;
205
205
  }
206
206
 
207
+ // Main-process palette-size hint for collision avoidance. The user can
208
+ // override the palette in their renderer-side config, but main can't see
209
+ // that, so we assume the default 12-slot palette here. If a user shrinks
210
+ // their palette, the worst case is that we'll still allocate indices 0–11
211
+ // uniformly — they just won't all be reachable in the renderer.
212
+ const COLLISION_AVOIDANCE_PALETTE_SIZE = DEFAULT_PALETTE.length;
213
+
207
214
  function seedForProjectRoot(root) {
208
215
  if (!root) return root;
209
- if (!projectSeedCache.has(root)) {
210
- projectSeedCache.set(root, createRandomSeed());
216
+ if (projectSeedCache.has(root)) return projectSeedCache.get(root);
217
+
218
+ // Figure out which palette indices are currently taken by other projects
219
+ // so we can prefer a slot that isn't already in use.
220
+ const paletteSize = COLLISION_AVOIDANCE_PALETTE_SIZE;
221
+ const usedIndices = new Set();
222
+ projectSeedCache.forEach((existingSeed) => {
223
+ usedIndices.add(hashToIndex(existingSeed, paletteSize));
224
+ });
225
+
226
+ // Brute-force candidate seeds until one hashes to an unused index, or we
227
+ // run out of attempts. Probability of a single attempt being clean is
228
+ // (paletteSize - usedIndices.size) / paletteSize, so 200 attempts is more
229
+ // than enough until ~11 colors are taken.
230
+ let seed = null;
231
+ if (usedIndices.size < paletteSize) {
232
+ for (let i = 0; i < 200; i++) {
233
+ const candidate = createRandomSeed(i);
234
+ if (!usedIndices.has(hashToIndex(candidate, paletteSize))) {
235
+ seed = candidate;
236
+ break;
237
+ }
238
+ }
211
239
  }
212
- return projectSeedCache.get(root);
240
+ // Fallback: more projects than colors, or 200 attempts didn't find a clean
241
+ // index — just take a random seed and accept the collision.
242
+ if (!seed) seed = createRandomSeed();
243
+
244
+ projectSeedCache.set(root, seed);
245
+ return seed;
213
246
  }
214
247
 
215
248
  exports.decorateSessionOptions = (options) => {
@@ -356,13 +389,9 @@ exports.onUnload = () => {
356
389
 
357
390
  // uid → seed, populated by the rpc listener installed lazily inside middleware.
358
391
  const uidToSeed = new Map();
359
- const uidToColor = new Map();
360
392
  let rpcListenerInstalled = false;
361
393
  let rpcSeedListener = null;
362
394
  let currentSeed = null;
363
- let tintVersion = 0;
364
-
365
- const WINDOWTINT_COLOR_CHANGE = 'WINDOWTINT_COLOR_CHANGE';
366
395
 
367
396
  function applyTint(color, opts) {
368
397
  if (typeof document === 'undefined' || !color) return;
@@ -378,16 +407,11 @@ function tintForUid(uid) {
378
407
  const seed = uidToSeed.get(uid) || uid;
379
408
  if (seed === currentSeed) return;
380
409
  currentSeed = seed;
381
- const color = colorForSeed(seed);
382
- applyTint(color, userOpts);
383
- uidToColor.set(uid, color);
410
+ applyTint(colorForSeed(seed), userOpts);
384
411
  }
385
412
 
386
413
  function setSeedForUid(uid, seed) {
387
414
  uidToSeed.set(uid, seed);
388
- const color = colorForSeed(seed);
389
- uidToColor.set(uid, color);
390
- return color;
391
415
  }
392
416
 
393
417
  function installRpcListener(store) {
@@ -399,8 +423,6 @@ function installRpcListener(store) {
399
423
  try {
400
424
  if (!payload || !payload.uid || !payload.seed) return;
401
425
  setSeedForUid(payload.uid, payload.seed);
402
- tintVersion += 1;
403
- store.dispatch({ type: WINDOWTINT_COLOR_CHANGE, version: tintVersion });
404
426
 
405
427
  // If the seed arrived after the session was already tinted (race —
406
428
  // shouldn't happen in practice because we emit seed before `session
@@ -425,9 +447,7 @@ exports.onRendererUnload = () => {
425
447
  rpcSeedListener = null;
426
448
  rpcListenerInstalled = false;
427
449
  uidToSeed.clear();
428
- uidToColor.clear();
429
450
  currentSeed = null;
430
- tintVersion = 0;
431
451
  } catch (e) { /* swallow */ }
432
452
  };
433
453
 
@@ -608,18 +628,6 @@ exports.decorateConfig = (config) => {
608
628
  // Per-tab outlines for inactive tabs may come back in a future release using
609
629
  // a different mechanism (e.g. a renderer-side DOM observer).
610
630
 
611
- exports.reduceUI = (state, action) => {
612
- if (!action || action.type !== WINDOWTINT_COLOR_CHANGE) return state;
613
- try {
614
- if (state && typeof state.set === 'function') {
615
- return state.set('windowTintVersion', action.version);
616
- }
617
- return Object.assign({}, state, { windowTintVersion: action.version });
618
- } catch (e) {
619
- return state;
620
- }
621
- };
622
-
623
631
  // Redux middleware: re-tint when sessions are added or switched, using the
624
632
  // project-group seed if available, falling back to session UID.
625
633
  exports.middleware = (store) => (next) => (action) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-windowtint",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Give Hyper terminal tabs distinct color tints, matching tabs from the same project during the current Hyper session.",
5
5
  "main": "index.js",
6
6
  "files": [