| 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 | |
| 6 | use clap::Parser; |
| 7 | use i_slint_compiler::ComponentSelection; |
| 8 | use itertools::Itertools; |
| 9 | use slint_interpreter::{ |
| 10 | json::JsonExt, ComponentDefinition, ComponentHandle, ComponentInstance, Value, |
| 11 | }; |
| 12 | use std::collections::HashMap; |
| 13 | use std::io::{BufReader, BufWriter}; |
| 14 | use std::path::PathBuf; |
| 15 | use std::sync::atomic::{AtomicU32, Ordering}; |
| 16 | use std::sync::{Arc, Mutex}; |
| 17 | |
| 18 | #[cfg (not(any(target_os = "windows" , all(target_arch = "aarch64" , target_os = "linux" ))))] |
| 19 | use tikv_jemallocator::Jemalloc; |
| 20 | |
| 21 | #[cfg (not(any(target_os = "windows" , all(target_arch = "aarch64" , target_os = "linux" ))))] |
| 22 | #[global_allocator ] |
| 23 | static GLOBAL: Jemalloc = Jemalloc; |
| 24 | |
| 25 | struct Error(Box<dyn std::error::Error>); |
| 26 | impl 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 | |
| 33 | impl<T> From<T> for Error |
| 34 | where |
| 35 | T: Into<Box<dyn std::error::Error>> + 'static, |
| 36 | { |
| 37 | fn from(value: T) -> Self { |
| 38 | Self(value.into()) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | type Result<T> = std::result::Result<T, Error>; |
| 43 | |
| 44 | #[derive (Clone, clap::Parser)] |
| 45 | #[command(author, version, about, long_about = None)] |
| 46 | struct 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 | |
| 102 | thread_local! {static CURRENT_INSTANCE: std::cell::RefCell<Option<ComponentInstance>> = Default::default();} |
| 103 | static EXIT_CODE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); |
| 104 | |
| 105 | fn 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 | |
| 181 | fn 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 | |
| 220 | fn 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 | |
| 253 | fn 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 | |
| 272 | static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0); |
| 273 | |
| 274 | fn 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 | |
| 299 | async 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 | |
| 332 | fn 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 | |
| 362 | fn 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 | |
| 383 | fn 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 | |