Repository navigation
Expand file tree
/
Copy pathprotocol.ts
More file actions
265 lines (252 loc) · 10 KB
/
Copy pathprotocol.ts
File metadata and controls
265 lines (252 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/* client/src/protocol.ts — public protocol types.
*
* Two layers, two sources:
* - Wire-protocol types (DeckMessage, PressMessage, ...) are
* generated from daemon/deckd/protocol.py — see
* scripts/codegen_protocol_ts.py. Edit the Python side and run
* `just check-protocol`; the drift guard fails CI if the two
* diverge (#76).
* - Schema-layer types (Widget, Icon, ...) are hand-curated. They
* mirror daemon/deckd/decks.py, which is YAML-loader concern,
* not wire. We keep them here for ergonomics — clients import
* ``Widget`` from this file — and rely on the daemon's Pydantic
* validators to backstop the editor's "save" path.
*
* The client-facing interfaces at the bottom (``ServerDeck`` etc.) are
* the names consumers import: the same wire shape, with ``widgets`` /
* ``icon`` widened to the schema-layer types.
*/
// Wire-protocol types — re-exported from the codegen artifact.
import type {
FocusedAppInfo,
DeckMessage,
StateMessage,
BrightnessMessage,
WidgetUpdateMessage,
MediaStateMessage,
ChromeMediaMessage,
ErrorMessage,
MediaErrorMessage,
MacroResultMessage,
ConfirmRequestMessage,
RunningWindowsMessage,
HelloMessage,
PressMessage,
JogMessage,
JogEndMessage,
PadMessage,
PadTapMessage,
PadDragMessage,
TypeMessage,
KeyMessage,
MediaCommandMessage,
SelectViewMessage,
ClearViewMessage,
RaiseWindowMessage,
ConfirmResponseMessage,
WindowListEntry,
} from "./protocol.generated";
export {
WINDOWS_VIEW_ID,
MPRIS_VIEW_ID,
EDITOR_VIEW_ID,
} from "./protocol.generated";
export type {
FocusedAppInfo,
DeckMessage,
StateMessage,
BrightnessMessage,
WidgetUpdateMessage,
MediaStateMessage,
MediaErrorMessage,
ChromeMediaMessage,
EventMessage,
MacroResultMessage,
ConfirmRequestMessage,
WindowListEntry,
RunningWindowsMessage,
ServerMessage,
HelloMessage,
PressMessage,
JogMessage,
JogEndMessage,
PadMessage,
PadTapMessage,
PadDragMessage,
TypeMessage,
KeyMessage,
MediaCommandMessage,
SelectViewMessage,
ClearViewMessage,
RaiseWindowMessage,
EnableEventsMessage,
DisableEventsMessage,
ConfirmResponseMessage,
MprisCommandRequest,
ErrorMessage,
ClientMessage,
} from "./protocol.generated";
/* Schema-layer types (hand-curated; mirror daemon/deckd/decks.py). */
/** An icon reference: ``source`` picks a client-side renderer (e.g.
* "lucide", "simple-icons"), ``name`` is resolved within it. The daemon
* relays this opaquely (ADR-0006); the client owns the source registry
* and renders a placeholder for an unknown source. */
export type Icon = { source: string; name: string };
/** One data point of a ``stats`` widget: a sensor ``source`` plus an
* optional short caption. When ``label`` is absent the client derives one
* from the source name (``cpu_percent`` → ``CPU``). */
export type Metric = { source: string; label?: string | null };
export type MediaHttp = { host?: string; port?: number; password_ref?: string | null };
export type MediaControl = "play" | "previous" | "next" | "volume" | "position" | "speed";
export type NowPlayingEmptyState = "show" | "hide";
/** A widget's extent in the reflow (ADR-0010). ``[w, h]`` is a column/row
* span (default ``[1, 1]``); the literal ``"full"`` opts the widget out of
* the flow to take the whole chrome-excluded surface. There is no position —
* widgets pack in list order, left-to-right, wrapping down. */
export type WidgetSize = [number, number] | "full";
export type Widget = {
id: string;
kind: "button" | "blank" | "jogstrip" | "trackpad" | "meter" | "stats" | "media" | "nowplaying";
label?: string | null;
icon?: Icon | null;
/** Reflow extent (ADR-0010). Absent means a ``[1, 1]`` single cell. */
size?: WidgetSize | null;
color?: string | null;
action?: Record<string, unknown> | null;
/** Macro steps: an ordered list of key/shell/dbus/delay actions that the
* daemon executes sequentially on press (issue #68). When present,
* ``action`` is ignored. */
macro?: { steps: { type: string; value: string }[]; continue_on_error?: boolean } | null;
source?: string | null;
min?: number | null;
max?: number | null;
metrics?: Metric[] | null;
controls?: MediaControl[] | null;
media_http?: MediaHttp | null;
previous_action?: Record<string, unknown> | null;
next_action?: Record<string, unknown> | null;
volume_up_action?: Record<string, unknown> | null;
volume_down_action?: Record<string, unknown> | null;
/** ``nowplaying`` knob (issue #50): whether the cell still renders
* a placeholder row when no MPRIS player is discovered. ``show``
* keeps the chrome's icon reachable; ``hide`` collapses the cell.
* Row order follows the daemon's ``row_ids`` (the session bus's
* ``ListNames`` reply order — matching GNOME Shell — issue #58). */
empty_state?: NowPlayingEmptyState | null;
/** Confirmation opt-in (issues #69 / #108). When ``true``, pressing
* the widget withholds execution on the daemon; the daemon mints a
* ``confirm_id``, sends a ``confirm_request`` to this client, and
* only runs the action / macro on a matching ``confirm_response``
* with ``decision: "confirm"``. Valid only on widgets that carry
* an ``action`` or a ``macro``; the daemon emits the field on
* every widget (`confirm: false` is the default). Optional in the
* TS shape for the same reason ``empty_state`` is: real daemon
* decks always carry it, but mock / test fixtures routinely
* omit it; consumers use ``=== true`` so an absent field falls
* back to ``false`` at the comparison. */
confirm?: boolean;
};
/* Client-facing message interfaces.
*
* The codegen emits the wire names (``DeckMessage``, ``MediaStateMessage``,
* ``HelloMessage`` etc.) but consumers import the ``Server<Kind>`` /
* ``Client<Kind>`` names below.
*
* ``ServerDeck`` widens ``widgets`` from the wire's opaque blob to
* the schema-layer ``Widget[]`` and ``icon`` to the typed ``Icon`` —
* the daemon relays both opaquely, but the client renders them as
* widgets with type-checked fields. Consumers can use the canonical
* ``DeckMessage`` if they want the strict wire shape. */
// ``ServerDeck`` is the typed client-side view of ``DeckMessage``.
// We declare it as a standalone interface rather than an Omit+& so it
// stays structurally compatible with the wire shape (the Omit+& form
// produces an intersection that TS won't widen back to DeckMessage).
// Consumers that need a strict-typed ``widgets`` can cast at the
// boundary; the daemon relays widgets opaquely (ADR-0006).
export interface ServerDeck {
type: "deck";
app?: string;
view?: string | null;
widgets: Widget[];
overflow?: "clip" | "shrink-to-fit";
jogstrip_enabled?: boolean;
display_name?: string | null;
theme?: string | null;
icon?: Icon | null;
web_app?: boolean;
error?: string | null;
/** Structured companions to ``error`` (issue #64): the human deck name
* and offending widget, when the daemon could identify them. Both are
* ``null``/absent for non-deck errors (e.g. a chrome view not found). */
error_deck?: string | null;
error_widget?: string | null;
focused_app?: FocusedAppInfo | null;
is_default?: boolean;
}
export type ServerState = StateMessage;
export type ServerBrightness = BrightnessMessage;
export type ServerWidgetUpdate = WidgetUpdateMessage;
export type ServerChromeMedia = ChromeMediaMessage;
export type ServerError = ErrorMessage;
export type ServerMediaError = MediaErrorMessage;
export type ServerMacroResult = MacroResultMessage;
export type ServerConfirmRequest = ConfirmRequestMessage;
// WindowListEntry carries an opaque ``icon`` field on the wire but the
// client renders it as a typed ``Icon``. Mirror the ServerDeck rule:
// declare the alias as a standalone interface so it stays compatible
// with the wire type at the message boundary.
export interface ServerWindowListEntry {
window_id: string;
label: string;
icon?: Icon | null;
}
export type ServerRunningWindows = Omit<RunningWindowsMessage, "windows"> & {
windows: ServerWindowListEntry[];
};
export type MediaState = MediaStateMessage;
export type ClientHello = HelloMessage;
export type ClientPress = PressMessage;
export type ClientJog = JogMessage;
export type ClientJogEnd = JogEndMessage;
export type ClientPad = PadMessage;
export type ClientPadTap = PadTapMessage;
export type ClientPadDrag = PadDragMessage;
export type ClientType = TypeMessage;
export type ClientKey = KeyMessage;
export type ClientMediaCommand = MediaCommandMessage;
export type ClientSelectView = SelectViewMessage;
export type ClientClearView = ClearViewMessage;
export type ClientRaiseWindow = RaiseWindowMessage;
export type ClientConfirmResponse = ConfirmResponseMessage;
/* Wire-to-schema coercion helpers (#76).
*
* ``DeckMessage`` and friends carry ``widgets`` / ``icon`` as opaque
* blobs (the daemon relays them, ADR-0006); the client renders them
* against the typed ``Widget`` / ``Icon`` shapes. Centralise the cast
* here so a single well-named helper replaces the scattered
* ``as unknown as Widget[]`` / ``as ServerDeck`` sites — consumers
* import one helper, and a future tightening of the wire shape has
* exactly one place to update.
*/
/** Coerce a wire ``DeckMessage`` to the typed-schema ``ServerDeck``
* (Widget[] for ``widgets``, typed ``Icon`` for ``icon``). Throws nothing
* — the wire shape is structurally compatible, so the cast is sound. */
export function wireDeckToServer(msg: DeckMessage): ServerDeck {
return msg as unknown as ServerDeck;
}
/** Coerce a wire snapshot of window rows to the typed-schema
* ``ServerWindowListEntry[]``. Same opaque-icon rationale as the
* deck helper. */
export function wireWindowsToServer(
windows: WindowListEntry[] | undefined,
): ServerWindowListEntry[] | undefined {
return windows as unknown as ServerWindowListEntry[] | undefined;
}
/** Lookup a widget by id in a wire ``DeckMessage``. Convenience so
* ``App.tsx``-style consumers don't repeat the opaque-blob cast. */
export function widgetById(
deck: DeckMessage,
id: string,
): Widget | undefined {
return (deck.widgets as unknown as Widget[]).find((w) => w.id === id);
}