@jacksontian/mwt 1.0.0 → 1.2.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.
@@ -2,16 +2,24 @@ export class LayoutManager {
2
2
  constructor(container, tabBar) {
3
3
  this.container = container;
4
4
  this.tabBar = tabBar;
5
- this.currentLayout = 'side-by-side';
5
+ this.currentLayout = 'columns';
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
- this.container.classList.remove('layout-side-by-side', 'layout-grid', 'layout-tabs');
22
+ this.container.classList.remove('layout-columns', 'layout-rows', 'layout-grid', 'layout-tabs');
15
23
  this.container.classList.add(`layout-${mode}`);
16
24
  this.currentLayout = mode;
17
25
 
@@ -37,8 +45,15 @@ export class LayoutManager {
37
45
  }
38
46
  }
39
47
 
40
- // Refit after layout change
41
- requestAnimationFrame(() => this.fitAll());
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
- // Activate another tab
59
- const firstTab = this.tabBar.querySelector('.tab');
60
- if (firstTab) {
61
- this.activateTab(firstTab.dataset.id);
85
+ if (nextTabId) {
86
+ this.activateTab(nextTabId);
62
87
  } else {
63
- this.activeTabId = null;
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,32 @@ 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
+ this.container.style.gridTemplateRows = '1fr';
228
+ }
229
+ }
230
+
231
+ onTerminalRestored() {
232
+ if (this.currentLayout === 'grid') {
233
+ this._updateGridColumns();
234
+ }
153
235
  }
154
236
 
155
237
  _updateGridColumns() {
156
238
  const count = this.container.querySelectorAll('.terminal-pane').length;
157
- const cols = count <= 1 ? 1 : count <= 4 ? 2 : count <= 9 ? 3 : 4;
239
+ const cols = Math.ceil(Math.sqrt(count));
240
+ const rows = Math.ceil(count / cols);
158
241
  this.container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
242
+ this.container.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
159
243
  }
160
244
  }
@@ -0,0 +1,102 @@
1
+ export class ShortcutManager {
2
+ /**
3
+ * @param {{ terminalManager, layoutManager, cmdKey: string }} opts
4
+ */
5
+ constructor({ terminalManager, layoutManager, cmdKey }) {
6
+ this._tm = terminalManager;
7
+ this._lm = layoutManager;
8
+ this._cmdKey = cmdKey;
9
+ this._handler = this._onKeyDown.bind(this);
10
+ document.addEventListener('keydown', this._handler, true);
11
+ terminalManager.setShortcutManager(this);
12
+ }
13
+
14
+ // Called from document capture phase
15
+ _onKeyDown(e) {
16
+ if (this.matchKeyDown(e)) {
17
+ e._shortcutHandled = true;
18
+ }
19
+ }
20
+
21
+ // Returns true if the event matched a shortcut (used by xterm handler to block key)
22
+ matchKeyDown(e) {
23
+ const ctrl = e.ctrlKey;
24
+ const alt = e.altKey;
25
+ const tm = this._tm;
26
+ const lm = this._lm;
27
+
28
+ // Ctrl+Alt+N: new terminal
29
+ if (ctrl && alt && e.code === 'KeyN') {
30
+ e.preventDefault();
31
+ tm.createTerminal();
32
+ return true;
33
+ }
34
+
35
+ // Ctrl+Alt+W: close active terminal
36
+ if (ctrl && alt && e.code === 'KeyW') {
37
+ e.preventDefault();
38
+ tm.closeActiveTerminal();
39
+ return true;
40
+ }
41
+
42
+ // Ctrl+Alt+ArrowRight: next terminal
43
+ if (ctrl && alt && e.code === 'ArrowRight') {
44
+ e.preventDefault();
45
+ const nextId = tm.focusNext();
46
+ if (nextId) {
47
+ tm.clearActivity(nextId);
48
+ if (lm.currentLayout === 'tabs') { lm.activateTab(nextId); }
49
+ }
50
+ return true;
51
+ }
52
+
53
+ // Ctrl+Alt+ArrowLeft: previous terminal
54
+ if (ctrl && alt && e.code === 'ArrowLeft') {
55
+ e.preventDefault();
56
+ const prevId = tm.focusPrev();
57
+ if (prevId) {
58
+ tm.clearActivity(prevId);
59
+ if (lm.currentLayout === 'tabs') { lm.activateTab(prevId); }
60
+ }
61
+ return true;
62
+ }
63
+
64
+ // Alt+1~9: switch to terminal N
65
+ if (alt && !ctrl && e.code >= 'Digit1' && e.code <= 'Digit9') {
66
+ e.preventDefault();
67
+ const ids = tm.getIds();
68
+ const idx = parseInt(e.code[5], 10) - 1;
69
+ if (idx < ids.length) {
70
+ const targetId = ids[idx];
71
+ tm.focusTerminal(targetId);
72
+ tm.clearActivity(targetId);
73
+ if (lm.currentLayout === 'tabs') { lm.activateTab(targetId); }
74
+ }
75
+ return true;
76
+ }
77
+
78
+ // Ctrl+Alt+M: maximize/restore active terminal
79
+ if (ctrl && alt && e.code === 'KeyM') {
80
+ e.preventDefault();
81
+ tm.toggleMaximizeActive();
82
+ return true;
83
+ }
84
+
85
+ // F11: toggle fullscreen
86
+ if (e.key === 'F11') {
87
+ e.preventDefault();
88
+ if (!document.fullscreenElement) {
89
+ document.documentElement.requestFullscreen();
90
+ } else {
91
+ document.exitFullscreen();
92
+ }
93
+ return true;
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ destroy() {
100
+ document.removeEventListener('keydown', this._handler, true);
101
+ }
102
+ }
@@ -1,20 +1,26 @@
1
1
  import { FitAddon } from '/vendor/xterm/addon-fit.mjs';
2
2
  import { WebLinksAddon } from '/vendor/xterm/addon-web-links.mjs';
3
+ import { DragManager } from './drag-manager.js';
3
4
 
4
5
  // xterm.js is loaded as UMD via <script> tag, access from global
5
6
  const { Terminal } = globalThis;
6
7
 
7
8
  export class TerminalManager {
8
- constructor(wsClient, container, themeManager) {
9
+ constructor(wsClient, container, themeManager, cmdKey = 'Ctrl', fontManager = null, i18n = null) {
9
10
  this.wsClient = wsClient;
10
11
  this.container = container;
11
12
  this.themeManager = themeManager;
13
+ this.fontManager = fontManager;
14
+ this.cmdKey = cmdKey;
15
+ this.i18n = i18n;
12
16
  this.terminals = new Map(); // id -> { term, fitAddon, element, resizeObserver }
13
17
  this.counter = 0;
14
18
  this.activeId = null;
15
19
  this.changeCallbacks = [];
16
20
  this.activityCallbacks = [];
21
+ this._dragManager = new DragManager((id1, id2) => this.swapTerminals(id1, id2));
17
22
  this.activitySet = new Set(); // terminal IDs with unread activity
23
+ this._shortcutManager = null;
18
24
 
19
25
  // Update all terminals when theme changes
20
26
  if (themeManager) {
@@ -25,6 +31,17 @@ export class TerminalManager {
25
31
  }
26
32
  });
27
33
  }
34
+
35
+ // Update all terminals when font changes
36
+ if (fontManager) {
37
+ fontManager.onChange(({ fontSize, fontFamily }) => {
38
+ for (const [, entry] of this.terminals) {
39
+ entry.term.options.fontSize = fontSize;
40
+ entry.term.options.fontFamily = fontFamily;
41
+ entry.fitAddon.fit();
42
+ }
43
+ });
44
+ }
28
45
  }
29
46
 
30
47
  _buildTerminal(id) {
@@ -40,14 +57,17 @@ export class TerminalManager {
40
57
 
41
58
  const title = document.createElement('span');
42
59
  title.className = 'pane-title';
43
- title.textContent = `Terminal ${num}`;
60
+ title.textContent = this.i18n ? this.i18n.t('terminalTitle', num) : `Terminal ${num}`;
61
+
62
+ // Drag to swap
63
+ this._dragManager.attach(pane, header, id);
44
64
 
45
65
  const headerActions = document.createElement('div');
46
66
  headerActions.className = 'pane-actions';
47
67
 
48
68
  const maximizeBtn = document.createElement('button');
49
69
  maximizeBtn.className = 'pane-maximize';
50
- maximizeBtn.title = 'Maximize terminal';
70
+ maximizeBtn.title = this.i18n ? this.i18n.t('maximizeTerminal', this.cmdKey) : `Maximize terminal (${this.cmdKey}+Shift+M)`;
51
71
  maximizeBtn.innerHTML = '<svg class="icon-maximize" width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5"/></svg>'
52
72
  + '<svg class="icon-restore" width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="4" y="1" width="11" height="11" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M4 5H2.5A1.5 1.5 0 001 6.5v8A1.5 1.5 0 002.5 16h8a1.5 1.5 0 001.5-1.5V13" stroke="currentColor" stroke-width="1.5"/></svg>';
53
73
  maximizeBtn.addEventListener('click', (e) => {
@@ -58,7 +78,7 @@ export class TerminalManager {
58
78
  const closeBtn = document.createElement('button');
59
79
  closeBtn.className = 'pane-close';
60
80
  closeBtn.textContent = '\u00d7';
61
- closeBtn.title = 'Close terminal';
81
+ closeBtn.title = this.i18n ? this.i18n.t('closeTerminalBtn', this.cmdKey) : `Close terminal (${this.cmdKey}+Shift+\`)`;
62
82
  closeBtn.addEventListener('click', (e) => {
63
83
  e.stopPropagation();
64
84
  this.closeTerminal(id);
@@ -80,9 +100,10 @@ export class TerminalManager {
80
100
  // Create xterm instance
81
101
  const term = new Terminal({
82
102
  cursorBlink: true,
83
- fontSize: 14,
84
- fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace",
103
+ fontSize: this.fontManager ? this.fontManager.fontSize : 14,
104
+ fontFamily: this.fontManager ? this.fontManager.fontFamily : "'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace",
85
105
  lineHeight: 1.15,
106
+ minimumContrastRatio: 4.5,
86
107
  theme: this.themeManager ? this.themeManager.getTerminalTheme() : {},
87
108
  allowProposedApi: true,
88
109
  });
@@ -93,6 +114,19 @@ export class TerminalManager {
93
114
  term.loadAddon(webLinksAddon);
94
115
  term.open(body);
95
116
 
117
+ // Block xterm from sending app shortcut keys to the terminal process
118
+ term.attachCustomKeyEventHandler((e) => {
119
+ if (e.type !== 'keydown') return true;
120
+ // Already handled by document capture phase — block xterm
121
+ if (e._shortcutHandled) return false;
122
+ // Focus is in xterm and capture phase didn't fire — try shortcut manager directly
123
+ if (this._shortcutManager) {
124
+ e._shortcutHandled = true;
125
+ if (this._shortcutManager.matchKeyDown(e)) return false;
126
+ }
127
+ return true;
128
+ });
129
+
96
130
  // Fit after layout settles
97
131
  requestAnimationFrame(() => {
98
132
  try { fitAddon.fit(); term.scrollToBottom(); } catch { /* ignore if not visible */ }
@@ -111,7 +145,7 @@ export class TerminalManager {
111
145
  // Wire xterm -> server (muted during buffer replay to suppress escape sequence responses)
112
146
  let muted = false;
113
147
  term.onData((data) => {
114
- if (!muted) this.wsClient.sendData(id, data);
148
+ if (!muted) {this.wsClient.sendData(id, data);}
115
149
  });
116
150
 
117
151
  term.onResize(({ cols, rows }) => {
@@ -139,18 +173,22 @@ export class TerminalManager {
139
173
  });
140
174
 
141
175
  // Track active terminal on focus and clear activity
142
- pane.addEventListener('mousedown', () => { this.activeId = id; this.clearActivity(id); });
143
- term.textarea?.addEventListener('focus', () => { this.activeId = id; this.clearActivity(id); });
176
+ pane.addEventListener('mousedown', () => { this.setActiveTerminal(id); this.clearActivity(id); });
177
+ term.textarea?.addEventListener('focus', () => { this.setActiveTerminal(id); this.clearActivity(id); });
144
178
 
145
179
  const setMuted = (v) => { muted = v; };
146
180
  this.terminals.set(id, { term, fitAddon, element: pane, resizeObserver, setMuted });
147
181
  this._notifyChange({ type: 'add', id });
148
- this.activeId = id;
182
+ this.setActiveTerminal(id);
149
183
  term.focus();
150
184
 
151
185
  return { term, fitAddon, element: pane, resizeObserver, setMuted };
152
186
  }
153
187
 
188
+ setShortcutManager(sm) {
189
+ this._shortcutManager = sm;
190
+ }
191
+
154
192
  createTerminal() {
155
193
  const id = `term-${++this.counter}`;
156
194
  const { fitAddon } = this._buildTerminal(id);
@@ -165,7 +203,7 @@ export class TerminalManager {
165
203
  restoreTerminal(id) {
166
204
  // Update counter to avoid future ID collisions
167
205
  const num = parseInt(id.replace('term-', ''), 10);
168
- if (num > this.counter) this.counter = num;
206
+ if (num > this.counter) {this.counter = num;}
169
207
 
170
208
  const { fitAddon, setMuted } = this._buildTerminal(id);
171
209
 
@@ -183,7 +221,7 @@ export class TerminalManager {
183
221
 
184
222
  unmuteTerminal(id) {
185
223
  const entry = this.terminals.get(id);
186
- if (entry) entry.setMuted(false);
224
+ if (entry) {entry.setMuted(false);}
187
225
  }
188
226
 
189
227
  unmuteAll() {
@@ -201,11 +239,15 @@ export class TerminalManager {
201
239
  }
202
240
  this.terminals.clear();
203
241
  this.counter = 0;
242
+ this.activeId = null;
204
243
  }
205
244
 
206
245
  closeTerminal(id) {
207
246
  const entry = this.terminals.get(id);
208
- if (!entry) return;
247
+ if (!entry) {return;}
248
+
249
+ const idsBefore = this.getIds();
250
+ const idx = idsBefore.indexOf(id);
209
251
 
210
252
  // If closing a maximized terminal, restore others first
211
253
  if (entry.element.classList.contains('maximized')) {
@@ -221,12 +263,51 @@ export class TerminalManager {
221
263
  entry.element.remove();
222
264
  this.terminals.delete(id);
223
265
 
266
+ if (this.activeId === id) {
267
+ let nextId = null;
268
+ if (idx >= 0) {
269
+ if (idx < idsBefore.length - 1) {
270
+ nextId = idsBefore[idx + 1];
271
+ } else if (idx > 0) {
272
+ nextId = idsBefore[idx - 1];
273
+ }
274
+ }
275
+ this.activeId = nextId;
276
+ }
277
+ this._syncPaneFocus();
278
+
279
+ const focusId = this.activeId;
280
+ if (focusId) {
281
+ requestAnimationFrame(() => {
282
+ if (this.terminals.has(focusId)) {
283
+ this.focusTerminal(focusId);
284
+ }
285
+ });
286
+ }
287
+
224
288
  this._notifyChange({ type: 'remove', id });
225
289
  }
226
290
 
291
+ /** Updates active terminal state and pane-focused styling (call before focus when switching programmatically). */
292
+ setActiveTerminal(id) {
293
+ if (!this.terminals.has(id)) {return;}
294
+ this.activeId = id;
295
+ this._syncPaneFocus();
296
+ }
297
+
298
+ _syncPaneFocus() {
299
+ const active = this.activeId;
300
+ for (const [tid, entry] of this.terminals) {
301
+ entry.element.classList.toggle('pane-focused', tid === active);
302
+ }
303
+ }
304
+
227
305
  focusTerminal(id) {
228
306
  const entry = this.terminals.get(id);
229
- if (entry) entry.term.focus();
307
+ if (entry) {
308
+ this.setActiveTerminal(id);
309
+ entry.term.focus();
310
+ }
230
311
  }
231
312
 
232
313
  fitAll() {
@@ -248,7 +329,7 @@ export class TerminalManager {
248
329
 
249
330
  toggleMaximize(id) {
250
331
  const entry = this.terminals.get(id);
251
- if (!entry) return;
332
+ if (!entry) {return;}
252
333
 
253
334
  const pane = entry.element;
254
335
  const isMaximized = pane.classList.contains('maximized');
@@ -276,7 +357,7 @@ export class TerminalManager {
276
357
 
277
358
  getMaximizedId() {
278
359
  for (const [id, entry] of this.terminals) {
279
- if (entry.element.classList.contains('maximized')) return id;
360
+ if (entry.element.classList.contains('maximized')) {return id;}
280
361
  }
281
362
  return null;
282
363
  }
@@ -289,44 +370,60 @@ export class TerminalManager {
289
370
  return [...this.terminals.keys()];
290
371
  }
291
372
 
373
+ updateI18n(i18n, cmdKey) {
374
+ this.i18n = i18n;
375
+ if (cmdKey) this.cmdKey = cmdKey;
376
+ for (const [id, entry] of this.terminals) {
377
+ const pane = entry.element;
378
+ const maximizeBtn = pane.querySelector('.pane-maximize');
379
+ const closeBtn = pane.querySelector('.pane-close');
380
+ if (maximizeBtn) maximizeBtn.title = i18n.t('maximizeTerminal', this.cmdKey);
381
+ if (closeBtn) closeBtn.title = i18n.t('closeTerminalBtn', this.cmdKey);
382
+ // Only update title if it hasn't been overridden by the shell (OSC)
383
+ const titleEl = pane.querySelector('.pane-title');
384
+ if (titleEl && !titleEl.title) {
385
+ const num = id.replace('term-', '');
386
+ titleEl.textContent = i18n.t('terminalTitle', num);
387
+ }
388
+ }
389
+ }
390
+
292
391
  getTitle(id) {
293
392
  const entry = this.terminals.get(id);
294
393
  if (entry) {
295
394
  const paneTitle = entry.element.querySelector('.pane-title');
296
- if (paneTitle) return paneTitle.textContent;
395
+ if (paneTitle) {return paneTitle.textContent;}
297
396
  }
298
397
  const idx = id.replace('term-', '');
299
- return `Terminal ${idx}`;
398
+ return this.i18n ? this.i18n.t('terminalTitle', idx) : `Terminal ${idx}`;
300
399
  }
301
400
 
302
401
  focusNext() {
303
402
  const ids = this.getIds();
304
- if (ids.length <= 1) return;
403
+ if (ids.length <= 1) {return;}
305
404
  const idx = ids.indexOf(this.activeId);
306
405
  const nextId = ids[(idx + 1) % ids.length];
307
- this.activeId = nextId;
308
406
  this.focusTerminal(nextId);
309
407
  return nextId;
310
408
  }
311
409
 
312
410
  focusPrev() {
313
411
  const ids = this.getIds();
314
- if (ids.length <= 1) return;
412
+ if (ids.length <= 1) {return;}
315
413
  const idx = ids.indexOf(this.activeId);
316
414
  const prevId = ids[(idx - 1 + ids.length) % ids.length];
317
- this.activeId = prevId;
318
415
  this.focusTerminal(prevId);
319
416
  return prevId;
320
417
  }
321
418
 
322
419
  closeActiveTerminal() {
323
420
  const id = this.activeId || this.getIds()[0];
324
- if (id) this.closeTerminal(id);
421
+ if (id) {this.closeTerminal(id);}
325
422
  }
326
423
 
327
424
  toggleMaximizeActive() {
328
425
  const id = this.activeId || this.getIds()[0];
329
- if (id) this.toggleMaximize(id);
426
+ if (id) {this.toggleMaximize(id);}
330
427
  }
331
428
 
332
429
  onChange(callback) {
@@ -344,10 +441,36 @@ export class TerminalManager {
344
441
  }
345
442
 
346
443
  _notifyChange(event) {
347
- for (const cb of this.changeCallbacks) cb(event);
444
+ for (const cb of this.changeCallbacks) {cb(event);}
348
445
  }
349
446
 
350
447
  _notifyActivity(id, cleared = false) {
351
- for (const cb of this.activityCallbacks) cb(id, cleared);
448
+ for (const cb of this.activityCallbacks) {cb(id, cleared);}
449
+ }
450
+
451
+ swapTerminals(id1, id2) {
452
+ const entry1 = this.terminals.get(id1);
453
+ const entry2 = this.terminals.get(id2);
454
+ if (!entry1 || !entry2) {return;}
455
+
456
+ const el1 = entry1.element;
457
+ const el2 = entry2.element;
458
+
459
+ // Swap DOM positions using a placeholder
460
+ const placeholder = document.createComment('swap');
461
+ el1.replaceWith(placeholder);
462
+ el2.replaceWith(el1);
463
+ placeholder.replaceWith(el2);
464
+
465
+ // Swap Map entries to keep iteration order consistent
466
+ const keys = [...this.terminals.keys()];
467
+ const idx1 = keys.indexOf(id1);
468
+ const idx2 = keys.indexOf(id2);
469
+ const entries = [...this.terminals.entries()];
470
+ [entries[idx1], entries[idx2]] = [entries[idx2], entries[idx1]];
471
+ this.terminals = new Map(entries);
472
+
473
+ this._notifyChange({ type: 'swap', id1, id2 });
474
+ requestAnimationFrame(() => this.fitAll());
352
475
  }
353
476
  }
@@ -1,21 +1,21 @@
1
- const STORAGE_KEY = 'myterminal-theme';
1
+ const STORAGE_KEY = 'mwt-theme';
2
2
 
3
3
  const DARK_TERMINAL_THEME = {
4
4
  background: '#0a0c12',
5
- foreground: '#d4d8e8',
5
+ foreground: '#e4e7f2',
6
6
  cursor: '#6c8cff',
7
7
  cursorAccent: '#0a0c12',
8
8
  selectionBackground: '#264f78',
9
9
  selectionForeground: '#ffffff',
10
- black: '#1c2035',
10
+ black: '#5c657c',
11
11
  red: '#e05560',
12
12
  green: '#7ec699',
13
13
  yellow: '#e6c07b',
14
14
  blue: '#6c8cff',
15
15
  magenta: '#c678dd',
16
16
  cyan: '#56b6c2',
17
- white: '#d4d8e8',
18
- brightBlack: '#555b72',
17
+ white: '#e4e7f2',
18
+ brightBlack: '#9aa4ba',
19
19
  brightRed: '#ff6b76',
20
20
  brightGreen: '#98e6b3',
21
21
  brightYellow: '#ffd68a',
@@ -91,6 +91,6 @@ export class ThemeManager {
91
91
 
92
92
  _apply(theme) {
93
93
  document.documentElement.dataset.theme = theme;
94
- for (const cb of this._listeners) cb(theme);
94
+ for (const cb of this._listeners) {cb(theme);}
95
95
  }
96
96
  }