Component demo · Machine state, sequences and sessions
Terminal
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Terminal Overview Demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.Terminal displays a stream of lines, such as an instrument session or an event log.</h2>
<p>
Lines arrive faster than they can be read, are kept to a fixed number and have to stay readable while the terminal scrolls. New lines are appended without re-rendering the existing ones, the ring buffer removes the oldest lines from the DOM so the page does not grow, and auto-scroll pauses while you scroll up to read and resumes when you scroll back to the end.
</p>
</section>
<section>
<h2>An SCPI session</h2>
<p>Type a command and press Enter. Try <code>*IDN?</code>, <code>MEAS:VOLT:DC?</code> or <code>SYST:ERR?</code>.</p>
<smart-terminal id="scpi" interactive prompt="SCPI> "></smart-terminal>
</section>
<section>
<h2>A live event log</h2>
<p>
The log runs at about 20 lines per second. Scroll up while it runs and the view stays in place. Scroll back to the bottom and the terminal follows the newest lines again.
</p>
<smart-terminal id="log"></smart-terminal>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="end">Jump to end</button>
<button type="button" id="clear">Clear</button>
<label>Filter <input type="text" id="filter" placeholder="text or /regex/" /></label>
<label>Timestamps
<select id="stamps">
<option value="none">none</option>
<option value="time" selected>time</option>
<option value="elapsed">elapsed</option>
</select>
</label>
<span id="stats" class="hint"></span>
</div>
</section>
<section>
<h2>ANSI colours</h2>
<p>The eight basic SGR foreground colour codes are supported, which is what bench instruments and serial devices emit.</p>
<smart-terminal id="ansi" ansi-colors></smart-terminal>
</section>
</div>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.terminal.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* A stand-in for a real instrument. The seam is the command event and write() - swap this
for a VISA bridge, a serial gateway or a WebSocket and nothing on the page changes. */
const SCPI = {
'*IDN?': 'KEITHLEY INSTRUMENTS,MODEL 2000,1234567,A20 /A02',
'*RST': null,
'MEAS:VOLT:DC?': function () {
return (4.9985 + Math.random() * 0.003).toFixed(6) + 'E+00';
},
'MEAS:CURR:DC?': function () {
return (0.10021 + Math.random() * 0.0004).toFixed(6) + 'E+00';
},
'SYST:ERR?': '0,"No error"',
'READ?': function () {
return (1.00023 + Math.random() * 0.0006).toFixed(6) + 'E+00';
}
};
const EVENTS = [
['PT-2101 discharge pressure 6.24 bar', 'info'],
['FT-2104 flow 428 L/min', 'info'],
['TT-2107 pre-heater outlet 68.9 °C', 'info'],
['P-2101A running, 55 kW', 'info'],
['LT-2201 feed tank 61.8 %', 'info'],
['Batch step 4 of 9 complete', 'success'],
['TT-2108 approaching high limit', 'warn'],
['CV-2140 position deviation 3.1 %', 'warn'],
['PS-4001 instrument air pressure low', 'error'],
['Recipe checksum verified', 'success']
];
window.onload = function () {
/* ── SCPI session ────────────────────────────────────────────────── */
const scpi = document.getElementById('scpi');
scpi.timestampFormat = 'elapsed';
scpi.write('Connected to GPIB0::16::INSTR', 'success');
function answerTo(command) {
const answer = SCPI[command.toUpperCase()];
/* The element echoes the command itself; the response is the host's job, because
the terminal does not know what is at the other end. */
if (answer === undefined) {
scpi.write('-113,"Undefined header; ' + command + '"', 'error');
return;
}
if (answer === null) {
scpi.write('OK', 'success');
return;
}
scpi.write(typeof answer === 'function' ? answer() : answer, 'response');
}
scpi.addEventListener('command', function (event) {
answerTo(event.detail.command.trim());
});
/* The session opens with the exchange a test engineer types first: identify, reset,
one reading, and the error queue. Each command is echoed with the prompt, as a
typed one would be. */
['*IDN?', '*RST', 'MEAS:VOLT:DC?', 'SYST:ERR?'].forEach(function (command, i) {
setTimeout(function () {
scpi.write(scpi.prompt + command, 'command');
answerTo(command);
}, 400 + i * 500);
});
setTimeout(function () {
scpi.write('Type a command and press Enter. Try READ? or MEAS:CURR:DC?', 'info');
}, 400 + 4 * 500);
/* ── Live event log ──────────────────────────────────────────────── */
const log = document.getElementById('log'),
stats = document.getElementById('stats');
log.maxLines = 400;
log.timestampFormat = 'time';
let tick = 0, timer = null;
function emit() {
const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
tick++;
log.write('[' + String(tick).padStart(5, '0') + '] ' + event[0], event[1]);
}
function run() {
timer = setInterval(emit, 50);
}
run();
log.addEventListener('lineAppended', function (event) {
stats.textContent = tick.toLocaleString() + ' emitted, ' + event.detail.length + ' held';
});
const pause = document.getElementById('pause');
pause.addEventListener('click', function () {
log.paused = !log.paused;
pause.textContent = log.paused ? 'Resume' : 'Pause';
});
document.getElementById('end').addEventListener('click', function () {
log.scrollToEnd();
});
document.getElementById('clear').addEventListener('click', function () {
log.clear();
});
document.getElementById('filter').addEventListener('input', function () {
log.filter = this.value;
});
document.getElementById('stamps').addEventListener('change', function () {
log.timestampFormat = this.value;
});
/* ── ANSI ────────────────────────────────────────────────────────── */
const ansi = document.getElementById('ansi'),
ESC = String.fromCharCode(27);
ansi.writeMany([
ESC + '[32mSELF-TEST PASSED' + ESC + '[0m',
ESC + '[1m' + ESC + '[33mWARNING' + ESC + '[0m calibration due in 14 days',
ESC + '[31mERROR' + ESC + '[0m channel 3 open circuit',
ESC + '[36mINFO' + ESC + '[0m firmware 2.14.0, serial 1234567',
'Plain text with no escape codes at all.'
], 'info');
};
styles.css
.demo-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.demo-actions label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }
.hint { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
#scpi { --smart-terminal-height: 220px; }
#ansi { --smart-terminal-height: 150px; }
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.Terminal displays a stream of lines, such as an instrument session or an event log.</h2>
<p>
Lines arrive faster than they can be read, are kept to a fixed number and have to stay readable while the terminal scrolls. New lines are appended without re-rendering the existing ones, the ring buffer removes the oldest lines from the DOM so the page does not grow, and auto-scroll pauses while you scroll up to read and resumes when you scroll back to the end.
</p>
</section>
<section>
<h2>An SCPI session</h2>
<p>Type a command and press Enter. Try <code>*IDN?</code>, <code>MEAS:VOLT:DC?</code> or <code>SYST:ERR?</code>.</p>
<smart-terminal id="scpi" [interactive]="true" prompt="SCPI> "></smart-terminal>
</section>
<section>
<h2>A live event log</h2>
<p>
The log runs at about 20 lines per second. Scroll up while it runs and the view stays in place. Scroll back to the bottom and the terminal follows the newest lines again.
</p>
<smart-terminal id="log"></smart-terminal>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="end">Jump to end</button>
<button type="button" id="clear">Clear</button>
<label>Filter <input type="text" id="filter" placeholder="text or /regex/" /></label>
<label>Timestamps
<select id="stamps">
<option value="none">none</option>
<option value="time" selected>time</option>
<option value="elapsed">elapsed</option>
</select>
</label>
<span id="stats" class="hint"></span>
</div>
</section>
<section>
<h2>ANSI colours</h2>
<p>The eight basic SGR foreground colour codes are supported, which is what bench instruments and serial devices emit.</p>
<smart-terminal id="ansi" [ansiColors]="true"></smart-terminal>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { TerminalModule } from 'smart-industrial/angular/terminal';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - Terminal in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [TerminalModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* A stand-in for a real instrument. The seam is the command event and write() - swap this
for a VISA bridge, a serial gateway or a WebSocket and nothing on the page changes. */
const SCPI = {
'*IDN?': 'KEITHLEY INSTRUMENTS,MODEL 2000,1234567,A20 /A02',
'*RST': null,
'MEAS:VOLT:DC?': function () {
return (4.9985 + Math.random() * 0.003).toFixed(6) + 'E+00';
},
'MEAS:CURR:DC?': function () {
return (0.10021 + Math.random() * 0.0004).toFixed(6) + 'E+00';
},
'SYST:ERR?': '0,"No error"',
'READ?': function () {
return (1.00023 + Math.random() * 0.0006).toFixed(6) + 'E+00';
}
};
const EVENTS = [
['PT-2101 discharge pressure 6.24 bar', 'info'],
['FT-2104 flow 428 L/min', 'info'],
['TT-2107 pre-heater outlet 68.9 °C', 'info'],
['P-2101A running, 55 kW', 'info'],
['LT-2201 feed tank 61.8 %', 'info'],
['Batch step 4 of 9 complete', 'success'],
['TT-2108 approaching high limit', 'warn'],
['CV-2140 position deviation 3.1 %', 'warn'],
['PS-4001 instrument air pressure low', 'error'],
['Recipe checksum verified', 'success']
];
export function runDemo(): void {
/* ── SCPI session ────────────────────────────────────────────────── */
const scpi = document.getElementById('scpi');
scpi.timestampFormat = 'elapsed';
scpi.write('Connected to GPIB0::16::INSTR', 'success');
function answerTo(command) {
const answer = SCPI[command.toUpperCase()];
/* The element echoes the command itself; the response is the host's job, because
the terminal does not know what is at the other end. */
if (answer === undefined) {
scpi.write('-113,"Undefined header; ' + command + '"', 'error');
return;
}
if (answer === null) {
scpi.write('OK', 'success');
return;
}
scpi.write(typeof answer === 'function' ? answer() : answer, 'response');
}
scpi.addEventListener('command', function (event) {
answerTo(event.detail.command.trim());
});
/* The session opens with the exchange a test engineer types first: identify, reset,
one reading, and the error queue. Each command is echoed with the prompt, as a
typed one would be. */
['*IDN?', '*RST', 'MEAS:VOLT:DC?', 'SYST:ERR?'].forEach(function (command, i) {
setTimeout(function () {
scpi.write(scpi.prompt + command, 'command');
answerTo(command);
}, 400 + i * 500);
});
setTimeout(function () {
scpi.write('Type a command and press Enter. Try READ? or MEAS:CURR:DC?', 'info');
}, 400 + 4 * 500);
/* ── Live event log ──────────────────────────────────────────────── */
const log = document.getElementById('log'),
stats = document.getElementById('stats');
log.maxLines = 400;
log.timestampFormat = 'time';
let tick = 0, timer = null;
function emit() {
const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
tick++;
log.write('[' + String(tick).padStart(5, '0') + '] ' + event[0], event[1]);
}
function run() {
timer = setInterval(emit, 50);
}
run();
log.addEventListener('lineAppended', function (event) {
stats.textContent = tick.toLocaleString() + ' emitted, ' + event.detail.length + ' held';
});
const pause = document.getElementById('pause');
pause.addEventListener('click', function () {
log.paused = !log.paused;
pause.textContent = log.paused ? 'Resume' : 'Pause';
});
document.getElementById('end').addEventListener('click', function () {
log.scrollToEnd();
});
document.getElementById('clear').addEventListener('click', function () {
log.clear();
});
document.getElementById('filter').addEventListener('input', function () {
log.filter = this.value;
});
document.getElementById('stamps').addEventListener('change', function () {
log.timestampFormat = this.value;
});
/* ── ANSI ────────────────────────────────────────────────────────── */
const ansi = document.getElementById('ansi'),
ESC = String.fromCharCode(27);
ansi.writeMany([
ESC + '[32mSELF-TEST PASSED' + ESC + '[0m',
ESC + '[1m' + ESC + '[33mWARNING' + ESC + '[0m calibration due in 14 days',
ESC + '[31mERROR' + ESC + '[0m channel 3 open circuit',
ESC + '[36mINFO' + ESC + '[0m firmware 2.14.0, serial 1234567',
'Plain text with no escape codes at all.'
], 'info');
}
angular/app.component.css
.demo-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.demo-actions label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }
.hint { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
#scpi { --smart-terminal-height: 220px; }
#ansi { --smart-terminal-height: 150px; }
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { Terminal } from 'smart-industrial/react/terminal';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* A stand-in for a real instrument. The seam is the command event and write() - swap this
for a VISA bridge, a serial gateway or a WebSocket and nothing on the page changes. */
const SCPI = {
'*IDN?': 'KEITHLEY INSTRUMENTS,MODEL 2000,1234567,A20 /A02',
'*RST': null,
'MEAS:VOLT:DC?': function () {
return (4.9985 + Math.random() * 0.003).toFixed(6) + 'E+00';
},
'MEAS:CURR:DC?': function () {
return (0.10021 + Math.random() * 0.0004).toFixed(6) + 'E+00';
},
'SYST:ERR?': '0,"No error"',
'READ?': function () {
return (1.00023 + Math.random() * 0.0006).toFixed(6) + 'E+00';
}
};
const EVENTS = [
['PT-2101 discharge pressure 6.24 bar', 'info'],
['FT-2104 flow 428 L/min', 'info'],
['TT-2107 pre-heater outlet 68.9 °C', 'info'],
['P-2101A running, 55 kW', 'info'],
['LT-2201 feed tank 61.8 %', 'info'],
['Batch step 4 of 9 complete', 'success'],
['TT-2108 approaching high limit', 'warn'],
['CV-2140 position deviation 3.1 %', 'warn'],
['PS-4001 instrument air pressure low', 'error'],
['Recipe checksum verified', 'success']
];
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
/* ── SCPI session ────────────────────────────────────────────────── */
const scpi = document.getElementById('scpi');
scpi.timestampFormat = 'elapsed';
scpi.write('Connected to GPIB0::16::INSTR', 'success');
function answerTo(command) {
const answer = SCPI[command.toUpperCase()];
/* The element echoes the command itself; the response is the host's job, because
the terminal does not know what is at the other end. */
if (answer === undefined) {
scpi.write('-113,"Undefined header; ' + command + '"', 'error');
return;
}
if (answer === null) {
scpi.write('OK', 'success');
return;
}
scpi.write(typeof answer === 'function' ? answer() : answer, 'response');
}
scpi.addEventListener('command', function (event) {
answerTo(event.detail.command.trim());
});
/* The session opens with the exchange a test engineer types first: identify, reset,
one reading, and the error queue. Each command is echoed with the prompt, as a
typed one would be. */
['*IDN?', '*RST', 'MEAS:VOLT:DC?', 'SYST:ERR?'].forEach(function (command, i) {
setTimeout(function () {
scpi.write(scpi.prompt + command, 'command');
answerTo(command);
}, 400 + i * 500);
});
setTimeout(function () {
scpi.write('Type a command and press Enter. Try READ? or MEAS:CURR:DC?', 'info');
}, 400 + 4 * 500);
/* ── Live event log ──────────────────────────────────────────────── */
const log = document.getElementById('log'),
stats = document.getElementById('stats');
log.maxLines = 400;
log.timestampFormat = 'time';
let tick = 0, timer = null;
function emit() {
const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
tick++;
log.write('[' + String(tick).padStart(5, '0') + '] ' + event[0], event[1]);
}
function run() {
timer = setInterval(emit, 50);
}
run();
log.addEventListener('lineAppended', function (event) {
stats.textContent = tick.toLocaleString() + ' emitted, ' + event.detail.length + ' held';
});
const pause = document.getElementById('pause');
pause.addEventListener('click', function () {
log.paused = !log.paused;
pause.textContent = log.paused ? 'Resume' : 'Pause';
});
document.getElementById('end').addEventListener('click', function () {
log.scrollToEnd();
});
document.getElementById('clear').addEventListener('click', function () {
log.clear();
});
document.getElementById('filter').addEventListener('input', function () {
log.filter = this.value;
});
document.getElementById('stamps').addEventListener('change', function () {
log.timestampFormat = this.value;
});
/* ── ANSI ────────────────────────────────────────────────────────── */
const ansi = document.getElementById('ansi'),
ESC = String.fromCharCode(27);
ansi.writeMany([
ESC + '[32mSELF-TEST PASSED' + ESC + '[0m',
ESC + '[1m' + ESC + '[33mWARNING' + ESC + '[0m calibration due in 14 days',
ESC + '[31mERROR' + ESC + '[0m channel 3 open circuit',
ESC + '[36mINFO' + ESC + '[0m firmware 2.14.0, serial 1234567',
'Plain text with no escape codes at all.'
], 'info');
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.Terminal displays a stream of lines, such as an instrument session or an event log.</h2>
<p>Lines arrive faster than they can be read, are kept to a fixed number and have to stay readable while the terminal scrolls. New lines are appended without re-rendering the existing ones, the ring buffer removes the oldest lines from the DOM so the page does not grow, and auto-scroll pauses while you scroll up to read and resumes when you scroll back to the end.</p>
</section>
<section>
<h2>An SCPI session</h2>
<p>Type a command and press Enter. Try <code>*IDN?</code>, <code>MEAS:VOLT:DC?</code> or <code>SYST:ERR?</code>.</p>
<Terminal id="scpi" interactive prompt="SCPI> "></Terminal>
</section>
<section>
<h2>A live event log</h2>
<p>The log runs at about 20 lines per second. Scroll up while it runs and the view stays in place. Scroll back to the bottom and the terminal follows the newest lines again.</p>
<Terminal id="log"></Terminal>
<div className="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="end">Jump to end</button>
<button type="button" id="clear">Clear</button>
<label>Filter <input type="text" id="filter" placeholder="text or /regex/" /></label>
<label>
Timestamps
<select id="stamps" defaultValue="time"><option value="none">none</option> <option value="time">time</option> <option value="elapsed">elapsed</option></select>
</label>
<span id="stats" className="hint"></span>
</div>
</section>
<section>
<h2>ANSI colours</h2>
<p>The eight basic SGR foreground colour codes are supported, which is what bench instruments and serial devices emit.</p>
<Terminal id="ansi" ansiColors></Terminal>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.demo-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.demo-actions label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }
.hint { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
#scpi { --smart-terminal-height: 220px; }
#ansi { --smart-terminal-height: 150px; }
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.Terminal displays a stream of lines, such as an instrument session or an event log.</h2>
<p>
Lines arrive faster than they can be read, are kept to a fixed number and have to stay readable while the terminal scrolls. New lines are appended without re-rendering the existing ones, the ring buffer removes the oldest lines from the DOM so the page does not grow, and auto-scroll pauses while you scroll up to read and resumes when you scroll back to the end.
</p>
</section>
<section>
<h2>An SCPI session</h2>
<p>Type a command and press Enter. Try <code>*IDN?</code>, <code>MEAS:VOLT:DC?</code> or <code>SYST:ERR?</code>.</p>
<smart-terminal id="scpi" interactive prompt="SCPI> "></smart-terminal>
</section>
<section>
<h2>A live event log</h2>
<p>
The log runs at about 20 lines per second. Scroll up while it runs and the view stays in place. Scroll back to the bottom and the terminal follows the newest lines again.
</p>
<smart-terminal id="log"></smart-terminal>
<div class="demo-actions">
<button type="button" id="pause">Pause</button>
<button type="button" id="end">Jump to end</button>
<button type="button" id="clear">Clear</button>
<label>Filter <input type="text" id="filter" placeholder="text or /regex/" /></label>
<label>Timestamps
<select id="stamps">
<option value="none">none</option>
<option value="time" selected>time</option>
<option value="elapsed">elapsed</option>
</select>
</label>
<span id="stats" class="hint"></span>
</div>
</section>
<section>
<h2>ANSI colours</h2>
<p>The eight basic SGR foreground colour codes are supported, which is what bench instruments and serial devices emit.</p>
<smart-terminal id="ansi" ansi-colors></smart-terminal>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.terminal.js";
/* A stand-in for a real instrument. The seam is the command event and write() - swap this
for a VISA bridge, a serial gateway or a WebSocket and nothing on the page changes. */
const SCPI = {
'*IDN?': 'KEITHLEY INSTRUMENTS,MODEL 2000,1234567,A20 /A02',
'*RST': null,
'MEAS:VOLT:DC?': function () {
return (4.9985 + Math.random() * 0.003).toFixed(6) + 'E+00';
},
'MEAS:CURR:DC?': function () {
return (0.10021 + Math.random() * 0.0004).toFixed(6) + 'E+00';
},
'SYST:ERR?': '0,"No error"',
'READ?': function () {
return (1.00023 + Math.random() * 0.0006).toFixed(6) + 'E+00';
}
};
const EVENTS = [
['PT-2101 discharge pressure 6.24 bar', 'info'],
['FT-2104 flow 428 L/min', 'info'],
['TT-2107 pre-heater outlet 68.9 °C', 'info'],
['P-2101A running, 55 kW', 'info'],
['LT-2201 feed tank 61.8 %', 'info'],
['Batch step 4 of 9 complete', 'success'],
['TT-2108 approaching high limit', 'warn'],
['CV-2140 position deviation 3.1 %', 'warn'],
['PS-4001 instrument air pressure low', 'error'],
['Recipe checksum verified', 'success']
];
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
/* ── SCPI session ────────────────────────────────────────────────── */
const scpi = document.getElementById('scpi');
scpi.timestampFormat = 'elapsed';
scpi.write('Connected to GPIB0::16::INSTR', 'success');
function answerTo(command) {
const answer = SCPI[command.toUpperCase()];
/* The element echoes the command itself; the response is the host's job, because
the terminal does not know what is at the other end. */
if (answer === undefined) {
scpi.write('-113,"Undefined header; ' + command + '"', 'error');
return;
}
if (answer === null) {
scpi.write('OK', 'success');
return;
}
scpi.write(typeof answer === 'function' ? answer() : answer, 'response');
}
scpi.addEventListener('command', function (event) {
answerTo(event.detail.command.trim());
});
/* The session opens with the exchange a test engineer types first: identify, reset,
one reading, and the error queue. Each command is echoed with the prompt, as a
typed one would be. */
['*IDN?', '*RST', 'MEAS:VOLT:DC?', 'SYST:ERR?'].forEach(function (command, i) {
setTimeout(function () {
scpi.write(scpi.prompt + command, 'command');
answerTo(command);
}, 400 + i * 500);
});
setTimeout(function () {
scpi.write('Type a command and press Enter. Try READ? or MEAS:CURR:DC?', 'info');
}, 400 + 4 * 500);
/* ── Live event log ──────────────────────────────────────────────── */
const log = document.getElementById('log'),
stats = document.getElementById('stats');
log.maxLines = 400;
log.timestampFormat = 'time';
let tick = 0, timer = null;
function emit() {
const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
tick++;
log.write('[' + String(tick).padStart(5, '0') + '] ' + event[0], event[1]);
}
function run() {
timer = setInterval(emit, 50);
}
run();
log.addEventListener('lineAppended', function (event) {
stats.textContent = tick.toLocaleString() + ' emitted, ' + event.detail.length + ' held';
});
const pause = document.getElementById('pause');
pause.addEventListener('click', function () {
log.paused = !log.paused;
pause.textContent = log.paused ? 'Resume' : 'Pause';
});
document.getElementById('end').addEventListener('click', function () {
log.scrollToEnd();
});
document.getElementById('clear').addEventListener('click', function () {
log.clear();
});
document.getElementById('filter').addEventListener('input', function () {
log.filter = this.value;
});
document.getElementById('stamps').addEventListener('change', function () {
log.timestampFormat = this.value;
});
/* ── ANSI ────────────────────────────────────────────────────────── */
const ansi = document.getElementById('ansi'),
ESC = String.fromCharCode(27);
ansi.writeMany([
ESC + '[32mSELF-TEST PASSED' + ESC + '[0m',
ESC + '[1m' + ESC + '[33mWARNING' + ESC + '[0m calibration due in 14 days',
ESC + '[31mERROR' + ESC + '[0m channel 3 open circuit',
ESC + '[36mINFO' + ESC + '[0m firmware 2.14.0, serial 1234567',
'Plain text with no escape codes at all.'
], 'info');
});
});
}
};
</script>
<style>
.demo-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.demo-actions label { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }
.hint { font-size: 12px; opacity: 0.75; font-variant-numeric: tabular-nums; }
#scpi { --smart-terminal-height: 220px; }
#ansi { --smart-terminal-height: 150px; }
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- An SCPI session
- A live event log
- ANSI colours