@glassly/miniapp 0.3.0-dev.1 → 0.3.0-dev.460

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 (255) hide show
  1. package/README.md +219 -74
  2. package/dist/background/index.d.ts +14 -8
  3. package/dist/background/index.d.ts.map +1 -1
  4. package/dist/background/index.js +4 -0
  5. package/dist/background/index.js.map +1 -1
  6. package/dist/background/register.d.ts +1 -1
  7. package/dist/background/register.js +1 -1
  8. package/dist/display/index.d.ts +23 -0
  9. package/dist/display/index.d.ts.map +1 -0
  10. package/dist/display/index.js +17 -0
  11. package/dist/display/index.js.map +1 -0
  12. package/dist/display/measurer/TextMeasurer.d.ts +160 -0
  13. package/dist/display/measurer/TextMeasurer.d.ts.map +1 -0
  14. package/dist/display/measurer/TextMeasurer.js +272 -0
  15. package/dist/display/measurer/TextMeasurer.js.map +1 -0
  16. package/dist/display/measurer/index.d.ts +10 -0
  17. package/dist/display/measurer/index.d.ts.map +1 -0
  18. package/dist/display/measurer/index.js +11 -0
  19. package/dist/display/measurer/index.js.map +1 -0
  20. package/dist/display/measurer/script-detection.d.ts +53 -0
  21. package/dist/display/measurer/script-detection.d.ts.map +1 -0
  22. package/dist/display/measurer/script-detection.js +184 -0
  23. package/dist/display/measurer/script-detection.js.map +1 -0
  24. package/dist/display/pixelArt.d.ts +22 -0
  25. package/dist/display/pixelArt.d.ts.map +1 -0
  26. package/dist/display/pixelArt.js +126 -0
  27. package/dist/display/pixelArt.js.map +1 -0
  28. package/dist/display/profiles/g1.d.ts +33 -0
  29. package/dist/display/profiles/g1.d.ts.map +1 -0
  30. package/dist/display/profiles/g1.js +214 -0
  31. package/dist/display/profiles/g1.js.map +1 -0
  32. package/dist/display/profiles/g2.d.ts +13 -0
  33. package/dist/display/profiles/g2.d.ts.map +1 -0
  34. package/dist/display/profiles/g2.js +34 -0
  35. package/dist/display/profiles/g2.js.map +1 -0
  36. package/dist/display/profiles/index.d.ts +12 -0
  37. package/dist/display/profiles/index.d.ts.map +1 -0
  38. package/dist/display/profiles/index.js +15 -0
  39. package/dist/display/profiles/index.js.map +1 -0
  40. package/dist/display/profiles/nex.d.ts +10 -0
  41. package/dist/display/profiles/nex.d.ts.map +1 -0
  42. package/dist/display/profiles/nex.js +150 -0
  43. package/dist/display/profiles/nex.js.map +1 -0
  44. package/dist/display/profiles/types.d.ts +103 -0
  45. package/dist/display/profiles/types.d.ts.map +1 -0
  46. package/dist/display/profiles/types.js +2 -0
  47. package/dist/display/profiles/types.js.map +1 -0
  48. package/dist/display/profiles/z100.d.ts +33 -0
  49. package/dist/display/profiles/z100.d.ts.map +1 -0
  50. package/dist/display/profiles/z100.js +182 -0
  51. package/dist/display/profiles/z100.js.map +1 -0
  52. package/dist/display/scene/types.d.ts +292 -0
  53. package/dist/display/scene/types.d.ts.map +1 -0
  54. package/dist/display/scene/types.js +0 -0
  55. package/dist/display/scene/types.js.map +1 -0
  56. package/dist/display/wrapper/TextWrapper.d.ts +102 -0
  57. package/dist/display/wrapper/TextWrapper.d.ts.map +1 -0
  58. package/dist/display/wrapper/TextWrapper.js +469 -0
  59. package/dist/display/wrapper/TextWrapper.js.map +1 -0
  60. package/dist/display/wrapper/index.d.ts +12 -0
  61. package/dist/display/wrapper/index.d.ts.map +1 -0
  62. package/dist/display/wrapper/index.js +12 -0
  63. package/dist/display/wrapper/index.js.map +1 -0
  64. package/dist/display/wrapper/types.d.ts +75 -0
  65. package/dist/display/wrapper/types.d.ts.map +1 -0
  66. package/dist/display/wrapper/types.js +11 -0
  67. package/dist/display/wrapper/types.js.map +1 -0
  68. package/dist/globals.d.ts +5 -5
  69. package/dist/globals.d.ts.map +1 -1
  70. package/dist/globals.js +1 -1
  71. package/dist/globals.js.map +1 -1
  72. package/dist/hardware.d.ts +4 -18
  73. package/dist/hardware.d.ts.map +1 -1
  74. package/dist/hardware.js +2 -13
  75. package/dist/hardware.js.map +1 -1
  76. package/dist/index.d.ts +16 -10
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +3 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/knownApps.d.ts +26 -0
  81. package/dist/knownApps.d.ts.map +1 -0
  82. package/dist/knownApps.js +140 -0
  83. package/dist/knownApps.js.map +1 -0
  84. package/dist/mirror/canvas.d.ts +22 -0
  85. package/dist/mirror/canvas.d.ts.map +1 -0
  86. package/dist/mirror/canvas.js +311 -0
  87. package/dist/mirror/canvas.js.map +1 -0
  88. package/dist/mirror/geometry.d.ts +69 -0
  89. package/dist/mirror/geometry.d.ts.map +1 -0
  90. package/dist/mirror/geometry.js +75 -0
  91. package/dist/mirror/geometry.js.map +1 -0
  92. package/dist/mirror/index.d.ts +3 -0
  93. package/dist/mirror/index.d.ts.map +1 -0
  94. package/dist/mirror/index.js +2 -0
  95. package/dist/mirror/index.js.map +1 -0
  96. package/dist/mirror/types.d.ts +40 -0
  97. package/dist/mirror/types.d.ts.map +1 -0
  98. package/dist/mirror/types.js +2 -0
  99. package/dist/mirror/types.js.map +1 -0
  100. package/dist/modules/auth.d.ts +1 -1
  101. package/dist/modules/auth.d.ts.map +1 -1
  102. package/dist/modules/auth.js +2 -2
  103. package/dist/modules/auth.js.map +1 -1
  104. package/dist/modules/compass.d.ts +47 -0
  105. package/dist/modules/compass.d.ts.map +1 -0
  106. package/dist/modules/compass.js +39 -0
  107. package/dist/modules/compass.js.map +1 -0
  108. package/dist/modules/display.d.ts +192 -8
  109. package/dist/modules/display.d.ts.map +1 -1
  110. package/dist/modules/display.js +40 -3
  111. package/dist/modules/display.js.map +1 -1
  112. package/dist/modules/events.d.ts +30 -2
  113. package/dist/modules/events.d.ts.map +1 -1
  114. package/dist/modules/events.js.map +1 -1
  115. package/dist/modules/input.d.ts +40 -1
  116. package/dist/modules/input.d.ts.map +1 -1
  117. package/dist/modules/input.js +45 -1
  118. package/dist/modules/input.js.map +1 -1
  119. package/dist/modules/latest-render-scheduler.d.ts +19 -0
  120. package/dist/modules/latest-render-scheduler.d.ts.map +1 -0
  121. package/dist/modules/latest-render-scheduler.js +54 -0
  122. package/dist/modules/latest-render-scheduler.js.map +1 -0
  123. package/dist/modules/lightSensor.d.ts +44 -0
  124. package/dist/modules/lightSensor.d.ts.map +1 -0
  125. package/dist/modules/lightSensor.js +33 -0
  126. package/dist/modules/lightSensor.js.map +1 -0
  127. package/dist/modules/mic.d.ts +3 -2
  128. package/dist/modules/mic.d.ts.map +1 -1
  129. package/dist/modules/mic.js +3 -2
  130. package/dist/modules/mic.js.map +1 -1
  131. package/dist/modules/miniapps.d.ts +2 -0
  132. package/dist/modules/miniapps.d.ts.map +1 -1
  133. package/dist/modules/miniapps.js.map +1 -1
  134. package/dist/modules/mirror.d.ts +16 -0
  135. package/dist/modules/mirror.d.ts.map +1 -0
  136. package/dist/modules/mirror.js +40 -0
  137. package/dist/modules/mirror.js.map +1 -0
  138. package/dist/modules/mono-text-rows.d.ts +20 -0
  139. package/dist/modules/mono-text-rows.d.ts.map +1 -0
  140. package/dist/modules/mono-text-rows.js +26 -0
  141. package/dist/modules/mono-text-rows.js.map +1 -0
  142. package/dist/modules/navigation.d.ts.map +1 -1
  143. package/dist/modules/navigation.js +21 -14
  144. package/dist/modules/navigation.js.map +1 -1
  145. package/dist/modules/phone.d.ts +26 -1
  146. package/dist/modules/phone.d.ts.map +1 -1
  147. package/dist/modules/phone.js +28 -0
  148. package/dist/modules/phone.js.map +1 -1
  149. package/dist/modules/settings.d.ts +45 -0
  150. package/dist/modules/settings.d.ts.map +1 -0
  151. package/dist/modules/settings.js +54 -0
  152. package/dist/modules/settings.js.map +1 -0
  153. package/dist/modules/system.d.ts +21 -0
  154. package/dist/modules/system.d.ts.map +1 -1
  155. package/dist/modules/system.js +12 -0
  156. package/dist/modules/system.js.map +1 -1
  157. package/dist/modules/transcription.d.ts +3 -2
  158. package/dist/modules/transcription.d.ts.map +1 -1
  159. package/dist/modules/transcription.js +3 -2
  160. package/dist/modules/transcription.js.map +1 -1
  161. package/dist/protocol.d.ts +65 -5
  162. package/dist/protocol.d.ts.map +1 -1
  163. package/dist/protocol.js +66 -5
  164. package/dist/protocol.js.map +1 -1
  165. package/dist/react/MirrorCanvas.d.ts +10 -0
  166. package/dist/react/MirrorCanvas.d.ts.map +1 -0
  167. package/dist/react/MirrorCanvas.js +23 -0
  168. package/dist/react/MirrorCanvas.js.map +1 -0
  169. package/dist/react/index.d.ts +2 -0
  170. package/dist/react/index.d.ts.map +1 -1
  171. package/dist/react/index.js +1 -0
  172. package/dist/react/index.js.map +1 -1
  173. package/dist/react/useColorScheme.d.ts +2 -3
  174. package/dist/react/useColorScheme.d.ts.map +1 -1
  175. package/dist/react/useColorScheme.js +15 -8
  176. package/dist/react/useColorScheme.js.map +1 -1
  177. package/dist/react/useRpc.js +2 -2
  178. package/dist/react/useRpc.js.map +1 -1
  179. package/dist/react/useSafeArea.d.ts.map +1 -1
  180. package/dist/react/useSafeArea.js +2 -2
  181. package/dist/react/useSafeArea.js.map +1 -1
  182. package/dist/session.d.ts +50 -4
  183. package/dist/session.d.ts.map +1 -1
  184. package/dist/session.js +35 -6
  185. package/dist/session.js.map +1 -1
  186. package/dist/transport/auto.d.ts +2 -2
  187. package/dist/transport/auto.js +2 -2
  188. package/dist/transport/dispatch.d.ts +1 -1
  189. package/dist/transport/dispatch.js +1 -1
  190. package/dist/transport/mock.d.ts +4 -4
  191. package/dist/transport/mock.d.ts.map +1 -1
  192. package/dist/transport/mock.js +31 -7
  193. package/dist/transport/mock.js.map +1 -1
  194. package/dist/ui/index.d.ts +2 -0
  195. package/dist/ui/index.d.ts.map +1 -1
  196. package/dist/ui/index.js +1 -0
  197. package/dist/ui/index.js.map +1 -1
  198. package/package.json +19 -3
  199. package/src/background/index.ts +26 -4
  200. package/src/background/register.ts +1 -1
  201. package/src/display/index.ts +83 -0
  202. package/src/display/measurer/TextMeasurer.ts +346 -0
  203. package/src/display/measurer/index.ts +21 -0
  204. package/src/display/measurer/script-detection.ts +218 -0
  205. package/src/display/pixelArt.ts +141 -0
  206. package/src/display/profiles/g1.ts +237 -0
  207. package/src/display/profiles/g2.ts +39 -0
  208. package/src/display/profiles/index.ts +28 -0
  209. package/src/display/profiles/nex.ts +166 -0
  210. package/src/display/profiles/types.ts +132 -0
  211. package/src/display/profiles/z100.ts +198 -0
  212. package/src/display/scene/types.ts +335 -0
  213. package/src/display/wrapper/TextWrapper.ts +546 -0
  214. package/src/display/wrapper/index.ts +15 -0
  215. package/src/display/wrapper/types.ts +102 -0
  216. package/src/globals.ts +5 -5
  217. package/src/hardware.ts +6 -21
  218. package/src/index.ts +34 -6
  219. package/src/knownApps.ts +170 -0
  220. package/src/mirror/canvas.ts +322 -0
  221. package/src/mirror/geometry.ts +128 -0
  222. package/src/mirror/index.ts +2 -0
  223. package/src/mirror/types.ts +38 -0
  224. package/src/modules/auth.ts +2 -2
  225. package/src/modules/compass.ts +69 -0
  226. package/src/modules/display.ts +222 -12
  227. package/src/modules/events.ts +27 -1
  228. package/src/modules/input.ts +55 -2
  229. package/src/modules/latest-render-scheduler.ts +55 -0
  230. package/src/modules/lightSensor.ts +61 -0
  231. package/src/modules/mic.ts +3 -2
  232. package/src/modules/miniapps.ts +2 -0
  233. package/src/modules/mirror.ts +42 -0
  234. package/src/modules/mono-text-rows.ts +30 -0
  235. package/src/modules/navigation.ts +20 -14
  236. package/src/modules/phone.ts +40 -1
  237. package/src/modules/settings.ts +64 -0
  238. package/src/modules/system.ts +30 -1
  239. package/src/modules/transcription.ts +3 -2
  240. package/src/protocol.ts +72 -5
  241. package/src/react/MirrorCanvas.tsx +35 -0
  242. package/src/react/index.ts +3 -0
  243. package/src/react/useColorScheme.ts +17 -8
  244. package/src/react/useRpc.ts +2 -2
  245. package/src/react/useSafeArea.ts +2 -2
  246. package/src/session.ts +78 -8
  247. package/src/transport/auto.ts +2 -2
  248. package/src/transport/dispatch.ts +1 -1
  249. package/src/transport/mock.ts +42 -11
  250. package/src/ui/index.ts +3 -0
  251. package/dist/modules/heading.d.ts +0 -16
  252. package/dist/modules/heading.d.ts.map +0 -1
  253. package/dist/modules/heading.js +0 -20
  254. package/dist/modules/heading.js.map +0 -1
  255. package/src/modules/heading.ts +0 -23
@@ -0,0 +1,128 @@
1
+ export const SHAPE_TYPES = new Set(["rect", "circle", "arc", "pie", "line", "triangle", "quad", "bezier", "svg"])
2
+
3
+ /** The mirror consumes processed frames; it does not own native scene state. */
4
+ export interface MirrorSceneElement {
5
+ id?: string
6
+ type?: string
7
+ box?: {x?: number; y?: number; w?: number; h?: number}
8
+ points?: readonly {x: number; y: number}[]
9
+ style?: {
10
+ color?: number
11
+ width?: number
12
+ border?: number
13
+ radius?: number
14
+ fill?: boolean
15
+ font?: string
16
+ fontSize?: number
17
+ }
18
+ startDeg?: number
19
+ endDeg?: number
20
+ rotation?: number
21
+ pivot?: {x: number; y: number}
22
+ pathScale?: number
23
+ d?: string
24
+ fillRule?: string
25
+ text?: string
26
+ data?: string
27
+ change?: string
28
+ transition?: {durationMs: number; easing?: string}
29
+ }
30
+
31
+ /** Tweenable state of one scene element, in canvas pixels. */
32
+ export interface SceneGeometry {
33
+ x: number
34
+ y: number
35
+ w: number
36
+ h: number
37
+ /** Ink intensity derived from the gray level (0..15 → 0..1). */
38
+ level: number
39
+ /** Stroke width in canvas px; 0 = no outline. */
40
+ stroke: number
41
+ radius: number
42
+ /** Flat x0, y0, x1, y1, … for point shapes. */
43
+ points: number[]
44
+ startDeg: number
45
+ endDeg: number
46
+ rotation: number
47
+ pivotX: number
48
+ pivotY: number
49
+ pathScale: number
50
+ }
51
+
52
+ /** Gray level 0..15 → normalized ink intensity. */
53
+ export function levelOpacity(color: unknown): number {
54
+ const level = Number(color)
55
+ return Number.isFinite(level) ? Math.max(0, Math.min(15, level)) / 15 : 1
56
+ }
57
+
58
+ export function sceneGeometry(element: MirrorSceneElement): SceneGeometry {
59
+ const box = element?.box ?? {}
60
+ const style = element?.style ?? {}
61
+ const type = element?.type
62
+ const stroke =
63
+ type !== "rect" && SHAPE_TYPES.has(type ?? "")
64
+ ? Number(style.width) || Number(style.border) || 1
65
+ : Number(style.border) || (type === "rect" && style.fill !== true ? 1 : 0)
66
+ const points: number[] = Array.isArray(element?.points)
67
+ ? element.points.flatMap((p) => [Number(p?.x) || 0, Number(p?.y) || 0])
68
+ : []
69
+ return {
70
+ x: Number(box.x) || 0,
71
+ y: Number(box.y) || 0,
72
+ w: Math.max(0, Number(box.w) || 0),
73
+ h: Math.max(0, Number(box.h) || 0),
74
+ level: levelOpacity(style.color),
75
+ stroke,
76
+ radius: Number(style.radius) || 0,
77
+ points,
78
+ startDeg: Number(element?.startDeg) || 0,
79
+ endDeg: Number(element?.endDeg) || 0,
80
+ rotation: Number(element?.rotation) || 0,
81
+ pivotX: Number(element?.pivot?.x ?? Number(box.x || 0) + Number(box.w || 0) / 2),
82
+ pivotY: Number(element?.pivot?.y ?? Number(box.y || 0) + Number(box.h || 0) / 2),
83
+ pathScale: Number(element?.pathScale ?? 256) / 256,
84
+ }
85
+ }
86
+
87
+ export function geometryEqual(a: SceneGeometry, b: SceneGeometry): boolean {
88
+ return (
89
+ a.x === b.x &&
90
+ a.y === b.y &&
91
+ a.w === b.w &&
92
+ a.h === b.h &&
93
+ a.level === b.level &&
94
+ a.stroke === b.stroke &&
95
+ a.radius === b.radius &&
96
+ a.startDeg === b.startDeg &&
97
+ a.endDeg === b.endDeg &&
98
+ a.pathScale === b.pathScale &&
99
+ a.points.length === b.points.length &&
100
+ a.points.every((p, i) => p === b.points[i])
101
+ )
102
+ }
103
+
104
+ /** Linear blend of two geometries at eased progress `t` (0..1). */
105
+ export function tweenAt(from: SceneGeometry, to: SceneGeometry, t: number): SceneGeometry {
106
+ "worklet"
107
+ if (t >= 1) return to
108
+ if (t <= 0) return from
109
+ const mix = (a: number, b: number) => a + (b - a) * t
110
+ const points = from.points.length === to.points.length ? to.points.map((p, i) => mix(from.points[i], p)) : to.points
111
+ return {
112
+ x: mix(from.x, to.x),
113
+ y: mix(from.y, to.y),
114
+ w: mix(from.w, to.w),
115
+ h: mix(from.h, to.h),
116
+ level: mix(from.level, to.level),
117
+ stroke: mix(from.stroke, to.stroke),
118
+ radius: mix(from.radius, to.radius),
119
+ points,
120
+ startDeg: mix(from.startDeg, to.startDeg),
121
+ endDeg: mix(from.endDeg, to.endDeg),
122
+ rotation: mix(from.rotation, to.rotation),
123
+ // Firmware changes the fixed pivot immediately when a rotation retargets.
124
+ pivotX: to.pivotX,
125
+ pivotY: to.pivotY,
126
+ pathScale: mix(from.pathScale, to.pathScale),
127
+ }
128
+ }
@@ -0,0 +1,2 @@
1
+ export {MirrorCanvasRenderer} from "./canvas"
2
+ export type {MirrorSnapshot, MirrorContent, MirrorElement} from "./types"
@@ -0,0 +1,38 @@
1
+ import type {
2
+ FrameElement,
3
+ SceneTextStyle,
4
+ SceneRectStyle,
5
+ SceneShapeStyle,
6
+ SceneStrokeStyle,
7
+ SceneImageStyle,
8
+ } from "../display/scene/types"
9
+
10
+ type ReadonlyValue<T> = T extends object ? {readonly [K in keyof T]: ReadonlyValue<T[K]>} : T
11
+
12
+ /** Ordered, processed scene element. Coordinates are glasses canvas pixels. */
13
+ export type MirrorElement = ReadonlyValue<
14
+ Omit<FrameElement, "path" | "contentHash" | "style"> & {
15
+ style?: SceneTextStyle & SceneRectStyle & SceneShapeStyle & SceneStrokeStyle & SceneImageStyle
16
+ }
17
+ >
18
+
19
+ export type MirrorContent =
20
+ | {readonly kind: "scene"; readonly elements: readonly MirrorElement[]; readonly epoch: number}
21
+ | {readonly kind: "bitmap"; readonly data: string}
22
+ | {readonly kind: "empty"}
23
+ | {readonly kind: "unavailable"; readonly reason: string}
24
+
25
+ /** Serializable intended output after display arbitration; never a hardware screenshot. */
26
+ export interface MirrorSnapshot {
27
+ /** Monotonic within this host runtime; useful when mixing requests and updates. */
28
+ readonly revision: number
29
+ readonly updatedAt: number
30
+ readonly connected: boolean
31
+ readonly model: string | null
32
+ readonly canvas: {readonly width: number; readonly height: number} | null
33
+ readonly view: "main" | "dashboard" | null
34
+ readonly appId: string | null
35
+ readonly content: MirrorContent
36
+ /** Rendered advances for the code points in this snapshot (builtin text). */
37
+ readonly glyphWidths: Readonly<Record<string, number>>
38
+ }
@@ -18,8 +18,8 @@ export class AuthModule {
18
18
  return this.session._getAuth()
19
19
  }
20
20
 
21
- get glasslyUserId(): string | null {
22
- return this.current?.glasslyUserId ?? null
21
+ get userId(): string | null {
22
+ return this.current?.userId ?? null
23
23
  }
24
24
 
25
25
  get oemId(): string | null {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Glasses compass. Subscribing starts the sensor like other glasses streams:
3
+ * on G2 custom firmware the raw compass API runs whenever any miniapp is
4
+ * subscribed; on stock firmware headings only arrive while the glasses' native
5
+ * navigation UI is open. Elsewhere the subscription succeeds and stays silent.
6
+ */
7
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
8
+ import type {MiniappSession} from "../session"
9
+ import type {UnsubscribeFn} from "./events"
10
+
11
+ export interface CompassOptions {
12
+ /** 50..2000 ms, default 1000. */
13
+ intervalMs?: number
14
+ /** Heading change threshold, 0..359 degrees, default 5. */
15
+ minChangeDegrees?: number
16
+ }
17
+ export interface CompassDiagnostics {
18
+ accuracy: 0 | 1 | 2 | 3 | null
19
+ anomalies: 0 | 1 | 2 | null
20
+ source: "unknown" | "grv" | "gmrv" | "rv"
21
+ sampleMatched: boolean
22
+ grvValid: boolean
23
+ gmrvValid: boolean
24
+ rvValid: boolean
25
+ magneticDataFresh: boolean
26
+ /** Firmware uptime, wraps at 2^32 ms. */
27
+ sampleTimestampMs: number
28
+ }
29
+ export interface CompassData {
30
+ /** Magnetic heading in degrees, 0 = north, 90 = east. Fused heading, not raw magnetometer XYZ. */
31
+ degrees: number
32
+ /** Phone reception time, Unix milliseconds. */
33
+ timestamp: number
34
+ /** Fusion quality; custom firmware only. */
35
+ diagnostics?: CompassDiagnostics
36
+ }
37
+ export interface CompassCalibrationData {
38
+ status: "start" | "complete"
39
+ }
40
+
41
+ export class CompassModule {
42
+ constructor(private readonly session: MiniappSession) {}
43
+ onUpdate(handler: (data: CompassData) => void): UnsubscribeFn {
44
+ return this.session._subscribe(MiniappStreamType.GLASSES_COMPASS, handler as (data: unknown) => void)
45
+ }
46
+ onCalibration(handler: (data: CompassCalibrationData) => void): UnsubscribeFn {
47
+ return this.session._subscribe(MiniappStreamType.COMPASS_CALIBRATION, handler as (data: unknown) => void)
48
+ }
49
+ /**
50
+ * Override the sampling this miniapp asks for. Subscribers already run with
51
+ * firmware defaults; the phone serves every subscriber the smallest interval
52
+ * and smallest change threshold requested across miniapps. Custom firmware only.
53
+ */
54
+ start(options: CompassOptions = {}): Promise<void> {
55
+ return this.control("start", options)
56
+ }
57
+ /** Drop this miniapp's sampling override. Live subscriptions keep the sensor running. */
58
+ stop(): Promise<void> {
59
+ return this.control("stop")
60
+ }
61
+ private control(operation: string, options: CompassOptions = {}): Promise<void> {
62
+ return this.session.sendRequest({
63
+ type: MiniappRequestType.GLASSES_SENSOR_CONTROL,
64
+ sensor: "compass",
65
+ operation,
66
+ options,
67
+ })
68
+ }
69
+ }
@@ -9,8 +9,9 @@
9
9
  * from bundles packed with older SDKs.
10
10
  */
11
11
 
12
- import {MiniappRequestType} from "../protocol"
12
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
13
13
  import {MiniappSession} from "../session"
14
+ import type {UnsubscribeFn} from "./events"
14
15
 
15
16
  export type ViewType = "main" | "dashboard"
16
17
 
@@ -57,26 +58,155 @@ export interface RenderTextStyle {
57
58
  * of 1.2 × size, and NOT re-wrapped by the host — lines are taken as given.
58
59
  * For grids (terminals, tables). Devices without a phone-side rasterizer
59
60
  * (stock G2 firmware) fall back to their own proportional font.
61
+ *
62
+ * "builtin": the glasses draw the text themselves with their built-in 20 px
63
+ * proportional font. On the Glassly CFW G2 the default face is drawn the
64
+ * same way, so text updates are a few bytes each and animate like shapes;
65
+ * lines are clipped to the box and the render window is pinned to the
66
+ * middle depth level. Other devices and stock firmware use the phone's
67
+ * proportional face.
60
68
  */
61
- font?: "default" | "mono"
69
+ font?: "default" | "mono" | "builtin"
62
70
  /** Font size in px for `font: "mono"`. Default 20. */
63
71
  fontSize?: number
72
+ /** Gray level 0..15 (15 = brightest) on devices that draw text themselves. Default 15. */
73
+ color?: number
64
74
  }
65
75
 
66
76
  export interface RenderRectStyle {
67
77
  border?: number
68
78
  radius?: number
79
+ /** Solid fill instead of an outline. */
80
+ fill?: boolean
81
+ /** Gray level 0..15 (15 = brightest). Default 15. */
82
+ color?: number
83
+ }
84
+
85
+ /** Closed shapes (circle, triangle, quad): an outline `border` px wide, or filled. */
86
+ export interface RenderShapeStyle {
87
+ border?: number
88
+ fill?: boolean
89
+ /** Gray level 0..15 (15 = brightest). Default 15. */
90
+ color?: number
91
+ }
92
+
93
+ /** Open strokes (line, bezier, arc). */
94
+ export interface RenderStrokeStyle {
95
+ /** Stroke width in px. Default 1. */
96
+ width?: number
97
+ /** Gray level 0..15 (15 = brightest). Default 15. */
98
+ color?: number
99
+ }
100
+
101
+ /** A point in raw canvas pixels (no anchoring). */
102
+ export interface RenderPoint {
103
+ x: number
104
+ y: number
105
+ }
106
+
107
+ export type RenderEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out"
108
+
109
+ /**
110
+ * Animate an element's geometry between renders. When an element with a
111
+ * stable `id` moves, resizes, or changes its points/angles/color, devices
112
+ * with `capabilities.display.animation` tween it on the glasses over
113
+ * `durationMs`; other devices jump. Content changes (new text or image
114
+ * pixels) always switch immediately.
115
+ */
116
+ export interface RenderTransition {
117
+ durationMs: number
118
+ /** Default "ease". */
119
+ easing?: RenderEasing
69
120
  }
70
121
 
71
122
  /**
72
123
  * One element of a rendered scene. `id` is optional but recommended for
73
124
  * anything that updates over time: elements with a stable id update in place on
74
125
  * the glasses (no flicker); the host matches unnamed elements by geometry.
126
+ *
127
+ * Shapes beyond `rect` need `capabilities.display.shapes` to list the type;
128
+ * a device without it drops the element (reported via `dropped`). Circles,
129
+ * arcs and pies are inscribed in their box (radius = half the shorter side;
130
+ * angles in degrees, 0 = right, increasing clockwise). Point shapes take
131
+ * raw canvas coordinates and are clipped by the device; `bezier` is
132
+ * quadratic with 3 points or cubic with 4.
75
133
  */
76
- export type RenderElement =
77
- | {type: "text"; id?: string; box: RenderBox; text: string; style?: RenderTextStyle}
78
- | {type: "image"; id?: string; box: RenderBox; data: string}
79
- | {type: "rect"; id?: string; box: RenderBox; style?: RenderRectStyle}
134
+ export type RenderElement = (
135
+ | {type: "text"; id?: string; box: RenderBox; text: string; style?: RenderTextStyle; transition?: RenderTransition}
136
+ | {type: "image"; id?: string; box: RenderBox; data: string; transition?: RenderTransition}
137
+ | {type: "rect"; id?: string; box: RenderBox; style?: RenderRectStyle; transition?: RenderTransition}
138
+ | {type: "circle"; id?: string; box: RenderBox; style?: RenderShapeStyle; transition?: RenderTransition}
139
+ | {
140
+ type: "arc"
141
+ id?: string
142
+ box: RenderBox
143
+ startDeg: number
144
+ endDeg: number
145
+ style?: RenderStrokeStyle
146
+ transition?: RenderTransition
147
+ }
148
+ | {
149
+ type: "pie"
150
+ id?: string
151
+ box: RenderBox
152
+ startDeg: number
153
+ endDeg: number
154
+ style?: {color?: number}
155
+ transition?: RenderTransition
156
+ }
157
+ | {
158
+ type: "line"
159
+ id?: string
160
+ points: [RenderPoint, RenderPoint]
161
+ style?: RenderStrokeStyle
162
+ transition?: RenderTransition
163
+ }
164
+ | {
165
+ type: "triangle"
166
+ id?: string
167
+ points: [RenderPoint, RenderPoint, RenderPoint]
168
+ style?: RenderShapeStyle
169
+ transition?: RenderTransition
170
+ }
171
+ | {
172
+ type: "quad"
173
+ id?: string
174
+ points: [RenderPoint, RenderPoint, RenderPoint, RenderPoint]
175
+ style?: RenderShapeStyle
176
+ transition?: RenderTransition
177
+ }
178
+ | {
179
+ type: "bezier"
180
+ id?: string
181
+ points: [RenderPoint, RenderPoint, RenderPoint] | [RenderPoint, RenderPoint, RenderPoint, RenderPoint]
182
+ style?: RenderStrokeStyle
183
+ transition?: RenderTransition
184
+ }
185
+ | {
186
+ type: "svg"
187
+ id?: string
188
+ box: RenderBox
189
+ d: string
190
+ scale?: number
191
+ fillRule?: "nonzero" | "evenodd"
192
+ style?: {color?: number}
193
+ transition?: RenderTransition
194
+ }
195
+ ) & {
196
+ /** Clockwise degrees, unwrapped (0 to 360 makes a full turn). Geometric shapes and SVG only. */
197
+ rotation?: number
198
+ /** Fixed pivot in canvas pixels; defaults to the target box center. */
199
+ pivot?: RenderPoint
200
+ /**
201
+ * Stereo depth in canvas pixels, -16..16: positive floats the element in
202
+ * front of the rest of the frame, negative sinks it behind. Devices with
203
+ * `capabilities.display.depth` shift it by `depth` px on the left lens and
204
+ * `-depth` px on the right; others draw it flat. Elements paint in array
205
+ * order, so a card that should sit on top goes last AND at a positive depth.
206
+ * A depth change with a `transition` glides toward or away from the viewer.
207
+ */
208
+ depth?: number
209
+ }
80
210
 
81
211
  export interface RenderOptions {
82
212
  view?: ViewType
@@ -84,6 +214,38 @@ export interface RenderOptions {
84
214
  durationMs?: number
85
215
  }
86
216
 
217
+ // ============================================================================
218
+ // The dashboard view — the overlay slot above the main content
219
+ // ============================================================================
220
+
221
+ /**
222
+ * "shown" = the requested view is showing (already showing counts).
223
+ * "unsupported" = the host refused: the glasses cannot show the dashboard
224
+ * view right now, or this app is not the designated dashboard app.
225
+ */
226
+ export interface ShowViewResult {
227
+ status: "shown" | "unsupported"
228
+ reason?: string
229
+ }
230
+
231
+ /**
232
+ * Why the showing view changed. "shown" and "hidden" answer this app's own
233
+ * showView calls; "replaced" means another overlay app took the view;
234
+ * "ineligible" means the glasses stopped being able to show the dashboard
235
+ * view (disconnected, dashboard turned off); "app_stopped" is implied when
236
+ * the app itself stops and never arrives as an event.
237
+ */
238
+ export type DisplayViewChangeReason = "shown" | "hidden" | "replaced" | "ineligible" | "app_stopped"
239
+
240
+ export interface DisplayViewChange {
241
+ /** The view now showing. */
242
+ view: ViewType
243
+ reason: DisplayViewChangeReason
244
+ }
245
+
246
+ /** Wire shape of the display_view stream. */
247
+ type DisplayViewEvent = {kind: "change"; view: ViewType; reason: DisplayViewChangeReason}
248
+
87
249
  // ============================================================================
88
250
  // Native navigation UI (capability `hasNativeNavigation`, e.g. G2)
89
251
  // ============================================================================
@@ -115,10 +277,12 @@ export interface NativeNavResult {
115
277
  }
116
278
 
117
279
  /**
118
- * Outcome of a render request, resolved when the host's display arbitration
119
- * settles it. "displayed" = accepted and sent to the device — NOT a
120
- * render confirmation. "blocked" = another app owns the display (or the
121
- * request failed); `reason` says why.
280
+ * Outcome of a render request. "displayed" = the host accepted the frame;
281
+ * on scene devices it resolves once the glasses have the frame and its
282
+ * transitions have finished playing (`presented`), or once the host knows
283
+ * the frame won't show right now (`presented: false`, `reason` says why —
284
+ * dashboard covering the view, screen off, nav UI up, glasses busy).
285
+ * "blocked" = another app owns the display (or the request failed).
122
286
  */
123
287
  export interface RenderResult {
124
288
  status: "displayed" | "blocked"
@@ -126,6 +290,13 @@ export interface RenderResult {
126
290
  degraded?: boolean
127
291
  /** Ids of elements the host dropped (budget/bounds/device limits) — dropped is never silent. */
128
292
  dropped?: string[]
293
+ /**
294
+ * The glasses showed this frame's content before the promise resolved.
295
+ * Absent on legacy/degrade paths, where "displayed" still just means sent.
296
+ */
297
+ presented?: boolean
298
+ /** A newer render() from this app landed before this one was reported; its content is on screen. */
299
+ superseded?: boolean
129
300
  reason?: string
130
301
  }
131
302
 
@@ -146,14 +317,21 @@ export class DisplayManager {
146
317
  * over-budget elements are dropped tail-first; both are reported via the
147
318
  * result, never silently.
148
319
  *
149
- * Awaiting is OPT-IN — a plain fire-and-forget call is fine. The returned
150
- * promise never rejects.
320
+ * Awaiting is OPT-IN — a plain fire-and-forget call is fine. Awaited, the
321
+ * promise resolves once the glasses have the frame and any transitions it
322
+ * started have finished (see `RenderResult.presented`), so `await` in a
323
+ * loop paces you at the real display rate. Frames that arrive faster than
324
+ * the host sends them are folded into the next one; each still resolves
325
+ * when its content is on screen. The returned promise never rejects.
151
326
  *
152
327
  * @example
153
328
  * const result = await session.display.render([
154
329
  * {type: "text", id: "stats", box: {x: 12, y: 9, w: 200, h: 28}, text: "863 m 11 min"},
155
330
  * {type: "image", id: "map", box: {x: 335, y: 14, w: 150, h: 150}, data: mapPng},
156
331
  * {type: "rect", box: {x: 0, y: 0, w: 500, h: 220}, style: {radius: 4}},
332
+ * // Re-render with a new box and the ball glides there over 300 ms on animating devices.
333
+ * {type: "circle", id: "ball", box: {x: 40, y: 100, w: 30, h: 30}, style: {fill: true},
334
+ * transition: {durationMs: 300, easing: "ease-in-out"}},
157
335
  * ])
158
336
  * if (result.degraded) console.warn("dropped:", result.dropped)
159
337
  */
@@ -174,6 +352,38 @@ export class DisplayManager {
174
352
  }))
175
353
  }
176
354
 
355
+ /**
356
+ * Ask the host to show a view. `"dashboard"` takes the overlay slot above
357
+ * the main content: frames rendered with `{view: "dashboard"}` reach the
358
+ * glasses while it shows. Claim the gestures you handle there with
359
+ * `session.input.setClaimedGestures`. `"main"` releases the slot; the
360
+ * retained main content replays. Both are idempotent. Only running apps
361
+ * whose manifest `type` is `"system_dashboard"` may show the dashboard
362
+ * view, and only while `capabilities.display.views` lists it; the most
363
+ * recent of them to ask holds it and the previous one hears "replaced".
364
+ */
365
+ showView(view: ViewType): Promise<ShowViewResult> {
366
+ return this.session.sendRequest<ShowViewResult>({type: MiniappRequestType.SHOW_VIEW, view}).catch((err) => ({
367
+ status: "unsupported" as const,
368
+ reason:
369
+ typeof err === "object" && err !== null && "message" in err
370
+ ? String((err as {message: unknown}).message)
371
+ : String(err),
372
+ }))
373
+ }
374
+
375
+ /**
376
+ * The showing view changed: the host granted the dashboard view, this app
377
+ * released it, another overlay app took it, or the glasses stopped being
378
+ * able to show it. Fires only for system_dashboard apps.
379
+ */
380
+ onViewChange(handler: (change: DisplayViewChange) => void): UnsubscribeFn {
381
+ return this.session._subscribe(MiniappStreamType.DISPLAY_VIEW, (data) => {
382
+ const event = data as DisplayViewEvent | null
383
+ if (event?.kind === "change") handler({view: event.view, reason: event.reason})
384
+ })
385
+ }
386
+
177
387
  /** Update the native nav UI's turn-by-turn card. */
178
388
  sendNativeNavInfo(info: NativeNavInfo): Promise<NativeNavResult> {
179
389
  return this.sendNativeNav({action: "info", info})
@@ -106,7 +106,7 @@ export interface LocationData {
106
106
  }
107
107
 
108
108
  export interface HeadingData {
109
- /** Compass heading in degrees, 0 = north, 90 = east. */
109
+ /** Phone compass heading in degrees, 0 = north, 90 = east. */
110
110
  degrees: number
111
111
  }
112
112
 
@@ -162,6 +162,9 @@ export interface TouchData {
162
162
  * The gesture: single_tap, double_tap, triple_tap, long_press, swipe_up, or
163
163
  * swipe_down — plus the raw press edges tap_start (finger down) and tap_end
164
164
  * (finger up), which bracket every gesture on hardware that reports them.
165
+ * tap_then_hold is the G2 2.2.9 tap-then-hold (Menu) gesture. touch_down is
166
+ * the R1 ring's finger-down, forwarded by the Glassly CFW (GLASSLYCFW/37 on
167
+ * G2 2.3.0.24); only the ring produces it, the temples report tap_start.
165
168
  */
166
169
  kind:
167
170
  | "single_tap"
@@ -172,9 +175,32 @@ export interface TouchData {
172
175
  | "swipe_down"
173
176
  | "tap_start"
174
177
  | "tap_end"
178
+ | "tap_then_hold"
179
+ | "touch_down"
175
180
  | string
176
181
  }
177
182
 
183
+ /**
184
+ * One key transition from a physical keyboard paired to the glasses (G2
185
+ * Glassly CFW revision 39+). Shapes follow the DOM: `key` is the logical key
186
+ * under the host's layout (US for now: "a", "A", "Enter", "ArrowUp", "F5",
187
+ * "AudioVolumeUp", …), `code` the physical key ("KeyA", "Digit1", "Space",
188
+ * "ControlLeft", …). `text` is the printable character a plain press types
189
+ * (down and repeat only, absent for Ctrl/Alt/Meta chords and named keys).
190
+ * `repeat` marks the host's synthesized auto-repeat while a key stays held;
191
+ * every gap in the link releases held keys with matching `up` events.
192
+ */
193
+ export interface KeyboardEventData {
194
+ phase: "down" | "up"
195
+ key: string
196
+ code: string
197
+ modifiers: {shift: boolean; ctrl: boolean; alt: boolean; meta: boolean}
198
+ text?: string
199
+ repeat: boolean
200
+ /** Phone receipt time, unix ms. */
201
+ timestamp: number
202
+ }
203
+
178
204
  export interface AudioChunkData {
179
205
  /** PCM or LC3, base64-encoded. Format depends on phone's mic mode. */
180
206
  data: string
@@ -15,17 +15,70 @@
15
15
  * the phone runtime fans out alongside the bare `touch_event` stream.
16
16
  */
17
17
 
18
- import {MiniappStreamType} from "../protocol"
18
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
19
19
  import {MiniappSession} from "../session"
20
- import type {ButtonPressData, TouchData, UnsubscribeFn} from "./events"
20
+ import type {ButtonPressData, KeyboardEventData, TouchData, UnsubscribeFn} from "./events"
21
+
22
+ export interface GestureClaimChange {
23
+ gesture: string
24
+ /** True when this app now holds the gesture, false when another app took it. */
25
+ held: boolean
26
+ }
21
27
 
22
28
  export class InputModule {
23
29
  constructor(private readonly session: MiniappSession) {}
24
30
 
31
+ /**
32
+ * Declare the gestures this miniapp is handling itself, replacing any
33
+ * previous claim set in the selected scope. Claims stack: the most recent app to claim a gesture
34
+ * holds it and receives it alone through `onTouch`, and the host skips its
35
+ * own default for it (a double tap otherwise leaves the visible miniapp or
36
+ * opens the glasses' dashboard). When the holder releases the gesture, or
37
+ * stops, the previous claimer holds it again; `onClaimChange` reports both
38
+ * sides of every handoff.
39
+ *
40
+ * Pass `[]` to release that scope. Omit `scope` for the default registration.
41
+ * Separate scopes preserve persistent shortcuts while a temporary page claims
42
+ * input: releasing the page scope restores the previous receiver without
43
+ * re-registering the shortcuts. All scopes are cleared automatically when
44
+ * the miniapp stops. One exception stands above the claims: an unclaimed
45
+ * double tap on a miniapp whose frame is visibly on the glasses leaves that
46
+ * app, so claim `double_tap` yourself while you are on screen if you use it.
47
+ *
48
+ * session.input.setClaimedGestures(["double_tap"]) // in-app back gesture
49
+ * session.input.setClaimedGestures([]) // hand it back
50
+ */
51
+ setClaimedGestures(gestures: string[], options?: {scope?: string}): Promise<void> {
52
+ return this.session
53
+ .sendRequest<void>({
54
+ type: MiniappRequestType.CLAIM_GESTURES,
55
+ gestures,
56
+ ...(options?.scope === undefined ? {} : {scope: options.scope}),
57
+ })
58
+ .then(() => undefined)
59
+ }
60
+
61
+ /** This app gained or lost a gesture it claimed (another app claimed it, or released it back). */
62
+ onClaimChange(handler: (change: GestureClaimChange) => void): UnsubscribeFn {
63
+ return this.session._subscribe(MiniappStreamType.GESTURE_CLAIM, handler as (data: unknown) => void)
64
+ }
65
+
25
66
  onButtonPress(handler: (data: ButtonPressData) => void): UnsubscribeFn {
26
67
  return this.session._subscribe(MiniappStreamType.BUTTON_PRESS, handler as (data: unknown) => void)
27
68
  }
28
69
 
70
+ /**
71
+ * Subscribe to a physical keyboard paired to the glasses (G2 Glassly CFW
72
+ * revision 39+; nothing arrives on other hardware). Every subscriber
73
+ * receives every key, like button presses: check `phase`, use `text` to
74
+ * type printable characters and `key`/`modifiers` for commands.
75
+ *
76
+ * session.input.onKey((e) => { if (e.phase === "down" && e.text) type(e.text) })
77
+ */
78
+ onKey(handler: (data: KeyboardEventData) => void): UnsubscribeFn {
79
+ return this.session._subscribe(MiniappStreamType.KEYBOARD_EVENT, handler as (data: unknown) => void)
80
+ }
81
+
29
82
  /**
30
83
  * Subscribe to touch events.
31
84
  *