Skip to content

Commit 2bedf53

Browse files
committed
feat: improve terminal interaction feedback
1 parent d0cca1d commit 2bedf53

3 files changed

Lines changed: 294 additions & 10 deletions

File tree

src/renderer/app.js

Lines changed: 181 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ const elements = {
6464
terminalContainer: document.getElementById('terminal-container'),
6565
terminalTitleText: document.getElementById('terminal-title-text'),
6666
terminalStatus: document.getElementById('terminal-status'),
67+
terminalPresence: document.getElementById('terminal-presence'),
68+
terminalPresenceLabel: document.getElementById('terminal-presence-label'),
69+
terminalPresenceHint: document.getElementById('terminal-presence-hint'),
6770
btnTerminalModeAdb: document.getElementById('btn-terminal-mode-adb'),
6871
btnTerminalModeSsh: document.getElementById('btn-terminal-mode-ssh'),
6972
btnCloseTerminal: document.getElementById('btn-close-terminal'),
@@ -126,6 +129,9 @@ class TerminalManager {
126129
this.mode = this.loadMode();
127130
this.credentials = null;
128131
this.rememberCredentials = false;
132+
this.localAdbInput = '';
133+
this.adbPromptVisible = false;
134+
this.adbPromptTimer = null;
129135
}
130136

131137
normalizeMode(mode) {
@@ -157,6 +163,163 @@ class TerminalManager {
157163
return this.normalizeMode(mode) === 'ssh' ? 'SSH Terminal' : 'ADB Terminal';
158164
}
159165

166+
clearAdbPromptTimer() {
167+
if (this.adbPromptTimer) {
168+
clearTimeout(this.adbPromptTimer);
169+
this.adbPromptTimer = null;
170+
}
171+
}
172+
173+
resetAdbFlowState() {
174+
this.clearAdbPromptTimer();
175+
this.localAdbInput = '';
176+
this.adbPromptVisible = false;
177+
}
178+
179+
updateInteractionState(stage, hint = '') {
180+
const presenceEl = elements.terminalPresence;
181+
const labelEl = elements.terminalPresenceLabel;
182+
const hintEl = elements.terminalPresenceHint;
183+
if (!presenceEl || !labelEl || !hintEl) return;
184+
185+
const defaults = {
186+
disconnected: {
187+
label: 'Disconnected',
188+
hint: 'Open the terminal to start a shell session.'
189+
},
190+
connecting: {
191+
label: `Opening ${this.getModeLabel()}`,
192+
hint: `Preparing the ${this.getModeLabel().toLowerCase()} session...`
193+
},
194+
credentials: {
195+
label: 'Waiting For Credentials',
196+
hint: 'Enter your SSH username and password to continue.'
197+
},
198+
ready: {
199+
label: `${this.getModeLabel(this.connectedMode || this.mode)} Ready`,
200+
hint: this.normalizeMode(this.connectedMode || this.mode) === 'adb'
201+
? 'Blue ">" means you can type. Press Enter to run the command.'
202+
: 'Use the remote shell prompt in the terminal to keep typing.'
203+
},
204+
typing: {
205+
label: 'Typing',
206+
hint: 'Your command is still being edited. Press Enter to run it.'
207+
},
208+
running: {
209+
label: 'Running',
210+
hint: 'The command was sent to the device. Waiting for response...'
211+
},
212+
output: {
213+
label: 'Streaming Output',
214+
hint: 'Device output is still arriving. A new prompt appears when it settles.'
215+
},
216+
error: {
217+
label: 'Needs Attention',
218+
hint: 'Check the latest terminal message for details.'
219+
}
220+
};
221+
222+
const next = defaults[stage] || defaults.disconnected;
223+
presenceEl.className = `terminal-presence is-${stage}`;
224+
labelEl.textContent = next.label;
225+
hintEl.textContent = hint || next.hint;
226+
}
227+
228+
renderAdbPrompt() {
229+
if (!this.terminal || this.connectedMode !== 'adb' || this.adbPromptVisible) {
230+
return;
231+
}
232+
233+
this.terminal.write('\x1b[38;2;96;165;250m>\x1b[0m ');
234+
this.adbPromptVisible = true;
235+
this.updateInteractionState('ready');
236+
}
237+
238+
scheduleAdbPrompt(delay = 220) {
239+
if (this.connectedMode !== 'adb') return;
240+
241+
this.clearAdbPromptTimer();
242+
this.adbPromptTimer = setTimeout(() => {
243+
if (!this.connected || this.connectedMode !== 'adb' || this.localAdbInput) {
244+
return;
245+
}
246+
this.renderAdbPrompt();
247+
}, delay);
248+
}
249+
250+
handleAdbLocalInput(data) {
251+
for (const char of data) {
252+
if (char === '\r') {
253+
this.terminal.write('\r\n');
254+
const hadCommand = this.localAdbInput.trim().length > 0;
255+
this.localAdbInput = '';
256+
this.adbPromptVisible = false;
257+
this.updateInteractionState(hadCommand ? 'running' : 'ready');
258+
if (!hadCommand) {
259+
this.scheduleAdbPrompt(120);
260+
}
261+
continue;
262+
}
263+
264+
if (char === '\u007f') {
265+
if (this.localAdbInput.length > 0) {
266+
this.localAdbInput = this.localAdbInput.slice(0, -1);
267+
this.terminal.write('\b \b');
268+
}
269+
this.updateInteractionState(this.localAdbInput ? 'typing' : 'ready');
270+
continue;
271+
}
272+
273+
if (char === '\t') {
274+
if (!this.adbPromptVisible) {
275+
this.renderAdbPrompt();
276+
}
277+
this.localAdbInput += ' ';
278+
this.terminal.write(' ');
279+
this.updateInteractionState('typing');
280+
continue;
281+
}
282+
283+
if (char === '\u0003') {
284+
this.localAdbInput = '';
285+
this.adbPromptVisible = false;
286+
this.terminal.write('^C\r\n');
287+
this.updateInteractionState('ready', 'Command interrupted. You can type the next one.');
288+
this.scheduleAdbPrompt(120);
289+
continue;
290+
}
291+
292+
if (char < ' ' || char === '\x7f') {
293+
continue;
294+
}
295+
296+
if (!this.adbPromptVisible) {
297+
this.renderAdbPrompt();
298+
}
299+
300+
this.localAdbInput += char;
301+
this.terminal.write(char);
302+
this.updateInteractionState('typing');
303+
}
304+
}
305+
306+
handleTerminalData(data) {
307+
if (!this.terminal) return;
308+
309+
if (this.connectedMode === 'adb') {
310+
this.clearAdbPromptTimer();
311+
this.adbPromptVisible = false;
312+
if (data && data.trim()) {
313+
this.updateInteractionState('output');
314+
}
315+
this.terminal.write(data);
316+
this.scheduleAdbPrompt();
317+
return;
318+
}
319+
320+
this.terminal.write(data);
321+
}
322+
160323
updateModeUI() {
161324
const isAdb = this.mode === 'adb';
162325

@@ -302,6 +465,9 @@ class TerminalManager {
302465
// Handle input
303466
this.terminal.onData((data) => {
304467
if (this.connected) {
468+
if (this.connectedMode === 'adb') {
469+
this.handleAdbLocalInput(data);
470+
}
305471
window.electronAPI.terminalWrite(data);
306472
}
307473
});
@@ -315,18 +481,18 @@ class TerminalManager {
315481

316482
// Listen for data from main process
317483
window.electronAPI.onTerminalData((data) => {
318-
if (this.terminal) {
319-
this.terminal.write(data);
320-
}
484+
this.handleTerminalData(data);
321485
});
322486

323487
// Listen for close event
324488
window.electronAPI.onTerminalClose((data) => {
325489
const closedMode = this.normalizeMode(data && data.mode ? data.mode : this.connectedMode);
490+
this.resetAdbFlowState();
326491
this.connected = false;
327492
this.connecting = false;
328493
this.connectedMode = null;
329494
this.updateStatus('disconnected');
495+
this.updateInteractionState('disconnected', `${this.getModeTitle(closedMode)} closed${data && data.reason ? `: ${data.reason}` : '.'}`);
330496
this.updateModeUI();
331497
this.terminal.write(`\r\n\x1b[33m[${this.getModeTitle(closedMode)} closed${data && data.reason ? `: ${data.reason}` : ''}]\x1b[0m\r\n`);
332498
});
@@ -337,6 +503,7 @@ class TerminalManager {
337503
});
338504

339505
this.updateModeUI();
506+
this.updateInteractionState('disconnected');
340507
}
341508

342509
/**
@@ -379,6 +546,7 @@ class TerminalManager {
379546
case 'connecting':
380547
statusEl.classList.add('connecting');
381548
statusEl.textContent = `Connecting ${this.getModeLabel()}...`;
549+
this.updateInteractionState('connecting');
382550
break;
383551
case 'connected':
384552
statusEl.classList.add('connected');
@@ -387,6 +555,7 @@ class TerminalManager {
387555
case 'error':
388556
statusEl.classList.add('error');
389557
statusEl.textContent = message || 'Error';
558+
this.updateInteractionState('error', message || '');
390559
break;
391560
default:
392561
statusEl.textContent = '';
@@ -598,13 +767,15 @@ class TerminalManager {
598767

599768
writeBanner(mode) {
600769
const title = this.getModeTitle(mode);
770+
this.resetAdbFlowState();
601771
this.terminal.clear();
602772
this.terminal.write(`\x1b[36m=== ${title} ===\x1b[0m\r\n`);
603773
if (mode === 'adb') {
604774
this.terminal.write('\x1b[90mDirect interactive shell over adb.\x1b[0m\r\n');
605-
this.terminal.write('\x1b[90mType directly in the terminal area after it opens.\x1b[0m\r\n\r\n');
775+
this.terminal.write('\x1b[90mBlue ">" means ready for input. Status strip shows typing, running and output.\x1b[0m\r\n\r\n');
606776
} else {
607-
this.terminal.write('\x1b[90mTermux SSH session over the adb tunnel.\x1b[0m\r\n\r\n');
777+
this.terminal.write('\x1b[90mTermux SSH session over the adb tunnel.\x1b[0m\r\n');
778+
this.terminal.write('\x1b[90mFollow the remote shell prompt shown by the device once connected.\x1b[0m\r\n\r\n');
608779
}
609780
}
610781

@@ -735,6 +906,7 @@ class TerminalManager {
735906
*/
736907
async connectSshMode() {
737908
console.log('[Terminal] Starting SSH connection process');
909+
this.updateInteractionState('credentials');
738910
this.terminal.write('\x1b[90mStep 1/3: Waiting for SSH credentials...\x1b[0m\r\n');
739911
const credentials = await this.promptCredentials();
740912

@@ -770,6 +942,7 @@ class TerminalManager {
770942
this.connectedMode = 'ssh';
771943
this.updateModeUI();
772944
this.updateStatus('connected');
945+
this.updateInteractionState('ready');
773946
this.terminal.write(`\r\n\x1b[32mSSH connected in ${elapsed}ms.\x1b[0m\r\n\r\n`);
774947
this.fit();
775948
this.focusTerminal();
@@ -808,6 +981,7 @@ class TerminalManager {
808981
this.updateModeUI();
809982
this.updateStatus('connected');
810983
this.terminal.write(`\r\n\x1b[32mADB shell connected in ${elapsed}ms.\x1b[0m\r\n\r\n`);
984+
this.renderAdbPrompt();
811985
this.fit();
812986
this.focusTerminal();
813987
return true;
@@ -842,8 +1016,10 @@ class TerminalManager {
8421016
this.connected = false;
8431017
this.connecting = false;
8441018
this.connectedMode = null;
1019+
this.resetAdbFlowState();
8451020
state.terminalLastError = '';
8461021
this.updateStatus('disconnected');
1022+
this.updateInteractionState('disconnected');
8471023
if (this.terminal && !silent) {
8481024
this.terminal.write('\r\n\x1b[33mDisconnected.\x1b[0m\r\n');
8491025
}

src/renderer/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ <h4>Quick Start</h4>
222222
<div class="terminal-status" id="terminal-status"></div>
223223
<button id="btn-close-terminal" class="btn-close-terminal" title="Close terminal" aria-label="Close terminal">&times;</button>
224224
</div>
225+
<div class="terminal-presence is-disconnected" id="terminal-presence">
226+
<span class="terminal-presence-dot" aria-hidden="true"></span>
227+
<span class="terminal-presence-label" id="terminal-presence-label">Disconnected</span>
228+
<span class="terminal-presence-hint" id="terminal-presence-hint">Open the terminal to start a shell session.</span>
229+
</div>
225230
<div id="terminal-container" class="terminal-container"></div>
226231
</div>
227232
</main>

0 commit comments

Comments
 (0)