1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | #![doc = include_str!("README.md" )] |
5 | |
6 | use i_slint_core::model::{Model, ModelRc}; |
7 | use i_slint_core::SharedVector; |
8 | use slint_interpreter::{ |
9 | ComponentDefinition, ComponentHandle, ComponentInstance, SharedString, Value, |
10 | }; |
11 | use std::collections::HashMap; |
12 | use std::io::{BufReader, BufWriter}; |
13 | use std::sync::atomic::{AtomicU32, Ordering}; |
14 | use std::sync::{Arc, Mutex}; |
15 | |
16 | use clap::Parser; |
17 | use itertools::Itertools; |
18 | |
19 | type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; |
20 | |
21 | #[derive (Clone, clap::Parser)] |
22 | #[command(author, version, about, long_about = None)] |
23 | struct Cli { |
24 | /// Include path for other .slint files or images |
25 | #[arg(short = 'I' , value_name = "include path" , number_of_values = 1, action)] |
26 | include_paths: Vec<std::path::PathBuf>, |
27 | |
28 | /// Specify Library location of the '@library' in the form 'library=/path/to/library' |
29 | #[arg(short = 'L' , value_name = "library=path" , number_of_values = 1, action)] |
30 | library_paths: Vec<String>, |
31 | |
32 | /// The .slint file to load ('-' for stdin) |
33 | #[arg(name = "path" , action)] |
34 | path: std::path::PathBuf, |
35 | |
36 | /// The style name ('native' or 'fluent') |
37 | #[arg(long, value_name = "style name" , action)] |
38 | style: Option<String>, |
39 | |
40 | /// The rendering backend |
41 | #[arg(long, value_name = "backend" , action)] |
42 | backend: Option<String>, |
43 | |
44 | /// Automatically watch the file system, and reload when it changes |
45 | #[arg(long, action)] |
46 | auto_reload: bool, |
47 | |
48 | /// Load properties from a json file ('-' for stdin) |
49 | #[arg(long, value_name = "json file" , action)] |
50 | load_data: Option<std::path::PathBuf>, |
51 | |
52 | /// Store properties values in a json file at exit ('-' for stdout) |
53 | #[arg(long, value_name = "json file" , action)] |
54 | save_data: Option<std::path::PathBuf>, |
55 | |
56 | /// Specify callbacks handler. |
57 | /// The first argument is the callback name, and the second argument is a string that is going |
58 | /// to be passed to the shell to be executed. Occurrences of `$1` will be replaced by the first argument, |
59 | /// and so on. |
60 | #[arg(long, value_names(&["callback" , "handler" ]), number_of_values = 2, action)] |
61 | on: Vec<String>, |
62 | |
63 | #[cfg (feature = "gettext" )] |
64 | /// Translation domain |
65 | #[arg(long = "translation-domain" , action)] |
66 | translation_domain: Option<String>, |
67 | |
68 | #[cfg (feature = "gettext" )] |
69 | /// Translation directory where the translation files are searched for |
70 | #[arg(long = "translation-dir" , action)] |
71 | translation_dir: Option<std::path::PathBuf>, |
72 | } |
73 | |
74 | thread_local! {static CURRENT_INSTANCE: std::cell::RefCell<Option<ComponentInstance>> = Default::default();} |
75 | static EXIT_CODE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); |
76 | |
77 | fn main() -> Result<()> { |
78 | env_logger::init(); |
79 | let args = Cli::parse(); |
80 | |
81 | if args.auto_reload && args.save_data.is_some() { |
82 | eprintln!("Cannot pass both --auto-reload and --save-data" ); |
83 | std::process::exit(-1); |
84 | } |
85 | |
86 | if let Some(backend) = &args.backend { |
87 | std::env::set_var("SLINT_BACKEND" , backend); |
88 | } |
89 | |
90 | #[cfg (feature = "gettext" )] |
91 | if let Some(dirname) = args.translation_dir.clone() { |
92 | i_slint_core::translations::gettext_bindtextdomain( |
93 | args.translation_domain.as_ref().map(String::as_str).unwrap_or_default(), |
94 | dirname, |
95 | )?; |
96 | }; |
97 | |
98 | let fswatcher = if args.auto_reload { Some(start_fswatch_thread(args.clone())?) } else { None }; |
99 | let mut compiler = init_compiler(&args, fswatcher); |
100 | |
101 | let c = spin_on::spin_on(compiler.build_from_path(args.path)); |
102 | slint_interpreter::print_diagnostics(compiler.diagnostics()); |
103 | |
104 | let c = match c { |
105 | Some(c) => c, |
106 | None => std::process::exit(-1), |
107 | }; |
108 | |
109 | let component = c.create().unwrap(); |
110 | init_dialog(&component); |
111 | |
112 | if let Some(data_path) = args.load_data { |
113 | load_data(&c, &component, &data_path)?; |
114 | } |
115 | install_callbacks(&component, &args.on); |
116 | |
117 | if args.auto_reload { |
118 | CURRENT_INSTANCE.with(|current| current.replace(Some(component.clone_strong()))); |
119 | } |
120 | |
121 | component.run().unwrap(); |
122 | |
123 | if let Some(data_path) = args.save_data { |
124 | let mut obj = serde_json::Map::new(); |
125 | for (name, _) in c.properties() { |
126 | fn to_json(val: slint_interpreter::Value) -> Option<serde_json::Value> { |
127 | match val { |
128 | slint_interpreter::Value::Number(x) => Some(x.into()), |
129 | slint_interpreter::Value::String(x) => Some(x.as_str().into()), |
130 | slint_interpreter::Value::Bool(x) => Some(x.into()), |
131 | slint_interpreter::Value::Model(model) => { |
132 | let mut res = Vec::with_capacity(model.row_count()); |
133 | for i in 0..model.row_count() { |
134 | res.push(to_json(model.row_data(i).unwrap())?); |
135 | } |
136 | Some(serde_json::Value::Array(res)) |
137 | } |
138 | slint_interpreter::Value::Struct(st) => { |
139 | let mut obj = serde_json::Map::new(); |
140 | for (k, v) in st.iter() { |
141 | obj.insert(k.into(), to_json(v.clone())?); |
142 | } |
143 | Some(obj.into()) |
144 | } |
145 | slint_interpreter::Value::EnumerationValue(_class, value) => { |
146 | Some(value.as_str().into()) |
147 | } |
148 | _ => None, |
149 | } |
150 | } |
151 | if let Some(v) = to_json(component.get_property(&name).unwrap()) { |
152 | obj.insert(name, v); |
153 | } |
154 | } |
155 | if data_path == std::path::Path::new("-" ) { |
156 | serde_json::to_writer_pretty(std::io::stdout(), &obj)?; |
157 | } else { |
158 | serde_json::to_writer_pretty(BufWriter::new(std::fs::File::create(data_path)?), &obj)?; |
159 | } |
160 | } |
161 | |
162 | std::process::exit(EXIT_CODE.load(std::sync::atomic::Ordering::Relaxed)) |
163 | } |
164 | |
165 | fn init_compiler( |
166 | args: &Cli, |
167 | fswatcher: Option<Arc<Mutex<notify::RecommendedWatcher>>>, |
168 | ) -> slint_interpreter::ComponentCompiler { |
169 | let mut compiler = slint_interpreter::ComponentCompiler::default(); |
170 | #[cfg (feature = "gettext" )] |
171 | if let Some(domain) = args.translation_domain.clone() { |
172 | compiler.set_translation_domain(domain); |
173 | } |
174 | compiler.set_include_paths(args.include_paths.clone()); |
175 | compiler.set_library_paths( |
176 | args.library_paths |
177 | .iter() |
178 | .filter_map(|entry| entry.split('=' ).collect_tuple().map(|(k, v)| (k.into(), v.into()))) |
179 | .collect(), |
180 | ); |
181 | if let Some(style) = &args.style { |
182 | compiler.set_style(style.clone()); |
183 | } |
184 | if let Some(watcher) = fswatcher { |
185 | notify::Watcher::watch( |
186 | &mut *watcher.lock().unwrap(), |
187 | &args.path, |
188 | notify::RecursiveMode::NonRecursive, |
189 | ) |
190 | .unwrap_or_else(|err| { |
191 | eprintln!("Warning: error while watching {}: {:?}" , args.path.display(), err) |
192 | }); |
193 | if let Some(data_path) = &args.load_data { |
194 | notify::Watcher::watch( |
195 | &mut *watcher.lock().unwrap(), |
196 | data_path, |
197 | notify::RecursiveMode::NonRecursive, |
198 | ) |
199 | .unwrap_or_else(|err| { |
200 | eprintln!("Warning: error while watching {}: {:?}" , data_path.display(), err) |
201 | }); |
202 | } |
203 | compiler.set_file_loader(move |path| { |
204 | notify::Watcher::watch( |
205 | &mut *watcher.lock().unwrap(), |
206 | path, |
207 | notify::RecursiveMode::NonRecursive, |
208 | ) |
209 | .unwrap_or_else(|err| { |
210 | eprintln!("Warning: error while watching {}: {:?}" , path.display(), err) |
211 | }); |
212 | Box::pin(async { None }) |
213 | }) |
214 | } |
215 | compiler |
216 | } |
217 | |
218 | fn init_dialog(instance: &ComponentInstance) { |
219 | for cb: String in instance.definition().callbacks() { |
220 | let exit_code: i32 = match cb.as_str() { |
221 | "ok-clicked" | "yes-clicked" | "close-clicked" => 0, |
222 | "cancel-clicked" | "no-clicked" => 1, |
223 | _ => continue, |
224 | }; |
225 | // this is a dialog, so clicking the "x" should cancel |
226 | EXIT_CODE.store(val:1, order:std::sync::atomic::Ordering::Relaxed); |
227 | instanceResult<(), SetCallbackError> |
228 | .set_callback(&cb, callback:move |_| { |
229 | EXIT_CODE.store(val:exit_code, order:std::sync::atomic::Ordering::Relaxed); |
230 | i_slint_core::api::quit_event_loop().unwrap(); |
231 | Default::default() |
232 | }) |
233 | .unwrap(); |
234 | } |
235 | } |
236 | |
237 | static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0); |
238 | |
239 | fn start_fswatch_thread(args: Cli) -> Result<Arc<Mutex<notify::RecommendedWatcher>>> { |
240 | let (tx: Sender>, rx: Receiver>) = std::sync::mpsc::channel(); |
241 | let w: Arc> = Arc::new(data:Mutex::new(notify::recommended_watcher(event_handler:tx)?)); |
242 | let w2: Arc> = w.clone(); |
243 | std::thread::spawn(move || { |
244 | while let Ok(event: Result) = rx.recv() { |
245 | use notify::EventKind::*; |
246 | if let Ok(event: Event) = event { |
247 | if (matches!(event.kind, Modify(_) | Remove(_) | Create(_))) |
248 | && PENDING_EVENTS.load(order:Ordering::SeqCst) == 0 |
249 | { |
250 | PENDING_EVENTS.fetch_add(val:1, order:Ordering::SeqCst); |
251 | let args: Cli = args.clone(); |
252 | let w2: Arc> = w2.clone(); |
253 | i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || { |
254 | i_slint_core::future::spawn_local(fut:reload(args, fswatcher:w2)).unwrap(); |
255 | }) |
256 | .unwrap(); |
257 | } |
258 | } |
259 | } |
260 | }); |
261 | Ok(w) |
262 | } |
263 | |
264 | async fn reload(args: Cli, fswatcher: Arc<Mutex<notify::RecommendedWatcher>>) { |
265 | let mut compiler = init_compiler(&args, Some(fswatcher)); |
266 | let c = compiler.build_from_path(&args.path).await; |
267 | slint_interpreter::print_diagnostics(compiler.diagnostics()); |
268 | |
269 | if let Some(c) = c { |
270 | CURRENT_INSTANCE.with(|current| { |
271 | let mut current = current.borrow_mut(); |
272 | if let Some(handle) = current.take() { |
273 | let window = handle.window(); |
274 | let new_handle = c.create_with_existing_window(window).unwrap(); |
275 | init_dialog(&new_handle); |
276 | current.replace(new_handle); |
277 | } else { |
278 | let handle = c.create().unwrap(); |
279 | init_dialog(&handle); |
280 | handle.show().unwrap(); |
281 | current.replace(handle); |
282 | } |
283 | if let Some(data_path) = args.load_data { |
284 | let _ = load_data(&c, current.as_ref().unwrap(), &data_path); |
285 | } |
286 | eprintln!("Successful reload of {}" , args.path.display()); |
287 | }); |
288 | } |
289 | |
290 | PENDING_EVENTS.fetch_sub(1, Ordering::SeqCst); |
291 | } |
292 | |
293 | fn load_data( |
294 | c: &ComponentDefinition, |
295 | instance: &ComponentInstance, |
296 | data_path: &std::path::Path, |
297 | ) -> Result<()> { |
298 | let json: serde_json::Value = if data_path == std::path::Path::new("-" ) { |
299 | serde_json::from_reader(std::io::stdin())? |
300 | } else { |
301 | serde_json::from_reader(BufReader::new(std::fs::File::open(data_path)?))? |
302 | }; |
303 | |
304 | let types = c.properties_and_callbacks().collect::<HashMap<_, _>>(); |
305 | let obj = json.as_object().ok_or("The data is not a JSON object" )?; |
306 | for (name, v) in obj { |
307 | fn from_json( |
308 | t: &i_slint_compiler::langtype::Type, |
309 | v: &serde_json::Value, |
310 | ) -> slint_interpreter::Value { |
311 | match v { |
312 | serde_json::Value::Null => slint_interpreter::Value::Void, |
313 | serde_json::Value::Bool(b) => (*b).into(), |
314 | serde_json::Value::Number(n) => { |
315 | slint_interpreter::Value::Number(n.as_f64().unwrap_or(f64::NAN)) |
316 | } |
317 | serde_json::Value::String(s) => match t { |
318 | i_slint_compiler::langtype::Type::Enumeration(e) => { |
319 | if e.values.contains(s) { |
320 | slint_interpreter::Value::EnumerationValue( |
321 | e.name.to_string(), |
322 | s.to_string(), |
323 | ) |
324 | } else { |
325 | eprintln!("Warning: Unexpected value for enum ' {}': {}" , e.name, s); |
326 | slint_interpreter::Value::Void |
327 | } |
328 | } |
329 | i_slint_compiler::langtype::Type::String => { |
330 | SharedString::from(s.as_str()).into() |
331 | } |
332 | _ => slint_interpreter::Value::Void, |
333 | }, |
334 | serde_json::Value::Array(array) => match t { |
335 | i_slint_compiler::langtype::Type::Array(it) => slint_interpreter::Value::Model( |
336 | ModelRc::new(i_slint_core::model::SharedVectorModel::from( |
337 | array.iter().map(|v| from_json(it, v)).collect::<SharedVector<Value>>(), |
338 | )), |
339 | ), |
340 | _ => slint_interpreter::Value::Void, |
341 | }, |
342 | serde_json::Value::Object(obj) => match t { |
343 | i_slint_compiler::langtype::Type::Struct { fields, .. } => obj |
344 | .iter() |
345 | .filter_map(|(k, v)| match fields.get(k) { |
346 | Some(t) => Some((k.clone(), from_json(t, v))), |
347 | None => { |
348 | eprintln!("Warning: ignoring unknown property: {}" , k); |
349 | None |
350 | } |
351 | }) |
352 | .collect::<slint_interpreter::Struct>() |
353 | .into(), |
354 | _ => slint_interpreter::Value::Void, |
355 | }, |
356 | } |
357 | } |
358 | |
359 | match types.get(name) { |
360 | Some(t) => { |
361 | match instance.set_property(name, from_json(t, v)) { |
362 | Ok(()) => (), |
363 | Err(e) => { |
364 | eprintln!("Warning: cannot set property ' {}' from data file: {:?}" , name, e) |
365 | } |
366 | }; |
367 | } |
368 | None => eprintln!("Warning: ignoring unknown property: {}" , name), |
369 | } |
370 | } |
371 | Ok(()) |
372 | } |
373 | |
374 | fn install_callbacks(instance: &ComponentInstance, callbacks: &[String]) { |
375 | assert!(callbacks.len() % 2 == 0); |
376 | for chunk: &[String] in callbacks.chunks(chunk_size:2) { |
377 | if let [callback: &String, cmd: &String] = chunk { |
378 | let cmd: String = cmd.clone(); |
379 | match instance.set_callback(name:callback, callback:move |args: &[Value]| { |
380 | match execute_cmd(&cmd, callback_args:args) { |
381 | Ok(()) => (), |
382 | Err(e: Box) => eprintln!("Error: {}" , e), |
383 | } |
384 | Value::Void |
385 | }) { |
386 | Ok(()) => (), |
387 | Err(e: SetCallbackError) => { |
388 | eprintln!("Warning: cannot set callback handler for ' {}': {}" , callback, e) |
389 | } |
390 | } |
391 | } |
392 | } |
393 | } |
394 | |
395 | fn execute_cmd(cmd: &str, callback_args: &[Value]) -> Result<()> { |
396 | let cmd_args = shlex::split(cmd).ok_or("Could not parse the command string" )?; |
397 | let program_name = cmd_args.first().ok_or("Missing program name" )?; |
398 | let mut command = std::process::Command::new(program_name); |
399 | let callback_args = callback_args |
400 | .iter() |
401 | .map(|v| { |
402 | Ok(match v { |
403 | Value::Number(x) => x.to_string(), |
404 | Value::String(x) => x.to_string(), |
405 | Value::Bool(x) => x.to_string(), |
406 | Value::Image(img) => { |
407 | img.path().map(|p| p.to_string_lossy()).unwrap_or_default().into() |
408 | } |
409 | _ => return Err(format!("Cannot convert argument to string: {:?}" , v).into()), |
410 | }) |
411 | }) |
412 | .collect::<Result<Vec<String>>>()?; |
413 | for mut a in cmd_args.into_iter().skip(1) { |
414 | for (idx, cb_a) in callback_args.iter().enumerate() { |
415 | a = a.replace(&format!("$ {}" , idx + 1), cb_a); |
416 | } |
417 | command.arg(a); |
418 | } |
419 | command.spawn()?; |
420 | Ok(()) |
421 | } |
422 | |