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,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" }