omarchy-ui 0.0.1-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 +7 -0
- data/App.qml +112 -0
- data/BarWidget.qml +47 -0
- data/Components/README.md +40 -0
- data/Components/Sparkline.qml +36 -0
- data/ControlNode.qml +745 -0
- data/LICENSE +22 -0
- data/Panel.qml +106 -0
- data/README.md +362 -0
- data/Service.qml +422 -0
- data/bin/omarchy_ui +7 -0
- data/lib/omarchy_ui/animation.rb +24 -0
- data/lib/omarchy_ui/application.rb +282 -0
- data/lib/omarchy_ui/builder.rb +230 -0
- data/lib/omarchy_ui/cli.rb +199 -0
- data/lib/omarchy_ui/command.rb +67 -0
- data/lib/omarchy_ui/component_registry.rb +87 -0
- data/lib/omarchy_ui/components.rb +43 -0
- data/lib/omarchy_ui/node.rb +30 -0
- data/lib/omarchy_ui/project.rb +127 -0
- data/lib/omarchy_ui/protocol.rb +29 -0
- data/lib/omarchy_ui/runtime.rb +31 -0
- data/lib/omarchy_ui/scheduler.rb +136 -0
- data/lib/omarchy_ui/state_store.rb +100 -0
- data/lib/omarchy_ui/value.rb +44 -0
- data/lib/omarchy_ui.rb +29 -0
- data/manifest.json +27 -0
- data/vendor/runtime/x86_64-linux/omarchy-ui-runtime +0 -0
- metadata +71 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
module OmarchyUI
|
|
10
|
+
class CLI
|
|
11
|
+
def self.run(arguments, out: $stdout, err: $stderr)
|
|
12
|
+
new(out:, err:).run(arguments)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(out:, err:)
|
|
16
|
+
@out = out
|
|
17
|
+
@err = err
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run(arguments)
|
|
21
|
+
command = arguments.shift
|
|
22
|
+
case command
|
|
23
|
+
when "run" then run_file(arguments)
|
|
24
|
+
when "launch" then launch_file(arguments)
|
|
25
|
+
when "new" then new_project(arguments)
|
|
26
|
+
when "bundle" then bundle_project(arguments)
|
|
27
|
+
when "push" then push(arguments)
|
|
28
|
+
when "validate" then validate(arguments)
|
|
29
|
+
when "version", "--version", "-v" then @out.puts(OmarchyUI::VERSION); 0
|
|
30
|
+
else
|
|
31
|
+
@err.puts("Usage: omarchy_ui <new NAME|run FILE|launch FILE|bundle [DIRECTORY]|push [DIRECTORY]|validate [DIRECTORY]|version>")
|
|
32
|
+
command.nil? ? 0 : 64
|
|
33
|
+
end
|
|
34
|
+
rescue ArgumentError, SystemCallError, JSON::ParserError => error
|
|
35
|
+
@err.puts("omarchy_ui: #{error.message}")
|
|
36
|
+
1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def run_file(arguments)
|
|
42
|
+
file = File.expand_path(arguments.shift || raise(ArgumentError, "run requires a Ruby file"))
|
|
43
|
+
raise ArgumentError, "Ruby file not found: #{file}" unless File.file?(file)
|
|
44
|
+
exec(RbConfig.ruby, "-I", File.join(FRAMEWORK_ROOT, "lib"), file, *arguments)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def launch_file(arguments)
|
|
48
|
+
file = File.expand_path(arguments.shift || raise(ArgumentError, "launch requires a Ruby file"))
|
|
49
|
+
raise ArgumentError, "Ruby file not found: #{file}" unless File.file?(file)
|
|
50
|
+
raise ArgumentError, "launch does not accept Ruby arguments" unless arguments.empty?
|
|
51
|
+
|
|
52
|
+
project_dir = File.dirname(file)
|
|
53
|
+
Dir.mktmpdir("omarchy-ui-app-") do |runtime_dir|
|
|
54
|
+
Project::RUNTIME_FILES.each do |name|
|
|
55
|
+
source = File.file?(File.join(project_dir, name)) ? File.join(project_dir, name) : File.join(FRAMEWORK_ROOT, name)
|
|
56
|
+
FileUtils.cp(source, runtime_dir)
|
|
57
|
+
end
|
|
58
|
+
%w[Commons Ui].each do |module_name|
|
|
59
|
+
source = File.join("/usr/share/omarchy/shell", module_name)
|
|
60
|
+
raise ArgumentError, "Omarchy QML module not found: #{source}" unless File.directory?(source)
|
|
61
|
+
FileUtils.ln_s(source, File.join(runtime_dir, module_name))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
environment = ENV.to_h.merge(
|
|
65
|
+
"OMARCHY_UI_PROJECT_DIR" => project_dir,
|
|
66
|
+
"OMARCHY_UI_RUBY_PROGRAM" => file,
|
|
67
|
+
"OMARCHY_UI_RUNTIME" => Runtime.executable
|
|
68
|
+
)
|
|
69
|
+
success = system(environment, "quickshell", "--path", File.join(runtime_dir, "App.qml"))
|
|
70
|
+
return success ? 0 : ($?&.exitstatus || 1)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def new_project(arguments)
|
|
75
|
+
name = arguments.shift || raise(ArgumentError, "new requires a project name")
|
|
76
|
+
raise ArgumentError, "new accepts only an application name" unless arguments.empty?
|
|
77
|
+
destination = File.expand_path(slug(name))
|
|
78
|
+
Project.new(path: destination, name: name).create
|
|
79
|
+
@out.puts("Created standalone Omarchy UI app in #{destination}")
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def slug(value)
|
|
84
|
+
result = value.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|\-\z/, "")
|
|
85
|
+
raise ArgumentError, "name must contain letters or numbers" if result.empty?
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate(arguments)
|
|
90
|
+
source = File.expand_path(arguments.shift || Dir.pwd)
|
|
91
|
+
with_staged_project(source) do |staging|
|
|
92
|
+
system("omarchy", "plugin", "validate", staging) ? 0 : 1
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def bundle_project(arguments)
|
|
97
|
+
source = File.expand_path(arguments.shift || Dir.pwd)
|
|
98
|
+
raise ArgumentError, "bundle accepts one directory" unless arguments.empty?
|
|
99
|
+
raise ArgumentError, "main.rb not found: #{source}" unless File.file?(File.join(source, "main.rb"))
|
|
100
|
+
destination = File.join(source, "dist", File.basename(source))
|
|
101
|
+
raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
|
|
102
|
+
FileUtils.mkdir_p(destination)
|
|
103
|
+
entries = Dir.children(source).reject { |entry| %w[.git dist].include?(entry) }
|
|
104
|
+
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination)
|
|
105
|
+
Project.install_runtime(destination)
|
|
106
|
+
runtime = File.join(destination, "omarchy-ui-runtime")
|
|
107
|
+
FileUtils.cp(Runtime::BUNDLED, runtime)
|
|
108
|
+
FileUtils.chmod(0o755, runtime)
|
|
109
|
+
%w[Commons Ui].each do |module_name|
|
|
110
|
+
FileUtils.ln_s(File.join("/usr/share/omarchy/shell", module_name), File.join(destination, module_name))
|
|
111
|
+
end
|
|
112
|
+
launcher = File.join(destination, "run")
|
|
113
|
+
File.write(launcher, <<~SH)
|
|
114
|
+
#!/bin/sh
|
|
115
|
+
set -eu
|
|
116
|
+
app_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
117
|
+
export OMARCHY_UI_RUNTIME="$app_dir/omarchy-ui-runtime"
|
|
118
|
+
export OMARCHY_UI_PROJECT_DIR="$app_dir"
|
|
119
|
+
export OMARCHY_UI_RUBY_PROGRAM="$app_dir/main.rb"
|
|
120
|
+
exec quickshell --path "$app_dir/App.qml"
|
|
121
|
+
SH
|
|
122
|
+
FileUtils.chmod(0o755, launcher)
|
|
123
|
+
@out.puts("Bundled application in #{destination}")
|
|
124
|
+
0
|
|
125
|
+
rescue StandardError
|
|
126
|
+
FileUtils.remove_entry(destination) if destination && File.directory?(destination)
|
|
127
|
+
raise
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def push(arguments)
|
|
131
|
+
enable = !arguments.delete("--no-enable")
|
|
132
|
+
restart = !arguments.delete("--no-restart")
|
|
133
|
+
source = File.expand_path(arguments.shift || Dir.pwd)
|
|
134
|
+
Runtime.install_shared
|
|
135
|
+
manifest_path = File.join(source, "manifest.json")
|
|
136
|
+
manifest = JSON.parse(File.read(manifest_path))
|
|
137
|
+
plugin_id = manifest.fetch("id")
|
|
138
|
+
raise ArgumentError, "invalid plugin id" unless VALID_ID.match?(plugin_id)
|
|
139
|
+
plugin_root = File.expand_path("~/.config/omarchy/plugins")
|
|
140
|
+
backup_root = File.expand_path("~/.local/state/omarchy-ui/backups")
|
|
141
|
+
destination = File.join(plugin_root, plugin_id)
|
|
142
|
+
raise ArgumentError, "cannot push an installed plugin onto itself" if source == destination
|
|
143
|
+
FileUtils.mkdir_p(plugin_root)
|
|
144
|
+
backup = nil
|
|
145
|
+
staging = stage_project(source, parent: plugin_root, prefix: ".#{plugin_id}.staging-")
|
|
146
|
+
raise ArgumentError, "staged plugin validation failed" unless system("omarchy", "plugin", "validate", staging)
|
|
147
|
+
|
|
148
|
+
if File.exist?(destination)
|
|
149
|
+
FileUtils.mkdir_p(backup_root)
|
|
150
|
+
backup = File.join(backup_root, "#{plugin_id}-#{Time.now.strftime('%Y%m%d%H%M%S')}-#{Process.pid}")
|
|
151
|
+
FileUtils.mv(destination, backup)
|
|
152
|
+
@out.puts("Backed up existing plugin to #{backup}")
|
|
153
|
+
end
|
|
154
|
+
FileUtils.mv(staging, destination)
|
|
155
|
+
staging = nil
|
|
156
|
+
activate_plugin(plugin_id) if enable
|
|
157
|
+
raise ArgumentError, "shell restart failed; plugin is installed but not active" if restart && !system("omarchy", "restart", "shell")
|
|
158
|
+
@out.puts("Pushed #{plugin_id} to #{destination}")
|
|
159
|
+
0
|
|
160
|
+
rescue StandardError
|
|
161
|
+
if backup && !File.exist?(destination) && File.exist?(backup)
|
|
162
|
+
FileUtils.mv(backup, destination)
|
|
163
|
+
@err.puts("Restored previous plugin after push failure")
|
|
164
|
+
end
|
|
165
|
+
raise
|
|
166
|
+
ensure
|
|
167
|
+
FileUtils.remove_entry(staging) if staging && File.exist?(staging)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def with_staged_project(source)
|
|
171
|
+
staging = stage_project(source)
|
|
172
|
+
yield staging
|
|
173
|
+
ensure
|
|
174
|
+
FileUtils.remove_entry(staging) if staging && File.exist?(staging)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def stage_project(source, parent: nil, prefix: ".omarchy-ui-staging-")
|
|
178
|
+
raise ArgumentError, "project directory not found: #{source}" unless File.directory?(source)
|
|
179
|
+
staging = parent ? Dir.mktmpdir(prefix, parent) : Dir.mktmpdir(prefix)
|
|
180
|
+
entries = Dir.children(source).reject { |entry| entry == ".git" }
|
|
181
|
+
FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, staging) unless entries.empty?
|
|
182
|
+
Project.install_runtime(staging) if File.file?(File.join(staging, "main.rb"))
|
|
183
|
+
staging
|
|
184
|
+
rescue StandardError
|
|
185
|
+
FileUtils.remove_entry(staging) if staging && File.exist?(staging)
|
|
186
|
+
raise
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def activate_plugin(plugin_id)
|
|
190
|
+
return if system("omarchy", "plugin", "enable", plugin_id)
|
|
191
|
+
system("omarchy-shell", "shell", "rescanPlugins")
|
|
192
|
+
20.times do
|
|
193
|
+
return if system("omarchy", "plugin", "enable", plugin_id, out: File::NULL, err: File::NULL)
|
|
194
|
+
sleep(0.1)
|
|
195
|
+
end
|
|
196
|
+
raise ArgumentError, "plugin was installed but Omarchy could not enable #{plugin_id}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module OmarchyUI
|
|
7
|
+
class CommandTimeout < StandardError; end
|
|
8
|
+
|
|
9
|
+
CommandResult = Struct.new(:stdout, :stderr, :status, keyword_init: true) do
|
|
10
|
+
def success? = status.success?
|
|
11
|
+
def exitstatus = status.exitstatus
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Command
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def run(argv, env: {}, chdir: nil, input: "", timeout: nil)
|
|
18
|
+
arguments = normalize_argv(argv)
|
|
19
|
+
options = {}
|
|
20
|
+
options[:chdir] = File.expand_path(chdir) if chdir
|
|
21
|
+
stdin = stdout = stderr = wait_thread = nil
|
|
22
|
+
Open3.popen3(normalize_env(env), *arguments, **options) do |child_stdin, child_stdout, child_stderr, child_wait|
|
|
23
|
+
stdin, stdout, stderr, wait_thread = child_stdin, child_stdout, child_stderr, child_wait
|
|
24
|
+
stdin.write(input.to_s)
|
|
25
|
+
stdin.close
|
|
26
|
+
stdout_reader = Thread.new { stdout.read }
|
|
27
|
+
stderr_reader = Thread.new { stderr.read }
|
|
28
|
+
status = timeout ? Timeout.timeout(Float(timeout)) { wait_thread.value } : wait_thread.value
|
|
29
|
+
return CommandResult.new(stdout: stdout_reader.value, stderr: stderr_reader.value, status:)
|
|
30
|
+
rescue Timeout::Error
|
|
31
|
+
terminate(wait_thread)
|
|
32
|
+
stdout_reader&.join(1)
|
|
33
|
+
stderr_reader&.join(1)
|
|
34
|
+
raise CommandTimeout, "command timed out after #{timeout}s: #{arguments.first}"
|
|
35
|
+
end
|
|
36
|
+
ensure
|
|
37
|
+
[stdin, stdout, stderr].each { |stream| stream&.close unless stream&.closed? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize_argv(argv)
|
|
41
|
+
raise ArgumentError, "command must be an argv array" unless argv.is_a?(Array) && !argv.empty?
|
|
42
|
+
argv.map do |argument|
|
|
43
|
+
raise ArgumentError, "command arguments must be strings" unless argument.is_a?(String)
|
|
44
|
+
raise ArgumentError, "command arguments cannot contain NUL" if argument.include?("\0")
|
|
45
|
+
argument
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
private_class_method :normalize_argv
|
|
49
|
+
|
|
50
|
+
def normalize_env(env)
|
|
51
|
+
raise ArgumentError, "command environment must be a hash" unless env.is_a?(Hash)
|
|
52
|
+
env.to_h { |key, value| [key.to_s, value.to_s] }
|
|
53
|
+
end
|
|
54
|
+
private_class_method :normalize_env
|
|
55
|
+
|
|
56
|
+
def terminate(wait_thread)
|
|
57
|
+
Process.kill("TERM", wait_thread.pid)
|
|
58
|
+
Timeout.timeout(1) { wait_thread.value }
|
|
59
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
60
|
+
nil
|
|
61
|
+
rescue Timeout::Error
|
|
62
|
+
Process.kill("KILL", wait_thread.pid)
|
|
63
|
+
wait_thread.value
|
|
64
|
+
end
|
|
65
|
+
private_class_method :terminate
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmarchyUI
|
|
4
|
+
Component = Struct.new(:name, :qml, :properties, :events, :property_map, :event_map, :container, :auto_bind, keyword_init: true) do
|
|
5
|
+
def to_h
|
|
6
|
+
{
|
|
7
|
+
"qml" => qml,
|
|
8
|
+
"properties" => properties.map(&:to_s),
|
|
9
|
+
"events" => events.map(&:to_s),
|
|
10
|
+
"property_map" => property_map.transform_keys(&:to_s).transform_values(&:to_s),
|
|
11
|
+
"event_map" => event_map.transform_keys(&:to_s).transform_values(&:to_s),
|
|
12
|
+
"container" => container,
|
|
13
|
+
"auto_bind" => auto_bind
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ComponentRegistry
|
|
19
|
+
QML_FILE = Object.new
|
|
20
|
+
def QML_FILE.match?(value)
|
|
21
|
+
text = value.to_s
|
|
22
|
+
stem = text.end_with?(".qml") ? text[0...-4] : ""
|
|
23
|
+
!stem.empty? && OmarchyUI::UPPER.include?(stem[0]) && stem.each_char.all? do |character|
|
|
24
|
+
(OmarchyUI::LOWER + OmarchyUI::UPPER + OmarchyUI::DIGITS).include?(character)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
NAME = AsciiPattern.new(min: 1, max: 64, first: LOWER, rest: LOWER + DIGITS + "_")
|
|
28
|
+
ITEM_PROPERTIES = %i[visible enabled opacity scale rotation z width height].freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@components = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register(name, qml:, properties: [], events: [], property_map: {}, event_map: {}, container: false, auto_bind: true)
|
|
35
|
+
key = name.to_sym
|
|
36
|
+
raise ArgumentError, "component already registered: #{key}" if @components.key?(key)
|
|
37
|
+
raise ArgumentError, "invalid component name: #{name.inspect}" unless NAME.match?(key.to_s)
|
|
38
|
+
raise ArgumentError, "invalid component adapter: #{qml.inspect}" unless QML_FILE.match?(qml.to_s)
|
|
39
|
+
property_names = (properties.map(&:to_sym) + property_map.keys.map(&:to_sym) + ITEM_PROPERTIES).uniq
|
|
40
|
+
event_names = (events.map(&:to_sym) + event_map.keys.map(&:to_sym)).uniq
|
|
41
|
+
normalized_property_map = properties.to_h { |property| [property.to_sym, property.to_sym] }
|
|
42
|
+
normalized_property_map.merge!(property_map.to_h { |key, value| [key.to_sym, value.to_sym] })
|
|
43
|
+
normalized_event_map = events.to_h { |event| [event.to_sym, event.to_sym] }
|
|
44
|
+
normalized_event_map.merge!(event_map.to_h { |key, value| [key.to_sym, value.to_sym] })
|
|
45
|
+
invalid_property = property_names.find { |property| !NAME.match?(property.to_s) }
|
|
46
|
+
invalid_event = event_names.find { |event| !NAME.match?(event.to_s) }
|
|
47
|
+
raise ArgumentError, "invalid property name: #{invalid_property.inspect}" if invalid_property
|
|
48
|
+
raise ArgumentError, "invalid event name: #{invalid_event.inspect}" if invalid_event
|
|
49
|
+
invalid_target = (normalized_property_map.values + normalized_event_map.values).find { |target| !NAME.match?(target.to_s) }
|
|
50
|
+
raise ArgumentError, "invalid QML member name: #{invalid_target.inspect}" if invalid_target
|
|
51
|
+
|
|
52
|
+
@components[key] = Component.new(
|
|
53
|
+
name: key,
|
|
54
|
+
qml: qml.to_s,
|
|
55
|
+
properties: property_names.uniq.freeze,
|
|
56
|
+
events: event_names.uniq.freeze,
|
|
57
|
+
property_map: normalized_property_map.freeze,
|
|
58
|
+
event_map: normalized_event_map.freeze,
|
|
59
|
+
container: !!container,
|
|
60
|
+
auto_bind: !!auto_bind
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch(name)
|
|
65
|
+
@components.fetch(name.to_sym) { raise ArgumentError, "unknown component: #{name}" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def key?(name)
|
|
69
|
+
@components.key?(name.to_sym)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def protocol_schema
|
|
73
|
+
@components.transform_keys(&:to_s).transform_values(&:to_h)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def dup
|
|
77
|
+
copy = self.class.new
|
|
78
|
+
@components.each_value do |component|
|
|
79
|
+
copy.register(component.name, qml: component.qml, properties: component.properties,
|
|
80
|
+
events: component.events, property_map: component.property_map,
|
|
81
|
+
event_map: component.event_map, container: component.container,
|
|
82
|
+
auto_bind: component.auto_bind)
|
|
83
|
+
end
|
|
84
|
+
copy
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmarchyUI
|
|
4
|
+
COMPONENTS = {
|
|
5
|
+
container: [%i[spacing padding bordered visible], %i[click], true],
|
|
6
|
+
row: [%i[spacing alignment visible], %i[click], true],
|
|
7
|
+
column: [%i[spacing alignment visible], %i[click], true],
|
|
8
|
+
grid: [%i[columns rows spacing row_spacing column_spacing visible], %i[click], true],
|
|
9
|
+
stack: [%i[visible], %i[click], true],
|
|
10
|
+
scroll: [%i[width height clip visible], %i[click], true],
|
|
11
|
+
rectangle: [%i[width height color radius border_color border_width padding visible], %i[click], true],
|
|
12
|
+
text: [%i[text style size bold color wrap width visible], [], false],
|
|
13
|
+
icon: [%i[name text size color visible], [], false],
|
|
14
|
+
image: [%i[source width height fill_mode visible], [], false],
|
|
15
|
+
spacer: [%i[width height visible], [], false],
|
|
16
|
+
button: [%i[text icon tooltip selected active cursor focusable bordered foreground background accent font_family font_size icon_size icon_rotation icon_spinning horizontal_padding vertical_padding left_align tooltip_background tooltip_foreground tooltip_border], %i[click right_click hover], false],
|
|
17
|
+
action_button: [%i[icon tooltip foreground hover_color font_family font_size size focusable cursor bordered], %i[click hover], false],
|
|
18
|
+
toggle: [%i[label description checked cursor rounded foreground accent font_family title_size description_size], %i[change hover], false],
|
|
19
|
+
toggle_switch: [%i[checked busy interactive cursor cursor_ring cursor_pad rounded foreground accent track_height track_width knob_size knob_inset], %i[change hover], false],
|
|
20
|
+
text_field: [%i[text placeholder password foreground accent selection_tint horizontal_padding vertical_padding cursor], %i[change submit focus blur input], false],
|
|
21
|
+
number_field: [%i[label value from to step foreground accent font_family font_size field_width cursor], %i[change hover], false],
|
|
22
|
+
slider: [%i[value minimum maximum step integer track_color fill_color knob_color track_height knob_size ticks tick_color], %i[input change right_click], false],
|
|
23
|
+
dropdown: [%i[label value options foreground background popup_border accent font_family row_height popup_row_height show_label cursor], %i[change hover], false],
|
|
24
|
+
searchable_dropdown: [%i[label value options placeholder empty_text trigger_label foreground background popup_border accent font_family row_height popup_row_height popup_min_height show_label cursor], %i[change hover], false],
|
|
25
|
+
multi_select: [%i[label values options options_command options_command_cwd placeholder empty_text no_selection_text trigger_label show_label foreground background popup_border accent font_family row_height popup_row_height popup_min_height cursor], %i[change hover], false],
|
|
26
|
+
button_group: [%i[value options foreground background accent font_family font_size focusable cursor_index], %i[change hover], false],
|
|
27
|
+
progress: [%i[value minimum maximum width height color visible], [], false],
|
|
28
|
+
separator: [%i[strength visible], [], false],
|
|
29
|
+
section_header: [%i[text visible], [], false],
|
|
30
|
+
confirm_dialog: [%i[opened message cancel_text confirm_text selected_index background foreground scrim selected_background selected_text font_family corner_radius], %i[cancel confirm], false],
|
|
31
|
+
panel_hero: [%i[title meta detail foreground font_family icon_size icon_opacity meta_opacity], [], false],
|
|
32
|
+
optical_glyph: [%i[text size color debug_bounds visible], [], false],
|
|
33
|
+
cursor_surface: [%i[cursor current outline bordered foreground accent fill current_fill], %i[click], true],
|
|
34
|
+
widget_button: [%i[text font_family font_size foreground active_color active horizontal_margin vertical_padding fixed_width fixed_height text_rotation keep_space dimmed concealed interactive pressable use_active_color maintain_indicator_reveal label_visible has_visual_content tooltip], %i[click right_click middle_click wheel], false],
|
|
35
|
+
list_view: [%i[items key_field label_field description_field icon_field selected orientation spacing width height empty_text visible], %i[activate change scroll], false]
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
DEFAULT_COMPONENTS = ComponentRegistry.new
|
|
39
|
+
COMPONENTS.each do |name, (properties, events, container)|
|
|
40
|
+
adapter = name.to_s.split("_").map(&:capitalize).join + ".qml"
|
|
41
|
+
DEFAULT_COMPONENTS.register(name, qml: adapter, properties:, events:, container:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmarchyUI
|
|
4
|
+
class Node
|
|
5
|
+
attr_reader :type, :id, :props, :children, :events
|
|
6
|
+
|
|
7
|
+
def initialize(type:, id:, props: {})
|
|
8
|
+
@type = type.to_s
|
|
9
|
+
@id = id.to_s
|
|
10
|
+
@props = props.transform_keys(&:to_s)
|
|
11
|
+
@children = []
|
|
12
|
+
@events = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
result = { "type" => type, "id" => id }
|
|
17
|
+
result["props"] = props unless props.empty?
|
|
18
|
+
result["children"] = children.map(&:to_h) unless children.empty?
|
|
19
|
+
result["events"] = events unless events.empty?
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def subscribe(event)
|
|
24
|
+
@events << event.to_s unless @events.include?(event.to_s)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Binding = Struct.new(:node, :property, :reader, :last_value, :animation, keyword_init: true)
|
|
29
|
+
StructuralBinding = Struct.new(:node, :renderer, :last_children, keyword_init: true)
|
|
30
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module OmarchyUI
|
|
6
|
+
class Project
|
|
7
|
+
RUNTIME_FILES = %w[Service.qml ControlNode.qml Panel.qml BarWidget.qml App.qml].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(path:, name: nil, framework_root: FRAMEWORK_ROOT)
|
|
10
|
+
@path = File.expand_path(path)
|
|
11
|
+
@name = name || File.basename(@path).split(/[-_]/).map(&:capitalize).join(" ")
|
|
12
|
+
@framework_root = framework_root
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
raise ArgumentError, "destination already exists: #{@path}" if File.exist?(@path)
|
|
17
|
+
created = true
|
|
18
|
+
FileUtils.mkdir_p(File.join(@path, "Components"))
|
|
19
|
+
File.write(File.join(@path, "main.rb"), main_program)
|
|
20
|
+
File.write(File.join(@path, "Components", "Welcome.qml"), welcome_component)
|
|
21
|
+
File.write(File.join(@path, "README.md"), readme)
|
|
22
|
+
@path
|
|
23
|
+
rescue StandardError
|
|
24
|
+
FileUtils.remove_entry(@path) if created && File.directory?(@path)
|
|
25
|
+
raise
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.install_runtime(path, framework_root: FRAMEWORK_ROOT)
|
|
29
|
+
RUNTIME_FILES.each do |file|
|
|
30
|
+
destination = File.join(path, file)
|
|
31
|
+
FileUtils.cp(File.join(framework_root, file), destination) unless File.exist?(destination)
|
|
32
|
+
end
|
|
33
|
+
bundled_runtime = File.join(framework_root, "vendor", "runtime", "x86_64-linux", "omarchy-ui-runtime")
|
|
34
|
+
if File.file?(bundled_runtime)
|
|
35
|
+
destination = File.join(path, "omarchy-ui-runtime")
|
|
36
|
+
FileUtils.cp(bundled_runtime, destination)
|
|
37
|
+
FileUtils.chmod(0o755, destination)
|
|
38
|
+
end
|
|
39
|
+
FileUtils.mkdir_p(File.join(path, "Components"))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def main_program
|
|
45
|
+
<<~RUBY
|
|
46
|
+
# frozen_string_literal: true
|
|
47
|
+
|
|
48
|
+
require "omarchy_ui" unless Object.const_defined?(:OmarchyUI)
|
|
49
|
+
|
|
50
|
+
OmarchyUI.plugin do
|
|
51
|
+
register_component :welcome,
|
|
52
|
+
qml: "Welcome.qml",
|
|
53
|
+
properties: %i[title message]
|
|
54
|
+
|
|
55
|
+
app :main, title: "#{@name}", width: 760, height: 520 do
|
|
56
|
+
component :welcome,
|
|
57
|
+
title: "Welcome to #{@name}",
|
|
58
|
+
message: "This is the official Omarchy UI framework."
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
RUBY
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def welcome_component
|
|
65
|
+
<<~QML
|
|
66
|
+
import QtQuick
|
|
67
|
+
import qs.Commons
|
|
68
|
+
import qs.Ui
|
|
69
|
+
|
|
70
|
+
BorderSurface {
|
|
71
|
+
id: root
|
|
72
|
+
|
|
73
|
+
property string title: "Welcome"
|
|
74
|
+
property string message: "This is the official Omarchy UI framework."
|
|
75
|
+
|
|
76
|
+
implicitWidth: 520
|
|
77
|
+
implicitHeight: 220
|
|
78
|
+
color: Color.popups.background
|
|
79
|
+
radius: Style.cornerRadius
|
|
80
|
+
borderSpec: Border.controlSpec("normal", Color.foreground, Color.accent)
|
|
81
|
+
|
|
82
|
+
Column {
|
|
83
|
+
anchors.centerIn: parent
|
|
84
|
+
spacing: Style.spacing.lg
|
|
85
|
+
|
|
86
|
+
Text {
|
|
87
|
+
anchors.horizontalCenter: parent.horizontalCenter
|
|
88
|
+
text: root.title
|
|
89
|
+
color: Color.foreground
|
|
90
|
+
font.family: Style.font.family
|
|
91
|
+
font.pixelSize: Style.font.heading
|
|
92
|
+
font.bold: true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Text {
|
|
96
|
+
anchors.horizontalCenter: parent.horizontalCenter
|
|
97
|
+
text: root.message
|
|
98
|
+
color: Color.foreground
|
|
99
|
+
font.family: Style.font.family
|
|
100
|
+
font.pixelSize: Style.font.body
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
QML
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def readme
|
|
108
|
+
<<~MARKDOWN
|
|
109
|
+
# #{@name}
|
|
110
|
+
|
|
111
|
+
A standalone application built with the official Omarchy UI framework.
|
|
112
|
+
|
|
113
|
+
## Run
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
omarchy_ui launch main.rb
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The shared `omarchy-ui-runtime` must be installed on `PATH`. No system Ruby,
|
|
120
|
+
application manifest, or copied framework QML files are required.
|
|
121
|
+
|
|
122
|
+
Edit `main.rb` for application state and behavior. Custom QML adapters live in
|
|
123
|
+
`Components/`; `Welcome.qml` is included as a working example.
|
|
124
|
+
MARKDOWN
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmarchyUI
|
|
4
|
+
PROTOCOL_VERSION = 1
|
|
5
|
+
MAX_MESSAGE_BYTES = 1_048_576
|
|
6
|
+
class AsciiPattern
|
|
7
|
+
def initialize(min:, max:, first: nil, rest:)
|
|
8
|
+
@min = min
|
|
9
|
+
@max = max
|
|
10
|
+
@first = first
|
|
11
|
+
@rest = rest
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def match?(value)
|
|
15
|
+
text = value.to_s
|
|
16
|
+
return false if text.length < @min || text.length > @max
|
|
17
|
+
return false if @first && !@first.include?(text[0])
|
|
18
|
+
text.each_char.all? { |character| @rest.include?(character) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
LOWER = ("a".."z").to_a.join.freeze
|
|
23
|
+
UPPER = ("A".."Z").to_a.join.freeze
|
|
24
|
+
DIGITS = ("0".."9").to_a.join.freeze
|
|
25
|
+
VALID_ID = AsciiPattern.new(min: 1, max: 128, rest: LOWER + UPPER + DIGITS + "_.:-")
|
|
26
|
+
VALID_EVENT = AsciiPattern.new(min: 1, max: 64, first: LOWER, rest: LOWER + DIGITS + "_")
|
|
27
|
+
|
|
28
|
+
class ProtocolError < StandardError; end
|
|
29
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module OmarchyUI
|
|
6
|
+
module Runtime
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
BUNDLED = File.expand_path("../../vendor/runtime/x86_64-linux/omarchy-ui-runtime", __dir__)
|
|
10
|
+
|
|
11
|
+
def executable
|
|
12
|
+
override = ENV["OMARCHY_UI_RUNTIME"]
|
|
13
|
+
return File.expand_path(override) if override && !override.empty?
|
|
14
|
+
return BUNDLED if File.executable?(BUNDLED)
|
|
15
|
+
"omarchy-ui-runtime"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def install_shared(destination: nil)
|
|
19
|
+
destination ||= File.expand_path("~/.local/bin/omarchy-ui-runtime")
|
|
20
|
+
raise ArgumentError, "bundled mruby runtime is missing" unless File.file?(BUNDLED)
|
|
21
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
22
|
+
temporary = "#{destination}.install-#{Process.pid}"
|
|
23
|
+
FileUtils.cp(BUNDLED, temporary)
|
|
24
|
+
FileUtils.chmod(0o755, temporary)
|
|
25
|
+
File.rename(temporary, destination)
|
|
26
|
+
destination
|
|
27
|
+
ensure
|
|
28
|
+
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|