@filamind-app/core 0.1.3 → 0.1.6
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.
- package/README.md +140 -45
- package/dist/index.d.ts +57 -19
- package/dist/index.js +83 -15
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,40 +1,68 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @filamind-app/core
|
|
2
4
|
|
|
3
|
-
The shared foundation of the FilaMind suite
|
|
4
|
-
|
|
5
|
+
The shared foundation of the FilaMind suite - one framework-agnostic TypeScript library
|
|
6
|
+
that talks to the printer, holds its state honestly, and looks the same everywhere.
|
|
7
|
+
|
|
8
|
+
**Built by Egyptian makers, for world makers. Happy printing.** 🇪🇬
|
|
9
|
+
|
|
10
|
+
A small-team hobby project, built and tested on real printers. The code is all here to read.
|
|
11
|
+
|
|
12
|
+
[](https://ko-fi.com/I2I119XEIV)
|
|
5
13
|
|
|
6
14
|
[](https://github.com/filamind-app/filamind-core/actions/workflows/ci.yml)
|
|
7
15
|
[](https://www.npmjs.com/package/@filamind-app/core)
|
|
8
16
|
[](LICENSE)
|
|
9
17
|
[](https://www.typescriptlang.org)
|
|
18
|
+
[](https://www.klipper3d.org)
|
|
19
|
+
[](https://moonraker.readthedocs.io)
|
|
20
|
+
|
|
21
|
+
[Install](#install) · [What's inside](#whats-inside) · [Quick start](#quick-start) · [Tested on real printers](#tested-on-real-printers) · [Languages](#languages) · [Docs](#documentation) · [Support](#support)
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
`@filamind-app/core` is the shared core of the FilaMind suite. It is the one place that knows how
|
|
26
|
+
to open a Moonraker connection, keep printer state honest, remember a user's settings, theme the UI,
|
|
27
|
+
and translate it - so the three FilaMind surfaces (**3d** on the web, **screen** on a touch panel,
|
|
28
|
+
and the **flow** touch app) all behave the same way without copying that logic three times. It is
|
|
29
|
+
pure TypeScript with no UI framework inside: the reactive layer is a plain observable, and each
|
|
30
|
+
surface adapts it to Vue, signals, or whatever it uses. One core, many faces.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
It is published to npm as `@filamind-app/core`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @filamind-app/core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The package is ESM-only and ships its own type declarations. It has **zero runtime dependencies** -
|
|
41
|
+
nothing is pulled in behind it. Inside the FilaMind suite it is consumed through the workspace during
|
|
42
|
+
development, so changes to the core are picked up by 3d, screen, and flow without a publish step.
|
|
43
|
+
|
|
44
|
+
## What's inside
|
|
45
|
+
|
|
46
|
+
| Piece | What it gives you |
|
|
13
47
|
| --- | --- |
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `i18n/locale-meta.ts` | the 19 shipped locales `{ code, name, rtl, dir }` + RTL list + CLDR `pluralCategory` (`Intl.PluralRules`) |
|
|
33
|
-
| `i18n/translator.ts` | framework-agnostic `translate` / `Translator` + the backend message-code contract (`resolveMessage`) |
|
|
34
|
-
| `i18n/locales/*.json` | drop-in catalogs (en + ar shipped as proof, incl. the full Arabic plural set) |
|
|
35
|
-
| `settings/settings.ts` | unified user settings (theme · locale · density · motif · reduced-motion): `SettingsStore` (patch/hydrate/reset/export/import), **`migrate()`** (versioned, enum-coercing — `import()` trusts no foreign JSON), `applySettings` (re-theme + RTL dir), persistence seam (Moonraker-DB / localStorage) |
|
|
36
|
-
|
|
37
|
-
## Wiring (the consumer entry point)
|
|
48
|
+
| **Moonraker client** | A reconnecting JSON-RPC WebSocket plus a REST file channel, behind a backend-agnostic connector seam. Backs off with jitter, re-subscribes on reconnect, and times out stuck requests. |
|
|
49
|
+
| **Zero-config discovery** | Finds the printer's WebSocket for you by racing the likely endpoints; the first to open wins, and an explicit override always wins first. |
|
|
50
|
+
| **FilaMindSession** | The orchestrator. It connects, identifies, queries, subscribes, and routes every update into one place - and re-seeds itself after a firmware restart so the UI never shows stale data as live. |
|
|
51
|
+
| **Printer state** | One reactive, merge-patched model of the printer, coalesced so the UI isn't thrashed, with a fast path so live motion stays smooth. |
|
|
52
|
+
| **Provenance** | "Never-lie" stamped values that carry where they came from and when, so a widget can dim or strike anything that is stale or unknown. |
|
|
53
|
+
| **WriteArbiter** | A single fail-closed chokepoint every mutation passes through: if state isn't trustworthy, or safe-mode is on, the write is refused. |
|
|
54
|
+
| **Settings store** | One unified, persisted user-settings model (theme, language, density, motifs) that roams across every surface on the same printer, with strict versioned migration. |
|
|
55
|
+
| **Theme tokens** | The FilaMind palette as `--fm-*` CSS variables - three signature Pharaonic themes plus a neutral light and dark - so one switch restyles everything. |
|
|
56
|
+
| **i18n** | Metadata for 19 locales (including right-to-left), correct plural rules, and a tiny framework-agnostic translator that 3d, screen, and flow all share. |
|
|
57
|
+
| **Widget registry & dashboard** | Features register themselves without touching the core, declare which surfaces they target, and resolve into one adaptive dashboard per screen size. |
|
|
58
|
+
| **Remote control bus** | A UI-only command bus so one FilaMind surface can steer another (navigate, message, locate). It can never move the printer - those still go through the WriteArbiter. |
|
|
59
|
+
| **Backup, identity, logging** | Reversible restore points, a stable per-machine id, and a ring-buffer logger that feeds the diagnostics bundle. |
|
|
60
|
+
|
|
61
|
+
The full public surface is exported from a single entry point; the details of how each piece works
|
|
62
|
+
live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
38
66
|
```ts
|
|
39
67
|
import { MoonrakerClient, FilaMindSession, FULL_CONTROL } from '@filamind-app/core'
|
|
40
68
|
|
|
@@ -45,33 +73,100 @@ const session = new FilaMindSession(connector, {
|
|
|
45
73
|
})
|
|
46
74
|
await session.start()
|
|
47
75
|
|
|
48
|
-
// reactive
|
|
49
|
-
session.live.subscribe((
|
|
50
|
-
session.printer.objects.subscribe((
|
|
51
|
-
session.prompt.subscribe((
|
|
76
|
+
// reactive and framework-agnostic - bind these to Pinia, signals, or anything:
|
|
77
|
+
session.live.subscribe((live) => {/* dim the UI when the data isn't trustworthy-live */})
|
|
78
|
+
session.printer.objects.subscribe((objects) => {/* render telemetry */})
|
|
79
|
+
session.prompt.subscribe((dialog) => {/* show a Klipper modal */})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Don't know the printer's address? Let the core find it:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { resolveMoonrakerUrl, MoonrakerClient } from '@filamind-app/core'
|
|
86
|
+
|
|
87
|
+
const url = await resolveMoonrakerUrl() // races the likely endpoints, first to open wins
|
|
88
|
+
const connector = new MoonrakerClient({ url })
|
|
52
89
|
```
|
|
53
90
|
|
|
54
|
-
|
|
91
|
+
There is a worked example for settings, theming, and adding a widget in
|
|
92
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#using-the-core).
|
|
93
|
+
|
|
94
|
+
## Tested on real printers
|
|
95
|
+
|
|
96
|
+
The core is exercised by the FilaMind suite on two machines that disagree on almost everything that
|
|
97
|
+
matters to a control surface. The first is a Sovol SV08: an STM32F103 mainboard, TMC2209 drivers
|
|
98
|
+
over UART, a USB toolhead, and a BTT CB1 host. The second is a Voron-class CoreXY: an STM32H723
|
|
99
|
+
mainboard, six TMC5160 drivers on a shared software-SPI bus, a CAN toolhead, and a Raspberry Pi 4.
|
|
100
|
+
The pieces here that hurt to get wrong - the reconnect and re-seed logic, the Klippy-aware live gate,
|
|
101
|
+
the fail-closed write path - are the ones those two printers stress, and the library ships **101
|
|
102
|
+
pure-logic tests** that pin that behaviour down without needing a printer attached.
|
|
103
|
+
|
|
104
|
+
## How it's built
|
|
105
|
+
|
|
106
|
+
Pure TypeScript in `strict` mode, no UI framework imported anywhere in the core. Reactivity is a
|
|
107
|
+
small hand-written observable that each surface adapts to its own framework. Everything that touches
|
|
108
|
+
the network - the WebSocket, the discovery probes - is injectable, so the whole library is unit-tested
|
|
109
|
+
without a real socket. It builds to a single ESM bundle with type declarations via
|
|
110
|
+
[tsup](https://tsup.egoist.dev), and a CI guard keeps the published code free of any reference to
|
|
111
|
+
other projects. The deeper design is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
112
|
+
|
|
113
|
+
## Languages
|
|
114
|
+
|
|
115
|
+
The core carries the suite's shared internationalization foundation: metadata for **19 locales**,
|
|
116
|
+
the correct CLDR plural rules for each, and a tiny translator that 3d, screen, and flow all use so
|
|
117
|
+
they translate identically. The catalogs themselves are drop-in JSON, one folder per locale - adding
|
|
118
|
+
a language needs no code change. English and Arabic (right-to-left, with the full Arabic plural set)
|
|
119
|
+
ship in the package as a working proof; the surfaces carry the rest.
|
|
120
|
+
|
|
121
|
+
| | | | |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| English | Español | 简体中文 (Simplified Chinese) | 日本語 (Japanese) |
|
|
124
|
+
| العربية (Arabic, RTL) | Français | 繁體中文 (Traditional Chinese) | 한국어 (Korean) |
|
|
125
|
+
| Deutsch (German) | Русский (Russian) | Português (Brasil) | Tiếng Việt (Vietnamese) |
|
|
126
|
+
| Italiano | Nederlands (Dutch) | Polski (Polish) | Bahasa Indonesia |
|
|
127
|
+
| Türkçe (Turkish) | Українська (Ukrainian) | हिन्दी (Hindi) | |
|
|
128
|
+
|
|
129
|
+
## Develop
|
|
130
|
+
|
|
55
131
|
```bash
|
|
56
132
|
npm install
|
|
57
|
-
npm run type-check # tsc --noEmit
|
|
58
|
-
npm test # vitest (
|
|
59
|
-
npm run build # tsup
|
|
133
|
+
npm run type-check # tsc --noEmit (strict)
|
|
134
|
+
npm test # vitest run (101 tests, 13 files)
|
|
135
|
+
npm run build # tsup > dist/ (ESM + .d.ts)
|
|
60
136
|
```
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
138
|
+
`npm run dev` rebuilds on change. CI runs the type check, the tests, the build, and the
|
|
139
|
+
no-external-references guard on every push and pull request. Releases are cut by bumping the
|
|
140
|
+
version and pushing a matching `vX.Y.Z` tag; the workflow builds, publishes to npm (with provenance,
|
|
141
|
+
when a token is configured), and creates a GitHub Release from the changelog. The full contributor
|
|
142
|
+
and release flow is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#building-testing-and-releasing).
|
|
143
|
+
|
|
144
|
+
## Documentation
|
|
145
|
+
|
|
146
|
+
| Document | What's inside |
|
|
147
|
+
| -------- | ------------- |
|
|
148
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design, key modules, data flow, build/release internals, and conventions |
|
|
149
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release history |
|
|
150
|
+
| [SECURITY.md](SECURITY.md) | How to report a vulnerability privately |
|
|
151
|
+
|
|
152
|
+
## Support
|
|
153
|
+
|
|
154
|
+
`@filamind-app/core` is free and open source, built and maintained in spare time. If the suite it
|
|
155
|
+
powers saved you a tuning session, or you just want to see it grow, a coffee helps keep the work
|
|
156
|
+
going. Code, data, and ideas are just as welcome.
|
|
157
|
+
|
|
158
|
+
<div align="center">
|
|
159
|
+
|
|
160
|
+
[](https://ko-fi.com/I2I119XEIV)
|
|
161
|
+
|
|
162
|
+
</div>
|
|
68
163
|
|
|
69
164
|
## Credits
|
|
70
165
|
|
|
71
|
-
Built and maintained by the DeltaFabs team
|
|
166
|
+
Built and maintained by the DeltaFabs team. **Built by Egyptian makers, for world makers.**
|
|
72
167
|
|
|
73
|
-
-
|
|
74
|
-
- Ahmed
|
|
168
|
+
- Abdelmonem Awad - <eg2@live.com>
|
|
169
|
+
- Ahmed Bebars - <Ahmedbebars1@gmail.com>
|
|
75
170
|
- Kareem Salama - <Golden.kiko@gmail.com>
|
|
76
171
|
|
|
77
172
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type Source = 'live' | 'cache' | 'optimistic' | 'unknown';
|
|
2
2
|
interface Stamped<T> {
|
|
3
3
|
value: T;
|
|
4
|
-
/** when this value arrived (ms epoch, client clock
|
|
4
|
+
/** when this value arrived (ms epoch, client clock - Moonraker has no server timestamp) */
|
|
5
5
|
ts: number;
|
|
6
6
|
source: Source;
|
|
7
7
|
/** ms after `ts` when the value should be treated as stale (omit = never goes stale on its own) */
|
|
@@ -169,7 +169,7 @@ type NotifyEvent = {
|
|
|
169
169
|
/** Parse a raw (method, params) notify into a typed event, or null for an unhandled method. */
|
|
170
170
|
declare function parseNotifyEvent(method: string, params: unknown): NotifyEvent | null;
|
|
171
171
|
|
|
172
|
-
/** Minimal WebSocket surface the client uses
|
|
172
|
+
/** Minimal WebSocket surface the client uses - the DOM WebSocket satisfies it; tests fake it. */
|
|
173
173
|
interface WebSocketLike {
|
|
174
174
|
send(data: string): void;
|
|
175
175
|
close(): void;
|
|
@@ -185,7 +185,7 @@ interface MoonrakerClientOptions {
|
|
|
185
185
|
url: string;
|
|
186
186
|
requestTimeoutMs?: number;
|
|
187
187
|
maxBackoffMs?: number;
|
|
188
|
-
/** stop reconnecting after this many consecutive attempts
|
|
188
|
+
/** stop reconnecting after this many consecutive attempts > terminal 'closed' (default: unlimited) */
|
|
189
189
|
maxReconnectAttempts?: number;
|
|
190
190
|
wsFactory?: (url: string) => WebSocketLike;
|
|
191
191
|
logger?: Logger;
|
|
@@ -223,13 +223,13 @@ declare class MoonrakerClient implements Connector {
|
|
|
223
223
|
private onMessage;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
/** A minimal view of `location`
|
|
226
|
+
/** A minimal view of `location` - the DOM Location satisfies it; tests pass a plain object. */
|
|
227
227
|
interface LocationLike {
|
|
228
228
|
protocol?: string;
|
|
229
229
|
hostname?: string;
|
|
230
230
|
}
|
|
231
231
|
interface DiscoveryOptions {
|
|
232
|
-
/** Explicit runtime override (e.g. from settings). If set, it wins immediately
|
|
232
|
+
/** Explicit runtime override (e.g. from settings). If set, it wins immediately - no probing. */
|
|
233
233
|
override?: string;
|
|
234
234
|
/** Candidate ws(s):// URLs to race. If omitted, they are derived from `location`. */
|
|
235
235
|
candidates?: string[];
|
|
@@ -262,7 +262,7 @@ declare const FULL_CONTROL: SubscriptionMap;
|
|
|
262
262
|
type SubscriptionTier = 'narrow' | 'full';
|
|
263
263
|
declare function tier(t: SubscriptionTier): SubscriptionMap;
|
|
264
264
|
/** Union of subscription maps (`null` = "all fields" wins). The single source for the
|
|
265
|
-
* null-wins merge rule
|
|
265
|
+
* null-wins merge rule - consumers compose a baseline tier with widget-declared needs. */
|
|
266
266
|
declare function mergeSubscriptions(...maps: SubscriptionMap[]): SubscriptionMap;
|
|
267
267
|
|
|
268
268
|
type KlippyState = 'ready' | 'startup' | 'shutdown' | 'error' | 'disconnected';
|
|
@@ -344,7 +344,7 @@ interface SessionOptions {
|
|
|
344
344
|
subscriptions?: SubscriptionMap;
|
|
345
345
|
identify?: IdentifyInfo;
|
|
346
346
|
logger?: Logger;
|
|
347
|
-
/** Receive cross-surface agent events (notify_agent_event)
|
|
347
|
+
/** Receive cross-surface agent events (notify_agent_event) - e.g. remote-control commands from
|
|
348
348
|
* another FilaMind surface. Discrete (fired per event, never replayed), so no stale re-fire. */
|
|
349
349
|
onAgentEvent?: (ev: AgentEvent) => void;
|
|
350
350
|
}
|
|
@@ -366,7 +366,7 @@ declare class FilaMindSession {
|
|
|
366
366
|
setSubscriptions(subs: SubscriptionMap): void;
|
|
367
367
|
start(): Promise<void>;
|
|
368
368
|
stop(): void;
|
|
369
|
-
/** Staged init
|
|
369
|
+
/** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
|
|
370
370
|
* Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
|
|
371
371
|
private bootstrap;
|
|
372
372
|
private onUpdate;
|
|
@@ -396,7 +396,7 @@ declare class WriteArbiter {
|
|
|
396
396
|
run<T>(action: string, fn: () => Promise<T>): Promise<T>;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
/** FNV-1a 32-bit hex
|
|
399
|
+
/** FNV-1a 32-bit hex - deterministic, dependency-free. */
|
|
400
400
|
declare function fnv1a(s: string): string;
|
|
401
401
|
/** Derive a stable `fm-<hash>` machine id from Moonraker. Falls back to `fm-unknown`. */
|
|
402
402
|
declare function deriveMachineId(connector: Connector): Promise<string>;
|
|
@@ -438,7 +438,7 @@ interface WidgetDefinition {
|
|
|
438
438
|
title: string;
|
|
439
439
|
icon?: string;
|
|
440
440
|
description?: string;
|
|
441
|
-
/** lazy component loader
|
|
441
|
+
/** lazy component loader - the host framework (Vue) resolves it (kept as `unknown` here) */
|
|
442
442
|
component: () => Promise<unknown>;
|
|
443
443
|
defaultSize?: {
|
|
444
444
|
w: number;
|
|
@@ -454,9 +454,45 @@ declare function getWidget(id: string): WidgetDefinition | undefined;
|
|
|
454
454
|
declare function getWidgets(target?: SurfaceTarget): WidgetDefinition[];
|
|
455
455
|
/** The union of the given widgets' declared subscriptions (subscribe once). */
|
|
456
456
|
declare function aggregateSubscriptions(ids: string[]): SubscriptionMap;
|
|
457
|
-
/** Test/host helper
|
|
457
|
+
/** Test/host helper - clear the registry. */
|
|
458
458
|
declare function _resetRegistry(): void;
|
|
459
459
|
|
|
460
|
+
type SurfaceHint = 'dense-desktop' | 'thumb-phone' | 'big-touch';
|
|
461
|
+
/** Derive the layout hint from the surface + viewport width (px). The touch screen is always
|
|
462
|
+
* big-touch; the 3d web UI is dense on wide viewports and a single thumb column when narrow. */
|
|
463
|
+
declare function surfaceHint(target: SurfaceTarget, viewportWidth: number): SurfaceHint;
|
|
464
|
+
/** One slot in the shared dashboard definition. */
|
|
465
|
+
interface DashboardSlot {
|
|
466
|
+
widgetId: string;
|
|
467
|
+
/** ascending display order; slots without it sort after ordered ones */
|
|
468
|
+
order?: number;
|
|
469
|
+
/** hide this widget on these hints (e.g. a dense-only widget hidden on thumb-phone) */
|
|
470
|
+
hideOn?: SurfaceHint[];
|
|
471
|
+
}
|
|
472
|
+
/** The single serializable dashboard definition (persisted in UserSettings, machineUUID-keyed). */
|
|
473
|
+
interface DashboardLayout {
|
|
474
|
+
version: number;
|
|
475
|
+
slots: DashboardSlot[];
|
|
476
|
+
}
|
|
477
|
+
declare const DASHBOARD_LAYOUT_VERSION = 1;
|
|
478
|
+
interface ResolvedDashboard {
|
|
479
|
+
hint: SurfaceHint;
|
|
480
|
+
columns: number;
|
|
481
|
+
/** the widgets to render, in order, already filtered for this hint */
|
|
482
|
+
widgets: WidgetDefinition[];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Adapt one layout definition + the available widgets to a surface hint.
|
|
486
|
+
*
|
|
487
|
+
* Slots are honoured first (ordered, minus those hidden on this hint); any registered widget the
|
|
488
|
+
* layout doesn't mention is appended (so newly added widgets still appear). A widget a slot hides
|
|
489
|
+
* on this hint stays hidden even though it is available. With no layout, all available widgets
|
|
490
|
+
* render in registry order.
|
|
491
|
+
*/
|
|
492
|
+
declare function resolveDashboard(layout: DashboardLayout | undefined, available: WidgetDefinition[], hint: SurfaceHint): ResolvedDashboard;
|
|
493
|
+
/** Coerce an arbitrary blob into a valid DashboardLayout, or undefined. Used by settings migrate. */
|
|
494
|
+
declare function coerceDashboardLayout(raw: unknown): DashboardLayout | undefined;
|
|
495
|
+
|
|
460
496
|
interface ThemeTokens {
|
|
461
497
|
bg: string;
|
|
462
498
|
surface: string;
|
|
@@ -526,14 +562,14 @@ interface BackendMessage {
|
|
|
526
562
|
declare function resolveMessage(catalog: Catalog, m: BackendMessage, locale?: string): string;
|
|
527
563
|
|
|
528
564
|
interface CommandSenderOptions {
|
|
529
|
-
/** Stable agent name (pinned
|
|
565
|
+
/** Stable agent name (pinned - a drifting name registers phantom duplicates on reconnect). */
|
|
530
566
|
client_name: string;
|
|
531
567
|
version: string;
|
|
532
568
|
/** Required: Moonraker documents `url` as mandatory for identify; omitting it can make a strict
|
|
533
569
|
* host reject the agent identify, silently stranding the bus. */
|
|
534
570
|
url: string;
|
|
535
571
|
logger?: Logger;
|
|
536
|
-
/** Fires whenever readiness flips (connected+identified)
|
|
572
|
+
/** Fires whenever readiness flips (connected+identified) - lets a UI gate its send affordances. */
|
|
537
573
|
onReadyChange?: (ready: boolean) => void;
|
|
538
574
|
}
|
|
539
575
|
declare class CommandSender {
|
|
@@ -546,14 +582,14 @@ declare class CommandSender {
|
|
|
546
582
|
private retryDelayMs;
|
|
547
583
|
private readonly log;
|
|
548
584
|
constructor(connector: Connector, opts: CommandSenderOptions);
|
|
549
|
-
/** true only when the bus is connected AND identified
|
|
585
|
+
/** true only when the bus is connected AND identified - gate remote-control affordances on this. */
|
|
550
586
|
get ready(): boolean;
|
|
551
587
|
/** Open the agent connection and identify once. Call alongside the app's main session start. */
|
|
552
588
|
start(): Promise<void>;
|
|
553
589
|
/** Close the agent connection (call on app teardown so the socket doesn't linger). */
|
|
554
590
|
stop(): void;
|
|
555
591
|
/** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
|
|
556
|
-
* while the bus is down is dropped (logged), never queued/replayed
|
|
592
|
+
* while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
|
|
557
593
|
* minutes later would yank a screen unexpectedly. */
|
|
558
594
|
send(cmd: RemoteCommand): Promise<void>;
|
|
559
595
|
navigate(view: RemoteView): Promise<void>;
|
|
@@ -582,12 +618,14 @@ interface UserSettings {
|
|
|
582
618
|
motifDensity: 'off' | 'subtle' | 'full';
|
|
583
619
|
/** reduce animation/illustration for low-power or accessibility */
|
|
584
620
|
reducedMotion: boolean;
|
|
621
|
+
/** the single per-surface dashboard definition (resolved per surface + viewport at render) */
|
|
622
|
+
dashboardLayout?: DashboardLayout;
|
|
585
623
|
}
|
|
586
624
|
declare const DEFAULT_SETTINGS: UserSettings;
|
|
587
|
-
/** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings
|
|
625
|
+
/** Coerce an arbitrary (old / foreign / partial) blob into valid UserSettings - drops unknown keys,
|
|
588
626
|
* validates every enum, stamps the current version. Used by hydrate() + import(). */
|
|
589
627
|
declare function migrate(raw: unknown): UserSettings;
|
|
590
|
-
/** App-provided persistence
|
|
628
|
+
/** App-provided persistence - the Moonraker-DB impl is keyed by machineUUID; tests use memory. */
|
|
591
629
|
interface SettingsPersistence {
|
|
592
630
|
load(): Promise<Partial<UserSettings>>;
|
|
593
631
|
save(settings: UserSettings): Promise<void>;
|
|
@@ -595,7 +633,7 @@ interface SettingsPersistence {
|
|
|
595
633
|
declare function memoryPersistence(): SettingsPersistence;
|
|
596
634
|
declare function localStoragePersistence(storageKey?: string): SettingsPersistence;
|
|
597
635
|
/** Settings persisted in the printer's Moonraker database, so they roam across every FilaMind
|
|
598
|
-
* surface on that printer (3d, screen, …)
|
|
636
|
+
* surface on that printer (3d, screen, …) - the F10 remote-config substrate. */
|
|
599
637
|
declare function moonrakerDbPersistence(connector: Connector): SettingsPersistence;
|
|
600
638
|
/** Roam a (locally-persisted) SettingsStore across surfaces via a shared remote store: pull the
|
|
601
639
|
* shared settings whenever the connection goes live (the shared copy wins on connect), and
|
|
@@ -623,4 +661,4 @@ declare function applySettings(s: UserSettings, el?: {
|
|
|
623
661
|
dir: 'ltr' | 'rtl';
|
|
624
662
|
};
|
|
625
663
|
|
|
626
|
-
export { type AgentAllow, type AgentEvent, type BackendMessage, type Catalog, CommandSender, type CommandSenderOptions, type ConnectionState, type Connector, type ConnectorCallbacks, DEFAULT_LOCALE, DEFAULT_SETTINGS, DEFAULT_THEME, type DiscoveryOptions, FILAMIND_COMMAND_EVENT, FULL_CONTROL, FilaMindSession, type IdentifyInfo, type KlippyState, LOCALES, type Listener, type LocaleMeta, type LocationLike, type LogEntry, type LogLevel, Logger, MoonrakerClient, type MoonrakerClientOptions, type MoonrakerMethods, NARROW_STATUS, NULL_LOGGER, type NotifyEvent, Observable, type Params, type PluralCategory, type PrinterInfo, type PrinterObjects, PrinterState, type PromptButton, type PromptDialog, type PromptEvent, PromptParser, type QueryResult, REMOTE_MESSAGE_LEVELS, REMOTE_VIEWS, RTL_LOCALES, type RemoteCommand, type RemoteMessageLevel, type RemoteView, type RestorePoint, RestorePoints, type RestoreStore, RpcError, SETTINGS_VERSION, SUBSCRIPTION_CONTRACT_VERSION, type ServerInfo, type SessionOptions, type SettingsPersistence, SettingsStore, type Source, type Stamped, type SubscriptionMap, type SubscriptionTier, type SurfaceTarget, type ThemeName, type ThemeTokens, Translator, UNKNOWN, type UserSettings, type WebSocketLike, type WidgetDefinition, WriteArbiter, type WriteGuard, WriteRefused, _resetRegistry, aggregateSubscriptions, applySettings, applyTheme, deepMerge, deriveCandidates, deriveKlippyState, deriveMachineId, fnv1a, freshness, getWidget, getWidgets, handleAgentCommand, isKlippyLive, isRtl, isStale, localStoragePersistence, localeMeta, memoryPersistence, memoryRestoreStore, mergeSubscriptions, migrate, moonrakerDbPersistence, parseAgentEvent, parseCommand, parseNotifyEvent, pluralCategory, registerWidget, resolveMessage, resolveMoonrakerUrl, roamSettings, stamp, themeToCssVars, themes, tier, translate };
|
|
664
|
+
export { type AgentAllow, type AgentEvent, type BackendMessage, type Catalog, CommandSender, type CommandSenderOptions, type ConnectionState, type Connector, type ConnectorCallbacks, DASHBOARD_LAYOUT_VERSION, DEFAULT_LOCALE, DEFAULT_SETTINGS, DEFAULT_THEME, type DashboardLayout, type DashboardSlot, type DiscoveryOptions, FILAMIND_COMMAND_EVENT, FULL_CONTROL, FilaMindSession, type IdentifyInfo, type KlippyState, LOCALES, type Listener, type LocaleMeta, type LocationLike, type LogEntry, type LogLevel, Logger, MoonrakerClient, type MoonrakerClientOptions, type MoonrakerMethods, NARROW_STATUS, NULL_LOGGER, type NotifyEvent, Observable, type Params, type PluralCategory, type PrinterInfo, type PrinterObjects, PrinterState, type PromptButton, type PromptDialog, type PromptEvent, PromptParser, type QueryResult, REMOTE_MESSAGE_LEVELS, REMOTE_VIEWS, RTL_LOCALES, type RemoteCommand, type RemoteMessageLevel, type RemoteView, type ResolvedDashboard, type RestorePoint, RestorePoints, type RestoreStore, RpcError, SETTINGS_VERSION, SUBSCRIPTION_CONTRACT_VERSION, type ServerInfo, type SessionOptions, type SettingsPersistence, SettingsStore, type Source, type Stamped, type SubscriptionMap, type SubscriptionTier, type SurfaceHint, type SurfaceTarget, type ThemeName, type ThemeTokens, Translator, UNKNOWN, type UserSettings, type WebSocketLike, type WidgetDefinition, WriteArbiter, type WriteGuard, WriteRefused, _resetRegistry, aggregateSubscriptions, applySettings, applyTheme, coerceDashboardLayout, deepMerge, deriveCandidates, deriveKlippyState, deriveMachineId, fnv1a, freshness, getWidget, getWidgets, handleAgentCommand, isKlippyLive, isRtl, isStale, localStoragePersistence, localeMeta, memoryPersistence, memoryRestoreStore, mergeSubscriptions, migrate, moonrakerDbPersistence, parseAgentEvent, parseCommand, parseNotifyEvent, pluralCategory, registerWidget, resolveDashboard, resolveMessage, resolveMoonrakerUrl, roamSettings, stamp, surfaceHint, themeToCssVars, themes, tier, translate };
|
package/dist/index.js
CHANGED
|
@@ -291,7 +291,8 @@ var MoonrakerClient = class {
|
|
|
291
291
|
if (!p) return;
|
|
292
292
|
clearTimeout(p.timer);
|
|
293
293
|
this.pending.delete(msg.id);
|
|
294
|
-
if (msg.error)
|
|
294
|
+
if (msg.error)
|
|
295
|
+
p.reject(new RpcError(msg.error.message ?? "rpc error", msg.error.code, msg.error.data));
|
|
295
296
|
else p.resolve(msg.result);
|
|
296
297
|
return;
|
|
297
298
|
}
|
|
@@ -617,18 +618,18 @@ var FilaMindSession = class {
|
|
|
617
618
|
this.connector.close();
|
|
618
619
|
this.setLive(false);
|
|
619
620
|
}
|
|
620
|
-
/** Staged init
|
|
621
|
+
/** Staged init - runs on first connect AND on reconnect AND on notify_klippy_ready.
|
|
621
622
|
* Guarded so overlapping triggers (reconnect + klippy_ready) can't interleave two seeds. */
|
|
622
623
|
async bootstrap() {
|
|
623
624
|
if (this.bootstrapping) return;
|
|
624
625
|
this.bootstrapping = true;
|
|
625
626
|
try {
|
|
626
627
|
if (this.opts.identify) {
|
|
627
|
-
await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch(
|
|
628
|
-
(e) => this.log.warn("identify failed", String(e))
|
|
629
|
-
);
|
|
628
|
+
await this.connector.call("server.connection.identify", { ...this.opts.identify }).catch((e) => this.log.warn("identify failed", String(e)));
|
|
630
629
|
}
|
|
631
|
-
const info = await this.connector.call(
|
|
630
|
+
const info = await this.connector.call(
|
|
631
|
+
"server.info"
|
|
632
|
+
);
|
|
632
633
|
this.capabilities.set(info?.components ?? []);
|
|
633
634
|
const ks = deriveKlippyState(info?.klippy_state);
|
|
634
635
|
this.klippy.set(ks);
|
|
@@ -754,7 +755,10 @@ function memoryRestoreStore() {
|
|
|
754
755
|
byMachine.set(m, arr);
|
|
755
756
|
},
|
|
756
757
|
remove: async (m, id) => {
|
|
757
|
-
byMachine.set(
|
|
758
|
+
byMachine.set(
|
|
759
|
+
m,
|
|
760
|
+
(byMachine.get(m) ?? []).filter((p) => p.id !== id)
|
|
761
|
+
);
|
|
758
762
|
}
|
|
759
763
|
};
|
|
760
764
|
}
|
|
@@ -834,9 +838,63 @@ function _resetRegistry() {
|
|
|
834
838
|
registry.clear();
|
|
835
839
|
}
|
|
836
840
|
|
|
841
|
+
// src/registry/dashboard-resolver.ts
|
|
842
|
+
var COLUMNS = {
|
|
843
|
+
"dense-desktop": 3,
|
|
844
|
+
"thumb-phone": 1,
|
|
845
|
+
"big-touch": 2
|
|
846
|
+
};
|
|
847
|
+
function surfaceHint(target, viewportWidth) {
|
|
848
|
+
if (target === "screen") return "big-touch";
|
|
849
|
+
return viewportWidth < 640 ? "thumb-phone" : "dense-desktop";
|
|
850
|
+
}
|
|
851
|
+
var DASHBOARD_LAYOUT_VERSION = 1;
|
|
852
|
+
var HINTS = ["dense-desktop", "thumb-phone", "big-touch"];
|
|
853
|
+
function resolveDashboard(layout, available, hint) {
|
|
854
|
+
const byId = new Map(available.map((w) => [w.id, w]));
|
|
855
|
+
const slots = layout?.slots ?? [];
|
|
856
|
+
const hiddenHere = new Set(
|
|
857
|
+
slots.filter((s) => (s.hideOn ?? []).includes(hint)).map((s) => s.widgetId)
|
|
858
|
+
);
|
|
859
|
+
const used = /* @__PURE__ */ new Set();
|
|
860
|
+
const widgets = [];
|
|
861
|
+
for (const slot of [...slots].filter((s) => !(s.hideOn ?? []).includes(hint)).sort(
|
|
862
|
+
(a, b) => (a.order ?? Number.POSITIVE_INFINITY) - (b.order ?? Number.POSITIVE_INFINITY)
|
|
863
|
+
)) {
|
|
864
|
+
const w = byId.get(slot.widgetId);
|
|
865
|
+
if (w && !used.has(w.id)) {
|
|
866
|
+
widgets.push(w);
|
|
867
|
+
used.add(w.id);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (const w of available) {
|
|
871
|
+
if (!used.has(w.id) && !hiddenHere.has(w.id)) widgets.push(w);
|
|
872
|
+
}
|
|
873
|
+
return { hint, columns: COLUMNS[hint], widgets };
|
|
874
|
+
}
|
|
875
|
+
function coerceDashboardLayout(raw) {
|
|
876
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
877
|
+
const r = raw;
|
|
878
|
+
if (!Array.isArray(r.slots)) return void 0;
|
|
879
|
+
const slots = [];
|
|
880
|
+
for (const item of r.slots) {
|
|
881
|
+
if (!item || typeof item !== "object") continue;
|
|
882
|
+
const s = item;
|
|
883
|
+
if (typeof s.widgetId !== "string" || !s.widgetId) continue;
|
|
884
|
+
const slot = { widgetId: s.widgetId };
|
|
885
|
+
if (typeof s.order === "number" && Number.isFinite(s.order)) slot.order = s.order;
|
|
886
|
+
if (Array.isArray(s.hideOn)) {
|
|
887
|
+
const hideOn = s.hideOn.filter((h) => HINTS.includes(h));
|
|
888
|
+
if (hideOn.length) slot.hideOn = hideOn;
|
|
889
|
+
}
|
|
890
|
+
slots.push(slot);
|
|
891
|
+
}
|
|
892
|
+
return { version: DASHBOARD_LAYOUT_VERSION, slots };
|
|
893
|
+
}
|
|
894
|
+
|
|
837
895
|
// src/theme/tokens.ts
|
|
838
896
|
var themes = {
|
|
839
|
-
// flagship default
|
|
897
|
+
// flagship default - obsidian + gold + lapis + turquoise
|
|
840
898
|
tutankhamun: {
|
|
841
899
|
bg: "#0E0F12",
|
|
842
900
|
surface: "#17171C",
|
|
@@ -884,7 +942,7 @@ var themes = {
|
|
|
884
942
|
warning: "#D98E2B",
|
|
885
943
|
danger: "#8C1F1A"
|
|
886
944
|
},
|
|
887
|
-
// neutral light
|
|
945
|
+
// neutral light - a conventional bright UI for users who prefer it
|
|
888
946
|
light: {
|
|
889
947
|
bg: "#F7F8FA",
|
|
890
948
|
surface: "#FFFFFF",
|
|
@@ -900,7 +958,7 @@ var themes = {
|
|
|
900
958
|
warning: "#B45309",
|
|
901
959
|
danger: "#DC2626"
|
|
902
960
|
},
|
|
903
|
-
// neutral dark
|
|
961
|
+
// neutral dark - a conventional dark UI, distinct from the warm Pharaonic palettes
|
|
904
962
|
dark: {
|
|
905
963
|
bg: "#0F1115",
|
|
906
964
|
surface: "#171A21",
|
|
@@ -1016,7 +1074,7 @@ var CommandSender = class {
|
|
|
1016
1074
|
this.opts = opts;
|
|
1017
1075
|
this.log = opts.logger ?? NULL_LOGGER;
|
|
1018
1076
|
connector.setCallbacks({
|
|
1019
|
-
// Moonraker forgets the agent identity on disconnect
|
|
1077
|
+
// Moonraker forgets the agent identity on disconnect > re-identify on every genuine reconnect.
|
|
1020
1078
|
onReconnected: () => {
|
|
1021
1079
|
this.setIdentified(false);
|
|
1022
1080
|
void this.ensureIdentified().catch(() => this.scheduleRetry());
|
|
@@ -1036,7 +1094,7 @@ var CommandSender = class {
|
|
|
1036
1094
|
retryTimer;
|
|
1037
1095
|
retryDelayMs = 500;
|
|
1038
1096
|
log;
|
|
1039
|
-
/** true only when the bus is connected AND identified
|
|
1097
|
+
/** true only when the bus is connected AND identified - gate remote-control affordances on this. */
|
|
1040
1098
|
get ready() {
|
|
1041
1099
|
return this._ready;
|
|
1042
1100
|
}
|
|
@@ -1052,7 +1110,7 @@ var CommandSender = class {
|
|
|
1052
1110
|
this.setIdentified(false);
|
|
1053
1111
|
}
|
|
1054
1112
|
/** Broadcast a UI-only command to the other FilaMind surfaces. Best-effort: a command issued
|
|
1055
|
-
* while the bus is down is dropped (logged), never queued/replayed
|
|
1113
|
+
* while the bus is down is dropped (logged), never queued/replayed - a stale "navigate" fired
|
|
1056
1114
|
* minutes later would yank a screen unexpectedly. */
|
|
1057
1115
|
async send(cmd) {
|
|
1058
1116
|
if (this.connector.state !== "ready") {
|
|
@@ -1061,7 +1119,10 @@ var CommandSender = class {
|
|
|
1061
1119
|
}
|
|
1062
1120
|
try {
|
|
1063
1121
|
await this.ensureIdentified();
|
|
1064
|
-
await this.connector.call("connection.send_event", {
|
|
1122
|
+
await this.connector.call("connection.send_event", {
|
|
1123
|
+
event: FILAMIND_COMMAND_EVENT,
|
|
1124
|
+
data: cmd
|
|
1125
|
+
});
|
|
1065
1126
|
} catch (e) {
|
|
1066
1127
|
this.log.warn("command send failed", String(e));
|
|
1067
1128
|
}
|
|
@@ -1144,7 +1205,7 @@ var DENSITIES = /* @__PURE__ */ new Set(["comfortable", "compact"]);
|
|
|
1144
1205
|
var MOTIFS = /* @__PURE__ */ new Set(["off", "subtle", "full"]);
|
|
1145
1206
|
function migrate(raw) {
|
|
1146
1207
|
const r = raw && typeof raw === "object" ? raw : {};
|
|
1147
|
-
|
|
1208
|
+
const out = {
|
|
1148
1209
|
version: SETTINGS_VERSION,
|
|
1149
1210
|
theme: typeof r.theme === "string" && r.theme in themes ? r.theme : DEFAULT_SETTINGS.theme,
|
|
1150
1211
|
locale: typeof r.locale === "string" && LOCALES.some((l) => l.code === r.locale) ? r.locale : DEFAULT_SETTINGS.locale,
|
|
@@ -1152,6 +1213,9 @@ function migrate(raw) {
|
|
|
1152
1213
|
motifDensity: typeof r.motifDensity === "string" && MOTIFS.has(r.motifDensity) ? r.motifDensity : DEFAULT_SETTINGS.motifDensity,
|
|
1153
1214
|
reducedMotion: typeof r.reducedMotion === "boolean" ? r.reducedMotion : DEFAULT_SETTINGS.reducedMotion
|
|
1154
1215
|
};
|
|
1216
|
+
const layout = coerceDashboardLayout(r.dashboardLayout);
|
|
1217
|
+
if (layout) out.dashboardLayout = layout;
|
|
1218
|
+
return out;
|
|
1155
1219
|
}
|
|
1156
1220
|
function memoryPersistence() {
|
|
1157
1221
|
let store;
|
|
@@ -1266,6 +1330,7 @@ function applySettings(s, el) {
|
|
|
1266
1330
|
}
|
|
1267
1331
|
export {
|
|
1268
1332
|
CommandSender,
|
|
1333
|
+
DASHBOARD_LAYOUT_VERSION,
|
|
1269
1334
|
DEFAULT_LOCALE,
|
|
1270
1335
|
DEFAULT_SETTINGS,
|
|
1271
1336
|
DEFAULT_THEME,
|
|
@@ -1296,6 +1361,7 @@ export {
|
|
|
1296
1361
|
aggregateSubscriptions,
|
|
1297
1362
|
applySettings,
|
|
1298
1363
|
applyTheme,
|
|
1364
|
+
coerceDashboardLayout,
|
|
1299
1365
|
deepMerge,
|
|
1300
1366
|
deriveCandidates,
|
|
1301
1367
|
deriveKlippyState,
|
|
@@ -1320,10 +1386,12 @@ export {
|
|
|
1320
1386
|
parseNotifyEvent,
|
|
1321
1387
|
pluralCategory,
|
|
1322
1388
|
registerWidget,
|
|
1389
|
+
resolveDashboard,
|
|
1323
1390
|
resolveMessage,
|
|
1324
1391
|
resolveMoonrakerUrl,
|
|
1325
1392
|
roamSettings,
|
|
1326
1393
|
stamp,
|
|
1394
|
+
surfaceHint,
|
|
1327
1395
|
themeToCssVars,
|
|
1328
1396
|
themes,
|
|
1329
1397
|
tier,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@filamind-app/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Shared foundation for the FilaMind suite
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Shared foundation for the FilaMind suite - Moonraker client, reactive printer state, provenance-stamped values, design tokens, and the widget/plugin registry. Framework-agnostic TypeScript.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
34
34
|
"type-check": "tsc --noEmit",
|
|
35
35
|
"test": "vitest run",
|
|
36
|
-
"prepublishOnly": "npm run build"
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"format": "prettier --write \"src/**/*.ts\" \"*.{ts,js,json}\"",
|
|
38
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"*.{ts,js,json}\""
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"prettier": "3.9.6",
|
|
39
42
|
"tsup": "^8.5.1",
|
|
40
43
|
"typescript": "~6.0.3",
|
|
41
|
-
"vitest": "^4.1.
|
|
44
|
+
"vitest": "^4.1.10"
|
|
42
45
|
}
|
|
43
46
|
}
|