@pokit/tabs-core 0.0.1 → 0.0.2
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/package.json +7 -7
- package/src/constants/help-content.ts +49 -0
- package/src/constants/index.ts +10 -0
- package/src/constants/keyboard.ts +37 -0
- package/src/hooks/index.ts +20 -0
- package/src/hooks/use-keyboard-handler.ts +318 -0
- package/src/hooks/use-tabs-state.ts +150 -0
- package/src/process-manager.ts +235 -0
- package/src/ring-buffer.ts +338 -0
- package/src/state-reducer.ts +206 -0
- package/src/types.ts +106 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Manager for CLI Tabs
|
|
3
|
+
*
|
|
4
|
+
* Manages spawning, output buffering, and lifecycle of child processes.
|
|
5
|
+
* Framework-agnostic - provides callbacks for UI frameworks to consume.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
9
|
+
import type { TabStatus, TabProcess } from './types.js';
|
|
10
|
+
|
|
11
|
+
export const OUTPUT_BATCH_MS = 16;
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A tab specification for the tabbed terminal UI.
|
|
19
|
+
* This is the internal format used by adapters after task resolution.
|
|
20
|
+
*/
|
|
21
|
+
export type TabSpec = {
|
|
22
|
+
label: string;
|
|
23
|
+
exec: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ProcessManagerCallbacks = {
|
|
27
|
+
/** Called when new output lines are available for a tab */
|
|
28
|
+
onOutputUpdate: (index: number, lines: string[]) => void;
|
|
29
|
+
/** Called when a process status changes */
|
|
30
|
+
onStatusChange: (index: number, status: TabStatus, exitCode?: number) => void;
|
|
31
|
+
/** Called when a process encounters an error */
|
|
32
|
+
onError: (index: number, error: string) => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ProcessManagerOptions = {
|
|
36
|
+
/** Working directory for commands */
|
|
37
|
+
cwd: string;
|
|
38
|
+
/** Environment variables for commands */
|
|
39
|
+
env: Record<string, string | undefined>;
|
|
40
|
+
/** Callbacks for state updates */
|
|
41
|
+
callbacks: ProcessManagerCallbacks;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type OutputBuffer = {
|
|
45
|
+
lines: string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// ProcessManager Class
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Manages child processes for tabbed terminal UI.
|
|
54
|
+
*
|
|
55
|
+
* Features:
|
|
56
|
+
* - Batched output updates (16ms) to reduce render frequency
|
|
57
|
+
* - Process lifecycle management (start, restart, kill)
|
|
58
|
+
* - Automatic cleanup on destroy
|
|
59
|
+
*/
|
|
60
|
+
export class ProcessManager {
|
|
61
|
+
private items: TabSpec[];
|
|
62
|
+
private options: ProcessManagerOptions;
|
|
63
|
+
private processes: (ChildProcess | null)[] = [];
|
|
64
|
+
private outputBuffers: Map<number, OutputBuffer> = new Map();
|
|
65
|
+
private flushScheduled = false;
|
|
66
|
+
private destroyed = false;
|
|
67
|
+
|
|
68
|
+
constructor(items: TabSpec[], options: ProcessManagerOptions) {
|
|
69
|
+
this.items = items;
|
|
70
|
+
this.options = options;
|
|
71
|
+
this.processes = new Array(items.length).fill(null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get initial tab process states
|
|
76
|
+
*/
|
|
77
|
+
getInitialTabs(): TabProcess[] {
|
|
78
|
+
return this.items.map((item, i) => ({
|
|
79
|
+
id: `tab-${i}`,
|
|
80
|
+
label: item.label,
|
|
81
|
+
exec: item.exec,
|
|
82
|
+
output: [],
|
|
83
|
+
status: 'running' as const,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Start all processes
|
|
89
|
+
*/
|
|
90
|
+
start(): void {
|
|
91
|
+
if (this.destroyed) return;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
94
|
+
this.spawnProcess(i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Restart a specific process
|
|
100
|
+
*/
|
|
101
|
+
restart(index: number): void {
|
|
102
|
+
if (this.destroyed) return;
|
|
103
|
+
|
|
104
|
+
const item = this.items[index];
|
|
105
|
+
if (!item) return;
|
|
106
|
+
|
|
107
|
+
// Kill existing process
|
|
108
|
+
const existingProc = this.processes[index];
|
|
109
|
+
if (existingProc && !existingProc.killed) {
|
|
110
|
+
existingProc.kill('SIGTERM');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Clear output buffer
|
|
114
|
+
this.outputBuffers.delete(index);
|
|
115
|
+
|
|
116
|
+
// Notify UI to show "Restarting..."
|
|
117
|
+
this.options.callbacks.onOutputUpdate(index, ['Restarting...', '']);
|
|
118
|
+
this.options.callbacks.onStatusChange(index, 'running');
|
|
119
|
+
|
|
120
|
+
// Spawn new process after short delay
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
if (!this.destroyed) {
|
|
123
|
+
this.spawnProcess(index);
|
|
124
|
+
}
|
|
125
|
+
}, 100);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Kill a specific process
|
|
130
|
+
*/
|
|
131
|
+
kill(index: number): void {
|
|
132
|
+
const proc = this.processes[index];
|
|
133
|
+
if (proc && !proc.killed) {
|
|
134
|
+
proc.kill('SIGTERM');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.options.callbacks.onOutputUpdate(index, ['', 'Stopped']);
|
|
138
|
+
this.options.callbacks.onStatusChange(index, 'stopped');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Kill all processes
|
|
143
|
+
*/
|
|
144
|
+
killAll(): void {
|
|
145
|
+
for (const proc of this.processes) {
|
|
146
|
+
if (proc && !proc.killed) {
|
|
147
|
+
proc.kill('SIGTERM');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Destroy the manager and cleanup all resources
|
|
154
|
+
*/
|
|
155
|
+
destroy(): void {
|
|
156
|
+
this.destroyed = true;
|
|
157
|
+
this.killAll();
|
|
158
|
+
this.outputBuffers.clear();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ===========================================================================
|
|
162
|
+
// Private Methods
|
|
163
|
+
// ===========================================================================
|
|
164
|
+
|
|
165
|
+
private spawnProcess(index: number): void {
|
|
166
|
+
const item = this.items[index];
|
|
167
|
+
if (!item) return;
|
|
168
|
+
|
|
169
|
+
const proc = spawn('sh', ['-c', item.exec], {
|
|
170
|
+
cwd: this.options.cwd,
|
|
171
|
+
env: {
|
|
172
|
+
...this.options.env,
|
|
173
|
+
FORCE_COLOR: '1',
|
|
174
|
+
} as NodeJS.ProcessEnv,
|
|
175
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.processes[index] = proc;
|
|
179
|
+
|
|
180
|
+
const handleData = (data: Buffer) => this.appendOutput(index, data);
|
|
181
|
+
|
|
182
|
+
proc.stdout?.on('data', handleData);
|
|
183
|
+
proc.stderr?.on('data', handleData);
|
|
184
|
+
|
|
185
|
+
proc.on('close', (code) => {
|
|
186
|
+
this.flushOutput();
|
|
187
|
+
const status: TabStatus = code === 0 ? 'done' : 'error';
|
|
188
|
+
this.options.callbacks.onStatusChange(index, status, code ?? undefined);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
proc.on('error', (err) => {
|
|
192
|
+
this.options.callbacks.onError(index, err.message);
|
|
193
|
+
this.options.callbacks.onStatusChange(index, 'error');
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private appendOutput(index: number, data: Buffer): void {
|
|
198
|
+
const text = data.toString('utf-8');
|
|
199
|
+
const lines = text.split(/\r?\n/).filter((line, idx, arr) => {
|
|
200
|
+
return line || idx < arr.length - 1;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (lines.length === 0) return;
|
|
204
|
+
|
|
205
|
+
let buffer = this.outputBuffers.get(index);
|
|
206
|
+
if (!buffer) {
|
|
207
|
+
buffer = { lines: [] };
|
|
208
|
+
this.outputBuffers.set(index, buffer);
|
|
209
|
+
}
|
|
210
|
+
buffer.lines.push(...lines);
|
|
211
|
+
|
|
212
|
+
this.scheduleFlush();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private scheduleFlush(): void {
|
|
216
|
+
if (this.flushScheduled) return;
|
|
217
|
+
this.flushScheduled = true;
|
|
218
|
+
setTimeout(() => this.flushOutput(), OUTPUT_BATCH_MS);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private flushOutput(): void {
|
|
222
|
+
this.flushScheduled = false;
|
|
223
|
+
|
|
224
|
+
const buffersSnapshot = this.outputBuffers;
|
|
225
|
+
this.outputBuffers = new Map();
|
|
226
|
+
|
|
227
|
+
if (buffersSnapshot.size === 0) return;
|
|
228
|
+
|
|
229
|
+
for (const [index, buffer] of buffersSnapshot.entries()) {
|
|
230
|
+
if (buffer.lines.length > 0) {
|
|
231
|
+
this.options.callbacks.onOutputUpdate(index, buffer.lines);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring Buffer Implementation
|
|
3
|
+
*
|
|
4
|
+
* A fixed-capacity circular buffer with O(1) push operations.
|
|
5
|
+
* When full, oldest elements are automatically overwritten.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - O(1) push operations (no array shifting)
|
|
9
|
+
* - Configurable capacity with buffer pressure warnings
|
|
10
|
+
* - Track number of dropped items
|
|
11
|
+
* - Dropped line indicator support
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export type RingBufferOptions = {
|
|
19
|
+
/** Maximum number of items to store */
|
|
20
|
+
capacity: number;
|
|
21
|
+
/** Maximum length of each line (truncate longer lines) */
|
|
22
|
+
maxLineLength?: number;
|
|
23
|
+
/** Percentage (0-100) at which to trigger pressure warning */
|
|
24
|
+
warnAtPercentage?: number;
|
|
25
|
+
/** Callback when buffer pressure threshold is reached */
|
|
26
|
+
onPressure?: (usage: number) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// RingBuffer Class
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A circular buffer that overwrites oldest items when full.
|
|
35
|
+
* All operations are O(1) except toArray() which is O(n).
|
|
36
|
+
*/
|
|
37
|
+
export class RingBuffer<T> {
|
|
38
|
+
private buffer: (T | undefined)[];
|
|
39
|
+
private head = 0; // Points to next write position
|
|
40
|
+
private _length = 0;
|
|
41
|
+
private readonly capacity: number;
|
|
42
|
+
private readonly maxLineLength?: number;
|
|
43
|
+
private readonly warnAtPercentage: number;
|
|
44
|
+
private readonly onPressure?: (usage: number) => void;
|
|
45
|
+
private _droppedCount = 0;
|
|
46
|
+
private warningEmitted = false;
|
|
47
|
+
|
|
48
|
+
constructor(options: RingBufferOptions) {
|
|
49
|
+
this.capacity = options.capacity;
|
|
50
|
+
this.maxLineLength = options.maxLineLength;
|
|
51
|
+
this.warnAtPercentage = options.warnAtPercentage ?? 80;
|
|
52
|
+
this.onPressure = options.onPressure;
|
|
53
|
+
this.buffer = new Array(this.capacity);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Number of items currently in the buffer
|
|
58
|
+
*/
|
|
59
|
+
get length(): number {
|
|
60
|
+
return this._length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Number of items that have been dropped due to capacity limits
|
|
65
|
+
*/
|
|
66
|
+
get droppedCount(): number {
|
|
67
|
+
return this._droppedCount;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Current buffer usage as a percentage (0-100)
|
|
72
|
+
*/
|
|
73
|
+
get usagePercentage(): number {
|
|
74
|
+
return (this._length / this.capacity) * 100;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the buffer is at capacity
|
|
79
|
+
*/
|
|
80
|
+
get isFull(): boolean {
|
|
81
|
+
return this._length === this.capacity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Push an item into the buffer.
|
|
86
|
+
* If full, the oldest item is overwritten and droppedCount is incremented.
|
|
87
|
+
*
|
|
88
|
+
* @param item - The item to add
|
|
89
|
+
* @returns true if an item was dropped, false otherwise
|
|
90
|
+
*/
|
|
91
|
+
push(item: T): boolean {
|
|
92
|
+
let dropped = false;
|
|
93
|
+
|
|
94
|
+
// Truncate string items if maxLineLength is set
|
|
95
|
+
let processedItem = item;
|
|
96
|
+
if (this.maxLineLength && typeof item === 'string') {
|
|
97
|
+
processedItem =
|
|
98
|
+
item.length > this.maxLineLength
|
|
99
|
+
? ((item.slice(0, this.maxLineLength) + '…') as unknown as T)
|
|
100
|
+
: item;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if we're about to overwrite
|
|
104
|
+
if (this._length === this.capacity) {
|
|
105
|
+
this._droppedCount++;
|
|
106
|
+
dropped = true;
|
|
107
|
+
} else {
|
|
108
|
+
this._length++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Write to current head position
|
|
112
|
+
this.buffer[this.head] = processedItem;
|
|
113
|
+
this.head = (this.head + 1) % this.capacity;
|
|
114
|
+
|
|
115
|
+
// Check for buffer pressure warning
|
|
116
|
+
this.checkPressure();
|
|
117
|
+
|
|
118
|
+
return dropped;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Push multiple items into the buffer.
|
|
123
|
+
*
|
|
124
|
+
* @param items - Array of items to add
|
|
125
|
+
* @returns Number of items that were dropped
|
|
126
|
+
*/
|
|
127
|
+
pushMany(items: T[]): number {
|
|
128
|
+
let droppedThisCall = 0;
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
if (this.push(item)) {
|
|
131
|
+
droppedThisCall++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return droppedThisCall;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Clear all items from the buffer and reset dropped count.
|
|
139
|
+
*/
|
|
140
|
+
clear(): void {
|
|
141
|
+
this.buffer = new Array(this.capacity);
|
|
142
|
+
this.head = 0;
|
|
143
|
+
this._length = 0;
|
|
144
|
+
this._droppedCount = 0;
|
|
145
|
+
this.warningEmitted = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert buffer contents to an array in correct order (oldest to newest).
|
|
150
|
+
* This is O(n) where n is the number of items in the buffer.
|
|
151
|
+
*/
|
|
152
|
+
toArray(): T[] {
|
|
153
|
+
if (this._length === 0) return [];
|
|
154
|
+
|
|
155
|
+
const result: T[] = new Array(this._length);
|
|
156
|
+
|
|
157
|
+
if (this._length < this.capacity) {
|
|
158
|
+
// Buffer not full yet, items are from 0 to head-1
|
|
159
|
+
for (let i = 0; i < this._length; i++) {
|
|
160
|
+
result[i] = this.buffer[i] as T;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Buffer is full, read from head (oldest) to head-1 (newest)
|
|
164
|
+
for (let i = 0; i < this._length; i++) {
|
|
165
|
+
const index = (this.head + i) % this.capacity;
|
|
166
|
+
result[i] = this.buffer[index] as T;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the item at the specified index (0 = oldest).
|
|
175
|
+
*/
|
|
176
|
+
get(index: number): T | undefined {
|
|
177
|
+
if (index < 0 || index >= this._length) return undefined;
|
|
178
|
+
|
|
179
|
+
if (this._length < this.capacity) {
|
|
180
|
+
return this.buffer[index];
|
|
181
|
+
} else {
|
|
182
|
+
const actualIndex = (this.head + index) % this.capacity;
|
|
183
|
+
return this.buffer[actualIndex];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get a slice of items from the buffer.
|
|
189
|
+
*
|
|
190
|
+
* @param start - Start index (inclusive)
|
|
191
|
+
* @param end - End index (exclusive), defaults to length
|
|
192
|
+
*/
|
|
193
|
+
slice(start: number, end?: number): T[] {
|
|
194
|
+
const actualEnd = end ?? this._length;
|
|
195
|
+
const normalizedStart = Math.max(0, start);
|
|
196
|
+
const normalizedEnd = Math.min(this._length, actualEnd);
|
|
197
|
+
|
|
198
|
+
if (normalizedStart >= normalizedEnd) return [];
|
|
199
|
+
|
|
200
|
+
const result: T[] = [];
|
|
201
|
+
for (let i = normalizedStart; i < normalizedEnd; i++) {
|
|
202
|
+
const item = this.get(i);
|
|
203
|
+
if (item !== undefined) {
|
|
204
|
+
result.push(item);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reset the warning flag so it can be emitted again.
|
|
212
|
+
* Useful after displaying a warning to the user.
|
|
213
|
+
*/
|
|
214
|
+
resetWarning(): void {
|
|
215
|
+
this.warningEmitted = false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ===========================================================================
|
|
219
|
+
// Private Methods
|
|
220
|
+
// ===========================================================================
|
|
221
|
+
|
|
222
|
+
private checkPressure(): void {
|
|
223
|
+
if (this.warningEmitted || !this.onPressure) return;
|
|
224
|
+
|
|
225
|
+
const usage = this.usagePercentage;
|
|
226
|
+
if (usage >= this.warnAtPercentage) {
|
|
227
|
+
this.warningEmitted = true;
|
|
228
|
+
this.onPressure(usage);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Output Buffer with Dropped Line Indicator
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
export type OutputBufferOptions = {
|
|
238
|
+
/** Maximum number of lines to store */
|
|
239
|
+
maxLines?: number;
|
|
240
|
+
/** Maximum length of each line */
|
|
241
|
+
maxLineLength?: number;
|
|
242
|
+
/** Warning threshold percentage */
|
|
243
|
+
warnAtPercentage?: number;
|
|
244
|
+
/** Callback when pressure threshold is reached */
|
|
245
|
+
onPressure?: (tabId: string, usage: number) => void;
|
|
246
|
+
/** Tab identifier for pressure callback */
|
|
247
|
+
tabId?: string;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const DEFAULT_MAX_LINES = 10_000;
|
|
251
|
+
const DEFAULT_MAX_LINE_LENGTH = 5_000;
|
|
252
|
+
const DEFAULT_WARN_PERCENTAGE = 80;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Specialized output buffer for tab process output.
|
|
256
|
+
* Wraps RingBuffer with dropped line indicator support.
|
|
257
|
+
*/
|
|
258
|
+
export class OutputBuffer {
|
|
259
|
+
private buffer: RingBuffer<string>;
|
|
260
|
+
private readonly tabId: string;
|
|
261
|
+
private lastReportedDropped = 0;
|
|
262
|
+
|
|
263
|
+
constructor(options: OutputBufferOptions = {}) {
|
|
264
|
+
this.tabId = options.tabId ?? 'unknown';
|
|
265
|
+
this.buffer = new RingBuffer<string>({
|
|
266
|
+
capacity: options.maxLines ?? DEFAULT_MAX_LINES,
|
|
267
|
+
maxLineLength: options.maxLineLength ?? DEFAULT_MAX_LINE_LENGTH,
|
|
268
|
+
warnAtPercentage: options.warnAtPercentage ?? DEFAULT_WARN_PERCENTAGE,
|
|
269
|
+
onPressure: options.onPressure
|
|
270
|
+
? (usage) => options.onPressure!(this.tabId, usage)
|
|
271
|
+
: undefined,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get length(): number {
|
|
276
|
+
return this.buffer.length;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get droppedCount(): number {
|
|
280
|
+
return this.buffer.droppedCount;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get usagePercentage(): number {
|
|
284
|
+
return this.buffer.usagePercentage;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get isFull(): boolean {
|
|
288
|
+
return this.buffer.isFull;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Push lines into the buffer.
|
|
293
|
+
*/
|
|
294
|
+
push(...lines: string[]): void {
|
|
295
|
+
this.buffer.pushMany(lines);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Clear the buffer.
|
|
300
|
+
*/
|
|
301
|
+
clear(): void {
|
|
302
|
+
this.buffer.clear();
|
|
303
|
+
this.lastReportedDropped = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get all lines as an array with dropped line indicator if applicable.
|
|
308
|
+
* The dropped indicator is inserted at the beginning if lines were dropped.
|
|
309
|
+
*/
|
|
310
|
+
toArray(): string[] {
|
|
311
|
+
const lines = this.buffer.toArray();
|
|
312
|
+
const dropped = this.buffer.droppedCount;
|
|
313
|
+
|
|
314
|
+
if (dropped > 0 && dropped !== this.lastReportedDropped) {
|
|
315
|
+
this.lastReportedDropped = dropped;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (dropped > 0) {
|
|
319
|
+
return [`... (${dropped.toLocaleString()} lines dropped) ...`, ...lines];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get lines without the dropped indicator.
|
|
327
|
+
*/
|
|
328
|
+
toArrayRaw(): string[] {
|
|
329
|
+
return this.buffer.toArray();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Reset the warning flag.
|
|
334
|
+
*/
|
|
335
|
+
resetWarning(): void {
|
|
336
|
+
this.buffer.resetWarning();
|
|
337
|
+
}
|
|
338
|
+
}
|