cli-tunnel 1.2.2 → 1.3.0-beta.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/package.json +1 -1
- package/remote-ui/app.js +90 -0
- package/remote-ui/index.html +1 -0
- package/remote-ui/styles.css +3 -0
package/package.json
CHANGED
package/remote-ui/app.js
CHANGED
|
@@ -52,6 +52,89 @@
|
|
|
52
52
|
var focusedIndex = 0;
|
|
53
53
|
var tmuxPreset = 'equal';
|
|
54
54
|
|
|
55
|
+
// ─── Terminal Recording (MediaRecorder API) ───────────────
|
|
56
|
+
var mediaRecorder = null;
|
|
57
|
+
var recordedChunks = [];
|
|
58
|
+
var isRecording = false;
|
|
59
|
+
var recordTimer = null;
|
|
60
|
+
|
|
61
|
+
function startRecording() {
|
|
62
|
+
var canvas = null;
|
|
63
|
+
if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
|
|
64
|
+
canvas = gridTerminals[focusedIndex].panel.querySelector('canvas');
|
|
65
|
+
} else {
|
|
66
|
+
canvas = termContainer.querySelector('canvas');
|
|
67
|
+
}
|
|
68
|
+
if (!canvas) { console.warn('No terminal canvas found'); return false; }
|
|
69
|
+
try {
|
|
70
|
+
var stream = canvas.captureStream(30); // 30 fps
|
|
71
|
+
var mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9'
|
|
72
|
+
: MediaRecorder.isTypeSupported('video/webm;codecs=vp8') ? 'video/webm;codecs=vp8'
|
|
73
|
+
: 'video/webm';
|
|
74
|
+
mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType, videoBitsPerSecond: 2500000 });
|
|
75
|
+
recordedChunks = [];
|
|
76
|
+
mediaRecorder.ondataavailable = function(e) {
|
|
77
|
+
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
|
|
78
|
+
};
|
|
79
|
+
mediaRecorder.onstop = function() {
|
|
80
|
+
var blob = new Blob(recordedChunks, { type: mimeType });
|
|
81
|
+
var url = URL.createObjectURL(blob);
|
|
82
|
+
var a = document.createElement('a');
|
|
83
|
+
var timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
84
|
+
a.href = url;
|
|
85
|
+
a.download = 'cli-tunnel-' + timestamp + '.webm';
|
|
86
|
+
document.body.appendChild(a);
|
|
87
|
+
a.click();
|
|
88
|
+
document.body.removeChild(a);
|
|
89
|
+
setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
|
|
90
|
+
};
|
|
91
|
+
mediaRecorder.start(1000); // collect data every 1s
|
|
92
|
+
isRecording = true;
|
|
93
|
+
// Auto-stop after 10 minutes to prevent memory issues
|
|
94
|
+
setTimeout(function() {
|
|
95
|
+
if (isRecording) { toggleRecording(); }
|
|
96
|
+
}, 10 * 60 * 1000);
|
|
97
|
+
return true;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('Recording failed:', e);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stopRecording() {
|
|
105
|
+
if (recordTimer) { clearInterval(recordTimer); recordTimer = null; }
|
|
106
|
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
|
107
|
+
mediaRecorder.stop();
|
|
108
|
+
}
|
|
109
|
+
isRecording = false;
|
|
110
|
+
mediaRecorder = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toggleRecording() {
|
|
114
|
+
var btn = document.getElementById('btn-record');
|
|
115
|
+
if (isRecording) {
|
|
116
|
+
stopRecording();
|
|
117
|
+
if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; }
|
|
118
|
+
} else {
|
|
119
|
+
if (startRecording()) {
|
|
120
|
+
if (btn) { btn.classList.add('recording'); btn.textContent = '⏹'; btn.title = 'Stop recording & download'; btn.setAttribute('aria-label', 'Stop recording'); }
|
|
121
|
+
var recordStartTime = Date.now();
|
|
122
|
+
recordTimer = setInterval(function() {
|
|
123
|
+
if (!isRecording) { clearInterval(recordTimer); recordTimer = null; return; }
|
|
124
|
+
var elapsed = Math.floor((Date.now() - recordStartTime) / 1000);
|
|
125
|
+
var min = Math.floor(elapsed / 60);
|
|
126
|
+
var sec = elapsed % 60;
|
|
127
|
+
if (btn) btn.textContent = '⏹ ' + min + ':' + (sec < 10 ? '0' : '') + sec;
|
|
128
|
+
}, 1000);
|
|
129
|
+
} else {
|
|
130
|
+
// Show error to user
|
|
131
|
+
var prevText = statusText ? statusText.textContent : '';
|
|
132
|
+
if (statusText) { statusText.textContent = 'Recording not available'; }
|
|
133
|
+
setTimeout(function() { if (statusText && statusText.textContent === 'Recording not available') statusText.textContent = prevText; }, 3000);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
55
138
|
// ─── xterm.js Terminal ───────────────────────────────────
|
|
56
139
|
let xterm = null;
|
|
57
140
|
let fitAddon = null;
|
|
@@ -598,6 +681,7 @@
|
|
|
598
681
|
}
|
|
599
682
|
|
|
600
683
|
function destroyGrid() {
|
|
684
|
+
if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
|
|
601
685
|
gridTerminals.forEach(function(gt) {
|
|
602
686
|
if (gt.ws) { try { gt.ws.close(); } catch(e) {} }
|
|
603
687
|
if (gt.xterm) { try { gt.xterm.dispose(); } catch(e) {} }
|
|
@@ -623,6 +707,7 @@
|
|
|
623
707
|
return;
|
|
624
708
|
}
|
|
625
709
|
if (currentView === 'terminal') {
|
|
710
|
+
if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
|
|
626
711
|
currentView = 'dashboard';
|
|
627
712
|
terminal.classList.add('hidden');
|
|
628
713
|
termContainer.classList.add('hidden');
|
|
@@ -853,6 +938,7 @@
|
|
|
853
938
|
if (xterm) { lastCols = 0; lastRows = 0; sendResize(); }
|
|
854
939
|
};
|
|
855
940
|
ws.onclose = () => {
|
|
941
|
+
if (isRecording) { stopRecording(); var btn = document.getElementById('btn-record'); if (btn) { btn.classList.remove('recording'); btn.textContent = '⏺'; btn.title = 'Record terminal'; } }
|
|
856
942
|
connected = false; acpReady = false; sessionId = null;
|
|
857
943
|
setStatus('offline', 'Disconnected');
|
|
858
944
|
const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempt)) + Math.random() * 1000;
|
|
@@ -1007,6 +1093,10 @@
|
|
|
1007
1093
|
};
|
|
1008
1094
|
keyBar.addEventListener('click', function(e) {
|
|
1009
1095
|
var btn = e.target;
|
|
1096
|
+
if (btn && btn.tagName === 'BUTTON' && btn.dataset.action === 'toggle-record') {
|
|
1097
|
+
toggleRecording();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1010
1100
|
if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
|
|
1011
1101
|
var key = keyMap[btn.dataset.key] || btn.dataset.key;
|
|
1012
1102
|
if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
|
package/remote-ui/index.html
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
<button data-key="\x03">Ctrl+C</button>
|
|
43
43
|
<button data-key=" ">Space</button>
|
|
44
44
|
<button data-key="\x7f">⌫</button>
|
|
45
|
+
<button id="btn-record" data-action="toggle-record" title="Record terminal" aria-label="Record terminal">⏺</button>
|
|
45
46
|
</div>
|
|
46
47
|
<form id="input-form">
|
|
47
48
|
<span class="prompt">></span>
|
package/remote-ui/styles.css
CHANGED
|
@@ -197,6 +197,9 @@ header {
|
|
|
197
197
|
-webkit-tap-highlight-color: transparent;
|
|
198
198
|
}
|
|
199
199
|
#key-bar button:active { background: var(--blue); color: #000; }
|
|
200
|
+
#btn-record { color: var(--text-dim); }
|
|
201
|
+
#btn-record.recording { color: var(--red); animation: pulse-record 1s infinite; }
|
|
202
|
+
@keyframes pulse-record { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
200
203
|
#input-form {
|
|
201
204
|
display: flex;
|
|
202
205
|
align-items: center;
|