@flowscripter/dynamic-cli-framework 1.4.0 → 1.4.1

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.
@@ -18,7 +18,7 @@ Lint:
18
18
 
19
19
  Generate HTML API Documentation:
20
20
 
21
- `bunx typedoc --readme none index.ts`
21
+ `bunx typedoc index.ts`
22
22
 
23
23
  ### Debug Logging
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -39,5 +39,11 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "typescript": "^6.0.3"
42
+ },
43
+ "typedocOptions": {
44
+ "readme": "none",
45
+ "exclude": [
46
+ "tests/**"
47
+ ]
42
48
  }
43
49
  }
@@ -233,7 +233,7 @@ export default class BaseCLI implements CLI {
233
233
  let completionService: DefaultCompletionService | undefined;
234
234
  if (this.#options.completionServiceEnabled) {
235
235
  completionService = new DefaultCompletionService();
236
- this.addServiceProvider(new CompletionServiceProvider(60, completionService));
236
+ this.addServiceProvider(new CompletionServiceProvider(5, completionService));
237
237
  }
238
238
 
239
239
  const configurationServiceProvider = new ConfigurationServiceProvider(
@@ -14,8 +14,10 @@ export interface PromptContext {
14
14
  export function physicalLineCount(terminal: Terminal, text: string): number {
15
15
  const cols = terminal.columns();
16
16
  if (cols <= 0) return 1;
17
- const width = Bun.stringWidth(text);
18
- return Math.max(1, Math.ceil(width / cols));
17
+ return text.split("\n").reduce((sum, segment) => {
18
+ const width = Bun.stringWidth(segment);
19
+ return sum + Math.max(1, Math.ceil(width / cols));
20
+ }, 0);
19
21
  }
20
22
 
21
23
  export function headerLineCount(terminal: Terminal, promptDef: Prompt): number {
@@ -27,6 +27,25 @@ export default class TtyKeyReader implements KeyReader {
27
27
  return new Promise((resolve) => {
28
28
  const onData = (data: Buffer): void => {
29
29
  this.#ttyReadStream.removeListener("data", onData);
30
+
31
+ // A lone 0x1b may be the start of a multi-byte escape sequence (e.g. arrow keys).
32
+ // Wait briefly for the rest of the sequence before decoding.
33
+ if (data.length === 1 && data[0] === 0x1b) {
34
+ const timeout = setTimeout(() => {
35
+ this.#ttyReadStream.removeListener("data", onMore);
36
+ resolve(TtyKeyReader.#decodeKeyEvent(data));
37
+ }, 20);
38
+
39
+ const onMore = (more: Buffer): void => {
40
+ clearTimeout(timeout);
41
+ this.#ttyReadStream.removeListener("data", onMore);
42
+ resolve(TtyKeyReader.#decodeKeyEvent(Buffer.concat([data, more])));
43
+ };
44
+
45
+ this.#ttyReadStream.once("data", onMore);
46
+ return;
47
+ }
48
+
30
49
  resolve(TtyKeyReader.#decodeKeyEvent(data));
31
50
  };
32
51
  this.#ttyReadStream.on("data", onData);
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { physicalLineCount } from "../../../../src/service/prompter/prompt/PromptContext.ts";
3
+ import type Terminal from "../../../../src/terminal/Terminal.ts";
4
+
5
+ class MockTerminal implements Terminal {
6
+ readonly #cols: number;
7
+ constructor(cols: number) {
8
+ this.#cols = cols;
9
+ }
10
+ columns(): number {
11
+ return this.#cols;
12
+ }
13
+ rows(): number {
14
+ return 24;
15
+ }
16
+ clearLine(): Promise<void> {
17
+ return Promise.resolve();
18
+ }
19
+ clearUpLines(): Promise<void> {
20
+ return Promise.resolve();
21
+ }
22
+ hideCursor(): Promise<void> {
23
+ return Promise.resolve();
24
+ }
25
+ showCursor(): Promise<void> {
26
+ return Promise.resolve();
27
+ }
28
+ write(): Promise<void> {
29
+ return Promise.resolve();
30
+ }
31
+ }
32
+
33
+ describe("physicalLineCount", () => {
34
+ test("single short text fits on one line", () => {
35
+ expect(physicalLineCount(new MockTerminal(80), "hello")).toBe(1);
36
+ });
37
+
38
+ test("text wider than terminal wraps", () => {
39
+ // 11 chars on a 10-col terminal => 2 lines
40
+ expect(physicalLineCount(new MockTerminal(10), "hello world")).toBe(2);
41
+ });
42
+
43
+ test("embedded newline on wide terminal counts as two lines", () => {
44
+ // Both parts fit on one line (terminal 300 cols > 127 and > 81),
45
+ // but the embedded \n always forces 2 rendered lines
46
+ const text =
47
+ "This will set up your terminal so that pressing TAB while typing commands will show possible options and autocomplete arguments.\n(Enabling autocompletion will modify configuration files in your home directory.)";
48
+ expect(physicalLineCount(new MockTerminal(300), text)).toBe(2);
49
+ });
50
+
51
+ test("embedded newline combined with wrapping", () => {
52
+ // "hello world" (11 chars) => ceil(11/10) = 2 lines
53
+ // "foo" (3 chars) => ceil(3/10) = 1 line
54
+ // total = 3
55
+ expect(physicalLineCount(new MockTerminal(10), "hello world\nfoo")).toBe(3);
56
+ });
57
+
58
+ test("zero columns returns 1", () => {
59
+ expect(physicalLineCount(new MockTerminal(0), "hello")).toBe(1);
60
+ });
61
+ });