// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #![doc = include_str!("README.md")] use clap::Parser; use i_slint_compiler::ComponentSelection; use itertools::Itertools; use slint_interpreter::{ json::JsonExt, ComponentDefinition, ComponentHandle, ComponentInstance, Value, }; use std::collections::HashMap; use std::io::{BufReader, BufWriter}; use std::path::Path; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; #[cfg(not(any(target_os = "windows", all(target_arch = "aarch64", target_os = "linux"))))] use tikv_jemallocator::Jemalloc; #[cfg(not(any(target_os = "windows", all(target_arch = "aarch64", target_os = "linux"))))] #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; struct Error(Box); impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Use the Display impl of the error instead of the error write!(f, "{}", self.0) } } impl From for Error where T: Into> + 'static, { fn from(value: T) -> Self { Self(value.into()) } } type Result = std::result::Result; #[derive(Clone, clap::Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// Include path for other .slint files or images #[arg(short = 'I', value_name = "include path", number_of_values = 1, action)] include_paths: Vec, /// Specify Library location of the '@library' in the form 'library=/path/to/library' #[arg(short = 'L', value_name = "library=path", number_of_values = 1, action)] library_paths: Vec, /// The .slint file to load ('-' for stdin) #[arg(name = "path", action)] path: std::path::PathBuf, /// The style name ('native' or 'fluent') #[arg(long, value_name = "style name", action)] style: Option, /// The name of the component to view. If unset, the last exported component of the file is used. /// If the component name is not in the .slint file , nothing will be shown #[arg(long, value_name = "component name", action)] component: Option, /// The rendering backend #[arg(long, value_name = "backend", action)] backend: Option, /// Automatically watch the file system, and reload when it changes #[arg(long, action)] auto_reload: bool, /// Load properties from a json file ('-' for stdin) #[arg(long, value_name = "json file", action)] load_data: Option, /// Store properties values in a json file at exit ('-' for stdout) #[arg(long, value_name = "json file", action)] save_data: Option, /// Specify callbacks handler. /// The first argument is the callback name, and the second argument is a string that is going /// to be passed to the shell to be executed. Occurrences of `$1` will be replaced by the first argument, /// and so on. #[arg(long, value_names(&["callback", "handler"]), number_of_values = 2, action)] on: Vec, #[cfg(feature = "gettext")] /// Translation domain #[arg(long = "translation-domain", action)] translation_domain: Option, #[cfg(feature = "gettext")] /// Translation directory where the translation files are searched for #[arg(long = "translation-dir", action)] translation_dir: Option, } thread_local! {static CURRENT_INSTANCE: std::cell::RefCell> = Default::default();} static EXIT_CODE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); fn main() -> Result<()> { env_logger::init(); let args = Cli::parse(); if args.auto_reload && args.save_data.is_some() { eprintln!("Cannot pass both --auto-reload and --save-data"); std::process::exit(-1); } if let Some(backend) = &args.backend { std::env::set_var("SLINT_BACKEND", backend); } #[cfg(feature = "gettext")] if let Some(dirname) = args.translation_dir.clone() { i_slint_core::translations::gettext_bindtextdomain( args.translation_domain.as_ref().map(String::as_str).unwrap_or_default(), dirname, )?; }; let fswatcher = if args.auto_reload { Some(start_fswatch_thread(args.clone())?) } else { None }; let compiler = init_compiler(&args, fswatcher); let r = spin_on::spin_on(compiler.build_from_path(&args.path)); r.print_diagnostics(); if r.has_errors() { std::process::exit(-1); } let Some(c) = r.components().next() else { match args.component { Some(name) => { eprintln!("Component '{name}' not found in file '{}'", args.path.display()); } None => { eprintln!("No component found in file '{}'", args.path.display()); } } std::process::exit(-1); }; let component = c.create()?; init_dialog(&component); if let Some(data_path) = args.load_data { load_data(&c, &component, &data_path)?; } install_callbacks(&component, &args.on); if args.auto_reload { CURRENT_INSTANCE.with(|current| current.replace(Some(component.clone_strong()))); } component.run()?; if let Some(data_path) = args.save_data { let mut obj = serde_json::Map::new(); for (name, _) in c.properties() { match component.get_property(&name).unwrap().to_json() { Ok(v) => { obj.insert(name, v); } Err(e) => { eprintln!("Failed to turn property {name} into JSON: {e}"); } } } if data_path == std::path::Path::new("-") { serde_json::to_writer_pretty(std::io::stdout(), &obj)?; } else { serde_json::to_writer_pretty(BufWriter::new(std::fs::File::create(data_path)?), &obj)?; } } std::process::exit(EXIT_CODE.load(std::sync::atomic::Ordering::Relaxed)) } fn init_compiler( args: &Cli, fswatcher: Option>>, ) -> slint_interpreter::Compiler { let mut compiler = slint_interpreter::Compiler::new(); #[cfg(feature = "gettext")] if let Some(domain) = args.translation_domain.clone() { compiler.set_translation_domain(domain); } compiler.set_include_paths(args.include_paths.clone()); compiler.set_library_paths( args.library_paths .iter() .filter_map(|entry| entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into()))) .collect(), ); if let Some(style) = &args.style { compiler.set_style(style.clone()); } if let Some(watcher) = fswatcher { watch_with_retry(&args.path, &watcher); if let Some(data_path) = &args.load_data { watch_with_retry(data_path, &watcher); } compiler.set_file_loader(move |path| { watch_with_retry(path, &watcher); Box::pin(async { None }) }) } compiler.compiler_configuration(i_slint_core::InternalToken).components_to_generate = match &args.component { Some(component) => ComponentSelection::Named(component.clone()), None => ComponentSelection::LastExported, }; compiler } fn watch_with_retry(path: &Path, watcher: &Arc>) { notify::Watcher::watch( &mut *watcher.lock().unwrap(), path, notify::RecursiveMode::NonRecursive, ) .unwrap_or_else(|err| match err.kind { notify::ErrorKind::PathNotFound | notify::ErrorKind::Generic(_) => { let path = path.to_path_buf(); let watcher = watcher.clone(); static RETRY_DURATION: u64 = 100; i_slint_core::timers::Timer::single_shot( std::time::Duration::from_millis(RETRY_DURATION), move || { notify::Watcher::watch( &mut *watcher.lock().unwrap(), &path, notify::RecursiveMode::NonRecursive, ) .unwrap_or_else(|err| { eprintln!( "Warning: error while watching missing path {}: {:?}", path.display(), err ) }); }, ); } _ => eprintln!("Warning: error while watching {}: {:?}", path.display(), err), }); } fn init_dialog(instance: &ComponentInstance) { for cb in instance.definition().callbacks() { let exit_code = match cb.as_str() { "ok-clicked" | "yes-clicked" | "close-clicked" => 0, "cancel-clicked" | "no-clicked" => 1, _ => continue, }; // this is a dialog, so clicking the "x" should cancel EXIT_CODE.store(1, std::sync::atomic::Ordering::Relaxed); instance .set_callback(&cb, move |_| { EXIT_CODE.store(exit_code, std::sync::atomic::Ordering::Relaxed); i_slint_core::api::quit_event_loop().unwrap(); Default::default() }) .unwrap(); } } static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0); fn start_fswatch_thread(args: Cli) -> Result>> { let (tx, rx) = std::sync::mpsc::channel(); let w = Arc::new(Mutex::new(notify::recommended_watcher(tx)?)); let w2 = w.clone(); std::thread::spawn(move || { while let Ok(event) = rx.recv() { use notify::EventKind::*; if let Ok(event) = event { if (matches!(event.kind, Modify(_) | Remove(_) | Create(_))) && PENDING_EVENTS.load(Ordering::SeqCst) == 0 { PENDING_EVENTS.fetch_add(1, Ordering::SeqCst); let args = args.clone(); let w2 = w2.clone(); i_slint_core::api::invoke_from_event_loop(move || { slint_interpreter::spawn_local(reload(args, w2)).unwrap(); }) .unwrap(); } } } }); Ok(w) } async fn reload(args: Cli, fswatcher: Arc>) { let compiler = init_compiler(&args, Some(fswatcher)); let r = compiler.build_from_path(&args.path).await; r.print_diagnostics(); if let Some(c) = r.components().next() { CURRENT_INSTANCE.with(|current| { let mut current = current.borrow_mut(); if let Some(handle) = current.take() { let window = handle.window(); let new_handle = c.create_with_existing_window(window).unwrap(); init_dialog(&new_handle); current.replace(new_handle); } else { let handle = c.create().unwrap(); init_dialog(&handle); handle.show().unwrap(); current.replace(handle); } if let Some(data_path) = args.load_data { let _ = load_data(&c, current.as_ref().unwrap(), &data_path); } eprintln!("Successful reload of {}", args.path.display()); }); } else if !r.has_errors() { match &args.component { Some(name) => println!("Component {name} not found"), None => println!("No component found"), } } PENDING_EVENTS.fetch_sub(1, Ordering::SeqCst); } fn load_data( c: &ComponentDefinition, instance: &ComponentInstance, data_path: &std::path::Path, ) -> Result<()> { let json: serde_json::Value = if data_path == std::path::Path::new("-") { serde_json::from_reader(std::io::stdin())? } else { serde_json::from_reader(BufReader::new(std::fs::File::open(data_path)?))? }; let types = c.properties_and_callbacks().collect::>(); let obj = json.as_object().ok_or("The data is not a JSON object")?; for (name, v) in obj { match types.get(name.as_str()) { Some((t, _)) => match slint_interpreter::Value::from_json(t, v) { Ok(v) => match instance.set_property(name, v) { Ok(()) => (), Err(e) => { eprintln!("Warning: cannot set property '{name}' from data file: {e}") } }, Err(e) => eprintln!("Warning: cannot set property '{name}' from data file: {e}"), }, None => eprintln!("Warning: ignoring unknown property: {name}"), } } Ok(()) } fn install_callbacks(instance: &ComponentInstance, callbacks: &[String]) { assert!(callbacks.len() % 2 == 0); for chunk in callbacks.chunks(2) { if let [callback, cmd] = chunk { let cmd = cmd.clone(); match instance.set_callback(callback, move |args| { match execute_cmd(&cmd, args) { Ok(()) => (), Err(e) => eprintln!("Error: {e:?}"), } Value::Void }) { Ok(()) => (), Err(e) => { eprintln!("Warning: cannot set callback handler for '{callback}': {e}") } } } } } fn execute_cmd(cmd: &str, callback_args: &[Value]) -> Result<()> { let cmd_args = shlex::split(cmd).ok_or("Could not parse the command string")?; let program_name = cmd_args.first().ok_or("Missing program name")?; let mut command = std::process::Command::new(program_name); let callback_args = callback_args .iter() .map(|v| { Ok(match v { Value::Number(x) => x.to_string(), Value::String(x) => x.to_string(), Value::Bool(x) => x.to_string(), Value::Image(img) => { img.path().map(|p| p.to_string_lossy()).unwrap_or_default().into() } Value::Struct(st) => { let mut obj = serde_json::Map::new(); for (k, v) in st.iter() { match v.to_json() { Ok(v) => { obj.insert(k.into(), v); } Err(e) => { eprintln!("Failed to convert field {k} to JSON: {e}"); } } } serde_json::to_string_pretty(&obj)? } _ => return Err(format!("Cannot convert argument to string: {v:?}").into()), }) }) .collect::>>()?; for mut a in cmd_args.into_iter().skip(1) { for (idx, cb_a) in callback_args.iter().enumerate() { a = a.replace(&format!("${}", idx + 1), cb_a); } command.arg(a); } command.spawn()?; Ok(()) }