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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Moussa Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/Panel.qml ADDED
@@ -0,0 +1,106 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import Quickshell.Wayland
4
+ import qs.Commons
5
+ import qs.Ui as OmarchyUi
6
+
7
+ Item {
8
+ id: root
9
+
10
+ property var shell: null
11
+ property var manifest: null
12
+ property var service: null
13
+ property bool opened: false
14
+ property string surfaceName: "counter"
15
+
16
+ readonly property string rootControlId: service ? service.rootId(surfaceName) : ""
17
+
18
+ function open(payloadJson) {
19
+ var payload = {}
20
+ try { payload = JSON.parse(payloadJson || "{}") || {} }
21
+ catch (error) { payload = {} }
22
+ if (typeof payload.surface === "string" && payload.surface !== "")
23
+ surfaceName = payload.surface
24
+ opened = true
25
+ Qt.callLater(function() { keyCatcher.forceActiveFocus() })
26
+ }
27
+
28
+ function close() {
29
+ opened = false
30
+ }
31
+
32
+ function dismiss() {
33
+ if (shell && manifest) shell.hide(manifest.id)
34
+ else close()
35
+ }
36
+
37
+ PanelWindow {
38
+ id: window
39
+ visible: root.opened
40
+ anchors { top: true; right: true; bottom: true; left: true }
41
+ color: "transparent"
42
+ exclusionMode: ExclusionMode.Ignore
43
+ WlrLayershell.namespace: "omarchy-ruby-ui-poc"
44
+ WlrLayershell.layer: WlrLayer.Overlay
45
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
46
+
47
+ Rectangle {
48
+ anchors.fill: parent
49
+ color: Qt.rgba(0, 0, 0, 0.62)
50
+
51
+ MouseArea {
52
+ anchors.fill: parent
53
+ onClicked: root.dismiss()
54
+ }
55
+ }
56
+
57
+ Item {
58
+ id: keyCatcher
59
+ anchors.fill: parent
60
+ focus: true
61
+ Keys.onEscapePressed: root.dismiss()
62
+
63
+ OmarchyUi.BorderSurface {
64
+ id: card
65
+ anchors.centerIn: parent
66
+ width: Math.min(parent.width - Style.space(32), Math.max(Style.space(320), renderer.implicitWidth + Style.space(48)))
67
+ height: Math.min(parent.height - Style.space(32), Math.max(Style.space(180), renderer.implicitHeight + Style.space(48)))
68
+ color: Color.popups.background
69
+ borderSpec: Border.localOrSurfaceSpec("popups", "border", Color.popups.border, Color.popups.border, Math.max(1, Style.normalBorderWidth))
70
+ radius: Style.cornerRadius
71
+
72
+ MouseArea {
73
+ anchors.fill: parent
74
+ onClicked: {}
75
+ }
76
+
77
+ ControlNode {
78
+ id: renderer
79
+ anchors.centerIn: parent
80
+ visible: root.service && root.rootControlId !== ""
81
+ bridge: root.service
82
+ surfaceName: root.surfaceName
83
+ controlId: root.rootControlId
84
+ foreground: Color.foreground
85
+ fontFamily: Style.font.family
86
+ }
87
+
88
+ Column {
89
+ anchors.centerIn: parent
90
+ spacing: Style.space(8)
91
+ visible: !renderer.visible
92
+
93
+ Text {
94
+ anchors.horizontalCenter: parent.horizontalCenter
95
+ text: root.service && root.service.lastError !== ""
96
+ ? root.service.lastError
97
+ : "Starting Ruby UI…"
98
+ color: Color.foreground
99
+ font.family: Style.font.family
100
+ font.pixelSize: Style.font.body
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
data/README.md ADDED
@@ -0,0 +1,362 @@
1
+ # Omarchy UI
2
+
3
+ > **Experimental:** the API and packaging format are being validated with real Omarchy apps.
4
+ > Pin the gem version for production projects and review release notes before upgrading.
5
+
6
+ Omarchy UI is the official-style Ruby application framework for building native Omarchy
7
+ interfaces. Ruby owns application state, events, tasks, commands, and models; a shared mruby
8
+ runtime communicates with QML over a validated protocol. Applications do not need system Ruby
9
+ and do not copy the framework runtime into every project.
10
+
11
+ ```ruby
12
+ require "omarchy_ui" unless Object.const_defined?(:OmarchyUI)
13
+
14
+ OmarchyUI.plugin do
15
+ state :count, 0
16
+
17
+ app :main, title: "Counter", width: 640, height: 420 do
18
+ column spacing: 12 do
19
+ text "Official Omarchy UI", style: :heading
20
+ text { "Count: #{state.count}" }
21
+ button("Increment") { state.count += 1 }
22
+ end
23
+ end
24
+ end
25
+ ```
26
+
27
+ ## Install
28
+
29
+ On Omarchy x86-64, install the gem and start building:
30
+
31
+ ```bash
32
+ gem install omarchy-ui
33
+ omarchy_ui new "My App"
34
+ ```
35
+
36
+ The gem includes the CLI, QML bridge, and a prebuilt mruby runtime with Omarchy UI embedded.
37
+ Developers do not install mruby, compile the runtime, copy framework files, or require Ruby on
38
+ machines that run a bundled application. Omarchy with Quickshell is the only host requirement.
39
+
40
+ Framework maintainers can rebuild the pinned mruby 4.0 binary with:
41
+
42
+ ```bash
43
+ ./scripts/build-mruby-runtime.sh
44
+ install -Dm755 build/runtime/omarchy-ui-runtime ~/.local/bin/omarchy-ui-runtime
45
+ ```
46
+
47
+ The stripped prebuilt runtime is approximately 1.8 MB and embeds the Ruby framework, JSON, regular
48
+ expressions, process support, and the native safe-command bridge.
49
+
50
+ ## Create and run an application
51
+
52
+ ```bash
53
+ omarchy_ui new "My App"
54
+ cd my-app
55
+ omarchy_ui launch main.rb
56
+ ```
57
+
58
+ The standalone generator creates no plugin manifest or copied runtime files:
59
+
60
+ ```text
61
+ my-app/
62
+ ├── Components/
63
+ │ └── Welcome.qml
64
+ ├── README.md
65
+ └── main.rb
66
+ ```
67
+
68
+ `launch` opens a compositor-managed window. Drag its title bar or use Super+drag, and close it
69
+ with Super+W. For protocol debugging without a window, use `omarchy_ui run main.rb`.
70
+
71
+ ## Bundle for another Omarchy computer
72
+
73
+ From an application directory:
74
+
75
+ ```bash
76
+ omarchy_ui bundle
77
+ ./dist/my-app/run
78
+ ```
79
+
80
+ `bundle` copies the working application, framework QML bridge, and prebuilt mruby executable into
81
+ `dist/<project-name>/`. The generated `run` launcher invokes Quickshell directly with the bundled
82
+ runtime, so the destination computer does not need Ruby or the `omarchy-ui` gem.
83
+
84
+ An Omarchy Shell plugin is a separate packaging mode. It requires `manifest.json` so the shell
85
+ can discover its ID, entry points, bar placement, and lifecycle. For a project that intentionally
86
+ contains a manifest:
87
+
88
+ ```bash
89
+ omarchy_ui validate path/to/plugin
90
+ omarchy_ui push path/to/plugin
91
+ ```
92
+
93
+ `push` stages and validates the project, injects the shared QML bridge files, backs up an existing
94
+ installation, installs atomically, optionally enables it, and restarts Omarchy Shell. Use
95
+ `--no-enable` or `--no-restart` when needed.
96
+
97
+ ## Surfaces and windows
98
+
99
+ ```ruby
100
+ bar_widget do
101
+ text "Weather"
102
+ on_click { open_panel :weather }
103
+ end
104
+
105
+ panel :weather do
106
+ text "Panel content"
107
+ end
108
+
109
+ app :main,
110
+ title: "Weather",
111
+ width: 900,
112
+ height: 600,
113
+ min_width: 480,
114
+ min_height: 320,
115
+ max_width: 1600,
116
+ max_height: 1200,
117
+ color: "#101713",
118
+ visible: true,
119
+ maximized: false,
120
+ fullscreen: false do
121
+ text "Application content"
122
+ end
123
+ ```
124
+
125
+ `app` supports `title`, `width`, `height`, `min_width`, `min_height`, `max_width`, `max_height`,
126
+ `color`, `visible`, `maximized`, and `fullscreen`.
127
+
128
+ ## State and reactivity
129
+
130
+ ```ruby
131
+ state :enabled, false
132
+ state :profile, { "name" => "Ada" }
133
+
134
+ toggle_control = toggle "Enabled", checked: state.enabled do |event|
135
+ state.enabled = event.fetch("value")
136
+ end
137
+ bind(toggle_control, :checked) { state.enabled }
138
+
139
+ text { state.enabled ? "Enabled" : "Disabled" }
140
+
141
+ transaction do
142
+ state.enabled = true
143
+ state.profile = { "name" => "Grace" }
144
+ end
145
+ ```
146
+
147
+ State accepts protocol-safe values: `nil`, booleans, finite numbers, strings, arrays, and hashes
148
+ with string/symbol keys. Bindings are reevaluated after changes and emit small property patches.
149
+ `transaction` batches related state writes.
150
+
151
+ Use `dynamic` when state changes the structure rather than only a property:
152
+
153
+ ```ruby
154
+ dynamic id: :results, spacing: 8 do
155
+ if state.items.empty?
156
+ text "Nothing found"
157
+ else
158
+ state.items.each { |item| text item.fetch("name") }
159
+ end
160
+ end
161
+ ```
162
+
163
+ ## Common properties
164
+
165
+ Every component supports `visible`, `enabled`, `opacity`, `scale`, `rotation`, `z`, `width`, and
166
+ `height`. Component-specific properties are listed below. Names use Ruby `snake_case`; the QML
167
+ bridge maps them to native property names.
168
+
169
+ ## Built-in component reference
170
+
171
+ ### Layout and display
172
+
173
+ | Ruby component | Component-specific properties | Events | Container |
174
+ | --- | --- | --- | --- |
175
+ | `container` | `spacing`, `padding`, `bordered` | `click` | yes |
176
+ | `row` | `spacing`, `alignment` (`start`, `center`, `end`) | `click` | yes |
177
+ | `column` | `spacing`, `alignment` (`start`, `center`, `end`) | `click` | yes |
178
+ | `grid` | `columns`, `rows`, `spacing`, `row_spacing`, `column_spacing` | `click` | yes |
179
+ | `stack` | — | `click` | yes |
180
+ | `scroll` | `clip` | `click` | yes |
181
+ | `rectangle` | `color`, `radius`, `border_color`, `border_width`, `padding` | `click` | yes |
182
+ | `text` | `text`, `style`, `size`, `bold`, `color`, `wrap` | — | no |
183
+ | `icon` | `name`, `text`, `size`, `color` | — | no |
184
+ | `image` | `source`, `fill_mode` | — | no |
185
+ | `spacer` | — | — | no |
186
+ | `progress` | `value`, `minimum`, `maximum`, `color` | — | no |
187
+ | `separator` | `strength` | — | no |
188
+ | `section_header` | `text` | — | no |
189
+ | `panel_hero` | `title`, `meta`, `detail`, `foreground`, `font_family`, `icon_size`, `icon_opacity`, `meta_opacity` | — | no |
190
+ | `optical_glyph` | `text`, `size`, `color`, `debug_bounds` | — | no |
191
+
192
+ ### Inputs and actions
193
+
194
+ | Ruby component | Component-specific properties | Events |
195
+ | --- | --- | --- |
196
+ | `button` | `text`, `icon`, `tooltip`, `selected`, `active`, `cursor`, `focusable`, `bordered`, colors, font/icon sizes, padding, `left_align` | `click`, `right_click`, `hover` |
197
+ | `action_button` | `icon`, `tooltip`, `foreground`, `hover_color`, font/size, `focusable`, `cursor`, `bordered` | `click`, `hover` |
198
+ | `toggle` | `label`, `description`, `checked`, `cursor`, `rounded`, colors, font/title/description sizes | `change`, `hover` |
199
+ | `toggle_switch` | `checked`, `busy`, `interactive`, `cursor`, `cursor_ring`, `cursor_pad`, `rounded`, colors, track/knob geometry | `change`, `hover` |
200
+ | `text_field` | `text`, `placeholder`, `password`, colors, selection tint, padding, `cursor` | `input`, `change`, `submit`, `focus`, `blur` |
201
+ | `number_field` | `label`, `value`, `from`, `to`, `step`, colors, font/field width, `cursor` | `change`, `hover` |
202
+ | `slider` | `value`, `minimum`, `maximum`, `step`, `integer`, track/fill/knob colors and sizes, `ticks`, `tick_color` | `input`, `change`, `right_click` |
203
+ | `dropdown` | `label`, `value`, `options`, colors, font, row sizes, `show_label`, `cursor` | `change`, `hover` |
204
+ | `searchable_dropdown` | dropdown fields plus `placeholder`, `empty_text`, `trigger_label`, popup sizing | `change`, `hover` |
205
+ | `multi_select` | `label`, `values`, `options`, command options, placeholder/empty labels, popup sizing, colors | `change`, `hover` |
206
+ | `button_group` | `value`, `options`, colors, font, `focusable`, `cursor_index` | `change`, `hover` |
207
+ | `confirm_dialog` | `opened`, `message`, cancel/confirm labels, `selected_index`, colors, font, `corner_radius` | `cancel`, `confirm` |
208
+ | `cursor_surface` | `cursor`, `current`, `outline`, `bordered`, `foreground`, `accent`, `fill`, `current_fill` | `click` |
209
+ | `widget_button` | text/font/colors, active state, dimensions, rotation, visibility states, interaction flags, tooltip | `click`, `right_click`, `middle_click`, `wheel` |
210
+ | `list_view` | `items`, key/label/description/icon fields, `selected`, `orientation`, `spacing`, `empty_text` | `change`, `activate`, `scroll` |
211
+
212
+ Convenience methods return their node, so it can be bound, animated, or passed to `on`:
213
+
214
+ ```ruby
215
+ field = text_field "", id: :query, placeholder: "Search" do |event|
216
+ state.query = event.fetch("value")
217
+ end
218
+
219
+ on(field, :submit) { |event| state.query = event.fetch("value") }
220
+ ```
221
+
222
+ Typical event payloads are:
223
+
224
+ - `click`, `right_click`, `confirm`, `cancel`: `{}`
225
+ - `change`, `input`, `submit`, `hover`: `{ "value" => ... }`
226
+ - `wheel`: `{ "delta" => number }`
227
+ - `list_view` change/activate: value, index, and original item
228
+ - `list_view` scroll: x and y offsets
229
+
230
+ Only declared and subscribed events cross the QML/Ruby boundary.
231
+
232
+ ## Bindings and properties
233
+
234
+ ```ruby
235
+ label = text "", id: :status
236
+ bind(label, :text) { state.message }
237
+
238
+ container do
239
+ property :opacity, 0.8
240
+ end
241
+ ```
242
+
243
+ `text { ... }` is shorthand for a reactive text binding. `property` binds or sets a property on
244
+ the current component. Explicit IDs are recommended for controls targeted by tests or external
245
+ effects; generated IDs are stable for the lifetime of one render.
246
+
247
+ ## Animation
248
+
249
+ Reactive binding transition:
250
+
251
+ ```ruby
252
+ card = rectangle width: 240, height: 120, opacity: 1.0
253
+ bind(card, :opacity, animation: animation(duration: 180, easing: :out_cubic)) do
254
+ state.visible ? 1.0 : 0.0
255
+ end
256
+ ```
257
+
258
+ Animate one or several properties immediately:
259
+
260
+ ```ruby
261
+ animate card,
262
+ { opacity: 0.25, scale: 1.08, rotation: 2 },
263
+ duration: 220,
264
+ easing: :in_out_quad,
265
+ delay: 40
266
+ ```
267
+
268
+ Sequential animation:
269
+
270
+ ```ruby
271
+ animate_sequence card, [
272
+ { to: { scale: 1.12 }, duration: 120, easing: :out_back },
273
+ { to: { scale: 1.0 }, duration: 160, easing: :out_cubic, pause: 30 }
274
+ ]
275
+ ```
276
+
277
+ Animation durations and delays are milliseconds from `0` to `60_000`. Supported easing names:
278
+
279
+ ```text
280
+ linear
281
+ in_quad, out_quad, in_out_quad
282
+ in_cubic, out_cubic, in_out_cubic
283
+ in_back, out_back, in_out_back
284
+ in_elastic, out_elastic, in_out_elastic
285
+ in_bounce, out_bounce, in_out_bounce
286
+ ```
287
+
288
+ All common numeric visual properties and declared numeric custom-adapter properties can be
289
+ animated. Parallel property hashes become parallel QML animation tracks.
290
+
291
+ ## Tasks and commands
292
+
293
+ ```ruby
294
+ after(0.5) { state.message = "Ready" }
295
+ every(5, immediate: true) { state.updated_at = Time.now.to_i }
296
+ async { state.result = run_command(["uname", "-r"], timeout: 2).stdout.strip }
297
+ ```
298
+
299
+ `after`, `every`, and `async` return cancellable task objects. MRI uses worker threads; mruby
300
+ uses cooperative ticks from the QML host. Command execution always takes an argv array and does
301
+ not invoke a shell. Results expose `stdout`, `stderr`, `exitstatus`, and `success?`; timeout raises
302
+ `OmarchyUI::CommandTimeout`.
303
+
304
+ ## Custom QML components
305
+
306
+ Any QtQuick, QtQuick.Controls, Quickshell, Omarchy `qs.Ui`, Canvas, shader, particle, or
307
+ third-party QML component can be exposed through a validated adapter contract:
308
+
309
+ ```ruby
310
+ register_component :sparkline,
311
+ qml: "Sparkline.qml",
312
+ properties: %i[values color line_width],
313
+ property_map: { color: :strokeColor, line_width: :lineWidth },
314
+ events: %i[click point_hover],
315
+ event_map: { point_hover: :pointHovered },
316
+ container: false,
317
+ auto_bind: true
318
+
319
+ chart = component :sparkline, values: [2, 8, 5], color: "#ff6655"
320
+ on(chart, :point_hover) { |event| state.hovered = event.fetch("index") }
321
+ ```
322
+
323
+ Place adapter files under `Components/`. Declared properties are assigned to the QML root and
324
+ declared signals are forwarded to Ruby. A container adapter can expose an `Item` property named
325
+ `contentHost`; framework children are parented into it automatically. See
326
+ [the QML support matrix](docs/qml-support.md) and [Sparkline.qml](Components/Sparkline.qml).
327
+
328
+ ## Architecture and safety
329
+
330
+ `Service.qml` supervises one long-lived mruby process and exchanges versioned NDJSON through
331
+ stdin/stdout. `ControlNode.qml` recursively renders validated component nodes. Property changes
332
+ send incremental patches; dynamic branches replace only affected children; animations run in
333
+ QML. Closing a window or panel does not evaluate Ruby or QML received over the protocol.
334
+
335
+ Component names, QML filenames, properties, events, IDs, effects, values, message sizes, and
336
+ animation limits are validated. Commands use argv arrays without a shell. Applications and
337
+ plugins still run with the current user's permissions.
338
+
339
+ ## Omarchy Phone example
340
+
341
+ `examples/omarchy-phone` demonstrates reactive controls, background discovery, safe commands,
342
+ ADB pairing and connection, scrcpy launching, iPhone discovery, and UxPlay AirPlay mirroring.
343
+
344
+ ```bash
345
+ omarchy_ui launch examples/omarchy-phone/main.rb
346
+ ```
347
+
348
+ ## Development and verification
349
+
350
+ ```bash
351
+ ./scripts/test.sh
352
+ ruby script/benchmark.rb
353
+ ./scripts/smoke-test.sh # live Omarchy session
354
+ ```
355
+
356
+ The suite covers state, bindings, repeated structures, event persistence, component schemas,
357
+ animation tracks and sequences, tasks, command safety, mruby compatibility, standalone project
358
+ generation, packaging, manifests, QML contracts, QML lint, and the phone backend.
359
+
360
+ ## License
361
+
362
+ MIT. See [LICENSE](LICENSE).