omarchy-ui 0.0.9-x86_64-linux → 0.0.10-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/README.md +31 -17
- data/Service.qml +23 -0
- data/lib/omarchy_ui/cli.rb +86 -10
- data/lib/omarchy_ui/generator.rb +36 -3
- data/lib/omarchy_ui/plugin_package.rb +147 -0
- data/lib/omarchy_ui/plugin_publisher.rb +263 -0
- data/lib/omarchy_ui/project_config.rb +1 -0
- data/lib/omarchy_ui/qml_compiler.rb +93 -111
- data/lib/omarchy_ui/runtime.rb +48 -20
- data/lib/omarchy_ui.rb +3 -1
- data/qml/DesktopStage.qml +88 -0
- data/qml/Positioned.qml +18 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba33f1f0b7fead5be0f82726faeeb3815b94386f679732ee4945d840374a1023
|
|
4
|
+
data.tar.gz: bc7bfb798a46491d22085e3f81cbde27da3b9f14149b1ec63bd3e36b182f83c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c76b00784fdf4a0371269d79d570e10ece613faf94449f2e40025a7a5a7a45b53080b259a7a68cedc7e21c89cc03e628cc3e3301c04db806145db19cc2b0d6ed
|
|
7
|
+
data.tar.gz: f4511e5523b4a033be1d80682305d99e0502721186cf8330d3c4d10e332bf0e85233d96f1b59840c54f7679e34abe4b8018b170357170b1bff6e1e38f4f697e6
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img alt="Omarchy UI 0.0.
|
|
13
|
+
<img alt="Omarchy UI 0.0.10" src="https://img.shields.io/badge/Omarchy_UI-0.0.10-b7ff5a?style=flat-square&labelColor=111711">
|
|
14
14
|
<img alt="Ruby 3.1 or newer" src="https://img.shields.io/badge/Ruby-3.1%2B-cc342d?style=flat-square&logo=ruby&logoColor=white">
|
|
15
15
|
<img alt="Zui 0.0.10" src="https://img.shields.io/badge/Zui-0.0.10-7ee14b?style=flat-square&labelColor=111711">
|
|
16
16
|
<img alt="241 components" src="https://img.shields.io/badge/components-241-75d943?style=flat-square&labelColor=111711">
|
|
@@ -294,6 +294,19 @@ omarchy_ui push
|
|
|
294
294
|
`validate` generates and checks the plugin without modifying the Ruby project. `push` builds,
|
|
295
295
|
validates, installs, enables, and reloads it for development.
|
|
296
296
|
|
|
297
|
+
To publish, target a separate public distribution repository. Omarchy UI rebuilds the thin package,
|
|
298
|
+
enforces the marketplace contract, pushes it, and prints the complete submission for review:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
omarchy_ui publish --repo YOUR_GITHUB_NAME/PLUGIN_REPOSITORY \
|
|
302
|
+
--category System --tags system,quickshell --create
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
After reviewing the printed title, metadata, and five marketplace attestations, rerun the same
|
|
306
|
+
command with `--submit`. It creates a submission or refreshes an existing open one. This two-step
|
|
307
|
+
flow prevents accidental replacement of the source repository and ensures the ownership statement
|
|
308
|
+
is reviewed before it is posted.
|
|
309
|
+
|
|
297
310
|
During development, suppress either lifecycle action when needed:
|
|
298
311
|
|
|
299
312
|
```bash
|
|
@@ -310,9 +323,10 @@ omarchy_ui <command> [arguments]
|
|
|
310
323
|
| Command | Arguments | Result |
|
|
311
324
|
| --- | --- | --- |
|
|
312
325
|
| `omarchy_ui new NAME` | One project name | Creates an Omarchy application project |
|
|
313
|
-
| `omarchy_ui new NAME --plugin` | One project name
|
|
326
|
+
| `omarchy_ui new NAME --plugin --github USER` | One project name, `--plugin`, and optional GitHub user | Creates a marketplace-ready Omarchy plugin project |
|
|
314
327
|
| `omarchy_ui run FILE` | One Ruby entry point | Runs an application through the Omarchy/Quickshell host |
|
|
315
328
|
| `omarchy_ui bundle [DIRECTORY]` | Zero or one project directory | Generates the distributable package and prints its location |
|
|
329
|
+
| `omarchy_ui publish [DIRECTORY]` | Required `--repo`, `--category`, `--tags`; optional `--create`, `--submit` | Builds and pushes a thin distribution repository, then previews or submits its marketplace issue |
|
|
316
330
|
| `omarchy_ui validate [DIRECTORY]` | Zero or one plugin directory | Stages the plugin and delegates validation to `omarchy plugin validate` |
|
|
317
331
|
| `omarchy_ui push [DIRECTORY]` | Optional `--no-enable` and `--no-restart` | Atomically installs and activates a validated plugin |
|
|
318
332
|
| `omarchy_ui version` | None | Prints the installed Omarchy UI version |
|
|
@@ -328,20 +342,20 @@ Create the distributable package from the project root:
|
|
|
328
342
|
omarchy_ui bundle
|
|
329
343
|
```
|
|
330
344
|
|
|
331
|
-
Omarchy UI assembles
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Current Omarchy
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
their target Omarchy release.
|
|
345
|
+
Omarchy UI assembles one bundled Ruby program and required assets, native runtime, manifest, and only
|
|
346
|
+
the Zui components used by the project. It AOT-compiles the generated QML into a content-addressed Qt
|
|
347
|
+
module, then discards the generated component, control, theme, and font source tree. Applications
|
|
348
|
+
receive a direct launcher; plugins must pass Omarchy validation before the command succeeds. Omarchy
|
|
349
|
+
UI pins Zui 0.0.10 so the compiled UI matches the Zui core embedded in the deterministic mruby
|
|
350
|
+
runtime.
|
|
351
|
+
|
|
352
|
+
Current Omarchy entry points resolve by filename, so a package retains only the required generated
|
|
353
|
+
loader shims. Applications keep `App.qml`; standard plugins keep `Service.qml`, `Panel.qml`, and
|
|
354
|
+
`BarWidget.qml`. Each shim contains only an import and a compiled type instance. The application
|
|
355
|
+
interface is authored in Ruby and stored in native `.so` artifacts. Distributable packages do not
|
|
356
|
+
include generated QML source, build reports, audit/provenance folders, checksums, tests, or CI files.
|
|
357
|
+
They also omit `config.rb` and Ruby files already folded into `main.rb`.
|
|
358
|
+
Compiled packages should be built against the Qt version shipped by their target Omarchy release.
|
|
345
359
|
|
|
346
360
|
The source `config.rb` is the build configuration. Omarchy UI generates plugin manifests from it;
|
|
347
361
|
developers never maintain a separate manifest.
|
|
@@ -416,7 +430,7 @@ Omarchy process bridge ───────────────── embed
|
|
|
416
430
|
▼
|
|
417
431
|
Tree-shaken Zui host + component catalog
|
|
418
432
|
│
|
|
419
|
-
│ bundle: Qt AOT module +
|
|
433
|
+
│ bundle: Qt AOT module + required loader shims
|
|
420
434
|
│
|
|
421
435
|
▼
|
|
422
436
|
App window · Omarchy panel · Omarchy bar widget
|
data/Service.qml
CHANGED
|
@@ -499,4 +499,27 @@ Item {
|
|
|
499
499
|
repeat: false
|
|
500
500
|
onTriggered: root.startRuby()
|
|
501
501
|
}
|
|
502
|
+
|
|
503
|
+
// A desktop surface is rendered by the same long-lived service that owns
|
|
504
|
+
// the Ruby runtime. Its DesktopStage adapter creates the layer-shell window;
|
|
505
|
+
// plugins keep all scene structure, state, and interaction in Ruby.
|
|
506
|
+
ControlNode {
|
|
507
|
+
id: desktopRenderer
|
|
508
|
+
visible: root.ready && root.rootId("desktop") !== ""
|
|
509
|
+
bridge: root
|
|
510
|
+
surfaceName: "desktop"
|
|
511
|
+
controlId: root.rootId(surfaceName)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
IpcHandler {
|
|
515
|
+
target: root.manifest && root.manifest.id ? String(root.manifest.id) : "omarchy-ui"
|
|
516
|
+
|
|
517
|
+
function playPause(): string {
|
|
518
|
+
var desktopId = root.rootId("desktop")
|
|
519
|
+
if (desktopId === "") return "not-ready"
|
|
520
|
+
return root.sendEvent("desktop", desktopId, "shortcut", { name: "play_pause" })
|
|
521
|
+
? "ok"
|
|
522
|
+
: "not-ready"
|
|
523
|
+
}
|
|
524
|
+
}
|
|
502
525
|
}
|
data/lib/omarchy_ui/cli.rb
CHANGED
|
@@ -23,11 +23,12 @@ module OmarchyUI
|
|
|
23
23
|
when "run" then run_file(arguments)
|
|
24
24
|
when "new" then new_project(arguments)
|
|
25
25
|
when "bundle" then bundle_project(arguments)
|
|
26
|
+
when "publish" then publish(arguments)
|
|
26
27
|
when "push" then push(arguments)
|
|
27
28
|
when "validate" then validate(arguments)
|
|
28
29
|
when "version", "--version", "-v" then @out.puts(OmarchyUI::VERSION); 0
|
|
29
30
|
else
|
|
30
|
-
@err.puts("Usage: omarchy_ui <new NAME [--plugin]|run FILE|bundle [DIRECTORY]|push [DIRECTORY]|validate [DIRECTORY]|version>")
|
|
31
|
+
@err.puts("Usage: omarchy_ui <new NAME [--plugin] [--github USER]|run FILE|bundle [DIRECTORY]|publish [DIRECTORY] --repo OWNER/REPO --category CATEGORY --tags TAGS [--create] [--submit]|push [DIRECTORY]|validate [DIRECTORY]|version>")
|
|
31
32
|
command.nil? ? 0 : 64
|
|
32
33
|
end
|
|
33
34
|
rescue Interrupt
|
|
@@ -68,11 +69,14 @@ module OmarchyUI
|
|
|
68
69
|
|
|
69
70
|
def new_project(arguments)
|
|
70
71
|
plugin = !arguments.delete("--plugin").nil?
|
|
72
|
+
github_user = extract_option!(arguments, "--github")
|
|
71
73
|
name = arguments.shift || raise(ArgumentError, "new requires a project name")
|
|
72
|
-
raise ArgumentError, "new accepts a name and
|
|
74
|
+
raise ArgumentError, "new accepts a name, --plugin, and --github USER" unless arguments.empty?
|
|
75
|
+
raise ArgumentError, "--github is only valid with --plugin" if github_user && !plugin
|
|
73
76
|
destination = File.expand_path(slug(name))
|
|
74
77
|
kind = plugin ? :plugin : :application
|
|
75
|
-
|
|
78
|
+
github_user ||= default_github_user if plugin
|
|
79
|
+
Generator.new(path: destination, name: name, kind:, author: default_author, github_user:).create
|
|
76
80
|
label = plugin ? "Omarchy-compliant plugin" : "self-contained Omarchy application"
|
|
77
81
|
@out.puts("Created #{label} in #{destination}")
|
|
78
82
|
0
|
|
@@ -91,6 +95,30 @@ module OmarchyUI
|
|
|
91
95
|
"Omarchy UI Developer"
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
def default_github_user
|
|
99
|
+
configured = ENV["OMARCHY_UI_GITHUB_USER"].to_s.strip
|
|
100
|
+
return configured unless configured.empty?
|
|
101
|
+
|
|
102
|
+
output, _error, status = Open3.capture3("gh", "api", "user", "--jq", ".login")
|
|
103
|
+
login = output.to_s.strip
|
|
104
|
+
return login if status.success? && !login.empty?
|
|
105
|
+
|
|
106
|
+
raise ArgumentError,
|
|
107
|
+
"plugin generation needs --github USER or OMARCHY_UI_GITHUB_USER (gh auth login can provide it automatically)"
|
|
108
|
+
rescue Errno::ENOENT
|
|
109
|
+
raise ArgumentError, "plugin generation needs --github USER or OMARCHY_UI_GITHUB_USER"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_option!(arguments, name)
|
|
113
|
+
index = arguments.index(name)
|
|
114
|
+
return nil unless index
|
|
115
|
+
|
|
116
|
+
value = arguments[index + 1]
|
|
117
|
+
raise ArgumentError, "#{name} requires a value" if value.nil? || value.start_with?("--")
|
|
118
|
+
arguments.slice!(index, 2)
|
|
119
|
+
value
|
|
120
|
+
end
|
|
121
|
+
|
|
94
122
|
def slug(value)
|
|
95
123
|
result = value.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|\-\z/, "")
|
|
96
124
|
raise ArgumentError, "name must contain letters or numbers" if result.empty?
|
|
@@ -123,10 +151,12 @@ module OmarchyUI
|
|
|
123
151
|
unless precompiled
|
|
124
152
|
config.write_manifest(destination) if config&.plugin?
|
|
125
153
|
bundle_ruby_entrypoint(source, destination, entrypoint:)
|
|
126
|
-
Runtime.install_package(destination, compiled:
|
|
154
|
+
Runtime.install_package(destination, compiled: true)
|
|
127
155
|
end
|
|
156
|
+
prune_bundled_ruby_sources(destination, entrypoint:)
|
|
128
157
|
plugin = config ? config.plugin? : File.file?(File.join(destination, "manifest.json"))
|
|
129
158
|
if plugin
|
|
159
|
+
PluginPackage.validate!(destination)
|
|
130
160
|
raise ArgumentError, "bundled plugin validation failed" unless system("omarchy", "plugin", "validate", destination)
|
|
131
161
|
@out.puts("Bundled plugin in #{destination}")
|
|
132
162
|
return 0
|
|
@@ -161,23 +191,56 @@ module OmarchyUI
|
|
|
161
191
|
end
|
|
162
192
|
|
|
163
193
|
def application_entries(source, config: ProjectConfig.load(source))
|
|
164
|
-
generated = Runtime::GENERATED_ENTRIES +
|
|
194
|
+
generated = Runtime::GENERATED_ENTRIES + %w[run Commons Ui]
|
|
165
195
|
generated.concat([ProjectConfig::FILE_NAME, "manifest.json"]) if config
|
|
166
|
-
Dir.children(source).reject
|
|
196
|
+
Dir.children(source).reject do |entry|
|
|
197
|
+
(PluginPackage::DEVELOPMENT_ENTRIES + PluginPackage::SOURCE_CONTROL_ENTRIES).include?(entry) ||
|
|
198
|
+
generated.include?(entry)
|
|
199
|
+
end
|
|
167
200
|
end
|
|
168
201
|
|
|
169
202
|
def package_entries(source)
|
|
170
|
-
|
|
203
|
+
excluded = PluginPackage::DEVELOPMENT_ENTRIES + PluginPackage::SOURCE_CONTROL_ENTRIES +
|
|
204
|
+
Runtime::LEGACY_METADATA_FILES
|
|
205
|
+
Dir.children(source).reject { |entry| excluded.include?(entry) }
|
|
171
206
|
end
|
|
172
207
|
|
|
173
208
|
def compiled_package?(source)
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
compiled_root = File.join(source, QmlCompiler::COMPILED_ROOT, "Bundles")
|
|
210
|
+
shims = QmlCompiler::ENTRY_TYPES.keys.any? { |entry| File.file?(File.join(source, entry)) }
|
|
211
|
+
return false unless File.directory?(compiled_root) && shims
|
|
176
212
|
|
|
177
213
|
QmlCompiler.verify!(source)
|
|
178
214
|
true
|
|
179
215
|
end
|
|
180
216
|
|
|
217
|
+
def publish(arguments)
|
|
218
|
+
repository = extract_option!(arguments, "--repo")
|
|
219
|
+
category = extract_option!(arguments, "--category")
|
|
220
|
+
tags = extract_option!(arguments, "--tags").to_s.split(",")
|
|
221
|
+
create = !arguments.delete("--create").nil?
|
|
222
|
+
submit = !arguments.delete("--submit").nil?
|
|
223
|
+
source = File.expand_path(arguments.shift || Dir.pwd)
|
|
224
|
+
raise ArgumentError, "publish accepts one directory and publish options" unless arguments.empty?
|
|
225
|
+
config = ProjectConfig.load(source)
|
|
226
|
+
raise ArgumentError, "publish requires an Omarchy UI plugin project" unless config&.plugin?
|
|
227
|
+
|
|
228
|
+
with_staged_project(source) do |staging|
|
|
229
|
+
raise ArgumentError, "staged plugin validation failed" unless system("omarchy", "plugin", "validate", staging)
|
|
230
|
+
PluginPublisher.publish!(
|
|
231
|
+
package: staging,
|
|
232
|
+
source:,
|
|
233
|
+
repository:,
|
|
234
|
+
category:,
|
|
235
|
+
tags:,
|
|
236
|
+
create:,
|
|
237
|
+
submit:,
|
|
238
|
+
out: @out
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
0
|
|
242
|
+
end
|
|
243
|
+
|
|
181
244
|
def push(arguments)
|
|
182
245
|
enable = !arguments.delete("--no-enable")
|
|
183
246
|
restart = !arguments.delete("--no-restart")
|
|
@@ -239,9 +302,11 @@ module OmarchyUI
|
|
|
239
302
|
entrypoint = config&.entrypoint || "main.rb"
|
|
240
303
|
if File.file?(File.join(staging, entrypoint))
|
|
241
304
|
bundle_ruby_entrypoint(source, staging, entrypoint:)
|
|
242
|
-
Runtime.install_package(staging, compiled:
|
|
305
|
+
Runtime.install_package(staging, compiled: true)
|
|
243
306
|
end
|
|
244
307
|
end
|
|
308
|
+
prune_bundled_ruby_sources(staging, entrypoint: "main.rb") if File.file?(File.join(staging, "manifest.json"))
|
|
309
|
+
PluginPackage.validate!(staging) if File.file?(File.join(staging, "manifest.json"))
|
|
245
310
|
staging
|
|
246
311
|
rescue StandardError
|
|
247
312
|
FileUtils.remove_entry(staging) if staging && File.exist?(staging)
|
|
@@ -255,6 +320,17 @@ module OmarchyUI
|
|
|
255
320
|
File.write(destination_entrypoint, SourceBundle.new(source_entrypoint, root: source).call)
|
|
256
321
|
end
|
|
257
322
|
|
|
323
|
+
def prune_bundled_ruby_sources(destination, entrypoint: "main.rb")
|
|
324
|
+
kept = File.expand_path(entrypoint, destination)
|
|
325
|
+
Dir.glob(File.join(destination, "**", "*.rb"), File::FNM_DOTMATCH).each do |path|
|
|
326
|
+
FileUtils.rm_f(path) unless File.expand_path(path) == kept
|
|
327
|
+
end
|
|
328
|
+
directories = Dir.glob(File.join(destination, "**", "*"), File::FNM_DOTMATCH)
|
|
329
|
+
.select { |path| File.directory?(path) && !File.symlink?(path) }
|
|
330
|
+
.sort_by { |path| -path.count(File::SEPARATOR) }
|
|
331
|
+
directories.each { |path| Dir.rmdir(path) if Dir.empty?(path) }
|
|
332
|
+
end
|
|
333
|
+
|
|
258
334
|
def activate_plugin(plugin_id)
|
|
259
335
|
return if system("omarchy", "plugin", "enable", plugin_id)
|
|
260
336
|
system("omarchy-shell", "shell", "rescanPlugins")
|
data/lib/omarchy_ui/generator.rb
CHANGED
|
@@ -5,12 +5,17 @@ require "fileutils"
|
|
|
5
5
|
module OmarchyUI
|
|
6
6
|
class Generator
|
|
7
7
|
PROJECT_KINDS = %i[application plugin].freeze
|
|
8
|
+
GITHUB_USER = /\A(?!-)[A-Za-z0-9-]{1,39}(?<!-)\z/
|
|
8
9
|
|
|
9
|
-
def initialize(path:, name: nil, kind: :application, plugin_id: nil, author: nil)
|
|
10
|
+
def initialize(path:, name: nil, kind: :application, plugin_id: nil, author: nil, github_user: nil)
|
|
10
11
|
@path = File.expand_path(path)
|
|
11
12
|
@name = name || File.basename(@path).split(/[-_]/).map(&:capitalize).join(" ")
|
|
12
13
|
@kind = kind.to_sym
|
|
13
|
-
@
|
|
14
|
+
@github_user = github_user.to_s.strip
|
|
15
|
+
if plugin? && !GITHUB_USER.match?(@github_user)
|
|
16
|
+
raise ArgumentError, "plugin generation needs a valid GitHub username"
|
|
17
|
+
end
|
|
18
|
+
@plugin_id = plugin_id || "io.github.#{@github_user.downcase}.#{project_slug}"
|
|
14
19
|
@author = author.to_s.strip.empty? ? "Omarchy UI Developer" : author.to_s.strip
|
|
15
20
|
raise ArgumentError, "unsupported project kind: #{@kind}" unless PROJECT_KINDS.include?(@kind)
|
|
16
21
|
end
|
|
@@ -124,6 +129,9 @@ module OmarchyUI
|
|
|
124
129
|
|
|
125
130
|
A pure Ruby desktop application built with Zui and the Omarchy UI host.
|
|
126
131
|
|
|
132
|
+
The interface and behavior are authored entirely in Ruby. Omarchy UI generates QML only
|
|
133
|
+
as a temporary build input, compiles it, and ships no handwritten QML source.
|
|
134
|
+
|
|
127
135
|
## Run
|
|
128
136
|
|
|
129
137
|
```bash
|
|
@@ -140,7 +148,9 @@ module OmarchyUI
|
|
|
140
148
|
```
|
|
141
149
|
|
|
142
150
|
Omarchy UI tree-shakes and compiles the UI, then generates the complete self-contained
|
|
143
|
-
application package required by Omarchy.
|
|
151
|
+
application package required by Omarchy. The package contains the bundled Ruby program,
|
|
152
|
+
native runtime, compiled Qt module, and one generated `App.qml` loader shim—no build reports
|
|
153
|
+
or generated QML source tree.
|
|
144
154
|
MARKDOWN
|
|
145
155
|
end
|
|
146
156
|
|
|
@@ -150,6 +160,26 @@ module OmarchyUI
|
|
|
150
160
|
|
|
151
161
|
A pure Ruby Omarchy plugin built with Omarchy UI and Zui.
|
|
152
162
|
|
|
163
|
+
The interface and behavior are authored entirely in Ruby. Omarchy UI generates QML only
|
|
164
|
+
as a temporary build input, compiles it, and ships no handwritten QML source.
|
|
165
|
+
|
|
166
|
+
## Install
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
omarchy plugin add https://github.com/#{@github_user}/#{project_slug}.git --enable
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Remove
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
omarchy plugin remove #{@plugin_id}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Dependencies
|
|
179
|
+
|
|
180
|
+
- Omarchy Quattro on x86-64 Linux
|
|
181
|
+
- No additional runtime dependency in the generated starter
|
|
182
|
+
|
|
153
183
|
## Validate
|
|
154
184
|
|
|
155
185
|
```bash
|
|
@@ -171,6 +201,9 @@ module OmarchyUI
|
|
|
171
201
|
Write the plugin interface and behavior in Ruby under `main.rb` and `components/`.
|
|
172
202
|
Public plugin identity and release metadata live in `config.rb`. Omarchy UI tree-shakes and
|
|
173
203
|
compiles the UI, then generates and validates the complete Omarchy-compatible plugin package.
|
|
204
|
+
The package contains the bundled Ruby program, native runtime, compiled Qt module, manifest,
|
|
205
|
+
and only the generated loader shims named by that manifest—no build reports, audit folder,
|
|
206
|
+
tests, or generated QML source tree.
|
|
174
207
|
MARKDOWN
|
|
175
208
|
end
|
|
176
209
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module OmarchyUI
|
|
6
|
+
class PluginPackage
|
|
7
|
+
DEVELOPMENT_ENTRIES = %w[
|
|
8
|
+
.github .gitattributes .gitignore audit build config.rb dist docs qml-source spec test tests vendor
|
|
9
|
+
].freeze
|
|
10
|
+
SOURCE_CONTROL_ENTRIES = %w[.git].freeze
|
|
11
|
+
README_PATTERN = /\Areadme(?:\.[^.]+)?\z/i
|
|
12
|
+
LICENSE_PATTERN = /\A(?:license|copying)(?:\.[^.]+)?\z/i
|
|
13
|
+
REQUIRED_MANIFEST_STRINGS = %w[id name version author license description].freeze
|
|
14
|
+
|
|
15
|
+
def self.validate!(path)
|
|
16
|
+
new(path).validate!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(path)
|
|
20
|
+
@path = File.expand_path(path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate!
|
|
24
|
+
validate_root_files!
|
|
25
|
+
manifest = read_manifest
|
|
26
|
+
validate_manifest!(manifest)
|
|
27
|
+
validate_readme!(manifest)
|
|
28
|
+
validate_runtime!
|
|
29
|
+
validate_ruby_surface!
|
|
30
|
+
validate_qml_surface!(manifest)
|
|
31
|
+
QmlCompiler.verify!(@path)
|
|
32
|
+
manifest
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def validate_root_files!
|
|
38
|
+
entries = Dir.children(@path)
|
|
39
|
+
forbidden = entries & (DEVELOPMENT_ENTRIES + Runtime::LEGACY_METADATA_FILES)
|
|
40
|
+
unless forbidden.empty?
|
|
41
|
+
raise ArgumentError, "plugin package contains development-only entries: #{forbidden.sort.join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
manifests = Dir.glob(File.join(@path, "**", "manifest.json"), File::FNM_DOTMATCH)
|
|
45
|
+
.select { |path| File.file?(path) }
|
|
46
|
+
unless manifests == [File.join(@path, "manifest.json")]
|
|
47
|
+
raise ArgumentError, "plugin package must contain exactly one root manifest.json"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
readmes = entries.grep(README_PATTERN).select { |entry| File.file?(File.join(@path, entry)) }
|
|
51
|
+
raise ArgumentError, "plugin package needs one root README" unless readmes.length == 1
|
|
52
|
+
|
|
53
|
+
licenses = entries.grep(LICENSE_PATTERN).select { |entry| File.file?(File.join(@path, entry)) }
|
|
54
|
+
raise ArgumentError, "plugin package needs one root license file" unless licenses.length == 1
|
|
55
|
+
|
|
56
|
+
symlinks = Dir.glob(File.join(@path, "**", "*"), File::FNM_DOTMATCH)
|
|
57
|
+
.select { |path| File.symlink?(path) }
|
|
58
|
+
unless symlinks.empty?
|
|
59
|
+
raise ArgumentError, "plugin package may not contain symbolic links"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_manifest
|
|
64
|
+
JSON.parse(File.read(File.join(@path, "manifest.json")))
|
|
65
|
+
rescue JSON::ParserError => error
|
|
66
|
+
raise ArgumentError, "invalid plugin manifest: #{error.message}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_manifest!(manifest)
|
|
70
|
+
unless manifest.is_a?(Hash) && manifest["schemaVersion"] == 1
|
|
71
|
+
raise ArgumentError, "plugin manifest must use schemaVersion 1"
|
|
72
|
+
end
|
|
73
|
+
REQUIRED_MANIFEST_STRINGS.each do |field|
|
|
74
|
+
value = manifest[field]
|
|
75
|
+
unless value.is_a?(String) && !value.strip.empty?
|
|
76
|
+
raise ArgumentError, "plugin manifest needs a non-empty #{field}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
plugin_id = manifest.fetch("id")
|
|
80
|
+
raise ArgumentError, "invalid plugin id" unless VALID_ID.match?(plugin_id)
|
|
81
|
+
if plugin_id.start_with?("omarchy.")
|
|
82
|
+
raise ArgumentError, "plugin id uses the reserved omarchy.* namespace"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
entry_points = manifest["entryPoints"]
|
|
86
|
+
unless entry_points.is_a?(Hash) && !entry_points.empty?
|
|
87
|
+
raise ArgumentError, "plugin manifest needs entryPoints"
|
|
88
|
+
end
|
|
89
|
+
files = entry_points.values
|
|
90
|
+
unless files.all? { |file| file.is_a?(String) } && files.uniq.length == files.length
|
|
91
|
+
raise ArgumentError, "plugin manifest entryPoints must be unique file names"
|
|
92
|
+
end
|
|
93
|
+
unsupported = files - QmlCompiler::ENTRY_TYPES.keys
|
|
94
|
+
unless unsupported.empty?
|
|
95
|
+
raise ArgumentError, "unsupported plugin entryPoints: #{unsupported.join(', ')}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_readme!(manifest)
|
|
100
|
+
name = Dir.children(@path).find { |entry| README_PATTERN.match?(entry) }
|
|
101
|
+
readme = File.read(File.join(@path, name))
|
|
102
|
+
raise ArgumentError, "plugin README needs installation instructions" unless readme.match?(/install/i)
|
|
103
|
+
unless readme.match?(/(?:remove|uninstall)/i)
|
|
104
|
+
raise ArgumentError, "plugin README needs removal instructions"
|
|
105
|
+
end
|
|
106
|
+
unless readme.include?("omarchy plugin add")
|
|
107
|
+
raise ArgumentError, "plugin README needs an omarchy plugin add command"
|
|
108
|
+
end
|
|
109
|
+
unless readme.include?("omarchy plugin remove #{manifest.fetch('id')}")
|
|
110
|
+
raise ArgumentError, "plugin README removal command must use the manifest id"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_runtime!
|
|
115
|
+
runtime = File.join(@path, "omarchy-ui-runtime")
|
|
116
|
+
unless File.file?(runtime) && File.executable?(runtime) && !File.symlink?(runtime)
|
|
117
|
+
raise ArgumentError, "plugin package needs an executable omarchy-ui-runtime"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate_ruby_surface!
|
|
122
|
+
entrypoint = File.join(@path, "main.rb")
|
|
123
|
+
unless File.file?(entrypoint) && !File.symlink?(entrypoint)
|
|
124
|
+
raise ArgumentError, "plugin package needs one bundled main.rb"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
ruby_files = Dir.glob(File.join(@path, "**", "*.rb"), File::FNM_DOTMATCH)
|
|
128
|
+
.select { |path| File.file?(path) }
|
|
129
|
+
.map { |path| path.delete_prefix("#{@path}/") }
|
|
130
|
+
.sort
|
|
131
|
+
unless ruby_files == ["main.rb"]
|
|
132
|
+
raise ArgumentError, "plugin package may contain only the bundled main.rb Ruby program"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_qml_surface!(manifest)
|
|
137
|
+
expected = manifest.fetch("entryPoints").values.sort
|
|
138
|
+
qml_files = Dir.glob(File.join(@path, "**", "*.qml"), File::FNM_DOTMATCH)
|
|
139
|
+
.select { |path| File.file?(path) }
|
|
140
|
+
.map { |path| path.delete_prefix("#{@path}/") }
|
|
141
|
+
.sort
|
|
142
|
+
unless qml_files == expected
|
|
143
|
+
raise ArgumentError, "plugin package may contain only manifest-declared generated QML shims"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
module OmarchyUI
|
|
9
|
+
class PluginPublisher
|
|
10
|
+
MARKETPLACE_REPOSITORY = "omacom/omarchy-plugin-marketplace"
|
|
11
|
+
CATEGORIES = [
|
|
12
|
+
"Appearance", "Desktop", "Developer Tools", "Hardware", "Kids", "Productivity", "System", "Widgets", "Other"
|
|
13
|
+
].freeze
|
|
14
|
+
TAGS = %w[
|
|
15
|
+
ai bar education games hyprland kids launcher media power-management quickshell security system workspaces
|
|
16
|
+
].freeze
|
|
17
|
+
REPOSITORY = /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/
|
|
18
|
+
|
|
19
|
+
def self.publish!(**arguments)
|
|
20
|
+
new(**arguments).publish!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(package:, source:, repository:, category:, tags:, create: false, submit: false,
|
|
24
|
+
marketplace_repository: ENV.fetch("OMARCHY_UI_MARKETPLACE_REPOSITORY", MARKETPLACE_REPOSITORY),
|
|
25
|
+
out: $stdout)
|
|
26
|
+
@package = File.expand_path(package)
|
|
27
|
+
@source = File.expand_path(source)
|
|
28
|
+
@repository = repository.to_s.strip
|
|
29
|
+
@category = category.to_s.strip
|
|
30
|
+
@tags = Array(tags).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
31
|
+
@create = create
|
|
32
|
+
@submit = submit
|
|
33
|
+
@marketplace_repository = marketplace_repository.to_s.strip
|
|
34
|
+
@out = out
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def publish!
|
|
38
|
+
validate_options!
|
|
39
|
+
manifest = PluginPackage.validate!(@package)
|
|
40
|
+
validate_install_url!
|
|
41
|
+
ensure_distinct_repository!
|
|
42
|
+
ensure_gh_authenticated!
|
|
43
|
+
ensure_distribution_repository!(manifest)
|
|
44
|
+
commit = publish_package!(manifest)
|
|
45
|
+
title = "[Plugin]: #{manifest.fetch('name')}"
|
|
46
|
+
body = submission_body(commit:)
|
|
47
|
+
show_submission(title, body)
|
|
48
|
+
if @submit
|
|
49
|
+
issue_url = submit_to_marketplace!(title, body)
|
|
50
|
+
@out.puts("Marketplace submission: #{issue_url}")
|
|
51
|
+
else
|
|
52
|
+
@out.puts("Review the submission above, then rerun with --submit to create or refresh it.")
|
|
53
|
+
end
|
|
54
|
+
@out.puts("Published #{@repository} at #{commit}")
|
|
55
|
+
commit
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_options!
|
|
61
|
+
raise ArgumentError, "publish requires --repo OWNER/REPOSITORY" unless REPOSITORY.match?(@repository)
|
|
62
|
+
raise ArgumentError, "invalid marketplace repository" unless REPOSITORY.match?(@marketplace_repository)
|
|
63
|
+
unless CATEGORIES.include?(@category)
|
|
64
|
+
raise ArgumentError, "publish category must be one of: #{CATEGORIES.join(', ')}"
|
|
65
|
+
end
|
|
66
|
+
unless (1..3).cover?(@tags.length) && (@tags - TAGS).empty?
|
|
67
|
+
raise ArgumentError, "publish needs one to three allowed tags: #{TAGS.join(', ')}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ensure_distinct_repository!
|
|
72
|
+
output, _error, status = capture("git", "-C", @source, "remote", "get-url", "origin")
|
|
73
|
+
return unless status.success?
|
|
74
|
+
|
|
75
|
+
source_repository = github_repository(output.strip)
|
|
76
|
+
if source_repository&.casecmp?(@repository)
|
|
77
|
+
raise ArgumentError,
|
|
78
|
+
"publish target must be a separate distribution repository; refusing to replace the source repository"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_install_url!
|
|
83
|
+
readme_name = Dir.children(@package).find { |entry| PluginPackage::README_PATTERN.match?(entry) }
|
|
84
|
+
readme = File.read(File.join(@package, readme_name))
|
|
85
|
+
repository_url = "https://github.com/#{@repository}"
|
|
86
|
+
return if readme.downcase.include?(repository_url.downcase)
|
|
87
|
+
|
|
88
|
+
raise ArgumentError,
|
|
89
|
+
"plugin README install command must use the distribution repository #{repository_url}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ensure_gh_authenticated!
|
|
93
|
+
_output, error, status = capture("gh", "auth", "status")
|
|
94
|
+
return if status.success?
|
|
95
|
+
|
|
96
|
+
detail = error.strip.lines.last.to_s.strip
|
|
97
|
+
raise ArgumentError, "GitHub CLI authentication is required#{detail.empty? ? '' : ": #{detail}"}"
|
|
98
|
+
rescue Errno::ENOENT
|
|
99
|
+
raise ArgumentError, "publish requires the GitHub CLI (gh)"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ensure_distribution_repository!(manifest)
|
|
103
|
+
output, error, status = capture(
|
|
104
|
+
"gh", "repo", "view", @repository,
|
|
105
|
+
"--json", "nameWithOwner,visibility", "--jq", "[.nameWithOwner,.visibility] | @tsv"
|
|
106
|
+
)
|
|
107
|
+
unless status.success?
|
|
108
|
+
unless @create
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"distribution repository #{@repository} was not found; create it first or rerun with --create"
|
|
111
|
+
end
|
|
112
|
+
run!(
|
|
113
|
+
"gh", "repo", "create", @repository, "--public",
|
|
114
|
+
"--description", "#{manifest.fetch('name')} — an Omarchy plugin built entirely in Ruby"
|
|
115
|
+
)
|
|
116
|
+
output, error, status = capture(
|
|
117
|
+
"gh", "repo", "view", @repository,
|
|
118
|
+
"--json", "nameWithOwner,visibility", "--jq", "[.nameWithOwner,.visibility] | @tsv"
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
unless status.success?
|
|
122
|
+
raise ArgumentError, "could not inspect distribution repository: #{error.strip}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
_name, visibility = output.strip.split("\t", 2)
|
|
126
|
+
raise ArgumentError, "distribution repository must be public" unless visibility == "PUBLIC"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def publish_package!(manifest)
|
|
130
|
+
Dir.mktmpdir("omarchy-ui-publish-") do |temporary|
|
|
131
|
+
checkout = File.join(temporary, "repository")
|
|
132
|
+
run!("gh", "repo", "clone", @repository, checkout, "--", "--quiet")
|
|
133
|
+
guard_existing_package!(checkout, manifest)
|
|
134
|
+
replace_checkout!(checkout)
|
|
135
|
+
run!("git", "-C", checkout, "add", "-A")
|
|
136
|
+
_output, _error, clean = capture("git", "-C", checkout, "diff", "--cached", "--quiet")
|
|
137
|
+
unless clean.success?
|
|
138
|
+
run!(
|
|
139
|
+
"git", "-C", checkout, "commit", "-m",
|
|
140
|
+
"Publish #{manifest.fetch('name')} #{manifest.fetch('version')}"
|
|
141
|
+
)
|
|
142
|
+
run!("git", "-C", checkout, "push", "origin", "HEAD")
|
|
143
|
+
end
|
|
144
|
+
output, _error, _status = capture("git", "-C", checkout, "rev-parse", "HEAD")
|
|
145
|
+
output.strip
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def guard_existing_package!(checkout, manifest)
|
|
150
|
+
entries = Dir.children(checkout) - [".git"]
|
|
151
|
+
return if entries.empty?
|
|
152
|
+
|
|
153
|
+
existing_manifest = File.join(checkout, "manifest.json")
|
|
154
|
+
unless File.file?(existing_manifest)
|
|
155
|
+
raise ArgumentError, "distribution repository is not empty and has no root manifest.json"
|
|
156
|
+
end
|
|
157
|
+
existing_id = JSON.parse(File.read(existing_manifest)).fetch("id")
|
|
158
|
+
return if existing_id == manifest.fetch("id")
|
|
159
|
+
|
|
160
|
+
raise ArgumentError,
|
|
161
|
+
"distribution repository contains plugin #{existing_id.inspect}, not #{manifest.fetch('id').inspect}"
|
|
162
|
+
rescue JSON::ParserError, KeyError => error
|
|
163
|
+
raise ArgumentError, "distribution repository has an invalid manifest: #{error.message}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def replace_checkout!(checkout)
|
|
167
|
+
(Dir.children(checkout) - [".git"]).each do |entry|
|
|
168
|
+
FileUtils.remove_entry(File.join(checkout, entry))
|
|
169
|
+
end
|
|
170
|
+
Dir.children(@package).each do |entry|
|
|
171
|
+
FileUtils.cp_r(File.join(@package, entry), File.join(checkout, entry), preserve: true)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def submission_body(commit:)
|
|
176
|
+
<<~MARKDOWN
|
|
177
|
+
### Repository URL
|
|
178
|
+
|
|
179
|
+
https://github.com/#{@repository}
|
|
180
|
+
|
|
181
|
+
### Category
|
|
182
|
+
|
|
183
|
+
#{@category}
|
|
184
|
+
|
|
185
|
+
### Tags
|
|
186
|
+
|
|
187
|
+
#{@tags.join(', ')}
|
|
188
|
+
|
|
189
|
+
### Suggest a missing tag
|
|
190
|
+
|
|
191
|
+
_No response_
|
|
192
|
+
|
|
193
|
+
### Maintainer notes
|
|
194
|
+
|
|
195
|
+
Built and published by Omarchy UI as a thin Ruby-authored package. Current review target: `#{commit}`.
|
|
196
|
+
|
|
197
|
+
### Submission checklist
|
|
198
|
+
|
|
199
|
+
- [x] The repository is public and contains installation and removal instructions.
|
|
200
|
+
- [x] I have documented the plugin license and any external dependencies.
|
|
201
|
+
- [x] I confirm that I own or have permission to submit this plugin and its preview assets.
|
|
202
|
+
- [x] The plugin does not overwrite user configuration without explicit consent.
|
|
203
|
+
- [x] I understand that approval is for listing and is not a security review.
|
|
204
|
+
MARKDOWN
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def show_submission(title, body)
|
|
208
|
+
@out.puts("Marketplace issue preview (#{@marketplace_repository}):")
|
|
209
|
+
@out.puts(title)
|
|
210
|
+
@out.puts(body)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def submit_to_marketplace!(title, body)
|
|
214
|
+
output, error, status = capture(
|
|
215
|
+
"gh", "issue", "list", "--repo", @marketplace_repository, "--state", "all", "--limit", "1000",
|
|
216
|
+
"--json", "number,body,state,url"
|
|
217
|
+
)
|
|
218
|
+
raise ArgumentError, "could not inspect marketplace submissions: #{error.strip}" unless status.success?
|
|
219
|
+
|
|
220
|
+
repository_url = "https://github.com/#{@repository}"
|
|
221
|
+
matching = JSON.parse(output).select { |issue| issue.fetch("body", "").include?(repository_url) }
|
|
222
|
+
open_issue = matching.find { |issue| issue.fetch("state") == "OPEN" }
|
|
223
|
+
if open_issue
|
|
224
|
+
run!(
|
|
225
|
+
"gh", "issue", "edit", open_issue.fetch("number").to_s,
|
|
226
|
+
"--repo", @marketplace_repository, "--title", title, "--body", body
|
|
227
|
+
)
|
|
228
|
+
return open_issue.fetch("url")
|
|
229
|
+
end
|
|
230
|
+
if matching.any?
|
|
231
|
+
raise ArgumentError,
|
|
232
|
+
"this repository already has a closed marketplace submission; use the marketplace verification/update form"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
created, create_error, create_status = capture(
|
|
236
|
+
"gh", "issue", "create", "--repo", @marketplace_repository, "--title", title, "--body", body
|
|
237
|
+
)
|
|
238
|
+
raise ArgumentError, "marketplace submission failed: #{create_error.strip}" unless create_status.success?
|
|
239
|
+
|
|
240
|
+
created.lines.map(&:strip).find { |line| line.start_with?("https://") } || created.strip
|
|
241
|
+
rescue JSON::ParserError, KeyError => error
|
|
242
|
+
raise ArgumentError, "invalid marketplace response: #{error.message}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def github_repository(url)
|
|
246
|
+
value = url.to_s.strip.sub(/\.git\z/, "")
|
|
247
|
+
match = value.match(%r{(?:github\.com[/:])([^/]+/[^/]+)\z}i)
|
|
248
|
+
match && match[1]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def capture(*command)
|
|
252
|
+
Open3.capture3(*command)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def run!(*command)
|
|
256
|
+
output, error, status = capture(*command)
|
|
257
|
+
return output if status.success?
|
|
258
|
+
|
|
259
|
+
detail = [output, error].join("\n").strip
|
|
260
|
+
raise ArgumentError, "command failed: #{command.first}#{detail.empty? ? '' : ": #{detail}"}"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -191,6 +191,7 @@ module OmarchyUI
|
|
|
191
191
|
def validate_plugin!
|
|
192
192
|
%i[id author license description].each { |key| required_string!(key) }
|
|
193
193
|
raise ArgumentError, "invalid plugin id" unless VALID_ID.match?(@data[:id])
|
|
194
|
+
raise ArgumentError, "plugin entrypoint must be main.rb" unless entrypoint == "main.rb"
|
|
194
195
|
|
|
195
196
|
widget = @data[:bar_widget]
|
|
196
197
|
raise ArgumentError, "plugin project config is missing bar widget metadata" unless widget.is_a?(Hash)
|
|
@@ -4,7 +4,6 @@ require "digest"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
6
|
require "open3"
|
|
7
|
-
require "pathname"
|
|
8
7
|
require "tmpdir"
|
|
9
8
|
|
|
10
9
|
module OmarchyUI
|
|
@@ -22,16 +21,17 @@ module OmarchyUI
|
|
|
22
21
|
CHECKSUM = "omarchy-ui-qml-bundle.sha256"
|
|
23
22
|
PROVENANCE = "QML_PROVENANCE.md"
|
|
24
23
|
|
|
25
|
-
def self.compile!(path)
|
|
26
|
-
new(path).compile!
|
|
24
|
+
def self.compile!(path, entry_files: ENTRY_TYPES.keys)
|
|
25
|
+
new(path, entry_files:).compile!
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
def self.verify!(path)
|
|
30
29
|
new(path).verify!
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
def initialize(path)
|
|
32
|
+
def initialize(path, entry_files: nil)
|
|
34
33
|
@path = File.expand_path(path)
|
|
34
|
+
@entry_types = entry_files ? select_entry_types(entry_files) : nil
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def compile!
|
|
@@ -61,66 +61,107 @@ module OmarchyUI
|
|
|
61
61
|
))
|
|
62
62
|
configure!(tools.fetch(:qt_cmake), source, build)
|
|
63
63
|
build!(tools.fetch(:cmake), build)
|
|
64
|
-
deploy!(
|
|
65
|
-
build:,
|
|
66
|
-
module_path:,
|
|
67
|
-
target:,
|
|
68
|
-
uri:,
|
|
69
|
-
fingerprint:,
|
|
70
|
-
qml_files:,
|
|
71
|
-
resources:,
|
|
72
|
-
source:,
|
|
73
|
-
expected_qt_version: qt_version
|
|
74
|
-
)
|
|
64
|
+
deploy!(build:, module_path:, target:, uri:, expected_qt_version: qt_version)
|
|
75
65
|
end
|
|
76
66
|
end
|
|
77
67
|
|
|
78
68
|
def verify!
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
raise ArgumentError, "
|
|
69
|
+
entry_types = packaged_entry_types
|
|
70
|
+
module_path, module_uri = packaged_module
|
|
71
|
+
entry_types.each do |entry, type|
|
|
72
|
+
shim = File.read(File.join(@path, entry)).strip
|
|
73
|
+
expected = entry_shim(module_path, type, uri: module_uri).strip
|
|
74
|
+
raise ArgumentError, "invalid compiled QML entry shim: #{entry}" unless shim == expected
|
|
85
75
|
end
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
|
|
77
|
+
segment = File.basename(module_path)
|
|
78
|
+
target = "omarchy_ui_bundle_#{segment.downcase}"
|
|
79
|
+
artifact_names = ["lib#{target}.so", "lib#{target}plugin.so"]
|
|
80
|
+
module_directory = File.join(@path, module_path)
|
|
81
|
+
expected_module_entries = (artifact_names + ["qmldir"]).sort
|
|
82
|
+
unless Dir.children(module_directory).sort == expected_module_entries
|
|
83
|
+
raise ArgumentError, "compiled QML module contains unexpected files"
|
|
88
84
|
end
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
artifacts = report.fetch("artifacts")
|
|
93
|
-
raise ArgumentError, "compiled QML package must contain two libraries" unless artifacts.length == 2
|
|
94
|
-
artifacts.each do |artifact|
|
|
95
|
-
relative = safe_relative_path(artifact.fetch("path"))
|
|
96
|
-
unless relative.start_with?("#{module_path}/") && relative.end_with?(".so")
|
|
97
|
-
raise ArgumentError, "compiled QML artifact is outside its module: #{relative}"
|
|
98
|
-
end
|
|
85
|
+
artifacts = artifact_names.map do |name|
|
|
86
|
+
relative = File.join(module_path, name)
|
|
99
87
|
absolute = File.join(@path, relative)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
88
|
+
{
|
|
89
|
+
"path" => relative,
|
|
90
|
+
"sha256" => Digest::SHA256.file(absolute).hexdigest,
|
|
91
|
+
"bytes" => File.size(absolute)
|
|
92
|
+
}
|
|
105
93
|
end
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
{
|
|
95
|
+
"format" => "qt-aot-qml-module",
|
|
96
|
+
"format_version" => FORMAT_VERSION,
|
|
97
|
+
"module_uri" => module_uri,
|
|
98
|
+
"module_path" => module_path,
|
|
99
|
+
"entry_shims" => entry_types.keys,
|
|
100
|
+
"artifacts" => artifacts
|
|
101
|
+
}
|
|
109
102
|
end
|
|
110
103
|
|
|
111
104
|
private
|
|
112
105
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
def select_entry_types(entry_files)
|
|
107
|
+
entries = Array(entry_files).map(&:to_s).uniq
|
|
108
|
+
unknown = entries - ENTRY_TYPES.keys
|
|
109
|
+
raise ArgumentError, "unsupported compiled QML entries: #{unknown.join(', ')}" unless unknown.empty?
|
|
110
|
+
raise ArgumentError, "compiled QML package needs at least one entry point" if entries.empty?
|
|
111
|
+
|
|
112
|
+
entries.to_h { |entry| [entry, ENTRY_TYPES.fetch(entry)] }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def packaged_entry_types
|
|
116
|
+
selected = if File.file?(File.join(@path, "manifest.json"))
|
|
117
|
+
manifest = JSON.parse(File.read(File.join(@path, "manifest.json")))
|
|
118
|
+
select_entry_types(manifest.fetch("entryPoints").values)
|
|
119
|
+
else
|
|
120
|
+
select_entry_types(ENTRY_TYPES.keys.select { |entry| File.file?(File.join(@path, entry)) })
|
|
121
|
+
end
|
|
122
|
+
actual = ENTRY_TYPES.keys.select { |entry| File.file?(File.join(@path, entry)) }
|
|
123
|
+
unless actual.sort == selected.keys.sort
|
|
124
|
+
raise ArgumentError, "compiled QML package contains unexpected entry shims"
|
|
118
125
|
end
|
|
119
|
-
|
|
126
|
+
selected
|
|
127
|
+
rescue JSON::ParserError, KeyError, TypeError => error
|
|
128
|
+
raise ArgumentError, "invalid plugin manifest: #{error.message}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def packaged_module
|
|
132
|
+
compiled_root = File.join(@path, COMPILED_ROOT)
|
|
133
|
+
unless File.directory?(compiled_root) && !File.symlink?(compiled_root) &&
|
|
134
|
+
Dir.children(compiled_root) == ["Bundles"]
|
|
135
|
+
raise ArgumentError, "compiled QML root must contain only the Bundles directory"
|
|
136
|
+
end
|
|
137
|
+
bundles = File.join(compiled_root, "Bundles")
|
|
138
|
+
directories = if File.directory?(bundles) && !File.symlink?(bundles)
|
|
139
|
+
Dir.children(bundles).sort.filter_map do |entry|
|
|
140
|
+
path = File.join(bundles, entry)
|
|
141
|
+
path if File.directory?(path) && !File.symlink?(path)
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
raise ArgumentError, "compiled QML package must contain exactly one module" unless directories.length == 1
|
|
147
|
+
|
|
148
|
+
directory = directories.first
|
|
149
|
+
module_path = directory.delete_prefix("#{@path}/")
|
|
150
|
+
qmldir = File.join(directory, "qmldir")
|
|
151
|
+
unless File.file?(qmldir) && !File.symlink?(qmldir)
|
|
152
|
+
raise ArgumentError, "compiled QML qmldir is missing"
|
|
153
|
+
end
|
|
154
|
+
module_uri = File.read(qmldir)[/^module\s+(\S+)\s*$/, 1]
|
|
155
|
+
raise ArgumentError, "compiled QML module declaration is missing" unless module_uri
|
|
156
|
+
unless File.join(*module_uri.split(".")) == module_path
|
|
157
|
+
raise ArgumentError, "compiled QML module path does not match its URI"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
[module_path, module_uri]
|
|
120
161
|
end
|
|
121
162
|
|
|
122
163
|
def validate_sources!
|
|
123
|
-
missing =
|
|
164
|
+
missing = @entry_types.keys.reject { |entry| File.file?(File.join(@path, entry)) }
|
|
124
165
|
missing << "ControlNode.qml" unless File.file?(File.join(@path, "ControlNode.qml"))
|
|
125
166
|
return if missing.empty?
|
|
126
167
|
|
|
@@ -163,7 +204,7 @@ module OmarchyUI
|
|
|
163
204
|
|
|
164
205
|
def stage_sources(destination)
|
|
165
206
|
FileUtils.mkdir_p(destination)
|
|
166
|
-
SOURCE_ENTRIES.each do |entry|
|
|
207
|
+
(@entry_types.keys + SOURCE_ENTRIES.drop(ENTRY_TYPES.length)).each do |entry|
|
|
167
208
|
source = File.join(@path, entry)
|
|
168
209
|
next unless File.exist?(source)
|
|
169
210
|
|
|
@@ -281,8 +322,7 @@ module OmarchyUI
|
|
|
281
322
|
raise ArgumentError, "#{label} failed#{detail.empty? ? '' : ":\n#{detail}"}"
|
|
282
323
|
end
|
|
283
324
|
|
|
284
|
-
def deploy!(build:, module_path:, target:, uri:,
|
|
285
|
-
expected_qt_version:)
|
|
325
|
+
def deploy!(build:, module_path:, target:, uri:, expected_qt_version:)
|
|
286
326
|
built_module = File.join(build, module_path)
|
|
287
327
|
artifact_names = ["lib#{target}.so", "lib#{target}plugin.so", "qmldir"]
|
|
288
328
|
missing = artifact_names.reject { |name| File.file?(File.join(built_module, name)) }
|
|
@@ -304,38 +344,10 @@ module OmarchyUI
|
|
|
304
344
|
File.write(qmldir, File.read(qmldir).rstrip + "\n")
|
|
305
345
|
|
|
306
346
|
SOURCE_ENTRIES.each { |entry| remove_generated(File.join(@path, entry)) }
|
|
307
|
-
|
|
347
|
+
@entry_types.each do |entry, type|
|
|
308
348
|
File.write(File.join(@path, entry), entry_shim(module_path, type, uri:))
|
|
309
349
|
end
|
|
310
|
-
|
|
311
|
-
artifact_paths = artifact_names.filter_map do |name|
|
|
312
|
-
next if name == "qmldir"
|
|
313
|
-
|
|
314
|
-
relative = File.join(module_path, name)
|
|
315
|
-
absolute = File.join(@path, relative)
|
|
316
|
-
{
|
|
317
|
-
"path" => relative,
|
|
318
|
-
"sha256" => Digest::SHA256.file(absolute).hexdigest,
|
|
319
|
-
"bytes" => File.size(absolute)
|
|
320
|
-
}
|
|
321
|
-
end
|
|
322
|
-
report = {
|
|
323
|
-
"format" => "qt-aot-qml-module",
|
|
324
|
-
"format_version" => FORMAT_VERSION,
|
|
325
|
-
"qt_version" => qt_version,
|
|
326
|
-
"module_uri" => uri,
|
|
327
|
-
"module_path" => module_path,
|
|
328
|
-
"source_fingerprint" => fingerprint,
|
|
329
|
-
"source_files" => qml_files.length,
|
|
330
|
-
"source_bytes" => (qml_files + resources).sum { |file| File.size(File.join(source, file)) },
|
|
331
|
-
"entry_shims" => ENTRY_TYPES.keys,
|
|
332
|
-
"artifacts" => artifact_paths
|
|
333
|
-
}
|
|
334
|
-
File.write(File.join(@path, REPORT), JSON.pretty_generate(report) + "\n")
|
|
335
|
-
checksums = artifact_paths.map { |artifact| "#{artifact.fetch('sha256')} #{artifact.fetch('path')}" }
|
|
336
|
-
File.write(File.join(@path, CHECKSUM), checksums.join("\n") + "\n")
|
|
337
|
-
File.write(File.join(@path, PROVENANCE), provenance(report))
|
|
338
|
-
report
|
|
350
|
+
verify!
|
|
339
351
|
end
|
|
340
352
|
|
|
341
353
|
def remove_generated(path)
|
|
@@ -352,35 +364,5 @@ module OmarchyUI
|
|
|
352
364
|
QML
|
|
353
365
|
end
|
|
354
366
|
|
|
355
|
-
def provenance(report)
|
|
356
|
-
artifacts = report.fetch("artifacts").map do |artifact|
|
|
357
|
-
"- `#{artifact.fetch('path')}` — `#{artifact.fetch('sha256')}`"
|
|
358
|
-
end.join("\n")
|
|
359
|
-
<<~MARKDOWN
|
|
360
|
-
# Compiled QML provenance
|
|
361
|
-
|
|
362
|
-
Omarchy UI generated this package's native Qt module from the tree-shaken Zui and
|
|
363
|
-
Omarchy host QML graph. Generated QML source contents were discarded after AOT compilation.
|
|
364
|
-
|
|
365
|
-
- Format: `#{report.fetch('format')}` version #{report.fetch('format_version')}
|
|
366
|
-
- Qt: `#{report.fetch('qt_version')}`
|
|
367
|
-
- Module: `#{report.fetch('module_uri')}`
|
|
368
|
-
- Source fingerprint: `#{report.fetch('source_fingerprint')}`
|
|
369
|
-
|
|
370
|
-
## Artifacts
|
|
371
|
-
|
|
372
|
-
#{artifacts}
|
|
373
|
-
|
|
374
|
-
Verify the packaged libraries from the plugin directory:
|
|
375
|
-
|
|
376
|
-
```bash
|
|
377
|
-
sha256sum --check #{CHECKSUM}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
`App.qml`, `Service.qml`, `Panel.qml`, and `BarWidget.qml` are the minimal loader shims
|
|
381
|
-
required by Omarchy's file-based entry-point contract. Application UI lives in the compiled
|
|
382
|
-
module recorded by `#{REPORT}`.
|
|
383
|
-
MARKDOWN
|
|
384
|
-
end
|
|
385
367
|
end
|
|
386
368
|
end
|
data/lib/omarchy_ui/runtime.rb
CHANGED
|
@@ -12,18 +12,18 @@ module OmarchyUI
|
|
|
12
12
|
ADAPTER_FILES = %w[Service.qml Panel.qml BarWidget.qml App.qml].freeze
|
|
13
13
|
ZUI_GENERATED_ENTRIES = %w[Desktop.qml ControlNode.qml Components Controls Theme Fonts].freeze
|
|
14
14
|
TREE_SHAKE_REPORT = "zui-tree-shake.json"
|
|
15
|
+
SHELL_COMPONENT_ROOT = File.expand_path("../../qml", __dir__)
|
|
16
|
+
SHELL_COMPONENTS = %w[DesktopStage.qml Positioned.qml].freeze
|
|
15
17
|
FILES = (ADAPTER_FILES + %w[ControlNode.qml]).freeze
|
|
16
|
-
|
|
18
|
+
LEGACY_METADATA_FILES = %W[
|
|
19
|
+
#{TREE_SHAKE_REPORT}
|
|
20
|
+
#{QmlCompiler::REPORT} #{QmlCompiler::CHECKSUM} #{QmlCompiler::PROVENANCE}
|
|
17
21
|
omarchy-ui-runtime.sha256 runtime-provenance.json RUNTIME_PROVENANCE.md
|
|
18
22
|
].freeze
|
|
19
23
|
GENERATED_ENTRIES = (ADAPTER_FILES + ZUI_GENERATED_ENTRIES + [
|
|
20
|
-
TREE_SHAKE_REPORT,
|
|
21
24
|
QmlCompiler::COMPILED_ROOT,
|
|
22
|
-
QmlCompiler::REPORT,
|
|
23
|
-
QmlCompiler::CHECKSUM,
|
|
24
|
-
QmlCompiler::PROVENANCE,
|
|
25
25
|
"omarchy-ui-runtime"
|
|
26
|
-
]).freeze
|
|
26
|
+
] + LEGACY_METADATA_FILES).freeze
|
|
27
27
|
|
|
28
28
|
def executable
|
|
29
29
|
override = ENV["OMARCHY_UI_RUNTIME"]
|
|
@@ -51,29 +51,17 @@ module OmarchyUI
|
|
|
51
51
|
generated = File.join(path, entry)
|
|
52
52
|
File.directory?(generated) ? FileUtils.remove_entry(generated) : FileUtils.rm_f(generated)
|
|
53
53
|
end
|
|
54
|
-
|
|
54
|
+
install_tree_shaken_zui(path, project:)
|
|
55
55
|
ADAPTER_FILES.each do |file|
|
|
56
56
|
FileUtils.cp(File.join(framework_root, file), File.join(path, file))
|
|
57
57
|
end
|
|
58
|
-
File.write(File.join(path, TREE_SHAKE_REPORT), JSON.pretty_generate({
|
|
59
|
-
"zui_version" => Zui::VERSION,
|
|
60
|
-
"components" => report.components.map(&:to_s),
|
|
61
|
-
"before_bytes" => report.before_bytes,
|
|
62
|
-
"after_bytes" => report.after_bytes,
|
|
63
|
-
"saved_bytes" => report.saved_bytes,
|
|
64
|
-
"warnings" => report.warnings
|
|
65
|
-
}) + "\n")
|
|
66
58
|
|
|
67
59
|
if File.file?(BUNDLED)
|
|
68
60
|
destination = File.join(path, "omarchy-ui-runtime")
|
|
69
61
|
FileUtils.cp(BUNDLED, destination)
|
|
70
62
|
FileUtils.chmod(0o755, destination)
|
|
71
63
|
end
|
|
72
|
-
|
|
73
|
-
source = File.join(framework_root, "vendor", "runtime", "x86_64-linux", file)
|
|
74
|
-
FileUtils.cp(source, File.join(path, file)) if File.file?(source)
|
|
75
|
-
end
|
|
76
|
-
QmlCompiler.compile!(path) if compiled
|
|
64
|
+
QmlCompiler.compile!(path, entry_files: package_entry_files(path)) if compiled
|
|
77
65
|
path
|
|
78
66
|
end
|
|
79
67
|
|
|
@@ -88,11 +76,51 @@ module OmarchyUI
|
|
|
88
76
|
FileUtils.mkdir_p(native)
|
|
89
77
|
Zui::Runtime.install_qml(qml)
|
|
90
78
|
report = Zui::TreeShaker.new(project:, framework: qml, native:).shake!
|
|
79
|
+
install_shell_components(qml)
|
|
91
80
|
FileUtils.rm_f([File.join(qml, "Desktop.qml"), File.join(qml, "Service.qml")])
|
|
92
81
|
entries = Dir.children(qml).map { |entry| File.join(qml, entry) }
|
|
93
82
|
FileUtils.cp_r(entries, destination) unless entries.empty?
|
|
94
83
|
return report
|
|
95
84
|
end
|
|
96
85
|
end
|
|
86
|
+
|
|
87
|
+
def package_entry_files(path)
|
|
88
|
+
manifest_path = File.join(path, "manifest.json")
|
|
89
|
+
return ["App.qml"] unless File.file?(manifest_path)
|
|
90
|
+
|
|
91
|
+
manifest = JSON.parse(File.read(manifest_path))
|
|
92
|
+
entries = manifest.fetch("entryPoints").values.map(&:to_s).uniq
|
|
93
|
+
unknown = entries - QmlCompiler::ENTRY_TYPES.keys
|
|
94
|
+
raise ArgumentError, "unsupported manifest QML entry points: #{unknown.join(', ')}" unless unknown.empty?
|
|
95
|
+
raise ArgumentError, "manifest has no QML entry points" if entries.empty?
|
|
96
|
+
|
|
97
|
+
entries
|
|
98
|
+
rescue JSON::ParserError, KeyError, TypeError => error
|
|
99
|
+
raise ArgumentError, "invalid plugin manifest: #{error.message}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def install_shell_components(qml)
|
|
103
|
+
builtins = File.join(qml, "Components", "Builtins")
|
|
104
|
+
FileUtils.mkdir_p(builtins)
|
|
105
|
+
SHELL_COMPONENTS.each do |file|
|
|
106
|
+
FileUtils.cp(File.join(SHELL_COMPONENT_ROOT, file), File.join(builtins, file))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
control_node = File.join(qml, "ControlNode.qml")
|
|
110
|
+
source = File.read(control_node)
|
|
111
|
+
source = source.sub(
|
|
112
|
+
'readonly property bool builtIn: ["text"',
|
|
113
|
+
'readonly property bool builtIn: ["desktop_stage", "positioned", "text"'
|
|
114
|
+
)
|
|
115
|
+
source = source.sub(
|
|
116
|
+
'readonly property bool structuralContainer: ["row"',
|
|
117
|
+
'readonly property bool structuralContainer: ["desktop_stage", "positioned", "row"'
|
|
118
|
+
)
|
|
119
|
+
source = source.sub(
|
|
120
|
+
'return bridge && bridge.projectDir ? bridge.projectDir + "/" + source : Qt.resolvedUrl(source)',
|
|
121
|
+
'return bridge && bridge.projectDir ? "file://" + bridge.projectDir + "/" + source : Qt.resolvedUrl(source)'
|
|
122
|
+
)
|
|
123
|
+
File.write(control_node, source)
|
|
124
|
+
end
|
|
97
125
|
end
|
|
98
126
|
end
|
data/lib/omarchy_ui.rb
CHANGED
|
@@ -11,7 +11,7 @@ rescue LoadError
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
module OmarchyUI
|
|
14
|
-
VERSION = "0.0.
|
|
14
|
+
VERSION = "0.0.10"
|
|
15
15
|
ZUI_VERSION = Zui::VERSION
|
|
16
16
|
FRAMEWORK_ROOT = File.expand_path("..", __dir__)
|
|
17
17
|
|
|
@@ -130,3 +130,5 @@ require_relative "omarchy_ui/project_config"
|
|
|
130
130
|
require_relative "omarchy_ui/generator"
|
|
131
131
|
require_relative "omarchy_ui/qml_compiler"
|
|
132
132
|
require_relative "omarchy_ui/runtime"
|
|
133
|
+
require_relative "omarchy_ui/plugin_package"
|
|
134
|
+
require_relative "omarchy_ui/plugin_publisher"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import Quickshell
|
|
3
|
+
import Quickshell.Wayland
|
|
4
|
+
|
|
5
|
+
PanelWindow {
|
|
6
|
+
id: window
|
|
7
|
+
|
|
8
|
+
required property var renderer
|
|
9
|
+
readonly property real requestedX: Number(renderer.prop("stage_x", 48))
|
|
10
|
+
readonly property real requestedY: Number(renderer.prop("stage_y", 220))
|
|
11
|
+
property real sceneX: requestedX
|
|
12
|
+
property real sceneY: requestedY
|
|
13
|
+
|
|
14
|
+
function boundedX(value) {
|
|
15
|
+
return Math.max(0, Math.min(width - stage.width, Number(value)))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function boundedY(value) {
|
|
19
|
+
return Math.max(0, Math.min(height - stage.height, Number(value)))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function send(name, payload) {
|
|
23
|
+
if (renderer.subscribed(name))
|
|
24
|
+
renderer.bridge.sendEvent(renderer.surfaceName, renderer.controlId, name, payload || {})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onRequestedXChanged: if (!sceneDrag.active) sceneX = boundedX(requestedX)
|
|
28
|
+
onRequestedYChanged: if (!sceneDrag.active) sceneY = boundedY(requestedY)
|
|
29
|
+
|
|
30
|
+
visible: renderer.prop("pinned", false) === true
|
|
31
|
+
anchors { top: true; right: true; bottom: true; left: true }
|
|
32
|
+
color: "transparent"
|
|
33
|
+
exclusionMode: ExclusionMode.Ignore
|
|
34
|
+
WlrLayershell.namespace: renderer.bridge && renderer.bridge.manifest && renderer.bridge.manifest.id
|
|
35
|
+
? String(renderer.bridge.manifest.id) + "-desktop"
|
|
36
|
+
: "omarchy-ui-desktop"
|
|
37
|
+
WlrLayershell.layer: WlrLayer.Overlay
|
|
38
|
+
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
39
|
+
|
|
40
|
+
// The rest of the full-screen surface remains click-through.
|
|
41
|
+
mask: Region { item: stage }
|
|
42
|
+
|
|
43
|
+
Item {
|
|
44
|
+
id: stage
|
|
45
|
+
x: window.boundedX(window.sceneX)
|
|
46
|
+
y: window.boundedY(window.sceneY)
|
|
47
|
+
width: Math.max(1, Number(renderer.prop("stage_width", 420)))
|
|
48
|
+
height: Math.max(1, Number(renderer.prop("stage_height", 420)))
|
|
49
|
+
|
|
50
|
+
DragHandler {
|
|
51
|
+
id: sceneDrag
|
|
52
|
+
target: null
|
|
53
|
+
enabled: renderer.prop("draggable", true) !== false
|
|
54
|
+
dragThreshold: 3
|
|
55
|
+
property real originX: 0
|
|
56
|
+
property real originY: 0
|
|
57
|
+
|
|
58
|
+
onActiveChanged: {
|
|
59
|
+
if (active) {
|
|
60
|
+
originX = stage.x
|
|
61
|
+
originY = stage.y
|
|
62
|
+
window.send("drag_start", { target_x: stage.x, target_y: stage.y })
|
|
63
|
+
} else {
|
|
64
|
+
window.sceneX = stage.x
|
|
65
|
+
window.sceneY = stage.y
|
|
66
|
+
window.send("drag_end", { target_x: stage.x, target_y: stage.y })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onTranslationChanged: {
|
|
71
|
+
if (!active) return
|
|
72
|
+
window.sceneX = window.boundedX(originX + translation.x)
|
|
73
|
+
window.sceneY = window.boundedY(originY + translation.y)
|
|
74
|
+
window.send("drag", { target_x: stage.x, target_y: stage.y })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
HoverHandler {
|
|
79
|
+
enabled: sceneDrag.enabled
|
|
80
|
+
cursorShape: sceneDrag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Repeater {
|
|
84
|
+
model: renderer.node && Array.isArray(renderer.node.children) ? renderer.node.children : []
|
|
85
|
+
delegate: renderer.childDelegateComponent
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
data/qml/Positioned.qml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
|
|
3
|
+
Item {
|
|
4
|
+
id: root
|
|
5
|
+
|
|
6
|
+
required property var renderer
|
|
7
|
+
|
|
8
|
+
x: Number(renderer.prop("position_x", 0))
|
|
9
|
+
y: Number(renderer.prop("position_y", 0))
|
|
10
|
+
width: Math.max(1, Number(renderer.prop("item_width", 1)))
|
|
11
|
+
height: Math.max(1, Number(renderer.prop("item_height", 1)))
|
|
12
|
+
clip: renderer.prop("clip", false) === true
|
|
13
|
+
|
|
14
|
+
Repeater {
|
|
15
|
+
model: renderer.node && Array.isArray(renderer.node.children) ? renderer.node.children : []
|
|
16
|
+
delegate: renderer.childDelegateComponent
|
|
17
|
+
}
|
|
18
|
+
}
|
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.10
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Moussa Ali
|
|
@@ -39,9 +39,13 @@ files:
|
|
|
39
39
|
- lib/omarchy_ui.rb
|
|
40
40
|
- lib/omarchy_ui/cli.rb
|
|
41
41
|
- lib/omarchy_ui/generator.rb
|
|
42
|
+
- lib/omarchy_ui/plugin_package.rb
|
|
43
|
+
- lib/omarchy_ui/plugin_publisher.rb
|
|
42
44
|
- lib/omarchy_ui/project_config.rb
|
|
43
45
|
- lib/omarchy_ui/qml_compiler.rb
|
|
44
46
|
- lib/omarchy_ui/runtime.rb
|
|
47
|
+
- qml/DesktopStage.qml
|
|
48
|
+
- qml/Positioned.qml
|
|
45
49
|
- vendor/runtime/x86_64-linux/RUNTIME_PROVENANCE.md
|
|
46
50
|
- vendor/runtime/x86_64-linux/omarchy-ui-runtime
|
|
47
51
|
- vendor/runtime/x86_64-linux/omarchy-ui-runtime.sha256
|