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 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.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
@@ -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">&#x2328;</button>
36
38
  <button class="h-btn" id="btn-toggle" title="Toolbar">&#x2699;</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;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rms-devremote",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Control your terminal remotely from your phone — mobile PWA with push notifications and zero open ports",
5
5
  "type": "module",
6
6
  "bin": {