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.
- checksums.yaml +7 -0
- data/.pre-commit-config.yaml +16 -0
- data/AGENTS.md +126 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +16 -0
- data/Cargo.toml +23 -0
- data/LICENSE +661 -0
- data/LICENSES/AGPL-3.0-or-later.txt +235 -0
- data/LICENSES/Apache-2.0.txt +73 -0
- data/LICENSES/CC-BY-SA-4.0.txt +170 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT.txt +18 -0
- data/README.md +45 -0
- data/README.rdoc +4 -0
- data/REUSE.toml +11 -0
- data/Rakefile +27 -0
- data/Steepfile +15 -0
- data/clippy.toml +5 -0
- data/clippy_exceptions.rb +59 -0
- data/doc/contributors/adr/001.md +187 -0
- data/doc/contributors/adr/002.md +132 -0
- data/doc/contributors/adr/003.md +116 -0
- data/doc/contributors/chats/001.md +3874 -0
- data/doc/contributors/plan/001.md +271 -0
- data/examples/verify_hello_world/app.rb +114 -0
- data/examples/verify_hello_world/index.html +88 -0
- data/examples/verify_ping_pong/README.md +0 -0
- data/examples/verify_ping_pong/app.rb +132 -0
- data/examples/verify_ping_pong/public/styles.css +182 -0
- data/examples/verify_ping_pong/views/index.erb +94 -0
- data/examples/verify_ping_pong/views/layout.erb +22 -0
- data/exe/semantic-highlight +0 -0
- data/ext/tokra/Cargo.toml +23 -0
- data/ext/tokra/extconf.rb +12 -0
- data/ext/tokra/src/lib.rs +719 -0
- data/lib/tokra/native.rb +79 -0
- data/lib/tokra/rack/handler.rb +177 -0
- data/lib/tokra/version.rb +12 -0
- data/lib/tokra.rb +19 -0
- data/mise.toml +8 -0
- data/rustfmt.toml +4 -0
- data/sig/tokra.rbs +7 -0
- data/tasks/lint.rake +151 -0
- data/tasks/rust.rake +63 -0
- data/tasks/steep.rake +11 -0
- data/tasks/test.rake +26 -0
- data/test_native.rb +37 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +112 -0
data/lib/tokra/native.rb
ADDED
|
@@ -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
|
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
data/rustfmt.toml
ADDED
data/sig/tokra.rbs
ADDED
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
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"
|