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
6use i_slint_core::model::{Model, ModelRc};
7use i_slint_core::SharedVector;
8use slint_interpreter::{
9 ComponentDefinition, ComponentHandle, ComponentInstance, SharedString, Value,
10};
11use std::collections::HashMap;
12use std::io::{BufReader, BufWriter};
13use std::sync::atomic::{AtomicU32, Ordering};
14use std::sync::{Arc, Mutex};
15
16use clap::Parser;
17use itertools::Itertools;
18
19type 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)]
23struct 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
74thread_local! {static CURRENT_INSTANCE: std::cell::RefCell<Option<ComponentInstance>> = Default::default();}
75static EXIT_CODE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
76
77fn 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
165fn 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
218fn 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
237static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0);
238
239fn 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
264async 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
293fn 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
374fn 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
395fn 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