gulp-mu-gulp-api 0.2.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 (3) hide show
  1. package/README.md +245 -0
  2. package/package.json +46 -0
  3. package/src/index.mjs +322 -0
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # gulp-mu-gulp-api
2
+
3
+ Public task API of the **µGulp™** orchestrator — progress reporting and interactive dashboard inputs from within gulp tasks.
4
+
5
+ Part of the [µGulp™ project on GitHub](https://github.com/mamekudz/microGulp) (subdirectory [`gulp-mu-gulp-api/`](https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api)).
6
+
7
+ [English](#english) · [Deutsch](#deutsch)
8
+
9
+ ---
10
+
11
+ # English
12
+
13
+ **gulp-mu-gulp-api** gives gulp tasks (and any npm package inside the task stream) access to the extended µGulp™ dashboard features: progress bars, validated text inputs, color and font pickers, single and multi selections, sliders and complete forms.
14
+
15
+ The module is deliberately **dependency-free** and knows nothing about µGulp™ internals: when the task runs under µGulp™, it talks directly to the engine over the worker process IPC channel. When the gulpfile runs through the classic gulp CLI, every function degrades gracefully — progress renders on the terminal, inputs are asked via readline (TTY) or answered with the declared defaults (CI/non-TTY). Tasks stay fully runnable without µGulp™.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install gulp-mu-gulp-api
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```javascript
26
+ import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsUGulp } from 'gulp-mu-gulp-api';
27
+
28
+ export async function BUILD_THEME() {
29
+ // single inputs
30
+ let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
31
+ let font = await RequestFontInput({ label: 'Headline font', options: ['Segoe UI', 'Cascadia Mono'] });
32
+ let title = await RequestTextInput({
33
+ label: 'Product title',
34
+ validate: { required: true, minLength: 3, pattern: '^[A-Za-z0-9 ]+$' },
35
+ });
36
+ let servers = await RequestMultiSelectInput({
37
+ label: 'Rollout servers',
38
+ options: [{ value: 'SMPMD', checked: true }, 'iMedOne', 'ACM'],
39
+ });
40
+
41
+ // progress (determinate neon bar in the µGulp dashboard)
42
+ let progress = CreateProgress('compiling theme');
43
+ for (let step = 1; step <= 10; step++) {
44
+ // ... work ...
45
+ progress.Update(step / 10);
46
+ }
47
+ progress.Done();
48
+
49
+ // sound + speech (rendered by the dashboard, configured in its settings)
50
+ PlaySignal('success');
51
+ Speak('Theme build finished.');
52
+ }
53
+ ```
54
+
55
+ Complete forms in one go:
56
+
57
+ ```javascript
58
+ let values = await RequestForm({
59
+ title: 'Build parameters',
60
+ fields: [
61
+ { id: 'mode', type: 'radio', label: 'Build mode', default: 'release', options: ['debug', 'release'] },
62
+ { id: 'threads', type: 'range', label: 'Worker threads', min: 1, max: 16, step: 1, default: 4 },
63
+ { id: 'notes', type: 'textarea', label: 'Release notes', rows: 4, placeholder: 'What changed?' },
64
+ { id: 'apikey', type: 'password', label: 'API key', validate: { required: true } },
65
+ { id: 'targets', type: 'checkbox', label: 'Targets', options: ['Alpha', 'Beta', 'Gamma'], validate: { minSelected: 1 } },
66
+ ],
67
+ });
68
+ // values => { mode: 'release', threads: '8', notes: '…', apikey: '…', targets: ['Alpha', 'Gamma'] }
69
+ ```
70
+
71
+ ## API reference
72
+
73
+ | Function | Purpose |
74
+ | :--- | :--- |
75
+ | `IsUGulp()` | `true` when the task runs under µGulp™ with an attached dashboard |
76
+ | `ReportProgress(value, label?)` | Report progress `0..1` to the task progress bar |
77
+ | `CreateProgress(label?)` | Reporter object with `Update(value, stepLabel?)` and `Done()` |
78
+ | `PlaySignal(signal?)` | Acoustic signal in the dashboard: `'success'` \| `'error'` \| `'attention'` (default) |
79
+ | `Speak(text, options?)` | Speech output in the dashboard; `options`: `{ rate?, pitch?, volume? }` per-call overrides |
80
+ | `RequestForm(form)` | Request a complete form; resolves with `{ fieldId: value, … }` |
81
+ | `RequestTextInput(options)` | Single text field (with `validate` rules) |
82
+ | `RequestColorInput(options)` | Color picker, returns `#rrggbb` |
83
+ | `RequestFontInput(options)` | Font selection (`options`: font list) |
84
+ | `RequestSelectInput(options)` | Single selection from a fixed option list |
85
+ | `RequestMultiSelectInput(options)` | Multi selection (checkbox group), always resolves to a `string[]` |
86
+
87
+ ### Field types (for `RequestForm`)
88
+
89
+ A field descriptor is `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
90
+
91
+ | `type` | Dashboard control | Notes |
92
+ | :--- | :--- | :--- |
93
+ | `text` | text field | default type |
94
+ | `password` | masked text field | |
95
+ | `number` | number field | honors `min`, `max`, `step` |
96
+ | `textarea` | multi-line text field | `rows`, `placeholder` |
97
+ | `color` | native color picker | value as `#rrggbb` |
98
+ | `font` | font selection | `options`: font family names |
99
+ | `select` | dropdown | single choice |
100
+ | `radio` | radio group | single exclusive choice, vertical option column |
101
+ | `checkbox` | checkbox group | **multi-select** — resolves to a `string[]` |
102
+ | `range` | slider + synced number box | `min`, `max`, `step`, `default` |
103
+ | `date` / `time` / `datetime` | native date/time pickers | |
104
+ | `file` | file selection | |
105
+
106
+ **Options** (`select`, `radio`, `checkbox`) accept plain strings or objects `{ value, label?, checked?, disabled? }` — `checked` preselects checkbox options, `disabled` greys an option out.
107
+
108
+ **Validation rules:** `{ required?, minLength?, pattern?, minSelected? }` (`minSelected` applies to `checkbox`). Checks run asynchronously in the dashboard; invalid forms cannot be submitted.
109
+
110
+ ## Fallback behavior without µGulp™
111
+
112
+ | Function | TTY (interactive shell) | non-TTY (CI, pipe) |
113
+ | :--- | :--- | :--- |
114
+ | `ReportProgress` | updating progress line | log line every 10% step |
115
+ | `PlaySignal` | terminal bell (`attention`/`error`) | silent |
116
+ | `Speak` | `[speech]` log line | `[speech]` log line |
117
+ | text/password/number/range input | readline prompt (Enter = default) | default value |
118
+ | font/select/radio input | numbered list + readline | default value |
119
+ | checkbox input | numbered list, comma-separated picks | values of the `checked` options |
120
+
121
+ Sound and speech playback (mute, volume, speech rate/pitch) is configured centrally in the µGulp™ dashboard settings (gear icon in the header) — tasks never need to care about the user's audio preferences.
122
+
123
+ ## License
124
+
125
+ MIT — © 2026 Meinolf Amekudzi
126
+
127
+ ---
128
+
129
+ # Deutsch
130
+
131
+ **gulp-mu-gulp-api** ist die öffentliche Task-API des **µGulp™**-Orchestrators. Gulp-Tasks (und beliebige npm-Pakete innerhalb des Task-Streams) erhalten damit Zugriff auf die erweiterten µGulp™-Dashboard-Funktionen: Fortschrittsanzeige, Texteingaben mit Validierung, Farb- und Schriftart-Auswahl, Einzel- und Mehrfachauswahl, Slider und komplette Formulare.
132
+
133
+ Teil des [µGulp™-Projekts auf GitHub](https://github.com/mamekudz/microGulp) (Unterverzeichnis [`gulp-mu-gulp-api/`](https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api)).
134
+
135
+ Das Modul ist bewusst **abhängigkeitsfrei** und kennt keine µGulp™-Interna: Läuft der Task unter µGulp™, spricht es direkt über den IPC-Kanal des Worker-Prozesses mit der Engine. Läuft das Gulpfile klassisch über die gulp-CLI, degradieren alle Funktionen sauber — Fortschritt landet auf dem Terminal, Eingaben werden per readline abgefragt (TTY) oder mit den deklarierten Default-Werten beantwortet (CI/non-TTY). Tasks bleiben ohne µGulp™ voll lauffähig.
136
+
137
+ ## Installation
138
+
139
+ ```bash
140
+ npm install gulp-mu-gulp-api
141
+ ```
142
+
143
+ ## Verwendung
144
+
145
+ ```javascript
146
+ import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsUGulp } from 'gulp-mu-gulp-api';
147
+
148
+ export async function BUILD_THEME() {
149
+ // Einzeleingaben
150
+ let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
151
+ let font = await RequestFontInput({ label: 'Headline font', options: ['Segoe UI', 'Cascadia Mono'] });
152
+ let title = await RequestTextInput({
153
+ label: 'Product title',
154
+ validate: { required: true, minLength: 3, pattern: '^[A-Za-z0-9 ]+$' },
155
+ });
156
+ let servers = await RequestMultiSelectInput({
157
+ label: 'Rollout servers',
158
+ options: [{ value: 'SMPMD', checked: true }, 'iMedOne', 'ACM'],
159
+ });
160
+
161
+ // Fortschritt (determinierte Neon-Progressbar im µGulp-Dashboard)
162
+ let progress = CreateProgress('compiling theme');
163
+ for (let step = 1; step <= 10; step++) {
164
+ // ... Arbeit ...
165
+ progress.Update(step / 10);
166
+ }
167
+ progress.Done();
168
+
169
+ // sound + speech (rendered by the dashboard, configured in its settings)
170
+ PlaySignal('success');
171
+ Speak('Theme build finished.');
172
+ }
173
+ ```
174
+
175
+ Komplette Formulare in einem Rutsch:
176
+
177
+ ```javascript
178
+ let values = await RequestForm({
179
+ title: 'Build parameters',
180
+ fields: [
181
+ { id: 'mode', type: 'radio', label: 'Build mode', default: 'release', options: ['debug', 'release'] },
182
+ { id: 'threads', type: 'range', label: 'Worker threads', min: 1, max: 16, step: 1, default: 4 },
183
+ { id: 'notes', type: 'textarea', label: 'Release notes', rows: 4, placeholder: 'What changed?' },
184
+ { id: 'apikey', type: 'password', label: 'API key', validate: { required: true } },
185
+ { id: 'targets', type: 'checkbox', label: 'Targets', options: ['Alpha', 'Beta', 'Gamma'], validate: { minSelected: 1 } },
186
+ ],
187
+ });
188
+ // values => { mode: 'release', threads: '8', notes: '…', apikey: '…', targets: ['Alpha', 'Gamma'] }
189
+ ```
190
+
191
+ ## API-Referenz
192
+
193
+ | Funktion | Zweck |
194
+ | :--- | :--- |
195
+ | `IsUGulp()` | `true`, wenn der Task unter µGulp™ mit angebundenem Dashboard läuft |
196
+ | `ReportProgress(value, label?)` | Fortschritt `0..1` an die Task-Progressbar melden |
197
+ | `CreateProgress(label?)` | Reporter-Objekt mit `Update(value, stepLabel?)` und `Done()` |
198
+ | `PlaySignal(signal?)` | Akustisches Signal im Dashboard: `'success'` \| `'error'` \| `'attention'` (Default) |
199
+ | `Speak(text, options?)` | Sprachausgabe im Dashboard; `options`: `{ rate?, pitch?, volume? }` je Aufruf |
200
+ | `RequestForm(form)` | Komplettes Formular anfordern; löst mit `{ fieldId: value, … }` auf |
201
+ | `RequestTextInput(options)` | Einzelnes Textfeld (mit `validate`-Regeln) |
202
+ | `RequestColorInput(options)` | Farbwähler, Rückgabe `#rrggbb` |
203
+ | `RequestFontInput(options)` | Schriftart-Auswahl (`options`: Font-Liste) |
204
+ | `RequestSelectInput(options)` | Einzelauswahl aus fester Optionsliste |
205
+ | `RequestMultiSelectInput(options)` | Mehrfachauswahl (Checkbox-Gruppe), löst immer mit `string[]` auf |
206
+
207
+ ### Feldtypen (für `RequestForm`)
208
+
209
+ Ein Feld-Deskriptor ist `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
210
+
211
+ | `type` | Dashboard-Steuerelement | Hinweise |
212
+ | :--- | :--- | :--- |
213
+ | `text` | Textfeld | Standardtyp |
214
+ | `password` | maskiertes Textfeld | |
215
+ | `number` | Zahlenfeld | berücksichtigt `min`, `max`, `step` |
216
+ | `textarea` | mehrzeiliges Textfeld | `rows`, `placeholder` |
217
+ | `color` | nativer Farbwähler | Wert als `#rrggbb` |
218
+ | `font` | Schriftart-Auswahl | `options`: Font-Familiennamen |
219
+ | `select` | Dropdown | Einzelauswahl |
220
+ | `radio` | Radiogruppe | exklusive Einzelauswahl, vertikale Optionsspalte |
221
+ | `checkbox` | Checkbox-Gruppe | **Mehrfachauswahl** — löst mit `string[]` auf |
222
+ | `range` | Slider + synchronisiertes Zahlenfeld | `min`, `max`, `step`, `default` |
223
+ | `date` / `time` / `datetime` | native Datums-/Zeitwähler | |
224
+ | `file` | Dateiauswahl | |
225
+
226
+ **Options** (`select`, `radio`, `checkbox`) akzeptieren einfache Strings oder Objekte `{ value, label?, checked?, disabled? }` — `checked` selektiert Checkbox-Optionen vor, `disabled` graut eine Option aus.
227
+
228
+ **Validierungsregeln:** `{ required?, minLength?, pattern?, minSelected? }` (`minSelected` gilt für `checkbox`). Die Prüfung läuft asynchron im Dashboard; ungültige Formulare können nicht abgeschickt werden.
229
+
230
+ ## Fallback-Verhalten ohne µGulp™
231
+
232
+ | Funktion | TTY (interaktive Shell) | non-TTY (CI, Pipe) |
233
+ | :--- | :--- | :--- |
234
+ | `ReportProgress` | aktualisierende Fortschrittszeile | Log-Zeile je 10-%-Schritt |
235
+ | `PlaySignal` | Terminal-Glocke (`attention`/`error`) | stumm |
236
+ | `Speak` | `[speech]`-Log-Zeile | `[speech]`-Log-Zeile |
237
+ | Text-/Passwort-/Zahlen-/Range-Eingabe | readline-Prompt (Enter = Default) | Default-Wert |
238
+ | Font-/Select-/Radio-Eingabe | nummerierte Liste + readline | Default-Wert |
239
+ | Checkbox-Eingabe | nummerierte Liste, Kommaeingabe | Werte der `checked`-Optionen |
240
+
241
+ Die Sound- und Sprachwiedergabe (Mute, Lautstärke, Sprechgeschwindigkeit/-höhe) wird zentral in den µGulp™-Dashboard-Settings konfiguriert (Zahnrad im Kopfbereich) — Tasks müssen sich um die Audio-Einstellungen der Anwender nicht kümmern.
242
+
243
+ ## Lizenz
244
+
245
+ MIT — © 2026 Meinolf Amekudzi
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "gulp-mu-gulp-api",
3
+ "version": "0.2.0",
4
+ "description": "Public task API for the µGulp orchestrator: progress reporting, sound signals, speech output and interactive UI inputs (text, password, number, textarea, color, font, select, radio, multi-select checkbox, range slider, date/time, file) from within gulp tasks — with graceful CLI fallbacks when running without µGulp.",
5
+ "type": "module",
6
+ "main": "src/index.mjs",
7
+ "exports": {
8
+ ".": "./src/index.mjs"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test \"tests/**/*.test.mjs\""
16
+ },
17
+ "keywords": [
18
+ "gulp",
19
+ "ugulp",
20
+ "microgulp",
21
+ "progress",
22
+ "interactive",
23
+ "sound",
24
+ "speech",
25
+ "prompt",
26
+ "multiselect",
27
+ "form",
28
+ "task-api"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/mamekudz/microGulp.git",
34
+ "directory": "gulp-mu-gulp-api"
35
+ },
36
+ "homepage": "https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/mamekudz/microGulp/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,322 @@
1
+ // ===========================================
2
+ // gulp-mu-gulp-api — µGulp task API
3
+ // © 2026 Meinolf Amekudzi
4
+ // (published under MIT license)
5
+ // ===========================================
6
+
7
+ /**
8
+ * Public API for gulp tasks running under the µGulp orchestrator.
9
+ *
10
+ * Under µGulp every task runs inside a forked worker process with an IPC
11
+ * channel to the engine; this module talks to that channel directly, so it
12
+ * has zero coupling to µGulp internals and works from any npm package in
13
+ * the pipeline. Without µGulp (plain "gulp" CLI run) every function
14
+ * degrades gracefully: progress renders on the terminal, inputs fall back
15
+ * to readline prompts on a TTY or to the declared defaults otherwise.
16
+ *
17
+ * @example
18
+ * import { ReportProgress, RequestTextInput, RequestColorInput } from 'gulp-mu-gulp-api';
19
+ *
20
+ * export async function BUILD_THEME() {
21
+ * let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
22
+ * for (let step = 0; step < 10; step++) {
23
+ * // ... work ...
24
+ * ReportProgress((step + 1) / 10, 'compiling theme');
25
+ * }
26
+ * }
27
+ *
28
+ * @module gulp-mu-gulp-api
29
+ */
30
+
31
+ let uiRequestCounter = 0;
32
+ let ipcListenerAttached = false;
33
+ let lastLoggedPercent = -1;
34
+ const pendingUiRequests = new Map();
35
+
36
+ /**
37
+ * @returns {boolean} true when the task runs inside a µGulp worker with an
38
+ * attached dashboard (progress and inputs are rendered in the webview)
39
+ */
40
+ export function IsUGulp() {
41
+ return typeof process.send === 'function' && process.env.UGULP === '1';
42
+ }
43
+
44
+ // -------------------------------------------------
45
+ // progress
46
+ // -------------------------------------------------
47
+
48
+ /**
49
+ * Reports task progress to the µGulp dashboard (determinate neon progress
50
+ * bar in the task's sector). CLI fallback: an updating line on a TTY,
51
+ * 10%-step log lines otherwise.
52
+ *
53
+ * @param {number} _value progress in the range 0..1
54
+ * @param {string} [_label] short status label shown next to the bar
55
+ */
56
+ export function ReportProgress(_value, _label) {
57
+ let value = Math.max(0, Math.min(1, Number(_value) || 0));
58
+ if (IsUGulp()) {
59
+ process.send({ type: 'progress', value, label: _label });
60
+ return;
61
+ }
62
+ let percent = Math.round(value * 100);
63
+ if (process.stdout.isTTY) {
64
+ process.stdout.write('\r' + (_label ?? 'progress') + ' ' + percent + '%' + (value >= 1 ? '\n' : ''));
65
+ } else if (percent >= 100 || percent - lastLoggedPercent >= 10) {
66
+ lastLoggedPercent = percent >= 100 ? -1 : percent;
67
+ console.log((_label ?? 'progress') + ' ' + percent + '%');
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Convenience wrapper that carries a fixed label.
73
+ *
74
+ * @param {string} [_label]
75
+ * @returns {{Update: function(number, string=): void, Done: function(): void}}
76
+ */
77
+ export function CreateProgress(_label) {
78
+ return {
79
+ Update(_value, _stepLabel) {
80
+ ReportProgress(_value, _stepLabel ?? _label);
81
+ },
82
+ Done() {
83
+ ReportProgress(1, _label);
84
+ },
85
+ };
86
+ }
87
+
88
+ // -------------------------------------------------
89
+ // sound & speech
90
+ // -------------------------------------------------
91
+
92
+ /**
93
+ * Plays an acoustic signal in the µGulp dashboard (WebAudio, respects the
94
+ * dashboard audio settings: mute/volume). CLI fallback: terminal bell for
95
+ * 'attention' and 'error' on a TTY, silent otherwise.
96
+ *
97
+ * @param {'success'|'error'|'attention'} [_signal]
98
+ */
99
+ export function PlaySignal(_signal = 'attention') {
100
+ if (IsUGulp()) {
101
+ process.send({ type: 'sound', signal: _signal });
102
+ return;
103
+ }
104
+ if (process.stdout.isTTY && (_signal === 'attention' || _signal === 'error')) {
105
+ process.stdout.write('\u0007');
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Speaks a text through the µGulp dashboard speech output (Web Speech API,
111
+ * respects the dashboard speech settings: enabled/volume/rate/pitch).
112
+ * CLI fallback: the text is printed as a log line.
113
+ *
114
+ * @param {string} _text
115
+ * @param {object} [_options] { rate?, pitch?, volume? } — per-call overrides (0..2 / 0..2 / 0..1)
116
+ */
117
+ export function Speak(_text, _options) {
118
+ let text = String(_text ?? '').trim();
119
+ if (!text) return;
120
+ if (IsUGulp()) {
121
+ process.send({ type: 'speech', text, options: _options ?? null });
122
+ return;
123
+ }
124
+ console.log('[speech] ' + text);
125
+ }
126
+
127
+ // -------------------------------------------------
128
+ // interactive inputs
129
+ // -------------------------------------------------
130
+
131
+ /**
132
+ * Requests a complete form from the µGulp dashboard.
133
+ *
134
+ * Field descriptor: { id, type, label, default?, options?, validate?,
135
+ * min?, max?, step?, rows?, placeholder? }
136
+ *
137
+ * Supported types: 'text' | 'password' | 'number' | 'textarea' | 'color' |
138
+ * 'font' | 'select' | 'radio' | 'checkbox' (multi-select, resolves to a
139
+ * string array) | 'range' (slider with synced number box) | 'date' |
140
+ * 'time' | 'datetime' | 'file'.
141
+ *
142
+ * Options (select/radio/checkbox) accept plain strings or objects
143
+ * { value, label?, checked?, disabled? }.
144
+ *
145
+ * Validation rules: { required?, minLength?, pattern?, minSelected? } —
146
+ * checks run asynchronously in the webview before submission.
147
+ *
148
+ * @param {object} _form { title, fields: [...] }
149
+ * @returns {Promise<object>} map fieldId -> value
150
+ */
151
+ export async function RequestForm(_form) {
152
+ if (IsUGulp()) {
153
+ _EnsureIpcListener();
154
+ let requestId = 'api-req-' + process.pid + '-' + (++uiRequestCounter);
155
+ return new Promise((_resolve) => {
156
+ pendingUiRequests.set(requestId, _resolve);
157
+ process.send({ type: 'ui-request', requestId, form: _form });
158
+ });
159
+ }
160
+ return _FallbackForm(_form);
161
+ }
162
+
163
+ /**
164
+ * Requests a single validated text value.
165
+ * @param {object} _options { label, title?, default?, validate? }
166
+ * @returns {Promise<string|null>}
167
+ */
168
+ export async function RequestTextInput(_options) {
169
+ return _SingleField({ ..._options, type: 'text' });
170
+ }
171
+
172
+ /**
173
+ * Requests a color (native color picker in the dashboard).
174
+ * @param {object} _options { label, title?, default? } — default as #rrggbb
175
+ * @returns {Promise<string|null>}
176
+ */
177
+ export async function RequestColorInput(_options) {
178
+ return _SingleField({ ..._options, type: 'color' });
179
+ }
180
+
181
+ /**
182
+ * Requests a font selection.
183
+ * @param {object} _options { label, title?, default?, options? } — options: font family names
184
+ * @returns {Promise<string|null>}
185
+ */
186
+ export async function RequestFontInput(_options) {
187
+ return _SingleField({ ..._options, type: 'font' });
188
+ }
189
+
190
+ /**
191
+ * Requests a selection from a fixed option list.
192
+ * @param {object} _options { label, title?, default?, options: string[] }
193
+ * @returns {Promise<string|null>}
194
+ */
195
+ export async function RequestSelectInput(_options) {
196
+ return _SingleField({ ..._options, type: 'select' });
197
+ }
198
+
199
+ /**
200
+ * Requests a multi-selection (checkbox group in the dashboard).
201
+ * @param {object} _options { label, title?, options: Array<string|{value, label?, checked?, disabled?}>, validate? }
202
+ * @returns {Promise<string[]>} selected values (empty array when nothing was picked)
203
+ */
204
+ export async function RequestMultiSelectInput(_options) {
205
+ let value = await _SingleField({ ..._options, type: 'checkbox' });
206
+ return Array.isArray(value) ? value : (value != null ? [value] : []);
207
+ }
208
+
209
+ // -------------------------------------------------
210
+ // internal
211
+ // -------------------------------------------------
212
+
213
+ function _EnsureIpcListener() {
214
+ if (ipcListenerAttached) return;
215
+ ipcListenerAttached = true;
216
+ process.on('message', (_message) => {
217
+ if (_message && _message.type === 'ui-response' && pendingUiRequests.has(_message.requestId)) {
218
+ let resolver = pendingUiRequests.get(_message.requestId);
219
+ pendingUiRequests.delete(_message.requestId);
220
+ resolver(_message.payload);
221
+ }
222
+ });
223
+ }
224
+
225
+ async function _SingleField(_options) {
226
+ let values = await RequestForm({
227
+ title: _options.title ?? _options.label,
228
+ fields: [{
229
+ id: 'value',
230
+ type: _options.type,
231
+ label: _options.label,
232
+ default: _options.default,
233
+ options: _options.options,
234
+ validate: _options.validate,
235
+ }],
236
+ });
237
+ return values.value ?? null;
238
+ }
239
+
240
+ async function _FallbackForm(_form) {
241
+ let values = {};
242
+ for (let field of _form.fields ?? []) {
243
+ values[field.id] = await _FallbackField(field);
244
+ }
245
+ return values;
246
+ }
247
+
248
+ function _NormalizeOptions(_options) {
249
+ let result = [];
250
+ for (let option of _options ?? []) {
251
+ if (option == null) continue;
252
+ if (typeof option === 'object') {
253
+ if (option.separator) continue;
254
+ let value = option.value ?? option.label ?? option.name ?? '';
255
+ result.push({
256
+ value: String(value),
257
+ label: String(option.label ?? option.name ?? value),
258
+ checked: !!option.checked,
259
+ });
260
+ } else {
261
+ result.push({ value: String(option), label: String(option), checked: false });
262
+ }
263
+ }
264
+ return result;
265
+ }
266
+
267
+ function _FallbackDefault(_field) {
268
+ if (_field.type === 'checkbox') {
269
+ return _NormalizeOptions(_field.options).filter((_option) => _option.checked).map((_option) => _option.value);
270
+ }
271
+ return _field.default ?? null;
272
+ }
273
+
274
+ async function _FallbackField(_field) {
275
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
276
+ return _FallbackDefault(_field);
277
+ }
278
+ const readline = await import('node:readline/promises');
279
+ let prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
280
+ try {
281
+ let label = _field.label ?? _field.id;
282
+ let options = _NormalizeOptions(_field.options);
283
+ let hasOptions = options.length > 0;
284
+ if (_field.type === 'checkbox' && hasOptions) {
285
+ options.forEach((_option, _index) => console.log(' [' + (_index + 1) + '] ' + _option.label + (_option.checked ? ' *' : '')));
286
+ let answer = await prompt.question(label + ' (comma-separated numbers, Enter = defaults marked *): ');
287
+ let picked = answer.split(',')
288
+ .map((_part) => parseInt(_part.trim(), 10))
289
+ .filter((_index) => _index >= 1 && _index <= options.length)
290
+ .map((_index) => options[_index - 1].value);
291
+ return picked.length > 0 ? picked : _FallbackDefault(_field);
292
+ }
293
+ if ((_field.type === 'select' || _field.type === 'font' || _field.type === 'radio') && hasOptions) {
294
+ options.forEach((_option, _index) => console.log(' [' + (_index + 1) + '] ' + _option.label));
295
+ let answer = await prompt.question(label + ' (1-' + options.length + (_field.default ? ', default "' + _field.default + '"' : '') + '): ');
296
+ let index = parseInt(answer, 10);
297
+ if (index >= 1 && index <= options.length) return options[index - 1].value;
298
+ return _field.default ?? null;
299
+ }
300
+ let hint = _field.type === 'range' || _field.type === 'number'
301
+ ? ' (' + (_field.min ?? 0) + '-' + (_field.max ?? 100) + (_field.default != null ? ', default ' + _field.default : '') + ')'
302
+ : (_field.default ? ' [' + _field.default + ']' : '');
303
+ let answer = await prompt.question(label + hint + ': ');
304
+ return answer || (_field.default ?? null);
305
+ } finally {
306
+ prompt.close();
307
+ }
308
+ }
309
+
310
+ export default {
311
+ IsUGulp,
312
+ ReportProgress,
313
+ CreateProgress,
314
+ PlaySignal,
315
+ Speak,
316
+ RequestForm,
317
+ RequestTextInput,
318
+ RequestColorInput,
319
+ RequestFontInput,
320
+ RequestSelectInput,
321
+ RequestMultiSelectInput,
322
+ };