@fusion.dev/helix-headless 0.212.1 → 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.
@@ -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. */
@@ -1,11 +1,16 @@
1
1
  import { ariaChord, matchesChord } from '@fusion.dev/helix-core';
2
2
  const CHORDS = {
3
- apple: { copy: undefined, find: 'cmd+f', 'tab-focus': 'ctrl+shift+m' },
4
- other: { copy: 'ctrl+shift+c', find: 'ctrl+shift+f', 'tab-focus': 'ctrl+m' },
3
+ apple: { copy: undefined, find: 'cmd+f', paste: undefined, 'tab-focus': 'ctrl+shift+m' },
4
+ other: {
5
+ copy: 'ctrl+shift+c',
6
+ find: 'ctrl+shift+f',
7
+ paste: 'ctrl+shift+v',
8
+ 'tab-focus': 'ctrl+m',
9
+ },
5
10
  };
6
11
  export const chordOf = (shortcut, apple) => CHORDS[apple ? 'apple' : 'other'][shortcut];
7
12
  export const shortcutOf = (press, apple) => {
8
- for (const shortcut of ['copy', 'find', 'tab-focus']) {
13
+ for (const shortcut of ['copy', 'find', 'paste', 'tab-focus']) {
9
14
  const chord = chordOf(shortcut, apple);
10
15
  if (chord !== undefined && matchesChord(press, chord, apple)) {
11
16
  return shortcut;
@@ -7,8 +7,14 @@ import '../../toolbar/index.js';
7
7
  export interface TerminalFindOptions {
8
8
  /** Toward the top of the output, for the previous match. */
9
9
  backward: boolean;
10
+ /** Whether case has to match, so "Error" does not find "error". */
11
+ caseSensitive: boolean;
10
12
  /** Whether the search is the reader still typing, which stays on the match it has where it still matches. */
11
13
  incremental: boolean;
14
+ /** Whether `query` is a regular expression, in JavaScript's syntax, which the frame has checked reads. */
15
+ regex: boolean;
16
+ /** Whether a match has to be a whole word. */
17
+ wholeWord: boolean;
12
18
  }
13
19
  /** What the frame asks of whatever draws the terminal, as the frame's settings stand. */
14
20
  export interface TerminalSettings {
@@ -120,6 +126,8 @@ export interface TerminalSink {
120
126
  * Shift+Enter move between the matches, and Escape closes the bar and
121
127
  * returns focus to the terminal.
122
128
  * - Control+Shift+C copies the selection. On a Mac, Command+C does.
129
+ * - Control+Shift+V pastes, as it does in the browser, and the program reads
130
+ * it as typed text. On a Mac, Command+V does.
123
131
  * - Control+M, or Control+Shift+M on a Mac, switches Tab between typing in the
124
132
  * terminal and moving focus out of it. The status line says when Tab moves
125
133
  * focus, and the terminal's input describes the key to a screen reader.
@@ -142,11 +150,14 @@ export interface TerminalSink {
142
150
  * @slot clear-icon - Replaces the glyph inside the clear button.
143
151
  * @slot copy-icon - Replaces the glyph inside the copy button.
144
152
  * @slot exit-actions - Controls of your own beside Restart, shown once the process exits, such as a button that opens the whole log.
145
- * @slot find-actions - Controls of your own in the find bar, between the count and the buttons that move between matches.
153
+ * @slot find-actions - Controls of your own in the find bar, between its options and the buttons that move between matches.
146
154
  * @slot find-close-icon - Replaces the glyph inside the button that closes the find bar.
147
155
  * @slot find-icon - Replaces the glyph inside the find button.
156
+ * @slot find-match-case-icon - Replaces the glyph inside the match case button.
148
157
  * @slot find-next-icon - Replaces the glyph inside the next match button.
149
158
  * @slot find-previous-icon - Replaces the glyph inside the previous match button.
159
+ * @slot find-regex-icon - Replaces the glyph inside the regular expression button.
160
+ * @slot find-whole-word-icon - Replaces the glyph inside the whole word button.
150
161
  * @slot fullscreen-icon - Replaces the glyph inside the fullscreen button.
151
162
  * @slot label - The terminal's name in the header, such as an icon beside the shell's name. Replaces `label` there.
152
163
  * @slot restart-icon - Replaces the glyph inside the Restart button.
@@ -187,6 +198,11 @@ export interface TerminalSink {
187
198
  * @csspart find-count - Which match is current and how many there are, such as "3 of 12 matches".
188
199
  * @csspart find-field - The search field in the find bar.
189
200
  * @csspart find-live - The find button's live region, which speaks while the button is pending.
201
+ * @csspart find-match-case - The button that matches case in the find bar. Pressed while it does.
202
+ * @csspart find-match-case-button - The match case button's inner button.
203
+ * @csspart find-match-case-live - The match case button's live region, which speaks while the button is pending.
204
+ * @csspart find-match-case-spinner - The ring the match case button turns while it is pending.
205
+ * @csspart find-match-case-text - The box the match case button's icon sits in.
190
206
  * @csspart find-next - The button that moves to the next match.
191
207
  * @csspart find-next-button - The next match button's inner button.
192
208
  * @csspart find-next-live - The next match button's live region, which speaks while the button is pending.
@@ -197,8 +213,18 @@ export interface TerminalSink {
197
213
  * @csspart find-previous-live - The previous match button's live region, which speaks while the button is pending.
198
214
  * @csspart find-previous-spinner - The ring the previous match button turns while it is pending.
199
215
  * @csspart find-previous-text - The box the previous match button's icon sits in.
216
+ * @csspart find-regex - The button that reads the find field as a regular expression. Pressed while it does.
217
+ * @csspart find-regex-button - The regular expression button's inner button.
218
+ * @csspart find-regex-live - The regular expression button's live region, which speaks while the button is pending.
219
+ * @csspart find-regex-spinner - The ring the regular expression button turns while it is pending.
220
+ * @csspart find-regex-text - The box the regular expression button's icon sits in.
200
221
  * @csspart find-spinner - The ring the find button turns while it is pending.
201
222
  * @csspart find-text - The box the find button's icon sits in.
223
+ * @csspart find-whole-word - The button that matches whole words only in the find bar. Pressed while it does.
224
+ * @csspart find-whole-word-button - The whole word button's inner button.
225
+ * @csspart find-whole-word-live - The whole word button's live region, which speaks while the button is pending.
226
+ * @csspart find-whole-word-spinner - The ring the whole word button turns while it is pending.
227
+ * @csspart find-whole-word-text - The box the whole word button's icon sits in.
202
228
  * @csspart fullscreen - The button that makes the terminal fill the screen. Pressed while it does.
203
229
  * @csspart fullscreen-button - The fullscreen button's inner button.
204
230
  * @csspart fullscreen-live - The fullscreen button's live region, which speaks while the button is pending.
@@ -285,12 +311,32 @@ export declare class HelixHeadlessTerminal extends HelixElement {
285
311
  findCloseLabel: string;
286
312
  /** The name of the find bar's row of buttons, read by screen readers. */
287
313
  findControlsLabel: string;
314
+ /** What the find bar says when its regular expression cannot be read. */
315
+ findInvalidLabel: string;
288
316
  /** The find button's label, and the find field's name and placeholder. */
289
317
  findLabel: string;
318
+ /**
319
+ * Whether the find bar matches case, so "Error" does not find "error". The
320
+ * reader turns it on and off from the bar.
321
+ */
322
+ findMatchCase: boolean;
323
+ /** The match case button's label. */
324
+ findMatchCaseLabel: string;
290
325
  /** The next match button's label. */
291
326
  findNextLabel: string;
292
327
  /** The previous match button's label. */
293
328
  findPreviousLabel: string;
329
+ /**
330
+ * Whether the find bar reads what the reader types as a regular expression,
331
+ * in JavaScript's syntax. The reader turns it on and off from the bar.
332
+ */
333
+ findRegex: boolean;
334
+ /** The regular expression button's label. */
335
+ findRegexLabel: string;
336
+ /** Whether the find bar matches whole words only. The reader turns it on and off from the bar. */
337
+ findWholeWord: boolean;
338
+ /** The whole word button's label. */
339
+ findWholeWordLabel: string;
294
340
  /** The fullscreen button's label. */
295
341
  fullscreenLabel: string;
296
342
  /** The name of the element the reader types into, which the renderer draws. */
@@ -336,6 +382,8 @@ export declare class HelixHeadlessTerminal extends HelixElement {
336
382
  private matchIndex;
337
383
  /** How many matches the search found, or `undefined` before one ran. */
338
384
  private matchCount;
385
+ /** Whether the regular expression in the field cannot be read, which the count says instead. */
386
+ private invalid;
339
387
  /** Whether Tab moves focus, after Control+M. */
340
388
  private tabMovesFocus;
341
389
  /** Whether any text is selected in the grid. */
@@ -437,6 +485,8 @@ export declare class HelixHeadlessTerminal extends HelixElement {
437
485
  private closeFind;
438
486
  /** Searches for what the field holds, toward the bottom or with `backward` toward the top. */
439
487
  private search;
488
+ /** Turns one of the find bar's options on or off, and searches again with it, staying on a match that still matches. */
489
+ private toggleOption;
440
490
  private readonly handleFindInput;
441
491
  private readonly handleFindKeys;
442
492
  /** Copies the selection, and says so. */
@@ -1,6 +1,6 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { CopyController, customElement, drawIcon, FullscreenController, formatWords, HelixElement, HelixTerminalBellEvent, HelixTerminalInputEvent, HelixTerminalLinkEvent, HelixTerminalResizeEvent, HelixTerminalRestartEvent, HelixTerminalTitleEvent, numberConverter, onApple, stopPress, words, } from '@fusion.dev/helix-core';
3
- import { arrowClockwise, broom, caretDown, caretUp, copy, cornersOut, magnifyingGlass, x, } from '@fusion.dev/helix-icons';
3
+ import { arrowClockwise, asterisk, broom, caretDown, caretUp, copy, cornersOut, magnifyingGlass, textAa, textAUnderline, x, } from '@fusion.dev/helix-icons';
4
4
  import { nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
6
  import { keyed } from 'lit/directives/keyed.js';
@@ -10,6 +10,15 @@ import '../../button/index.js';
10
10
  import '../../icon-button/index.js';
11
11
  import '../../toolbar/index.js';
12
12
  import { isTab, shortcutOf, spokenChord } from '../keys.js';
13
+ const readable = (source) => {
14
+ try {
15
+ new RegExp(source);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
13
22
  const ANSI = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
14
23
  const camel = (name) => name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
15
24
  const unset = (color) => color === '' || color === 'transparent' || /^rgba\(\s*0,\s*0,\s*0,\s*0\s*\)$/.test(color);
@@ -27,9 +36,16 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
27
36
  this.fallbackLabel = 'Terminal';
28
37
  this.findCloseLabel = 'Close find';
29
38
  this.findControlsLabel = 'Find controls';
39
+ this.findInvalidLabel = 'Not a valid regular expression';
30
40
  this.findLabel = 'Find';
41
+ this.findMatchCase = false;
42
+ this.findMatchCaseLabel = 'Match case';
31
43
  this.findNextLabel = 'Next match';
32
44
  this.findPreviousLabel = 'Previous match';
45
+ this.findRegex = false;
46
+ this.findRegexLabel = 'Use regular expression';
47
+ this.findWholeWord = false;
48
+ this.findWholeWordLabel = 'Match whole word';
33
49
  this.fullscreenLabel = 'Fill the screen';
34
50
  this.inputLabel = 'Terminal input';
35
51
  this.label = '';
@@ -44,6 +60,7 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
44
60
  this.query = '';
45
61
  this.matchIndex = -1;
46
62
  this.matchCount = undefined;
63
+ this.invalid = false;
47
64
  this.tabMovesFocus = false;
48
65
  this.anySelected = false;
49
66
  this.hasActions = false;
@@ -83,6 +100,10 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
83
100
  void this.copySelection();
84
101
  return;
85
102
  }
103
+ if (shortcut === 'paste') {
104
+ event.stopPropagation();
105
+ return;
106
+ }
86
107
  if (shortcut === 'tab-focus') {
87
108
  event.preventDefault();
88
109
  event.stopPropagation();
@@ -279,6 +300,9 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
279
300
  : renderer.find !== undefined;
280
301
  }
281
302
  get countText() {
303
+ if (this.invalid) {
304
+ return this.findInvalidLabel;
305
+ }
282
306
  return this.matchCount === undefined
283
307
  ? ''
284
308
  : formatWords(this, 'matchesLabel', {
@@ -365,12 +389,28 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
365
389
  this.focus();
366
390
  }
367
391
  search(backward, incremental = false) {
368
- if (this.query === '') {
392
+ if (this.query === '' || (this.findRegex && !readable(this.query))) {
393
+ this.invalid = this.query !== '';
369
394
  this.matchCount = undefined;
370
395
  this.renderer?.findEnd?.();
396
+ if (this.invalid) {
397
+ this.say(this.findInvalidLabel);
398
+ }
371
399
  return;
372
400
  }
373
- this.renderer?.find?.(this.query, { backward, incremental });
401
+ this.invalid = false;
402
+ this.renderer?.find?.(this.query, {
403
+ backward,
404
+ caseSensitive: this.findMatchCase,
405
+ incremental,
406
+ regex: this.findRegex,
407
+ wholeWord: this.findWholeWord,
408
+ });
409
+ }
410
+ toggleOption(option) {
411
+ this[option] = !this[option];
412
+ this.renderer?.findEnd?.();
413
+ this.search(false, true);
374
414
  }
375
415
  async copySelection() {
376
416
  const text = this.renderer?.selection?.() ?? '';
@@ -488,6 +528,36 @@ let HelixHeadlessTerminal = class HelixHeadlessTerminal extends HelixElement {
488
528
  />
489
529
  <span part="find-count" aria-hidden="true">${this.countText}</span>
490
530
  <${toolbar} part="find-controls" exportparts="toolbar: find-controls-toolbar" label=${this.findControlsLabel}>
531
+ <${button}
532
+ part="find-match-case"
533
+ exportparts="button: find-match-case-button, live: find-match-case-live, spinner: find-match-case-spinner, text: find-match-case-text"
534
+ aria-pressed=${this.findMatchCase ? 'true' : 'false'}
535
+ label=${this.findMatchCaseLabel}
536
+ @click=${() => this.toggleOption('findMatchCase')}
537
+ @helix-press=${stopPress}
538
+ >
539
+ <slot name="find-match-case-icon">${drawIcon(textAa)}</slot>
540
+ </${button}>
541
+ <${button}
542
+ part="find-whole-word"
543
+ exportparts="button: find-whole-word-button, live: find-whole-word-live, spinner: find-whole-word-spinner, text: find-whole-word-text"
544
+ aria-pressed=${this.findWholeWord ? 'true' : 'false'}
545
+ label=${this.findWholeWordLabel}
546
+ @click=${() => this.toggleOption('findWholeWord')}
547
+ @helix-press=${stopPress}
548
+ >
549
+ <slot name="find-whole-word-icon">${drawIcon(textAUnderline)}</slot>
550
+ </${button}>
551
+ <${button}
552
+ part="find-regex"
553
+ exportparts="button: find-regex-button, live: find-regex-live, spinner: find-regex-spinner, text: find-regex-text"
554
+ aria-pressed=${this.findRegex ? 'true' : 'false'}
555
+ label=${this.findRegexLabel}
556
+ @click=${() => this.toggleOption('findRegex')}
557
+ @helix-press=${stopPress}
558
+ >
559
+ <slot name="find-regex-icon">${drawIcon(asterisk)}</slot>
560
+ </${button}>
491
561
  <slot name="find-actions" part="find-actions"></slot>
492
562
  <${button}
493
563
  part="find-previous"
@@ -615,10 +685,21 @@ __decorate([
615
685
  words,
616
686
  property({ attribute: 'find-controls-label' })
617
687
  ], HelixHeadlessTerminal.prototype, "findControlsLabel", void 0);
688
+ __decorate([
689
+ words,
690
+ property({ attribute: 'find-invalid-label' })
691
+ ], HelixHeadlessTerminal.prototype, "findInvalidLabel", void 0);
618
692
  __decorate([
619
693
  words,
620
694
  property({ attribute: 'find-label' })
621
695
  ], HelixHeadlessTerminal.prototype, "findLabel", void 0);
696
+ __decorate([
697
+ property({ attribute: 'find-match-case', type: Boolean })
698
+ ], HelixHeadlessTerminal.prototype, "findMatchCase", void 0);
699
+ __decorate([
700
+ words,
701
+ property({ attribute: 'find-match-case-label' })
702
+ ], HelixHeadlessTerminal.prototype, "findMatchCaseLabel", void 0);
622
703
  __decorate([
623
704
  words,
624
705
  property({ attribute: 'find-next-label' })
@@ -627,6 +708,20 @@ __decorate([
627
708
  words,
628
709
  property({ attribute: 'find-previous-label' })
629
710
  ], HelixHeadlessTerminal.prototype, "findPreviousLabel", void 0);
711
+ __decorate([
712
+ property({ attribute: 'find-regex', type: Boolean })
713
+ ], HelixHeadlessTerminal.prototype, "findRegex", void 0);
714
+ __decorate([
715
+ words,
716
+ property({ attribute: 'find-regex-label' })
717
+ ], HelixHeadlessTerminal.prototype, "findRegexLabel", void 0);
718
+ __decorate([
719
+ property({ attribute: 'find-whole-word', type: Boolean })
720
+ ], HelixHeadlessTerminal.prototype, "findWholeWord", void 0);
721
+ __decorate([
722
+ words,
723
+ property({ attribute: 'find-whole-word-label' })
724
+ ], HelixHeadlessTerminal.prototype, "findWholeWordLabel", void 0);
630
725
  __decorate([
631
726
  words,
632
727
  property({ attribute: 'fullscreen-label' })
@@ -679,6 +774,9 @@ __decorate([
679
774
  __decorate([
680
775
  state()
681
776
  ], HelixHeadlessTerminal.prototype, "matchCount", void 0);
777
+ __decorate([
778
+ state()
779
+ ], HelixHeadlessTerminal.prototype, "invalid", void 0);
682
780
  __decorate([
683
781
  state()
684
782
  ], HelixHeadlessTerminal.prototype, "tabMovesFocus", void 0);