@jacksontian/mwt 1.1.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.
package/public/index.html CHANGED
@@ -11,67 +11,77 @@
11
11
  <body>
12
12
  <div id="toolbar">
13
13
  <div class="toolbar-left">
14
- <button id="btn-new-terminal" title="New Terminal">
15
- <span class="btn-icon">+</span> New Terminal
14
+ <button id="btn-new-terminal" data-i18n-title="newTerminalTooltip">
15
+ <span class="btn-icon">+</span> <span data-i18n="newTerminal">New Terminal</span>
16
16
  </button>
17
17
  </div>
18
18
  <div class="toolbar-center">
19
19
  <div class="layout-switcher">
20
- <button class="layout-btn active" data-layout="side-by-side" title="Side by Side">
20
+ <button class="layout-btn active" data-layout="columns" data-i18n-title="columns">
21
21
  <svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="2" width="6" height="12" rx="1" fill="currentColor" opacity="0.8"/><rect x="9" y="2" width="6" height="12" rx="1" fill="currentColor" opacity="0.5"/></svg>
22
- <span>Side by Side</span>
22
+ <span data-i18n="columns">Columns</span>
23
23
  </button>
24
- <button class="layout-btn" data-layout="grid" title="Grid">
24
+ <button class="layout-btn" data-layout="rows" data-i18n-title="rows">
25
+ <svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="2" width="14" height="5" rx="1" fill="currentColor" opacity="0.8"/><rect x="1" y="9" width="14" height="5" rx="1" fill="currentColor" opacity="0.5"/></svg>
26
+ <span data-i18n="rows">Rows</span>
27
+ </button>
28
+ <button class="layout-btn" data-layout="grid" data-i18n-title="grid">
25
29
  <svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="6" rx="1" fill="currentColor" opacity="0.8"/><rect x="9" y="1" width="6" height="6" rx="1" fill="currentColor" opacity="0.5"/><rect x="1" y="9" width="6" height="6" rx="1" fill="currentColor" opacity="0.5"/><rect x="9" y="9" width="6" height="6" rx="1" fill="currentColor" opacity="0.3"/></svg>
26
- <span>Grid</span>
30
+ <span data-i18n="grid">Grid</span>
27
31
  </button>
28
- <button class="layout-btn" data-layout="tabs" title="Tabs">
32
+ <button class="layout-btn" data-layout="tabs" data-i18n-title="tabs">
29
33
  <svg width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="4" width="14" height="11" rx="1" fill="currentColor" opacity="0.3"/><rect x="1" y="2" width="6" height="4" rx="1" fill="currentColor" opacity="0.8"/><rect x="8" y="2" width="4" height="4" rx="1" fill="currentColor" opacity="0.4"/></svg>
30
- <span>Tabs</span>
34
+ <span data-i18n="tabs">Tabs</span>
31
35
  </button>
32
36
  </div>
33
37
  </div>
34
38
  <div class="toolbar-right">
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
+ <div id="lang-dropdown">
40
+ <button id="btn-lang" title="Language">
41
+ <span id="btn-lang-label"></span>
42
+ <svg class="lang-chevron" viewBox="0 0 8 8" fill="none"><path d="M1 2.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
43
+ </button>
44
+ <div id="lang-menu"></div>
45
+ </div>
46
+ <button id="btn-shortcuts" data-i18n-title="shortcutsTooltip">
39
47
  <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
48
  </button>
41
- <button id="btn-fullscreen" title="Toggle fullscreen">
49
+ <button id="btn-fullscreen" data-i18n-title="fullscreenTooltip">
42
50
  <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>
43
51
  <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>
44
52
  </button>
45
- <button id="btn-theme-toggle" title="Toggle theme">
53
+ <button id="btn-theme-toggle" data-i18n-title="toggleTheme">
46
54
  <svg class="icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 1.5v1.5M8 13v1.5M1.5 8H3M13 8h1.5M3.17 3.17l1.06 1.06M11.77 11.77l1.06 1.06M3.17 12.83l1.06-1.06M11.77 4.23l1.06-1.06" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
47
55
  <svg class="icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M13.36 10.06A5.5 5.5 0 015.94 2.64a6 6 0 107.42 7.42z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
48
56
  </button>
49
- <span id="terminal-count">0 terminals</span>
57
+ <button id="btn-settings" data-i18n-title="settings">
58
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><line x1="1" y1="4" x2="15" y2="4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><line x1="1" y1="8" x2="15" y2="8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><line x1="1" y1="12" x2="15" y2="12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="5" cy="4" r="1.8" fill="currentColor" stroke="none"/><circle cx="10" cy="8" r="1.8" fill="currentColor" stroke="none"/><circle cx="6" cy="12" r="1.8" fill="currentColor" stroke="none"/></svg>
59
+ </button>
50
60
  </div>
51
61
  </div>
52
62
 
53
63
  <div id="tab-bar" class="hidden"></div>
54
64
 
55
65
  <div id="main-content">
56
- <div id="terminal-container" class="layout-side-by-side"></div>
66
+ <div id="terminal-container" class="layout-columns"></div>
57
67
  </div>
58
68
 
59
69
  <div id="settings-modal" class="modal-overlay hidden">
60
70
  <div class="modal">
61
71
  <div class="modal-header">
62
- <span class="modal-title">Settings</span>
72
+ <span class="modal-title" data-i18n="settingsTitle">Settings</span>
63
73
  <button id="btn-settings-close" class="modal-close">&times;</button>
64
74
  </div>
65
75
  <div class="modal-body">
66
76
  <div class="settings-group">
67
- <div class="settings-group-title">Font</div>
77
+ <div class="settings-group-title" data-i18n="settingsFont">Font</div>
68
78
  <div class="settings-row">
69
- <label class="settings-label" for="input-font-size">Size <span id="font-size-value"></span></label>
79
+ <label class="settings-label" for="input-font-size"><span data-i18n="settingsFontSize">Size</span> <span id="font-size-value"></span></label>
70
80
  <input type="range" id="input-font-size" min="8" max="32" step="1">
71
81
  </div>
72
82
  <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">
83
+ <label class="settings-label" for="input-font-family" data-i18n="settingsFontFamily">Family</label>
84
+ <input type="text" id="input-font-family" class="settings-text-input" data-i18n-placeholder="settingsFontFamilyPlaceholder" placeholder="e.g. Menlo, monospace">
75
85
  </div>
76
86
  </div>
77
87
  </div>
@@ -81,25 +91,25 @@
81
91
  <div id="shortcuts-modal" class="modal-overlay hidden">
82
92
  <div class="modal">
83
93
  <div class="modal-header">
84
- <span class="modal-title">Keyboard Shortcuts</span>
94
+ <span class="modal-title" data-i18n="shortcutsTitle">Keyboard Shortcuts</span>
85
95
  <button id="btn-shortcuts-close" class="modal-close">&times;</button>
86
96
  </div>
87
97
  <div class="modal-body">
88
98
  <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>
99
+ <div class="shortcut-group-title" data-i18n="shortcutsGroupTerminals">Terminals</div>
100
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsNewTerminal">New Terminal</span><kbd class="shortcut-key" data-shortcut="ctrl+alt+N"></kbd></div>
101
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsCloseTerminal">Close Terminal</span><kbd class="shortcut-key" data-shortcut="ctrl+alt+W"></kbd></div>
102
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsMaximizeTerminal">Maximize / Restore Terminal</span><kbd class="shortcut-key" data-shortcut="ctrl+alt+M"></kbd></div>
93
103
  </div>
94
104
  <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>
105
+ <div class="shortcut-group-title" data-i18n="shortcutsGroupNavigation">Navigation</div>
106
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsNextTerminal">Next Terminal</span><kbd class="shortcut-key" data-shortcut="ctrl+alt+→"></kbd></div>
107
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsPrevTerminal">Previous Terminal</span><kbd class="shortcut-key" data-shortcut="ctrl+alt+←"></kbd></div>
108
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsSwitchTerminal">Switch to Terminal 1–9</span><kbd class="shortcut-key" data-shortcut="alt+1~9"></kbd></div>
99
109
  </div>
100
110
  <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>
111
+ <div class="shortcut-group-title" data-i18n="shortcutsGroupWindow">Window</div>
112
+ <div class="shortcut-row"><span class="shortcut-desc" data-i18n="shortcutsToggleFullscreen">Toggle Fullscreen</span><kbd class="shortcut-key" data-shortcut="F11"></kbd></div>
103
113
  </div>
104
114
  </div>
105
115
  </div>
package/public/js/app.js CHANGED
@@ -3,19 +3,47 @@ import { TerminalManager } from './terminal-manager.js';
3
3
  import { LayoutManager } from './layout-manager.js';
4
4
  import { ThemeManager } from './theme-manager.js';
5
5
  import { FontManager } from './font-manager.js';
6
+ import { ShortcutManager } from './shortcut-manager.js';
7
+ import { i18n, langLabels } from './i18n.js';
6
8
 
7
9
  const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
8
10
  const cmdKey = isMac ? 'Cmd' : 'Ctrl';
9
11
 
12
+ // Initialize i18n
13
+ i18n.applyToDOM();
14
+
15
+ function _applyDynamicTitles() {
16
+ document.getElementById('btn-new-terminal').title = i18n.t('newTerminalTooltip', cmdKey);
17
+ document.getElementById('btn-fullscreen').title = i18n.t('fullscreenTooltip');
18
+ document.getElementById('btn-shortcuts').title = i18n.t('shortcutsTooltip');
19
+ }
20
+
10
21
  const wsClient = new WSClient();
11
22
  const container = document.getElementById('terminal-container');
12
23
  const tabBar = document.getElementById('tab-bar');
13
- const terminalCount = document.getElementById('terminal-count');
14
24
 
15
25
  const themeManager = new ThemeManager();
16
26
  const fontManager = new FontManager();
17
27
  const layoutManager = new LayoutManager(container, tabBar);
18
- const terminalManager = new TerminalManager(wsClient, container, themeManager, cmdKey, fontManager);
28
+ const terminalManager = new TerminalManager(wsClient, container, themeManager, cmdKey, fontManager, i18n);
29
+
30
+ // Restore layout immediately (like theme/font) so it's consistent on page load
31
+ const LAYOUT_KEY = 'mwt-layout';
32
+ const ACTIVE_TAB_KEY = 'mwt-active-tab';
33
+ {
34
+ const savedLayout = localStorage.getItem(LAYOUT_KEY);
35
+ if (savedLayout) {
36
+ document.querySelectorAll('.layout-btn').forEach(b => {
37
+ b.classList.toggle('active', b.dataset.layout === savedLayout);
38
+ });
39
+ layoutManager.currentLayout = savedLayout;
40
+ container.classList.remove('layout-columns', 'layout-rows', 'layout-grid', 'layout-tabs');
41
+ container.classList.add(`layout-${savedLayout}`);
42
+ if (savedLayout === 'tabs') {
43
+ tabBar.classList.remove('hidden');
44
+ }
45
+ }
46
+ }
19
47
 
20
48
  layoutManager.onTabActivate = (id) => {
21
49
  terminalManager.focusTerminal(id);
@@ -28,6 +56,42 @@ layoutManager.onLayoutApplied = () => {
28
56
  }
29
57
  };
30
58
 
59
+ // Language dropdown
60
+ const langDropdown = document.getElementById('lang-dropdown');
61
+ const btnLang = document.getElementById('btn-lang');
62
+ const btnLangLabel = document.getElementById('btn-lang-label');
63
+ const langMenu = document.getElementById('lang-menu');
64
+
65
+ function buildLangMenu() {
66
+ langMenu.innerHTML = '';
67
+ for (const [code, label] of Object.entries(langLabels)) {
68
+ const btn = document.createElement('button');
69
+ btn.className = 'lang-option' + (code === i18n.lang ? ' active' : '');
70
+ btn.innerHTML = `<span>${label}</span><svg class="lang-check" width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M1.5 5l2.5 2.5 4.5-4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
71
+ btn.addEventListener('click', () => {
72
+ i18n.setLang(code);
73
+ btnLangLabel.textContent = label;
74
+ buildLangMenu();
75
+ langDropdown.classList.remove('open');
76
+ _applyDynamicTitles();
77
+ terminalManager.updateI18n(i18n, cmdKey);
78
+ });
79
+ langMenu.appendChild(btn);
80
+ }
81
+ }
82
+
83
+ btnLangLabel.textContent = langLabels[i18n.lang];
84
+ buildLangMenu();
85
+
86
+ btnLang.addEventListener('click', (e) => {
87
+ e.stopPropagation();
88
+ langDropdown.classList.toggle('open');
89
+ });
90
+
91
+ document.addEventListener('click', () => {
92
+ langDropdown.classList.remove('open');
93
+ });
94
+
31
95
  // Theme toggle
32
96
  document.getElementById('btn-theme-toggle').addEventListener('click', () => {
33
97
  themeManager.toggle();
@@ -72,8 +136,8 @@ terminalManager.onActivity((id, cleared) => {
72
136
  if (now - lastNotify > NOTIFICATION_DEBOUNCE_MS) {
73
137
  notificationCooldown.set(id, now);
74
138
  const title = terminalManager.getTitle(id);
75
- const n = new Notification(`${title} has new output`, {
76
- body: 'Click to switch to this terminal',
139
+ const n = new Notification(i18n.t('terminalHasNewOutput', title), {
140
+ body: i18n.t('notificationBody'),
77
141
  tag: `mwt-${id}`,
78
142
  });
79
143
  n.onclick = () => {
@@ -109,11 +173,6 @@ document.addEventListener('visibilitychange', () => {
109
173
  // Request notification permission on first user interaction
110
174
  document.addEventListener('click', requestNotificationPermission, { once: true });
111
175
 
112
- function updateCount() {
113
- const count = terminalManager.getCount();
114
- terminalCount.textContent = `${count} terminal${count !== 1 ? 's' : ''}`;
115
- }
116
-
117
176
  terminalManager.onChange((event) => {
118
177
  if (event.type === 'add') {
119
178
  layoutManager.onTerminalAdded(event.id);
@@ -126,7 +185,6 @@ terminalManager.onChange((event) => {
126
185
  } else if (event.type === 'restore') {
127
186
  layoutManager.onTerminalRestored();
128
187
  }
129
- updateCount();
130
188
  });
131
189
 
132
190
  // New Terminal button
@@ -156,7 +214,9 @@ document.getElementById('btn-settings-close').addEventListener('click', () => {
156
214
  });
157
215
 
158
216
  settingsModal.addEventListener('click', (e) => {
159
- if (e.target === settingsModal) settingsModal.classList.add('hidden');
217
+ if (e.target === settingsModal) {
218
+ settingsModal.classList.add('hidden');
219
+ }
160
220
  });
161
221
 
162
222
  inputFontSize.addEventListener('input', () => {
@@ -166,13 +226,11 @@ inputFontSize.addEventListener('input', () => {
166
226
 
167
227
  inputFontFamily.addEventListener('change', () => {
168
228
  const val = inputFontFamily.value.trim();
169
- if (val) fontManager.setFontFamily(val);
229
+ if (val) {fontManager.setFontFamily(val);}
170
230
  });
171
231
 
172
232
  // 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`;
233
+ _applyDynamicTitles();
176
234
 
177
235
  // Shortcuts modal
178
236
  const shortcutsModal = document.getElementById('shortcuts-modal');
@@ -193,20 +251,33 @@ shortcutsModal.addEventListener('click', (e) => {
193
251
 
194
252
  document.addEventListener('keydown', (e) => {
195
253
  if (e.key === 'Escape') {
196
- if (!shortcutsModal.classList.contains('hidden')) shortcutsModal.classList.add('hidden');
197
- if (!settingsModal.classList.contains('hidden')) settingsModal.classList.add('hidden');
254
+ if (!shortcutsModal.classList.contains('hidden')) {shortcutsModal.classList.add('hidden');}
255
+ if (!settingsModal.classList.contains('hidden')) {settingsModal.classList.add('hidden');}
198
256
  }
199
257
  }, true);
200
258
 
259
+
201
260
  // 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
- });
261
+ function renderShortcutKeys() {
262
+ document.querySelectorAll('.shortcut-key[data-shortcut]').forEach(el => {
263
+ const raw = el.dataset.shortcut;
264
+ if (isMac) {
265
+ el.textContent = raw
266
+ .replace(/ctrl/gi, '⌃')
267
+ .replace(/alt/gi, '⌥')
268
+ .replace(/shift/gi, '⇧')
269
+ .replace(/\+/g, '');
270
+ } else {
271
+ el.textContent = raw
272
+ .replace(/ctrl/gi, 'Ctrl')
273
+ .replace(/alt/gi, 'Alt')
274
+ .replace(/shift/gi, 'Shift');
275
+ }
276
+ });
277
+ }
278
+ renderShortcutKeys();
205
279
 
206
280
  // Layout persistence
207
- const LAYOUT_KEY = 'myterminal-layout';
208
- const ACTIVE_TAB_KEY = 'myterminal-active-tab';
209
-
210
281
  function saveLayoutState() {
211
282
  localStorage.setItem(LAYOUT_KEY, layoutManager.currentLayout);
212
283
  if (layoutManager.activeTabId) {
@@ -243,84 +314,7 @@ document.addEventListener('fullscreenchange', () => {
243
314
  });
244
315
 
245
316
  // Keyboard shortcuts
246
- document.addEventListener('keydown', (e) => {
247
- const ctrl = e.ctrlKey || e.metaKey;
248
- const shift = e.shiftKey;
249
-
250
- // Ctrl+`: new terminal
251
- if (ctrl && !shift && e.key === '`') {
252
- e.preventDefault();
253
- terminalManager.createTerminal();
254
- return;
255
- }
256
-
257
- // Ctrl+Shift+`: close active terminal
258
- if (ctrl && shift && e.key === '`') {
259
- e.preventDefault();
260
- terminalManager.closeActiveTerminal();
261
- return;
262
- }
263
-
264
- // Ctrl+PageDown: next terminal
265
- if (ctrl && e.key === 'PageDown') {
266
- e.preventDefault();
267
- const nextId = terminalManager.focusNext();
268
- if (nextId) {
269
- terminalManager.clearActivity(nextId);
270
- if (layoutManager.currentLayout === 'tabs') {
271
- layoutManager.activateTab(nextId);
272
- }
273
- }
274
- return;
275
- }
276
-
277
- // Ctrl+PageUp: previous terminal
278
- if (ctrl && e.key === 'PageUp') {
279
- e.preventDefault();
280
- const prevId = terminalManager.focusPrev();
281
- if (prevId) {
282
- terminalManager.clearActivity(prevId);
283
- if (layoutManager.currentLayout === 'tabs') {
284
- layoutManager.activateTab(prevId);
285
- }
286
- }
287
- return;
288
- }
289
-
290
- // Ctrl+Shift+1~9: switch to terminal N
291
- if (ctrl && shift && e.key >= '1' && e.key <= '9') {
292
- e.preventDefault();
293
- const ids = terminalManager.getIds();
294
- const idx = parseInt(e.key, 10) - 1;
295
- if (idx < ids.length) {
296
- const targetId = ids[idx];
297
- terminalManager.focusTerminal(targetId);
298
- terminalManager.clearActivity(targetId);
299
- if (layoutManager.currentLayout === 'tabs') {
300
- layoutManager.activateTab(targetId);
301
- }
302
- }
303
- return;
304
- }
305
-
306
- // Ctrl+Shift+M: maximize/restore active terminal
307
- if (ctrl && shift && e.key === 'M') {
308
- e.preventDefault();
309
- terminalManager.toggleMaximizeActive();
310
- return;
311
- }
312
-
313
- // F11: toggle fullscreen
314
- if (e.key === 'F11') {
315
- e.preventDefault();
316
- if (!document.fullscreenElement) {
317
- document.documentElement.requestFullscreen();
318
- } else {
319
- document.exitFullscreen();
320
- }
321
- return;
322
- }
323
- });
317
+ new ShortcutManager({ terminalManager, layoutManager, cmdKey });
324
318
 
325
319
  // Prevent browser back navigation to avoid losing terminal sessions
326
320
  history.pushState(null, '', location.href);
@@ -362,10 +356,31 @@ wsClient.onSessionRestore((terminalIds) => {
362
356
  layoutManager.activateTab(savedActiveTab);
363
357
  }
364
358
 
365
- updateCount();
366
359
  });
367
360
 
368
361
  // Unmute terminals after buffer replay completes
369
362
  wsClient.onRestoreComplete(() => {
370
363
  terminalManager.unmuteAll();
371
364
  });
365
+
366
+ // Show overlay when this tab is rejected (another tab already connected)
367
+ wsClient.onAlreadyConnected(() => {
368
+ const overlay = document.createElement('div');
369
+ overlay.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg-primary,#1e1e1e);z-index:9999;';
370
+ const p = document.createElement('p');
371
+ p.style.cssText = 'color:var(--text-primary,#ccc);font-family:monospace;font-size:14px;';
372
+ p.textContent = i18n.t('alreadyConnected');
373
+ overlay.appendChild(p);
374
+ document.body.appendChild(overlay);
375
+ });
376
+
377
+ // Show overlay when reconnection gives up after too many failures
378
+ wsClient.onGiveUp(() => {
379
+ const overlay = document.createElement('div');
380
+ overlay.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg-primary,#1e1e1e);z-index:9999;';
381
+ const p = document.createElement('p');
382
+ p.style.cssText = 'color:var(--text-primary,#ccc);font-family:monospace;font-size:14px;';
383
+ p.textContent = i18n.t('serverDisconnected');
384
+ overlay.appendChild(p);
385
+ document.body.appendChild(overlay);
386
+ });
@@ -0,0 +1,42 @@
1
+ export class DragManager {
2
+ /**
3
+ * @param {(id1: string, id2: string) => void} swapTerminals
4
+ */
5
+ constructor(swapTerminals) {
6
+ this._swap = swapTerminals;
7
+ }
8
+
9
+ /** Attach drag-to-swap event listeners to a terminal pane and its header. */
10
+ attach(pane, header, id) {
11
+ header.draggable = true;
12
+ header.addEventListener('dragstart', (e) => {
13
+ e.dataTransfer.effectAllowed = 'move';
14
+ e.dataTransfer.setData('text/plain', id);
15
+ header.classList.add('dragging-source');
16
+ });
17
+ header.addEventListener('dragend', () => {
18
+ header.classList.remove('dragging-source');
19
+ pane.closest('#terminal-container')
20
+ ?.querySelectorAll('.terminal-pane.drag-over')
21
+ .forEach(el => el.classList.remove('drag-over'));
22
+ });
23
+ pane.addEventListener('dragover', (e) => {
24
+ e.preventDefault();
25
+ e.dataTransfer.dropEffect = 'move';
26
+ pane.classList.add('drag-over');
27
+ });
28
+ pane.addEventListener('dragleave', (e) => {
29
+ if (!pane.contains(e.relatedTarget)) {
30
+ pane.classList.remove('drag-over');
31
+ }
32
+ });
33
+ pane.addEventListener('drop', (e) => {
34
+ e.preventDefault();
35
+ pane.classList.remove('drag-over');
36
+ const sourceId = e.dataTransfer.getData('text/plain');
37
+ if (sourceId && sourceId !== id) {
38
+ this._swap(sourceId, id);
39
+ }
40
+ });
41
+ }
42
+ }
@@ -1,7 +1,7 @@
1
- const STORAGE_KEY_SIZE = 'myterminal-font-size';
2
- const STORAGE_KEY_FAMILY = 'myterminal-font-family';
1
+ const STORAGE_KEY_SIZE = 'mwt-font-size';
2
+ const STORAGE_KEY_FAMILY = 'mwt-font-family';
3
3
 
4
- const DEFAULT_FONT_SIZE = 14;
4
+ const DEFAULT_FONT_SIZE = 12;
5
5
  const DEFAULT_FONT_FAMILY = "'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace";
6
6
 
7
7
  export class FontManager {