omarchy-ui 0.0.2-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: d8c832cd4301f1b237ef13b46fea6eed08cafdae66a1411eae88a887a59a5cf0
4
- data.tar.gz: 60b933f9795e67a7b6c1ce2d8e90cb15716dbc9821250bbd8e85a4c7e840d1f1
3
+ metadata.gz: 4f53f63774323d862684e519c9884aed320d675b9391d228354def2b7768f0e4
4
+ data.tar.gz: 0ed0b927c051f03f6418f737769b51bc16c80114a70e362254daf23832f72196
5
5
  SHA512:
6
- metadata.gz: 415009fe051699cef4821e70e394dec94b883aa86dfd325a8c2e8afd38a7a3076c9ec21df6efea747ae791b715c5a5de92ef837ccef84f458683f49d805e295e
7
- data.tar.gz: 3a017f0872adcec2dff15d35f03e7ac40a2d463dcee56a978e2e514aee187c804ecfce9e8573e3cf1b4312fad5364ec0686af3e7e9e6f7f9eab9ea09a45a1eab
6
+ metadata.gz: 20c6694b5a1641f360402835d44b2eabefd164a373e28f94b878d0e41e1351cb3c40a67db4a19e2d7ff0df5298d1a6ff0ed8316ea7aa89ffe2073ffef2f7dfd0
7
+ data.tar.gz: 00457faae9811da4e493d35d77b7c0ed514200c16acd51174841e5e3a172f9687a891d44f18c5dca60ad1be1ea47398a4b7128160d66dfd8c37c1ad796dde9df
data/README.md CHANGED
@@ -25,29 +25,28 @@ end
25
25
 
26
26
  ## Install
27
27
 
28
- On Omarchy x86-64, install the gem and start building:
28
+ On Omarchy x86-64, install the gem:
29
29
 
30
30
  ```bash
31
31
  gem install omarchy-ui
32
- omarchy_ui new "My App"
33
32
  ```
34
33
 
35
- The gem contains everything required to create, launch, and bundle an application. Developers do
34
+ The gem contains everything required to launch and bundle an application. Developers do
36
35
  not need to install a separate runtime or copy framework files. Bundled applications require only
37
36
  an Omarchy computer to run.
38
37
 
39
- ## Create and run an application
38
+ ## Start an application
40
39
 
41
40
  ```bash
42
- omarchy_ui new "My App"
43
- cd my-app
41
+ omarchy_ui new MyApp
42
+ cd myapp
44
43
  omarchy_ui launch main.rb
45
44
  ```
46
45
 
47
- The standalone generator creates no plugin manifest or copied runtime files:
46
+ A new standalone project contains only application-owned files:
48
47
 
49
48
  ```text
50
- my-app/
49
+ myapp/
51
50
  ├── Components/
52
51
  │ └── Welcome.qml
53
52
  ├── README.md
@@ -63,10 +62,10 @@ From an application directory:
63
62
 
64
63
  ```bash
65
64
  omarchy_ui bundle
66
- ./dist/my-app/run
65
+ ./dist/myapp/run
67
66
  ```
68
67
 
69
- `bundle` creates a self-contained application under `dist/<project-name>/`. The generated `run`
68
+ `bundle` creates a self-contained application under `dist/<project-name>/`. Its `run`
70
69
  launcher works on another Omarchy computer without Ruby or the `omarchy-ui` gem.
71
70
 
72
71
  An Omarchy Shell plugin is a separate packaging mode. It requires `manifest.json` so the shell
@@ -306,15 +305,6 @@ Component names, files, properties, events, IDs, effects, values, message sizes,
306
305
  limits are validated. Commands use argv arrays without a shell. Applications and plugins run with
307
306
  the current user's permissions, so review third-party code before installing it.
308
307
 
309
- ## Omarchy Phone example
310
-
311
- `examples/omarchy-phone` demonstrates reactive controls, background discovery, safe commands,
312
- ADB pairing and connection, scrcpy launching, iPhone discovery, and UxPlay AirPlay mirroring.
313
-
314
- ```bash
315
- omarchy_ui launch examples/omarchy-phone/main.rb
316
- ```
317
-
318
308
  ## Development and verification
319
309
 
320
310
  ```bash
@@ -324,7 +314,7 @@ ruby script/benchmark.rb
324
314
  ```
325
315
 
326
316
  The suite covers state, bindings, repeated structures, event persistence, component schemas,
327
- animation tracks and sequences, tasks, command safety, standalone project generation, packaging,
317
+ animation tracks and sequences, tasks, command safety, standalone projects, packaging,
328
318
  manifests, component contracts, linting, and the phone backend.
329
319
 
330
320
  ## License
@@ -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
 
@@ -103,12 +103,15 @@ module OmarchyUI
103
103
  destination = File.join(source, "dist", File.basename(source))
104
104
  raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
105
105
  FileUtils.mkdir_p(destination)
106
- entries = Dir.children(source).reject { |entry| %w[.git dist].include?(entry) }
106
+ entries = application_entries(source)
107
107
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination)
108
108
  Project.install_runtime(destination)
109
- runtime = File.join(destination, "omarchy-ui-runtime")
110
- FileUtils.cp(Runtime::BUNDLED, runtime)
111
- 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
+
112
115
  %w[Commons Ui].each do |module_name|
113
116
  FileUtils.ln_s(File.join("/usr/share/omarchy/shell", module_name), File.join(destination, module_name))
114
117
  end
@@ -135,11 +138,15 @@ module OmarchyUI
135
138
  [ENV["QT_LOGGING_RULES"], "qt.qpa.services.warning=false"].compact.reject(&:empty?).join(";")
136
139
  end
137
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
+
138
146
  def push(arguments)
139
147
  enable = !arguments.delete("--no-enable")
140
148
  restart = !arguments.delete("--no-restart")
141
149
  source = File.expand_path(arguments.shift || Dir.pwd)
142
- Runtime.install_shared
143
150
  manifest_path = File.join(source, "manifest.json")
144
151
  manifest = JSON.parse(File.read(manifest_path))
145
152
  plugin_id = manifest.fetch("id")
@@ -185,7 +192,7 @@ module OmarchyUI
185
192
  def stage_project(source, parent: nil, prefix: ".omarchy-ui-staging-")
186
193
  raise ArgumentError, "project directory not found: #{source}" unless File.directory?(source)
187
194
  staging = parent ? Dir.mktmpdir(prefix, parent) : Dir.mktmpdir(prefix)
188
- entries = Dir.children(source).reject { |entry| entry == ".git" }
195
+ entries = application_entries(source)
189
196
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, staging) unless entries.empty?
190
197
  Project.install_runtime(staging) if File.file?(File.join(staging, "main.rb"))
191
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.2"
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.2
4
+ version: 0.0.3
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Adam Moussa Ali