jupyterlab_colourful_tab_extension 1.0.10
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/LICENSE +29 -0
- package/README.md +57 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +319 -0
- package/package.json +197 -0
- package/src/__tests__/jupyterlab_colourful_tab_extension.spec.ts +29 -0
- package/src/index.ts +385 -0
- package/style/.ipynb_checkpoints/base-checkpoint.css +45 -0
- package/style/.ipynb_checkpoints/index-checkpoint.css +1 -0
- package/style/.ipynb_checkpoints/index-checkpoint.js +1 -0
- package/style/base.css +145 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Stellars Henson
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# jupyterlab_colourful_tab_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stellarshenson/jupyterlab_colourful_tab_extension/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/jupyterlab_colourful_tab_extension)
|
|
5
|
+
[](https://pypi.org/project/jupyterlab-colourful-tab-extension/)
|
|
6
|
+
[](https://pepy.tech/project/jupyterlab-colourful-tab-extension)
|
|
7
|
+
[](https://jupyterlab.readthedocs.io/en/stable/)
|
|
8
|
+
[](https://kolomolo.com)
|
|
9
|
+
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
|
+
|
|
11
|
+
A JupyterLab extension that applies pastel colours to tabs for visual identification when many tabs are open.
|
|
12
|
+
|
|
13
|
+
Coloured tabs make it easy to identify different notebooks and files at a glance.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
Right-click any tab to select a colour from the "Tab Colour" submenu.
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
Assign colours to tabs via right-click context menu for easy visual identification when working with multiple notebooks and files.
|
|
24
|
+
|
|
25
|
+
**Colour options**:
|
|
26
|
+
|
|
27
|
+
- Red, Orange, Yellow, Green, Blue, Purple
|
|
28
|
+
- Pastel shades optimised for both light and dark themes
|
|
29
|
+
|
|
30
|
+
**Key features**:
|
|
31
|
+
|
|
32
|
+
- Right-click any tab to assign a colour from the "Set Tab Colour" submenu
|
|
33
|
+
- Colours persist across browser refreshes via localStorage
|
|
34
|
+
- Notebook toolbar matches the active tab colour
|
|
35
|
+
- Automatic cleanup when tabs are closed
|
|
36
|
+
|
|
37
|
+
Yes, this is yet another mass-produced JupyterLab extension that does one trivially simple thing. We are almost embarrassed by how utterly unremarkable it is - just some CSS classes and a context menu. But someone had to do it, and here we are. You're welcome.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- JupyterLab >= 4.0.0
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
To install the extension, execute:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install jupyterlab_colourful_tab_extension
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Uninstall
|
|
52
|
+
|
|
53
|
+
To remove the extension, execute:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip uninstall jupyterlab_colourful_tab_extension
|
|
57
|
+
```
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
2
|
+
/**
|
|
3
|
+
* Colour definitions - CSS classes are defined in style/base.css
|
|
4
|
+
* CSS variables handle light/dark theme switching automatically
|
|
5
|
+
*/
|
|
6
|
+
const COLOURS = [
|
|
7
|
+
{ name: 'Red', id: 'rose', cssClass: 'jp-colourful-tab-rose' },
|
|
8
|
+
{ name: 'Orange', id: 'peach', cssClass: 'jp-colourful-tab-peach' },
|
|
9
|
+
{ name: 'Yellow', id: 'lemon', cssClass: 'jp-colourful-tab-lemon' },
|
|
10
|
+
{ name: 'Green', id: 'mint', cssClass: 'jp-colourful-tab-mint' },
|
|
11
|
+
{ name: 'Blue', id: 'sky', cssClass: 'jp-colourful-tab-sky' },
|
|
12
|
+
{ name: 'Purple', id: 'lavender', cssClass: 'jp-colourful-tab-lavender' }
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* LocalStorage key for persisting tab colours
|
|
16
|
+
*/
|
|
17
|
+
const STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
|
|
18
|
+
/**
|
|
19
|
+
* Storage for tab colours (persists across refreshes via localStorage)
|
|
20
|
+
* Maps widget ID to colour index
|
|
21
|
+
*/
|
|
22
|
+
const tabColours = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Load tab colours from localStorage
|
|
25
|
+
*/
|
|
26
|
+
function loadTabColours() {
|
|
27
|
+
try {
|
|
28
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
if (stored) {
|
|
30
|
+
const data = JSON.parse(stored);
|
|
31
|
+
Object.entries(data).forEach(([widgetId, colourIndex]) => {
|
|
32
|
+
tabColours.set(widgetId, colourIndex);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.warn('Colourful Tab: Failed to load colours from storage', e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Save tab colours to localStorage
|
|
42
|
+
*/
|
|
43
|
+
function saveTabColours() {
|
|
44
|
+
try {
|
|
45
|
+
const data = {};
|
|
46
|
+
tabColours.forEach((colourIndex, widgetId) => {
|
|
47
|
+
data[widgetId] = colourIndex;
|
|
48
|
+
});
|
|
49
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
console.warn('Colourful Tab: Failed to save colours to storage', e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Currently right-clicked tab element (set by contextmenu event)
|
|
57
|
+
*/
|
|
58
|
+
let currentTabElement = null;
|
|
59
|
+
/**
|
|
60
|
+
* Reference to terminal tracker for getting session names
|
|
61
|
+
*/
|
|
62
|
+
let terminalTracker = null;
|
|
63
|
+
/**
|
|
64
|
+
* Build a map from widget ID to terminal session name
|
|
65
|
+
*/
|
|
66
|
+
function getTerminalSessionMap() {
|
|
67
|
+
const widgetToSession = new Map();
|
|
68
|
+
if (terminalTracker) {
|
|
69
|
+
terminalTracker.forEach(widget => {
|
|
70
|
+
var _a, _b;
|
|
71
|
+
const session = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.session;
|
|
72
|
+
if ((_b = session === null || session === void 0 ? void 0 : session.model) === null || _b === void 0 ? void 0 : _b.name) {
|
|
73
|
+
widgetToSession.set(widget.id, `terminal:${session.model.name}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return widgetToSession;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Apply colour class to a tab element
|
|
81
|
+
*/
|
|
82
|
+
function applyTabColour(tabElement, colourIndex) {
|
|
83
|
+
// Remove all existing colour classes
|
|
84
|
+
COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
|
|
85
|
+
// Apply new colour class
|
|
86
|
+
if (colourIndex >= 0 && colourIndex < COLOURS.length) {
|
|
87
|
+
tabElement.classList.add(COLOURS[colourIndex].cssClass);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Clear colour from a tab element
|
|
92
|
+
*/
|
|
93
|
+
function clearTabColour(tabElement) {
|
|
94
|
+
COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get stable identifier for a tab.
|
|
98
|
+
* - For files: extract Path from title attribute
|
|
99
|
+
* - For terminals: use terminal session name (e.g., "terminal:1")
|
|
100
|
+
*/
|
|
101
|
+
function getStableTabId(tabElement) {
|
|
102
|
+
const title = tabElement.getAttribute('title');
|
|
103
|
+
const widgetId = tabElement.dataset.id;
|
|
104
|
+
// For files: title contains "Path: /path/to/file.ipynb"
|
|
105
|
+
if (title && title.includes('Path:')) {
|
|
106
|
+
const pathMatch = title.match(/Path:\s*(.+?)(?:\n|$)/);
|
|
107
|
+
if (pathMatch && pathMatch[1]) {
|
|
108
|
+
return pathMatch[1].trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// For terminals: use session name which persists across browser refresh
|
|
112
|
+
if (widgetId) {
|
|
113
|
+
const terminalMap = getTerminalSessionMap();
|
|
114
|
+
const sessionId = terminalMap.get(widgetId);
|
|
115
|
+
if (sessionId) {
|
|
116
|
+
return sessionId;
|
|
117
|
+
}
|
|
118
|
+
// Fallback to widget ID for non-terminal widgets
|
|
119
|
+
return widgetId;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Find all current tabs with their stable identifiers
|
|
125
|
+
*/
|
|
126
|
+
function getAllTabsByStableId() {
|
|
127
|
+
const idToTab = new Map();
|
|
128
|
+
const tabs = document.querySelectorAll('#jp-main-dock-panel .lm-TabBar-tab');
|
|
129
|
+
tabs.forEach(tab => {
|
|
130
|
+
const tabElement = tab;
|
|
131
|
+
const stableId = getStableTabId(tabElement);
|
|
132
|
+
if (stableId) {
|
|
133
|
+
idToTab.set(stableId, tabElement);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return idToTab;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Clean up colours for tabs that no longer exist
|
|
140
|
+
*/
|
|
141
|
+
function cleanupStaleColours() {
|
|
142
|
+
const currentTabs = getAllTabsByStableId();
|
|
143
|
+
const staleIds = [];
|
|
144
|
+
tabColours.forEach((_, storedId) => {
|
|
145
|
+
if (!currentTabs.has(storedId)) {
|
|
146
|
+
staleIds.push(storedId);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (staleIds.length > 0) {
|
|
150
|
+
staleIds.forEach(id => tabColours.delete(id));
|
|
151
|
+
saveTabColours();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Refresh all tab colours (useful after DOM changes)
|
|
156
|
+
*/
|
|
157
|
+
function refreshAllTabColours() {
|
|
158
|
+
const currentTabs = getAllTabsByStableId();
|
|
159
|
+
tabColours.forEach((colourIndex, storedId) => {
|
|
160
|
+
const tabElement = currentTabs.get(storedId);
|
|
161
|
+
if (tabElement &&
|
|
162
|
+
!tabElement.classList.contains(COLOURS[colourIndex].cssClass)) {
|
|
163
|
+
applyTabColour(tabElement, colourIndex);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// Clean up colours for closed tabs
|
|
167
|
+
cleanupStaleColours();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Debounce timer for refresh
|
|
171
|
+
*/
|
|
172
|
+
let refreshTimer = null;
|
|
173
|
+
/**
|
|
174
|
+
* Debounced refresh to avoid excessive DOM operations
|
|
175
|
+
*/
|
|
176
|
+
function debouncedRefresh() {
|
|
177
|
+
if (refreshTimer) {
|
|
178
|
+
clearTimeout(refreshTimer);
|
|
179
|
+
}
|
|
180
|
+
refreshTimer = setTimeout(() => {
|
|
181
|
+
refreshAllTabColours();
|
|
182
|
+
applyToolbarColour();
|
|
183
|
+
refreshTimer = null;
|
|
184
|
+
}, 50);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Apply colour to the active notebook's toolbar based on its tab colour
|
|
188
|
+
*/
|
|
189
|
+
function applyToolbarColour() {
|
|
190
|
+
// Find the currently active tab
|
|
191
|
+
const activeTab = document.querySelector('#jp-main-dock-panel .lm-TabBar-tab.lm-mod-current');
|
|
192
|
+
if (!activeTab) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Get the stable ID for this tab
|
|
196
|
+
const stableId = getStableTabId(activeTab);
|
|
197
|
+
if (!stableId) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Get the colour index for this tab
|
|
201
|
+
const colourIndex = tabColours.get(stableId);
|
|
202
|
+
// Find all toolbars and clear their colours first
|
|
203
|
+
const toolbars = document.querySelectorAll('jp-toolbar');
|
|
204
|
+
toolbars.forEach(toolbar => {
|
|
205
|
+
COLOURS.forEach(c => toolbar.classList.remove(c.cssClass));
|
|
206
|
+
});
|
|
207
|
+
// If the active tab has a colour, apply it to the active panel's toolbar
|
|
208
|
+
if (colourIndex !== undefined &&
|
|
209
|
+
colourIndex >= 0 &&
|
|
210
|
+
colourIndex < COLOURS.length) {
|
|
211
|
+
// Find the active panel's toolbar - it's in the currently visible notebook panel
|
|
212
|
+
const activePanel = document.querySelector('#jp-main-dock-panel .lm-DockPanel-widget:not(.lm-mod-hidden) jp-toolbar');
|
|
213
|
+
if (activePanel) {
|
|
214
|
+
activePanel.classList.add(COLOURS[colourIndex].cssClass);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Initialization data for the jupyterlab_colourful_tab_extension extension.
|
|
220
|
+
*/
|
|
221
|
+
const plugin = {
|
|
222
|
+
id: 'jupyterlab_colourful_tab_extension:plugin',
|
|
223
|
+
description: 'JupyterLab extension that makes tabs coloured using pastel colours to help identify them when many are open',
|
|
224
|
+
autoStart: true,
|
|
225
|
+
optional: [ITerminalTracker],
|
|
226
|
+
activate: (app, tracker) => {
|
|
227
|
+
console.log('JupyterLab extension jupyterlab_colourful_tab_extension is activated!');
|
|
228
|
+
// Store terminal tracker reference for session name lookups
|
|
229
|
+
terminalTracker = tracker;
|
|
230
|
+
// Load persisted colours from localStorage
|
|
231
|
+
loadTabColours();
|
|
232
|
+
const { commands } = app;
|
|
233
|
+
// Track right-clicked tab using capture phase to get it before menu opens
|
|
234
|
+
document.addEventListener('contextmenu', (event) => {
|
|
235
|
+
const target = event.target;
|
|
236
|
+
// Only capture tabs within the main dock panel tab bar
|
|
237
|
+
const tabElement = target.closest('#jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab');
|
|
238
|
+
if (tabElement && tabElement.classList.contains('lm-TabBar-tab')) {
|
|
239
|
+
currentTabElement = tabElement;
|
|
240
|
+
}
|
|
241
|
+
}, true // Use capture phase
|
|
242
|
+
);
|
|
243
|
+
// Register colour commands
|
|
244
|
+
COLOURS.forEach((colour, index) => {
|
|
245
|
+
commands.addCommand(`colourful-tab:set-${colour.id}`, {
|
|
246
|
+
label: colour.name,
|
|
247
|
+
caption: `Set tab colour to ${colour.name}`,
|
|
248
|
+
execute: () => {
|
|
249
|
+
// Verify we have a valid tab element
|
|
250
|
+
if (currentTabElement &&
|
|
251
|
+
currentTabElement.classList.contains('lm-TabBar-tab')) {
|
|
252
|
+
const stableId = getStableTabId(currentTabElement);
|
|
253
|
+
if (stableId) {
|
|
254
|
+
tabColours.set(stableId, index);
|
|
255
|
+
saveTabColours();
|
|
256
|
+
applyTabColour(currentTabElement, index);
|
|
257
|
+
applyToolbarColour();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
// Register clear command
|
|
264
|
+
commands.addCommand('colourful-tab:clear', {
|
|
265
|
+
label: 'Clear',
|
|
266
|
+
caption: 'Remove tab colour',
|
|
267
|
+
execute: () => {
|
|
268
|
+
// Verify we have a valid tab element
|
|
269
|
+
if (currentTabElement &&
|
|
270
|
+
currentTabElement.classList.contains('lm-TabBar-tab')) {
|
|
271
|
+
const stableId = getStableTabId(currentTabElement);
|
|
272
|
+
if (stableId) {
|
|
273
|
+
clearTabColour(currentTabElement);
|
|
274
|
+
tabColours.delete(stableId);
|
|
275
|
+
saveTabColours();
|
|
276
|
+
applyToolbarColour();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// Watch for DOM changes to reapply colours (e.g., when tabs are reordered or classes reset)
|
|
282
|
+
app.restored.then(() => {
|
|
283
|
+
const observer = new MutationObserver(mutations => {
|
|
284
|
+
// Check if any mutation affects tab elements
|
|
285
|
+
const affectsTabs = mutations.some(mutation => {
|
|
286
|
+
// Check for class attribute changes on tabs
|
|
287
|
+
if (mutation.type === 'attributes' &&
|
|
288
|
+
mutation.attributeName === 'class') {
|
|
289
|
+
const target = mutation.target;
|
|
290
|
+
return target.classList.contains('lm-TabBar-tab');
|
|
291
|
+
}
|
|
292
|
+
// Check for child list changes in tab bars
|
|
293
|
+
if (mutation.type === 'childList') {
|
|
294
|
+
const target = mutation.target;
|
|
295
|
+
return (target.classList.contains('lm-TabBar-content') ||
|
|
296
|
+
target.closest('.lm-TabBar') !== null);
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
});
|
|
300
|
+
if (affectsTabs) {
|
|
301
|
+
debouncedRefresh();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
const dockPanel = document.getElementById('jp-main-dock-panel');
|
|
305
|
+
if (dockPanel) {
|
|
306
|
+
observer.observe(dockPanel, {
|
|
307
|
+
childList: true,
|
|
308
|
+
subtree: true,
|
|
309
|
+
attributes: true,
|
|
310
|
+
attributeFilter: ['class']
|
|
311
|
+
});
|
|
312
|
+
// Initial application of colours
|
|
313
|
+
refreshAllTabColours();
|
|
314
|
+
applyToolbarColour();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jupyterlab_colourful_tab_extension",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "Jupyterlab extension that makes the tabs coloured (using pastel colours) to help you find tab when many of them are opened. Colour persists between refreshes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"jupyterlab-extension"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "BSD-3-Clause",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Stellars Henson",
|
|
17
|
+
"email": "konrad.jelen@gmail.com"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
21
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
22
|
+
"src/**/*.{ts,tsx}"
|
|
23
|
+
],
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"types": "lib/index.d.ts",
|
|
26
|
+
"style": "style/index.css",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
33
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
34
|
+
"build:labextension": "jupyter labextension build .",
|
|
35
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
36
|
+
"build:lib": "tsc --sourceMap",
|
|
37
|
+
"build:lib:prod": "tsc",
|
|
38
|
+
"clean": "jlpm clean:lib",
|
|
39
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
40
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
41
|
+
"clean:labextension": "rimraf jupyterlab_colourful_tab_extension/labextension jupyterlab_colourful_tab_extension/_version.py",
|
|
42
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
43
|
+
"eslint": "jlpm eslint:check --fix",
|
|
44
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
45
|
+
"install:extension": "jlpm build",
|
|
46
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
47
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
48
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
49
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
50
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
51
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
52
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
53
|
+
"test": "jest --coverage",
|
|
54
|
+
"watch": "run-p watch:src watch:labextension",
|
|
55
|
+
"watch:src": "tsc -w --sourceMap",
|
|
56
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@jupyterlab/application": "^4.0.0",
|
|
60
|
+
"@jupyterlab/terminal": "^4.0.0",
|
|
61
|
+
"@lumino/widgets": "^2.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
65
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
66
|
+
"@types/jest": "^29.2.0",
|
|
67
|
+
"@types/json-schema": "^7.0.11",
|
|
68
|
+
"@types/react": "^18.0.26",
|
|
69
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
70
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
71
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
72
|
+
"css-loader": "^6.7.1",
|
|
73
|
+
"eslint": "^8.36.0",
|
|
74
|
+
"eslint-config-prettier": "^8.8.0",
|
|
75
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
76
|
+
"jest": "^29.2.0",
|
|
77
|
+
"mkdirp": "^1.0.3",
|
|
78
|
+
"npm-run-all2": "^7.0.1",
|
|
79
|
+
"prettier": "^3.0.0",
|
|
80
|
+
"rimraf": "^5.0.1",
|
|
81
|
+
"source-map-loader": "^1.0.2",
|
|
82
|
+
"style-loader": "^3.3.1",
|
|
83
|
+
"stylelint": "^15.10.1",
|
|
84
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
85
|
+
"stylelint-config-standard": "^34.0.0",
|
|
86
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
87
|
+
"stylelint-prettier": "^4.0.0",
|
|
88
|
+
"typescript": "~5.5.4"
|
|
89
|
+
},
|
|
90
|
+
"sideEffects": [
|
|
91
|
+
"style/*.css",
|
|
92
|
+
"style/index.js"
|
|
93
|
+
],
|
|
94
|
+
"styleModule": "style/index.js",
|
|
95
|
+
"publishConfig": {
|
|
96
|
+
"access": "public"
|
|
97
|
+
},
|
|
98
|
+
"jupyterlab": {
|
|
99
|
+
"extension": true,
|
|
100
|
+
"outputDir": "jupyterlab_colourful_tab_extension/labextension",
|
|
101
|
+
"schemaDir": "schema"
|
|
102
|
+
},
|
|
103
|
+
"eslintIgnore": [
|
|
104
|
+
"node_modules",
|
|
105
|
+
"dist",
|
|
106
|
+
"coverage",
|
|
107
|
+
"**/*.d.ts",
|
|
108
|
+
"tests",
|
|
109
|
+
"**/__tests__",
|
|
110
|
+
"ui-tests"
|
|
111
|
+
],
|
|
112
|
+
"eslintConfig": {
|
|
113
|
+
"extends": [
|
|
114
|
+
"eslint:recommended",
|
|
115
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
116
|
+
"plugin:@typescript-eslint/recommended",
|
|
117
|
+
"plugin:prettier/recommended"
|
|
118
|
+
],
|
|
119
|
+
"parser": "@typescript-eslint/parser",
|
|
120
|
+
"parserOptions": {
|
|
121
|
+
"project": "tsconfig.json",
|
|
122
|
+
"sourceType": "module"
|
|
123
|
+
},
|
|
124
|
+
"plugins": [
|
|
125
|
+
"@typescript-eslint"
|
|
126
|
+
],
|
|
127
|
+
"rules": {
|
|
128
|
+
"@typescript-eslint/naming-convention": [
|
|
129
|
+
"error",
|
|
130
|
+
{
|
|
131
|
+
"selector": "interface",
|
|
132
|
+
"format": [
|
|
133
|
+
"PascalCase"
|
|
134
|
+
],
|
|
135
|
+
"custom": {
|
|
136
|
+
"regex": "^I[A-Z]",
|
|
137
|
+
"match": true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"@typescript-eslint/no-unused-vars": [
|
|
142
|
+
"warn",
|
|
143
|
+
{
|
|
144
|
+
"args": "none"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
148
|
+
"@typescript-eslint/no-namespace": "off",
|
|
149
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
150
|
+
"@typescript-eslint/quotes": [
|
|
151
|
+
"error",
|
|
152
|
+
"single",
|
|
153
|
+
{
|
|
154
|
+
"avoidEscape": true,
|
|
155
|
+
"allowTemplateLiterals": false
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"curly": [
|
|
159
|
+
"error",
|
|
160
|
+
"all"
|
|
161
|
+
],
|
|
162
|
+
"eqeqeq": "error",
|
|
163
|
+
"prefer-arrow-callback": "error"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"prettier": {
|
|
167
|
+
"singleQuote": true,
|
|
168
|
+
"trailingComma": "none",
|
|
169
|
+
"arrowParens": "avoid",
|
|
170
|
+
"endOfLine": "auto",
|
|
171
|
+
"overrides": [
|
|
172
|
+
{
|
|
173
|
+
"files": "package.json",
|
|
174
|
+
"options": {
|
|
175
|
+
"tabWidth": 4
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
"stylelint": {
|
|
181
|
+
"extends": [
|
|
182
|
+
"stylelint-config-recommended",
|
|
183
|
+
"stylelint-config-standard",
|
|
184
|
+
"stylelint-prettier/recommended"
|
|
185
|
+
],
|
|
186
|
+
"plugins": [
|
|
187
|
+
"stylelint-csstree-validator"
|
|
188
|
+
],
|
|
189
|
+
"rules": {
|
|
190
|
+
"csstree/validator": true,
|
|
191
|
+
"property-no-vendor-prefix": null,
|
|
192
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
193
|
+
"selector-no-vendor-prefix": null,
|
|
194
|
+
"value-no-vendor-prefix": null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for jupyterlab_colourful_tab_extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('jupyterlab_colourful_tab_extension', () => {
|
|
6
|
+
it('should define light theme colours', () => {
|
|
7
|
+
const lightColours = [
|
|
8
|
+
{ name: 'Rose', colour: '#ffd6e0' },
|
|
9
|
+
{ name: 'Peach', colour: '#ffe5cc' },
|
|
10
|
+
{ name: 'Lemon', colour: '#fff9c4' },
|
|
11
|
+
{ name: 'Mint', colour: '#c8f7c5' },
|
|
12
|
+
{ name: 'Sky', colour: '#c5e8f7' },
|
|
13
|
+
{ name: 'Lavender', colour: '#e5d6f7' }
|
|
14
|
+
];
|
|
15
|
+
expect(lightColours.length).toEqual(6);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should define dark theme colours', () => {
|
|
19
|
+
const darkColours = [
|
|
20
|
+
{ name: 'Rose', colour: '#5c3a42' },
|
|
21
|
+
{ name: 'Peach', colour: '#5c4a3a' },
|
|
22
|
+
{ name: 'Lemon', colour: '#5c5a3a' },
|
|
23
|
+
{ name: 'Mint', colour: '#3a5c3f' },
|
|
24
|
+
{ name: 'Sky', colour: '#3a4a5c' },
|
|
25
|
+
{ name: 'Lavender', colour: '#4a3a5c' }
|
|
26
|
+
];
|
|
27
|
+
expect(darkColours.length).toEqual(6);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Colour definitions - CSS classes are defined in style/base.css
|
|
9
|
+
* CSS variables handle light/dark theme switching automatically
|
|
10
|
+
*/
|
|
11
|
+
const COLOURS = [
|
|
12
|
+
{ name: 'Red', id: 'rose', cssClass: 'jp-colourful-tab-rose' },
|
|
13
|
+
{ name: 'Orange', id: 'peach', cssClass: 'jp-colourful-tab-peach' },
|
|
14
|
+
{ name: 'Yellow', id: 'lemon', cssClass: 'jp-colourful-tab-lemon' },
|
|
15
|
+
{ name: 'Green', id: 'mint', cssClass: 'jp-colourful-tab-mint' },
|
|
16
|
+
{ name: 'Blue', id: 'sky', cssClass: 'jp-colourful-tab-sky' },
|
|
17
|
+
{ name: 'Purple', id: 'lavender', cssClass: 'jp-colourful-tab-lavender' }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* LocalStorage key for persisting tab colours
|
|
22
|
+
*/
|
|
23
|
+
const STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Storage for tab colours (persists across refreshes via localStorage)
|
|
27
|
+
* Maps widget ID to colour index
|
|
28
|
+
*/
|
|
29
|
+
const tabColours: Map<string, number> = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load tab colours from localStorage
|
|
33
|
+
*/
|
|
34
|
+
function loadTabColours(): void {
|
|
35
|
+
try {
|
|
36
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
37
|
+
if (stored) {
|
|
38
|
+
const data = JSON.parse(stored) as Record<string, number>;
|
|
39
|
+
Object.entries(data).forEach(([widgetId, colourIndex]) => {
|
|
40
|
+
tabColours.set(widgetId, colourIndex);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.warn('Colourful Tab: Failed to load colours from storage', e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Save tab colours to localStorage
|
|
50
|
+
*/
|
|
51
|
+
function saveTabColours(): void {
|
|
52
|
+
try {
|
|
53
|
+
const data: Record<string, number> = {};
|
|
54
|
+
tabColours.forEach((colourIndex, widgetId) => {
|
|
55
|
+
data[widgetId] = colourIndex;
|
|
56
|
+
});
|
|
57
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn('Colourful Tab: Failed to save colours to storage', e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Currently right-clicked tab element (set by contextmenu event)
|
|
65
|
+
*/
|
|
66
|
+
let currentTabElement: HTMLElement | null = null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reference to terminal tracker for getting session names
|
|
70
|
+
*/
|
|
71
|
+
let terminalTracker: ITerminalTracker | null = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a map from widget ID to terminal session name
|
|
75
|
+
*/
|
|
76
|
+
function getTerminalSessionMap(): Map<string, string> {
|
|
77
|
+
const widgetToSession = new Map<string, string>();
|
|
78
|
+
if (terminalTracker) {
|
|
79
|
+
terminalTracker.forEach(widget => {
|
|
80
|
+
const session = widget.content?.session;
|
|
81
|
+
if (session?.model?.name) {
|
|
82
|
+
widgetToSession.set(widget.id, `terminal:${session.model.name}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return widgetToSession;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply colour class to a tab element
|
|
91
|
+
*/
|
|
92
|
+
function applyTabColour(tabElement: HTMLElement, colourIndex: number): void {
|
|
93
|
+
// Remove all existing colour classes
|
|
94
|
+
COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
|
|
95
|
+
|
|
96
|
+
// Apply new colour class
|
|
97
|
+
if (colourIndex >= 0 && colourIndex < COLOURS.length) {
|
|
98
|
+
tabElement.classList.add(COLOURS[colourIndex].cssClass);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clear colour from a tab element
|
|
104
|
+
*/
|
|
105
|
+
function clearTabColour(tabElement: HTMLElement): void {
|
|
106
|
+
COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get stable identifier for a tab.
|
|
111
|
+
* - For files: extract Path from title attribute
|
|
112
|
+
* - For terminals: use terminal session name (e.g., "terminal:1")
|
|
113
|
+
*/
|
|
114
|
+
function getStableTabId(tabElement: HTMLElement): string | null {
|
|
115
|
+
const title = tabElement.getAttribute('title');
|
|
116
|
+
const widgetId = tabElement.dataset.id;
|
|
117
|
+
|
|
118
|
+
// For files: title contains "Path: /path/to/file.ipynb"
|
|
119
|
+
if (title && title.includes('Path:')) {
|
|
120
|
+
const pathMatch = title.match(/Path:\s*(.+?)(?:\n|$)/);
|
|
121
|
+
if (pathMatch && pathMatch[1]) {
|
|
122
|
+
return pathMatch[1].trim();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// For terminals: use session name which persists across browser refresh
|
|
127
|
+
if (widgetId) {
|
|
128
|
+
const terminalMap = getTerminalSessionMap();
|
|
129
|
+
const sessionId = terminalMap.get(widgetId);
|
|
130
|
+
if (sessionId) {
|
|
131
|
+
return sessionId;
|
|
132
|
+
}
|
|
133
|
+
// Fallback to widget ID for non-terminal widgets
|
|
134
|
+
return widgetId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Find all current tabs with their stable identifiers
|
|
142
|
+
*/
|
|
143
|
+
function getAllTabsByStableId(): Map<string, HTMLElement> {
|
|
144
|
+
const idToTab = new Map<string, HTMLElement>();
|
|
145
|
+
const tabs = document.querySelectorAll('#jp-main-dock-panel .lm-TabBar-tab');
|
|
146
|
+
tabs.forEach(tab => {
|
|
147
|
+
const tabElement = tab as HTMLElement;
|
|
148
|
+
const stableId = getStableTabId(tabElement);
|
|
149
|
+
if (stableId) {
|
|
150
|
+
idToTab.set(stableId, tabElement);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return idToTab;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clean up colours for tabs that no longer exist
|
|
158
|
+
*/
|
|
159
|
+
function cleanupStaleColours(): void {
|
|
160
|
+
const currentTabs = getAllTabsByStableId();
|
|
161
|
+
const staleIds: string[] = [];
|
|
162
|
+
|
|
163
|
+
tabColours.forEach((_, storedId) => {
|
|
164
|
+
if (!currentTabs.has(storedId)) {
|
|
165
|
+
staleIds.push(storedId);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (staleIds.length > 0) {
|
|
170
|
+
staleIds.forEach(id => tabColours.delete(id));
|
|
171
|
+
saveTabColours();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Refresh all tab colours (useful after DOM changes)
|
|
177
|
+
*/
|
|
178
|
+
function refreshAllTabColours(): void {
|
|
179
|
+
const currentTabs = getAllTabsByStableId();
|
|
180
|
+
|
|
181
|
+
tabColours.forEach((colourIndex, storedId) => {
|
|
182
|
+
const tabElement = currentTabs.get(storedId);
|
|
183
|
+
if (
|
|
184
|
+
tabElement &&
|
|
185
|
+
!tabElement.classList.contains(COLOURS[colourIndex].cssClass)
|
|
186
|
+
) {
|
|
187
|
+
applyTabColour(tabElement, colourIndex);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Clean up colours for closed tabs
|
|
192
|
+
cleanupStaleColours();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Debounce timer for refresh
|
|
197
|
+
*/
|
|
198
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Debounced refresh to avoid excessive DOM operations
|
|
202
|
+
*/
|
|
203
|
+
function debouncedRefresh(): void {
|
|
204
|
+
if (refreshTimer) {
|
|
205
|
+
clearTimeout(refreshTimer);
|
|
206
|
+
}
|
|
207
|
+
refreshTimer = setTimeout(() => {
|
|
208
|
+
refreshAllTabColours();
|
|
209
|
+
applyToolbarColour();
|
|
210
|
+
refreshTimer = null;
|
|
211
|
+
}, 50);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply colour to the active notebook's toolbar based on its tab colour
|
|
216
|
+
*/
|
|
217
|
+
function applyToolbarColour(): void {
|
|
218
|
+
// Find the currently active tab
|
|
219
|
+
const activeTab = document.querySelector(
|
|
220
|
+
'#jp-main-dock-panel .lm-TabBar-tab.lm-mod-current'
|
|
221
|
+
) as HTMLElement;
|
|
222
|
+
if (!activeTab) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get the stable ID for this tab
|
|
227
|
+
const stableId = getStableTabId(activeTab);
|
|
228
|
+
if (!stableId) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get the colour index for this tab
|
|
233
|
+
const colourIndex = tabColours.get(stableId);
|
|
234
|
+
|
|
235
|
+
// Find all toolbars and clear their colours first
|
|
236
|
+
const toolbars = document.querySelectorAll('jp-toolbar');
|
|
237
|
+
toolbars.forEach(toolbar => {
|
|
238
|
+
COLOURS.forEach(c => toolbar.classList.remove(c.cssClass));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// If the active tab has a colour, apply it to the active panel's toolbar
|
|
242
|
+
if (
|
|
243
|
+
colourIndex !== undefined &&
|
|
244
|
+
colourIndex >= 0 &&
|
|
245
|
+
colourIndex < COLOURS.length
|
|
246
|
+
) {
|
|
247
|
+
// Find the active panel's toolbar - it's in the currently visible notebook panel
|
|
248
|
+
const activePanel = document.querySelector(
|
|
249
|
+
'#jp-main-dock-panel .lm-DockPanel-widget:not(.lm-mod-hidden) jp-toolbar'
|
|
250
|
+
);
|
|
251
|
+
if (activePanel) {
|
|
252
|
+
activePanel.classList.add(COLOURS[colourIndex].cssClass);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Initialization data for the jupyterlab_colourful_tab_extension extension.
|
|
259
|
+
*/
|
|
260
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
261
|
+
id: 'jupyterlab_colourful_tab_extension:plugin',
|
|
262
|
+
description:
|
|
263
|
+
'JupyterLab extension that makes tabs coloured using pastel colours to help identify them when many are open',
|
|
264
|
+
autoStart: true,
|
|
265
|
+
optional: [ITerminalTracker],
|
|
266
|
+
activate: (app: JupyterFrontEnd, tracker: ITerminalTracker | null) => {
|
|
267
|
+
console.log(
|
|
268
|
+
'JupyterLab extension jupyterlab_colourful_tab_extension is activated!'
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Store terminal tracker reference for session name lookups
|
|
272
|
+
terminalTracker = tracker;
|
|
273
|
+
|
|
274
|
+
// Load persisted colours from localStorage
|
|
275
|
+
loadTabColours();
|
|
276
|
+
|
|
277
|
+
const { commands } = app;
|
|
278
|
+
|
|
279
|
+
// Track right-clicked tab using capture phase to get it before menu opens
|
|
280
|
+
document.addEventListener(
|
|
281
|
+
'contextmenu',
|
|
282
|
+
(event: MouseEvent) => {
|
|
283
|
+
const target = event.target as HTMLElement;
|
|
284
|
+
// Only capture tabs within the main dock panel tab bar
|
|
285
|
+
const tabElement = target.closest(
|
|
286
|
+
'#jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab'
|
|
287
|
+
) as HTMLElement;
|
|
288
|
+
if (tabElement && tabElement.classList.contains('lm-TabBar-tab')) {
|
|
289
|
+
currentTabElement = tabElement;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
true // Use capture phase
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Register colour commands
|
|
296
|
+
COLOURS.forEach((colour, index) => {
|
|
297
|
+
commands.addCommand(`colourful-tab:set-${colour.id}`, {
|
|
298
|
+
label: colour.name,
|
|
299
|
+
caption: `Set tab colour to ${colour.name}`,
|
|
300
|
+
execute: () => {
|
|
301
|
+
// Verify we have a valid tab element
|
|
302
|
+
if (
|
|
303
|
+
currentTabElement &&
|
|
304
|
+
currentTabElement.classList.contains('lm-TabBar-tab')
|
|
305
|
+
) {
|
|
306
|
+
const stableId = getStableTabId(currentTabElement);
|
|
307
|
+
if (stableId) {
|
|
308
|
+
tabColours.set(stableId, index);
|
|
309
|
+
saveTabColours();
|
|
310
|
+
applyTabColour(currentTabElement, index);
|
|
311
|
+
applyToolbarColour();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Register clear command
|
|
319
|
+
commands.addCommand('colourful-tab:clear', {
|
|
320
|
+
label: 'Clear',
|
|
321
|
+
caption: 'Remove tab colour',
|
|
322
|
+
execute: () => {
|
|
323
|
+
// Verify we have a valid tab element
|
|
324
|
+
if (
|
|
325
|
+
currentTabElement &&
|
|
326
|
+
currentTabElement.classList.contains('lm-TabBar-tab')
|
|
327
|
+
) {
|
|
328
|
+
const stableId = getStableTabId(currentTabElement);
|
|
329
|
+
if (stableId) {
|
|
330
|
+
clearTabColour(currentTabElement);
|
|
331
|
+
tabColours.delete(stableId);
|
|
332
|
+
saveTabColours();
|
|
333
|
+
applyToolbarColour();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Watch for DOM changes to reapply colours (e.g., when tabs are reordered or classes reset)
|
|
340
|
+
app.restored.then(() => {
|
|
341
|
+
const observer = new MutationObserver(mutations => {
|
|
342
|
+
// Check if any mutation affects tab elements
|
|
343
|
+
const affectsTabs = mutations.some(mutation => {
|
|
344
|
+
// Check for class attribute changes on tabs
|
|
345
|
+
if (
|
|
346
|
+
mutation.type === 'attributes' &&
|
|
347
|
+
mutation.attributeName === 'class'
|
|
348
|
+
) {
|
|
349
|
+
const target = mutation.target as HTMLElement;
|
|
350
|
+
return target.classList.contains('lm-TabBar-tab');
|
|
351
|
+
}
|
|
352
|
+
// Check for child list changes in tab bars
|
|
353
|
+
if (mutation.type === 'childList') {
|
|
354
|
+
const target = mutation.target as HTMLElement;
|
|
355
|
+
return (
|
|
356
|
+
target.classList.contains('lm-TabBar-content') ||
|
|
357
|
+
target.closest('.lm-TabBar') !== null
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (affectsTabs) {
|
|
364
|
+
debouncedRefresh();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const dockPanel = document.getElementById('jp-main-dock-panel');
|
|
369
|
+
if (dockPanel) {
|
|
370
|
+
observer.observe(dockPanel, {
|
|
371
|
+
childList: true,
|
|
372
|
+
subtree: true,
|
|
373
|
+
attributes: true,
|
|
374
|
+
attributeFilter: ['class']
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Initial application of colours
|
|
378
|
+
refreshAllTabColours();
|
|
379
|
+
applyToolbarColour();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export default plugin;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
See the JupyterLab Developer Guide for useful CSS Patterns:
|
|
3
|
+
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Colour selection menu */
|
|
7
|
+
.jp-colourful-tab-menu {
|
|
8
|
+
position: fixed;
|
|
9
|
+
z-index: 10000;
|
|
10
|
+
background-color: var(--jp-layout-color1);
|
|
11
|
+
border: 1px solid var(--jp-border-color1);
|
|
12
|
+
border-radius: 4px;
|
|
13
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
14
|
+
min-width: 140px;
|
|
15
|
+
padding: 4px 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.jp-colourful-tab-menu-item {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
padding: 6px 12px;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
color: var(--jp-ui-font-color1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.jp-colourful-tab-menu-item:hover {
|
|
28
|
+
background-color: var(--jp-layout-color2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.jp-colourful-tab-swatch {
|
|
32
|
+
display: inline-block;
|
|
33
|
+
width: 16px;
|
|
34
|
+
height: 16px;
|
|
35
|
+
border-radius: 3px;
|
|
36
|
+
margin-right: 8px;
|
|
37
|
+
border: 1px solid var(--jp-border-color2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.jp-colourful-tab-menu-clear {
|
|
41
|
+
border-top: 1px solid var(--jp-border-color1);
|
|
42
|
+
margin-top: 4px;
|
|
43
|
+
padding-top: 8px;
|
|
44
|
+
color: var(--jp-ui-font-color2);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|
package/style/base.css
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
See the JupyterLab Developer Guide for useful CSS Patterns:
|
|
3
|
+
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Light theme pastel colours */
|
|
7
|
+
:root {
|
|
8
|
+
--jp-colourful-tab-rose: #ffd6e0;
|
|
9
|
+
--jp-colourful-tab-peach: #ffe5cc;
|
|
10
|
+
--jp-colourful-tab-lemon: #fff9c4;
|
|
11
|
+
--jp-colourful-tab-mint: #c8f7c5;
|
|
12
|
+
--jp-colourful-tab-sky: #a8d4f0;
|
|
13
|
+
--jp-colourful-tab-lavender: #e5d6f7;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Dark theme muted colours */
|
|
17
|
+
[data-jp-theme-light='false'] {
|
|
18
|
+
--jp-colourful-tab-rose: #5a3840;
|
|
19
|
+
--jp-colourful-tab-peach: #5a4a3a;
|
|
20
|
+
--jp-colourful-tab-lemon: #5a563a;
|
|
21
|
+
--jp-colourful-tab-mint: #3a5a3f;
|
|
22
|
+
--jp-colourful-tab-sky: #2e4258;
|
|
23
|
+
--jp-colourful-tab-lavender: #4a3a5a;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Colour selection menu */
|
|
27
|
+
.jp-colourful-tab-menu {
|
|
28
|
+
position: fixed;
|
|
29
|
+
z-index: 10000;
|
|
30
|
+
background-color: var(--jp-layout-color1);
|
|
31
|
+
border: 1px solid var(--jp-border-color1);
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
|
|
34
|
+
min-width: 140px;
|
|
35
|
+
padding: 4px 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.jp-colourful-tab-menu-item {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
padding: 6px 12px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
color: var(--jp-ui-font-color1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.jp-colourful-tab-menu-item:hover {
|
|
48
|
+
background-color: var(--jp-layout-color2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.jp-colourful-tab-swatch {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
width: 16px;
|
|
54
|
+
height: 16px;
|
|
55
|
+
border-radius: 3px;
|
|
56
|
+
margin-right: 8px;
|
|
57
|
+
border: 1px solid var(--jp-border-color2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Swatch colours */
|
|
61
|
+
.jp-colourful-tab-swatch-rose {
|
|
62
|
+
background-color: var(--jp-colourful-tab-rose);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.jp-colourful-tab-swatch-peach {
|
|
66
|
+
background-color: var(--jp-colourful-tab-peach);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.jp-colourful-tab-swatch-lemon {
|
|
70
|
+
background-color: var(--jp-colourful-tab-lemon);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.jp-colourful-tab-swatch-mint {
|
|
74
|
+
background-color: var(--jp-colourful-tab-mint);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.jp-colourful-tab-swatch-sky {
|
|
78
|
+
background-color: var(--jp-colourful-tab-sky);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.jp-colourful-tab-swatch-lavender {
|
|
82
|
+
background-color: var(--jp-colourful-tab-lavender);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Tab colours - apply to tab, inner elements, and shadow DOM parts */
|
|
86
|
+
.jp-colourful-tab-rose,
|
|
87
|
+
.jp-colourful-tab-rose * {
|
|
88
|
+
background-color: var(--jp-colourful-tab-rose) !important;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.jp-colourful-tab-rose::part(positioning-region) {
|
|
92
|
+
background-color: var(--jp-colourful-tab-rose) !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.jp-colourful-tab-peach,
|
|
96
|
+
.jp-colourful-tab-peach * {
|
|
97
|
+
background-color: var(--jp-colourful-tab-peach) !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.jp-colourful-tab-peach::part(positioning-region) {
|
|
101
|
+
background-color: var(--jp-colourful-tab-peach) !important;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.jp-colourful-tab-lemon,
|
|
105
|
+
.jp-colourful-tab-lemon * {
|
|
106
|
+
background-color: var(--jp-colourful-tab-lemon) !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.jp-colourful-tab-lemon::part(positioning-region) {
|
|
110
|
+
background-color: var(--jp-colourful-tab-lemon) !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.jp-colourful-tab-mint,
|
|
114
|
+
.jp-colourful-tab-mint * {
|
|
115
|
+
background-color: var(--jp-colourful-tab-mint) !important;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.jp-colourful-tab-mint::part(positioning-region) {
|
|
119
|
+
background-color: var(--jp-colourful-tab-mint) !important;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.jp-colourful-tab-sky,
|
|
123
|
+
.jp-colourful-tab-sky * {
|
|
124
|
+
background-color: var(--jp-colourful-tab-sky) !important;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.jp-colourful-tab-sky::part(positioning-region) {
|
|
128
|
+
background-color: var(--jp-colourful-tab-sky) !important;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.jp-colourful-tab-lavender,
|
|
132
|
+
.jp-colourful-tab-lavender * {
|
|
133
|
+
background-color: var(--jp-colourful-tab-lavender) !important;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.jp-colourful-tab-lavender::part(positioning-region) {
|
|
137
|
+
background-color: var(--jp-colourful-tab-lavender) !important;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.jp-colourful-tab-menu-clear {
|
|
141
|
+
border-top: 1px solid var(--jp-border-color1);
|
|
142
|
+
margin-top: 4px;
|
|
143
|
+
padding-top: 8px;
|
|
144
|
+
color: var(--jp-ui-font-color2);
|
|
145
|
+
}
|
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|