diff --git a/.gitignore b/.gitignore index 8b5e6ff..7934e49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ __pycache__ .zipignore .gitignore .DS_Store +node_modules +dist +src-tauri/target +src-tauri/gen diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..aebbdf0 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,9 @@ +{ + "identifier": "default", + "description": "Default capabilities", + "permissions": [ + "core:default", + "dialog:default", + "run-python-action" + ] +} diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..1bf7508 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/permissions/run-python-action.toml b/src-tauri/permissions/run-python-action.toml new file mode 100644 index 0000000..b358fb3 --- /dev/null +++ b/src-tauri/permissions/run-python-action.toml @@ -0,0 +1,4 @@ +[[permission]] +identifier = "run-python-action" +description = "Allow the UI to invoke the Python backend" +commands.allow = ["run_python_action"] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..765b7fc --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,86 @@ +use serde_json::Value; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +fn repo_root() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf() +} + +fn run_python(script: &str, input: &str) -> Result, String> { + let cwd = repo_root(); + let mut last_err: Option = None; + for candidate in ["python3", "python"] { + let mut child = match Command::new(candidate) + .arg(script) + .current_dir(&cwd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(err) => { + last_err = Some(format!("{candidate}: {err}")); + continue; + } + }; + + if let Some(mut stdin) = child.stdin.take() { + if let Err(err) = stdin.write_all(input.as_bytes()) { + return Err(format!("Failed to write to Python stdin: {err}")); + } + } + + let output = match child.wait_with_output() { + Ok(output) => output, + Err(err) => { + last_err = Some(format!("{candidate}: {err}")); + continue; + } + }; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Python backend failed: {stderr}")); + } + + return Ok(output.stdout); + } + + Err(last_err.unwrap_or_else(|| "Python not found".to_string())) +} + +#[tauri::command] +fn run_python_action(action: String, payload: Value) -> Result { + let script = repo_root().join("concept_api.py"); + if !script.exists() { + return Err("concept_api.py not found".to_string()); + } + + let req = serde_json::json!({ + "action": action, + "payload": payload, + }); + let input = serde_json::to_string(&req).map_err(|e| e.to_string())?; + let stdout = run_python(script.to_string_lossy().as_ref(), &input)?; + let resp: Value = serde_json::from_slice(&stdout).map_err(|e| e.to_string())?; + if resp.get("ok") == Some(&Value::Bool(true)) { + Ok(resp.get("data").cloned().unwrap_or(Value::Null)) + } else { + let error = resp + .get("error") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown backend error"); + Err(error.to_string()) + } +} + +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![run_python_action]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..e78c3db --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + concept_maker::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..d88e1e2 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/schema.json", + "package": { + "productName": "Concept Maker", + "version": "0.1.0" + }, + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:5173", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Idea -> Concept", + "width": 1200, + "height": 820, + "minWidth": 900, + "minHeight": 640 + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "icon": ["icons/icon.png"] + } +} diff --git a/src/App.tsx b/src/App.tsx index 5c992dd..5d41e4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -136,8 +136,8 @@ export default function App() { let unlisten: (() => void) | undefined; const attach = async () => { try { - const current = getCurrent(); - unlisten = await current.onFileDropEvent((event) => { + const current = getCurrent() as any; + unlisten = await current.onFileDropEvent((event: any) => { if (event.payload.type === "drop") { const paths = event.payload.paths ?? []; if (paths.length) {