turbo_desktop-rails 0.0.1 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +28 -1
- data/app/controllers/turbo_desktop/inspector_assets_controller.rb +44 -0
- data/config/routes.rb +4 -0
- data/lib/generators/turbo_desktop/install/install_generator.rb +30 -0
- data/lib/generators/turbo_desktop/install/templates/initializer.rb.tt +49 -0
- data/lib/turbo_desktop/configuration.rb +10 -2
- data/lib/turbo_desktop/detection.rb +38 -0
- data/lib/turbo_desktop/inspector_assets/inspector/bridge-tap.js +59 -0
- data/lib/turbo_desktop/inspector_assets/inspector/catalog.js +75 -0
- data/lib/turbo_desktop/inspector_assets/inspector/panel.js +148 -0
- data/lib/turbo_desktop/inspector_assets/inspector/state.js +61 -0
- data/lib/turbo_desktop/inspector_assets/inspector.js +57 -0
- data/lib/turbo_desktop/version.rb +1 -1
- data/lib/turbo_desktop/view_helpers.rb +46 -1
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28aa3a8403d578104476a660dd9ee06a6c89e662f16264f7142451413ba32963
|
|
4
|
+
data.tar.gz: 99e458936a6838a91b3aa205ed21931436b726a14758ba52d6bebeb6e643a35e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 846e2a35b02b73557bfcfb11cde442d93d5a3b32be010cfaf0433d78c375650c41c143c17a35f14ce9d8f34e87e183f95a3a5a79dbe6e220b23557f4d1e5525e
|
|
7
|
+
data.tar.gz: c04b4ac951f6b2176af91eb021e6278602b814db6a99e74bd1d015a6562bb6c8020a5b8901b8ee78c2cb1b848006b2c9eaf1d491bfdf2116f9394671227fd330
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 (2026-09-27)
|
|
4
|
+
|
|
5
|
+
Version aligned with the desktop shell's 0.2.2 release. The CLI now scaffolds
|
|
6
|
+
new apps with `gem "turbo_desktop-rails", "~> 0.2"`; it previously wrote
|
|
7
|
+
`"~> 0.1"`, which resolved to 0.1.1. First 0.2.x version published to RubyGems.
|
|
8
|
+
No gem-side API changes.
|
|
9
|
+
|
|
10
|
+
## 0.2.1 (2026-07-29)
|
|
11
|
+
|
|
12
|
+
Version aligned with the desktop shell's 0.2.1 release, which fixes bridge
|
|
13
|
+
events (shell/sudo output streaming, drag & drop, component onReceive) never
|
|
14
|
+
reaching pages loaded from the app server. No gem-side API changes.
|
|
15
|
+
|
|
16
|
+
## 0.2.0 (2026-07-29)
|
|
17
|
+
|
|
18
|
+
Version aligned with the desktop shell's 0.2.0 release (server auto-start,
|
|
19
|
+
Windows support, cross-platform sudo, drag & drop, file associations,
|
|
20
|
+
clipboard, launch-at-login). No gem-side API changes.
|
|
21
|
+
|
|
22
|
+
## 0.1.1 (2026-07-27)
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Install generator: `rails generate turbo_desktop:install` scaffolds the initializer.
|
|
27
|
+
- Dev Inspector support:
|
|
28
|
+
- `config.inspector_enabled` and the `turbo_desktop_inspector_meta_tag` view helper to enable
|
|
29
|
+
the in-app inspector overlay (dev only).
|
|
30
|
+
- The gem now serves the inspector's JavaScript **same-origin** at `/turbo-desktop/inspector.js`
|
|
31
|
+
(and its sub-modules), so the desktop shell can `import()` it without extra setup.
|
|
32
|
+
- `config.inspector_mount_path` to match a custom engine mount point.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Minimum Ruby version is now 3.3.
|
|
37
|
+
|
|
3
38
|
## 0.0.1 (2026-03-22)
|
|
4
39
|
|
|
5
40
|
- Initial release
|
data/README.md
CHANGED
|
@@ -56,6 +56,33 @@ turbo_desktop_arch # => "aarch64", "x86_64", or nil
|
|
|
56
56
|
) %>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Desktop-only templates
|
|
60
|
+
|
|
61
|
+
Requests from the desktop app are marked with a Rails variant, so a whole
|
|
62
|
+
template can be written for it rather than branching inside a shared one:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
app/views/orders/show.html.erb # everyone
|
|
66
|
+
app/views/orders/show.html+desktop.erb # the desktop app
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Layouts work the same way — `app/views/layouts/application.html+desktop.erb`.
|
|
70
|
+
Rails falls back to the plain template wherever no variant exists, so this costs
|
|
71
|
+
nothing until you add one.
|
|
72
|
+
|
|
73
|
+
The block helpers above are still the right tool for a button or a nav bar. Reach
|
|
74
|
+
for a variant when the whole page differs.
|
|
75
|
+
|
|
76
|
+
Rename it, or turn it off, in the initializer:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
TurboDesktop.configure do |config|
|
|
80
|
+
config.variant = :desktop # nil leaves variants alone
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It is added to any variants you have already set rather than replacing them.
|
|
85
|
+
|
|
59
86
|
### Path Configuration
|
|
60
87
|
|
|
61
88
|
The gem mounts a path configuration endpoint at `/turbo-desktop/path-configuration.json`:
|
|
@@ -76,7 +103,7 @@ end
|
|
|
76
103
|
|
|
77
104
|
## Requirements
|
|
78
105
|
|
|
79
|
-
- Ruby >= 3.
|
|
106
|
+
- Ruby >= 3.3
|
|
80
107
|
- Rails >= 7.0
|
|
81
108
|
- turbo-rails >= 1.0
|
|
82
109
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module TurboDesktop
|
|
2
|
+
# Serves the Dev Inspector's JavaScript from the Rails origin so the desktop
|
|
3
|
+
# shell's `turbo-desktop.js` can `import()` it same-origin.
|
|
4
|
+
#
|
|
5
|
+
# The shell points its WebView at your Rails app (server_url), so a relative
|
|
6
|
+
# `import("./inspector.js")` resolves against the Rails origin — which is why
|
|
7
|
+
# these assets must be served here, not from the bundled Tauri frontend.
|
|
8
|
+
#
|
|
9
|
+
# Only the fixed set of inspector modules is served (strict allow-list, so no
|
|
10
|
+
# path traversal). Enabled implicitly by mounting the engine; the meta-tag
|
|
11
|
+
# helper points the shell at these URLs.
|
|
12
|
+
class InspectorAssetsController < ActionController::Base
|
|
13
|
+
ASSET_ROOT = TurboDesktop::Engine.root.join("lib/turbo_desktop/inspector_assets").freeze
|
|
14
|
+
|
|
15
|
+
# Relative paths (as requested by the browser) → allowed. Anything else 404s.
|
|
16
|
+
ALLOWED = %w[
|
|
17
|
+
inspector.js
|
|
18
|
+
inspector/state.js
|
|
19
|
+
inspector/panel.js
|
|
20
|
+
inspector/bridge-tap.js
|
|
21
|
+
inspector/catalog.js
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# GET /turbo-desktop/inspector.js
|
|
25
|
+
# GET /turbo-desktop/inspector/<module>.js
|
|
26
|
+
def show
|
|
27
|
+
rel = requested_asset
|
|
28
|
+
return head(:not_found) unless ALLOWED.include?(rel)
|
|
29
|
+
|
|
30
|
+
path = ASSET_ROOT.join(rel)
|
|
31
|
+
return head(:not_found) unless File.file?(path)
|
|
32
|
+
|
|
33
|
+
response.set_header("Cache-Control", "public, max-age=3600")
|
|
34
|
+
render body: File.read(path), content_type: "text/javascript"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def requested_asset
|
|
40
|
+
mod = params[:module]
|
|
41
|
+
mod.blank? ? "inspector.js" : "inspector/#{mod}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
TurboDesktop::Engine.routes.draw do
|
|
2
2
|
get "path-configuration", to: "path_configurations#show", defaults: { format: :json }
|
|
3
|
+
|
|
4
|
+
# Dev Inspector assets, served same-origin so the desktop shell can import() them.
|
|
5
|
+
get "inspector.js", to: "inspector_assets#show", format: false
|
|
6
|
+
get "inspector/*module", to: "inspector_assets#show", format: false
|
|
3
7
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module TurboDesktop
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Install Turbo Desktop into your Rails application"
|
|
9
|
+
|
|
10
|
+
def copy_initializer
|
|
11
|
+
template "initializer.rb.tt", "config/initializers/turbo_desktop.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mount_engine
|
|
15
|
+
route 'mount TurboDesktop::Engine => "/turbo-desktop"'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show_next_steps
|
|
19
|
+
say ""
|
|
20
|
+
say "Turbo Desktop installed!", :green
|
|
21
|
+
say ""
|
|
22
|
+
say "Next steps:"
|
|
23
|
+
say " 1. npx turbo-desktop init # Scaffold the desktop shell"
|
|
24
|
+
say " 2. rails server # Start your Rails app"
|
|
25
|
+
say " 3. npx turbo-desktop dev # Launch the desktop app"
|
|
26
|
+
say ""
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# config/initializers/turbo_desktop.rb
|
|
2
|
+
#
|
|
3
|
+
# Configure how the Turbo Desktop native shell presents your Rails routes.
|
|
4
|
+
# This path configuration follows the same pattern as Hotwire Native (turbo-ios/turbo-android).
|
|
5
|
+
#
|
|
6
|
+
# Presentations:
|
|
7
|
+
# "default" — Navigate in the current window (standard Turbo Drive behavior)
|
|
8
|
+
# "modal" — Open in a modal/sheet window
|
|
9
|
+
# "new_window" — Open in a separate window
|
|
10
|
+
# "replace" — Replace the current page (no back button)
|
|
11
|
+
# "native" — Route to a fully native screen (handled by Rust/Tauri)
|
|
12
|
+
# "none" — Do nothing (handled by a bridge component)
|
|
13
|
+
|
|
14
|
+
TurboDesktop.configure do |config|
|
|
15
|
+
# Dev Inspector — an in-app overlay (Cmd/Ctrl+Shift+D) that surfaces available
|
|
16
|
+
# bridge components, a live web↔native message log, and the current path-config
|
|
17
|
+
# presentation. Enable it in development only:
|
|
18
|
+
config.inspector_enabled = Rails.env.development?
|
|
19
|
+
|
|
20
|
+
# Requests from the desktop app are marked with a Rails variant, so you can
|
|
21
|
+
# write a whole template for it instead of branching inside a shared one:
|
|
22
|
+
#
|
|
23
|
+
# app/views/orders/show.html.erb # everyone
|
|
24
|
+
# app/views/orders/show.html+desktop.erb # the desktop app
|
|
25
|
+
#
|
|
26
|
+
# Layouts work the same way (layouts/application.html+desktop.erb), and Rails
|
|
27
|
+
# falls back to the plain template wherever no variant exists. Rename it, or
|
|
28
|
+
# set it to nil to leave variants alone.
|
|
29
|
+
# config.variant = :desktop
|
|
30
|
+
|
|
31
|
+
config.path_configuration = {
|
|
32
|
+
settings: {
|
|
33
|
+
screenshots_enabled: false
|
|
34
|
+
},
|
|
35
|
+
rules: [
|
|
36
|
+
# Default: all pages navigate in the current window
|
|
37
|
+
{ patterns: ["/"], properties: { presentation: "default" } },
|
|
38
|
+
|
|
39
|
+
# Forms open in a modal window
|
|
40
|
+
{ patterns: ["/new$", "/edit$"], properties: { presentation: "modal" } },
|
|
41
|
+
|
|
42
|
+
# Example: settings could be a native screen
|
|
43
|
+
# { patterns: ["/settings"], properties: { presentation: "native" } },
|
|
44
|
+
|
|
45
|
+
# Example: external links open in a new window
|
|
46
|
+
# { patterns: ["^http"], properties: { presentation: "new_window" } },
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
end
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
module TurboDesktop
|
|
2
2
|
class Configuration
|
|
3
|
-
attr_accessor :path_configuration, :user_agent_pattern
|
|
3
|
+
attr_accessor :path_configuration, :user_agent_pattern, :inspector_enabled,
|
|
4
|
+
:inspector_mount_path, :variant
|
|
4
5
|
|
|
5
6
|
def initialize
|
|
6
7
|
@path_configuration = default_path_configuration
|
|
7
8
|
@user_agent_pattern = /Turbo Desktop/
|
|
9
|
+
@inspector_enabled = false
|
|
10
|
+
# Rails variant set on requests from the desktop app, so views can be
|
|
11
|
+
# written as show.html+desktop.erb. Set to nil to leave variants alone.
|
|
12
|
+
@variant = :desktop
|
|
13
|
+
# Where the engine is mounted; the inspector meta tag advertises assets
|
|
14
|
+
# under this prefix. Override if you mount the engine elsewhere.
|
|
15
|
+
@inspector_mount_path = "/turbo-desktop"
|
|
8
16
|
end
|
|
9
17
|
|
|
10
18
|
def path_configuration_json
|
|
@@ -19,7 +27,7 @@ module TurboDesktop
|
|
|
19
27
|
screenshots_enabled: false
|
|
20
28
|
},
|
|
21
29
|
rules: [
|
|
22
|
-
{ patterns: ["/"], properties: { presentation: "default" } }
|
|
30
|
+
{ patterns: [ "/" ], properties: { presentation: "default" } }
|
|
23
31
|
]
|
|
24
32
|
}
|
|
25
33
|
end
|
|
@@ -11,6 +11,23 @@ module TurboDesktop
|
|
|
11
11
|
|
|
12
12
|
included do
|
|
13
13
|
helper_method :turbo_desktop_app?, :turbo_desktop_platform, :turbo_desktop_arch
|
|
14
|
+
|
|
15
|
+
before_action :set_turbo_desktop_variant
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The variant list a request should end up with, or nil to leave it alone.
|
|
19
|
+
#
|
|
20
|
+
# Kept separate from the controller so the decision can be exercised on its
|
|
21
|
+
# own. Adds to whatever variants are already set rather than replacing them:
|
|
22
|
+
# an app may well be using variants for something else.
|
|
23
|
+
def self.variant_for(current:, desktop:, configured:)
|
|
24
|
+
return nil if configured.blank? || !desktop
|
|
25
|
+
|
|
26
|
+
current = Array(current).map(&:to_sym)
|
|
27
|
+
configured = configured.to_sym
|
|
28
|
+
return nil if current.include?(configured)
|
|
29
|
+
|
|
30
|
+
current + [ configured ]
|
|
14
31
|
end
|
|
15
32
|
|
|
16
33
|
# Returns true if the request is from a Turbo Desktop app.
|
|
@@ -31,6 +48,27 @@ module TurboDesktop
|
|
|
31
48
|
end
|
|
32
49
|
end
|
|
33
50
|
|
|
51
|
+
# Mark desktop requests with a Rails variant, so a whole template can be
|
|
52
|
+
# written for the desktop app instead of branching inside a shared one:
|
|
53
|
+
#
|
|
54
|
+
# app/views/orders/show.html.erb # everyone
|
|
55
|
+
# app/views/orders/show.html+desktop.erb # the desktop app
|
|
56
|
+
#
|
|
57
|
+
# Layouts pick it up too — layouts/application.html+desktop.erb. Rails falls
|
|
58
|
+
# back to the plain template wherever no variant exists, so this costs
|
|
59
|
+
# nothing until you add one.
|
|
60
|
+
#
|
|
61
|
+
# Set config.variant to nil to turn it off.
|
|
62
|
+
def set_turbo_desktop_variant
|
|
63
|
+
variant = TurboDesktop::Detection.variant_for(
|
|
64
|
+
current: request.variant,
|
|
65
|
+
desktop: turbo_desktop_app?,
|
|
66
|
+
configured: TurboDesktop.configuration.variant
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
request.variant = variant if variant
|
|
70
|
+
end
|
|
71
|
+
|
|
34
72
|
# Returns the architecture: "aarch64", "x86_64", or nil.
|
|
35
73
|
def turbo_desktop_arch
|
|
36
74
|
return nil unless turbo_desktop_app?
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observes bridge traffic without altering it.
|
|
3
|
+
*
|
|
4
|
+
* install() wraps host.sendBridgeMessage so every outbound call is recorded
|
|
5
|
+
* then forwarded to the original exactly once, with its return/throw preserved.
|
|
6
|
+
* observeResponse() records inbound bridge-response payloads.
|
|
7
|
+
*
|
|
8
|
+
* Recording runs inside its own try/catch so a logging bug can never affect a
|
|
9
|
+
* real bridge call. No DOM access.
|
|
10
|
+
*/
|
|
11
|
+
export class BridgeTap {
|
|
12
|
+
constructor(host, { onRecord, now = () => Date.now() } = {}) {
|
|
13
|
+
this.host = host;
|
|
14
|
+
this.onRecord = onRecord;
|
|
15
|
+
this.now = now;
|
|
16
|
+
this._installed = false;
|
|
17
|
+
this._original = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
install() {
|
|
21
|
+
if (this._installed) return;
|
|
22
|
+
// Store the raw property so uninstall can restore the exact same reference.
|
|
23
|
+
this._original = this.host.sendBridgeMessage;
|
|
24
|
+
const original = this._original.bind(this.host);
|
|
25
|
+
const self = this;
|
|
26
|
+
this.host.sendBridgeMessage = function (component, event, data = {}) {
|
|
27
|
+
self._safeRecord({ direction: "out", component, event, data, ts: self.now() });
|
|
28
|
+
return original(component, event, data); // pass-through, exactly once
|
|
29
|
+
};
|
|
30
|
+
this._installed = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Record an inbound bridge-response payload. */
|
|
34
|
+
observeResponse(payload) {
|
|
35
|
+
if (!payload) return;
|
|
36
|
+
this._safeRecord({
|
|
37
|
+
direction: "in",
|
|
38
|
+
component: payload.component,
|
|
39
|
+
event: payload.event || "response",
|
|
40
|
+
data: payload.data !== undefined ? payload.data : payload,
|
|
41
|
+
ts: this.now(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
uninstall() {
|
|
46
|
+
if (this._installed && this._original) {
|
|
47
|
+
this.host.sendBridgeMessage = this._original;
|
|
48
|
+
this._installed = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_safeRecord(record) {
|
|
53
|
+
try {
|
|
54
|
+
if (this.onRecord) this.onRecord(record);
|
|
55
|
+
} catch (_e) {
|
|
56
|
+
/* recording must never break the host */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static catalog of built-in bridge components.
|
|
3
|
+
* Single source of truth for the Inspector's "Available" list and snippets.
|
|
4
|
+
* Each entry: { description, erb, stimulus }.
|
|
5
|
+
*/
|
|
6
|
+
export const CATALOG = {
|
|
7
|
+
"notification": {
|
|
8
|
+
description: "Show native OS notifications.",
|
|
9
|
+
erb: `<button data-controller="notification"\n data-action="click->notification#notify"\n data-body="Saved!">Notify</button>`,
|
|
10
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "notification") {\n notify(e) { this.sendBridge("connect", { title: "My App", body: e.target.dataset.body }) }\n}`,
|
|
11
|
+
},
|
|
12
|
+
"menu-item": {
|
|
13
|
+
description: "Register an item in the native menu bar.",
|
|
14
|
+
erb: `<%= tag.button "Export PDF",\n **turbo_desktop_bridge("menu-item", title: "Export PDF", shortcut: "Cmd+E") %>`,
|
|
15
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "menu-item") {\n connect() { super.connect(); this.sendBridge("register", { title: "Export PDF", shortcut: "Cmd+E" }) }\n}`,
|
|
16
|
+
},
|
|
17
|
+
"file-picker": {
|
|
18
|
+
description: "Open a native file open/save dialog.",
|
|
19
|
+
erb: `<button data-controller="file-picker"\n data-action="click->file-picker#open">Choose file…</button>`,
|
|
20
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "file-picker") {\n open() { this.sendBridge("open", { multiple: false }) }\n receiveBridge(msg) { console.log("picked", msg.data) }\n}`,
|
|
21
|
+
},
|
|
22
|
+
"badge": {
|
|
23
|
+
description: "Set the dock / taskbar badge count.",
|
|
24
|
+
erb: `<span data-controller="badge" data-badge-count-value="3"></span>`,
|
|
25
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "badge") {\n static values = { count: Number }\n connect() { super.connect(); this.sendBridge("set", { count: this.countValue }) }\n}`,
|
|
26
|
+
},
|
|
27
|
+
"shortcut": {
|
|
28
|
+
description: "Register a global keyboard shortcut.",
|
|
29
|
+
erb: `<div data-controller="shortcut" data-shortcut-keys-value="CmdOrCtrl+K"></div>`,
|
|
30
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "shortcut") {\n static values = { keys: String }\n connect() { super.connect(); this.sendBridge("register", { keys: this.keysValue }) }\n receiveBridge() { /* fired when the shortcut is pressed */ }\n}`,
|
|
31
|
+
},
|
|
32
|
+
"shell": {
|
|
33
|
+
description: "Spawn and manage native shell processes.",
|
|
34
|
+
erb: `<button data-controller="shell" data-action="click->shell#run">Run</button>`,
|
|
35
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "shell") {\n run() { TurboDesktop.shell.spawn("job-1", "echo", ["hello"]) }\n}`,
|
|
36
|
+
},
|
|
37
|
+
"filesystem": {
|
|
38
|
+
description: "Read and write files through the native filesystem bridge.",
|
|
39
|
+
erb: `<button data-controller="fs" data-action="click->fs#read">Read file</button>`,
|
|
40
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "filesystem") {\n read() { this.sendBridge("read", { path: "~/notes.txt" }) }\n receiveBridge(msg) { console.log(msg.data) }\n}`,
|
|
41
|
+
},
|
|
42
|
+
"sudo": {
|
|
43
|
+
description: "Run a privileged command via the native elevation prompt.",
|
|
44
|
+
erb: `<button data-controller="sudo" data-action="click->sudo#elevate">Install</button>`,
|
|
45
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "sudo") {\n elevate() { this.sendBridge("execute", { command: "brew install foo" }) }\n}`,
|
|
46
|
+
},
|
|
47
|
+
"tray": {
|
|
48
|
+
description: "Add items to the system tray / menu-bar icon.",
|
|
49
|
+
erb: `<div data-controller="tray" data-tray-title-value="My App"></div>`,
|
|
50
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "tray") {\n static values = { title: String }\n connect() { super.connect(); this.sendBridge("set", { tooltip: this.titleValue }) }\n}`,
|
|
51
|
+
},
|
|
52
|
+
"deep-link": {
|
|
53
|
+
description: "Handle custom-scheme deep links opened from outside the app.",
|
|
54
|
+
erb: `<div data-controller="deep-link"></div>`,
|
|
55
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "deep-link") {\n receiveBridge(msg) { Turbo.visit(msg.data.path) }\n}`,
|
|
56
|
+
},
|
|
57
|
+
"updater": {
|
|
58
|
+
description: "Check for and apply native app updates.",
|
|
59
|
+
erb: `<button data-controller="updater" data-action="click->updater#check">Check for updates</button>`,
|
|
60
|
+
stimulus: `import { Controller } from "@hotwired/stimulus"\nexport default class extends TurboDesktop.stimulusBridge(Controller, "updater") {\n check() { this.sendBridge("check", {}) }\n receiveBridge(msg) { console.log("update status", msg.data) }\n}`,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const entry of Object.values(CATALOG)) Object.freeze(entry);
|
|
65
|
+
Object.freeze(CATALOG);
|
|
66
|
+
|
|
67
|
+
/** Names of every catalogued component. */
|
|
68
|
+
export function listComponents() {
|
|
69
|
+
return Object.keys(CATALOG);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Look up one component's metadata, or null if unknown. */
|
|
73
|
+
export function getComponent(name) {
|
|
74
|
+
return Object.prototype.hasOwnProperty.call(CATALOG, name) ? CATALOG[name] : null;
|
|
75
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { listComponents, getComponent } from "./catalog.js";
|
|
2
|
+
|
|
3
|
+
function escapeHtml(value) {
|
|
4
|
+
return String(value)
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function escapeAttr(value) {
|
|
11
|
+
return escapeHtml(value).replace(/"/g, """);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shadow-DOM overlay that renders an InspectorState.
|
|
16
|
+
* Subscribes to state changes; never mutates state. No Tauri access.
|
|
17
|
+
*/
|
|
18
|
+
export class InspectorPanel {
|
|
19
|
+
constructor(state, { document }) {
|
|
20
|
+
this.state = state;
|
|
21
|
+
this.document = document;
|
|
22
|
+
this.visible = false;
|
|
23
|
+
this.activeTab = "components";
|
|
24
|
+
this.filter = "";
|
|
25
|
+
this.hostEl = null;
|
|
26
|
+
this.root = null; // shadow root
|
|
27
|
+
this._unsub = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
mount(parent) {
|
|
31
|
+
const host = this.document.createElement("div");
|
|
32
|
+
host.setAttribute("data-turbo-desktop-inspector", "");
|
|
33
|
+
host.style.cssText = "position:fixed;right:0;bottom:0;z-index:2147483647;";
|
|
34
|
+
this.root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
|
|
35
|
+
this.hostEl = host;
|
|
36
|
+
parent.appendChild(host);
|
|
37
|
+
this._unsub = this.state.subscribe(() => this.render());
|
|
38
|
+
this.render();
|
|
39
|
+
this._applyVisibility();
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
unmount() {
|
|
44
|
+
if (this._unsub) this._unsub();
|
|
45
|
+
if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
show() { this.visible = true; this._applyVisibility(); }
|
|
49
|
+
hide() { this.visible = false; this._applyVisibility(); }
|
|
50
|
+
toggle() { this.visible = !this.visible; this._applyVisibility(); }
|
|
51
|
+
|
|
52
|
+
selectTab(tab) { this.activeTab = tab; this.render(); }
|
|
53
|
+
setFilter(text) { this.filter = text || ""; this.render(); }
|
|
54
|
+
|
|
55
|
+
_applyVisibility() {
|
|
56
|
+
if (this.hostEl) this.hostEl.style.display = this.visible ? "block" : "none";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
if (!this.root) return;
|
|
61
|
+
try {
|
|
62
|
+
this.root.innerHTML = this._html();
|
|
63
|
+
this._wire();
|
|
64
|
+
} catch (_e) {
|
|
65
|
+
this.root.innerHTML = "<div><unrenderable></div>";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_wire() {
|
|
70
|
+
const q = (sel) => this.root.querySelectorAll(sel);
|
|
71
|
+
q("[data-tab]").forEach((btn) => {
|
|
72
|
+
btn.addEventListener("click", () => this.selectTab(btn.dataset.tab));
|
|
73
|
+
});
|
|
74
|
+
const input = this.root.querySelector("[data-filter]");
|
|
75
|
+
if (input) input.addEventListener("input", (e) => this.setFilter(e.target.value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_html() {
|
|
79
|
+
return `
|
|
80
|
+
<style>
|
|
81
|
+
:host { all: initial; }
|
|
82
|
+
.wrap { font: 12px/1.4 monospace; width: 420px; height: 320px; background:#111; color:#eee; border:1px solid #333; display:flex; flex-direction:column; }
|
|
83
|
+
.tabs { display:flex; }
|
|
84
|
+
.tabs button { flex:1; background:#222; color:#ccc; border:0; padding:6px; cursor:pointer; }
|
|
85
|
+
.tabs button[aria-selected="true"] { background:#0a84ff; color:#fff; }
|
|
86
|
+
.body { flex:1; overflow:auto; padding:8px; }
|
|
87
|
+
.row { padding:2px 0; border-bottom:1px solid #222; }
|
|
88
|
+
.muted { color:#888; }
|
|
89
|
+
input { width:100%; box-sizing:border-box; margin-bottom:6px; background:#000; color:#eee; border:1px solid #333; }
|
|
90
|
+
</style>
|
|
91
|
+
<div class="wrap">
|
|
92
|
+
<div class="tabs">
|
|
93
|
+
${["components", "messages", "navigation", "shell"].map((t) =>
|
|
94
|
+
`<button data-tab="${t}" aria-selected="${this.activeTab === t}">${t}</button>`).join("")}
|
|
95
|
+
</div>
|
|
96
|
+
<div class="body">
|
|
97
|
+
${this._panelHtml()}
|
|
98
|
+
</div>
|
|
99
|
+
</div>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_panelHtml() {
|
|
103
|
+
switch (this.activeTab) {
|
|
104
|
+
case "messages": return this._messagesHtml();
|
|
105
|
+
case "navigation": return this._navHtml();
|
|
106
|
+
case "shell": return this._shellHtml();
|
|
107
|
+
default: return this._componentsHtml();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_componentsHtml() {
|
|
112
|
+
const active = new Map(this.state.components().map((c) => [c.name, c]));
|
|
113
|
+
const rows = listComponents().map((name) => {
|
|
114
|
+
const c = getComponent(name);
|
|
115
|
+
const seen = active.get(name);
|
|
116
|
+
const tag = seen ? `<span>×${escapeHtml(seen.count)} (${escapeHtml(seen.lastEvent)})</span>` : `<span class="muted">available</span>`;
|
|
117
|
+
return `<div class="row"><strong>${escapeHtml(name)}</strong> ${tag}<br><span class="muted">${escapeHtml(c.description)}</span></div>`;
|
|
118
|
+
}).join("");
|
|
119
|
+
return `<div data-panel="components">${rows}</div>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_messagesHtml() {
|
|
123
|
+
const f = this.filter.toLowerCase();
|
|
124
|
+
const rows = this.state.messages
|
|
125
|
+
.filter((m) => !f || (m.component || "").toLowerCase().includes(f))
|
|
126
|
+
.map((m) => {
|
|
127
|
+
const arrow = m.direction === "out" ? "↑" : "↓";
|
|
128
|
+
return `<div class="row" data-message-row>${arrow} <strong>${escapeHtml(m.component)}</strong> ${escapeHtml(m.event)} <span class="muted">${escapeHtml(JSON.stringify(m.data))}</span></div>`;
|
|
129
|
+
}).join("");
|
|
130
|
+
return `<div data-panel="messages"><input data-filter placeholder="filter by component" value="${escapeAttr(this.filter)}">${rows}</div>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_navHtml() {
|
|
134
|
+
const n = this.state.nav;
|
|
135
|
+
return `<div data-panel="navigation"><div class="row">URL: ${escapeHtml(n.url || "—")}</div><div class="row">Presentation: <strong>${escapeHtml(n.presentation || "default")}</strong></div></div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_shellHtml() {
|
|
139
|
+
const s = this.state.shell;
|
|
140
|
+
return `<div data-panel="shell">
|
|
141
|
+
<div class="row">Platform: ${escapeHtml(s.platform || "—")}</div>
|
|
142
|
+
<div class="row">Arch: ${escapeHtml(s.arch || "—")}</div>
|
|
143
|
+
<div class="row">Version: ${escapeHtml(s.version || "—")}</div>
|
|
144
|
+
<div class="row">Server: ${escapeHtml(s.serverUrl || "—")}</div>
|
|
145
|
+
<div class="row">Updater: ${escapeHtml(s.updater || "—")}</div>
|
|
146
|
+
</div>`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory model for the Dev Inspector.
|
|
3
|
+
* Holds a bounded ring buffer of message records plus a derived component
|
|
4
|
+
* summary and nav/shell facts. Emits "change" to subscribers. No DOM.
|
|
5
|
+
*/
|
|
6
|
+
export class InspectorState {
|
|
7
|
+
constructor({ capacity = 200 } = {}) {
|
|
8
|
+
this.capacity = capacity;
|
|
9
|
+
this.messages = [];
|
|
10
|
+
this._components = new Map(); // name -> { count, lastEvent }
|
|
11
|
+
this.nav = { url: null, presentation: null };
|
|
12
|
+
this.shell = { platform: null, arch: null, version: null, serverUrl: null, updater: null };
|
|
13
|
+
this._listeners = new Set();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Append a record { direction, component, event, data, ts }. */
|
|
17
|
+
record(record) {
|
|
18
|
+
this.messages.push(record);
|
|
19
|
+
if (this.messages.length > this.capacity) this.messages.shift();
|
|
20
|
+
|
|
21
|
+
const prev = this._components.get(record.component) || { count: 0, lastEvent: null };
|
|
22
|
+
prev.count += 1;
|
|
23
|
+
prev.lastEvent = record.event;
|
|
24
|
+
this._components.set(record.component, prev);
|
|
25
|
+
|
|
26
|
+
this._emit();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Derived component summary as an array. */
|
|
30
|
+
components() {
|
|
31
|
+
return [...this._components.entries()].map(([name, v]) => ({ name, count: v.count, lastEvent: v.lastEvent }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setNav(nav) {
|
|
35
|
+
this.nav = { ...this.nav, ...nav };
|
|
36
|
+
this._emit();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setShell(shell) {
|
|
40
|
+
this.shell = { ...this.shell, ...shell };
|
|
41
|
+
this._emit();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear() {
|
|
45
|
+
this.messages = [];
|
|
46
|
+
this._components.clear();
|
|
47
|
+
this._emit();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Subscribe to changes; returns an unsubscribe function. */
|
|
51
|
+
subscribe(fn) {
|
|
52
|
+
this._listeners.add(fn);
|
|
53
|
+
return () => this._listeners.delete(fn);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_emit() {
|
|
57
|
+
for (const fn of this._listeners) {
|
|
58
|
+
try { fn(this); } catch (_e) { /* a bad subscriber must not break others */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Inspector entry point.
|
|
3
|
+
*
|
|
4
|
+
* startInspector(host, env) wires the inspector units onto a TurboDesktop-like
|
|
5
|
+
* host: it installs a BridgeTap on the host's sendBridgeMessage, subscribes to
|
|
6
|
+
* inbound bridge-response events, seeds shell facts, mounts the Shadow-DOM
|
|
7
|
+
* panel, and binds the toggle hotkey (Cmd/Ctrl+Shift+D).
|
|
8
|
+
*
|
|
9
|
+
* This module is loaded lazily by turbo-desktop.js only when the inspector gate
|
|
10
|
+
* passes, so it ships no code to production builds.
|
|
11
|
+
*/
|
|
12
|
+
import { BridgeTap } from "./inspector/bridge-tap.js";
|
|
13
|
+
import { InspectorState } from "./inspector/state.js";
|
|
14
|
+
import { InspectorPanel } from "./inspector/panel.js";
|
|
15
|
+
|
|
16
|
+
export function startInspector(host, { doc = document, win = window } = {}) {
|
|
17
|
+
const state = new InspectorState();
|
|
18
|
+
|
|
19
|
+
const now = (win.Date && typeof win.Date.now === "function") ? () => win.Date.now() : () => 0;
|
|
20
|
+
const tap = new BridgeTap(host, { onRecord: (r) => state.record(r), now });
|
|
21
|
+
tap.install();
|
|
22
|
+
|
|
23
|
+
const internals = win.__TAURI_INTERNALS__;
|
|
24
|
+
if (internals && internals.event && typeof internals.event.listen === "function") {
|
|
25
|
+
internals.event.listen("bridge-response", (e) => tap.observeResponse(e && e.payload));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
state.setShell({ platform: host.platform, version: host.version });
|
|
29
|
+
if (typeof host.getWindowInfo === "function") {
|
|
30
|
+
host.getWindowInfo().then((info) => { if (info) state.setShell(info); }).catch(() => {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof host.proposeVisit === "function") {
|
|
34
|
+
const originalProposeVisit = host.proposeVisit.bind(host);
|
|
35
|
+
host.proposeVisit = async function (url, action) {
|
|
36
|
+
const result = await originalProposeVisit(url, action);
|
|
37
|
+
try {
|
|
38
|
+
state.setNav({ url, presentation: result && result.presentation ? result.presentation : "default" });
|
|
39
|
+
} catch (_e) { /* nav recording must never break navigation */ }
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const panel = new InspectorPanel(state, { document: doc });
|
|
45
|
+
panel.mount(doc.body);
|
|
46
|
+
|
|
47
|
+
win.addEventListener("keydown", (e) => {
|
|
48
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === "D" || e.key === "d")) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
panel.toggle();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return { state, tap, panel };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default startInspector;
|
|
@@ -26,12 +26,20 @@ module TurboDesktop
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Attribute name fragments we are willing to build a data-* attribute from.
|
|
30
|
+
# Values are escaped by the tag helpers, but names are interpolated, so they
|
|
31
|
+
# are restricted rather than escaped.
|
|
32
|
+
BRIDGE_NAME_PATTERN = /\A[a-zA-Z0-9_-]+\z/
|
|
33
|
+
|
|
29
34
|
# Renders a bridge component data attribute.
|
|
30
35
|
#
|
|
31
36
|
# <%= tag.button "Export", **turbo_desktop_bridge("menu-item", title: "Export PDF", shortcut: "Cmd+E") %>
|
|
32
37
|
def turbo_desktop_bridge(component, **options)
|
|
33
|
-
|
|
38
|
+
validate_bridge_name!(component, "component")
|
|
39
|
+
|
|
40
|
+
attrs = { "data-turbo-desktop-bridge" => component.to_s }
|
|
34
41
|
options.each do |key, value|
|
|
42
|
+
validate_bridge_name!(key, "option name")
|
|
35
43
|
attrs["data-turbo-desktop-bridge-#{key}"] = value.to_s
|
|
36
44
|
end
|
|
37
45
|
attrs
|
|
@@ -46,5 +54,42 @@ module TurboDesktop
|
|
|
46
54
|
def turbo_web_only(&block)
|
|
47
55
|
capture(&block) unless turbo_desktop_app?
|
|
48
56
|
end
|
|
57
|
+
|
|
58
|
+
# Returns true when the Dev Inspector is enabled in configuration.
|
|
59
|
+
def turbo_desktop_inspector?
|
|
60
|
+
TurboDesktop.configuration.inspector_enabled
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Emits the <meta> tag that enables the Dev Inspector in the browser, or nil
|
|
64
|
+
# when the inspector is disabled. Place in your layout <head>; it is a no-op
|
|
65
|
+
# in production unless you explicitly enable the inspector there.
|
|
66
|
+
#
|
|
67
|
+
# The tag also carries the same-origin URL of the inspector entry module
|
|
68
|
+
# (served by this engine) so the desktop shell's turbo-desktop.js can
|
|
69
|
+
# import() it instead of guessing a relative path.
|
|
70
|
+
#
|
|
71
|
+
# <%= turbo_desktop_inspector_meta_tag %>
|
|
72
|
+
def turbo_desktop_inspector_meta_tag
|
|
73
|
+
return nil unless turbo_desktop_inspector?
|
|
74
|
+
|
|
75
|
+
tag.meta(name: "turbo-desktop-inspector", content: "enabled",
|
|
76
|
+
data: { inspector_url: turbo_desktop_inspector_url })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Same-origin URL of the inspector entry module, under the engine's mount
|
|
80
|
+
# path (configurable via config.inspector_mount_path).
|
|
81
|
+
def turbo_desktop_inspector_url
|
|
82
|
+
"#{TurboDesktop.configuration.inspector_mount_path.chomp("/")}/inspector.js"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def validate_bridge_name!(name, label)
|
|
88
|
+
return if name.to_s.match?(BRIDGE_NAME_PATTERN)
|
|
89
|
+
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"turbo_desktop_bridge #{label} #{name.inspect} may only contain " \
|
|
92
|
+
"letters, numbers, hyphens and underscores"
|
|
93
|
+
end
|
|
49
94
|
end
|
|
50
95
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turbo_desktop-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- aguspe
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -48,13 +48,21 @@ files:
|
|
|
48
48
|
- CHANGELOG.md
|
|
49
49
|
- LICENSE
|
|
50
50
|
- README.md
|
|
51
|
+
- app/controllers/turbo_desktop/inspector_assets_controller.rb
|
|
51
52
|
- app/controllers/turbo_desktop/path_configurations_controller.rb
|
|
52
53
|
- config/routes.rb
|
|
54
|
+
- lib/generators/turbo_desktop/install/install_generator.rb
|
|
55
|
+
- lib/generators/turbo_desktop/install/templates/initializer.rb.tt
|
|
53
56
|
- lib/turbo_desktop-rails.rb
|
|
54
57
|
- lib/turbo_desktop.rb
|
|
55
58
|
- lib/turbo_desktop/configuration.rb
|
|
56
59
|
- lib/turbo_desktop/detection.rb
|
|
57
60
|
- lib/turbo_desktop/engine.rb
|
|
61
|
+
- lib/turbo_desktop/inspector_assets/inspector.js
|
|
62
|
+
- lib/turbo_desktop/inspector_assets/inspector/bridge-tap.js
|
|
63
|
+
- lib/turbo_desktop/inspector_assets/inspector/catalog.js
|
|
64
|
+
- lib/turbo_desktop/inspector_assets/inspector/panel.js
|
|
65
|
+
- lib/turbo_desktop/inspector_assets/inspector/state.js
|
|
58
66
|
- lib/turbo_desktop/version.rb
|
|
59
67
|
- lib/turbo_desktop/view_helpers.rb
|
|
60
68
|
homepage: https://github.com/aguspe/turbo_desktop
|
|
@@ -72,14 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
72
80
|
requirements:
|
|
73
81
|
- - ">="
|
|
74
82
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 3.
|
|
83
|
+
version: 3.2.0
|
|
76
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
85
|
requirements:
|
|
78
86
|
- - ">="
|
|
79
87
|
- !ruby/object:Gem::Version
|
|
80
88
|
version: '0'
|
|
81
89
|
requirements: []
|
|
82
|
-
rubygems_version:
|
|
90
|
+
rubygems_version: 4.0.6
|
|
83
91
|
specification_version: 4
|
|
84
92
|
summary: Turbo Native for Desktop — Rails integration
|
|
85
93
|
test_files: []
|