omarchy-ui 0.0.1-x86_64-linux

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.
data/Service.qml ADDED
@@ -0,0 +1,422 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import Quickshell.Io
4
+
5
+ Item {
6
+ id: root
7
+
8
+ property var shell: null
9
+ property var manifest: null
10
+ readonly property string pluginDir: manifest && manifest.__sourceDir
11
+ ? String(manifest.__sourceDir)
12
+ : ""
13
+ readonly property string rubyProgram: pluginDir + "/main.rb"
14
+ property string program: ""
15
+ readonly property string effectiveRubyProgram: program !== "" ? program : rubyProgram
16
+
17
+ property bool ready: false
18
+ property bool stopping: false
19
+ property string lastError: ""
20
+ property var surfaces: ({})
21
+ property var surfaceOptions: ({})
22
+ property var nodeIndex: ({})
23
+ property var componentDefinitions: ({})
24
+ property int revision: 0
25
+ property int restartDelayMs: 500
26
+ property int eventSequence: 0
27
+
28
+ property var allowedTypes: ({})
29
+ property var allowedProperties: ({})
30
+
31
+ signal effectReceived(string name, var payload)
32
+
33
+ function validId(value) {
34
+ return typeof value === "string"
35
+ && value.length > 0
36
+ && value.length <= 128
37
+ && /^[a-zA-Z0-9_.:-]+$/.test(value)
38
+ }
39
+
40
+ function plainObject(value) {
41
+ return value !== null && typeof value === "object" && !Array.isArray(value)
42
+ }
43
+
44
+ function validateNode(node, index, depth, count) {
45
+ if (!plainObject(node) || depth > 32 || count.value >= 2000) return false
46
+ if (!allowedTypes[String(node.type || "")] || !validId(node.id)) return false
47
+ if (index[node.id] !== undefined) return false
48
+
49
+ var props = node.props === undefined ? {} : node.props
50
+ if (!plainObject(props)) return false
51
+ var whitelist = allowedProperties[node.type]
52
+ for (var key in props) {
53
+ if (!whitelist[key]) return false
54
+ var value = props[key]
55
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && !Array.isArray(value) && !plainObject(value))
56
+ return false
57
+ }
58
+
59
+ var children = node.children === undefined ? [] : node.children
60
+ if (!Array.isArray(children)) return false
61
+ if (!componentDefinitions[node.type].container && children.length !== 0)
62
+ return false
63
+ var subscriptions = node.events === undefined ? [] : node.events
64
+ if (!Array.isArray(subscriptions)) return false
65
+ for (var e = 0; e < subscriptions.length; e++) {
66
+ var eventName = String(subscriptions[e])
67
+ if (eventName !== "mount" && eventName !== "unmount" && componentDefinitions[node.type].events.indexOf(eventName) < 0)
68
+ return false
69
+ }
70
+
71
+ index[node.id] = node
72
+ count.value += 1
73
+ for (var i = 0; i < children.length; i++)
74
+ if (!validateNode(children[i], index, depth + 1, count)) return false
75
+ return true
76
+ }
77
+
78
+ function validateComponents(components) {
79
+ if (!plainObject(components)) return false
80
+ var names = Object.keys(components)
81
+ if (names.length === 0 || names.length > 256) return false
82
+ var validated = ({})
83
+ for (var i = 0; i < names.length; i++) {
84
+ var name = names[i]
85
+ var definition = components[name]
86
+ if (!/^[a-z][a-z0-9_]*$/.test(name) || !plainObject(definition)) return false
87
+ if (!/^[A-Z][A-Za-z0-9]*\.qml$/.test(String(definition.qml || ""))) return false
88
+ if (!Array.isArray(definition.properties) || !Array.isArray(definition.events)
89
+ || !plainObject(definition.property_map || {}) || !plainObject(definition.event_map || {})) return false
90
+ var propertyMap = ({})
91
+ for (var p = 0; p < definition.properties.length; p++) {
92
+ var propertyName = String(definition.properties[p])
93
+ if (!/^[a-z][a-z0-9_]*$/.test(propertyName)) return false
94
+ propertyMap[propertyName] = true
95
+ }
96
+ validated[name] = {
97
+ qml: definition.qml,
98
+ properties: propertyMap,
99
+ events: definition.events,
100
+ propertyMap: definition.property_map || {},
101
+ eventMap: definition.event_map || {},
102
+ container: definition.container === true,
103
+ autoBind: definition.auto_bind !== false
104
+ }
105
+ }
106
+ componentDefinitions = validated
107
+ return true
108
+ }
109
+
110
+ function installRender(message) {
111
+ if (!plainObject(message.surfaces)) return reject("render surfaces must be an object")
112
+ if (message.surface_options !== undefined && !validateSurfaceOptions(message.surface_options))
113
+ return reject("invalid surface options")
114
+ if (!validateComponents(message.components)) return reject("invalid component registry")
115
+ var dynamicTypes = ({})
116
+ var dynamicProperties = ({})
117
+ for (var componentName in componentDefinitions) {
118
+ dynamicTypes[componentName] = true
119
+ dynamicProperties[componentName] = componentDefinitions[componentName].properties
120
+ }
121
+ allowedTypes = dynamicTypes
122
+ allowedProperties = dynamicProperties
123
+ var nextIndex = ({})
124
+ var count = { value: 0 }
125
+ var names = Object.keys(message.surfaces)
126
+ if (names.length === 0 || names.length > 32) return reject("invalid surface count")
127
+
128
+ for (var i = 0; i < names.length; i++) {
129
+ if (!validId(names[i]) || !validateNode(message.surfaces[names[i]], nextIndex, 0, count))
130
+ return reject("invalid control tree")
131
+ }
132
+
133
+ surfaces = message.surfaces
134
+ surfaceOptions = message.surface_options || ({})
135
+ nodeIndex = nextIndex
136
+ revision += 1
137
+ lastError = ""
138
+ }
139
+
140
+ function validateSurfaceOptions(options) {
141
+ if (!plainObject(options)) return false
142
+ var allowed = {
143
+ title: true, width: true, height: true, min_width: true, min_height: true,
144
+ max_width: true, max_height: true, color: true, visible: true,
145
+ maximized: true, fullscreen: true
146
+ }
147
+ for (var surfaceName in options) {
148
+ if (!validId(surfaceName) || !plainObject(options[surfaceName])) return false
149
+ for (var key in options[surfaceName]) {
150
+ if (!allowed[key]) return false
151
+ var value = options[surfaceName][key]
152
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
153
+ return false
154
+ }
155
+ }
156
+ return true
157
+ }
158
+
159
+ function applyPatch(message) {
160
+ if (!validId(message.id))
161
+ return reject("invalid patch")
162
+ var node = nodeIndex[message.id]
163
+ if (!node) return reject("patch target rejected")
164
+
165
+ if (message.op === "replace_children") {
166
+ if (!componentDefinitions[node.type].container || !Array.isArray(message.children))
167
+ return reject("children patch rejected")
168
+ var removed = ({})
169
+ function markRemoved(child) {
170
+ removed[child.id] = true
171
+ var nested = Array.isArray(child.children) ? child.children : []
172
+ for (var r = 0; r < nested.length; r++) markRemoved(nested[r])
173
+ }
174
+ var oldChildren = Array.isArray(node.children) ? node.children : []
175
+ for (var oldIndex = 0; oldIndex < oldChildren.length; oldIndex++) markRemoved(oldChildren[oldIndex])
176
+
177
+ var childrenIndex = ({})
178
+ for (var existingId in nodeIndex)
179
+ if (!removed[existingId] && existingId !== node.id) childrenIndex[existingId] = nodeIndex[existingId]
180
+ var count = { value: Object.keys(childrenIndex).length }
181
+ for (var childIndex = 0; childIndex < message.children.length; childIndex++)
182
+ if (!validateNode(message.children[childIndex], childrenIndex, 0, count)) return reject("invalid children patch")
183
+
184
+ var containerReplacement = ({ type: node.type, id: node.id, props: node.props || {}, children: message.children })
185
+ if (node.events !== undefined) containerReplacement.events = node.events
186
+ childrenIndex[node.id] = containerReplacement
187
+ nodeIndex = childrenIndex
188
+ revision += 1
189
+ return true
190
+ }
191
+
192
+ if (message.op === "animate") {
193
+ if (!Array.isArray(message.tracks) || message.tracks.length === 0 || message.tracks.length > 64)
194
+ return reject("animation tracks rejected")
195
+ var animatedProps = ({})
196
+ var validatedTracks = []
197
+ var sourceProps = node.props || {}
198
+ for (var trackIndex = 0; trackIndex < message.tracks.length; trackIndex++) {
199
+ var track = message.tracks[trackIndex]
200
+ if (!plainObject(track) || typeof track.property !== "string"
201
+ || !allowedProperties[node.type][track.property] || !validAnimation(track))
202
+ return reject("animation track rejected")
203
+ var targetValue = track.to
204
+ if (targetValue !== null && typeof targetValue !== "string" && typeof targetValue !== "number")
205
+ return reject("animation target rejected")
206
+ animatedProps[track.property] = targetValue
207
+ validatedTracks.push(track)
208
+ }
209
+ var animatedReplacement = ({ type: node.type, id: node.id })
210
+ var finalProps = ({})
211
+ for (var sourceKey in sourceProps) finalProps[sourceKey] = sourceProps[sourceKey]
212
+ for (var animatedKey in animatedProps) finalProps[animatedKey] = animatedProps[animatedKey]
213
+ animatedReplacement.props = finalProps
214
+ if (node.children !== undefined) animatedReplacement.children = node.children
215
+ if (node.events !== undefined) animatedReplacement.events = node.events
216
+ animatedReplacement.transitions = validatedTracks
217
+ var animatedIndex = ({})
218
+ for (var animatedId in nodeIndex) animatedIndex[animatedId] = nodeIndex[animatedId]
219
+ animatedIndex[node.id] = animatedReplacement
220
+ nodeIndex = animatedIndex
221
+ revision += 1
222
+ return true
223
+ }
224
+
225
+ if (message.op !== "set" || typeof message.property !== "string") return reject("invalid patch")
226
+ if (!allowedProperties[node.type][message.property]) return reject("patch target rejected")
227
+ var value = message.value
228
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && !Array.isArray(value) && !plainObject(value))
229
+ return reject("patch value rejected")
230
+
231
+ var animation = message.animation
232
+ if (animation !== undefined && !validAnimation(animation)) return reject("patch animation rejected")
233
+
234
+ var replacement = ({ type: node.type, id: node.id })
235
+ var props = ({})
236
+ var oldProps = node.props || {}
237
+ for (var key in oldProps) props[key] = oldProps[key]
238
+ props[message.property] = value
239
+ replacement.props = props
240
+ if (node.children !== undefined) replacement.children = node.children
241
+ if (node.events !== undefined) replacement.events = node.events
242
+ if (animation !== undefined) {
243
+ replacement.transition = {
244
+ property: message.property,
245
+ from: oldProps[message.property],
246
+ to: value,
247
+ duration: animation.duration,
248
+ delay: animation.delay,
249
+ easing: animation.easing,
250
+ sequence: revision + 1
251
+ }
252
+ }
253
+
254
+ var nextIndex = ({})
255
+ for (var id in nodeIndex) nextIndex[id] = nodeIndex[id]
256
+ nextIndex[node.id] = replacement
257
+ nodeIndex = nextIndex
258
+ revision += 1
259
+ }
260
+
261
+ function validAnimation(animation) {
262
+ if (!plainObject(animation)
263
+ || typeof animation.duration !== "number" || animation.duration < 0 || animation.duration > 60000
264
+ || typeof animation.delay !== "number" || animation.delay < 0 || animation.delay > 60000
265
+ || typeof animation.easing !== "string") return false
266
+ var easings = ["linear", "in_quad", "out_quad", "in_out_quad", "in_cubic", "out_cubic", "in_out_cubic",
267
+ "in_back", "out_back", "in_out_back", "in_elastic", "out_elastic", "in_out_elastic",
268
+ "in_bounce", "out_bounce", "in_out_bounce"]
269
+ return easings.indexOf(animation.easing) >= 0
270
+ }
271
+
272
+ function handleLine(line) {
273
+ var raw = String(line || "").trim()
274
+ if (raw === "" || raw.length > 1048576) return reject("invalid message size")
275
+
276
+ var message
277
+ try { message = JSON.parse(raw) }
278
+ catch (error) { return reject("invalid JSON from Ruby") }
279
+ if (!plainObject(message) || message.v !== 1 || typeof message.type !== "string")
280
+ return reject("invalid protocol envelope")
281
+
282
+ if (message.type === "ready") {
283
+ ready = true
284
+ restartDelayMs = 500
285
+ lastError = ""
286
+ } else if (message.type === "render") {
287
+ installRender(message)
288
+ } else if (message.type === "patch") {
289
+ applyPatch(message)
290
+ } else if (message.type === "effect") {
291
+ handleEffect(message)
292
+ } else if (message.type === "ack") {
293
+ // Acknowledgements are consumed by benchmarks and future diagnostics.
294
+ } else if (message.type === "handler_error" || message.type === "runtime_error" || message.type === "protocol_error") {
295
+ lastError = String(message.message || message.code || "Ruby runtime error")
296
+ } else {
297
+ reject("unknown message type")
298
+ }
299
+ }
300
+
301
+ function handleEffect(message) {
302
+ if (typeof message.name !== "string" || !plainObject(message.payload || {}))
303
+ return reject("invalid effect")
304
+ var payload = message.payload || {}
305
+ effectReceived(message.name, payload)
306
+ if (!shell || !manifest) return
307
+ if (message.name === "open_panel")
308
+ shell.summon(manifest.id, JSON.stringify(payload))
309
+ else if (message.name === "close_panel")
310
+ shell.hide(manifest.id)
311
+ else
312
+ reject("effect not allowed")
313
+ }
314
+
315
+ function reject(reason) {
316
+ lastError = String(reason || "protocol error")
317
+ console.warn("omarchy-ui bridge:", lastError)
318
+ return false
319
+ }
320
+
321
+ function rootId(surfaceName) {
322
+ var surface = surfaces[String(surfaceName || "")]
323
+ return surface && validId(surface.id) ? surface.id : ""
324
+ }
325
+
326
+ function optionsFor(surfaceName) {
327
+ var options = surfaceOptions[String(surfaceName || "")]
328
+ return plainObject(options) ? options : ({})
329
+ }
330
+
331
+ function nodeFor(controlId) {
332
+ return nodeIndex[String(controlId || "")] || null
333
+ }
334
+
335
+ function componentSource(typeName) {
336
+ var definition = componentDefinitions[String(typeName || "")]
337
+ return definition ? pluginDir + "/Components/" + definition.qml : ""
338
+ }
339
+
340
+ function componentDefinition(typeName) {
341
+ return componentDefinitions[String(typeName || "")] || null
342
+ }
343
+
344
+ function sendEvent(surfaceName, controlId, eventName, payload) {
345
+ if (!rubyProcess.running || !validId(controlId) || !/^[a-z][a-z0-9_]{0,63}$/.test(eventName)) return false
346
+ var target = nodeIndex[String(controlId)]
347
+ var definition = target ? componentDefinitions[String(target.type)] : null
348
+ var subscriptions = target && Array.isArray(target.events) ? target.events : []
349
+ if (!definition || subscriptions.indexOf(eventName) < 0) return false
350
+ eventSequence += 1
351
+ rubyProcess.write(JSON.stringify({
352
+ v: 1,
353
+ type: "event",
354
+ surface: String(surfaceName || ""),
355
+ id: String(controlId),
356
+ event: eventName,
357
+ seq: eventSequence,
358
+ payload: plainObject(payload) ? payload : {}
359
+ }) + "\n")
360
+ return true
361
+ }
362
+
363
+ function startRuby() {
364
+ if (stopping || pluginDir === "" || rubyProcess.running) return
365
+ var configuredRuntime = String(Quickshell.env("OMARCHY_UI_RUNTIME") || "")
366
+ rubyProcess.command = [
367
+ configuredRuntime !== "" ? configuredRuntime : pluginDir + "/omarchy-ui-runtime",
368
+ effectiveRubyProgram
369
+ ]
370
+ rubyProcess.workingDirectory = pluginDir
371
+ rubyProcess.running = true
372
+ }
373
+
374
+ onPluginDirChanged: Qt.callLater(startRuby)
375
+
376
+ Component.onDestruction: {
377
+ stopping = true
378
+ restartTimer.stop()
379
+ if (rubyProcess.running) rubyProcess.running = false
380
+ }
381
+
382
+ Process {
383
+ id: rubyProcess
384
+ stdinEnabled: true
385
+
386
+ stdout: SplitParser {
387
+ onRead: function(line) { root.handleLine(line) }
388
+ }
389
+
390
+ stderr: SplitParser {
391
+ onRead: function(line) {
392
+ var message = String(line || "").trim()
393
+ if (message !== "") console.warn("omarchy-ui ruby:", message)
394
+ }
395
+ }
396
+
397
+ onExited: function(exitCode) {
398
+ root.ready = false
399
+ if (root.stopping || root.pluginDir === "") return
400
+ root.lastError = exitCode === 0
401
+ ? "Ruby UI runtime stopped"
402
+ : "Ruby UI runtime crashed (exit " + exitCode + ")"
403
+ restartTimer.interval = root.restartDelayMs
404
+ restartTimer.start()
405
+ root.restartDelayMs = Math.min(30000, root.restartDelayMs * 2)
406
+ }
407
+ }
408
+
409
+ Timer {
410
+ interval: 100
411
+ repeat: true
412
+ running: rubyProcess.running
413
+ onTriggered: rubyProcess.write('{"v":1,"type":"tick"}\n')
414
+ }
415
+
416
+ Timer {
417
+ id: restartTimer
418
+ interval: 500
419
+ repeat: false
420
+ onTriggered: root.startRuby()
421
+ }
422
+ }
data/bin/omarchy_ui ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/omarchy_ui"
5
+ require_relative "../lib/omarchy_ui/cli"
6
+
7
+ exit OmarchyUI::CLI.run(ARGV)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmarchyUI
4
+ class Animation
5
+ EASINGS = %w[linear in_quad out_quad in_out_quad in_cubic out_cubic in_out_cubic
6
+ in_back out_back in_out_back in_elastic out_elastic in_out_elastic
7
+ in_bounce out_bounce in_out_bounce].freeze
8
+
9
+ attr_reader :duration, :easing, :delay
10
+
11
+ def initialize(duration: 200, easing: :in_out_quad, delay: 0)
12
+ @duration = Integer(duration)
13
+ @delay = Integer(delay)
14
+ @easing = easing.to_s
15
+ raise ArgumentError, "animation duration must be between 0 and 60 seconds" unless (0..60_000).cover?(@duration)
16
+ raise ArgumentError, "animation delay must be between 0 and 60 seconds" unless (0..60_000).cover?(@delay)
17
+ raise ArgumentError, "unsupported easing: #{@easing}" unless EASINGS.include?(@easing)
18
+ end
19
+
20
+ def to_h
21
+ { "duration" => duration, "easing" => easing, "delay" => delay }
22
+ end
23
+ end
24
+ end