omarchy-ui 0.0.1-x86_64-linux → 0.0.3-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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d11e68e81c3b65cc3b41b2dae80f301e7ffb3e29f44cfdde1495a92a6a562e8d
4
- data.tar.gz: c7baff4c2f09d68dad37f3b0742c00d3abca2a661fa71f425443db9ae1850b85
3
+ metadata.gz: 4f53f63774323d862684e519c9884aed320d675b9391d228354def2b7768f0e4
4
+ data.tar.gz: 0ed0b927c051f03f6418f737769b51bc16c80114a70e362254daf23832f72196
5
5
  SHA512:
6
- metadata.gz: 3f256ebcdcc59f1c4bf984900e8f0f1f51896c719f5b956677bb734be45139349d56ee6a05ee6d117c7d79fa325ca13348f0e9088317a49d8ac03829d9d3d3c6
7
- data.tar.gz: dc8c3e1df89667cc9204218cb39b9c80ef50599cb78858041650202135a80a98424b08cdfe7932a6854f2e39de3e40a76e0e7466e8d0e8f35a73e7012830a994
6
+ metadata.gz: 20c6694b5a1641f360402835d44b2eabefd164a373e28f94b878d0e41e1351cb3c40a67db4a19e2d7ff0df5298d1a6ff0ed8316ea7aa89ffe2073ffef2f7dfd0
7
+ data.tar.gz: 00457faae9811da4e493d35d77b7c0ed514200c16acd51174841e5e3a172f9687a891d44f18c5dca60ad1be1ea47398a4b7128160d66dfd8c37c1ad796dde9df
data/README.md CHANGED
@@ -3,10 +3,9 @@
3
3
  > **Experimental:** the API and packaging format are being validated with real Omarchy apps.
4
4
  > Pin the gem version for production projects and review release notes before upgrading.
5
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.
6
+ Omarchy UI is a Ruby framework for building native Omarchy applications. It provides reactive
7
+ state, components, events, animation, tasks, commands, standalone windows, and shell plugin
8
+ integration through one compact API.
10
9
 
11
10
  ```ruby
12
11
  require "omarchy_ui" unless Object.const_defined?(:OmarchyUI)
@@ -26,39 +25,28 @@ end
26
25
 
27
26
  ## Install
28
27
 
29
- On Omarchy x86-64, install the gem and start building:
28
+ On Omarchy x86-64, install the gem:
30
29
 
31
30
  ```bash
32
31
  gem install omarchy-ui
33
- omarchy_ui new "My App"
34
32
  ```
35
33
 
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.
34
+ The gem contains everything required to launch and bundle an application. Developers do
35
+ not need to install a separate runtime or copy framework files. Bundled applications require only
36
+ an Omarchy computer to run.
39
37
 
40
- Framework maintainers can rebuild the pinned mruby 4.0 binary with:
38
+ ## Start an application
41
39
 
42
40
  ```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
41
+ omarchy_ui new MyApp
42
+ cd myapp
55
43
  omarchy_ui launch main.rb
56
44
  ```
57
45
 
58
- The standalone generator creates no plugin manifest or copied runtime files:
46
+ A new standalone project contains only application-owned files:
59
47
 
60
48
  ```text
61
- my-app/
49
+ myapp/
62
50
  ├── Components/
63
51
  │ └── Welcome.qml
64
52
  ├── README.md
@@ -74,12 +62,11 @@ From an application directory:
74
62
 
75
63
  ```bash
76
64
  omarchy_ui bundle
77
- ./dist/my-app/run
65
+ ./dist/myapp/run
78
66
  ```
79
67
 
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.
68
+ `bundle` creates a self-contained application under `dist/<project-name>/`. Its `run`
69
+ launcher works on another Omarchy computer without Ruby or the `omarchy-ui` gem.
83
70
 
84
71
  An Omarchy Shell plugin is a separate packaging mode. It requires `manifest.json` so the shell
85
72
  can discover its ID, entry points, bar placement, and lifecycle. For a project that intentionally
@@ -90,7 +77,7 @@ omarchy_ui validate path/to/plugin
90
77
  omarchy_ui push path/to/plugin
91
78
  ```
92
79
 
93
- `push` stages and validates the project, injects the shared QML bridge files, backs up an existing
80
+ `push` stages and validates the project, adds the framework support files, backs up an existing
94
81
  installation, installs atomically, optionally enables it, and restarts Omarchy Shell. Use
95
82
  `--no-enable` or `--no-restart` when needed.
96
83
 
@@ -146,25 +133,13 @@ end
146
133
 
147
134
  State accepts protocol-safe values: `nil`, booleans, finite numbers, strings, arrays, and hashes
148
135
  with string/symbol keys. Bindings are reevaluated after changes and emit small property patches.
136
+ Ruby blocks passed to `text`, `property`, or `bind` are reactive; no wrapper is required.
149
137
  `transaction` batches related state writes.
150
138
 
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
139
  ## Common properties
164
140
 
165
141
  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.
142
+ `height`. Component-specific properties are listed below. Property names use Ruby `snake_case`.
168
143
 
169
144
  ## Built-in component reference
170
145
 
@@ -227,7 +202,7 @@ Typical event payloads are:
227
202
  - `list_view` change/activate: value, index, and original item
228
203
  - `list_view` scroll: x and y offsets
229
204
 
230
- Only declared and subscribed events cross the QML/Ruby boundary.
205
+ Only declared and subscribed events are delivered to application handlers.
231
206
 
232
207
  ## Bindings and properties
233
208
 
@@ -286,7 +261,7 @@ in_bounce, out_bounce, in_out_bounce
286
261
  ```
287
262
 
288
263
  All common numeric visual properties and declared numeric custom-adapter properties can be
289
- animated. Parallel property hashes become parallel QML animation tracks.
264
+ animated. Properties in the same hash animate in parallel.
290
265
 
291
266
  ## Tasks and commands
292
267
 
@@ -296,10 +271,9 @@ every(5, immediate: true) { state.updated_at = Time.now.to_i }
296
271
  async { state.result = run_command(["uname", "-r"], timeout: 2).stdout.strip }
297
272
  ```
298
273
 
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`.
274
+ `after`, `every`, and `async` return cancellable task objects. Command execution always takes an
275
+ argv array and does not invoke a shell. Results expose `stdout`, `stderr`, `exitstatus`, and
276
+ `success?`; timeout raises `OmarchyUI::CommandTimeout`.
303
277
 
304
278
  ## Custom QML components
305
279
 
@@ -325,25 +299,11 @@ declared signals are forwarded to Ruby. A container adapter can expose an `Item`
325
299
  `contentHost`; framework children are parented into it automatically. See
326
300
  [the QML support matrix](docs/qml-support.md) and [Sparkline.qml](Components/Sparkline.qml).
327
301
 
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.
302
+ ## Safety
338
303
 
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
- ```
304
+ Component names, files, properties, events, IDs, effects, values, message sizes, and animation
305
+ limits are validated. Commands use argv arrays without a shell. Applications and plugins run with
306
+ the current user's permissions, so review third-party code before installing it.
347
307
 
348
308
  ## Development and verification
349
309
 
@@ -354,8 +314,8 @@ ruby script/benchmark.rb
354
314
  ```
355
315
 
356
316
  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.
317
+ animation tracks and sequences, tasks, command safety, standalone projects, packaging,
318
+ manifests, component contracts, linting, and the phone backend.
359
319
 
360
320
  ## License
361
321
 
@@ -190,16 +190,50 @@ module OmarchyUI
190
190
  end
191
191
 
192
192
  def reconcile_structure(structure)
193
+ previous_children = structure.last_children
193
194
  structure.node.children.dup.each { |child| unregister_subtree(child) }
194
195
  structure.node.children.clear
195
196
  @builder.rebuild(structure.node, &structure.renderer)
196
197
  children = structure.node.children.map(&:to_h)
197
198
  return if children == structure.last_children
199
+ if patchable_trees?(previous_children, children)
200
+ emit_property_patches(previous_children, children)
201
+ structure.last_children = children
202
+ return
203
+ end
198
204
  structure.last_children = children
199
205
  emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "replace_children",
200
206
  "id" => structure.node.id, "children" => children)
201
207
  end
202
208
 
209
+ def patchable_trees?(previous, current)
210
+ return false unless previous.length == current.length
211
+ previous.zip(current).all? do |before, after|
212
+ before["id"] == after["id"] && before["type"] == after["type"] &&
213
+ before.fetch("events", []) == after.fetch("events", []) &&
214
+ before.fetch("props", {}).keys.sort == after.fetch("props", {}).keys.sort &&
215
+ patchable_trees?(before.fetch("children", []), after.fetch("children", []))
216
+ end
217
+ end
218
+
219
+ def emit_property_patches(previous, current)
220
+ previous.zip(current).each do |before, after|
221
+ before.fetch("props", {}).each do |property, old_value|
222
+ value = after.fetch("props", {}).fetch(property)
223
+ next if value == old_value
224
+ next if echoed_input_patch?(after.fetch("id"), property, value)
225
+ emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "set",
226
+ "id" => after.fetch("id"), "property" => property, "value" => value)
227
+ end
228
+ emit_property_patches(before.fetch("children", []), after.fetch("children", []))
229
+ end
230
+ end
231
+
232
+ def echoed_input_patch?(control_id, property, value)
233
+ @active_event && @active_event["event"] == "input" && @active_event["id"] == control_id &&
234
+ %w[text value].include?(property) && @active_event.dig("payload", "value") == value
235
+ end
236
+
203
237
  def unregister_subtree(node)
204
238
  node.children.each { |child| unregister_subtree(child) }
205
239
  @nodes.delete(node.id)
@@ -212,7 +246,12 @@ module OmarchyUI
212
246
  key = [message.fetch("id"), message.fetch("event")]
213
247
  handler = @handlers[key]
214
248
  raise ProtocolError, "unknown event target: #{key.join('/')}" unless handler
215
- @builder.instance_exec(message["payload"] || {}, &handler)
249
+ begin
250
+ @active_event = message
251
+ @builder.instance_exec(message["payload"] || {}, &handler)
252
+ ensure
253
+ @active_event = nil
254
+ end
216
255
  acknowledgement = {
217
256
  "v" => PROTOCOL_VERSION, "type" => "ack", "seq" => message["seq"],
218
257
  "id" => message.fetch("id"), "event" => message.fetch("event")
@@ -16,11 +16,12 @@ module OmarchyUI
16
16
  def initialize(application)
17
17
  @application = application
18
18
  @stack = []
19
+ @dynamic_scopes = []
19
20
  end
20
21
 
21
22
  def component(type, id: nil, **props, &block)
22
23
  definition = @application.components.fetch(type)
23
- node = @application.build_node(type, explicit_id: id, props:)
24
+ node = @application.build_node(type, explicit_id: id || scoped_id(type), props:)
24
25
  append(node)
25
26
  if block
26
27
  raise ArgumentError, "#{type} is not a container" unless definition.container
@@ -64,7 +65,8 @@ module OmarchyUI
64
65
  definition = @application.components.fetch(type)
65
66
  raise ArgumentError, "dynamic component must be a container: #{type}" unless definition.container
66
67
 
67
- node = component(type, id:, **props, &renderer)
68
+ node = component(type, id:, **props)
69
+ within_dynamic(node, &renderer)
68
70
  @application.register_structure(node, renderer)
69
71
  node
70
72
  end
@@ -166,7 +168,7 @@ module OmarchyUI
166
168
  def every(seconds, immediate: false, &block) = @application.schedule(:every, interval: seconds, immediate:, &block)
167
169
  def async(&block) = @application.schedule(:async, &block)
168
170
  def run_command(argv, **options) = Command.run(argv, **options)
169
- def rebuild(node, &renderer) = within(node, &renderer)
171
+ def rebuild(node, &renderer) = within_dynamic(node, &renderer)
170
172
  def open_panel(name) = @application.emit_effect("open_panel", "surface" => name.to_s)
171
173
 
172
174
  def close_panel(name = nil)
@@ -175,6 +177,20 @@ module OmarchyUI
175
177
 
176
178
  private
177
179
 
180
+ def scoped_id(type)
181
+ return nil if @dynamic_scopes.empty?
182
+ scope = @dynamic_scopes.last
183
+ scope[:sequence] += 1
184
+ "#{scope.fetch(:id)}.#{type}.#{scope.fetch(:sequence)}"
185
+ end
186
+
187
+ def within_dynamic(node, &block)
188
+ @dynamic_scopes.push({ id: node.id, sequence: 0 })
189
+ within(node, &block)
190
+ ensure
191
+ @dynamic_scopes.pop
192
+ end
193
+
178
194
  def surface(name, id:, options: {}, &block)
179
195
  raise ArgumentError, "surface requires a block" unless block
180
196
  node = @application.build_node(:container, explicit_id: id)
@@ -192,7 +208,8 @@ module OmarchyUI
192
208
  def input_component(type, property, value, id:, props:, handler: nil)
193
209
  props = props.merge(property => value) unless value.equal?(UNSET)
194
210
  node = component(type, id:, **props)
195
- @application.register_handler(node.id, :change, handler) if handler
211
+ event = type == :text_field ? :input : :change
212
+ @application.register_handler(node.id, event, handler) if handler
196
213
  node
197
214
  end
198
215
 
@@ -31,6 +31,8 @@ module OmarchyUI
31
31
  @err.puts("Usage: omarchy_ui <new NAME|run FILE|launch FILE|bundle [DIRECTORY]|push [DIRECTORY]|validate [DIRECTORY]|version>")
32
32
  command.nil? ? 0 : 64
33
33
  end
34
+ rescue Interrupt
35
+ 130
34
36
  rescue ArgumentError, SystemCallError, JSON::ParserError => error
35
37
  @err.puts("omarchy_ui: #{error.message}")
36
38
  1
@@ -64,7 +66,8 @@ module OmarchyUI
64
66
  environment = ENV.to_h.merge(
65
67
  "OMARCHY_UI_PROJECT_DIR" => project_dir,
66
68
  "OMARCHY_UI_RUBY_PROGRAM" => file,
67
- "OMARCHY_UI_RUNTIME" => Runtime.executable
69
+ "OMARCHY_UI_RUNTIME" => Runtime.executable,
70
+ "QT_LOGGING_RULES" => qt_logging_rules
68
71
  )
69
72
  success = system(environment, "quickshell", "--path", File.join(runtime_dir, "App.qml"))
70
73
  return success ? 0 : ($?&.exitstatus || 1)
@@ -100,12 +103,15 @@ module OmarchyUI
100
103
  destination = File.join(source, "dist", File.basename(source))
101
104
  raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
102
105
  FileUtils.mkdir_p(destination)
103
- entries = Dir.children(source).reject { |entry| %w[.git dist].include?(entry) }
106
+ entries = application_entries(source)
104
107
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination)
105
108
  Project.install_runtime(destination)
106
- runtime = File.join(destination, "omarchy-ui-runtime")
107
- FileUtils.cp(Runtime::BUNDLED, runtime)
108
- FileUtils.chmod(0o755, runtime)
109
+ if File.file?(File.join(destination, "manifest.json"))
110
+ raise ArgumentError, "bundled plugin validation failed" unless system("omarchy", "plugin", "validate", destination)
111
+ @out.puts("Bundled plugin in #{destination}")
112
+ return 0
113
+ end
114
+
109
115
  %w[Commons Ui].each do |module_name|
110
116
  FileUtils.ln_s(File.join("/usr/share/omarchy/shell", module_name), File.join(destination, module_name))
111
117
  end
@@ -117,6 +123,7 @@ module OmarchyUI
117
123
  export OMARCHY_UI_RUNTIME="$app_dir/omarchy-ui-runtime"
118
124
  export OMARCHY_UI_PROJECT_DIR="$app_dir"
119
125
  export OMARCHY_UI_RUBY_PROGRAM="$app_dir/main.rb"
126
+ export QT_LOGGING_RULES="${QT_LOGGING_RULES:+$QT_LOGGING_RULES;}qt.qpa.services.warning=false"
120
127
  exec quickshell --path "$app_dir/App.qml"
121
128
  SH
122
129
  FileUtils.chmod(0o755, launcher)
@@ -127,11 +134,19 @@ module OmarchyUI
127
134
  raise
128
135
  end
129
136
 
137
+ def qt_logging_rules
138
+ [ENV["QT_LOGGING_RULES"], "qt.qpa.services.warning=false"].compact.reject(&:empty?).join(";")
139
+ end
140
+
141
+ def application_entries(source)
142
+ generated = Project::RUNTIME_FILES + %w[omarchy-ui-runtime run Commons Ui]
143
+ Dir.children(source).reject { |entry| %w[.git dist].include?(entry) || generated.include?(entry) }
144
+ end
145
+
130
146
  def push(arguments)
131
147
  enable = !arguments.delete("--no-enable")
132
148
  restart = !arguments.delete("--no-restart")
133
149
  source = File.expand_path(arguments.shift || Dir.pwd)
134
- Runtime.install_shared
135
150
  manifest_path = File.join(source, "manifest.json")
136
151
  manifest = JSON.parse(File.read(manifest_path))
137
152
  plugin_id = manifest.fetch("id")
@@ -177,7 +192,7 @@ module OmarchyUI
177
192
  def stage_project(source, parent: nil, prefix: ".omarchy-ui-staging-")
178
193
  raise ArgumentError, "project directory not found: #{source}" unless File.directory?(source)
179
194
  staging = parent ? Dir.mktmpdir(prefix, parent) : Dir.mktmpdir(prefix)
180
- entries = Dir.children(source).reject { |entry| entry == ".git" }
195
+ entries = application_entries(source)
181
196
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, staging) unless entries.empty?
182
197
  Project.install_runtime(staging) if File.file?(File.join(staging, "main.rb"))
183
198
  staging
@@ -28,7 +28,7 @@ module OmarchyUI
28
28
  def self.install_runtime(path, framework_root: FRAMEWORK_ROOT)
29
29
  RUNTIME_FILES.each do |file|
30
30
  destination = File.join(path, file)
31
- FileUtils.cp(File.join(framework_root, file), destination) unless File.exist?(destination)
31
+ FileUtils.cp(File.join(framework_root, file), destination)
32
32
  end
33
33
  bundled_runtime = File.join(framework_root, "vendor", "runtime", "x86_64-linux", "omarchy-ui-runtime")
34
34
  if File.file?(bundled_runtime)
data/lib/omarchy_ui.rb CHANGED
@@ -15,7 +15,7 @@ require_relative "omarchy_ui/project"
15
15
  require_relative "omarchy_ui/runtime"
16
16
 
17
17
  module OmarchyUI
18
- VERSION = "0.0.1"
18
+ VERSION = "0.0.3"
19
19
  FRAMEWORK_ROOT = File.expand_path("..", __dir__)
20
20
 
21
21
  def self.plugin(&definition)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omarchy-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Adam Moussa Ali
@@ -9,8 +9,7 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: A persistent Ruby runtime, reactive UI model, and safe QML renderer for
13
- Omarchy.
12
+ description: The application framework for building native Omarchy apps with Ruby.
14
13
  executables:
15
14
  - omarchy_ui
16
15
  extensions: []