tokra 0.0.1.pre.1

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.pre-commit-config.yaml +16 -0
  3. data/AGENTS.md +126 -0
  4. data/CHANGELOG.md +21 -0
  5. data/CODE_OF_CONDUCT.md +16 -0
  6. data/Cargo.toml +23 -0
  7. data/LICENSE +661 -0
  8. data/LICENSES/AGPL-3.0-or-later.txt +235 -0
  9. data/LICENSES/Apache-2.0.txt +73 -0
  10. data/LICENSES/CC-BY-SA-4.0.txt +170 -0
  11. data/LICENSES/CC0-1.0.txt +121 -0
  12. data/LICENSES/MIT.txt +18 -0
  13. data/README.md +45 -0
  14. data/README.rdoc +4 -0
  15. data/REUSE.toml +11 -0
  16. data/Rakefile +27 -0
  17. data/Steepfile +15 -0
  18. data/clippy.toml +5 -0
  19. data/clippy_exceptions.rb +59 -0
  20. data/doc/contributors/adr/001.md +187 -0
  21. data/doc/contributors/adr/002.md +132 -0
  22. data/doc/contributors/adr/003.md +116 -0
  23. data/doc/contributors/chats/001.md +3874 -0
  24. data/doc/contributors/plan/001.md +271 -0
  25. data/examples/verify_hello_world/app.rb +114 -0
  26. data/examples/verify_hello_world/index.html +88 -0
  27. data/examples/verify_ping_pong/README.md +0 -0
  28. data/examples/verify_ping_pong/app.rb +132 -0
  29. data/examples/verify_ping_pong/public/styles.css +182 -0
  30. data/examples/verify_ping_pong/views/index.erb +94 -0
  31. data/examples/verify_ping_pong/views/layout.erb +22 -0
  32. data/exe/semantic-highlight +0 -0
  33. data/ext/tokra/Cargo.toml +23 -0
  34. data/ext/tokra/extconf.rb +12 -0
  35. data/ext/tokra/src/lib.rs +719 -0
  36. data/lib/tokra/native.rb +79 -0
  37. data/lib/tokra/rack/handler.rb +177 -0
  38. data/lib/tokra/version.rb +12 -0
  39. data/lib/tokra.rb +19 -0
  40. data/mise.toml +8 -0
  41. data/rustfmt.toml +4 -0
  42. data/sig/tokra.rbs +7 -0
  43. data/tasks/lint.rake +151 -0
  44. data/tasks/rust.rake +63 -0
  45. data/tasks/steep.rake +11 -0
  46. data/tasks/test.rake +26 -0
  47. data/test_native.rb +37 -0
  48. data/vendor/goodcop/base.yml +1047 -0
  49. metadata +112 -0
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ #
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+ #++
8
+
9
+ module Tokra
10
+ # Native Rust bindings for tao (windowing) and wry (WebView).
11
+ #
12
+ # These classes are thin wrappers around the Rust FFI layer.
13
+ # All application logic should remain in Ruby; these are "dumb pipes"
14
+ # for platform-native operations only.
15
+ #
16
+ # ## Thread Safety
17
+ #
18
+ # EventLoop, Window, and WebView are main-thread-only types. They use
19
+ # Tauri's pattern of `unsafe impl Send` with the invariant that all
20
+ # access happens on the main thread via the event loop callback.
21
+ #
22
+ # Only Proxy is genuinely Send-safe and can be shared with Worker Ractors.
23
+ #
24
+ # @example Basic usage
25
+ # event_loop = Tokra::Native::EventLoop.new
26
+ # proxy = event_loop.create_proxy
27
+ #
28
+ # window = Tokra::Native::Window.new(event_loop)
29
+ # window.set_title("My App")
30
+ # window.set_size(800, 600)
31
+ #
32
+ # ipc_handler = Ractor.shareable_proc { |msg| puts msg }
33
+ # webview = Tokra::Native::WebView.new(window, "https://example.com", ipc_handler, proxy)
34
+ #
35
+ # event_loop.run do |event|
36
+ # case event
37
+ # when Tokra::Native::IpcEvent
38
+ # puts "IPC: #{event.message}"
39
+ # when Tokra::Native::WakeUpEvent
40
+ # puts "Wake: #{event.payload}"
41
+ # when Tokra::Native::WindowCloseEvent
42
+ # puts "Goodbye!"
43
+ # end
44
+ # end
45
+ #
46
+ # @see Tokra::Native::EventLoop
47
+ # @see Tokra::Native::Window
48
+ # @see Tokra::Native::WebView
49
+ # @see Tokra::Native::Proxy
50
+ module Native
51
+ # Native classes defined in Rust extension:
52
+ #
53
+ # - EventLoop: Main thread event loop
54
+ # - .new -> EventLoop
55
+ # - #create_proxy -> Proxy (must call before #run)
56
+ # - #run(callback) -> never returns
57
+ #
58
+ # - Window: Native window
59
+ # - .new(event_loop) -> Window
60
+ # - #set_title(string)
61
+ # - #set_size(width, height)
62
+ # - #id -> String
63
+ #
64
+ # - WebView: WebKit/WebView2 webview
65
+ # - .new(window, url, ipc_callback, proxy) -> WebView
66
+ # - #eval(js_string)
67
+ #
68
+ # - Proxy: Thread-safe handle (genuinely Send-safe)
69
+ # - #wake_up(payload) - Send wake event to main thread
70
+ #
71
+ # - IpcEvent: Event fired when WebView sends IPC message
72
+ # - #message -> String
73
+ #
74
+ # - WakeUpEvent: Event fired when Proxy#wake_up is called
75
+ # - #payload -> String
76
+ #
77
+ # - WindowCloseEvent: Event fired when window close is requested
78
+ end
79
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ require "uri"
9
+ require "stringio"
10
+
11
+ module Rack # :nodoc:
12
+ module Handler # :nodoc:
13
+ # Tokra Rack Handler - Bridge between tokra:// custom protocol and Rack apps.
14
+ #
15
+ # This allows any Rack application (Rails, Roda, Sinatra, etc.) to run inside
16
+ # a Tokra desktop window without a TCP server. The WebView makes requests to
17
+ # +tokra://localhost/path+ and this handler translates them to Rack calls.
18
+ #
19
+ # == Usage
20
+ #
21
+ # The simplest way to run a Rack app in a desktop window:
22
+ #
23
+ # Rack::Handler::Tokra.run(MyRodaApp)
24
+ #
25
+ # With options:
26
+ #
27
+ # Rack::Handler::Tokra.run(MyRodaApp, title: "My App", width: 800, height: 600)
28
+ #
29
+ # This follows the standard Rack handler interface, so it works with +rackup+:
30
+ #
31
+ # rackup -s tokra config.ru
32
+ #
33
+ class Tokra
34
+ MAX_REDIRECTS = 10 # :nodoc:
35
+
36
+ # Default window configuration
37
+ DEFAULT_OPTIONS = {
38
+ title: "Tokra",
39
+ width: 800.0,
40
+ height: 600.0,
41
+ }.freeze
42
+
43
+ class << self
44
+ # Run a Rack application in a Tokra desktop window.
45
+ #
46
+ # This is the primary entry point, handling all wiring automatically:
47
+ # - Creates the event loop, proxy, window, and webview
48
+ # - Registers the tokra:// protocol handler
49
+ # - Dispatches HTTP requests to the Rack app
50
+ # - Runs the event loop until the window closes
51
+ #
52
+ # +app+ is any Rack application (responds to #call).
53
+ # +options+ may include:
54
+ # - +:title+ - Window title (default: "Tokra")
55
+ # - +:width+ - Window width in pixels (default: 800)
56
+ # - +:height+ - Window height in pixels (default: 600)
57
+ #
58
+ # == Example
59
+ #
60
+ # class MyApp < Roda
61
+ # route { |r| r.root { "Hello" } }
62
+ # end
63
+ #
64
+ # Rack::Handler::Tokra.run(MyApp.freeze.app)
65
+ #
66
+ def run(app, **options)
67
+ opts = DEFAULT_OPTIONS.merge(options)
68
+
69
+ # Create Tokra components
70
+ event_loop = ::Tokra::Native::EventLoop.new
71
+ proxy = event_loop.create_proxy
72
+
73
+ window = ::Tokra::Native::Window.new(event_loop)
74
+ window.set_title(opts[:title])
75
+ window.set_size(opts[:width].to_f, opts[:height].to_f)
76
+
77
+ # Create the handler
78
+ handler = new(app, proxy)
79
+
80
+ # Create WebView with tokra:// protocol
81
+ ::Tokra::Native::WebView.new_with_protocol(window, proxy)
82
+
83
+ # Run the event loop, dispatching HTTP requests to the Rack app
84
+ event_loop.run(
85
+ lambda { |event|
86
+ handler.call(event) if event.is_a?(::Tokra::Native::HttpRequestEvent)
87
+ }
88
+ )
89
+ end
90
+ end
91
+
92
+ # Create a new Rack handler.
93
+ #
94
+ # +app+ is any Rack application (responds to #call).
95
+ # +proxy+ is the Tokra::Native::Proxy for responding to requests.
96
+ #
97
+ # Note: For most use cases, prefer +Rack::Handler::Tokra.run(app)+
98
+ # which handles all setup automatically.
99
+ def initialize(app, proxy)
100
+ @app = app
101
+ @proxy = proxy
102
+ end
103
+
104
+ # Handle an HTTP request event from the custom protocol.
105
+ #
106
+ # This method:
107
+ # 1. Translates the HttpRequestEvent into a Rack environment
108
+ # 2. Calls the Rack app
109
+ # 3. Follows redirects internally (WKWebView doesn't follow redirects for custom protocols)
110
+ # 4. Sends the final response back through the protocol
111
+ def call(event)
112
+ uri = URI.parse(event.uri)
113
+ env = build_env(uri, event.method, event.body)
114
+
115
+ # Follow redirects internally (WKWebView doesn't follow for custom protocols)
116
+ redirect_count = 0
117
+ cookies = nil
118
+
119
+ loop do
120
+ status, headers, body = @app.call(env)
121
+
122
+ # Check for redirect (3xx with Location header)
123
+ if (300..399).cover?(status) && headers["location"]
124
+ body.close if body.respond_to?(:close)
125
+
126
+ redirect_count += 1
127
+ if redirect_count > MAX_REDIRECTS
128
+ break respond_with_error(event.request_id, 508, "Too many redirects")
129
+ end
130
+
131
+ # Capture Set-Cookie for the next request (flash/sessions need this)
132
+ cookies = headers["set-cookie"] if headers["set-cookie"]
133
+
134
+ # Follow the redirect with GET (PRG pattern)
135
+ new_uri = URI.parse(headers["location"])
136
+ env = build_env(new_uri, "GET", "", cookies)
137
+ next
138
+ end
139
+
140
+ # Send the response
141
+ header_pairs = headers.map { |k, v| [k.to_s, v.to_s] }
142
+
143
+ body_str = String.new
144
+ body.each { |chunk| body_str << chunk }
145
+ body.close if body.respond_to?(:close)
146
+
147
+ @proxy.respond(event.request_id, status, header_pairs, body_str)
148
+ break
149
+ end
150
+ end
151
+
152
+ private def build_env(uri, method, body, cookies = nil)
153
+ env = {
154
+ "REQUEST_METHOD" => method,
155
+ "SCRIPT_NAME" => "",
156
+ "PATH_INFO" => uri.path.empty? ? "/" : uri.path,
157
+ "QUERY_STRING" => uri.query || "",
158
+ "SERVER_NAME" => uri.host || "localhost",
159
+ "SERVER_PORT" => (uri.port || 80).to_s,
160
+ "rack.version" => [2, 0],
161
+ "rack.url_scheme" => "tokra",
162
+ "rack.input" => StringIO.new(body),
163
+ "rack.errors" => $stderr,
164
+ "rack.multithread" => false,
165
+ "rack.multiprocess" => false,
166
+ "rack.run_once" => false,
167
+ }
168
+ env["HTTP_COOKIE"] = cookies if cookies
169
+ env
170
+ end
171
+
172
+ private def respond_with_error(request_id, status, message)
173
+ @proxy.respond(request_id, status, [["content-type", "text/plain"]], message)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ #
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+ #++
8
+
9
+ module Tokra
10
+ # TODO: Document me.
11
+ VERSION = "0.0.1.pre.1"
12
+ end
data/lib/tokra.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ #
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+ #++
8
+
9
+ require_relative "tokra/version"
10
+ require_relative "tokra/native"
11
+ require_relative "tokra/tokra"
12
+ require_relative "tokra/rack/handler"
13
+
14
+ # Tokra is a port of Tauri to Ruby.
15
+ module Tokra
16
+ # TODO: Document me.
17
+ class Error < StandardError; end
18
+ # Your code goes here...
19
+ end
data/mise.toml ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [tools]
5
+ ruby = "4.0.1"
6
+ python = "3.12"
7
+ pre-commit = "latest"
8
+ rust = "1.93.0"
data/rustfmt.toml ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ edition = "2021"
data/sig/tokra.rbs ADDED
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ module Tokra
5
+ VERSION: String
6
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
7
+ end
data/tasks/lint.rake ADDED
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ require "rubocop/rake_task"
9
+ require "rubycritic/rake_task"
10
+ require "inch/rake"
11
+
12
+ RuboCop::RakeTask.new
13
+
14
+ # Run rubycritic in a shell to prevent it from exiting the rake process
15
+ task :rubycritic do
16
+ sh "bundle exec rubycritic --no-browser exe lib sig"
17
+ end
18
+
19
+ Inch::Rake::Suggest.new("doc:suggest", "exe/**/*.rb", "lib/**/*.rb", "sig/**/*.rbs") do |suggest|
20
+ suggest.args << ""
21
+ end
22
+
23
+ namespace :reuse do
24
+ desc "Run the REUSE Tool to confirm REUSE compliance"
25
+ task :lint do
26
+ sh "reuse lint"
27
+ end
28
+
29
+ desc "Add SPDX headers to files missing them (per AGENTS.md standards)"
30
+ task :fix do
31
+ copyright = "Kerrick Long <me@kerricklong.com>"
32
+
33
+ # Code files: AGPL-3.0-or-later
34
+ code_extensions = %w[rb rbs rs rake gemspec rbs toml yml yaml json lock].freeze
35
+ code_license = "AGPL-3.0-or-later"
36
+
37
+ # Documentation files: CC-BY-SA-4.0
38
+ doc_extensions = %w[md txt].freeze
39
+ doc_license = "CC-BY-SA-4.0"
40
+
41
+ # Find files missing headers (listed after "no copyright and licensing" message)
42
+ puts "Checking for files missing REUSE headers..."
43
+ output = `reuse lint 2>&1`
44
+ in_missing_section = false
45
+ missing_files = output.lines.filter_map do |line|
46
+ in_missing_section = true if line.include?("no copyright and licensing")
47
+ in_missing_section = false if line.start_with?("# ") && !line.include?("copyright")
48
+ next unless in_missing_section
49
+
50
+ line.match(/^\* (.+)/)&.[](1)
51
+ end
52
+
53
+ if missing_files.empty?
54
+ puts "All files have REUSE headers!"
55
+ else
56
+ missing_files.each do |file|
57
+ ext = File.extname(file).delete(".")
58
+ license = if code_extensions.include?(ext)
59
+ code_license
60
+ elsif doc_extensions.include?(ext)
61
+ doc_license
62
+ else
63
+ puts " Skipping #{file} (unknown extension: .#{ext})"
64
+ next
65
+ end
66
+
67
+ puts " Annotating #{file} with #{license}"
68
+ sh "reuse annotate --license #{license} --copyright '#{copyright}' --skip-existing '#{file}'", verbose: false
69
+ end
70
+ end
71
+ end
72
+
73
+ desc "Normalize Ruby files: frozen_string_literal at top, SPDX in #--/#++ block"
74
+ task :normalize_ruby do
75
+ ruby_extensions = %w[rb rake gemspec].freeze
76
+ ruby_files = Dir.glob("**/*.{#{ruby_extensions.join(',')}}")
77
+ .reject { |f| f.start_with?("vendor/", "tmp/", ".") }
78
+
79
+ fixed_count = 0
80
+ ruby_files.each do |file|
81
+ content = File.read(file)
82
+ original = content.dup
83
+
84
+ # Skip if no SPDX header
85
+ next unless content.match?(/# SPDX-/)
86
+
87
+ # Extract components
88
+ frozen = content.match?(/^# frozen_string_literal: true/)
89
+ spdx_match = content.match(/(# SPDX-FileCopyrightText:[^\n]+\n(?:#[^\n]*\n)*# SPDX-License-Identifier:[^\n]+\n)/m)
90
+ next unless spdx_match
91
+
92
+ spdx_block = spdx_match[1]
93
+
94
+ # Remove existing frozen_string_literal and SPDX block (and any #--/#+++)
95
+ cleaned = content
96
+ .sub(/^# frozen_string_literal: true\n+/, "")
97
+ .sub(/^#--\s*\n/, "")
98
+ .sub(spdx_block, "")
99
+ .sub(/^#\+\+\s*\n/, "")
100
+ .sub(/\A\n+/, "") # Remove leading blank lines
101
+
102
+ # Rebuild file in correct order: frozen, blank, #--, SPDX, #++, rest
103
+ new_content = ""
104
+ new_content += "# frozen_string_literal: true\n\n" if frozen
105
+ new_content += "#--\n#{spdx_block}#++\n\n"
106
+ new_content += cleaned.sub(/\A\n+/, "") # Ensure no double blank lines
107
+
108
+ if new_content != original
109
+ File.write(file, new_content)
110
+ puts " Normalized #{file}"
111
+ fixed_count += 1
112
+ end
113
+ end
114
+
115
+ puts fixed_count.zero? ? "All Ruby files properly normalized!" : "Fixed #{fixed_count} files."
116
+ end
117
+ end
118
+ task(:reuse) { Rake::Task["reuse:lint"].invoke }
119
+
120
+ namespace :lint do
121
+ task :safe_rdoc_coverage do
122
+ sh "bundle exec rake rdoc:coverage"
123
+ end
124
+
125
+ task docs: %w[rdoc:coverage rubycritic reuse:lint]
126
+ task code: %w[rubocop rubycritic lint:rust]
127
+ task licenses: %w[reuse:lint]
128
+ task all: %w[docs code licenses]
129
+
130
+ namespace :fix do
131
+ desc "Auto-fix RuboCop offenses (most aggressive)"
132
+ task :rubocop do
133
+ sh "bundle exec rubocop --autocorrect-all"
134
+ end
135
+
136
+ desc "Add SPDX headers and normalize Ruby file structure"
137
+ task reuse: %w[reuse:fix reuse:normalize_ruby]
138
+
139
+ desc "Run all auto-fix tasks"
140
+ task all: %w[lint:fix:rubocop lint:fix:reuse rust:fmt:fix]
141
+ end
142
+ end
143
+
144
+ desc "Run all lint auto-fix tasks"
145
+ task("lint:fix") { Rake::Task["lint:fix:all"].invoke }
146
+
147
+ # Aliases for convenience
148
+ task "rubocop:autocorrect_all" => "lint:fix:rubocop"
149
+
150
+ desc "Run all lint tasks"
151
+ task(:lint) { Rake::Task["lint:all"].invoke }
data/tasks/rust.rake ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ class ClippyExceptions
9
+ def self.load(path) = new.tap { |e| e.instance_eval(File.read(path)) }
10
+ def allow(path, line:, reason:) = list << "#{path}:#{line}"
11
+ def to_a = list
12
+ def list = @list ||= []
13
+ end
14
+
15
+ namespace :rust do
16
+ desc "Check Rust formatting with cargo fmt"
17
+ task :fmt do
18
+ sh "cargo fmt --check"
19
+ end
20
+
21
+ namespace :fmt do
22
+ desc "Auto-fix Rust formatting with cargo fmt"
23
+ task :fix do
24
+ sh "cargo fmt"
25
+ end
26
+ end
27
+
28
+ desc "Run Clippy with strict settings (configured in Cargo.toml)"
29
+ task :clippy do
30
+ sh "cargo clippy -- -D warnings"
31
+ end
32
+
33
+ desc "Fast type checking with cargo check"
34
+ task :check do
35
+ sh "cargo check"
36
+ end
37
+
38
+ desc "Forbid #[allow(clippy::...)] except approved exceptions"
39
+ task :no_allows do
40
+ exceptions = ClippyExceptions.load(File.expand_path("../clippy_exceptions.rb", __dir__))
41
+
42
+ violations = Dir.glob("ext/**/*.rs")
43
+ .reject { |f| f.include?("/target/") }
44
+ .flat_map { |file| File.readlines(file).map.with_index(1) { |line, n| [file, n, line] } }
45
+ .select { |_, _, line| line.include?("#[allow(clippy::") }
46
+ .reject { |file, n, _| exceptions.to_a.any? { |ex| "#{file}:#{n}".start_with?(ex) } }
47
+ .map { |file, n, line| " #{file}:#{n}: #{line.strip}" }
48
+
49
+ abort <<~ERROR if violations.any?
50
+ ❌ Unapproved #[allow(clippy::...)] found:
51
+ #{violations.join("\n")}
52
+
53
+ Add to clippy_exceptions.rb with justification if truly unavoidable.
54
+ ERROR
55
+
56
+ puts "✅ No unapproved Clippy allows found"
57
+ end
58
+ end
59
+
60
+ namespace :lint do
61
+ desc "Run all Rust linting (clippy + fmt check + no unapproved allows)"
62
+ task rust: %w[rust:clippy rust:fmt rust:no_allows]
63
+ end
data/tasks/steep.rake ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ desc "Run Steep type checker"
9
+ task :steep do
10
+ sh "bundle exec steep check"
11
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ require "minitest/test_task"
9
+
10
+ # Clear the default test task created by Minitest::TestTask if it exists
11
+ Rake::Task["test"].clear if Rake::Task.task_defined?("test")
12
+
13
+ desc "Run all tests (Rust first, then Ruby)"
14
+ task test: %w[test:rust test:ruby]
15
+
16
+ namespace :test do
17
+ desc "Run Rust tests (requires compile)"
18
+ task rust: :compile do
19
+ sh "cargo test"
20
+ end
21
+
22
+ # Create a specific Minitest task for Ruby tests
23
+ Minitest::TestTask.create(:ruby) do |t|
24
+ t.test_globs = ["test/**/*.rb", "examples/**/test_*.rb"]
25
+ end
26
+ end
data/test_native.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ #
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+ #++
8
+
9
+ # !/usr/bin/env ruby
10
+
11
+ # Basic smoke test for Tokra native extension
12
+
13
+ require_relative "lib/tokra"
14
+
15
+ puts "Tokra v#{Tokra::VERSION}"
16
+ puts "Testing native extension load..."
17
+
18
+ # Test that the module and classes are defined
19
+ raise "Missing Tokra::Native" unless defined?(Tokra::Native)
20
+ raise "Missing EventLoop" unless defined?(Tokra::Native::EventLoop)
21
+ raise "Missing Window" unless defined?(Tokra::Native::Window)
22
+ raise "Missing WebView" unless defined?(Tokra::Native::WebView)
23
+ raise "Missing Proxy" unless defined?(Tokra::Native::Proxy)
24
+ raise "Missing IpcEvent" unless defined?(Tokra::Native::IpcEvent)
25
+ raise "Missing WakeUpEvent" unless defined?(Tokra::Native::WakeUpEvent)
26
+ raise "Missing WindowCloseEvent" unless defined?(Tokra::Native::WindowCloseEvent)
27
+
28
+ puts "✓ Native extension loaded successfully!"
29
+ puts ""
30
+ puts "Available classes:"
31
+ puts " Tokra::Native::EventLoop"
32
+ puts " Tokra::Native::Window"
33
+ puts " Tokra::Native::WebView"
34
+ puts " Tokra::Native::Proxy"
35
+ puts " Tokra::Native::IpcEvent"
36
+ puts " Tokra::Native::WakeUpEvent"
37
+ puts " Tokra::Native::WindowCloseEvent"