busybar-kit 0.1.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 (70) hide show
  1. package/README.md +124 -0
  2. package/configs/eslint.config.js +58 -0
  3. package/configs/prettier.json +5 -0
  4. package/configs/tsconfig.base.json +20 -0
  5. package/dist/colors.d.ts +18 -0
  6. package/dist/colors.d.ts.map +1 -0
  7. package/dist/colors.js +17 -0
  8. package/dist/colors.js.map +1 -0
  9. package/dist/config/bar.d.ts +51 -0
  10. package/dist/config/bar.d.ts.map +1 -0
  11. package/dist/config/bar.js +65 -0
  12. package/dist/config/bar.js.map +1 -0
  13. package/dist/config/env.d.ts +15 -0
  14. package/dist/config/env.d.ts.map +1 -0
  15. package/dist/config/env.js +36 -0
  16. package/dist/config/env.js.map +1 -0
  17. package/dist/config/index.d.ts +3 -0
  18. package/dist/config/index.d.ts.map +1 -0
  19. package/dist/config/index.js +3 -0
  20. package/dist/config/index.js.map +1 -0
  21. package/dist/device.d.ts +48 -0
  22. package/dist/device.d.ts.map +1 -0
  23. package/dist/device.js +67 -0
  24. package/dist/device.js.map +1 -0
  25. package/dist/elements.d.ts +14 -0
  26. package/dist/elements.d.ts.map +1 -0
  27. package/dist/elements.js +33 -0
  28. package/dist/elements.js.map +1 -0
  29. package/dist/errors.d.ts +11 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +53 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/format.d.ts +11 -0
  34. package/dist/format.d.ts.map +1 -0
  35. package/dist/format.js +66 -0
  36. package/dist/format.js.map +1 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +9 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/preview/font.d.ts +7 -0
  42. package/dist/preview/font.d.ts.map +1 -0
  43. package/dist/preview/font.js +75 -0
  44. package/dist/preview/font.js.map +1 -0
  45. package/dist/preview/index.d.ts +4 -0
  46. package/dist/preview/index.d.ts.map +1 -0
  47. package/dist/preview/index.js +4 -0
  48. package/dist/preview/index.js.map +1 -0
  49. package/dist/preview/png.d.ts +18 -0
  50. package/dist/preview/png.d.ts.map +1 -0
  51. package/dist/preview/png.js +122 -0
  52. package/dist/preview/png.js.map +1 -0
  53. package/dist/preview/raster.d.ts +6 -0
  54. package/dist/preview/raster.d.ts.map +1 -0
  55. package/dist/preview/raster.js +97 -0
  56. package/dist/preview/raster.js.map +1 -0
  57. package/dist/ticker/index.d.ts +3 -0
  58. package/dist/ticker/index.d.ts.map +1 -0
  59. package/dist/ticker/index.js +3 -0
  60. package/dist/ticker/index.js.map +1 -0
  61. package/dist/ticker/text.d.ts +15 -0
  62. package/dist/ticker/text.d.ts.map +1 -0
  63. package/dist/ticker/text.js +97 -0
  64. package/dist/ticker/text.js.map +1 -0
  65. package/dist/ticker/ticker.d.ts +22 -0
  66. package/dist/ticker/ticker.d.ts.map +1 -0
  67. package/dist/ticker/ticker.js +33 -0
  68. package/dist/ticker/ticker.js.map +1 -0
  69. package/package.json +75 -0
  70. package/scripts/fix-esm-extensions.mjs +33 -0
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # busybar-kit
2
+
3
+ Shared building blocks for [BUSY Bar](https://busy.bar) apps. Everything here is
4
+ app-agnostic: screen geometry, the palette neutrals, formatters, ticker text,
5
+ the Bar's error taxonomy, the connection half of a config, and an offline PNG
6
+ renderer that draws the same elements the device does.
7
+
8
+ It is the common half of [busybar-dota](https://github.com/viirtualp1/busybar-dota),
9
+ [busybar-mydota](https://github.com/viirtualp1/busybar-mydota) and
10
+ [busybar-livesplit](https://github.com/viirtualp1/busybar-livesplit).
11
+
12
+ ```bash
13
+ npm install busybar-kit
14
+ ```
15
+
16
+ `@busy-app/busy-lib` is a peer dependency — the app owns that version.
17
+
18
+ ## What is in it
19
+
20
+ | Import | What it gives you |
21
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
22
+ | `busybar-kit/device` | `FRONT` / `BACK` screen sizes, `FONT_WIDTH`, `textWidth`, `clipToWidth`, `fittingChars`, `rowY`, `backRowGrid` |
23
+ | `busybar-kit/colors` | `BASE_COLORS` — neutrals, chrome, and the transparent value that hides an element |
24
+ | `busybar-kit/format` | `formatClock`, `formatGold`, `formatCountdown`, `formatStartTime`, `formatKda`, `formatPercent`, … |
25
+ | `busybar-kit/elements` | `AnyElement`, `band()`, `fillWidth()` |
26
+ | `busybar-kit/ticker` | `paginate` / `pageAt` / `scrollAt` / `tickerLine`, and `EventTicker` |
27
+ | `busybar-kit/errors` | `BarApiError`, `toBarError`, `isLowPriority`, `isClientError`, `isForbidden` |
28
+ | `busybar-kit/config` | `loadEnvFile`, `envReader`, `loadBarConfig` |
29
+ | `busybar-kit/preview` | `renderFront` / `renderBack` / `Bitmap` — device-accurate PNGs with no hardware |
30
+ | `busybar-kit/tsconfig.json`, `busybar-kit/eslint`, `busybar-kit/prettier` | the shared toolchain |
31
+ | `busybar-fix-esm` (bin) | rewrites relative imports in `dist/` to carry `.js`, for Node ESM |
32
+
33
+ ## Geometry is hardware, not layout
34
+
35
+ `FRONT` (72×16) and `BACK` (160×80, with its header rows and 12px grid) are
36
+ facts about the device. An app spreads its own named slots on top:
37
+
38
+ ```ts
39
+ import { FRONT as DEVICE, BACK as DEVICE_BACK } from 'busybar-kit/device';
40
+
41
+ export const FRONT = {
42
+ ...DEVICE,
43
+ topY: 0,
44
+ bottomY: 11,
45
+ clockWidth: 24,
46
+ } as const;
47
+ ```
48
+
49
+ An app whose header is a different height lays out its own row grid, and reads
50
+ rows off that instead of the default:
51
+
52
+ ```ts
53
+ import { backRowGrid, rowY as gridRowY } from 'busybar-kit/device';
54
+
55
+ export const BACK = { ...DEVICE_BACK, ...backRowGrid(16), nameX: 8 } as const;
56
+
57
+ export const rowY = (index: number) => gridRowY(index, BACK);
58
+ ```
59
+
60
+ `clipToWidth` takes the truncation mark as a fourth argument (`'.'` by
61
+ default, `'..'` where names are cut often enough to want the emphasis).
62
+
63
+ Colours work the same way — `BASE_COLORS` holds what every app needs, and the
64
+ app adds its own semantics:
65
+
66
+ ```ts
67
+ import { BASE_COLORS } from 'busybar-kit/colors';
68
+
69
+ export const COLORS = {
70
+ ...BASE_COLORS,
71
+ radiant: '#3FBF5FFF',
72
+ dire: '#E14B3AFF',
73
+ } as const;
74
+ ```
75
+
76
+ ## Config
77
+
78
+ `loadBarConfig` resolves which Bar to talk to and over which transport, and
79
+ warns about credentials that transport will ignore. That is all it covers —
80
+ render cadence and request timeouts are the app's own tempo (a speedrun timer
81
+ wants 60ms frames where a match ticker is happy at 200), so they stay in the
82
+ app's config with `DEFAULTS` / `LIMITS` as the values worth starting from.
83
+
84
+ It hands back the same env reader, so an app's own settings collect their
85
+ warnings into one list:
86
+
87
+ ```ts
88
+ import { loadBarConfig, loadEnvFile } from 'busybar-kit/config';
89
+
90
+ loadEnvFile();
91
+
92
+ export function loadConfig(env = process.env) {
93
+ const warnings: string[] = [];
94
+ const { bar, env: read } = loadBarConfig(env, warnings);
95
+
96
+ return {
97
+ warnings,
98
+ config: {
99
+ ...bar,
100
+ gsiPort: read.number('GSI_PORT', 3080, { min: 1024, max: 65_535 }),
101
+ },
102
+ };
103
+ }
104
+ ```
105
+
106
+ ## Preview
107
+
108
+ `renderFront` / `renderBack` rasterise the very element list you send to the
109
+ device — a 3×5 pixel font, the same anchoring rules, and the back panel
110
+ flattened to the 16 greys the real OLED shows. Useful for screenshots in a
111
+ README and for asserting a layout in tests without a Bar on the desk.
112
+
113
+ ```ts
114
+ import { renderFront } from 'busybar-kit/preview';
115
+
116
+ writeFileSync('front.png', renderFront(frontElements(frame)).scale(8).toPng());
117
+ ```
118
+
119
+ ## Scripts
120
+
121
+ ```bash
122
+ npm run check # lint + typecheck + test
123
+ npm run build # tsc + .js extension fixup
124
+ ```
@@ -0,0 +1,58 @@
1
+ import js from '@eslint/js';
2
+ import prettier from 'eslint-config-prettier';
3
+ import tseslint from 'typescript-eslint';
4
+
5
+ /**
6
+ * The lint rules every BUSY Bar app shares. `rootDir` is the app's own
7
+ * directory, so type-aware rules read that app's tsconfig:
8
+ *
9
+ * import { barEslintConfig } from 'busybar-kit/eslint';
10
+ * export default barEslintConfig(import.meta.dirname);
11
+ */
12
+ export function barEslintConfig(rootDir, ...extra) {
13
+ return tseslint.config(
14
+ { ignores: ['dist/**', 'node_modules/**', 'scripts/**'] },
15
+ js.configs.recommended,
16
+ ...tseslint.configs.recommendedTypeChecked,
17
+ {
18
+ languageOptions: {
19
+ parserOptions: {
20
+ project: ['./tsconfig.json'],
21
+ tsconfigRootDir: rootDir,
22
+ },
23
+ },
24
+ rules: {
25
+ '@typescript-eslint/no-unused-vars': [
26
+ 'error',
27
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
28
+ ],
29
+ '@typescript-eslint/consistent-type-imports': [
30
+ 'error',
31
+ { fixStyle: 'inline-type-imports' },
32
+ ],
33
+ '@typescript-eslint/no-floating-promises': 'error',
34
+ '@typescript-eslint/switch-exhaustiveness-check': 'error',
35
+ eqeqeq: ['error', 'always'],
36
+ 'no-console': 'off',
37
+ 'prefer-const': 'error',
38
+ },
39
+ },
40
+ {
41
+ files: ['test/**/*.ts'],
42
+ rules: {
43
+ '@typescript-eslint/no-non-null-assertion': 'off',
44
+ // node:test returns a promise that is meant to be left alone
45
+ '@typescript-eslint/no-floating-promises': 'off',
46
+ '@typescript-eslint/require-await': 'off',
47
+ },
48
+ },
49
+ {
50
+ files: ['**/*.js'],
51
+ extends: [tseslint.configs.disableTypeChecked],
52
+ },
53
+ prettier,
54
+ ...extra,
55
+ );
56
+ }
57
+
58
+ export default barEslintConfig(import.meta.dirname);
@@ -0,0 +1,5 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "printWidth": 90
5
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "esnext",
4
+ "moduleResolution": "bundler",
5
+ "target": "esnext",
6
+ "lib": ["esnext"],
7
+ "types": ["node"],
8
+ "noEmit": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "strict": true,
12
+ "noImplicitOverride": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "verbatimModuleSyntax": true,
15
+ "isolatedModules": true,
16
+ "noUncheckedSideEffectImports": true,
17
+ "moduleDetection": "force",
18
+ "skipLibCheck": true
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The palette every app shares: neutrals, chrome and the transparent value the
3
+ * Bar uses to hide an element. App-specific semantics (team colours, ahead/
4
+ * behind, gold) are spread on top of this in the app's own colours module.
5
+ */
6
+ export declare const BASE_COLORS: {
7
+ readonly white: "#FFFFFFFF";
8
+ readonly transparent: "#00000000";
9
+ readonly muted: "#9AA0A6FF";
10
+ readonly dim: "#6B7075FF";
11
+ readonly backText: "#E6E6E6FF";
12
+ readonly backDivider: "#5A5A5AFF";
13
+ readonly clock: "#D8DCE0FF";
14
+ readonly ticker: "#FFFFFFFF";
15
+ readonly panelDark: "#000000FF";
16
+ };
17
+ export type BaseColor = keyof typeof BASE_COLORS;
18
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,WAAW,CAAC"}
package/dist/colors.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The palette every app shares: neutrals, chrome and the transparent value the
3
+ * Bar uses to hide an element. App-specific semantics (team colours, ahead/
4
+ * behind, gold) are spread on top of this in the app's own colours module.
5
+ */
6
+ export const BASE_COLORS = {
7
+ white: '#FFFFFFFF',
8
+ transparent: '#00000000',
9
+ muted: '#9AA0A6FF',
10
+ dim: '#6B7075FF',
11
+ backText: '#E6E6E6FF',
12
+ backDivider: '#5A5A5AFF',
13
+ clock: '#D8DCE0FF',
14
+ ticker: '#FFFFFFFF',
15
+ panelDark: '#000000FF',
16
+ };
17
+ //# sourceMappingURL=colors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.js","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,KAAK,EAAE,WAAW;IAClB,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,WAAW;IAClB,GAAG,EAAE,WAAW;IAChB,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,WAAW;IAClB,MAAM,EAAE,WAAW;IACnB,SAAS,EAAE,WAAW;CACd,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { type EnvReader } from './env.js';
2
+ export declare const DEFAULTS: {
3
+ readonly usbAddr: "10.0.4.20";
4
+ readonly cloudAddr: "https://api.busy.app";
5
+ readonly drawPriority: 40;
6
+ readonly frameMs: 200;
7
+ readonly requestTimeoutMs: 10000;
8
+ };
9
+ export declare const LIMITS: {
10
+ readonly drawPriority: {
11
+ readonly min: 0;
12
+ readonly max: 100;
13
+ };
14
+ readonly frameMs: {
15
+ readonly min: 50;
16
+ readonly max: 2000;
17
+ };
18
+ readonly requestTimeoutMs: {
19
+ readonly min: 1000;
20
+ readonly max: 30000;
21
+ };
22
+ };
23
+ /**
24
+ * How an app reaches the Bar, and nothing else. Render cadence and request
25
+ * timeouts are the app's own tempo — a speedrun timer wants 60ms frames where
26
+ * a match ticker is happy at 200 — so they stay in the app's config, with
27
+ * `DEFAULTS` / `LIMITS` here as the values worth starting from.
28
+ */
29
+ export type BarConfig = {
30
+ busyAddr: string;
31
+ isCloud: boolean;
32
+ isUsb: boolean;
33
+ busyToken: string;
34
+ busyHttpPassword: string;
35
+ drawPriority: number;
36
+ };
37
+ export declare function isCloudAddr(addr: string): boolean;
38
+ export declare function isUsbAddr(addr: string): boolean;
39
+ export type LoadedBarConfig = {
40
+ bar: BarConfig;
41
+ warnings: string[];
42
+ env: EnvReader;
43
+ };
44
+ /**
45
+ * Resolves the connection half of an app's config: which Bar, over which
46
+ * transport, with which credential — and warns about credentials that will be
47
+ * ignored on the transport in use. Apps build their own config on top, reusing
48
+ * the returned reader so their warnings land in the same list.
49
+ */
50
+ export declare function loadBarConfig(env?: NodeJS.ProcessEnv, warnings?: string[]): LoadedBarConfig;
51
+ //# sourceMappingURL=bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bar.d.ts","sourceRoot":"","sources":["../../src/config/bar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,eAAO,MAAM,QAAQ;;;;;;CAMX,CAAC;AAEX,eAAO,MAAM,MAAM;;;;;;;;;;;;;CAIT,CAAC;AAEX;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,WAEvC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,WAOrC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,SAAS,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,EAAE,SAAS,CAAC;CAChB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,MAAM,EAAO,GACtB,eAAe,CA0CjB"}
@@ -0,0 +1,65 @@
1
+ import { envReader } from './env.js';
2
+ export const DEFAULTS = {
3
+ usbAddr: '10.0.4.20',
4
+ cloudAddr: 'https://api.busy.app',
5
+ drawPriority: 40,
6
+ frameMs: 200,
7
+ requestTimeoutMs: 10_000,
8
+ };
9
+ export const LIMITS = {
10
+ drawPriority: { min: 0, max: 100 },
11
+ frameMs: { min: 50, max: 2000 },
12
+ requestTimeoutMs: { min: 1000, max: 30_000 },
13
+ };
14
+ export function isCloudAddr(addr) {
15
+ return /api(?:\.(?:dev|test|stage))?\.busy\.app/i.test(addr);
16
+ }
17
+ export function isUsbAddr(addr) {
18
+ try {
19
+ const url = /^https?:\/\//i.test(addr) ? new URL(addr) : new URL(`http://${addr}`);
20
+ return url.hostname === DEFAULTS.usbAddr;
21
+ }
22
+ catch {
23
+ return addr.includes(DEFAULTS.usbAddr);
24
+ }
25
+ }
26
+ /**
27
+ * Resolves the connection half of an app's config: which Bar, over which
28
+ * transport, with which credential — and warns about credentials that will be
29
+ * ignored on the transport in use. Apps build their own config on top, reusing
30
+ * the returned reader so their warnings land in the same list.
31
+ */
32
+ export function loadBarConfig(env = process.env, warnings = []) {
33
+ const reader = envReader(env, warnings);
34
+ const { read, number } = reader;
35
+ const token = read('BUSY_TOKEN');
36
+ const httpPassword = read('BUSY_HTTP_PASSWORD');
37
+ const busyAddr = read('BUSY_ADDR') || (token ? DEFAULTS.cloudAddr : DEFAULTS.usbAddr);
38
+ const cloud = isCloudAddr(busyAddr);
39
+ const usb = isUsbAddr(busyAddr);
40
+ if (cloud && httpPassword) {
41
+ warnings.push('BUSY_HTTP_PASSWORD is ignored on cloud, only BUSY_TOKEN is used');
42
+ }
43
+ if (!cloud && token) {
44
+ warnings.push(`BUSY_TOKEN is ignored for ${busyAddr}, that token only works on cloud`);
45
+ }
46
+ if (usb && httpPassword) {
47
+ warnings.push('BUSY_HTTP_PASSWORD is ignored over USB, no auth is required there');
48
+ }
49
+ if (!cloud && !usb && !httpPassword) {
50
+ warnings.push('Wi-Fi needs BUSY_HTTP_PASSWORD (Bar web UI → Network → HTTP API access)');
51
+ }
52
+ return {
53
+ warnings,
54
+ env: reader,
55
+ bar: {
56
+ busyAddr,
57
+ isCloud: cloud,
58
+ isUsb: usb,
59
+ busyToken: cloud ? token : '',
60
+ busyHttpPassword: cloud || usb ? '' : httpPassword,
61
+ drawPriority: number('DRAW_PRIORITY', DEFAULTS.drawPriority, LIMITS.drawPriority),
62
+ },
63
+ };
64
+ }
65
+ //# sourceMappingURL=bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bar.js","sourceRoot":"","sources":["../../src/config/bar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export type Limits = {
2
+ min: number;
3
+ max: number;
4
+ };
5
+ export declare function loadEnvFile(cwd?: string): void;
6
+ export type EnvReader = {
7
+ read: (name: string) => string;
8
+ number: (name: string, fallback: number, limits: Limits, round?: boolean) => number;
9
+ };
10
+ /**
11
+ * Reads settings out of the environment, clamping numbers into range and
12
+ * collecting a human-readable warning for anything it had to correct.
13
+ */
14
+ export declare function envReader(env: NodeJS.ProcessEnv, warnings: string[]): EnvReader;
15
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,wBAAgB,WAAW,CAAC,GAAG,SAAgB,QAO9C;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;CACrF,CAAC;AAEF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,CAwB/E"}
@@ -0,0 +1,36 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ export function loadEnvFile(cwd = process.cwd()) {
4
+ const path = resolve(cwd, '.env');
5
+ if (!existsSync(path)) {
6
+ return;
7
+ }
8
+ // Real environment variables win over the file, same as `node --env-file`.
9
+ process.loadEnvFile(path);
10
+ }
11
+ /**
12
+ * Reads settings out of the environment, clamping numbers into range and
13
+ * collecting a human-readable warning for anything it had to correct.
14
+ */
15
+ export function envReader(env, warnings) {
16
+ const read = (name) => env[name]?.trim() ?? '';
17
+ const number = (name, fallback, limits, round = true) => {
18
+ const raw = read(name);
19
+ if (!raw) {
20
+ return fallback;
21
+ }
22
+ const value = Number(raw);
23
+ if (!Number.isFinite(value)) {
24
+ warnings.push(`${name}=${raw} is not a number, using ${fallback}`);
25
+ return fallback;
26
+ }
27
+ const wanted = round ? Math.round(value) : value;
28
+ const clamped = Math.min(limits.max, Math.max(limits.min, wanted));
29
+ if (clamped !== value) {
30
+ warnings.push(`${name}=${raw} is out of range ${limits.min}..${limits.max}, using ${clamped}`);
31
+ }
32
+ return clamped;
33
+ };
34
+ return { read, number };
35
+ }
36
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,MAAM,UAAU,WAAW,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO;IACT,CAAC;IACD,2EAA2E;IAC3E,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,GAAsB,EAAE,QAAkB;IAClE,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAEvD,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAE,MAAc,EAAE,KAAK,GAAG,IAAI,EAAE,EAAE;QAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,2BAA2B,QAAQ,EAAE,CAAC,CAAC;YACnE,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;QACnE,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CACX,GAAG,IAAI,IAAI,GAAG,oBAAoB,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,GAAG,WAAW,OAAO,EAAE,CAChF,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './env.js';
2
+ export * from './bar.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './env.js';
2
+ export * from './bar.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Physical facts about the BUSY Bar screens. These are hardware, not app
3
+ * choices: every app draws into the same 72x16 front strip and 160x80 back
4
+ * panel. Per-app named slots (where the clock sits, how wide a hero column is)
5
+ * belong in the app's own layout module, spread on top of these.
6
+ */
7
+ export declare const FRONT: {
8
+ readonly width: 72;
9
+ readonly height: 16;
10
+ };
11
+ export declare const BACK: {
12
+ readonly width: 160;
13
+ readonly height: 80;
14
+ readonly headerY: 2;
15
+ readonly subHeaderY: 9;
16
+ readonly firstRowY: 18;
17
+ readonly rowHeight: 12;
18
+ readonly maxRows: number;
19
+ readonly leftX: 2;
20
+ readonly rightX: 82;
21
+ readonly columnWidth: 76;
22
+ };
23
+ export type BarFont = 'tiny' | 'small' | 'bold';
24
+ /**
25
+ * Upper bound per glyph. The device fonts may be narrower, so clipping is
26
+ * conservative: text can come out shorter than strictly necessary, never wider
27
+ * than its box.
28
+ */
29
+ export declare const FONT_WIDTH: Record<BarFont, number>;
30
+ export declare function textWidth(text: string, font: BarFont): number;
31
+ export declare function clipToWidth(text: string, widthPx: number, font: BarFont, ellipsis?: string): string;
32
+ export declare function fittingChars(widthPx: number, glyphWidth: number): number;
33
+ export type RowGeometry = {
34
+ firstRowY: number;
35
+ rowHeight: number;
36
+ };
37
+ /** Top edge of back-panel row `index`. Apps with a different grid pass their own. */
38
+ export declare function rowY(index: number, geometry?: RowGeometry): number;
39
+ /**
40
+ * A back-panel row grid: how many rows of `rowHeight` fit below `firstRowY`.
41
+ * An app with a taller header (or none) starts its rows somewhere else.
42
+ */
43
+ export declare function backRowGrid(firstRowY: number, rowHeight?: number, height?: number): {
44
+ readonly firstRowY: number;
45
+ readonly rowHeight: number;
46
+ readonly maxRows: number;
47
+ };
48
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,KAAK;;;CAGR,CAAC;AAMX,eAAO,MAAM,IAAI;;;;;;;;;;;CAWP,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAI9C,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,UAEpD;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,OAAO,EACb,QAAQ,SAAM,UAYf;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,UAE/D;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE,qFAAqF;AACrF,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,WAAkB,UAE/D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAAuB,EAClC,MAAM,GAAE,MAAoB;;;;EAO7B"}
package/dist/device.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Physical facts about the BUSY Bar screens. These are hardware, not app
3
+ * choices: every app draws into the same 72x16 front strip and 160x80 back
4
+ * panel. Per-app named slots (where the clock sits, how wide a hero column is)
5
+ * belong in the app's own layout module, spread on top of these.
6
+ */
7
+ export const FRONT = {
8
+ width: 72,
9
+ height: 16,
10
+ };
11
+ const BACK_HEIGHT = 80;
12
+ const BACK_FIRST_ROW_Y = 18;
13
+ const BACK_ROW_HEIGHT = 12;
14
+ export const BACK = {
15
+ width: 160,
16
+ height: BACK_HEIGHT,
17
+ headerY: 2,
18
+ subHeaderY: 9,
19
+ firstRowY: BACK_FIRST_ROW_Y,
20
+ rowHeight: BACK_ROW_HEIGHT,
21
+ maxRows: Math.floor((BACK_HEIGHT - BACK_FIRST_ROW_Y) / BACK_ROW_HEIGHT),
22
+ leftX: 2,
23
+ rightX: 82,
24
+ columnWidth: 76,
25
+ };
26
+ /**
27
+ * Upper bound per glyph. The device fonts may be narrower, so clipping is
28
+ * conservative: text can come out shorter than strictly necessary, never wider
29
+ * than its box.
30
+ */
31
+ export const FONT_WIDTH = {
32
+ tiny: 4,
33
+ small: 4,
34
+ bold: 8,
35
+ };
36
+ export function textWidth(text, font) {
37
+ return text.length * FONT_WIDTH[font];
38
+ }
39
+ export function clipToWidth(text, widthPx, font, ellipsis = '.') {
40
+ const maxChars = Math.max(1, Math.floor(widthPx / FONT_WIDTH[font]));
41
+ if (text.length <= maxChars) {
42
+ return text;
43
+ }
44
+ if (maxChars <= 2 || maxChars <= ellipsis.length) {
45
+ return text.slice(0, maxChars);
46
+ }
47
+ return `${text.slice(0, maxChars - ellipsis.length)}${ellipsis}`;
48
+ }
49
+ export function fittingChars(widthPx, glyphWidth) {
50
+ return Math.max(0, Math.floor(widthPx / glyphWidth));
51
+ }
52
+ /** Top edge of back-panel row `index`. Apps with a different grid pass their own. */
53
+ export function rowY(index, geometry = BACK) {
54
+ return geometry.firstRowY + index * geometry.rowHeight;
55
+ }
56
+ /**
57
+ * A back-panel row grid: how many rows of `rowHeight` fit below `firstRowY`.
58
+ * An app with a taller header (or none) starts its rows somewhere else.
59
+ */
60
+ export function backRowGrid(firstRowY, rowHeight = BACK.rowHeight, height = BACK.height) {
61
+ return {
62
+ firstRowY,
63
+ rowHeight,
64
+ maxRows: Math.floor((height - firstRowY) / rowHeight),
65
+ };
66
+ }
67
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;CACF,CAAC;AAEX,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,gBAAgB;IAC3B,SAAS,EAAE,eAAe;IAC1B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,gBAAgB,CAAC,GAAG,eAAe,CAAC;IACvE,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,EAAE;IACV,WAAW,EAAE,EAAE;CACP,CAAC;AAIX;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAA4B;IACjD,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;CACR,CAAC;AAEF,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAa;IACnD,OAAO,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,OAAe,EACf,IAAa,EACb,QAAQ,GAAG,GAAG;IAEd,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrE,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,QAAQ,IAAI,CAAC,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACjD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,UAAkB;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC;AACvD,CAAC;AAID,qFAAqF;AACrF,MAAM,UAAU,IAAI,CAAC,KAAa,EAAE,WAAwB,IAAI;IAC9D,OAAO,QAAQ,CAAC,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,SAAiB,EACjB,YAAoB,IAAI,CAAC,SAAS,EAClC,SAAiB,IAAI,CAAC,MAAM;IAE5B,OAAO;QACL,SAAS;QACT,SAAS;QACT,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;KAC7C,CAAC;AACb,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { RectangleElement, TextElement } from '@busy-app/busy-lib';
2
+ export type AnyElement = TextElement | RectangleElement;
3
+ /**
4
+ * A full-height coloured band across part of the front strip. Both Dota apps
5
+ * use one pair of these to show who is ahead.
6
+ */
7
+ export declare function band(id: string, x: number, width: number, color: string, height?: number): RectangleElement;
8
+ export declare const MIN_FILL = 6;
9
+ /**
10
+ * Split a strip between two sides by `share` (0..1), always leaving both
11
+ * colours visible so the bar never reads as a single solid block.
12
+ */
13
+ export declare function fillWidth(share: number, width?: number): number;
14
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIxE,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAExD;;;GAGG;AACH,wBAAgB,IAAI,CAClB,EAAE,EAAE,MAAM,EACV,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAqB,GAC5B,gBAAgB,CAgBlB;AAED,eAAO,MAAM,QAAQ,IAAI,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAoB,UAInE"}
@@ -0,0 +1,33 @@
1
+ import { BASE_COLORS } from './colors.js';
2
+ import { FRONT } from './device.js';
3
+ /**
4
+ * A full-height coloured band across part of the front strip. Both Dota apps
5
+ * use one pair of these to show who is ahead.
6
+ */
7
+ export function band(id, x, width, color, height = FRONT.height) {
8
+ return {
9
+ id,
10
+ type: 'rectangle',
11
+ display: 'front',
12
+ align: 'top_left',
13
+ x,
14
+ y: 0,
15
+ width: Math.max(1, width),
16
+ height,
17
+ fill: 'solid',
18
+ fill_colors: [color],
19
+ border_width: 0,
20
+ border_color: BASE_COLORS.transparent,
21
+ timeout: 0,
22
+ };
23
+ }
24
+ export const MIN_FILL = 6;
25
+ /**
26
+ * Split a strip between two sides by `share` (0..1), always leaving both
27
+ * colours visible so the bar never reads as a single solid block.
28
+ */
29
+ export function fillWidth(share, width = FRONT.width) {
30
+ const raw = Math.round(Math.max(0, Math.min(1, share)) * width);
31
+ return Math.max(MIN_FILL, Math.min(width - MIN_FILL, raw));
32
+ }
33
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAIjC;;;GAGG;AACH,MAAM,UAAU,IAAI,CAClB,EAAU,EACV,CAAS,EACT,KAAa,EACb,KAAa,EACb,SAAiB,KAAK,CAAC,MAAM;IAE7B,OAAO;QACL,EAAE;QACF,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE,UAAU;QACjB,CAAC;QACD,CAAC,EAAE,CAAC;QACJ,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC;QACzB,MAAM;QACN,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,CAAC,KAAK,CAAC;QACpB,YAAY,EAAE,CAAC;QACf,YAAY,EAAE,WAAW,CAAC,WAAW;QACrC,OAAO,EAAE,CAAC;KACX,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC;AAE1B;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,QAAgB,KAAK,CAAC,KAAK;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAEhE,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;AAC7D,CAAC"}