@ionyx-apps/cli 0.4.8 → 0.4.9
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.
- package/bin/ionyx.exe +0 -0
- package/ionyx-cli/src/templates/angular/src-ionyx/Cargo.toml +30 -0
- package/ionyx-cli/src/templates/angular/src-ionyx/build.rs +5 -0
- package/ionyx-cli/src/templates/angular/src-ionyx/main.rs +48 -0
- package/ionyx-cli/src/templates/angular/src-ionyx/src/lib.rs +267 -0
- package/ionyx-cli/src/templates/basic/ionyx.config.json +3 -0
- package/ionyx-cli/src/templates/basic/package.json +6 -1
- package/ionyx-cli/src/templates/basic/src-ionyx/Cargo.toml +14 -12
- package/ionyx-cli/src/templates/basic/src-ionyx/main.rs +46 -3
- package/ionyx-cli/src/templates/leptos/package.json +6 -1
- package/ionyx-cli/src/templates/leptos/src-ionyx/Cargo.toml +14 -8
- package/ionyx-cli/src/templates/leptos/src-ionyx/main.rs +46 -3
- package/ionyx-cli/src/templates/mod.rs +1 -5
- package/ionyx-cli/src/templates/react/ionyx.config.json +4 -1
- package/ionyx-cli/src/templates/react/package.json +6 -1
- package/ionyx-cli/src/templates/react/src-ionyx/Cargo.toml +14 -12
- package/ionyx-cli/src/templates/react/src-ionyx/main.rs +46 -3
- package/ionyx-cli/src/templates/src-ionyx/Cargo.toml +15 -5
- package/ionyx-cli/src/templates/src-ionyx/bridge.rs +380 -0
- package/ionyx-cli/src/templates/src-ionyx/fusion.rs +47 -0
- package/ionyx-cli/src/templates/src-ionyx/ionyx.js +100 -0
- package/ionyx-cli/src/templates/src-ionyx/ipc_bridge.rs +206 -0
- package/ionyx-cli/src/templates/src-ionyx/lib.rs +204 -114
- package/ionyx-cli/src/templates/src-ionyx/main.rs +268 -3
- package/ionyx-cli/src/templates/src-ionyx/protocol.rs +98 -0
- package/ionyx-cli/src/templates/src-ionyx/window.rs +13 -0
- package/ionyx-cli/src/templates/svelte/package.json +6 -1
- package/ionyx-cli/src/templates/svelte/src-ionyx/Cargo.toml +14 -12
- package/ionyx-cli/src/templates/svelte/src-ionyx/main.rs +46 -3
- package/ionyx-cli/src/templates/vanilla/ionyx.config.json +3 -0
- package/ionyx-cli/src/templates/vanilla/package.json +6 -1
- package/ionyx-cli/src/templates/vanilla/src-ionyx/Cargo.toml +14 -12
- package/ionyx-cli/src/templates/vanilla/src-ionyx/main.rs +46 -3
- package/ionyx-cli/src/templates/vue/ionyx.config.json +4 -1
- package/ionyx-cli/src/templates/vue/package.json +6 -1
- package/ionyx-cli/src/templates/vue/src-ionyx/Cargo.toml +14 -12
- package/ionyx-cli/src/templates/vue/src-ionyx/main.rs +46 -3
- package/package.json +1 -1
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
use ionyx::{config, security, protocol};
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
#[tokio::main]
|
|
4
|
+
async fn main() {
|
|
5
|
+
println!("🚀 Ionyx Application starting...");
|
|
6
|
+
|
|
7
|
+
// Load application configuration
|
|
8
|
+
let app_config = match config::Config::load().await {
|
|
9
|
+
Ok(config) => {
|
|
10
|
+
println!("✅ Configuration loaded successfully");
|
|
11
|
+
config
|
|
12
|
+
}
|
|
13
|
+
Err(e) => {
|
|
14
|
+
eprintln!("⚠️ Failed to load config, using defaults: {}", e);
|
|
15
|
+
config::Config::default()
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Initialize security manager for demonstration
|
|
20
|
+
let security_manager: crate::security::SecurityManager = security::SecurityManager::new(app_config.clone());
|
|
21
|
+
|
|
22
|
+
// Demonstrate comprehensive security manager functionality
|
|
23
|
+
let test_paths = ["./app-data", "/system", "../outside"];
|
|
24
|
+
for &test_path in &test_paths {
|
|
25
|
+
let is_secure = security_manager.is_path_allowed(test_path);
|
|
26
|
+
println!("🔒 Security check: '{}' -> {}",
|
|
27
|
+
test_path,
|
|
28
|
+
if is_secure { "ALLOWED" } else { "DENIED" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Demonstrate security validation
|
|
32
|
+
if let Ok(()) = security_manager.validate_path_operation("./app-data", "read") {
|
|
33
|
+
println!("🔒 Security validation: Path operation approved");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initialize protocol handler for demonstration
|
|
37
|
+
let _protocol_handler = protocol::CustomProtocolHandler::new();
|
|
38
|
+
println!("🌐 Protocol handler initialized");
|
|
39
|
+
|
|
40
|
+
// Create and run the Ionyx application with automatic Vite server startup
|
|
41
|
+
println!("🚀 Launching Ionyx application...");
|
|
42
|
+
let builder = ionyx::Builder::new().config(app_config);
|
|
43
|
+
|
|
44
|
+
if let Err(e) = builder.run() {
|
|
45
|
+
eprintln!("❌ Application failed to start: {}", e);
|
|
46
|
+
std::process::exit(1);
|
|
47
|
+
}
|
|
5
48
|
}
|
|
@@ -4,14 +4,24 @@ name = "my-ionyx-app"
|
|
|
4
4
|
version = "0.4.2"
|
|
5
5
|
edition = "2021"
|
|
6
6
|
|
|
7
|
-
[[bin]]
|
|
8
|
-
name = "my-ionyx-app"
|
|
9
|
-
path = "main.rs"
|
|
10
|
-
|
|
11
7
|
[dependencies]
|
|
12
8
|
include_dir = "0.7"
|
|
13
|
-
|
|
9
|
+
# Web framework dependencies
|
|
10
|
+
wry = "0.54"
|
|
11
|
+
tao = "0.34"
|
|
12
|
+
tokio = { version = "1.50", features = ["full"] }
|
|
13
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
14
|
+
serde_json = "1.0"
|
|
14
15
|
anyhow = "1.0"
|
|
16
|
+
toml = "0.8"
|
|
17
|
+
tracing = "0.1"
|
|
18
|
+
tracing-subscriber = "0.3"
|
|
19
|
+
os_info = "3"
|
|
20
|
+
gethostname = "0.4"
|
|
21
|
+
|
|
22
|
+
[[bin]]
|
|
23
|
+
name = "my-ionyx-app"
|
|
24
|
+
path = "main.rs"
|
|
15
25
|
|
|
16
26
|
[profile.release]
|
|
17
27
|
opt-level = 3
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
use tracing::{debug, error, info};
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
use crate::config::Config;
|
|
8
|
+
use crate::ipc_bridge::{AppInfo, IpcBridgeManager};
|
|
9
|
+
use crate::security::SecurityManager;
|
|
10
|
+
use rfd::AsyncFileDialog;
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
13
|
+
pub struct IpcRequest {
|
|
14
|
+
pub id: String,
|
|
15
|
+
pub command: String,
|
|
16
|
+
pub payload: Value,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
use crate::fusion::FusionManager;
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
22
|
+
pub struct IpcResponse {
|
|
23
|
+
pub id: String,
|
|
24
|
+
pub success: bool,
|
|
25
|
+
pub data: Option<Value>,
|
|
26
|
+
pub error: Option<String>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub struct IpcBridge {
|
|
30
|
+
config: Config,
|
|
31
|
+
security: SecurityManager,
|
|
32
|
+
dispatcher: crate::IpcDispatcher,
|
|
33
|
+
bridge_manager: IpcBridgeManager,
|
|
34
|
+
fusion_manager: Arc<FusionManager>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl IpcBridge {
|
|
38
|
+
pub fn new(config: Config, dispatcher: crate::IpcDispatcher, fusion_manager: Arc<FusionManager>) -> Self {
|
|
39
|
+
let security = SecurityManager::new(config.clone());
|
|
40
|
+
let bridge_manager = IpcBridgeManager::new(config.clone(), config.app.name.clone());
|
|
41
|
+
Self {
|
|
42
|
+
config,
|
|
43
|
+
security,
|
|
44
|
+
dispatcher,
|
|
45
|
+
bridge_manager,
|
|
46
|
+
fusion_manager,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub async fn handle_request_with_response(&self, request: String) -> Result<()> {
|
|
51
|
+
debug!("Received IPC request: {}", request);
|
|
52
|
+
|
|
53
|
+
let ipc_request: IpcRequest = serde_json::from_str(&request)
|
|
54
|
+
.map_err(|e| anyhow::anyhow!("Failed to parse IPC request: {}", e))?;
|
|
55
|
+
|
|
56
|
+
info!("Processing IPC command: {}", ipc_request.command);
|
|
57
|
+
|
|
58
|
+
// Process the command and get the result
|
|
59
|
+
let response = match self.process_command(&ipc_request).await {
|
|
60
|
+
Ok(data) => {
|
|
61
|
+
info!(
|
|
62
|
+
"IPC command '{}' completed successfully",
|
|
63
|
+
ipc_request.command
|
|
64
|
+
);
|
|
65
|
+
IpcResponse {
|
|
66
|
+
id: ipc_request.id,
|
|
67
|
+
success: true,
|
|
68
|
+
data: Some(data),
|
|
69
|
+
error: None,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
Err(e) => {
|
|
73
|
+
error!("IPC command '{}' failed: {}", ipc_request.command, e);
|
|
74
|
+
IpcResponse {
|
|
75
|
+
id: ipc_request.id,
|
|
76
|
+
success: false,
|
|
77
|
+
data: None,
|
|
78
|
+
error: Some(e.to_string()),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
debug!("IPC request processed with response: {:?}", response);
|
|
84
|
+
|
|
85
|
+
// Send response back to frontend using dispatcher
|
|
86
|
+
self.dispatcher.resolve_promise(
|
|
87
|
+
&response.id,
|
|
88
|
+
response.success,
|
|
89
|
+
response.data,
|
|
90
|
+
response.error,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
Ok(())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async fn process_command(&self, request: &IpcRequest) -> Result<Value> {
|
|
97
|
+
match request.command.as_str() {
|
|
98
|
+
"fs.readFile" => self.handle_read_file(&request.payload).await,
|
|
99
|
+
"fs.writeFile" => self.handle_write_file(&request.payload).await,
|
|
100
|
+
"fs.exists" => self.handle_file_exists(&request.payload).await,
|
|
101
|
+
"fs.readdir" => self.handle_read_dir(&request.payload).await,
|
|
102
|
+
"os.info" => self.handle_os_info().await,
|
|
103
|
+
"dialog.openFile" => self.handle_open_file_dialog(&request.payload).await,
|
|
104
|
+
"dialog.saveFile" => self.handle_save_file_dialog(&request.payload).await,
|
|
105
|
+
"network.request" => self.handle_network_request(&request.payload).await,
|
|
106
|
+
"app.getVersion" => self.handle_get_version().await,
|
|
107
|
+
"app.getConfig" => self.handle_get_config().await,
|
|
108
|
+
// IPC Bridge commands
|
|
109
|
+
"bridge.registerApp" => self.handle_bridge_register_app(&request.payload).await,
|
|
110
|
+
"bridge.unregisterApp" => self.handle_bridge_unregister_app(&request.payload).await,
|
|
111
|
+
"bridge.getApps" => self.handle_bridge_get_apps(&request.payload).await,
|
|
112
|
+
"bridge.sendToApp" => self.handle_bridge_send_to_app(&request.payload).await,
|
|
113
|
+
"bridge.broadcast" => self.handle_bridge_broadcast(&request.payload).await,
|
|
114
|
+
"fusion.update" => self.handle_fusion_update(&request.payload).await,
|
|
115
|
+
_ => Err(anyhow::anyhow!("Unknown command: {}", request.command)),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async fn handle_fusion_update(&self, payload: &Value) -> Result<Value> {
|
|
120
|
+
let key = payload["key"].as_str().ok_or_else(|| anyhow::anyhow!("Missing key"))?;
|
|
121
|
+
let value = payload["value"].clone();
|
|
122
|
+
|
|
123
|
+
info!("Fusion update from JS: {} = {:?}", key, value);
|
|
124
|
+
self.fusion_manager.set(key, value);
|
|
125
|
+
|
|
126
|
+
Ok(json!({ "success": true }))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async fn handle_read_file(&self, payload: &Value) -> Result<Value> {
|
|
130
|
+
let path: String = serde_json::from_value(payload["path"].clone())?;
|
|
131
|
+
|
|
132
|
+
self.security.validate_path_operation(&path, "read")?;
|
|
133
|
+
let content = tokio::fs::read_to_string(&path).await?;
|
|
134
|
+
Ok(json!({ "content": content }))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async fn handle_write_file(&self, payload: &Value) -> Result<Value> {
|
|
138
|
+
let path: String = serde_json::from_value(payload["path"].clone())?;
|
|
139
|
+
let content: String = serde_json::from_value(payload["content"].clone())?;
|
|
140
|
+
|
|
141
|
+
self.security.validate_path_operation(&path, "write")?;
|
|
142
|
+
tokio::fs::write(&path, content).await?;
|
|
143
|
+
Ok(json!({ "success": true }))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async fn handle_file_exists(&self, payload: &Value) -> Result<Value> {
|
|
147
|
+
let path: String = serde_json::from_value(payload["path"].clone())?;
|
|
148
|
+
|
|
149
|
+
self.security.validate_path_operation(&path, "exists")?;
|
|
150
|
+
let exists = tokio::fs::metadata(&path).await.is_ok();
|
|
151
|
+
Ok(json!({ "exists": exists }))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async fn handle_read_dir(&self, payload: &Value) -> Result<Value> {
|
|
155
|
+
let path: String = serde_json::from_value(payload["path"].clone())?;
|
|
156
|
+
|
|
157
|
+
self.security.validate_path_operation(&path, "read")?;
|
|
158
|
+
let mut entries = Vec::new();
|
|
159
|
+
let mut dir = tokio::fs::read_dir(&path).await?;
|
|
160
|
+
|
|
161
|
+
while let Some(entry) = dir.next_entry().await? {
|
|
162
|
+
let metadata = entry.metadata().await?;
|
|
163
|
+
entries.push(json!({
|
|
164
|
+
"name": entry.file_name().to_string_lossy(),
|
|
165
|
+
"path": entry.path().to_string_lossy(),
|
|
166
|
+
"isFile": metadata.is_file(),
|
|
167
|
+
"isDir": metadata.is_dir(),
|
|
168
|
+
"size": metadata.len(),
|
|
169
|
+
"modified": metadata.modified()
|
|
170
|
+
.ok()
|
|
171
|
+
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
172
|
+
.map(|d| d.as_secs())
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
Ok(json!({ "entries": entries }))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async fn handle_os_info(&self) -> Result<Value> {
|
|
180
|
+
if !self.config.permissions.os_info {
|
|
181
|
+
return Err(anyhow::anyhow!("OS info access is disabled"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Ok(json!({
|
|
185
|
+
"platform": std::env::consts::OS,
|
|
186
|
+
"arch": std::env::consts::ARCH,
|
|
187
|
+
"version": os_info::get().version().to_string(),
|
|
188
|
+
"hostname": gethostname::gethostname().to_string_lossy().to_string()
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async fn handle_open_file_dialog(&self, _payload: &Value) -> Result<Value> {
|
|
193
|
+
if !self.config.permissions.dialogs {
|
|
194
|
+
return Err(anyhow::anyhow!("Dialog access is disabled"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let file = AsyncFileDialog::new()
|
|
198
|
+
.set_title("Open File")
|
|
199
|
+
.pick_file()
|
|
200
|
+
.await;
|
|
201
|
+
|
|
202
|
+
match file {
|
|
203
|
+
Some(handle) => Ok(json!({ "filePath": handle.path().to_string_lossy() })),
|
|
204
|
+
None => Ok(json!({ "filePath": null })),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async fn handle_save_file_dialog(&self, _payload: &Value) -> Result<Value> {
|
|
209
|
+
if !self.config.permissions.dialogs {
|
|
210
|
+
return Err(anyhow::anyhow!("Dialog access is disabled"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let file = AsyncFileDialog::new()
|
|
214
|
+
.set_title("Save File")
|
|
215
|
+
.save_file()
|
|
216
|
+
.await;
|
|
217
|
+
|
|
218
|
+
match file {
|
|
219
|
+
Some(handle) => Ok(json!({ "filePath": handle.path().to_string_lossy() })),
|
|
220
|
+
None => Ok(json!({ "filePath": null })),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async fn handle_network_request(&self, payload: &Value) -> Result<Value> {
|
|
225
|
+
if !self.config.permissions.network {
|
|
226
|
+
return Err(anyhow::anyhow!("Network access is disabled"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let url: String = serde_json::from_value(payload["url"].clone())?;
|
|
230
|
+
let method: String = payload["method"].as_str().unwrap_or("GET").to_string();
|
|
231
|
+
|
|
232
|
+
// Basic HTTP request implementation
|
|
233
|
+
let client = reqwest::Client::new();
|
|
234
|
+
let request = match method.as_str() {
|
|
235
|
+
"GET" => client.get(&url),
|
|
236
|
+
"POST" => {
|
|
237
|
+
let body = payload.get("body").cloned().unwrap_or(json!(null));
|
|
238
|
+
client.post(&url).json(&body)
|
|
239
|
+
}
|
|
240
|
+
"PUT" => {
|
|
241
|
+
let body = payload.get("body").cloned().unwrap_or(json!(null));
|
|
242
|
+
client.put(&url).json(&body)
|
|
243
|
+
}
|
|
244
|
+
"DELETE" => client.delete(&url),
|
|
245
|
+
_ => return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
let response = request.send().await?;
|
|
249
|
+
let status = response.status().as_u16();
|
|
250
|
+
let headers = response
|
|
251
|
+
.headers()
|
|
252
|
+
.iter()
|
|
253
|
+
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
|
254
|
+
.collect::<std::collections::HashMap<_, _>>();
|
|
255
|
+
let body = response.text().await?;
|
|
256
|
+
|
|
257
|
+
Ok(json!({
|
|
258
|
+
"status": status,
|
|
259
|
+
"headers": headers,
|
|
260
|
+
"body": body
|
|
261
|
+
}))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async fn handle_get_version(&self) -> Result<Value> {
|
|
265
|
+
Ok(json!({
|
|
266
|
+
"name": self.config.app.name.clone(),
|
|
267
|
+
"version": self.config.app.version.clone()
|
|
268
|
+
}))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async fn handle_get_config(&self) -> Result<Value> {
|
|
272
|
+
Ok(json!({
|
|
273
|
+
"window": self.config.window,
|
|
274
|
+
"permissions": self.config.permissions
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// IPC Bridge methods
|
|
279
|
+
async fn handle_bridge_register_app(&self, payload: &Value) -> Result<Value> {
|
|
280
|
+
let name: String = serde_json::from_value(payload["name"].clone())?;
|
|
281
|
+
let version: String = serde_json::from_value(payload["version"].clone())?;
|
|
282
|
+
let capabilities: Vec<String> = serde_json::from_value(payload["capabilities"].clone())?;
|
|
283
|
+
|
|
284
|
+
info!("Registering app via bridge: {} v{}", name, version);
|
|
285
|
+
|
|
286
|
+
let app_info = AppInfo {
|
|
287
|
+
name: name.clone(),
|
|
288
|
+
version: version.clone(),
|
|
289
|
+
pid: std::process::id(),
|
|
290
|
+
capabilities,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
self.bridge_manager.register_app(app_info.clone()).await?;
|
|
294
|
+
|
|
295
|
+
Ok(json!({
|
|
296
|
+
"registered": true,
|
|
297
|
+
"app": {
|
|
298
|
+
"name": app_info.name,
|
|
299
|
+
"version": app_info.version,
|
|
300
|
+
"pid": app_info.pid,
|
|
301
|
+
"capabilities": app_info.capabilities,
|
|
302
|
+
"registered_at": chrono::Utc::now().to_rfc3339()
|
|
303
|
+
},
|
|
304
|
+
"message": format!("App '{}' registered successfully", name)
|
|
305
|
+
}))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async fn handle_bridge_unregister_app(&self, payload: &Value) -> Result<Value> {
|
|
309
|
+
let name: String = serde_json::from_value(payload["name"].clone())?;
|
|
310
|
+
|
|
311
|
+
info!("Unregistering app via bridge: {}", name);
|
|
312
|
+
|
|
313
|
+
self.bridge_manager.unregister_app(&name).await?;
|
|
314
|
+
|
|
315
|
+
Ok(json!({
|
|
316
|
+
"unregistered": true,
|
|
317
|
+
"app": name,
|
|
318
|
+
"message": format!("App '{}' unregistered successfully", name)
|
|
319
|
+
}))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async fn handle_bridge_get_apps(&self, _payload: &Value) -> Result<Value> {
|
|
323
|
+
info!("Getting registered apps via bridge");
|
|
324
|
+
|
|
325
|
+
let apps: Vec<AppInfo> = self.bridge_manager.get_registered_apps().await?;
|
|
326
|
+
|
|
327
|
+
Ok(json!({
|
|
328
|
+
"apps": apps,
|
|
329
|
+
"total_count": apps.len(),
|
|
330
|
+
"message": "Retrieved registered applications"
|
|
331
|
+
}))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async fn handle_bridge_send_to_app(&self, payload: &Value) -> Result<Value> {
|
|
335
|
+
let target_app: String = serde_json::from_value(payload["target_app"].clone())?;
|
|
336
|
+
let command: String = serde_json::from_value(payload["command"].clone())?;
|
|
337
|
+
let command_payload: Value = payload["payload"].clone();
|
|
338
|
+
|
|
339
|
+
info!("Sending to app via bridge {}: {}", target_app, command);
|
|
340
|
+
|
|
341
|
+
let request = crate::ipc_bridge::IpcBridgeRequest {
|
|
342
|
+
source_app: self.config.app.name.clone(),
|
|
343
|
+
target_app: target_app.clone(),
|
|
344
|
+
command,
|
|
345
|
+
payload: command_payload,
|
|
346
|
+
id: format!("bridge_msg_{}", chrono::Utc::now().timestamp_millis()),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
let response = self.bridge_manager.send_to_app(request).await?;
|
|
350
|
+
|
|
351
|
+
Ok(json!({
|
|
352
|
+
"sent": true,
|
|
353
|
+
"delivery": response,
|
|
354
|
+
"message": format!("Message sent to '{}' successfully", target_app)
|
|
355
|
+
}))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async fn handle_bridge_broadcast(&self, payload: &Value) -> Result<Value> {
|
|
359
|
+
let message: Value = payload["message"].clone();
|
|
360
|
+
|
|
361
|
+
info!("Broadcasting message via bridge: {:?}", message);
|
|
362
|
+
|
|
363
|
+
let request = crate::ipc_bridge::IpcBridgeRequest {
|
|
364
|
+
source_app: self.config.app.name.clone(),
|
|
365
|
+
target_app: "*".to_string(), // Convention for broadcast
|
|
366
|
+
command: "system.broadcast".to_string(),
|
|
367
|
+
payload: message,
|
|
368
|
+
id: format!("broadcast_{}", chrono::Utc::now().timestamp_millis()),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Use process_bridge_request directly for broadcast logic
|
|
372
|
+
let response = self.bridge_manager.send_to_app_direct(request).await?;
|
|
373
|
+
|
|
374
|
+
Ok(json!({
|
|
375
|
+
"broadcast_sent": true,
|
|
376
|
+
"broadcast": response,
|
|
377
|
+
"message": "Broadcast sent to all recipients successfully"
|
|
378
|
+
}))
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::sync::{Arc, Mutex};
|
|
5
|
+
|
|
6
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
7
|
+
pub struct FusionState {
|
|
8
|
+
pub data: HashMap<String, Value>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub struct FusionManager {
|
|
12
|
+
state: Arc<Mutex<FusionState>>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl FusionManager {
|
|
16
|
+
pub fn new() -> Self {
|
|
17
|
+
Self {
|
|
18
|
+
state: Arc::new(Mutex::new(FusionState {
|
|
19
|
+
data: HashMap::new(),
|
|
20
|
+
})),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn set(&self, key: &str, value: Value) {
|
|
25
|
+
let mut state = self.state.lock().unwrap();
|
|
26
|
+
state.data.insert(key.to_string(), value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[allow(dead_code)]
|
|
30
|
+
pub fn get(&self, key: &str) -> Option<Value> {
|
|
31
|
+
let state = self.state.lock().unwrap();
|
|
32
|
+
state.data.get(key).cloned()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn get_all(&self) -> Value {
|
|
36
|
+
let state = self.state.lock().unwrap();
|
|
37
|
+
serde_json::to_value(&state.data).unwrap_or(Value::Object(serde_json::Map::new()))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn generate_sync_script(&self) -> String {
|
|
41
|
+
let data = self.get_all();
|
|
42
|
+
format!(
|
|
43
|
+
"if (window.ionyx) {{ window.ionyx._fusion_sync({}); }}",
|
|
44
|
+
data.to_string()
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Ionyx IPC Bridge JavaScript
|
|
2
|
+
(function() {
|
|
3
|
+
const _promises = {};
|
|
4
|
+
let _fusion_data = {};
|
|
5
|
+
|
|
6
|
+
window.ionyx = {
|
|
7
|
+
_promises,
|
|
8
|
+
_fusion_sync: (data) => {
|
|
9
|
+
console.log("🧬 Fusion Sync:", data);
|
|
10
|
+
_fusion_data = data;
|
|
11
|
+
},
|
|
12
|
+
resolveResponse: (responseId, response) => {
|
|
13
|
+
const promiseHandlers = _promises[responseId];
|
|
14
|
+
if (promiseHandlers) {
|
|
15
|
+
console.log("📥 Received IPC response:", response);
|
|
16
|
+
clearTimeout(promiseHandlers.timeoutId);
|
|
17
|
+
if (response.success) {
|
|
18
|
+
promiseHandlers.resolve(response.data);
|
|
19
|
+
} else {
|
|
20
|
+
promiseHandlers.reject(new Error(response.error));
|
|
21
|
+
}
|
|
22
|
+
delete _promises[responseId];
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
invoke: (command, payload = {}) => {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}_${command}`;
|
|
28
|
+
const request = {
|
|
29
|
+
id,
|
|
30
|
+
command,
|
|
31
|
+
payload: payload || {}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
console.log("🚀 Sending IPC request:", request);
|
|
35
|
+
|
|
36
|
+
const timeoutId = setTimeout(() => {
|
|
37
|
+
console.error("❌ IPC request timeout for:", command);
|
|
38
|
+
reject(new Error("IPC request timeout"));
|
|
39
|
+
delete _promises[id];
|
|
40
|
+
}, 15000);
|
|
41
|
+
|
|
42
|
+
_promises[id] = { resolve, reject, timeoutId };
|
|
43
|
+
|
|
44
|
+
if (window.ipc && typeof window.ipc.postMessage === 'function') {
|
|
45
|
+
console.log("📤 Sending via window.ipc.postMessage");
|
|
46
|
+
window.ipc.postMessage(JSON.stringify(request));
|
|
47
|
+
} else {
|
|
48
|
+
console.error("❌ window.ipc not available");
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
reject(new Error("IPC not available natively through window.ipc"));
|
|
51
|
+
delete _promises[id];
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Add Tauri-like namespaces
|
|
58
|
+
const invoke = window.ionyx.invoke;
|
|
59
|
+
|
|
60
|
+
window.ionyx.fs = {
|
|
61
|
+
readFile: (path) => invoke("fs.readFile", { path }),
|
|
62
|
+
writeFile: (path, content) => invoke("fs.writeFile", { path, content }),
|
|
63
|
+
exists: (path) => invoke("fs.exists", { path }),
|
|
64
|
+
readDir: (path) => invoke("fs.readdir", { path })
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
window.ionyx.os = {
|
|
68
|
+
info: () => invoke("os.info")
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
window.ionyx.dialog = {
|
|
72
|
+
openFile: () => invoke("dialog.openFile", {}),
|
|
73
|
+
saveFile: () => invoke("dialog.saveFile", {})
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
window.ionyx.app = {
|
|
77
|
+
getVersion: () => invoke("app.getVersion"),
|
|
78
|
+
getConfig: () => invoke("app.getConfig")
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
window.ionyx.network = {
|
|
82
|
+
request: (url, method = "GET", body = null) => invoke("network.request", { url, method, body })
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Inspired by NW.js Unified Context:
|
|
86
|
+
// Native Fusion allows direct access to shared state.
|
|
87
|
+
window.fusion = new Proxy({}, {
|
|
88
|
+
get: (target, prop) => {
|
|
89
|
+
return _fusion_data[prop];
|
|
90
|
+
},
|
|
91
|
+
set: (target, prop, value) => {
|
|
92
|
+
_fusion_data[prop] = value;
|
|
93
|
+
invoke("fusion.update", { key: prop, value });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
})();
|
|
99
|
+
|
|
100
|
+
console.log("🔧 Ionyx IPC Bridge injected successfully.");
|