roxane 0.0.1-x64-mingw-ucrt

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2da1266447e9382b95782d156ac447cfe3370b324132ae707f36247f4e88ca5
4
+ data.tar.gz: 0d359d442a7cb58dffffafa269bd2a45d3ed1b430a38dca96a88443d29ad537e
5
+ SHA512:
6
+ metadata.gz: c41309310016de49bf210438955f330274fe0bda6a12cd819bd163d402b2069226ba46280593285ae7e579e0eec60ce5e8072cb0c037b60a280dcdcca4bed70b
7
+ data.tar.gz: 2f9eb6dc4c63eb87170f5baa5ce5ee8f089831777696726350fd925bdbb4853c621975049dd53ff4702f11e35ba222117d273b40f198b8bd4a72126771c2e3f2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BKK Riese
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Roxane
2
+
3
+ A pure-Ruby desktop shell: a native window over the operating system's webview
4
+ (WebView2 / WKWebView / WebKitGTK, via the [webview](https://github.com/webview/webview)
5
+ C library), with a Ruby⇄JS **operation bridge**. A lightweight, Tauri-shaped way
6
+ to ship a web frontend as a desktop app — in Ruby.
7
+
8
+ Named for Cyrano's Roxane.
9
+
10
+ ## What it is (and isn't)
11
+
12
+ Roxane is the **host and data plane** — a window, the system webview, an
13
+ `invoke`/`emit` bridge, and a loopback asset server. It serves whatever frontend
14
+ you give it and is **agnostic to look-and-feel** (no CSS, no widgets, no layout
15
+ opinions). The same frontend can run in the desktop shell and in a hosted browser.
16
+
17
+ ## Example
18
+
19
+ ```ruby
20
+ require "roxane"
21
+
22
+ win = Roxane::Window.new(title: "Hello", size: [900, 600])
23
+
24
+ # JS → Ruby: await window.roxane.invoke("greet", "world")
25
+ win.on("greet") { |name| "Hello, #{name}!" }
26
+
27
+ # Ruby → JS: window.roxane.on("tick", ({ at }) => ...)
28
+ win.emit("tick", { at: Time.now.to_i })
29
+
30
+ win.serve(File.expand_path("ui")) # serve a built frontend bundle, or:
31
+ # win.load("http://localhost:5173") # point at a dev server, or:
32
+ # win.html("<!doctype html>…") # inline HTML
33
+
34
+ win.run
35
+ ```
36
+
37
+ Invocation handlers run **off the UI thread**, and `emit`/`eval` are safe from any
38
+ thread — so long-running work (e.g. an AI call) never freezes the window.
39
+
40
+ ## Requirements
41
+
42
+ The `webview` C library (`libwebview`) must be available at runtime. The
43
+ precompiled platform gem (e.g. `x86_64-linux`) **vendors it**, so you only need
44
+ the system webview runtime (WebKitGTK on Linux). Otherwise set `ROXANE_LIBWEBVIEW`
45
+ to a `libwebview` path, or have it on the loader path.
46
+
47
+ ## Packaging (precompiled platform gems)
48
+
49
+ Roxane ships a prebuilt `libwebview` per platform (the nokogiri model) so users
50
+ need no toolchain. For x86_64-linux:
51
+
52
+ ```sh
53
+ rake vendor:linux # build libwebview into vendor/x86_64-linux/
54
+ rake gem:linux # build roxane-<ver>-x86_64-linux.gem (vendoring the lib)
55
+ rake verify:linux # install that gem in a clean container and smoke-test it
56
+ ```
57
+
58
+ macOS / Windows / aarch64 cross-builds are next.
59
+
60
+ ## Testing
61
+
62
+ ```sh
63
+ ./run.sh # Docker: Ruby + libwebview + headless Xvfb → full suite
64
+ rake test # host: unit tests only (the asset server); the windowed
65
+ # smoke test self-skips unless ROXANE_INTEGRATION=1
66
+ ```
67
+
68
+ ## Status
69
+
70
+ Early but working. The runtime (window + system webview + invoke/emit bridge +
71
+ loopback asset server) is green headlessly, and **precompiled x86_64-linux
72
+ packaging works** — `rake verify:linux` builds the platform gem and proves it
73
+ installs and runs on a clean machine (system WebKitGTK only, no toolchain). Next:
74
+ macOS / Windows / aarch64 cross-builds + RubyGems publish, then richer window
75
+ options (menus, frameless).
76
+
77
+ ## License
78
+
79
+ MIT
data/assets/bridge.js ADDED
@@ -0,0 +1,34 @@
1
+ // Roxane bridge — injected before page scripts. Defines window.roxane, the
2
+ // symmetric counterpart to the Ruby Window API:
3
+ // window.roxane.invoke(op, payload) -> Promise (Ruby `win.on(op){}`)
4
+ // window.roxane.on(event, cb) / off(event, cb) (Ruby `win.emit(event, payload)`)
5
+ (function () {
6
+ if (window.roxane) return;
7
+ var handlers = {}; // event name -> array of callbacks
8
+
9
+ window.roxane = {
10
+ // Call a Ruby operation handler; resolves with its return value (rejects on error).
11
+ invoke: function (op, payload) {
12
+ return window.__roxane_dispatch(op, payload === undefined ? null : payload);
13
+ },
14
+ // Subscribe to an event emitted from Ruby; returns an unsubscribe function.
15
+ on: function (event, cb) {
16
+ (handlers[event] || (handlers[event] = [])).push(cb);
17
+ return function () { window.roxane.off(event, cb); };
18
+ },
19
+ off: function (event, cb) {
20
+ var list = handlers[event];
21
+ if (!list) return;
22
+ var i = list.indexOf(cb);
23
+ if (i !== -1) list.splice(i, 1);
24
+ },
25
+ // Called by Ruby (via eval) to deliver an emitted event.
26
+ __receive: function (event, payload) {
27
+ var list = handlers[event];
28
+ if (!list) return;
29
+ list.slice().forEach(function (cb) {
30
+ try { cb(payload); } catch (e) { console.error("roxane handler for '" + event + "':", e); }
31
+ });
32
+ }
33
+ };
34
+ })();
data/lib/roxane/ffi.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Roxane
6
+ # Thin FFI binding to the maintained webview/webview C library. Resilient at load
7
+ # time: if libwebview can't be found (e.g. on a host without it yet), the gem
8
+ # still loads — `AVAILABLE` is false and `Window.new` raises a clear error —
9
+ # so pure-Ruby parts (the asset server) remain usable and testable.
10
+ module FFI
11
+ extend ::FFI::Library
12
+
13
+ HINT_NONE = 0
14
+ HINT_MIN = 1
15
+ HINT_MAX = 2
16
+ HINT_FIXED = 3
17
+
18
+ begin
19
+ ffi_lib Roxane.library_path
20
+
21
+ # webview_t is an opaque pointer; setters return webview_error_t (int).
22
+ attach_function :webview_create, [:int, :pointer], :pointer
23
+ attach_function :webview_destroy, [:pointer], :int
24
+ # blocking: true releases the GIL while the UI loop runs, so Ruby worker
25
+ # threads (engine/AI work) keep running alongside the webview.
26
+ attach_function :webview_run, [:pointer], :int, blocking: true
27
+ attach_function :webview_terminate, [:pointer], :int
28
+ attach_function :webview_set_title, [:pointer, :string], :int
29
+ attach_function :webview_set_size, [:pointer, :int, :int, :int], :int
30
+ attach_function :webview_navigate, [:pointer, :string], :int
31
+ attach_function :webview_set_html, [:pointer, :string], :int
32
+ attach_function :webview_init, [:pointer, :string], :int
33
+ attach_function :webview_eval, [:pointer, :string], :int
34
+
35
+ callback :bind_fn, [:string, :string, :pointer], :void
36
+ attach_function :webview_bind, [:pointer, :string, :bind_fn, :pointer], :int
37
+ attach_function :webview_return, [:pointer, :string, :int, :string], :int
38
+
39
+ # dispatch runs a function on the UI thread — the safe way to call webview
40
+ # from worker threads (eval / return).
41
+ callback :dispatch_fn, [:pointer, :pointer], :void
42
+ attach_function :webview_dispatch, [:pointer, :dispatch_fn, :pointer], :int
43
+
44
+ AVAILABLE = true
45
+ LOAD_ERROR = nil
46
+ rescue LoadError, ::FFI::NotFoundError => e
47
+ AVAILABLE = false
48
+ LOAD_ERROR = e
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "uri"
5
+
6
+ module Roxane
7
+ # Tiny loopback-only static file server — dependency-free (stdlib sockets). It
8
+ # serves a built frontend bundle to the embedded webview over a real http://
9
+ # origin, so ES modules / fetch / SPA routing behave (which file:// breaks), and
10
+ # the desktop path mirrors the hosted-web one (same frontend, local origin).
11
+ # GET-only, bound to 127.0.0.1, path-traversal-safe.
12
+ class StaticServer
13
+ MIME = {
14
+ ".html" => "text/html", ".htm" => "text/html",
15
+ ".js" => "text/javascript", ".mjs" => "text/javascript",
16
+ ".css" => "text/css", ".json" => "application/json", ".map" => "application/json",
17
+ ".svg" => "image/svg+xml", ".png" => "image/png", ".jpg" => "image/jpeg",
18
+ ".jpeg" => "image/jpeg", ".gif" => "image/gif", ".webp" => "image/webp",
19
+ ".ico" => "image/x-icon", ".woff" => "font/woff", ".woff2" => "font/woff2",
20
+ ".ttf" => "font/ttf", ".wasm" => "application/wasm", ".txt" => "text/plain"
21
+ }.freeze
22
+
23
+ attr_reader :root, :port
24
+
25
+ def initialize(root, spa: true)
26
+ @root = File.expand_path(root)
27
+ @spa = spa
28
+ end
29
+
30
+ # Start on an ephemeral loopback port in a background thread; returns base URL.
31
+ def start
32
+ @server = TCPServer.new("127.0.0.1", 0)
33
+ @port = @server.addr[1]
34
+ @thread = Thread.new { accept_loop }
35
+ "http://127.0.0.1:#{@port}/"
36
+ end
37
+
38
+ def stop
39
+ @server&.close
40
+ @thread&.kill
41
+ end
42
+
43
+ private
44
+
45
+ def accept_loop
46
+ loop { socket = @server.accept; Thread.new(socket) { |s| handle(s) } }
47
+ rescue IOError, Errno::EBADF
48
+ nil # server closed
49
+ end
50
+
51
+ def handle(socket)
52
+ request_line = socket.gets
53
+ return unless request_line
54
+
55
+ method, path, = request_line.split
56
+ while (line = socket.gets) && line != "\r\n"; end # drain headers
57
+ return respond(socket, 405, "text/plain", "method not allowed") unless method == "GET"
58
+
59
+ file = resolve(path)
60
+ if file
61
+ respond(socket, 200, mime(file), File.binread(file))
62
+ else
63
+ respond(socket, 404, "text/plain", "not found")
64
+ end
65
+ rescue Errno::EPIPE, Errno::ECONNRESET
66
+ nil # client went away
67
+ ensure
68
+ socket.close rescue nil
69
+ end
70
+
71
+ # Map a URL path to a file inside root, blocking traversal; SPA-fallback to
72
+ # index.html for extension-less routes.
73
+ def resolve(path)
74
+ rel = URI.decode_www_form_component(path.to_s.split("?", 2).first.to_s)
75
+ rel = "/index.html" if rel == "/" || rel.empty?
76
+ candidate = File.expand_path(File.join(@root, rel))
77
+ return nil unless candidate == @root || candidate.start_with?(@root + File::SEPARATOR)
78
+ return candidate if File.file?(candidate)
79
+
80
+ if @spa && File.extname(rel).empty?
81
+ index = File.join(@root, "index.html")
82
+ return index if File.file?(index)
83
+ end
84
+ nil
85
+ end
86
+
87
+ def mime(file)
88
+ MIME.fetch(File.extname(file).downcase, "application/octet-stream")
89
+ end
90
+
91
+ def respond(socket, status, type, body)
92
+ reason = { 200 => "OK", 404 => "Not Found", 405 => "Method Not Allowed" }.fetch(status, "OK")
93
+ body = body.to_s
94
+ socket.write("HTTP/1.1 #{status} #{reason}\r\n")
95
+ socket.write("Content-Type: #{type}\r\n")
96
+ socket.write("Content-Length: #{body.bytesize}\r\n")
97
+ socket.write("Cache-Control: no-cache\r\n")
98
+ socket.write("Connection: close\r\n\r\n")
99
+ socket.write(body)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roxane
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Roxane
6
+ # A native window hosting the system webview, with a Ruby<->JS operation bridge.
7
+ # The shell/transport only: it serves whatever frontend you give it and is
8
+ # agnostic to look-and-feel.
9
+ #
10
+ # win = Roxane::Window.new(title: "Hello", size: [900, 600])
11
+ # win.on("greet") { |name| "Hello, #{name}!" } # window.roxane.invoke("greet", "x")
12
+ # win.emit("tick", { at: 1 }) # window.roxane.on("tick", cb)
13
+ # win.serve(File.expand_path("ui")) # serve a built frontend dir
14
+ # win.run
15
+ class Window
16
+ DISPATCH = "__roxane_dispatch" # the single C binding behind window.roxane.invoke
17
+
18
+ def initialize(title: "Roxane", size: [800, 600], min_size: nil, resizable: true, debug: false)
19
+ unless FFI::AVAILABLE
20
+ raise Error, "libwebview is not available (#{FFI::LOAD_ERROR}). " \
21
+ "Set ROXANE_LIBWEBVIEW or install the webview C library."
22
+ end
23
+
24
+ @handlers = {} # operation name (String) -> handler block
25
+ @callbacks = [] # retained FFI::Function refs (must outlive the window)
26
+ @on_close = nil
27
+ @server = nil
28
+ @ui_queue = Queue.new # procs to run on the UI thread
29
+
30
+ @w = FFI.webview_create(debug ? 1 : 0, nil)
31
+ raise Error, "could not create webview" if @w.null?
32
+
33
+ FFI.webview_set_title(@w, title.to_s)
34
+ apply_size(size, min_size, resizable)
35
+ install_ui_pump
36
+ install_bridge
37
+ end
38
+
39
+ # --- public API ---------------------------------------------------------
40
+
41
+ # Handle window.roxane.invoke(op, payload). The block runs OFF the UI thread;
42
+ # its return value resolves the JS promise, a raised error rejects it.
43
+ def on(op, &block)
44
+ @handlers[op.to_s] = block
45
+ self
46
+ end
47
+
48
+ # Push an event to the frontend (delivered to window.roxane.on(event, cb)).
49
+ # Safe to call from any thread.
50
+ def emit(event, payload = nil)
51
+ eval("window.roxane && window.roxane.__receive(#{JSON.generate(event.to_s)}, #{JSON.generate(payload)})")
52
+ end
53
+
54
+ # Run arbitrary JS in the page. Safe from any thread (marshalled to the UI thread).
55
+ def eval(js)
56
+ on_ui_thread { FFI.webview_eval(@w, js.to_s) }
57
+ self
58
+ end
59
+
60
+ # Low-level escape hatch: expose window.<name>(...args) -> Promise.
61
+ def bind(name, &block)
62
+ cb = ::FFI::Function.new(:void, [:string, :string, :pointer]) do |seq, req, _arg|
63
+ run_handler(seq) { block.call(*JSON.parse(req)) }
64
+ end
65
+ @callbacks << cb
66
+ FFI.webview_bind(@w, name.to_s, cb, nil)
67
+ self
68
+ end
69
+
70
+ # Serve a built frontend directory over loopback http and load it.
71
+ def serve(dir)
72
+ @server = StaticServer.new(dir)
73
+ load(@server.start)
74
+ end
75
+
76
+ def load(url)
77
+ FFI.webview_navigate(@w, url.to_s)
78
+ self
79
+ end
80
+
81
+ def html(markup)
82
+ FFI.webview_set_html(@w, markup.to_s)
83
+ self
84
+ end
85
+
86
+ def on_close(&block)
87
+ @on_close = block
88
+ self
89
+ end
90
+
91
+ # Run the native UI loop. Blocks the calling thread until the window closes.
92
+ def run
93
+ FFI.webview_run(@w)
94
+ ensure
95
+ @on_close&.call
96
+ @server&.stop
97
+ FFI.webview_destroy(@w)
98
+ end
99
+
100
+ # Stop the loop / close the window. Safe from any thread.
101
+ def close
102
+ FFI.webview_terminate(@w)
103
+ end
104
+
105
+ # --- internals ----------------------------------------------------------
106
+ private
107
+
108
+ def apply_size(size, min_size, resizable)
109
+ width, height = size
110
+ FFI.webview_set_size(@w, width, height, resizable ? FFI::HINT_NONE : FFI::HINT_FIXED)
111
+ FFI.webview_set_size(@w, min_size[0], min_size[1], FFI::HINT_MIN) if min_size && resizable
112
+ end
113
+
114
+ # One persistent dispatch callback drains a queue of procs on the UI thread —
115
+ # the safe way to call into webview from worker threads.
116
+ def install_ui_pump
117
+ @ui_pump = ::FFI::Function.new(:void, [:pointer, :pointer]) do |_w, _arg|
118
+ job = (@ui_queue.pop(true) rescue nil)
119
+ job&.call
120
+ end
121
+ @callbacks << @ui_pump
122
+ end
123
+
124
+ def on_ui_thread(&block)
125
+ @ui_queue << block
126
+ FFI.webview_dispatch(@w, @ui_pump, nil)
127
+ end
128
+
129
+ def install_bridge
130
+ cb = ::FFI::Function.new(:void, [:string, :string, :pointer]) do |seq, req, _arg|
131
+ op, payload = JSON.parse(req)
132
+ handler = @handlers[op]
133
+ if handler
134
+ run_handler(seq) { handler.call(payload) }
135
+ else
136
+ resolve(seq, 1, { "error" => "no handler for operation: #{op}" })
137
+ end
138
+ end
139
+ @callbacks << cb
140
+ FFI.webview_bind(@w, DISPATCH, cb, nil)
141
+ FFI.webview_init(@w, bridge_js)
142
+ end
143
+
144
+ # Run a handler off the UI thread, then resolve/reject the JS promise.
145
+ def run_handler(seq)
146
+ Thread.new do
147
+ resolve(seq, 0, yield)
148
+ rescue StandardError => e
149
+ resolve(seq, 1, { "error" => e.message })
150
+ end
151
+ end
152
+
153
+ def resolve(seq, status, value)
154
+ on_ui_thread { FFI.webview_return(@w, seq, status, JSON.generate(value)) }
155
+ end
156
+
157
+ def bridge_js
158
+ @bridge_js ||= File.read(File.expand_path("../../assets/bridge.js", __dir__))
159
+ end
160
+ end
161
+ end
data/lib/roxane.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+ require_relative "roxane/version"
6
+
7
+ # Roxane — a pure-Ruby desktop shell. A native window hosting the operating
8
+ # system's webview (WebView2 / WKWebView / WebKitGTK, via the `webview` C library),
9
+ # bridged to Ruby with an invoke/emit operation channel. The host/transport only:
10
+ # it serves whatever web frontend you give it and is agnostic to look-and-feel.
11
+ #
12
+ # Named for Cyrano's Roxane.
13
+ module Roxane
14
+ class Error < StandardError; end
15
+
16
+ # Path to the libwebview shared library: an explicit override wins, then a
17
+ # vendored (precompiled) copy shipped with the gem, then the platform's default
18
+ # name resolved by the system loader.
19
+ def self.library_path
20
+ ENV["ROXANE_LIBWEBVIEW"] || bundled_library || default_library_name
21
+ end
22
+
23
+ def self.bundled_library
24
+ platform_dir = File.join(__dir__, "..", "vendor", Gem::Platform.local.to_s)
25
+ Dir[File.join(platform_dir, "libwebview.{so,dylib,dll}*")].sort.first ||
26
+ Dir[File.join(__dir__, "..", "vendor", "**", "libwebview.{so,dylib,dll}*")].sort.first
27
+ end
28
+
29
+ def self.default_library_name
30
+ case RbConfig::CONFIG["host_os"]
31
+ when /darwin/ then "libwebview.dylib"
32
+ when /mswin|mingw|cygwin/ then "webview.dll"
33
+ else "libwebview.so"
34
+ end
35
+ end
36
+ end
37
+
38
+ require_relative "roxane/ffi"
39
+ require_relative "roxane/static_server"
40
+ require_relative "roxane/window"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roxane
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: x64-mingw-ucrt
6
+ authors:
7
+ - BKK Riese
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ description: Roxane embeds the operating system's webview (WebView2 / WKWebView /
28
+ WebKitGTK, via the webview C library) in a native window and bridges it to Ruby
29
+ with an invoke/emit operation channel — a lightweight, Tauri-shaped way to ship
30
+ a web frontend as a desktop app, in Ruby.
31
+ email:
32
+ - brian@quarterstack.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - LICENSE
38
+ - README.md
39
+ - assets/bridge.js
40
+ - lib/roxane.rb
41
+ - lib/roxane/ffi.rb
42
+ - lib/roxane/static_server.rb
43
+ - lib/roxane/version.rb
44
+ - lib/roxane/window.rb
45
+ - vendor/x64-mingw-ucrt/libwebview.dll
46
+ homepage: https://github.com/quarterstack/roxane
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '3.1'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.5.22
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: 'Pure-Ruby desktop shell: a native window over the system webview, bridged
69
+ to Ruby.'
70
+ test_files: []