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