hyper-windowtint 0.3.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.
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/index.js +703 -0
- package/package.json +39 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `hyper-windowtint` will be documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-05-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Project-group color assignment: tabs whose cwd resolves to the same `.git`
|
|
12
|
+
root share a color for the current Hyper run.
|
|
13
|
+
- Live retint on `cd` for shells that emit OSC 7 cwd reports.
|
|
14
|
+
- Optional corner badge showing the current color name (`showBadge`).
|
|
15
|
+
- Optional inner glow (`glow`).
|
|
16
|
+
- Custom palette support with strict per-entry validation
|
|
17
|
+
(non-empty `name`, 6- or 8-digit hex).
|
|
18
|
+
- Shell helper snippets (zsh / bash) in the README for shells that do not
|
|
19
|
+
emit OSC 7 by default.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Color assignment is now ephemeral per Hyper main-process lifetime, not
|
|
23
|
+
permanent per project.
|
|
24
|
+
- Project root resolution moved off the synchronous session-spawn path
|
|
25
|
+
using `fs.promises`.
|
|
26
|
+
- Tab accents are prop-driven through `mapHeaderState`, `getTabProps`, and
|
|
27
|
+
`decorateTab` instead of DOM query hacks.
|
|
28
|
+
- `win.rpc.emit` wrapper and `windowtint:cwd-change` listener are now
|
|
29
|
+
restored / removed on `onUnload`.
|
|
30
|
+
- Renderer-side `windowtint:session-seed` listener removal is more
|
|
31
|
+
defensive about which method Hyper exposes (`removeListener` / `off`).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- 8-digit hex colors in a user-supplied palette no longer produce invalid
|
|
35
|
+
10-character color strings when composing glow/tab-bg/box-shadow.
|
|
36
|
+
- Listener removal in `onUnload` no longer assumes `rpc.emitter` —
|
|
37
|
+
falls back across `rpc.removeListener`, `rpc.off`, `rpc.emitter.removeListener`.
|
|
38
|
+
- `parseOsc7Cwd` no longer throws when `process` is undefined in renderers
|
|
39
|
+
with Electron contextIsolation enabled.
|
|
40
|
+
- Various silent error swallowing on the main side so a single bad cwd
|
|
41
|
+
lookup cannot crash session spawn.
|
|
42
|
+
|
|
43
|
+
## [0.1.0]
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Initial release. Each session UID got a stable, deterministic color
|
|
47
|
+
for its lifetime, drawn from a fixed palette.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Ricchiuti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# hyper-windowtint
|
|
2
|
+
|
|
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
|
+
|
|
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:
|
|
6
|
+
|
|
7
|
+
- a thin colored border around the window
|
|
8
|
+
- matching accents on tabs
|
|
9
|
+
- a tiny color-name badge in the top-right corner (e.g. `ROSE`, `TEAL`)
|
|
10
|
+
|
|
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 gets the other project's color. A different repo or directory gets its own initially random color. If cwd resolution ever fails entirely, the plugin falls back to the session UID, so the tab still gets a color.
|
|
12
|
+
|
|
13
|
+
## Screenshots
|
|
14
|
+
|
|
15
|
+
<!-- TODO: drop a screenshot or short GIF at docs/demo.gif and reference it here.
|
|
16
|
+
A 5–10s capture of two tabs in different repos plus one `cd` between them
|
|
17
|
+
sells the plugin better than any prose. -->
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
### From npm (recommended)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
hyper i hyper-windowtint
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`hyper i` adds the plugin to the `plugins` array in `~/.hyper.js` and installs
|
|
30
|
+
it. Restart Hyper afterward.
|
|
31
|
+
|
|
32
|
+
If you prefer to edit `~/.hyper.js` by hand:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
module.exports = {
|
|
36
|
+
config: {
|
|
37
|
+
// ...your existing config
|
|
38
|
+
},
|
|
39
|
+
plugins: ['hyper-windowtint'],
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Local dev
|
|
44
|
+
|
|
45
|
+
Hyper supports local plugins out of the box. Drop this folder into:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
~/.hyper_plugins/local/hyper-windowtint
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then in `~/.hyper.js`:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
module.exports = {
|
|
55
|
+
config: {
|
|
56
|
+
// ...your existing config
|
|
57
|
+
},
|
|
58
|
+
plugins: [],
|
|
59
|
+
localPlugins: ['hyper-windowtint'],
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Restart Hyper (or `Cmd+Shift+R` to reload the renderer).
|
|
64
|
+
|
|
65
|
+
## Config
|
|
66
|
+
|
|
67
|
+
All options live under `config.windowTint` in `~/.hyper.js`:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
module.exports = {
|
|
71
|
+
config: {
|
|
72
|
+
windowTint: {
|
|
73
|
+
borderWidth: '3px', // CSS string
|
|
74
|
+
showBadge: true, // tiny color-name pill in the corner
|
|
75
|
+
glow: true, // inner glow effect
|
|
76
|
+
palette: [ // optional — override the default palette
|
|
77
|
+
{ name: 'red', hex: '#ef4444' },
|
|
78
|
+
{ name: 'green', hex: '#22c55e' },
|
|
79
|
+
{ name: 'blue', hex: '#3b82f6' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
localPlugins: ['hyper-windowtint'],
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Palette entries must include both a non-empty `name` and a 6- or 8-digit hex color. Invalid entries are ignored; if none are valid, the default palette is used.
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
**Main process:**
|
|
92
|
+
|
|
93
|
+
- `decorateSessionOptions(options)` runs when a new session is about to be spawned. Hyper has already resolved both `options.uid` and `options.cwd` by this point. The plugin schedules async cwd resolution through `realpath`, then walks upward looking for a `.git` directory or file; if found, that path is the project group, otherwise the raw cwd is the group. Each group gets an ephemeral random seed that is reused only for the current Hyper main-process lifetime. Result is stashed in a module-scoped `uid → seed` map with a short expiry and cached per cwd so repeat lookups are cheap.
|
|
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→cwd 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.
|
|
95
|
+
- `onUnload` clears the caches.
|
|
96
|
+
|
|
97
|
+
**Renderer process:**
|
|
98
|
+
|
|
99
|
+
- `decorateTerm` listens for OSC 7 cwd reports from xterm.js. When a tab changes directories, the renderer asks the main process for that cwd's current project-group seed and retints the tab.
|
|
100
|
+
- `decorateConfig` injects CSS that styles `.hyper_main`, the tab bar, and the badge using CSS custom properties (`--tint-color`, `--tint-glow`, `--tint-tab-bg`, `--tint-name`).
|
|
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 somehow arrives after the session has already been tinted, the active session retints immediately.
|
|
102
|
+
- `getTabProps` and `decorateTab` add a small color accent to each tab.
|
|
103
|
+
- 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
|
+
|
|
105
|
+
## Project grouping
|
|
106
|
+
|
|
107
|
+
Color assignment is intentionally not permanent. The grouping rules are:
|
|
108
|
+
|
|
109
|
+
- Inside a git repo, every tab currently in that repo shares the same color for the current Hyper run.
|
|
110
|
+
- Outside a git repo, tabs currently in the same cwd share a color for the current Hyper run.
|
|
111
|
+
- After restarting Hyper, those groups can receive different colors.
|
|
112
|
+
- If Hyper does not provide a cwd, the plugin falls back to the session UID.
|
|
113
|
+
|
|
114
|
+
Live updates after `cd` require OSC 7 cwd reporting from the shell. Many modern prompts/shell integrations already emit it; if yours does not, the color updates on new tabs but not after directory changes inside an existing tab.
|
|
115
|
+
|
|
116
|
+
The plugin uses two custom Hyper RPC event names internally: `windowtint:session-seed` and `windowtint:cwd-change`. This depends on Hyper's current runtime RPC passthrough for arbitrary event names.
|
|
117
|
+
|
|
118
|
+
## Enabling OSC 7 in your shell
|
|
119
|
+
|
|
120
|
+
Add one of the snippets below if `cd`-ing inside a tab does not retint it. Most
|
|
121
|
+
modern prompts (Starship, Powerlevel10k, fish ≥ 3, recent macOS zsh,
|
|
122
|
+
Warp/iTerm integrations) already emit OSC 7, so try it first before adding
|
|
123
|
+
anything.
|
|
124
|
+
|
|
125
|
+
### zsh
|
|
126
|
+
|
|
127
|
+
Add to `~/.zshrc`:
|
|
128
|
+
|
|
129
|
+
```zsh
|
|
130
|
+
_osc7_cwd() {
|
|
131
|
+
local host="${HOST:-localhost}"
|
|
132
|
+
printf '\e]7;file://%s%s\e\\' "$host" "$PWD"
|
|
133
|
+
}
|
|
134
|
+
autoload -Uz add-zsh-hook
|
|
135
|
+
add-zsh-hook chpwd _osc7_cwd
|
|
136
|
+
add-zsh-hook precmd _osc7_cwd
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### bash
|
|
140
|
+
|
|
141
|
+
Add to `~/.bashrc`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
_osc7_cwd() {
|
|
145
|
+
printf '\e]7;file://%s%s\e\\' "${HOSTNAME:-localhost}" "$PWD"
|
|
146
|
+
}
|
|
147
|
+
PROMPT_COMMAND='_osc7_cwd;'"${PROMPT_COMMAND:-}"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### fish
|
|
151
|
+
|
|
152
|
+
Add to `~/.config/fish/config.fish`:
|
|
153
|
+
|
|
154
|
+
```fish
|
|
155
|
+
function osc7_cwd --on-variable PWD
|
|
156
|
+
printf '\e]7;file://%s%s\e\\' (hostname) "$PWD"
|
|
157
|
+
end
|
|
158
|
+
osc7_cwd
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Roadmap
|
|
162
|
+
|
|
163
|
+
1. **Optional color labels.** Expose the current in-memory project group color so helper scripts can find matching windows without making the assignment permanent.
|
|
164
|
+
2. **Admin/sudo override.** Force red for elevated shells (steal this from `hyperborder`'s `adminBorderColors`).
|
|
165
|
+
3. **OKLCH-spaced palette generator** for any N colors with guaranteed perceptual distinctness.
|
|
166
|
+
4. **Tests.** Pure-function tests for `hashToIndex`, `parseOsc7Cwd`, `readUserConfig`, and `withAlpha`.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hyper-windowtint
|
|
5
|
+
*
|
|
6
|
+
* Assigns each Hyper project group an ephemeral random color from a curated
|
|
7
|
+
* palette, then paints the window border, tab accent, and (optionally) a
|
|
8
|
+
* small corner badge with the color's name.
|
|
9
|
+
*
|
|
10
|
+
* v0.1: seeded color by session UID (each new tab got a stable color for
|
|
11
|
+
* its lifetime).
|
|
12
|
+
*
|
|
13
|
+
* v0.2: groups sessions by the project root of the session's cwd, then gives
|
|
14
|
+
* each project root a random color seed for the current Hyper main-process
|
|
15
|
+
* lifetime. Two open terminals in the same project match; restarting Hyper
|
|
16
|
+
* can assign that project a different color. The project root is found by
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* This module is loaded in BOTH Hyper processes. decorateSessionOptions /
|
|
21
|
+
* onWindow / onUnload run in main; decorateConfig / middleware / decorateTerm
|
|
22
|
+
* / decorateTab / getTabProps run in renderer. The two sides communicate via
|
|
23
|
+
* win.rpc — we piggyback a `windowtint:session-seed` event onto the normal
|
|
24
|
+
* `session add` rpc emit so the renderer has the project-group seed before
|
|
25
|
+
* SESSION_ADD reaches the Redux store (no uid→project color flicker).
|
|
26
|
+
* Renderer-side OSC 7 handling updates the seed when a tab changes
|
|
27
|
+
* directories.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Curated palette — Tailwind 400s, picked for distinctness on dark backgrounds.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const DEFAULT_PALETTE = [
|
|
34
|
+
{ name: 'rose', hex: '#fb7185' },
|
|
35
|
+
{ name: 'orange', hex: '#fb923c' },
|
|
36
|
+
{ name: 'amber', hex: '#fbbf24' },
|
|
37
|
+
{ name: 'lime', hex: '#a3e635' },
|
|
38
|
+
{ name: 'emerald', hex: '#34d399' },
|
|
39
|
+
{ name: 'teal', hex: '#2dd4bf' },
|
|
40
|
+
{ name: 'cyan', hex: '#22d3ee' },
|
|
41
|
+
{ name: 'sky', hex: '#38bdf8' },
|
|
42
|
+
{ name: 'indigo', hex: '#818cf8' },
|
|
43
|
+
{ name: 'violet', hex: '#a78bfa' },
|
|
44
|
+
{ name: 'fuchsia', hex: '#e879f9' },
|
|
45
|
+
{ name: 'pink', hex: '#f472b6' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Seed hash → palette index (FNV-1a, no native deps).
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
function hashToIndex(str, mod) {
|
|
52
|
+
let h = 2166136261;
|
|
53
|
+
for (let i = 0; i < str.length; i++) {
|
|
54
|
+
h ^= str.charCodeAt(i);
|
|
55
|
+
h = Math.imul(h, 16777619);
|
|
56
|
+
}
|
|
57
|
+
return Math.abs(h) % mod;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pickColor(seed, palette) {
|
|
61
|
+
return palette[hashToIndex(String(seed || 'default'), palette.length)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function colorForSeed(seed) {
|
|
65
|
+
return pickColor(seed, userOpts.palette);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize a possibly-8-digit hex (#RRGGBBAA) to #RRGGBB and append an
|
|
69
|
+
// explicit 2-char alpha. Without this, composing `color.hex + '55'` against a
|
|
70
|
+
// user-supplied 8-digit hex produced an invalid 10-char color string.
|
|
71
|
+
function withAlpha(hex, alpha) {
|
|
72
|
+
if (typeof hex !== 'string') return hex;
|
|
73
|
+
const m = hex.match(/^#([0-9a-fA-F]{6})(?:[0-9a-fA-F]{2})?$/);
|
|
74
|
+
if (!m) return hex;
|
|
75
|
+
return '#' + m[1] + alpha;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// User config (read at decorateConfig time, captured in module scope).
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
let userOpts = {
|
|
82
|
+
palette: DEFAULT_PALETTE,
|
|
83
|
+
borderWidth: '3px',
|
|
84
|
+
showBadge: true,
|
|
85
|
+
glow: true,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function readUserConfig(config) {
|
|
89
|
+
const u = (config && config.windowTint) || {};
|
|
90
|
+
const palette = Array.isArray(u.palette)
|
|
91
|
+
? u.palette.filter((item) => (
|
|
92
|
+
item &&
|
|
93
|
+
typeof item.name === 'string' &&
|
|
94
|
+
item.name.trim().length > 0 &&
|
|
95
|
+
typeof item.hex === 'string' &&
|
|
96
|
+
/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(item.hex)
|
|
97
|
+
))
|
|
98
|
+
: DEFAULT_PALETTE;
|
|
99
|
+
return {
|
|
100
|
+
palette: palette.length ? palette : DEFAULT_PALETTE,
|
|
101
|
+
borderWidth: typeof u.borderWidth === 'string' ? u.borderWidth : '3px',
|
|
102
|
+
showBadge: u.showBadge !== false,
|
|
103
|
+
glow: u.glow !== false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ===========================================================================
|
|
108
|
+
// MAIN-PROCESS SECTION
|
|
109
|
+
// ===========================================================================
|
|
110
|
+
// decorateSessionOptions, onWindow, onUnload are only invoked in main.
|
|
111
|
+
// Module-scope maps declared here live in the main process when populated
|
|
112
|
+
// from main-side hooks.
|
|
113
|
+
// ===========================================================================
|
|
114
|
+
|
|
115
|
+
// cwd → resolved project root (walk up to .git, else cwd itself). Cached so
|
|
116
|
+
// repeat sessions in the same tree don't redo the fs walk.
|
|
117
|
+
const projectRootCache = new Map();
|
|
118
|
+
const projectRootPromises = new Map();
|
|
119
|
+
|
|
120
|
+
// project root → ephemeral random seed. This intentionally does not persist
|
|
121
|
+
// across Hyper restarts; it only keeps same-project terminals matched while
|
|
122
|
+
// the app is running.
|
|
123
|
+
const projectSeedCache = new Map();
|
|
124
|
+
|
|
125
|
+
// uid → seed, populated in decorateSessionOptions, drained when the
|
|
126
|
+
// matching `session add` rpc emit fires for that uid in onWindow's wrap.
|
|
127
|
+
const pendingSeeds = new Map();
|
|
128
|
+
const pendingSeedTimers = new Map();
|
|
129
|
+
const tintedWindows = new Set();
|
|
130
|
+
|
|
131
|
+
const PENDING_SEED_TTL_MS = 30000;
|
|
132
|
+
|
|
133
|
+
function deletePendingSeed(uid) {
|
|
134
|
+
const timer = pendingSeedTimers.get(uid);
|
|
135
|
+
if (timer) {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
pendingSeedTimers.delete(uid);
|
|
138
|
+
}
|
|
139
|
+
pendingSeeds.delete(uid);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setPendingSeed(uid, seed) {
|
|
143
|
+
deletePendingSeed(uid);
|
|
144
|
+
pendingSeeds.set(uid, seed);
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
deletePendingSeed(uid);
|
|
147
|
+
}, PENDING_SEED_TTL_MS);
|
|
148
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
149
|
+
pendingSeedTimers.set(uid, timer);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function resolveProjectRootAsync(cwd) {
|
|
153
|
+
if (!cwd) return cwd;
|
|
154
|
+
if (projectRootCache.has(cwd)) return projectRootCache.get(cwd);
|
|
155
|
+
if (projectRootPromises.has(cwd)) return projectRootPromises.get(cwd);
|
|
156
|
+
|
|
157
|
+
const promise = (async () => {
|
|
158
|
+
let root = cwd;
|
|
159
|
+
let cacheKey = cwd;
|
|
160
|
+
try {
|
|
161
|
+
const path = require('path');
|
|
162
|
+
const fs = require('fs');
|
|
163
|
+
let dir = cwd;
|
|
164
|
+
try {
|
|
165
|
+
dir = await fs.promises.realpath(cwd);
|
|
166
|
+
cacheKey = dir;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// Fall back to the provided cwd if realpath fails.
|
|
169
|
+
}
|
|
170
|
+
if (projectRootCache.has(cacheKey)) {
|
|
171
|
+
const cachedRoot = projectRootCache.get(cacheKey);
|
|
172
|
+
projectRootCache.set(cwd, cachedRoot);
|
|
173
|
+
return cachedRoot;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let prev = null;
|
|
177
|
+
while (dir && dir !== prev) {
|
|
178
|
+
try {
|
|
179
|
+
await fs.promises.access(path.join(dir, '.git'));
|
|
180
|
+
root = dir;
|
|
181
|
+
break;
|
|
182
|
+
} catch (e) { /* keep walking */ }
|
|
183
|
+
prev = dir;
|
|
184
|
+
dir = path.dirname(dir);
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
// If require or fs access fails, fall back to the raw cwd.
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
projectRootCache.set(cwd, root);
|
|
191
|
+
projectRootCache.set(cacheKey, root);
|
|
192
|
+
return root;
|
|
193
|
+
})();
|
|
194
|
+
|
|
195
|
+
projectRootPromises.set(cwd, promise);
|
|
196
|
+
try {
|
|
197
|
+
return await promise;
|
|
198
|
+
} finally {
|
|
199
|
+
projectRootPromises.delete(cwd);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createRandomSeed() {
|
|
204
|
+
return `windowtint:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function seedForProjectRoot(root) {
|
|
208
|
+
if (!root) return root;
|
|
209
|
+
if (!projectSeedCache.has(root)) {
|
|
210
|
+
projectSeedCache.set(root, createRandomSeed());
|
|
211
|
+
}
|
|
212
|
+
return projectSeedCache.get(root);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
exports.decorateSessionOptions = (options) => {
|
|
216
|
+
try {
|
|
217
|
+
if (options && options.uid && options.cwd) {
|
|
218
|
+
resolveProjectRootAsync(options.cwd).then((root) => {
|
|
219
|
+
const seed = seedForProjectRoot(root);
|
|
220
|
+
if (!seed) return;
|
|
221
|
+
setPendingSeed(options.uid, seed);
|
|
222
|
+
broadcastSessionSeed(options.uid, seed);
|
|
223
|
+
}).catch(() => {});
|
|
224
|
+
}
|
|
225
|
+
} catch (e) { /* never break session spawn over a tint lookup */ }
|
|
226
|
+
return options;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function broadcastSessionSeed(uid, seed) {
|
|
230
|
+
tintedWindows.forEach((win) => {
|
|
231
|
+
try {
|
|
232
|
+
if (win && win.rpc && typeof win.rpc.emit === 'function') {
|
|
233
|
+
win.rpc.emit('windowtint:session-seed', { uid, seed });
|
|
234
|
+
}
|
|
235
|
+
} catch (e) { /* swallow */ }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Wrap win.rpc.emit once per window so we can inject `windowtint:session-seed`
|
|
240
|
+
// immediately before Hyper's own `session add` reaches the renderer. The
|
|
241
|
+
// per-rpc state object survives plugin reloads, so the persistent wrapper can
|
|
242
|
+
// consume seeds from the newest module instance instead of a stale closure.
|
|
243
|
+
exports.onWindow = (win) => {
|
|
244
|
+
try {
|
|
245
|
+
if (!win || !win.rpc || typeof win.rpc.emit !== 'function') return;
|
|
246
|
+
tintedWindows.add(win);
|
|
247
|
+
|
|
248
|
+
const state = win.rpc.__windowtint_state__ || {};
|
|
249
|
+
state.consumeSeed = (uid) => {
|
|
250
|
+
const seed = pendingSeeds.get(uid);
|
|
251
|
+
if (seed) deletePendingSeed(uid);
|
|
252
|
+
return seed;
|
|
253
|
+
};
|
|
254
|
+
state.resolveCwd = (payload) => {
|
|
255
|
+
try {
|
|
256
|
+
if (!payload || !payload.uid || !payload.cwd) return;
|
|
257
|
+
resolveProjectRootAsync(payload.cwd).then((root) => {
|
|
258
|
+
const seed = seedForProjectRoot(root);
|
|
259
|
+
if (seed) win.rpc.emit('windowtint:session-seed', { uid: payload.uid, seed });
|
|
260
|
+
}).catch(() => {});
|
|
261
|
+
} catch (e) { /* swallow */ }
|
|
262
|
+
};
|
|
263
|
+
win.rpc.__windowtint_state__ = state;
|
|
264
|
+
|
|
265
|
+
if (!state.cwdListenerInstalled && typeof win.rpc.on === 'function') {
|
|
266
|
+
state.cwdListener = (payload) => {
|
|
267
|
+
const rpcState = win.rpc.__windowtint_state__;
|
|
268
|
+
if (rpcState && rpcState.resolveCwd) rpcState.resolveCwd(payload);
|
|
269
|
+
};
|
|
270
|
+
win.rpc.on('windowtint:cwd-change', state.cwdListener);
|
|
271
|
+
state.cwdListenerInstalled = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (state.wrappedEmit) return;
|
|
275
|
+
if (win.rpc.emit.__windowtint_wrapped__ && win.rpc.emit.__windowtint_uses_state__) {
|
|
276
|
+
state.wrappedEmit = win.rpc.emit;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const originalEmit = win.rpc.emit;
|
|
281
|
+
const wrappedEmit = function (...args) {
|
|
282
|
+
const channel = args[0];
|
|
283
|
+
const payload = args[1];
|
|
284
|
+
try {
|
|
285
|
+
if (channel === 'session add' && payload && payload.uid) {
|
|
286
|
+
const rpcState = win.rpc.__windowtint_state__;
|
|
287
|
+
const seed = rpcState && rpcState.consumeSeed && rpcState.consumeSeed(payload.uid);
|
|
288
|
+
if (seed) {
|
|
289
|
+
Reflect.apply(originalEmit, this, ['windowtint:session-seed', { uid: payload.uid, seed }]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (e) { /* swallow — the original emit must still run */ }
|
|
293
|
+
return Reflect.apply(originalEmit, this, args);
|
|
294
|
+
};
|
|
295
|
+
wrappedEmit.__windowtint_wrapped__ = true;
|
|
296
|
+
wrappedEmit.__windowtint_uses_state__ = true;
|
|
297
|
+
wrappedEmit.__windowtint_original__ = originalEmit;
|
|
298
|
+
state.wrappedEmit = wrappedEmit;
|
|
299
|
+
win.rpc.emit = wrappedEmit;
|
|
300
|
+
} catch (e) { /* swallow */ }
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
function removeRpcListener(rpc, event, listener) {
|
|
304
|
+
if (!rpc || !listener) return;
|
|
305
|
+
// Hyper exposes the listener on `rpc` directly; older shapes used
|
|
306
|
+
// `rpc.emitter`. Try the documented paths in order and stop at the first
|
|
307
|
+
// that actually accepts the call.
|
|
308
|
+
const targets = [
|
|
309
|
+
[rpc, 'removeListener'],
|
|
310
|
+
[rpc, 'off'],
|
|
311
|
+
[rpc.emitter, 'removeListener'],
|
|
312
|
+
[rpc.emitter, 'off'],
|
|
313
|
+
];
|
|
314
|
+
for (const [target, method] of targets) {
|
|
315
|
+
if (target && typeof target[method] === 'function') {
|
|
316
|
+
try { target[method](event, listener); return; } catch (e) { /* try next */ }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
exports.onUnload = () => {
|
|
322
|
+
try {
|
|
323
|
+
tintedWindows.forEach((win) => {
|
|
324
|
+
try {
|
|
325
|
+
const state = win && win.rpc && win.rpc.__windowtint_state__;
|
|
326
|
+
if (state && state.cwdListener && win && win.rpc) {
|
|
327
|
+
removeRpcListener(win.rpc, 'windowtint:cwd-change', state.cwdListener);
|
|
328
|
+
}
|
|
329
|
+
if (state) {
|
|
330
|
+
state.cwdListener = null;
|
|
331
|
+
state.cwdListenerInstalled = false;
|
|
332
|
+
state.resolveCwd = null;
|
|
333
|
+
if (
|
|
334
|
+
state.wrappedEmit &&
|
|
335
|
+
state.wrappedEmit.__windowtint_original__ &&
|
|
336
|
+
win.rpc &&
|
|
337
|
+
win.rpc.emit === state.wrappedEmit
|
|
338
|
+
) {
|
|
339
|
+
win.rpc.emit = state.wrappedEmit.__windowtint_original__;
|
|
340
|
+
}
|
|
341
|
+
state.wrappedEmit = null;
|
|
342
|
+
state.consumeSeed = null;
|
|
343
|
+
}
|
|
344
|
+
} catch (e) { /* swallow */ }
|
|
345
|
+
});
|
|
346
|
+
tintedWindows.clear();
|
|
347
|
+
projectSeedCache.clear();
|
|
348
|
+
projectRootPromises.clear();
|
|
349
|
+
projectRootCache.clear();
|
|
350
|
+
} catch (e) { /* swallow */ }
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// ===========================================================================
|
|
354
|
+
// RENDERER-PROCESS SECTION
|
|
355
|
+
// ===========================================================================
|
|
356
|
+
|
|
357
|
+
// uid → seed, populated by the rpc listener installed lazily inside middleware.
|
|
358
|
+
const uidToSeed = new Map();
|
|
359
|
+
const uidToColor = new Map();
|
|
360
|
+
let rpcListenerInstalled = false;
|
|
361
|
+
let rpcSeedListener = null;
|
|
362
|
+
let currentSeed = null;
|
|
363
|
+
let tintVersion = 0;
|
|
364
|
+
|
|
365
|
+
const WINDOWTINT_COLOR_CHANGE = 'WINDOWTINT_COLOR_CHANGE';
|
|
366
|
+
|
|
367
|
+
function applyTint(color, opts) {
|
|
368
|
+
if (typeof document === 'undefined' || !color) return;
|
|
369
|
+
const root = document.documentElement;
|
|
370
|
+
root.style.setProperty('--tint-color', color.hex);
|
|
371
|
+
root.style.setProperty('--tint-glow', opts.glow ? withAlpha(color.hex, '55') : 'transparent');
|
|
372
|
+
root.style.setProperty('--tint-tab-bg', withAlpha(color.hex, '22'));
|
|
373
|
+
root.style.setProperty('--tint-name', `"${color.name}"`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function tintForUid(uid) {
|
|
377
|
+
if (!uid) return;
|
|
378
|
+
const seed = uidToSeed.get(uid) || uid;
|
|
379
|
+
if (seed === currentSeed) return;
|
|
380
|
+
currentSeed = seed;
|
|
381
|
+
const color = colorForSeed(seed);
|
|
382
|
+
applyTint(color, userOpts);
|
|
383
|
+
uidToColor.set(uid, color);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function setSeedForUid(uid, seed) {
|
|
387
|
+
uidToSeed.set(uid, seed);
|
|
388
|
+
const color = colorForSeed(seed);
|
|
389
|
+
uidToColor.set(uid, color);
|
|
390
|
+
return color;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function installRpcListener(store) {
|
|
394
|
+
if (rpcListenerInstalled) return;
|
|
395
|
+
if (typeof window === 'undefined' || !window.rpc || typeof window.rpc.on !== 'function') return;
|
|
396
|
+
rpcListenerInstalled = true;
|
|
397
|
+
|
|
398
|
+
rpcSeedListener = (payload) => {
|
|
399
|
+
try {
|
|
400
|
+
if (!payload || !payload.uid || !payload.seed) return;
|
|
401
|
+
setSeedForUid(payload.uid, payload.seed);
|
|
402
|
+
tintVersion += 1;
|
|
403
|
+
store.dispatch({ type: WINDOWTINT_COLOR_CHANGE, version: tintVersion });
|
|
404
|
+
|
|
405
|
+
// If the seed arrived after the session was already tinted (race —
|
|
406
|
+
// shouldn't happen in practice because we emit seed before `session
|
|
407
|
+
// add`, but cheap insurance), retint the active session now.
|
|
408
|
+
const state = store.getState();
|
|
409
|
+
const activeUid = state.sessions && state.sessions.activeUid;
|
|
410
|
+
if (activeUid === payload.uid) {
|
|
411
|
+
currentSeed = null; // force re-eval
|
|
412
|
+
tintForUid(activeUid);
|
|
413
|
+
}
|
|
414
|
+
} catch (e) { /* swallow */ }
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
window.rpc.on('windowtint:session-seed', rpcSeedListener);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
exports.onRendererUnload = () => {
|
|
421
|
+
try {
|
|
422
|
+
if (rpcSeedListener && typeof window !== 'undefined' && window.rpc) {
|
|
423
|
+
removeRpcListener(window.rpc, 'windowtint:session-seed', rpcSeedListener);
|
|
424
|
+
}
|
|
425
|
+
rpcSeedListener = null;
|
|
426
|
+
rpcListenerInstalled = false;
|
|
427
|
+
uidToSeed.clear();
|
|
428
|
+
uidToColor.clear();
|
|
429
|
+
currentSeed = null;
|
|
430
|
+
tintVersion = 0;
|
|
431
|
+
} catch (e) { /* swallow */ }
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
function parseOsc7Cwd(value) {
|
|
435
|
+
if (!value || typeof value !== 'string') return null;
|
|
436
|
+
try {
|
|
437
|
+
const url = new URL(value);
|
|
438
|
+
if (url.protocol !== 'file:') return null;
|
|
439
|
+
let pathname = decodeURIComponent(url.pathname || '');
|
|
440
|
+
// Guard `process`: with Electron contextIsolation the renderer may not
|
|
441
|
+
// have a `process` global. Without the guard this throws a ReferenceError
|
|
442
|
+
// and we silently fail to update on cwd change.
|
|
443
|
+
const isWin32 =
|
|
444
|
+
typeof process !== 'undefined' && process && process.platform === 'win32';
|
|
445
|
+
if (isWin32 && /^\/[a-zA-Z]:\//.test(pathname)) {
|
|
446
|
+
pathname = pathname.slice(1);
|
|
447
|
+
}
|
|
448
|
+
return pathname || null;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function sendCwdChange(uid, cwd) {
|
|
455
|
+
if (
|
|
456
|
+
!uid ||
|
|
457
|
+
!cwd ||
|
|
458
|
+
typeof window === 'undefined' ||
|
|
459
|
+
!window.rpc ||
|
|
460
|
+
typeof window.rpc.emit !== 'function'
|
|
461
|
+
) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
window.rpc.emit('windowtint:cwd-change', { uid, cwd });
|
|
466
|
+
} catch (e) { /* swallow */ }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function installOsc7Handler(uid, term) {
|
|
470
|
+
try {
|
|
471
|
+
if (!uid || !term || !term.parser || typeof term.parser.registerOscHandler !== 'function') return null;
|
|
472
|
+
return term.parser.registerOscHandler(7, (value) => {
|
|
473
|
+
const cwd = parseOsc7Cwd(value);
|
|
474
|
+
if (cwd) sendCwdChange(uid, cwd);
|
|
475
|
+
return true;
|
|
476
|
+
});
|
|
477
|
+
} catch (e) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
exports.decorateTerm = (Term, { React }) => {
|
|
483
|
+
return class WindowTintTerm extends React.PureComponent {
|
|
484
|
+
constructor(props) {
|
|
485
|
+
super(props);
|
|
486
|
+
this.osc7Disposable = null;
|
|
487
|
+
this.termComponent = null;
|
|
488
|
+
this.installInterval = null;
|
|
489
|
+
this.installStartedAt = 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
tryInstallOsc7 = () => {
|
|
493
|
+
if (this.osc7Disposable || !this.termComponent) return;
|
|
494
|
+
if (this.termComponent.term) {
|
|
495
|
+
this.osc7Disposable = installOsc7Handler(this.props.uid, this.termComponent.term);
|
|
496
|
+
if (this.installInterval) {
|
|
497
|
+
clearInterval(this.installInterval);
|
|
498
|
+
this.installInterval = null;
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (!this.installInterval) {
|
|
503
|
+
this.installStartedAt = Date.now();
|
|
504
|
+
this.installInterval = setInterval(() => {
|
|
505
|
+
if (Date.now() - this.installStartedAt > 5000) {
|
|
506
|
+
clearInterval(this.installInterval);
|
|
507
|
+
this.installInterval = null;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.tryInstallOsc7();
|
|
511
|
+
}, 50);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
disposeOsc7 = () => {
|
|
516
|
+
if (this.installInterval) {
|
|
517
|
+
clearInterval(this.installInterval);
|
|
518
|
+
this.installInterval = null;
|
|
519
|
+
}
|
|
520
|
+
if (this.osc7Disposable && typeof this.osc7Disposable.dispose === 'function') {
|
|
521
|
+
this.osc7Disposable.dispose();
|
|
522
|
+
}
|
|
523
|
+
this.osc7Disposable = null;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
onDecorated = (termComponent) => {
|
|
527
|
+
if (this.props.onDecorated) {
|
|
528
|
+
this.props.onDecorated(termComponent);
|
|
529
|
+
}
|
|
530
|
+
this.termComponent = termComponent;
|
|
531
|
+
if (!termComponent) {
|
|
532
|
+
this.disposeOsc7();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.tryInstallOsc7();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
componentDidMount() {
|
|
539
|
+
this.tryInstallOsc7();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
componentWillUnmount() {
|
|
543
|
+
this.disposeOsc7();
|
|
544
|
+
this.termComponent = null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
render() {
|
|
548
|
+
return React.createElement(Term, Object.assign({}, this.props, { onDecorated: this.onDecorated }));
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
exports.decorateConfig = (config) => {
|
|
554
|
+
userOpts = readUserConfig(config);
|
|
555
|
+
|
|
556
|
+
const badgeCSS = userOpts.showBadge ? `
|
|
557
|
+
.hyper_main::after {
|
|
558
|
+
content: var(--tint-name, '');
|
|
559
|
+
position: absolute;
|
|
560
|
+
top: 6px;
|
|
561
|
+
right: 14px;
|
|
562
|
+
font: 600 10px/1 -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
|
|
563
|
+
text-transform: uppercase;
|
|
564
|
+
letter-spacing: 0.1em;
|
|
565
|
+
color: var(--tint-color, transparent);
|
|
566
|
+
opacity: 0.55;
|
|
567
|
+
pointer-events: none;
|
|
568
|
+
z-index: 1000;
|
|
569
|
+
}` : '';
|
|
570
|
+
|
|
571
|
+
const css = `
|
|
572
|
+
/* hyper-windowtint v0.2 */
|
|
573
|
+
.hyper_main {
|
|
574
|
+
position: relative;
|
|
575
|
+
}
|
|
576
|
+
.hyper_main::before {
|
|
577
|
+
content: '';
|
|
578
|
+
position: absolute;
|
|
579
|
+
inset: 0;
|
|
580
|
+
pointer-events: none;
|
|
581
|
+
border: ${userOpts.borderWidth} solid var(--tint-color, transparent);
|
|
582
|
+
box-shadow: inset 0 0 28px -10px var(--tint-glow, transparent);
|
|
583
|
+
z-index: 999;
|
|
584
|
+
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
|
585
|
+
}
|
|
586
|
+
.hyper_main .tabs_title,
|
|
587
|
+
.hyper_main .tabs_borderShim {
|
|
588
|
+
border-top-color: var(--tint-color, transparent) !important;
|
|
589
|
+
}
|
|
590
|
+
.hyper_main .tab_tab.tab_active {
|
|
591
|
+
background: linear-gradient(180deg, var(--tint-tab-bg, transparent), transparent);
|
|
592
|
+
}
|
|
593
|
+
.hyper_main .tab_tab.tab_active .windowtint_tabAccent {
|
|
594
|
+
height: 3px;
|
|
595
|
+
opacity: 1;
|
|
596
|
+
}
|
|
597
|
+
${badgeCSS}
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
return Object.assign({}, config, {
|
|
601
|
+
css: `${config.css || ''}\n${css}`,
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
exports.decorateTab = (Tab, { React }) => {
|
|
606
|
+
return class WindowTintTab extends React.PureComponent {
|
|
607
|
+
render() {
|
|
608
|
+
const uid = this.props.windowTintUid;
|
|
609
|
+
const color = this.props.windowTintColor || null;
|
|
610
|
+
const accent = React.createElement('span', {
|
|
611
|
+
className: 'windowtint_tabAccent',
|
|
612
|
+
'data-windowtint-uid': uid,
|
|
613
|
+
style: {
|
|
614
|
+
position: 'absolute',
|
|
615
|
+
left: 0,
|
|
616
|
+
right: 0,
|
|
617
|
+
bottom: 0,
|
|
618
|
+
height: this.props.isActive ? 3 : 2,
|
|
619
|
+
background: color ? color.hex : 'transparent',
|
|
620
|
+
boxShadow: color ? `0 0 12px ${withAlpha(color.hex, '66')}` : 'none',
|
|
621
|
+
opacity: color ? (this.props.isActive ? 1 : 0.65) : 0,
|
|
622
|
+
pointerEvents: 'none',
|
|
623
|
+
transition: 'background 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease, height 0.2s ease',
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
const existing = this.props.customChildrenBefore;
|
|
627
|
+
const customChildrenBefore = existing ? [accent].concat(existing) : accent;
|
|
628
|
+
return React.createElement(Tab, Object.assign({}, this.props, { customChildrenBefore }));
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
exports.getTabProps = (tab, parentProps, props) => {
|
|
634
|
+
try {
|
|
635
|
+
if (!tab || !tab.uid) return props;
|
|
636
|
+
const colors = parentProps && parentProps.windowTintTabColors;
|
|
637
|
+
return Object.assign({}, props, {
|
|
638
|
+
windowTintUid: tab.uid,
|
|
639
|
+
windowTintColor: colors && colors[tab.uid] ? colors[tab.uid] : null,
|
|
640
|
+
windowTintVersion: parentProps && parentProps.windowTintVersion,
|
|
641
|
+
});
|
|
642
|
+
} catch (e) {
|
|
643
|
+
return props;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
exports.mapHeaderState = (state, props) => {
|
|
648
|
+
try {
|
|
649
|
+
const colors = {};
|
|
650
|
+
const activeSessions = state.termGroups && state.termGroups.activeSessions;
|
|
651
|
+
if (activeSessions) {
|
|
652
|
+
Object.keys(activeSessions).forEach((rootGroupUid) => {
|
|
653
|
+
const sessionUid = activeSessions[rootGroupUid];
|
|
654
|
+
const color = uidToColor.get(sessionUid);
|
|
655
|
+
if (color) colors[rootGroupUid] = color;
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return Object.assign({}, props, {
|
|
659
|
+
windowTintTabColors: colors,
|
|
660
|
+
windowTintVersion: state.ui && state.ui.windowTintVersion,
|
|
661
|
+
});
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return props;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
exports.reduceUI = (state, action) => {
|
|
668
|
+
if (!action || action.type !== WINDOWTINT_COLOR_CHANGE) return state;
|
|
669
|
+
try {
|
|
670
|
+
if (state && typeof state.set === 'function') {
|
|
671
|
+
return state.set('windowTintVersion', action.version);
|
|
672
|
+
}
|
|
673
|
+
return Object.assign({}, state, { windowTintVersion: action.version });
|
|
674
|
+
} catch (e) {
|
|
675
|
+
return state;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Redux middleware: re-tint when sessions are added or switched, using the
|
|
680
|
+
// project-group seed if available, falling back to session UID.
|
|
681
|
+
exports.middleware = (store) => (next) => (action) => {
|
|
682
|
+
installRpcListener(store);
|
|
683
|
+
|
|
684
|
+
const result = next(action);
|
|
685
|
+
|
|
686
|
+
if (
|
|
687
|
+
action.type === 'SESSION_ADD' ||
|
|
688
|
+
action.type === 'SESSION_SET_ACTIVE'
|
|
689
|
+
) {
|
|
690
|
+
try {
|
|
691
|
+
const state = store.getState();
|
|
692
|
+
const activeUid =
|
|
693
|
+
(state.sessions && state.sessions.activeUid) ||
|
|
694
|
+
action.uid ||
|
|
695
|
+
null;
|
|
696
|
+
tintForUid(activeUid);
|
|
697
|
+
} catch (e) {
|
|
698
|
+
// Decoration only — never crash the terminal over a tint.
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return result;
|
|
703
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hyper-windowtint",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Give Hyper terminal tabs distinct color tints, matching tabs from the same project during the current Hyper session.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CHANGELOG.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"hyper",
|
|
14
|
+
"hyper-plugin",
|
|
15
|
+
"hyper.app",
|
|
16
|
+
"hyperterm",
|
|
17
|
+
"terminal",
|
|
18
|
+
"theme",
|
|
19
|
+
"tabs",
|
|
20
|
+
"color",
|
|
21
|
+
"tint",
|
|
22
|
+
"cwd",
|
|
23
|
+
"project",
|
|
24
|
+
"osc7"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/willytop8/Hyper-WindowTint.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/willytop8/Hyper-WindowTint/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/willytop8/Hyper-WindowTint#readme",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": "William Ricchiuti",
|
|
36
|
+
"engines": {
|
|
37
|
+
"hyper": ">=3.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|