termites 1.0.28 → 1.0.29

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +261 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termites",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "description": "Web terminal with server-client architecture for remote shell access",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -635,7 +635,7 @@ class TermitesServer {
635
635
  /* Mobile toolbar */
636
636
  .mobile-toolbar {
637
637
  display: none; flex-shrink: 0; padding: 6px 8px; gap: 6px;
638
- border-top: 1px solid; overflow-x: auto; -webkit-overflow-scrolling: touch;
638
+ border-top: 1px solid; flex-wrap: wrap; justify-content: center;
639
639
  }
640
640
  .mobile-toolbar.show { display: flex; }
641
641
  .mobile-toolbar button {
@@ -645,7 +645,6 @@ class TermitesServer {
645
645
  }
646
646
  .mobile-toolbar button:active { opacity: 0.6; transform: scale(0.95); }
647
647
  .mobile-toolbar button.active { background: var(--btn-active, rgba(255,255,255,0.15)); }
648
- .mobile-toolbar .sep { width: 1px; background: currentColor; opacity: 0.2; margin: 0 4px; }
649
648
  .sel-marker {
650
649
  position: absolute; z-index: 50; padding: 4px 8px; border-radius: 4px;
651
650
  font-size: 11px; font-weight: bold; cursor: grab; touch-action: none;
@@ -653,6 +652,38 @@ class TermitesServer {
653
652
  }
654
653
  #sel-start-marker { background: #22c55e; color: #fff; }
655
654
  #sel-end-marker { background: #ef4444; color: #fff; }
655
+ .float-copy-btn {
656
+ position: absolute; z-index: 60; padding: 8px 16px; border-radius: 6px;
657
+ background: #3b82f6; color: #fff; border: none; font-size: 14px; font-weight: bold;
658
+ cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.3);
659
+ }
660
+ .float-copy-btn:active { background: #2563eb; transform: scale(0.95); }
661
+ /* History overlay */
662
+ .history-overlay {
663
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 40;
664
+ display: none; flex-direction: column; overflow: hidden;
665
+ }
666
+ .history-overlay.show { display: flex; }
667
+ .history-header {
668
+ padding: 8px 12px; display: flex; align-items: center; justify-content: space-between;
669
+ border-bottom: 1px solid; font-size: 12px; flex-shrink: 0;
670
+ }
671
+ .history-header span { opacity: 0.7; }
672
+ .history-close {
673
+ background: none; border: 1px solid; border-radius: 4px; padding: 4px 12px;
674
+ color: inherit; cursor: pointer; font-size: 12px;
675
+ }
676
+ .history-content {
677
+ flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch;
678
+ padding: 4px; font-family: inherit; white-space: pre-wrap; word-break: break-all;
679
+ user-select: text; -webkit-user-select: text;
680
+ }
681
+ .history-indicator {
682
+ position: absolute; top: 50px; left: 50%; transform: translateX(-50%);
683
+ padding: 6px 16px; border-radius: 20px; font-size: 12px; z-index: 30;
684
+ cursor: pointer; opacity: 0; transition: opacity 0.3s; pointer-events: none;
685
+ }
686
+ .history-indicator.show { opacity: 1; pointer-events: auto; }
656
687
  </style>
657
688
  </head>
658
689
  <body>
@@ -722,28 +753,37 @@ class TermitesServer {
722
753
  <button data-key="Tab">Tab</button>
723
754
  <button data-mod="ctrl" class="mod-btn">Ctrl</button>
724
755
  <button data-mod="alt" class="mod-btn">Alt</button>
725
- <div class="sep"></div>
726
756
  <button data-key="ArrowLeft">←</button>
727
757
  <button data-key="ArrowRight">→</button>
728
758
  <button data-key="ArrowUp">↑</button>
729
759
  <button data-key="ArrowDown">↓</button>
730
- <div class="sep"></div>
731
760
  <button data-seq="/">/</button>
732
761
  <button data-seq="[">[</button>
733
762
  <button data-seq="]">]</button>
734
- <button id="sel-btn" class="mod-btn">Sel</button>
735
- <button id="copy-btn">Copy</button>
736
- <button id="paste-btn">Paste</button>
763
+ <button id="hist-btn">Hist</button>
737
764
  </div>
738
765
  <div id="sel-start-marker" class="sel-marker" style="display:none">▼Start</div>
739
766
  <div id="sel-end-marker" class="sel-marker" style="display:none">▲End</div>
767
+ <button id="float-copy-btn" class="float-copy-btn" style="display:none">Copy</button>
768
+ <div id="history-indicator" class="history-indicator">↑ View History</div>
769
+ <div id="history-overlay" class="history-overlay">
770
+ <div class="history-header">
771
+ <span>Terminal History (scroll to view)</span>
772
+ <button class="history-close" id="history-close">Back to Terminal</button>
773
+ </div>
774
+ <div class="history-content" id="history-content"></div>
775
+ </div>
740
776
 
741
777
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
742
778
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
779
+ <script src="https://cdn.jsdelivr.net/npm/ansi_up@5.2.1/ansi_up.min.js"></script>
743
780
  <script>
744
781
  let ws, term, fitAddon;
745
782
  let clients = [];
746
783
  let selectedClientId = null;
784
+ // History buffer for iTerm-like scrolling (stores raw output)
785
+ let historyBuffer = [];
786
+ const MAX_HISTORY_SIZE = 100000; // Max characters to store
747
787
 
748
788
  const themes = {
749
789
  'solarized-light': {
@@ -867,6 +907,20 @@ class TermitesServer {
867
907
  toolbar.style.background = t.headerBg;
868
908
  toolbar.style.borderColor = t.headerBorder;
869
909
  toolbar.style.color = t.foreground;
910
+ // History overlay styling
911
+ const historyOverlay = document.getElementById('history-overlay');
912
+ historyOverlay.style.background = t.background;
913
+ historyOverlay.style.color = t.foreground;
914
+ const historyHeader = historyOverlay.querySelector('.history-header');
915
+ historyHeader.style.background = t.headerBg;
916
+ historyHeader.style.borderColor = t.headerBorder;
917
+ const historyContent = document.getElementById('history-content');
918
+ historyContent.style.fontFamily = currentFont;
919
+ historyContent.style.fontSize = currentFontSize + 'px';
920
+ const historyIndicator = document.getElementById('history-indicator');
921
+ historyIndicator.style.background = t.headerBg;
922
+ historyIndicator.style.color = t.foreground;
923
+ historyIndicator.style.border = '1px solid ' + t.headerBorder;
870
924
  const style = document.documentElement.style;
871
925
  style.setProperty('--user-color', t.userColor);
872
926
  style.setProperty('--host-color', t.hostColor);
@@ -1042,44 +1096,61 @@ class TermitesServer {
1042
1096
  btn.addEventListener('click', handler);
1043
1097
  });
1044
1098
 
1045
- // Copy button - copy selection
1046
- const copyBtn = document.getElementById('copy-btn');
1047
- const handleCopy = async (e) => {
1048
- e.preventDefault();
1049
- const selection = term.getSelection();
1050
- if (selection) {
1051
- await navigator.clipboard.writeText(selection);
1052
- term.clearSelection();
1053
- }
1054
- };
1055
- copyBtn.addEventListener('touchstart', handleCopy, { passive: false });
1056
- copyBtn.addEventListener('click', handleCopy);
1057
-
1058
- // Paste button
1059
- const pasteBtn = document.getElementById('paste-btn');
1060
- const handlePaste = async (e) => {
1099
+ // History button
1100
+ const histBtn = document.getElementById('hist-btn');
1101
+ const handleHist = (e) => {
1061
1102
  e.preventDefault();
1062
- try {
1063
- const text = await navigator.clipboard.readText();
1064
- if (text && ws?.readyState === WebSocket.OPEN && selectedClientId) {
1065
- ws.send(JSON.stringify({ type: 'input', clientId: selectedClientId, text }));
1066
- }
1067
- } catch (e) {
1068
- console.error('Paste failed:', e);
1069
- }
1070
- term.focus();
1103
+ showHistoryOverlay();
1071
1104
  };
1072
- pasteBtn.addEventListener('touchstart', handlePaste, { passive: false });
1073
- pasteBtn.addEventListener('click', handlePaste);
1105
+ histBtn.addEventListener('touchstart', handleHist, { passive: false });
1106
+ histBtn.addEventListener('click', handleHist);
1074
1107
 
1075
1108
  // Selection mode with draggable markers
1076
1109
  let selMode = false;
1077
1110
  let selStart = { col: 0, row: 0 };
1078
1111
  let selEnd = { col: 0, row: 0 };
1079
- const selBtn = document.getElementById('sel-btn');
1080
1112
  const startMarker = document.getElementById('sel-start-marker');
1081
1113
  const endMarker = document.getElementById('sel-end-marker');
1082
1114
  const termContainer = document.getElementById('terminal-container');
1115
+ const floatCopyBtn = document.getElementById('float-copy-btn');
1116
+
1117
+ function showFloatCopyBtn() {
1118
+ if (!selMode) return;
1119
+ const cell = getCellSize();
1120
+ // Position at bottom-left of end marker
1121
+ const x = Math.max(4, (selEnd.col * cell.width) - 60);
1122
+ const y = ((selEnd.row + 1) * cell.height) + 5;
1123
+ floatCopyBtn.style.left = x + 'px';
1124
+ floatCopyBtn.style.top = y + 'px';
1125
+ floatCopyBtn.style.display = 'block';
1126
+ }
1127
+
1128
+ function hideFloatCopyBtn() {
1129
+ floatCopyBtn.style.display = 'none';
1130
+ }
1131
+
1132
+ function exitSelMode() {
1133
+ if (selMode) {
1134
+ selMode = false;
1135
+ startMarker.style.display = 'none';
1136
+ endMarker.style.display = 'none';
1137
+ hideFloatCopyBtn();
1138
+ term.clearSelection();
1139
+ }
1140
+ }
1141
+
1142
+ function enterSelMode(row) {
1143
+ if (!selMode) {
1144
+ selMode = true;
1145
+ const targetRow = row !== undefined ? row : term.buffer.active.cursorY;
1146
+ selStart = { col: 0, row: Math.max(0, targetRow - 2) };
1147
+ selEnd = { col: term.cols - 1, row: targetRow };
1148
+ startMarker.style.display = 'block';
1149
+ endMarker.style.display = 'block';
1150
+ updateSelection();
1151
+ showFloatCopyBtn();
1152
+ }
1153
+ }
1083
1154
 
1084
1155
  function getCellSize() {
1085
1156
  try {
@@ -1111,27 +1182,24 @@ class TermitesServer {
1111
1182
  updateMarkerPositions();
1112
1183
  }
1113
1184
 
1114
- function toggleSelMode(e) {
1115
- e.preventDefault();
1116
- selMode = !selMode;
1117
- selBtn.classList.toggle('active', selMode);
1118
- if (selMode) {
1119
- // Enter selection mode
1120
- const lastRow = term.buffer.active.cursorY;
1121
- selStart = { col: 0, row: Math.max(0, lastRow - 2) };
1122
- selEnd = { col: term.cols - 1, row: lastRow };
1123
- startMarker.style.display = 'block';
1124
- endMarker.style.display = 'block';
1125
- updateSelection();
1126
- } else {
1127
- // Exit selection mode
1128
- startMarker.style.display = 'none';
1129
- endMarker.style.display = 'none';
1130
- term.clearSelection();
1131
- }
1132
- }
1133
- selBtn.addEventListener('touchstart', toggleSelMode, { passive: false });
1134
- selBtn.addEventListener('click', toggleSelMode);
1185
+ // Long press to enter selection mode
1186
+ let longPressTimer = null;
1187
+ termContainer.addEventListener('touchstart', (e) => {
1188
+ longPressTimer = setTimeout(() => {
1189
+ // Calculate row from touch position
1190
+ const cell = getCellSize();
1191
+ const rect = termContainer.getBoundingClientRect();
1192
+ const y = e.touches[0].clientY - rect.top;
1193
+ const row = Math.floor(y / cell.height);
1194
+ enterSelMode(row);
1195
+ }, 500); // 500ms long press
1196
+ }, { passive: true });
1197
+ termContainer.addEventListener('touchend', () => {
1198
+ if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
1199
+ }, { passive: true });
1200
+ termContainer.addEventListener('touchmove', () => {
1201
+ if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
1202
+ }, { passive: true });
1135
1203
 
1136
1204
  // Make markers draggable
1137
1205
  function setupDraggable(marker, isStart) {
@@ -1170,6 +1238,7 @@ class TermitesServer {
1170
1238
  const onEnd = () => {
1171
1239
  dragging = false;
1172
1240
  marker.style.cursor = 'grab';
1241
+ showFloatCopyBtn(); // Show floating copy button after drag
1173
1242
  };
1174
1243
 
1175
1244
  marker.addEventListener('touchstart', onStart, { passive: false });
@@ -1183,28 +1252,145 @@ class TermitesServer {
1183
1252
  setupDraggable(startMarker, true);
1184
1253
  setupDraggable(endMarker, false);
1185
1254
 
1186
- // Touch scrolling for terminal
1187
- let touchScrollY = null;
1188
- const termEl = document.getElementById('terminal-container');
1189
- termEl.addEventListener('touchstart', (e) => {
1190
- if (e.touches.length === 1 && !selMode) {
1191
- touchScrollY = e.touches[0].clientY;
1255
+ // Floating copy button click handler
1256
+ const handleFloatCopy = async (e) => {
1257
+ e.preventDefault();
1258
+ e.stopPropagation();
1259
+ // Reuse the same copy logic
1260
+ let selection = term.getSelection();
1261
+ if (!selection && selMode) {
1262
+ let text = '';
1263
+ const buffer = term.buffer.active;
1264
+ for (let row = selStart.row; row <= selEnd.row; row++) {
1265
+ const line = buffer.getLine(row);
1266
+ if (line) {
1267
+ const startCol = (row === selStart.row) ? selStart.col : 0;
1268
+ const endCol = (row === selEnd.row) ? selEnd.col : term.cols - 1;
1269
+ for (let col = startCol; col <= endCol; col++) {
1270
+ const cell = line.getCell(col);
1271
+ if (cell) text += cell.getChars() || ' ';
1272
+ }
1273
+ if (row < selEnd.row) text += '\\n';
1274
+ }
1275
+ }
1276
+ selection = text.replace(/\\s+$/gm, '');
1277
+ }
1278
+ if (selection) {
1279
+ try {
1280
+ await navigator.clipboard.writeText(selection);
1281
+ } catch (err) {
1282
+ const textarea = document.createElement('textarea');
1283
+ textarea.value = selection;
1284
+ textarea.style.position = 'fixed';
1285
+ textarea.style.opacity = '0';
1286
+ document.body.appendChild(textarea);
1287
+ textarea.select();
1288
+ document.execCommand('copy');
1289
+ document.body.removeChild(textarea);
1290
+ }
1291
+ exitSelMode();
1292
+ }
1293
+ };
1294
+ floatCopyBtn.addEventListener('touchstart', handleFloatCopy, { passive: false });
1295
+ floatCopyBtn.addEventListener('click', handleFloatCopy);
1296
+ }
1297
+
1298
+ // History functions for iTerm-like scrolling
1299
+ function addToHistory(data) {
1300
+ historyBuffer.push(data);
1301
+ // Trim if too large
1302
+ const totalSize = historyBuffer.join('').length;
1303
+ if (totalSize > MAX_HISTORY_SIZE) {
1304
+ while (historyBuffer.length > 0 && historyBuffer.join('').length > MAX_HISTORY_SIZE * 0.8) {
1305
+ historyBuffer.shift();
1192
1306
  }
1193
- }, { passive: true });
1307
+ }
1308
+ }
1194
1309
 
1195
- termEl.addEventListener('touchmove', (e) => {
1196
- if (touchScrollY !== null && e.touches.length === 1 && !selMode) {
1197
- const deltaY = touchScrollY - e.touches[0].clientY;
1198
- const lines = Math.round(deltaY / 20); // ~20px per line
1199
- if (lines !== 0) {
1200
- term.scrollLines(lines);
1201
- touchScrollY = e.touches[0].clientY;
1310
+ function clearHistory() {
1311
+ historyBuffer = [];
1312
+ }
1313
+
1314
+ function showHistoryOverlay() {
1315
+ const overlay = document.getElementById('history-overlay');
1316
+ const content = document.getElementById('history-content');
1317
+ // Convert ANSI to HTML with colors using ansi_up
1318
+ const ansi_up = new AnsiUp();
1319
+ ansi_up.use_classes = false;
1320
+ const html = ansi_up.ansi_to_html(historyBuffer.join(''));
1321
+ content.innerHTML = html;
1322
+ overlay.classList.add('show');
1323
+ // Scroll to bottom of history
1324
+ content.scrollTop = content.scrollHeight;
1325
+ // Listen for any key to close
1326
+ document.addEventListener('keydown', historyKeyHandler);
1327
+ }
1328
+
1329
+ function historyKeyHandler(e) {
1330
+ // Allow copy/paste shortcuts (Ctrl+C, Ctrl+V, Cmd+C, Cmd+V)
1331
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'v' || e.key === 'a')) {
1332
+ return; // Don't close on copy/paste/select-all
1333
+ }
1334
+ hideHistoryOverlay();
1335
+ document.removeEventListener('keydown', historyKeyHandler);
1336
+ }
1337
+
1338
+ function hideHistoryOverlay() {
1339
+ document.getElementById('history-overlay').classList.remove('show');
1340
+ document.removeEventListener('keydown', historyKeyHandler);
1341
+ term.focus();
1342
+ }
1343
+
1344
+ function setupHistoryOverlay() {
1345
+ const closeBtn = document.getElementById('history-close');
1346
+ const indicator = document.getElementById('history-indicator');
1347
+ const overlay = document.getElementById('history-overlay');
1348
+
1349
+ closeBtn.addEventListener('click', hideHistoryOverlay);
1350
+ closeBtn.addEventListener('touchstart', (e) => { e.preventDefault(); hideHistoryOverlay(); }, { passive: false });
1351
+
1352
+ indicator.addEventListener('click', showHistoryOverlay);
1353
+ indicator.addEventListener('touchstart', (e) => { e.preventDefault(); showHistoryOverlay(); }, { passive: false });
1354
+
1355
+ // Track if already at top to detect "scroll up more" gesture
1356
+ let atTopCount = 0;
1357
+ let lastViewportY = -1;
1358
+
1359
+ const checkScrollAndMaybeOpenHistory = (scrollingUp) => {
1360
+ if (!term || historyBuffer.length === 0) return;
1361
+ const viewport = term.buffer.active.viewportY;
1362
+
1363
+ // If at top of scrollback and scrolling up, open history
1364
+ if (viewport === 0 && scrollingUp) {
1365
+ atTopCount++;
1366
+ if (atTopCount >= 2) { // Need 2 scroll-up gestures at top
1367
+ showHistoryOverlay();
1368
+ atTopCount = 0;
1202
1369
  }
1370
+ } else {
1371
+ atTopCount = 0;
1203
1372
  }
1373
+ lastViewportY = viewport;
1374
+ };
1375
+
1376
+ // Check on wheel scroll
1377
+ const termContainer = document.getElementById('terminal-container');
1378
+ termContainer.addEventListener('wheel', (e) => {
1379
+ const scrollingUp = e.deltaY < 0;
1380
+ setTimeout(() => checkScrollAndMaybeOpenHistory(scrollingUp), 50);
1204
1381
  }, { passive: true });
1205
1382
 
1206
- termEl.addEventListener('touchend', () => {
1207
- touchScrollY = null;
1383
+ // Touch scroll detection
1384
+ let touchStartY = 0;
1385
+ termContainer.addEventListener('touchstart', (e) => {
1386
+ touchStartY = e.touches[0].clientY;
1387
+ }, { passive: true });
1388
+ termContainer.addEventListener('touchend', (e) => {
1389
+ const touchEndY = e.changedTouches[0].clientY;
1390
+ const scrollingUp = touchEndY > touchStartY + 20; // Finger moved down = scroll up
1391
+ if (scrollingUp) {
1392
+ setTimeout(() => checkScrollAndMaybeOpenHistory(true), 50);
1393
+ }
1208
1394
  }, { passive: true });
1209
1395
  }
1210
1396
 
@@ -1241,6 +1427,7 @@ class TermitesServer {
1241
1427
  '<span class="sep">@</span><span class="host">' + client.hostname + '</span>';
1242
1428
  }
1243
1429
  term.clear();
1430
+ clearHistory(); // Clear history when switching clients
1244
1431
  ws.send(JSON.stringify({ type: 'select', clientId }));
1245
1432
  updateClientList();
1246
1433
  document.getElementById('drawer').classList.remove('open');
@@ -1252,6 +1439,7 @@ class TermitesServer {
1252
1439
  loadSettings();
1253
1440
  setupDrawer();
1254
1441
  setupMobileToolbar();
1442
+ setupHistoryOverlay();
1255
1443
  term = new Terminal({
1256
1444
  theme: themes[currentTheme], fontFamily: currentFont,
1257
1445
  fontSize: currentFontSize, cursorBlink: true,
@@ -1394,6 +1582,7 @@ class TermitesServer {
1394
1582
  break;
1395
1583
  case 'output':
1396
1584
  if (d.clientId === selectedClientId) {
1585
+ addToHistory(d.data);
1397
1586
  term.write(d.data);
1398
1587
  term.scrollToBottom();
1399
1588
  }