feat: enhance download functionality with selectable log types and command history management
This commit is contained in:
+81
-7
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
Reference in New Issue
Block a user