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 +4 -4
- data/ControlNode.qml +24 -15
- data/README.md +6 -2
- data/Service.qml +30 -8
- data/lib/omarchy_ui/application.rb +1 -0
- data/lib/omarchy_ui/cli.rb +14 -3
- data/lib/omarchy_ui/command.rb +32 -4
- data/lib/omarchy_ui/project.rb +31 -50
- data/lib/omarchy_ui/source_bundle.rb +59 -0
- data/lib/omarchy_ui.rb +2 -1
- data/vendor/runtime/x86_64-linux/RUNTIME_PROVENANCE.md +10 -0
- data/vendor/runtime/x86_64-linux/omarchy-ui-runtime +0 -0
- data/vendor/runtime/x86_64-linux/omarchy-ui-runtime.sha256 +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b39c650e3541f41aea662dccf79373f790cc9fc84466b065ccb7b8836d74739e
|
|
4
|
+
data.tar.gz: 985d17d6917e19b2fc10996d7f20d8a99e6dd49bb5160a88aa7122220056e025
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, "<")
|
|
47
|
+
.replace(/>/g, ">")
|
|
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:
|
|
265
|
+
text: root.escapeAutoText(root.prop("text", ""))
|
|
257
266
|
iconText: root.iconGlyph(root.prop("icon", ""))
|
|
258
|
-
tooltipText:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
├──
|
|
51
|
-
│ └──
|
|
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 (
|
|
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
|
|
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" ||
|
|
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)
|
|
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
|
|
data/lib/omarchy_ui/cli.rb
CHANGED
|
@@ -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" =>
|
|
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
|
-
|
|
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")
|
data/lib/omarchy_ui/command.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
27
|
-
stderr_reader = Thread.new { stderr
|
|
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
|
-
|
|
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|
|
data/lib/omarchy_ui/project.rb
CHANGED
|
@@ -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, "
|
|
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, "
|
|
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"
|
|
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
|
-
|
|
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
|
-
<<~
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
123
|
-
`
|
|
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.
|
|
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).
|
|
Binary file
|
|
@@ -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.
|
|
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
|