feat: F4.7 the data exchange window, measured against Java's
One row per frame (its highest layer's), Java's detail tree, the table as Java's text file, "Tabellen löschen" for every card (clearCapture in the room), a tab per card with Java's title, Java's layer colours as tokens, the chosen frame and autoscroll per tab. New oracle ExchangeWindowRun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ export * from './runtime/speed.js';
|
||||
export * from './runtime/trace.js';
|
||||
export * from './runtime/capture.js';
|
||||
export * from './runtime/captureRows.js';
|
||||
export * from './runtime/captureWindow.js';
|
||||
export * from './runtime/viewers.js';
|
||||
export * from './runtime/threads.js';
|
||||
export * from './runtime/apps/program.js';
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// The data exchange window as Java FILIUS shows a card's frames
|
||||
// (filius.gui.nachrichtensicht, the AggregatedExchangeDialog that filius.ini
|
||||
// chooses; measured with tools/java-oracle's ExchangeWindowRun): a table with
|
||||
// one row per frame — the row of its highest layer, with addresses —, below
|
||||
// it the frame's details (MessageDetailsPanel, the default without the layer
|
||||
// visualization), and the table as a text file (AggregatedMessageTable
|
||||
// .writeToStream). The rows are captureRows'; the words are the viewer's.
|
||||
//
|
||||
// Departure from Java: an empty comment is exported empty, where Java writes
|
||||
// "null" (prepareDataArrays leaves the cell unset, String.format writes it).
|
||||
|
||||
import type { CaptureRow } from './captureRows.js';
|
||||
|
||||
/** The table's row of a frame: the row of its highest layer (AggregatedMessageTable.update). */
|
||||
export function exchangeRow(rows: readonly CaptureRow[]): CaptureRow {
|
||||
const row = rows.at(-1);
|
||||
if (row === undefined) throw new RangeError('A frame has at least its Ethernet row');
|
||||
return row;
|
||||
}
|
||||
|
||||
/** The words of the details (rp_lauscher_msg1–5, 7). */
|
||||
export interface CaptureDetailWords {
|
||||
readonly number: string;
|
||||
readonly time: string;
|
||||
readonly source: string;
|
||||
readonly destination: string;
|
||||
readonly protocol: string;
|
||||
readonly comment: string;
|
||||
}
|
||||
|
||||
/** A frame's details as Java's tree shows them, each node as its depth and text. */
|
||||
export interface CaptureDetailLine {
|
||||
readonly depth: number;
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
/** Java's String.format("%-15s", …). */
|
||||
const label = (text: string) => text.padEnd(15);
|
||||
|
||||
/**
|
||||
* MessageDetailsPanel.update: "<Nr.>: n / <Zeit>: t", then per layer (its
|
||||
* name) the source, destination and protocol that are not empty, and the
|
||||
* comment — of the application layer with its length in "Bytes" (Java's
|
||||
* String length) — on its own line below its label when it has a line break
|
||||
* or is longer than 60 characters. `rows` are the frame's layered rows
|
||||
* (ports only), `comments` their comments in words.
|
||||
*/
|
||||
export function captureDetailLines(
|
||||
rows: readonly CaptureRow[],
|
||||
time: string,
|
||||
words: CaptureDetailWords,
|
||||
layerName: (row: CaptureRow) => string,
|
||||
comment: (row: CaptureRow) => string,
|
||||
): CaptureDetailLine[] {
|
||||
const first = rows[0];
|
||||
if (first === undefined) return [];
|
||||
const lines: CaptureDetailLine[] = [{ depth: 0, text: `${words.number}: ${first.number} / ${words.time}: ${time}` }];
|
||||
for (const row of rows) {
|
||||
lines.push({ depth: 1, text: layerName(row) });
|
||||
if (row.source !== null && row.source !== '') lines.push({ depth: 2, text: label(`${words.source}: `) + row.source });
|
||||
if (row.destination !== null && row.destination !== '') lines.push({ depth: 2, text: label(`${words.destination}: `) + row.destination });
|
||||
if (row.protocol !== '') lines.push({ depth: 2, text: label(`${words.protocol}: `) + row.protocol });
|
||||
const text = comment(row);
|
||||
if (text === '') continue;
|
||||
const heading = `${words.comment}${row.layer === 'application' ? ` (${text.length} Bytes)` : ''}: `;
|
||||
if (text.includes('\n') || text.length > 60) {
|
||||
lines.push({ depth: 2, text: heading }, { depth: 3, text });
|
||||
} else {
|
||||
lines.push({ depth: 2, text: label(heading) + text });
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
const COMMENT_COLUMN = 6;
|
||||
const COMMENT_WIDTH = 40;
|
||||
const MAXIMUM_COMMENT_LINES = 5;
|
||||
|
||||
/**
|
||||
* The table as Java exports it: a frame of "=" around the header, then each
|
||||
* row — its comment broken into lines of at most 40 characters, at most five,
|
||||
* "..." for more — and a line of "-" after it; the number 10 characters
|
||||
* wide, the comment 40, every other cell 20, lines ending in CR LF. `rows`:
|
||||
* the seven cells of each row as the table shows them.
|
||||
*/
|
||||
export function exchangeExportText(header: readonly string[], rows: readonly (readonly string[])[]): string {
|
||||
const width = (column: number) => (column === COMMENT_COLUMN ? COMMENT_WIDTH : column === 0 ? 10 : 20);
|
||||
const line = (cells: readonly string[]) => `| ${header.map((_, column) => (cells[column] ?? '').padEnd(width(column))).join(' | ')} | \r\n`;
|
||||
const separator = `+${header.map((_, column) => '-'.repeat(width(column) + 2)).join('+')}+\r\n`;
|
||||
let text = separator.replaceAll('-', '=') + line(header) + separator.replaceAll('-', '=');
|
||||
for (const row of rows) {
|
||||
const remarks = splitRemark(row[COMMENT_COLUMN] ?? '', COMMENT_WIDTH);
|
||||
text += line(row.map((cell, column) => (column === COMMENT_COLUMN ? remarks[0]! : cell)));
|
||||
for (const remark of remarks.slice(1, MAXIMUM_COMMENT_LINES)) text += line(header.map((_, column) => (column === COMMENT_COLUMN ? remark : '')));
|
||||
if (remarks.length > MAXIMUM_COMMENT_LINES) text += line(header.map((_, column) => (column === COMMENT_COLUMN ? '...' : '')));
|
||||
text += separator;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/** StringUtils.normalizeSpace: whitespace runs as one space, none at the ends. */
|
||||
function normalizeSpace(text: string): string {
|
||||
return text.replace(/[\s\u00a0]+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/** AggregatedMessageTable.splitString: words onto lines of at most `maximum` characters, a longer word cut into pieces of it; no words, one empty line. */
|
||||
function splitRemark(text: string, maximum: number): string[] {
|
||||
const lines: string[] = [];
|
||||
let current = '';
|
||||
for (const token of normalizeSpace(text).split(' ')) {
|
||||
if (current.length + token.length <= maximum) {
|
||||
current += `${token} `;
|
||||
} else if (token.length > maximum) {
|
||||
if (current.length > 0) lines.push(current.trim());
|
||||
const pieces: string[] = [];
|
||||
for (let start = 0; start < token.length; start += maximum) pieces.push(token.slice(start, start + maximum));
|
||||
lines.push(...pieces.slice(0, -1));
|
||||
current = `${pieces.at(-1)!} `;
|
||||
} else {
|
||||
lines.push(current.trim());
|
||||
current = `${token} `;
|
||||
}
|
||||
}
|
||||
lines.push(current.trim());
|
||||
return lines;
|
||||
}
|
||||
Reference in New Issue
Block a user