zui 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ae2e4258c9dcaea2c3717920199c8cca6a5105b891f2fd3940b2b4843210999
4
- data.tar.gz: b8185c66c6bef8b2a447819f6a474521bf7773246bdd50e7dcc2e5618efd8837
3
+ metadata.gz: 00eeeccbcd56e9deb1c78c93b913ee8e137af12cbd00dab4cc8ba3f4c13e3b9b
4
+ data.tar.gz: dd8e6f8d38493ca5e060be45adf6ffdc7584adac357a4789d59dbdb11bcdc8d5
5
5
  SHA512:
6
- metadata.gz: c17ff8fbdd6c51cc13f924d123ec8a72f789a096dbf965e3cff6b30d7423c29f23090e962422009091e46e4f8889a358043cf3bbe4d07267d103b228d8a2baa7
7
- data.tar.gz: c966b6a472b0b43e3e7eed8f607b17b271e3fda93cb7e23a619a5367a1c320b6e708b7a9f53e26b179e25c69e8e29f6091f41fa3c9e31f2079cd3cfffd51b41e
6
+ metadata.gz: 6bdcbd3b815283726e6854913ff620209452d989080a501dc2c717945a76ecd957e3cda6ddaa11e6ff0ca9c32d960eb07c0832b0012afebc4ca2e0f92c6a2b2c
7
+ data.tar.gz: da469681842aed14ff1176f66bf5b31767802a3097a9d6741560f594dbca66ae7af39a7d32a2d7b2c9931cd7af9adf9a796a083d224bcfc95445cfbc9b9c55a8
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Zui
2
2
 
3
- Zui is a platform-neutral Ruby framework for native desktop interfaces powered by Qt and QML.
4
- Ruby owns the application, component tree, state, events, bindings, animation descriptions, and
5
- business logic. QML is the rendering backend, not application code.
3
+ Zui builds beautiful native desktop applications in pure Ruby, with first-class UI, state,
4
+ events, bindings, animation, media, GPU effects, 3D, and application logic. Applications are
5
+ Ruby and assets—there is no application UI language to learn alongside Ruby.
6
6
 
7
7
  This repository is the reusable core. Distribution integrations live at its edges:
8
8
 
@@ -38,6 +38,7 @@ end
38
38
 
39
39
  ```bash
40
40
  gem install zui
41
+ zui doctor --fix
41
42
  zui new telemetry-console
42
43
  cd telemetry-console
43
44
  zui run main.rb
@@ -48,23 +49,32 @@ Build and install the gem directly from a checkout:
48
49
 
49
50
  ```bash
50
51
  gem build zui.gemspec
51
- gem install ./zui-0.0.1.gem
52
+ gem install ./zui-0.0.2.gem
52
53
  ```
53
54
 
54
- `zui run` uses the bundled host for supported release targets. If a matching host is not
55
- bundled, Zui builds and caches it from the checked-in C++ source with CMake and Qt 6.8 or newer.
55
+ `zui doctor --fix` is the one-time setup for each Zui version and platform. It downloads the
56
+ matching versioned client from GitHub Releases, verifies its SHA-256 checksum and manifest, and
57
+ installs it in the native user cache. `zui configure` performs the same explicit setup operation.
58
+ The prebuilt client is never packaged in or downloaded from the RubyGem. It contains the native
59
+ host and its Qt/QML engine libraries—not the developer's application, Ruby source, or the Zui
60
+ Ruby/QML framework. Setup never changes the global shell environment; `zui run` supplies the
61
+ client paths only to the child application.
56
62
 
57
- `zui bundle` produces an application directory on Linux and Windows and a standard `.app` bundle
58
- on macOS. Each package includes the application, Zui Ruby runtime, QML renderer, component catalog,
59
- theme, controls, and native host. Ruby and the required Qt runtime libraries must be available on
60
- the destination machine; native installers can add them with the platform's normal deployment
61
- tools.
63
+ Developers install Ruby and the `zui` gem. They do not install CMake, a C++ compiler, or Qt. Those
64
+ tools are used only by Zui's release CI to produce the platform clients.
65
+
66
+ `zui bundle` uses a platform template to produce an application directory on Linux and Windows or
67
+ a standard `.app` on macOS. The package combines the application's Ruby/assets, the Zui framework
68
+ runtime and catalog, and a private copy of the configured native Qt/QML client. No system Qt
69
+ installation is used by the finished bundle. The current bundle format expects Ruby 3.1 or newer
70
+ on the destination and supports `ZUI_RUBY` when an installer provides a private Ruby executable.
62
71
 
63
72
  See [Platform support](docs/platforms.md) for host requirements and bundle layouts.
64
73
 
65
- Zui has no separate validation command. `run` opens the app directly, and
66
- `bundle` packages the project directly. Ruby, DSL, protocol, resource, and QML
67
- errors are reported by the operation that actually encounters them.
74
+ `zui doctor` is read-only: it reports platform, Ruby, client, run, and bundle readiness. Add
75
+ `--fix` to download and install the missing client from GitHub Releases. Zui has no separate
76
+ validation command; Ruby, DSL, protocol, resource, and QML errors are reported by the operation
77
+ that encounters them.
68
78
 
69
79
  ## Reusable application UI
70
80
 
data/lib/zui/cli.rb CHANGED
@@ -5,7 +5,7 @@ require "rbconfig"
5
5
 
6
6
  module Zui
7
7
  class CLI
8
- USAGE = "Usage: zui <new NAME|run FILE|bundle [DIRECTORY]|doctor|version>"
8
+ USAGE = "Usage: zui <new NAME|configure|doctor [--fix]|run FILE|bundle [DIRECTORY]|version>"
9
9
 
10
10
  def self.run(arguments, out: $stdout, err: $stderr)
11
11
  new(out:, err:).run(arguments.dup)
@@ -20,6 +20,7 @@ module Zui
20
20
  command = arguments.shift
21
21
  case command
22
22
  when "new" then new_project(arguments)
23
+ when "configure" then configure(arguments)
23
24
  when "run" then run_file(arguments)
24
25
  when "bundle" then bundle_project(arguments)
25
26
  when "doctor" then doctor(arguments)
@@ -63,15 +64,44 @@ module Zui
63
64
  end
64
65
 
65
66
  def doctor(arguments)
66
- raise ArgumentError, "doctor accepts no arguments" unless arguments.empty?
67
+ fix = arguments.delete("--fix")
68
+ raise ArgumentError, "doctor accepts only --fix" unless arguments.empty?
67
69
  platform = Platform.current
68
- host = Host.new(platform:)
69
70
  @out.puts("Zui #{VERSION}")
70
71
  @out.puts("Platform: #{platform.id}#{platform.supported? ? '' : ' (unsupported)'}")
71
72
  @out.puts("Ruby: #{RbConfig.ruby} (#{RUBY_VERSION})")
72
- @out.puts("Host: #{host.executable(build: false) || 'not built'}") if platform.supported?
73
- @out.puts("Qt build requirements: #{host.platform_help}") if platform.supported? && !host.available?
74
- platform.supported? ? 0 : 1
73
+ return 1 unless platform.supported?
74
+
75
+ client = Client.new(platform:)
76
+ if client.configured?
77
+ report_ready(client)
78
+ 0
79
+ elsif fix
80
+ @out.puts("Repair: downloading the verified native client from GitHub Releases...")
81
+ client.configure!
82
+ report_ready(client)
83
+ 0
84
+ else
85
+ @out.puts("Client: not configured")
86
+ @out.puts("Run `zui doctor --fix` to install the native client and bundle support.")
87
+ 1
88
+ end
89
+ end
90
+
91
+ def configure(arguments)
92
+ raise ArgumentError, "configure accepts no arguments" unless arguments.empty?
93
+ platform = Platform.current.assert_supported!
94
+ client = Client.new(platform:)
95
+ @out.puts("Configuring Zui #{VERSION} for #{platform.id}...")
96
+ client.configure!
97
+ report_ready(client)
98
+ 0
99
+ end
100
+
101
+ def report_ready(client)
102
+ @out.puts("Client: #{client.root}")
103
+ @out.puts("Run: ready")
104
+ @out.puts("Bundle: ready")
75
105
  end
76
106
 
77
107
  def option_value(arguments, name)
data/lib/zui/client.rb ADDED
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "net/http"
7
+ require "openssl"
8
+ require "tmpdir"
9
+ require "timeout"
10
+ require "uri"
11
+ require "zlib"
12
+ require "rubygems/package"
13
+
14
+ module Zui
15
+ class Client
16
+ FORMAT = 1
17
+ MAX_ARCHIVE_BYTES = 1_073_741_824
18
+ MAX_EXPANDED_BYTES = 2_147_483_648
19
+ MAX_ENTRIES = 100_000
20
+ REDIRECT_LIMIT = 5
21
+
22
+ attr_reader :platform, :version
23
+
24
+ def initialize(platform: Platform.current, version: VERSION, environment: ENV,
25
+ cache_root: nil, release_base_url: nil, downloader: nil)
26
+ @platform = platform.assert_supported!
27
+ @version = version.to_s
28
+ @environment = environment
29
+ @cache_root = cache_root
30
+ @release_base_url = release_base_url
31
+ @downloader = downloader || method(:download)
32
+ end
33
+
34
+ def root
35
+ override = @environment["ZUI_CLIENT_ROOT"]
36
+ return File.expand_path(override) if override && !override.empty?
37
+
38
+ File.join(cache_root, "zui", "clients", version, platform.id)
39
+ end
40
+
41
+ def configured?
42
+ validate!(root)
43
+ true
44
+ rescue ArgumentError, Errno::ENOENT
45
+ false
46
+ end
47
+
48
+ def configure!
49
+ return root if configured?
50
+ raise ArgumentError, "ZUI_CLIENT_ROOT is not a valid configured client: #{root}" if client_root_override?
51
+
52
+ FileUtils.mkdir_p(File.dirname(root))
53
+ File.open("#{root}.lock", File::RDWR | File::CREAT, 0o600) do |lock|
54
+ lock.flock(File::LOCK_EX)
55
+ return root if configured?
56
+
57
+ install_download!
58
+ end
59
+ root
60
+ end
61
+
62
+ def executable
63
+ manifest = validate!(root)
64
+ File.join(root, manifest.fetch("executable"))
65
+ end
66
+
67
+ def executable_relative_path
68
+ validate!(root).fetch("executable")
69
+ end
70
+
71
+ def environment_entries
72
+ manifest = validate!(root)
73
+ manifest.fetch("environment", {}).transform_values do |paths|
74
+ Array(paths).map { |path| safe_manifest_path!(path, "client environment path") }
75
+ end
76
+ end
77
+
78
+ def environment(base = ENV.to_h, client_root: root)
79
+ environment_entries.each_with_object(base.to_h.dup) do |(name, paths), result|
80
+ unless name.match?(/\A[A-Z][A-Z0-9_]*\z/)
81
+ raise ArgumentError, "invalid environment variable in Zui client: #{name.inspect}"
82
+ end
83
+
84
+ resolved = paths.map { |path| File.join(client_root, path) }
85
+ previous = result[name]
86
+ resolved << previous unless previous.nil? || previous.empty?
87
+ result[name] = resolved.join(File::PATH_SEPARATOR)
88
+ end
89
+ end
90
+
91
+ def copy_to(destination)
92
+ manifest = validate!(root)
93
+ raise ArgumentError, "client destination already exists: #{destination}" if File.exist?(destination)
94
+
95
+ created = true
96
+ FileUtils.mkdir_p(destination)
97
+ FileUtils.cp_r(Dir.children(root).map { |entry| File.join(root, entry) }, destination)
98
+ validate!(destination)
99
+ manifest
100
+ rescue StandardError
101
+ FileUtils.remove_entry(destination) if created && destination && File.exist?(destination)
102
+ raise
103
+ end
104
+
105
+ def manifest
106
+ validate!(root)
107
+ end
108
+
109
+ def archive_name = "zui-client-#{platform.id}.tar.gz"
110
+
111
+ def archive_url
112
+ override = @environment["ZUI_CLIENT_ARCHIVE"]
113
+ return File.expand_path(override) if override && !override.empty?
114
+
115
+ URI.join("#{release_base_url}/", archive_name)
116
+ end
117
+
118
+ def checksum_url
119
+ override = @environment["ZUI_CLIENT_CHECKSUM"]
120
+ return File.expand_path(override) if override && !override.empty?
121
+
122
+ source = archive_url
123
+ source.is_a?(URI) ? URI("#{source}.sha256") : "#{source}.sha256"
124
+ end
125
+
126
+ private
127
+
128
+ def install_download!
129
+ Dir.mktmpdir(".zui-client-", File.dirname(root)) do |temporary|
130
+ archive = File.join(temporary, archive_name)
131
+ checksum = "#{archive}.sha256"
132
+ @downloader.call(archive_url, archive)
133
+ @downloader.call(checksum_url, checksum)
134
+ verify_checksum!(archive, checksum)
135
+
136
+ staged = File.join(temporary, "client")
137
+ FileUtils.mkdir_p(staged)
138
+ extract!(archive, staged)
139
+ validate!(staged)
140
+
141
+ installed = "#{root}.install-#{Process.pid}-#{rand(1_000_000)}"
142
+ File.rename(staged, installed)
143
+ if File.exist?(root)
144
+ invalid = "#{root}.invalid-#{Time.now.to_i}-#{Process.pid}"
145
+ File.rename(root, invalid)
146
+ end
147
+ File.rename(installed, root)
148
+ ensure
149
+ FileUtils.remove_entry(installed) if installed && File.exist?(installed)
150
+ end
151
+ validate!(root)
152
+ root
153
+ end
154
+
155
+ def validate!(directory)
156
+ manifest_path = File.join(directory, "client.json")
157
+ raise ArgumentError, "Zui client manifest not found: #{manifest_path}" unless File.file?(manifest_path)
158
+ raise ArgumentError, "Zui client manifest is too large" if File.size(manifest_path) > 65_536
159
+
160
+ manifest = JSON.parse(File.read(manifest_path))
161
+ raise ArgumentError, "invalid Zui client manifest root" unless manifest.is_a?(Hash)
162
+ expected = {
163
+ "format" => FORMAT,
164
+ "framework" => "zui",
165
+ "client_version" => version,
166
+ "platform" => platform.id
167
+ }
168
+ expected.each do |key, value|
169
+ actual = manifest[key]
170
+ raise ArgumentError, "invalid Zui client #{key}: expected #{value.inspect}, got #{actual.inspect}" unless actual == value
171
+ end
172
+ raise ArgumentError, "Zui client is not bundle capable" unless manifest["bundle_capable"] == true
173
+ unless manifest["payload"] == %w[native-host qt-engine]
174
+ raise ArgumentError, "invalid Zui client payload: expected native host and Qt engine only"
175
+ end
176
+
177
+ required_paths = manifest["required_paths"]
178
+ unless required_paths.is_a?(Array) && !required_paths.empty? &&
179
+ required_paths.all? { |path| path.is_a?(String) }
180
+ raise ArgumentError, "invalid Zui client required paths"
181
+ end
182
+ required_paths.each do |path|
183
+ relative_path = safe_manifest_path!(path, "client required path")
184
+ resolved_path = File.join(directory, relative_path)
185
+ raise ArgumentError, "Zui client required path is missing: #{resolved_path}" unless File.exist?(resolved_path)
186
+ end
187
+
188
+ relative = safe_manifest_path!(manifest["executable"], "client executable")
189
+ executable = File.join(directory, relative)
190
+ unless File.file?(executable) && executable_for_target?(executable)
191
+ raise ArgumentError, "Zui client executable is missing or not executable: #{executable}"
192
+ end
193
+
194
+ client_environment = manifest.fetch("environment", {})
195
+ raise ArgumentError, "invalid Zui client environment" unless client_environment.is_a?(Hash)
196
+ client_environment.each do |name, paths|
197
+ unless name.is_a?(String) && name.match?(/\A[A-Z][A-Z0-9_]*\z/)
198
+ raise ArgumentError, "invalid client environment entry: #{name.inspect}"
199
+ end
200
+ unless paths.is_a?(Array) && !paths.empty? && paths.all? { |path| path.is_a?(String) }
201
+ raise ArgumentError, "invalid client environment paths for #{name}"
202
+ end
203
+ paths.each do |path|
204
+ relative_path = safe_manifest_path!(path, "client environment path")
205
+ resolved_path = File.join(directory, relative_path)
206
+ raise ArgumentError, "Zui client environment path is missing: #{resolved_path}" unless File.exist?(resolved_path)
207
+ end
208
+ end
209
+ manifest
210
+ rescue JSON::ParserError => error
211
+ raise ArgumentError, "invalid Zui client manifest: #{error.message}"
212
+ end
213
+
214
+ def executable_for_target?(path)
215
+ # Windows cannot represent the POSIX executable bit when validating a
216
+ # Linux or macOS archive. Native POSIX clients still require that bit.
217
+ platform.windows? || Gem.win_platform? || File.executable?(path)
218
+ end
219
+
220
+ def verify_checksum!(archive, checksum_file)
221
+ expected = File.read(checksum_file, 4096)[/\A\s*([0-9a-fA-F]{64})(?:\s|\z)/, 1]
222
+ raise ArgumentError, "invalid Zui client checksum file" unless expected
223
+
224
+ actual = Digest::SHA256.file(archive).hexdigest
225
+ raise ArgumentError, "Zui client checksum mismatch" unless actual.casecmp?(expected)
226
+ end
227
+
228
+ def extract!(archive, destination)
229
+ count = 0
230
+ expanded = 0
231
+ Zlib::GzipReader.open(archive) do |gzip|
232
+ Gem::Package::TarReader.new(gzip) do |tar|
233
+ tar.each do |entry|
234
+ count += 1
235
+ raise ArgumentError, "Zui client archive contains too many files" if count > MAX_ENTRIES
236
+
237
+ relative = safe_relative_path!(entry.full_name, "archive entry")
238
+ target = File.join(destination, relative)
239
+ if entry.directory?
240
+ FileUtils.mkdir_p(target)
241
+ elsif entry.file?
242
+ expanded += entry.header.size
243
+ raise ArgumentError, "Zui client archive expands beyond the safety limit" if expanded > MAX_EXPANDED_BYTES
244
+
245
+ FileUtils.mkdir_p(File.dirname(target))
246
+ File.open(target, "wb", entry.header.mode & 0o111 == 0 ? 0o644 : 0o755) do |file|
247
+ IO.copy_stream(entry, file)
248
+ end
249
+ else
250
+ raise ArgumentError, "Zui client archive contains an unsupported link or special file: #{entry.full_name}"
251
+ end
252
+ end
253
+ end
254
+ end
255
+ rescue Zlib::GzipFile::Error, Gem::Package::TarInvalidError => error
256
+ raise ArgumentError, "invalid Zui client archive: #{error.message}"
257
+ end
258
+
259
+ def download(source, destination, redirects = REDIRECT_LIMIT)
260
+ if source.is_a?(String) && File.file?(source)
261
+ FileUtils.cp(source, destination)
262
+ return destination
263
+ end
264
+ uri = source.is_a?(URI) ? source : URI(source.to_s)
265
+ if uri.scheme == "file"
266
+ FileUtils.cp(URI::DEFAULT_PARSER.unescape(uri.path), destination)
267
+ return destination
268
+ end
269
+ raise ArgumentError, "Zui client download must use HTTPS" unless uri.scheme == "https"
270
+ raise ArgumentError, "too many Zui client download redirects" if redirects.negative?
271
+
272
+ http = Net::HTTP.new(uri.host, uri.port, :ENV)
273
+ http.use_ssl = true
274
+ http.open_timeout = 20
275
+ http.read_timeout = 120
276
+ http.write_timeout = 120 if http.respond_to?(:write_timeout=)
277
+ http.start do
278
+ http.request(Net::HTTP::Get.new(uri.request_uri)) do |response|
279
+ case response
280
+ when Net::HTTPSuccess
281
+ written = 0
282
+ File.open(destination, "wb") do |file|
283
+ response.read_body do |chunk|
284
+ written += chunk.bytesize
285
+ raise ArgumentError, "Zui client download exceeds the safety limit" if written > MAX_ARCHIVE_BYTES
286
+ file.write(chunk)
287
+ end
288
+ end
289
+ when Net::HTTPRedirection
290
+ location = response["location"] || raise(ArgumentError, "Zui client redirect has no location")
291
+ return download(URI.join(uri, location), destination, redirects - 1)
292
+ else
293
+ raise ArgumentError, "Zui client download failed: HTTP #{response.code}"
294
+ end
295
+ end
296
+ end
297
+ destination
298
+ rescue URI::InvalidURIError => error
299
+ raise ArgumentError, "invalid Zui client URL: #{error.message}"
300
+ rescue Timeout::Error, SocketError, OpenSSL::SSL::SSLError => error
301
+ raise ArgumentError, "Zui client download failed: #{error.class}: #{error.message}"
302
+ end
303
+
304
+ def safe_relative_path!(value, label)
305
+ path = value.to_s
306
+ if path.empty? || path.include?("\\") || path.match?(/[[:cntrl:]]/) || path.start_with?("/") ||
307
+ path.match?(/\A[A-Za-z]:/) ||
308
+ path.split("/").any? { |part| part.empty? || part == "." || part == ".." }
309
+ raise ArgumentError, "unsafe #{label}: #{value.inspect}"
310
+ end
311
+ path
312
+ end
313
+
314
+ def safe_manifest_path!(value, label)
315
+ path = safe_relative_path!(value, label)
316
+ unless path.match?(/\A[A-Za-z0-9._+ -]+(?:\/[A-Za-z0-9._+ -]+)*\z/)
317
+ raise ArgumentError, "unsafe #{label}: #{value.inspect}"
318
+ end
319
+ path
320
+ end
321
+
322
+ def release_base_url
323
+ @release_base_url || @environment["ZUI_CLIENT_BASE_URL"] ||
324
+ "https://github.com/AdamMusa/zui/releases/download/v#{version}"
325
+ end
326
+
327
+ def cache_root
328
+ return File.expand_path(@cache_root) if @cache_root
329
+
330
+ override = @environment["ZUI_CACHE_HOME"]
331
+ return File.expand_path(override) if override && !override.empty?
332
+
333
+ home = @environment["HOME"] || @environment["USERPROFILE"] || Dir.home
334
+ if platform.windows?
335
+ @environment["LOCALAPPDATA"] || @environment["APPDATA"] || File.join(home, "AppData", "Local")
336
+ elsif platform.macos?
337
+ File.join(home, "Library", "Caches")
338
+ else
339
+ @environment["XDG_CACHE_HOME"] || File.join(home, ".cache")
340
+ end
341
+ end
342
+
343
+ def client_root_override?
344
+ value = @environment["ZUI_CLIENT_ROOT"]
345
+ value && !value.empty?
346
+ end
347
+
348
+ end
349
+ end
@@ -7,20 +7,25 @@ module Zui
7
7
  class Distribution
8
8
  attr_reader :platform
9
9
 
10
- def initialize(host: Host.new, platform: Platform.current, framework_root: FRAMEWORK_ROOT)
11
- @host = host
10
+ def initialize(client: nil, platform: Platform.current, framework_root: FRAMEWORK_ROOT)
12
11
  @platform = platform.assert_supported!
12
+ @client = client || Client.new(platform: @platform)
13
13
  @framework_root = framework_root
14
14
  end
15
15
 
16
16
  def bundle(source, name: nil, destination: nil)
17
+ created = false
17
18
  project = File.expand_path(source)
18
19
  raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
19
20
  raise ArgumentError, "main.rb not found: #{project}" unless File.file?(File.join(project, "main.rb"))
21
+ unless @client.configured?
22
+ raise ArgumentError, "Zui is not configured for #{platform.id}; run `zui doctor --fix` before bundling"
23
+ end
20
24
  app_name = name || titleize(File.basename(project))
21
25
  destination ||= default_destination(project, app_name)
22
26
  destination = File.expand_path(destination)
23
27
  raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
28
+ created = true
24
29
 
25
30
  case platform.os
26
31
  when :linux then bundle_linux(project, destination, app_name)
@@ -29,19 +34,18 @@ module Zui
29
34
  end
30
35
  destination
31
36
  rescue StandardError
32
- FileUtils.remove_entry(destination) if destination && File.exist?(destination)
37
+ FileUtils.remove_entry(destination) if created && destination && File.exist?(destination)
33
38
  raise
34
39
  end
35
40
 
36
41
  private
37
42
 
38
43
  def bundle_linux(project, destination, app_name)
39
- FileUtils.mkdir_p([File.join(destination, "app"), File.join(destination, "bin"),
44
+ FileUtils.mkdir_p([File.join(destination, "app"),
40
45
  File.join(destination, "runtime"), File.join(destination, "share", "applications")])
41
46
  install_application(project, File.join(destination, "app"))
42
47
  install_runtime(File.join(destination, "runtime"))
43
- FileUtils.cp(@host.executable, File.join(destination, "bin", "zui-host"))
44
- FileUtils.chmod(0o755, File.join(destination, "bin", "zui-host"))
48
+ @client.copy_to(File.join(destination, "runtime", "native"))
45
49
  write_linux_launcher(destination, app_name)
46
50
  desktop_name = "#{slug(app_name)}.desktop"
47
51
  File.write(File.join(destination, "share", "applications", desktop_name), <<~DESKTOP)
@@ -62,8 +66,7 @@ module Zui
62
66
  FileUtils.mkdir_p([macos, File.join(resources, "app"), File.join(resources, "runtime")])
63
67
  install_application(project, File.join(resources, "app"))
64
68
  install_runtime(File.join(resources, "runtime"))
65
- FileUtils.cp(@host.executable, File.join(macos, "zui-host"))
66
- FileUtils.chmod(0o755, File.join(macos, "zui-host"))
69
+ @client.copy_to(File.join(resources, "runtime", "native"))
67
70
  File.write(File.join(macos, "run"), macos_launcher(app_name))
68
71
  FileUtils.chmod(0o755, File.join(macos, "run"))
69
72
  File.write(File.join(contents, "Info.plist"), info_plist(app_name))
@@ -71,11 +74,11 @@ module Zui
71
74
  end
72
75
 
73
76
  def bundle_windows(project, destination, app_name)
74
- FileUtils.mkdir_p([File.join(destination, "app"), File.join(destination, "bin"),
77
+ FileUtils.mkdir_p([File.join(destination, "app"),
75
78
  File.join(destination, "runtime")])
76
79
  install_application(project, File.join(destination, "app"))
77
80
  install_runtime(File.join(destination, "runtime"))
78
- FileUtils.cp(@host.executable, File.join(destination, "bin", "zui-host.exe"))
81
+ @client.copy_to(File.join(destination, "runtime", "native"))
79
82
  File.write(File.join(destination, "run.rb"), windows_ruby_launcher(app_name))
80
83
  File.write(File.join(destination, "run.cmd"), windows_command_launcher)
81
84
  write_manifest(destination, app_name)
@@ -93,7 +96,11 @@ module Zui
93
96
  Runtime.install_qml(File.join(destination, "qml"), framework_root: @framework_root)
94
97
  FileUtils.mkdir_p(File.join(destination, "lib"))
95
98
  FileUtils.cp(File.join(@framework_root, "lib", "zui.rb"), File.join(destination, "lib", "zui.rb"))
96
- FileUtils.cp_r(File.join(@framework_root, "lib", "zui"), File.join(destination, "lib", "zui"))
99
+ source = File.join(@framework_root, "lib", "zui")
100
+ target = File.join(destination, "lib", "zui")
101
+ FileUtils.mkdir_p(target)
102
+ entries = Dir.children(source).reject { |entry| %w[client_builder.rb client_packager.rb].include?(entry) }
103
+ FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, target) unless entries.empty?
97
104
  end
98
105
 
99
106
  def write_linux_launcher(destination, app_name)
@@ -101,8 +108,10 @@ module Zui
101
108
  #!/bin/sh
102
109
  set -eu
103
110
  bundle_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
111
+ native_dir="$bundle_dir/runtime/native"
112
+ #{posix_client_environment}
104
113
  ruby_command=${ZUI_RUBY:-ruby}
105
- exec "$bundle_dir/bin/zui-host" \
114
+ exec "$native_dir/#{@client.executable_relative_path}" \
106
115
  --qml-root "$bundle_dir/runtime/qml" \
107
116
  --project "$bundle_dir/app" \
108
117
  --program "$bundle_dir/app/main.rb" \
@@ -119,8 +128,10 @@ module Zui
119
128
  set -eu
120
129
  contents=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
121
130
  resources="$contents/Resources"
131
+ native_dir="$resources/runtime/native"
132
+ #{posix_client_environment}
122
133
  ruby_command=${ZUI_RUBY:-ruby}
123
- exec "$contents/MacOS/zui-host" \
134
+ exec "$native_dir/#{@client.executable_relative_path}" \
124
135
  --qml-root "$resources/runtime/qml" \
125
136
  --project "$resources/app" \
126
137
  --program "$resources/app/main.rb" \
@@ -137,8 +148,11 @@ module Zui
137
148
  require "rbconfig"
138
149
 
139
150
  bundle_dir = File.expand_path(__dir__)
151
+ native_dir = File.join(bundle_dir, "runtime", "native")
152
+ environment = {}
153
+ #{windows_environment_builder}
140
154
  arguments = [
141
- File.join(bundle_dir, "bin", "zui-host.exe"),
155
+ File.join(native_dir, #{@client.executable_relative_path.dump}),
142
156
  "--qml-root", File.join(bundle_dir, "runtime", "qml"),
143
157
  "--project", File.join(bundle_dir, "app"),
144
158
  "--program", File.join(bundle_dir, "app", "main.rb"),
@@ -146,7 +160,7 @@ module Zui
146
160
  "--load-path", File.join(bundle_dir, "runtime", "lib"),
147
161
  "--name", #{app_name.to_s.dump}
148
162
  ]
149
- exec(*arguments)
163
+ exec(environment, *arguments)
150
164
  RUBY
151
165
  end
152
166
 
@@ -183,7 +197,7 @@ module Zui
183
197
  File.write(File.join(directory, "zui-bundle.json"), JSON.pretty_generate(
184
198
  "format" => 1, "framework" => "zui", "version" => VERSION,
185
199
  "platform" => platform.os.to_s, "architecture" => platform.arch.to_s,
186
- "name" => app_name
200
+ "name" => app_name, "client_version" => @client.manifest.fetch("client_version")
187
201
  ))
188
202
  end
189
203
 
@@ -196,5 +210,28 @@ module Zui
196
210
  def slug(value) = value.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
197
211
  def shell_quote(value) = "'#{value.to_s.gsub("'", %q('"'"'))}'"
198
212
  def xml_escape(value) = value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
213
+
214
+ def posix_client_environment
215
+ @client.environment_entries.map do |name, paths|
216
+ value = paths.map { |path| "$native_dir/#{path}" }.join(":")
217
+ <<~SH.chomp
218
+ zui_environment="#{value}"
219
+ [ -z "${#{name}:-}" ] || zui_environment="$zui_environment:${#{name}}"
220
+ export #{name}="$zui_environment"
221
+ SH
222
+ end.join("\n")
223
+ end
224
+
225
+ def windows_client_environment = @client.environment_entries
226
+
227
+ def windows_environment_builder
228
+ @client.environment_entries.keys.map do |name|
229
+ <<~RUBY.chomp
230
+ values = #{windows_client_environment.fetch(name).inspect}.map { |path| File.join(native_dir, path) }
231
+ values << ENV[#{name.dump}] unless ENV[#{name.dump}].nil? || ENV[#{name.dump}].empty?
232
+ environment[#{name.dump}] = values.join(File::PATH_SEPARATOR)
233
+ RUBY
234
+ end.join("\n")
235
+ end
199
236
  end
200
237
  end
data/lib/zui/generator.rb CHANGED
@@ -69,6 +69,7 @@ module Zui
69
69
  A native Linux, macOS, and Windows desktop application written in Ruby with Zui.
70
70
 
71
71
  ```bash
72
+ zui doctor --fix # once for each Zui version
72
73
  zui run main.rb
73
74
  zui bundle
74
75
  ```
data/lib/zui/host.rb CHANGED
@@ -1,123 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "tmpdir"
5
-
6
3
  module Zui
7
4
  class Host
8
- attr_reader :platform
5
+ attr_reader :client, :platform
9
6
 
10
- def initialize(platform: Platform.current, framework_root: FRAMEWORK_ROOT, environment: ENV)
7
+ def initialize(platform: Platform.current, environment: ENV, client: nil)
11
8
  @platform = platform.assert_supported!
12
- @framework_root = framework_root
13
9
  @environment = environment
10
+ @client = client || Client.new(platform:, environment:)
14
11
  end
15
12
 
16
- def executable(build: true)
13
+ def executable
17
14
  override = @environment["ZUI_HOST"]
18
15
  return checked_executable(override) if override && !override.empty?
19
- return bundled if File.executable?(bundled)
20
- return cached if File.executable?(cached)
21
- return nil unless build
16
+ return client.executable if client.configured?
22
17
 
23
- build!(cached)
18
+ raise ArgumentError, "Zui is not configured for #{platform.id}; run `zui doctor --fix`"
24
19
  end
25
20
 
26
- def available? = !executable(build: false).nil?
27
-
28
- def build!(destination = cached)
29
- cmake = find_command("cmake")
30
- raise ArgumentError, platform_help unless cmake
31
-
32
- FileUtils.mkdir_p(File.dirname(destination))
33
- Dir.mktmpdir("zui-host-build-") do |build_dir|
34
- configure = Command.run([
35
- cmake, "-S", File.join(@framework_root, "native"), "-B", build_dir,
36
- "-DCMAKE_BUILD_TYPE=Release"
37
- ], timeout: 180, max_output_bytes: 4_194_304)
38
- raise ArgumentError, "Zui host configuration failed:\n#{configure.stderr}" unless configure.success?
39
-
40
- compile = Command.run([cmake, "--build", build_dir, "--config", "Release", "--parallel"],
41
- timeout: 600, max_output_bytes: 8_388_608)
42
- raise ArgumentError, "Zui host build failed:\n#{compile.stderr}" unless compile.success?
43
-
44
- built = locate_build(build_dir)
45
- raise ArgumentError, "Zui host build produced no executable" unless built
46
- temporary = "#{destination}.install-#{Process.pid}"
47
- FileUtils.cp(built, temporary)
48
- FileUtils.chmod(0o755, temporary)
49
- File.rename(temporary, destination)
50
- ensure
51
- FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
52
- end
53
- destination
54
- end
21
+ def available?
22
+ override = @environment["ZUI_HOST"]
23
+ return File.executable?(File.expand_path(override)) if override && !override.empty?
55
24
 
56
- def platform_help
57
- if platform.macos?
58
- "building Zui on macOS requires CMake and Qt 6 (for example: brew install cmake qt)"
59
- elsif platform.windows?
60
- "building Zui on Windows requires CMake, Qt 6, and a C++17 toolchain (Visual Studio Build Tools or LLVM/MinGW)"
61
- else
62
- "building Zui on Linux requires CMake, a C++17 compiler, and Qt 6 Core/Gui/Qml/Quick development packages"
63
- end
25
+ client.configured?
64
26
  end
65
27
 
66
- private
67
-
68
- def bundled
69
- File.join(@framework_root, "vendor", "host", platform.id, executable_name)
70
- end
28
+ def configure! = client.configure!
71
29
 
72
- def cached
73
- cache_root = @environment["XDG_CACHE_HOME"]
74
- cache_root = default_cache_root if cache_root.nil? || cache_root.empty?
75
- File.join(cache_root, "zui", "host", VERSION, platform.id, executable_name)
76
- end
30
+ def environment(base = ENV.to_h)
31
+ override = @environment["ZUI_HOST"]
32
+ return base.to_h.dup if override && !override.empty?
77
33
 
78
- def default_cache_root
79
- home = @environment["HOME"] || @environment["USERPROFILE"] || Dir.home
80
- if platform.windows?
81
- @environment["LOCALAPPDATA"] || @environment["APPDATA"] || File.join(home, "AppData", "Local")
82
- elsif platform.macos?
83
- File.join(home, "Library", "Caches")
84
- else
85
- File.join(home, ".cache")
86
- end
34
+ client.environment(base)
87
35
  end
88
36
 
89
- def executable_name = platform.os == :windows ? "zui-host.exe" : "zui-host"
37
+ private
90
38
 
91
39
  def checked_executable(path)
92
40
  expanded = File.expand_path(path)
93
41
  raise ArgumentError, "ZUI_HOST is not executable: #{expanded}" unless File.executable?(expanded)
94
42
  expanded
95
43
  end
96
-
97
- def find_command(name)
98
- @environment.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
99
- command_names(name).each do |command_name|
100
- path = File.join(directory, command_name)
101
- return path if File.executable?(path) && !File.directory?(path)
102
- end
103
- end
104
- nil
105
- end
106
-
107
- def command_names(name)
108
- return [name] unless platform.windows? && File.extname(name).empty?
109
-
110
- extensions = @environment.fetch("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
111
- [name] + extensions.flat_map { |extension| ["#{name}#{extension.downcase}", "#{name}#{extension.upcase}"] }.uniq
112
- end
113
-
114
- def locate_build(build_dir)
115
- candidates = [
116
- File.join(build_dir, executable_name),
117
- File.join(build_dir, "Release", executable_name),
118
- File.join(build_dir, "zui-host.app", "Contents", "MacOS", "zui-host")
119
- ]
120
- candidates.find { |path| File.executable?(path) }
121
- end
122
44
  end
123
45
  end
data/lib/zui/runner.rb CHANGED
@@ -24,7 +24,7 @@ module Zui
24
24
  "--load-path", File.join(@framework_root, "lib"),
25
25
  "--name", name || project_name(project)
26
26
  ]
27
- system(environment, *arguments)
27
+ system(@host.environment(environment), *arguments)
28
28
  $?&.exitstatus || 1
29
29
  end
30
30
 
data/lib/zui.rb CHANGED
@@ -14,13 +14,14 @@ require_relative "zui/application"
14
14
  require_relative "zui/source_bundle"
15
15
  require_relative "zui/platform"
16
16
  require_relative "zui/runtime"
17
+ require_relative "zui/client"
17
18
  require_relative "zui/host"
18
19
  require_relative "zui/runner"
19
20
  require_relative "zui/generator"
20
21
  require_relative "zui/distribution"
21
22
 
22
23
  module Zui
23
- VERSION = "0.0.1"
24
+ VERSION = "0.0.2"
24
25
  FRAMEWORK_ROOT = File.expand_path("..", __dir__)
25
26
 
26
27
  def self.app(&definition)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Moussa Ali
@@ -9,7 +9,8 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: A platform-neutral Ruby UI framework powered by Qt and QML.
12
+ description: First-class UI, state, events, animation, media, GPU effects, and 3D
13
+ for Ruby desktop applications.
13
14
  executables:
14
15
  - zui
15
16
  extensions: []
@@ -314,6 +315,7 @@ files:
314
315
  - lib/zui/application.rb
315
316
  - lib/zui/builder.rb
316
317
  - lib/zui/cli.rb
318
+ - lib/zui/client.rb
317
319
  - lib/zui/command.rb
318
320
  - lib/zui/component_registry.rb
319
321
  - lib/zui/components.rb
@@ -329,15 +331,6 @@ files:
329
331
  - lib/zui/source_bundle.rb
330
332
  - lib/zui/state_store.rb
331
333
  - lib/zui/value.rb
332
- - native/CMakeLists.txt
333
- - native/ZuiClipboard.cpp
334
- - native/ZuiClipboard.h
335
- - native/ZuiProcess.cpp
336
- - native/ZuiProcess.h
337
- - native/main.cpp
338
- - vendor/host/linux-x86_64/PROVENANCE.md
339
- - vendor/host/linux-x86_64/zui-host
340
- - vendor/host/linux-x86_64/zui-host.sha256
341
334
  homepage: https://github.com/AdamMusa/zui
342
335
  licenses:
343
336
  - MIT
@@ -361,5 +354,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
361
354
  requirements: []
362
355
  rubygems_version: 4.0.18
363
356
  specification_version: 4
364
- summary: Build cross-platform native desktop applications in Ruby
357
+ summary: Build beautiful native desktop applications in pure Ruby
365
358
  test_files: []
@@ -1,45 +0,0 @@
1
- cmake_minimum_required(VERSION 3.21)
2
- project(zui-host VERSION 0.1.0 LANGUAGES CXX)
3
-
4
- set(CMAKE_CXX_STANDARD 17)
5
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
- set(CMAKE_AUTOMOC ON)
7
-
8
- find_package(Qt6 6.8 REQUIRED COMPONENTS
9
- Core
10
- Gui
11
- Multimedia
12
- Qml
13
- Quick
14
- QuickControls2
15
- QuickVectorImage
16
- )
17
- find_package(Qt6 6.8 QUIET COMPONENTS WebEngineQuick)
18
-
19
- qt_add_executable(zui-host
20
- main.cpp
21
- ZuiClipboard.cpp
22
- ZuiClipboard.h
23
- ZuiProcess.cpp
24
- ZuiProcess.h
25
- )
26
-
27
- target_link_libraries(zui-host PRIVATE
28
- Qt6::Core
29
- Qt6::Gui
30
- Qt6::Multimedia
31
- Qt6::Qml
32
- Qt6::Quick
33
- Qt6::QuickControls2
34
- Qt6::QuickVectorImage
35
- )
36
-
37
- if(TARGET Qt6::WebEngineQuick)
38
- target_link_libraries(zui-host PRIVATE Qt6::WebEngineQuick)
39
- target_compile_definitions(zui-host PRIVATE ZUI_HAS_WEBENGINE=1)
40
- endif()
41
-
42
- install(TARGETS zui-host
43
- BUNDLE DESTINATION .
44
- RUNTIME DESTINATION bin
45
- )
@@ -1,16 +0,0 @@
1
- #include "ZuiClipboard.h"
2
-
3
- #include <QGuiApplication>
4
- #include <QClipboard>
5
-
6
- ZuiClipboard::ZuiClipboard(QObject *parent) : QObject(parent) {
7
- connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &ZuiClipboard::textChanged);
8
- }
9
-
10
- QString ZuiClipboard::text() const { return QGuiApplication::clipboard()->text(); }
11
-
12
- void ZuiClipboard::setText(const QString &text) {
13
- if (this->text() == text)
14
- return;
15
- QGuiApplication::clipboard()->setText(text);
16
- }
@@ -1,16 +0,0 @@
1
- #pragma once
2
-
3
- #include <QObject>
4
-
5
- class ZuiClipboard final : public QObject {
6
- Q_OBJECT
7
- Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
8
-
9
- public:
10
- explicit ZuiClipboard(QObject *parent = nullptr);
11
- QString text() const;
12
- void setText(const QString &text);
13
-
14
- signals:
15
- void textChanged();
16
- };
@@ -1,84 +0,0 @@
1
- #include "ZuiProcess.h"
2
-
3
- #include <QProcessEnvironment>
4
- #include <QTimer>
5
-
6
- ZuiProcess::ZuiProcess(QObject *parent) : QObject(parent) {
7
- connect(&m_process, &QProcess::stateChanged, this, [this] { emit runningChanged(); });
8
- connect(&m_process, &QProcess::readyReadStandardOutput, this, [this] {
9
- m_stdoutBuffer += m_process.readAllStandardOutput();
10
- consume(m_stdoutBuffer, false);
11
- });
12
- connect(&m_process, &QProcess::readyReadStandardError, this, [this] {
13
- m_stderrBuffer += m_process.readAllStandardError();
14
- consume(m_stderrBuffer, true);
15
- });
16
- connect(&m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
17
- emit errorLineReceived(m_process.errorString());
18
- });
19
- connect(&m_process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
20
- [this](int exitCode, QProcess::ExitStatus) {
21
- m_stdoutBuffer += m_process.readAllStandardOutput();
22
- m_stderrBuffer += m_process.readAllStandardError();
23
- flush(m_stdoutBuffer, false);
24
- flush(m_stderrBuffer, true);
25
- emit exited(exitCode);
26
- });
27
- }
28
-
29
- ZuiProcess::~ZuiProcess() { stop(); }
30
-
31
- bool ZuiProcess::running() const { return m_process.state() != QProcess::NotRunning; }
32
-
33
- void ZuiProcess::start(const QString &executable, const QString &program,
34
- const QString &workingDirectory, const QString &loadPath) {
35
- if (running() || executable.isEmpty() || program.isEmpty())
36
- return;
37
-
38
- QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
39
- environment.insert(QStringLiteral("ZUI_PROJECT_DIR"), workingDirectory);
40
- m_process.setProcessEnvironment(environment);
41
- m_process.setWorkingDirectory(workingDirectory);
42
- m_process.setProcessChannelMode(QProcess::SeparateChannels);
43
-
44
- QStringList arguments;
45
- if (!loadPath.isEmpty())
46
- arguments << QStringLiteral("-I") << loadPath;
47
- arguments << program;
48
- m_process.start(executable, arguments, QIODevice::ReadWrite);
49
- }
50
-
51
- void ZuiProcess::write(const QString &data) {
52
- if (!running())
53
- return;
54
- m_process.write(data.toUtf8());
55
- }
56
-
57
- void ZuiProcess::stop() {
58
- if (!running())
59
- return;
60
- m_process.closeWriteChannel();
61
- m_process.terminate();
62
- if (!m_process.waitForFinished(1000)) {
63
- m_process.kill();
64
- m_process.waitForFinished(1000);
65
- }
66
- }
67
-
68
- void ZuiProcess::consume(QByteArray &buffer, bool errorStream) {
69
- qsizetype newline = -1;
70
- while ((newline = buffer.indexOf('\n')) >= 0) {
71
- const QString line = QString::fromUtf8(buffer.left(newline));
72
- buffer.remove(0, newline + 1);
73
- errorStream ? emit errorLineReceived(line) : emit lineReceived(line);
74
- }
75
- }
76
-
77
- void ZuiProcess::flush(QByteArray &buffer, bool errorStream) {
78
- consume(buffer, errorStream);
79
- if (buffer.isEmpty())
80
- return;
81
- const QString line = QString::fromUtf8(buffer);
82
- buffer.clear();
83
- errorStream ? emit errorLineReceived(line) : emit lineReceived(line);
84
- }
data/native/ZuiProcess.h DELETED
@@ -1,34 +0,0 @@
1
- #pragma once
2
-
3
- #include <QObject>
4
- #include <QProcess>
5
-
6
- class ZuiProcess final : public QObject {
7
- Q_OBJECT
8
- Q_PROPERTY(bool running READ running NOTIFY runningChanged)
9
-
10
- public:
11
- explicit ZuiProcess(QObject *parent = nullptr);
12
- ~ZuiProcess() override;
13
-
14
- bool running() const;
15
-
16
- Q_INVOKABLE void start(const QString &executable, const QString &program,
17
- const QString &workingDirectory, const QString &loadPath);
18
- Q_INVOKABLE void write(const QString &data);
19
- Q_INVOKABLE void stop();
20
-
21
- signals:
22
- void runningChanged();
23
- void lineReceived(const QString &line);
24
- void errorLineReceived(const QString &line);
25
- void exited(int exitCode);
26
-
27
- private:
28
- void consume(QByteArray &buffer, bool errorStream);
29
- void flush(QByteArray &buffer, bool errorStream);
30
-
31
- QProcess m_process;
32
- QByteArray m_stdoutBuffer;
33
- QByteArray m_stderrBuffer;
34
- };
data/native/main.cpp DELETED
@@ -1,64 +0,0 @@
1
- #include "ZuiClipboard.h"
2
- #include "ZuiProcess.h"
3
-
4
- #include <QCommandLineParser>
5
- #include <QDir>
6
- #include <QFileInfo>
7
- #include <QGuiApplication>
8
- #include <QQmlApplicationEngine>
9
- #include <QQmlContext>
10
- #include <QQuickStyle>
11
-
12
- #ifdef ZUI_HAS_WEBENGINE
13
- #include <QtWebEngineQuick/qtwebenginequickglobal.h>
14
- #endif
15
-
16
- int main(int argc, char *argv[]) {
17
- #ifdef ZUI_HAS_WEBENGINE
18
- QtWebEngineQuick::initialize();
19
- #endif
20
- QGuiApplication application(argc, argv);
21
- QCoreApplication::setApplicationName(QStringLiteral("Zui"));
22
- QCoreApplication::setOrganizationName(QStringLiteral("Zui"));
23
- QQuickStyle::setStyle(QStringLiteral("Fusion"));
24
-
25
- QCommandLineParser parser;
26
- parser.setApplicationDescription(QStringLiteral("Zui cross-platform Qt host"));
27
- parser.addHelpOption();
28
- parser.addOption({QStringLiteral("qml-root"), QStringLiteral("Zui QML runtime directory"), QStringLiteral("path")});
29
- parser.addOption({QStringLiteral("project"), QStringLiteral("Application project directory"), QStringLiteral("path")});
30
- parser.addOption({QStringLiteral("program"), QStringLiteral("Ruby application entrypoint"), QStringLiteral("path")});
31
- parser.addOption({QStringLiteral("ruby"), QStringLiteral("Ruby executable"), QStringLiteral("path"), QStringLiteral("ruby")});
32
- parser.addOption({QStringLiteral("load-path"), QStringLiteral("Ruby framework load path"), QStringLiteral("path")});
33
- parser.addOption({QStringLiteral("name"), QStringLiteral("Application name"), QStringLiteral("name"), QStringLiteral("Zui Application")});
34
- parser.process(application);
35
-
36
- const QString qmlRootValue = parser.value(QStringLiteral("qml-root"));
37
- const QString projectValue = parser.value(QStringLiteral("project"));
38
- const QString programValue = parser.value(QStringLiteral("program"));
39
- const QString qmlRoot = QFileInfo(qmlRootValue).absoluteFilePath();
40
- const QString project = QFileInfo(projectValue).absoluteFilePath();
41
- const QString program = QFileInfo(programValue).absoluteFilePath();
42
- const QFileInfo desktopFile(QDir(qmlRoot).filePath(QStringLiteral("Desktop.qml")));
43
- if (qmlRootValue.isEmpty() || projectValue.isEmpty() || programValue.isEmpty()
44
- || !desktopFile.isFile() || !QFileInfo(project).isDir() || !QFileInfo(program).isFile())
45
- parser.showHelp(64);
46
-
47
- ZuiProcess process;
48
- ZuiClipboard clipboard;
49
- QQmlApplicationEngine engine;
50
- engine.rootContext()->setContextProperty(QStringLiteral("zuiProcess"), &process);
51
- engine.rootContext()->setContextProperty(QStringLiteral("zuiClipboard"), &clipboard);
52
- engine.rootContext()->setContextProperty(QStringLiteral("zuiProjectDir"), project);
53
- engine.rootContext()->setContextProperty(QStringLiteral("zuiComponentDir"), QDir(qmlRoot).filePath(QStringLiteral("Components/Builtins")));
54
- engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyExecutable"), parser.value(QStringLiteral("ruby")));
55
- engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyProgram"), program);
56
- engine.rootContext()->setContextProperty(QStringLiteral("zuiRubyLoadPath"), parser.value(QStringLiteral("load-path")));
57
- engine.rootContext()->setContextProperty(QStringLiteral("zuiApplicationName"), parser.value(QStringLiteral("name")));
58
- engine.addImportPath(qmlRoot);
59
-
60
- QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &application,
61
- [] { QCoreApplication::exit(1); }, Qt::QueuedConnection);
62
- engine.load(QUrl::fromLocalFile(QDir(qmlRoot).filePath(QStringLiteral("Desktop.qml"))));
63
- return application.exec();
64
- }
@@ -1,15 +0,0 @@
1
- # Zui Linux host provenance
2
-
3
- - Target: Linux x86-64
4
- - Source: `native/` in this repository
5
- - Framework version: Zui 0.1.0
6
- - Compiler: GNU C++ 16.2.1
7
- - Qt: 6.11.2
8
- - Build type: CMake `Release`
9
-
10
- The binary is reproducible from the checked-in C++ sources with:
11
-
12
- ```bash
13
- cmake -S native -B build -DCMAKE_BUILD_TYPE=Release
14
- cmake --build build --parallel
15
- ```
Binary file
@@ -1 +0,0 @@
1
- de446bb598f922a7a58fe30127d88cae9602cf54bd819aa2b5c84447a51da7a4 zui-host