hyper-windowtint 0.3.3 → 0.3.6
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/CHANGELOG.md +31 -0
- package/README.md +16 -18
- package/index.js +51 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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.6] - 2026-05-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- README now shows a real screenshot of four Hyper windows in four projects
|
|
12
|
+
(`docs/demo-hero.png`) instead of the placeholder. Image is referenced via
|
|
13
|
+
full GitHub raw URL so it renders on both github.com and npmjs.com.
|
|
14
|
+
|
|
15
|
+
## [0.3.5] - 2026-05-16
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Color collisions across projects. Previously every project root got an
|
|
19
|
+
independent random seed, and two random seeds could hash to the same
|
|
20
|
+
palette index — so two unrelated projects regularly came out the same
|
|
21
|
+
color. `seedForProjectRoot` now tracks which palette indices are in use
|
|
22
|
+
by other active projects and rolls candidate seeds until it finds one
|
|
23
|
+
that hashes to an unused slot. The first 12 distinct project roots in
|
|
24
|
+
a Hyper session are now guaranteed to get 12 distinct colors; only
|
|
25
|
+
projects 13+ have to collide.
|
|
26
|
+
|
|
27
|
+
## [0.3.4] - 2026-05-16
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- README now references real screenshots in `docs/` instead of a placeholder.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
- Dead code left over from 0.3.3's `decorateTab` removal: the `uidToColor`
|
|
34
|
+
map, the `tintVersion` counter and its `WINDOWTINT_COLOR_CHANGE` dispatch,
|
|
35
|
+
and the `reduceUI` export. None affected runtime; they only existed to
|
|
36
|
+
feed the now-removed per-tab decorations. `index.js` drops from 647 to 622
|
|
37
|
+
lines.
|
|
38
|
+
|
|
8
39
|
## [0.3.3] - 2026-05-16
|
|
9
40
|
|
|
10
41
|
### Changed
|
package/README.md
CHANGED
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Give every Hyper terminal window/tab a color so you can tell them apart at a glance, while matching tabs that are currently in the same project during the current Hyper session.
|
|
4
4
|
|
|
5
|
-
Groups each session by its cwd, walked up to the nearest `.git` repo root when possible, then assigns that project group a random color from a curated 12-color palette for the current Hyper run. Restarting Hyper can assign a different color. Shells that emit OSC 7 update the group live when you `cd`. The plugin paints:
|
|
5
|
+
Groups each session by its cwd, walked up to the nearest `.git` repo root when possible, then assigns that project group a random color from a curated 12-color palette for the current Hyper run. Restarting Hyper can assign a different color. Shells that emit OSC 7 update the group live when you `cd`. The plugin paints, in the active project's color:
|
|
6
6
|
|
|
7
|
-
- a thin
|
|
8
|
-
-
|
|
9
|
-
- a
|
|
7
|
+
- a thin border around the window
|
|
8
|
+
- a colored top line in the tab bar
|
|
9
|
+
- a subtle gradient tint on the active tab's background
|
|
10
|
+
- an optional uppercase color-name label in the top-right corner (off by default; opt in with `showBadge: true`)
|
|
10
11
|
|
|
11
|
-
The result: two tabs opened inside the same repo use the same color while they remain in that repo. If one tab moves to a different project and the shell reports cwd changes with OSC 7, that tab
|
|
12
|
+
The result: two tabs opened inside the same repo use the same color while they remain in that repo. If one tab moves to a different project and the shell reports cwd changes with OSC 7, that tab's color follows. A different repo or directory gets its own random color (the plugin prefers palette slots that aren't already in use by other open projects, so collisions only start once you have more than 12 projects open in one Hyper session). If cwd resolution ever fails entirely, the plugin falls back to the session UID so the tab still gets a color.
|
|
12
13
|
|
|
13
14
|
## Screenshots
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
A 5–10s capture of two tabs in different repos plus one `cd` between them
|
|
17
|
-
sells the plugin better than any prose. -->
|
|
16
|
+

|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Four Hyper windows open against four different projects — each gets its own border color, so a glance at the screen tells you which terminal belongs to which codebase.
|
|
20
19
|
|
|
21
20
|
## Install
|
|
22
21
|
|
|
@@ -71,8 +70,8 @@ module.exports = {
|
|
|
71
70
|
config: {
|
|
72
71
|
windowTint: {
|
|
73
72
|
borderWidth: '3px', // CSS string
|
|
74
|
-
showBadge:
|
|
75
|
-
glow: true, // inner glow effect
|
|
73
|
+
showBadge: false, // optional uppercase color-name label in the corner (default off)
|
|
74
|
+
glow: true, // inner glow effect on the window border
|
|
76
75
|
palette: [ // optional — override the default palette
|
|
77
76
|
{ name: 'red', hex: '#ef4444' },
|
|
78
77
|
{ name: 'green', hex: '#22c55e' },
|
|
@@ -90,16 +89,15 @@ Palette entries must include both a non-empty `name` and a 6- or 8-digit hex col
|
|
|
90
89
|
|
|
91
90
|
**Main process:**
|
|
92
91
|
|
|
93
|
-
- `decorateSessionOptions(options)` runs when a new session is about to be spawned.
|
|
94
|
-
- `onWindow(win)` wraps `win.rpc.emit` so that immediately before Hyper's own `'session add'` IPC reaches the renderer, the plugin emits a `'windowtint:session-seed'` event with `{uid, seed}`. This avoids a uid→
|
|
95
|
-
- `onUnload` clears the caches.
|
|
92
|
+
- `decorateSessionOptions(options)` runs when a new session is about to be spawned. The plugin schedules async cwd resolution through `realpath`, walks upward looking for a `.git` directory or file (if found, that path is the project group; otherwise the raw cwd is the group), and assigns the group a random seed for the current Hyper main-process lifetime. Seed assignment prefers palette indices not already used by other open projects so the first 12 distinct projects get 12 distinct colors. Caches `cwd → project root`, `project root → seed`, and `uid → seed` (with a short expiry).
|
|
93
|
+
- `onWindow(win)` wraps `win.rpc.emit` so that immediately before Hyper's own `'session add'` IPC reaches the renderer, the plugin emits a `'windowtint:session-seed'` event with `{uid, seed}`. This avoids a uid→project color flicker on session creation. The wrap is idempotent per window, and reload-stable state on `win.rpc` lets the persistent wrapper consume seeds from the newest plugin module after hot reloads.
|
|
94
|
+
- `onUnload` clears the caches and restores the original `win.rpc.emit`.
|
|
96
95
|
|
|
97
96
|
**Renderer process:**
|
|
98
97
|
|
|
99
|
-
- `decorateTerm`
|
|
100
|
-
- `decorateConfig` injects CSS that styles `.hyper_main
|
|
101
|
-
- A `window.rpc.on('windowtint:session-seed', ...)` listener (installed lazily by the middleware and removed on renderer unload) caches `uid → seed`. If a seed
|
|
102
|
-
- `getTabProps` and `decorateTab` add a small color accent to each tab.
|
|
98
|
+
- `decorateTerm` registers an OSC 7 handler on each tab's xterm instance. When a shell reports a new cwd, the renderer asks the main process for that cwd's current project-group seed and retints.
|
|
99
|
+
- `decorateConfig` injects CSS that styles `.hyper_main` (window border + glow), the tab bar's top line, and the active tab's background gradient using CSS custom properties (`--tint-color`, `--tint-glow`, `--tint-tab-bg`, `--tint-name`).
|
|
100
|
+
- A `window.rpc.on('windowtint:session-seed', ...)` listener (installed lazily by the middleware and removed on renderer unload) caches `uid → seed`. If a seed arrives after the session has already been tinted, the active session retints immediately.
|
|
103
101
|
- Redux middleware listens for `SESSION_ADD` and `SESSION_SET_ACTIVE`, looks up the cached seed by uid (falls back to the uid itself), maps it to the palette, and writes the resulting color to the root element's CSS variables.
|
|
104
102
|
|
|
105
103
|
## Project grouping
|
package/index.js
CHANGED
|
@@ -4,22 +4,21 @@
|
|
|
4
4
|
* hyper-windowtint
|
|
5
5
|
*
|
|
6
6
|
* Assigns each Hyper project group an ephemeral random color from a curated
|
|
7
|
-
* palette, then paints the window border, tab
|
|
8
|
-
*
|
|
7
|
+
* 12-color palette, then paints the window border, the tab bar's top line,
|
|
8
|
+
* and the active tab's background gradient in that color. Optionally shows
|
|
9
|
+
* the color's name in the top-right corner (`config.windowTint.showBadge`).
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* walking up from cwd to the nearest `.git`; if none, the raw cwd is used.
|
|
18
|
-
* Falls back to session UID if cwd never arrives.
|
|
11
|
+
* Groups sessions by the project root of the session's cwd, walking up to
|
|
12
|
+
* the nearest `.git` directory or file; if none, the raw cwd is the group.
|
|
13
|
+
* Each project root gets a random seed for the current Hyper main-process
|
|
14
|
+
* lifetime — seeds prefer palette slots not already in use by other open
|
|
15
|
+
* projects, so the first 12 distinct projects get 12 distinct colors.
|
|
16
|
+
* Restarting Hyper reassigns colors. Falls back to session UID if cwd
|
|
17
|
+
* resolution fails entirely.
|
|
19
18
|
*
|
|
20
19
|
* This module is loaded in BOTH Hyper processes. decorateSessionOptions /
|
|
21
20
|
* onWindow / onUnload run in main; decorateConfig / middleware / decorateTerm
|
|
22
|
-
* /
|
|
21
|
+
* / onRendererUnload run in renderer. The two sides communicate via
|
|
23
22
|
* win.rpc — we piggyback a `windowtint:session-seed` event onto the normal
|
|
24
23
|
* `session add` rpc emit so the renderer has the project-group seed before
|
|
25
24
|
* SESSION_ADD reaches the Redux store (no uid→project color flicker).
|
|
@@ -81,7 +80,7 @@ function withAlpha(hex, alpha) {
|
|
|
81
80
|
let userOpts = {
|
|
82
81
|
palette: DEFAULT_PALETTE,
|
|
83
82
|
borderWidth: '3px',
|
|
84
|
-
showBadge:
|
|
83
|
+
showBadge: false,
|
|
85
84
|
glow: true,
|
|
86
85
|
};
|
|
87
86
|
|
|
@@ -200,16 +199,49 @@ async function resolveProjectRootAsync(cwd) {
|
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
function createRandomSeed() {
|
|
204
|
-
return `windowtint:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
202
|
+
function createRandomSeed(salt) {
|
|
203
|
+
return `windowtint:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}${salt != null ? ':' + salt : ''}`;
|
|
205
204
|
}
|
|
206
205
|
|
|
206
|
+
// Main-process palette-size hint for collision avoidance. The user can
|
|
207
|
+
// override the palette in their renderer-side config, but main can't see
|
|
208
|
+
// that, so we assume the default 12-slot palette here. If a user shrinks
|
|
209
|
+
// their palette, the worst case is that we'll still allocate indices 0–11
|
|
210
|
+
// uniformly — they just won't all be reachable in the renderer.
|
|
211
|
+
const COLLISION_AVOIDANCE_PALETTE_SIZE = DEFAULT_PALETTE.length;
|
|
212
|
+
|
|
207
213
|
function seedForProjectRoot(root) {
|
|
208
214
|
if (!root) return root;
|
|
209
|
-
if (
|
|
210
|
-
|
|
215
|
+
if (projectSeedCache.has(root)) return projectSeedCache.get(root);
|
|
216
|
+
|
|
217
|
+
// Figure out which palette indices are currently taken by other projects
|
|
218
|
+
// so we can prefer a slot that isn't already in use.
|
|
219
|
+
const paletteSize = COLLISION_AVOIDANCE_PALETTE_SIZE;
|
|
220
|
+
const usedIndices = new Set();
|
|
221
|
+
projectSeedCache.forEach((existingSeed) => {
|
|
222
|
+
usedIndices.add(hashToIndex(existingSeed, paletteSize));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Brute-force candidate seeds until one hashes to an unused index, or we
|
|
226
|
+
// run out of attempts. Probability of a single attempt being clean is
|
|
227
|
+
// (paletteSize - usedIndices.size) / paletteSize, so 200 attempts is more
|
|
228
|
+
// than enough until ~11 colors are taken.
|
|
229
|
+
let seed = null;
|
|
230
|
+
if (usedIndices.size < paletteSize) {
|
|
231
|
+
for (let i = 0; i < 200; i++) {
|
|
232
|
+
const candidate = createRandomSeed(i);
|
|
233
|
+
if (!usedIndices.has(hashToIndex(candidate, paletteSize))) {
|
|
234
|
+
seed = candidate;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
211
238
|
}
|
|
212
|
-
|
|
239
|
+
// Fallback: more projects than colors, or 200 attempts didn't find a clean
|
|
240
|
+
// index — just take a random seed and accept the collision.
|
|
241
|
+
if (!seed) seed = createRandomSeed();
|
|
242
|
+
|
|
243
|
+
projectSeedCache.set(root, seed);
|
|
244
|
+
return seed;
|
|
213
245
|
}
|
|
214
246
|
|
|
215
247
|
exports.decorateSessionOptions = (options) => {
|
|
@@ -356,13 +388,9 @@ exports.onUnload = () => {
|
|
|
356
388
|
|
|
357
389
|
// uid → seed, populated by the rpc listener installed lazily inside middleware.
|
|
358
390
|
const uidToSeed = new Map();
|
|
359
|
-
const uidToColor = new Map();
|
|
360
391
|
let rpcListenerInstalled = false;
|
|
361
392
|
let rpcSeedListener = null;
|
|
362
393
|
let currentSeed = null;
|
|
363
|
-
let tintVersion = 0;
|
|
364
|
-
|
|
365
|
-
const WINDOWTINT_COLOR_CHANGE = 'WINDOWTINT_COLOR_CHANGE';
|
|
366
394
|
|
|
367
395
|
function applyTint(color, opts) {
|
|
368
396
|
if (typeof document === 'undefined' || !color) return;
|
|
@@ -378,16 +406,11 @@ function tintForUid(uid) {
|
|
|
378
406
|
const seed = uidToSeed.get(uid) || uid;
|
|
379
407
|
if (seed === currentSeed) return;
|
|
380
408
|
currentSeed = seed;
|
|
381
|
-
|
|
382
|
-
applyTint(color, userOpts);
|
|
383
|
-
uidToColor.set(uid, color);
|
|
409
|
+
applyTint(colorForSeed(seed), userOpts);
|
|
384
410
|
}
|
|
385
411
|
|
|
386
412
|
function setSeedForUid(uid, seed) {
|
|
387
413
|
uidToSeed.set(uid, seed);
|
|
388
|
-
const color = colorForSeed(seed);
|
|
389
|
-
uidToColor.set(uid, color);
|
|
390
|
-
return color;
|
|
391
414
|
}
|
|
392
415
|
|
|
393
416
|
function installRpcListener(store) {
|
|
@@ -399,8 +422,6 @@ function installRpcListener(store) {
|
|
|
399
422
|
try {
|
|
400
423
|
if (!payload || !payload.uid || !payload.seed) return;
|
|
401
424
|
setSeedForUid(payload.uid, payload.seed);
|
|
402
|
-
tintVersion += 1;
|
|
403
|
-
store.dispatch({ type: WINDOWTINT_COLOR_CHANGE, version: tintVersion });
|
|
404
425
|
|
|
405
426
|
// If the seed arrived after the session was already tinted (race —
|
|
406
427
|
// shouldn't happen in practice because we emit seed before `session
|
|
@@ -425,9 +446,7 @@ exports.onRendererUnload = () => {
|
|
|
425
446
|
rpcSeedListener = null;
|
|
426
447
|
rpcListenerInstalled = false;
|
|
427
448
|
uidToSeed.clear();
|
|
428
|
-
uidToColor.clear();
|
|
429
449
|
currentSeed = null;
|
|
430
|
-
tintVersion = 0;
|
|
431
450
|
} catch (e) { /* swallow */ }
|
|
432
451
|
};
|
|
433
452
|
|
|
@@ -608,18 +627,6 @@ exports.decorateConfig = (config) => {
|
|
|
608
627
|
// Per-tab outlines for inactive tabs may come back in a future release using
|
|
609
628
|
// a different mechanism (e.g. a renderer-side DOM observer).
|
|
610
629
|
|
|
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
630
|
// Redux middleware: re-tint when sessions are added or switched, using the
|
|
624
631
|
// project-group seed if available, falling back to session UID.
|
|
625
632
|
exports.middleware = (store) => (next) => (action) => {
|
package/package.json
CHANGED