omarchy-ui 0.0.3-x86_64-linux → 0.0.4-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: 4f53f63774323d862684e519c9884aed320d675b9391d228354def2b7768f0e4
4
- data.tar.gz: 0ed0b927c051f03f6418f737769b51bc16c80114a70e362254daf23832f72196
3
+ metadata.gz: b39c650e3541f41aea662dccf79373f790cc9fc84466b065ccb7b8836d74739e
4
+ data.tar.gz: 985d17d6917e19b2fc10996d7f20d8a99e6dd49bb5160a88aa7122220056e025
5
5
  SHA512:
6
- metadata.gz: 20c6694b5a1641f360402835d44b2eabefd164a373e28f94b878d0e41e1351cb3c40a67db4a19e2d7ff0df5298d1a6ff0ed8316ea7aa89ffe2073ffef2f7dfd0
7
- data.tar.gz: 00457faae9811da4e493d35d77b7c0ed514200c16acd51174841e5e3a172f9687a891d44f18c5dca60ad1be1ea47398a4b7128160d66dfd8c37c1ad796dde9df
6
+ metadata.gz: cfa24ef79ecfdffcb0e00c4243b26600a4df4e2b4cf197aad307bceef54011631c482a15d055b2c88710acda2e8baceba0ebec1106ae2c290850016ea70793b9
7
+ data.tar.gz: bf1ed984d03aea86c9db37cdd3dc1556faad23d23fef443d81669770a3e7431e87aa8632932794be1ad820117a98181990764c6401cb3de44513b9cc07d8e768
data/ControlNode.qml CHANGED
@@ -40,6 +40,13 @@ Loader {
40
40
  return icons[key] || key
41
41
  }
42
42
 
43
+ function escapeAutoText(value) {
44
+ return String(value === undefined || value === null ? "" : value)
45
+ .replace(/&/g, "&")
46
+ .replace(/</g, "&lt;")
47
+ .replace(/>/g, "&gt;")
48
+ }
49
+
43
50
  function easingType(name) {
44
51
  var easings = {
45
52
  linear: Easing.Linear,
@@ -224,6 +231,7 @@ Loader {
224
231
 
225
232
  Text {
226
233
  text: String(root.prop("text", ""))
234
+ textFormat: Text.PlainText
227
235
  color: root.prop("color", root.foreground)
228
236
  font.family: root.fontFamily
229
237
  font.pixelSize: {
@@ -243,6 +251,7 @@ Loader {
243
251
 
244
252
  Text {
245
253
  text: root.iconGlyph(root.prop("name", root.prop("text", "")))
254
+ textFormat: Text.PlainText
246
255
  color: root.foreground
247
256
  font.family: root.fontFamily
248
257
  font.pixelSize: Number(root.prop("size", Style.font.icon))
@@ -253,9 +262,9 @@ Loader {
253
262
  id: buttonComponent
254
263
 
255
264
  OmarchyUi.Button {
256
- text: String(root.prop("text", ""))
265
+ text: root.escapeAutoText(root.prop("text", ""))
257
266
  iconText: root.iconGlyph(root.prop("icon", ""))
258
- tooltipText: String(root.prop("tooltip", ""))
267
+ tooltipText: root.escapeAutoText(root.prop("tooltip", ""))
259
268
  selected: root.prop("selected", false) === true
260
269
  active: root.prop("active", false) === true
261
270
  hasCursor: root.prop("cursor", false) === true
@@ -461,7 +470,7 @@ Loader {
461
470
  Component {
462
471
  id: actionButtonComponent
463
472
  OmarchyUi.PanelActionButton {
464
- iconText: root.iconGlyph(root.prop("icon", "")); tooltipText: String(root.prop("tooltip", ""))
473
+ iconText: root.iconGlyph(root.prop("icon", "")); tooltipText: root.escapeAutoText(root.prop("tooltip", ""))
465
474
  bordered: root.prop("bordered", false) === true; focusable: root.prop("focusable", false) === true
466
475
  hasCursor: root.prop("cursor", false) === true
467
476
  size: Number(root.prop("size", 28)); foreground: root.prop("foreground", root.foreground)
@@ -475,7 +484,7 @@ Loader {
475
484
  Component {
476
485
  id: toggleComponent
477
486
  OmarchyUi.Toggle {
478
- label: String(root.prop("label", "")); description: String(root.prop("description", ""))
487
+ label: root.escapeAutoText(root.prop("label", "")); description: root.escapeAutoText(root.prop("description", ""))
479
488
  checked: root.prop("checked", false) === true; hasCursor: root.prop("cursor", false) === true
480
489
  rounded: root.prop("rounded", Style.cornerRadius > 0) === true
481
490
  foreground: root.prop("foreground", root.foreground); accent: root.prop("accent", Color.accent)
@@ -522,7 +531,7 @@ Loader {
522
531
  Component {
523
532
  id: numberFieldComponent
524
533
  OmarchyUi.NumberField {
525
- label: String(root.prop("label", "")); value: Number(root.prop("value", 0))
534
+ label: root.escapeAutoText(root.prop("label", "")); value: Number(root.prop("value", 0))
526
535
  from: Number(root.prop("from", 0)); to: Number(root.prop("to", 100)); stepSize: Number(root.prop("step", 1))
527
536
  foreground: root.prop("foreground", root.foreground); accent: root.prop("accent", Color.accent)
528
537
  fontFamily: String(root.prop("font_family", root.fontFamily)); fontSize: Number(root.prop("font_size", Style.font.body))
@@ -551,7 +560,7 @@ Loader {
551
560
  Component {
552
561
  id: dropdownComponent
553
562
  OmarchyUi.Dropdown {
554
- label: String(root.prop("label", "")); value: String(root.prop("value", "")); options: root.prop("options", [])
563
+ label: root.escapeAutoText(root.prop("label", "")); value: String(root.prop("value", "")); options: root.prop("options", [])
555
564
  implicitWidth: Number(root.prop("width", 240)); foreground: root.prop("foreground", root.foreground)
556
565
  background: root.prop("background", Color.popups.background); popupBorder: root.prop("popup_border", Color.popups.border)
557
566
  accent: root.prop("accent", Color.accent); fontFamily: String(root.prop("font_family", root.fontFamily))
@@ -565,7 +574,7 @@ Loader {
565
574
  Component {
566
575
  id: multiSelectComponent
567
576
  OmarchyUi.MultiSelect {
568
- label: String(root.prop("label", "")); values: root.prop("values", []); options: root.prop("options", [])
577
+ label: root.escapeAutoText(root.prop("label", "")); values: root.prop("values", []); options: root.prop("options", [])
569
578
  placeholderText: String(root.prop("placeholder", "Search...")); enabled: root.prop("enabled", true) !== false
570
579
  optionsCommand: root.prop("options_command", []); optionsCommandCwd: String(root.prop("options_command_cwd", ""))
571
580
  emptyText: String(root.prop("empty_text", "No options")); noSelectionText: String(root.prop("no_selection_text", "None selected"))
@@ -604,12 +613,12 @@ Loader {
604
613
  }
605
614
 
606
615
  Component { id: separatorComponent; OmarchyUi.PanelSeparator { foreground: root.foreground; strength: Number(root.prop("strength", 0.12)) } }
607
- Component { id: sectionHeaderComponent; OmarchyUi.PanelSectionHeader { text: String(root.prop("text", "")); foreground: root.foreground; fontFamily: root.fontFamily } }
616
+ Component { id: sectionHeaderComponent; OmarchyUi.PanelSectionHeader { text: root.escapeAutoText(root.prop("text", "")); foreground: root.foreground; fontFamily: root.fontFamily } }
608
617
 
609
618
  Component {
610
619
  id: searchableDropdownComponent
611
620
  OmarchyUi.SearchableDropdown {
612
- label: String(root.prop("label", "")); value: String(root.prop("value", "")); options: root.prop("options", [])
621
+ label: root.escapeAutoText(root.prop("label", "")); value: String(root.prop("value", "")); options: root.prop("options", [])
613
622
  placeholderText: String(root.prop("placeholder", "Search...")); emptyText: String(root.prop("empty_text", "No matches"))
614
623
  triggerLabel: String(root.prop("trigger_label", "")); implicitWidth: Number(root.prop("width", 240))
615
624
  foreground: root.prop("foreground", root.foreground); background: root.prop("background", Color.popups.background)
@@ -640,7 +649,7 @@ Loader {
640
649
  Component {
641
650
  id: panelHeroComponent
642
651
  OmarchyUi.PanelHero {
643
- title: String(root.prop("title", "")); meta: String(root.prop("meta", "")); detail: String(root.prop("detail", ""))
652
+ title: root.escapeAutoText(root.prop("title", "")); meta: root.escapeAutoText(root.prop("meta", "")); detail: root.escapeAutoText(root.prop("detail", ""))
644
653
  iconSize: Number(root.prop("icon_size", Style.font.display)); iconOpacity: Number(root.prop("icon_opacity", 1))
645
654
  metaOpacity: Number(root.prop("meta_opacity", 1)); foreground: root.prop("foreground", root.foreground)
646
655
  fontFamily: String(root.prop("font_family", root.fontFamily))
@@ -665,7 +674,7 @@ Loader {
665
674
  Component {
666
675
  id: widgetButtonComponent
667
676
  OmarchyUi.WidgetButton {
668
- text: String(root.prop("text", "")); tooltipText: String(root.prop("tooltip", "")); active: root.prop("active", false) === true
677
+ text: root.escapeAutoText(root.prop("text", "")); tooltipText: root.escapeAutoText(root.prop("tooltip", "")); active: root.prop("active", false) === true
669
678
  dimmed: root.prop("dimmed", false) === true; concealed: root.prop("concealed", false) === true
670
679
  interactive: root.prop("interactive", true) !== false; pressable: root.prop("pressable", true) !== false
671
680
  fontFamily: String(root.prop("font_family", root.fontFamily)); fontSize: Number(root.prop("font_size", Style.font.body))
@@ -720,10 +729,10 @@ Loader {
720
729
  id: rowContent
721
730
  anchors.centerIn: parent
722
731
  spacing: Style.spacing.controlGap
723
- Text { visible: text !== ""; text: root.iconGlyph(typeof modelData === "object" ? modelData[listControl.iconField] : ""); color: root.foreground; font.family: root.fontFamily }
732
+ Text { visible: text !== ""; text: root.iconGlyph(typeof modelData === "object" ? modelData[listControl.iconField] : ""); textFormat: Text.PlainText; color: root.foreground; font.family: root.fontFamily }
724
733
  Column {
725
- Text { text: String(typeof modelData === "object" ? (modelData[listControl.labelField] ?? value) : modelData); color: root.foreground; font.family: root.fontFamily }
726
- Text { visible: text !== ""; text: String(typeof modelData === "object" ? (modelData[listControl.descriptionField] || "") : ""); color: Qt.darker(root.foreground, 1.4); font.family: root.fontFamily; font.pixelSize: Style.font.caption }
734
+ Text { text: String(typeof modelData === "object" ? (modelData[listControl.labelField] ?? value) : modelData); textFormat: Text.PlainText; color: root.foreground; font.family: root.fontFamily }
735
+ Text { visible: text !== ""; text: String(typeof modelData === "object" ? (modelData[listControl.descriptionField] || "") : ""); textFormat: Text.PlainText; color: Qt.darker(root.foreground, 1.4); font.family: root.fontFamily; font.pixelSize: Style.font.caption }
727
736
  }
728
737
  }
729
738
  MouseArea {
@@ -738,7 +747,7 @@ Loader {
738
747
 
739
748
  Text {
740
749
  anchors.centerIn: parent; visible: listControl.count === 0
741
- text: String(root.prop("empty_text", "No items")); color: Qt.darker(root.foreground, 1.4); font.family: root.fontFamily
750
+ text: String(root.prop("empty_text", "No items")); textFormat: Text.PlainText; color: Qt.darker(root.foreground, 1.4); font.family: root.fontFamily
742
751
  }
743
752
  }
744
753
  }
data/README.md CHANGED
@@ -47,12 +47,16 @@ A new standalone project contains only application-owned files:
47
47
 
48
48
  ```text
49
49
  myapp/
50
- ├── Components/
51
- │ └── Welcome.qml
50
+ ├── components/
51
+ │ └── welcome.rb
52
52
  ├── README.md
53
53
  └── main.rb
54
54
  ```
55
55
 
56
+ The generated application UI is entirely Ruby. `main.rb` loads reusable Ruby components from
57
+ `components/`; no QML source is generated into the application project. The framework creates the
58
+ required native bridge files only when launching or bundling the app.
59
+
56
60
  `launch` opens a compositor-managed window. Drag its title bar or use Super+drag, and close it
57
61
  with Super+W. For protocol debugging without a window, use `omarchy_ui run main.rb`.
58
62
 
data/Service.qml CHANGED
@@ -24,6 +24,8 @@ Item {
24
24
  property int revision: 0
25
25
  property int restartDelayMs: 500
26
26
  property int eventSequence: 0
27
+ readonly property int maxStringLength: 16384
28
+ readonly property int maxCollectionItems: 256
27
29
 
28
30
  property var allowedTypes: ({})
29
31
  property var allowedProperties: ({})
@@ -41,6 +43,26 @@ Item {
41
43
  return value !== null && typeof value === "object" && !Array.isArray(value)
42
44
  }
43
45
 
46
+ function boundedValue(value, depth) {
47
+ if (depth > 8) return false
48
+ if (value === null || typeof value === "number" || typeof value === "boolean") return true
49
+ if (typeof value === "string") return value.length <= maxStringLength
50
+ if (Array.isArray(value)) {
51
+ if (value.length > maxCollectionItems) return false
52
+ for (var i = 0; i < value.length; i++) if (!boundedValue(value[i], depth + 1)) return false
53
+ return true
54
+ }
55
+ if (plainObject(value)) {
56
+ var keys = Object.keys(value)
57
+ if (keys.length > maxCollectionItems) return false
58
+ for (var k = 0; k < keys.length; k++) {
59
+ if (keys[k].length > 128 || !boundedValue(value[keys[k]], depth + 1)) return false
60
+ }
61
+ return true
62
+ }
63
+ return false
64
+ }
65
+
44
66
  function validateNode(node, index, depth, count) {
45
67
  if (!plainObject(node) || depth > 32 || count.value >= 2000) return false
46
68
  if (!allowedTypes[String(node.type || "")] || !validId(node.id)) return false
@@ -52,8 +74,7 @@ Item {
52
74
  for (var key in props) {
53
75
  if (!whitelist[key]) return false
54
76
  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
77
+ if (!boundedValue(value, 0)) return false
57
78
  }
58
79
 
59
80
  var children = node.children === undefined ? [] : node.children
@@ -225,8 +246,7 @@ Item {
225
246
  if (message.op !== "set" || typeof message.property !== "string") return reject("invalid patch")
226
247
  if (!allowedProperties[node.type][message.property]) return reject("patch target rejected")
227
248
  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")
249
+ if (!boundedValue(value, 0)) return reject("patch value rejected")
230
250
 
231
251
  var animation = message.animation
232
252
  if (animation !== undefined && !validAnimation(animation)) return reject("patch animation rejected")
@@ -292,14 +312,15 @@ Item {
292
312
  } else if (message.type === "ack") {
293
313
  // Acknowledgements are consumed by benchmarks and future diagnostics.
294
314
  } else if (message.type === "handler_error" || message.type === "runtime_error" || message.type === "protocol_error") {
295
- lastError = String(message.message || message.code || "Ruby runtime error")
315
+ lastError = String(message.message || message.code || "Ruby runtime error").slice(0, 512)
296
316
  } else {
297
317
  reject("unknown message type")
298
318
  }
299
319
  }
300
320
 
301
321
  function handleEffect(message) {
302
- if (typeof message.name !== "string" || !plainObject(message.payload || {}))
322
+ if (typeof message.name !== "string" || message.name.length > 64
323
+ || !plainObject(message.payload || {}) || !boundedValue(message.payload || {}, 0))
303
324
  return reject("invalid effect")
304
325
  var payload = message.payload || {}
305
326
  effectReceived(message.name, payload)
@@ -313,7 +334,7 @@ Item {
313
334
  }
314
335
 
315
336
  function reject(reason) {
316
- lastError = String(reason || "protocol error")
337
+ lastError = String(reason || "protocol error").slice(0, 512)
317
338
  console.warn("omarchy-ui bridge:", lastError)
318
339
  return false
319
340
  }
@@ -342,7 +363,8 @@ Item {
342
363
  }
343
364
 
344
365
  function sendEvent(surfaceName, controlId, eventName, payload) {
345
- if (!rubyProcess.running || !validId(controlId) || !/^[a-z][a-z0-9_]{0,63}$/.test(eventName)) return false
366
+ if (!rubyProcess.running || !validId(controlId) || !/^[a-z][a-z0-9_]{0,63}$/.test(eventName)
367
+ || !plainObject(payload || {}) || !boundedValue(payload || {}, 0)) return false
346
368
  var target = nodeIndex[String(controlId)]
347
369
  var definition = target ? componentDefinitions[String(target.type)] : null
348
370
  var subscriptions = target && Array.isArray(target.events) ? target.events : []
@@ -298,6 +298,7 @@ module OmarchyUI
298
298
  def emit(message)
299
299
  return unless @running && @output
300
300
  encoded = JSON.generate(message)
301
+ raise ProtocolError, "outgoing message exceeds #{MAX_MESSAGE_BYTES} bytes" if encoded.bytesize > MAX_MESSAGE_BYTES
301
302
  @write_lock.synchronize { @output.puts(encoded) }
302
303
  end
303
304
 
@@ -53,6 +53,8 @@ module OmarchyUI
53
53
 
54
54
  project_dir = File.dirname(file)
55
55
  Dir.mktmpdir("omarchy-ui-app-") do |runtime_dir|
56
+ runtime_program = File.join(runtime_dir, "main.rb")
57
+ File.write(runtime_program, SourceBundle.new(file, root: project_dir).call)
56
58
  Project::RUNTIME_FILES.each do |name|
57
59
  source = File.file?(File.join(project_dir, name)) ? File.join(project_dir, name) : File.join(FRAMEWORK_ROOT, name)
58
60
  FileUtils.cp(source, runtime_dir)
@@ -65,7 +67,7 @@ module OmarchyUI
65
67
 
66
68
  environment = ENV.to_h.merge(
67
69
  "OMARCHY_UI_PROJECT_DIR" => project_dir,
68
- "OMARCHY_UI_RUBY_PROGRAM" => file,
70
+ "OMARCHY_UI_RUBY_PROGRAM" => runtime_program,
69
71
  "OMARCHY_UI_RUNTIME" => Runtime.executable,
70
72
  "QT_LOGGING_RULES" => qt_logging_rules
71
73
  )
@@ -105,6 +107,7 @@ module OmarchyUI
105
107
  FileUtils.mkdir_p(destination)
106
108
  entries = application_entries(source)
107
109
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination)
110
+ bundle_ruby_entrypoint(source, destination)
108
111
  Project.install_runtime(destination)
109
112
  if File.file?(File.join(destination, "manifest.json"))
110
113
  raise ArgumentError, "bundled plugin validation failed" unless system("omarchy", "plugin", "validate", destination)
@@ -139,7 +142,7 @@ module OmarchyUI
139
142
  end
140
143
 
141
144
  def application_entries(source)
142
- generated = Project::RUNTIME_FILES + %w[omarchy-ui-runtime run Commons Ui]
145
+ generated = Project::RUNTIME_FILES + Project::RUNTIME_AUDIT_FILES + %w[omarchy-ui-runtime run Commons Ui]
143
146
  Dir.children(source).reject { |entry| %w[.git dist].include?(entry) || generated.include?(entry) }
144
147
  end
145
148
 
@@ -194,13 +197,21 @@ module OmarchyUI
194
197
  staging = parent ? Dir.mktmpdir(prefix, parent) : Dir.mktmpdir(prefix)
195
198
  entries = application_entries(source)
196
199
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, staging) unless entries.empty?
197
- Project.install_runtime(staging) if File.file?(File.join(staging, "main.rb"))
200
+ if File.file?(File.join(staging, "main.rb"))
201
+ bundle_ruby_entrypoint(source, staging)
202
+ Project.install_runtime(staging)
203
+ end
198
204
  staging
199
205
  rescue StandardError
200
206
  FileUtils.remove_entry(staging) if staging && File.exist?(staging)
201
207
  raise
202
208
  end
203
209
 
210
+ def bundle_ruby_entrypoint(source, destination)
211
+ entrypoint = File.join(source, "main.rb")
212
+ File.write(File.join(destination, "main.rb"), SourceBundle.new(entrypoint, root: source).call)
213
+ end
214
+
204
215
  def activate_plugin(plugin_id)
205
216
  return if system("omarchy", "plugin", "enable", plugin_id)
206
217
  system("omarchy-shell", "shell", "rescanPlugins")
@@ -5,6 +5,7 @@ require "timeout"
5
5
 
6
6
  module OmarchyUI
7
7
  class CommandTimeout < StandardError; end
8
+ class CommandOutputLimit < StandardError; end
8
9
 
9
10
  CommandResult = Struct.new(:stdout, :stderr, :status, keyword_init: true) do
10
11
  def success? = status.success?
@@ -14,8 +15,12 @@ module OmarchyUI
14
15
  module Command
15
16
  module_function
16
17
 
17
- def run(argv, env: {}, chdir: nil, input: "", timeout: nil)
18
+ DEFAULT_MAX_OUTPUT_BYTES = 1_048_576
19
+
20
+ def run(argv, env: {}, chdir: nil, input: "", timeout: nil, max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES)
18
21
  arguments = normalize_argv(argv)
22
+ output_limit = Integer(max_output_bytes)
23
+ raise ArgumentError, "max_output_bytes must be positive" unless output_limit.positive?
19
24
  options = {}
20
25
  options[:chdir] = File.expand_path(chdir) if chdir
21
26
  stdin = stdout = stderr = wait_thread = nil
@@ -23,10 +28,15 @@ module OmarchyUI
23
28
  stdin, stdout, stderr, wait_thread = child_stdin, child_stdout, child_stderr, child_wait
24
29
  stdin.write(input.to_s)
25
30
  stdin.close
26
- stdout_reader = Thread.new { stdout.read }
27
- stderr_reader = Thread.new { stderr.read }
31
+ stdout_reader = Thread.new { bounded_read(stdout, output_limit) }
32
+ stderr_reader = Thread.new { bounded_read(stderr, output_limit) }
28
33
  status = timeout ? Timeout.timeout(Float(timeout)) { wait_thread.value } : wait_thread.value
29
- return CommandResult.new(stdout: stdout_reader.value, stderr: stderr_reader.value, status:)
34
+ stdout_value, stdout_limited = stdout_reader.value
35
+ stderr_value, stderr_limited = stderr_reader.value
36
+ if stdout_limited || stderr_limited
37
+ raise CommandOutputLimit, "command output exceeded #{output_limit} bytes: #{arguments.first}"
38
+ end
39
+ return CommandResult.new(stdout: stdout_value, stderr: stderr_value, status:)
30
40
  rescue Timeout::Error
31
41
  terminate(wait_thread)
32
42
  stdout_reader&.join(1)
@@ -37,6 +47,24 @@ module OmarchyUI
37
47
  [stdin, stdout, stderr].each { |stream| stream&.close unless stream&.closed? }
38
48
  end
39
49
 
50
+ def bounded_read(stream, limit)
51
+ output = +""
52
+ exceeded = false
53
+ loop do
54
+ chunk = stream.readpartial(16_384)
55
+ remaining = limit - output.bytesize
56
+ if remaining.positive?
57
+ output << chunk.byteslice(0, remaining)
58
+ exceeded = true if chunk.bytesize > remaining
59
+ else
60
+ exceeded = true
61
+ end
62
+ end
63
+ rescue EOFError
64
+ [output, exceeded]
65
+ end
66
+ private_class_method :bounded_read
67
+
40
68
  def normalize_argv(argv)
41
69
  raise ArgumentError, "command must be an argv array" unless argv.is_a?(Array) && !argv.empty?
42
70
  argv.map do |argument|
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  module OmarchyUI
6
6
  class Project
7
7
  RUNTIME_FILES = %w[Service.qml ControlNode.qml Panel.qml BarWidget.qml App.qml].freeze
8
+ RUNTIME_AUDIT_FILES = %w[omarchy-ui-runtime.sha256 RUNTIME_PROVENANCE.md].freeze
8
9
 
9
10
  def initialize(path:, name: nil, framework_root: FRAMEWORK_ROOT)
10
11
  @path = File.expand_path(path)
@@ -15,9 +16,9 @@ module OmarchyUI
15
16
  def create
16
17
  raise ArgumentError, "destination already exists: #{@path}" if File.exist?(@path)
17
18
  created = true
18
- FileUtils.mkdir_p(File.join(@path, "Components"))
19
+ FileUtils.mkdir_p(File.join(@path, "components"))
19
20
  File.write(File.join(@path, "main.rb"), main_program)
20
- File.write(File.join(@path, "Components", "Welcome.qml"), welcome_component)
21
+ File.write(File.join(@path, "components", "welcome.rb"), welcome_component)
21
22
  File.write(File.join(@path, "README.md"), readme)
22
23
  @path
23
24
  rescue StandardError
@@ -36,6 +37,10 @@ module OmarchyUI
36
37
  FileUtils.cp(bundled_runtime, destination)
37
38
  FileUtils.chmod(0o755, destination)
38
39
  end
40
+ RUNTIME_AUDIT_FILES.each do |file|
41
+ source = File.join(framework_root, "vendor", "runtime", "x86_64-linux", file)
42
+ FileUtils.cp(source, File.join(path, file)) if File.file?(source)
43
+ end
39
44
  FileUtils.mkdir_p(File.join(path, "Components"))
40
45
  end
41
46
 
@@ -45,63 +50,37 @@ module OmarchyUI
45
50
  <<~RUBY
46
51
  # frozen_string_literal: true
47
52
 
48
- require "omarchy_ui" unless Object.const_defined?(:OmarchyUI)
53
+ require "omarchy_ui"
54
+ require_relative "components/welcome"
49
55
 
50
56
  OmarchyUI.plugin do
51
- register_component :welcome,
52
- qml: "Welcome.qml",
53
- properties: %i[title message]
54
-
55
57
  app :main, title: "#{@name}", width: 760, height: 520 do
56
- component :welcome,
58
+ welcome_card(
57
59
  title: "Welcome to #{@name}",
58
60
  message: "This is the official Omarchy UI framework."
61
+ )
59
62
  end
60
63
  end
61
64
  RUBY
62
65
  end
63
66
 
64
67
  def welcome_component
65
- <<~QML
66
- import QtQuick
67
- import qs.Commons
68
- import qs.Ui
69
-
70
- BorderSurface {
71
- id: root
72
-
73
- property string title: "Welcome"
74
- property string message: "This is the official Omarchy UI framework."
75
-
76
- implicitWidth: 520
77
- implicitHeight: 220
78
- color: Color.popups.background
79
- radius: Style.cornerRadius
80
- borderSpec: Border.controlSpec("normal", Color.foreground, Color.accent)
81
-
82
- Column {
83
- anchors.centerIn: parent
84
- spacing: Style.spacing.lg
85
-
86
- Text {
87
- anchors.horizontalCenter: parent.horizontalCenter
88
- text: root.title
89
- color: Color.foreground
90
- font.family: Style.font.family
91
- font.pixelSize: Style.font.heading
92
- font.bold: true
93
- }
94
-
95
- Text {
96
- anchors.horizontalCenter: parent.horizontalCenter
97
- text: root.message
98
- color: Color.foreground
99
- font.family: Style.font.family
100
- font.pixelSize: Style.font.body
101
- }
102
- }
103
- }
104
- QML
68
+ <<~RUBY
69
+ # frozen_string_literal: true
70
+
71
+ module WelcomeComponent
72
+ def welcome_card(title:, message:)
73
+ container padding: 24, bordered: true do
74
+ column spacing: 12 do
75
+ text title, style: :heading
76
+ text message, wrap: true
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ OmarchyUI::Builder.include(WelcomeComponent)
83
+ RUBY
105
84
  end
106
85
 
107
86
  def readme
@@ -119,8 +98,10 @@ module OmarchyUI
119
98
  The shared `omarchy-ui-runtime` must be installed on `PATH`. No system Ruby,
120
99
  application manifest, or copied framework QML files are required.
121
100
 
122
- Edit `main.rb` for application state and behavior. Custom QML adapters live in
123
- `Components/`; `Welcome.qml` is included as a working example.
101
+ Edit `main.rb` for application state and behavior. Reusable Ruby UI components live in
102
+ `components/` and load with ordinary Ruby `require_relative`; `welcome.rb` is included as a
103
+ working example. `omarchy_ui bundle` generates
104
+ the native QML bridge and embeds the runtime for distribution.
124
105
  MARKDOWN
125
106
  end
126
107
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmarchyUI
4
+ class SourceBundle
5
+ REQUIRE_RELATIVE = /\A\s*require_relative\s*(?:\(\s*)?["']([^"']+)["']\s*\)?\s*(?:#.*)?\z/
6
+ EMBEDDED_FRAMEWORK_REQUIRE = /\A\s*require\s*(?:\(\s*)?["']omarchy_ui["']\s*\)?\s*(?:#.*)?\z/
7
+
8
+ def initialize(entrypoint, root: File.dirname(entrypoint))
9
+ @entrypoint = File.expand_path(entrypoint)
10
+ @root = File.expand_path(root)
11
+ @loaded = {}
12
+ @loading = []
13
+ end
14
+
15
+ def call
16
+ expand(@entrypoint)
17
+ end
18
+
19
+ private
20
+
21
+ def expand(path)
22
+ path = ruby_path(File.expand_path(path))
23
+ ensure_inside_root!(path)
24
+ return "" if @loaded[path]
25
+ raise ArgumentError, "circular require_relative: #{relative(path)}" if @loading.include?(path)
26
+ raise ArgumentError, "required Ruby file not found: #{relative(path)}" unless File.file?(path)
27
+
28
+ @loading << path
29
+ source = File.readlines(path, chomp: true).map do |line|
30
+ match = REQUIRE_RELATIVE.match(line)
31
+ if match
32
+ expand(File.expand_path(match[1], File.dirname(path)))
33
+ elsif EMBEDDED_FRAMEWORK_REQUIRE.match?(line)
34
+ ""
35
+ else
36
+ line
37
+ end
38
+ end.join("\n")
39
+ @loaded[path] = true
40
+ "# source: #{relative(path)}\n#{source}\n"
41
+ ensure
42
+ @loading.pop if @loading.last == path
43
+ end
44
+
45
+ def ruby_path(path)
46
+ File.extname(path).empty? ? "#{path}.rb" : path
47
+ end
48
+
49
+ def ensure_inside_root!(path)
50
+ return if path == @root || path.start_with?("#{@root}#{File::SEPARATOR}")
51
+
52
+ raise ArgumentError, "require_relative escapes the application directory: #{path}"
53
+ end
54
+
55
+ def relative(path)
56
+ path.delete_prefix("#{@root}#{File::SEPARATOR}")
57
+ end
58
+ end
59
+ end
data/lib/omarchy_ui.rb CHANGED
@@ -11,11 +11,12 @@ require_relative "omarchy_ui/component_registry"
11
11
  require_relative "omarchy_ui/components"
12
12
  require_relative "omarchy_ui/builder"
13
13
  require_relative "omarchy_ui/application"
14
+ require_relative "omarchy_ui/source_bundle"
14
15
  require_relative "omarchy_ui/project"
15
16
  require_relative "omarchy_ui/runtime"
16
17
 
17
18
  module OmarchyUI
18
- VERSION = "0.0.3"
19
+ VERSION = "0.0.4"
19
20
  FRAMEWORK_ROOT = File.expand_path("..", __dir__)
20
21
 
21
22
  def self.plugin(&definition)
@@ -0,0 +1,10 @@
1
+ # Omarchy UI runtime provenance
2
+
3
+ - Omarchy UI source: https://github.com/AdamMusa/omarchy-ui
4
+ - Omarchy UI source revision: `1a516ce535156c15b90d685b030443257e79fe57`
5
+ - mruby source revision: `831da26b9021de0369d17b71b5667e2941a1a32d`
6
+ - Target: x86-64 Linux
7
+ - SHA-256: `65214c89ca9ba4787d574e48b17b774cf59bb589712bc5565d2e3794877b9e47`
8
+
9
+ Rebuild and verification instructions are committed at
10
+ [`docs/runtime-build.md`](https://github.com/AdamMusa/omarchy-ui/blob/1a516ce535156c15b90d685b030443257e79fe57/docs/runtime-build.md).
@@ -0,0 +1 @@
1
+ 65214c89ca9ba4787d574e48b17b774cf59bb589712bc5565d2e3794877b9e47 omarchy-ui-runtime
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.3
4
+ version: 0.0.4
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Adam Moussa Ali
@@ -38,10 +38,13 @@ files:
38
38
  - lib/omarchy_ui/protocol.rb
39
39
  - lib/omarchy_ui/runtime.rb
40
40
  - lib/omarchy_ui/scheduler.rb
41
+ - lib/omarchy_ui/source_bundle.rb
41
42
  - lib/omarchy_ui/state_store.rb
42
43
  - lib/omarchy_ui/value.rb
43
44
  - manifest.json
45
+ - vendor/runtime/x86_64-linux/RUNTIME_PROVENANCE.md
44
46
  - vendor/runtime/x86_64-linux/omarchy-ui-runtime
47
+ - vendor/runtime/x86_64-linux/omarchy-ui-runtime.sha256
45
48
  homepage: https://github.com/AdamMusa/omarchy-ui
46
49
  licenses:
47
50
  - MIT