@yuneta/gobj-ui 7.23.28 → 7.23.30

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.
@@ -0,0 +1,125 @@
1
+ import {describe, it, expect, beforeAll} from "vitest";
2
+ import {
3
+ SDATA,
4
+ SDATA_END,
5
+ SDATACM,
6
+ data_type_t,
7
+ event_flag_t,
8
+ gclass_create,
9
+ gclass_flag_t,
10
+ sdata_flag_t,
11
+ } from "@yuneta/gobj-js";
12
+
13
+ import {bits2names, describe_local_gclass} from "./gclass_describe.js";
14
+
15
+ /* A gclass with one of everything the viewer draws. */
16
+ beforeAll(() => {
17
+ const attrs_table = [
18
+ SDATA(data_type_t.DTP_STRING, "title", sdata_flag_t.SDF_RD, "", "The title"),
19
+ SDATA(data_type_t.DTP_INTEGER, "timeout",
20
+ sdata_flag_t.SDF_WR|sdata_flag_t.SDF_PERSIST, 1000, "Timeout"),
21
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "The DOM node"),
22
+ SDATA_END(),
23
+ ];
24
+ const command_table = [
25
+ SDATACM(data_type_t.DTP_STRING, "help", ["h"], [
26
+ SDATA(data_type_t.DTP_STRING, "cmd", 0, "", "The command"),
27
+ SDATA_END(),
28
+ ], null, "Ask for help"),
29
+ SDATA_END(),
30
+ ];
31
+
32
+ gclass_create(
33
+ "C_TEST_DESCRIBE",
34
+ [
35
+ ["EV_GO", 0],
36
+ ["EV_DONE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
37
+ ],
38
+ [
39
+ ["ST_IDLE", [["EV_GO", function ac_go() {}, "ST_WORKING"]]],
40
+ ["ST_WORKING", [["EV_DONE", function ac_done() {}, "ST_IDLE"]]],
41
+ ],
42
+ {mt_create: function() {}, mt_start: function() {}},
43
+ 0,
44
+ attrs_table,
45
+ {},
46
+ 0,
47
+ command_table,
48
+ 0,
49
+ gclass_flag_t.gcflag_singleton
50
+ );
51
+ });
52
+
53
+ describe("bits2names", () => {
54
+ it("names every bit set, and nothing else", () => {
55
+ expect(bits2names(sdata_flag_t,
56
+ sdata_flag_t.SDF_RD|sdata_flag_t.SDF_PERSIST))
57
+ .toEqual(["SDF_RD", "SDF_PERSIST"]);
58
+ expect(bits2names(sdata_flag_t, 0)).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("describe_local_gclass", () => {
63
+ it("an unregistered gclass is null, never an empty document", () => {
64
+ expect(describe_local_gclass("C_NOT_THERE")).toBe(null);
65
+ expect(describe_local_gclass("")).toBe(null);
66
+ });
67
+
68
+ it("answers the shape the backend view-gclass answers", () => {
69
+ let g = describe_local_gclass("C_TEST_DESCRIBE");
70
+ expect(Object.keys(g)).toEqual([
71
+ "id", "gcflag", "attrs", "commands", "gclass_methods",
72
+ "internal_methods", "FSM", "instances", "trace_levels",
73
+ ]);
74
+ expect(g.id).toBe("C_TEST_DESCRIBE");
75
+ expect(g.gcflag).toEqual(["gcflag_singleton"]);
76
+ });
77
+
78
+ it("names the type and the flags of every attr", () => {
79
+ let attrs = describe_local_gclass("C_TEST_DESCRIBE").attrs;
80
+ expect(attrs.length).toBe(3);
81
+ expect(attrs[0]).toEqual({
82
+ id: "title",
83
+ type: "DTP_STRING",
84
+ flag: ["SDF_RD"],
85
+ description: "The title",
86
+ default_value: "",
87
+ });
88
+ expect(attrs[1].flag).toEqual(["SDF_WR", "SDF_PERSIST"]);
89
+ expect(attrs[1].default_value).toBe(1000);
90
+ });
91
+
92
+ it("never prints a pointer default -- it names it", () => {
93
+ let attrs = describe_local_gclass("C_TEST_DESCRIBE").attrs;
94
+ let container = attrs.find(a => a.id === "$container");
95
+ expect(container.type).toBe("DTP_POINTER");
96
+ expect(container.default_value).toBe(null);
97
+ });
98
+
99
+ it("carries the commands with their parameters", () => {
100
+ let cmds = describe_local_gclass("C_TEST_DESCRIBE").commands;
101
+ expect(cmds.length).toBe(1);
102
+ expect(cmds[0].id).toBe("help");
103
+ expect(cmds[0].alias).toEqual(["h"]);
104
+ expect(cmds[0].parameters.map(p => p.id)).toEqual(["cmd"]);
105
+ });
106
+
107
+ it("lists only the methods the gclass implements", () => {
108
+ let g = describe_local_gclass("C_TEST_DESCRIBE");
109
+ expect(g.gclass_methods).toEqual(["mt_create", "mt_start"]);
110
+ expect(g.internal_methods).toEqual([]);
111
+ });
112
+
113
+ it("draws the FSM as events plus what each state does with them", () => {
114
+ let fsm = describe_local_gclass("C_TEST_DESCRIBE").FSM;
115
+ expect(fsm.events).toEqual([
116
+ {event_name: "EV_GO", event_flag: []},
117
+ {event_name: "EV_DONE",
118
+ event_flag: ["EVF_NO_WARN_SUBS", "EVF_OUTPUT_EVENT"]},
119
+ ]);
120
+ expect(Object.keys(fsm.states)).toEqual(["ST_IDLE", "ST_WORKING"]);
121
+ expect(fsm.states["ST_IDLE"]).toEqual([
122
+ {event_name: "EV_GO", action: "ac_go", next_state: "ST_WORKING"},
123
+ ]);
124
+ });
125
+ });
@@ -0,0 +1,436 @@
1
+ /***********************************************************************
2
+ * gobj_tree_model.js
3
+ *
4
+ * The gobj tree of a yuno, as plain data.
5
+ *
6
+ * The view (C_YUI_GOBJ_TREE_JS) draws NODE DESCRIPTORS, never
7
+ * gobjs. There are two producers of a descriptor, because the
8
+ * view has to show the gobjs of the BACKEND too:
9
+ *
10
+ * - describe_js_gobj() the gobj tree of THIS browser yuno
11
+ * - describe_backend_tree() the answer of the backend command
12
+ * `view-gobj-tree` of another yuno
13
+ *
14
+ * Both answer the SAME shape, and that shape is the backend's:
15
+ * the field names are the ones `gobj2json()` writes in
16
+ * `kernel/c/gobj-c/src/gobj.c` (fullname, shortname, gclass,
17
+ * name, parent, state, running, playing, service, disabled,
18
+ * volatil, bottom_gobj, gobj_flags, commands, gobj_trace_level).
19
+ * Anything the browser runtime cannot answer is simply absent,
20
+ * and every reader here treats absent as "not known", never as
21
+ * false.
22
+ *
23
+ * Copyright (c) 2026, ArtGins.
24
+ * All Rights Reserved.
25
+ ***********************************************************************/
26
+ import {
27
+ gobj_gclass_name,
28
+ gobj_name,
29
+ gobj_short_name,
30
+ gobj_full_name,
31
+ gobj_parent,
32
+ gobj_current_state,
33
+ gobj_is_running,
34
+ gobj_is_playing,
35
+ gobj_is_service,
36
+ gobj_is_pure_child,
37
+ gobj_is_volatil,
38
+ gobj_bottom_gobj,
39
+ } from "@yuneta/gobj-js";
40
+
41
+
42
+ /******************************
43
+ * Roles and status
44
+ ******************************/
45
+
46
+
47
+ /* What a node IS. One value: a gobj is drawn once, so it gets one
48
+ * colour. The order is the order of specificity, and the yuno wins
49
+ * over everything because the root is the yuno by construction. */
50
+ const ROLES = ["yuno", "service", "volatil", "pure", "child"];
51
+
52
+ /* What a node is DOING. `disabled` is not one of these -- a disabled
53
+ * gobj is still stopped or running, so it travels as its own mark. */
54
+ const STATUSES = ["stopped", "running", "playing"];
55
+
56
+
57
+ /************************************************************
58
+ * The role of a node: "yuno" | "service" | "volatil" | "pure" | "child"
59
+ ************************************************************/
60
+ function node_role(d)
61
+ {
62
+ if(!d) {
63
+ return "child";
64
+ }
65
+ if(d.is_yuno) {
66
+ return "yuno";
67
+ }
68
+ if(d.service) {
69
+ return "service";
70
+ }
71
+ if(d.volatil) {
72
+ return "volatil";
73
+ }
74
+ if(d.pure_child) {
75
+ return "pure";
76
+ }
77
+ return "child";
78
+ }
79
+
80
+ /************************************************************
81
+ * What the node is doing: "stopped" | "running" | "playing"
82
+ ************************************************************/
83
+ function node_status(d)
84
+ {
85
+ if(!d || !d.running) {
86
+ return "stopped";
87
+ }
88
+ return d.playing? "playing": "running";
89
+ }
90
+
91
+ /************************************************************
92
+ * The i18n key of the status, which is NOT the status itself:
93
+ * a gobj that runs and does not play is PAUSED to the reader,
94
+ * and "running" alone never said which of the two it was.
95
+ ************************************************************/
96
+ function node_status_key(d)
97
+ {
98
+ switch(node_status(d)) {
99
+ case "playing":
100
+ return "playing";
101
+ case "running":
102
+ return "running (paused)";
103
+ default:
104
+ return "stopped";
105
+ }
106
+ }
107
+
108
+
109
+ /******************************
110
+ * Producers
111
+ ******************************/
112
+
113
+
114
+ /************************************************************
115
+ * A descriptor of a LIVE gobj of this browser yuno, children
116
+ * included (the whole subtree, depth-first).
117
+ *
118
+ * `is_root` marks the yuno itself: the JS runtime has no
119
+ * gobj_flag_yuno test of its own.
120
+ ************************************************************/
121
+ function describe_js_gobj(target_gobj, is_root)
122
+ {
123
+ if(!target_gobj) {
124
+ return null;
125
+ }
126
+
127
+ let parent_gobj = gobj_parent(target_gobj);
128
+ let bottom = gobj_bottom_gobj(target_gobj);
129
+
130
+ let d = {
131
+ gclass: gobj_gclass_name(target_gobj) || "",
132
+ name: gobj_name(target_gobj) || "",
133
+ shortname: gobj_short_name(target_gobj) || "",
134
+ fullname: gobj_full_name(target_gobj) || "",
135
+ parent: parent_gobj? (gobj_short_name(parent_gobj) || ""): "",
136
+ state: gobj_current_state(target_gobj) || "",
137
+ running: !!gobj_is_running(target_gobj),
138
+ playing: !!gobj_is_playing(target_gobj),
139
+ service: !!gobj_is_service(target_gobj),
140
+ volatil: !!gobj_is_volatil(target_gobj),
141
+ pure_child: !!gobj_is_pure_child(target_gobj),
142
+
143
+ /* gobj-js exports no gobj_is_disabled() (the function exists,
144
+ * the export list does not carry it), so the flag is read from
145
+ * the instance the way gobj_enable/gobj_disable write it. The
146
+ * backend answers the same name in `view-gobj-tree`. */
147
+ disabled: !!target_gobj.disabled,
148
+
149
+ bottom_gobj: bottom? (gobj_short_name(bottom) || ""): "",
150
+ commands: !!(target_gobj.gclass && target_gobj.gclass.command_table),
151
+ is_yuno: !!is_root,
152
+ children: [],
153
+ };
154
+
155
+ let children = target_gobj.dl_children || [];
156
+ for(let child of children) {
157
+ let cd = describe_js_gobj(child, false);
158
+ if(cd) {
159
+ d.children.push(cd);
160
+ }
161
+ }
162
+
163
+ return d;
164
+ }
165
+
166
+ /************************************************************
167
+ * A descriptor from ONE node of the `view-gobj-tree` answer
168
+ * of a backend yuno.
169
+ *
170
+ * `shortname` is the key the node hangs from in that answer;
171
+ * the node itself repeats it, so the argument is only the
172
+ * fallback for a filtered answer that dropped the field.
173
+ ************************************************************/
174
+ function describe_backend_node(shortname, node, is_root)
175
+ {
176
+ if(!node || typeof node !== "object") {
177
+ return null;
178
+ }
179
+
180
+ let flags = Array.isArray(node.gobj_flags)? node.gobj_flags: [];
181
+ let has = (flag) => flags.indexOf(flag) !== -1;
182
+
183
+ let d = {
184
+ gclass: node.gclass || "",
185
+ name: node.name || "",
186
+ shortname: node.shortname || shortname || "",
187
+ fullname: node.fullname || "",
188
+ parent: node.parent || "",
189
+ state: node.state || "",
190
+ running: !!node.running,
191
+ playing: !!node.playing,
192
+ service: !!node.service || has("gobj_flag_service") ||
193
+ has("gobj_flag_default_service"),
194
+ volatil: !!node.volatil || has("gobj_flag_volatil"),
195
+ pure_child: has("gobj_flag_pure_child"),
196
+ disabled: !!node.disabled,
197
+ bottom_gobj: node.bottom_gobj || "",
198
+ commands: !!node.commands,
199
+ gobj_flags: flags,
200
+ gobj_trace_level: Array.isArray(node.gobj_trace_level)?
201
+ node.gobj_trace_level: [],
202
+ is_yuno: (is_root !== undefined)?
203
+ !!is_root: has("gobj_flag_yuno"),
204
+ children: [],
205
+ };
206
+
207
+ let children = (node.children && typeof node.children === "object")?
208
+ node.children: null;
209
+ if(children) {
210
+ for(let key of Object.keys(children)) {
211
+ let cd = describe_backend_node(key, children[key], false);
212
+ if(cd) {
213
+ d.children.push(cd);
214
+ }
215
+ }
216
+ }
217
+
218
+ return d;
219
+ }
220
+
221
+ /************************************************************
222
+ * The whole `view-gobj-tree` answer -> the root descriptor.
223
+ *
224
+ * That answer is a dict of ONE key (the shortname of the gobj
225
+ * the command was asked about) even though it is shaped as a
226
+ * dict all the way down. Returns null for anything else, and
227
+ * the caller reports it: an empty tree and a malformed answer
228
+ * must not look the same.
229
+ ************************************************************/
230
+ function describe_backend_tree(data)
231
+ {
232
+ if(!data || typeof data !== "object" || Array.isArray(data)) {
233
+ return null;
234
+ }
235
+ let keys = Object.keys(data);
236
+ if(keys.length === 0) {
237
+ return null;
238
+ }
239
+ return describe_backend_node(keys[0], data[keys[0]], true);
240
+ }
241
+
242
+
243
+ /******************************
244
+ * Readouts
245
+ ******************************/
246
+
247
+
248
+ /************************************************************
249
+ * The i18n key of a role. Two of the five roles are not the
250
+ * word the reader wants ("pure" and "volatil" alone say
251
+ * nothing), so the map is explicit.
252
+ ************************************************************/
253
+ function role_i18n_key(role)
254
+ {
255
+ switch(role) {
256
+ case "yuno":
257
+ return "yuno";
258
+ case "service":
259
+ return "service";
260
+ case "volatil":
261
+ return "volatil child";
262
+ case "pure":
263
+ return "pure child";
264
+ default:
265
+ return "child";
266
+ }
267
+ }
268
+
269
+ /************************************************************
270
+ * The marks a node carries besides its role: what the reader
271
+ * would otherwise have to open the popover to learn.
272
+ *
273
+ * Returns i18n KEYS, in a fixed order, so the card and the
274
+ * popover cannot disagree about what a node is.
275
+ ************************************************************/
276
+ function node_badge_keys(d)
277
+ {
278
+ let badges = [];
279
+ if(!d) {
280
+ return badges;
281
+ }
282
+
283
+ let role = node_role(d);
284
+ if(role !== "child") {
285
+ badges.push(role_i18n_key(role));
286
+ }
287
+
288
+ if(d.disabled) {
289
+ badges.push("disabled");
290
+ }
291
+ if(d.bottom_gobj) {
292
+ badges.push("bottom");
293
+ }
294
+ if(d.commands) {
295
+ badges.push("commands");
296
+ }
297
+
298
+ return badges;
299
+ }
300
+
301
+ /************************************************************
302
+ * The vocabulary of this view, translated.
303
+ *
304
+ * Written as literal t("...") calls and not as t(key): a key
305
+ * reached through a variable is invisible to the locale
306
+ * validator, which is how a fixed vocabulary ships in one
307
+ * language and never changes it.
308
+ ************************************************************/
309
+ function node_labels(t)
310
+ {
311
+ if(typeof t !== "function") {
312
+ t = (k) => k;
313
+ }
314
+ return {
315
+ role: {
316
+ "yuno": t("yuno"),
317
+ "service": t("service"),
318
+ "volatil child": t("volatil child"),
319
+ "pure child": t("pure child"),
320
+ "child": t("child"),
321
+ },
322
+ /* The precise words, for the popover, which has room. */
323
+ status: {
324
+ "playing": t("playing"),
325
+ "running (paused)": t("running (paused)"),
326
+ "stopped": t("stopped"),
327
+ },
328
+ /* The glanceable ones, for the pill on the card, which has
329
+ * not: "Ejecutando (en pausa)" pushes the FSM state off its
330
+ * own row. The colour carries the rest, and the pill's
331
+ * `title` says it in full. */
332
+ status_short: {
333
+ "playing": t("playing"),
334
+ "running": t("running"),
335
+ "stopped": t("stopped"),
336
+ },
337
+ badge: {
338
+ "yuno": t("yuno"),
339
+ "service": t("service"),
340
+ "volatil child": t("volatil child"),
341
+ "pure child": t("pure child"),
342
+ "disabled": t("disabled"),
343
+ "bottom": t("bottom"),
344
+ "commands": t("commands"),
345
+ },
346
+ field: {
347
+ gclass: t("gclass"),
348
+ name: t("name"),
349
+ full_name: t("full name"),
350
+ role: t("role"),
351
+ status: t("status"),
352
+ state: t("state"),
353
+ disabled: t("disabled"),
354
+ parent: t("parent"),
355
+ bottom_gobj: t("bottom gobj"),
356
+ commands: t("commands"),
357
+ traces: t("traces"),
358
+ flags: t("flags"),
359
+ children: t("children"),
360
+ yes: t("yes"),
361
+ collapsed: t("(collapsed)"),
362
+ },
363
+ };
364
+ }
365
+
366
+ /************************************************************
367
+ * The rows of the popover: [label, value], both translated.
368
+ *
369
+ * `t` is the app's translator -- the library has none of its
370
+ * own. Values that are a fixed vocabulary (the role, the
371
+ * status, yes) go through it too; free text does not.
372
+ ************************************************************/
373
+ function node_info_rows(d, t)
374
+ {
375
+ let rows = [];
376
+ if(!d) {
377
+ return rows;
378
+ }
379
+ let L = node_labels(t);
380
+ let f = L.field;
381
+
382
+ rows.push([f.gclass, d.gclass || "\u2014"]);
383
+ rows.push([f.name, d.name || "\u2014"]);
384
+
385
+ if(d.fullname && d.fullname !== d.shortname) {
386
+ rows.push([f.full_name, d.fullname]);
387
+ }
388
+
389
+ rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
390
+ rows.push([f.status, L.status[node_status_key(d)]]);
391
+ rows.push([f.state, d.state || "\u2014"]);
392
+
393
+ if(d.disabled) {
394
+ rows.push([f.disabled, f.yes]);
395
+ }
396
+ if(d.parent) {
397
+ rows.push([f.parent, d.parent]);
398
+ }
399
+ if(d.bottom_gobj) {
400
+ rows.push([f.bottom_gobj, d.bottom_gobj]);
401
+ }
402
+ if(d.commands) {
403
+ rows.push([f.commands, f.yes]);
404
+ }
405
+ if(Array.isArray(d.gobj_trace_level) && d.gobj_trace_level.length > 0) {
406
+ rows.push([f.traces, d.gobj_trace_level.join(", ")]);
407
+ }
408
+ if(Array.isArray(d.gobj_flags) && d.gobj_flags.length > 0) {
409
+ rows.push([f.flags, d.gobj_flags.join(", ")]);
410
+ }
411
+
412
+ let kids = Array.isArray(d.children)? d.children.length: 0;
413
+ if(kids > 0) {
414
+ rows.push([
415
+ f.children,
416
+ d.is_collapsed? (kids + " " + f.collapsed): String(kids)
417
+ ]);
418
+ }
419
+
420
+ return rows;
421
+ }
422
+
423
+ export {
424
+ ROLES,
425
+ STATUSES,
426
+ node_role,
427
+ node_status,
428
+ node_status_key,
429
+ node_badge_keys,
430
+ node_labels,
431
+ node_info_rows,
432
+ role_i18n_key,
433
+ describe_js_gobj,
434
+ describe_backend_node,
435
+ describe_backend_tree,
436
+ };