Skip to main content

tauri_utils/
config.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! The Tauri configuration used at runtime.
6//!
7//! It is pulled from a `tauri.conf.json` file and the [`Config`] struct is generated at compile time.
8//!
9//! # Stability
10//!
11//! This is a core functionality that is not considered part of the stable API.
12//! If you use it, note that it may include breaking changes in the future.
13//!
14//! These items are intended to be non-breaking from a de/serialization standpoint only.
15//! Using and modifying existing config values will try to avoid breaking changes, but they are
16//! free to add fields in the future - causing breaking changes for creating and full destructuring.
17//!
18//! To avoid this, [ignore unknown fields when destructuring] with the `{my, config, ..}` pattern.
19//! If you need to create the Rust config directly without deserializing, then create the struct
20//! the [Struct Update Syntax] with `..Default::default()`, which may need a
21//! `#[allow(clippy::needless_update)]` attribute if you are declaring all fields.
22//!
23//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
24//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
25
26use http::response::Builder;
27#[cfg(feature = "schema")]
28use schemars::schema::Schema;
29#[cfg(feature = "schema")]
30use schemars::JsonSchema;
31use semver::Version;
32use serde::{
33  de::{Deserializer, Error as DeError, Visitor},
34  Deserialize, Serialize, Serializer,
35};
36use serde_json::Value as JsonValue;
37use serde_untagged::UntaggedEnumVisitor;
38use serde_with::skip_serializing_none;
39use url::Url;
40
41use std::{
42  collections::{HashMap, HashSet},
43  fmt::{self, Display},
44  fs::read_to_string,
45  path::PathBuf,
46  str::FromStr,
47};
48
49#[cfg(feature = "schema")]
50fn add_description(schema: Schema, description: impl Into<String>) -> Schema {
51  let value = description.into();
52  if value.is_empty() {
53    schema
54  } else {
55    let mut schema_obj = schema.into_object();
56    schema_obj.metadata().description = value.into();
57    Schema::Object(schema_obj)
58  }
59}
60
61/// Items to help with parsing content into a [`Config`].
62pub mod parse;
63
64use crate::{acl::capability::Capability, TitleBarStyle, WindowEffect, WindowEffectState};
65
66pub use self::parse::parse;
67
68fn default_true() -> bool {
69  true
70}
71
72/// An URL to open on a Tauri webview window.
73#[derive(PartialEq, Eq, Debug, Clone, Serialize)]
74#[cfg_attr(feature = "schema", derive(JsonSchema))]
75#[serde(untagged)]
76#[non_exhaustive]
77pub enum WebviewUrl {
78  /// An external URL. Must use either the `http` or `https` schemes.
79  External(Url),
80  /// The path portion of an app URL.
81  /// For instance, to load `tauri://localhost/users/john`,
82  /// you can simply provide `users/john` in this configuration.
83  App(PathBuf),
84  /// A custom protocol url, for example, `doom://index.html`
85  CustomProtocol(Url),
86}
87
88impl<'de> Deserialize<'de> for WebviewUrl {
89  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
90  where
91    D: Deserializer<'de>,
92  {
93    #[derive(Deserialize)]
94    #[serde(untagged)]
95    enum WebviewUrlDeserializer {
96      Url(Url),
97      Path(PathBuf),
98    }
99
100    match WebviewUrlDeserializer::deserialize(deserializer)? {
101      WebviewUrlDeserializer::Url(u) => {
102        if u.scheme() == "https" || u.scheme() == "http" {
103          Ok(Self::External(u))
104        } else {
105          Ok(Self::CustomProtocol(u))
106        }
107      }
108      WebviewUrlDeserializer::Path(p) => Ok(Self::App(p)),
109    }
110  }
111}
112
113impl fmt::Display for WebviewUrl {
114  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115    match self {
116      Self::External(url) | Self::CustomProtocol(url) => write!(f, "{url}"),
117      Self::App(path) => write!(f, "{}", path.display()),
118    }
119  }
120}
121
122impl Default for WebviewUrl {
123  fn default() -> Self {
124    Self::App("index.html".into())
125  }
126}
127
128/// A bundle referenced by tauri-bundler.
129#[derive(Debug, PartialEq, Eq, Clone)]
130#[cfg_attr(feature = "schema", derive(JsonSchema))]
131#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
132pub enum BundleType {
133  /// The debian bundle (.deb).
134  Deb,
135  /// The RPM bundle (.rpm).
136  Rpm,
137  /// The AppImage bundle (.appimage).
138  AppImage,
139  /// The Microsoft Installer bundle (.msi).
140  Msi,
141  /// The NSIS bundle (.exe).
142  Nsis,
143  /// The macOS application bundle (.app).
144  App,
145  /// The Apple Disk Image bundle (.dmg).
146  Dmg,
147}
148
149impl BundleType {
150  /// All bundle types.
151  fn all() -> &'static [Self] {
152    &[
153      BundleType::Deb,
154      BundleType::Rpm,
155      BundleType::AppImage,
156      BundleType::Msi,
157      BundleType::Nsis,
158      BundleType::App,
159      BundleType::Dmg,
160    ]
161  }
162}
163
164impl Display for BundleType {
165  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166    write!(
167      f,
168      "{}",
169      match self {
170        Self::Deb => "deb",
171        Self::Rpm => "rpm",
172        Self::AppImage => "appimage",
173        Self::Msi => "msi",
174        Self::Nsis => "nsis",
175        Self::App => "app",
176        Self::Dmg => "dmg",
177      }
178    )
179  }
180}
181
182impl Serialize for BundleType {
183  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
184  where
185    S: Serializer,
186  {
187    serializer.serialize_str(self.to_string().as_ref())
188  }
189}
190
191impl<'de> Deserialize<'de> for BundleType {
192  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
193  where
194    D: Deserializer<'de>,
195  {
196    let s = String::deserialize(deserializer)?;
197    match s.to_lowercase().as_str() {
198      "deb" => Ok(Self::Deb),
199      "rpm" => Ok(Self::Rpm),
200      "appimage" => Ok(Self::AppImage),
201      "msi" => Ok(Self::Msi),
202      "nsis" => Ok(Self::Nsis),
203      "app" => Ok(Self::App),
204      "dmg" => Ok(Self::Dmg),
205      _ => Err(DeError::custom(format!("unknown bundle target '{s}'"))),
206    }
207  }
208}
209
210/// Targets to bundle. Each value is case insensitive.
211#[derive(Debug, PartialEq, Eq, Clone, Default)]
212pub enum BundleTarget {
213  /// Bundle all targets.
214  #[default]
215  All,
216  /// A list of bundle targets.
217  List(Vec<BundleType>),
218  /// A single bundle target.
219  One(BundleType),
220}
221
222#[cfg(feature = "schema")]
223impl schemars::JsonSchema for BundleTarget {
224  fn schema_name() -> std::string::String {
225    "BundleTarget".to_owned()
226  }
227
228  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
229    let any_of = vec![
230      schemars::schema::SchemaObject {
231        const_value: Some("all".into()),
232        metadata: Some(Box::new(schemars::schema::Metadata {
233          description: Some("Bundle all targets.".to_owned()),
234          ..Default::default()
235        })),
236        ..Default::default()
237      }
238      .into(),
239      add_description(
240        gen.subschema_for::<Vec<BundleType>>(),
241        "A list of bundle targets.",
242      ),
243      add_description(gen.subschema_for::<BundleType>(), "A single bundle target."),
244    ];
245
246    schemars::schema::SchemaObject {
247      subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
248        any_of: Some(any_of),
249        ..Default::default()
250      })),
251      metadata: Some(Box::new(schemars::schema::Metadata {
252        description: Some("Targets to bundle. Each value is case insensitive.".to_owned()),
253        ..Default::default()
254      })),
255      ..Default::default()
256    }
257    .into()
258  }
259}
260
261impl Serialize for BundleTarget {
262  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
263  where
264    S: Serializer,
265  {
266    match self {
267      Self::All => serializer.serialize_str("all"),
268      Self::List(l) => l.serialize(serializer),
269      Self::One(t) => serializer.serialize_str(t.to_string().as_ref()),
270    }
271  }
272}
273
274impl<'de> Deserialize<'de> for BundleTarget {
275  fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
276  where
277    D: Deserializer<'de>,
278  {
279    #[derive(Deserialize, Serialize)]
280    #[serde(untagged)]
281    pub enum BundleTargetInner {
282      List(Vec<BundleType>),
283      One(BundleType),
284      All(String),
285    }
286
287    match BundleTargetInner::deserialize(deserializer)? {
288      BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All),
289      BundleTargetInner::All(t) => Err(DeError::custom(format!(
290        "invalid bundle type {t}, expected one of `all`, {}",
291        BundleType::all()
292          .iter()
293          .map(|b| format!("`{b}`"))
294          .collect::<Vec<_>>()
295          .join(", ")
296      ))),
297      BundleTargetInner::List(l) => Ok(Self::List(l)),
298      BundleTargetInner::One(t) => Ok(Self::One(t)),
299    }
300  }
301}
302
303impl BundleTarget {
304  /// Gets the bundle targets as a [`Vec`]. The vector is empty when set to [`BundleTarget::All`].
305  #[allow(dead_code)]
306  pub fn to_vec(&self) -> Vec<BundleType> {
307    match self {
308      Self::All => BundleType::all().to_vec(),
309      Self::List(list) => list.clone(),
310      Self::One(i) => vec![i.clone()],
311    }
312  }
313}
314
315/// Configuration for AppImage bundles.
316///
317/// See more: <https://v2.tauri.app/reference/config/#appimageconfig>
318#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
319#[cfg_attr(feature = "schema", derive(JsonSchema))]
320#[serde(rename_all = "camelCase", deny_unknown_fields)]
321pub struct AppImageConfig {
322  /// Include additional gstreamer dependencies needed for audio and video playback.
323  /// This increases the bundle size by ~15-35MB depending on your build system.
324  #[serde(default, alias = "bundle-media-framework")]
325  pub bundle_media_framework: bool,
326  /// The files to include in the Appimage Binary.
327  #[serde(default)]
328  pub files: HashMap<PathBuf, PathBuf>,
329}
330
331/// Configuration for Debian (.deb) bundles.
332///
333/// See more: <https://v2.tauri.app/reference/config/#debconfig>
334#[skip_serializing_none]
335#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
336#[cfg_attr(feature = "schema", derive(JsonSchema))]
337#[serde(rename_all = "camelCase", deny_unknown_fields)]
338pub struct DebConfig {
339  /// The list of deb dependencies your application relies on.
340  pub depends: Option<Vec<String>>,
341  /// The list of deb dependencies your application recommends.
342  pub recommends: Option<Vec<String>>,
343  /// The list of dependencies the package provides.
344  pub provides: Option<Vec<String>>,
345  /// The list of package conflicts.
346  pub conflicts: Option<Vec<String>>,
347  /// The list of package replaces.
348  pub replaces: Option<Vec<String>>,
349  /// The files to include on the package.
350  #[serde(default)]
351  pub files: HashMap<PathBuf, PathBuf>,
352  /// Define the section in Debian Control file. See : https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections
353  pub section: Option<String>,
354  /// Change the priority of the Debian Package. By default, it is set to `optional`.
355  /// Recognized Priorities as of now are :  `required`, `important`, `standard`, `optional`, `extra`
356  pub priority: Option<String>,
357  /// Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See
358  /// <https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes>
359  pub changelog: Option<PathBuf>,
360  /// Path to a custom desktop file Handlebars template.
361  ///
362  /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.
363  #[serde(alias = "desktop-template")]
364  pub desktop_template: Option<PathBuf>,
365  /// Path to script that will be executed before the package is unpacked. See
366  /// <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>
367  #[serde(alias = "pre-install-script")]
368  pub pre_install_script: Option<PathBuf>,
369  /// Path to script that will be executed after the package is unpacked. See
370  /// <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>
371  #[serde(alias = "post-install-script")]
372  pub post_install_script: Option<PathBuf>,
373  /// Path to script that will be executed before the package is removed. See
374  /// <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>
375  #[serde(alias = "pre-remove-script")]
376  pub pre_remove_script: Option<PathBuf>,
377  /// Path to script that will be executed after the package is removed. See
378  /// <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>
379  #[serde(alias = "post-remove-script")]
380  pub post_remove_script: Option<PathBuf>,
381}
382
383/// Configuration for Linux bundles.
384///
385/// See more: <https://v2.tauri.app/reference/config/#linuxconfig>
386#[skip_serializing_none]
387#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
388#[cfg_attr(feature = "schema", derive(JsonSchema))]
389#[serde(rename_all = "camelCase", deny_unknown_fields)]
390pub struct LinuxConfig {
391  /// Configuration for the AppImage bundle.
392  #[serde(default)]
393  pub appimage: AppImageConfig,
394  /// Configuration for the Debian bundle.
395  #[serde(default)]
396  pub deb: DebConfig,
397  /// Configuration for the RPM bundle.
398  #[serde(default)]
399  pub rpm: RpmConfig,
400}
401
402/// Compression algorithms used when bundling RPM packages.
403#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
404#[cfg_attr(feature = "schema", derive(JsonSchema))]
405#[serde(rename_all = "camelCase", deny_unknown_fields, tag = "type")]
406#[non_exhaustive]
407pub enum RpmCompression {
408  /// Gzip compression
409  Gzip {
410    /// Gzip compression level
411    level: u32,
412  },
413  /// Zstd compression
414  Zstd {
415    /// Zstd compression level
416    level: i32,
417  },
418  /// Xz compression
419  Xz {
420    /// Xz compression level
421    level: u32,
422  },
423  /// Bzip2 compression
424  Bzip2 {
425    /// Bzip2 compression level
426    level: u32,
427  },
428  /// Disable compression
429  None,
430}
431
432/// Configuration for RPM bundles.
433#[skip_serializing_none]
434#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
435#[cfg_attr(feature = "schema", derive(JsonSchema))]
436#[serde(rename_all = "camelCase", deny_unknown_fields)]
437pub struct RpmConfig {
438  /// The list of RPM dependencies your application relies on.
439  pub depends: Option<Vec<String>>,
440  /// The list of RPM dependencies your application recommends.
441  pub recommends: Option<Vec<String>>,
442  /// The list of RPM dependencies your application provides.
443  pub provides: Option<Vec<String>>,
444  /// The list of RPM dependencies your application conflicts with. They must not be present
445  /// in order for the package to be installed.
446  pub conflicts: Option<Vec<String>>,
447  /// The list of RPM dependencies your application supersedes - if this package is installed,
448  /// packages listed as "obsoletes" will be automatically removed (if they are present).
449  pub obsoletes: Option<Vec<String>>,
450  /// The RPM release tag.
451  #[serde(default = "default_release")]
452  pub release: String,
453  /// The RPM epoch.
454  #[serde(default)]
455  pub epoch: u32,
456  /// The files to include on the package.
457  #[serde(default)]
458  pub files: HashMap<PathBuf, PathBuf>,
459  /// Path to a custom desktop file Handlebars template.
460  ///
461  /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.
462  #[serde(alias = "desktop-template")]
463  pub desktop_template: Option<PathBuf>,
464  /// Path to script that will be executed before the package is unpacked. See
465  /// <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>
466  #[serde(alias = "pre-install-script")]
467  pub pre_install_script: Option<PathBuf>,
468  /// Path to script that will be executed after the package is unpacked. See
469  /// <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>
470  #[serde(alias = "post-install-script")]
471  pub post_install_script: Option<PathBuf>,
472  /// Path to script that will be executed before the package is removed. See
473  /// <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>
474  #[serde(alias = "pre-remove-script")]
475  pub pre_remove_script: Option<PathBuf>,
476  /// Path to script that will be executed after the package is removed. See
477  /// <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>
478  #[serde(alias = "post-remove-script")]
479  pub post_remove_script: Option<PathBuf>,
480  /// Compression algorithm and level. Defaults to `Gzip` with level 6.
481  pub compression: Option<RpmCompression>,
482}
483
484impl Default for RpmConfig {
485  fn default() -> Self {
486    Self {
487      depends: None,
488      recommends: None,
489      provides: None,
490      conflicts: None,
491      obsoletes: None,
492      release: default_release(),
493      epoch: 0,
494      files: Default::default(),
495      desktop_template: None,
496      pre_install_script: None,
497      post_install_script: None,
498      pre_remove_script: None,
499      post_remove_script: None,
500      compression: None,
501    }
502  }
503}
504
505fn default_release() -> String {
506  "1".into()
507}
508
509/// Position coordinates struct.
510#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
511#[cfg_attr(feature = "schema", derive(JsonSchema))]
512#[serde(rename_all = "camelCase", deny_unknown_fields)]
513pub struct Position {
514  /// X coordinate.
515  pub x: u32,
516  /// Y coordinate.
517  pub y: u32,
518}
519
520/// Position coordinates struct.
521#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize)]
522#[cfg_attr(feature = "schema", derive(JsonSchema))]
523#[serde(rename_all = "camelCase", deny_unknown_fields)]
524pub struct LogicalPosition {
525  /// X coordinate.
526  pub x: f64,
527  /// Y coordinate.
528  pub y: f64,
529}
530
531/// Size of the window.
532#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
533#[cfg_attr(feature = "schema", derive(JsonSchema))]
534#[serde(rename_all = "camelCase", deny_unknown_fields)]
535pub struct Size {
536  /// Width of the window.
537  pub width: u32,
538  /// Height of the window.
539  pub height: u32,
540}
541
542/// Configuration for Apple Disk Image (.dmg) bundles.
543///
544/// See more: <https://v2.tauri.app/reference/config/#dmgconfig>
545#[skip_serializing_none]
546#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
547#[cfg_attr(feature = "schema", derive(JsonSchema))]
548#[serde(rename_all = "camelCase", deny_unknown_fields)]
549pub struct DmgConfig {
550  /// Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`.
551  pub background: Option<PathBuf>,
552  /// Position of volume window on screen.
553  pub window_position: Option<Position>,
554  /// Size of volume window.
555  #[serde(default = "dmg_window_size", alias = "window-size")]
556  pub window_size: Size,
557  /// Position of app file on window.
558  #[serde(default = "dmg_app_position", alias = "app-position")]
559  pub app_position: Position,
560  /// Position of application folder on window.
561  #[serde(
562    default = "dmg_application_folder_position",
563    alias = "application-folder-position"
564  )]
565  pub application_folder_position: Position,
566}
567
568impl Default for DmgConfig {
569  fn default() -> Self {
570    Self {
571      background: None,
572      window_position: None,
573      window_size: dmg_window_size(),
574      app_position: dmg_app_position(),
575      application_folder_position: dmg_application_folder_position(),
576    }
577  }
578}
579
580fn dmg_window_size() -> Size {
581  Size {
582    width: 660,
583    height: 400,
584  }
585}
586
587fn dmg_app_position() -> Position {
588  Position { x: 180, y: 170 }
589}
590
591fn dmg_application_folder_position() -> Position {
592  Position { x: 480, y: 170 }
593}
594
595fn de_macos_minimum_system_version<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
596where
597  D: Deserializer<'de>,
598{
599  let version = Option::<String>::deserialize(deserializer)?;
600  match version {
601    Some(v) if v.is_empty() => Ok(macos_minimum_system_version()),
602    e => Ok(e),
603  }
604}
605
606/// Configuration for the macOS bundles.
607///
608/// See more: <https://v2.tauri.app/reference/config/#macconfig>
609#[skip_serializing_none]
610#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
611#[cfg_attr(feature = "schema", derive(JsonSchema))]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct MacConfig {
614  /// A list of strings indicating any macOS X frameworks that need to be bundled with the application.
615  ///
616  /// If a name is used, ".framework" must be omitted and it will look for standard install locations. You may also use a path to a specific framework.
617  pub frameworks: Option<Vec<String>>,
618  /// The files to include in the application relative to the Contents directory.
619  #[serde(default)]
620  pub files: HashMap<PathBuf, PathBuf>,
621  /// The version of the build that identifies an iteration of the bundle.
622  ///
623  /// Translates to the bundle's CFBundleVersion property.
624  #[serde(alias = "bundle-version")]
625  pub bundle_version: Option<String>,
626  /// The name of the builder that built the bundle.
627  ///
628  /// Translates to the bundle's CFBundleName property.
629  ///
630  /// If not set, defaults to the package's product name.
631  #[serde(alias = "bundle-name")]
632  pub bundle_name: Option<String>,
633  /// A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.
634  ///
635  /// Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist`
636  /// and the `MACOSX_DEPLOYMENT_TARGET` environment variable.
637  ///
638  /// Ignored in `tauri dev`.
639  ///
640  /// An empty string is considered an invalid value so the default value is used.
641  #[serde(
642    deserialize_with = "de_macos_minimum_system_version",
643    default = "macos_minimum_system_version",
644    alias = "minimum-system-version"
645  )]
646  pub minimum_system_version: Option<String>,
647  /// Allows your application to communicate with the outside world.
648  /// It should be a lowercase, without port and protocol domain name.
649  #[serde(alias = "exception-domain")]
650  pub exception_domain: Option<String>,
651  /// Identity to use for code signing.
652  #[serde(alias = "signing-identity")]
653  pub signing_identity: Option<String>,
654  /// Whether the codesign should enable [hardened runtime](https://developer.apple.com/documentation/security/hardened_runtime) (for executables) or not.
655  #[serde(alias = "hardened-runtime", default = "default_true")]
656  pub hardened_runtime: bool,
657  /// Provider short name for notarization.
658  #[serde(alias = "provider-short-name")]
659  pub provider_short_name: Option<String>,
660  /// Path to the entitlements file.
661  pub entitlements: Option<String>,
662  /// Path to a Info.plist file to merge with the default Info.plist.
663  ///
664  /// Note that Tauri also looks for a `Info.plist` file in the same directory as the Tauri configuration file.
665  #[serde(alias = "info-plist")]
666  pub info_plist: Option<PathBuf>,
667  /// DMG-specific settings.
668  #[serde(default)]
669  pub dmg: DmgConfig,
670}
671
672impl Default for MacConfig {
673  fn default() -> Self {
674    Self {
675      frameworks: None,
676      files: HashMap::new(),
677      bundle_version: None,
678      bundle_name: None,
679      minimum_system_version: macos_minimum_system_version(),
680      exception_domain: None,
681      signing_identity: None,
682      hardened_runtime: true,
683      provider_short_name: None,
684      entitlements: None,
685      info_plist: None,
686      dmg: Default::default(),
687    }
688  }
689}
690
691fn macos_minimum_system_version() -> Option<String> {
692  Some("10.13".into())
693}
694
695fn ios_minimum_system_version() -> String {
696  "14.0".into()
697}
698
699/// Configuration for a target language for the WiX build.
700///
701/// See more: <https://v2.tauri.app/reference/config/#wixlanguageconfig>
702#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
703#[cfg_attr(feature = "schema", derive(JsonSchema))]
704#[serde(rename_all = "camelCase", deny_unknown_fields)]
705pub struct WixLanguageConfig {
706  /// The path to a locale (`.wxl`) file. See <https://wixtoolset.org/documentation/manual/v3/howtos/ui_and_localization/build_a_localized_version.html>.
707  #[serde(alias = "locale-path")]
708  pub locale_path: Option<String>,
709}
710
711/// The languages to build using WiX.
712#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
713#[cfg_attr(feature = "schema", derive(JsonSchema))]
714#[serde(untagged)]
715pub enum WixLanguage {
716  /// A single language to build, without configuration.
717  One(String),
718  /// A list of languages to build, without configuration.
719  List(Vec<String>),
720  /// A map of languages and its configuration.
721  Localized(HashMap<String, WixLanguageConfig>),
722}
723
724impl Default for WixLanguage {
725  fn default() -> Self {
726    Self::One("en-US".into())
727  }
728}
729
730/// Configuration for the MSI bundle using WiX.
731///
732/// See more: <https://v2.tauri.app/reference/config/#wixconfig>
733#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
734#[cfg_attr(feature = "schema", derive(JsonSchema))]
735#[serde(rename_all = "camelCase", deny_unknown_fields)]
736pub struct WixConfig {
737  /// MSI installer version in the format `major.minor.patch.build` (build is optional).
738  ///
739  /// Because a valid version is required for MSI installer, it will be derived from [`Config::version`] if this field is not set.
740  ///
741  /// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
742  /// The third and fourth fields have a maximum value of 65,535.
743  ///
744  /// See <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.
745  pub version: Option<String>,
746  /// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
747  /// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
748  ///
749  /// By default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
750  /// You can use Tauri's CLI to generate and print this code for you, run `tauri inspect wix-upgrade-code`.
751  ///
752  /// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code
753  /// whenever you want to change your product name.
754  #[serde(alias = "upgrade-code")]
755  pub upgrade_code: Option<uuid::Uuid>,
756  /// The installer languages to build. See <https://docs.microsoft.com/en-us/windows/win32/msi/localizing-the-error-and-actiontext-tables>.
757  #[serde(default)]
758  pub language: WixLanguage,
759  /// A custom .wxs template to use.
760  pub template: Option<PathBuf>,
761  /// A list of paths to .wxs files with WiX fragments to use.
762  #[serde(default, alias = "fragment-paths")]
763  pub fragment_paths: Vec<PathBuf>,
764  /// The ComponentGroup element ids you want to reference from the fragments.
765  #[serde(default, alias = "component-group-refs")]
766  pub component_group_refs: Vec<String>,
767  /// The Component element ids you want to reference from the fragments.
768  #[serde(default, alias = "component-refs")]
769  pub component_refs: Vec<String>,
770  /// The FeatureGroup element ids you want to reference from the fragments.
771  #[serde(default, alias = "feature-group-refs")]
772  pub feature_group_refs: Vec<String>,
773  /// The Feature element ids you want to reference from the fragments.
774  #[serde(default, alias = "feature-refs")]
775  pub feature_refs: Vec<String>,
776  /// The Merge element ids you want to reference from the fragments.
777  #[serde(default, alias = "merge-refs")]
778  pub merge_refs: Vec<String>,
779  /// Create an elevated update task within Windows Task Scheduler.
780  #[serde(default, alias = "enable-elevated-update-task")]
781  pub enable_elevated_update_task: bool,
782  /// Path to a bitmap file to use as the installation user interface banner.
783  /// This bitmap will appear at the top of all but the first page of the installer.
784  ///
785  /// The required dimensions are 493px × 58px.
786  #[serde(alias = "banner-path")]
787  pub banner_path: Option<PathBuf>,
788  /// Path to a bitmap file to use on the installation user interface dialogs.
789  /// It is used on the welcome and completion dialogs.
790  ///
791  /// The required dimensions are 493px × 312px.
792  #[serde(alias = "dialog-image-path")]
793  pub dialog_image_path: Option<PathBuf>,
794  /// Enables FIPS compliant algorithms.
795  /// Can also be enabled via the `TAURI_BUNDLER_WIX_FIPS_COMPLIANT` env var.
796  #[serde(default, alias = "fips-compliant")]
797  pub fips_compliant: bool,
798}
799
800/// Compression algorithms used in the NSIS installer.
801///
802/// See <https://nsis.sourceforge.io/Reference/SetCompressor>
803#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
804#[cfg_attr(feature = "schema", derive(JsonSchema))]
805#[serde(rename_all = "camelCase", deny_unknown_fields)]
806pub enum NsisCompression {
807  /// ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.
808  Zlib,
809  /// BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.
810  Bzip2,
811  /// LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB.
812  #[default]
813  Lzma,
814  /// Disable compression
815  None,
816}
817
818/// Install Modes for the NSIS installer.
819#[derive(Default, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase", deny_unknown_fields)]
821#[cfg_attr(feature = "schema", derive(JsonSchema))]
822pub enum NSISInstallerMode {
823  /// Default mode for the installer.
824  ///
825  /// Install the app by default in a directory that doesn't require Administrator access.
826  ///
827  /// Installer metadata will be saved under the `HKCU` registry path.
828  #[default]
829  CurrentUser,
830  /// Install the app by default in the `Program Files` folder directory requires Administrator
831  /// access for the installation.
832  ///
833  /// Installer metadata will be saved under the `HKLM` registry path.
834  PerMachine,
835  /// Combines both modes and allows the user to choose at install time
836  /// whether to install for the current user or per machine. Note that this mode
837  /// will require Administrator access even if the user wants to install it for the current user only.
838  ///
839  /// Installer metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.
840  Both,
841}
842
843/// Configuration for the Installer bundle using NSIS.
844#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
845#[cfg_attr(feature = "schema", derive(JsonSchema))]
846#[serde(rename_all = "camelCase", deny_unknown_fields)]
847pub struct NsisConfig {
848  /// A custom .nsi template to use.
849  pub template: Option<PathBuf>,
850  /// The path to a bitmap file to display on the header of installers pages.
851  ///
852  /// The recommended dimensions are 150px x 57px.
853  #[serde(alias = "header-image")]
854  pub header_image: Option<PathBuf>,
855  /// The path to a bitmap file for the Welcome page and the Finish page.
856  ///
857  /// The recommended dimensions are 164px x 314px.
858  #[serde(alias = "sidebar-image")]
859  pub sidebar_image: Option<PathBuf>,
860  // TODO: Change the alias to installer-icon in v3
861  /// The path to an icon file used as the installer icon.
862  #[serde(alias = "install-icon")]
863  pub installer_icon: Option<PathBuf>,
864  /// The path to an icon file used as the uninstaller icon.
865  #[serde(alias = "uninstaller-icon")]
866  pub uninstaller_icon: Option<PathBuf>,
867  /// The path to a bitmap file to display on the header of uninstallers pages.
868  /// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`
869  ///
870  /// The recommended dimensions are 150px x 57px.
871  #[serde(alias = "uninstaller-header-image")]
872  pub uninstaller_header_image: Option<PathBuf>,
873  /// Whether the installation will be for all users or just the current user.
874  #[serde(default, alias = "install-mode")]
875  pub install_mode: NSISInstallerMode,
876  /// A list of installer languages. Default to `["English"]` if not set.
877  ///
878  /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used.
879  /// To allow the user to select the language, set `display_language_selector` to `true`.
880  ///
881  /// See <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files> for the complete list of languages.
882  pub languages: Option<Vec<String>>,
883  /// A key-value pair where the key is the language and the
884  /// value is the path to a custom `.nsh` file that holds the translated text for tauri's custom messages.
885  ///
886  /// See <https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-bundler/src/bundle/windows/nsis/languages/English.nsh> for an example `.nsh` file.
887  ///
888  /// **Note**: the key must be a valid NSIS language and it must be added to the [`Self::languages`] array,
889  pub custom_language_files: Option<HashMap<String, PathBuf>>,
890  /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not.
891  /// By default the OS language is selected, with a fallback to the first language in the `languages` array.
892  #[serde(default, alias = "display-language-selector")]
893  pub display_language_selector: bool,
894  /// Set the compression algorithm used to compress files in the installer.
895  ///
896  /// See <https://nsis.sourceforge.io/Reference/SetCompressor>
897  #[serde(default)]
898  pub compression: NsisCompression,
899  /// Set the folder name for the start menu shortcut.
900  ///
901  /// Use this option if you have multiple apps and wish to group their shortcuts under one folder
902  /// or if you generally prefer to set your shortcut inside a folder.
903  ///
904  /// Examples:
905  /// - `AwesomePublisher`, shortcut will be placed in `%AppData%\Microsoft\Windows\Start Menu\Programs\AwesomePublisher\<your-app>.lnk`
906  /// - If unset, shortcut will be placed in `%AppData%\Microsoft\Windows\Start Menu\Programs\<your-app>.lnk`
907  #[serde(alias = "start-menu-folder")]
908  pub start_menu_folder: Option<String>,
909  /// A path to a `.nsh` file that contains special NSIS macros to be hooked into the
910  /// main installer.nsi script.
911  ///
912  /// Supported hooks are:
913  ///
914  /// - `NSIS_HOOK_PREINSTALL`: This hook runs before copying files, setting registry key values and creating shortcuts.
915  /// - `NSIS_HOOK_POSTINSTALL`: This hook runs after the installer has finished copying all files, setting the registry keys and created shortcuts.
916  /// - `NSIS_HOOK_PREUNINSTALL`: This hook runs before removing any files, registry keys and shortcuts.
917  /// - `NSIS_HOOK_POSTUNINSTALL`: This hook runs after files, registry keys and shortcuts have been removed.
918  ///
919  /// ### Example
920  ///
921  /// ```nsh
922  /// !macro NSIS_HOOK_PREINSTALL
923  ///   MessageBox MB_OK "PreInstall"
924  /// !macroend
925  ///
926  /// !macro NSIS_HOOK_POSTINSTALL
927  ///   MessageBox MB_OK "PostInstall"
928  /// !macroend
929  ///
930  /// !macro NSIS_HOOK_PREUNINSTALL
931  ///   MessageBox MB_OK "PreUnInstall"
932  /// !macroend
933  ///
934  /// !macro NSIS_HOOK_POSTUNINSTALL
935  ///   MessageBox MB_OK "PostUninstall"
936  /// !macroend
937  /// ```
938  #[serde(alias = "installer-hooks")]
939  pub installer_hooks: Option<PathBuf>,
940  /// Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle >  windows > minimumWebview2Version`) instead.
941  ///
942  /// Try to ensure that the WebView2 version is equal to or newer than this version,
943  /// if the user's WebView2 is older than this version,
944  /// the installer will try to trigger a WebView2 update.
945  #[deprecated(
946    since = "2.10.0",
947    note = "Use `WindowsConfig::minimum_webview2_version` instead."
948  )]
949  #[serde(alias = "minimum-webview2-version")]
950  pub minimum_webview2_version: Option<String>,
951}
952
953/// Install modes for the Webview2 runtime.
954/// Note that for the updater bundle [`Self::DownloadBootstrapper`] is used.
955///
956/// For more information see <https://v2.tauri.app/distribute/windows-installer/#webview2-installation-options>.
957#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
958#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
959#[cfg_attr(feature = "schema", derive(JsonSchema))]
960pub enum WebviewInstallMode {
961  /// Do not install the Webview2 as part of the Windows Installer.
962  Skip,
963  /// Download the bootstrapper and run it.
964  /// Requires an internet connection.
965  /// Results in a smaller installer size, but is not recommended on Windows 7.
966  DownloadBootstrapper {
967    /// Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.
968    #[serde(default = "default_true")]
969    silent: bool,
970  },
971  /// Embed the bootstrapper and run it.
972  /// Requires an internet connection.
973  /// Increases the installer size by around 1.8MB, but offers better support on Windows 7.
974  EmbedBootstrapper {
975    /// Instructs the installer to run the bootstrapper in silent mode. Defaults to `true`.
976    #[serde(default = "default_true")]
977    silent: bool,
978  },
979  /// Embed the offline installer and run it.
980  /// Does not require an internet connection.
981  /// Increases the installer size by around 127MB.
982  OfflineInstaller {
983    /// Instructs the installer to run the installer in silent mode. Defaults to `true`.
984    #[serde(default = "default_true")]
985    silent: bool,
986  },
987  /// Embed a fixed webview2 version and use it at runtime.
988  /// Increases the installer size by around 180MB.
989  FixedRuntime {
990    /// The path to the fixed runtime to use.
991    ///
992    /// The fixed version can be downloaded [on the official website](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section).
993    /// The `.cab` file must be extracted to a folder and this folder path must be defined on this field.
994    path: PathBuf,
995  },
996}
997
998impl Default for WebviewInstallMode {
999  fn default() -> Self {
1000    Self::DownloadBootstrapper { silent: true }
1001  }
1002}
1003
1004/// Custom Signing Command configuration.
1005#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1006#[cfg_attr(feature = "schema", derive(JsonSchema))]
1007#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1008pub enum CustomSignCommandConfig {
1009  /// A string notation of the script to execute.
1010  ///
1011  /// "%1" will be replaced with the path to the binary to be signed.
1012  ///
1013  /// This is a simpler notation for the command.
1014  /// Tauri will split the string with `' '` and use the first element as the command name and the rest as arguments.
1015  ///
1016  /// If you need to use whitespace in the command or arguments, use the object notation [`Self::CommandWithOptions`].
1017  Command(String),
1018  /// An object notation of the command.
1019  ///
1020  /// This is more complex notation for the command but
1021  /// this allows you to use whitespace in the command and arguments.
1022  CommandWithOptions {
1023    /// The command to run to sign the binary.
1024    cmd: String,
1025    /// The arguments to pass to the command.
1026    ///
1027    /// "%1" will be replaced with the path to the binary to be signed.
1028    args: Vec<String>,
1029  },
1030}
1031
1032/// Windows bundler configuration.
1033///
1034/// See more: <https://v2.tauri.app/reference/config/#windowsconfig>
1035#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1036#[cfg_attr(feature = "schema", derive(JsonSchema))]
1037#[serde(rename_all = "camelCase", deny_unknown_fields)]
1038pub struct WindowsConfig {
1039  /// Specifies the file digest algorithm to use for creating file signatures.
1040  /// Required for code signing. SHA-256 is recommended.
1041  #[serde(alias = "digest-algorithm")]
1042  pub digest_algorithm: Option<String>,
1043  /// Specifies the SHA1 hash of the signing certificate.
1044  #[serde(alias = "certificate-thumbprint")]
1045  pub certificate_thumbprint: Option<String>,
1046  /// Server to use during timestamping.
1047  #[serde(alias = "timestamp-url")]
1048  pub timestamp_url: Option<String>,
1049  /// Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may
1050  /// use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.
1051  #[serde(default)]
1052  pub tsp: bool,
1053  /// The installation mode for the Webview2 runtime.
1054  #[serde(default, alias = "webview-install-mode")]
1055  pub webview_install_mode: WebviewInstallMode,
1056  /// Validates a second app installation, blocking the user from installing an older version if set to `false`.
1057  ///
1058  /// For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.
1059  ///
1060  /// The default value of this flag is `true`.
1061  #[serde(default = "default_true", alias = "allow-downgrades")]
1062  pub allow_downgrades: bool,
1063  /// Try to ensure that the WebView2 version is equal to or newer than this version,
1064  /// if the user's WebView2 is older than this version,
1065  /// the installer will try to trigger a WebView2 update.
1066  #[serde(alias = "minimum-webview2-version")]
1067  pub minimum_webview2_version: Option<String>,
1068  /// Configuration for the MSI generated with WiX.
1069  pub wix: Option<WixConfig>,
1070  /// Configuration for the installer generated with NSIS.
1071  pub nsis: Option<NsisConfig>,
1072  /// Specify a custom command to sign the binaries.
1073  /// This command needs to have a `%1` in args which is just a placeholder for the binary path,
1074  /// which we will detect and replace before calling the command.
1075  ///
1076  /// By Default we use `signtool.exe` which can be found only on Windows so
1077  /// if you are on another platform and want to cross-compile and sign you will
1078  /// need to use another tool like `osslsigncode`.
1079  #[serde(alias = "sign-command")]
1080  pub sign_command: Option<CustomSignCommandConfig>,
1081}
1082
1083impl Default for WindowsConfig {
1084  fn default() -> Self {
1085    Self {
1086      digest_algorithm: None,
1087      certificate_thumbprint: None,
1088      timestamp_url: None,
1089      tsp: false,
1090      webview_install_mode: Default::default(),
1091      allow_downgrades: true,
1092      minimum_webview2_version: None,
1093      wix: None,
1094      nsis: None,
1095      sign_command: None,
1096    }
1097  }
1098}
1099
1100/// macOS-only. Corresponds to CFBundleTypeRole
1101#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1102#[cfg_attr(feature = "schema", derive(JsonSchema))]
1103pub enum BundleTypeRole {
1104  /// CFBundleTypeRole.Editor. Files can be read and edited.
1105  #[default]
1106  Editor,
1107  /// CFBundleTypeRole.Viewer. Files can be read.
1108  Viewer,
1109  /// CFBundleTypeRole.Shell
1110  Shell,
1111  /// CFBundleTypeRole.QLGenerator
1112  QLGenerator,
1113  /// CFBundleTypeRole.None
1114  None,
1115}
1116
1117impl Display for BundleTypeRole {
1118  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1119    match self {
1120      Self::Editor => write!(f, "Editor"),
1121      Self::Viewer => write!(f, "Viewer"),
1122      Self::Shell => write!(f, "Shell"),
1123      Self::QLGenerator => write!(f, "QLGenerator"),
1124      Self::None => write!(f, "None"),
1125    }
1126  }
1127}
1128
1129// Issue #13159 - Missing the LSHandlerRank and Apple warns after uploading to App Store Connect.
1130// https://github.com/tauri-apps/tauri/issues/13159
1131/// Corresponds to LSHandlerRank
1132#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1133#[cfg_attr(feature = "schema", derive(JsonSchema))]
1134pub enum HandlerRank {
1135  /// LSHandlerRank.Default. This app is an opener of files of this type; this value is also used if no rank is specified.
1136  #[default]
1137  Default,
1138  /// LSHandlerRank.Owner. This app is the primary creator of files of this type.
1139  Owner,
1140  /// LSHandlerRank.Alternate. This app is a secondary viewer of files of this type.
1141  Alternate,
1142  /// LSHandlerRank.None. This app is never selected to open files of this type, but it accepts drops of files of this type.
1143  None,
1144}
1145
1146impl Display for HandlerRank {
1147  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1148    match self {
1149      Self::Default => write!(f, "Default"),
1150      Self::Owner => write!(f, "Owner"),
1151      Self::Alternate => write!(f, "Alternate"),
1152      Self::None => write!(f, "None"),
1153    }
1154  }
1155}
1156
1157/// An extension for a [`FileAssociation`].
1158///
1159/// A leading `.` is automatically stripped.
1160#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
1161#[cfg_attr(feature = "schema", derive(JsonSchema))]
1162pub struct AssociationExt(pub String);
1163
1164impl fmt::Display for AssociationExt {
1165  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1166    write!(f, "{}", self.0)
1167  }
1168}
1169
1170impl<'d> serde::Deserialize<'d> for AssociationExt {
1171  fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
1172    let ext = String::deserialize(deserializer)?;
1173    if let Some(ext) = ext.strip_prefix('.') {
1174      Ok(AssociationExt(ext.into()))
1175    } else {
1176      Ok(AssociationExt(ext))
1177    }
1178  }
1179}
1180
1181/// File association
1182#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1183#[cfg_attr(feature = "schema", derive(JsonSchema))]
1184#[serde(rename_all = "camelCase", deny_unknown_fields)]
1185pub struct FileAssociation {
1186  /// File extensions to associate with this app. e.g. 'png'
1187  pub ext: Vec<AssociationExt>,
1188  /// Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.
1189  ///
1190  /// This allows supporting any file format declared by another application that conforms to this type.
1191  /// Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].
1192  #[serde(alias = "content-types")]
1193  pub content_types: Option<Vec<String>>,
1194  /// The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`
1195  pub name: Option<String>,
1196  /// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.
1197  pub description: Option<String>,
1198  /// The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS.
1199  #[serde(default)]
1200  pub role: BundleTypeRole,
1201  /// The mime-type of the association, e.g. `'image/png'` or `'text/plain'`.
1202  ///
1203  /// - **Linux**: written as `MimeType=` in the `.desktop` file.
1204  /// - **macOS / iOS**: added as `public.mime-type` in the `UTTypeTagSpecification` dictionary of
1205  ///   the `UTExportedTypeDeclarations` entry in `Info.plist`.
1206  /// - **Android**: used as `android:mimeType` in the `<data>` element of an `<intent-filter>`
1207  ///   in `AndroidManifest.xml`.
1208  #[serde(alias = "mime-type")]
1209  pub mime_type: Option<String>,
1210  /// The ranking of this app among apps that declare themselves as editors or viewers of the given file type.  Maps to `LSHandlerRank` on macOS.
1211  #[serde(default)]
1212  pub rank: HandlerRank,
1213  /// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
1214  ///
1215  /// You should define this if the associated file is a custom file type defined by your application.
1216  pub exported_type: Option<ExportedFileAssociation>,
1217  /// Intent action filters for this file association.
1218  ///
1219  /// By default all filters are used.
1220  #[serde(alias = "android-intent-action-filters")]
1221  pub android_intent_action_filters: Option<Vec<AndroidIntentAction>>,
1222}
1223
1224/// Android intent action.
1225#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Hash)]
1226#[cfg_attr(feature = "schema", derive(JsonSchema))]
1227#[serde(rename_all = "camelCase")]
1228#[non_exhaustive]
1229pub enum AndroidIntentAction {
1230  /// ACTION_SEND.
1231  ///
1232  /// <https://developer.android.com/reference/android/content/Intent#ACTION_SEND>
1233  Send,
1234  /// ACTION_SEND_MULTIPLE.
1235  ///
1236  /// <https://developer.android.com/reference/android/content/Intent#ACTION_SEND_MULTIPLE>
1237  SendMultiple,
1238  /// ACTION_VIEW.
1239  ///
1240  /// <https://developer.android.com/reference/android/content/Intent#ACTION_SEND>
1241  View,
1242}
1243
1244/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
1245#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1246#[cfg_attr(feature = "schema", derive(JsonSchema))]
1247#[serde(rename_all = "camelCase", deny_unknown_fields)]
1248pub struct ExportedFileAssociation {
1249  /// The unique identifier for the exported type. Maps to `UTTypeIdentifier`.
1250  pub identifier: String,
1251  /// The types that this type conforms to. Maps to `UTTypeConformsTo`.
1252  ///
1253  /// Examples are `public.data`, `public.image`, `public.json` and `public.database`.
1254  #[serde(alias = "conforms-to")]
1255  pub conforms_to: Option<Vec<String>>,
1256}
1257
1258impl FileAssociation {
1259  /// Infers UTIs (Uniform Type Identifiers) from file extensions and mime types.
1260  /// This is useful for macOS and iOS to automatically populate `LSItemContentTypes`
1261  /// in the Info.plist for share sheet and file association support.
1262  ///
1263  /// Returns a vector of UTIs that should be included in `LSItemContentTypes`.
1264  /// Explicitly provided content types are included first, followed by inferred types.
1265  pub fn infer_content_types(&self) -> HashSet<String> {
1266    let mut content_types = HashSet::new();
1267
1268    // when we have an exported type, we only reference it
1269    if let Some(exported_type) = &self.exported_type {
1270      content_types.insert(exported_type.identifier.clone());
1271      return content_types;
1272    }
1273
1274    // Start with explicitly provided content types
1275    if let Some(explicit_types) = &self.content_types {
1276      content_types.extend(explicit_types.iter().cloned());
1277    }
1278
1279    // Infer from extensions and add to content_types (avoiding duplicates)
1280    for ext in &self.ext {
1281      if let Some(uti) = extension_to_uti(&ext.0) {
1282        content_types.insert(uti.to_string());
1283      }
1284    }
1285
1286    // Also infer from mime type if available (avoiding duplicates)
1287    if let Some(mime_type) = &self.mime_type {
1288      if let Some(uti) = mime_type_to_uti(mime_type) {
1289        content_types.insert(uti.to_string());
1290      }
1291    }
1292
1293    content_types
1294  }
1295}
1296
1297/// Generates plist dictionary entries for file associations.
1298/// This is used by both macOS and iOS bundlers to populate Info.plist.
1299///
1300/// Returns a plist dictionary containing `UTExportedTypeDeclarations` and `CFBundleDocumentTypes`
1301/// if there are any file associations configured.
1302pub fn file_associations_plist(associations: &[FileAssociation]) -> Option<plist::Value> {
1303  use plist::{Dictionary, Value};
1304
1305  if associations.is_empty() {
1306    return None;
1307  }
1308
1309  let exported_associations = associations
1310    .iter()
1311    .filter_map(|association| {
1312      association.exported_type.as_ref().map(|exported_type| {
1313        let mut dict = Dictionary::new();
1314
1315        dict.insert(
1316          "UTTypeIdentifier".into(),
1317          exported_type.identifier.clone().into(),
1318        );
1319        if let Some(description) = &association.description {
1320          dict.insert("UTTypeDescription".into(), description.clone().into());
1321        }
1322        if let Some(conforms_to) = &exported_type.conforms_to {
1323          dict.insert(
1324            "UTTypeConformsTo".into(),
1325            Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
1326          );
1327        }
1328
1329        let mut specification = Dictionary::new();
1330        specification.insert(
1331          "public.filename-extension".into(),
1332          Value::Array(
1333            association
1334              .ext
1335              .iter()
1336              .map(|s| s.to_string().into())
1337              .collect(),
1338          ),
1339        );
1340        if let Some(mime_type) = &association.mime_type {
1341          specification.insert("public.mime-type".into(), mime_type.clone().into());
1342        }
1343
1344        dict.insert("UTTypeTagSpecification".into(), specification.into());
1345
1346        Value::Dictionary(dict)
1347      })
1348    })
1349    .collect::<Vec<_>>();
1350
1351  let document_types = associations
1352    .iter()
1353    .map(|association| {
1354      let mut dict = Dictionary::new();
1355
1356      if !association.ext.is_empty() {
1357        dict.insert(
1358          "CFBundleTypeExtensions".into(),
1359          Value::Array(
1360            association
1361              .ext
1362              .iter()
1363              .map(|ext| ext.to_string().into())
1364              .collect(),
1365          ),
1366        );
1367      }
1368
1369      // For macOS/iOS share sheet, we need LSItemContentTypes with standard UTIs
1370      let content_types = association.infer_content_types();
1371
1372      // Add LSItemContentTypes if we have any content types
1373      if !content_types.is_empty() {
1374        dict.insert(
1375          "LSItemContentTypes".into(),
1376          Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
1377        );
1378      }
1379
1380      let type_name = association
1381        .name
1382        .clone()
1383        .or_else(|| association.ext.first().map(|ext| ext.0.clone()))
1384        .unwrap_or_default();
1385      dict.insert("CFBundleTypeName".into(), type_name.into());
1386      dict.insert(
1387        "CFBundleTypeRole".into(),
1388        association.role.to_string().into(),
1389      );
1390      dict.insert("LSHandlerRank".into(), association.rank.to_string().into());
1391
1392      Value::Dictionary(dict)
1393    })
1394    .collect::<Vec<_>>();
1395
1396  if exported_associations.is_empty() && document_types.is_empty() {
1397    return None;
1398  }
1399
1400  let mut plist = Dictionary::new();
1401  if !exported_associations.is_empty() {
1402    plist.insert(
1403      "UTExportedTypeDeclarations".into(),
1404      Value::Array(exported_associations),
1405    );
1406  }
1407  if !document_types.is_empty() {
1408    plist.insert("CFBundleDocumentTypes".into(), Value::Array(document_types));
1409  }
1410
1411  Some(Value::Dictionary(plist))
1412}
1413
1414/// Maps file extensions to their standard UTIs for macOS/iOS share sheet support
1415fn extension_to_uti(ext: &str) -> Option<&'static str> {
1416  match ext.to_lowercase().as_str() {
1417    // Images
1418    "png" => Some("public.png"),
1419    "jpg" | "jpeg" => Some("public.jpeg"),
1420    "gif" => Some("com.compuserve.gif"),
1421    "bmp" => Some("com.microsoft.bmp"),
1422    "tiff" | "tif" => Some("public.tiff"),
1423    "ico" => Some("com.microsoft.ico"),
1424    "heic" | "heif" => Some("public.heif-standard-image"),
1425    "webp" => Some("org.webmproject.webp"),
1426    "svg" => Some("public.svg-image"),
1427    // Videos
1428    "mp4" => Some("public.mpeg-4"),
1429    "mov" => Some("com.apple.quicktime-movie"),
1430    "avi" => Some("public.avi"),
1431    "mkv" => Some("public.mpeg-4"),
1432    // Audio
1433    "mp3" => Some("public.mp3"),
1434    "wav" => Some("com.microsoft.waveform-audio"),
1435    "aac" => Some("public.aac-audio"),
1436    "m4a" => Some("public.mpeg-4-audio"),
1437    // Documents
1438    "pdf" => Some("com.adobe.pdf"),
1439    "txt" => Some("public.plain-text"),
1440    "rtf" => Some("public.rtf"),
1441    "html" | "htm" => Some("public.html"),
1442    "json" => Some("public.json"),
1443    "xml" => Some("public.xml"),
1444    _ => None,
1445  }
1446}
1447
1448/// Infers UTIs from mime type
1449fn mime_type_to_uti(mime_type: &str) -> Option<&'static str> {
1450  match mime_type {
1451    "image/png" => Some("public.png"),
1452    "image/jpeg" | "image/jpg" => Some("public.jpeg"),
1453    "image/gif" => Some("com.compuserve.gif"),
1454    "image/bmp" => Some("com.microsoft.bmp"),
1455    "image/tiff" => Some("public.tiff"),
1456    "image/heic" | "image/heif" => Some("public.heif-standard-image"),
1457    "image/webp" => Some("org.webmproject.webp"),
1458    "image/svg+xml" => Some("public.svg-image"),
1459    mime if mime.starts_with("image/") => Some("public.image"),
1460    "video/mp4" => Some("public.mpeg-4"),
1461    "video/quicktime" => Some("com.apple.quicktime-movie"),
1462    "video/x-msvideo" => Some("public.avi"),
1463    mime if mime.starts_with("video/") => Some("public.movie"),
1464    "audio/mpeg" | "audio/mp3" => Some("public.mp3"),
1465    "audio/wav" | "audio/wave" => Some("com.microsoft.waveform-audio"),
1466    "audio/aac" => Some("public.aac-audio"),
1467    "audio/mp4" => Some("public.mpeg-4-audio"),
1468    mime if mime.starts_with("audio/") => Some("public.audio"),
1469    "application/pdf" => Some("com.adobe.pdf"),
1470    "text/plain" => Some("public.plain-text"),
1471    "text/rtf" => Some("public.rtf"),
1472    "text/html" => Some("public.html"),
1473    "application/json" => Some("public.json"),
1474    "application/xml" | "text/xml" => Some("public.xml"),
1475    _ => None,
1476  }
1477}
1478
1479/// Deep link protocol configuration.
1480#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1481#[cfg_attr(feature = "schema", derive(JsonSchema))]
1482#[serde(rename_all = "camelCase", deny_unknown_fields)]
1483pub struct DeepLinkProtocol {
1484  /// URL schemes to associate with this app without `://`. For example `my-app`
1485  #[serde(default)]
1486  pub schemes: Vec<String>,
1487  /// Domains to associate with this app. For example `example.com`.
1488  /// Currently only supported on macOS, translating to an [universal app link].
1489  ///
1490  /// Note that universal app links require signed apps with a provisioning profile to work.
1491  /// You can accomplish that by including the `embedded.provisionprofile` file in the `macOS > files` option.
1492  ///
1493  /// [universal app link]: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
1494  #[serde(default)]
1495  pub domains: Vec<String>,
1496  /// The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `<bundle-id>.<schemes[0]>`
1497  pub name: Option<String>,
1498  /// The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.
1499  #[serde(default)]
1500  pub role: BundleTypeRole,
1501}
1502
1503/// Definition for bundle resources.
1504/// Can be either a list of paths to include or a map of source to target paths.
1505#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1506#[cfg_attr(feature = "schema", derive(JsonSchema))]
1507#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1508pub enum BundleResources {
1509  /// A list of paths to include.
1510  List(Vec<String>),
1511  /// A map of source to target paths.
1512  Map(HashMap<String, String>),
1513}
1514
1515impl BundleResources {
1516  /// Adds a path to the resource collection.
1517  pub fn push(&mut self, path: impl Into<String>) {
1518    match self {
1519      Self::List(l) => l.push(path.into()),
1520      Self::Map(l) => {
1521        let path = path.into();
1522        l.insert(path.clone(), path);
1523      }
1524    }
1525  }
1526}
1527
1528/// Updater type
1529#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1530#[cfg_attr(feature = "schema", derive(JsonSchema))]
1531#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1532pub enum Updater {
1533  /// Generates legacy zipped v1 compatible updaters
1534  String(V1Compatible),
1535  /// Produce updaters and their signatures or not
1536  // Can't use untagged on enum field here: https://github.com/GREsau/schemars/issues/222
1537  Bool(bool),
1538}
1539
1540impl Default for Updater {
1541  fn default() -> Self {
1542    Self::Bool(false)
1543  }
1544}
1545
1546/// Generates legacy zipped v1 compatible updaters
1547#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1548#[cfg_attr(feature = "schema", derive(JsonSchema))]
1549#[serde(rename_all = "camelCase", deny_unknown_fields)]
1550pub enum V1Compatible {
1551  /// Generates legacy zipped v1 compatible updaters
1552  V1Compatible,
1553}
1554
1555/// Configuration for tauri-bundler.
1556///
1557/// See more: <https://v2.tauri.app/reference/config/#bundleconfig>
1558#[skip_serializing_none]
1559#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1560#[cfg_attr(feature = "schema", derive(JsonSchema))]
1561#[serde(rename_all = "camelCase", deny_unknown_fields)]
1562pub struct BundleConfig {
1563  /// Whether Tauri should bundle your application or just output the executable.
1564  #[serde(default)]
1565  pub active: bool,
1566  /// The bundle targets, currently supports ["deb", "rpm", "appimage", "nsis", "msi", "app", "dmg"] or "all".
1567  #[serde(default)]
1568  pub targets: BundleTarget,
1569  #[serde(default)]
1570  /// Produce updaters and their signatures or not
1571  pub create_updater_artifacts: Updater,
1572  /// The application's publisher. Defaults to the second element in the identifier string.
1573  ///
1574  /// Currently maps to the Manufacturer property of the Windows Installer
1575  /// and the Maintainer field of debian packages if the Cargo.toml does not have the authors field.
1576  pub publisher: Option<String>,
1577  /// A url to the home page of your application. If unset, will
1578  /// fallback to `homepage` defined in `Cargo.toml`.
1579  ///
1580  /// Supported bundle targets: `deb`, `rpm`, `nsis` and `msi`.
1581  pub homepage: Option<String>,
1582  /// The app's icons
1583  #[serde(default)]
1584  pub icon: Vec<String>,
1585  /// App resources to bundle.
1586  /// Each resource is a path to a file or directory.
1587  /// Glob patterns are supported.
1588  ///
1589  /// ## Examples
1590  ///
1591  /// To include a list of files:
1592  ///
1593  /// ```json
1594  /// {
1595  ///   "bundle": {
1596  ///     "resources": [
1597  ///       "./path/to/some-file.txt",
1598  ///       "/absolute/path/to/textfile.txt",
1599  ///       "../relative/path/to/jsonfile.json",
1600  ///       "some-folder/",
1601  ///       "resources/**/*.md"
1602  ///     ]
1603  ///   }
1604  /// }
1605  /// ```
1606  ///
1607  /// The bundled files will be in `$RESOURCES/` with the original directory structure preserved,
1608  /// for example: `./path/to/some-file.txt` -> `$RESOURCE/path/to/some-file.txt`
1609  ///
1610  /// To fine control where the files will get copied to, use a map instead
1611  ///
1612  /// ```json
1613  /// {
1614  ///   "bundle": {
1615  ///     "resources": {
1616  ///       "/absolute/path/to/textfile.txt": "resources/textfile.txt",
1617  ///       "relative/path/to/jsonfile.json": "resources/jsonfile.json",
1618  ///       "resources/": "",
1619  ///       "docs/**/*md": "website-docs/"
1620  ///     }
1621  ///   }
1622  /// }
1623  /// ```
1624  ///
1625  /// Note that when using glob pattern in this case, the original directory structure is not preserved,
1626  /// everything gets copied to the target directory directly
1627  ///
1628  /// See more: <https://v2.tauri.app/develop/resources/>
1629  pub resources: Option<BundleResources>,
1630  /// A copyright string associated with your application.
1631  pub copyright: Option<String>,
1632  /// The package's license identifier to be included in the appropriate bundles.
1633  /// If not set, defaults to the license from the Cargo.toml file.
1634  pub license: Option<String>,
1635  /// The path to the license file to be included in the appropriate bundles.
1636  #[serde(alias = "license-file")]
1637  pub license_file: Option<PathBuf>,
1638  /// The application kind.
1639  ///
1640  /// Should be one of the following:
1641  /// Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather.
1642  pub category: Option<String>,
1643  /// File types to associate with the application.
1644  pub file_associations: Option<Vec<FileAssociation>>,
1645  /// A short description of your application.
1646  #[serde(alias = "short-description")]
1647  pub short_description: Option<String>,
1648  /// A longer, multi-line description of the application.
1649  #[serde(alias = "long-description")]
1650  pub long_description: Option<String>,
1651  /// Whether to use the project's `target` directory, for caching build tools (e.g., Wix and NSIS) when building this application. Defaults to `false`.
1652  ///
1653  /// If true, tools will be cached in `target/.tauri/`.
1654  /// If false, tools will be cached in the current user's platform-specific cache directory.
1655  ///
1656  /// An example where it can be appropriate to set this to `true` is when building this application as a Windows System user (e.g., AWS EC2 workloads),
1657  /// because the Window system's app data directory is restricted.
1658  #[serde(default, alias = "use-local-tools-dir")]
1659  pub use_local_tools_dir: bool,
1660  /// A list of—either absolute or relative—paths to binaries to embed with your application.
1661  ///
1662  /// Note that Tauri will look for system-specific binaries following the pattern "binary-name{-target-triple}{.system-extension}".
1663  ///
1664  /// E.g. for the external binary "my-binary", Tauri looks for:
1665  ///
1666  /// - "my-binary-x86_64-pc-windows-msvc.exe" for Windows
1667  /// - "my-binary-x86_64-apple-darwin" for macOS
1668  /// - "my-binary-x86_64-unknown-linux-gnu" for Linux
1669  ///
1670  /// so don't forget to provide binaries for all targeted platforms.
1671  #[serde(alias = "external-bin")]
1672  pub external_bin: Option<Vec<String>>,
1673  /// Configuration for the Windows bundles.
1674  #[serde(default)]
1675  pub windows: WindowsConfig,
1676  /// Configuration for the Linux bundles.
1677  #[serde(default)]
1678  pub linux: LinuxConfig,
1679  /// Configuration for the macOS bundles.
1680  #[serde(rename = "macOS", alias = "macos", default)]
1681  pub macos: MacConfig,
1682  /// iOS configuration.
1683  #[serde(rename = "iOS", alias = "ios", default)]
1684  pub ios: IosConfig,
1685  /// Android configuration.
1686  #[serde(default)]
1687  pub android: AndroidConfig,
1688}
1689
1690/// A tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.
1691#[derive(Debug, PartialEq, Eq, Serialize, Default, Clone, Copy)]
1692#[serde(rename_all = "camelCase", deny_unknown_fields)]
1693pub struct Color(pub u8, pub u8, pub u8, pub u8);
1694
1695impl From<Color> for (u8, u8, u8, u8) {
1696  fn from(value: Color) -> Self {
1697    (value.0, value.1, value.2, value.3)
1698  }
1699}
1700
1701impl From<Color> for (u8, u8, u8) {
1702  fn from(value: Color) -> Self {
1703    (value.0, value.1, value.2)
1704  }
1705}
1706
1707impl From<(u8, u8, u8, u8)> for Color {
1708  fn from(value: (u8, u8, u8, u8)) -> Self {
1709    Color(value.0, value.1, value.2, value.3)
1710  }
1711}
1712
1713impl From<(u8, u8, u8)> for Color {
1714  fn from(value: (u8, u8, u8)) -> Self {
1715    Color(value.0, value.1, value.2, 255)
1716  }
1717}
1718
1719impl From<Color> for [u8; 4] {
1720  fn from(value: Color) -> Self {
1721    [value.0, value.1, value.2, value.3]
1722  }
1723}
1724
1725impl From<Color> for [u8; 3] {
1726  fn from(value: Color) -> Self {
1727    [value.0, value.1, value.2]
1728  }
1729}
1730
1731impl From<[u8; 4]> for Color {
1732  fn from(value: [u8; 4]) -> Self {
1733    Color(value[0], value[1], value[2], value[3])
1734  }
1735}
1736
1737impl From<[u8; 3]> for Color {
1738  fn from(value: [u8; 3]) -> Self {
1739    Color(value[0], value[1], value[2], 255)
1740  }
1741}
1742
1743impl FromStr for Color {
1744  type Err = String;
1745  fn from_str(mut color: &str) -> Result<Self, Self::Err> {
1746    color = color.trim().strip_prefix('#').unwrap_or(color);
1747    let color = match color.len() {
1748      // TODO: use repeat_n once our MSRV is bumped to 1.82
1749      3 => color.chars()
1750            .flat_map(|c| std::iter::repeat(c).take(2))
1751            .chain(std::iter::repeat('f').take(2))
1752            .collect(),
1753      6 => format!("{color}FF"),
1754      8 => color.to_string(),
1755      _ => return Err("Invalid hex color length, must be either 3, 6 or 8, for example: #fff, #ffffff, or #ffffffff".into()),
1756    };
1757
1758    let r = u8::from_str_radix(&color[0..2], 16).map_err(|e| e.to_string())?;
1759    let g = u8::from_str_radix(&color[2..4], 16).map_err(|e| e.to_string())?;
1760    let b = u8::from_str_radix(&color[4..6], 16).map_err(|e| e.to_string())?;
1761    let a = u8::from_str_radix(&color[6..8], 16).map_err(|e| e.to_string())?;
1762
1763    Ok(Color(r, g, b, a))
1764  }
1765}
1766
1767fn default_alpha() -> u8 {
1768  255
1769}
1770
1771#[derive(Deserialize)]
1772#[cfg_attr(feature = "schema", derive(JsonSchema))]
1773#[serde(untagged)]
1774enum InnerColor {
1775  /// Color hex string, for example: #fff, #ffffff, or #ffffffff.
1776  String(String),
1777  /// Array of RGB colors. Each value has minimum of 0 and maximum of 255.
1778  Rgb((u8, u8, u8)),
1779  /// Array of RGBA colors. Each value has minimum of 0 and maximum of 255.
1780  Rgba((u8, u8, u8, u8)),
1781  /// Object of red, green, blue, alpha color values. Each value has minimum of 0 and maximum of 255.
1782  RgbaObject {
1783    red: u8,
1784    green: u8,
1785    blue: u8,
1786    #[serde(default = "default_alpha")]
1787    alpha: u8,
1788  },
1789}
1790
1791impl<'de> Deserialize<'de> for Color {
1792  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1793  where
1794    D: Deserializer<'de>,
1795  {
1796    let color = InnerColor::deserialize(deserializer)?;
1797    let color = match color {
1798      InnerColor::String(string) => string.parse().map_err(serde::de::Error::custom)?,
1799      InnerColor::Rgb(rgb) => Color(rgb.0, rgb.1, rgb.2, 255),
1800      InnerColor::Rgba(rgb) => rgb.into(),
1801      InnerColor::RgbaObject {
1802        red,
1803        green,
1804        blue,
1805        alpha,
1806      } => Color(red, green, blue, alpha),
1807    };
1808
1809    Ok(color)
1810  }
1811}
1812
1813#[cfg(feature = "schema")]
1814impl schemars::JsonSchema for Color {
1815  fn schema_name() -> String {
1816    "Color".to_string()
1817  }
1818
1819  fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
1820    let mut schema = schemars::schema_for!(InnerColor).schema;
1821    schema.metadata = None; // Remove `title: InnerColor` from schema
1822
1823    // add hex color pattern validation
1824    let any_of = schema.subschemas().any_of.as_mut().unwrap();
1825    let schemars::schema::Schema::Object(str_schema) = any_of.first_mut().unwrap() else {
1826      unreachable!()
1827    };
1828    str_schema.string().pattern = Some("^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$".into());
1829
1830    schema.into()
1831  }
1832}
1833
1834/// Background throttling policy.
1835#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1836#[cfg_attr(feature = "schema", derive(JsonSchema))]
1837#[serde(rename_all = "camelCase", deny_unknown_fields)]
1838pub enum BackgroundThrottlingPolicy {
1839  /// A policy where background throttling is disabled
1840  Disabled,
1841  /// A policy where a web view that's not in a window fully suspends tasks. This is usually the default behavior in case no policy is set.
1842  Suspend,
1843  /// A policy where a web view that's not in a window limits processing, but does not fully suspend tasks.
1844  Throttle,
1845}
1846
1847/// The window effects configuration object
1848#[skip_serializing_none]
1849#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1850#[cfg_attr(feature = "schema", derive(JsonSchema))]
1851#[serde(rename_all = "camelCase", deny_unknown_fields)]
1852pub struct WindowEffectsConfig {
1853  /// List of Window effects to apply to the Window.
1854  /// Conflicting effects will apply the first one and ignore the rest.
1855  pub effects: Vec<WindowEffect>,
1856  /// Window effect state **macOS Only**
1857  pub state: Option<WindowEffectState>,
1858  /// Window effect corner radius **macOS Only**
1859  pub radius: Option<f64>,
1860  /// Window effect color. Affects [`WindowEffect::Blur`] and [`WindowEffect::Acrylic`] only
1861  /// on Windows 10 v1903+. Doesn't have any effect on Windows 7 or Windows 11.
1862  pub color: Option<Color>,
1863}
1864
1865/// Enable prevent overflow with a margin
1866/// so that the window's size + this margin won't overflow the workarea
1867#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1868#[cfg_attr(feature = "schema", derive(JsonSchema))]
1869#[serde(rename_all = "camelCase", deny_unknown_fields)]
1870pub struct PreventOverflowMargin {
1871  /// Horizontal margin in physical pixels
1872  pub width: u32,
1873  /// Vertical margin in physical pixels
1874  pub height: u32,
1875}
1876
1877/// Prevent overflow with a margin
1878#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1879#[cfg_attr(feature = "schema", derive(JsonSchema))]
1880#[serde(untagged)]
1881pub enum PreventOverflowConfig {
1882  /// Enable prevent overflow or not
1883  Enable(bool),
1884  /// Enable prevent overflow with a margin
1885  /// so that the window's size + this margin won't overflow the workarea
1886  Margin(PreventOverflowMargin),
1887}
1888
1889/// The scrollbar style to use in the webview.
1890///
1891/// ## Platform-specific
1892///
1893/// - **Windows**: This option must be given the same value for all webviews that target the same data directory.
1894#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
1895#[cfg_attr(feature = "schema", derive(JsonSchema))]
1896#[serde(rename_all = "camelCase", deny_unknown_fields)]
1897#[non_exhaustive]
1898pub enum ScrollBarStyle {
1899  #[default]
1900  /// The scrollbar style to use in the webview.
1901  Default,
1902
1903  /// Fluent UI style overlay scrollbars. **Windows Only**
1904  ///
1905  /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,
1906  /// see <https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541>
1907  FluentOverlay,
1908}
1909
1910/// The window configuration object.
1911///
1912/// See more: <https://v2.tauri.app/reference/config/#windowconfig>
1913#[skip_serializing_none]
1914#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
1915#[cfg_attr(feature = "schema", derive(JsonSchema))]
1916#[serde(rename_all = "camelCase", deny_unknown_fields)]
1917pub struct WindowConfig {
1918  /// The window identifier. It must be alphanumeric.
1919  #[serde(default = "default_window_label")]
1920  pub label: String,
1921  /// Whether Tauri should create this window at app startup or not.
1922  ///
1923  /// When this is set to `false` you must manually grab the config object via `app.config().app.windows`
1924  /// and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).
1925  ///
1926  /// ## Example:
1927  ///
1928  /// ```rust
1929  /// tauri::Builder::default()
1930  ///   .setup(|app| {
1931  ///     tauri::WebviewWindowBuilder::from_config(app.handle(), &app.config().app.windows[0])?.build()?;
1932  ///     Ok(())
1933  ///   });
1934  /// ```
1935  #[serde(default = "default_true")]
1936  pub create: bool,
1937  /// The window webview URL.
1938  #[serde(default)]
1939  pub url: WebviewUrl,
1940  /// The user agent for the webview
1941  #[serde(alias = "user-agent")]
1942  pub user_agent: Option<String>,
1943  /// Whether the drag and drop is enabled or not on the webview. By default it is enabled.
1944  ///
1945  /// Disabling it is required to use HTML5 drag and drop on the frontend on Windows.
1946  #[serde(default = "default_true", alias = "drag-drop-enabled")]
1947  pub drag_drop_enabled: bool,
1948  /// Whether or not the window starts centered or not.
1949  #[serde(default)]
1950  pub center: bool,
1951  /// The horizontal position of the window's top left corner in logical pixels
1952  pub x: Option<f64>,
1953  /// The vertical position of the window's top left corner in logical pixels
1954  pub y: Option<f64>,
1955  /// The window width in logical pixels.
1956  #[serde(default = "default_width")]
1957  pub width: f64,
1958  /// The window height in logical pixels.
1959  #[serde(default = "default_height")]
1960  pub height: f64,
1961  /// The min window width in logical pixels.
1962  #[serde(alias = "min-width")]
1963  pub min_width: Option<f64>,
1964  /// The min window height in logical pixels.
1965  #[serde(alias = "min-height")]
1966  pub min_height: Option<f64>,
1967  /// The max window width in logical pixels.
1968  #[serde(alias = "max-width")]
1969  pub max_width: Option<f64>,
1970  /// The max window height in logical pixels.
1971  #[serde(alias = "max-height")]
1972  pub max_height: Option<f64>,
1973  /// Whether or not to prevent the window from overflowing the workarea
1974  ///
1975  /// ## Platform-specific
1976  ///
1977  /// - **iOS / Android:** Unsupported.
1978  #[serde(alias = "prevent-overflow")]
1979  pub prevent_overflow: Option<PreventOverflowConfig>,
1980  /// Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.
1981  #[serde(default = "default_true")]
1982  pub resizable: bool,
1983  /// Whether the window's native maximize button is enabled or not.
1984  /// If resizable is set to false, this setting is ignored.
1985  ///
1986  /// ## Platform-specific
1987  ///
1988  /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode.
1989  /// - **Linux / iOS / Android:** Unsupported.
1990  #[serde(default = "default_true")]
1991  pub maximizable: bool,
1992  /// Whether the window's native minimize button is enabled or not.
1993  ///
1994  /// ## Platform-specific
1995  ///
1996  /// - **Linux / iOS / Android:** Unsupported.
1997  #[serde(default = "default_true")]
1998  pub minimizable: bool,
1999  /// Whether the window's native close button is enabled or not.
2000  ///
2001  /// ## Platform-specific
2002  ///
2003  /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button.
2004  ///   Depending on the system, this function may not have any effect when called on a window that is already visible"
2005  /// - **iOS / Android:** Unsupported.
2006  #[serde(default = "default_true")]
2007  pub closable: bool,
2008  /// The window title.
2009  #[serde(default = "default_title")]
2010  pub title: String,
2011  /// Whether the window starts as fullscreen or not.
2012  #[serde(default)]
2013  pub fullscreen: bool,
2014  /// Whether the window will be initially focused or not.
2015  #[serde(default = "default_true")]
2016  pub focus: bool,
2017  /// Whether the window will be focusable or not.
2018  #[serde(default = "default_true")]
2019  pub focusable: bool,
2020  /// Whether the window is transparent or not.
2021  ///
2022  /// Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.
2023  /// WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.
2024  #[serde(default)]
2025  pub transparent: bool,
2026  /// Whether the window is maximized or not.
2027  #[serde(default)]
2028  pub maximized: bool,
2029  /// Whether the window is visible or not.
2030  #[serde(default = "default_true")]
2031  pub visible: bool,
2032  /// Whether the window should have borders and bars.
2033  #[serde(default = "default_true")]
2034  pub decorations: bool,
2035  /// Whether the window should always be below other windows.
2036  #[serde(default, alias = "always-on-bottom")]
2037  pub always_on_bottom: bool,
2038  /// Whether the window should always be on top of other windows.
2039  #[serde(default, alias = "always-on-top")]
2040  pub always_on_top: bool,
2041  /// Whether the window should be visible on all workspaces or virtual desktops.
2042  ///
2043  /// ## Platform-specific
2044  ///
2045  /// - **Windows / iOS / Android:** Unsupported.
2046  #[serde(default, alias = "visible-on-all-workspaces")]
2047  pub visible_on_all_workspaces: bool,
2048  /// Prevents the window contents from being captured by other apps.
2049  #[serde(default, alias = "content-protected")]
2050  pub content_protected: bool,
2051  /// If `true`, hides the window icon from the taskbar on Windows and Linux.
2052  #[serde(default, alias = "skip-taskbar")]
2053  pub skip_taskbar: bool,
2054  /// The name of the window class created on Windows to create the window. **Windows only**.
2055  pub window_classname: Option<String>,
2056  /// The initial window theme. Defaults to the system theme. Only implemented on Windows and macOS 10.14+.
2057  pub theme: Option<crate::Theme>,
2058  /// The style of the macOS title bar.
2059  #[serde(default, alias = "title-bar-style")]
2060  pub title_bar_style: TitleBarStyle,
2061  /// The position of the window controls on macOS.
2062  ///
2063  /// Requires titleBarStyle: Overlay and decorations: true.
2064  #[serde(default, alias = "traffic-light-position")]
2065  pub traffic_light_position: Option<LogicalPosition>,
2066  /// If `true`, sets the window title to be hidden on macOS.
2067  #[serde(default, alias = "hidden-title")]
2068  pub hidden_title: bool,
2069  /// Whether clicking an inactive window also clicks through to the webview on macOS.
2070  #[serde(default, alias = "accept-first-mouse")]
2071  pub accept_first_mouse: bool,
2072  /// Defines the window [tabbing identifier] for macOS.
2073  ///
2074  /// Windows with matching tabbing identifiers will be grouped together.
2075  /// If the tabbing identifier is not set, automatic tabbing will be disabled.
2076  ///
2077  /// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
2078  #[serde(default, alias = "tabbing-identifier")]
2079  pub tabbing_identifier: Option<String>,
2080  /// Defines additional browser arguments on Windows. By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection`
2081  /// so if you use this method, you also need to disable these components by yourself if you want.
2082  #[serde(default, alias = "additional-browser-args")]
2083  pub additional_browser_args: Option<String>,
2084  /// Whether or not the window has shadow.
2085  ///
2086  /// ## Platform-specific
2087  ///
2088  /// - **Windows:**
2089  ///   - `false` has no effect on decorated window, shadow are always ON.
2090  ///   - `true` will make undecorated window have a 1px white border,
2091  /// and on Windows 11, it will have a rounded corners.
2092  /// - **Linux:** Unsupported.
2093  #[serde(default = "default_true")]
2094  pub shadow: bool,
2095  /// Window effects.
2096  ///
2097  /// Requires the window to be transparent.
2098  ///
2099  /// ## Platform-specific:
2100  ///
2101  /// - **Windows**: If using decorations or shadows, you may want to try this workaround <https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891>
2102  /// - **Linux**: Unsupported
2103  #[serde(default, alias = "window-effects")]
2104  pub window_effects: Option<WindowEffectsConfig>,
2105  /// Whether or not the webview should be launched in incognito  mode.
2106  ///
2107  /// ## Platform-specific:
2108  ///
2109  /// - **Android**: Unsupported.
2110  #[serde(default)]
2111  pub incognito: bool,
2112  /// Sets the window associated with this label to be the parent of the window to be created.
2113  ///
2114  /// ## Platform-specific
2115  ///
2116  /// - **Windows**: This sets the passed parent as an owner window to the window to be created.
2117  ///   From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows):
2118  ///     - An owned window is always above its owner in the z-order.
2119  ///     - The system automatically destroys an owned window when its owner is destroyed.
2120  ///     - An owned window is hidden when its owner is minimized.
2121  /// - **Linux**: This makes the new window transient for parent, see <https://docs.gtk.org/gtk3/method.Window.set_transient_for.html>
2122  /// - **macOS**: This adds the window as a child of parent, see <https://developer.apple.com/documentation/appkit/nswindow/1419152-addchildwindow?language=objc>
2123  pub parent: Option<String>,
2124  /// The proxy URL for the WebView for all network requests.
2125  ///
2126  /// Must be either a `http://` or a `socks5://` URL.
2127  ///
2128  /// ## Platform-specific
2129  ///
2130  /// - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+.
2131  #[serde(alias = "proxy-url")]
2132  pub proxy_url: Option<Url>,
2133  /// Whether page zooming by hotkeys is enabled
2134  ///
2135  /// ## Platform-specific:
2136  ///
2137  /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting.
2138  /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`,
2139  /// 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission
2140  ///
2141  /// - **Android / iOS**: Unsupported.
2142  #[serde(default, alias = "zoom-hotkeys-enabled")]
2143  pub zoom_hotkeys_enabled: bool,
2144  /// Whether browser extensions can be installed for the webview process
2145  ///
2146  /// ## Platform-specific:
2147  ///
2148  /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)
2149  /// - **MacOS / Linux / iOS / Android** - Unsupported.
2150  #[serde(default, alias = "browser-extensions-enabled")]
2151  pub browser_extensions_enabled: bool,
2152
2153  /// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
2154  ///
2155  /// ## Note
2156  ///
2157  /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
2158  ///
2159  /// ## Warning
2160  ///
2161  /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
2162  #[serde(default, alias = "use-https-scheme")]
2163  pub use_https_scheme: bool,
2164  /// Enable web inspector which is usually called browser devtools. Enabled by default.
2165  ///
2166  /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds.
2167  ///
2168  /// ## Platform-specific
2169  ///
2170  /// - macOS: This will call private functions on **macOS**.
2171  /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android.
2172  /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window.
2173  pub devtools: Option<bool>,
2174
2175  /// Set the window and webview background color.
2176  ///
2177  /// ## Platform-specific:
2178  ///
2179  /// - **Windows**: alpha channel is ignored for the window layer.
2180  /// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
2181  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored for the webview layer.
2182  #[serde(alias = "background-color")]
2183  pub background_color: Option<Color>,
2184
2185  /// Change the default background throttling behaviour.
2186  ///
2187  /// By default, browsers use a suspend policy that will throttle timers and even unload
2188  /// the whole tab (view) to free resources after roughly 5 minutes when a view became
2189  /// minimized or hidden. This will pause all tasks until the documents visibility state
2190  /// changes back from hidden to visible by bringing the view back to the foreground.
2191  ///
2192  /// ## Platform-specific
2193  ///
2194  /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.
2195  /// - **iOS**: Supported since version 17.0+.
2196  /// - **macOS**: Supported since version 14.0+.
2197  ///
2198  /// see <https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578>
2199  #[serde(default, alias = "background-throttling")]
2200  pub background_throttling: Option<BackgroundThrottlingPolicy>,
2201  /// Whether we should disable JavaScript code execution on the webview or not.
2202  #[serde(default, alias = "javascript-disabled")]
2203  pub javascript_disabled: bool,
2204  /// on macOS and iOS there is a link preview on long pressing links, this is enabled by default.
2205  /// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
2206  #[serde(default = "default_true", alias = "allow-link-preview")]
2207  pub allow_link_preview: bool,
2208  /// Allows disabling the input accessory view on iOS.
2209  ///
2210  /// The accessory view is the view that appears above the keyboard when a text input element is focused.
2211  /// It usually displays a view with "Done", "Next" buttons.
2212  #[serde(
2213    default,
2214    alias = "disable-input-accessory-view",
2215    alias = "disable_input_accessory_view"
2216  )]
2217  pub disable_input_accessory_view: bool,
2218  ///
2219  /// Set a custom path for the webview's data directory (localStorage, cache, etc.) **relative to [`appDataDir()`]/${label}**.
2220  ///
2221  /// To set absolute paths, use [`WebviewWindowBuilder::data_directory`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.data_directory)
2222  ///
2223  /// #### Platform-specific:
2224  ///
2225  /// - **Windows**: WebViews with different values for settings like `additionalBrowserArgs`, `browserExtensionsEnabled` or `scrollBarStyle` must have different data directories.
2226  /// - **macOS / iOS**: Unsupported, use `dataStoreIdentifier` instead.
2227  /// - **Android**: Unsupported.
2228  #[serde(default, alias = "data-directory")]
2229  pub data_directory: Option<PathBuf>,
2230  ///
2231  /// Initialize the WebView with a custom data store identifier. This can be seen as a replacement for `dataDirectory` which is unavailable in WKWebView.
2232  /// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore/init(foridentifier:)?language=objc
2233  ///
2234  /// The array must contain 16 u8 numbers.
2235  ///
2236  /// #### Platform-specific:
2237  ///
2238  /// - **iOS**: Supported since version 17.0+.
2239  /// - **macOS**: Supported since version 14.0+.
2240  /// - **Windows / Linux / Android**: Unsupported.
2241  #[serde(default, alias = "data-store-identifier")]
2242  pub data_store_identifier: Option<[u8; 16]>,
2243
2244  /// Specifies the native scrollbar style to use with the webview.
2245  /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here.
2246  ///
2247  /// Defaults to `default`, which is the browser default.
2248  ///
2249  /// ## Platform-specific
2250  ///
2251  /// - **Windows**:
2252  ///   - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher,
2253  ///     and does nothing on older versions.
2254  ///   - This option must be given the same value for all webviews that target the same data directory.
2255  /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.
2256  #[serde(default, alias = "scroll-bar-style")]
2257  pub scroll_bar_style: ScrollBarStyle,
2258  /// The name of the Android activity to create for this window.
2259  #[serde(default, alias = "activity-name")]
2260  pub activity_name: Option<String>,
2261  /// The name of the Android activity that is creating this webview window.
2262  ///
2263  /// This is important to determine which stack the activity will belong to.
2264  #[serde(default, alias = "created-by-activity-name")]
2265  pub created_by_activity_name: Option<String>,
2266
2267  /// Sets the identifier of the scene that is requesting the new scene,
2268  /// establishing a relationship between the two scenes.
2269  ///
2270  /// By default the system uses the foreground scene.
2271  #[serde(default, alias = "requested-by-scene-identifier")]
2272  pub requested_by_scene_identifier: Option<String>,
2273  /// Controls the WebView's browser-level general autofill behavior.
2274  ///
2275  /// **This option does not disable password or credit card autofill.**
2276  ///
2277  /// When set to `false`, the WebView will not automatically populate
2278  /// general form fields using previously stored data such as addresses
2279  /// or contact information.
2280  ///
2281  /// If not specified, this is `true` by default.
2282  ///
2283  /// ## Platform-specific
2284  ///
2285  /// - **Windows**: Supported. WebView2's autofill feature (called
2286  ///   "Suggestions") may not honor `autocomplete="off"` on input
2287  ///   elements in some cases.
2288  /// - **Linux / Android / iOS / macOS**: Unsupported and performs no
2289  ///   operation.
2290  #[serde(default = "default_true", alias = "general-autofill-enabled")]
2291  pub general_autofill_enabled: bool,
2292}
2293
2294impl Default for WindowConfig {
2295  fn default() -> Self {
2296    Self {
2297      label: default_window_label(),
2298      url: WebviewUrl::default(),
2299      create: true,
2300      user_agent: None,
2301      drag_drop_enabled: true,
2302      center: false,
2303      x: None,
2304      y: None,
2305      width: default_width(),
2306      height: default_height(),
2307      min_width: None,
2308      min_height: None,
2309      max_width: None,
2310      max_height: None,
2311      prevent_overflow: None,
2312      resizable: true,
2313      maximizable: true,
2314      minimizable: true,
2315      closable: true,
2316      title: default_title(),
2317      fullscreen: false,
2318      focus: true,
2319      focusable: true,
2320      transparent: false,
2321      maximized: false,
2322      visible: true,
2323      decorations: true,
2324      always_on_bottom: false,
2325      always_on_top: false,
2326      visible_on_all_workspaces: false,
2327      content_protected: false,
2328      skip_taskbar: false,
2329      window_classname: None,
2330      theme: None,
2331      title_bar_style: Default::default(),
2332      traffic_light_position: None,
2333      hidden_title: false,
2334      accept_first_mouse: false,
2335      tabbing_identifier: None,
2336      additional_browser_args: None,
2337      shadow: true,
2338      window_effects: None,
2339      incognito: false,
2340      parent: None,
2341      proxy_url: None,
2342      zoom_hotkeys_enabled: false,
2343      browser_extensions_enabled: false,
2344      use_https_scheme: false,
2345      devtools: None,
2346      background_color: None,
2347      background_throttling: None,
2348      javascript_disabled: false,
2349      allow_link_preview: true,
2350      disable_input_accessory_view: false,
2351      data_directory: None,
2352      data_store_identifier: None,
2353      scroll_bar_style: ScrollBarStyle::Default,
2354      activity_name: None,
2355      created_by_activity_name: None,
2356      requested_by_scene_identifier: None,
2357      general_autofill_enabled: true,
2358    }
2359  }
2360}
2361
2362fn default_window_label() -> String {
2363  "main".to_string()
2364}
2365
2366fn default_width() -> f64 {
2367  800.
2368}
2369
2370fn default_height() -> f64 {
2371  600.
2372}
2373
2374fn default_title() -> String {
2375  "Tauri App".to_string()
2376}
2377
2378/// A Content-Security-Policy directive source list.
2379/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources>.
2380#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2381#[cfg_attr(feature = "schema", derive(JsonSchema))]
2382#[serde(rename_all = "camelCase", untagged)]
2383pub enum CspDirectiveSources {
2384  /// An inline list of CSP sources. Same as [`Self::List`], but concatenated with a space separator.
2385  Inline(String),
2386  /// A list of CSP sources. The collection will be concatenated with a space separator for the CSP string.
2387  List(Vec<String>),
2388}
2389
2390impl Default for CspDirectiveSources {
2391  fn default() -> Self {
2392    Self::List(Vec::new())
2393  }
2394}
2395
2396impl From<CspDirectiveSources> for Vec<String> {
2397  fn from(sources: CspDirectiveSources) -> Self {
2398    match sources {
2399      CspDirectiveSources::Inline(source) => source.split(' ').map(|s| s.to_string()).collect(),
2400      CspDirectiveSources::List(l) => l,
2401    }
2402  }
2403}
2404
2405impl CspDirectiveSources {
2406  /// Whether the given source is configured on this directive or not.
2407  pub fn contains(&self, source: &str) -> bool {
2408    match self {
2409      Self::Inline(s) => s.contains(&format!("{source} ")) || s.contains(&format!(" {source}")),
2410      Self::List(l) => l.contains(&source.into()),
2411    }
2412  }
2413
2414  /// Appends the given source to this directive.
2415  pub fn push<S: AsRef<str>>(&mut self, source: S) {
2416    match self {
2417      Self::Inline(s) => {
2418        s.push(' ');
2419        s.push_str(source.as_ref());
2420      }
2421      Self::List(l) => {
2422        l.push(source.as_ref().to_string());
2423      }
2424    }
2425  }
2426
2427  /// Extends this CSP directive source list with the given array of sources.
2428  pub fn extend(&mut self, sources: Vec<String>) {
2429    for s in sources {
2430      self.push(s);
2431    }
2432  }
2433}
2434
2435/// A Content-Security-Policy definition.
2436/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
2437#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2438#[cfg_attr(feature = "schema", derive(JsonSchema))]
2439#[serde(rename_all = "camelCase", untagged)]
2440pub enum Csp {
2441  /// The entire CSP policy in a single text string.
2442  Policy(String),
2443  /// An object mapping a directive with its sources values as a list of strings.
2444  DirectiveMap(HashMap<String, CspDirectiveSources>),
2445}
2446
2447impl From<HashMap<String, CspDirectiveSources>> for Csp {
2448  fn from(map: HashMap<String, CspDirectiveSources>) -> Self {
2449    Self::DirectiveMap(map)
2450  }
2451}
2452
2453impl From<Csp> for HashMap<String, CspDirectiveSources> {
2454  fn from(csp: Csp) -> Self {
2455    match csp {
2456      Csp::Policy(policy) => {
2457        let mut map = HashMap::new();
2458        for directive in policy.split(';') {
2459          let mut tokens = directive.trim().split(' ');
2460          if let Some(directive) = tokens.next() {
2461            let sources = tokens.map(|s| s.to_string()).collect::<Vec<String>>();
2462            map.insert(directive.to_string(), CspDirectiveSources::List(sources));
2463          }
2464        }
2465        map
2466      }
2467      Csp::DirectiveMap(m) => m,
2468    }
2469  }
2470}
2471
2472impl Display for Csp {
2473  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2474    match self {
2475      Self::Policy(s) => write!(f, "{s}"),
2476      Self::DirectiveMap(m) => {
2477        let len = m.len();
2478        let mut i = 0;
2479        for (directive, sources) in m {
2480          let sources: Vec<String> = sources.clone().into();
2481          write!(f, "{} {}", directive, sources.join(" "))?;
2482          i += 1;
2483          if i != len {
2484            write!(f, "; ")?;
2485          }
2486        }
2487        Ok(())
2488      }
2489    }
2490  }
2491}
2492
2493/// The possible values for the `dangerous_disable_asset_csp_modification` config option.
2494#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2495#[serde(untagged)]
2496#[cfg_attr(feature = "schema", derive(JsonSchema))]
2497pub enum DisabledCspModificationKind {
2498  /// If `true`, disables all CSP modification.
2499  /// `false` is the default value and it configures Tauri to control the CSP.
2500  Flag(bool),
2501  /// Disables the given list of CSP directives modifications.
2502  List(Vec<String>),
2503}
2504
2505impl DisabledCspModificationKind {
2506  /// Determines whether the given CSP directive can be modified or not.
2507  pub fn can_modify(&self, directive: &str) -> bool {
2508    match self {
2509      Self::Flag(f) => !f,
2510      Self::List(l) => !l.contains(&directive.into()),
2511    }
2512  }
2513}
2514
2515impl Default for DisabledCspModificationKind {
2516  fn default() -> Self {
2517    Self::Flag(false)
2518  }
2519}
2520
2521/// Protocol scope definition.
2522/// It is a list of glob patterns that restrict the API access from the webview.
2523///
2524/// Each pattern can start with a variable that resolves to a system base directory.
2525/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
2526/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
2527/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$TEMP`,
2528/// `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
2529#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2530#[serde(untagged)]
2531#[cfg_attr(feature = "schema", derive(JsonSchema))]
2532pub enum FsScope {
2533  /// A list of paths that are allowed by this scope.
2534  AllowedPaths(Vec<PathBuf>),
2535  /// A complete scope configuration.
2536  #[serde(rename_all = "camelCase")]
2537  Scope {
2538    /// A list of paths that are allowed by this scope.
2539    #[serde(default)]
2540    allow: Vec<PathBuf>,
2541    /// A list of paths that are not allowed by this scope.
2542    /// This gets precedence over the [`Self::Scope::allow`] list.
2543    #[serde(default)]
2544    deny: Vec<PathBuf>,
2545    /// Whether or not paths that contain components that start with a `.`
2546    /// will require that `.` appears literally in the pattern; `*`, `?`, `**`,
2547    /// or `[...]` will not match. This is useful because such files are
2548    /// conventionally considered hidden on Unix systems and it might be
2549    /// desirable to skip them when listing files.
2550    ///
2551    /// Defaults to `true` on Unix systems and `false` on Windows
2552    // dotfiles are not supposed to be exposed by default on unix
2553    #[serde(alias = "require-literal-leading-dot")]
2554    require_literal_leading_dot: Option<bool>,
2555  },
2556}
2557
2558impl Default for FsScope {
2559  fn default() -> Self {
2560    Self::AllowedPaths(Vec::new())
2561  }
2562}
2563
2564impl FsScope {
2565  /// The list of allowed paths.
2566  pub fn allowed_paths(&self) -> &Vec<PathBuf> {
2567    match self {
2568      Self::AllowedPaths(p) => p,
2569      Self::Scope { allow, .. } => allow,
2570    }
2571  }
2572
2573  /// The list of forbidden paths.
2574  pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
2575    match self {
2576      Self::AllowedPaths(_) => None,
2577      Self::Scope { deny, .. } => Some(deny),
2578    }
2579  }
2580}
2581
2582/// Config for the asset custom protocol.
2583///
2584/// See more: <https://v2.tauri.app/reference/config/#assetprotocolconfig>
2585#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2586#[cfg_attr(feature = "schema", derive(JsonSchema))]
2587#[serde(rename_all = "camelCase", deny_unknown_fields)]
2588pub struct AssetProtocolConfig {
2589  /// The access scope for the asset protocol.
2590  #[serde(default)]
2591  pub scope: FsScope,
2592  /// Enables the asset protocol.
2593  #[serde(default)]
2594  pub enable: bool,
2595}
2596
2597/// definition of a header source
2598///
2599/// The header value to a header name
2600#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2601#[cfg_attr(feature = "schema", derive(JsonSchema))]
2602#[serde(rename_all = "camelCase", untagged)]
2603pub enum HeaderSource {
2604  /// string version of the header Value
2605  Inline(String),
2606  /// list version of the header value. Item are joined by "," for the real header value
2607  List(Vec<String>),
2608  /// (Rust struct | Json | JavaScript Object) equivalent of the header value. Items are composed from: key + space + value. Item are then joined by ";" for the real header value
2609  Map(HashMap<String, String>),
2610}
2611
2612impl Display for HeaderSource {
2613  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2614    match self {
2615      Self::Inline(s) => write!(f, "{s}"),
2616      Self::List(l) => write!(f, "{}", l.join(", ")),
2617      Self::Map(m) => {
2618        let len = m.len();
2619        let mut i = 0;
2620        for (key, value) in m {
2621          write!(f, "{key} {value}")?;
2622          i += 1;
2623          if i != len {
2624            write!(f, "; ")?;
2625          }
2626        }
2627        Ok(())
2628      }
2629    }
2630  }
2631}
2632
2633/// A trait which implements on the [`Builder`] of the http create
2634///
2635/// Must add headers defined in the tauri configuration file to http responses
2636pub trait HeaderAddition {
2637  /// adds all headers defined on the config file, given the current HeaderConfig
2638  fn add_configured_headers(self, headers: Option<&HeaderConfig>) -> http::response::Builder;
2639}
2640
2641impl HeaderAddition for Builder {
2642  /// Add the headers defined in the tauri configuration file to http responses
2643  ///
2644  /// this is a utility function, which is used in the same way as the `.header(..)` of the rust http library
2645  fn add_configured_headers(mut self, headers: Option<&HeaderConfig>) -> http::response::Builder {
2646    if let Some(headers) = headers {
2647      // Add the header Access-Control-Allow-Credentials, if we find a value for it
2648      if let Some(value) = &headers.access_control_allow_credentials {
2649        self = self.header("Access-Control-Allow-Credentials", value.to_string());
2650      };
2651
2652      // Add the header Access-Control-Allow-Headers, if we find a value for it
2653      if let Some(value) = &headers.access_control_allow_headers {
2654        self = self.header("Access-Control-Allow-Headers", value.to_string());
2655      };
2656
2657      // Add the header Access-Control-Allow-Methods, if we find a value for it
2658      if let Some(value) = &headers.access_control_allow_methods {
2659        self = self.header("Access-Control-Allow-Methods", value.to_string());
2660      };
2661
2662      // Add the header Access-Control-Expose-Headers, if we find a value for it
2663      if let Some(value) = &headers.access_control_expose_headers {
2664        self = self.header("Access-Control-Expose-Headers", value.to_string());
2665      };
2666
2667      // Add the header Access-Control-Max-Age, if we find a value for it
2668      if let Some(value) = &headers.access_control_max_age {
2669        self = self.header("Access-Control-Max-Age", value.to_string());
2670      };
2671
2672      // Add the header Cross-Origin-Embedder-Policy, if we find a value for it
2673      if let Some(value) = &headers.cross_origin_embedder_policy {
2674        self = self.header("Cross-Origin-Embedder-Policy", value.to_string());
2675      };
2676
2677      // Add the header Cross-Origin-Opener-Policy, if we find a value for it
2678      if let Some(value) = &headers.cross_origin_opener_policy {
2679        self = self.header("Cross-Origin-Opener-Policy", value.to_string());
2680      };
2681
2682      // Add the header Cross-Origin-Resource-Policy, if we find a value for it
2683      if let Some(value) = &headers.cross_origin_resource_policy {
2684        self = self.header("Cross-Origin-Resource-Policy", value.to_string());
2685      };
2686
2687      // Add the header Permission-Policy, if we find a value for it
2688      if let Some(value) = &headers.permissions_policy {
2689        self = self.header("Permission-Policy", value.to_string());
2690      };
2691
2692      if let Some(value) = &headers.service_worker_allowed {
2693        self = self.header("Service-Worker-Allowed", value.to_string());
2694      }
2695
2696      // Add the header Timing-Allow-Origin, if we find a value for it
2697      if let Some(value) = &headers.timing_allow_origin {
2698        self = self.header("Timing-Allow-Origin", value.to_string());
2699      };
2700
2701      // Add the header X-Content-Type-Options, if we find a value for it
2702      if let Some(value) = &headers.x_content_type_options {
2703        self = self.header("X-Content-Type-Options", value.to_string());
2704      };
2705
2706      // Add the header Tauri-Custom-Header, if we find a value for it
2707      if let Some(value) = &headers.tauri_custom_header {
2708        // Keep in mind to correctly set the Access-Control-Expose-Headers
2709        self = self.header("Tauri-Custom-Header", value.to_string());
2710      };
2711    }
2712    self
2713  }
2714}
2715
2716/// A struct, where the keys are some specific http header names.
2717///
2718/// If the values to those keys are defined, then they will be send as part of a response message.
2719/// This does not include error messages and ipc messages
2720///
2721/// ## Example configuration
2722/// ```javascript
2723/// {
2724///  //..
2725///   app:{
2726///     //..
2727///     security: {
2728///       headers: {
2729///         "Cross-Origin-Opener-Policy": "same-origin",
2730///         "Cross-Origin-Embedder-Policy": "require-corp",
2731///         "Timing-Allow-Origin": [
2732///           "https://developer.mozilla.org",
2733///           "https://example.com",
2734///         ],
2735///         "Access-Control-Expose-Headers": "Tauri-Custom-Header",
2736///         "Tauri-Custom-Header": {
2737///           "key1": "'value1' 'value2'",
2738///           "key2": "'value3'"
2739///         }
2740///       },
2741///       csp: "default-src 'self'; connect-src ipc: http://ipc.localhost",
2742///     }
2743///     //..
2744///   }
2745///  //..
2746/// }
2747/// ```
2748/// In this example `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` are set to allow for the use of [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer).
2749/// The result is, that those headers are then set on every response sent via the `get_response` function in crates/tauri/src/protocol/tauri.rs.
2750/// The Content-Security-Policy header is defined separately, because it is also handled separately.
2751///
2752/// For the helloworld example, this config translates into those response headers:
2753/// ```http
2754/// access-control-allow-origin:  http://tauri.localhost
2755/// access-control-expose-headers: Tauri-Custom-Header
2756/// content-security-policy: default-src 'self'; connect-src ipc: http://ipc.localhost; script-src 'self' 'sha256-Wjjrs6qinmnr+tOry8x8PPwI77eGpUFR3EEGZktjJNs='
2757/// content-type: text/html
2758/// cross-origin-embedder-policy: require-corp
2759/// cross-origin-opener-policy: same-origin
2760/// tauri-custom-header: key1 'value1' 'value2'; key2 'value3'
2761/// timing-allow-origin: https://developer.mozilla.org, https://example.com
2762/// ```
2763/// Since the resulting header values are always 'string-like'. So depending on the what data type the HeaderSource is, they need to be converted.
2764///  - `String`(JS/Rust): stay the same for the resulting header value
2765///  - `Array`(JS)/`Vec\<String\>`(Rust): Item are joined by ", " for the resulting header value
2766///  - `Object`(JS)/ `Hashmap\<String,String\>`(Rust): Items are composed from: key + space + value. Item are then joined by "; " for the resulting header value
2767#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2768#[cfg_attr(feature = "schema", derive(JsonSchema))]
2769#[serde(deny_unknown_fields)]
2770pub struct HeaderConfig {
2771  /// The Access-Control-Allow-Credentials response header tells browsers whether the
2772  /// server allows cross-origin HTTP requests to include credentials.
2773  ///
2774  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials>
2775  #[serde(rename = "Access-Control-Allow-Credentials")]
2776  pub access_control_allow_credentials: Option<HeaderSource>,
2777  /// The Access-Control-Allow-Headers response header is used in response
2778  /// to a preflight request which includes the Access-Control-Request-Headers
2779  /// to indicate which HTTP headers can be used during the actual request.
2780  ///
2781  /// This header is required if the request has an Access-Control-Request-Headers header.
2782  ///
2783  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers>
2784  #[serde(rename = "Access-Control-Allow-Headers")]
2785  pub access_control_allow_headers: Option<HeaderSource>,
2786  /// The Access-Control-Allow-Methods response header specifies one or more methods
2787  /// allowed when accessing a resource in response to a preflight request.
2788  ///
2789  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods>
2790  #[serde(rename = "Access-Control-Allow-Methods")]
2791  pub access_control_allow_methods: Option<HeaderSource>,
2792  /// The Access-Control-Expose-Headers response header allows a server to indicate
2793  /// which response headers should be made available to scripts running in the browser,
2794  /// in response to a cross-origin request.
2795  ///
2796  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers>
2797  #[serde(rename = "Access-Control-Expose-Headers")]
2798  pub access_control_expose_headers: Option<HeaderSource>,
2799  /// The Access-Control-Max-Age response header indicates how long the results of a
2800  /// preflight request (that is the information contained in the
2801  /// Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can
2802  /// be cached.
2803  ///
2804  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age>
2805  #[serde(rename = "Access-Control-Max-Age")]
2806  pub access_control_max_age: Option<HeaderSource>,
2807  /// The HTTP Cross-Origin-Embedder-Policy (COEP) response header configures embedding
2808  /// cross-origin resources into the document.
2809  ///
2810  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy>
2811  #[serde(rename = "Cross-Origin-Embedder-Policy")]
2812  pub cross_origin_embedder_policy: Option<HeaderSource>,
2813  /// The HTTP Cross-Origin-Opener-Policy (COOP) response header allows you to ensure a
2814  /// top-level document does not share a browsing context group with cross-origin documents.
2815  /// COOP will process-isolate your document and potential attackers can't access your global
2816  /// object if they were to open it in a popup, preventing a set of cross-origin attacks dubbed XS-Leaks.
2817  ///
2818  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy>
2819  #[serde(rename = "Cross-Origin-Opener-Policy")]
2820  pub cross_origin_opener_policy: Option<HeaderSource>,
2821  /// The HTTP Cross-Origin-Resource-Policy response header conveys a desire that the
2822  /// browser blocks no-cors cross-origin/cross-site requests to the given resource.
2823  ///
2824  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy>
2825  #[serde(rename = "Cross-Origin-Resource-Policy")]
2826  pub cross_origin_resource_policy: Option<HeaderSource>,
2827  /// The HTTP Permissions-Policy header provides a mechanism to allow and deny the
2828  /// use of browser features in a document or within any \<iframe\> elements in the document.
2829  ///
2830  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy>
2831  #[serde(rename = "Permissions-Policy")]
2832  pub permissions_policy: Option<HeaderSource>,
2833  /// The HTTP Service-Worker-Allowed response header is used to broaden the path restriction for a
2834  /// service worker's default scope.
2835  ///
2836  /// By default, the scope for a service worker registration is the directory where the service
2837  /// worker script is located. For example, if the script `sw.js` is located in `/js/sw.js`,
2838  /// it can only control URLs under `/js/` by default. Servers can use the `Service-Worker-Allowed`
2839  /// header to allow a service worker to control URLs outside of its own directory.
2840  ///
2841  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Service-Worker-Allowed>
2842  #[serde(rename = "Service-Worker-Allowed")]
2843  pub service_worker_allowed: Option<HeaderSource>,
2844  /// The Timing-Allow-Origin response header specifies origins that are allowed to see values
2845  /// of attributes retrieved via features of the Resource Timing API, which would otherwise be
2846  /// reported as zero due to cross-origin restrictions.
2847  ///
2848  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin>
2849  #[serde(rename = "Timing-Allow-Origin")]
2850  pub timing_allow_origin: Option<HeaderSource>,
2851  /// The X-Content-Type-Options response HTTP header is a marker used by the server to indicate
2852  /// that the MIME types advertised in the Content-Type headers should be followed and not be
2853  /// changed. The header allows you to avoid MIME type sniffing by saying that the MIME types
2854  /// are deliberately configured.
2855  ///
2856  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options>
2857  #[serde(rename = "X-Content-Type-Options")]
2858  pub x_content_type_options: Option<HeaderSource>,
2859  /// A custom header field Tauri-Custom-Header, don't use it.
2860  /// Remember to set Access-Control-Expose-Headers accordingly
2861  ///
2862  /// **NOT INTENDED FOR PRODUCTION USE**
2863  #[serde(rename = "Tauri-Custom-Header")]
2864  pub tauri_custom_header: Option<HeaderSource>,
2865}
2866
2867impl HeaderConfig {
2868  /// creates a new header config
2869  pub fn new() -> Self {
2870    HeaderConfig {
2871      access_control_allow_credentials: None,
2872      access_control_allow_methods: None,
2873      access_control_allow_headers: None,
2874      access_control_expose_headers: None,
2875      access_control_max_age: None,
2876      cross_origin_embedder_policy: None,
2877      cross_origin_opener_policy: None,
2878      cross_origin_resource_policy: None,
2879      permissions_policy: None,
2880      service_worker_allowed: None,
2881      timing_allow_origin: None,
2882      x_content_type_options: None,
2883      tauri_custom_header: None,
2884    }
2885  }
2886}
2887
2888/// Security configuration.
2889///
2890/// See more: <https://v2.tauri.app/reference/config/#securityconfig>
2891#[skip_serializing_none]
2892#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
2893#[cfg_attr(feature = "schema", derive(JsonSchema))]
2894#[serde(rename_all = "camelCase", deny_unknown_fields)]
2895pub struct SecurityConfig {
2896  /// The Content Security Policy that will be injected on all HTML files on the built application.
2897  /// If [`dev_csp`](#SecurityConfig.devCsp) is not specified, this value is also injected on dev.
2898  ///
2899  /// This is a really important part of the configuration since it helps you ensure your WebView is secured.
2900  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
2901  pub csp: Option<Csp>,
2902  /// The Content Security Policy that will be injected on all HTML files on development.
2903  ///
2904  /// This is a really important part of the configuration since it helps you ensure your WebView is secured.
2905  /// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
2906  #[serde(alias = "dev-csp")]
2907  pub dev_csp: Option<Csp>,
2908  /// Freeze the `Object.prototype` when using the custom protocol.
2909  #[serde(default, alias = "freeze-prototype")]
2910  pub freeze_prototype: bool,
2911  /// Disables the Tauri-injected CSP sources.
2912  ///
2913  /// At compile time, Tauri parses all the frontend assets and changes the Content-Security-Policy
2914  /// to only allow loading of your own scripts and styles by injecting nonce and hash sources.
2915  /// This stricts your CSP, which may introduce issues when using along with other flexing sources.
2916  ///
2917  /// This configuration option allows both a boolean and a list of strings as value.
2918  /// A boolean instructs Tauri to disable the injection for all CSP injections,
2919  /// and a list of strings indicates the CSP directives that Tauri cannot inject.
2920  ///
2921  /// **WARNING:** Only disable this if you know what you are doing and have properly configured the CSP.
2922  /// Your application might be vulnerable to XSS attacks without this Tauri protection.
2923  #[serde(default, alias = "dangerous-disable-asset-csp-modification")]
2924  pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
2925  /// Custom protocol config.
2926  #[serde(default, alias = "asset-protocol")]
2927  pub asset_protocol: AssetProtocolConfig,
2928  /// The pattern to use.
2929  #[serde(default)]
2930  pub pattern: PatternKind,
2931  /// List of capabilities that are enabled on the application.
2932  ///
2933  /// By default (not set or empty list), all capability files from `./capabilities/` are included,
2934  /// by setting values in this entry, you have fine grained control over which capabilities are included
2935  ///
2936  /// You can either reference a capability file defined in `./capabilities/` with its identifier or inline a [`Capability`]
2937  ///
2938  /// ### Example
2939  ///
2940  /// ```json
2941  /// {
2942  ///   "app": {
2943  ///     "capabilities": [
2944  ///       "main-window",
2945  ///       {
2946  ///         "identifier": "drag-window",
2947  ///         "permissions": ["core:window:allow-start-dragging"]
2948  ///       }
2949  ///     ]
2950  ///   }
2951  /// }
2952  /// ```
2953  #[serde(default)]
2954  pub capabilities: Vec<CapabilityEntry>,
2955  /// The headers, which are added to every http response from tauri to the web view
2956  /// This doesn't include IPC Messages and error responses
2957  #[serde(default)]
2958  pub headers: Option<HeaderConfig>,
2959}
2960
2961/// A capability entry which can be either an inlined capability or a reference to a capability defined on its own file.
2962#[derive(Debug, Clone, PartialEq, Serialize)]
2963#[cfg_attr(feature = "schema", derive(JsonSchema))]
2964#[serde(untagged)]
2965pub enum CapabilityEntry {
2966  /// An inlined capability.
2967  Inlined(Capability),
2968  /// Reference to a capability identifier.
2969  Reference(String),
2970}
2971
2972impl<'de> Deserialize<'de> for CapabilityEntry {
2973  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2974  where
2975    D: Deserializer<'de>,
2976  {
2977    UntaggedEnumVisitor::new()
2978      .string(|string| Ok(Self::Reference(string.to_owned())))
2979      .map(|map| map.deserialize::<Capability>().map(Self::Inlined))
2980      .deserialize(deserializer)
2981  }
2982}
2983
2984/// The application pattern.
2985#[skip_serializing_none]
2986#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
2987#[serde(rename_all = "lowercase", tag = "use", content = "options")]
2988#[cfg_attr(feature = "schema", derive(JsonSchema))]
2989pub enum PatternKind {
2990  /// Brownfield pattern.
2991  #[default]
2992  Brownfield,
2993  /// Isolation pattern. Recommended for security purposes.
2994  Isolation {
2995    /// The dir containing the index.html file that contains the secure isolation application.
2996    dir: PathBuf,
2997  },
2998}
2999
3000/// The App configuration object.
3001///
3002/// See more: <https://v2.tauri.app/reference/config/#appconfig>
3003#[skip_serializing_none]
3004#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3005#[cfg_attr(feature = "schema", derive(JsonSchema))]
3006#[serde(rename_all = "camelCase", deny_unknown_fields)]
3007pub struct AppConfig {
3008  /// The app windows configuration.
3009  ///
3010  /// ## Example:
3011  ///
3012  /// To create a window at app startup
3013  ///
3014  /// ```json
3015  /// {
3016  ///   "app": {
3017  ///     "windows": [
3018  ///       { "width": 800, "height": 600 }
3019  ///     ]
3020  ///   }
3021  /// }
3022  /// ```
3023  ///
3024  /// If not specified, the window's label (its identifier) defaults to "main",
3025  /// you can use this label to get the window through
3026  /// `app.get_webview_window` in Rust or `WebviewWindow.getByLabel` in JavaScript
3027  ///
3028  /// When working with multiple windows, each window will need an unique label
3029  ///
3030  /// ```json
3031  /// {
3032  ///   "app": {
3033  ///     "windows": [
3034  ///       { "label": "main", "width": 800, "height": 600 },
3035  ///       { "label": "secondary", "width": 800, "height": 600 }
3036  ///     ]
3037  ///   }
3038  /// }
3039  /// ```
3040  ///
3041  /// You can also set `create` to false and use this config through the Rust APIs
3042  ///
3043  /// ```json
3044  /// {
3045  ///   "app": {
3046  ///     "windows": [
3047  ///       { "create": false, "width": 800, "height": 600 }
3048  ///     ]
3049  ///   }
3050  /// }
3051  /// ```
3052  ///
3053  /// and use it like this
3054  ///
3055  /// ```rust
3056  /// tauri::Builder::default()
3057  ///   .setup(|app| {
3058  ///     tauri::WebviewWindowBuilder::from_config(app.handle(), &app.config().app.windows[0])?.build()?;
3059  ///     Ok(())
3060  ///   });
3061  /// ```
3062  #[serde(default)]
3063  pub windows: Vec<WindowConfig>,
3064  /// Security configuration.
3065  #[serde(default)]
3066  pub security: SecurityConfig,
3067  /// Configuration for app tray icon.
3068  #[serde(alias = "tray-icon")]
3069  pub tray_icon: Option<TrayIconConfig>,
3070  /// MacOS private API configuration. Enables the transparent background API and sets the `fullScreenEnabled` preference to `true`.
3071  #[serde(rename = "macOSPrivateApi", alias = "macos-private-api", default)]
3072  pub macos_private_api: bool,
3073  /// Whether we should inject the Tauri API on `window.__TAURI__` or not.
3074  #[serde(default, alias = "with-global-tauri")]
3075  pub with_global_tauri: bool,
3076  /// If set to true "identifier" will be set as GTK app ID (on systems that use GTK).
3077  #[serde(rename = "enableGTKAppId", alias = "enable-gtk-app-id", default)]
3078  pub enable_gtk_app_id: bool,
3079}
3080
3081impl AppConfig {
3082  /// Returns all Cargo features.
3083  pub fn all_features() -> Vec<&'static str> {
3084    vec![
3085      "tray-icon",
3086      "macos-private-api",
3087      "protocol-asset",
3088      "isolation",
3089    ]
3090  }
3091
3092  /// Returns the enabled Cargo features.
3093  pub fn features(&self) -> Vec<&str> {
3094    let mut features = Vec::new();
3095    if self.tray_icon.is_some() {
3096      features.push("tray-icon");
3097    }
3098    if self.macos_private_api {
3099      features.push("macos-private-api");
3100    }
3101    if self.security.asset_protocol.enable {
3102      features.push("protocol-asset");
3103    }
3104
3105    if let PatternKind::Isolation { .. } = self.security.pattern {
3106      features.push("isolation");
3107    }
3108
3109    features.sort_unstable();
3110    features
3111  }
3112}
3113
3114/// Configuration for application tray icon.
3115///
3116/// See more: <https://v2.tauri.app/reference/config/#trayiconconfig>
3117#[skip_serializing_none]
3118#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
3119#[cfg_attr(feature = "schema", derive(JsonSchema))]
3120#[serde(rename_all = "camelCase", deny_unknown_fields)]
3121pub struct TrayIconConfig {
3122  /// Set an id for this tray icon so you can reference it later, defaults to `main`.
3123  pub id: Option<String>,
3124  /// Path to the default icon to use for the tray icon.
3125  ///
3126  /// Note: this stores the image in raw pixels to the final binary,
3127  /// so keep the icon size (width and height) small
3128  /// or else it's going to bloat your final executable
3129  #[serde(alias = "icon-path")]
3130  pub icon_path: PathBuf,
3131  /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.
3132  #[serde(default, alias = "icon-as-template")]
3133  pub icon_as_template: bool,
3134  /// A Boolean value that determines whether the menu should appear when the tray icon receives a left click.
3135  ///
3136  /// ## Platform-specific:
3137  ///
3138  /// - **Linux**: Unsupported.
3139  #[serde(default = "default_true", alias = "menu-on-left-click")]
3140  #[deprecated(since = "2.2.0", note = "Use `show_menu_on_left_click` instead.")]
3141  pub menu_on_left_click: bool,
3142  /// A Boolean value that determines whether the menu should appear when the tray icon receives a left click.
3143  ///
3144  /// ## Platform-specific:
3145  ///
3146  /// - **Linux**: Unsupported.
3147  #[serde(default = "default_true", alias = "show-menu-on-left-click")]
3148  pub show_menu_on_left_click: bool,
3149  /// Title for MacOS tray
3150  pub title: Option<String>,
3151  /// Tray icon tooltip on Windows and macOS
3152  pub tooltip: Option<String>,
3153}
3154
3155/// General configuration for the iOS target.
3156#[skip_serializing_none]
3157#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3158#[cfg_attr(feature = "schema", derive(JsonSchema))]
3159#[serde(rename_all = "camelCase", deny_unknown_fields)]
3160pub struct IosConfig {
3161  /// A custom [XcodeGen] project.yml template to use.
3162  ///
3163  /// [XcodeGen]: <https://github.com/yonaskolb/XcodeGen>
3164  pub template: Option<PathBuf>,
3165  /// A list of strings indicating any iOS frameworks that need to be bundled with the application.
3166  ///
3167  /// Note that you need to recreate the iOS project for the changes to be applied.
3168  pub frameworks: Option<Vec<String>>,
3169  /// The development team. This value is required for iOS development because code signing is enforced.
3170  /// The `APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it.
3171  #[serde(alias = "development-team")]
3172  pub development_team: Option<String>,
3173  /// The version of the build that identifies an iteration of the bundle.
3174  ///
3175  /// Translates to the bundle's CFBundleVersion property.
3176  #[serde(alias = "bundle-version")]
3177  pub bundle_version: Option<String>,
3178  /// A version string indicating the minimum iOS version that the bundled application supports. Defaults to `13.0`.
3179  ///
3180  /// Maps to the IPHONEOS_DEPLOYMENT_TARGET value.
3181  #[serde(
3182    alias = "minimum-system-version",
3183    default = "ios_minimum_system_version"
3184  )]
3185  pub minimum_system_version: String,
3186  /// Path to a Info.plist file to merge with the default Info.plist.
3187  ///
3188  /// Note that Tauri also looks for a `Info.plist` and `Info.ios.plist` file in the same directory as the Tauri configuration file.
3189  #[serde(alias = "info-plist")]
3190  pub info_plist: Option<PathBuf>,
3191}
3192
3193impl Default for IosConfig {
3194  fn default() -> Self {
3195    Self {
3196      template: None,
3197      frameworks: None,
3198      development_team: None,
3199      bundle_version: None,
3200      minimum_system_version: ios_minimum_system_version(),
3201      info_plist: None,
3202    }
3203  }
3204}
3205
3206/// General configuration for the Android target.
3207#[skip_serializing_none]
3208#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3209#[cfg_attr(feature = "schema", derive(JsonSchema))]
3210#[serde(rename_all = "camelCase", deny_unknown_fields)]
3211pub struct AndroidConfig {
3212  /// The minimum API level required for the application to run.
3213  /// The Android system will prevent the user from installing the application if the system's API level is lower than the value specified.
3214  #[serde(alias = "min-sdk-version", default = "default_min_sdk_version")]
3215  pub min_sdk_version: u32,
3216
3217  /// The version code of the application.
3218  /// It is limited to 2,100,000,000 as per Google Play Store requirements.
3219  ///
3220  /// By default we use your configured version and perform the following math:
3221  /// versionCode = version.major * 1000000 + version.minor * 1000 + version.patch
3222  #[serde(alias = "version-code")]
3223  #[cfg_attr(feature = "schema", validate(range(min = 1, max = 2_100_000_000)))]
3224  pub version_code: Option<u32>,
3225
3226  /// Whether to automatically increment the `versionCode` on each build.
3227  ///
3228  /// - If `true`, the generator will try to read the last `versionCode` from
3229  ///   `tauri.properties` and increment it by 1 for every build.
3230  /// - If `false` or not set, it falls back to `version_code` or semver-derived logic.
3231  ///
3232  /// Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.
3233  #[serde(alias = "auto-increment-version-code", default)]
3234  pub auto_increment_version_code: bool,
3235
3236  /// Application ID suffix to append for debug builds.
3237  /// This allows installing debug and release versions side-by-side on the same device.
3238  /// Example: ".debug" will make debug builds use "com.example.app.debug" as the application ID.
3239  #[serde(alias = "debug-application-id-suffix")]
3240  pub debug_application_id_suffix: Option<String>,
3241}
3242
3243impl Default for AndroidConfig {
3244  fn default() -> Self {
3245    Self {
3246      min_sdk_version: default_min_sdk_version(),
3247      version_code: None,
3248      auto_increment_version_code: false,
3249      debug_application_id_suffix: None,
3250    }
3251  }
3252}
3253
3254fn default_min_sdk_version() -> u32 {
3255  24
3256}
3257
3258/// Defines the URL or assets to embed in the application.
3259#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3260#[cfg_attr(feature = "schema", derive(JsonSchema))]
3261#[serde(untagged, deny_unknown_fields)]
3262#[non_exhaustive]
3263pub enum FrontendDist {
3264  /// An external URL that should be used as the default application URL. No assets are embedded in the app in this case.
3265  Url(Url),
3266  /// Path to a directory containing the frontend dist assets.
3267  Directory(PathBuf),
3268  /// An array of files to embed in the app.
3269  Files(Vec<PathBuf>),
3270}
3271
3272impl std::fmt::Display for FrontendDist {
3273  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3274    match self {
3275      Self::Url(url) => write!(f, "{url}"),
3276      Self::Directory(p) => write!(f, "{}", p.display()),
3277      Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
3278    }
3279  }
3280}
3281
3282/// Describes the shell command to run before `tauri dev`.
3283#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3284#[cfg_attr(feature = "schema", derive(JsonSchema))]
3285#[serde(rename_all = "camelCase", untagged)]
3286pub enum BeforeDevCommand {
3287  /// Run the given script with the default options.
3288  Script(String),
3289  /// Run the given script with custom options.
3290  ScriptWithOptions {
3291    /// The script to execute.
3292    script: String,
3293    /// The current working directory.
3294    cwd: Option<String>,
3295    /// Whether `tauri dev` should wait for the command to finish or not. Defaults to `false`.
3296    #[serde(default)]
3297    wait: bool,
3298  },
3299}
3300
3301/// Describes a shell command to be executed when a CLI hook is triggered.
3302#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3303#[cfg_attr(feature = "schema", derive(JsonSchema))]
3304#[serde(rename_all = "camelCase", untagged)]
3305pub enum HookCommand {
3306  /// Run the given script with the default options.
3307  Script(String),
3308  /// Run the given script with custom options.
3309  ScriptWithOptions {
3310    /// The script to execute.
3311    script: String,
3312    /// The current working directory.
3313    cwd: Option<String>,
3314  },
3315}
3316
3317/// The runner configuration.
3318#[skip_serializing_none]
3319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3320#[cfg_attr(feature = "schema", derive(JsonSchema))]
3321#[serde(untagged)]
3322pub enum RunnerConfig {
3323  /// A string specifying the binary to run.
3324  String(String),
3325  /// An object with advanced configuration options.
3326  Object {
3327    /// The binary to run.
3328    cmd: String,
3329    /// The current working directory to run the command from.
3330    cwd: Option<String>,
3331    /// Arguments to pass to the command.
3332    args: Option<Vec<String>>,
3333  },
3334}
3335
3336impl Default for RunnerConfig {
3337  fn default() -> Self {
3338    RunnerConfig::String("cargo".to_string())
3339  }
3340}
3341
3342impl RunnerConfig {
3343  /// Returns the command to run.
3344  pub fn cmd(&self) -> &str {
3345    match self {
3346      RunnerConfig::String(cmd) => cmd,
3347      RunnerConfig::Object { cmd, .. } => cmd,
3348    }
3349  }
3350
3351  /// Returns the working directory.
3352  pub fn cwd(&self) -> Option<&str> {
3353    match self {
3354      RunnerConfig::String(_) => None,
3355      RunnerConfig::Object { cwd, .. } => cwd.as_deref(),
3356    }
3357  }
3358
3359  /// Returns the arguments.
3360  pub fn args(&self) -> Option<&[String]> {
3361    match self {
3362      RunnerConfig::String(_) => None,
3363      RunnerConfig::Object { args, .. } => args.as_deref(),
3364    }
3365  }
3366}
3367
3368impl std::str::FromStr for RunnerConfig {
3369  type Err = std::convert::Infallible;
3370
3371  fn from_str(s: &str) -> Result<Self, Self::Err> {
3372    Ok(RunnerConfig::String(s.to_string()))
3373  }
3374}
3375
3376impl From<&str> for RunnerConfig {
3377  fn from(s: &str) -> Self {
3378    RunnerConfig::String(s.to_string())
3379  }
3380}
3381
3382impl From<String> for RunnerConfig {
3383  fn from(s: String) -> Self {
3384    RunnerConfig::String(s)
3385  }
3386}
3387
3388/// The Build configuration object.
3389///
3390/// See more: <https://v2.tauri.app/reference/config/#buildconfig>
3391#[skip_serializing_none]
3392#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
3393#[cfg_attr(feature = "schema", derive(JsonSchema))]
3394#[serde(rename_all = "camelCase", deny_unknown_fields)]
3395pub struct BuildConfig {
3396  /// The binary used to build and run the application.
3397  pub runner: Option<RunnerConfig>,
3398  /// The URL to load in development.
3399  ///
3400  /// This is usually an URL to a dev server, which serves your application assets with hot-reload and HMR.
3401  /// Most modern JavaScript bundlers like [Vite](https://vite.dev/guide/) provides a way to start a dev server by default.
3402  ///
3403  /// If you don't have a dev server or don't want to use one, ignore this option and use [`frontendDist`](BuildConfig::frontend_dist)
3404  /// and point to a web assets directory, and Tauri CLI will run its built-in dev server and provide a simple hot-reload experience.
3405  #[serde(alias = "dev-url")]
3406  pub dev_url: Option<Url>,
3407  /// The path to the application assets (usually the `dist` folder of your javascript bundler)
3408  /// or a URL that could be either a custom protocol registered in the tauri app (for example: `myprotocol://`)
3409  /// or a remote URL (for example: `https://site.com/app`).
3410  ///
3411  /// When a path relative to the configuration file is provided,
3412  /// it is read recursively and all files are embedded in the application binary.
3413  /// Tauri then looks for an `index.html` and serves it as the default entry point for your application.
3414  ///
3415  /// You can also provide a list of paths to be embedded, which allows granular control over what files are added to the binary.
3416  /// In this case, all files are added to the root and you must reference it that way in your HTML files.
3417  ///
3418  /// When a URL is provided, the application won't have bundled assets
3419  /// and the application will load that URL by default.
3420  #[serde(alias = "frontend-dist")]
3421  pub frontend_dist: Option<FrontendDist>,
3422  /// A shell command to run before `tauri dev` kicks in.
3423  ///
3424  /// The TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG environment variables are set if you perform conditional compilation.
3425  #[serde(alias = "before-dev-command")]
3426  pub before_dev_command: Option<BeforeDevCommand>,
3427  /// A shell command to run before `tauri build` kicks in.
3428  ///
3429  /// The TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG environment variables are set if you perform conditional compilation.
3430  #[serde(alias = "before-build-command")]
3431  pub before_build_command: Option<HookCommand>,
3432  /// A shell command to run before the bundling phase in `tauri build` kicks in.
3433  ///
3434  /// The TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG environment variables are set if you perform conditional compilation.
3435  #[serde(alias = "before-bundle-command")]
3436  pub before_bundle_command: Option<HookCommand>,
3437  /// Features passed to `cargo` commands.
3438  pub features: Option<Vec<String>>,
3439  /// Try to remove unused commands registered from plugins base on the ACL list during `tauri build`,
3440  /// the way it works is that tauri-cli will read this and set the environment variables for the build script and macros,
3441  /// and they'll try to get all the allowed commands and remove the rest
3442  ///
3443  /// Note:
3444  ///   - This won't be accounting for dynamically added ACLs when you use features from the `dynamic-acl` (currently enabled by default) feature flag, so make sure to check it when using this
3445  ///   - This feature requires tauri-plugin 2.1 and tauri 2.4
3446  #[serde(alias = "remove-unused-commands", default)]
3447  pub remove_unused_commands: bool,
3448  /// Additional paths to watch for changes when running `tauri dev`.
3449  #[serde(alias = "additional-watch-directories", default)]
3450  pub additional_watch_folders: Vec<PathBuf>,
3451}
3452
3453#[derive(Debug, PartialEq, Eq)]
3454struct PackageVersion(String);
3455
3456impl<'d> serde::Deserialize<'d> for PackageVersion {
3457  fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
3458    struct PackageVersionVisitor;
3459
3460    impl Visitor<'_> for PackageVersionVisitor {
3461      type Value = PackageVersion;
3462
3463      fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3464        write!(
3465          formatter,
3466          "a semver string or a path to a package.json file"
3467        )
3468      }
3469
3470      fn visit_str<E: DeError>(self, value: &str) -> Result<PackageVersion, E> {
3471        let path = PathBuf::from(value);
3472        if path.exists() {
3473          let json_str = read_to_string(&path)
3474            .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3475          let package_json: serde_json::Value = serde_json::from_str(&json_str)
3476            .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3477          if let Some(obj) = package_json.as_object() {
3478            let version = obj
3479              .get("version")
3480              .ok_or_else(|| DeError::custom("JSON must contain a `version` field"))?
3481              .as_str()
3482              .ok_or_else(|| {
3483                DeError::custom(format!("`{} > version` must be a string", path.display()))
3484              })?;
3485            Ok(PackageVersion(
3486              Version::from_str(version)
3487                .map_err(|_| {
3488                  DeError::custom("`tauri.conf.json > version` must be a semver string")
3489                })?
3490                .to_string(),
3491            ))
3492          } else {
3493            Err(DeError::custom(
3494              "`tauri.conf.json > version` value is not a path to a JSON object",
3495            ))
3496          }
3497        } else {
3498          Ok(PackageVersion(
3499            Version::from_str(value)
3500              .map_err(|_| DeError::custom("`tauri.conf.json > version` must be a semver string"))?
3501              .to_string(),
3502          ))
3503        }
3504      }
3505    }
3506
3507    deserializer.deserialize_string(PackageVersionVisitor {})
3508  }
3509}
3510
3511fn version_deserializer<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
3512where
3513  D: Deserializer<'de>,
3514{
3515  Option::<PackageVersion>::deserialize(deserializer).map(|v| v.map(|v| v.0))
3516}
3517
3518/// The Tauri configuration object.
3519/// It is read from a file where you can define your frontend assets,
3520/// configure the bundler and define a tray icon.
3521///
3522/// The configuration file is generated by the
3523/// [`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in
3524/// your Tauri application source directory (src-tauri).
3525///
3526/// Once generated, you may modify it at will to customize your Tauri application.
3527///
3528/// ## File Formats
3529///
3530/// By default, the configuration is defined as a JSON file named `tauri.conf.json`.
3531///
3532/// Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.
3533/// The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.
3534/// The TOML file name is `Tauri.toml`.
3535///
3536/// ## Platform-Specific Configuration
3537///
3538/// In addition to the default configuration file, Tauri can
3539/// read a platform-specific configuration from `tauri.linux.conf.json`,
3540/// `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`
3541/// (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),
3542/// which gets merged with the main configuration object.
3543///
3544/// ## Configuration Structure
3545///
3546/// The configuration is composed of the following objects:
3547///
3548/// - [`app`](#appconfig): The Tauri configuration
3549/// - [`build`](#buildconfig): The build configuration
3550/// - [`bundle`](#bundleconfig): The bundle configurations
3551/// - [`plugins`](#pluginconfig): The plugins configuration
3552///
3553/// Example tauri.config.json file:
3554///
3555/// ```json
3556/// {
3557///   "productName": "tauri-app",
3558///   "version": "0.1.0",
3559///   "build": {
3560///     "beforeBuildCommand": "",
3561///     "beforeDevCommand": "",
3562///     "devUrl": "http://localhost:3000",
3563///     "frontendDist": "../dist"
3564///   },
3565///   "app": {
3566///     "security": {
3567///       "csp": null
3568///     },
3569///     "windows": [
3570///       {
3571///         "fullscreen": false,
3572///         "height": 600,
3573///         "resizable": true,
3574///         "title": "Tauri App",
3575///         "width": 800
3576///       }
3577///     ]
3578///   },
3579///   "bundle": {},
3580///   "plugins": {}
3581/// }
3582/// ```
3583#[skip_serializing_none]
3584#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3585#[cfg_attr(feature = "schema", derive(JsonSchema))]
3586#[serde(rename_all = "camelCase", deny_unknown_fields)]
3587pub struct Config {
3588  /// The JSON schema for the Tauri config.
3589  #[serde(rename = "$schema")]
3590  pub schema: Option<String>,
3591  /// App name.
3592  #[serde(alias = "product-name")]
3593  #[cfg_attr(feature = "schema", validate(regex(pattern = "^[^/\\:*?\"<>|]+$")))]
3594  pub product_name: Option<String>,
3595  /// Overrides app's main binary filename.
3596  ///
3597  /// By default, Tauri uses the output binary from `cargo`, by setting this, we will rename that binary in `tauri-cli`'s
3598  /// `tauri build` command, and target `tauri bundle` to it
3599  ///
3600  /// If possible, change the [`package name`] or set the [`name field`] instead,
3601  /// and if that's not enough and you're using nightly, consider using the [`different-binary-name`] feature instead
3602  ///
3603  /// Note: this config should not include the binary extension (e.g. `.exe`), we'll add that for you
3604  ///
3605  /// [`package name`]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field
3606  /// [`name field`]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-name-field
3607  /// [`different-binary-name`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#different-binary-name
3608  #[serde(alias = "main-binary-name")]
3609  pub main_binary_name: Option<String>,
3610  /// App version. It is a semver version number or a path to a `package.json` file containing the `version` field.
3611  ///
3612  /// If removed the version number from `Cargo.toml` is used.
3613  /// It's recommended to manage the app versioning in the Tauri config.
3614  ///
3615  /// ## Platform-specific
3616  ///
3617  /// - **macOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.
3618  ///    You can set an specific bundle version using [`bundle > macOS > bundleVersion`](MacConfig::bundle_version).
3619  /// - **iOS**: Translates to the bundle's CFBundleShortVersionString property and is used as the default CFBundleVersion.
3620  ///    You can set an specific bundle version using [`bundle > iOS > bundleVersion`](IosConfig::bundle_version).
3621  ///    The `tauri ios build` CLI command has a `--build-number <number>` option that lets you append a build number to the app version.
3622  /// - **Android**: By default version 1.0 is used. You can set a version code using [`bundle > android > versionCode`](AndroidConfig::version_code).
3623  ///
3624  /// By default version 1.0 is used on Android.
3625  #[serde(deserialize_with = "version_deserializer", default)]
3626  pub version: Option<String>,
3627  /// The application identifier in reverse domain name notation (e.g. `com.tauri.example`).
3628  /// This string must be unique across applications since it is used in system configurations like
3629  /// the bundle ID and path to the webview data directory.
3630  /// This string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-),
3631  /// and periods (.).
3632  pub identifier: String,
3633  /// The App configuration.
3634  #[serde(default)]
3635  pub app: AppConfig,
3636  /// The build configuration.
3637  #[serde(default)]
3638  pub build: BuildConfig,
3639  /// The bundler configuration.
3640  #[serde(default)]
3641  pub bundle: BundleConfig,
3642  /// The plugins config.
3643  #[serde(default)]
3644  pub plugins: PluginConfig,
3645}
3646
3647/// The plugin configs holds a HashMap mapping a plugin name to its configuration object.
3648///
3649/// See more: <https://v2.tauri.app/reference/config/#pluginconfig>
3650#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
3651#[cfg_attr(feature = "schema", derive(JsonSchema))]
3652pub struct PluginConfig(pub HashMap<String, JsonValue>);
3653
3654/// Implement `ToTokens` for all config structs, allowing a literal `Config` to be built.
3655///
3656/// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can
3657/// then be consumed by another crate. Useful for passing a config to both the build script and the
3658/// application using tauri while only parsing it once (in the build script).
3659#[cfg(any(feature = "build", feature = "build-2"))]
3660mod build {
3661  use super::*;
3662  use crate::{literal_struct, tokens::*};
3663  use proc_macro2::TokenStream;
3664  use quote::{quote, ToTokens, TokenStreamExt};
3665  use std::convert::identity;
3666
3667  impl ToTokens for WebviewUrl {
3668    fn to_tokens(&self, tokens: &mut TokenStream) {
3669      let prefix = quote! { ::tauri::utils::config::WebviewUrl };
3670
3671      tokens.append_all(match self {
3672        Self::App(path) => {
3673          let path = path_buf_lit(path);
3674          quote! { #prefix::App(#path) }
3675        }
3676        Self::External(url) => {
3677          let url = url_lit(url);
3678          quote! { #prefix::External(#url) }
3679        }
3680        Self::CustomProtocol(url) => {
3681          let url = url_lit(url);
3682          quote! { #prefix::CustomProtocol(#url) }
3683        }
3684      })
3685    }
3686  }
3687
3688  impl ToTokens for BackgroundThrottlingPolicy {
3689    fn to_tokens(&self, tokens: &mut TokenStream) {
3690      let prefix = quote! { ::tauri::utils::config::BackgroundThrottlingPolicy };
3691      tokens.append_all(match self {
3692        Self::Disabled => quote! { #prefix::Disabled },
3693        Self::Throttle => quote! { #prefix::Throttle },
3694        Self::Suspend => quote! { #prefix::Suspend },
3695      })
3696    }
3697  }
3698
3699  impl ToTokens for crate::Theme {
3700    fn to_tokens(&self, tokens: &mut TokenStream) {
3701      let prefix = quote! { ::tauri::utils::Theme };
3702
3703      tokens.append_all(match self {
3704        Self::Light => quote! { #prefix::Light },
3705        Self::Dark => quote! { #prefix::Dark },
3706      })
3707    }
3708  }
3709
3710  impl ToTokens for Color {
3711    fn to_tokens(&self, tokens: &mut TokenStream) {
3712      let Color(r, g, b, a) = self;
3713      tokens.append_all(quote! {::tauri::utils::config::Color(#r,#g,#b,#a)});
3714    }
3715  }
3716  impl ToTokens for WindowEffectsConfig {
3717    fn to_tokens(&self, tokens: &mut TokenStream) {
3718      let effects = vec_lit(self.effects.clone(), |d| d);
3719      let state = opt_lit(self.state.as_ref());
3720      let radius = opt_lit(self.radius.as_ref());
3721      let color = opt_lit(self.color.as_ref());
3722
3723      literal_struct!(
3724        tokens,
3725        ::tauri::utils::config::WindowEffectsConfig,
3726        effects,
3727        state,
3728        radius,
3729        color
3730      )
3731    }
3732  }
3733
3734  impl ToTokens for crate::TitleBarStyle {
3735    fn to_tokens(&self, tokens: &mut TokenStream) {
3736      let prefix = quote! { ::tauri::utils::TitleBarStyle };
3737
3738      tokens.append_all(match self {
3739        Self::Visible => quote! { #prefix::Visible },
3740        Self::Transparent => quote! { #prefix::Transparent },
3741        Self::Overlay => quote! { #prefix::Overlay },
3742      })
3743    }
3744  }
3745
3746  impl ToTokens for LogicalPosition {
3747    fn to_tokens(&self, tokens: &mut TokenStream) {
3748      let LogicalPosition { x, y } = self;
3749      literal_struct!(tokens, ::tauri::utils::config::LogicalPosition, x, y)
3750    }
3751  }
3752
3753  impl ToTokens for crate::WindowEffect {
3754    fn to_tokens(&self, tokens: &mut TokenStream) {
3755      let prefix = quote! { ::tauri::utils::WindowEffect };
3756
3757      #[allow(deprecated)]
3758      tokens.append_all(match self {
3759        WindowEffect::AppearanceBased => quote! { #prefix::AppearanceBased},
3760        WindowEffect::Light => quote! { #prefix::Light},
3761        WindowEffect::Dark => quote! { #prefix::Dark},
3762        WindowEffect::MediumLight => quote! { #prefix::MediumLight},
3763        WindowEffect::UltraDark => quote! { #prefix::UltraDark},
3764        WindowEffect::Titlebar => quote! { #prefix::Titlebar},
3765        WindowEffect::Selection => quote! { #prefix::Selection},
3766        WindowEffect::Menu => quote! { #prefix::Menu},
3767        WindowEffect::Popover => quote! { #prefix::Popover},
3768        WindowEffect::Sidebar => quote! { #prefix::Sidebar},
3769        WindowEffect::HeaderView => quote! { #prefix::HeaderView},
3770        WindowEffect::Sheet => quote! { #prefix::Sheet},
3771        WindowEffect::WindowBackground => quote! { #prefix::WindowBackground},
3772        WindowEffect::HudWindow => quote! { #prefix::HudWindow},
3773        WindowEffect::FullScreenUI => quote! { #prefix::FullScreenUI},
3774        WindowEffect::Tooltip => quote! { #prefix::Tooltip},
3775        WindowEffect::ContentBackground => quote! { #prefix::ContentBackground},
3776        WindowEffect::UnderWindowBackground => quote! { #prefix::UnderWindowBackground},
3777        WindowEffect::UnderPageBackground => quote! { #prefix::UnderPageBackground},
3778        WindowEffect::Mica => quote! { #prefix::Mica},
3779        WindowEffect::MicaDark => quote! { #prefix::MicaDark},
3780        WindowEffect::MicaLight => quote! { #prefix::MicaLight},
3781        WindowEffect::Blur => quote! { #prefix::Blur},
3782        WindowEffect::Acrylic => quote! { #prefix::Acrylic},
3783        WindowEffect::Tabbed => quote! { #prefix::Tabbed },
3784        WindowEffect::TabbedDark => quote! { #prefix::TabbedDark },
3785        WindowEffect::TabbedLight => quote! { #prefix::TabbedLight },
3786      })
3787    }
3788  }
3789
3790  impl ToTokens for crate::WindowEffectState {
3791    fn to_tokens(&self, tokens: &mut TokenStream) {
3792      let prefix = quote! { ::tauri::utils::WindowEffectState };
3793
3794      #[allow(deprecated)]
3795      tokens.append_all(match self {
3796        WindowEffectState::Active => quote! { #prefix::Active},
3797        WindowEffectState::FollowsWindowActiveState => quote! { #prefix::FollowsWindowActiveState},
3798        WindowEffectState::Inactive => quote! { #prefix::Inactive},
3799      })
3800    }
3801  }
3802
3803  impl ToTokens for PreventOverflowMargin {
3804    fn to_tokens(&self, tokens: &mut TokenStream) {
3805      let width = self.width;
3806      let height = self.height;
3807
3808      literal_struct!(
3809        tokens,
3810        ::tauri::utils::config::PreventOverflowMargin,
3811        width,
3812        height
3813      )
3814    }
3815  }
3816
3817  impl ToTokens for PreventOverflowConfig {
3818    fn to_tokens(&self, tokens: &mut TokenStream) {
3819      let prefix = quote! { ::tauri::utils::config::PreventOverflowConfig };
3820
3821      #[allow(deprecated)]
3822      tokens.append_all(match self {
3823        Self::Enable(enable) => quote! { #prefix::Enable(#enable) },
3824        Self::Margin(margin) => quote! { #prefix::Margin(#margin) },
3825      })
3826    }
3827  }
3828
3829  impl ToTokens for ScrollBarStyle {
3830    fn to_tokens(&self, tokens: &mut TokenStream) {
3831      let prefix = quote! { ::tauri::utils::config::ScrollBarStyle };
3832
3833      tokens.append_all(match self {
3834        Self::Default => quote! { #prefix::Default },
3835        Self::FluentOverlay => quote! { #prefix::FluentOverlay },
3836      })
3837    }
3838  }
3839
3840  impl ToTokens for WindowConfig {
3841    fn to_tokens(&self, tokens: &mut TokenStream) {
3842      let label = str_lit(&self.label);
3843      let create = &self.create;
3844      let url = &self.url;
3845      let user_agent = opt_str_lit(self.user_agent.as_ref());
3846      let drag_drop_enabled = self.drag_drop_enabled;
3847      let center = self.center;
3848      let x = opt_lit(self.x.as_ref());
3849      let y = opt_lit(self.y.as_ref());
3850      let width = self.width;
3851      let height = self.height;
3852      let min_width = opt_lit(self.min_width.as_ref());
3853      let min_height = opt_lit(self.min_height.as_ref());
3854      let max_width = opt_lit(self.max_width.as_ref());
3855      let max_height = opt_lit(self.max_height.as_ref());
3856      let prevent_overflow = opt_lit(self.prevent_overflow.as_ref());
3857      let resizable = self.resizable;
3858      let maximizable = self.maximizable;
3859      let minimizable = self.minimizable;
3860      let closable = self.closable;
3861      let title = str_lit(&self.title);
3862      let proxy_url = opt_lit(self.proxy_url.as_ref().map(url_lit).as_ref());
3863      let fullscreen = self.fullscreen;
3864      let focus = self.focus;
3865      let focusable = self.focusable;
3866      let transparent = self.transparent;
3867      let maximized = self.maximized;
3868      let visible = self.visible;
3869      let decorations = self.decorations;
3870      let always_on_bottom = self.always_on_bottom;
3871      let always_on_top = self.always_on_top;
3872      let visible_on_all_workspaces = self.visible_on_all_workspaces;
3873      let content_protected = self.content_protected;
3874      let skip_taskbar = self.skip_taskbar;
3875      let window_classname = opt_str_lit(self.window_classname.as_ref());
3876      let theme = opt_lit(self.theme.as_ref());
3877      let title_bar_style = &self.title_bar_style;
3878      let traffic_light_position = opt_lit(self.traffic_light_position.as_ref());
3879      let hidden_title = self.hidden_title;
3880      let accept_first_mouse = self.accept_first_mouse;
3881      let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
3882      let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref());
3883      let shadow = self.shadow;
3884      let window_effects = opt_lit(self.window_effects.as_ref());
3885      let incognito = self.incognito;
3886      let parent = opt_str_lit(self.parent.as_ref());
3887      let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled;
3888      let browser_extensions_enabled = self.browser_extensions_enabled;
3889      let use_https_scheme = self.use_https_scheme;
3890      let devtools = opt_lit(self.devtools.as_ref());
3891      let background_color = opt_lit(self.background_color.as_ref());
3892      let background_throttling = opt_lit(self.background_throttling.as_ref());
3893      let javascript_disabled = self.javascript_disabled;
3894      let allow_link_preview = self.allow_link_preview;
3895      let disable_input_accessory_view = self.disable_input_accessory_view;
3896      let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref());
3897      let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity);
3898      let scroll_bar_style = &self.scroll_bar_style;
3899      let activity_name = opt_lit(self.activity_name.as_ref());
3900      let created_by_activity_name = opt_lit(self.created_by_activity_name.as_ref());
3901      let requested_by_scene_identifier = opt_lit(self.requested_by_scene_identifier.as_ref());
3902      let general_autofill_enabled = self.general_autofill_enabled;
3903
3904      literal_struct!(
3905        tokens,
3906        ::tauri::utils::config::WindowConfig,
3907        label,
3908        url,
3909        create,
3910        user_agent,
3911        drag_drop_enabled,
3912        center,
3913        x,
3914        y,
3915        width,
3916        height,
3917        min_width,
3918        min_height,
3919        max_width,
3920        max_height,
3921        prevent_overflow,
3922        resizable,
3923        maximizable,
3924        minimizable,
3925        closable,
3926        title,
3927        proxy_url,
3928        fullscreen,
3929        focus,
3930        focusable,
3931        transparent,
3932        maximized,
3933        visible,
3934        decorations,
3935        always_on_bottom,
3936        always_on_top,
3937        visible_on_all_workspaces,
3938        content_protected,
3939        skip_taskbar,
3940        window_classname,
3941        theme,
3942        title_bar_style,
3943        traffic_light_position,
3944        hidden_title,
3945        accept_first_mouse,
3946        tabbing_identifier,
3947        additional_browser_args,
3948        shadow,
3949        window_effects,
3950        incognito,
3951        parent,
3952        zoom_hotkeys_enabled,
3953        browser_extensions_enabled,
3954        use_https_scheme,
3955        devtools,
3956        background_color,
3957        background_throttling,
3958        javascript_disabled,
3959        allow_link_preview,
3960        disable_input_accessory_view,
3961        data_directory,
3962        data_store_identifier,
3963        scroll_bar_style,
3964        activity_name,
3965        created_by_activity_name,
3966        requested_by_scene_identifier,
3967        general_autofill_enabled
3968      );
3969    }
3970  }
3971
3972  impl ToTokens for PatternKind {
3973    fn to_tokens(&self, tokens: &mut TokenStream) {
3974      let prefix = quote! { ::tauri::utils::config::PatternKind };
3975
3976      tokens.append_all(match self {
3977        Self::Brownfield => quote! { #prefix::Brownfield },
3978        #[cfg(not(feature = "isolation"))]
3979        Self::Isolation { dir: _ } => quote! { #prefix::Brownfield },
3980        #[cfg(feature = "isolation")]
3981        Self::Isolation { dir } => {
3982          let dir = path_buf_lit(dir);
3983          quote! { #prefix::Isolation { dir: #dir } }
3984        }
3985      })
3986    }
3987  }
3988
3989  impl ToTokens for WebviewInstallMode {
3990    fn to_tokens(&self, tokens: &mut TokenStream) {
3991      let prefix = quote! { ::tauri::utils::config::WebviewInstallMode };
3992
3993      tokens.append_all(match self {
3994        Self::Skip => quote! { #prefix::Skip },
3995        Self::DownloadBootstrapper { silent } => {
3996          quote! { #prefix::DownloadBootstrapper { silent: #silent } }
3997        }
3998        Self::EmbedBootstrapper { silent } => {
3999          quote! { #prefix::EmbedBootstrapper { silent: #silent } }
4000        }
4001        Self::OfflineInstaller { silent } => {
4002          quote! { #prefix::OfflineInstaller { silent: #silent } }
4003        }
4004        Self::FixedRuntime { path } => {
4005          let path = path_buf_lit(path);
4006          quote! { #prefix::FixedRuntime { path: #path } }
4007        }
4008      })
4009    }
4010  }
4011
4012  impl ToTokens for WindowsConfig {
4013    fn to_tokens(&self, tokens: &mut TokenStream) {
4014      let webview_install_mode = &self.webview_install_mode;
4015      tokens.append_all(quote! { ::tauri::utils::config::WindowsConfig {
4016        webview_install_mode: #webview_install_mode,
4017        ..Default::default()
4018      }})
4019    }
4020  }
4021
4022  impl ToTokens for BundleConfig {
4023    fn to_tokens(&self, tokens: &mut TokenStream) {
4024      let publisher = quote!(None);
4025      let homepage = quote!(None);
4026      let icon = vec_lit(&self.icon, str_lit);
4027      let active = self.active;
4028      let targets = quote!(Default::default());
4029      let create_updater_artifacts = quote!(Default::default());
4030      let resources = quote!(None);
4031      let copyright = quote!(None);
4032      let category = quote!(None);
4033      let file_associations = quote!(None);
4034      let short_description = quote!(None);
4035      let long_description = quote!(None);
4036      let use_local_tools_dir = self.use_local_tools_dir;
4037      let external_bin = opt_vec_lit(self.external_bin.as_ref(), str_lit);
4038      let windows = &self.windows;
4039      let license = opt_str_lit(self.license.as_ref());
4040      let license_file = opt_lit(self.license_file.as_ref().map(path_buf_lit).as_ref());
4041      let linux = quote!(Default::default());
4042      let macos = quote!(Default::default());
4043      let ios = quote!(Default::default());
4044      let android = quote!(Default::default());
4045
4046      literal_struct!(
4047        tokens,
4048        ::tauri::utils::config::BundleConfig,
4049        active,
4050        publisher,
4051        homepage,
4052        icon,
4053        targets,
4054        create_updater_artifacts,
4055        resources,
4056        copyright,
4057        category,
4058        license,
4059        license_file,
4060        file_associations,
4061        short_description,
4062        long_description,
4063        use_local_tools_dir,
4064        external_bin,
4065        windows,
4066        linux,
4067        macos,
4068        ios,
4069        android
4070      );
4071    }
4072  }
4073
4074  impl ToTokens for FrontendDist {
4075    fn to_tokens(&self, tokens: &mut TokenStream) {
4076      let prefix = quote! { ::tauri::utils::config::FrontendDist };
4077
4078      tokens.append_all(match self {
4079        Self::Url(url) => {
4080          let url = url_lit(url);
4081          quote! { #prefix::Url(#url) }
4082        }
4083        Self::Directory(path) => {
4084          let path = path_buf_lit(path);
4085          quote! { #prefix::Directory(#path) }
4086        }
4087        Self::Files(files) => {
4088          let files = vec_lit(files, path_buf_lit);
4089          quote! { #prefix::Files(#files) }
4090        }
4091      })
4092    }
4093  }
4094
4095  impl ToTokens for RunnerConfig {
4096    fn to_tokens(&self, tokens: &mut TokenStream) {
4097      let prefix = quote! { ::tauri::utils::config::RunnerConfig };
4098
4099      tokens.append_all(match self {
4100        Self::String(cmd) => {
4101          let cmd = cmd.as_str();
4102          quote!(#prefix::String(#cmd.into()))
4103        }
4104        Self::Object { cmd, cwd, args } => {
4105          let cmd = cmd.as_str();
4106          let cwd = opt_str_lit(cwd.as_ref());
4107          let args = opt_lit(args.as_ref().map(|v| vec_lit(v, str_lit)).as_ref());
4108          quote!(#prefix::Object {
4109            cmd: #cmd.into(),
4110            cwd: #cwd,
4111            args: #args,
4112          })
4113        }
4114      })
4115    }
4116  }
4117
4118  impl ToTokens for BuildConfig {
4119    fn to_tokens(&self, tokens: &mut TokenStream) {
4120      let dev_url = opt_lit(self.dev_url.as_ref().map(url_lit).as_ref());
4121      let frontend_dist = opt_lit(self.frontend_dist.as_ref());
4122      let runner = opt_lit(self.runner.as_ref());
4123      let before_dev_command = quote!(None);
4124      let before_build_command = quote!(None);
4125      let before_bundle_command = quote!(None);
4126      let features = quote!(None);
4127      let remove_unused_commands = quote!(false);
4128      let additional_watch_folders = quote!(Vec::new());
4129
4130      literal_struct!(
4131        tokens,
4132        ::tauri::utils::config::BuildConfig,
4133        runner,
4134        dev_url,
4135        frontend_dist,
4136        before_dev_command,
4137        before_build_command,
4138        before_bundle_command,
4139        features,
4140        remove_unused_commands,
4141        additional_watch_folders
4142      );
4143    }
4144  }
4145
4146  impl ToTokens for CspDirectiveSources {
4147    fn to_tokens(&self, tokens: &mut TokenStream) {
4148      let prefix = quote! { ::tauri::utils::config::CspDirectiveSources };
4149
4150      tokens.append_all(match self {
4151        Self::Inline(sources) => {
4152          let sources = sources.as_str();
4153          quote!(#prefix::Inline(#sources.into()))
4154        }
4155        Self::List(list) => {
4156          let list = vec_lit(list, str_lit);
4157          quote!(#prefix::List(#list))
4158        }
4159      })
4160    }
4161  }
4162
4163  impl ToTokens for Csp {
4164    fn to_tokens(&self, tokens: &mut TokenStream) {
4165      let prefix = quote! { ::tauri::utils::config::Csp };
4166
4167      tokens.append_all(match self {
4168        Self::Policy(policy) => {
4169          let policy = policy.as_str();
4170          quote!(#prefix::Policy(#policy.into()))
4171        }
4172        Self::DirectiveMap(list) => {
4173          // Pass a sorted vec so the HashMap constructor is deterministic
4174          // see: https://github.com/tauri-apps/tauri/issues/14978
4175          // TODO: Remove this in v3, use a BTreeMap instead of a HashMap
4176          let mut sorted: Vec<_> = list.iter().collect();
4177          sorted.sort_by_key(|(k, _)| *k);
4178          let map = map_lit(
4179            quote! { ::std::collections::HashMap },
4180            sorted,
4181            str_lit,
4182            identity,
4183          );
4184          quote!(#prefix::DirectiveMap(#map))
4185        }
4186      })
4187    }
4188  }
4189
4190  impl ToTokens for DisabledCspModificationKind {
4191    fn to_tokens(&self, tokens: &mut TokenStream) {
4192      let prefix = quote! { ::tauri::utils::config::DisabledCspModificationKind };
4193
4194      tokens.append_all(match self {
4195        Self::Flag(flag) => {
4196          quote! { #prefix::Flag(#flag) }
4197        }
4198        Self::List(directives) => {
4199          let directives = vec_lit(directives, str_lit);
4200          quote! { #prefix::List(#directives) }
4201        }
4202      });
4203    }
4204  }
4205
4206  impl ToTokens for CapabilityEntry {
4207    fn to_tokens(&self, tokens: &mut TokenStream) {
4208      let prefix = quote! { ::tauri::utils::config::CapabilityEntry };
4209
4210      tokens.append_all(match self {
4211        Self::Inlined(capability) => {
4212          quote! { #prefix::Inlined(#capability) }
4213        }
4214        Self::Reference(id) => {
4215          let id = str_lit(id);
4216          quote! { #prefix::Reference(#id) }
4217        }
4218      });
4219    }
4220  }
4221
4222  impl ToTokens for HeaderSource {
4223    fn to_tokens(&self, tokens: &mut TokenStream) {
4224      let prefix = quote! { ::tauri::utils::config::HeaderSource };
4225
4226      tokens.append_all(match self {
4227        Self::Inline(s) => {
4228          let line = s.as_str();
4229          quote!(#prefix::Inline(#line.into()))
4230        }
4231        Self::List(l) => {
4232          let list = vec_lit(l, str_lit);
4233          quote!(#prefix::List(#list))
4234        }
4235        Self::Map(m) => {
4236          // Pass a sorted vec so the HashMap constructor is deterministic
4237          // see: https://github.com/tauri-apps/tauri/issues/14978
4238          // TODO: Remove this in v3, use a BTreeMap instead of a HashMap
4239          let mut sorted: Vec<_> = m.iter().collect();
4240          sorted.sort_by_key(|(k, _)| *k);
4241          let map = map_lit(
4242            quote! { ::std::collections::HashMap },
4243            sorted,
4244            str_lit,
4245            str_lit,
4246          );
4247          quote!(#prefix::Map(#map))
4248        }
4249      })
4250    }
4251  }
4252
4253  impl ToTokens for HeaderConfig {
4254    fn to_tokens(&self, tokens: &mut TokenStream) {
4255      let access_control_allow_credentials =
4256        opt_lit(self.access_control_allow_credentials.as_ref());
4257      let access_control_allow_headers = opt_lit(self.access_control_allow_headers.as_ref());
4258      let access_control_allow_methods = opt_lit(self.access_control_allow_methods.as_ref());
4259      let access_control_expose_headers = opt_lit(self.access_control_expose_headers.as_ref());
4260      let access_control_max_age = opt_lit(self.access_control_max_age.as_ref());
4261      let cross_origin_embedder_policy = opt_lit(self.cross_origin_embedder_policy.as_ref());
4262      let cross_origin_opener_policy = opt_lit(self.cross_origin_opener_policy.as_ref());
4263      let cross_origin_resource_policy = opt_lit(self.cross_origin_resource_policy.as_ref());
4264      let permissions_policy = opt_lit(self.permissions_policy.as_ref());
4265      let service_worker_allowed = opt_lit(self.service_worker_allowed.as_ref());
4266      let timing_allow_origin = opt_lit(self.timing_allow_origin.as_ref());
4267      let x_content_type_options = opt_lit(self.x_content_type_options.as_ref());
4268      let tauri_custom_header = opt_lit(self.tauri_custom_header.as_ref());
4269
4270      literal_struct!(
4271        tokens,
4272        ::tauri::utils::config::HeaderConfig,
4273        access_control_allow_credentials,
4274        access_control_allow_headers,
4275        access_control_allow_methods,
4276        access_control_expose_headers,
4277        access_control_max_age,
4278        cross_origin_embedder_policy,
4279        cross_origin_opener_policy,
4280        cross_origin_resource_policy,
4281        permissions_policy,
4282        service_worker_allowed,
4283        timing_allow_origin,
4284        x_content_type_options,
4285        tauri_custom_header
4286      );
4287    }
4288  }
4289
4290  impl ToTokens for SecurityConfig {
4291    fn to_tokens(&self, tokens: &mut TokenStream) {
4292      let csp = opt_lit(self.csp.as_ref());
4293      let dev_csp = opt_lit(self.dev_csp.as_ref());
4294      let freeze_prototype = self.freeze_prototype;
4295      let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;
4296      let asset_protocol = &self.asset_protocol;
4297      let pattern = &self.pattern;
4298      let capabilities = vec_lit(&self.capabilities, identity);
4299      let headers = opt_lit(self.headers.as_ref());
4300
4301      literal_struct!(
4302        tokens,
4303        ::tauri::utils::config::SecurityConfig,
4304        csp,
4305        dev_csp,
4306        freeze_prototype,
4307        dangerous_disable_asset_csp_modification,
4308        asset_protocol,
4309        pattern,
4310        capabilities,
4311        headers
4312      );
4313    }
4314  }
4315
4316  impl ToTokens for TrayIconConfig {
4317    fn to_tokens(&self, tokens: &mut TokenStream) {
4318      // For [`Self::menu_on_left_click`]
4319      tokens.append_all(quote!(#[allow(deprecated)]));
4320
4321      let id = opt_str_lit(self.id.as_ref());
4322      let icon_as_template = self.icon_as_template;
4323      #[allow(deprecated)]
4324      let menu_on_left_click = self.menu_on_left_click;
4325      let show_menu_on_left_click = self.show_menu_on_left_click;
4326      let icon_path = path_buf_lit(&self.icon_path);
4327      let title = opt_str_lit(self.title.as_ref());
4328      let tooltip = opt_str_lit(self.tooltip.as_ref());
4329      literal_struct!(
4330        tokens,
4331        ::tauri::utils::config::TrayIconConfig,
4332        id,
4333        icon_path,
4334        icon_as_template,
4335        menu_on_left_click,
4336        show_menu_on_left_click,
4337        title,
4338        tooltip
4339      );
4340    }
4341  }
4342
4343  impl ToTokens for FsScope {
4344    fn to_tokens(&self, tokens: &mut TokenStream) {
4345      let prefix = quote! { ::tauri::utils::config::FsScope };
4346
4347      tokens.append_all(match self {
4348        Self::AllowedPaths(allow) => {
4349          let allowed_paths = vec_lit(allow, path_buf_lit);
4350          quote! { #prefix::AllowedPaths(#allowed_paths) }
4351        }
4352        Self::Scope { allow, deny , require_literal_leading_dot} => {
4353          let allow = vec_lit(allow, path_buf_lit);
4354          let deny = vec_lit(deny, path_buf_lit);
4355          let  require_literal_leading_dot = opt_lit(require_literal_leading_dot.as_ref());
4356          quote! { #prefix::Scope { allow: #allow, deny: #deny, require_literal_leading_dot: #require_literal_leading_dot } }
4357        }
4358      });
4359    }
4360  }
4361
4362  impl ToTokens for AssetProtocolConfig {
4363    fn to_tokens(&self, tokens: &mut TokenStream) {
4364      let scope = &self.scope;
4365      tokens.append_all(quote! { ::tauri::utils::config::AssetProtocolConfig { scope: #scope, ..Default::default() } })
4366    }
4367  }
4368
4369  impl ToTokens for AppConfig {
4370    fn to_tokens(&self, tokens: &mut TokenStream) {
4371      let windows = vec_lit(&self.windows, identity);
4372      let security = &self.security;
4373      let tray_icon = opt_lit(self.tray_icon.as_ref());
4374      let macos_private_api = self.macos_private_api;
4375      let with_global_tauri = self.with_global_tauri;
4376      let enable_gtk_app_id = self.enable_gtk_app_id;
4377
4378      literal_struct!(
4379        tokens,
4380        ::tauri::utils::config::AppConfig,
4381        windows,
4382        security,
4383        tray_icon,
4384        macos_private_api,
4385        with_global_tauri,
4386        enable_gtk_app_id
4387      );
4388    }
4389  }
4390
4391  impl ToTokens for PluginConfig {
4392    fn to_tokens(&self, tokens: &mut TokenStream) {
4393      // Pass a sorted vec so the HashMap constructor is deterministic
4394      // see: https://github.com/tauri-apps/tauri/issues/14978
4395      // TODO: Remove this in v3, use a BTreeMap instead of a HashMap
4396      let mut sorted: Vec<_> = self.0.iter().collect();
4397      sorted.sort_by_key(|(k, _)| *k);
4398      let config = map_lit(
4399        quote! { ::std::collections::HashMap },
4400        sorted,
4401        str_lit,
4402        json_value_lit,
4403      );
4404      tokens.append_all(quote! { ::tauri::utils::config::PluginConfig(#config) })
4405    }
4406  }
4407
4408  impl ToTokens for Config {
4409    fn to_tokens(&self, tokens: &mut TokenStream) {
4410      let schema = quote!(None);
4411      let product_name = opt_str_lit(self.product_name.as_ref());
4412      let main_binary_name = opt_str_lit(self.main_binary_name.as_ref());
4413      let version = opt_str_lit(self.version.as_ref());
4414      let identifier = str_lit(&self.identifier);
4415      let app = &self.app;
4416      let build = &self.build;
4417      let bundle = &self.bundle;
4418      let plugins = &self.plugins;
4419
4420      literal_struct!(
4421        tokens,
4422        ::tauri::utils::config::Config,
4423        schema,
4424        product_name,
4425        main_binary_name,
4426        version,
4427        identifier,
4428        app,
4429        build,
4430        bundle,
4431        plugins
4432      );
4433    }
4434  }
4435}
4436
4437#[cfg(test)]
4438mod test {
4439  use super::*;
4440
4441  // TODO: create a test that compares a config to a json config
4442
4443  #[test]
4444  // test all of the default functions
4445  fn test_defaults() {
4446    // get default app config
4447    let a_config = AppConfig::default();
4448    // get default build config
4449    let b_config = BuildConfig::default();
4450    // get default window
4451    let d_windows: Vec<WindowConfig> = vec![];
4452    // get default bundle
4453    let d_bundle = BundleConfig::default();
4454
4455    // create a tauri config.
4456    let app = AppConfig {
4457      windows: vec![],
4458      security: SecurityConfig {
4459        csp: None,
4460        dev_csp: None,
4461        freeze_prototype: false,
4462        dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
4463        asset_protocol: AssetProtocolConfig::default(),
4464        pattern: Default::default(),
4465        capabilities: Vec::new(),
4466        headers: None,
4467      },
4468      tray_icon: None,
4469      macos_private_api: false,
4470      with_global_tauri: false,
4471      enable_gtk_app_id: false,
4472    };
4473
4474    // create a build config
4475    let build = BuildConfig {
4476      runner: None,
4477      dev_url: None,
4478      frontend_dist: None,
4479      before_dev_command: None,
4480      before_build_command: None,
4481      before_bundle_command: None,
4482      features: None,
4483      remove_unused_commands: false,
4484      additional_watch_folders: Vec::new(),
4485    };
4486
4487    // create a bundle config
4488    let bundle = BundleConfig {
4489      active: false,
4490      targets: Default::default(),
4491      create_updater_artifacts: Default::default(),
4492      publisher: None,
4493      homepage: None,
4494      icon: Vec::new(),
4495      resources: None,
4496      copyright: None,
4497      category: None,
4498      file_associations: None,
4499      short_description: None,
4500      long_description: None,
4501      use_local_tools_dir: false,
4502      license: None,
4503      license_file: None,
4504      linux: Default::default(),
4505      macos: Default::default(),
4506      external_bin: None,
4507      windows: Default::default(),
4508      ios: Default::default(),
4509      android: Default::default(),
4510    };
4511
4512    // test the configs
4513    assert_eq!(a_config, app);
4514    assert_eq!(b_config, build);
4515    assert_eq!(d_bundle, bundle);
4516    assert_eq!(d_windows, app.windows);
4517  }
4518
4519  #[test]
4520  fn parse_hex_color() {
4521    use super::Color;
4522
4523    assert_eq!(Color(255, 255, 255, 255), "fff".parse().unwrap());
4524    assert_eq!(Color(255, 255, 255, 255), "#fff".parse().unwrap());
4525    assert_eq!(Color(0, 0, 0, 255), "#000000".parse().unwrap());
4526    assert_eq!(Color(0, 0, 0, 255), "#000000ff".parse().unwrap());
4527    assert_eq!(Color(0, 255, 0, 255), "#00ff00ff".parse().unwrap());
4528  }
4529
4530  #[test]
4531  fn test_runner_config_string_format() {
4532    use super::RunnerConfig;
4533
4534    // Test string format deserialization
4535    let json = r#""cargo""#;
4536    let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4537
4538    assert_eq!(runner.cmd(), "cargo");
4539    assert_eq!(runner.cwd(), None);
4540    assert_eq!(runner.args(), None);
4541
4542    // Test string format serialization
4543    let serialized = serde_json::to_string(&runner).unwrap();
4544    assert_eq!(serialized, r#""cargo""#);
4545  }
4546
4547  #[test]
4548  fn test_runner_config_object_format_full() {
4549    use super::RunnerConfig;
4550
4551    // Test object format with all fields
4552    let json = r#"{"cmd": "my_runner", "cwd": "/tmp/build", "args": ["--quiet", "--verbose"]}"#;
4553    let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4554
4555    assert_eq!(runner.cmd(), "my_runner");
4556    assert_eq!(runner.cwd(), Some("/tmp/build"));
4557    assert_eq!(
4558      runner.args(),
4559      Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4560    );
4561
4562    // Test object format serialization
4563    let serialized = serde_json::to_string(&runner).unwrap();
4564    let deserialized: RunnerConfig = serde_json::from_str(&serialized).unwrap();
4565    assert_eq!(runner, deserialized);
4566  }
4567
4568  #[test]
4569  fn test_runner_config_object_format_minimal() {
4570    use super::RunnerConfig;
4571
4572    // Test object format with only cmd field
4573    let json = r#"{"cmd": "cross"}"#;
4574    let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4575
4576    assert_eq!(runner.cmd(), "cross");
4577    assert_eq!(runner.cwd(), None);
4578    assert_eq!(runner.args(), None);
4579  }
4580
4581  #[test]
4582  fn test_runner_config_default() {
4583    use super::RunnerConfig;
4584
4585    let default_runner = RunnerConfig::default();
4586    assert_eq!(default_runner.cmd(), "cargo");
4587    assert_eq!(default_runner.cwd(), None);
4588    assert_eq!(default_runner.args(), None);
4589  }
4590
4591  #[test]
4592  fn test_runner_config_from_str() {
4593    use super::RunnerConfig;
4594
4595    // Test From<&str> trait
4596    let runner: RunnerConfig = "my_runner".into();
4597    assert_eq!(runner.cmd(), "my_runner");
4598    assert_eq!(runner.cwd(), None);
4599    assert_eq!(runner.args(), None);
4600  }
4601
4602  #[test]
4603  fn test_runner_config_from_string() {
4604    use super::RunnerConfig;
4605
4606    // Test From<String> trait
4607    let runner: RunnerConfig = "another_runner".to_string().into();
4608    assert_eq!(runner.cmd(), "another_runner");
4609    assert_eq!(runner.cwd(), None);
4610    assert_eq!(runner.args(), None);
4611  }
4612
4613  #[test]
4614  fn test_runner_config_from_str_parse() {
4615    use super::RunnerConfig;
4616    use std::str::FromStr;
4617
4618    // Test FromStr trait
4619    let runner = RunnerConfig::from_str("parsed_runner").unwrap();
4620    assert_eq!(runner.cmd(), "parsed_runner");
4621    assert_eq!(runner.cwd(), None);
4622    assert_eq!(runner.args(), None);
4623  }
4624
4625  #[test]
4626  fn test_runner_config_in_build_config() {
4627    use super::BuildConfig;
4628
4629    // Test string format in BuildConfig
4630    let json = r#"{"runner": "cargo"}"#;
4631    let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4632
4633    let runner = build_config.runner.unwrap();
4634    assert_eq!(runner.cmd(), "cargo");
4635    assert_eq!(runner.cwd(), None);
4636    assert_eq!(runner.args(), None);
4637  }
4638
4639  #[test]
4640  fn test_runner_config_in_build_config_object() {
4641    use super::BuildConfig;
4642
4643    // Test object format in BuildConfig
4644    let json = r#"{"runner": {"cmd": "cross", "cwd": "/workspace", "args": ["--target", "x86_64-unknown-linux-gnu"]}}"#;
4645    let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4646
4647    let runner = build_config.runner.unwrap();
4648    assert_eq!(runner.cmd(), "cross");
4649    assert_eq!(runner.cwd(), Some("/workspace"));
4650    assert_eq!(
4651      runner.args(),
4652      Some(
4653        &[
4654          "--target".to_string(),
4655          "x86_64-unknown-linux-gnu".to_string()
4656        ][..]
4657      )
4658    );
4659  }
4660
4661  #[test]
4662  fn test_runner_config_in_full_config() {
4663    use super::Config;
4664
4665    // Test runner config in full Tauri config
4666    let json = r#"{
4667      "productName": "Test App",
4668      "version": "1.0.0",
4669      "identifier": "com.test.app",
4670      "build": {
4671        "runner": {
4672          "cmd": "my_custom_cargo",
4673          "cwd": "/tmp/build",
4674          "args": ["--quiet", "--verbose"]
4675        }
4676      }
4677    }"#;
4678
4679    let config: Config = serde_json::from_str(json).unwrap();
4680    let runner = config.build.runner.unwrap();
4681
4682    assert_eq!(runner.cmd(), "my_custom_cargo");
4683    assert_eq!(runner.cwd(), Some("/tmp/build"));
4684    assert_eq!(
4685      runner.args(),
4686      Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4687    );
4688  }
4689
4690  #[test]
4691  fn test_runner_config_equality() {
4692    use super::RunnerConfig;
4693
4694    let runner1 = RunnerConfig::String("cargo".to_string());
4695    let runner2 = RunnerConfig::String("cargo".to_string());
4696    let runner3 = RunnerConfig::String("cross".to_string());
4697
4698    assert_eq!(runner1, runner2);
4699    assert_ne!(runner1, runner3);
4700
4701    let runner4 = RunnerConfig::Object {
4702      cmd: "cargo".to_string(),
4703      cwd: Some("/tmp".to_string()),
4704      args: Some(vec!["--quiet".to_string()]),
4705    };
4706    let runner5 = RunnerConfig::Object {
4707      cmd: "cargo".to_string(),
4708      cwd: Some("/tmp".to_string()),
4709      args: Some(vec!["--quiet".to_string()]),
4710    };
4711
4712    assert_eq!(runner4, runner5);
4713    assert_ne!(runner1, runner4);
4714  }
4715
4716  #[test]
4717  fn test_runner_config_untagged_serialization() {
4718    use super::RunnerConfig;
4719
4720    // Test that serde untagged works correctly - string should serialize as string, not object
4721    let string_runner = RunnerConfig::String("cargo".to_string());
4722    let string_json = serde_json::to_string(&string_runner).unwrap();
4723    assert_eq!(string_json, r#""cargo""#);
4724
4725    // Test that object serializes as object
4726    let object_runner = RunnerConfig::Object {
4727      cmd: "cross".to_string(),
4728      cwd: None,
4729      args: None,
4730    };
4731    let object_json = serde_json::to_string(&object_runner).unwrap();
4732    assert!(object_json.contains("\"cmd\":\"cross\""));
4733    // With skip_serializing_none, null values should not be included
4734    assert!(object_json.contains("\"cwd\":null") || !object_json.contains("cwd"));
4735    assert!(object_json.contains("\"args\":null") || !object_json.contains("args"));
4736  }
4737
4738  #[test]
4739  fn window_config_default_same_as_deserialize() {
4740    let config_from_deserialization: WindowConfig = serde_json::from_str("{}").unwrap();
4741    let config_from_default: WindowConfig = WindowConfig::default();
4742
4743    assert_eq!(config_from_deserialization, config_from_default);
4744  }
4745}