1#[cfg(feature = "schema")]
27use schemars::JsonSchema;
28use semver::Version;
29use serde::{
30 Deserialize, Serialize, Serializer,
31 de::{Deserializer, Error as DeError, Visitor},
32};
33use serde_json::Value as JsonValue;
34use serde_untagged::UntaggedEnumVisitor;
35use serde_with::skip_serializing_none;
36use url::Url;
37
38use std::{
39 collections::{BTreeMap, HashMap, HashSet},
40 fmt::{self, Display},
41 fs::read_to_string,
42 path::PathBuf,
43 str::FromStr,
44};
45
46pub mod parse;
48
49use crate::{TitleBarStyle, WindowEffect, WindowEffectState, acl::capability::Capability};
50
51pub use self::parse::parse;
52
53fn default_true() -> bool {
54 true
55}
56
57#[derive(PartialEq, Eq, Debug, Clone, Serialize)]
59#[cfg_attr(feature = "schema", derive(JsonSchema))]
60#[serde(untagged)]
61#[non_exhaustive]
62pub enum WebviewUrl {
63 External(Url),
65 App(PathBuf),
69 CustomProtocol(Url),
71}
72
73impl<'de> Deserialize<'de> for WebviewUrl {
74 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 #[derive(Deserialize)]
79 #[serde(untagged)]
80 enum WebviewUrlDeserializer {
81 Url(Url),
82 Path(PathBuf),
83 }
84
85 match WebviewUrlDeserializer::deserialize(deserializer)? {
86 WebviewUrlDeserializer::Url(u) => {
87 if u.scheme() == "https" || u.scheme() == "http" {
88 Ok(Self::External(u))
89 } else {
90 Ok(Self::CustomProtocol(u))
91 }
92 }
93 WebviewUrlDeserializer::Path(p) => Ok(Self::App(p)),
94 }
95 }
96}
97
98impl fmt::Display for WebviewUrl {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::External(url) | Self::CustomProtocol(url) => write!(f, "{url}"),
102 Self::App(path) => write!(f, "{}", path.display()),
103 }
104 }
105}
106
107impl Default for WebviewUrl {
108 fn default() -> Self {
109 Self::App("index.html".into())
110 }
111}
112
113#[derive(Debug, PartialEq, Eq, Clone)]
115#[cfg_attr(feature = "schema", derive(JsonSchema))]
116#[cfg_attr(feature = "schema", schemars(rename_all = "lowercase"))]
117pub enum BundleType {
118 Deb,
120 Rpm,
122 AppImage,
124 Msi,
126 Nsis,
128 App,
130 Dmg,
132}
133
134impl BundleType {
135 fn all() -> &'static [Self] {
137 &[
138 BundleType::Deb,
139 BundleType::Rpm,
140 BundleType::AppImage,
141 BundleType::Msi,
142 BundleType::Nsis,
143 BundleType::App,
144 BundleType::Dmg,
145 ]
146 }
147}
148
149impl Display for BundleType {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 write!(
152 f,
153 "{}",
154 match self {
155 Self::Deb => "deb",
156 Self::Rpm => "rpm",
157 Self::AppImage => "appimage",
158 Self::Msi => "msi",
159 Self::Nsis => "nsis",
160 Self::App => "app",
161 Self::Dmg => "dmg",
162 }
163 )
164 }
165}
166
167impl Serialize for BundleType {
168 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
169 where
170 S: Serializer,
171 {
172 serializer.serialize_str(self.to_string().as_ref())
173 }
174}
175
176impl<'de> Deserialize<'de> for BundleType {
177 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
178 where
179 D: Deserializer<'de>,
180 {
181 let s = String::deserialize(deserializer)?;
182 match s.to_lowercase().as_str() {
183 "deb" => Ok(Self::Deb),
184 "rpm" => Ok(Self::Rpm),
185 "appimage" => Ok(Self::AppImage),
186 "msi" => Ok(Self::Msi),
187 "nsis" => Ok(Self::Nsis),
188 "app" => Ok(Self::App),
189 "dmg" => Ok(Self::Dmg),
190 _ => Err(DeError::custom(format!("unknown bundle target '{s}'"))),
191 }
192 }
193}
194
195#[derive(Debug, PartialEq, Eq, Clone, Default)]
197#[cfg_attr(
198 feature = "schema",
199 derive(JsonSchema),
200 schemars(rename_all = "lowercase")
201)]
202pub enum BundleTarget {
203 #[default]
205 All,
206 #[cfg_attr(feature = "schema", schemars(untagged))]
207 List(Vec<BundleType>),
209 #[cfg_attr(feature = "schema", schemars(untagged))]
210 One(BundleType),
212}
213
214impl Serialize for BundleTarget {
215 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
216 where
217 S: Serializer,
218 {
219 match self {
220 Self::All => serializer.serialize_str("all"),
221 Self::List(l) => l.serialize(serializer),
222 Self::One(t) => serializer.serialize_str(t.to_string().as_ref()),
223 }
224 }
225}
226
227impl<'de> Deserialize<'de> for BundleTarget {
228 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
229 where
230 D: Deserializer<'de>,
231 {
232 #[derive(Deserialize, Serialize)]
233 #[serde(untagged)]
234 pub enum BundleTargetInner {
235 List(Vec<BundleType>),
236 One(BundleType),
237 All(String),
238 }
239
240 match BundleTargetInner::deserialize(deserializer)? {
241 BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All),
242 BundleTargetInner::All(t) => Err(DeError::custom(format!(
243 "invalid bundle type {t}, expected one of `all`, {}",
244 BundleType::all()
245 .iter()
246 .map(|b| format!("`{b}`"))
247 .collect::<Vec<_>>()
248 .join(", ")
249 ))),
250 BundleTargetInner::List(l) => Ok(Self::List(l)),
251 BundleTargetInner::One(t) => Ok(Self::One(t)),
252 }
253 }
254}
255
256impl BundleTarget {
257 #[allow(dead_code)]
259 pub fn to_vec(&self) -> Vec<BundleType> {
260 match self {
261 Self::All => BundleType::all().to_vec(),
262 Self::List(list) => list.clone(),
263 Self::One(i) => vec![i.clone()],
264 }
265 }
266}
267
268#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
272#[cfg_attr(feature = "schema", derive(JsonSchema))]
273#[serde(rename_all = "camelCase", deny_unknown_fields)]
274pub struct AppImageConfig {
275 #[serde(default, alias = "bundle-media-framework")]
278 pub bundle_media_framework: bool,
279 #[serde(default)]
281 pub files: HashMap<PathBuf, PathBuf>,
282}
283
284#[skip_serializing_none]
288#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
289#[cfg_attr(feature = "schema", derive(JsonSchema))]
290#[serde(rename_all = "camelCase", deny_unknown_fields)]
291pub struct DebConfig {
292 pub depends: Option<Vec<String>>,
294 pub recommends: Option<Vec<String>>,
296 pub provides: Option<Vec<String>>,
298 pub conflicts: Option<Vec<String>>,
300 pub replaces: Option<Vec<String>>,
302 #[serde(default)]
304 pub files: HashMap<PathBuf, PathBuf>,
305 pub section: Option<String>,
307 pub priority: Option<String>,
310 pub changelog: Option<PathBuf>,
313 #[serde(alias = "desktop-template")]
317 pub desktop_template: Option<PathBuf>,
318 #[serde(alias = "pre-install-script")]
321 pub pre_install_script: Option<PathBuf>,
322 #[serde(alias = "post-install-script")]
325 pub post_install_script: Option<PathBuf>,
326 #[serde(alias = "pre-remove-script")]
329 pub pre_remove_script: Option<PathBuf>,
330 #[serde(alias = "post-remove-script")]
333 pub post_remove_script: Option<PathBuf>,
334}
335
336#[skip_serializing_none]
340#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
341#[cfg_attr(feature = "schema", derive(JsonSchema))]
342#[serde(rename_all = "camelCase", deny_unknown_fields)]
343pub struct LinuxConfig {
344 #[serde(default)]
346 pub appimage: AppImageConfig,
347 #[serde(default)]
349 pub deb: DebConfig,
350 #[serde(default)]
352 pub rpm: RpmConfig,
353}
354
355#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
357#[cfg_attr(feature = "schema", derive(JsonSchema))]
358#[serde(rename_all = "camelCase", deny_unknown_fields, tag = "type")]
359#[non_exhaustive]
360pub enum RpmCompression {
361 Gzip {
363 level: u32,
365 },
366 Zstd {
368 level: i32,
370 },
371 Xz {
373 level: u32,
375 },
376 Bzip2 {
378 level: u32,
380 },
381 None,
383}
384
385#[skip_serializing_none]
387#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
388#[cfg_attr(feature = "schema", derive(JsonSchema))]
389#[serde(rename_all = "camelCase", deny_unknown_fields)]
390pub struct RpmConfig {
391 pub depends: Option<Vec<String>>,
393 pub recommends: Option<Vec<String>>,
395 pub provides: Option<Vec<String>>,
397 pub conflicts: Option<Vec<String>>,
400 pub obsoletes: Option<Vec<String>>,
403 #[serde(default = "default_release")]
405 pub release: String,
406 #[serde(default)]
408 pub epoch: u32,
409 #[serde(default)]
411 pub files: HashMap<PathBuf, PathBuf>,
412 #[serde(alias = "desktop-template")]
416 pub desktop_template: Option<PathBuf>,
417 #[serde(alias = "pre-install-script")]
420 pub pre_install_script: Option<PathBuf>,
421 #[serde(alias = "post-install-script")]
424 pub post_install_script: Option<PathBuf>,
425 #[serde(alias = "pre-remove-script")]
428 pub pre_remove_script: Option<PathBuf>,
429 #[serde(alias = "post-remove-script")]
432 pub post_remove_script: Option<PathBuf>,
433 pub compression: Option<RpmCompression>,
435}
436
437impl Default for RpmConfig {
438 fn default() -> Self {
439 Self {
440 depends: None,
441 recommends: None,
442 provides: None,
443 conflicts: None,
444 obsoletes: None,
445 release: default_release(),
446 epoch: 0,
447 files: Default::default(),
448 desktop_template: None,
449 pre_install_script: None,
450 post_install_script: None,
451 pre_remove_script: None,
452 post_remove_script: None,
453 compression: None,
454 }
455 }
456}
457
458fn default_release() -> String {
459 "1".into()
460}
461
462#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
464#[cfg_attr(feature = "schema", derive(JsonSchema))]
465#[serde(rename_all = "camelCase", deny_unknown_fields)]
466pub struct Position {
467 pub x: u32,
469 pub y: u32,
471}
472
473#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize)]
475#[cfg_attr(feature = "schema", derive(JsonSchema))]
476#[serde(rename_all = "camelCase", deny_unknown_fields)]
477pub struct LogicalPosition {
478 pub x: f64,
480 pub y: f64,
482}
483
484#[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
486#[cfg_attr(feature = "schema", derive(JsonSchema))]
487#[serde(rename_all = "camelCase", deny_unknown_fields)]
488pub struct Size {
489 pub width: u32,
491 pub height: u32,
493}
494
495#[skip_serializing_none]
499#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
500#[cfg_attr(feature = "schema", derive(JsonSchema))]
501#[serde(rename_all = "camelCase", deny_unknown_fields)]
502pub struct DmgConfig {
503 pub background: Option<PathBuf>,
505 pub window_position: Option<Position>,
507 #[serde(default = "dmg_window_size", alias = "window-size")]
509 pub window_size: Size,
510 #[serde(default = "dmg_app_position", alias = "app-position")]
512 pub app_position: Position,
513 #[serde(
515 default = "dmg_application_folder_position",
516 alias = "application-folder-position"
517 )]
518 pub application_folder_position: Position,
519}
520
521impl Default for DmgConfig {
522 fn default() -> Self {
523 Self {
524 background: None,
525 window_position: None,
526 window_size: dmg_window_size(),
527 app_position: dmg_app_position(),
528 application_folder_position: dmg_application_folder_position(),
529 }
530 }
531}
532
533fn dmg_window_size() -> Size {
534 Size {
535 width: 660,
536 height: 400,
537 }
538}
539
540fn dmg_app_position() -> Position {
541 Position { x: 180, y: 170 }
542}
543
544fn dmg_application_folder_position() -> Position {
545 Position { x: 480, y: 170 }
546}
547
548fn de_macos_minimum_system_version<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
549where
550 D: Deserializer<'de>,
551{
552 let version = Option::<String>::deserialize(deserializer)?;
553 match version {
554 Some(v) if v.is_empty() => Ok(macos_minimum_system_version()),
555 e => Ok(e),
556 }
557}
558
559#[skip_serializing_none]
563#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
564#[cfg_attr(feature = "schema", derive(JsonSchema))]
565#[serde(rename_all = "camelCase", deny_unknown_fields)]
566pub struct MacConfig {
567 pub frameworks: Option<Vec<String>>,
571 #[serde(default)]
573 pub files: HashMap<PathBuf, PathBuf>,
574 #[serde(alias = "bundle-version")]
578 pub bundle_version: Option<String>,
579 #[serde(alias = "bundle-name")]
585 pub bundle_name: Option<String>,
586 #[serde(
595 deserialize_with = "de_macos_minimum_system_version",
596 default = "macos_minimum_system_version",
597 alias = "minimum-system-version"
598 )]
599 pub minimum_system_version: Option<String>,
600 #[serde(alias = "exception-domain")]
603 pub exception_domain: Option<String>,
604 #[serde(alias = "signing-identity")]
606 pub signing_identity: Option<String>,
607 #[serde(alias = "hardened-runtime", default = "default_true")]
609 pub hardened_runtime: bool,
610 #[serde(alias = "provider-short-name")]
612 pub provider_short_name: Option<String>,
613 pub entitlements: Option<String>,
615 #[serde(alias = "info-plist")]
619 pub info_plist: Option<PathBuf>,
620 #[serde(default)]
622 pub dmg: DmgConfig,
623}
624
625impl Default for MacConfig {
626 fn default() -> Self {
627 Self {
628 frameworks: None,
629 files: HashMap::new(),
630 bundle_version: None,
631 bundle_name: None,
632 minimum_system_version: macos_minimum_system_version(),
633 exception_domain: None,
634 signing_identity: None,
635 hardened_runtime: true,
636 provider_short_name: None,
637 entitlements: None,
638 info_plist: None,
639 dmg: Default::default(),
640 }
641 }
642}
643
644fn macos_minimum_system_version() -> Option<String> {
645 Some("10.13".into())
646}
647
648fn ios_minimum_system_version() -> String {
649 "15.0".into()
650}
651
652#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
656#[cfg_attr(feature = "schema", derive(JsonSchema))]
657#[serde(rename_all = "camelCase", deny_unknown_fields)]
658pub struct WixLanguageConfig {
659 #[serde(alias = "locale-path")]
661 pub locale_path: Option<String>,
662}
663
664#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
666#[cfg_attr(feature = "schema", derive(JsonSchema))]
667#[serde(untagged)]
668pub enum WixLanguage {
669 One(String),
671 List(Vec<String>),
673 Localized(HashMap<String, WixLanguageConfig>),
675}
676
677impl Default for WixLanguage {
678 fn default() -> Self {
679 Self::One("en-US".into())
680 }
681}
682
683#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
687#[cfg_attr(feature = "schema", derive(JsonSchema))]
688#[serde(rename_all = "camelCase", deny_unknown_fields)]
689pub struct WixConfig {
690 pub version: Option<String>,
699 #[serde(alias = "upgrade-code")]
708 pub upgrade_code: Option<uuid::Uuid>,
709 #[serde(default)]
711 pub language: WixLanguage,
712 pub template: Option<PathBuf>,
714 #[serde(default, alias = "fragment-paths")]
716 pub fragment_paths: Vec<PathBuf>,
717 #[serde(default, alias = "component-group-refs")]
719 pub component_group_refs: Vec<String>,
720 #[serde(default, alias = "component-refs")]
722 pub component_refs: Vec<String>,
723 #[serde(default, alias = "feature-group-refs")]
725 pub feature_group_refs: Vec<String>,
726 #[serde(default, alias = "feature-refs")]
728 pub feature_refs: Vec<String>,
729 #[serde(default, alias = "merge-refs")]
731 pub merge_refs: Vec<String>,
732 #[serde(default, alias = "enable-elevated-update-task")]
734 pub enable_elevated_update_task: bool,
735 #[serde(alias = "banner-path")]
740 pub banner_path: Option<PathBuf>,
741 #[serde(alias = "dialog-image-path")]
746 pub dialog_image_path: Option<PathBuf>,
747 #[serde(default, alias = "fips-compliant")]
750 pub fips_compliant: bool,
751}
752
753#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
757#[cfg_attr(feature = "schema", derive(JsonSchema))]
758#[serde(rename_all = "camelCase", deny_unknown_fields)]
759pub enum NsisCompression {
760 Zlib,
762 Bzip2,
764 #[default]
766 Lzma,
767 None,
769}
770
771#[derive(Default, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase", deny_unknown_fields)]
774#[cfg_attr(feature = "schema", derive(JsonSchema))]
775pub enum NSISInstallerMode {
776 #[default]
782 CurrentUser,
783 PerMachine,
788 Both,
794}
795
796#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
798#[cfg_attr(feature = "schema", derive(JsonSchema))]
799#[serde(rename_all = "camelCase", deny_unknown_fields)]
800pub struct NsisConfig {
801 pub template: Option<PathBuf>,
803 #[serde(alias = "header-image")]
807 pub header_image: Option<PathBuf>,
808 #[serde(alias = "sidebar-image")]
812 pub sidebar_image: Option<PathBuf>,
813 #[serde(alias = "install-icon")]
816 pub installer_icon: Option<PathBuf>,
817 #[serde(alias = "uninstaller-icon")]
819 pub uninstaller_icon: Option<PathBuf>,
820 #[serde(alias = "uninstaller-header-image")]
825 pub uninstaller_header_image: Option<PathBuf>,
826 #[serde(default, alias = "install-mode")]
828 pub install_mode: NSISInstallerMode,
829 pub languages: Option<Vec<String>>,
836 pub custom_language_files: Option<HashMap<String, PathBuf>>,
843 #[serde(default, alias = "display-language-selector")]
846 pub display_language_selector: bool,
847 #[serde(default)]
851 pub compression: NsisCompression,
852 #[serde(alias = "start-menu-folder")]
861 pub start_menu_folder: Option<String>,
862 #[serde(alias = "installer-hooks")]
892 pub installer_hooks: Option<PathBuf>,
893 #[deprecated(
899 since = "2.10.0",
900 note = "Use `WindowsConfig::minimum_webview2_version` instead."
901 )]
902 #[serde(alias = "minimum-webview2-version")]
903 pub minimum_webview2_version: Option<String>,
904}
905
906#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
911#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
912#[cfg_attr(feature = "schema", derive(JsonSchema))]
913pub enum WebviewInstallMode {
914 Skip,
916 DownloadBootstrapper {
920 #[serde(default = "default_true")]
922 silent: bool,
923 },
924 EmbedBootstrapper {
928 #[serde(default = "default_true")]
930 silent: bool,
931 },
932 OfflineInstaller {
936 #[serde(default = "default_true")]
938 silent: bool,
939 },
940 FixedRuntime {
943 path: PathBuf,
948 },
949}
950
951impl Default for WebviewInstallMode {
952 fn default() -> Self {
953 Self::DownloadBootstrapper { silent: true }
954 }
955}
956
957#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
959#[cfg_attr(feature = "schema", derive(JsonSchema))]
960#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
961pub enum CustomSignCommandConfig {
962 Command(String),
971 CommandWithOptions {
976 cmd: String,
978 args: Vec<String>,
982 },
983}
984
985#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
989#[cfg_attr(feature = "schema", derive(JsonSchema))]
990#[serde(rename_all = "camelCase", deny_unknown_fields)]
991pub struct WindowsConfig {
992 #[serde(alias = "digest-algorithm")]
995 pub digest_algorithm: Option<String>,
996 #[serde(alias = "certificate-thumbprint")]
998 pub certificate_thumbprint: Option<String>,
999 #[serde(alias = "timestamp-url")]
1001 pub timestamp_url: Option<String>,
1002 #[serde(default)]
1005 pub tsp: bool,
1006 #[serde(default, alias = "webview-install-mode")]
1008 pub webview_install_mode: WebviewInstallMode,
1009 #[serde(default = "default_true", alias = "allow-downgrades")]
1015 pub allow_downgrades: bool,
1016 #[serde(alias = "minimum-webview2-version")]
1020 pub minimum_webview2_version: Option<String>,
1021 pub wix: Option<WixConfig>,
1023 pub nsis: Option<NsisConfig>,
1025 #[serde(alias = "sign-command")]
1033 pub sign_command: Option<CustomSignCommandConfig>,
1034 #[serde(
1041 default,
1042 rename = "bundleVCRuntime",
1043 alias = "bundle-vc-runtime",
1044 alias = "bundleVcRuntime"
1045 )]
1046 pub bundle_vc_runtime: bool,
1047}
1048
1049impl Default for WindowsConfig {
1050 fn default() -> Self {
1051 Self {
1052 digest_algorithm: None,
1053 certificate_thumbprint: None,
1054 timestamp_url: None,
1055 tsp: false,
1056 webview_install_mode: Default::default(),
1057 allow_downgrades: true,
1058 minimum_webview2_version: None,
1059 wix: None,
1060 nsis: None,
1061 sign_command: None,
1062 bundle_vc_runtime: false,
1063 }
1064 }
1065}
1066
1067#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1069#[cfg_attr(feature = "schema", derive(JsonSchema))]
1070pub enum BundleTypeRole {
1071 #[default]
1073 Editor,
1074 Viewer,
1076 Shell,
1078 QLGenerator,
1080 None,
1082}
1083
1084impl Display for BundleTypeRole {
1085 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1086 match self {
1087 Self::Editor => write!(f, "Editor"),
1088 Self::Viewer => write!(f, "Viewer"),
1089 Self::Shell => write!(f, "Shell"),
1090 Self::QLGenerator => write!(f, "QLGenerator"),
1091 Self::None => write!(f, "None"),
1092 }
1093 }
1094}
1095
1096#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1100#[cfg_attr(feature = "schema", derive(JsonSchema))]
1101pub enum HandlerRank {
1102 #[default]
1104 Default,
1105 Owner,
1107 Alternate,
1109 None,
1111}
1112
1113impl Display for HandlerRank {
1114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115 match self {
1116 Self::Default => write!(f, "Default"),
1117 Self::Owner => write!(f, "Owner"),
1118 Self::Alternate => write!(f, "Alternate"),
1119 Self::None => write!(f, "None"),
1120 }
1121 }
1122}
1123
1124#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
1128#[cfg_attr(feature = "schema", derive(JsonSchema))]
1129pub struct AssociationExt(pub String);
1130
1131impl fmt::Display for AssociationExt {
1132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1133 write!(f, "{}", self.0)
1134 }
1135}
1136
1137impl<'d> serde::Deserialize<'d> for AssociationExt {
1138 fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
1139 let ext = String::deserialize(deserializer)?;
1140 if let Some(ext) = ext.strip_prefix('.') {
1141 Ok(AssociationExt(ext.into()))
1142 } else {
1143 Ok(AssociationExt(ext))
1144 }
1145 }
1146}
1147
1148#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1150#[cfg_attr(feature = "schema", derive(JsonSchema))]
1151#[serde(rename_all = "camelCase", deny_unknown_fields)]
1152pub struct FileAssociation {
1153 pub ext: Vec<AssociationExt>,
1155 #[serde(alias = "content-types")]
1160 pub content_types: Option<Vec<String>>,
1161 pub name: Option<String>,
1163 pub description: Option<String>,
1165 #[serde(default)]
1167 pub role: BundleTypeRole,
1168 #[serde(alias = "mime-type")]
1176 pub mime_type: Option<String>,
1177 #[serde(default)]
1179 pub rank: HandlerRank,
1180 pub exported_type: Option<ExportedFileAssociation>,
1184 #[serde(alias = "android-intent-action-filters")]
1188 pub android_intent_action_filters: Option<Vec<AndroidIntentAction>>,
1189}
1190
1191#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Hash)]
1193#[cfg_attr(feature = "schema", derive(JsonSchema))]
1194#[serde(rename_all = "camelCase")]
1195#[non_exhaustive]
1196pub enum AndroidIntentAction {
1197 Send,
1201 SendMultiple,
1205 View,
1209}
1210
1211#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1213#[cfg_attr(feature = "schema", derive(JsonSchema))]
1214#[serde(rename_all = "camelCase", deny_unknown_fields)]
1215pub struct ExportedFileAssociation {
1216 pub identifier: String,
1218 #[serde(alias = "conforms-to")]
1222 pub conforms_to: Option<Vec<String>>,
1223}
1224
1225impl FileAssociation {
1226 pub fn infer_content_types(&self) -> HashSet<String> {
1233 let mut content_types = HashSet::new();
1234
1235 if let Some(exported_type) = &self.exported_type {
1237 content_types.insert(exported_type.identifier.clone());
1238 return content_types;
1239 }
1240
1241 if let Some(explicit_types) = &self.content_types {
1243 content_types.extend(explicit_types.iter().cloned());
1244 }
1245
1246 for ext in &self.ext {
1248 if let Some(uti) = extension_to_uti(&ext.0) {
1249 content_types.insert(uti.to_string());
1250 }
1251 }
1252
1253 if let Some(mime_type) = &self.mime_type
1255 && let Some(uti) = mime_type_to_uti(mime_type)
1256 {
1257 content_types.insert(uti.to_string());
1258 }
1259
1260 content_types
1261 }
1262}
1263
1264pub fn file_associations_plist(associations: &[FileAssociation]) -> Option<plist::Value> {
1270 use plist::{Dictionary, Value};
1271
1272 if associations.is_empty() {
1273 return None;
1274 }
1275
1276 let exported_associations = associations
1277 .iter()
1278 .filter_map(|association| {
1279 association.exported_type.as_ref().map(|exported_type| {
1280 let mut dict = Dictionary::new();
1281
1282 dict.insert(
1283 "UTTypeIdentifier".into(),
1284 exported_type.identifier.clone().into(),
1285 );
1286 if let Some(description) = &association.description {
1287 dict.insert("UTTypeDescription".into(), description.clone().into());
1288 }
1289 if let Some(conforms_to) = &exported_type.conforms_to {
1290 dict.insert(
1291 "UTTypeConformsTo".into(),
1292 Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
1293 );
1294 }
1295
1296 let mut specification = Dictionary::new();
1297 specification.insert(
1298 "public.filename-extension".into(),
1299 Value::Array(
1300 association
1301 .ext
1302 .iter()
1303 .map(|s| s.to_string().into())
1304 .collect(),
1305 ),
1306 );
1307 if let Some(mime_type) = &association.mime_type {
1308 specification.insert("public.mime-type".into(), mime_type.clone().into());
1309 }
1310
1311 dict.insert("UTTypeTagSpecification".into(), specification.into());
1312
1313 Value::Dictionary(dict)
1314 })
1315 })
1316 .collect::<Vec<_>>();
1317
1318 let document_types = associations
1319 .iter()
1320 .map(|association| {
1321 let mut dict = Dictionary::new();
1322
1323 if !association.ext.is_empty() {
1324 dict.insert(
1325 "CFBundleTypeExtensions".into(),
1326 Value::Array(
1327 association
1328 .ext
1329 .iter()
1330 .map(|ext| ext.to_string().into())
1331 .collect(),
1332 ),
1333 );
1334 }
1335
1336 let content_types = association.infer_content_types();
1338
1339 if !content_types.is_empty() {
1341 dict.insert(
1342 "LSItemContentTypes".into(),
1343 Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
1344 );
1345 }
1346
1347 let type_name = association
1348 .name
1349 .clone()
1350 .or_else(|| association.ext.first().map(|ext| ext.0.clone()))
1351 .unwrap_or_default();
1352 dict.insert("CFBundleTypeName".into(), type_name.into());
1353 dict.insert(
1354 "CFBundleTypeRole".into(),
1355 association.role.to_string().into(),
1356 );
1357 dict.insert("LSHandlerRank".into(), association.rank.to_string().into());
1358
1359 Value::Dictionary(dict)
1360 })
1361 .collect::<Vec<_>>();
1362
1363 if exported_associations.is_empty() && document_types.is_empty() {
1364 return None;
1365 }
1366
1367 let mut plist = Dictionary::new();
1368 if !exported_associations.is_empty() {
1369 plist.insert(
1370 "UTExportedTypeDeclarations".into(),
1371 Value::Array(exported_associations),
1372 );
1373 }
1374 if !document_types.is_empty() {
1375 plist.insert("CFBundleDocumentTypes".into(), Value::Array(document_types));
1376 }
1377
1378 Some(Value::Dictionary(plist))
1379}
1380
1381fn extension_to_uti(ext: &str) -> Option<&'static str> {
1383 match ext.to_lowercase().as_str() {
1384 "png" => Some("public.png"),
1386 "jpg" | "jpeg" => Some("public.jpeg"),
1387 "gif" => Some("com.compuserve.gif"),
1388 "bmp" => Some("com.microsoft.bmp"),
1389 "tiff" | "tif" => Some("public.tiff"),
1390 "ico" => Some("com.microsoft.ico"),
1391 "heic" | "heif" => Some("public.heif-standard-image"),
1392 "webp" => Some("org.webmproject.webp"),
1393 "svg" => Some("public.svg-image"),
1394 "mp4" => Some("public.mpeg-4"),
1396 "mov" => Some("com.apple.quicktime-movie"),
1397 "avi" => Some("public.avi"),
1398 "mkv" => Some("public.mpeg-4"),
1399 "mp3" => Some("public.mp3"),
1401 "wav" => Some("com.microsoft.waveform-audio"),
1402 "aac" => Some("public.aac-audio"),
1403 "m4a" => Some("public.mpeg-4-audio"),
1404 "pdf" => Some("com.adobe.pdf"),
1406 "txt" => Some("public.plain-text"),
1407 "rtf" => Some("public.rtf"),
1408 "html" | "htm" => Some("public.html"),
1409 "json" => Some("public.json"),
1410 "xml" => Some("public.xml"),
1411 _ => None,
1412 }
1413}
1414
1415fn mime_type_to_uti(mime_type: &str) -> Option<&'static str> {
1417 match mime_type {
1418 "image/png" => Some("public.png"),
1419 "image/jpeg" | "image/jpg" => Some("public.jpeg"),
1420 "image/gif" => Some("com.compuserve.gif"),
1421 "image/bmp" => Some("com.microsoft.bmp"),
1422 "image/tiff" => Some("public.tiff"),
1423 "image/heic" | "image/heif" => Some("public.heif-standard-image"),
1424 "image/webp" => Some("org.webmproject.webp"),
1425 "image/svg+xml" => Some("public.svg-image"),
1426 mime if mime.starts_with("image/") => Some("public.image"),
1427 "video/mp4" => Some("public.mpeg-4"),
1428 "video/quicktime" => Some("com.apple.quicktime-movie"),
1429 "video/x-msvideo" => Some("public.avi"),
1430 mime if mime.starts_with("video/") => Some("public.movie"),
1431 "audio/mpeg" | "audio/mp3" => Some("public.mp3"),
1432 "audio/wav" | "audio/wave" => Some("com.microsoft.waveform-audio"),
1433 "audio/aac" => Some("public.aac-audio"),
1434 "audio/mp4" => Some("public.mpeg-4-audio"),
1435 mime if mime.starts_with("audio/") => Some("public.audio"),
1436 "application/pdf" => Some("com.adobe.pdf"),
1437 "text/plain" => Some("public.plain-text"),
1438 "text/rtf" => Some("public.rtf"),
1439 "text/html" => Some("public.html"),
1440 "application/json" => Some("public.json"),
1441 "application/xml" | "text/xml" => Some("public.xml"),
1442 _ => None,
1443 }
1444}
1445
1446#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1448#[cfg_attr(feature = "schema", derive(JsonSchema))]
1449#[serde(rename_all = "camelCase", deny_unknown_fields)]
1450pub struct DeepLinkProtocol {
1451 #[serde(default)]
1453 pub schemes: Vec<String>,
1454 #[serde(default)]
1462 pub domains: Vec<String>,
1463 pub name: Option<String>,
1465 #[serde(default)]
1467 pub role: BundleTypeRole,
1468}
1469
1470#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1473#[cfg_attr(feature = "schema", derive(JsonSchema))]
1474#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1475pub enum BundleResources {
1476 List(Vec<String>),
1478 Map(HashMap<String, String>),
1480}
1481
1482impl BundleResources {
1483 pub fn push(&mut self, path: impl Into<String>) {
1485 match self {
1486 Self::List(l) => l.push(path.into()),
1487 Self::Map(l) => {
1488 let path = path.into();
1489 l.insert(path.clone(), path);
1490 }
1491 }
1492 }
1493}
1494
1495#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1497#[cfg_attr(feature = "schema", derive(JsonSchema))]
1498#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1499pub enum Updater {
1500 String(V1Compatible),
1502 Bool(bool),
1505}
1506
1507impl Default for Updater {
1508 fn default() -> Self {
1509 Self::Bool(false)
1510 }
1511}
1512
1513#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1515#[cfg_attr(feature = "schema", derive(JsonSchema))]
1516#[serde(rename_all = "camelCase", deny_unknown_fields)]
1517pub enum V1Compatible {
1518 V1Compatible,
1520}
1521
1522#[skip_serializing_none]
1526#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1527#[cfg_attr(feature = "schema", derive(JsonSchema))]
1528#[serde(rename_all = "camelCase", deny_unknown_fields)]
1529pub struct BundleConfig {
1530 #[serde(default)]
1532 pub active: bool,
1533 #[serde(default)]
1535 pub targets: BundleTarget,
1536 #[serde(default)]
1537 pub create_updater_artifacts: Updater,
1539 pub publisher: Option<String>,
1544 pub homepage: Option<String>,
1549 #[serde(default)]
1551 pub icon: Vec<String>,
1552 pub resources: Option<BundleResources>,
1597 pub copyright: Option<String>,
1599 pub license: Option<String>,
1602 #[serde(alias = "license-file")]
1604 pub license_file: Option<PathBuf>,
1605 pub category: Option<String>,
1610 pub file_associations: Option<Vec<FileAssociation>>,
1612 #[serde(alias = "short-description")]
1614 pub short_description: Option<String>,
1615 #[serde(alias = "long-description")]
1617 pub long_description: Option<String>,
1618 #[serde(default, alias = "use-local-tools-dir")]
1626 pub use_local_tools_dir: bool,
1627 #[serde(alias = "external-bin")]
1639 pub external_bin: Option<Vec<String>>,
1640 #[serde(default)]
1642 pub windows: WindowsConfig,
1643 #[serde(default)]
1645 pub linux: LinuxConfig,
1646 #[serde(rename = "macOS", alias = "macos", default)]
1648 pub macos: MacConfig,
1649 #[serde(rename = "iOS", alias = "ios", default)]
1651 pub ios: IosConfig,
1652 #[serde(default)]
1654 pub android: AndroidConfig,
1655 #[serde(default)]
1657 pub cef: CefConfig,
1658}
1659
1660#[skip_serializing_none]
1665#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1666#[cfg_attr(feature = "schema", derive(JsonSchema))]
1667#[serde(rename_all = "camelCase", deny_unknown_fields)]
1668pub struct CefConfig {
1669 #[serde(default = "default_true")]
1680 pub embed: bool,
1681}
1682
1683impl Default for CefConfig {
1684 fn default() -> Self {
1685 Self { embed: true }
1686 }
1687}
1688
1689#[derive(Debug, PartialEq, Eq, Serialize, Default, Clone, Copy)]
1691#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "InnerColor"))]
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 3 => color.chars()
1749 .flat_map(|c| std::iter::repeat_n(c, 2))
1750 .chain(std::iter::repeat_n('f', 2))
1751 .collect(),
1752 6 => format!("{color}FF"),
1753 8 => color.to_string(),
1754 _ => return Err("Invalid hex color length, must be either 3, 6 or 8, for example: #fff, #ffffff, or #ffffffff".into()),
1755 };
1756
1757 let r = u8::from_str_radix(&color[0..2], 16).map_err(|e| e.to_string())?;
1758 let g = u8::from_str_radix(&color[2..4], 16).map_err(|e| e.to_string())?;
1759 let b = u8::from_str_radix(&color[4..6], 16).map_err(|e| e.to_string())?;
1760 let a = u8::from_str_radix(&color[6..8], 16).map_err(|e| e.to_string())?;
1761
1762 Ok(Color(r, g, b, a))
1763 }
1764}
1765
1766fn default_alpha() -> u8 {
1767 255
1768}
1769
1770#[derive(Deserialize)]
1771#[cfg_attr(feature = "schema", derive(JsonSchema))]
1772#[serde(untagged)]
1773enum InnerColor {
1774 String(
1776 #[cfg_attr(
1777 feature = "schema",
1778 schemars(pattern("^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$"))
1779 )]
1780 String,
1781 ),
1782 Rgb((u8, u8, u8)),
1784 Rgba((u8, u8, u8, u8)),
1786 RgbaObject {
1788 red: u8,
1789 green: u8,
1790 blue: u8,
1791 #[serde(default = "default_alpha")]
1792 alpha: u8,
1793 },
1794}
1795
1796impl<'de> Deserialize<'de> for Color {
1797 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1798 where
1799 D: Deserializer<'de>,
1800 {
1801 let color = InnerColor::deserialize(deserializer)?;
1802 let color = match color {
1803 InnerColor::String(string) => string.parse().map_err(serde::de::Error::custom)?,
1804 InnerColor::Rgb(rgb) => Color(rgb.0, rgb.1, rgb.2, 255),
1805 InnerColor::Rgba(rgb) => rgb.into(),
1806 InnerColor::RgbaObject {
1807 red,
1808 green,
1809 blue,
1810 alpha,
1811 } => Color(red, green, blue, alpha),
1812 };
1813
1814 Ok(color)
1815 }
1816}
1817
1818#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1820#[cfg_attr(feature = "schema", derive(JsonSchema))]
1821#[serde(rename_all = "camelCase", deny_unknown_fields)]
1822pub enum BackgroundThrottlingPolicy {
1823 Disabled,
1825 Suspend,
1827 Throttle,
1829}
1830
1831#[skip_serializing_none]
1833#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1834#[cfg_attr(feature = "schema", derive(JsonSchema))]
1835#[serde(rename_all = "camelCase", deny_unknown_fields)]
1836pub struct WindowEffectsConfig {
1837 pub effects: Vec<WindowEffect>,
1840 pub state: Option<WindowEffectState>,
1842 pub radius: Option<f64>,
1844 pub color: Option<Color>,
1847}
1848
1849#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1852#[cfg_attr(feature = "schema", derive(JsonSchema))]
1853#[serde(rename_all = "camelCase", deny_unknown_fields)]
1854pub struct PreventOverflowMargin {
1855 pub width: u32,
1857 pub height: u32,
1859}
1860
1861#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1863#[cfg_attr(feature = "schema", derive(JsonSchema))]
1864#[serde(untagged)]
1865pub enum PreventOverflowConfig {
1866 Enable(bool),
1868 Margin(PreventOverflowMargin),
1871}
1872
1873#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
1879#[cfg_attr(feature = "schema", derive(JsonSchema))]
1880#[serde(rename_all = "camelCase", deny_unknown_fields)]
1881#[non_exhaustive]
1882pub enum ScrollBarStyle {
1883 #[default]
1884 Default,
1886
1887 FluentOverlay,
1892}
1893
1894#[skip_serializing_none]
1898#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
1899#[cfg_attr(feature = "schema", derive(JsonSchema))]
1900#[serde(rename_all = "camelCase", deny_unknown_fields)]
1901pub struct WindowConfig {
1902 #[serde(default = "default_window_label")]
1904 pub label: String,
1905 #[serde(default = "default_true")]
1920 pub create: bool,
1921 #[serde(default)]
1923 pub url: WebviewUrl,
1924 #[serde(alias = "user-agent")]
1926 pub user_agent: Option<String>,
1927 #[serde(default = "default_true", alias = "drag-drop-enabled")]
1937 pub drag_drop_enabled: bool,
1938 #[serde(default)]
1940 pub center: bool,
1941 pub x: Option<f64>,
1943 pub y: Option<f64>,
1945 #[serde(default = "default_width")]
1947 pub width: f64,
1948 #[serde(default = "default_height")]
1950 pub height: f64,
1951 #[serde(alias = "min-width")]
1953 pub min_width: Option<f64>,
1954 #[serde(alias = "min-height")]
1956 pub min_height: Option<f64>,
1957 #[serde(alias = "max-width")]
1959 pub max_width: Option<f64>,
1960 #[serde(alias = "max-height")]
1962 pub max_height: Option<f64>,
1963 #[serde(alias = "prevent-overflow")]
1969 pub prevent_overflow: Option<PreventOverflowConfig>,
1970 #[serde(default = "default_true")]
1972 pub resizable: bool,
1973 #[serde(default = "default_true")]
1981 pub maximizable: bool,
1982 #[serde(default = "default_true")]
1988 pub minimizable: bool,
1989 #[serde(default = "default_true")]
1997 pub closable: bool,
1998 #[serde(default = "default_title")]
2000 pub title: String,
2001 #[serde(default)]
2003 pub fullscreen: bool,
2004 #[serde(default = "default_true")]
2006 pub focus: bool,
2007 #[serde(default = "default_true")]
2009 pub focusable: bool,
2010 #[serde(default)]
2017 pub transparent: bool,
2018 #[serde(default)]
2020 pub maximized: bool,
2021 #[serde(default = "default_true")]
2023 pub visible: bool,
2024 #[serde(default = "default_true")]
2026 pub decorations: bool,
2027 #[serde(default, alias = "always-on-bottom")]
2029 pub always_on_bottom: bool,
2030 #[serde(default, alias = "always-on-top")]
2032 pub always_on_top: bool,
2033 #[serde(default, alias = "visible-on-all-workspaces")]
2039 pub visible_on_all_workspaces: bool,
2040 #[serde(default, alias = "content-protected")]
2042 pub content_protected: bool,
2043 #[serde(default, alias = "skip-taskbar")]
2045 pub skip_taskbar: bool,
2046 pub window_classname: Option<String>,
2048 #[serde(default, alias = "no-redirection-bitmap")]
2053 pub no_redirection_bitmap: bool,
2054 pub theme: Option<crate::Theme>,
2056 #[serde(default, alias = "title-bar-style")]
2058 pub title_bar_style: TitleBarStyle,
2059 #[serde(default, alias = "traffic-light-position")]
2063 pub traffic_light_position: Option<LogicalPosition>,
2064 #[serde(default, alias = "hidden-title")]
2066 pub hidden_title: bool,
2067 #[serde(default, alias = "accept-first-mouse")]
2075 pub accept_first_mouse: bool,
2076 #[serde(default, alias = "tabbing-identifier")]
2083 pub tabbing_identifier: Option<String>,
2084 #[serde(default, alias = "additional-browser-args")]
2093 pub additional_browser_args: Option<String>,
2094 #[serde(default = "default_true")]
2104 pub shadow: bool,
2105 #[serde(default, alias = "window-effects")]
2114 pub window_effects: Option<WindowEffectsConfig>,
2115 #[serde(default)]
2121 pub incognito: bool,
2122 pub parent: Option<String>,
2134 #[serde(alias = "proxy-url")]
2142 pub proxy_url: Option<Url>,
2143 #[serde(default, alias = "zoom-hotkeys-enabled")]
2153 pub zoom_hotkeys_enabled: bool,
2154 #[serde(default, alias = "browser-extensions-enabled")]
2161 pub browser_extensions_enabled: bool,
2162
2163 #[serde(default, alias = "use-https-scheme")]
2173 pub use_https_scheme: bool,
2174 pub devtools: Option<bool>,
2184
2185 #[serde(alias = "background-color")]
2193 pub background_color: Option<Color>,
2194
2195 #[serde(default, alias = "background-throttling")]
2210 pub background_throttling: Option<BackgroundThrottlingPolicy>,
2211 #[serde(default, alias = "javascript-disabled")]
2213 pub javascript_disabled: bool,
2214 #[serde(default = "default_true", alias = "allow-link-preview")]
2217 pub allow_link_preview: bool,
2218 #[serde(
2223 default,
2224 alias = "disable-input-accessory-view",
2225 alias = "disable_input_accessory_view"
2226 )]
2227 pub disable_input_accessory_view: bool,
2228 #[serde(default, alias = "data-directory")]
2239 pub data_directory: Option<PathBuf>,
2240 #[serde(default, alias = "data-store-identifier")]
2252 pub data_store_identifier: Option<[u8; 16]>,
2253
2254 #[serde(default, alias = "scroll-bar-style")]
2267 pub scroll_bar_style: ScrollBarStyle,
2268
2269 #[serde(default, alias = "limit-navigations-to-app-bound-domains")]
2319 pub limit_navigations_to_app_bound_domains: bool,
2320 #[serde(default, alias = "activity-name")]
2322 pub activity_name: Option<String>,
2323 #[serde(default, alias = "created-by-activity-name")]
2327 pub created_by_activity_name: Option<String>,
2328
2329 #[serde(default, alias = "requested-by-scene-identifier")]
2334 pub requested_by_scene_identifier: Option<String>,
2335 #[serde(default = "default_true", alias = "general-autofill-enabled")]
2353 pub general_autofill_enabled: bool,
2354}
2355
2356impl Default for WindowConfig {
2357 fn default() -> Self {
2358 Self {
2359 label: default_window_label(),
2360 url: WebviewUrl::default(),
2361 create: true,
2362 user_agent: None,
2363 drag_drop_enabled: true,
2364 center: false,
2365 x: None,
2366 y: None,
2367 width: default_width(),
2368 height: default_height(),
2369 min_width: None,
2370 min_height: None,
2371 max_width: None,
2372 max_height: None,
2373 prevent_overflow: None,
2374 resizable: true,
2375 maximizable: true,
2376 minimizable: true,
2377 closable: true,
2378 title: default_title(),
2379 fullscreen: false,
2380 focus: true,
2381 focusable: true,
2382 transparent: false,
2383 maximized: false,
2384 visible: true,
2385 decorations: true,
2386 always_on_bottom: false,
2387 always_on_top: false,
2388 visible_on_all_workspaces: false,
2389 content_protected: false,
2390 skip_taskbar: false,
2391 window_classname: None,
2392 no_redirection_bitmap: false,
2393 theme: None,
2394 title_bar_style: Default::default(),
2395 traffic_light_position: None,
2396 hidden_title: false,
2397 accept_first_mouse: false,
2398 tabbing_identifier: None,
2399 additional_browser_args: None,
2400 shadow: true,
2401 window_effects: None,
2402 incognito: false,
2403 parent: None,
2404 proxy_url: None,
2405 zoom_hotkeys_enabled: false,
2406 browser_extensions_enabled: false,
2407 use_https_scheme: false,
2408 devtools: None,
2409 background_color: None,
2410 background_throttling: None,
2411 javascript_disabled: false,
2412 allow_link_preview: true,
2413 disable_input_accessory_view: false,
2414 data_directory: None,
2415 data_store_identifier: None,
2416 scroll_bar_style: ScrollBarStyle::Default,
2417 limit_navigations_to_app_bound_domains: false,
2418 activity_name: None,
2419 created_by_activity_name: None,
2420 requested_by_scene_identifier: None,
2421 general_autofill_enabled: true,
2422 }
2423 }
2424}
2425
2426fn default_window_label() -> String {
2427 "main".to_string()
2428}
2429
2430fn default_width() -> f64 {
2431 800.
2432}
2433
2434fn default_height() -> f64 {
2435 600.
2436}
2437
2438fn default_title() -> String {
2439 "Tauri App".to_string()
2440}
2441
2442#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2445#[cfg_attr(feature = "schema", derive(JsonSchema))]
2446#[serde(rename_all = "camelCase", untagged)]
2447pub enum CspDirectiveSources {
2448 Inline(String),
2450 List(Vec<String>),
2452}
2453
2454impl Default for CspDirectiveSources {
2455 fn default() -> Self {
2456 Self::List(Vec::new())
2457 }
2458}
2459
2460impl From<CspDirectiveSources> for Vec<String> {
2461 fn from(sources: CspDirectiveSources) -> Self {
2462 match sources {
2463 CspDirectiveSources::Inline(source) => source.split(' ').map(|s| s.to_string()).collect(),
2464 CspDirectiveSources::List(l) => l,
2465 }
2466 }
2467}
2468
2469impl CspDirectiveSources {
2470 pub fn contains(&self, source: &str) -> bool {
2472 match self {
2473 Self::Inline(s) => s.contains(&format!("{source} ")) || s.contains(&format!(" {source}")),
2474 Self::List(l) => l.contains(&source.into()),
2475 }
2476 }
2477
2478 pub fn push<S: AsRef<str>>(&mut self, source: S) {
2480 match self {
2481 Self::Inline(s) => {
2482 s.push(' ');
2483 s.push_str(source.as_ref());
2484 }
2485 Self::List(l) => {
2486 l.push(source.as_ref().to_string());
2487 }
2488 }
2489 }
2490
2491 pub fn extend(&mut self, sources: Vec<String>) {
2493 for s in sources {
2494 self.push(s);
2495 }
2496 }
2497}
2498
2499#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
2502#[cfg_attr(feature = "schema", derive(JsonSchema))]
2503#[serde(rename_all = "camelCase", untagged)]
2504pub enum Csp {
2505 Policy(String),
2507 DirectiveMap(HashMap<String, CspDirectiveSources>),
2509}
2510
2511impl Serialize for Csp {
2512 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2513 where
2514 S: Serializer,
2515 {
2516 match self {
2517 Self::Policy(policy) => serializer.serialize_str(policy),
2518 Self::DirectiveMap(map) => {
2519 let btree_map: BTreeMap<_, _> = map.iter().collect();
2523 btree_map.serialize(serializer)
2524 }
2525 }
2526 }
2527}
2528
2529impl From<HashMap<String, CspDirectiveSources>> for Csp {
2530 fn from(map: HashMap<String, CspDirectiveSources>) -> Self {
2531 Self::DirectiveMap(map)
2532 }
2533}
2534
2535impl From<Csp> for HashMap<String, CspDirectiveSources> {
2536 fn from(csp: Csp) -> Self {
2537 match csp {
2538 Csp::Policy(policy) => {
2539 let mut map = HashMap::new();
2540 for directive in policy.split(';') {
2541 let mut tokens = directive.trim().split(' ');
2542 if let Some(directive) = tokens.next() {
2543 let sources = tokens.map(|s| s.to_string()).collect::<Vec<String>>();
2544 map.insert(directive.to_string(), CspDirectiveSources::List(sources));
2545 }
2546 }
2547 map
2548 }
2549 Csp::DirectiveMap(m) => m,
2550 }
2551 }
2552}
2553
2554impl Display for Csp {
2555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2556 match self {
2557 Self::Policy(s) => write!(f, "{s}"),
2558 Self::DirectiveMap(m) => {
2559 let len = m.len();
2560 let mut i = 0;
2561 for (directive, sources) in m {
2562 let sources: Vec<String> = sources.clone().into();
2563 write!(f, "{} {}", directive, sources.join(" "))?;
2564 i += 1;
2565 if i != len {
2566 write!(f, "; ")?;
2567 }
2568 }
2569 Ok(())
2570 }
2571 }
2572 }
2573}
2574
2575#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2577#[serde(untagged)]
2578#[cfg_attr(feature = "schema", derive(JsonSchema))]
2579pub enum DisabledCspModificationKind {
2580 Flag(bool),
2583 List(Vec<String>),
2585}
2586
2587impl DisabledCspModificationKind {
2588 pub fn can_modify(&self, directive: &str) -> bool {
2590 match self {
2591 Self::Flag(f) => !f,
2592 Self::List(l) => !l.contains(&directive.into()),
2593 }
2594 }
2595}
2596
2597impl Default for DisabledCspModificationKind {
2598 fn default() -> Self {
2599 Self::Flag(false)
2600 }
2601}
2602
2603#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2612#[serde(untagged)]
2613#[cfg_attr(feature = "schema", derive(JsonSchema))]
2614pub enum FsScope {
2615 AllowedPaths(Vec<PathBuf>),
2617 #[serde(rename_all = "camelCase")]
2619 Scope {
2620 #[serde(default)]
2622 allow: Vec<PathBuf>,
2623 #[serde(default)]
2626 deny: Vec<PathBuf>,
2627 #[serde(alias = "require-literal-leading-dot")]
2636 require_literal_leading_dot: Option<bool>,
2637 },
2638}
2639
2640impl Default for FsScope {
2641 fn default() -> Self {
2642 Self::AllowedPaths(Vec::new())
2643 }
2644}
2645
2646impl FsScope {
2647 pub fn allowed_paths(&self) -> &Vec<PathBuf> {
2649 match self {
2650 Self::AllowedPaths(p) => p,
2651 Self::Scope { allow, .. } => allow,
2652 }
2653 }
2654
2655 pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
2657 match self {
2658 Self::AllowedPaths(_) => None,
2659 Self::Scope { deny, .. } => Some(deny),
2660 }
2661 }
2662}
2663
2664#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2668#[cfg_attr(feature = "schema", derive(JsonSchema))]
2669#[serde(rename_all = "camelCase", deny_unknown_fields)]
2670pub struct AssetProtocolConfig {
2671 #[serde(default)]
2673 pub scope: FsScope,
2674 #[serde(default)]
2676 pub enable: bool,
2677}
2678
2679#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
2683#[cfg_attr(feature = "schema", derive(JsonSchema))]
2684#[serde(rename_all = "camelCase", untagged)]
2685pub enum HeaderSource {
2686 Inline(String),
2688 List(Vec<String>),
2690 Map(HashMap<String, String>),
2692}
2693
2694impl Serialize for HeaderSource {
2695 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2696 where
2697 S: Serializer,
2698 {
2699 match self {
2700 Self::Inline(s) => serializer.serialize_str(s),
2701 Self::List(l) => l.serialize(serializer),
2702 Self::Map(m) => {
2703 let btree_map: BTreeMap<_, _> = m.iter().collect();
2707 btree_map.serialize(serializer)
2708 }
2709 }
2710 }
2711}
2712
2713impl Display for HeaderSource {
2714 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2715 match self {
2716 Self::Inline(s) => write!(f, "{s}"),
2717 Self::List(l) => write!(f, "{}", l.join(", ")),
2718 Self::Map(m) => {
2719 let len = m.len();
2720 let mut i = 0;
2721 for (key, value) in m {
2722 write!(f, "{key} {value}")?;
2723 i += 1;
2724 if i != len {
2725 write!(f, "; ")?;
2726 }
2727 }
2728 Ok(())
2729 }
2730 }
2731 }
2732}
2733
2734pub trait HeaderAddition {
2738 fn add_configured_headers(self, headers: Option<&HeaderConfig>) -> http::response::Builder;
2740}
2741
2742impl HeaderAddition for http::response::Builder {
2743 fn add_configured_headers(mut self, headers: Option<&HeaderConfig>) -> http::response::Builder {
2747 if let Some(headers) = headers {
2748 if let Some(value) = &headers.access_control_allow_credentials {
2750 self = self.header("Access-Control-Allow-Credentials", value.to_string());
2751 };
2752
2753 if let Some(value) = &headers.access_control_allow_headers {
2755 self = self.header("Access-Control-Allow-Headers", value.to_string());
2756 };
2757
2758 if let Some(value) = &headers.access_control_allow_methods {
2760 self = self.header("Access-Control-Allow-Methods", value.to_string());
2761 };
2762
2763 if let Some(value) = &headers.access_control_expose_headers {
2765 self = self.header("Access-Control-Expose-Headers", value.to_string());
2766 };
2767
2768 if let Some(value) = &headers.access_control_max_age {
2770 self = self.header("Access-Control-Max-Age", value.to_string());
2771 };
2772
2773 if let Some(value) = &headers.cross_origin_embedder_policy {
2775 self = self.header("Cross-Origin-Embedder-Policy", value.to_string());
2776 };
2777
2778 if let Some(value) = &headers.cross_origin_opener_policy {
2780 self = self.header("Cross-Origin-Opener-Policy", value.to_string());
2781 };
2782
2783 if let Some(value) = &headers.cross_origin_resource_policy {
2785 self = self.header("Cross-Origin-Resource-Policy", value.to_string());
2786 };
2787
2788 if let Some(value) = &headers.permissions_policy {
2790 self = self.header("Permission-Policy", value.to_string());
2791 };
2792
2793 if let Some(value) = &headers.service_worker_allowed {
2794 self = self.header("Service-Worker-Allowed", value.to_string());
2795 }
2796
2797 if let Some(value) = &headers.timing_allow_origin {
2799 self = self.header("Timing-Allow-Origin", value.to_string());
2800 };
2801
2802 if let Some(value) = &headers.x_content_type_options {
2804 self = self.header("X-Content-Type-Options", value.to_string());
2805 };
2806
2807 if let Some(value) = &headers.tauri_custom_header {
2809 self = self.header("Tauri-Custom-Header", value.to_string());
2811 };
2812 }
2813 self
2814 }
2815}
2816
2817#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2869#[cfg_attr(feature = "schema", derive(JsonSchema))]
2870#[serde(deny_unknown_fields)]
2871pub struct HeaderConfig {
2872 #[serde(rename = "Access-Control-Allow-Credentials")]
2877 pub access_control_allow_credentials: Option<HeaderSource>,
2878 #[serde(rename = "Access-Control-Allow-Headers")]
2886 pub access_control_allow_headers: Option<HeaderSource>,
2887 #[serde(rename = "Access-Control-Allow-Methods")]
2892 pub access_control_allow_methods: Option<HeaderSource>,
2893 #[serde(rename = "Access-Control-Expose-Headers")]
2899 pub access_control_expose_headers: Option<HeaderSource>,
2900 #[serde(rename = "Access-Control-Max-Age")]
2907 pub access_control_max_age: Option<HeaderSource>,
2908 #[serde(rename = "Cross-Origin-Embedder-Policy")]
2913 pub cross_origin_embedder_policy: Option<HeaderSource>,
2914 #[serde(rename = "Cross-Origin-Opener-Policy")]
2921 pub cross_origin_opener_policy: Option<HeaderSource>,
2922 #[serde(rename = "Cross-Origin-Resource-Policy")]
2927 pub cross_origin_resource_policy: Option<HeaderSource>,
2928 #[serde(rename = "Permissions-Policy")]
2933 pub permissions_policy: Option<HeaderSource>,
2934 #[serde(rename = "Service-Worker-Allowed")]
2944 pub service_worker_allowed: Option<HeaderSource>,
2945 #[serde(rename = "Timing-Allow-Origin")]
2951 pub timing_allow_origin: Option<HeaderSource>,
2952 #[serde(rename = "X-Content-Type-Options")]
2959 pub x_content_type_options: Option<HeaderSource>,
2960 #[serde(rename = "Tauri-Custom-Header")]
2965 pub tauri_custom_header: Option<HeaderSource>,
2966}
2967
2968impl HeaderConfig {
2969 pub fn new() -> Self {
2971 HeaderConfig {
2972 access_control_allow_credentials: None,
2973 access_control_allow_methods: None,
2974 access_control_allow_headers: None,
2975 access_control_expose_headers: None,
2976 access_control_max_age: None,
2977 cross_origin_embedder_policy: None,
2978 cross_origin_opener_policy: None,
2979 cross_origin_resource_policy: None,
2980 permissions_policy: None,
2981 service_worker_allowed: None,
2982 timing_allow_origin: None,
2983 x_content_type_options: None,
2984 tauri_custom_header: None,
2985 }
2986 }
2987}
2988
2989#[skip_serializing_none]
2993#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
2994#[cfg_attr(feature = "schema", derive(JsonSchema))]
2995#[serde(rename_all = "camelCase", deny_unknown_fields)]
2996pub struct SecurityConfig {
2997 pub csp: Option<Csp>,
3003 #[serde(alias = "dev-csp")]
3008 pub dev_csp: Option<Csp>,
3009 #[serde(default, alias = "freeze-prototype")]
3011 pub freeze_prototype: bool,
3012 #[serde(default, alias = "dangerous-disable-asset-csp-modification")]
3025 pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
3026 #[serde(default, alias = "asset-protocol")]
3028 pub asset_protocol: AssetProtocolConfig,
3029 #[serde(default)]
3031 pub pattern: PatternKind,
3032 #[serde(default)]
3055 pub capabilities: Vec<CapabilityEntry>,
3056 #[serde(default)]
3059 pub headers: Option<HeaderConfig>,
3060}
3061
3062#[derive(Debug, Clone, PartialEq, Serialize)]
3064#[cfg_attr(feature = "schema", derive(JsonSchema))]
3065#[serde(untagged)]
3066pub enum CapabilityEntry {
3067 Inlined(Capability),
3069 Reference(String),
3071}
3072
3073impl<'de> Deserialize<'de> for CapabilityEntry {
3074 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3075 where
3076 D: Deserializer<'de>,
3077 {
3078 UntaggedEnumVisitor::new()
3079 .string(|string| Ok(Self::Reference(string.to_owned())))
3080 .map(|map| map.deserialize::<Capability>().map(Self::Inlined))
3081 .deserialize(deserializer)
3082 }
3083}
3084
3085#[skip_serializing_none]
3087#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
3088#[serde(rename_all = "lowercase", tag = "use", content = "options")]
3089#[cfg_attr(feature = "schema", derive(JsonSchema))]
3090pub enum PatternKind {
3091 #[default]
3093 Brownfield,
3094 Isolation {
3096 dir: PathBuf,
3098 },
3099}
3100
3101#[skip_serializing_none]
3105#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3106#[cfg_attr(feature = "schema", derive(JsonSchema))]
3107#[serde(rename_all = "camelCase", deny_unknown_fields)]
3108pub struct AppConfig {
3109 #[serde(default)]
3164 pub windows: Vec<WindowConfig>,
3165 #[serde(default)]
3167 pub security: SecurityConfig,
3168 #[serde(alias = "tray-icon")]
3170 pub tray_icon: Option<TrayIconConfig>,
3171 #[serde(rename = "macOSPrivateApi", alias = "macos-private-api", default)]
3173 pub macos_private_api: bool,
3174 #[serde(default, alias = "with-global-tauri")]
3176 pub with_global_tauri: bool,
3177 #[serde(rename = "enableGTKAppId", alias = "enable-gtk-app-id", default)]
3179 pub enable_gtk_app_id: bool,
3180}
3181
3182impl AppConfig {
3183 pub fn all_features() -> Vec<&'static str> {
3185 vec![
3186 "tray-icon",
3187 "macos-private-api",
3188 "protocol-asset",
3189 "isolation",
3190 ]
3191 }
3192
3193 pub fn features(&self) -> Vec<&str> {
3195 let mut features = Vec::new();
3196 if self.tray_icon.is_some() {
3197 features.push("tray-icon");
3198 }
3199 if self.macos_private_api {
3200 features.push("macos-private-api");
3201 }
3202 if self.security.asset_protocol.enable {
3203 features.push("protocol-asset");
3204 }
3205
3206 if let PatternKind::Isolation { .. } = self.security.pattern {
3207 features.push("isolation");
3208 }
3209
3210 features.sort_unstable();
3211 features
3212 }
3213}
3214
3215#[skip_serializing_none]
3219#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
3220#[cfg_attr(feature = "schema", derive(JsonSchema))]
3221#[serde(rename_all = "camelCase", deny_unknown_fields)]
3222pub struct TrayIconConfig {
3223 pub id: Option<String>,
3225 #[serde(alias = "icon-path")]
3231 pub icon_path: PathBuf,
3232 #[serde(default, alias = "icon-as-template")]
3234 pub icon_as_template: bool,
3235 #[serde(default = "default_true", alias = "menu-on-left-click")]
3243 #[deprecated(
3244 since = "2.2.0",
3245 note = "No longer works, use `show_menu_on_left_click` instead."
3246 )]
3247 pub menu_on_left_click: bool,
3248 #[serde(default = "default_true", alias = "show-menu-on-left-click")]
3254 pub show_menu_on_left_click: bool,
3255 pub title: Option<String>,
3257 pub tooltip: Option<String>,
3259}
3260
3261#[skip_serializing_none]
3263#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3264#[cfg_attr(feature = "schema", derive(JsonSchema))]
3265#[serde(rename_all = "camelCase", deny_unknown_fields)]
3266pub struct IosConfig {
3267 pub template: Option<PathBuf>,
3271 pub frameworks: Option<Vec<String>>,
3275 #[serde(alias = "development-team")]
3278 pub development_team: Option<String>,
3279 #[serde(alias = "bundle-version")]
3283 pub bundle_version: Option<String>,
3284 #[serde(
3288 alias = "minimum-system-version",
3289 default = "ios_minimum_system_version"
3290 )]
3291 pub minimum_system_version: String,
3292 #[serde(alias = "info-plist")]
3296 pub info_plist: Option<PathBuf>,
3297}
3298
3299impl Default for IosConfig {
3300 fn default() -> Self {
3301 Self {
3302 template: None,
3303 frameworks: None,
3304 development_team: None,
3305 bundle_version: None,
3306 minimum_system_version: ios_minimum_system_version(),
3307 info_plist: None,
3308 }
3309 }
3310}
3311
3312#[skip_serializing_none]
3314#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3315#[cfg_attr(feature = "schema", derive(JsonSchema))]
3316#[serde(rename_all = "camelCase", deny_unknown_fields)]
3317pub struct AndroidConfig {
3318 #[serde(alias = "min-sdk-version", default = "default_min_sdk_version")]
3321 pub min_sdk_version: u32,
3322
3323 #[serde(alias = "version-code")]
3329 #[cfg_attr(feature = "schema", validate(range(min = 1, max = 2_100_000_000)))]
3330 pub version_code: Option<u32>,
3331
3332 #[serde(alias = "auto-increment-version-code", default)]
3340 pub auto_increment_version_code: bool,
3341
3342 #[serde(alias = "debug-application-id-suffix")]
3346 pub debug_application_id_suffix: Option<String>,
3347}
3348
3349impl Default for AndroidConfig {
3350 fn default() -> Self {
3351 Self {
3352 min_sdk_version: default_min_sdk_version(),
3353 version_code: None,
3354 auto_increment_version_code: false,
3355 debug_application_id_suffix: None,
3356 }
3357 }
3358}
3359
3360fn default_min_sdk_version() -> u32 {
3361 24
3362}
3363
3364#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3366#[cfg_attr(feature = "schema", derive(JsonSchema))]
3367#[serde(untagged, deny_unknown_fields)]
3368#[non_exhaustive]
3369pub enum FrontendDist {
3370 Url(Url),
3372 Directory(PathBuf),
3374 Files(Vec<PathBuf>),
3376}
3377
3378impl std::fmt::Display for FrontendDist {
3379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3380 match self {
3381 Self::Url(url) => write!(f, "{url}"),
3382 Self::Directory(p) => write!(f, "{}", p.display()),
3383 Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
3384 }
3385 }
3386}
3387
3388#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3390#[cfg_attr(feature = "schema", derive(JsonSchema))]
3391#[serde(rename_all = "camelCase", untagged)]
3392pub enum BeforeDevCommand {
3393 Script(String),
3395 ScriptWithOptions {
3397 script: String,
3399 cwd: Option<String>,
3401 #[serde(default)]
3403 wait: bool,
3404 },
3405}
3406
3407#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3409#[cfg_attr(feature = "schema", derive(JsonSchema))]
3410#[serde(rename_all = "camelCase", untagged)]
3411pub enum HookCommand {
3412 Script(String),
3414 ScriptWithOptions {
3416 script: String,
3418 cwd: Option<String>,
3420 },
3421}
3422
3423#[skip_serializing_none]
3425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3426#[cfg_attr(feature = "schema", derive(JsonSchema))]
3427#[serde(untagged)]
3428pub enum RunnerConfig {
3429 String(String),
3431 Object {
3433 cmd: String,
3435 cwd: Option<String>,
3437 args: Option<Vec<String>>,
3439 },
3440}
3441
3442impl Default for RunnerConfig {
3443 fn default() -> Self {
3444 RunnerConfig::String("cargo".to_string())
3445 }
3446}
3447
3448impl RunnerConfig {
3449 pub fn cmd(&self) -> &str {
3451 match self {
3452 RunnerConfig::String(cmd) => cmd,
3453 RunnerConfig::Object { cmd, .. } => cmd,
3454 }
3455 }
3456
3457 pub fn cwd(&self) -> Option<&str> {
3459 match self {
3460 RunnerConfig::String(_) => None,
3461 RunnerConfig::Object { cwd, .. } => cwd.as_deref(),
3462 }
3463 }
3464
3465 pub fn args(&self) -> Option<&[String]> {
3467 match self {
3468 RunnerConfig::String(_) => None,
3469 RunnerConfig::Object { args, .. } => args.as_deref(),
3470 }
3471 }
3472}
3473
3474impl std::str::FromStr for RunnerConfig {
3475 type Err = std::convert::Infallible;
3476
3477 fn from_str(s: &str) -> Result<Self, Self::Err> {
3478 Ok(RunnerConfig::String(s.to_string()))
3479 }
3480}
3481
3482impl From<&str> for RunnerConfig {
3483 fn from(s: &str) -> Self {
3484 RunnerConfig::String(s.to_string())
3485 }
3486}
3487
3488impl From<String> for RunnerConfig {
3489 fn from(s: String) -> Self {
3490 RunnerConfig::String(s)
3491 }
3492}
3493
3494#[skip_serializing_none]
3498#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
3499#[cfg_attr(feature = "schema", derive(JsonSchema))]
3500#[serde(rename_all = "camelCase", deny_unknown_fields)]
3501pub struct BuildConfig {
3502 pub runner: Option<RunnerConfig>,
3504 #[serde(alias = "dev-url")]
3512 pub dev_url: Option<Url>,
3513 #[serde(alias = "frontend-dist")]
3527 pub frontend_dist: Option<FrontendDist>,
3528 #[serde(alias = "before-dev-command")]
3532 pub before_dev_command: Option<BeforeDevCommand>,
3533 #[serde(alias = "before-build-command")]
3537 pub before_build_command: Option<HookCommand>,
3538 #[serde(alias = "before-bundle-command")]
3542 pub before_bundle_command: Option<HookCommand>,
3543 pub features: Option<Vec<String>>,
3545 #[serde(alias = "remove-unused-commands", default)]
3553 pub remove_unused_commands: bool,
3554 #[serde(alias = "additional-watch-directories", default)]
3556 pub additional_watch_folders: Vec<PathBuf>,
3557 #[serde(default)]
3559 pub windows: WindowsBuildConfig,
3560}
3561
3562#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3564#[cfg_attr(feature = "schema", derive(JsonSchema))]
3565#[serde(rename_all = "camelCase", deny_unknown_fields)]
3566pub struct WindowsBuildConfig {
3567 #[serde(
3569 default = "default_true",
3570 rename = "staticVCRuntime",
3571 alias = "static-vc-runtime",
3572 alias = "staticVcRuntime"
3573 )]
3574 pub static_vc_runtime: bool,
3575}
3576
3577impl Default for WindowsBuildConfig {
3578 fn default() -> Self {
3579 Self {
3580 static_vc_runtime: true,
3581 }
3582 }
3583}
3584
3585#[derive(Debug, PartialEq, Eq)]
3586struct PackageVersion(String);
3587
3588impl<'d> serde::Deserialize<'d> for PackageVersion {
3589 fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
3590 struct PackageVersionVisitor;
3591
3592 impl Visitor<'_> for PackageVersionVisitor {
3593 type Value = PackageVersion;
3594
3595 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3596 write!(
3597 formatter,
3598 "a semver string or a path to a package.json file"
3599 )
3600 }
3601
3602 fn visit_str<E: DeError>(self, value: &str) -> Result<PackageVersion, E> {
3603 let path = PathBuf::from(value);
3604 if path.exists() {
3605 let json_str = read_to_string(&path)
3606 .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3607 let package_json: serde_json::Value = serde_json::from_str(&json_str)
3608 .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3609 if let Some(obj) = package_json.as_object() {
3610 let version = obj
3611 .get("version")
3612 .ok_or_else(|| DeError::custom("JSON must contain a `version` field"))?
3613 .as_str()
3614 .ok_or_else(|| {
3615 DeError::custom(format!("`{} > version` must be a string", path.display()))
3616 })?;
3617 Ok(PackageVersion(
3618 Version::from_str(version)
3619 .map_err(|_| {
3620 DeError::custom("`tauri.conf.json > version` must be a semver string")
3621 })?
3622 .to_string(),
3623 ))
3624 } else {
3625 Err(DeError::custom(
3626 "`tauri.conf.json > version` value is not a path to a JSON object",
3627 ))
3628 }
3629 } else {
3630 Ok(PackageVersion(
3631 Version::from_str(value)
3632 .map_err(|_| DeError::custom("`tauri.conf.json > version` must be a semver string"))?
3633 .to_string(),
3634 ))
3635 }
3636 }
3637 }
3638
3639 deserializer.deserialize_string(PackageVersionVisitor {})
3640 }
3641}
3642
3643fn version_deserializer<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
3644where
3645 D: Deserializer<'de>,
3646{
3647 Option::<PackageVersion>::deserialize(deserializer).map(|v| v.map(|v| v.0))
3648}
3649
3650#[skip_serializing_none]
3716#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3717#[cfg_attr(feature = "schema", derive(JsonSchema))]
3718#[serde(rename_all = "camelCase", deny_unknown_fields)]
3719pub struct Config {
3720 #[serde(rename = "$schema")]
3722 pub schema: Option<String>,
3723 #[serde(alias = "product-name")]
3741 #[cfg_attr(feature = "schema", schemars(regex(pattern = "^[^/\\:*?\"<>|]+$")))]
3742 pub product_name: Option<String>,
3743 #[serde(alias = "main-binary-name")]
3757 pub main_binary_name: Option<String>,
3758 #[serde(deserialize_with = "version_deserializer", default)]
3774 pub version: Option<String>,
3775 pub identifier: String,
3783 #[serde(default)]
3785 pub app: AppConfig,
3786 #[serde(default)]
3788 pub build: BuildConfig,
3789 #[serde(default)]
3791 pub bundle: BundleConfig,
3792 #[serde(default)]
3794 pub plugins: PluginConfig,
3795}
3796
3797#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
3801#[cfg_attr(feature = "schema", derive(JsonSchema))]
3802pub struct PluginConfig(pub HashMap<String, JsonValue>);
3803
3804impl Serialize for PluginConfig {
3805 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
3806 where
3807 S: Serializer,
3808 {
3809 let btree_map: BTreeMap<_, _> = self.0.iter().collect();
3813 btree_map.serialize(serializer)
3814 }
3815}
3816
3817#[cfg(any(feature = "build", feature = "build-2"))]
3823mod build {
3824 use super::*;
3825 use crate::{literal_struct, tokens::*};
3826 use proc_macro2::TokenStream;
3827 use quote::{ToTokens, TokenStreamExt, quote};
3828 use std::convert::identity;
3829
3830 impl ToTokens for WebviewUrl {
3831 fn to_tokens(&self, tokens: &mut TokenStream) {
3832 let prefix = quote! { ::tauri::utils::config::WebviewUrl };
3833
3834 tokens.append_all(match self {
3835 Self::App(path) => {
3836 let path = path_buf_lit(path);
3837 quote! { #prefix::App(#path) }
3838 }
3839 Self::External(url) => {
3840 let url = url_lit(url);
3841 quote! { #prefix::External(#url) }
3842 }
3843 Self::CustomProtocol(url) => {
3844 let url = url_lit(url);
3845 quote! { #prefix::CustomProtocol(#url) }
3846 }
3847 })
3848 }
3849 }
3850
3851 impl ToTokens for BackgroundThrottlingPolicy {
3852 fn to_tokens(&self, tokens: &mut TokenStream) {
3853 let prefix = quote! { ::tauri::utils::config::BackgroundThrottlingPolicy };
3854 tokens.append_all(match self {
3855 Self::Disabled => quote! { #prefix::Disabled },
3856 Self::Throttle => quote! { #prefix::Throttle },
3857 Self::Suspend => quote! { #prefix::Suspend },
3858 })
3859 }
3860 }
3861
3862 impl ToTokens for crate::Theme {
3863 fn to_tokens(&self, tokens: &mut TokenStream) {
3864 let prefix = quote! { ::tauri::utils::Theme };
3865
3866 tokens.append_all(match self {
3867 Self::Light => quote! { #prefix::Light },
3868 Self::Dark => quote! { #prefix::Dark },
3869 })
3870 }
3871 }
3872
3873 impl ToTokens for Color {
3874 fn to_tokens(&self, tokens: &mut TokenStream) {
3875 let Color(r, g, b, a) = self;
3876 tokens.append_all(quote! {::tauri::utils::config::Color(#r,#g,#b,#a)});
3877 }
3878 }
3879 impl ToTokens for WindowEffectsConfig {
3880 fn to_tokens(&self, tokens: &mut TokenStream) {
3881 let effects = vec_lit(self.effects.clone(), |d| d);
3882 let state = opt_lit(self.state.as_ref());
3883 let radius = opt_lit(self.radius.as_ref());
3884 let color = opt_lit(self.color.as_ref());
3885
3886 literal_struct!(
3887 tokens,
3888 ::tauri::utils::config::WindowEffectsConfig,
3889 effects,
3890 state,
3891 radius,
3892 color
3893 )
3894 }
3895 }
3896
3897 impl ToTokens for crate::TitleBarStyle {
3898 fn to_tokens(&self, tokens: &mut TokenStream) {
3899 let prefix = quote! { ::tauri::utils::TitleBarStyle };
3900
3901 tokens.append_all(match self {
3902 Self::Visible => quote! { #prefix::Visible },
3903 Self::Transparent => quote! { #prefix::Transparent },
3904 Self::Overlay => quote! { #prefix::Overlay },
3905 })
3906 }
3907 }
3908
3909 impl ToTokens for LogicalPosition {
3910 fn to_tokens(&self, tokens: &mut TokenStream) {
3911 let LogicalPosition { x, y } = self;
3912 literal_struct!(tokens, ::tauri::utils::config::LogicalPosition, x, y)
3913 }
3914 }
3915
3916 impl ToTokens for crate::WindowEffect {
3917 fn to_tokens(&self, tokens: &mut TokenStream) {
3918 let prefix = quote! { ::tauri::utils::WindowEffect };
3919
3920 #[allow(deprecated)]
3921 tokens.append_all(match self {
3922 WindowEffect::AppearanceBased => quote! { #prefix::AppearanceBased},
3923 WindowEffect::Light => quote! { #prefix::Light},
3924 WindowEffect::Dark => quote! { #prefix::Dark},
3925 WindowEffect::MediumLight => quote! { #prefix::MediumLight},
3926 WindowEffect::UltraDark => quote! { #prefix::UltraDark},
3927 WindowEffect::Titlebar => quote! { #prefix::Titlebar},
3928 WindowEffect::Selection => quote! { #prefix::Selection},
3929 WindowEffect::Menu => quote! { #prefix::Menu},
3930 WindowEffect::Popover => quote! { #prefix::Popover},
3931 WindowEffect::Sidebar => quote! { #prefix::Sidebar},
3932 WindowEffect::HeaderView => quote! { #prefix::HeaderView},
3933 WindowEffect::Sheet => quote! { #prefix::Sheet},
3934 WindowEffect::WindowBackground => quote! { #prefix::WindowBackground},
3935 WindowEffect::HudWindow => quote! { #prefix::HudWindow},
3936 WindowEffect::FullScreenUI => quote! { #prefix::FullScreenUI},
3937 WindowEffect::Tooltip => quote! { #prefix::Tooltip},
3938 WindowEffect::ContentBackground => quote! { #prefix::ContentBackground},
3939 WindowEffect::UnderWindowBackground => quote! { #prefix::UnderWindowBackground},
3940 WindowEffect::UnderPageBackground => quote! { #prefix::UnderPageBackground},
3941 WindowEffect::Mica => quote! { #prefix::Mica},
3942 WindowEffect::MicaDark => quote! { #prefix::MicaDark},
3943 WindowEffect::MicaLight => quote! { #prefix::MicaLight},
3944 WindowEffect::Blur => quote! { #prefix::Blur},
3945 WindowEffect::Acrylic => quote! { #prefix::Acrylic},
3946 WindowEffect::Tabbed => quote! { #prefix::Tabbed },
3947 WindowEffect::TabbedDark => quote! { #prefix::TabbedDark },
3948 WindowEffect::TabbedLight => quote! { #prefix::TabbedLight },
3949 })
3950 }
3951 }
3952
3953 impl ToTokens for crate::WindowEffectState {
3954 fn to_tokens(&self, tokens: &mut TokenStream) {
3955 let prefix = quote! { ::tauri::utils::WindowEffectState };
3956
3957 #[allow(deprecated)]
3958 tokens.append_all(match self {
3959 WindowEffectState::Active => quote! { #prefix::Active},
3960 WindowEffectState::FollowsWindowActiveState => quote! { #prefix::FollowsWindowActiveState},
3961 WindowEffectState::Inactive => quote! { #prefix::Inactive},
3962 })
3963 }
3964 }
3965
3966 impl ToTokens for PreventOverflowMargin {
3967 fn to_tokens(&self, tokens: &mut TokenStream) {
3968 let width = self.width;
3969 let height = self.height;
3970
3971 literal_struct!(
3972 tokens,
3973 ::tauri::utils::config::PreventOverflowMargin,
3974 width,
3975 height
3976 )
3977 }
3978 }
3979
3980 impl ToTokens for PreventOverflowConfig {
3981 fn to_tokens(&self, tokens: &mut TokenStream) {
3982 let prefix = quote! { ::tauri::utils::config::PreventOverflowConfig };
3983
3984 #[allow(deprecated)]
3985 tokens.append_all(match self {
3986 Self::Enable(enable) => quote! { #prefix::Enable(#enable) },
3987 Self::Margin(margin) => quote! { #prefix::Margin(#margin) },
3988 })
3989 }
3990 }
3991
3992 impl ToTokens for ScrollBarStyle {
3993 fn to_tokens(&self, tokens: &mut TokenStream) {
3994 let prefix = quote! { ::tauri::utils::config::ScrollBarStyle };
3995
3996 tokens.append_all(match self {
3997 Self::Default => quote! { #prefix::Default },
3998 Self::FluentOverlay => quote! { #prefix::FluentOverlay },
3999 })
4000 }
4001 }
4002
4003 impl ToTokens for WindowConfig {
4004 fn to_tokens(&self, tokens: &mut TokenStream) {
4005 let label = str_lit(&self.label);
4006 let create = &self.create;
4007 let url = &self.url;
4008 let user_agent = opt_str_lit(self.user_agent.as_ref());
4009 let drag_drop_enabled = self.drag_drop_enabled;
4010 let center = self.center;
4011 let x = opt_lit(self.x.as_ref());
4012 let y = opt_lit(self.y.as_ref());
4013 let width = self.width;
4014 let height = self.height;
4015 let min_width = opt_lit(self.min_width.as_ref());
4016 let min_height = opt_lit(self.min_height.as_ref());
4017 let max_width = opt_lit(self.max_width.as_ref());
4018 let max_height = opt_lit(self.max_height.as_ref());
4019 let prevent_overflow = opt_lit(self.prevent_overflow.as_ref());
4020 let resizable = self.resizable;
4021 let maximizable = self.maximizable;
4022 let minimizable = self.minimizable;
4023 let closable = self.closable;
4024 let title = str_lit(&self.title);
4025 let proxy_url = opt_lit(self.proxy_url.as_ref().map(url_lit).as_ref());
4026 let fullscreen = self.fullscreen;
4027 let focus = self.focus;
4028 let focusable = self.focusable;
4029 let transparent = self.transparent;
4030 let maximized = self.maximized;
4031 let visible = self.visible;
4032 let decorations = self.decorations;
4033 let always_on_bottom = self.always_on_bottom;
4034 let always_on_top = self.always_on_top;
4035 let visible_on_all_workspaces = self.visible_on_all_workspaces;
4036 let content_protected = self.content_protected;
4037 let skip_taskbar = self.skip_taskbar;
4038 let window_classname = opt_str_lit(self.window_classname.as_ref());
4039 let no_redirection_bitmap = self.no_redirection_bitmap;
4040 let theme = opt_lit(self.theme.as_ref());
4041 let title_bar_style = &self.title_bar_style;
4042 let traffic_light_position = opt_lit(self.traffic_light_position.as_ref());
4043 let hidden_title = self.hidden_title;
4044 let accept_first_mouse = self.accept_first_mouse;
4045 let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
4046 let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref());
4047 let shadow = self.shadow;
4048 let window_effects = opt_lit(self.window_effects.as_ref());
4049 let incognito = self.incognito;
4050 let parent = opt_str_lit(self.parent.as_ref());
4051 let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled;
4052 let browser_extensions_enabled = self.browser_extensions_enabled;
4053 let use_https_scheme = self.use_https_scheme;
4054 let devtools = opt_lit(self.devtools.as_ref());
4055 let background_color = opt_lit(self.background_color.as_ref());
4056 let background_throttling = opt_lit(self.background_throttling.as_ref());
4057 let javascript_disabled = self.javascript_disabled;
4058 let allow_link_preview = self.allow_link_preview;
4059 let disable_input_accessory_view = self.disable_input_accessory_view;
4060 let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref());
4061 let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity);
4062 let scroll_bar_style = &self.scroll_bar_style;
4063 let limit_navigations_to_app_bound_domains = self.limit_navigations_to_app_bound_domains;
4064 let activity_name = opt_lit(self.activity_name.as_ref());
4065 let created_by_activity_name = opt_lit(self.created_by_activity_name.as_ref());
4066 let requested_by_scene_identifier = opt_lit(self.requested_by_scene_identifier.as_ref());
4067 let general_autofill_enabled = self.general_autofill_enabled;
4068
4069 literal_struct!(
4070 tokens,
4071 ::tauri::utils::config::WindowConfig,
4072 label,
4073 url,
4074 create,
4075 user_agent,
4076 drag_drop_enabled,
4077 center,
4078 x,
4079 y,
4080 width,
4081 height,
4082 min_width,
4083 min_height,
4084 max_width,
4085 max_height,
4086 prevent_overflow,
4087 resizable,
4088 maximizable,
4089 minimizable,
4090 closable,
4091 title,
4092 proxy_url,
4093 fullscreen,
4094 focus,
4095 focusable,
4096 transparent,
4097 maximized,
4098 visible,
4099 decorations,
4100 always_on_bottom,
4101 always_on_top,
4102 visible_on_all_workspaces,
4103 content_protected,
4104 skip_taskbar,
4105 window_classname,
4106 no_redirection_bitmap,
4107 theme,
4108 title_bar_style,
4109 traffic_light_position,
4110 hidden_title,
4111 accept_first_mouse,
4112 tabbing_identifier,
4113 additional_browser_args,
4114 shadow,
4115 window_effects,
4116 incognito,
4117 parent,
4118 zoom_hotkeys_enabled,
4119 browser_extensions_enabled,
4120 use_https_scheme,
4121 devtools,
4122 background_color,
4123 background_throttling,
4124 javascript_disabled,
4125 allow_link_preview,
4126 disable_input_accessory_view,
4127 data_directory,
4128 data_store_identifier,
4129 scroll_bar_style,
4130 limit_navigations_to_app_bound_domains,
4131 activity_name,
4132 created_by_activity_name,
4133 requested_by_scene_identifier,
4134 general_autofill_enabled
4135 );
4136 }
4137 }
4138
4139 impl ToTokens for PatternKind {
4140 fn to_tokens(&self, tokens: &mut TokenStream) {
4141 let prefix = quote! { ::tauri::utils::config::PatternKind };
4142
4143 tokens.append_all(match self {
4144 Self::Brownfield => quote! { #prefix::Brownfield },
4145 #[cfg(not(feature = "isolation"))]
4146 Self::Isolation { dir: _ } => quote! { #prefix::Brownfield },
4147 #[cfg(feature = "isolation")]
4148 Self::Isolation { dir } => {
4149 let dir = path_buf_lit(dir);
4150 quote! { #prefix::Isolation { dir: #dir } }
4151 }
4152 })
4153 }
4154 }
4155
4156 impl ToTokens for WebviewInstallMode {
4157 fn to_tokens(&self, tokens: &mut TokenStream) {
4158 let prefix = quote! { ::tauri::utils::config::WebviewInstallMode };
4159
4160 tokens.append_all(match self {
4161 Self::Skip => quote! { #prefix::Skip },
4162 Self::DownloadBootstrapper { silent } => {
4163 quote! { #prefix::DownloadBootstrapper { silent: #silent } }
4164 }
4165 Self::EmbedBootstrapper { silent } => {
4166 quote! { #prefix::EmbedBootstrapper { silent: #silent } }
4167 }
4168 Self::OfflineInstaller { silent } => {
4169 quote! { #prefix::OfflineInstaller { silent: #silent } }
4170 }
4171 Self::FixedRuntime { path } => {
4172 let path = path_buf_lit(path);
4173 quote! { #prefix::FixedRuntime { path: #path } }
4174 }
4175 })
4176 }
4177 }
4178
4179 impl ToTokens for WindowsConfig {
4180 fn to_tokens(&self, tokens: &mut TokenStream) {
4181 let webview_install_mode = &self.webview_install_mode;
4182 tokens.append_all(quote! { ::tauri::utils::config::WindowsConfig {
4183 webview_install_mode: #webview_install_mode,
4184 ..Default::default()
4185 }})
4186 }
4187 }
4188
4189 impl ToTokens for BundleResources {
4190 fn to_tokens(&self, tokens: &mut TokenStream) {
4191 let prefix = quote! { ::tauri::utils::config::BundleResources };
4192
4193 tokens.append_all(match self {
4194 Self::List(paths) => {
4195 let paths = vec_lit(paths, str_lit);
4196 quote! { #prefix::List(#paths) }
4197 }
4198 Self::Map(map) => {
4199 let map = map_lit(
4200 quote! { ::std::collections::HashMap },
4201 map,
4202 str_lit,
4203 str_lit,
4204 );
4205 quote! { #prefix::Map(#map) }
4206 }
4207 })
4208 }
4209 }
4210
4211 impl ToTokens for BundleConfig {
4212 fn to_tokens(&self, tokens: &mut TokenStream) {
4213 let publisher = quote!(None);
4214 let homepage = quote!(None);
4215 let icon = vec_lit(&self.icon, str_lit);
4216 let active = self.active;
4217 let targets = quote!(Default::default());
4218 let create_updater_artifacts = quote!(Default::default());
4219 let resources = opt_lit(self.resources.as_ref());
4220 let copyright = quote!(None);
4221 let category = quote!(None);
4222 let file_associations = quote!(None);
4223 let short_description = quote!(None);
4224 let long_description = quote!(None);
4225 let use_local_tools_dir = self.use_local_tools_dir;
4226 let external_bin = opt_vec_lit(self.external_bin.as_ref(), str_lit);
4227 let windows = &self.windows;
4228 let license = opt_str_lit(self.license.as_ref());
4229 let license_file = opt_lit(self.license_file.as_ref().map(path_buf_lit).as_ref());
4230 let linux = quote!(Default::default());
4231 let macos = quote!(Default::default());
4232 let ios = quote!(Default::default());
4233 let android = quote!(Default::default());
4234 let cef = quote!(Default::default());
4235
4236 literal_struct!(
4237 tokens,
4238 ::tauri::utils::config::BundleConfig,
4239 active,
4240 publisher,
4241 homepage,
4242 icon,
4243 targets,
4244 create_updater_artifacts,
4245 resources,
4246 copyright,
4247 category,
4248 license,
4249 license_file,
4250 file_associations,
4251 short_description,
4252 long_description,
4253 use_local_tools_dir,
4254 external_bin,
4255 windows,
4256 linux,
4257 macos,
4258 ios,
4259 android,
4260 cef
4261 );
4262 }
4263 }
4264
4265 impl ToTokens for FrontendDist {
4266 fn to_tokens(&self, tokens: &mut TokenStream) {
4267 let prefix = quote! { ::tauri::utils::config::FrontendDist };
4268
4269 tokens.append_all(match self {
4270 Self::Url(url) => {
4271 let url = url_lit(url);
4272 quote! { #prefix::Url(#url) }
4273 }
4274 Self::Directory(path) => {
4275 let path = path_buf_lit(path);
4276 quote! { #prefix::Directory(#path) }
4277 }
4278 Self::Files(files) => {
4279 let files = vec_lit(files, path_buf_lit);
4280 quote! { #prefix::Files(#files) }
4281 }
4282 })
4283 }
4284 }
4285
4286 impl ToTokens for RunnerConfig {
4287 fn to_tokens(&self, tokens: &mut TokenStream) {
4288 let prefix = quote! { ::tauri::utils::config::RunnerConfig };
4289
4290 tokens.append_all(match self {
4291 Self::String(cmd) => {
4292 let cmd = cmd.as_str();
4293 quote!(#prefix::String(#cmd.into()))
4294 }
4295 Self::Object { cmd, cwd, args } => {
4296 let cmd = cmd.as_str();
4297 let cwd = opt_str_lit(cwd.as_ref());
4298 let args = opt_lit(args.as_ref().map(|v| vec_lit(v, str_lit)).as_ref());
4299 quote!(#prefix::Object {
4300 cmd: #cmd.into(),
4301 cwd: #cwd,
4302 args: #args,
4303 })
4304 }
4305 })
4306 }
4307 }
4308
4309 impl ToTokens for BuildConfig {
4310 fn to_tokens(&self, tokens: &mut TokenStream) {
4311 let dev_url = opt_lit(self.dev_url.as_ref().map(url_lit).as_ref());
4312 let frontend_dist = opt_lit(self.frontend_dist.as_ref());
4313 let runner = opt_lit(self.runner.as_ref());
4314 let before_dev_command = quote!(None);
4315 let before_build_command = quote!(None);
4316 let before_bundle_command = quote!(None);
4317 let features = quote!(None);
4318 let remove_unused_commands = quote!(false);
4319 let additional_watch_folders = quote!(Vec::new());
4320 let windows = &self.windows;
4321
4322 literal_struct!(
4323 tokens,
4324 ::tauri::utils::config::BuildConfig,
4325 runner,
4326 dev_url,
4327 frontend_dist,
4328 before_dev_command,
4329 before_build_command,
4330 before_bundle_command,
4331 features,
4332 remove_unused_commands,
4333 additional_watch_folders,
4334 windows
4335 );
4336 }
4337 }
4338
4339 impl ToTokens for WindowsBuildConfig {
4340 fn to_tokens(&self, tokens: &mut TokenStream) {
4341 let static_vc_runtime = self.static_vc_runtime;
4342
4343 literal_struct!(
4344 tokens,
4345 ::tauri::utils::config::WindowsBuildConfig,
4346 static_vc_runtime
4347 );
4348 }
4349 }
4350
4351 impl ToTokens for CspDirectiveSources {
4352 fn to_tokens(&self, tokens: &mut TokenStream) {
4353 let prefix = quote! { ::tauri::utils::config::CspDirectiveSources };
4354
4355 tokens.append_all(match self {
4356 Self::Inline(sources) => {
4357 let sources = sources.as_str();
4358 quote!(#prefix::Inline(#sources.into()))
4359 }
4360 Self::List(list) => {
4361 let list = vec_lit(list, str_lit);
4362 quote!(#prefix::List(#list))
4363 }
4364 })
4365 }
4366 }
4367
4368 impl ToTokens for Csp {
4369 fn to_tokens(&self, tokens: &mut TokenStream) {
4370 let prefix = quote! { ::tauri::utils::config::Csp };
4371
4372 tokens.append_all(match self {
4373 Self::Policy(policy) => {
4374 let policy = policy.as_str();
4375 quote!(#prefix::Policy(#policy.into()))
4376 }
4377 Self::DirectiveMap(list) => {
4378 let mut sorted: Vec<_> = list.iter().collect();
4382 sorted.sort_by_key(|(k, _)| *k);
4383 let map = map_lit(
4384 quote! { ::std::collections::HashMap },
4385 sorted,
4386 str_lit,
4387 identity,
4388 );
4389 quote!(#prefix::DirectiveMap(#map))
4390 }
4391 })
4392 }
4393 }
4394
4395 impl ToTokens for DisabledCspModificationKind {
4396 fn to_tokens(&self, tokens: &mut TokenStream) {
4397 let prefix = quote! { ::tauri::utils::config::DisabledCspModificationKind };
4398
4399 tokens.append_all(match self {
4400 Self::Flag(flag) => {
4401 quote! { #prefix::Flag(#flag) }
4402 }
4403 Self::List(directives) => {
4404 let directives = vec_lit(directives, str_lit);
4405 quote! { #prefix::List(#directives) }
4406 }
4407 });
4408 }
4409 }
4410
4411 impl ToTokens for CapabilityEntry {
4412 fn to_tokens(&self, tokens: &mut TokenStream) {
4413 let prefix = quote! { ::tauri::utils::config::CapabilityEntry };
4414
4415 tokens.append_all(match self {
4416 Self::Inlined(capability) => {
4417 quote! { #prefix::Inlined(#capability) }
4418 }
4419 Self::Reference(id) => {
4420 let id = str_lit(id);
4421 quote! { #prefix::Reference(#id) }
4422 }
4423 });
4424 }
4425 }
4426
4427 impl ToTokens for HeaderSource {
4428 fn to_tokens(&self, tokens: &mut TokenStream) {
4429 let prefix = quote! { ::tauri::utils::config::HeaderSource };
4430
4431 tokens.append_all(match self {
4432 Self::Inline(s) => {
4433 let line = s.as_str();
4434 quote!(#prefix::Inline(#line.into()))
4435 }
4436 Self::List(l) => {
4437 let list = vec_lit(l, str_lit);
4438 quote!(#prefix::List(#list))
4439 }
4440 Self::Map(m) => {
4441 let mut sorted: Vec<_> = m.iter().collect();
4445 sorted.sort_by_key(|(k, _)| *k);
4446 let map = map_lit(
4447 quote! { ::std::collections::HashMap },
4448 sorted,
4449 str_lit,
4450 str_lit,
4451 );
4452 quote!(#prefix::Map(#map))
4453 }
4454 })
4455 }
4456 }
4457
4458 impl ToTokens for HeaderConfig {
4459 fn to_tokens(&self, tokens: &mut TokenStream) {
4460 let access_control_allow_credentials =
4461 opt_lit(self.access_control_allow_credentials.as_ref());
4462 let access_control_allow_headers = opt_lit(self.access_control_allow_headers.as_ref());
4463 let access_control_allow_methods = opt_lit(self.access_control_allow_methods.as_ref());
4464 let access_control_expose_headers = opt_lit(self.access_control_expose_headers.as_ref());
4465 let access_control_max_age = opt_lit(self.access_control_max_age.as_ref());
4466 let cross_origin_embedder_policy = opt_lit(self.cross_origin_embedder_policy.as_ref());
4467 let cross_origin_opener_policy = opt_lit(self.cross_origin_opener_policy.as_ref());
4468 let cross_origin_resource_policy = opt_lit(self.cross_origin_resource_policy.as_ref());
4469 let permissions_policy = opt_lit(self.permissions_policy.as_ref());
4470 let service_worker_allowed = opt_lit(self.service_worker_allowed.as_ref());
4471 let timing_allow_origin = opt_lit(self.timing_allow_origin.as_ref());
4472 let x_content_type_options = opt_lit(self.x_content_type_options.as_ref());
4473 let tauri_custom_header = opt_lit(self.tauri_custom_header.as_ref());
4474
4475 literal_struct!(
4476 tokens,
4477 ::tauri::utils::config::HeaderConfig,
4478 access_control_allow_credentials,
4479 access_control_allow_headers,
4480 access_control_allow_methods,
4481 access_control_expose_headers,
4482 access_control_max_age,
4483 cross_origin_embedder_policy,
4484 cross_origin_opener_policy,
4485 cross_origin_resource_policy,
4486 permissions_policy,
4487 service_worker_allowed,
4488 timing_allow_origin,
4489 x_content_type_options,
4490 tauri_custom_header
4491 );
4492 }
4493 }
4494
4495 impl ToTokens for SecurityConfig {
4496 fn to_tokens(&self, tokens: &mut TokenStream) {
4497 let csp = opt_lit(self.csp.as_ref());
4498 let dev_csp = opt_lit(self.dev_csp.as_ref());
4499 let freeze_prototype = self.freeze_prototype;
4500 let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;
4501 let asset_protocol = &self.asset_protocol;
4502 let pattern = &self.pattern;
4503 let capabilities = vec_lit(&self.capabilities, identity);
4504 let headers = opt_lit(self.headers.as_ref());
4505
4506 literal_struct!(
4507 tokens,
4508 ::tauri::utils::config::SecurityConfig,
4509 csp,
4510 dev_csp,
4511 freeze_prototype,
4512 dangerous_disable_asset_csp_modification,
4513 asset_protocol,
4514 pattern,
4515 capabilities,
4516 headers
4517 );
4518 }
4519 }
4520
4521 impl ToTokens for TrayIconConfig {
4522 fn to_tokens(&self, tokens: &mut TokenStream) {
4523 tokens.append_all(quote!(#[allow(deprecated)]));
4525
4526 let id = opt_str_lit(self.id.as_ref());
4527 let icon_as_template = self.icon_as_template;
4528 #[allow(deprecated)]
4529 let menu_on_left_click = self.menu_on_left_click;
4530 let show_menu_on_left_click = self.show_menu_on_left_click;
4531 let icon_path = path_buf_lit(&self.icon_path);
4532 let title = opt_str_lit(self.title.as_ref());
4533 let tooltip = opt_str_lit(self.tooltip.as_ref());
4534 literal_struct!(
4535 tokens,
4536 ::tauri::utils::config::TrayIconConfig,
4537 id,
4538 icon_path,
4539 icon_as_template,
4540 menu_on_left_click,
4541 show_menu_on_left_click,
4542 title,
4543 tooltip
4544 );
4545 }
4546 }
4547
4548 impl ToTokens for FsScope {
4549 fn to_tokens(&self, tokens: &mut TokenStream) {
4550 let prefix = quote! { ::tauri::utils::config::FsScope };
4551
4552 tokens.append_all(match self {
4553 Self::AllowedPaths(allow) => {
4554 let allowed_paths = vec_lit(allow, path_buf_lit);
4555 quote! { #prefix::AllowedPaths(#allowed_paths) }
4556 }
4557 Self::Scope { allow, deny , require_literal_leading_dot} => {
4558 let allow = vec_lit(allow, path_buf_lit);
4559 let deny = vec_lit(deny, path_buf_lit);
4560 let require_literal_leading_dot = opt_lit(require_literal_leading_dot.as_ref());
4561 quote! { #prefix::Scope { allow: #allow, deny: #deny, require_literal_leading_dot: #require_literal_leading_dot } }
4562 }
4563 });
4564 }
4565 }
4566
4567 impl ToTokens for AssetProtocolConfig {
4568 fn to_tokens(&self, tokens: &mut TokenStream) {
4569 let scope = &self.scope;
4570 tokens.append_all(quote! { ::tauri::utils::config::AssetProtocolConfig { scope: #scope, ..Default::default() } })
4571 }
4572 }
4573
4574 impl ToTokens for AppConfig {
4575 fn to_tokens(&self, tokens: &mut TokenStream) {
4576 let windows = vec_lit(&self.windows, identity);
4577 let security = &self.security;
4578 let tray_icon = opt_lit(self.tray_icon.as_ref());
4579 let macos_private_api = self.macos_private_api;
4580 let with_global_tauri = self.with_global_tauri;
4581 let enable_gtk_app_id = self.enable_gtk_app_id;
4582
4583 literal_struct!(
4584 tokens,
4585 ::tauri::utils::config::AppConfig,
4586 windows,
4587 security,
4588 tray_icon,
4589 macos_private_api,
4590 with_global_tauri,
4591 enable_gtk_app_id
4592 );
4593 }
4594 }
4595
4596 impl ToTokens for PluginConfig {
4597 fn to_tokens(&self, tokens: &mut TokenStream) {
4598 let mut sorted: Vec<_> = self.0.iter().collect();
4602 sorted.sort_by_key(|(k, _)| *k);
4603 let config = map_lit(
4604 quote! { ::std::collections::HashMap },
4605 sorted,
4606 str_lit,
4607 json_value_lit,
4608 );
4609 tokens.append_all(quote! { ::tauri::utils::config::PluginConfig(#config) })
4610 }
4611 }
4612
4613 impl ToTokens for Config {
4614 fn to_tokens(&self, tokens: &mut TokenStream) {
4615 let schema = quote!(None);
4616 let product_name = opt_str_lit(self.product_name.as_ref());
4617 let main_binary_name = opt_str_lit(self.main_binary_name.as_ref());
4618 let version = opt_str_lit(self.version.as_ref());
4619 let identifier = str_lit(&self.identifier);
4620 let app = &self.app;
4621 let build = &self.build;
4622 let bundle = &self.bundle;
4623 let plugins = &self.plugins;
4624
4625 literal_struct!(
4626 tokens,
4627 ::tauri::utils::config::Config,
4628 schema,
4629 product_name,
4630 main_binary_name,
4631 version,
4632 identifier,
4633 app,
4634 build,
4635 bundle,
4636 plugins
4637 );
4638 }
4639 }
4640}
4641
4642#[cfg(test)]
4643mod test {
4644 use super::*;
4645
4646 #[test]
4649 fn test_defaults() {
4651 let a_config = AppConfig::default();
4653 let b_config = BuildConfig::default();
4655 let d_windows: Vec<WindowConfig> = vec![];
4657 let d_bundle = BundleConfig::default();
4659
4660 let app = AppConfig {
4662 windows: vec![],
4663 security: SecurityConfig {
4664 csp: None,
4665 dev_csp: None,
4666 freeze_prototype: false,
4667 dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
4668 asset_protocol: AssetProtocolConfig::default(),
4669 pattern: Default::default(),
4670 capabilities: Vec::new(),
4671 headers: None,
4672 },
4673 tray_icon: None,
4674 macos_private_api: false,
4675 with_global_tauri: false,
4676 enable_gtk_app_id: false,
4677 };
4678
4679 let build = BuildConfig {
4681 runner: None,
4682 dev_url: None,
4683 frontend_dist: None,
4684 before_dev_command: None,
4685 before_build_command: None,
4686 before_bundle_command: None,
4687 features: None,
4688 remove_unused_commands: false,
4689 additional_watch_folders: Vec::new(),
4690 windows: WindowsBuildConfig::default(),
4691 };
4692
4693 let bundle = BundleConfig {
4695 active: false,
4696 targets: Default::default(),
4697 create_updater_artifacts: Default::default(),
4698 publisher: None,
4699 homepage: None,
4700 icon: Vec::new(),
4701 resources: None,
4702 copyright: None,
4703 category: None,
4704 file_associations: None,
4705 short_description: None,
4706 long_description: None,
4707 use_local_tools_dir: false,
4708 license: None,
4709 license_file: None,
4710 linux: Default::default(),
4711 macos: Default::default(),
4712 external_bin: None,
4713 windows: Default::default(),
4714 ios: Default::default(),
4715 android: Default::default(),
4716 cef: Default::default(),
4717 };
4718
4719 assert_eq!(a_config, app);
4721 assert_eq!(b_config, build);
4722 assert_eq!(d_bundle, bundle);
4723 assert_eq!(d_windows, app.windows);
4724 }
4725
4726 #[test]
4727 fn parse_hex_color() {
4728 use super::Color;
4729
4730 assert_eq!(Color(255, 255, 255, 255), "fff".parse().unwrap());
4731 assert_eq!(Color(255, 255, 255, 255), "#fff".parse().unwrap());
4732 assert_eq!(Color(0, 0, 0, 255), "#000000".parse().unwrap());
4733 assert_eq!(Color(0, 0, 0, 255), "#000000ff".parse().unwrap());
4734 assert_eq!(Color(0, 255, 0, 255), "#00ff00ff".parse().unwrap());
4735 }
4736
4737 #[test]
4738 fn test_runner_config_string_format() {
4739 use super::RunnerConfig;
4740
4741 let json = r#""cargo""#;
4743 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4744
4745 assert_eq!(runner.cmd(), "cargo");
4746 assert_eq!(runner.cwd(), None);
4747 assert_eq!(runner.args(), None);
4748
4749 let serialized = serde_json::to_string(&runner).unwrap();
4751 assert_eq!(serialized, r#""cargo""#);
4752 }
4753
4754 #[test]
4755 fn test_runner_config_object_format_full() {
4756 use super::RunnerConfig;
4757
4758 let json = r#"{"cmd": "my_runner", "cwd": "/tmp/build", "args": ["--quiet", "--verbose"]}"#;
4760 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4761
4762 assert_eq!(runner.cmd(), "my_runner");
4763 assert_eq!(runner.cwd(), Some("/tmp/build"));
4764 assert_eq!(
4765 runner.args(),
4766 Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4767 );
4768
4769 let serialized = serde_json::to_string(&runner).unwrap();
4771 let deserialized: RunnerConfig = serde_json::from_str(&serialized).unwrap();
4772 assert_eq!(runner, deserialized);
4773 }
4774
4775 #[test]
4776 fn test_runner_config_object_format_minimal() {
4777 use super::RunnerConfig;
4778
4779 let json = r#"{"cmd": "cross"}"#;
4781 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4782
4783 assert_eq!(runner.cmd(), "cross");
4784 assert_eq!(runner.cwd(), None);
4785 assert_eq!(runner.args(), None);
4786 }
4787
4788 #[test]
4789 fn test_runner_config_default() {
4790 use super::RunnerConfig;
4791
4792 let default_runner = RunnerConfig::default();
4793 assert_eq!(default_runner.cmd(), "cargo");
4794 assert_eq!(default_runner.cwd(), None);
4795 assert_eq!(default_runner.args(), None);
4796 }
4797
4798 #[test]
4799 fn test_runner_config_from_str() {
4800 use super::RunnerConfig;
4801
4802 let runner: RunnerConfig = "my_runner".into();
4804 assert_eq!(runner.cmd(), "my_runner");
4805 assert_eq!(runner.cwd(), None);
4806 assert_eq!(runner.args(), None);
4807 }
4808
4809 #[test]
4810 fn test_runner_config_from_string() {
4811 use super::RunnerConfig;
4812
4813 let runner: RunnerConfig = "another_runner".to_string().into();
4815 assert_eq!(runner.cmd(), "another_runner");
4816 assert_eq!(runner.cwd(), None);
4817 assert_eq!(runner.args(), None);
4818 }
4819
4820 #[test]
4821 fn test_runner_config_from_str_parse() {
4822 use super::RunnerConfig;
4823 use std::str::FromStr;
4824
4825 let runner = RunnerConfig::from_str("parsed_runner").unwrap();
4827 assert_eq!(runner.cmd(), "parsed_runner");
4828 assert_eq!(runner.cwd(), None);
4829 assert_eq!(runner.args(), None);
4830 }
4831
4832 #[test]
4833 fn test_runner_config_in_build_config() {
4834 use super::BuildConfig;
4835
4836 let json = r#"{"runner": "cargo"}"#;
4838 let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4839
4840 let runner = build_config.runner.unwrap();
4841 assert_eq!(runner.cmd(), "cargo");
4842 assert_eq!(runner.cwd(), None);
4843 assert_eq!(runner.args(), None);
4844 }
4845
4846 #[test]
4847 fn test_runner_config_in_build_config_object() {
4848 use super::BuildConfig;
4849
4850 let json = r#"{"runner": {"cmd": "cross", "cwd": "/workspace", "args": ["--target", "x86_64-unknown-linux-gnu"]}}"#;
4852 let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4853
4854 let runner = build_config.runner.unwrap();
4855 assert_eq!(runner.cmd(), "cross");
4856 assert_eq!(runner.cwd(), Some("/workspace"));
4857 assert_eq!(
4858 runner.args(),
4859 Some(
4860 &[
4861 "--target".to_string(),
4862 "x86_64-unknown-linux-gnu".to_string()
4863 ][..]
4864 )
4865 );
4866 }
4867
4868 #[test]
4869 fn test_runner_config_in_full_config() {
4870 use super::Config;
4871
4872 let json = r#"{
4874 "productName": "Test App",
4875 "version": "1.0.0",
4876 "identifier": "com.test.app",
4877 "build": {
4878 "runner": {
4879 "cmd": "my_custom_cargo",
4880 "cwd": "/tmp/build",
4881 "args": ["--quiet", "--verbose"]
4882 }
4883 }
4884 }"#;
4885
4886 let config: Config = serde_json::from_str(json).unwrap();
4887 let runner = config.build.runner.unwrap();
4888
4889 assert_eq!(runner.cmd(), "my_custom_cargo");
4890 assert_eq!(runner.cwd(), Some("/tmp/build"));
4891 assert_eq!(
4892 runner.args(),
4893 Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4894 );
4895 }
4896
4897 #[test]
4898 fn test_runner_config_equality() {
4899 use super::RunnerConfig;
4900
4901 let runner1 = RunnerConfig::String("cargo".to_string());
4902 let runner2 = RunnerConfig::String("cargo".to_string());
4903 let runner3 = RunnerConfig::String("cross".to_string());
4904
4905 assert_eq!(runner1, runner2);
4906 assert_ne!(runner1, runner3);
4907
4908 let runner4 = RunnerConfig::Object {
4909 cmd: "cargo".to_string(),
4910 cwd: Some("/tmp".to_string()),
4911 args: Some(vec!["--quiet".to_string()]),
4912 };
4913 let runner5 = RunnerConfig::Object {
4914 cmd: "cargo".to_string(),
4915 cwd: Some("/tmp".to_string()),
4916 args: Some(vec!["--quiet".to_string()]),
4917 };
4918
4919 assert_eq!(runner4, runner5);
4920 assert_ne!(runner1, runner4);
4921 }
4922
4923 #[test]
4924 fn test_runner_config_untagged_serialization() {
4925 use super::RunnerConfig;
4926
4927 let string_runner = RunnerConfig::String("cargo".to_string());
4929 let string_json = serde_json::to_string(&string_runner).unwrap();
4930 assert_eq!(string_json, r#""cargo""#);
4931
4932 let object_runner = RunnerConfig::Object {
4934 cmd: "cross".to_string(),
4935 cwd: None,
4936 args: None,
4937 };
4938 let object_json = serde_json::to_string(&object_runner).unwrap();
4939 assert!(object_json.contains("\"cmd\":\"cross\""));
4940 assert!(object_json.contains("\"cwd\":null") || !object_json.contains("cwd"));
4942 assert!(object_json.contains("\"args\":null") || !object_json.contains("args"));
4943 }
4944
4945 #[test]
4946 fn window_config_default_same_as_deserialize() {
4947 let config_from_deserialization: WindowConfig = serde_json::from_str("{}").unwrap();
4948 let config_from_default: WindowConfig = WindowConfig::default();
4949
4950 assert_eq!(config_from_deserialization, config_from_default);
4951 }
4952}