@flowscripter/dynamic-cli-framework 11.1.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 (31) hide show
  1. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  2. package/dist/src/cli/BaseCLI.js +5 -2
  3. package/dist/src/service/printer/PrinterServiceProvider.d.ts +3 -1
  4. package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
  5. package/dist/src/service/printer/PrinterServiceProvider.js +7 -4
  6. package/dist/src/service/printer/command/ThemeCommand.d.ts +24 -0
  7. package/dist/src/service/printer/command/ThemeCommand.d.ts.map +1 -0
  8. package/dist/src/service/printer/command/ThemeCommand.js +40 -0
  9. package/dist/src/terminal/Ansi.d.ts +4 -0
  10. package/dist/src/terminal/Ansi.d.ts.map +1 -1
  11. package/dist/src/terminal/Ansi.js +7 -3
  12. package/dist/src/terminal/BackgroundDetector.d.ts +25 -0
  13. package/dist/src/terminal/BackgroundDetector.d.ts.map +1 -0
  14. package/dist/src/terminal/BackgroundDetector.js +63 -0
  15. package/dist/src/terminal/KeyReader.d.ts +7 -0
  16. package/dist/src/terminal/KeyReader.d.ts.map +1 -1
  17. package/dist/src/terminal/TtyKeyReader.d.ts +1 -0
  18. package/dist/src/terminal/TtyKeyReader.d.ts.map +1 -1
  19. package/dist/src/terminal/TtyKeyReader.js +30 -0
  20. package/package.json +1 -1
  21. package/src/cli/BaseCLI.ts +10 -3
  22. package/src/service/printer/PrinterServiceProvider.ts +11 -4
  23. package/src/service/printer/command/ThemeCommand.ts +47 -0
  24. package/src/terminal/Ansi.ts +7 -3
  25. package/src/terminal/BackgroundDetector.ts +78 -0
  26. package/src/terminal/KeyReader.ts +7 -0
  27. package/src/terminal/TtyKeyReader.ts +35 -0
  28. package/dist/src/service/printer/command/DarkModeCommand.d.ts +0 -17
  29. package/dist/src/service/printer/command/DarkModeCommand.d.ts.map +0 -1
  30. package/dist/src/service/printer/command/DarkModeCommand.js +0 -24
  31. package/src/service/printer/command/DarkModeCommand.ts +0 -32
@@ -1 +1 @@
1
- {"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAU3E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAqCpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;IAazC,YACE,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,qBAAqB,EAuEhC;IAED;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe,QAEzD;IAED;;;;;;;;OAQG;IACH;;;;;OAKG;IACI,cAAc,CAAC,IAAI,EAAE,WAAW,QAEtC;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,QAQjC;IAEK,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAqQzD;CACF"}
1
+ {"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAU3E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAEhD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAqCpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;IAazC,YACE,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,qBAAqB,EAuEhC;IAED;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe,QAEzD;IAED;;;;;;;;OAQG;IACH;;;;;OAKG;IACI,cAAc,CAAC,IAAI,EAAE,WAAW,QAEtC;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,QAQjC;IAEK,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CA2QzD;CACF"}
@@ -21,6 +21,7 @@ import { awaitBackgroundTasks } from "../runtime/lifecycle/BackgroundTasks.js";
21
21
  import { printRuntimeError } from "../util/runnerHelper.js";
22
22
  import { WritableStream } from "node:stream/web";
23
23
  import DefaultPrinterService from "../service/printer/DefaultPrinterService.js";
24
+ import { detectDarkBackground } from "../terminal/BackgroundDetector.js";
24
25
  import DefaultPrompterService, { DEFAULT_PROMPTER_CONFIG, } from "../service/prompter/DefaultPrompterService.js";
25
26
  import PrompterServiceProvider from "../service/prompter/PrompterServiceProvider.js";
26
27
  import DefaultArgumentPrompterService from "../service/argumentPrompter/DefaultArgumentPrompterService.js";
@@ -189,9 +190,11 @@ export default class BaseCLI {
189
190
  // create and add core services
190
191
  this.addServiceProvider(new ShutdownServiceProvider(SHUTDOWN_SERVICE_PRIORITY));
191
192
  this.addServiceProvider(this.#startupServiceProvider);
192
- this.addServiceProvider(new PrinterServiceProvider(PRINTER_SERVICE_PRIORITY, this.#printerService));
193
- this.addServiceProvider(new TableGeneratorServiceProvider(TABLE_GENERATOR_SERVICE_PRIORITY));
194
193
  const canPrompt = this.#context.isTty.stdin && this.#context.isTty.stderr;
194
+ this.addServiceProvider(new PrinterServiceProvider(PRINTER_SERVICE_PRIORITY, this.#printerService, canPrompt && this.#keyReader !== undefined
195
+ ? () => detectDarkBackground(this.#keyReader, this.#stderrTerminal)
196
+ : undefined));
197
+ this.addServiceProvider(new TableGeneratorServiceProvider(TABLE_GENERATOR_SERVICE_PRIORITY));
195
198
  if (this.#options.promptingEnabled && !canPrompt) {
196
199
  throw new Error("promptingEnabled requires a keyReader and a TTY stderr terminal");
197
200
  }
@@ -9,13 +9,15 @@ export default class PrinterServiceProvider implements ServiceProvider {
9
9
  readonly serviceId: string;
10
10
  readonly servicePriority: number;
11
11
  readonly printerService: PrinterService;
12
+ readonly detectDarkBackground: (() => Promise<boolean | undefined>) | undefined;
12
13
  /**
13
14
  * Create an instance of the service provider with the specified details.
14
15
  *
15
16
  * @param servicePriority the priority of the service.
16
17
  * @param printerService the PrinterService implementation to use.
18
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
17
19
  */
18
- constructor(servicePriority: number, printerService: PrinterService);
20
+ constructor(servicePriority: number, printerService: PrinterService, detectDarkBackground?: () => Promise<boolean | undefined>);
19
21
  getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo>;
20
22
  initService(context: Context): Promise<void>;
21
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PrinterServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/PrinterServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAK9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKzE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IAExC;;;;;OAKG;IACH,YAAmB,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAGzE;IAEM,cAAc,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUjE;IAED,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3C;CACF"}
1
+ {"version":3,"file":"PrinterServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/PrinterServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAK9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKzE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IAEhF;;;;;;OAMG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,cAAc,EAC9B,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,EAK1D;IAEM,cAAc,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUjE;IAED,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3C;CACF"}
@@ -1,6 +1,6 @@
1
1
  import LogLevelCommand from "./command/LogLevelCommand.js";
2
2
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
- import DarkModeCommand from "./command/DarkModeCommand.js";
3
+ import ThemeCommand from "./command/ThemeCommand.js";
4
4
  import NoColorCommand from "./command/NoColorCommand.js";
5
5
  import NoHyperlinksCommand from "./command/NoHyperlinksCommand.js";
6
6
  import { SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
@@ -12,23 +12,26 @@ export default class PrinterServiceProvider {
12
12
  serviceId = PRINTER_SERVICE_ID;
13
13
  servicePriority;
14
14
  printerService;
15
+ detectDarkBackground;
15
16
  /**
16
17
  * Create an instance of the service provider with the specified details.
17
18
  *
18
19
  * @param servicePriority the priority of the service.
19
20
  * @param printerService the PrinterService implementation to use.
21
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
20
22
  */
21
- constructor(servicePriority, printerService) {
23
+ constructor(servicePriority, printerService, detectDarkBackground) {
22
24
  this.servicePriority = servicePriority;
23
25
  this.printerService = printerService;
26
+ this.detectDarkBackground = detectDarkBackground;
24
27
  }
25
28
  getServiceInfo(_cliConfig) {
26
29
  return Promise.resolve({
27
30
  service: this.printerService,
28
31
  commands: [
29
32
  new NoHyperlinksCommand(this.servicePriority + 3),
30
- new DarkModeCommand(this.servicePriority + 2),
31
- new NoColorCommand(this.servicePriority + 1),
33
+ new ThemeCommand(this.servicePriority + 1, this.detectDarkBackground),
34
+ new NoColorCommand(this.servicePriority + 2),
32
35
  new LogLevelCommand(this.servicePriority),
33
36
  ],
34
37
  });
@@ -0,0 +1,24 @@
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 the dark/light theme for the {@link PrinterService}.
7
+ *
8
+ * With the `auto` value the terminal background is detected and light is used if it cannot be determined.
9
+ */
10
+ export default class ThemeCommand implements GlobalModifierCommand {
11
+ #private;
12
+ readonly name = "theme";
13
+ readonly description = "Theme for output";
14
+ readonly enableConfiguration = true;
15
+ readonly argument: GlobalCommandArgument;
16
+ readonly executePriority: number;
17
+ /**
18
+ * @param executePriority the priority of the command.
19
+ * @param detect optional function detecting whether the terminal background is dark.
20
+ */
21
+ constructor(executePriority: number, detect?: () => Promise<boolean | undefined>);
22
+ execute(context: Context, argumentValue: SingleValueType): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=ThemeCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/printer/command/ThemeCommand.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;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,qBAAqB;;IAChE,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,WAAW,sBAAsB;IAC1C,QAAQ,CAAC,mBAAmB,QAAQ;IACpC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAKtC;IACF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAGjC;;;OAGG;IACH,YAAmB,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,EAGtF;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAWpF;CACF"}
@@ -0,0 +1,40 @@
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 the dark/light theme for the {@link PrinterService}.
5
+ *
6
+ * With the `auto` value the terminal background is detected and light is used if it cannot be determined.
7
+ */
8
+ export default class ThemeCommand {
9
+ name = "theme";
10
+ description = "Theme for output";
11
+ enableConfiguration = true;
12
+ argument = {
13
+ type: ValueTypeName.STRING,
14
+ defaultValue: "auto",
15
+ allowableValues: ["auto", "dark", "light"],
16
+ configurationKey: "THEME",
17
+ };
18
+ executePriority;
19
+ #detect;
20
+ /**
21
+ * @param executePriority the priority of the command.
22
+ * @param detect optional function detecting whether the terminal background is dark.
23
+ */
24
+ constructor(executePriority, detect) {
25
+ this.executePriority = executePriority;
26
+ this.#detect = detect;
27
+ }
28
+ async execute(context, argumentValue) {
29
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
30
+ if (argumentValue === "auto") {
31
+ printerService.darkMode =
32
+ printerService.colorEnabled && this.#detect !== undefined
33
+ ? ((await this.#detect()) ?? false)
34
+ : false;
35
+ }
36
+ else {
37
+ printerService.darkMode = argumentValue === "dark";
38
+ }
39
+ }
40
+ }
@@ -1,3 +1,5 @@
1
+ export declare const ESC = "\u001B";
2
+ export declare const BEL = "\u0007";
1
3
  export declare const CLEAR_LINE: string;
2
4
  export declare const CURSOR_LEFT: string;
3
5
  export declare const CURSOR_UP: string;
@@ -7,6 +9,8 @@ export declare const ITALIC_START: string;
7
9
  export declare const ITALIC_END: string;
8
10
  export declare const FOREGROUND_COLOR_END: string;
9
11
  export declare const BACKGROUND_COLOR_END: string;
12
+ export declare const QUERY_BACKGROUND_COLOR = "\u001B]11;?\u001B\\";
13
+ export declare const QUERY_DEVICE_ATTRIBUTES: string;
10
14
  export declare const HYPERLINK_END = "\u001B]8;;\u0007";
11
15
  export declare function foregroundColorStart(r: number, g: number, b: number): string;
12
16
  export declare function backgroundColorStart(r: number, g: number, b: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"Ansi.d.ts","sourceRoot":"","sources":["../../../src/terminal/Ansi.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,QAAa,CAAC;AACrC,eAAO,MAAM,WAAW,QAAY,CAAC;AACrC,eAAO,MAAM,SAAS,QAAa,CAAC;AACpC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,YAAY,QAAa,CAAC;AACvC,eAAO,MAAM,UAAU,QAAc,CAAC;AACtC,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,aAAa,qBAA8B,CAAC;AAEzD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
1
+ {"version":3,"file":"Ansi.d.ts","sourceRoot":"","sources":["../../../src/terminal/Ansi.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,WAAS,CAAC;AAC1B,eAAO,MAAM,GAAG,WAAS,CAAC;AAM1B,eAAO,MAAM,UAAU,QAAa,CAAC;AACrC,eAAO,MAAM,WAAW,QAAY,CAAC;AACrC,eAAO,MAAM,SAAS,QAAa,CAAC;AACpC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,YAAY,QAAa,CAAC;AACvC,eAAO,MAAM,UAAU,QAAc,CAAC;AACtC,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,sBAAsB,wBAAoB,CAAC;AACxD,eAAO,MAAM,uBAAuB,QAAY,CAAC;AACjD,eAAO,MAAM,aAAa,qBAA8B,CAAC;AAEzD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -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
  export const CLEAR_LINE = CSI + "2K";
6
8
  export const CURSOR_LEFT = CSI + "G";
@@ -11,6 +13,8 @@ export const ITALIC_START = CSI + "3m";
11
13
  export const ITALIC_END = CSI + "23m";
12
14
  export const FOREGROUND_COLOR_END = CSI + "39m";
13
15
  export const BACKGROUND_COLOR_END = CSI + "49m";
16
+ export const QUERY_BACKGROUND_COLOR = `${OSC}11;?${ST}`;
17
+ export const QUERY_DEVICE_ATTRIBUTES = CSI + "c";
14
18
  export const HYPERLINK_END = `${OSC}8${SEP}${SEP}${BEL}`;
15
19
  export function foregroundColorStart(r, g, b) {
16
20
  return `\x1b[38;2;${r};${g};${b}m`;
@@ -0,0 +1,25 @@
1
+ import type KeyReader from "./KeyReader.ts";
2
+ import type Terminal from "./Terminal.ts";
3
+ export declare const DEFAULT_DETECTION_TIMEOUT_MS = 150;
4
+ /**
5
+ * Parse the COLORFGBG environment variable value into a dark background flag.
6
+ *
7
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
8
+ */
9
+ export declare function parseColorFgBg(value: string | undefined): boolean | undefined;
10
+ /**
11
+ * Parse an OSC 11 background colour reply into a dark background flag.
12
+ *
13
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if no reply is present.
14
+ */
15
+ export declare function parseOsc11Reply(data: string): boolean | undefined;
16
+ /**
17
+ * Detect whether the terminal background is dark.
18
+ *
19
+ * Checks COLORFGBG first and then queries the terminal for its background colour (OSC 11).
20
+ * The query is skipped unless the terminal is a TTY and is bounded by a timeout.
21
+ *
22
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
23
+ */
24
+ export declare function detectDarkBackground(keyReader: KeyReader, terminal: Terminal, env?: Record<string, string | undefined>, timeoutMs?: number): Promise<boolean | undefined>;
25
+ //# sourceMappingURL=BackgroundDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundDetector.d.ts","sourceRoot":"","sources":["../../../src/terminal/BackgroundDetector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAQ1C,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAc7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CASjE;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,EAClB,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,EACrD,SAAS,GAAE,MAAqC,GAC/C,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAe9B"}
@@ -0,0 +1,63 @@
1
+ import process from "node:process";
2
+ import { BEL, ESC, QUERY_BACKGROUND_COLOR, QUERY_DEVICE_ATTRIBUTES } from "./Ansi.js";
3
+ const OSC_11_REPLY = new RegExp(`${ESC}\\]11;rgb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})(?:${BEL}|${ESC}\\\\)`);
4
+ const DA1_REPLY = new RegExp(`${ESC}\\[\\?[0-9;]*c`);
5
+ export const DEFAULT_DETECTION_TIMEOUT_MS = 150;
6
+ /**
7
+ * Parse the COLORFGBG environment variable value into a dark background flag.
8
+ *
9
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
10
+ */
11
+ export function parseColorFgBg(value) {
12
+ if (value === undefined) {
13
+ return undefined;
14
+ }
15
+ const parts = value.split(";");
16
+ const background = parts[parts.length - 1];
17
+ if (!/^\d{1,2}$/.test(background)) {
18
+ return undefined;
19
+ }
20
+ const index = Number(background);
21
+ if (index > 15) {
22
+ return undefined;
23
+ }
24
+ return index <= 6 || index === 8;
25
+ }
26
+ /**
27
+ * Parse an OSC 11 background colour reply into a dark background flag.
28
+ *
29
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if no reply is present.
30
+ */
31
+ export function parseOsc11Reply(data) {
32
+ const match = OSC_11_REPLY.exec(data);
33
+ if (match === null) {
34
+ return undefined;
35
+ }
36
+ const [r, g, b] = [match[1], match[2], match[3]].map((channel) => parseInt(channel, 16) / (16 ** channel.length - 1));
37
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5;
38
+ }
39
+ /**
40
+ * Detect whether the terminal background is dark.
41
+ *
42
+ * Checks COLORFGBG first and then queries the terminal for its background colour (OSC 11).
43
+ * The query is skipped unless the terminal is a TTY and is bounded by a timeout.
44
+ *
45
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
46
+ */
47
+ export async function detectDarkBackground(keyReader, terminal, env = process.env, timeoutMs = DEFAULT_DETECTION_TIMEOUT_MS) {
48
+ if (!terminal.isTty() || env.TERM === "dumb") {
49
+ return undefined;
50
+ }
51
+ const fromEnv = parseColorFgBg(env.COLORFGBG);
52
+ if (fromEnv !== undefined) {
53
+ return fromEnv;
54
+ }
55
+ try {
56
+ const reply = keyReader.readRaw((received) => DA1_REPLY.test(received), timeoutMs);
57
+ await terminal.write(QUERY_BACKGROUND_COLOR + QUERY_DEVICE_ATTRIBUTES);
58
+ return parseOsc11Reply(await reply);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
@@ -18,5 +18,12 @@ export default interface KeyReader {
18
18
  enableRawMode(): void;
19
19
  disableRawMode(): void;
20
20
  readKey(): Promise<KeyEvent>;
21
+ /**
22
+ * Read raw input until `isComplete` returns `true` for the accumulated input or `timeoutMs` elapses.
23
+ * Raw mode and paused state are restored afterwards.
24
+ *
25
+ * @returns the accumulated input.
26
+ */
27
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string>;
21
28
  }
22
29
  //# sourceMappingURL=KeyReader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"KeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/KeyReader.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,EAAE,IAAI;IACN,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;IACT,KAAK,IAAI;IACT,KAAK,IAAI;IACT,SAAS,IAAI;IACb,MAAM,IAAI;IACV,GAAG,IAAI;IACP,SAAS,IAAI;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,WAAW,SAAS;IAChC,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"KeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/KeyReader.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,EAAE,IAAI;IACN,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;IACT,KAAK,IAAI;IACT,KAAK,IAAI;IACT,SAAS,IAAI;IACb,MAAM,IAAI;IACV,GAAG,IAAI;IACP,SAAS,IAAI;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,WAAW,SAAS;IAChC,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACxF"}
@@ -7,5 +7,6 @@ export default class TtyKeyReader implements KeyReader {
7
7
  enableRawMode(): void;
8
8
  disableRawMode(): void;
9
9
  readKey(): Promise<KeyEvent>;
10
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string>;
10
11
  }
11
12
  //# sourceMappingURL=TtyKeyReader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TtyKeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyKeyReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAE3D,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,SAAS;;IAGpD,YAAY,aAAa,EAAE,UAAU,EAKpC;IAED,aAAa,IAAI,IAAI,CAGpB;IAED,cAAc,IAAI,IAAI,CAGrB;IAED,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CA2B3B;CAiDF"}
1
+ {"version":3,"file":"TtyKeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyKeyReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAE3D,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,SAAS;;IAGpD,YAAY,aAAa,EAAE,UAAU,EAKpC;IAED,aAAa,IAAI,IAAI,CAGpB;IAED,cAAc,IAAI,IAAI,CAGrB;IAED,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CA2B3B;IAED,OAAO,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCrF;CAiDF"}
@@ -41,6 +41,36 @@ export default class TtyKeyReader {
41
41
  this.#ttyReadStream.on("data", onData);
42
42
  });
43
43
  }
44
+ readRaw(isComplete, timeoutMs) {
45
+ const stream = this.#ttyReadStream;
46
+ const wasRaw = stream.isRaw === true;
47
+ const wasPaused = stream.isPaused();
48
+ return new Promise((resolve) => {
49
+ let received = "";
50
+ let timer;
51
+ const finish = () => {
52
+ clearTimeout(timer);
53
+ stream.removeListener("data", onData);
54
+ if (!wasRaw) {
55
+ stream.setRawMode(false);
56
+ }
57
+ if (wasPaused) {
58
+ stream.pause();
59
+ }
60
+ resolve(received);
61
+ };
62
+ const onData = (data) => {
63
+ received += data.toString();
64
+ if (isComplete(received)) {
65
+ finish();
66
+ }
67
+ };
68
+ stream.setRawMode(true);
69
+ stream.on("data", onData);
70
+ stream.resume();
71
+ timer = setTimeout(finish, timeoutMs);
72
+ });
73
+ }
44
74
  static #decodeKeyEvent(data) {
45
75
  if (data.length === 0) {
46
76
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "11.1.0",
3
+ "version": "12.0.0",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -44,6 +44,7 @@ import type Terminal from "../terminal/Terminal.ts";
44
44
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
45
45
  import DefaultPrinterService from "../service/printer/DefaultPrinterService.ts";
46
46
  import type Styler from "../terminal/Styler.ts";
47
+ import { detectDarkBackground } from "../terminal/BackgroundDetector.ts";
47
48
  import type KeyReader from "../terminal/KeyReader.ts";
48
49
  import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
49
50
  import DefaultPrompterService, {
@@ -276,13 +277,19 @@ export default class BaseCLI implements CLI {
276
277
  // create and add core services
277
278
  this.addServiceProvider(new ShutdownServiceProvider(SHUTDOWN_SERVICE_PRIORITY));
278
279
  this.addServiceProvider(this.#startupServiceProvider);
280
+ const canPrompt = this.#context.isTty.stdin && this.#context.isTty.stderr;
281
+
279
282
  this.addServiceProvider(
280
- new PrinterServiceProvider(PRINTER_SERVICE_PRIORITY, this.#printerService),
283
+ new PrinterServiceProvider(
284
+ PRINTER_SERVICE_PRIORITY,
285
+ this.#printerService,
286
+ canPrompt && this.#keyReader !== undefined
287
+ ? () => detectDarkBackground(this.#keyReader!, this.#stderrTerminal)
288
+ : undefined,
289
+ ),
281
290
  );
282
291
  this.addServiceProvider(new TableGeneratorServiceProvider(TABLE_GENERATOR_SERVICE_PRIORITY));
283
292
 
284
- const canPrompt = this.#context.isTty.stdin && this.#context.isTty.stderr;
285
-
286
293
  if (this.#options.promptingEnabled && !canPrompt) {
287
294
  throw new Error("promptingEnabled requires a keyReader and a TTY stderr terminal");
288
295
  }
@@ -3,7 +3,7 @@ import LogLevelCommand from "./command/LogLevelCommand.ts";
3
3
  import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
5
5
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
- import DarkModeCommand from "./command/DarkModeCommand.ts";
6
+ import ThemeCommand from "./command/ThemeCommand.ts";
7
7
  import NoColorCommand from "./command/NoColorCommand.ts";
8
8
  import NoHyperlinksCommand from "./command/NoHyperlinksCommand.ts";
9
9
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
@@ -18,16 +18,23 @@ export default class PrinterServiceProvider implements ServiceProvider {
18
18
  readonly serviceId: string = PRINTER_SERVICE_ID;
19
19
  readonly servicePriority: number;
20
20
  readonly printerService: PrinterService;
21
+ readonly detectDarkBackground: (() => Promise<boolean | undefined>) | undefined;
21
22
 
22
23
  /**
23
24
  * Create an instance of the service provider with the specified details.
24
25
  *
25
26
  * @param servicePriority the priority of the service.
26
27
  * @param printerService the PrinterService implementation to use.
28
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
27
29
  */
28
- public constructor(servicePriority: number, printerService: PrinterService) {
30
+ public constructor(
31
+ servicePriority: number,
32
+ printerService: PrinterService,
33
+ detectDarkBackground?: () => Promise<boolean | undefined>,
34
+ ) {
29
35
  this.servicePriority = servicePriority;
30
36
  this.printerService = printerService;
37
+ this.detectDarkBackground = detectDarkBackground;
31
38
  }
32
39
 
33
40
  public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
@@ -35,8 +42,8 @@ export default class PrinterServiceProvider implements ServiceProvider {
35
42
  service: this.printerService,
36
43
  commands: [
37
44
  new NoHyperlinksCommand(this.servicePriority + 3),
38
- new DarkModeCommand(this.servicePriority + 2),
39
- new NoColorCommand(this.servicePriority + 1),
45
+ new ThemeCommand(this.servicePriority + 1, this.detectDarkBackground),
46
+ new NoColorCommand(this.servicePriority + 2),
40
47
  new LogLevelCommand(this.servicePriority),
41
48
  ],
42
49
  });
@@ -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
- }