rms-devremote 3.0.0 → 3.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/dist/index.js +1 -1
- package/dist/server/frontend.js +169 -0
- package/dist/server/index.js +42 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const program = new Command();
|
|
|
4
4
|
program
|
|
5
5
|
.name('rms-devremote')
|
|
6
6
|
.description('Share your local terminal remotely with push notifications')
|
|
7
|
-
.version('2.0
|
|
7
|
+
.version('3.2.0');
|
|
8
8
|
// Helper: lazy-load a command module by path (relative to dist/)
|
|
9
9
|
async function runCommand(modulePath) {
|
|
10
10
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
package/dist/server/frontend.js
CHANGED
|
@@ -32,6 +32,8 @@ export function buildFrontendHTML() {
|
|
|
32
32
|
<span class="h-status" id="status-text">connecting</span>
|
|
33
33
|
</div>
|
|
34
34
|
<div class="h-right">
|
|
35
|
+
<button class="h-btn" id="btn-wins" title="Windows"><span id="win-label">1</span></button>
|
|
36
|
+
<button class="h-btn" id="btn-new-win" title="New window">+</button>
|
|
35
37
|
<button class="h-btn" id="btn-kb" title="Keyboard">⌨</button>
|
|
36
38
|
<button class="h-btn" id="btn-toggle" title="Toolbar">⚙</button>
|
|
37
39
|
</div>
|
|
@@ -46,6 +48,11 @@ export function buildFrontendHTML() {
|
|
|
46
48
|
<div class="info-row"><span class="info-label">Uptime</span><span class="info-value" id="info-uptime">--</span></div>
|
|
47
49
|
</div>
|
|
48
50
|
|
|
51
|
+
<!-- Window selector (dropdown from header) -->
|
|
52
|
+
<div id="win-dropdown" class="hidden">
|
|
53
|
+
<div id="win-list"></div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
49
56
|
<!-- Terminal -->
|
|
50
57
|
<div id="terminal-container" class="hidden"></div>
|
|
51
58
|
|
|
@@ -125,6 +132,7 @@ const CSS = `
|
|
|
125
132
|
--muted: #888888;
|
|
126
133
|
--border: #333333;
|
|
127
134
|
--header-h: 44px;
|
|
135
|
+
--tabbar-h: 0px;
|
|
128
136
|
--input-h: 48px;
|
|
129
137
|
--toolbar-h: 104px;
|
|
130
138
|
--safe-t: env(safe-area-inset-top, 0px);
|
|
@@ -221,6 +229,49 @@ body { touch-action: none; }
|
|
|
221
229
|
.info-value.err { color: var(--danger); }
|
|
222
230
|
.info-value.info-path { font-size: 12px; max-width: 60%; text-align: right; word-break: break-all; }
|
|
223
231
|
|
|
232
|
+
/* ── Window selector dropdown ───────────── */
|
|
233
|
+
#btn-wins {
|
|
234
|
+
font-size: 14px; font-weight: 800; min-width: 36px;
|
|
235
|
+
}
|
|
236
|
+
#btn-wins.active { border-color: var(--accent); color: var(--accent); background: rgba(0,255,170,0.08); }
|
|
237
|
+
#btn-new-win { font-size: 22px; font-weight: 400; }
|
|
238
|
+
#win-dropdown {
|
|
239
|
+
position: fixed;
|
|
240
|
+
top: calc(var(--header-h) + var(--safe-t));
|
|
241
|
+
right: 0; left: 0;
|
|
242
|
+
background: var(--surface);
|
|
243
|
+
border-bottom: 2px solid var(--border);
|
|
244
|
+
padding: 8px calc(14px + var(--safe-l)) 10px calc(14px + var(--safe-r));
|
|
245
|
+
z-index: 92;
|
|
246
|
+
animation: slideDown 0.15s ease-out;
|
|
247
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
248
|
+
}
|
|
249
|
+
#win-dropdown.hidden { display: none !important; }
|
|
250
|
+
.win-row {
|
|
251
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
252
|
+
padding: 10px 12px;
|
|
253
|
+
background: var(--surface2); border: 2px solid var(--border);
|
|
254
|
+
border-radius: 8px; cursor: pointer;
|
|
255
|
+
-webkit-tap-highlight-color: transparent;
|
|
256
|
+
transition: border-color 0.12s, background 0.12s;
|
|
257
|
+
}
|
|
258
|
+
.win-row:active { background: #222; }
|
|
259
|
+
.win-row.active { border-color: var(--accent); background: rgba(0,255,170,0.06); }
|
|
260
|
+
.win-name {
|
|
261
|
+
font-size: 14px; font-weight: 600; color: var(--text);
|
|
262
|
+
font-family: 'JetBrains Mono', monospace;
|
|
263
|
+
}
|
|
264
|
+
.win-row.active .win-name { color: var(--accent); }
|
|
265
|
+
.win-close {
|
|
266
|
+
width: 24px; height: 24px;
|
|
267
|
+
display: flex; align-items: center; justify-content: center;
|
|
268
|
+
font-size: 16px; font-weight: 700;
|
|
269
|
+
color: var(--muted); background: none; border: none;
|
|
270
|
+
border-radius: 4px; cursor: pointer;
|
|
271
|
+
-webkit-tap-highlight-color: transparent;
|
|
272
|
+
}
|
|
273
|
+
.win-close:active { background: rgba(255,51,85,0.3); color: var(--danger); }
|
|
274
|
+
|
|
224
275
|
/* ── Terminal ───────────────────────────── */
|
|
225
276
|
#terminal-container {
|
|
226
277
|
position: fixed;
|
|
@@ -472,6 +523,7 @@ const JS = `
|
|
|
472
523
|
inputBar.classList.remove('hidden');
|
|
473
524
|
toolbar.classList.remove('hidden');
|
|
474
525
|
connected = true;
|
|
526
|
+
startWindowsPoll();
|
|
475
527
|
setTimeout(doResize, 50);
|
|
476
528
|
showGestureHint();
|
|
477
529
|
}
|
|
@@ -480,11 +532,13 @@ const JS = `
|
|
|
480
532
|
container.classList.add('hidden');
|
|
481
533
|
inputBar.classList.add('hidden');
|
|
482
534
|
toolbar.classList.add('hidden');
|
|
535
|
+
toggleWinDropdown(false);
|
|
483
536
|
disconnect.classList.remove('hidden');
|
|
484
537
|
dcTitle.textContent = title;
|
|
485
538
|
dcReason.textContent = reason;
|
|
486
539
|
dcIcon.className = 'dc-icon' + (iconClass ? ' ' + iconClass : '');
|
|
487
540
|
connected = false;
|
|
541
|
+
stopWindowsPoll();
|
|
488
542
|
}
|
|
489
543
|
|
|
490
544
|
function connect() {
|
|
@@ -509,6 +563,7 @@ const JS = `
|
|
|
509
563
|
try {
|
|
510
564
|
var msg = JSON.parse(evt.data);
|
|
511
565
|
if (msg.type === 'output') term.write(msg.data);
|
|
566
|
+
else if (msg.type === 'windows-updated') fetchWindows();
|
|
512
567
|
else if (msg.type === 'exit') {
|
|
513
568
|
showDisconnect('Session Ended', 'The tmux session has ended.', 'error');
|
|
514
569
|
setStatus('session ended', false);
|
|
@@ -829,6 +884,120 @@ const JS = `
|
|
|
829
884
|
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
830
885
|
}
|
|
831
886
|
|
|
887
|
+
// ── Window management (header buttons + dropdown) ───────
|
|
888
|
+
var btnWins = document.getElementById('btn-wins');
|
|
889
|
+
var btnNewWin = document.getElementById('btn-new-win');
|
|
890
|
+
var winLabel = document.getElementById('win-label');
|
|
891
|
+
var winDropdown = document.getElementById('win-dropdown');
|
|
892
|
+
var winList = document.getElementById('win-list');
|
|
893
|
+
var windowsCache = [];
|
|
894
|
+
var windowsPollTimer = null;
|
|
895
|
+
var winDropdownOpen = false;
|
|
896
|
+
|
|
897
|
+
function fetchWindows() {
|
|
898
|
+
fetch('/windows', { credentials: 'same-origin' })
|
|
899
|
+
.then(function(r) { return r.json(); })
|
|
900
|
+
.then(function(wins) {
|
|
901
|
+
windowsCache = wins;
|
|
902
|
+
updateWinLabel(wins);
|
|
903
|
+
if (winDropdownOpen) renderWinDropdown(wins);
|
|
904
|
+
})
|
|
905
|
+
.catch(function() {});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function updateWinLabel(wins) {
|
|
909
|
+
if (!wins || wins.length === 0) { winLabel.textContent = '0'; return; }
|
|
910
|
+
var active = wins.find(function(w) { return w.active; });
|
|
911
|
+
winLabel.textContent = active ? active.index : wins.length;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function renderWinDropdown(wins) {
|
|
915
|
+
while (winList.firstChild) winList.removeChild(winList.firstChild);
|
|
916
|
+
if (!wins) return;
|
|
917
|
+
|
|
918
|
+
wins.forEach(function(w) {
|
|
919
|
+
var row = document.createElement('div');
|
|
920
|
+
row.className = 'win-row' + (w.active ? ' active' : '');
|
|
921
|
+
|
|
922
|
+
var name = document.createElement('span');
|
|
923
|
+
name.className = 'win-name';
|
|
924
|
+
name.textContent = w.index + ' : ' + (w.name || 'bash');
|
|
925
|
+
row.appendChild(name);
|
|
926
|
+
|
|
927
|
+
// Close button (only if more than 1 window)
|
|
928
|
+
if (wins.length > 1) {
|
|
929
|
+
var closeBtn = document.createElement('button');
|
|
930
|
+
closeBtn.className = 'win-close';
|
|
931
|
+
closeBtn.textContent = '\\u00d7';
|
|
932
|
+
closeBtn.setAttribute('tabindex', '-1');
|
|
933
|
+
closeBtn.addEventListener('touchstart', function(e) {
|
|
934
|
+
e.stopPropagation(); e.preventDefault();
|
|
935
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
936
|
+
sendWsMsg({ type: 'window-close', index: w.index });
|
|
937
|
+
}, { passive: false });
|
|
938
|
+
closeBtn.addEventListener('click', function(e) {
|
|
939
|
+
e.stopPropagation();
|
|
940
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
941
|
+
sendWsMsg({ type: 'window-close', index: w.index });
|
|
942
|
+
});
|
|
943
|
+
row.appendChild(closeBtn);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Switch on tap
|
|
947
|
+
row.addEventListener('touchstart', function(e) {
|
|
948
|
+
e.preventDefault();
|
|
949
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
950
|
+
sendWsMsg({ type: 'window-switch', index: w.index });
|
|
951
|
+
toggleWinDropdown(false);
|
|
952
|
+
}, { passive: false });
|
|
953
|
+
row.addEventListener('click', function() {
|
|
954
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
955
|
+
sendWsMsg({ type: 'window-switch', index: w.index });
|
|
956
|
+
toggleWinDropdown(false);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
winList.appendChild(row);
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function toggleWinDropdown(force) {
|
|
964
|
+
winDropdownOpen = force !== undefined ? force : !winDropdownOpen;
|
|
965
|
+
winDropdown.classList.toggle('hidden', !winDropdownOpen);
|
|
966
|
+
btnWins.classList.toggle('active', winDropdownOpen);
|
|
967
|
+
// Close info panel if open
|
|
968
|
+
if (winDropdownOpen && infoOpen) { infoOpen = false; infoPanel.classList.add('hidden'); }
|
|
969
|
+
if (winDropdownOpen) renderWinDropdown(windowsCache);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
btnWins.addEventListener('click', function() { toggleWinDropdown(); });
|
|
973
|
+
|
|
974
|
+
// New window button
|
|
975
|
+
btnNewWin.addEventListener('touchstart', function(e) {
|
|
976
|
+
e.preventDefault();
|
|
977
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
978
|
+
sendWsMsg({ type: 'window-create' });
|
|
979
|
+
}, { passive: false });
|
|
980
|
+
btnNewWin.addEventListener('click', function() {
|
|
981
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
982
|
+
sendWsMsg({ type: 'window-create' });
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
function sendWsMsg(obj) {
|
|
986
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
987
|
+
ws.send(JSON.stringify(obj));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function startWindowsPoll() {
|
|
992
|
+
stopWindowsPoll();
|
|
993
|
+
fetchWindows();
|
|
994
|
+
windowsPollTimer = setInterval(fetchWindows, 2000);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function stopWindowsPoll() {
|
|
998
|
+
if (windowsPollTimer) { clearInterval(windowsPollTimer); windowsPollTimer = null; }
|
|
999
|
+
}
|
|
1000
|
+
|
|
832
1001
|
// ── Info panel ────────────────────────────────────────────
|
|
833
1002
|
var infoPanel = document.getElementById('info-panel');
|
|
834
1003
|
var infoOpen = false;
|
package/dist/server/index.js
CHANGED
|
@@ -107,6 +107,23 @@ app.get('/', (_req, res) => {
|
|
|
107
107
|
app.get('/health', (_req, res) => {
|
|
108
108
|
res.json({ status: 'ok', uptime: process.uptime() });
|
|
109
109
|
});
|
|
110
|
+
// ── tmux windows list ──────────────────────────────────────────────────
|
|
111
|
+
app.get('/windows', (_req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const raw = execFileSync('tmux', [
|
|
114
|
+
'list-windows', '-t', 'devremote',
|
|
115
|
+
'-F', '#{window_index}\t#{window_name}\t#{window_active}',
|
|
116
|
+
], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
117
|
+
const windows = raw.split('\n').map((line) => {
|
|
118
|
+
const [index, name, active] = line.split('\t');
|
|
119
|
+
return { index: Number(index), name, active: active === '1' };
|
|
120
|
+
});
|
|
121
|
+
res.json(windows);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
res.json([]);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
110
127
|
app.get('/status', (_req, res) => {
|
|
111
128
|
// Get tmux current pane path
|
|
112
129
|
let tmuxPath = '';
|
|
@@ -201,6 +218,31 @@ wss.on('connection', (ws) => {
|
|
|
201
218
|
activeSession.resize(parsed.cols, parsed.rows);
|
|
202
219
|
return;
|
|
203
220
|
}
|
|
221
|
+
// ── Window management (tmux) ──────────────────────────
|
|
222
|
+
if (parsed.type === 'window-create') {
|
|
223
|
+
try {
|
|
224
|
+
execFileSync('tmux', ['new-window', '-t', 'devremote'], { timeout: 3000 });
|
|
225
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
226
|
+
}
|
|
227
|
+
catch { /* ignore */ }
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (parsed.type === 'window-switch' && parsed.index !== undefined) {
|
|
231
|
+
try {
|
|
232
|
+
execFileSync('tmux', ['select-window', '-t', `devremote:${parsed.index}`], { timeout: 3000 });
|
|
233
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
234
|
+
}
|
|
235
|
+
catch { /* ignore */ }
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (parsed.type === 'window-close' && parsed.index !== undefined) {
|
|
239
|
+
try {
|
|
240
|
+
execFileSync('tmux', ['kill-window', '-t', `devremote:${parsed.index}`], { timeout: 3000 });
|
|
241
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
204
246
|
}
|
|
205
247
|
catch {
|
|
206
248
|
// Ignore malformed messages
|