@flowscripter/dynamic-cli-framework 11.0.0 → 12.0.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.
Files changed (52) hide show
  1. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  2. package/dist/src/cli/BaseCLI.js +10 -3
  3. package/dist/src/runtime/DefaultContext.d.ts +2 -1
  4. package/dist/src/runtime/DefaultContext.d.ts.map +1 -1
  5. package/dist/src/runtime/DefaultContext.js +3 -1
  6. package/dist/src/runtime/command/CommandValidator.d.ts.map +1 -1
  7. package/dist/src/runtime/command/CommandValidator.js +18 -6
  8. package/dist/src/runtime/parser.d.ts.map +1 -1
  9. package/dist/src/runtime/parser.js +13 -4
  10. package/dist/src/runtime/values/argumentValueValidation.d.ts +26 -4
  11. package/dist/src/runtime/values/argumentValueValidation.d.ts.map +1 -1
  12. package/dist/src/runtime/values/argumentValueValidation.js +53 -50
  13. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.d.ts +4 -0
  14. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.d.ts.map +1 -1
  15. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.js +53 -21
  16. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  17. package/dist/src/service/configuration/KeyValueServiceProvider.js +1 -0
  18. package/dist/src/service/printer/PrinterServiceProvider.d.ts +3 -1
  19. package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
  20. package/dist/src/service/printer/PrinterServiceProvider.js +7 -4
  21. package/dist/src/service/printer/command/ThemeCommand.d.ts +24 -0
  22. package/dist/src/service/printer/command/ThemeCommand.d.ts.map +1 -0
  23. package/dist/src/service/printer/command/ThemeCommand.js +40 -0
  24. package/dist/src/terminal/Ansi.d.ts +4 -0
  25. package/dist/src/terminal/Ansi.d.ts.map +1 -1
  26. package/dist/src/terminal/Ansi.js +7 -3
  27. package/dist/src/terminal/BackgroundDetector.d.ts +25 -0
  28. package/dist/src/terminal/BackgroundDetector.d.ts.map +1 -0
  29. package/dist/src/terminal/BackgroundDetector.js +63 -0
  30. package/dist/src/terminal/KeyReader.d.ts +7 -0
  31. package/dist/src/terminal/KeyReader.d.ts.map +1 -1
  32. package/dist/src/terminal/TtyKeyReader.d.ts +1 -0
  33. package/dist/src/terminal/TtyKeyReader.d.ts.map +1 -1
  34. package/dist/src/terminal/TtyKeyReader.js +30 -0
  35. package/package.json +2 -2
  36. package/src/cli/BaseCLI.ts +15 -4
  37. package/src/runtime/DefaultContext.ts +4 -1
  38. package/src/runtime/command/CommandValidator.ts +35 -5
  39. package/src/runtime/parser.ts +14 -0
  40. package/src/runtime/values/argumentValueValidation.ts +82 -50
  41. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +92 -26
  42. package/src/service/configuration/KeyValueServiceProvider.ts +1 -0
  43. package/src/service/printer/PrinterServiceProvider.ts +11 -4
  44. package/src/service/printer/command/ThemeCommand.ts +47 -0
  45. package/src/terminal/Ansi.ts +7 -3
  46. package/src/terminal/BackgroundDetector.ts +78 -0
  47. package/src/terminal/KeyReader.ts +7 -0
  48. package/src/terminal/TtyKeyReader.ts +35 -0
  49. package/dist/src/service/printer/command/DarkModeCommand.d.ts +0 -17
  50. package/dist/src/service/printer/command/DarkModeCommand.d.ts.map +0 -1
  51. package/dist/src/service/printer/command/DarkModeCommand.js +0 -24
  52. package/src/service/printer/command/DarkModeCommand.ts +0 -32
@@ -0,0 +1,47 @@
1
+ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
+ import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
+ import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
7
+
8
+ /**
9
+ * Command allowing the specification of the dark/light theme for the {@link PrinterService}.
10
+ *
11
+ * With the `auto` value the terminal background is detected and light is used if it cannot be determined.
12
+ */
13
+ export default class ThemeCommand implements GlobalModifierCommand {
14
+ readonly name = "theme";
15
+ readonly description = "Theme for output";
16
+ readonly enableConfiguration = true;
17
+ readonly argument: GlobalCommandArgument = {
18
+ type: ValueTypeName.STRING,
19
+ defaultValue: "auto",
20
+ allowableValues: ["auto", "dark", "light"],
21
+ configurationKey: "THEME",
22
+ };
23
+ readonly executePriority: number;
24
+ readonly #detect: (() => Promise<boolean | undefined>) | undefined;
25
+
26
+ /**
27
+ * @param executePriority the priority of the command.
28
+ * @param detect optional function detecting whether the terminal background is dark.
29
+ */
30
+ public constructor(executePriority: number, detect?: () => Promise<boolean | undefined>) {
31
+ this.executePriority = executePriority;
32
+ this.#detect = detect;
33
+ }
34
+
35
+ public async execute(context: Context, argumentValue: SingleValueType): Promise<void> {
36
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
37
+
38
+ if (argumentValue === "auto") {
39
+ printerService.darkMode =
40
+ printerService.colorEnabled && this.#detect !== undefined
41
+ ? ((await this.#detect()) ?? false)
42
+ : false;
43
+ } else {
44
+ printerService.darkMode = argumentValue === "dark";
45
+ }
46
+ }
47
+ }
@@ -1,6 +1,8 @@
1
- const CSI = "\x1b[";
2
- const OSC = "\x1b]";
3
- const BEL = "\x07";
1
+ export const ESC = "\x1b";
2
+ export const BEL = "\x07";
3
+ const CSI = ESC + "[";
4
+ const OSC = ESC + "]";
5
+ const ST = ESC + "\\";
4
6
  const SEP = ";";
5
7
 
6
8
  export const CLEAR_LINE = CSI + "2K";
@@ -12,6 +14,8 @@ export const ITALIC_START = CSI + "3m";
12
14
  export const ITALIC_END = CSI + "23m";
13
15
  export const FOREGROUND_COLOR_END = CSI + "39m";
14
16
  export const BACKGROUND_COLOR_END = CSI + "49m";
17
+ export const QUERY_BACKGROUND_COLOR = `${OSC}11;?${ST}`;
18
+ export const QUERY_DEVICE_ATTRIBUTES = CSI + "c";
15
19
  export const HYPERLINK_END = `${OSC}8${SEP}${SEP}${BEL}`;
16
20
 
17
21
  export function foregroundColorStart(r: number, g: number, b: number): string {
@@ -0,0 +1,78 @@
1
+ import process from "node:process";
2
+ import type KeyReader from "./KeyReader.ts";
3
+ import type Terminal from "./Terminal.ts";
4
+ import { BEL, ESC, QUERY_BACKGROUND_COLOR, QUERY_DEVICE_ATTRIBUTES } from "./Ansi.ts";
5
+
6
+ const OSC_11_REPLY = new RegExp(
7
+ `${ESC}\\]11;rgb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})(?:${BEL}|${ESC}\\\\)`,
8
+ );
9
+ const DA1_REPLY = new RegExp(`${ESC}\\[\\?[0-9;]*c`);
10
+
11
+ export const DEFAULT_DETECTION_TIMEOUT_MS = 150;
12
+
13
+ /**
14
+ * Parse the COLORFGBG environment variable value into a dark background flag.
15
+ *
16
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
17
+ */
18
+ export function parseColorFgBg(value: string | undefined): boolean | undefined {
19
+ if (value === undefined) {
20
+ return undefined;
21
+ }
22
+ const parts = value.split(";");
23
+ const background = parts[parts.length - 1]!;
24
+ if (!/^\d{1,2}$/.test(background)) {
25
+ return undefined;
26
+ }
27
+ const index = Number(background);
28
+ if (index > 15) {
29
+ return undefined;
30
+ }
31
+ return index <= 6 || index === 8;
32
+ }
33
+
34
+ /**
35
+ * Parse an OSC 11 background colour reply into a dark background flag.
36
+ *
37
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if no reply is present.
38
+ */
39
+ export function parseOsc11Reply(data: string): boolean | undefined {
40
+ const match = OSC_11_REPLY.exec(data);
41
+ if (match === null) {
42
+ return undefined;
43
+ }
44
+ const [r, g, b] = [match[1]!, match[2]!, match[3]!].map(
45
+ (channel) => parseInt(channel, 16) / (16 ** channel.length - 1),
46
+ ) as [number, number, number];
47
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5;
48
+ }
49
+
50
+ /**
51
+ * Detect whether the terminal background is dark.
52
+ *
53
+ * Checks COLORFGBG first and then queries the terminal for its background colour (OSC 11).
54
+ * The query is skipped unless the terminal is a TTY and is bounded by a timeout.
55
+ *
56
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
57
+ */
58
+ export async function detectDarkBackground(
59
+ keyReader: KeyReader,
60
+ terminal: Terminal,
61
+ env: Record<string, string | undefined> = process.env,
62
+ timeoutMs: number = DEFAULT_DETECTION_TIMEOUT_MS,
63
+ ): Promise<boolean | undefined> {
64
+ if (!terminal.isTty() || env.TERM === "dumb") {
65
+ return undefined;
66
+ }
67
+ const fromEnv = parseColorFgBg(env.COLORFGBG);
68
+ if (fromEnv !== undefined) {
69
+ return fromEnv;
70
+ }
71
+ try {
72
+ const reply = keyReader.readRaw((received) => DA1_REPLY.test(received), timeoutMs);
73
+ await terminal.write(QUERY_BACKGROUND_COLOR + QUERY_DEVICE_ATTRIBUTES);
74
+ return parseOsc11Reply(await reply);
75
+ } catch {
76
+ return undefined;
77
+ }
78
+ }
@@ -20,4 +20,11 @@ export default interface KeyReader {
20
20
  enableRawMode(): void;
21
21
  disableRawMode(): void;
22
22
  readKey(): Promise<KeyEvent>;
23
+ /**
24
+ * Read raw input until `isComplete` returns `true` for the accumulated input or `timeoutMs` elapses.
25
+ * Raw mode and paused state are restored afterwards.
26
+ *
27
+ * @returns the accumulated input.
28
+ */
29
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string>;
23
30
  }
@@ -52,6 +52,41 @@ export default class TtyKeyReader implements KeyReader {
52
52
  });
53
53
  }
54
54
 
55
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string> {
56
+ const stream = this.#ttyReadStream;
57
+ const wasRaw = stream.isRaw === true;
58
+ const wasPaused = stream.isPaused();
59
+
60
+ return new Promise((resolve) => {
61
+ let received = "";
62
+ let timer: ReturnType<typeof setTimeout>;
63
+
64
+ const finish = (): void => {
65
+ clearTimeout(timer);
66
+ stream.removeListener("data", onData);
67
+ if (!wasRaw) {
68
+ stream.setRawMode(false);
69
+ }
70
+ if (wasPaused) {
71
+ stream.pause();
72
+ }
73
+ resolve(received);
74
+ };
75
+
76
+ const onData = (data: Buffer | string): void => {
77
+ received += data.toString();
78
+ if (isComplete(received)) {
79
+ finish();
80
+ }
81
+ };
82
+
83
+ stream.setRawMode(true);
84
+ stream.on("data", onData);
85
+ stream.resume();
86
+ timer = setTimeout(finish, timeoutMs);
87
+ });
88
+ }
89
+
55
90
  static #decodeKeyEvent(data: Buffer): KeyEvent {
56
91
  if (data.length === 0) {
57
92
  return {};
@@ -1,17 +0,0 @@
1
- import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
2
- import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
- import { type SingleValueType } from "@flowscripter/dynamic-cli-framework-api";
4
- import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- /**
6
- * Command allowing the specification of dark/light mode for the {@link PrinterService}.
7
- */
8
- export default class DarkModeCommand implements GlobalModifierCommand {
9
- readonly name = "dark-mode";
10
- readonly description = "Enable dark mode for output";
11
- readonly enableConfiguration = true;
12
- readonly argument: GlobalCommandArgument;
13
- readonly executePriority: number;
14
- constructor(executePriority: number);
15
- execute(context: Context, argumentValue: SingleValueType): Promise<void>;
16
- }
17
- //# sourceMappingURL=DarkModeCommand.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DarkModeCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/printer/command/DarkModeCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,yCAAyC,CAAC;AAC9F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAIvE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,eAAgB,YAAW,qBAAqB;IACnE,QAAQ,CAAC,IAAI,eAAe;IAC5B,QAAQ,CAAC,WAAW,iCAAiC;IACrD,QAAQ,CAAC,mBAAmB,QAAQ;IACpC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAItC;IACF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAEjC,YAAmB,eAAe,EAAE,MAAM,EAEzC;IAEM,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9E;CACF"}
@@ -1,24 +0,0 @@
1
- import { ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
2
- import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
- /**
4
- * Command allowing the specification of dark/light mode for the {@link PrinterService}.
5
- */
6
- export default class DarkModeCommand {
7
- name = "dark-mode";
8
- description = "Enable dark mode for output";
9
- enableConfiguration = true;
10
- argument = {
11
- type: ValueTypeName.BOOLEAN,
12
- defaultValue: false,
13
- configurationKey: "DARK_MODE",
14
- };
15
- executePriority;
16
- constructor(executePriority) {
17
- this.executePriority = executePriority;
18
- }
19
- execute(context, argumentValue) {
20
- const printerService = context.getServiceById(PRINTER_SERVICE_ID);
21
- printerService.darkMode = argumentValue;
22
- return Promise.resolve();
23
- }
24
- }
@@ -1,32 +0,0 @@
1
- import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
2
- import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
- import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
- import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
- import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
7
-
8
- /**
9
- * Command allowing the specification of dark/light mode for the {@link PrinterService}.
10
- */
11
- export default class DarkModeCommand implements GlobalModifierCommand {
12
- readonly name = "dark-mode";
13
- readonly description = "Enable dark mode for output";
14
- readonly enableConfiguration = true;
15
- readonly argument: GlobalCommandArgument = {
16
- type: ValueTypeName.BOOLEAN,
17
- defaultValue: false,
18
- configurationKey: "DARK_MODE",
19
- };
20
- readonly executePriority: number;
21
-
22
- public constructor(executePriority: number) {
23
- this.executePriority = executePriority;
24
- }
25
-
26
- public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
27
- const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
28
- printerService.darkMode = argumentValue as boolean;
29
-
30
- return Promise.resolve();
31
- }
32
- }