retold 4.0.1 → 4.0.3

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 (78) hide show
  1. package/.claude/settings.local.json +38 -1
  2. package/README.md +92 -2
  3. package/docs/README.md +7 -6
  4. package/docs/_sidebar.md +36 -21
  5. package/docs/_topbar.md +2 -2
  6. package/docs/architecture/comprehensions.md +282 -0
  7. package/docs/architecture/fluid-models.md +355 -0
  8. package/docs/architecture/module-architecture.md +234 -0
  9. package/docs/{modules.md → architecture/modules.md} +25 -22
  10. package/docs/cover.md +2 -2
  11. package/docs/css/docuserve.css +6 -6
  12. package/docs/examples/examples.md +71 -0
  13. package/docs/examples/todolist/todo-list-cli-client.md +178 -0
  14. package/docs/examples/todolist/todo-list-console-client.md +152 -0
  15. package/docs/examples/todolist/todo-list-model.md +114 -0
  16. package/docs/examples/todolist/todo-list-server.md +128 -0
  17. package/docs/examples/todolist/todo-list-web-client.md +177 -0
  18. package/docs/examples/todolist/todo-list.md +162 -0
  19. package/docs/getting-started.md +8 -7
  20. package/docs/index.html +4 -4
  21. package/docs/{meadow.md → modules/meadow.md} +4 -6
  22. package/docs/{orator.md → modules/orator.md} +1 -0
  23. package/docs/{pict.md → modules/pict.md} +30 -8
  24. package/docs/{utility.md → modules/utility.md} +0 -9
  25. package/docs/retold-catalog.json +1792 -231
  26. package/docs/retold-keyword-index.json +136439 -64616
  27. package/examples/todo-list/Dockerfile +45 -0
  28. package/examples/todo-list/README.md +394 -0
  29. package/examples/todo-list/cli-client/package-lock.json +418 -0
  30. package/examples/todo-list/cli-client/package.json +19 -0
  31. package/examples/todo-list/cli-client/source/TodoCLI-CLIProgram.js +30 -0
  32. package/examples/todo-list/cli-client/source/TodoCLI-Run.js +3 -0
  33. package/examples/todo-list/cli-client/source/commands/add/TodoCLI-Command-Add.js +74 -0
  34. package/examples/todo-list/cli-client/source/commands/complete/TodoCLI-Command-Complete.js +84 -0
  35. package/examples/todo-list/cli-client/source/commands/list/TodoCLI-Command-List.js +110 -0
  36. package/examples/todo-list/cli-client/source/commands/remove/TodoCLI-Command-Remove.js +49 -0
  37. package/examples/todo-list/cli-client/source/services/TodoCLI-Service-API.js +92 -0
  38. package/examples/todo-list/console-client/console-client.cjs +913 -0
  39. package/examples/todo-list/console-client/package-lock.json +426 -0
  40. package/examples/todo-list/console-client/package.json +19 -0
  41. package/examples/todo-list/console-client/views/PictView-TUI-Header.cjs +43 -0
  42. package/examples/todo-list/console-client/views/PictView-TUI-Layout.cjs +58 -0
  43. package/examples/todo-list/console-client/views/PictView-TUI-StatusBar.cjs +41 -0
  44. package/examples/todo-list/console-client/views/PictView-TUI-TaskList.cjs +104 -0
  45. package/examples/todo-list/docker-motd.sh +36 -0
  46. package/examples/todo-list/docker-run.sh +2 -0
  47. package/examples/todo-list/docker-shell.sh +2 -0
  48. package/examples/todo-list/model/MeadowSchema-Task.json +152 -0
  49. package/examples/todo-list/model/Task-Compiled.json +25 -0
  50. package/examples/todo-list/model/Task.mddl +15 -0
  51. package/examples/todo-list/model/data/seeded_todo_events.csv +1001 -0
  52. package/examples/todo-list/server/database-initialization-service.cjs +273 -0
  53. package/examples/todo-list/server/package-lock.json +6113 -0
  54. package/examples/todo-list/server/package.json +19 -0
  55. package/examples/todo-list/server/server.cjs +138 -0
  56. package/examples/todo-list/web-client/css/todolist-theme.css +235 -0
  57. package/examples/todo-list/web-client/generate-build-config.cjs +18 -0
  58. package/examples/todo-list/web-client/html/index.html +18 -0
  59. package/examples/todo-list/web-client/package-lock.json +12030 -0
  60. package/examples/todo-list/web-client/package.json +43 -0
  61. package/examples/todo-list/web-client/source/TodoList-Application-Config.json +12 -0
  62. package/examples/todo-list/web-client/source/TodoList-Application.cjs +383 -0
  63. package/examples/todo-list/web-client/source/providers/Provider-TaskData.cjs +243 -0
  64. package/examples/todo-list/web-client/source/providers/Router-Config.json +32 -0
  65. package/examples/todo-list/web-client/source/views/View-Layout.cjs +75 -0
  66. package/examples/todo-list/web-client/source/views/View-TaskForm.cjs +87 -0
  67. package/examples/todo-list/web-client/source/views/View-TaskList.cjs +127 -0
  68. package/examples/todo-list/web-client/source/views/calendar/View-MonthView.cjs +293 -0
  69. package/examples/todo-list/web-client/source/views/calendar/View-WeekView.cjs +149 -0
  70. package/examples/todo-list/web-client/source/views/calendar/View-YearView.cjs +226 -0
  71. package/modules/Include-Retold-Module-List.sh +2 -2
  72. package/package.json +5 -5
  73. package/docs/js/pict.min.js +0 -12
  74. package/docs/js/pict.min.js.map +0 -1
  75. package/docs/pict-docuserve.min.js +0 -58
  76. package/docs/pict-docuserve.min.js.map +0 -1
  77. /package/docs/{architecture.md → architecture/architecture.md} +0 -0
  78. /package/docs/{fable.md → modules/fable.md} +0 -0
@@ -0,0 +1,913 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Retold Todo List -- Console Client
4
+ *
5
+ * Demonstrates pict-terminalui + blessed for a TUI that connects
6
+ * to the same API server as the web client.
7
+ *
8
+ * Requires the server to be running: cd ../server && npm start
9
+ *
10
+ * Run: node console-client.cjs
11
+ * Quit: q or Ctrl-C
12
+ *
13
+ * Keys:
14
+ * Up/Down Navigate task selection
15
+ * Enter View selected task detail
16
+ * E Edit selected task
17
+ * A Add new task
18
+ * D Delete selected task
19
+ * S Sort order picker
20
+ * / Search / filter tasks
21
+ * R Refresh task list
22
+ * Q Quit
23
+ */
24
+
25
+ // Suppress blessed's Setulc stderr noise before anything loads
26
+ const _origStderrWrite = process.stderr.write;
27
+ process.stderr.write = function (pChunk)
28
+ {
29
+ if (typeof pChunk === 'string' && pChunk.indexOf('Setulc') !== -1)
30
+ {
31
+ return true;
32
+ }
33
+ return _origStderrWrite.apply(process.stderr, arguments);
34
+ };
35
+
36
+ const libHttp = require('http');
37
+ const blessed = require('blessed');
38
+ const libPict = require('pict');
39
+ const libPictApplication = require('pict-application');
40
+
41
+ const libPictTerminalUI = require('pict-terminalui');
42
+
43
+ // Views
44
+ const libViewLayout = require('./views/PictView-TUI-Layout.cjs');
45
+ const libViewHeader = require('./views/PictView-TUI-Header.cjs');
46
+ const libViewTaskList = require('./views/PictView-TUI-TaskList.cjs');
47
+ const libViewStatusBar = require('./views/PictView-TUI-StatusBar.cjs');
48
+
49
+ const API_BASE = 'http://localhost:8086';
50
+
51
+ // ─────────────────────────────────────────────
52
+ // Sort options available in the sort picker
53
+ // ─────────────────────────────────────────────
54
+ const SORT_OPTIONS =
55
+ [
56
+ { Label: 'Due Date (newest first)', Column: 'DueDate', Direction: 'DESC' },
57
+ { Label: 'Due Date (oldest first)', Column: 'DueDate', Direction: 'ASC' },
58
+ { Label: 'Name (A-Z)', Column: 'Name', Direction: 'ASC' },
59
+ { Label: 'Name (Z-A)', Column: 'Name', Direction: 'DESC' },
60
+ { Label: 'Status (A-Z)', Column: 'Status', Direction: 'ASC' },
61
+ { Label: 'Status (Z-A)', Column: 'Status', Direction: 'DESC' },
62
+ { Label: 'Hours (most first)', Column: 'LengthInHours', Direction: 'DESC' },
63
+ { Label: 'Hours (least first)', Column: 'LengthInHours', Direction: 'ASC' },
64
+ { Label: 'Recently added', Column: 'IDTask', Direction: 'DESC' },
65
+ { Label: 'Oldest added', Column: 'IDTask', Direction: 'ASC' }
66
+ ];
67
+
68
+ // ─────────────────────────────────────────────
69
+ // HTTP helper (uses Node.js http module)
70
+ // ─────────────────────────────────────────────
71
+ function httpRequest(pMethod, pPath, pBody, fCallback)
72
+ {
73
+ let tmpURL = new URL(pPath, API_BASE);
74
+ let tmpOptions =
75
+ {
76
+ method: pMethod,
77
+ hostname: tmpURL.hostname,
78
+ port: tmpURL.port,
79
+ path: tmpURL.pathname + tmpURL.search,
80
+ headers: { 'Content-Type': 'application/json' }
81
+ };
82
+
83
+ let tmpReq = libHttp.request(tmpOptions,
84
+ (pResponse) =>
85
+ {
86
+ let tmpData = '';
87
+ pResponse.on('data', (pChunk) => { tmpData += pChunk; });
88
+ pResponse.on('end', () =>
89
+ {
90
+ try
91
+ {
92
+ let tmpParsed = JSON.parse(tmpData);
93
+ return fCallback(null, tmpParsed);
94
+ }
95
+ catch (pParseError)
96
+ {
97
+ return fCallback(pParseError);
98
+ }
99
+ });
100
+ });
101
+
102
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
103
+
104
+ if (pBody)
105
+ {
106
+ tmpReq.write(JSON.stringify(pBody));
107
+ }
108
+ tmpReq.end();
109
+ }
110
+
111
+ // ─────────────────────────────────────────────
112
+ // Application class
113
+ // ─────────────────────────────────────────────
114
+ class TodoListConsoleApplication extends libPictApplication
115
+ {
116
+ constructor(pFable, pOptions, pServiceHash)
117
+ {
118
+ super(pFable, pOptions, pServiceHash);
119
+
120
+ this.terminalUI = null;
121
+ this._screen = null;
122
+ this._contentBox = null;
123
+
124
+ // Track whether a modal is currently open so we don't double-open
125
+ this._modalOpen = false;
126
+
127
+ // Add views
128
+ this.pict.addView('TUI-Layout', libViewLayout.default_configuration, libViewLayout);
129
+ this.pict.addView('TUI-Header', libViewHeader.default_configuration, libViewHeader);
130
+ this.pict.addView('TUI-TaskList', libViewTaskList.default_configuration, libViewTaskList);
131
+ this.pict.addView('TUI-StatusBar', libViewStatusBar.default_configuration, libViewStatusBar);
132
+ }
133
+
134
+ onAfterInitializeAsync(fCallback)
135
+ {
136
+ // Initialize shared application state
137
+ this.pict.AppData.TodoList =
138
+ {
139
+ Tasks: [],
140
+ SelectedIndex: 0,
141
+ StatusMessage: 'Loading tasks...',
142
+ TaskListDisplay: '',
143
+
144
+ // List state drives server-side sort and search
145
+ ListState:
146
+ {
147
+ SortColumn: 'DueDate',
148
+ SortDirection: 'DESC',
149
+ SearchText: ''
150
+ }
151
+ };
152
+
153
+ // Create the terminal UI environment
154
+ this.terminalUI = new libPictTerminalUI(this.pict,
155
+ {
156
+ Title: 'Todo List Console Client'
157
+ });
158
+
159
+ // Create the blessed screen
160
+ this._screen = this.terminalUI.createScreen();
161
+
162
+ // Build the blessed widget layout
163
+ this._createBlessedLayout(this._screen);
164
+
165
+ // Bind navigation keys
166
+ this._bindNavigation(this._screen);
167
+
168
+ // Load tasks from the API
169
+ this._loadTasks(
170
+ () =>
171
+ {
172
+ // Render the layout view (which triggers child view renders)
173
+ this.pict.views['TUI-Layout'].render();
174
+
175
+ // Do the initial blessed screen render
176
+ this._screen.render();
177
+
178
+ return super.onAfterInitializeAsync(fCallback);
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Create the blessed widget layout and register widgets.
184
+ */
185
+ _createBlessedLayout(pScreen)
186
+ {
187
+ // Application container
188
+ let tmpAppContainer = blessed.box(
189
+ {
190
+ parent: pScreen,
191
+ top: 0,
192
+ left: 0,
193
+ width: '100%',
194
+ height: '100%'
195
+ });
196
+ this.terminalUI.registerWidget('#TUI-Application-Container', tmpAppContainer);
197
+
198
+ // Header bar
199
+ let tmpHeader = blessed.box(
200
+ {
201
+ parent: pScreen,
202
+ top: 0,
203
+ left: 0,
204
+ width: '100%',
205
+ height: 3,
206
+ tags: true,
207
+ style:
208
+ {
209
+ fg: 'white',
210
+ bg: 'blue',
211
+ bold: true
212
+ }
213
+ });
214
+ this.terminalUI.registerWidget('#TUI-Header', tmpHeader);
215
+
216
+ // Main content area
217
+ this._contentBox = blessed.box(
218
+ {
219
+ parent: pScreen,
220
+ top: 3,
221
+ left: 0,
222
+ width: '100%',
223
+ bottom: 1,
224
+ tags: true,
225
+ scrollable: true,
226
+ mouse: true,
227
+ keys: true,
228
+ vi: true,
229
+ scrollbar:
230
+ {
231
+ style: { bg: 'green' }
232
+ },
233
+ border:
234
+ {
235
+ type: 'line'
236
+ },
237
+ style:
238
+ {
239
+ border: { fg: 'cyan' }
240
+ },
241
+ label: ' Tasks ',
242
+ padding:
243
+ {
244
+ left: 1,
245
+ right: 1
246
+ }
247
+ });
248
+ this.terminalUI.registerWidget('#TUI-Content', this._contentBox);
249
+
250
+ // Status bar
251
+ let tmpStatusBar = blessed.box(
252
+ {
253
+ parent: pScreen,
254
+ bottom: 0,
255
+ left: 0,
256
+ width: '100%',
257
+ height: 1,
258
+ tags: true,
259
+ style:
260
+ {
261
+ fg: 'white',
262
+ bg: 'gray'
263
+ }
264
+ });
265
+ this.terminalUI.registerWidget('#TUI-StatusBar', tmpStatusBar);
266
+ }
267
+
268
+ /**
269
+ * Bind keyboard shortcuts.
270
+ */
271
+ _bindNavigation(pScreen)
272
+ {
273
+ let tmpSelf = this;
274
+
275
+ pScreen.key(['up'],
276
+ () =>
277
+ {
278
+ if (tmpSelf._modalOpen) return;
279
+ if (tmpSelf.pict.AppData.TodoList.SelectedIndex > 0)
280
+ {
281
+ tmpSelf.pict.AppData.TodoList.SelectedIndex--;
282
+ tmpSelf.pict.views['TUI-TaskList'].render();
283
+ tmpSelf._screen.render();
284
+ }
285
+ });
286
+
287
+ pScreen.key(['down'],
288
+ () =>
289
+ {
290
+ if (tmpSelf._modalOpen) return;
291
+ let tmpTasks = tmpSelf.pict.AppData.TodoList.Tasks;
292
+ if (tmpSelf.pict.AppData.TodoList.SelectedIndex < tmpTasks.length - 1)
293
+ {
294
+ tmpSelf.pict.AppData.TodoList.SelectedIndex++;
295
+ tmpSelf.pict.views['TUI-TaskList'].render();
296
+ tmpSelf._screen.render();
297
+ }
298
+ });
299
+
300
+ // Add task
301
+ pScreen.key(['a'],
302
+ () =>
303
+ {
304
+ if (tmpSelf._modalOpen) return;
305
+ tmpSelf._showEditModal(null);
306
+ });
307
+
308
+ // View task detail (Enter)
309
+ pScreen.key(['enter'],
310
+ () =>
311
+ {
312
+ if (tmpSelf._modalOpen) return;
313
+ let tmpTasks = tmpSelf.pict.AppData.TodoList.Tasks;
314
+ if (tmpTasks.length > 0)
315
+ {
316
+ let tmpTask = tmpTasks[tmpSelf.pict.AppData.TodoList.SelectedIndex];
317
+ tmpSelf._showViewModal(tmpTask);
318
+ }
319
+ });
320
+
321
+ // Edit task
322
+ pScreen.key(['e'],
323
+ () =>
324
+ {
325
+ if (tmpSelf._modalOpen) return;
326
+ let tmpTasks = tmpSelf.pict.AppData.TodoList.Tasks;
327
+ if (tmpTasks.length > 0)
328
+ {
329
+ let tmpTask = tmpTasks[tmpSelf.pict.AppData.TodoList.SelectedIndex];
330
+ tmpSelf._showEditModal(tmpTask);
331
+ }
332
+ });
333
+
334
+ // Delete task
335
+ pScreen.key(['d'],
336
+ () =>
337
+ {
338
+ if (tmpSelf._modalOpen) return;
339
+ let tmpTasks = tmpSelf.pict.AppData.TodoList.Tasks;
340
+ if (tmpTasks.length > 0)
341
+ {
342
+ let tmpTask = tmpTasks[tmpSelf.pict.AppData.TodoList.SelectedIndex];
343
+ tmpSelf._deleteTask(tmpTask.IDTask);
344
+ }
345
+ });
346
+
347
+ // Sort picker
348
+ pScreen.key(['s'],
349
+ () =>
350
+ {
351
+ if (tmpSelf._modalOpen) return;
352
+ tmpSelf._showSortModal();
353
+ });
354
+
355
+ // Search / filter
356
+ pScreen.key(['/'],
357
+ () =>
358
+ {
359
+ if (tmpSelf._modalOpen) return;
360
+ tmpSelf._showSearchModal();
361
+ });
362
+
363
+ // Refresh
364
+ pScreen.key(['r'],
365
+ () =>
366
+ {
367
+ if (tmpSelf._modalOpen) return;
368
+ tmpSelf._setStatus('Refreshing...');
369
+ tmpSelf._loadTasks(
370
+ () =>
371
+ {
372
+ tmpSelf._setStatus('Refreshed.');
373
+ tmpSelf.pict.views['TUI-TaskList'].render();
374
+ tmpSelf.pict.views['TUI-StatusBar'].render();
375
+ tmpSelf._screen.render();
376
+ });
377
+ });
378
+
379
+ // Quit
380
+ pScreen.key(['q', 'C-c'],
381
+ () =>
382
+ {
383
+ if (tmpSelf._modalOpen) return;
384
+ process.exit(0);
385
+ });
386
+ }
387
+
388
+ // ─────────────────────────────────────────
389
+ // API methods
390
+ // ─────────────────────────────────────────
391
+
392
+ /**
393
+ * Build the FilteredTo URL path based on the current ListState.
394
+ */
395
+ _buildFilteredPath()
396
+ {
397
+ let tmpState = this.pict.AppData.TodoList.ListState;
398
+ let tmpFilter = 'FSF~' + tmpState.SortColumn + '~' + tmpState.SortDirection + '~0';
399
+
400
+ if (tmpState.SearchText)
401
+ {
402
+ let tmpSearchEncoded = encodeURIComponent('%' + tmpState.SearchText + '%');
403
+ tmpFilter = 'FBV~Name~LK~' + tmpSearchEncoded
404
+ + '~FBVOR~Description~LK~' + tmpSearchEncoded
405
+ + '~' + tmpFilter;
406
+ }
407
+
408
+ return '/1.0/Tasks/FilteredTo/' + tmpFilter + '/0/250';
409
+ }
410
+
411
+ /**
412
+ * Load tasks from the API using the current sort and filter.
413
+ */
414
+ _loadTasks(fCallback)
415
+ {
416
+ let tmpSelf = this;
417
+ let tmpPath = tmpSelf._buildFilteredPath();
418
+
419
+ httpRequest('GET', tmpPath, null,
420
+ (pError, pTasks) =>
421
+ {
422
+ if (pError)
423
+ {
424
+ tmpSelf._setStatus('Error: ' + pError.message);
425
+ if (fCallback) return fCallback();
426
+ return;
427
+ }
428
+
429
+ if (Array.isArray(pTasks))
430
+ {
431
+ tmpSelf.pict.AppData.TodoList.Tasks = pTasks;
432
+ // Keep selected index in bounds
433
+ if (tmpSelf.pict.AppData.TodoList.SelectedIndex >= pTasks.length)
434
+ {
435
+ tmpSelf.pict.AppData.TodoList.SelectedIndex = Math.max(0, pTasks.length - 1);
436
+ }
437
+ tmpSelf._setStatus(pTasks.length + ' task(s) loaded.');
438
+ }
439
+ else
440
+ {
441
+ tmpSelf._setStatus('Unexpected response from server.');
442
+ }
443
+
444
+ if (fCallback) return fCallback();
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Delete a task.
450
+ */
451
+ _deleteTask(pIDTask)
452
+ {
453
+ let tmpSelf = this;
454
+ tmpSelf._setStatus('Deleting task ' + pIDTask + '...');
455
+
456
+ httpRequest('DELETE', '/1.0/Task/' + pIDTask, null,
457
+ (pError) =>
458
+ {
459
+ if (pError)
460
+ {
461
+ tmpSelf._setStatus('Delete error: ' + pError.message);
462
+ tmpSelf.pict.views['TUI-StatusBar'].render();
463
+ tmpSelf._screen.render();
464
+ return;
465
+ }
466
+ tmpSelf._loadTasks(
467
+ () =>
468
+ {
469
+ tmpSelf._setStatus('Task deleted.');
470
+ tmpSelf.pict.views['TUI-TaskList'].render();
471
+ tmpSelf.pict.views['TUI-StatusBar'].render();
472
+ tmpSelf._screen.render();
473
+ });
474
+ });
475
+ }
476
+
477
+ /**
478
+ * Save a task (create or update).
479
+ */
480
+ _saveTask(pExistingTask, pFieldData)
481
+ {
482
+ let tmpSelf = this;
483
+ let tmpIsEdit = !!pExistingTask;
484
+
485
+ let tmpTaskData =
486
+ {
487
+ Name: pFieldData.Name,
488
+ Description: pFieldData.Description,
489
+ DueDate: pFieldData.DueDate,
490
+ LengthInHours: parseFloat(pFieldData.LengthInHours) || 0,
491
+ Status: pFieldData.Status
492
+ };
493
+
494
+ if (tmpIsEdit)
495
+ {
496
+ tmpTaskData.IDTask = pExistingTask.IDTask;
497
+ tmpSelf._setStatus('Updating task ' + tmpTaskData.IDTask + '...');
498
+ httpRequest('PUT', '/1.0/Task', tmpTaskData,
499
+ (pError) =>
500
+ {
501
+ if (pError)
502
+ {
503
+ tmpSelf._setStatus('Update error: ' + pError.message);
504
+ }
505
+ else
506
+ {
507
+ tmpSelf._setStatus('Task updated.');
508
+ }
509
+ tmpSelf._loadTasks(
510
+ () =>
511
+ {
512
+ tmpSelf.pict.views['TUI-TaskList'].render();
513
+ tmpSelf.pict.views['TUI-StatusBar'].render();
514
+ tmpSelf._screen.render();
515
+ });
516
+ });
517
+ }
518
+ else
519
+ {
520
+ tmpSelf._setStatus('Creating task...');
521
+ httpRequest('POST', '/1.0/Task', tmpTaskData,
522
+ (pError) =>
523
+ {
524
+ if (pError)
525
+ {
526
+ tmpSelf._setStatus('Create error: ' + pError.message);
527
+ }
528
+ else
529
+ {
530
+ tmpSelf._setStatus('Task created.');
531
+ }
532
+ tmpSelf._loadTasks(
533
+ () =>
534
+ {
535
+ tmpSelf.pict.views['TUI-TaskList'].render();
536
+ tmpSelf.pict.views['TUI-StatusBar'].render();
537
+ tmpSelf._screen.render();
538
+ });
539
+ });
540
+ }
541
+ }
542
+
543
+ // ─────────────────────────────────────────
544
+ // View Modal (read-only detail)
545
+ // ─────────────────────────────────────────
546
+
547
+ /**
548
+ * Show a read-only detail box for a task.
549
+ * Press Escape or Enter to close, E to jump to edit.
550
+ */
551
+ _showViewModal(pTask)
552
+ {
553
+ let tmpSelf = this;
554
+ tmpSelf._modalOpen = true;
555
+
556
+ let tmpDueDate = (pTask.DueDate || '-');
557
+ if (tmpDueDate.length > 10)
558
+ {
559
+ tmpDueDate = tmpDueDate.substring(0, 10);
560
+ }
561
+
562
+ let tmpContent = [
563
+ '{bold}' + (pTask.Name || '(untitled)') + '{/bold}',
564
+ '',
565
+ '{bold}Status:{/bold} ' + (pTask.Status || '-'),
566
+ '{bold}Due Date:{/bold} ' + tmpDueDate,
567
+ '{bold}Hours:{/bold} ' + (pTask.LengthInHours || 0),
568
+ '',
569
+ '{bold}Description:{/bold}',
570
+ (pTask.Description || '(none)'),
571
+ '',
572
+ '{center}{gray-fg}[Esc] Close [E] Edit{/gray-fg}{/center}'
573
+ ].join('\n');
574
+
575
+ let tmpBox = blessed.box(
576
+ {
577
+ parent: tmpSelf._screen,
578
+ top: 'center',
579
+ left: 'center',
580
+ width: '70%',
581
+ height: 'shrink',
582
+ padding: 1,
583
+ border: { type: 'line' },
584
+ style:
585
+ {
586
+ border: { fg: 'cyan' },
587
+ bg: 'black',
588
+ fg: 'white'
589
+ },
590
+ tags: true,
591
+ keys: true,
592
+ label: ' Task Detail ',
593
+ content: tmpContent
594
+ });
595
+
596
+ tmpBox.focus();
597
+
598
+ tmpBox.key(['escape', 'q'],
599
+ () =>
600
+ {
601
+ tmpBox.destroy();
602
+ tmpSelf._modalOpen = false;
603
+ tmpSelf._screen.render();
604
+ });
605
+
606
+ tmpBox.key(['e'],
607
+ () =>
608
+ {
609
+ tmpBox.destroy();
610
+ tmpSelf._modalOpen = false;
611
+ tmpSelf._showEditModal(pTask);
612
+ });
613
+
614
+ tmpSelf._screen.render();
615
+ }
616
+
617
+ // ─────────────────────────────────────────
618
+ // Edit Modal (sequential field prompts)
619
+ // ─────────────────────────────────────────
620
+
621
+ /**
622
+ * Prompt the user for task fields (add or edit).
623
+ *
624
+ * @param {Object|null} pExistingTask - If non-null, edit this task. Otherwise create new.
625
+ */
626
+ _showEditModal(pExistingTask)
627
+ {
628
+ let tmpSelf = this;
629
+ tmpSelf._modalOpen = true;
630
+
631
+ let tmpIsEdit = !!pExistingTask;
632
+ let tmpDefaults =
633
+ {
634
+ Name: tmpIsEdit ? (pExistingTask.Name || '') : '',
635
+ Description: tmpIsEdit ? (pExistingTask.Description || '') : '',
636
+ DueDate: tmpIsEdit ? (pExistingTask.DueDate || '') : '',
637
+ LengthInHours: tmpIsEdit ? (pExistingTask.LengthInHours || 0) : 0,
638
+ Status: tmpIsEdit ? (pExistingTask.Status || 'Pending') : 'Pending'
639
+ };
640
+
641
+ // Truncate DueDate to just the date portion for editing
642
+ if (tmpDefaults.DueDate.length > 10)
643
+ {
644
+ tmpDefaults.DueDate = tmpDefaults.DueDate.substring(0, 10);
645
+ }
646
+
647
+ let tmpFields = ['Name', 'Description', 'DueDate', 'LengthInHours', 'Status'];
648
+ let tmpResults = {};
649
+ let tmpFieldIndex = 0;
650
+
651
+ let tmpPrompt = blessed.prompt(
652
+ {
653
+ parent: tmpSelf._screen,
654
+ top: 'center',
655
+ left: 'center',
656
+ width: '60%',
657
+ height: 'shrink',
658
+ border: { type: 'line' },
659
+ style:
660
+ {
661
+ border: { fg: 'yellow' },
662
+ bg: 'black',
663
+ fg: 'white'
664
+ },
665
+ tags: true,
666
+ keys: true,
667
+ vi: true,
668
+ label: tmpIsEdit ? ' Edit Task ' : ' New Task '
669
+ });
670
+
671
+ function promptNextField()
672
+ {
673
+ if (tmpFieldIndex >= tmpFields.length)
674
+ {
675
+ // All fields collected -- save the task
676
+ tmpPrompt.destroy();
677
+ tmpSelf._modalOpen = false;
678
+ tmpSelf._saveTask(pExistingTask, tmpResults);
679
+ return;
680
+ }
681
+
682
+ let tmpFieldName = tmpFields[tmpFieldIndex];
683
+ let tmpDefault = String(tmpDefaults[tmpFieldName]);
684
+ let tmpLabel = tmpFieldName + ' [' + tmpDefault + ']:';
685
+
686
+ tmpPrompt.input(tmpLabel, tmpDefault,
687
+ (pError, pValue) =>
688
+ {
689
+ if (pError || pValue === null || pValue === undefined)
690
+ {
691
+ // User cancelled (Escape)
692
+ tmpPrompt.destroy();
693
+ tmpSelf._modalOpen = false;
694
+ tmpSelf._screen.render();
695
+ return;
696
+ }
697
+ tmpResults[tmpFieldName] = pValue;
698
+ tmpFieldIndex++;
699
+ promptNextField();
700
+ });
701
+ }
702
+
703
+ promptNextField();
704
+ }
705
+
706
+ // ─────────────────────────────────────────
707
+ // Sort Modal
708
+ // ─────────────────────────────────────────
709
+
710
+ /**
711
+ * Show a list picker for sort order.
712
+ */
713
+ _showSortModal()
714
+ {
715
+ let tmpSelf = this;
716
+ tmpSelf._modalOpen = true;
717
+
718
+ let tmpLabels = [];
719
+ for (let i = 0; i < SORT_OPTIONS.length; i++)
720
+ {
721
+ tmpLabels.push(SORT_OPTIONS[i].Label);
722
+ }
723
+
724
+ let tmpList = blessed.list(
725
+ {
726
+ parent: tmpSelf._screen,
727
+ top: 'center',
728
+ left: 'center',
729
+ width: '50%',
730
+ height: SORT_OPTIONS.length + 2,
731
+ border: { type: 'line' },
732
+ style:
733
+ {
734
+ border: { fg: 'green' },
735
+ bg: 'black',
736
+ fg: 'white',
737
+ selected:
738
+ {
739
+ bg: 'green',
740
+ fg: 'black',
741
+ bold: true
742
+ }
743
+ },
744
+ keys: true,
745
+ vi: true,
746
+ mouse: true,
747
+ tags: true,
748
+ label: ' Sort Order ',
749
+ items: tmpLabels
750
+ });
751
+
752
+ // Pre-select the current sort
753
+ let tmpState = tmpSelf.pict.AppData.TodoList.ListState;
754
+ for (let i = 0; i < SORT_OPTIONS.length; i++)
755
+ {
756
+ if (SORT_OPTIONS[i].Column === tmpState.SortColumn && SORT_OPTIONS[i].Direction === tmpState.SortDirection)
757
+ {
758
+ tmpList.select(i);
759
+ break;
760
+ }
761
+ }
762
+
763
+ tmpList.focus();
764
+
765
+ tmpList.on('select',
766
+ (pItem, pIndex) =>
767
+ {
768
+ let tmpOption = SORT_OPTIONS[pIndex];
769
+ tmpSelf.pict.AppData.TodoList.ListState.SortColumn = tmpOption.Column;
770
+ tmpSelf.pict.AppData.TodoList.ListState.SortDirection = tmpOption.Direction;
771
+
772
+ tmpList.destroy();
773
+ tmpSelf._modalOpen = false;
774
+ tmpSelf._setStatus('Sorting by ' + tmpOption.Label + '...');
775
+
776
+ tmpSelf._loadTasks(
777
+ () =>
778
+ {
779
+ tmpSelf.pict.views['TUI-TaskList'].render();
780
+ tmpSelf.pict.views['TUI-StatusBar'].render();
781
+ tmpSelf._screen.render();
782
+ });
783
+ });
784
+
785
+ tmpList.key(['escape'],
786
+ () =>
787
+ {
788
+ tmpList.destroy();
789
+ tmpSelf._modalOpen = false;
790
+ tmpSelf._screen.render();
791
+ });
792
+
793
+ tmpSelf._screen.render();
794
+ }
795
+
796
+ // ─────────────────────────────────────────
797
+ // Search Modal
798
+ // ─────────────────────────────────────────
799
+
800
+ /**
801
+ * Prompt for a search string, then reload the filtered list.
802
+ * An empty string clears the filter.
803
+ */
804
+ _showSearchModal()
805
+ {
806
+ let tmpSelf = this;
807
+ tmpSelf._modalOpen = true;
808
+
809
+ let tmpCurrentSearch = tmpSelf.pict.AppData.TodoList.ListState.SearchText || '';
810
+
811
+ let tmpPrompt = blessed.prompt(
812
+ {
813
+ parent: tmpSelf._screen,
814
+ top: 'center',
815
+ left: 'center',
816
+ width: '60%',
817
+ height: 'shrink',
818
+ border: { type: 'line' },
819
+ style:
820
+ {
821
+ border: { fg: 'magenta' },
822
+ bg: 'black',
823
+ fg: 'white'
824
+ },
825
+ tags: true,
826
+ keys: true,
827
+ vi: true,
828
+ label: ' Search Tasks '
829
+ });
830
+
831
+ let tmpLabel = 'Search name/description (empty to clear):';
832
+ tmpPrompt.input(tmpLabel, tmpCurrentSearch,
833
+ (pError, pValue) =>
834
+ {
835
+ tmpPrompt.destroy();
836
+ tmpSelf._modalOpen = false;
837
+
838
+ if (pError || pValue === null || pValue === undefined)
839
+ {
840
+ // User cancelled
841
+ tmpSelf._screen.render();
842
+ return;
843
+ }
844
+
845
+ tmpSelf.pict.AppData.TodoList.ListState.SearchText = pValue;
846
+ tmpSelf.pict.AppData.TodoList.SelectedIndex = 0;
847
+
848
+ if (pValue)
849
+ {
850
+ tmpSelf._setStatus('Searching for "' + pValue + '"...');
851
+ }
852
+ else
853
+ {
854
+ tmpSelf._setStatus('Filter cleared.');
855
+ }
856
+
857
+ tmpSelf._loadTasks(
858
+ () =>
859
+ {
860
+ tmpSelf.pict.views['TUI-TaskList'].render();
861
+ tmpSelf.pict.views['TUI-StatusBar'].render();
862
+ tmpSelf._screen.render();
863
+ });
864
+ });
865
+ }
866
+
867
+ // ─────────────────────────────────────────
868
+ // Helpers
869
+ // ─────────────────────────────────────────
870
+
871
+ /**
872
+ * Update the status message.
873
+ */
874
+ _setStatus(pMessage)
875
+ {
876
+ this.pict.AppData.TodoList.StatusMessage = pMessage;
877
+ }
878
+
879
+ /**
880
+ * No-op for layout template expression.
881
+ */
882
+ renderLayoutWidgets()
883
+ {
884
+ return '';
885
+ }
886
+ }
887
+
888
+ // ─────────────────────────────────────────────
889
+ // Bootstrap
890
+ // ─────────────────────────────────────────────
891
+ let _Pict = new libPict(
892
+ {
893
+ Product: 'TodoListConsole',
894
+ LogNoisiness: 0
895
+ });
896
+
897
+ let _App = _Pict.addApplication('TodoList-Console',
898
+ {
899
+ Name: 'TodoList-Console',
900
+ MainViewportViewIdentifier: 'TUI-Layout',
901
+ AutoRenderMainViewportViewAfterInitialize: false,
902
+ AutoSolveAfterInitialize: false
903
+ }, TodoListConsoleApplication);
904
+
905
+ _App.initializeAsync(
906
+ (pError) =>
907
+ {
908
+ if (pError)
909
+ {
910
+ console.error('Application initialization failed:', pError);
911
+ process.exit(1);
912
+ }
913
+ });