@getorrery/cells 0.2.2 → 0.2.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.
@@ -97,6 +97,8 @@ export interface DockView {
97
97
  }
98
98
  /** The dock: a rule with title and step, the question, its body. */
99
99
  export declare function dock(p: DockView, w: number, m: boolean): Item[];
100
+ /** The most choices the dock shows at once; a longer list scrolls. */
101
+ export declare const DOCK_ROWS = 10;
100
102
  /** A choice on the consent bar. */
101
103
  export interface ConsentChoice {
102
104
  key: string;
@@ -4,12 +4,16 @@
4
4
  * the processes behind `p`; one row's details behind Enter — port of
5
5
  * `el/extensions.rs`. The host formats every row (`orrery_registry::browse`);
6
6
  * this draws them. The marks are glyphs (● ○ ↑) and every state is also in
7
- * words, so mono reads the same. Read-only for now: no selection, no acts.
7
+ * words, so mono reads the same. Decision 0107: two-line rows, the search box
8
+ * on top, the focus (search, filters, list) and the marks are the client's,
9
+ * merged into the props before drawing; a batch in flight is drawn under the list.
8
10
  */
9
11
  import { type Item } from "../cells";
10
12
  /** One row of the list, as the host formats it (`orrery_registry::browse::Row`). The chat draws the same fields. */
11
13
  export interface ExtRowV {
12
14
  id: string;
15
+ /** Its name; the id when absent. */
16
+ name?: string;
13
17
  /** The installed version, else the listed one. */
14
18
  version?: string;
15
19
  /** What it is, kebab-case: `tools`, `session-store`. */
@@ -41,6 +45,21 @@ export interface ProcRowV {
41
45
  up: string;
42
46
  hosts: string;
43
47
  }
48
+ /** One extension of a batch in flight (decision 0107 §4). */
49
+ export interface BatchItemV {
50
+ id: string;
51
+ /** `download`, `verify`, `consent`, `install`. */
52
+ step?: string;
53
+ /** `running`, `done` or `failed`. */
54
+ state?: string;
55
+ /** What to do when it failed. */
56
+ fix?: string | null;
57
+ }
58
+ /** A batch in flight: a line per extension and a summary (empty while it runs). */
59
+ export interface BatchV {
60
+ items?: BatchItemV[];
61
+ summary?: string;
62
+ }
44
63
  /** One row's details. A third item is the value's style class (`d` is dim). */
45
64
  export interface DetailV {
46
65
  title: string;
@@ -85,6 +104,12 @@ export interface ExtensionsView {
85
104
  selected?: number | null;
86
105
  /** The screen rows the client has for the overlay: past it the rows are windowed around the cursor, the head, totals and keys always drawn. */
87
106
  height?: number | null;
107
+ /** Where the keys are (the client's): `search`, `filters` or `list` (the default). */
108
+ focus?: string | null;
109
+ /** The ids marked with Space (the client's). */
110
+ marked?: string[];
111
+ /** A batch in flight (the host's). */
112
+ batch?: BatchV | null;
88
113
  }
89
114
  /** Draw the overlay at `w` cells: the list, the processes, or one row's details. */
90
115
  export declare function extensions(p: ExtensionsView, w: number, m: boolean): Item[];
package/dist/el/mod.d.ts CHANGED
@@ -23,6 +23,12 @@ export declare class BadProps extends Error {
23
23
  readonly reason: string;
24
24
  constructor(id: string, reason: string);
25
25
  }
26
+ /**
27
+ * A `null` object member is the member absent, as the Rust crate reads it:
28
+ * every defaulted field takes its default rather than failing on `null`.
29
+ * Nulls in arrays stay. Returns a copy; the host's props are not changed.
30
+ */
31
+ export declare function dropNulls(v: unknown): unknown;
26
32
  /**
27
33
  * Draw element `id` from its props at `w` columns, in mono when `m`.
28
34
  *
package/dist/from.d.ts CHANGED
@@ -123,8 +123,8 @@ export declare function turn(tv: TurnView, o: TurnOpts, w: number, m: boolean):
123
123
  export declare function options(el: string, props: unknown): number | null;
124
124
  /** An element's props with this client's cursor put in them. */
125
125
  export declare function withCursor(el: string, props: unknown, cursor: string | null | undefined): unknown;
126
- /** An overlay's child as rows: as {@link node}, and an element that windows its rows (`/extensions`) is told the `rows` the overlay has. */
127
- export declare function overlayNode(doc: UiDoc, id: string, cursor: string | null | undefined, rows: number, w: number, m: boolean): Line[];
126
+ /** An overlay's child as rows: as {@link node}, and an element that windows its rows (`/extensions`) is told the `rows` the overlay has and gets the client's own state merged in (`view`: its focus and marks). */
127
+ export declare function overlayNode(doc: UiDoc, id: string, cursor: string | null | undefined, view: Record<string, unknown>, rows: number, w: number, m: boolean): Line[];
128
128
  /** A UI node (Tier 2, or an element) as rows. `cursor` is where this client moved a list's cursor. */
129
129
  export declare function node(doc: UiDoc, id: string, cursor: string | null | undefined, w: number, m: boolean): Line[];
130
130
  /** Whether a node's children are drawn by the node itself. */
package/dist/index.js CHANGED
@@ -636,6 +636,7 @@ ${text2(got)}`;
636
636
  // src/el/chrome.ts
637
637
  var chrome_exports = {};
638
638
  __export(chrome_exports, {
639
+ DOCK_ROWS: () => DOCK_ROWS,
639
640
  INDICATOR_MARK: () => INDICATOR_MARK,
640
641
  SLASH_ROWS: () => SLASH_ROWS,
641
642
  SWITCHER_NARROW: () => SWITCHER_NARROW,
@@ -1619,18 +1620,33 @@ function dock(p, w, m) {
1619
1620
  } else {
1620
1621
  rows2 = [hd];
1621
1622
  if (prompt !== "" || (p.items ?? []).length > 0) rows2.push(q);
1622
- const lw = w < 60 ? 17 : 20;
1623
+ const items2 = p.items ?? [];
1624
+ const base = w < 60 ? 17 : 20;
1625
+ const longest = Math.max(0, ...items2.map((it) => chars(it.label)));
1626
+ const lw = Math.min(Math.max(longest + 1, base), Math.max(w - 4, base));
1623
1627
  const sl = p.selected ?? 0;
1624
- (p.items ?? []).forEach((it, i) => {
1628
+ const n = items2.length;
1629
+ let from = 0;
1630
+ let to = n;
1631
+ if (n > DOCK_ROWS) {
1632
+ const cur = Math.min(sl, n - 1);
1633
+ from = Math.min(Math.max(cur - Math.floor(DOCK_ROWS / 2), 0), n - DOCK_ROWS);
1634
+ to = from + DOCK_ROWS;
1635
+ }
1636
+ if (from > 0) rows2.push([sp(4), t(`${from} more above`, "d")]);
1637
+ items2.forEach((it, i) => {
1638
+ if (i < from || i >= to) return;
1625
1639
  const on = i === sl;
1626
1640
  const d0 = it.detail ?? "";
1627
1641
  const detail2 = w < 60 ? d0.split(" \xB7 ")[1] ?? d0 : d0;
1628
1642
  const l = [sp(2), t(on ? "\u276F " : " ", on ? "ac" : ""), t(ps(it.label, lw), on ? "b" : "i"), t(detail2, "d")];
1629
1643
  rows2.push(on ? sel(l) : l);
1630
1644
  });
1645
+ if (to < n) rows2.push([sp(4), t(`${n - to} more below`, "d")]);
1631
1646
  }
1632
1647
  return out(rows2);
1633
1648
  }
1649
+ var DOCK_ROWS = 10;
1634
1650
  function consent(p, w, m) {
1635
1651
  const state = p.state ?? "asking";
1636
1652
  const selected = p.selected ?? 0;
@@ -1951,6 +1967,7 @@ __export(mod_exports, {
1951
1967
  chrome: () => chrome_exports,
1952
1968
  content: () => content_exports,
1953
1969
  context: () => context_exports,
1970
+ dropNulls: () => dropNulls,
1954
1971
  errLines: () => errLines,
1955
1972
  extensions: () => extensions_exports,
1956
1973
  feedback: () => feedback_exports,
@@ -3078,19 +3095,19 @@ function options(el, props) {
3078
3095
  }
3079
3096
  function withCursor(el, props, cursor) {
3080
3097
  if (cursor == null || !/^\d+$/.test(cursor)) return props;
3081
- if (el === "tasks" && props !== null && typeof props === "object" && !Array.isArray(props)) {
3098
+ if ((el === "tasks" || el === "extensions") && props !== null && typeof props === "object" && !Array.isArray(props)) {
3082
3099
  return { ...props, cursor: Number(cursor) };
3083
3100
  }
3084
3101
  if (options(el, props) == null) return props;
3085
3102
  if (props === null || typeof props !== "object" || Array.isArray(props)) return props;
3086
3103
  return { ...props, selected: Number(cursor) };
3087
3104
  }
3088
- function overlayNode(doc, id, cursor, rows2, w, m) {
3105
+ function overlayNode(doc, id, cursor, view, rows2, w, m) {
3089
3106
  const n = doc.nodes[id];
3090
3107
  if (n?.type !== "element" || n.el !== "extensions") return node(doc, id, cursor, w, m);
3091
3108
  const p = withCursor(n.el, n.props ?? {}, cursor);
3092
3109
  try {
3093
- return linesOf(render2(n.el, p !== null && typeof p === "object" ? { ...p, height: rows2 } : p, w, m));
3110
+ return linesOf(render2(n.el, p !== null && typeof p === "object" ? { ...p, ...view, height: rows2 } : p, w, m));
3094
3111
  } catch (e) {
3095
3112
  const msg = e instanceof BadProps ? e.message : e instanceof Error ? e.message : String(e);
3096
3113
  return para([g(m, "\u2717", "error", "er"), sp(1)], msg, w, "er", null);
@@ -3524,29 +3541,36 @@ function list2(p, w, m) {
3524
3541
  const noList = l.age == null;
3525
3542
  const loading = noList && l.refreshing === true;
3526
3543
  const lost = noList && l.refreshing !== true && (l.offline === true || l.failed != null);
3527
- const searching = p.query != null;
3528
3544
  const c = p.counts ?? {};
3529
3545
  const age = l.age ?? "";
3530
3546
  const fail = l.failed ?? "";
3531
3547
  const head = lost ? l.offline ? "offline" : "no list" : !noList && l.offline ? `offline \xB7 list from ${age} ago` : !noList && l.failed != null ? `list from ${age} ago` : "esc close";
3532
3548
  const lines = [panelHead(w, "Extensions", [], head, true, m)];
3549
+ const on = p.filter ? p.filter : "all";
3550
+ const query = p.query != null && p.query !== "" ? p.query : null;
3551
+ const focus = p.focus ?? "list";
3552
+ const lead = (at) => focus === at ? t("\u276F ", "ac") : sp(2);
3533
3553
  const unknown = (n2) => noList ? "\u2014" : String(n2 ?? 0);
3534
3554
  const tabs = [
3535
3555
  ["All", unknown(c.all), "all"],
3536
3556
  ["Installed", String(c.installed ?? 0), "installed"],
3537
3557
  ["Updates", unknown(c.updates), "updates"]
3538
3558
  ];
3539
- const on = p.filter ? p.filter : "all";
3540
- const f = [sp(2)];
3559
+ const f = [lead("filters")];
3541
3560
  tabs.forEach(([name, n2, key], i) => {
3542
3561
  if (i > 0) f.push(sp(narrow ? 1 : 2));
3543
3562
  f.push(key === on ? m ? t(`[${name} ${n2}]`, "b") : t(` ${name} ${n2} `, "btn b") : t(` ${name} ${n2} `, "d"));
3544
3563
  });
3545
- lines.push(lr(f, narrow ? [] : [t("tab filter \xB7 / search", "dd")], w, true));
3546
- if (p.query != null) {
3564
+ lines.push(lr(f, narrow ? [] : [t("\u2190\u2192 filter", "dd")], w, true));
3565
+ const typing = focus === "search";
3566
+ let sbox = query != null ? [t(query, "i")] : [];
3567
+ if (typing) sbox.push(t(" ", "cur"));
3568
+ if (query == null) sbox.push(t("Search extensions\u2026", "dd"));
3569
+ if (query != null) {
3547
3570
  const total = on === "installed" ? c.installed ?? 0 : on === "updates" ? c.updates ?? 0 : c.all ?? 0;
3548
- lines.push(lr([sp(2), t("/ ", "ac"), t(p.query, "i"), t(" ", "cur")], [t(`${rows2.length} of ${total}`, "d")], w, true));
3571
+ sbox = lr(sbox, [t(`${rows2.length} of ${total}`, "d")], Math.max(w - 4, 0), true);
3549
3572
  }
3573
+ lines.push(...typing ? inputBox(sbox, w, m) : boxed([sbox], w));
3550
3574
  const missing = [];
3551
3575
  if (p.roles) {
3552
3576
  const r = p.roles;
@@ -3590,34 +3614,55 @@ function list2(p, w, m) {
3590
3614
  }
3591
3615
  for (const role of missing) {
3592
3616
  const [fix, why] = role === "provider" ? ["Orrery cannot answer", "Orrery cannot answer until one is"] : role === "client" ? ["nothing draws Orrery", "nothing draws Orrery until one is"] : ["sessions are not kept", "sessions are not kept until one is"];
3593
- const text2 = narrow ? `No ${role}: ${fix}.` : `No ${role} is installed: ${why}. / ${role} finds one; orrery install <id> adds it.`;
3617
+ const text2 = narrow ? `No ${role}: ${fix}.` : `No ${role} is installed: ${why}. Search "${role}" to find one; orrery install <id> adds it.`;
3594
3618
  lines.push(...para([sp(2), g(m, "\u2717", "missing", "er"), sp(1)], text2, w, "er"));
3595
3619
  lines.push([]);
3596
3620
  }
3597
3621
  if (rows2.length === 0) {
3598
- let first;
3599
- let then;
3600
- if (p.query != null) {
3601
- first = `Nothing matches "${p.query}" in names, descriptions, authors, tags or kinds.`;
3602
- then = "Esc clears the search. Not in the list? orrery install takes a git URL, a folder or npm:<package>.";
3622
+ let first2;
3623
+ let then2;
3624
+ if (query != null) {
3625
+ first2 = `Nothing matches "${query}" in names, descriptions, authors, tags or kinds.`;
3626
+ then2 = "Esc clears the search. Not in the list? orrery install takes a git URL, a folder or npm:<package>.";
3603
3627
  } else if (on === "updates") {
3604
- first = "No updates: every installed extension is on its listed version.";
3605
- then = "Built-ins update with Orrery (/update).";
3628
+ first2 = "No updates: every installed extension is on its listed version.";
3629
+ then2 = "Built-ins update with Orrery (/update).";
3606
3630
  } else {
3607
3631
  const platform = globalThis.process?.platform;
3608
3632
  const sep = platform === "win32" ? "\\" : "/";
3609
- first = `No extensions in ~${sep}.orrery${sep}extensions or .orrery${sep}extensions`;
3610
- then = "orrery install <source>";
3633
+ first2 = `No extensions in ~${sep}.orrery${sep}extensions or .orrery${sep}extensions`;
3634
+ then2 = "orrery install <source>";
3611
3635
  }
3612
- lines.push(...para([sp(2)], first, w, "i"));
3613
- lines.push(...para([sp(2)], then, w, "d"));
3636
+ lines.push(...para([sp(2)], first2, w, "i"));
3637
+ lines.push(...para([sp(2)], then2, w, "d"));
3614
3638
  }
3615
3639
  const tail = [[]];
3640
+ if (p.batch) {
3641
+ for (const it of p.batch.items ?? []) {
3642
+ const mark2 = it.state === "done" ? g(m, "\u2713", "done", "ok") : it.state === "failed" ? g(m, "\u2717", "failed", "er") : m ? t("[running]", "i") : spin("lv");
3643
+ tail.push(fit([sp(2), mark2, sp(1), t(it.id, "b"), t(` \xB7 ${it.step ?? ""}`, "d")], w));
3644
+ if (it.state === "failed" && it.fix) tail.push(...para([sp(4)], it.fix, w, "er"));
3645
+ }
3646
+ if (p.batch.summary) tail.push(fit([sp(2), t(p.batch.summary, "d")], w));
3647
+ }
3616
3648
  if (p.totals) tail.push([sp(2), t(fitTotals(p.totals, w - 2), "d")]);
3617
3649
  const retry2 = lost || l.failed != null && l.offline !== true;
3618
- const spec = searching ? narrow ? "enter details|esc clear" : "\u2191\u2193 move|enter details|tab filter|esc clear the search" : narrow ? `${retry2 ? "r retry|" : ""}enter details|esc close` : `${retry2 ? "r retry the list|" : ""}\u2191\u2193 move|tab filter|/ search|enter details|p processes|esc close`;
3619
- tail.push(keys(spec, w, 2));
3620
- if (narrow && !searching) tail.push(keys("tab filter|/ search|p processes", w, 2));
3650
+ const esc = (p.marked ?? []).length === 0 ? "esc close" : "esc unselect";
3651
+ let first;
3652
+ let then = null;
3653
+ if (focus === "search") {
3654
+ first = narrow ? "enter the list|esc clear" : "type to search|\u2191 filters|\u2193 the list|enter the list|esc clear";
3655
+ } else if (focus === "filters") {
3656
+ first = narrow ? "\u2190\u2192 filter|enter the list" : "\u2190\u2192 filter|\u2193 search|enter the list|esc the list";
3657
+ } else if (narrow) {
3658
+ first = `${retry2 ? "r retry|" : ""}space select|enter details|${esc}`;
3659
+ then = "i install|x remove|p processes";
3660
+ } else {
3661
+ first = `${retry2 ? "r retry the list|" : ""}\u2191\u2193 move|space select|enter details|\u2190\u2192 filter|${esc}`;
3662
+ then = "i install|u update|x remove|l load or unload|p processes";
3663
+ }
3664
+ tail.push(keys(first, w, 2));
3665
+ if (then) tail.push(keys(then, w, 2));
3621
3666
  const n = rows2.length;
3622
3667
  const room = p.height == null ? Infinity : Math.max(p.height - lines.length - tail.length, 0);
3623
3668
  let from = 0;
@@ -3629,21 +3674,40 @@ function list2(p, w, m) {
3629
3674
  to = from + k;
3630
3675
  }
3631
3676
  if (from > 0) lines.push([sp(4), t(`${from} more above`, "d")]);
3632
- for (let i = from; i < to; i++) lines.push(...row2(rows2[i], cursor === i, w));
3677
+ for (let i = from; i < to; i++) {
3678
+ const r = rows2[i];
3679
+ lines.push(...row2(r, focus === "list" && cursor === i, (p.marked ?? []).includes(r.id), w));
3680
+ }
3633
3681
  if (to < n) lines.push([sp(4), t(`${n - to} more below`, "d")]);
3634
3682
  lines.push(...tail);
3635
3683
  return lines;
3636
3684
  }
3637
- function row2(r, cur, w) {
3685
+ function row2(r, cur, marked2, w) {
3638
3686
  const installed = r.installed === true;
3639
3687
  const dim2 = installed && r.loaded !== true;
3640
3688
  const mark2 = r.update != null ? t("\u2191", "ac") : !installed ? t("\u25CB", "d") : t("\u25CF", dim2 ? "dd" : "ok");
3641
- const left = [t(cur ? "\u276F " : " ", cur ? "ac" : ""), sp(2), mark2, sp(1), t(r.id, dim2 ? "dd" : "b")];
3689
+ const name = r.name ? r.name : r.id;
3690
+ const left = [t(cur ? "\u276F " : " ", cur ? "ac" : ""), marked2 ? t("\u2713 ", "ok") : sp(2), mark2, sp(1), t(name, dim2 ? "dd" : "b")];
3691
+ if (r.version) left.push(t(dim2 ? ` (${r.version} \xB7 unloaded)` : ` (${r.version})`, dim2 ? "dd" : "d"));
3642
3692
  const right = r.update != null ? [t(`\u2191 ${r.update}`, "ac")] : [];
3643
3693
  let first = lr(left, right, w, true);
3644
3694
  if (cur) first = sel(first);
3645
3695
  const cls2 = r.failed === true ? "er" : dim2 ? "dd" : "d";
3646
- const second = fit([sp(6), t(tr(r.subtitle ?? "", Math.max(w - 6, 0)), cls2)], w);
3696
+ const author = r.author ?? "";
3697
+ const description = r.description ?? "";
3698
+ const old = r.failed === true || author === "" && description === "";
3699
+ let text2;
3700
+ if (old && r.subtitle) {
3701
+ text2 = r.subtitle;
3702
+ } else {
3703
+ const parts3 = [];
3704
+ if ((r.kinds ?? []).length > 0) parts3.push(r.kinds.join(", "));
3705
+ if (author) parts3.push(`by ${author}`);
3706
+ else if (r.builtin === true) parts3.push("comes with Orrery");
3707
+ if (w >= WIDE && description) parts3.push(description);
3708
+ text2 = parts3.join(" \xB7 ");
3709
+ }
3710
+ const second = fit([sp(6), t(tr(text2, Math.max(w - 6, 0)), cls2)], w);
3647
3711
  return [first, second];
3648
3712
  }
3649
3713
  function grid(head, rows2, keep, cursor, w) {
@@ -4593,13 +4657,21 @@ var REGISTRY = {
4593
4657
  extensions,
4594
4658
  blocks: (p, w, m) => out(blocks(p.blocks, w, m))
4595
4659
  };
4660
+ function dropNulls(v) {
4661
+ if (Array.isArray(v)) return v.map(dropNulls);
4662
+ if (v === null || typeof v !== "object") return v;
4663
+ const o = {};
4664
+ for (const [k, x] of Object.entries(v)) if (x !== null) o[k] = dropNulls(x);
4665
+ return o;
4666
+ }
4596
4667
  function render2(id, props, w, m) {
4597
4668
  const f = Object.prototype.hasOwnProperty.call(REGISTRY, id) ? REGISTRY[id] : void 0;
4598
4669
  if (f === void 0) throw new BadProps(id, "no such element");
4599
4670
  if (props === null || typeof props !== "object" || Array.isArray(props)) throw new BadProps(id, "expected an object");
4600
4671
  let items2;
4601
4672
  try {
4602
- items2 = soft(() => f(props, w, m));
4673
+ const p = dropNulls(props);
4674
+ items2 = soft(() => f(p, w, m));
4603
4675
  } catch (e) {
4604
4676
  if (e instanceof BadProps) throw e;
4605
4677
  throw new BadProps(id, e instanceof Error ? e.message : String(e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getorrery/cells",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "The terminal design as cells: every harness element drawn as rows of styled cells, checked against the design's own goldens. A port of the orrery-cells crate.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,8 +12,8 @@
12
12
  "directory": "harness/clients/cells-ts"
13
13
  },
14
14
  "dependencies": {
15
- "@getorrery/client": "0.2.2",
16
- "@getorrery/protocol": "0.2.2"
15
+ "@getorrery/client": "0.2.3",
16
+ "@getorrery/protocol": "0.2.3"
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": "^5.9.3",