@fusion.dev/helix-headless 0.212.2 → 0.213.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.
@@ -1,6 +1,21 @@
1
1
  import { type LogRun } from './ansi.js';
2
2
  /** How much a line matters, which a stylesheet draws and a screen reader may hear. */
3
- export type LogLevel = 'debug' | 'error' | 'info' | 'warn';
3
+ export type LogLevel = 'debug' | 'error' | 'info' | 'log' | 'warn';
4
+ /**
5
+ * The levels from the quietest to the loudest, which `level` on a log counts
6
+ * up from: `log` is a plain line, a console's `console.log`, below `info`.
7
+ */
8
+ export declare const LOG_LEVELS: readonly LogLevel[];
9
+ /** Whether a line at `level` shows under a log whose quietest level shown is `floor`. A line with no level is `info`. */
10
+ export declare const loudEnough: (level: LogLevel | undefined, floor: LogLevel | undefined) => boolean;
11
+ /**
12
+ * The words a screen reader hears before a line's text, or nothing where the
13
+ * text already starts with them: `console.error(error)` writes `Error: …`,
14
+ * which `Error:` before it would read twice. The words' closing colon is not
15
+ * needed, and a word that only starts the text's first word, such as `Error`
16
+ * before `Errors`, is still said.
17
+ */
18
+ export declare const levelSaid: (words: string | undefined, text: string) => string | undefined;
4
19
  /** Where a step of the run stands. */
5
20
  export type LogStatus = 'done' | 'failed' | 'pending' | 'running' | 'skipped';
6
21
  /** One line of output, as `lines` holds it and `parse` returns it. */
@@ -9,6 +24,11 @@ export interface LogLine {
9
24
  at?: number | string;
10
25
  /** The `id` of the group in `groups` the line belongs to. */
11
26
  group?: string;
27
+ /**
28
+ * How many times it was written in a row, where the log folds repeats with
29
+ * `collapse`. Absent is once.
30
+ */
31
+ count?: number;
12
32
  /** How much it matters. Absent is `info`. */
13
33
  level?: LogLevel;
14
34
  /** What was written, with any ANSI escape codes still in it. */
@@ -95,6 +115,8 @@ export declare class LogBuffer {
95
115
  dropped: number;
96
116
  /** The most finished lines kept, or 0 to keep them all. */
97
117
  limit: number;
118
+ /** Whether a line written again straight after itself folds into it, with a count. */
119
+ collapse: boolean;
98
120
  /** Reads each finished line of written text. */
99
121
  parse: LogParser | undefined;
100
122
  /** The steps `parse` opened, in order, replaced as a whole when one changes. */
@@ -119,8 +141,13 @@ export declare class LogBuffer {
119
141
  * lines it finished.
120
142
  */
121
143
  write(chunk: string): number;
122
- /** Replaces every line, starting a fresh buffer that keeps its `parse` and `limit`. */
144
+ /** Replaces every line, starting a fresh buffer that keeps its `parse`, `limit` and `collapse`. */
123
145
  replace(lines: readonly LogLine[]): void;
146
+ /**
147
+ * Adds one finished line as data, after the lines held, folding it where
148
+ * `collapse` says, and lets the oldest go past `limit`.
149
+ */
150
+ push(line: LogLine): void;
124
151
  /** Lets every line go. */
125
152
  clear(): void;
126
153
  /** The line still being written, or undefined where the last line ended. */
@@ -142,6 +169,12 @@ export declare class LogBuffer {
142
169
  * as a CI page folds a step that passed.
143
170
  */
144
171
  private closeStep;
172
+ /**
173
+ * Keeps a finished line, or folds it into the line before it where it
174
+ * repeats that line and `collapse` is set. The fold is a new object, so a
175
+ * log comparing lines sees it change, drawn in the runs of the first.
176
+ */
177
+ private add;
145
178
  /** Lets the oldest lines go past `limit`, all at once. */
146
179
  private trim;
147
180
  }
@@ -1,4 +1,20 @@
1
1
  import { parseAnsi } from './ansi.js';
2
+ export const LOG_LEVELS = ['debug', 'log', 'info', 'warn', 'error'];
3
+ export const loudEnough = (level, floor) => floor === undefined || LOG_LEVELS.indexOf(level ?? 'info') >= LOG_LEVELS.indexOf(floor);
4
+ export const levelSaid = (words, text) => {
5
+ const word = words?.trim().replace(/:$/, '').toLocaleLowerCase();
6
+ if (!word) {
7
+ return undefined;
8
+ }
9
+ const start = text.trimStart();
10
+ const repeats = start.slice(0, word.length).toLocaleLowerCase() === word &&
11
+ !/^[\p{L}\p{N}]/u.test(start.slice(word.length));
12
+ return repeats ? undefined : words;
13
+ };
14
+ const repeats = (line, last) => last !== undefined &&
15
+ last.text === line.text &&
16
+ last.level === line.level &&
17
+ last.group === line.group;
2
18
  export const stepDuration = (seconds) => {
3
19
  const whole = Math.max(0, Math.round(seconds));
4
20
  if (whole < 60) {
@@ -23,6 +39,7 @@ export class LogBuffer {
23
39
  this.lines = [];
24
40
  this.dropped = 0;
25
41
  this.limit = 0;
42
+ this.collapse = false;
26
43
  this.parse = undefined;
27
44
  this.steps = [];
28
45
  this.openStep = undefined;
@@ -60,7 +77,10 @@ export class LogBuffer {
60
77
  return finished;
61
78
  }
62
79
  replace(lines) {
63
- this.lines = [...lines];
80
+ this.lines = [];
81
+ for (const line of lines) {
82
+ this.add(line);
83
+ }
64
84
  this.dropped = 0;
65
85
  this.partial = '';
66
86
  this.returned = false;
@@ -72,6 +92,10 @@ export class LogBuffer {
72
92
  this.stepStarted = undefined;
73
93
  this.trim();
74
94
  }
95
+ push(line) {
96
+ this.add(line);
97
+ this.trim();
98
+ }
75
99
  clear() {
76
100
  this.replace([]);
77
101
  }
@@ -126,7 +150,7 @@ export class LogBuffer {
126
150
  this.stepEnded = at ?? this.stepEnded;
127
151
  }
128
152
  this.runs.set(line, line.text === raw ? flow.runs : parseAnsi(line.text, this.lineStyle).runs);
129
- this.lines.push(line);
153
+ this.add(line);
130
154
  }
131
155
  }
132
156
  this.lineStyle = flow.style;
@@ -166,6 +190,19 @@ export class LogBuffer {
166
190
  this.steps = this.steps.map((each, place) => (place === index ? closed : each));
167
191
  this.openStep = undefined;
168
192
  }
193
+ add(line) {
194
+ const last = this.lines.at(-1);
195
+ if (!this.collapse || !last || !repeats(line, last)) {
196
+ this.lines.push(line);
197
+ return;
198
+ }
199
+ const folded = { ...last, count: (last.count ?? 1) + (line.count ?? 1) };
200
+ const runs = this.runs.get(last);
201
+ if (runs) {
202
+ this.runs.set(folded, runs);
203
+ }
204
+ this.lines[this.lines.length - 1] = folded;
205
+ }
169
206
  trim() {
170
207
  const over = this.limit > 0 ? this.lines.length - this.limit : 0;
171
208
  if (over > 0) {
@@ -1,5 +1,5 @@
1
1
  export { type LogRun, type LogStyle, parseAnsi, stripAnsi } from './ansi.js';
2
- export type { LogGroup, LogLevel, LogLine, LogParsed, LogParser, LogStatus } from './buffer.js';
2
+ export { type LogGroup, type LogLevel, type LogLine, type LogParsed, type LogParser, type LogStatus, levelSaid, } from './buffer.js';
3
3
  export { HelixHeadlessLogGroup } from './group/log-group.js';
4
4
  export { HelixHeadlessLogLine, type LogTimes } from './line/log-line.js';
5
5
  export { parseGitHubActions, parseVercel } from './parsers.js';
package/dist/log/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { parseAnsi, stripAnsi } from './ansi.js';
2
+ export { levelSaid, } from './buffer.js';
2
3
  export { HelixHeadlessLogGroup } from './group/log-group.js';
3
4
  export { HelixHeadlessLogLine } from './line/log-line.js';
4
5
  export { parseGitHubActions, parseVercel } from './parsers.js';
@@ -13,8 +13,10 @@ export type LogTimes = 'absolute' | 'relative';
13
13
  * and tells it whether to show its number and its time.
14
14
  *
15
15
  * `level` says how much a line matters, and the styled tier draws `warn` and
16
- * `error` lines apart. The text is the line's own content, so you can mark it
17
- * up yourself.
16
+ * `error` lines apart. A line at `debug`, `warn` or `error` says its level in
17
+ * words before its text, so a reader moving through the log hears it as well
18
+ * as the color shows it. The text is the line's own content, so you can mark
19
+ * it up yourself.
18
20
  *
19
21
  * @since 0.200.0
20
22
  * @status experimental
@@ -22,6 +24,9 @@ export type LogTimes = 'absolute' | 'relative';
22
24
  *
23
25
  * @slot - The text of the line.
24
26
  *
27
+ * @csspart count - How many times a line written again straight after itself was written, where the log's `collapse` folds repeats. Hidden from screen readers, which hear `count-label`.
28
+ * @csspart count-label - What a screen reader hears for the count, such as "3 times". Visually hidden in the styled tier.
29
+ * @csspart level - What a screen reader hears before the text for a line at `debug`, `warn` or `error`, such as "Warning:". Visually hidden in the styled tier.
25
30
  * @csspart line - The row the number, the time, and the text sit in.
26
31
  * @csspart line-debug - The row of a line at `debug`.
27
32
  * @csspart line-error - The row of a line at `error`.
@@ -43,7 +48,7 @@ export type LogTimes = 'absolute' | 'relative';
43
48
  * @cssproperty --log-run-background-color - Written on a `run` with a background, in the same form as `--log-run-color`.
44
49
  * @cssproperty --log-run-color - Written on a `run` with a text color: one of the sixteen `--log-ansi-*` colors the log publishes, or the color itself.
45
50
  *
46
- * @cssstate filtered - The log's `query` does not match the line, so it draws nothing.
51
+ * @cssstate filtered - The log's `query` does not match the line, or its `level` is quieter than the log shows, so it draws nothing.
47
52
  */
48
53
  export declare class HelixHeadlessLogLine extends HelixElement {
49
54
  /** The key this family's words are registered under, its styled tag. */
@@ -52,8 +57,18 @@ export declare class HelixHeadlessLogLine extends HelixElement {
52
57
  at: number | string;
53
58
  /** How much the line matters. Absent is `info`. */
54
59
  level?: LogLevel;
60
+ /**
61
+ * What a screen reader hears before the text of a line at each level, for a
62
+ * page in another language, keyed by level, such as `{ warn: 'Warnung:' }`.
63
+ * A line at `info` or `log` hears nothing extra, and nor does a line whose
64
+ * text already starts with its words, such as `Error: Cannot find module`.
65
+ * As an attribute it is that object as JSON.
66
+ */
67
+ levelWords: Partial<Record<LogLevel, string>>;
55
68
  /** What a screen reader hears for the link on a line's number. `{number}` is the number. */
56
69
  lineLabel: string;
70
+ /** @internal Set by the log: how many times the line was written in a row, where `collapse` folds repeats. */
71
+ logCount: number;
57
72
  /** @internal Set by the log: whether the query hides the line. */
58
73
  logFiltered: boolean;
59
74
  /** @internal Set by the log: whether the line belongs to a step. */
@@ -80,11 +95,23 @@ export declare class HelixHeadlessLogLine extends HelixElement {
80
95
  logTimes: '' | LogTimes;
81
96
  /** @internal Set by the log: the text as styled runs, for a line drawn from data. */
82
97
  runs: readonly LogRun[] | undefined;
98
+ /**
99
+ * What a screen reader hears after a line written more than once in a row.
100
+ * `{count}` is how many times, in the locale.
101
+ */
102
+ timesLabel: string;
83
103
  /**
84
104
  * @internal Read by the log to find its lines among its children and inside
85
105
  * its groups, which is cheaper than `instanceof`.
86
106
  */
87
107
  readonly isLogLine = true;
108
+ /**
109
+ * How the text a page wrote starts, so a line whose text already says its
110
+ * level does not hear it twice. Read from the second update on, which a log
111
+ * asks for once it has hydrated, as the server has no children to read and
112
+ * the first render is the server's.
113
+ */
114
+ private written;
88
115
  protected willUpdate(changed: PropertyValues): void;
89
116
  /**
90
117
  * The number: a link where the log gave `line-href`, a button where the log
@@ -116,6 +143,10 @@ export declare class HelixHeadlessLogLine extends HelixElement {
116
143
  * ink.
117
144
  */
118
145
  private renderRuns;
146
+ /** The count of a folded line, shown as a number and heard as words. */
147
+ private renderCount;
148
+ /** Reads the new text where the line says a level, which it may already say. */
149
+ private readonly handleSlot;
119
150
  protected render(): TemplateResult | typeof nothing;
120
151
  }
121
152
  declare global {
@@ -1,8 +1,8 @@
1
1
  import { __decorate } from "tslib";
2
- import { customElement, formatWords, HelixElement, words, wordsLocale, } from '@fusion.dev/helix-core';
2
+ import { customElement, formatWords, HelixElement, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
- import { timeOf } from '../buffer.js';
5
+ import { levelSaid, timeOf } from '../buffer.js';
6
6
  import { markMatches } from '../rows.js';
7
7
  const NAME = /^[a-z-]+$/;
8
8
  const elapsed = (seconds) => {
@@ -16,7 +16,13 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
16
16
  constructor() {
17
17
  super(...arguments);
18
18
  this.at = '';
19
+ this.levelWords = {
20
+ debug: 'Debug:',
21
+ error: 'Error:',
22
+ warn: 'Warning:',
23
+ };
19
24
  this.lineLabel = 'Line {number}';
25
+ this.logCount = 1;
20
26
  this.logFiltered = false;
21
27
  this.logGrouped = false;
22
28
  this.logHref = '';
@@ -30,12 +36,22 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
30
36
  this.logStop = false;
31
37
  this.logTimes = '';
32
38
  this.runs = undefined;
39
+ this.timesLabel = '{count} times';
33
40
  this.isLogLine = true;
41
+ this.written = '';
42
+ this.handleSlot = () => {
43
+ if (this.level !== undefined && this.levelWords[this.level]) {
44
+ this.requestUpdate();
45
+ }
46
+ };
34
47
  }
35
48
  static { this.words = 'helix-log'; }
36
49
  willUpdate(changed) {
37
50
  super.willUpdate(changed);
38
51
  this.toggleState('filtered', this.logFiltered);
52
+ if (this.hasUpdated && !this.runs) {
53
+ this.written = (this.textContent ?? '').trimStart().slice(0, 40);
54
+ }
39
55
  }
40
56
  renderNumber() {
41
57
  if (!this.logNumbers || this.logNumber <= 0) {
@@ -83,7 +99,7 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
83
99
  return html `<time part="time" datetime=${new Date(time).toISOString()}>${reads}</time>`;
84
100
  }
85
101
  renderText() {
86
- return html `<slot>${this.runs ? this.renderRuns() : nothing}</slot>`;
102
+ return html `<slot @slotchange=${this.handleSlot}>${this.runs ? this.renderRuns() : nothing}</slot>`;
87
103
  }
88
104
  renderRuns() {
89
105
  return markMatches(this.runs ?? [], this.logQuery).map((piece) => {
@@ -106,14 +122,23 @@ let HelixHeadlessLogLine = class HelixHeadlessLogLine extends HelixElement {
106
122
  return piece.match ? html `<mark part="match">${run}</mark>` : run;
107
123
  });
108
124
  }
125
+ renderCount() {
126
+ if (this.logCount < 2) {
127
+ return nothing;
128
+ }
129
+ return html ` <span part="count" aria-hidden="true">${this.logCount}</span><span part="count-label"
130
+ >${formatWords(this, 'timesLabel', { count: this.logCount })}</span
131
+ >`;
132
+ }
109
133
  render() {
110
134
  if (this.logFiltered) {
111
135
  return nothing;
112
136
  }
113
137
  const level = this.level;
138
+ const said = levelSaid(level === undefined ? undefined : this.levelWords[level], this.runs ? this.runs.map((run) => run.text).join('') : this.written);
114
139
  return html `<div
115
- part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}${this.logSelected ? ' line-selected' : ''}${this.logGrouped ? ' line-grouped' : ''}"
116
- >${this.renderNumber()}${this.renderTime()}<span part="text">${this.renderText()}</span></div>`;
140
+ part="line${level ? { debug: ' line-debug', error: ' line-error', info: '', log: '', warn: ' line-warn' }[level] : ''}${this.logMarked ? ' line-marked' : ''}${this.logSelected ? ' line-selected' : ''}${this.logGrouped ? ' line-grouped' : ''}"
141
+ >${this.renderNumber()}${this.renderTime()}${said ? html `<span part="level">${said} </span>` : nothing}<span part="text">${this.renderText()}</span>${this.renderCount()}</div>`;
117
142
  }
118
143
  };
119
144
  __decorate([
@@ -122,10 +147,17 @@ __decorate([
122
147
  __decorate([
123
148
  property({ reflect: true })
124
149
  ], HelixHeadlessLogLine.prototype, "level", void 0);
150
+ __decorate([
151
+ words,
152
+ property({ attribute: 'level-words', converter: wordsConverter })
153
+ ], HelixHeadlessLogLine.prototype, "levelWords", void 0);
125
154
  __decorate([
126
155
  words,
127
156
  property({ attribute: 'line-label' })
128
157
  ], HelixHeadlessLogLine.prototype, "lineLabel", void 0);
158
+ __decorate([
159
+ property({ attribute: false })
160
+ ], HelixHeadlessLogLine.prototype, "logCount", void 0);
129
161
  __decorate([
130
162
  property({ attribute: false })
131
163
  ], HelixHeadlessLogLine.prototype, "logFiltered", void 0);
@@ -165,6 +197,10 @@ __decorate([
165
197
  __decorate([
166
198
  property({ attribute: false })
167
199
  ], HelixHeadlessLogLine.prototype, "runs", void 0);
200
+ __decorate([
201
+ words,
202
+ property({ attribute: 'times-label' })
203
+ ], HelixHeadlessLogLine.prototype, "timesLabel", void 0);
168
204
  HelixHeadlessLogLine = __decorate([
169
205
  customElement('helix-headless-log-line')
170
206
  ], HelixHeadlessLogLine);
@@ -1,6 +1,6 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
- import { type LogGroup, type LogLine, type LogParser } from '../buffer.js';
3
+ import { type LogGroup, type LogLevel, type LogLine, type LogParser } from '../buffer.js';
4
4
  import type { LogTimes } from '../line/log-line.js';
5
5
  /** Text a log reads as it arrives: a stream, or anything `for await` reads. */
6
6
  export type LogSource = AsyncIterable<string | Uint8Array> | ReadableStream<string | Uint8Array>;
@@ -117,9 +117,12 @@ declare const HelixHeadlessLog_base: (new (...args: any[]) => import("@fusion.de
117
117
  * @csspart group-summary-running - The button of a step drawn from `groups` that is `running`.
118
118
  * @csspart group-summary-skipped - The button of a step drawn from `groups` that was `skipped`.
119
119
  * @csspart line - The row of a line drawn from data.
120
+ * @csspart line-count - How many times a folded line was written, under `collapse`. Hidden from screen readers, which hear `line-count-label`.
121
+ * @csspart line-count-label - What a screen reader hears for a folded line's count, such as "3 times". Visually hidden in the styled tier.
120
122
  * @csspart line-debug - The row of a line drawn from data at `debug`.
121
123
  * @csspart line-error - The row of a line drawn from data at `error`.
122
124
  * @csspart line-grouped - The row of a line drawn from data that belongs to a step.
125
+ * @csspart line-level - What a screen reader hears before the text of a line drawn from data at `debug`, `warn` or `error`. Visually hidden in the styled tier.
123
126
  * @csspart line-marked - The row of a line drawn from data that `mark-lines` names.
124
127
  * @csspart line-match - Text in a line drawn from data that matches `query`.
125
128
  * @csspart line-number - The number of a line drawn from data.
@@ -164,7 +167,7 @@ declare const HelixHeadlessLog_base: (new (...args: any[]) => import("@fusion.de
164
167
  *
165
168
  * @cssstate done - The run is over: `done` is set, or `source` ended.
166
169
  * @cssstate empty - The log has no lines.
167
- * @cssstate filtered - `query` is set, so only matching lines are shown.
170
+ * @cssstate filtered - `query` or `level` is set, so only the lines they keep are shown.
168
171
  * @cssstate pinned - The log follows its newest line.
169
172
  */
170
173
  export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
@@ -176,6 +179,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
176
179
  static words: string;
177
180
  /** How tall a row is assumed to be before one has been drawn, in pixels. */
178
181
  static readonly ESTIMATE = 20;
182
+ /**
183
+ * Folds a line written again straight after itself into it, with a count,
184
+ * as a browser's console does, so a loop writing one warning a thousand
185
+ * times is one line. For lines written or given as data, read as they
186
+ * arrive; numbers, `line-href` and the selection count a folded line once.
187
+ */
188
+ collapse: boolean;
179
189
  /**
180
190
  * Whether the log opens following its newest line, when `pinned` is not set.
181
191
  * `default-pinned="false"` opens it where it starts.
@@ -202,6 +212,7 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
202
212
  errorLabel: string;
203
213
  /** The log's name, read by screen readers, where `label` is empty. */
204
214
  fallbackLabel: string;
215
+ set format(next: 'github-actions' | 'vercel' | undefined);
205
216
  /**
206
217
  * The format written text is in, read with a parser Helix ships:
207
218
  * `github-actions` reads a GitHub Actions log's workflow commands into steps
@@ -209,7 +220,6 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
209
220
  * reads none. `parse` wins over it.
210
221
  */
211
222
  get format(): 'github-actions' | 'vercel' | undefined;
212
- set format(next: 'github-actions' | 'vercel' | undefined);
213
223
  /**
214
224
  * What a screen reader hears when a step finishes or fails. `{heading}` is
215
225
  * the step's heading and `{status}` is `done` or `failed`.
@@ -225,6 +235,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
225
235
  groups: readonly LogGroup[];
226
236
  /** The log's name, read by screen readers. */
227
237
  label: string;
238
+ /**
239
+ * The quietest level shown: `info` hides `debug` and `log` lines, `warn`
240
+ * shows warnings and errors, and `error` errors only. Unset shows every line.
241
+ * Lines it hides are kept, so lowering it shows them again, and a step shows
242
+ * while it holds a line that shows.
243
+ */
244
+ level?: LogLevel;
228
245
  /**
229
246
  * The most lines kept, or 0 to keep them all. Past it the oldest lines are
230
247
  * let go, and the line above the log says how many.
@@ -382,6 +399,13 @@ export declare class HelixHeadlessLog extends HelixHeadlessLog_base {
382
399
  * after the last one is the line still being written.
383
400
  */
384
401
  write(chunk: string): void;
402
+ /**
403
+ * Adds one line as data, after the lines the log holds: its text, and its
404
+ * level, time and group where it has them. For lines that arrive one at a
405
+ * time with a level of their own, such as a console's, where `write` takes
406
+ * text.
407
+ */
408
+ writeLine(line: LogLine): void;
385
409
  /**
386
410
  * Lets every line go, and forgets which groups the reader opened, keeping
387
411
  * `groups`, `parse`, and `limit`.
@@ -9,7 +9,7 @@ import { repeat } from 'lit/directives/repeat.js';
9
9
  import { html } from 'lit/static-html.js';
10
10
  import { lineRuns } from '../../code-block/lines.js';
11
11
  import { stripAnsi } from '../ansi.js';
12
- import { LogBuffer, timeOf, } from '../buffer.js';
12
+ import { LogBuffer, loudEnough, timeOf, } from '../buffer.js';
13
13
  import { LOG_FORMATS } from '../parsers.js';
14
14
  import { logRows } from '../rows.js';
15
15
  const STATUS_NAMES = {
@@ -24,6 +24,7 @@ const inRuns = (runs, number) => runs.some(([first, last]) => number >= first &&
24
24
  let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixElement) {
25
25
  constructor() {
26
26
  super(...arguments);
27
+ this.collapse = false;
27
28
  this.defaultPinned = true;
28
29
  this.doneLabel = 'Finished';
29
30
  this.droppedLabel = '{count, plural, one {# earlier line is} other {# earlier lines are}} not kept';
@@ -199,15 +200,15 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
199
200
  this.doneState = next;
200
201
  this.requestUpdate('done', previous);
201
202
  }
202
- get format() {
203
- return this.formatValue;
204
- }
205
203
  set format(next) {
206
204
  const previous = this.formatValue;
207
205
  this.formatValue = next;
208
206
  this.buffer.parse = this.parseValue ?? this.formatParser;
209
207
  this.requestUpdate('format', previous);
210
208
  }
209
+ get format() {
210
+ return this.formatValue;
211
+ }
211
212
  get lines() {
212
213
  return this.buffer.lines;
213
214
  }
@@ -292,6 +293,10 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
292
293
  this.buffer.write(chunk);
293
294
  this.requestUpdate();
294
295
  }
296
+ writeLine(line) {
297
+ this.buffer.push(line);
298
+ this.requestUpdate();
299
+ }
295
300
  clear() {
296
301
  this.buffer.clear();
297
302
  this.opened.clear();
@@ -519,7 +524,8 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
519
524
  const start = this.childLines.map((line) => timeOf(line.at)).find((time) => time !== undefined);
520
525
  const stop = this.interactive ? this.stopAmong(this.reachable()) : 0;
521
526
  this.childLines.forEach((line, at) => {
522
- const filtered = needle !== '' && !(line.textContent ?? '').toLowerCase().includes(needle);
527
+ const filtered = (needle !== '' && !(line.textContent ?? '').toLowerCase().includes(needle)) ||
528
+ !loudEnough(line.level, this.level);
523
529
  whenHydrated(line, () => {
524
530
  line.logNumber = at + 1;
525
531
  line.logNumbers = this.lineNumbers;
@@ -533,7 +539,7 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
533
539
  line.logStop = at + 1 === stop;
534
540
  line.logGrouped = this.childGroups.some((group) => group.contains(line));
535
541
  });
536
- if (!filtered && needle !== '') {
542
+ if (!filtered && (needle !== '' || this.level !== undefined)) {
537
543
  for (const group of this.childGroups) {
538
544
  if (!group.open && group.contains(line)) {
539
545
  whenHydrated(group, () => {
@@ -719,6 +725,7 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
719
725
  this.lastGroups = this.allGroups;
720
726
  if (changed.has('limit')) {
721
727
  this.buffer.limit = Math.max(0, Math.floor(this.limit || 0));
728
+ this.buffer.collapse = this.collapse;
722
729
  this.buffer.write('');
723
730
  }
724
731
  if (!this.fed) {
@@ -742,6 +749,7 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
742
749
  this.view = logRows({
743
750
  current: this.buffer.current(),
744
751
  groups: this.allGroups,
752
+ level: this.level,
745
753
  lines: this.buffer.lines,
746
754
  open: (id) => this.isOpen(id),
747
755
  query: this.query,
@@ -753,7 +761,7 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
753
761
  }
754
762
  this.toggleState('pinned', this.pinned);
755
763
  this.toggleState('done', this.done);
756
- this.toggleState('filtered', this.query.trim() !== '');
764
+ this.toggleState('filtered', this.query.trim() !== '' || this.level !== undefined);
757
765
  this.toggleState('empty', this.childrenKnown && !this.fed && this.childLines.length === 0);
758
766
  }
759
767
  updated(changed) {
@@ -798,7 +806,8 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
798
806
  const line = this.tag('log-line');
799
807
  const { at, level } = row.line;
800
808
  return html `<${line}
801
- exportparts="line, line-debug, line-error, line-grouped, line-marked, line-selected, line-warn, match: line-match,
809
+ exportparts="line, line-debug, line-error, line-grouped, line-marked, line-selected, line-warn, count: line-count,
810
+ count-label: line-count-label, level: line-level, match: line-match,
802
811
  number: line-number, run: line-run, run-bold: line-run-bold, run-dim: line-run-dim,
803
812
  run-italic: line-run-italic, run-strike: line-run-strike, run-underline: line-run-underline,
804
813
  text: line-text, time: line-time"
@@ -813,6 +822,7 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
813
822
  .logSelectable=${this.selectable}
814
823
  .logSelected=${this.selected(row.number)}
815
824
  .logStop=${row.number === stop}
825
+ .logCount=${row.line.count ?? 1}
816
826
  .logGrouped=${row.line.group !== undefined}
817
827
  .logStart=${start}
818
828
  .logTimes=${this.timestamps ?? ''}
@@ -884,6 +894,9 @@ let HelixHeadlessLog = class HelixHeadlessLog extends MixinCollection(HelixEleme
884
894
  </div>`;
885
895
  }
886
896
  };
897
+ __decorate([
898
+ property({ type: Boolean })
899
+ ], HelixHeadlessLog.prototype, "collapse", void 0);
887
900
  __decorate([
888
901
  property({ attribute: 'default-pinned', converter: onUnlessFalse })
889
902
  ], HelixHeadlessLog.prototype, "defaultPinned", void 0);
@@ -923,6 +936,9 @@ __decorate([
923
936
  __decorate([
924
937
  property()
925
938
  ], HelixHeadlessLog.prototype, "label", void 0);
939
+ __decorate([
940
+ property()
941
+ ], HelixHeadlessLog.prototype, "level", void 0);
926
942
  __decorate([
927
943
  property({ converter: numberConverter('limit', 0) })
928
944
  ], HelixHeadlessLog.prototype, "limit", void 0);
@@ -1,5 +1,5 @@
1
1
  import { type LogRun } from './ansi.js';
2
- import type { LogGroup, LogLine } from './buffer.js';
2
+ import { type LogGroup, type LogLevel, type LogLine } from './buffer.js';
3
3
  /** One row of a log drawn from data: a line, or the heading of a group. */
4
4
  export type LogRow = {
5
5
  /** The group, as `groups` holds it. */
@@ -27,6 +27,8 @@ export interface LogRowsInput {
27
27
  groups: readonly LogGroup[];
28
28
  /** The finished lines, oldest first. */
29
29
  lines: readonly LogLine[];
30
+ /** The quietest level shown, or undefined for every level. */
31
+ level?: LogLevel | undefined;
30
32
  /** Whether a group shows its lines. */
31
33
  open: (id: string) => boolean;
32
34
  /** Text a line has to hold to be shown, ignoring case. Empty shows every line. */
@@ -47,7 +49,7 @@ export interface LogRowsInput {
47
49
  * where it is rather than hidden by the fold. A group with no match is left
48
50
  * out, heading and all.
49
51
  */
50
- export declare const logRows: ({ current, groups, lines, open, query, start }: LogRowsInput) => LogRows;
52
+ export declare const logRows: ({ current, groups, level, lines, open, query, start, }: LogRowsInput) => LogRows;
51
53
  /** A run, and whether it is part of a match. */
52
54
  export interface LogPiece extends LogRun {
53
55
  /** Whether the text matches the search. */
package/dist/log/rows.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { stripAnsi } from './ansi.js';
2
+ import { loudEnough } from './buffer.js';
2
3
  const plain = new WeakMap();
3
4
  const holds = (line, needle) => {
4
5
  if (needle === '') {
@@ -11,12 +12,15 @@ const holds = (line, needle) => {
11
12
  }
12
13
  return text.includes(needle);
13
14
  };
14
- export const logRows = ({ current, groups, lines, open, query, start }) => {
15
+ export const logRows = ({ current, groups, level, lines, open, query, start, }) => {
15
16
  const needle = query.trim().toLowerCase();
16
17
  const byId = new Map(groups.map((group) => [group.id, group]));
17
18
  const all = current ? [...lines, current] : lines;
18
19
  const hits = new Set();
19
- const matches = needle === '' ? undefined : all.map((line) => holds(line, needle));
20
+ const filtering = needle !== '' || level !== undefined;
21
+ const matches = filtering
22
+ ? all.map((line) => holds(line, needle) && loudEnough(line.level, level))
23
+ : undefined;
20
24
  let matched = 0;
21
25
  if (matches) {
22
26
  all.forEach((line, index) => {
@@ -35,22 +39,22 @@ export const logRows = ({ current, groups, lines, open, query, start }) => {
35
39
  const group = line.group === undefined ? undefined : byId.get(line.group);
36
40
  if (group && !placed.has(group.id)) {
37
41
  placed.add(group.id);
38
- if (needle === '' || hits.has(group.id)) {
39
- rows.push({ group, kind: 'group', open: needle !== '' || open(group.id) });
42
+ if (!filtering || hits.has(group.id)) {
43
+ rows.push({ group, kind: 'group', open: filtering || open(group.id) });
40
44
  }
41
45
  }
42
- const shown = group === undefined || needle !== '' || open(group.id);
46
+ const shown = group === undefined || filtering || open(group.id);
43
47
  if (shown && (matches?.[index] ?? true)) {
44
48
  rows.push({ kind: 'line', line, number: start + index + 1 });
45
49
  }
46
50
  }
47
- if (needle === '') {
51
+ if (!filtering) {
48
52
  const last = groups.findLastIndex((group) => placed.has(group.id));
49
53
  for (const group of groups.slice(last + 1)) {
50
54
  rows.push({ group, kind: 'group', open: open(group.id) });
51
55
  }
52
56
  }
53
- return { matched: needle === '' ? all.length : matched, rows };
57
+ return { matched: filtering ? matched : all.length, rows };
54
58
  };
55
59
  export const markMatches = (runs, query) => {
56
60
  const needle = query.trim().toLowerCase();
@@ -1,6 +1,6 @@
1
1
  import { type ChordPress } from '@fusion.dev/helix-core';
2
2
  /** What a terminal frame does with a key press before the program sees it. */
3
- export type TerminalShortcut = 'copy' | 'find' | 'tab-focus';
3
+ export type TerminalShortcut = 'copy' | 'find' | 'paste' | 'tab-focus';
4
4
  /** The chord for a shortcut on this keyboard, or `undefined` where the frame takes none. */
5
5
  export declare const chordOf: (shortcut: TerminalShortcut, apple: boolean) => string | undefined;
6
6
  /** Which shortcut a press is, or `null` for a press the program should have. */