@step-forge/step-forge 0.0.25-alpha.1 → 0.0.25-alpha.3

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/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
10
10
  import { parseArgs } from "node:util";
11
11
  import { spawn } from "node:child_process";
12
12
  import * as readline from "node:readline";
13
+ import { PassThrough } from "node:stream";
13
14
  //#region src/runtime/config.ts
14
15
  const DEFAULT_FEATURES = "**/*.feature";
15
16
  const DEFAULT_STEPS = "**/*.steps.ts";
@@ -526,9 +527,14 @@ var Prompt = class {
526
527
  results = freshResults();
527
528
  failScroll = 0;
528
529
  started = false;
530
+ /** Filtered key stream: raw stdin minus the mouse sequences we handle. */
531
+ input;
529
532
  keyListener;
533
+ dataListener;
530
534
  resizeListener;
531
535
  signalCleanup;
536
+ /** Carry an incomplete mouse sequence split across stdin chunks. */
537
+ mousePending = "";
532
538
  constructor(options) {
533
539
  this.handlers = options.handlers;
534
540
  this.prefix = options.prefix ?? "› ";
@@ -583,11 +589,14 @@ var Prompt = class {
583
589
  start() {
584
590
  if (this.started) return;
585
591
  this.started = true;
586
- readline.emitKeypressEvents(process.stdin);
587
592
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
588
- write("\x1B[?1049h");
593
+ write("\x1B[?1049h\x1B[?1000h\x1B[?1006h");
594
+ this.input = new PassThrough();
595
+ readline.emitKeypressEvents(this.input);
589
596
  this.keyListener = (str, key) => this.onKey(str, key ?? {});
590
- process.stdin.on("keypress", this.keyListener);
597
+ this.input.on("keypress", this.keyListener);
598
+ this.dataListener = (chunk) => this.onStdinData(chunk);
599
+ process.stdin.on("data", this.dataListener);
591
600
  process.stdin.resume();
592
601
  this.resizeListener = () => this.render();
593
602
  process.stdout.on("resize", this.resizeListener);
@@ -598,11 +607,14 @@ var Prompt = class {
598
607
  stop() {
599
608
  if (!this.started) return;
600
609
  this.started = false;
601
- if (this.keyListener) process.stdin.off("keypress", this.keyListener);
610
+ if (this.dataListener) process.stdin.off("data", this.dataListener);
611
+ if (this.keyListener) this.input?.off("keypress", this.keyListener);
602
612
  if (this.resizeListener) process.stdout.off("resize", this.resizeListener);
603
613
  this.signalCleanup?.();
604
614
  this.restoreTerminal();
605
615
  process.stdin.pause();
616
+ this.input = void 0;
617
+ this.mousePending = "";
606
618
  }
607
619
  /** Force a repaint (after mutating `status`, say). No-op until started. */
608
620
  redraw() {
@@ -615,6 +627,7 @@ var Prompt = class {
615
627
  */
616
628
  restoreTerminal() {
617
629
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
630
+ write("\x1B[?1006l\x1B[?1000l");
618
631
  write("\x1B[?25h");
619
632
  write("\x1B[?1049l");
620
633
  }
@@ -718,12 +731,72 @@ var Prompt = class {
718
731
  this.reWindow();
719
732
  this.render();
720
733
  }
721
- /** Scroll the failures pane by a page (clamped in {@link render}). */
734
+ /** Scroll the failures pane by whole pages (PgUp/PgDn). */
722
735
  scrollFailures(pages) {
723
- this.failScroll = Math.max(0, this.failScroll + pages * this.failPage);
724
- this.render();
736
+ this.adjustFailScroll(pages * this.failPage);
725
737
  }
726
738
  failPage = 1;
739
+ /**
740
+ * Move the failures viewport by `deltaLines` (clamped in {@link render}).
741
+ * Scrolling repaints via {@link scheduleRender} so a burst of wheel events
742
+ * (a trackpad fires many per gesture) collapses into a single frame instead of
743
+ * one full-screen repaint each — which is what made small scrolls stutter.
744
+ */
745
+ adjustFailScroll(deltaLines) {
746
+ this.failScroll = Math.max(0, this.failScroll + deltaLines);
747
+ this.scheduleRender();
748
+ }
749
+ renderQueued = false;
750
+ /** Coalesce repaints scheduled within the same tick into one frame. */
751
+ scheduleRender() {
752
+ if (this.renderQueued) return;
753
+ this.renderQueued = true;
754
+ queueMicrotask(() => {
755
+ this.renderQueued = false;
756
+ if (this.started) this.render();
757
+ });
758
+ }
759
+ /**
760
+ * Split a raw stdin chunk: decode SGR/legacy mouse sequences here (only the
761
+ * wheel is acted on — it scrolls the failures pane by a few lines) and forward
762
+ * everything else to readline. An incomplete trailing mouse sequence is held
763
+ * in {@link mousePending} for the next chunk. `latin1` keeps every byte 1:1 so
764
+ * the forwarded bytes reassemble exactly (including multibyte input).
765
+ */
766
+ onStdinData(chunk) {
767
+ const buf = this.mousePending + chunk.toString("latin1");
768
+ let out = "";
769
+ let i = 0;
770
+ while (i < buf.length) {
771
+ if (buf.startsWith("\x1B[<", i)) {
772
+ const rel = /[Mm]/.exec(buf.slice(i + 3));
773
+ if (!rel) break;
774
+ const end = i + 3 + rel.index + 1;
775
+ this.handleMouse(buf.slice(i, end));
776
+ i = end;
777
+ continue;
778
+ }
779
+ if (buf.startsWith("\x1B[M", i)) {
780
+ if (i + 6 > buf.length) break;
781
+ this.handleMouse(buf.slice(i, i + 6));
782
+ i += 6;
783
+ continue;
784
+ }
785
+ out += buf[i];
786
+ i++;
787
+ }
788
+ this.mousePending = buf.slice(i);
789
+ if (out) this.input?.write(Buffer.from(out, "latin1"));
790
+ }
791
+ /** Act on a decoded mouse sequence — wheel up/down scrolls failures; else ignore. */
792
+ handleMouse(seq) {
793
+ let button = null;
794
+ const sgr = /^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(seq);
795
+ if (sgr) button = parseInt(sgr[1], 10);
796
+ else if (seq.length === 6) button = seq.charCodeAt(3) - 32;
797
+ if (button === null || (button & 64) === 0) return;
798
+ this.adjustFailScroll((button & 1) === 0 ? -1 : 1);
799
+ }
727
800
  refilter() {
728
801
  const q = this.query.trim().toLowerCase();
729
802
  this.matches = this.all.map((s, index) => ({