@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/README.md +24 -6
- package/lib/pty-manager.js +40 -0
- package/lib/server.js +66 -80
- package/lib/session-manager.js +66 -0
- package/package.json +2 -2
- package/public/css/style.css +121 -16
- package/public/index.html +42 -32
- package/public/js/app.js +117 -102
- package/public/js/drag-manager.js +42 -0
- package/public/js/font-manager.js +3 -3
- package/public/js/i18n.js +176 -0
- package/public/js/layout-manager.js +5 -2
- package/public/js/shortcut-manager.js +102 -0
- package/public/js/terminal-manager.js +45 -34
- package/public/js/theme-manager.js +1 -1
- package/public/js/ws-client.js +23 -2
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="
|
|
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="
|
|
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>
|
|
22
|
+
<span data-i18n="columns">Columns</span>
|
|
23
23
|
</button>
|
|
24
|
-
<button class="layout-btn" data-layout="
|
|
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="
|
|
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
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
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="
|
|
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="
|
|
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
|
-
<
|
|
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-
|
|
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">×</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">×</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="
|
|
91
|
-
<div class="shortcut-row"><span class="shortcut-desc">Close Terminal</span><kbd class="shortcut-key" data-shortcut="
|
|
92
|
-
<div class="shortcut-row"><span class="shortcut-desc">Maximize / Restore Terminal</span><kbd class="shortcut-key" data-shortcut="
|
|
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="
|
|
97
|
-
<div class="shortcut-row"><span class="shortcut-desc">Previous Terminal</span><kbd class="shortcut-key" data-shortcut="
|
|
98
|
-
<div class="shortcut-row"><span class="shortcut-desc">Switch to Terminal 1–9</span><kbd class="shortcut-key" data-shortcut="
|
|
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(
|
|
76
|
-
body: '
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 = '
|
|
2
|
-
const STORAGE_KEY_FAMILY = '
|
|
1
|
+
const STORAGE_KEY_SIZE = 'mwt-font-size';
|
|
2
|
+
const STORAGE_KEY_FAMILY = 'mwt-font-family';
|
|
3
3
|
|
|
4
|
-
const DEFAULT_FONT_SIZE =
|
|
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 {
|