@fusion.dev/helix-headless 0.212.2 → 0.214.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,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);