lognal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/core/filter.d.ts +29 -0
- package/dist/core/filter.js +82 -0
- package/dist/core/layout/entry-lines.d.ts +16 -0
- package/dist/core/layout/entry-lines.js +120 -0
- package/dist/core/layout/layout.d.ts +152 -0
- package/dist/core/layout/layout.js +784 -0
- package/dist/core/layout/row-index.d.ts +33 -0
- package/dist/core/layout/row-index.js +99 -0
- package/dist/core/layout/types.d.ts +128 -0
- package/dist/core/layout/types.js +8 -0
- package/dist/core/store.d.ts +96 -0
- package/dist/core/store.js +204 -0
- package/dist/core/text/ansi.d.ts +20 -0
- package/dist/core/text/ansi.js +187 -0
- package/dist/core/text/graphemes.d.ts +17 -0
- package/dist/core/text/graphemes.js +70 -0
- package/dist/core/text/line-splitter.d.ts +18 -0
- package/dist/core/text/line-splitter.js +56 -0
- package/dist/core/text/measure.d.ts +10 -0
- package/dist/core/text/measure.js +38 -0
- package/dist/core/text/shape.d.ts +24 -0
- package/dist/core/text/shape.js +220 -0
- package/dist/core/text/unicode-width-data.d.ts +54 -0
- package/dist/core/text/unicode-width-data.js +227 -0
- package/dist/core/text/width.d.ts +20 -0
- package/dist/core/text/width.js +157 -0
- package/dist/core/text/wrap.d.ts +14 -0
- package/dist/core/text/wrap.js +94 -0
- package/dist/core/time.d.ts +10 -0
- package/dist/core/time.js +24 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +9 -0
- package/dist/core/value/preview.d.ts +16 -0
- package/dist/core/value/preview.js +207 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/lognal.css +589 -0
- package/dist/react/LogViewer.d.ts +32 -0
- package/dist/react/LogViewer.js +183 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +2 -0
- package/dist/renderer/canvas/canvas-renderer.d.ts +45 -0
- package/dist/renderer/canvas/canvas-renderer.js +464 -0
- package/dist/renderer/canvas/palette.d.ts +6 -0
- package/dist/renderer/canvas/palette.js +25 -0
- package/dist/renderer/theme.d.ts +3 -0
- package/dist/renderer/theme.js +52 -0
- package/dist/renderer/types.d.ts +82 -0
- package/dist/renderer/types.js +1 -0
- package/dist/sources/console/format.d.ts +32 -0
- package/dist/sources/console/format.js +189 -0
- package/dist/sources/console/hook.d.ts +27 -0
- package/dist/sources/console/hook.js +94 -0
- package/dist/sources/console/recorder.d.ts +41 -0
- package/dist/sources/console/recorder.js +239 -0
- package/dist/sources/console/snapshot.d.ts +22 -0
- package/dist/sources/console/snapshot.js +547 -0
- package/dist/sources/console/table.d.ts +9 -0
- package/dist/sources/console/table.js +87 -0
- package/dist/sources/text/encoding.d.ts +11 -0
- package/dist/sources/text/encoding.js +59 -0
- package/dist/sources/text/follow-file.d.ts +49 -0
- package/dist/sources/text/follow-file.js +109 -0
- package/dist/sources/text/read-file.d.ts +63 -0
- package/dist/sources/text/read-file.js +82 -0
- package/dist/viewer/icons.d.ts +14 -0
- package/dist/viewer/icons.js +30 -0
- package/dist/viewer/input-line.d.ts +46 -0
- package/dist/viewer/input-line.js +144 -0
- package/dist/viewer/labels.d.ts +35 -0
- package/dist/viewer/labels.js +61 -0
- package/dist/viewer/scrollbar.d.ts +27 -0
- package/dist/viewer/scrollbar.js +143 -0
- package/dist/viewer/theme.d.ts +15 -0
- package/dist/viewer/theme.js +105 -0
- package/dist/viewer/viewer.d.ts +217 -0
- package/dist/viewer/viewer.js +1201 -0
- package/package.json +92 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the number of rows of every visible entry, and answers which entry a row belongs to.
|
|
3
|
+
*
|
|
4
|
+
* Appending an entry and dropping entries from the front are cheap, which is what a log does
|
|
5
|
+
* almost all the time. Changing row counts in the middle marks the running totals after the
|
|
6
|
+
* first change as stale, and they are recomputed once, the next time a position is read, so
|
|
7
|
+
* many changes in a row cost one pass.
|
|
8
|
+
*/
|
|
9
|
+
export declare class RowIndex {
|
|
10
|
+
private counts;
|
|
11
|
+
/** `sums[i]` is the number of rows before item `i`. It has one more element than `counts`. */
|
|
12
|
+
private sums;
|
|
13
|
+
private start;
|
|
14
|
+
/** The first element of `sums` that is out of date, or `Infinity` when all are current. */
|
|
15
|
+
private staleFrom;
|
|
16
|
+
/** The number of items. */
|
|
17
|
+
get length(): number;
|
|
18
|
+
/** The number of rows of all items. */
|
|
19
|
+
get total(): number;
|
|
20
|
+
push(rows: number): void;
|
|
21
|
+
/** Removes items from the front. */
|
|
22
|
+
shift(count: number): void;
|
|
23
|
+
clear(): void;
|
|
24
|
+
/** Returns the number of rows of an item. */
|
|
25
|
+
get(index: number): number;
|
|
26
|
+
/** Changes the number of rows of an item. */
|
|
27
|
+
set(index: number, rows: number): void;
|
|
28
|
+
/** Returns the first row of an item. */
|
|
29
|
+
rowOf(index: number): number;
|
|
30
|
+
/** Returns the item that holds a row, or -1 when the row is past the end. */
|
|
31
|
+
find(row: number): number;
|
|
32
|
+
private refresh;
|
|
33
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** How many removed items the index tolerates at the front before it compacts its arrays. */
|
|
2
|
+
const COMPACT_THRESHOLD = 4096;
|
|
3
|
+
/**
|
|
4
|
+
* Keeps the number of rows of every visible entry, and answers which entry a row belongs to.
|
|
5
|
+
*
|
|
6
|
+
* Appending an entry and dropping entries from the front are cheap, which is what a log does
|
|
7
|
+
* almost all the time. Changing row counts in the middle marks the running totals after the
|
|
8
|
+
* first change as stale, and they are recomputed once, the next time a position is read, so
|
|
9
|
+
* many changes in a row cost one pass.
|
|
10
|
+
*/
|
|
11
|
+
export class RowIndex {
|
|
12
|
+
counts = [];
|
|
13
|
+
/** `sums[i]` is the number of rows before item `i`. It has one more element than `counts`. */
|
|
14
|
+
sums = [0];
|
|
15
|
+
start = 0;
|
|
16
|
+
/** The first element of `sums` that is out of date, or `Infinity` when all are current. */
|
|
17
|
+
staleFrom = Number.POSITIVE_INFINITY;
|
|
18
|
+
/** The number of items. */
|
|
19
|
+
get length() {
|
|
20
|
+
return this.counts.length - this.start;
|
|
21
|
+
}
|
|
22
|
+
/** The number of rows of all items. */
|
|
23
|
+
get total() {
|
|
24
|
+
this.refresh();
|
|
25
|
+
return this.sums[this.counts.length] - this.sums[this.start];
|
|
26
|
+
}
|
|
27
|
+
push(rows) {
|
|
28
|
+
this.refresh();
|
|
29
|
+
this.counts.push(rows);
|
|
30
|
+
this.sums.push(this.sums[this.sums.length - 1] + rows);
|
|
31
|
+
}
|
|
32
|
+
/** Removes items from the front. */
|
|
33
|
+
shift(count) {
|
|
34
|
+
this.refresh();
|
|
35
|
+
this.start = Math.min(this.counts.length, this.start + count);
|
|
36
|
+
if (this.start > COMPACT_THRESHOLD && this.start > this.counts.length / 2) {
|
|
37
|
+
const base = this.sums[this.start];
|
|
38
|
+
this.counts = this.counts.slice(this.start);
|
|
39
|
+
this.sums = this.sums.slice(this.start).map((sum) => sum - base);
|
|
40
|
+
this.start = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this.counts = [];
|
|
45
|
+
this.sums = [0];
|
|
46
|
+
this.start = 0;
|
|
47
|
+
this.staleFrom = Number.POSITIVE_INFINITY;
|
|
48
|
+
}
|
|
49
|
+
/** Returns the number of rows of an item. */
|
|
50
|
+
get(index) {
|
|
51
|
+
return this.counts[this.start + index] ?? 0;
|
|
52
|
+
}
|
|
53
|
+
/** Changes the number of rows of an item. */
|
|
54
|
+
set(index, rows) {
|
|
55
|
+
const position = this.start + index;
|
|
56
|
+
if (this.counts[position] === rows) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.counts[position] = rows;
|
|
60
|
+
this.staleFrom = Math.min(this.staleFrom, position + 1);
|
|
61
|
+
}
|
|
62
|
+
/** Returns the first row of an item. */
|
|
63
|
+
rowOf(index) {
|
|
64
|
+
this.refresh();
|
|
65
|
+
return this.sums[this.start + index] - this.sums[this.start];
|
|
66
|
+
}
|
|
67
|
+
/** Returns the item that holds a row, or -1 when the row is past the end. */
|
|
68
|
+
find(row) {
|
|
69
|
+
if (row < 0 || row >= this.total) {
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
const target = row + this.sums[this.start];
|
|
73
|
+
let low = this.start;
|
|
74
|
+
let high = this.counts.length - 1;
|
|
75
|
+
while (low < high) {
|
|
76
|
+
const middle = (low + high + 1) >> 1;
|
|
77
|
+
if (this.sums[middle] <= target) {
|
|
78
|
+
low = middle;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
high = middle - 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Skip items with no rows, which share their first row with the next item.
|
|
85
|
+
while (low < this.counts.length - 1 && this.counts[low] === 0) {
|
|
86
|
+
low++;
|
|
87
|
+
}
|
|
88
|
+
return low - this.start;
|
|
89
|
+
}
|
|
90
|
+
refresh() {
|
|
91
|
+
if (this.staleFrom === Number.POSITIVE_INFINITY) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (let index = this.staleFrom; index < this.sums.length; index++) {
|
|
95
|
+
this.sums[index] = this.sums[index - 1] + this.counts[index - 1];
|
|
96
|
+
}
|
|
97
|
+
this.staleFrom = Number.POSITIVE_INFINITY;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { LogEntry, StyleToken, TextStyle } from '../types.js';
|
|
2
|
+
/** What happens when a span is clicked. */
|
|
3
|
+
export type LineAction = {
|
|
4
|
+
type: 'toggle-value';
|
|
5
|
+
path: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'toggle-group';
|
|
8
|
+
};
|
|
9
|
+
/** A run of text on a logical line. */
|
|
10
|
+
export interface LineTextSpan {
|
|
11
|
+
text: string;
|
|
12
|
+
token?: StyleToken;
|
|
13
|
+
style?: TextStyle;
|
|
14
|
+
action?: LineAction;
|
|
15
|
+
}
|
|
16
|
+
/** A small drawn symbol that takes two cells, such as the triangle that expands a value. */
|
|
17
|
+
export interface LineIconSpan {
|
|
18
|
+
icon: 'expander';
|
|
19
|
+
expanded: boolean;
|
|
20
|
+
action: LineAction;
|
|
21
|
+
}
|
|
22
|
+
export type LineSpan = LineTextSpan | LineIconSpan;
|
|
23
|
+
/**
|
|
24
|
+
* One line of an entry before wrapping. An entry has one logical line per line of text, plus
|
|
25
|
+
* one for every visible row of an expanded value.
|
|
26
|
+
*/
|
|
27
|
+
export interface LogicalLine {
|
|
28
|
+
/** Cells of indentation before the content, repeated on every wrapped row. */
|
|
29
|
+
indent: number;
|
|
30
|
+
spans: LineSpan[];
|
|
31
|
+
/** `false` when the line holds a text part that must not wrap. */
|
|
32
|
+
wrap?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** How a cluster lets a wrapped row break next to it. */
|
|
35
|
+
export declare const BREAK_NORMAL = 0;
|
|
36
|
+
/** A space: a row may break after it. */
|
|
37
|
+
export declare const BREAK_SPACE = 1;
|
|
38
|
+
/** A wide character that allows breaks on both sides, such as a Han ideograph. */
|
|
39
|
+
export declare const BREAK_WIDE = 2;
|
|
40
|
+
/** A wide character that keeps words together, such as a Hangul syllable. */
|
|
41
|
+
export declare const BREAK_KEEP = 3;
|
|
42
|
+
/** A span after control characters and tabs were replaced for display. */
|
|
43
|
+
export interface ShapedSpan {
|
|
44
|
+
text: string;
|
|
45
|
+
token?: StyleToken;
|
|
46
|
+
style?: TextStyle;
|
|
47
|
+
action?: LineAction;
|
|
48
|
+
icon?: 'expander';
|
|
49
|
+
expanded?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A logical line split into grapheme clusters, each with its width in cells.
|
|
53
|
+
*
|
|
54
|
+
* Most log lines are plain ASCII. Such a line is kept as `text`, where every character is one
|
|
55
|
+
* cluster one cell wide, and the per-cluster arrays are left out to save memory.
|
|
56
|
+
*/
|
|
57
|
+
export interface ShapedLine {
|
|
58
|
+
indent: number;
|
|
59
|
+
spans: ShapedSpan[];
|
|
60
|
+
/** The index of the first cluster of every span. */
|
|
61
|
+
spanStarts: Uint32Array;
|
|
62
|
+
/** Whether every cluster is a single printable ASCII character. */
|
|
63
|
+
simple: boolean;
|
|
64
|
+
/** The whole line. For a simple line, character `i` is cluster `i`. */
|
|
65
|
+
text: string;
|
|
66
|
+
/** The clusters of a line that is not simple. */
|
|
67
|
+
clusters: string[] | null;
|
|
68
|
+
/** The width of every cluster of a line that is not simple. */
|
|
69
|
+
widths: Uint8Array | null;
|
|
70
|
+
/** One of the `BREAK_*` values for every cluster of a line that is not simple. */
|
|
71
|
+
breaks: Uint8Array | null;
|
|
72
|
+
/** The number of clusters. */
|
|
73
|
+
length: number;
|
|
74
|
+
/** Total width in cells, without the indentation. */
|
|
75
|
+
cells: number;
|
|
76
|
+
/** Whether the line may wrap. A line that holds a part with `wrap: false` never does. */
|
|
77
|
+
wrap: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** How long lines are handled. */
|
|
80
|
+
export type WrapMode = 'word' | 'char' | 'none';
|
|
81
|
+
/** A run of clusters on one visual row that share a span. */
|
|
82
|
+
export interface RowRun {
|
|
83
|
+
/** Column where the run starts, counted from the start of the content area. */
|
|
84
|
+
column: number;
|
|
85
|
+
/** Width in cells. */
|
|
86
|
+
cells: number;
|
|
87
|
+
/** The text of the run. */
|
|
88
|
+
text: string;
|
|
89
|
+
/** The clusters of the run, for runs that must be drawn one cluster at a time. */
|
|
90
|
+
clusters: string[];
|
|
91
|
+
/** The width of every cluster in `clusters`. */
|
|
92
|
+
widths: number[];
|
|
93
|
+
/** Whether the run is plain ASCII that can be drawn in one call. */
|
|
94
|
+
simple: boolean;
|
|
95
|
+
token?: StyleToken;
|
|
96
|
+
style?: TextStyle;
|
|
97
|
+
action?: LineAction;
|
|
98
|
+
icon?: 'expander';
|
|
99
|
+
expanded?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** One row of the screen. */
|
|
102
|
+
export interface VisualRow {
|
|
103
|
+
entry: LogEntry;
|
|
104
|
+
/** Index of the logical line within the entry. */
|
|
105
|
+
line: number;
|
|
106
|
+
/** Index of the row within the logical line. */
|
|
107
|
+
lineRow: number;
|
|
108
|
+
/** Index of the row within the entry. */
|
|
109
|
+
entryRow: number;
|
|
110
|
+
/** Whether this is the first row of the entry. */
|
|
111
|
+
first: boolean;
|
|
112
|
+
/** Whether this is the last row of the entry. */
|
|
113
|
+
last: boolean;
|
|
114
|
+
/** Cells of indentation before the first run. */
|
|
115
|
+
indent: number;
|
|
116
|
+
/** Cell offset of the row's first cluster within the logical line, without indentation. */
|
|
117
|
+
startCell: number;
|
|
118
|
+
/** Width of the row's content in cells, without indentation. */
|
|
119
|
+
cells: number;
|
|
120
|
+
runs: RowRun[];
|
|
121
|
+
}
|
|
122
|
+
/** A position in the text of an entry, stable across wrapping. */
|
|
123
|
+
export interface TextPosition {
|
|
124
|
+
entryId: number;
|
|
125
|
+
line: number;
|
|
126
|
+
/** Cell offset within the logical line, without indentation. */
|
|
127
|
+
cell: number;
|
|
128
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** How a cluster lets a wrapped row break next to it. */
|
|
2
|
+
export const BREAK_NORMAL = 0;
|
|
3
|
+
/** A space: a row may break after it. */
|
|
4
|
+
export const BREAK_SPACE = 1;
|
|
5
|
+
/** A wide character that allows breaks on both sides, such as a Han ideograph. */
|
|
6
|
+
export const BREAK_WIDE = 2;
|
|
7
|
+
/** A wide character that keeps words together, such as a Hangul syllable. */
|
|
8
|
+
export const BREAK_KEEP = 3;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AnsiParser } from './text/ansi.js';
|
|
2
|
+
import type { LogEntry, LogEntryInit, LogKind, LogLevel, StyleToken, TextStyle } from './types.js';
|
|
3
|
+
export interface LogStoreOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The most entries the store keeps. Once it is full, the oldest entry is dropped for every
|
|
6
|
+
* new one. Use `Infinity` to keep everything.
|
|
7
|
+
*/
|
|
8
|
+
maxEntries: number;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a message identical to the one before it increases that entry's repeat count
|
|
11
|
+
* instead of adding a new entry. Only entries of the `message` kind are merged, and only when
|
|
12
|
+
* every part is text or a value that cannot be expanded; an error is never merged.
|
|
13
|
+
*/
|
|
14
|
+
mergeRepeats: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_STORE_OPTIONS: LogStoreOptions;
|
|
17
|
+
/** A change to the contents of a store. */
|
|
18
|
+
export type StoreChange = {
|
|
19
|
+
type: 'append';
|
|
20
|
+
entries: readonly LogEntry[];
|
|
21
|
+
} | {
|
|
22
|
+
type: 'update';
|
|
23
|
+
entry: LogEntry;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'trim';
|
|
26
|
+
count: number;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'clear';
|
|
29
|
+
};
|
|
30
|
+
export type StoreListener = (change: StoreChange) => void;
|
|
31
|
+
/** Options for adding plain text. */
|
|
32
|
+
export interface WriteOptions {
|
|
33
|
+
level?: LogLevel;
|
|
34
|
+
kind?: LogKind;
|
|
35
|
+
time?: number;
|
|
36
|
+
groups?: readonly number[];
|
|
37
|
+
token?: StyleToken;
|
|
38
|
+
style?: TextStyle;
|
|
39
|
+
/** Set to `false` to keep every line on one row, for text such as a table. */
|
|
40
|
+
wrap?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether ANSI escape codes in the text are turned into styles. Pass a parser to keep the
|
|
43
|
+
* style running across several calls. Defaults to `false`.
|
|
44
|
+
*/
|
|
45
|
+
ansi?: boolean | AnsiParser;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Holds log entries in the order they were added.
|
|
49
|
+
*
|
|
50
|
+
* The store knows nothing about how entries are displayed, so one store can feed several
|
|
51
|
+
* viewers, or collect messages before any viewer exists.
|
|
52
|
+
*/
|
|
53
|
+
export declare class LogStore {
|
|
54
|
+
private options;
|
|
55
|
+
private items;
|
|
56
|
+
private start;
|
|
57
|
+
private nextId;
|
|
58
|
+
private oldestId;
|
|
59
|
+
private lastSignature;
|
|
60
|
+
private readonly listeners;
|
|
61
|
+
constructor(options?: Partial<LogStoreOptions>);
|
|
62
|
+
/** The number of entries held. */
|
|
63
|
+
get size(): number;
|
|
64
|
+
/** The id of the oldest entry held. When the store is empty, the id the next entry gets. */
|
|
65
|
+
get firstId(): number;
|
|
66
|
+
/** The id of the newest entry held, or `firstId - 1` when the store is empty. */
|
|
67
|
+
get lastId(): number;
|
|
68
|
+
getOptions(): Readonly<LogStoreOptions>;
|
|
69
|
+
setOptions(options: Partial<LogStoreOptions>): void;
|
|
70
|
+
/** Returns the entry at a position, where 0 is the oldest entry held. */
|
|
71
|
+
at(index: number): LogEntry | undefined;
|
|
72
|
+
/** Returns the entry with an id, if the store still holds it. */
|
|
73
|
+
get(id: number): LogEntry | undefined;
|
|
74
|
+
/** Returns every entry, oldest first. */
|
|
75
|
+
toArray(): LogEntry[];
|
|
76
|
+
[Symbol.iterator](): Iterator<LogEntry>;
|
|
77
|
+
/**
|
|
78
|
+
* Adds one entry or several, and returns the entries that were created. A message merged into
|
|
79
|
+
* the entry before it only raises that entry's `repeat` count and is not returned.
|
|
80
|
+
*/
|
|
81
|
+
append(init: LogEntryInit | readonly LogEntryInit[]): LogEntry[];
|
|
82
|
+
/** Adds text as one entry. Line breaks stay inside the entry. */
|
|
83
|
+
write(text: string, options?: WriteOptions): LogEntry | undefined;
|
|
84
|
+
/** Adds text as one entry per line. */
|
|
85
|
+
writeLines(text: string, options?: WriteOptions): LogEntry[];
|
|
86
|
+
/** Removes every entry. */
|
|
87
|
+
clear(): void;
|
|
88
|
+
/** Collapses or expands a group header and hides or shows its members. */
|
|
89
|
+
setCollapsed(id: number, collapsed: boolean): void;
|
|
90
|
+
/** Calls a listener for every change. Returns a function that removes the listener. */
|
|
91
|
+
subscribe(listener: StoreListener): () => void;
|
|
92
|
+
private initOf;
|
|
93
|
+
private textParts;
|
|
94
|
+
private trimToLimit;
|
|
95
|
+
private emit;
|
|
96
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AnsiParser } from './text/ansi.js';
|
|
2
|
+
import { splitLines } from './text/line-splitter.js';
|
|
3
|
+
export const DEFAULT_STORE_OPTIONS = {
|
|
4
|
+
maxEntries: 10000,
|
|
5
|
+
mergeRepeats: true
|
|
6
|
+
};
|
|
7
|
+
/** How many dropped slots the store tolerates before it compacts its array. */
|
|
8
|
+
const COMPACT_THRESHOLD = 4096;
|
|
9
|
+
const MERGE_SEPARATOR = String.fromCharCode(0);
|
|
10
|
+
/** Returns a string that is equal for two messages that should be merged, or `null`. */
|
|
11
|
+
const signatureOf = (init, level, kind) => {
|
|
12
|
+
if (kind !== 'message') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const pieces = [level, (init.groups ?? []).join(',')];
|
|
16
|
+
for (const part of init.parts) {
|
|
17
|
+
if (part.type === 'text') {
|
|
18
|
+
pieces.push(`t${part.wrap === false ? 'n' : ''}${part.text}${part.token ?? ''}${part.style ? JSON.stringify(part.style) : ''}`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const { value } = part;
|
|
22
|
+
if (value.children !== undefined || value.kind === 'error') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
pieces.push(`v${value.kind}:${value.value ?? ''}:${value.className ?? ''}`);
|
|
26
|
+
}
|
|
27
|
+
return pieces.join(MERGE_SEPARATOR);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Holds log entries in the order they were added.
|
|
31
|
+
*
|
|
32
|
+
* The store knows nothing about how entries are displayed, so one store can feed several
|
|
33
|
+
* viewers, or collect messages before any viewer exists.
|
|
34
|
+
*/
|
|
35
|
+
export class LogStore {
|
|
36
|
+
options;
|
|
37
|
+
items = [];
|
|
38
|
+
start = 0;
|
|
39
|
+
nextId = 1;
|
|
40
|
+
oldestId = 1;
|
|
41
|
+
lastSignature = null;
|
|
42
|
+
listeners = new Set();
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.options = { ...DEFAULT_STORE_OPTIONS, ...options };
|
|
45
|
+
}
|
|
46
|
+
/** The number of entries held. */
|
|
47
|
+
get size() {
|
|
48
|
+
return this.items.length - this.start;
|
|
49
|
+
}
|
|
50
|
+
/** The id of the oldest entry held. When the store is empty, the id the next entry gets. */
|
|
51
|
+
get firstId() {
|
|
52
|
+
return this.oldestId;
|
|
53
|
+
}
|
|
54
|
+
/** The id of the newest entry held, or `firstId - 1` when the store is empty. */
|
|
55
|
+
get lastId() {
|
|
56
|
+
return this.nextId - 1;
|
|
57
|
+
}
|
|
58
|
+
getOptions() {
|
|
59
|
+
return this.options;
|
|
60
|
+
}
|
|
61
|
+
setOptions(options) {
|
|
62
|
+
this.options = { ...this.options, ...options };
|
|
63
|
+
this.trimToLimit();
|
|
64
|
+
}
|
|
65
|
+
/** Returns the entry at a position, where 0 is the oldest entry held. */
|
|
66
|
+
at(index) {
|
|
67
|
+
if (index < 0 || index >= this.size) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return this.items[this.start + index];
|
|
71
|
+
}
|
|
72
|
+
/** Returns the entry with an id, if the store still holds it. */
|
|
73
|
+
get(id) {
|
|
74
|
+
return this.at(id - this.oldestId);
|
|
75
|
+
}
|
|
76
|
+
/** Returns every entry, oldest first. */
|
|
77
|
+
toArray() {
|
|
78
|
+
return this.items.slice(this.start);
|
|
79
|
+
}
|
|
80
|
+
[Symbol.iterator]() {
|
|
81
|
+
return this.toArray()[Symbol.iterator]();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Adds one entry or several, and returns the entries that were created. A message merged into
|
|
85
|
+
* the entry before it only raises that entry's `repeat` count and is not returned.
|
|
86
|
+
*/
|
|
87
|
+
append(init) {
|
|
88
|
+
const inits = Array.isArray(init) ? init : [init];
|
|
89
|
+
const created = [];
|
|
90
|
+
for (const item of inits) {
|
|
91
|
+
const level = item.level ?? 'log';
|
|
92
|
+
const kind = item.kind ?? 'message';
|
|
93
|
+
const signature = this.options.mergeRepeats ? signatureOf(item, level, kind) : null;
|
|
94
|
+
const last = this.at(this.size - 1);
|
|
95
|
+
if (signature !== null && last && signature === this.lastSignature) {
|
|
96
|
+
last.repeat++;
|
|
97
|
+
last.version++;
|
|
98
|
+
this.emit({ type: 'update', entry: last });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const entry = {
|
|
102
|
+
id: this.nextId++,
|
|
103
|
+
time: item.time ?? Date.now(),
|
|
104
|
+
level,
|
|
105
|
+
kind,
|
|
106
|
+
parts: item.parts,
|
|
107
|
+
groups: item.groups ?? [],
|
|
108
|
+
collapsed: item.collapsed ?? false,
|
|
109
|
+
repeat: 1,
|
|
110
|
+
version: 0
|
|
111
|
+
};
|
|
112
|
+
this.items.push(entry);
|
|
113
|
+
this.lastSignature = signature;
|
|
114
|
+
created.push(entry);
|
|
115
|
+
}
|
|
116
|
+
if (created.length > 0) {
|
|
117
|
+
this.emit({ type: 'append', entries: created });
|
|
118
|
+
this.trimToLimit();
|
|
119
|
+
}
|
|
120
|
+
return created;
|
|
121
|
+
}
|
|
122
|
+
/** Adds text as one entry. Line breaks stay inside the entry. */
|
|
123
|
+
write(text, options = {}) {
|
|
124
|
+
const parts = this.textParts(text, options);
|
|
125
|
+
return this.append({ ...this.initOf(options), parts })[0] ?? this.at(this.size - 1);
|
|
126
|
+
}
|
|
127
|
+
/** Adds text as one entry per line. */
|
|
128
|
+
writeLines(text, options = {}) {
|
|
129
|
+
const parser = options.ansi instanceof AnsiParser ? options.ansi : options.ansi ? new AnsiParser() : null;
|
|
130
|
+
const base = this.initOf(options);
|
|
131
|
+
return this.append(splitLines(text).map((line) => ({
|
|
132
|
+
...base,
|
|
133
|
+
parts: this.textParts(line, { ...options, ansi: parser ?? false })
|
|
134
|
+
})));
|
|
135
|
+
}
|
|
136
|
+
/** Removes every entry. */
|
|
137
|
+
clear() {
|
|
138
|
+
this.items = [];
|
|
139
|
+
this.start = 0;
|
|
140
|
+
this.oldestId = this.nextId;
|
|
141
|
+
this.lastSignature = null;
|
|
142
|
+
this.emit({ type: 'clear' });
|
|
143
|
+
}
|
|
144
|
+
/** Collapses or expands a group header and hides or shows its members. */
|
|
145
|
+
setCollapsed(id, collapsed) {
|
|
146
|
+
const entry = this.get(id);
|
|
147
|
+
if (!entry || entry.collapsed === collapsed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
entry.collapsed = collapsed;
|
|
151
|
+
entry.version++;
|
|
152
|
+
this.emit({ type: 'update', entry });
|
|
153
|
+
}
|
|
154
|
+
/** Calls a listener for every change. Returns a function that removes the listener. */
|
|
155
|
+
subscribe(listener) {
|
|
156
|
+
this.listeners.add(listener);
|
|
157
|
+
return () => {
|
|
158
|
+
this.listeners.delete(listener);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
initOf(options) {
|
|
162
|
+
return { level: options.level, kind: options.kind, time: options.time, groups: options.groups };
|
|
163
|
+
}
|
|
164
|
+
textParts(text, options) {
|
|
165
|
+
if (options.ansi) {
|
|
166
|
+
const parser = options.ansi instanceof AnsiParser ? options.ansi : new AnsiParser();
|
|
167
|
+
return parser.parse(text).map((part) => ({
|
|
168
|
+
...part,
|
|
169
|
+
token: part.token ?? options.token,
|
|
170
|
+
...(options.wrap === false ? { wrap: false } : {})
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text,
|
|
177
|
+
token: options.token,
|
|
178
|
+
style: options.style,
|
|
179
|
+
...(options.wrap === false ? { wrap: false } : {})
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
trimToLimit() {
|
|
184
|
+
const excess = this.size - this.options.maxEntries;
|
|
185
|
+
if (!(excess > 0)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.start += excess;
|
|
189
|
+
this.oldestId += excess;
|
|
190
|
+
if (this.start > COMPACT_THRESHOLD && this.start > this.items.length / 2) {
|
|
191
|
+
this.items = this.items.slice(this.start);
|
|
192
|
+
this.start = 0;
|
|
193
|
+
}
|
|
194
|
+
if (this.size === 0) {
|
|
195
|
+
this.lastSignature = null;
|
|
196
|
+
}
|
|
197
|
+
this.emit({ type: 'trim', count: excess });
|
|
198
|
+
}
|
|
199
|
+
emit(change) {
|
|
200
|
+
for (const listener of this.listeners) {
|
|
201
|
+
listener(change);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TextPart } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turns text with ANSI escape codes into styled parts.
|
|
4
|
+
*
|
|
5
|
+
* Select Graphic Rendition codes (colors, bold, italic, underline and so on) become styles.
|
|
6
|
+
* Every other escape sequence, such as cursor movement or an OSC hyperlink wrapper, is removed
|
|
7
|
+
* so it cannot show up as stray characters. The style carries over between calls, the way a
|
|
8
|
+
* terminal keeps it from one line to the next.
|
|
9
|
+
*/
|
|
10
|
+
export declare class AnsiParser {
|
|
11
|
+
private style;
|
|
12
|
+
/** Parses one piece of text, usually a line. */
|
|
13
|
+
parse(text: string): TextPart[];
|
|
14
|
+
/** Forgets the current style. */
|
|
15
|
+
reset(): void;
|
|
16
|
+
private createPart;
|
|
17
|
+
private applySgr;
|
|
18
|
+
}
|
|
19
|
+
/** Removes every ANSI escape sequence from text. */
|
|
20
|
+
export declare const stripAnsi: (text: string) => string;
|