feat(editor): F4.6b the command line on a computer's desktop

A new oracle, TerminalWindowRun, types into Java's own command line
window: the window writes each line after its prompt, takes a blank
line, cls, reset and exit itself, keeps the lines for the arrow keys,
and keeps its text, history and working directory over a stop and a
start.

The room now holds a command line window per computer for everyone
(TerminalWindow), kept over stops; the protocol carries its lines and
the one way to read them (terminalScreen); the core goes on in the
working directory of the run before (continueIn). The desktop opens
programs in Java's frame, and the command line works with keyboard and
finger: grey on black, the prompt hidden while a command runs, history,
chips for the commands, Ctrl+C. The room's failure is now tested in the
browser.

Fixed: the room handed the engine the wall clock with fractions of a
millisecond, and a ping hung after its first timeout. The engine and
the room count whole milliseconds, as Java's clock does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 14:22:49 +00:00
co-authored by Claude Opus 5.5
parent dfd5ec03ce
commit ac48e969f4
23 changed files with 1255 additions and 47 deletions
+7 -10
View File
@@ -20,9 +20,10 @@ import { Palette, type PaletteKind } from './design/Palette.js';
import { RouterDialog } from './design/RouterDialog.js';
import { DeviceSheet } from './sheet/DeviceSheet.js';
import { DesktopWindow } from './desktop/DesktopWindow.js';
import type { DesktopProgram } from './desktop/programs.js';
import { useTranslate } from './i18n/i18n.js';
import { ModeBar } from './simulation/ModeBar.js';
import { useSimulation, type Simulation, type SimHostFactory } from './simulation/useSimulation.js';
import { useSimulation, type SimHostFactory } from './simulation/useSimulation.js';
/** The document's drawing, read anew after every change. */
export function useTopologyView(doc: Y.Doc): TopologyView {
@@ -79,6 +80,7 @@ interface OpenDesktop {
readonly x: number;
readonly y: number;
readonly z: number;
readonly program: DesktopProgram | null;
}
/** Brings a desktop to the top by its stacking, not its place in the page: moved in the page, it would lose the press on it. */
@@ -87,13 +89,6 @@ const toFront = (open: readonly OpenDesktop[], id: string): readonly OpenDesktop
return open.some((each) => each.id === id && each.z === top) ? open : open.map((each) => (each.id === id ? { ...each, z: top + 1 } : each));
};
/** A desktop that watches its computer's network while it is shown. */
function WatchedDesktop({ simulation, ...props }: Omit<Parameters<typeof DesktopWindow>[0], 'settings'> & { readonly simulation: Simulation }) {
const { watch } = simulation;
useEffect(() => watch([`node:${props.id}`]), [watch, props.id]);
return <DesktopWindow {...props} settings={simulation.nodes.get(props.id)} />;
}
export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly createSimHost?: SimHostFactory }) {
const t = useTranslate();
const view = useTopologyView(doc);
@@ -146,7 +141,7 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
setDesktops((open) => {
if (open.some((each) => each.id === id)) return toFront(open, id);
// Java stacks a desktop that does not fit beside the others a little further on.
return [...open, { id, x: 24 + 24 * open.length, y: 76 + 24 * open.length, z: Math.max(0, ...open.map((each) => each.z)) + 1 }];
return [...open, { id, x: 24 + 24 * open.length, y: 76 + 24 * open.length, z: Math.max(0, ...open.map((each) => each.z)) + 1, program: null }];
});
}, []);
@@ -305,7 +300,7 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
</div>
{simulating
? desktops.map((desktop) => (
<WatchedDesktop
<DesktopWindow
key={desktop.id}
simulation={simulation}
doc={doc}
@@ -314,6 +309,8 @@ export function Editor({ doc, createSimHost }: { readonly doc: Y.Doc; readonly c
x={desktop.x}
y={desktop.y}
z={desktop.z}
program={desktop.program}
onProgram={(program) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, program } : each)))}
onMove={(x, y) => setDesktops((open) => open.map((each) => (each.id === desktop.id ? { ...each, x, y } : each)))}
onFront={() => setDesktops((open) => toFront(open, desktop.id))}
onClose={() => setDesktops((open) => open.filter((each) => each.id !== desktop.id))}