pispas_configurator_html/
main.rs

1#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2//! # `pispas_configurator_html` — one-shot config UI
3//!
4//! Small GUI binary launched from the tray or the installer. It loads an
5//! embedded HTML page (bundled in `binaries/configurator_html/dist/`),
6//! shows it in a native webview window, and wires two JS-to-Rust commands:
7//!
8//! * `loadConfig`  — reads the current
9//!   [`ConfigEnv`](../sharing/utils/struct.ConfigEnv.html) from the `.env`
10//!   under `config_file_path()` and returns it as JSON.
11//! * `saveConfig`  — writes the edited JSON back to the `.env` and then
12//!   triggers a restart of the `pispas-modules` service so the new config
13//!   takes effect.
14//!
15//! When the user saves, the configurator sends a `BASE/RESTART`
16//! [`Action`](../sharing/service/enum.Action.html) to the service via a
17//! plain TCP socket (not WS — this is the installer-internal control
18//! channel, not the public API).
19//!
20//! ## How it fits in
21//!
22//! ```text
23//!   tray-app  --"open configurator"-->  configurator_html.exe
24//!                                             |
25//!                                             | read/write .env
26//!                                             v
27//!                                       sharing::ConfigEnv
28//!                                             |
29//!                                             | RESTART
30//!                                             v
31//!                                      pispas-modules service
32//! ```
33//!
34//! When you add a new configurable field, keep three places in sync
35//! (see `CLAUDE.md §5`): `ConfigEnv`, this file's `save_config`, and
36//! `dist/index.html`.
37
38use std::io::Write;
39use std::net::TcpStream;
40use serde::{Deserialize, Serialize};
41use easy_trace::instruments::tracing;
42use sharing::paths::{config_file_path};
43use sharing::service::Action;
44use sharing::utils::{ConfigEnv};
45use serde_json::{json, Value};
46
47#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
48pub struct PispasConfigurator {
49    pub cfg_json: ConfigEnv,
50    pub is_dark_mode: bool,
51}
52
53impl Default for PispasConfigurator {
54    fn default() -> Self {
55        Self {
56            cfg_json: ConfigEnv::load(),
57            is_dark_mode: false,
58        }
59    }
60}
61
62impl PispasConfigurator {
63    pub fn toggle_mode(&mut self) {
64        self.is_dark_mode = !self.is_dark_mode;
65    }
66
67    pub fn load() -> Self {        
68        Self {
69            cfg_json: ConfigEnv::load(),
70            is_dark_mode: true,
71        }
72    }
73
74    pub fn exists() -> bool {
75        config_file_path().exists()
76    }
77}
78
79const MAIN_LOG_FILE: &str = "webview-configurator.log";
80
81fn send_action(action: Action) {
82    match TcpStream::connect(sharing::CHANNEL_NAME) {
83        Ok(mut stream) => {
84            stream.write_all(action.to_string().as_str().as_bytes()).expect("failed to write to stream");
85            tracing::info!("Send message to restart service");
86        }
87        Err(e) => {
88            tracing::error!("Error connecting to server: {}", e);
89        }
90    }
91}
92
93#[tauri::command]
94fn get_initial_config() -> Result<Value, String> {
95    let pispas_configurator = PispasConfigurator::load();
96    println!("Config JSON: {:?}", pispas_configurator.cfg_json);
97    let bin_dir = sharing::paths::bin_dir().display().to_string();
98
99    let mut config_json_value: Value = serde_json::from_str(&serde_json::to_string(&pispas_configurator.cfg_json).unwrap_or_default())
100        .map_err(|e| format!("Failed to deserialize config JSON: {}", e))?;
101    println!("Config JSON Value: {:?}", config_json_value);
102
103    if let Some(modules) = config_json_value["modules"].as_array() {
104        let module_names: Vec<String> = modules
105            .iter()
106            .filter_map(|module| module.as_str().map(String::from))
107            .collect();
108
109        for module_name in module_names {
110            config_json_value[module_name] = json!(true);
111        }
112    }
113
114    // Agregar bin_dir al objeto de configuración
115    config_json_value["bin_dir"] = json!(bin_dir);
116
117    Ok(config_json_value)
118}
119
120#[tauri::command]
121fn save_config(data: Value) -> Result<String, String> {
122    let mut pispas_configurator = PispasConfigurator::load();
123
124    let service_name = data["service_name"].as_str().unwrap_or_default();
125    let service_vers = data["service_vers"].as_str().unwrap_or_default();
126    let local_host = data["local_host"].as_str().unwrap_or_default();
127    let local_port = data["local_port"].as_str().unwrap_or_default();
128    let local_ussl = data["local_ussl"].as_str().unwrap_or_default();
129    let remote_ussl = data["remote_ussl"].as_str().unwrap_or_default();
130    let remote_host = data["remote_host"].as_str().unwrap_or_default();
131    let remote_port = data["remote_port"].as_str().unwrap_or_default();
132    let pispas_host = data["pispas_host"].as_str().unwrap_or_default();
133    let modules = data["modules"]
134        .as_array()
135        .unwrap_or(&vec![])
136        .iter()
137        .filter_map(|v| v.as_str())
138        .map(String::from)
139        .collect::<Vec<_>>();
140
141    tracing::info!("modules {:?}, data[modules]:{:?}", modules, data["modules"]);
142
143    pispas_configurator.cfg_json.change_service_name(service_name);
144    pispas_configurator.cfg_json.change_service_vers(service_vers);
145    pispas_configurator.cfg_json.change_local_host(local_host);
146    pispas_configurator.cfg_json.change_local_port(local_port.parse().unwrap_or(0));
147    pispas_configurator.cfg_json.change_local_ussl(local_ussl.parse().unwrap_or(false));
148    pispas_configurator.cfg_json.change_remote_ussl(remote_ussl.parse().unwrap_or(false));
149    pispas_configurator.cfg_json.change_remote_host(remote_host);
150    pispas_configurator.cfg_json.change_remote_port(remote_port.parse().unwrap_or(0));
151    pispas_configurator.cfg_json.change_pispas_host(pispas_host);
152    pispas_configurator.cfg_json.change_modules(modules.clone());
153    pispas_configurator.cfg_json.save();
154
155    send_action(Action::Restart);
156    Ok("Configuration saved successfully".to_string())
157}
158
159#[tauri::command]
160fn open_folder_tauri() -> Result<String, String> {
161    let bin_dir = sharing::paths::bin_dir();
162    let path_str = bin_dir.display().to_string();
163
164    println!("Opening folder: {}", path_str);
165
166    // Verificar que el directorio existe
167    if !bin_dir.exists() {
168        println!("Directory doesn't exist, creating: {}", path_str);
169        std::fs::create_dir_all(&bin_dir)
170            .map_err(|e| format!("Failed to create directory: {}", e))?;
171    }
172
173    // Implementación multiplataforma simple
174    let result = if cfg!(target_os = "windows") {
175        std::process::Command::new("explorer")
176            .arg(&path_str)
177            .spawn()
178    } else if cfg!(target_os = "macos") {
179        std::process::Command::new("open")
180            .arg(&path_str)
181            .spawn()
182    } else {
183        // Linux y otros
184        std::process::Command::new("xdg-open")
185            .arg(&path_str)
186            .spawn()
187    };
188
189    match result {
190        Ok(_) => {
191            println!("Folder opened successfully");
192            Ok("Folder opened".to_string())
193        }
194        Err(e) => {
195            let error_msg = format!("Failed to open folder: {}", e);
196            println!("{}", error_msg);
197            Err(error_msg)
198        }
199    }
200}
201
202#[tauri::command]
203fn download_tools() -> Result<String, String> {
204    match sharing::utils::extract_all_resources(&sharing::paths::win_dir(), sharing::utils::RESOURCES_TOOLS) {
205        Ok(_) => {
206            tracing::info!("Resources extracted");
207            if let Err(e) = open_folder_tauri() {
208                tracing::error!("Error opening folder: {}", e);
209                return Err(format!("Error opening folder: {}", e));
210            }
211            Ok("Tools downloaded successfully".to_string())
212        }
213        Err(e) => {
214            tracing::error!("Error extracting resources: {}", e);
215            Err(format!("Error extracting resources: {}", e))
216        }
217    }
218}
219
220fn main() {
221    easy_trace::init(
222        sharing::paths::user_log_dir().to_str().unwrap_or_default(),
223        MAIN_LOG_FILE,
224    );
225
226    tauri::Builder::default()
227        .setup(|_app| {
228            Ok(())
229        })
230        .invoke_handler(tauri::generate_handler![
231            get_initial_config,
232            save_config,
233            open_folder_tauri,
234            download_tools
235        ])
236        .run(tauri::generate_context!())
237        .expect("error while running tauri application");
238}