termites 1.0.27 → 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 +269 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termites",
3
- "version": "1.0.27",
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 });
@@ -1182,6 +1251,147 @@ class TermitesServer {
1182
1251
 
1183
1252
  setupDraggable(startMarker, true);
1184
1253
  setupDraggable(endMarker, false);
1254
+
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();
1306
+ }
1307
+ }
1308
+ }
1309
+
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;
1369
+ }
1370
+ } else {
1371
+ atTopCount = 0;
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);
1381
+ }, { passive: true });
1382
+
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
+ }
1394
+ }, { passive: true });
1185
1395
  }
1186
1396
 
1187
1397
  function updateClientList() {
@@ -1217,6 +1427,7 @@ class TermitesServer {
1217
1427
  '<span class="sep">@</span><span class="host">' + client.hostname + '</span>';
1218
1428
  }
1219
1429
  term.clear();
1430
+ clearHistory(); // Clear history when switching clients
1220
1431
  ws.send(JSON.stringify({ type: 'select', clientId }));
1221
1432
  updateClientList();
1222
1433
  document.getElementById('drawer').classList.remove('open');
@@ -1228,6 +1439,7 @@ class TermitesServer {
1228
1439
  loadSettings();
1229
1440
  setupDrawer();
1230
1441
  setupMobileToolbar();
1442
+ setupHistoryOverlay();
1231
1443
  term = new Terminal({
1232
1444
  theme: themes[currentTheme], fontFamily: currentFont,
1233
1445
  fontSize: currentFontSize, cursorBlink: true,
@@ -1370,6 +1582,7 @@ class TermitesServer {
1370
1582
  break;
1371
1583
  case 'output':
1372
1584
  if (d.clientId === selectedClientId) {
1585
+ addToHistory(d.data);
1373
1586
  term.write(d.data);
1374
1587
  term.scrollToBottom();
1375
1588
  }