@jacksontian/mwt 1.0.0 → 1.1.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/lib/ring-buffer.js +1 -1
- package/lib/server.js +148 -148
- package/package.json +7 -2
- package/public/css/style.css +263 -3
- package/public/index.html +57 -2
- package/public/js/app.js +107 -14
- package/public/js/font-manager.js +43 -0
- package/public/js/layout-manager.js +94 -13
- package/public/js/terminal-manager.js +136 -24
- package/public/js/theme-manager.js +5 -5
- package/public/js/ws-client.js +4 -4
package/public/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<body>
|
|
12
12
|
<div id="toolbar">
|
|
13
13
|
<div class="toolbar-left">
|
|
14
|
-
<button id="btn-new-terminal" title="New Terminal
|
|
14
|
+
<button id="btn-new-terminal" title="New Terminal">
|
|
15
15
|
<span class="btn-icon">+</span> New Terminal
|
|
16
16
|
</button>
|
|
17
17
|
</div>
|
|
@@ -32,7 +32,13 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
<div class="toolbar-right">
|
|
35
|
-
<button id="btn-
|
|
35
|
+
<button id="btn-settings" title="Settings">
|
|
36
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 10a2 2 0 100-4 2 2 0 000 4z" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M2.93 2.93l1.06 1.06M12.01 12.01l1.06 1.06M2.93 13.07l1.06-1.06M12.01 3.99l1.06-1.06" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
37
|
+
</button>
|
|
38
|
+
<button id="btn-shortcuts" title="Keyboard Shortcuts">
|
|
39
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="3" width="14" height="10" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M4 7h1M7 7h1M10 7h1M4 9.5h8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
|
40
|
+
</button>
|
|
41
|
+
<button id="btn-fullscreen" title="Toggle fullscreen">
|
|
36
42
|
<svg class="icon-enter-fs" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
37
43
|
<svg class="icon-exit-fs" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 2v4H2M10 6h4V2M10 14v-4h4M6 10H2v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
38
44
|
</button>
|
|
@@ -50,6 +56,55 @@
|
|
|
50
56
|
<div id="terminal-container" class="layout-side-by-side"></div>
|
|
51
57
|
</div>
|
|
52
58
|
|
|
59
|
+
<div id="settings-modal" class="modal-overlay hidden">
|
|
60
|
+
<div class="modal">
|
|
61
|
+
<div class="modal-header">
|
|
62
|
+
<span class="modal-title">Settings</span>
|
|
63
|
+
<button id="btn-settings-close" class="modal-close">×</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="modal-body">
|
|
66
|
+
<div class="settings-group">
|
|
67
|
+
<div class="settings-group-title">Font</div>
|
|
68
|
+
<div class="settings-row">
|
|
69
|
+
<label class="settings-label" for="input-font-size">Size <span id="font-size-value"></span></label>
|
|
70
|
+
<input type="range" id="input-font-size" min="8" max="32" step="1">
|
|
71
|
+
</div>
|
|
72
|
+
<div class="settings-row">
|
|
73
|
+
<label class="settings-label" for="input-font-family">Family</label>
|
|
74
|
+
<input type="text" id="input-font-family" class="settings-text-input" placeholder="e.g. Menlo, monospace">
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div id="shortcuts-modal" class="modal-overlay hidden">
|
|
82
|
+
<div class="modal">
|
|
83
|
+
<div class="modal-header">
|
|
84
|
+
<span class="modal-title">Keyboard Shortcuts</span>
|
|
85
|
+
<button id="btn-shortcuts-close" class="modal-close">×</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="modal-body">
|
|
88
|
+
<div class="shortcut-group">
|
|
89
|
+
<div class="shortcut-group-title">Terminals</div>
|
|
90
|
+
<div class="shortcut-row"><span class="shortcut-desc">New Terminal</span><kbd class="shortcut-key" data-shortcut="Ctrl+`"></kbd></div>
|
|
91
|
+
<div class="shortcut-row"><span class="shortcut-desc">Close Terminal</span><kbd class="shortcut-key" data-shortcut="Ctrl+Shift+`"></kbd></div>
|
|
92
|
+
<div class="shortcut-row"><span class="shortcut-desc">Maximize / Restore Terminal</span><kbd class="shortcut-key" data-shortcut="Ctrl+Shift+M"></kbd></div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="shortcut-group">
|
|
95
|
+
<div class="shortcut-group-title">Navigation</div>
|
|
96
|
+
<div class="shortcut-row"><span class="shortcut-desc">Next Terminal</span><kbd class="shortcut-key" data-shortcut="Ctrl+PageDown"></kbd></div>
|
|
97
|
+
<div class="shortcut-row"><span class="shortcut-desc">Previous Terminal</span><kbd class="shortcut-key" data-shortcut="Ctrl+PageUp"></kbd></div>
|
|
98
|
+
<div class="shortcut-row"><span class="shortcut-desc">Switch to Terminal 1–9</span><kbd class="shortcut-key" data-shortcut="Ctrl+Shift+1~9"></kbd></div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="shortcut-group">
|
|
101
|
+
<div class="shortcut-group-title">Window</div>
|
|
102
|
+
<div class="shortcut-row"><span class="shortcut-desc">Toggle Fullscreen</span><kbd class="shortcut-key" data-shortcut="F11"></kbd></div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
53
108
|
<script src="/vendor/xterm/xterm.js"></script>
|
|
54
109
|
<script type="module" src="/js/app.js"></script>
|
|
55
110
|
</body>
|
package/public/js/app.js
CHANGED
|
@@ -2,14 +2,31 @@ import { WSClient } from './ws-client.js';
|
|
|
2
2
|
import { TerminalManager } from './terminal-manager.js';
|
|
3
3
|
import { LayoutManager } from './layout-manager.js';
|
|
4
4
|
import { ThemeManager } from './theme-manager.js';
|
|
5
|
+
import { FontManager } from './font-manager.js';
|
|
6
|
+
|
|
7
|
+
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
8
|
+
const cmdKey = isMac ? 'Cmd' : 'Ctrl';
|
|
9
|
+
|
|
5
10
|
const wsClient = new WSClient();
|
|
6
11
|
const container = document.getElementById('terminal-container');
|
|
7
12
|
const tabBar = document.getElementById('tab-bar');
|
|
8
13
|
const terminalCount = document.getElementById('terminal-count');
|
|
9
14
|
|
|
10
15
|
const themeManager = new ThemeManager();
|
|
16
|
+
const fontManager = new FontManager();
|
|
11
17
|
const layoutManager = new LayoutManager(container, tabBar);
|
|
12
|
-
const terminalManager = new TerminalManager(wsClient, container, themeManager);
|
|
18
|
+
const terminalManager = new TerminalManager(wsClient, container, themeManager, cmdKey, fontManager);
|
|
19
|
+
|
|
20
|
+
layoutManager.onTabActivate = (id) => {
|
|
21
|
+
terminalManager.focusTerminal(id);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
layoutManager.onLayoutApplied = () => {
|
|
25
|
+
const id = terminalManager.activeId || terminalManager.getIds()[0];
|
|
26
|
+
if (id) {
|
|
27
|
+
terminalManager.focusTerminal(id);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
13
30
|
|
|
14
31
|
// Theme toggle
|
|
15
32
|
document.getElementById('btn-theme-toggle').addEventListener('click', () => {
|
|
@@ -19,6 +36,7 @@ document.getElementById('btn-theme-toggle').addEventListener('click', () => {
|
|
|
19
36
|
// Cross-wire layout manager and terminal manager
|
|
20
37
|
layoutManager.fitAll = () => terminalManager.fitAll();
|
|
21
38
|
layoutManager.closeTerminal = (id) => terminalManager.closeTerminal(id);
|
|
39
|
+
layoutManager.swapTerminals = (id1, id2) => terminalManager.swapTerminals(id1, id2);
|
|
22
40
|
|
|
23
41
|
// Activity notification: visual indicators + browser notifications + title flash
|
|
24
42
|
const originalTitle = document.title;
|
|
@@ -60,7 +78,6 @@ terminalManager.onActivity((id, cleared) => {
|
|
|
60
78
|
});
|
|
61
79
|
n.onclick = () => {
|
|
62
80
|
window.focus();
|
|
63
|
-
terminalManager.activeId = id;
|
|
64
81
|
terminalManager.focusTerminal(id);
|
|
65
82
|
if (layoutManager.currentLayout === 'tabs') {
|
|
66
83
|
layoutManager.activateTab(id);
|
|
@@ -104,6 +121,10 @@ terminalManager.onChange((event) => {
|
|
|
104
121
|
layoutManager.onTerminalRemoved(event.id);
|
|
105
122
|
} else if (event.type === 'title') {
|
|
106
123
|
layoutManager.updateTabTitle(event.id, event.title);
|
|
124
|
+
} else if (event.type === 'maximize') {
|
|
125
|
+
layoutManager.onTerminalMaximized();
|
|
126
|
+
} else if (event.type === 'restore') {
|
|
127
|
+
layoutManager.onTerminalRestored();
|
|
107
128
|
}
|
|
108
129
|
updateCount();
|
|
109
130
|
});
|
|
@@ -113,6 +134,75 @@ document.getElementById('btn-new-terminal').addEventListener('click', () => {
|
|
|
113
134
|
terminalManager.createTerminal();
|
|
114
135
|
});
|
|
115
136
|
|
|
137
|
+
// Settings modal
|
|
138
|
+
const settingsModal = document.getElementById('settings-modal');
|
|
139
|
+
const inputFontSize = document.getElementById('input-font-size');
|
|
140
|
+
const inputFontFamily = document.getElementById('input-font-family');
|
|
141
|
+
const fontSizeValue = document.getElementById('font-size-value');
|
|
142
|
+
|
|
143
|
+
function syncSettingsUI() {
|
|
144
|
+
inputFontSize.value = fontManager.fontSize;
|
|
145
|
+
fontSizeValue.textContent = `${fontManager.fontSize}px`;
|
|
146
|
+
inputFontFamily.value = fontManager.fontFamily;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
document.getElementById('btn-settings').addEventListener('click', () => {
|
|
150
|
+
syncSettingsUI();
|
|
151
|
+
settingsModal.classList.remove('hidden');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
document.getElementById('btn-settings-close').addEventListener('click', () => {
|
|
155
|
+
settingsModal.classList.add('hidden');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
settingsModal.addEventListener('click', (e) => {
|
|
159
|
+
if (e.target === settingsModal) settingsModal.classList.add('hidden');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
inputFontSize.addEventListener('input', () => {
|
|
163
|
+
fontSizeValue.textContent = `${inputFontSize.value}px`;
|
|
164
|
+
fontManager.setFontSize(parseInt(inputFontSize.value, 10));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
inputFontFamily.addEventListener('change', () => {
|
|
168
|
+
const val = inputFontFamily.value.trim();
|
|
169
|
+
if (val) fontManager.setFontFamily(val);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Update button titles with platform-appropriate shortcut hints
|
|
173
|
+
document.getElementById('btn-new-terminal').title = `New Terminal (${cmdKey}+\`)`;
|
|
174
|
+
document.getElementById('btn-fullscreen').title = `Toggle fullscreen (F11)`;
|
|
175
|
+
document.getElementById('btn-shortcuts').title = `Keyboard Shortcuts`;
|
|
176
|
+
|
|
177
|
+
// Shortcuts modal
|
|
178
|
+
const shortcutsModal = document.getElementById('shortcuts-modal');
|
|
179
|
+
|
|
180
|
+
document.getElementById('btn-shortcuts').addEventListener('click', () => {
|
|
181
|
+
shortcutsModal.classList.remove('hidden');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
document.getElementById('btn-shortcuts-close').addEventListener('click', () => {
|
|
185
|
+
shortcutsModal.classList.add('hidden');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
shortcutsModal.addEventListener('click', (e) => {
|
|
189
|
+
if (e.target === shortcutsModal) {
|
|
190
|
+
shortcutsModal.classList.add('hidden');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
document.addEventListener('keydown', (e) => {
|
|
195
|
+
if (e.key === 'Escape') {
|
|
196
|
+
if (!shortcutsModal.classList.contains('hidden')) shortcutsModal.classList.add('hidden');
|
|
197
|
+
if (!settingsModal.classList.contains('hidden')) settingsModal.classList.add('hidden');
|
|
198
|
+
}
|
|
199
|
+
}, true);
|
|
200
|
+
|
|
201
|
+
// Fill platform-appropriate shortcut keys in modal
|
|
202
|
+
document.querySelectorAll('.shortcut-key[data-shortcut]').forEach(el => {
|
|
203
|
+
el.textContent = el.dataset.shortcut.replace(/^Ctrl/, cmdKey);
|
|
204
|
+
});
|
|
205
|
+
|
|
116
206
|
// Layout persistence
|
|
117
207
|
const LAYOUT_KEY = 'myterminal-layout';
|
|
118
208
|
const ACTIVE_TAB_KEY = 'myterminal-active-tab';
|
|
@@ -129,6 +219,11 @@ document.querySelectorAll('.layout-btn').forEach(btn => {
|
|
|
129
219
|
btn.addEventListener('click', () => {
|
|
130
220
|
document.querySelectorAll('.layout-btn').forEach(b => b.classList.remove('active'));
|
|
131
221
|
btn.classList.add('active');
|
|
222
|
+
// Sync active terminal to layout manager before switching, so tabs
|
|
223
|
+
// activates the same terminal the user is currently focused on.
|
|
224
|
+
if (terminalManager.activeId) {
|
|
225
|
+
layoutManager.activeTabId = terminalManager.activeId;
|
|
226
|
+
}
|
|
132
227
|
layoutManager.setLayout(btn.dataset.layout);
|
|
133
228
|
saveLayoutState();
|
|
134
229
|
});
|
|
@@ -151,24 +246,23 @@ document.addEventListener('fullscreenchange', () => {
|
|
|
151
246
|
document.addEventListener('keydown', (e) => {
|
|
152
247
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
153
248
|
const shift = e.shiftKey;
|
|
154
|
-
const alt = e.altKey;
|
|
155
249
|
|
|
156
|
-
// Ctrl
|
|
157
|
-
if (ctrl && shift && e.key === '
|
|
250
|
+
// Ctrl+`: new terminal
|
|
251
|
+
if (ctrl && !shift && e.key === '`') {
|
|
158
252
|
e.preventDefault();
|
|
159
253
|
terminalManager.createTerminal();
|
|
160
254
|
return;
|
|
161
255
|
}
|
|
162
256
|
|
|
163
|
-
// Ctrl+Shift
|
|
164
|
-
if (ctrl && shift && e.key === '
|
|
257
|
+
// Ctrl+Shift+`: close active terminal
|
|
258
|
+
if (ctrl && shift && e.key === '`') {
|
|
165
259
|
e.preventDefault();
|
|
166
260
|
terminalManager.closeActiveTerminal();
|
|
167
261
|
return;
|
|
168
262
|
}
|
|
169
263
|
|
|
170
|
-
// Ctrl+
|
|
171
|
-
if (ctrl &&
|
|
264
|
+
// Ctrl+PageDown: next terminal
|
|
265
|
+
if (ctrl && e.key === 'PageDown') {
|
|
172
266
|
e.preventDefault();
|
|
173
267
|
const nextId = terminalManager.focusNext();
|
|
174
268
|
if (nextId) {
|
|
@@ -180,8 +274,8 @@ document.addEventListener('keydown', (e) => {
|
|
|
180
274
|
return;
|
|
181
275
|
}
|
|
182
276
|
|
|
183
|
-
// Ctrl+
|
|
184
|
-
if (ctrl &&
|
|
277
|
+
// Ctrl+PageUp: previous terminal
|
|
278
|
+
if (ctrl && e.key === 'PageUp') {
|
|
185
279
|
e.preventDefault();
|
|
186
280
|
const prevId = terminalManager.focusPrev();
|
|
187
281
|
if (prevId) {
|
|
@@ -193,14 +287,13 @@ document.addEventListener('keydown', (e) => {
|
|
|
193
287
|
return;
|
|
194
288
|
}
|
|
195
289
|
|
|
196
|
-
//
|
|
197
|
-
if (
|
|
290
|
+
// Ctrl+Shift+1~9: switch to terminal N
|
|
291
|
+
if (ctrl && shift && e.key >= '1' && e.key <= '9') {
|
|
198
292
|
e.preventDefault();
|
|
199
293
|
const ids = terminalManager.getIds();
|
|
200
294
|
const idx = parseInt(e.key, 10) - 1;
|
|
201
295
|
if (idx < ids.length) {
|
|
202
296
|
const targetId = ids[idx];
|
|
203
|
-
terminalManager.activeId = targetId;
|
|
204
297
|
terminalManager.focusTerminal(targetId);
|
|
205
298
|
terminalManager.clearActivity(targetId);
|
|
206
299
|
if (layoutManager.currentLayout === 'tabs') {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const STORAGE_KEY_SIZE = 'myterminal-font-size';
|
|
2
|
+
const STORAGE_KEY_FAMILY = 'myterminal-font-family';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_FONT_SIZE = 14;
|
|
5
|
+
const DEFAULT_FONT_FAMILY = "'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace";
|
|
6
|
+
|
|
7
|
+
export class FontManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._listeners = [];
|
|
10
|
+
this._fontSize = parseInt(localStorage.getItem(STORAGE_KEY_SIZE), 10) || DEFAULT_FONT_SIZE;
|
|
11
|
+
this._fontFamily = localStorage.getItem(STORAGE_KEY_FAMILY) || DEFAULT_FONT_FAMILY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get fontSize() {
|
|
15
|
+
return this._fontSize;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get fontFamily() {
|
|
19
|
+
return this._fontFamily;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setFontSize(size) {
|
|
23
|
+
this._fontSize = size;
|
|
24
|
+
localStorage.setItem(STORAGE_KEY_SIZE, size);
|
|
25
|
+
this._notify();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setFontFamily(family) {
|
|
29
|
+
this._fontFamily = family;
|
|
30
|
+
localStorage.setItem(STORAGE_KEY_FAMILY, family);
|
|
31
|
+
this._notify();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onChange(callback) {
|
|
35
|
+
this._listeners.push(callback);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_notify() {
|
|
39
|
+
for (const cb of this._listeners) {
|
|
40
|
+
cb({ fontSize: this._fontSize, fontFamily: this._fontFamily });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -4,12 +4,20 @@ export class LayoutManager {
|
|
|
4
4
|
this.tabBar = tabBar;
|
|
5
5
|
this.currentLayout = 'side-by-side';
|
|
6
6
|
this.activeTabId = null;
|
|
7
|
+
/** @type {((id: string) => void) | null} Called after tab becomes active (focus PTY). */
|
|
8
|
+
this.onTabActivate = null;
|
|
9
|
+
/** @type {((layout: string) => void) | null} After refit when layout mode changes (focus PTY). */
|
|
10
|
+
this.onLayoutApplied = null;
|
|
7
11
|
// These will be set by app.js after construction
|
|
8
12
|
this.fitAll = () => {};
|
|
9
13
|
this.closeTerminal = () => {};
|
|
14
|
+
this.swapTerminals = null;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
setLayout(mode) {
|
|
18
|
+
// Clear maximized state before switching — layout takes priority
|
|
19
|
+
this._clearMaximizedState();
|
|
20
|
+
|
|
13
21
|
// Remove all layout classes
|
|
14
22
|
this.container.classList.remove('layout-side-by-side', 'layout-grid', 'layout-tabs');
|
|
15
23
|
this.container.classList.add(`layout-${mode}`);
|
|
@@ -37,8 +45,15 @@ export class LayoutManager {
|
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
// Refit after layout change
|
|
41
|
-
requestAnimationFrame(() =>
|
|
48
|
+
// Refit after layout change, then restore keyboard focus to the active terminal
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
this.fitAll();
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
if (this.onLayoutApplied) {
|
|
53
|
+
this.onLayoutApplied(this.currentLayout);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
onTerminalAdded(id) {
|
|
@@ -53,14 +68,29 @@ export class LayoutManager {
|
|
|
53
68
|
|
|
54
69
|
onTerminalRemoved(id) {
|
|
55
70
|
if (this.currentLayout === 'tabs') {
|
|
71
|
+
let nextTabId = null;
|
|
72
|
+
if (this.activeTabId === id) {
|
|
73
|
+
const tabs = [...this.tabBar.querySelectorAll('.tab')];
|
|
74
|
+
const tidx = tabs.findIndex(t => t.dataset.id === id);
|
|
75
|
+
if (tidx >= 0) {
|
|
76
|
+
if (tidx < tabs.length - 1) {
|
|
77
|
+
nextTabId = tabs[tidx + 1].dataset.id;
|
|
78
|
+
} else if (tidx > 0) {
|
|
79
|
+
nextTabId = tabs[tidx - 1].dataset.id;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
56
83
|
this._removeTab(id);
|
|
57
84
|
if (this.activeTabId === id) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (firstTab) {
|
|
61
|
-
this.activateTab(firstTab.dataset.id);
|
|
85
|
+
if (nextTabId) {
|
|
86
|
+
this.activateTab(nextTabId);
|
|
62
87
|
} else {
|
|
63
|
-
|
|
88
|
+
const firstTab = this.tabBar.querySelector('.tab');
|
|
89
|
+
if (firstTab) {
|
|
90
|
+
this.activateTab(firstTab.dataset.id);
|
|
91
|
+
} else {
|
|
92
|
+
this.activeTabId = null;
|
|
93
|
+
}
|
|
64
94
|
}
|
|
65
95
|
}
|
|
66
96
|
} else if (this.currentLayout === 'grid') {
|
|
@@ -85,21 +115,25 @@ export class LayoutManager {
|
|
|
85
115
|
// Clear activity indicator for the activated tab
|
|
86
116
|
this.clearActivity(id);
|
|
87
117
|
|
|
118
|
+
if (this.onTabActivate) {
|
|
119
|
+
this.onTabActivate(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
requestAnimationFrame(() => this.fitAll());
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
markActivity(id) {
|
|
92
126
|
const tab = this.tabBar.querySelector(`.tab[data-id="${id}"]`);
|
|
93
|
-
if (tab) tab.classList.add('has-activity');
|
|
127
|
+
if (tab) {tab.classList.add('has-activity');}
|
|
94
128
|
const pane = this.container.querySelector(`.terminal-pane[data-id="${id}"]`);
|
|
95
|
-
if (pane) pane.classList.add('has-activity');
|
|
129
|
+
if (pane) {pane.classList.add('has-activity');}
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
clearActivity(id) {
|
|
99
133
|
const tab = this.tabBar.querySelector(`.tab[data-id="${id}"]`);
|
|
100
|
-
if (tab) tab.classList.remove('has-activity');
|
|
134
|
+
if (tab) {tab.classList.remove('has-activity');}
|
|
101
135
|
const pane = this.container.querySelector(`.terminal-pane[data-id="${id}"]`);
|
|
102
|
-
if (pane) pane.classList.remove('has-activity');
|
|
136
|
+
if (pane) {pane.classList.remove('has-activity');}
|
|
103
137
|
}
|
|
104
138
|
|
|
105
139
|
_rebuildTabBar() {
|
|
@@ -115,6 +149,7 @@ export class LayoutManager {
|
|
|
115
149
|
const tab = document.createElement('div');
|
|
116
150
|
tab.className = 'tab';
|
|
117
151
|
tab.dataset.id = id;
|
|
152
|
+
tab.draggable = true;
|
|
118
153
|
|
|
119
154
|
const title = document.createElement('span');
|
|
120
155
|
title.className = 'tab-title';
|
|
@@ -136,6 +171,35 @@ export class LayoutManager {
|
|
|
136
171
|
this.activateTab(id);
|
|
137
172
|
});
|
|
138
173
|
|
|
174
|
+
tab.addEventListener('dragstart', (e) => {
|
|
175
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
176
|
+
e.dataTransfer.setData('text/plain', id);
|
|
177
|
+
});
|
|
178
|
+
tab.addEventListener('dragover', (e) => {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
e.dataTransfer.dropEffect = 'move';
|
|
181
|
+
tab.classList.add('drag-over');
|
|
182
|
+
});
|
|
183
|
+
tab.addEventListener('dragleave', () => {
|
|
184
|
+
tab.classList.remove('drag-over');
|
|
185
|
+
});
|
|
186
|
+
tab.addEventListener('drop', (e) => {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
tab.classList.remove('drag-over');
|
|
189
|
+
const sourceId = e.dataTransfer.getData('text/plain');
|
|
190
|
+
if (sourceId && sourceId !== id && this.swapTerminals) {
|
|
191
|
+
this.swapTerminals(sourceId, id);
|
|
192
|
+
// Reorder tab DOM to match
|
|
193
|
+
const sourceTab = this.tabBar.querySelector(`.tab[data-id="${sourceId}"]`);
|
|
194
|
+
if (sourceTab) {
|
|
195
|
+
const placeholder = document.createComment('tab-swap');
|
|
196
|
+
sourceTab.replaceWith(placeholder);
|
|
197
|
+
tab.replaceWith(sourceTab);
|
|
198
|
+
placeholder.replaceWith(tab);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
139
203
|
this.tabBar.appendChild(tab);
|
|
140
204
|
}
|
|
141
205
|
|
|
@@ -149,12 +213,29 @@ export class LayoutManager {
|
|
|
149
213
|
|
|
150
214
|
_removeTab(id) {
|
|
151
215
|
const tab = this.tabBar.querySelector(`.tab[data-id="${id}"]`);
|
|
152
|
-
if (tab) tab.remove();
|
|
216
|
+
if (tab) {tab.remove();}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_clearMaximizedState() {
|
|
220
|
+
this.container.querySelectorAll('.terminal-pane.maximized').forEach(el => el.classList.remove('maximized'));
|
|
221
|
+
this.container.querySelectorAll('.terminal-pane.hidden-by-maximize').forEach(el => el.classList.remove('hidden-by-maximize'));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
onTerminalMaximized() {
|
|
225
|
+
if (this.currentLayout === 'grid') {
|
|
226
|
+
this.container.style.gridTemplateColumns = '1fr';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
onTerminalRestored() {
|
|
231
|
+
if (this.currentLayout === 'grid') {
|
|
232
|
+
this._updateGridColumns();
|
|
233
|
+
}
|
|
153
234
|
}
|
|
154
235
|
|
|
155
236
|
_updateGridColumns() {
|
|
156
237
|
const count = this.container.querySelectorAll('.terminal-pane').length;
|
|
157
|
-
const cols = count
|
|
238
|
+
const cols = Math.ceil(Math.sqrt(count));
|
|
158
239
|
this.container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
|
159
240
|
}
|
|
160
241
|
}
|