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,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
data/LICENSES/MIT.txt ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Tokra
8
+
9
+ TODO: Delete this and the text below, and describe your gem
10
+
11
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tokra`. To experiment with that code, run `bin/console` for an interactive prompt.
12
+
13
+ ## Installation
14
+
15
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
16
+
17
+ Install the gem and add to the application's Gemfile by executing:
18
+
19
+ ```bash
20
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ If bundler is not being used to manage dependencies, install the gem by executing:
24
+
25
+ ```bash
26
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ TODO: Write usage instructions here
32
+
33
+ ## Development
34
+
35
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
+
37
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tokra. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/tokra/blob/main/CODE_OF_CONDUCT.md).
42
+
43
+ ## Code of Conduct
44
+
45
+ Everyone interacting in the Tokra project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/tokra/blob/main/CODE_OF_CONDUCT.md).
data/README.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ == Build Desktop Apps with Ruby and Web Technologies
2
+
3
+ Tokra is a port of Tauri to Ruby, enabling you to build desktop applications
4
+ with web technologies backed by Ruby.
data/REUSE.toml ADDED
@@ -0,0 +1,11 @@
1
+ version = 1
2
+
3
+ [[annotations]]
4
+ path = 'Gemfile.lock'
5
+ SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
6
+ SPDX-License-Identifier = "CC0-1.0"
7
+
8
+ [[annotations]]
9
+ path = 'README.rdoc'
10
+ SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
11
+ SPDX-License-Identifier = "CC-BY-SA-4.0"
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require "bundler/gem_tasks"
7
+
8
+ require "rdoc/task"
9
+ RDoc::Task.new do |rdoc|
10
+ rdoc.rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
11
+ end
12
+
13
+ # Rust extension compilation
14
+ require "rb_sys/extensiontask"
15
+
16
+ GEMSPEC = Gem::Specification.load("tokra.gemspec")
17
+
18
+ RbSys::ExtensionTask.new("tokra", GEMSPEC) do |ext|
19
+ ext.lib_dir = "lib/tokra"
20
+ end
21
+
22
+ task build: :compile
23
+
24
+ # Import all tasks from the tasks/ directory
25
+ Dir.glob("tasks/*.rake").each { |r| import r }
26
+
27
+ task default: %w[compile lint:fix test lint steep lint:rust]
data/Steepfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ target :lib do
7
+ signature "sig"
8
+ check "lib"
9
+
10
+ library "pathname"
11
+ library "fileutils"
12
+ library "minitest"
13
+ library "date"
14
+ library "timeout"
15
+ end
data/clippy.toml ADDED
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ # Clippy configuration for lint parameters only.
5
+ # Lint levels (forbid/deny/warn/allow) are configured in Cargo.toml [lints] section.
@@ -0,0 +1,59 @@
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
+ # Approved Clippy allow exceptions.
9
+ # Each entry must explain WHY the exception is unavoidable.
10
+ # Review carefully before adding new entries.
11
+
12
+ allow "ext/tokra/src/lib.rs", line: 7, reason: <<~END
13
+ Magnus FFI requires owned types at the Ruby-Rust boundary. Ruby strings must be
14
+ converted to owned Rust Strings when crossing the FFI, as borrowed references
15
+ cannot safely outlive the Ruby GC cycle. This is a fundamental constraint of
16
+ the magnus crate's type marshaling, not a code smell to be fixed.
17
+ END
18
+
19
+ allow "ext/tokra/src/lib.rs", lines: [77, 189, 249], lint: "unsafe_code", reason: <<~END
20
+ Tauri Pattern: unsafe impl Send for RbEventLoop, RbWindow, RbWebView
21
+
22
+ These types wrap platform-native GUI objects (tao::Window, wry::WebView) that
23
+ are bound to the main OS thread. Magnus's TypedData trait requires Send, but
24
+ these types are not inherently thread-safe.
25
+
26
+ INVARIANT: All access to these types happens exclusively on the main thread
27
+ via the tao event loop. Ruby code cannot access these objects from Worker
28
+ Ractors - only through the Send-safe Proxy which marshals commands to the
29
+ main thread.
30
+
31
+ This pattern is identical to how Tauri implements this in tauri-runtime-wry:
32
+ - `unsafe impl Send for WindowsStore {}` (lib.rs:433)
33
+ - `unsafe impl Send for DispatcherMainThreadContext<T> {}` (lib.rs:451)
34
+ - `unsafe impl Send for WindowBuilderWrapper {}` (lib.rs:809)
35
+
36
+ The invariant is enforced by:
37
+ 1. The event loop callback runs only on the main thread
38
+ 2. Ractors can only hold the Proxy (which IS genuinely Send+Sync-safe)
39
+ 3. Integration tests verify invariants:
40
+ test/integration/thread_safety_invariant_test.rb:23-38 (Proxy is Ractor-safe)
41
+ test/integration/thread_safety_invariant_test.rb:44-55 (main thread instantiation)
42
+ test/integration/thread_safety_invariant_test.rb:60-73 (main thread operations)
43
+ END
44
+
45
+ allow "ext/tokra/src/lib.rs", lines: [310], lint: "unsafe_code", reason: <<~END
46
+ Magnus TypedData for RbProxy: unsafe impl TypedData for RbProxy
47
+
48
+ This is SAFE because EventLoopProxy<T> is genuinely Send+Sync by design.
49
+ We use this to enable the `frozen_shareable` flag which makes the Ruby
50
+ object Ractor-shareable when frozen.
51
+
52
+ Unlike the other unsafe impls (which are for main-thread-only types),
53
+ this one is actually safe - tao explicitly designed EventLoopProxy for
54
+ cross-thread communication. The unsafe is only required by Magnus's
55
+ TypedData trait signature, not because the operation is unsafe.
56
+
57
+ VERIFIED BY: test/integration/thread_safety_invariant_test.rb:23-38
58
+ Tests that frozen Proxy becomes Ractor-shareable.
59
+ END
@@ -0,0 +1,187 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # ADR 001: Tokra Architecture - Ruby-Host with Thin Rust Bindings
8
+
9
+ ## Status
10
+ Accepted
11
+
12
+ ## Date
13
+ 2026-01-29
14
+
15
+ ## Context
16
+ We are building **Tokra**, a desktop application framework for Ruby 4.0.1+.
17
+ The goal is to enable developers to build performant, native-feeling desktop apps using HTML/CSS/JS for the frontend and **Pure Ruby** for the backend logic.
18
+
19
+ Historically, frameworks like Tauri required significant Rust code to manage state, commands, and sidecars. However, with the release of Ruby 4.0 (Ractors, YJIT), Ruby is now fast enough and concurrent enough to handle these responsibilities directly.
20
+
21
+ We need an architecture that:
22
+ 1. Minimizes Rust maintenance overhead (cost & complexity).
23
+ 2. Maximizes Ruby developer experience (logic stays in Ruby).
24
+ 3. Ensures 60FPS UI performance by never blocking the main thread.
25
+ 4. Maintains a secure barrier between the WebView and the System.
26
+
27
+ ## Decision
28
+ We will adopt the **"Thin Binding" + "Ruby Host"** architecture.
29
+
30
+ 1. **Rust Layer (The Dumb Pipe):** We will use `magnus` to create a thin, un-opinionated wrapper around `tao` (Windowing) and `wry` (WebView). It will not know about "commands" or "state"; it will strictly pass raw string messages to Ruby.
31
+ 2. **Ruby Layer (The Brain):** The application lifecycle, command routing, security allowlists, and state management will be implemented entirely in Ruby.
32
+ 3. **Concurrency Model (Ractor Isolation):**
33
+ * **Main Ractor:** Dedicated exclusively to the `Tao` event loop and Window management.
34
+ * **Worker Ractor:** Dedicated to user business logic, file I/O, and heavy computation.
35
+ * **IPC:** Communication between layers uses `Ractor.send(obj, move: true)` for zero-copy overhead.
36
+
37
+ ## Architecture Specification
38
+
39
+ ### 1. High-Level Diagram
40
+
41
+ [ WebView (HTML/JS) ]
42
+ |
43
+ | IPC (Raw JSON String)
44
+ v
45
+ [ Rust Layer (TokraFFI) ] <-- "Dumb Pipe"
46
+ |
47
+ | Callback
48
+ v
49
+ [ Main Ractor (UI) ] <-- "The Window Manager"
50
+ | 1. Security Check (Allowlist)
51
+ | 2. Ractor.send(move: true)
52
+ v
53
+ [ Worker Ractor (Logic) ] <-- "The User App"
54
+ | 1. Command Routing
55
+ | 2. Business Logic / DB / IO
56
+ |
57
+ | proxy.send_event()
58
+ v
59
+ [ Main Ractor (UI) ] <-- Wakes up Event Loop
60
+ |
61
+ v
62
+ [ Rust Layer (eval_js) ]
63
+ |
64
+ v
65
+ [ WebView (Update UI) ]
66
+
67
+ ### 2. Rust Deliverables (`ext/tokra/src/lib.rs`)
68
+
69
+ The Rust extension must expose three primary classes via Magnus. It should use `cdylib` crate type.
70
+
71
+ #### A. `Tokra::Native::EventLoop`
72
+ * **Responsibility:** Hijack the OS Main Thread for `tao`.
73
+ * **Signature:** `run(&self, callback: Proc)` -> `!` (Never returns)
74
+ * **Signal Safety:** Must implement a `ctrlc` handler that triggers `UserEvent::Exit` to wake the loop on SIGINT. Ruby's `Signal.trap` will not work while Tao is running.
75
+ * **Behavior:**
76
+ * Iterates `event_loop.run_return` or `run`.
77
+ * On `UserEvent` (from Proxy), triggers the callback.
78
+ * On `WindowEvent` (Resize, Close), triggers the callback.
79
+
80
+ #### B. `Tokra::Native::Window`
81
+ * **Responsibility:** Wrap `tao::window::Window`.
82
+ * **Methods:** `new`, `set_title`, `set_size`, `id`.
83
+
84
+ #### C. `Tokra::Native::WebView`
85
+ * **Responsibility:** Wrap `wry::WebView`.
86
+ * **Methods:**
87
+ * `new(window, url, ipc_callback)`
88
+ * `eval(js_string)`
89
+ * **Security Detail:** The `ipc_callback` passed here is the "Border Crossing". Rust blindly fires this Proc whenever the WebView emits a message.
90
+
91
+ #### D. `Tokra::Native::Proxy`
92
+ * **Responsibility:** Thread-safe handle to wake up the loop.
93
+ * **Methods:** `wake_up(payload)`
94
+ * **Implementation:** Wraps `tao::event_loop::EventLoopProxy`. This is critical for Ractors to talk back to the UI.
95
+
96
+ ### 3. Ruby Implementation (`lib/tokra.rb`)
97
+
98
+ The Ruby gem bootstraps the Ractor model.
99
+
100
+ #### The Main Ractor (UI Thread)
101
+
102
+ ```ruby
103
+ module Tokra
104
+ class App
105
+ def self.run(initial_url)
106
+ # 1. Create communication ports
107
+ ipc_port = Ractor::Port.new # Main receives IPC from WebView
108
+ response_port = Ractor::Port.new # Main receives responses from Worker
109
+
110
+ # 2. Setup the logic worker
111
+ worker = Ractor.new(response_port) do |resp_port|
112
+ loop do
113
+ msg = Ractor.receive
114
+ result = yield(msg) # User's logic runs here
115
+ resp_port << result
116
+ end
117
+ end
118
+
119
+ # 3. Setup Rust Windowing (Main Thread)
120
+ event_loop = Tokra::Native::EventLoop.new
121
+ proxy = event_loop.create_proxy
122
+
123
+ # 4. Define the IPC "Bridge" (must be shareable)
124
+ ipc_handler = Ractor.shareable_proc do |raw_msg|
125
+ ipc_port << raw_msg
126
+ end
127
+
128
+ window = Tokra::Native::Window.new(event_loop)
129
+ webview = Tokra::Native::WebView.new(window, initial_url, ipc_handler)
130
+
131
+ # 5. Block forever on Main Thread
132
+ event_loop.run do |event|
133
+ case event
134
+ when Tokra::Native::IpcEvent
135
+ # Security: Validate before forwarding
136
+ worker << { type: :ipc, payload: event.message, proxy: proxy }
137
+ when Tokra::Native::WakeUpEvent
138
+ # Response from worker - send to WebView
139
+ webview.eval("window.__tokra_callback(#{event.payload.to_json})")
140
+ when Tokra::Native::WindowCloseEvent
141
+ break
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ #### The Security Implementation
150
+
151
+ Security is enforced in **Ruby**, not Rust.
152
+
153
+ ```ruby
154
+ class SecurityGuard
155
+ ALLOWLIST = {
156
+ "fs_read" => ->(path) { path.start_with?(APP_DATA_DIR) },
157
+ "net_req" => ->(url) { url.start_with?("https://api.myapp.com") }
158
+ }.freeze
159
+
160
+ def self.authorize!(command, args)
161
+ raise SecurityError, "Access Denied: #{command}" unless ALLOWLIST[command]&.call(args)
162
+ end
163
+ end
164
+ ```
165
+
166
+ ### 4. Build System Specification
167
+ * **Tooling:** Must use `rb_sys` gem to bridge Cargo and Make.
168
+ * **Manifest:** `ext/tokra/extconf.rb` must use `create_rust_makefile("tokra/tokra")`.
169
+ * **Extension:** The compiled binary must be reachable via `require "tokra/tokra"`.
170
+
171
+ ## Consequences
172
+
173
+ ### Positive
174
+ * **Dev Velocity:** Logic changes happen in Ruby (Instant reload potential). Rust compilation is rare.
175
+ * **Cost:** Rust contractor scope is fixed and small (~500 LOC).
176
+ * **Performance:** UI is unblockable. Ractor move semantics avoid GC pressure during IPC.
177
+
178
+ ### Negative
179
+ * **Complexity:** Ractor message passing is strictly async. The user must write async-aware Ruby code (promises/callbacks).
180
+ * **Signals:** Standard Ruby signal trapping (Ctrl+C) is disabled; we rely on the Rust layer to catch OS signals.
181
+ * **Memory:** Minimum 2 Ractors means slightly higher base memory usage than a single-threaded app.
182
+ * **Packaging:** Requires bundling the compiled `.so`/`.dll` inside the gem.
183
+
184
+ ## References
185
+ * [Magnus Documentation](https://docs.rs/magnus)
186
+ * [Ruby 4.0 Ractor Guide](https://docs.ruby-lang.org/en/master/ractor.md)
187
+ * [Wry IPC Interface](https://docs.rs/wry/latest/wry/struct.WebViewBuilder.html#method.with_ipc_handler)
@@ -0,0 +1,132 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # ADR 002: Tokra Rails - The Rack Adapter & "Serverless" Desktop
8
+
9
+ ## Status
10
+ Accepted
11
+
12
+ ## Date
13
+ 2026-01-29
14
+
15
+ ## Context
16
+ Developers want to use **Ruby on Rails** to build Tokra desktop applications.
17
+ The traditional approach (Electron) involves spawning a child process running a TCP web server (like Puma) and pointing the WebView to `http://localhost:3000`.
18
+
19
+ This approach has significant downsides:
20
+ 1. **Security:** It opens a TCP port on the user's machine, which other processes can sniff.
21
+ 2. **Performance:** It incurs TCP/IP loopback overhead.
22
+ 3. **Management:** Dealing with "orphan" server processes when the app closes is difficult.
23
+
24
+ We need a way to run Rails *inside* the Tokra process, leveraging the Ruby 4.0 architecture defined in [ADR 001].
25
+
26
+ ## Decision
27
+ We will implement **Tokra Rails** as a direct Rack Adapter over a Wry Custom Protocol.
28
+
29
+ We will **not** bundle Puma or any TCP server. Instead, we will treat the WebView's resource loader as the "Web Server" and the Rails App as a library function call.
30
+
31
+ ## The "100 Lines" Architecture
32
+
33
+ ### 1. The Mechanism: Custom Protocol (`rails://`)
34
+ Instead of IPC (which is for JSON messages), we will use Wry's **Custom Protocol** feature.
35
+ * **Rust Layer:** We add *one* generic method to our FFI: `register_protocol(scheme, ruby_proc)`.
36
+ * **The Flow:**
37
+ 1. Frontend requests `rails://app/posts/1` (via Turbo, HTMX, or `fetch`).
38
+ 2. Rust intercepts the request.
39
+ 3. Rust passes the HTTP verb, headers, and body to the **Main Ractor** (Ruby).
40
+ 4. Main Ractor sends it to the **Rails Ractor**.
41
+ 5. Rails Ractor runs `MyApp::Application.call(env)`.
42
+ 6. The response (HTML/JSON) is handed back to Rust.
43
+
44
+ ### 2. The Ruby Code (`lib/tokra/rails/handler.rb`)
45
+
46
+ This is the "100 lines" of Ruby. It is a standard `Rack::Handler` that translates the Tokra Protocol Struct into a Rack Environment Hash.
47
+
48
+ ```ruby
49
+ # frozen_string_literal: true
50
+
51
+ module Rack
52
+ module Handler
53
+ class Tokra
54
+ def self.run(app, options = {})
55
+ # The protocol handler must be a shareable proc since it's
56
+ # called from Rust across potential Ractor boundaries
57
+ handler = Ractor.shareable_proc do |req|
58
+ # 1. Map Request -> Rack Env
59
+ env = {
60
+ "REQUEST_METHOD" => req.method,
61
+ "SCRIPT_NAME" => "",
62
+ "PATH_INFO" => req.path,
63
+ "QUERY_STRING" => req.query,
64
+ "SERVER_NAME" => "tokra",
65
+ "SERVER_PORT" => "80",
66
+ "rack.version" => Rack::VERSION,
67
+ "rack.input" => StringIO.new(req.body),
68
+ "rack.errors" => $stderr,
69
+ "rack.multithread" => false, # Requests are serialized through protocol handler
70
+ "rack.run_once" => false,
71
+ "rack.url_scheme" => "rails"
72
+ }
73
+
74
+ # 2. Call Rails (In-Memory)
75
+ status, headers, body_proxy = app.call(env)
76
+
77
+ # 3. Map Response -> Rust Response
78
+ body_string = +""
79
+ body_proxy.each { |chunk| body_string << chunk }
80
+ body_proxy.close if body_proxy.respond_to?(:close)
81
+
82
+ # Return to Rust
83
+ { status: status, headers: headers, body: body_string }
84
+ end
85
+
86
+ # Register the "rails://" protocol hook in Rust
87
+ ::Tokra::Native.register_protocol("rails", &handler)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### 3. The JS Code (`10 lines`)
95
+ We need a tiny shim to ensure relative links work and Turbo behaves correctly with the custom protocol.
96
+
97
+ // app/javascript/tokra_shim.js
98
+ document.addEventListener("turbo:before-fetch-request", (event) => {
99
+ // Ensure Turbo knows we are using a custom protocol
100
+ // (Turbo 8+ handles non-HTTP protocols gracefully usually)
101
+ })
102
+
103
+ // Optional: Bridge for "Server Push" via standard IPC
104
+ window.Tokra.on("rails:stream", (html) => {
105
+ Turbo.renderStreamMessage(html)
106
+ })
107
+
108
+ ## Integration with Solid* & SQLite
109
+
110
+ Because the Rails app runs inside the **Worker Ractor** (defined in ADR 001), strict isolation rules apply:
111
+
112
+ 1. **SQLite Ownership:** `SQLite3::Database` objects are NOT Ractor-shareable.
113
+ * **Constraint:** You cannot establish the DB connection in the Main Ractor and pass it down.
114
+ * **Implementation:** The Rails app must lazy-load the connection (`ActiveRecord::Base.establish_connection`) *only* after the Worker Ractor has started.
115
+ 2. **Solid Queue:** The background worker threads for Solid Queue spawn *inside* the Worker Ractor (or we spawn a dedicated `Queue Ractor`).
116
+ 3. **Solid Cache:** Writes to the local SQLite DB.
117
+
118
+ ## Consequences
119
+
120
+ ### Positive
121
+ * **Zero Latency:** No TCP handshake. Request/Response is a memory copy (or move).
122
+ * **Native Assets:** Images/CSS served via `rails://app/assets/...` work exactly like standard web pages.
123
+ * **Standard Rails:** Developers use `routes.rb`, Controllers, and Views exactly as normal. No "API-only" mode required.
124
+
125
+ ### Negative
126
+ * **Streaming Limitations:** The `rails://` custom protocol does NOT support `ActionController::Live` or Turbo Streams (chunked responses).
127
+ * **Mitigation:** Streaming features must use the IPC Bridge (`Tokra.on('stream')`) instead of the protocol handler.
128
+ * **Cookies:** The WebView manages cookies for the custom protocol, but we must ensure `set-cookie` headers are respected by Wry's custom protocol implementation.
129
+
130
+ ## Validation
131
+ * **Is it 100 lines?** Yes. The Rack Handler logic is mostly hash mapping.
132
+ * **Does Solid work?** Yes. Solid is just Ruby code and SQL. It doesn't know it's not running in a Puma process.