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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Tokra Native Extension: Thin Rust Bindings Implementation
|
|
8
|
+
|
|
9
|
+
This plan implements the native Rust extension layer for Tokra as specified in [ADR 001](file:///Users/kerrick/Developer/tokra/doc/contributors/adr/001.md). The extension provides thin Magnus bindings around `tao` (windowing) and `wry` (WebView), following the "dumb pipe" principle where Rust handles platform-native window/webview operations while Ruby manages all application logic, state, and routing.
|
|
10
|
+
|
|
11
|
+
## User Review Required
|
|
12
|
+
|
|
13
|
+
> [!IMPORTANT]
|
|
14
|
+
> **Ruby 4.0.1 Compatibility**: Magnus 0.9.0 (unreleased, on main branch) explicitly supports Ruby 4.0. We'll use the git dependency until 0.9.0 is published to crates.io.
|
|
15
|
+
|
|
16
|
+
> [!WARNING]
|
|
17
|
+
> **Signal Handling**: When `tao` hijacks the main thread, Ruby's `Signal.trap("INT")` won't fire. The solution is to use the `ctrlc` crate in Rust to catch SIGINT and call `proxy.send_event(UserEvent::Exit)` to wake tao and allow clean shutdown.
|
|
18
|
+
|
|
19
|
+
## Proposed Changes
|
|
20
|
+
|
|
21
|
+
### Toolchain Configuration
|
|
22
|
+
|
|
23
|
+
#### [MODIFY] [mise.toml](file:///Users/kerrick/Developer/tokra/mise.toml)
|
|
24
|
+
|
|
25
|
+
Add Rust to the project's tool versions for consistent builds:
|
|
26
|
+
|
|
27
|
+
```diff
|
|
28
|
+
[tools]
|
|
29
|
+
ruby = "4.0.1"
|
|
30
|
+
python = "3.12"
|
|
31
|
+
pre-commit = "latest"
|
|
32
|
+
+rust = "1.93.0"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### Dependency Updates
|
|
38
|
+
|
|
39
|
+
#### [MODIFY] [Cargo.toml](file:///Users/kerrick/Developer/tokra/ext/tokra/Cargo.toml)
|
|
40
|
+
|
|
41
|
+
Update dependencies to latest compatible versions:
|
|
42
|
+
|
|
43
|
+
```diff
|
|
44
|
+
[dependencies]
|
|
45
|
+
-magnus = "0.7"
|
|
46
|
+
+magnus = { git = "https://github.com/matsadler/magnus", branch = "main" }
|
|
47
|
+
-tao = "0.35"
|
|
48
|
+
+tao = "0.37"
|
|
49
|
+
-wry = "0.52"
|
|
50
|
+
+wry = "0.52.2"
|
|
51
|
+
+ctrlc = "3.4"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Version notes:
|
|
55
|
+
- `magnus` → Git main branch (0.9.0-dev) for Ruby 4.0 support, requires Rust 1.85+
|
|
56
|
+
- `tao` 0.35.0 → 0.37.0 (two minor versions, contains API improvements)
|
|
57
|
+
- `wry` 0.52.0 → 0.52.2 (patch updates, bug fixes)
|
|
58
|
+
- `ctrlc` 3.4 → For SIGINT handling when tao owns the main thread
|
|
59
|
+
- `rb-sys` 0.9.114 → Already at latest
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### Core Implementation
|
|
64
|
+
|
|
65
|
+
#### [MODIFY] [lib.rs](file:///Users/kerrick/Developer/tokra/ext/tokra/src/lib.rs)
|
|
66
|
+
|
|
67
|
+
Complete implementation of the four Native classes per ADR 001 and ADR 002:
|
|
68
|
+
|
|
69
|
+
**1. `Tokra::Native::EventLoop`**
|
|
70
|
+
- Wraps `tao::event_loop::EventLoop<UserEvent>`
|
|
71
|
+
- `run(callback: Proc)` → Starts the blocking event loop (never returns)
|
|
72
|
+
- `create_proxy()` → Returns a `Tokra::Native::Proxy` for cross-Ractor communication
|
|
73
|
+
- Handles `ctrlc` for SIGINT signal safety (since Ruby's `Signal.trap` won't work)
|
|
74
|
+
- Fires callback on `UserEvent` and `WindowEvent` occurrences
|
|
75
|
+
|
|
76
|
+
**2. `Tokra::Native::Window`**
|
|
77
|
+
- Wraps `tao::window::Window`
|
|
78
|
+
- Constructor: `new(event_loop)`
|
|
79
|
+
- Methods: `set_title(string)`, `set_size(width, height)`, `id()`
|
|
80
|
+
|
|
81
|
+
**3. `Tokra::Native::WebView`**
|
|
82
|
+
- Wraps `wry::WebView`
|
|
83
|
+
- Constructor: `new(window, url, ipc_callback)` - The `ipc_callback` must be a `Ractor.shareable_proc`
|
|
84
|
+
- `eval(js_string)` - Execute JavaScript in the webview
|
|
85
|
+
- `register_protocol(scheme, callback)` - Register custom protocol handler; `callback` must be a `Ractor.shareable_proc`
|
|
86
|
+
|
|
87
|
+
**4. `Tokra::Native::Proxy`**
|
|
88
|
+
- Wraps `tao::event_loop::EventLoopProxy<UserEvent>`
|
|
89
|
+
- `wake_up(payload)` - Thread-safe awakening from Ractors
|
|
90
|
+
- Critical for Worker Ractor → Main Ractor communication
|
|
91
|
+
|
|
92
|
+
**Implementation structure:**
|
|
93
|
+
|
|
94
|
+
```rust
|
|
95
|
+
// Custom UserEvent for cross-thread communication
|
|
96
|
+
enum UserEvent {
|
|
97
|
+
IpcMessage(String),
|
|
98
|
+
WakeUp(String),
|
|
99
|
+
ProtocolRequest { scheme: String, request: ProtocolRequest },
|
|
100
|
+
Exit,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Each Native class wraps its native type in a TypedData struct
|
|
104
|
+
#[magnus::wrap(class = "Tokra::Native::EventLoop")]
|
|
105
|
+
struct RbEventLoop { inner: RefCell<Option<EventLoop<UserEvent>>> }
|
|
106
|
+
|
|
107
|
+
impl RbEventLoop {
|
|
108
|
+
fn create_proxy(&self) -> RbProxy { /* ... */ }
|
|
109
|
+
fn run(&self, callback: Proc) -> ! { /* ... */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[magnus::wrap(class = "Tokra::Native::Window")]
|
|
113
|
+
struct RbWindow { inner: tao::window::Window }
|
|
114
|
+
|
|
115
|
+
#[magnus::wrap(class = "Tokra::Native::WebView")]
|
|
116
|
+
struct RbWebView { inner: WebView }
|
|
117
|
+
|
|
118
|
+
impl RbWebView {
|
|
119
|
+
fn eval(&self, js: String) { /* ... */ }
|
|
120
|
+
fn register_protocol(&self, scheme: String, callback: Proc) { /* ... */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[magnus::wrap(class = "Tokra::Native::Proxy")]
|
|
124
|
+
struct RbProxy { inner: EventLoopProxy<UserEvent> }
|
|
125
|
+
|
|
126
|
+
impl RbProxy {
|
|
127
|
+
fn wake_up(&self, payload: String) { /* ... */ }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Ruby Integration
|
|
134
|
+
|
|
135
|
+
#### [NEW] [lib/tokra/native.rb](file:///Users/kerrick/Developer/tokra/lib/tokra/native.rb)
|
|
136
|
+
|
|
137
|
+
Ruby-side module to load and organize the Native classes:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# frozen_string_literal: true
|
|
141
|
+
|
|
142
|
+
module Tokra
|
|
143
|
+
module Native
|
|
144
|
+
# Native classes loaded from extension:
|
|
145
|
+
# - Tokra::Native::EventLoop
|
|
146
|
+
# - Tokra::Native::Window
|
|
147
|
+
# - Tokra::Native::WebView
|
|
148
|
+
# - Tokra::Native::Proxy
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
require_relative "tokra/tokra" # The native extension
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### [MODIFY] [lib/tokra.rb](file:///Users/kerrick/Developer/tokra/lib/tokra.rb)
|
|
156
|
+
|
|
157
|
+
Add require for the Native module to the main entry point.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Verification Plan
|
|
162
|
+
|
|
163
|
+
### Automated Tests
|
|
164
|
+
|
|
165
|
+
**Build verification:**
|
|
166
|
+
```bash
|
|
167
|
+
cd /Users/kerrick/Developer/tokra
|
|
168
|
+
bundle exec rake compile
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Unit tests:**
|
|
172
|
+
```bash
|
|
173
|
+
bundle exec rake test
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Extension loading test:**
|
|
177
|
+
```ruby
|
|
178
|
+
require "tokra"
|
|
179
|
+
# Should not raise LoadError
|
|
180
|
+
|
|
181
|
+
# All Native classes should be defined
|
|
182
|
+
Tokra::Native::EventLoop
|
|
183
|
+
Tokra::Native::Window
|
|
184
|
+
Tokra::Native::WebView
|
|
185
|
+
Tokra::Native::Proxy
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Ping Pong Integration Test (ADR 003)
|
|
189
|
+
|
|
190
|
+
The full round-trip validation test proving Ractor bridge, Main Loop, and IPC are functional:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# test/integration/ping_pong_test.rb
|
|
194
|
+
# frozen_string_literal: true
|
|
195
|
+
|
|
196
|
+
require "tokra"
|
|
197
|
+
require "timeout"
|
|
198
|
+
|
|
199
|
+
class PingPongTest < Minitest::Test
|
|
200
|
+
def test_full_round_trip
|
|
201
|
+
Timeout.timeout(0.5) do # 500ms for CI stability
|
|
202
|
+
# Communication ports (explicit Ractor::Port)
|
|
203
|
+
ipc_port = Ractor::Port.new
|
|
204
|
+
response_port = Ractor::Port.new
|
|
205
|
+
|
|
206
|
+
# Worker Ractor processes messages and responds via port
|
|
207
|
+
worker = Ractor.new(response_port) do |resp_port|
|
|
208
|
+
msg = Ractor.receive
|
|
209
|
+
resp_port << (msg + "PONG")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
event_loop = Tokra::Native::EventLoop.new
|
|
213
|
+
proxy = event_loop.create_proxy
|
|
214
|
+
result = nil
|
|
215
|
+
|
|
216
|
+
# IPC handler must be a shareable_proc
|
|
217
|
+
ipc_handler = Ractor.shareable_proc do |raw_msg|
|
|
218
|
+
ipc_port << raw_msg
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
window = Tokra::Native::Window.new(event_loop)
|
|
222
|
+
webview = Tokra::Native::WebView.new(
|
|
223
|
+
window,
|
|
224
|
+
"data:text/html,<script>window.ipc.postMessage('PING')</script>",
|
|
225
|
+
ipc_handler
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
event_loop.run do |event|
|
|
229
|
+
case event
|
|
230
|
+
when Tokra::Native::IpcEvent
|
|
231
|
+
worker << event.message
|
|
232
|
+
when Tokra::Native::WakeUpEvent
|
|
233
|
+
result = response_port.receive
|
|
234
|
+
webview.eval("window.result = '#{result}'")
|
|
235
|
+
break
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
assert_equal "PINGPONG", result
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Manual Verification
|
|
246
|
+
|
|
247
|
+
**Interactive smoke test:**
|
|
248
|
+
```ruby
|
|
249
|
+
# frozen_string_literal: true
|
|
250
|
+
|
|
251
|
+
require "tokra"
|
|
252
|
+
|
|
253
|
+
event_loop = Tokra::Native::EventLoop.new
|
|
254
|
+
proxy = event_loop.create_proxy
|
|
255
|
+
window = Tokra::Native::Window.new(event_loop)
|
|
256
|
+
window.set_title("Tokra Test")
|
|
257
|
+
|
|
258
|
+
# IPC handler must be shareable
|
|
259
|
+
ipc_handler = Ractor.shareable_proc do |msg|
|
|
260
|
+
puts "IPC received: #{msg}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
webview = Tokra::Native::WebView.new(window, "https://example.com", ipc_handler)
|
|
264
|
+
|
|
265
|
+
# Opens a native window - visual confirmation
|
|
266
|
+
event_loop.run do |event|
|
|
267
|
+
puts "Event received: #{event.inspect}"
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The window should appear, respond to resize/close, and properly terminate on Ctrl+C.
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
# !/usr/bin/env ruby
|
|
9
|
+
|
|
10
|
+
# Hello World Example - Serve static index.html via tokra:// protocol
|
|
11
|
+
#
|
|
12
|
+
# This demonstrates that new_with_protocol works for simple static sites,
|
|
13
|
+
# not just complex Rack-style apps. Just like Tauri's WebviewUrl::App("index.html").
|
|
14
|
+
|
|
15
|
+
require_relative "../../lib/tokra"
|
|
16
|
+
require "logger"
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Logging Setup
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
LOGGER = Logger.new($stdout)
|
|
23
|
+
LOGGER.level = Logger::DEBUG
|
|
24
|
+
LOGGER.progname = "HelloWorld"
|
|
25
|
+
LOGGER.formatter = proc do |severity, datetime, progname, msg|
|
|
26
|
+
timestamp = datetime.strftime("%H:%M:%S.%L")
|
|
27
|
+
"#{timestamp} [#{severity.ljust(5)}] #{progname}: #{msg}\n"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Read the static HTML file
|
|
31
|
+
html_path = File.expand_path("index.html", __dir__)
|
|
32
|
+
html_content = File.read(html_path)
|
|
33
|
+
|
|
34
|
+
LOGGER.info { "=" * 50 }
|
|
35
|
+
LOGGER.info { "Tokra Hello World" }
|
|
36
|
+
LOGGER.info { "=" * 50 }
|
|
37
|
+
LOGGER.debug { "Ruby version: #{RUBY_VERSION}" }
|
|
38
|
+
LOGGER.debug { "Tokra version: #{Tokra::VERSION}" }
|
|
39
|
+
LOGGER.info { "Serving: #{html_path}" }
|
|
40
|
+
LOGGER.debug { "HTML content length: #{html_content.bytesize} bytes" }
|
|
41
|
+
|
|
42
|
+
# Create the event loop and window
|
|
43
|
+
LOGGER.debug { "Creating EventLoop" }
|
|
44
|
+
event_loop = Tokra::Native::EventLoop.new
|
|
45
|
+
|
|
46
|
+
LOGGER.debug { "Creating Proxy" }
|
|
47
|
+
proxy = event_loop.create_proxy
|
|
48
|
+
|
|
49
|
+
LOGGER.debug { "Creating Window" }
|
|
50
|
+
window = Tokra::Native::Window.new(event_loop)
|
|
51
|
+
window.set_title("Hello, Tokra!")
|
|
52
|
+
window.set_size(600.0, 400.0)
|
|
53
|
+
LOGGER.info { "Window created: 600x400, title='Hello, Tokra!'" }
|
|
54
|
+
|
|
55
|
+
# Create WebView - navigates to tokra://localhost/
|
|
56
|
+
LOGGER.debug { "Creating WebView with tokra:// protocol" }
|
|
57
|
+
Tokra::Native::WebView.new_with_protocol(window, proxy)
|
|
58
|
+
LOGGER.info { "WebView created, navigating to tokra://localhost/" }
|
|
59
|
+
|
|
60
|
+
LOGGER.info { "-" * 50 }
|
|
61
|
+
LOGGER.info { "Event loop starting..." }
|
|
62
|
+
|
|
63
|
+
request_count = 0
|
|
64
|
+
page_load_count = 0
|
|
65
|
+
|
|
66
|
+
event_loop.run(
|
|
67
|
+
lambda { |event|
|
|
68
|
+
case event
|
|
69
|
+
when Tokra::Native::HttpRequestEvent
|
|
70
|
+
request_count += 1
|
|
71
|
+
request_id = event.request_id
|
|
72
|
+
method = event.method
|
|
73
|
+
uri = event.uri
|
|
74
|
+
|
|
75
|
+
LOGGER.info { "HTTP #{method} #{uri}" }
|
|
76
|
+
LOGGER.debug { "Request ##{request_count}, ID=#{request_id}" }
|
|
77
|
+
|
|
78
|
+
if method == "GET"
|
|
79
|
+
proxy.respond(
|
|
80
|
+
request_id,
|
|
81
|
+
200,
|
|
82
|
+
[["Content-Type", "text/html; charset=utf-8"]],
|
|
83
|
+
html_content
|
|
84
|
+
)
|
|
85
|
+
LOGGER.debug { "Responded with 200 OK, #{html_content.bytesize} bytes" }
|
|
86
|
+
else
|
|
87
|
+
proxy.respond(
|
|
88
|
+
request_id,
|
|
89
|
+
405,
|
|
90
|
+
[["Content-Type", "text/plain"]],
|
|
91
|
+
"Method Not Allowed"
|
|
92
|
+
)
|
|
93
|
+
LOGGER.debug { "Responded with 405 Method Not Allowed" }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
when Tokra::Native::PageLoadEvent
|
|
97
|
+
page_load_count += 1
|
|
98
|
+
if event.started?
|
|
99
|
+
LOGGER.info { "Page load started: #{event.url}" }
|
|
100
|
+
else
|
|
101
|
+
LOGGER.info { "Page load finished: #{event.url}" }
|
|
102
|
+
end
|
|
103
|
+
LOGGER.debug { "Page load event ##{page_load_count}" }
|
|
104
|
+
|
|
105
|
+
when Tokra::Native::WindowCloseEvent
|
|
106
|
+
LOGGER.info { "-" * 50 }
|
|
107
|
+
LOGGER.info { "Window close requested" }
|
|
108
|
+
LOGGER.info { "Session stats: #{request_count} requests, #{page_load_count} page loads" }
|
|
109
|
+
LOGGER.info { "Goodbye!" }
|
|
110
|
+
end
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
LOGGER.info { "Event loop exited" }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
-->
|
|
6
|
+
<!DOCTYPE html>
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<title>Tokra</title>
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;600&display=swap" rel="stylesheet">
|
|
15
|
+
<style>
|
|
16
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--text: oklch(25% 0.02 250);
|
|
20
|
+
--text-muted: oklch(50% 0.015 250);
|
|
21
|
+
--surface: oklch(98% 0.005 250);
|
|
22
|
+
--accent: oklch(55% 0.18 150);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
html {
|
|
26
|
+
font-family: 'Instrument Sans', system-ui, sans-serif;
|
|
27
|
+
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
|
28
|
+
line-height: 1.6;
|
|
29
|
+
color: var(--text);
|
|
30
|
+
background: var(--surface);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
min-height: 100dvh;
|
|
35
|
+
display: grid;
|
|
36
|
+
place-items: center;
|
|
37
|
+
padding: clamp(1.5rem, 4vw, 3rem);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main {
|
|
41
|
+
max-width: 32rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
h1 {
|
|
45
|
+
font-size: clamp(2rem, 1.6rem + 2vw, 3rem);
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
letter-spacing: -0.02em;
|
|
48
|
+
line-height: 1.1;
|
|
49
|
+
margin-block-end: 1.5rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
p {
|
|
53
|
+
color: var(--text-muted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p + p {
|
|
57
|
+
margin-block-start: 0.75rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
code {
|
|
61
|
+
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
|
|
62
|
+
font-size: 0.9em;
|
|
63
|
+
background: oklch(94% 0.01 250);
|
|
64
|
+
padding: 0.15em 0.4em;
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.status {
|
|
69
|
+
display: inline-block;
|
|
70
|
+
margin-block-start: 2rem;
|
|
71
|
+
padding: 0.5rem 1rem;
|
|
72
|
+
font-size: 0.85rem;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
color: var(--accent);
|
|
75
|
+
background: oklch(95% 0.04 150);
|
|
76
|
+
border-radius: 6px;
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<main>
|
|
82
|
+
<h1>Hello from Tokra</h1>
|
|
83
|
+
<p>This page was served through the <code>tokra://</code> custom protocol, loaded directly from <code>index.html</code>.</p>
|
|
84
|
+
<p>Your Ruby app handles HTTP requests internally, just like Tauri does with <code>tauri://</code>.</p>
|
|
85
|
+
<span class="status">Protocol working</span>
|
|
86
|
+
</main>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
# !/usr/bin/env ruby
|
|
9
|
+
# :nodoc:
|
|
10
|
+
|
|
11
|
+
# Ping-Pong Verification Example - Real Roda App
|
|
12
|
+
#
|
|
13
|
+
# This demonstrates Tokra's Rack adapter with Roda: a real Roda application
|
|
14
|
+
# running via the tokra:// custom protocol, just like Tauri's tauri://.
|
|
15
|
+
|
|
16
|
+
require_relative "../../lib/tokra"
|
|
17
|
+
require "roda"
|
|
18
|
+
require "json"
|
|
19
|
+
require "uri"
|
|
20
|
+
require "stringio"
|
|
21
|
+
require "securerandom"
|
|
22
|
+
require "logger"
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Logging Setup
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
LOGGER = Logger.new($stdout)
|
|
29
|
+
LOGGER.level = Logger::DEBUG
|
|
30
|
+
LOGGER.progname = "PingPong"
|
|
31
|
+
LOGGER.formatter = proc do |severity, datetime, progname, msg|
|
|
32
|
+
timestamp = datetime.strftime("%H:%M:%S.%L")
|
|
33
|
+
"#{timestamp} [#{severity.ljust(5)}] #{progname}: #{msg}\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
MESSAGE = ARGV[0] || "Default message - pass an argument!" # :nodoc:
|
|
37
|
+
|
|
38
|
+
LOGGER.info { "Starting Ping-Pong verification example" }
|
|
39
|
+
LOGGER.debug { "Ruby version: #{RUBY_VERSION}" }
|
|
40
|
+
LOGGER.debug { "Tokra version: #{Tokra::VERSION}" }
|
|
41
|
+
LOGGER.debug { "Message argument: #{MESSAGE.inspect}" }
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# The Roda Application
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
class PingPongApp < Roda
|
|
48
|
+
# Session secret - just needs to be consistent for this process
|
|
49
|
+
plugin :sessions, secret: SecureRandom.hex(32)
|
|
50
|
+
plugin :flash
|
|
51
|
+
plugin :render, views: File.expand_path("views", __dir__), escape: true
|
|
52
|
+
plugin :json
|
|
53
|
+
plugin :all_verbs
|
|
54
|
+
plugin :public, root: File.expand_path("public", __dir__)
|
|
55
|
+
|
|
56
|
+
# Store message in opts so it's accessible in route block
|
|
57
|
+
opts[:message] = MESSAGE
|
|
58
|
+
opts[:logger] = LOGGER
|
|
59
|
+
|
|
60
|
+
route do |r|
|
|
61
|
+
opts[:logger].debug { "Routing: #{r.request_method} #{r.path}" }
|
|
62
|
+
|
|
63
|
+
# Serve static assets from public/
|
|
64
|
+
r.public
|
|
65
|
+
|
|
66
|
+
# GET / - serve the main page
|
|
67
|
+
r.root do
|
|
68
|
+
opts[:logger].info { "Serving index page" }
|
|
69
|
+
opts[:logger].debug { "Flash contents: #{flash.inspect}" }
|
|
70
|
+
view("index", locals: { message: opts[:message], flash: })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# POST /submit - handle form submission, redirect back
|
|
74
|
+
r.post "submit" do
|
|
75
|
+
response = r.params["response"]
|
|
76
|
+
opts[:logger].info { "Form submission received" }
|
|
77
|
+
opts[:logger].debug { "Form response value: #{response.inspect}" }
|
|
78
|
+
|
|
79
|
+
flash["success"] = "Form submitted! You said: \"#{response}\""
|
|
80
|
+
opts[:logger].debug { "Flash set, redirecting to root" }
|
|
81
|
+
|
|
82
|
+
r.redirect "tokra://localhost/"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# POST /api - JSON API endpoint
|
|
86
|
+
r.post "api" do
|
|
87
|
+
body = r.body.read
|
|
88
|
+
opts[:logger].info { "API request received" }
|
|
89
|
+
opts[:logger].debug { "API request body: #{body.inspect}" }
|
|
90
|
+
|
|
91
|
+
data = begin
|
|
92
|
+
JSON.parse(body)
|
|
93
|
+
rescue
|
|
94
|
+
{ "raw" => body }
|
|
95
|
+
end
|
|
96
|
+
opts[:logger].debug { "Parsed API data: #{data.inspect}" }
|
|
97
|
+
|
|
98
|
+
response = {
|
|
99
|
+
status: "ok",
|
|
100
|
+
received: data,
|
|
101
|
+
timestamp: Time.now.iso8601,
|
|
102
|
+
}
|
|
103
|
+
opts[:logger].debug { "API response: #{response.inspect}" }
|
|
104
|
+
|
|
105
|
+
response
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
LOGGER.info { "Roda application class defined" }
|
|
111
|
+
LOGGER.debug { "Plugins loaded: sessions, flash, render, json, all_verbs" }
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# Run the app
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
LOGGER.info { "=" * 50 }
|
|
118
|
+
LOGGER.info { "Tokra Ping-Pong (Roda App)" }
|
|
119
|
+
LOGGER.info { "=" * 50 }
|
|
120
|
+
|
|
121
|
+
LOGGER.debug { "Freezing Roda app" }
|
|
122
|
+
rack_app = PingPongApp.freeze.app
|
|
123
|
+
LOGGER.info { "Starting Rack::Handler::Tokra.run" }
|
|
124
|
+
|
|
125
|
+
Rack::Handler::Tokra.run(
|
|
126
|
+
rack_app,
|
|
127
|
+
title: "Tokra Ping-Pong",
|
|
128
|
+
width: 500,
|
|
129
|
+
height: 600
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
LOGGER.info { "Event loop exited" }
|