365 lines
11 KiB
HTML
365 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<title>NukiHub WebSerial</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style type="text/css">
|
|
div {
|
|
display: block;
|
|
}
|
|
|
|
a {
|
|
margin: 0.4rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
*,
|
|
::after,
|
|
::before {
|
|
box-sizing: border-box;
|
|
border-width: 0;
|
|
}
|
|
|
|
html {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
overscroll-behavior: none;
|
|
height: 100%;
|
|
width: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI",
|
|
Roboto, "Helvetica Neue", sans-serif;
|
|
background: #000;
|
|
height: 100%;
|
|
}
|
|
|
|
.app {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
}
|
|
|
|
.gap-2 {
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.content {
|
|
flex: 1 1 0%;
|
|
align-content: flex-end;
|
|
height: 100%;
|
|
color: #d4d4d8;
|
|
font-family: monospace;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.content p {
|
|
margin: 0;
|
|
overflow-wrap: break-word;
|
|
text-wrap: wrap;
|
|
white-space: pre-line;
|
|
}
|
|
|
|
.panel {
|
|
position: relative;
|
|
border: #fff 0.5rem solid;
|
|
border-radius: 1rem;
|
|
max-width: 45rem;
|
|
width: calc(100% - 1rem);
|
|
font-size: medium;
|
|
}
|
|
|
|
.buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
border-bottom-width: 1px;
|
|
flex-direction: row;
|
|
column-gap: 1.5rem;
|
|
padding-top: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
padding-right: 0.5rem;
|
|
color: #a1a1aa;
|
|
}
|
|
|
|
.buttons button {
|
|
cursor: pointer;
|
|
padding: 8px 10px 8px;
|
|
font-size: medium;
|
|
outline-style: none;
|
|
border: 0px;
|
|
color: #a1a1aa;
|
|
background-color: transparent;
|
|
}
|
|
|
|
.buttons button svg {
|
|
width: 1.4rem;
|
|
height: 1.4rem;
|
|
}
|
|
|
|
.buttons button:hover {
|
|
background-color: #18181b;
|
|
}
|
|
|
|
.w-full {
|
|
width: 100%;
|
|
}
|
|
|
|
.rounded {
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.flex {
|
|
display: flex;
|
|
}
|
|
|
|
.grow {
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.shadow {
|
|
filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
|
|
}
|
|
|
|
.items-center {
|
|
align-items: center;
|
|
}
|
|
|
|
.command_container {
|
|
border-top-width: 1px;
|
|
border-bottom: 1px;
|
|
border-color: #18181b;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem;
|
|
}
|
|
|
|
input {
|
|
margin: 0;
|
|
padding: .375rem .75rem;
|
|
border: 0 solid #6b7280;
|
|
border-radius: .25rem;
|
|
font-family: monospace;
|
|
font-size: .875rem;
|
|
line-height: 1.5rem;
|
|
background-color: #ffffff0d;
|
|
color: #fff;
|
|
}
|
|
|
|
.command {
|
|
width: 100%;
|
|
}
|
|
|
|
#submit-button {
|
|
border: 0 solid #e5e7eb;
|
|
margin: 0;
|
|
text-transform: none;
|
|
background-image: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
width: 100%;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: .25rem;
|
|
padding: .5rem 1.5rem;
|
|
font-size: .875rem;
|
|
line-height: 1.25rem;
|
|
color: #fff;
|
|
background-color: rgb(29 78 216);
|
|
}
|
|
|
|
#submit-button svg {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
|
|
.ml-4 {
|
|
margin-left: 1rem;
|
|
}
|
|
|
|
.connection-status {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
border-bottom-width: 1px;
|
|
padding: 0.5rem 1rem;
|
|
font-size: .75rem;
|
|
line-height: 1rem;
|
|
color: rgb(113 113 122);
|
|
align-items: center;
|
|
border-color: rgb(24 24 27);
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.badge {
|
|
width: 0.375rem;
|
|
height: 0.375rem;
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
.badge.green {
|
|
background-color: #22c55e;
|
|
}
|
|
|
|
.badge.orange {
|
|
background-color: #f59e0b;
|
|
}
|
|
|
|
.badge.red {
|
|
background-color: #ef4444;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="app">
|
|
<header>
|
|
<div class="buttons">
|
|
<div>
|
|
Buffer size:
|
|
<input type="text" id="buffer" class="rounded shadow" placeholder="Buffer size" value="1000">
|
|
</div>
|
|
<button class="rounded shadow" onclick="terminalClean()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M4 7l16 0" />
|
|
<path d="M10 11l0 6" />
|
|
<path d="M14 11l0 6" />
|
|
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
|
|
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
|
</svg>
|
|
</button>
|
|
<button class="rounded shadow" onclick="enableScroll=!enableScroll">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z" />
|
|
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" />
|
|
<path d="M8 11v-4a4 4 0 1 1 8 0v4" />
|
|
</svg>
|
|
</button>
|
|
<button class="rounded shadow" onclick="enableTimestamp=!enableTimestamp">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
|
<path d="M12 7v5l3 3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content"></div>
|
|
|
|
<footer>
|
|
<div class="command_container">
|
|
<form class="flex w-full items-center">
|
|
<input id="command" autocomplete="off" type="text" required="" class="command"
|
|
placeholder="Enter command here">
|
|
<div class="ml-4">
|
|
<button id="submit-button" type="submit">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
stroke-linejoin="round">
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M10 14l11 -11" />
|
|
<path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="connection-status"></div>
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
|
|
<script type="text/javascript">
|
|
let enableScroll = true;
|
|
let enableTimestamp = true;
|
|
|
|
let url = `ws://${window.location.hostname}/ws`;
|
|
let websocket;
|
|
let contentArea = document.querySelector('.content');
|
|
let connectionStatus = document.querySelector('.connection-status');
|
|
|
|
initApp();
|
|
|
|
function initApp() {
|
|
connectionStatus.innerHTML = '';
|
|
initWebSocket();
|
|
|
|
document.querySelector('form').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
let command = document.getElementById('command').value;
|
|
websocket.send(command);
|
|
document.getElementById('command').value = '';
|
|
});
|
|
}
|
|
|
|
function initWebSocket() {
|
|
connectionStatus.innerHTML = '<div class="badge orange"></div> Connecting...';
|
|
websocket = new WebSocket(url);
|
|
websocket.onopen = onOpen;
|
|
websocket.onclose = onClose;
|
|
websocket.onmessage = onMessage;
|
|
}
|
|
|
|
function onOpen(event) {
|
|
connectionStatus.innerHTML = '<div class="badge green"></div> Connected';
|
|
terminalWrite('Connected to ' + url);
|
|
}
|
|
|
|
function onClose(event) {
|
|
connectionStatus.innerHTML = '<div class="badge red"></div> Disconnected';
|
|
setTimeout(initWebSocket, 2000);
|
|
}
|
|
|
|
function onMessage(event) {
|
|
terminalWrite(event.data);
|
|
}
|
|
|
|
function terminalWrite(data) {
|
|
if (enableTimestamp) {
|
|
let now = new Date();
|
|
data = "[" + now.toLocaleTimeString() + "] " + data;
|
|
}
|
|
contentArea.innerHTML += '<p>' + data + '</p>';
|
|
if (enableScroll) {
|
|
contentArea.scrollTop = contentArea.scrollHeight;
|
|
}
|
|
|
|
// Limit buffer size to avoid memory issues in the browser
|
|
let bufferSize = parseInt(document.getElementById('buffer').value);
|
|
if (isNaN(bufferSize)) {
|
|
bufferSize = 1000;
|
|
}
|
|
|
|
let lines = contentArea.querySelectorAll('p');
|
|
if (lines.length > bufferSize) {
|
|
for (let i = 0; i < lines.length - bufferSize; i++) {
|
|
contentArea.removeChild(lines[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function terminalClean() {
|
|
contentArea.innerHTML = '';
|
|
}
|
|
</script>
|
|
|
|
</html> |