feat: enhance download functionality with selectable log types and command history management

This commit is contained in:
roy01
2026-06-04 15:53:42 +08:00
parent 7991f1fcba
commit 750bea70ec
3 changed files with 174 additions and 26 deletions
+81 -7
View File
@@ -24,6 +24,7 @@ const sendBtn = document.getElementById('send-btn');
const clearBtn = document.getElementById('clear-btn');
const addSessionBtn = document.getElementById('add-session-btn');
const sessionListContainer = document.getElementById('session-list');
const downloadTypeSelect = document.getElementById('download-type');
// PortSession Class definition
class PortSession {
@@ -36,7 +37,12 @@ class PortSession {
this.reader = null;
this.writer = null;
this.keepReading = false;
this.receivedLogBuffer = [];
this.rxLogBuffer = [];
this.txLogBuffer = [];
this.combinedLogBuffer = [];
this.downloadType = 'rx_tx';
this.commandHistory = [];
this.historyIndex = -1;
this.isTerminalEmpty = true;
this.deviceInfo = '';
@@ -113,6 +119,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (activeSession) activeSession.sendText = sendInput.value;
});
downloadTypeSelect.addEventListener('change', () => {
if (activeSession) activeSession.downloadType = downloadTypeSelect.value;
});
// Initialize with a default session
addSession();
});
@@ -237,8 +247,9 @@ function selectSession(session) {
sendFormatSelect.value = activeSession.sendFormat;
sendEolSelect.value = activeSession.sendEol;
sendInput.value = activeSession.sendText;
downloadTypeSelect.value = activeSession.downloadType;
downloadBtn.disabled = activeSession.isTerminalEmpty;
downloadBtn.disabled = (activeSession.rxLogBuffer.length === 0 && activeSession.txLogBuffer.length === 0);
// Sync status banner and buttons
const isConnected = activeSession.port !== null && activeSession.port.readable;
@@ -485,7 +496,8 @@ function handleIncomingSessionData(session, uint8Array, decoder) {
formattedText = decoder.decode(uint8Array);
}
session.receivedLogBuffer.push(formattedText);
session.rxLogBuffer.push(formattedText);
session.combinedLogBuffer.push(formattedText);
appendSessionTerminalText(session, formattedText, 'received');
}
@@ -504,6 +516,19 @@ async function sendSessionData() {
const rawInput = sendInput.value;
if (!rawInput) return;
// Add command to history (maximum 10 items)
const trimmed = rawInput.trim();
if (trimmed) {
const history = session.commandHistory;
if (history.length === 0 || history[history.length - 1] !== trimmed) {
history.push(trimmed);
if (history.length > 10) {
history.shift();
}
}
session.historyIndex = history.length;
}
const format = session.sendFormat;
const eol = session.sendEol;
let bytesToSend;
@@ -552,6 +577,11 @@ async function sendSessionData() {
}
appendSessionTerminalText(session, `\n[Sent] ${displaySent}\n`, 'sent');
// Save sent log
session.txLogBuffer.push(`[Sent] ${displaySent}\n`);
session.combinedLogBuffer.push(`\n[Sent] ${displaySent}\n`);
sendInput.value = '';
session.sendText = '';
@@ -614,15 +644,35 @@ function clearTerminal() {
activeSession.terminalOutputElement.innerHTML = 'No data received yet...';
activeSession.terminalOutputElement.classList.add('empty');
activeSession.isTerminalEmpty = true;
activeSession.receivedLogBuffer = [];
activeSession.rxLogBuffer = [];
activeSession.txLogBuffer = [];
activeSession.combinedLogBuffer = [];
downloadBtn.disabled = true;
showToast('Terminal cleared.', 'success');
}
function downloadLog() {
if (!activeSession || activeSession.receivedLogBuffer.length === 0) return;
if (!activeSession) return;
let content = '';
let filenameSuffix = '';
if (activeSession.downloadType === 'rx') {
content = activeSession.rxLogBuffer.join('');
filenameSuffix = 'rx';
} else if (activeSession.downloadType === 'tx') {
content = activeSession.txLogBuffer.join('');
filenameSuffix = 'tx';
} else {
content = activeSession.combinedLogBuffer.join('');
filenameSuffix = 'combined';
}
if (!content) {
showToast('No logs to download for the selected type.', 'error');
return;
}
const content = activeSession.receivedLogBuffer.join('');
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
@@ -630,7 +680,7 @@ function downloadLog() {
const a = document.createElement('a');
a.href = url;
a.download = `uart_log_${activeSession.name.replace(/\s+/g, '_')}_${dateStr}.txt`;
a.download = `uart_log_${activeSession.name.replace(/\s+/g, '_')}_${filenameSuffix}_${dateStr}.txt`;
document.body.appendChild(a);
a.click();
@@ -664,6 +714,30 @@ sendInput.addEventListener('keydown', (e) => {
e.preventDefault();
sendSessionData();
}
if (e.key === 'ArrowUp') {
e.preventDefault();
if (activeSession && activeSession.commandHistory.length > 0) {
if (activeSession.historyIndex > 0) {
activeSession.historyIndex--;
sendInput.value = activeSession.commandHistory[activeSession.historyIndex];
activeSession.sendText = sendInput.value;
}
}
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (activeSession && activeSession.commandHistory.length > 0) {
if (activeSession.historyIndex < activeSession.commandHistory.length - 1) {
activeSession.historyIndex++;
sendInput.value = activeSession.commandHistory[activeSession.historyIndex];
activeSession.sendText = sendInput.value;
} else {
activeSession.historyIndex = activeSession.commandHistory.length;
sendInput.value = '';
activeSession.sendText = '';
}
}
}
if (e.key === 'Tab' && e.ctrlKey) {
e.preventDefault();
const start = sendInput.selectionStart;
+54
View File
@@ -896,3 +896,57 @@ body.light-theme .session-item.active {
.hidden {
display: none !important;
}
/* Download Group Control Styling */
.download-group {
display: flex;
align-items: center;
gap: 0.25rem;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 3px 6px 3px 3px;
height: 48px;
box-shadow: var(--shadow-sm);
transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.download-group:hover {
border-color: var(--border-hover);
}
.download-group .download-btn {
height: 40px;
width: 40px;
border-radius: 6px;
box-shadow: none;
}
.download-group .download-btn:hover:not(:disabled) {
transform: none;
}
.download-select-wrapper {
width: 105px;
flex-shrink: 0;
}
.download-select-wrapper select {
background-color: transparent !important;
border: none !important;
padding: 0.4rem 1.75rem 0.4rem 0.5rem !important;
font-weight: 600;
font-size: 0.85rem;
color: var(--text-main);
box-shadow: none !important;
}
.download-select-wrapper select option {
background-color: var(--bg-panel);
color: var(--text-main);
}
.download-select-wrapper::after {
right: 0.5rem;
}
+39 -19
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -7,9 +8,12 @@
<!-- Google Fonts for premium design: Inter for UI, JetBrains Mono for monospace data -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="./index.css">
</head>
<body class="dark-theme">
<div class="app-container">
<!-- Header -->
@@ -17,7 +21,8 @@
<h1>UART Terminal</h1>
<button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
<!-- Sun Icon -->
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
@@ -29,7 +34,8 @@
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<!-- Moon Icon -->
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
@@ -43,7 +49,8 @@
<h2>Sessions</h2>
<button id="add-session-btn" class="add-session-btn" title="Add new session">
<!-- Plus Icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
@@ -56,7 +63,7 @@
<!-- Main Workspace -->
<main class="app-main">
<!-- Connection Controls & Status -->
<section class="control-row">
<div class="action-buttons">
@@ -70,20 +77,30 @@
<rect x="5" y="5" width="14" height="14" rx="2"></rect>
</svg>
</button>
<button id="download-btn" class="download-btn" title="Download received log" disabled>
<!-- Download Icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<div class="download-group">
<button id="download-btn" class="download-btn" title="Download log" disabled>
<!-- Download Icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<div class="select-wrapper select-wrapper-sm download-select-wrapper">
<select id="download-type" title="Select download content">
<option value="rx_tx" selected>RX & TX</option>
<option value="rx">RX Only</option>
<option value="tx">TX Only</option>
</select>
</div>
</div>
</div>
<!-- Connection Status Banner -->
<div id="status-banner" class="status-banner warn">
<svg class="status-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="status-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
@@ -188,7 +205,7 @@
<section class="send-data-section">
<div class="send-bar">
<div class="input-container">
<input type="text" id="send-input" placeholder="Enter data to send... (Ctrl+Tab to insert tab)" autocomplete="off">
<input type="text" id="send-input" placeholder="Enter data to send..." autocomplete="off">
</div>
<div class="select-wrapper select-wrapper-sm send-format-wrapper">
<select id="send-format">
@@ -206,14 +223,16 @@
</div>
<button id="send-btn" class="send-btn" title="Send Data (Ctrl+Enter)">
<!-- Send / Paper Airplane Icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
<button id="clear-btn" class="clear-btn" title="Clear Terminal Console">
<!-- Trash Icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
@@ -229,4 +248,5 @@
<script type="module" src="./app.js"></script>
</body>
</html>
</html>