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.
@@ -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