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 = "installer-icon")]
815 pub installer_icon: Option<PathBuf>,
816 #[serde(alias = "uninstaller-icon")]
818 pub uninstaller_icon: Option<PathBuf>,
819 #[serde(alias = "uninstaller-header-image")]
824 pub uninstaller_header_image: Option<PathBuf>,
825 #[serde(default, alias = "install-mode")]
827 pub install_mode: NSISInstallerMode,
828 pub languages: Option<Vec<String>>,
835 pub custom_language_files: Option<HashMap<String, PathBuf>>,
842 #[serde(default, alias = "display-language-selector")]
845 pub display_language_selector: bool,
846 #[serde(default)]
850 pub compression: NsisCompression,
851 #[serde(alias = "start-menu-folder")]
860 pub start_menu_folder: Option<String>,
861 #[serde(alias = "installer-hooks")]
891 pub installer_hooks: Option<PathBuf>,
892 #[deprecated(
898 since = "2.10.0",
899 note = "Use `WindowsConfig::minimum_webview2_version` instead."
900 )]
901 #[serde(alias = "minimum-webview2-version")]
902 pub minimum_webview2_version: Option<String>,
903}
904
905#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
910#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
911#[cfg_attr(feature = "schema", derive(JsonSchema))]
912pub enum WebviewInstallMode {
913 Skip,
915 DownloadBootstrapper {
919 #[serde(default = "default_true")]
921 silent: bool,
922 },
923 EmbedBootstrapper {
927 #[serde(default = "default_true")]
929 silent: bool,
930 },
931 OfflineInstaller {
935 #[serde(default = "default_true")]
937 silent: bool,
938 },
939 FixedRuntime {
942 path: PathBuf,
947 },
948}
949
950impl Default for WebviewInstallMode {
951 fn default() -> Self {
952 Self::DownloadBootstrapper { silent: true }
953 }
954}
955
956#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
958#[cfg_attr(feature = "schema", derive(JsonSchema))]
959#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
960pub enum CustomSignCommandConfig {
961 Command(String),
970 CommandWithOptions {
975 cmd: String,
977 args: Vec<String>,
981 },
982}
983
984#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
988#[cfg_attr(feature = "schema", derive(JsonSchema))]
989#[serde(rename_all = "camelCase", deny_unknown_fields)]
990pub struct WindowsConfig {
991 #[serde(alias = "digest-algorithm")]
994 pub digest_algorithm: Option<String>,
995 #[serde(alias = "certificate-thumbprint")]
997 pub certificate_thumbprint: Option<String>,
998 #[serde(alias = "timestamp-url")]
1000 pub timestamp_url: Option<String>,
1001 #[serde(default)]
1004 pub tsp: bool,
1005 #[serde(default, alias = "webview-install-mode")]
1007 pub webview_install_mode: WebviewInstallMode,
1008 #[serde(default = "default_true", alias = "allow-downgrades")]
1014 pub allow_downgrades: bool,
1015 #[serde(alias = "minimum-webview2-version")]
1019 pub minimum_webview2_version: Option<String>,
1020 pub wix: Option<WixConfig>,
1022 pub nsis: Option<NsisConfig>,
1024 #[serde(alias = "sign-command")]
1032 pub sign_command: Option<CustomSignCommandConfig>,
1033 #[serde(
1040 default,
1041 rename = "bundleVCRuntime",
1042 alias = "bundle-vc-runtime",
1043 alias = "bundleVcRuntime"
1044 )]
1045 pub bundle_vc_runtime: bool,
1046}
1047
1048impl Default for WindowsConfig {
1049 fn default() -> Self {
1050 Self {
1051 digest_algorithm: None,
1052 certificate_thumbprint: None,
1053 timestamp_url: None,
1054 tsp: false,
1055 webview_install_mode: Default::default(),
1056 allow_downgrades: true,
1057 minimum_webview2_version: None,
1058 wix: None,
1059 nsis: None,
1060 sign_command: None,
1061 bundle_vc_runtime: false,
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1068#[cfg_attr(feature = "schema", derive(JsonSchema))]
1069pub enum BundleTypeRole {
1070 #[default]
1072 Editor,
1073 Viewer,
1075 Shell,
1077 QLGenerator,
1079 None,
1081}
1082
1083impl Display for BundleTypeRole {
1084 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1085 match self {
1086 Self::Editor => write!(f, "Editor"),
1087 Self::Viewer => write!(f, "Viewer"),
1088 Self::Shell => write!(f, "Shell"),
1089 Self::QLGenerator => write!(f, "QLGenerator"),
1090 Self::None => write!(f, "None"),
1091 }
1092 }
1093}
1094
1095#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1099#[cfg_attr(feature = "schema", derive(JsonSchema))]
1100pub enum HandlerRank {
1101 #[default]
1103 Default,
1104 Owner,
1106 Alternate,
1108 None,
1110}
1111
1112impl Display for HandlerRank {
1113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1114 match self {
1115 Self::Default => write!(f, "Default"),
1116 Self::Owner => write!(f, "Owner"),
1117 Self::Alternate => write!(f, "Alternate"),
1118 Self::None => write!(f, "None"),
1119 }
1120 }
1121}
1122
1123#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
1127#[cfg_attr(feature = "schema", derive(JsonSchema))]
1128pub struct AssociationExt(pub String);
1129
1130impl fmt::Display for AssociationExt {
1131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1132 write!(f, "{}", self.0)
1133 }
1134}
1135
1136impl<'d> serde::Deserialize<'d> for AssociationExt {
1137 fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
1138 let ext = String::deserialize(deserializer)?;
1139 if let Some(ext) = ext.strip_prefix('.') {
1140 Ok(AssociationExt(ext.into()))
1141 } else {
1142 Ok(AssociationExt(ext))
1143 }
1144 }
1145}
1146
1147#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1149#[cfg_attr(feature = "schema", derive(JsonSchema))]
1150#[serde(rename_all = "camelCase", deny_unknown_fields)]
1151pub struct FileAssociation {
1152 pub ext: Vec<AssociationExt>,
1154 #[serde(alias = "content-types")]
1159 pub content_types: Option<Vec<String>>,
1160 pub name: Option<String>,
1162 pub description: Option<String>,
1164 #[serde(default)]
1166 pub role: BundleTypeRole,
1167 #[serde(alias = "mime-type")]
1175 pub mime_type: Option<String>,
1176 #[serde(default)]
1178 pub rank: HandlerRank,
1179 pub exported_type: Option<ExportedFileAssociation>,
1183 #[serde(alias = "android-intent-action-filters")]
1187 pub android_intent_action_filters: Option<Vec<AndroidIntentAction>>,
1188}
1189
1190#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Hash)]
1192#[cfg_attr(feature = "schema", derive(JsonSchema))]
1193#[serde(rename_all = "camelCase")]
1194#[non_exhaustive]
1195pub enum AndroidIntentAction {
1196 Send,
1200 SendMultiple,
1204 View,
1208}
1209
1210#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1212#[cfg_attr(feature = "schema", derive(JsonSchema))]
1213#[serde(rename_all = "camelCase", deny_unknown_fields)]
1214pub struct ExportedFileAssociation {
1215 pub identifier: String,
1217 #[serde(alias = "conforms-to")]
1221 pub conforms_to: Option<Vec<String>>,
1222}
1223
1224impl FileAssociation {
1225 pub fn infer_content_types(&self) -> HashSet<String> {
1232 let mut content_types = HashSet::new();
1233
1234 if let Some(exported_type) = &self.exported_type {
1236 content_types.insert(exported_type.identifier.clone());
1237 return content_types;
1238 }
1239
1240 if let Some(explicit_types) = &self.content_types {
1242 content_types.extend(explicit_types.iter().cloned());
1243 }
1244
1245 for ext in &self.ext {
1247 if let Some(uti) = extension_to_uti(&ext.0) {
1248 content_types.insert(uti.to_string());
1249 }
1250 }
1251
1252 if let Some(mime_type) = &self.mime_type
1254 && let Some(uti) = mime_type_to_uti(mime_type)
1255 {
1256 content_types.insert(uti.to_string());
1257 }
1258
1259 content_types
1260 }
1261}
1262
1263pub fn file_associations_plist(associations: &[FileAssociation]) -> Option<plist::Value> {
1269 use plist::{Dictionary, Value};
1270
1271 if associations.is_empty() {
1272 return None;
1273 }
1274
1275 let exported_associations = associations
1276 .iter()
1277 .filter_map(|association| {
1278 association.exported_type.as_ref().map(|exported_type| {
1279 let mut dict = Dictionary::new();
1280
1281 dict.insert(
1282 "UTTypeIdentifier".into(),
1283 exported_type.identifier.clone().into(),
1284 );
1285 if let Some(description) = &association.description {
1286 dict.insert("UTTypeDescription".into(), description.clone().into());
1287 }
1288 if let Some(conforms_to) = &exported_type.conforms_to {
1289 dict.insert(
1290 "UTTypeConformsTo".into(),
1291 Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
1292 );
1293 }
1294
1295 let mut specification = Dictionary::new();
1296 specification.insert(
1297 "public.filename-extension".into(),
1298 Value::Array(
1299 association
1300 .ext
1301 .iter()
1302 .map(|s| s.to_string().into())
1303 .collect(),
1304 ),
1305 );
1306 if let Some(mime_type) = &association.mime_type {
1307 specification.insert("public.mime-type".into(), mime_type.clone().into());
1308 }
1309
1310 dict.insert("UTTypeTagSpecification".into(), specification.into());
1311
1312 Value::Dictionary(dict)
1313 })
1314 })
1315 .collect::<Vec<_>>();
1316
1317 let document_types = associations
1318 .iter()
1319 .map(|association| {
1320 let mut dict = Dictionary::new();
1321
1322 if !association.ext.is_empty() {
1323 dict.insert(
1324 "CFBundleTypeExtensions".into(),
1325 Value::Array(
1326 association
1327 .ext
1328 .iter()
1329 .map(|ext| ext.to_string().into())
1330 .collect(),
1331 ),
1332 );
1333 }
1334
1335 let content_types = association.infer_content_types();
1337
1338 if !content_types.is_empty() {
1340 dict.insert(
1341 "LSItemContentTypes".into(),
1342 Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
1343 );
1344 }
1345
1346 let type_name = association
1347 .name
1348 .clone()
1349 .or_else(|| association.ext.first().map(|ext| ext.0.clone()))
1350 .unwrap_or_default();
1351 dict.insert("CFBundleTypeName".into(), type_name.into());
1352 dict.insert(
1353 "CFBundleTypeRole".into(),
1354 association.role.to_string().into(),
1355 );
1356 dict.insert("LSHandlerRank".into(), association.rank.to_string().into());
1357
1358 Value::Dictionary(dict)
1359 })
1360 .collect::<Vec<_>>();
1361
1362 if exported_associations.is_empty() && document_types.is_empty() {
1363 return None;
1364 }
1365
1366 let mut plist = Dictionary::new();
1367 if !exported_associations.is_empty() {
1368 plist.insert(
1369 "UTExportedTypeDeclarations".into(),
1370 Value::Array(exported_associations),
1371 );
1372 }
1373 if !document_types.is_empty() {
1374 plist.insert("CFBundleDocumentTypes".into(), Value::Array(document_types));
1375 }
1376
1377 Some(Value::Dictionary(plist))
1378}
1379
1380fn extension_to_uti(ext: &str) -> Option<&'static str> {
1382 match ext.to_lowercase().as_str() {
1383 "png" => Some("public.png"),
1385 "jpg" | "jpeg" => Some("public.jpeg"),
1386 "gif" => Some("com.compuserve.gif"),
1387 "bmp" => Some("com.microsoft.bmp"),
1388 "tiff" | "tif" => Some("public.tiff"),
1389 "ico" => Some("com.microsoft.ico"),
1390 "heic" | "heif" => Some("public.heif-standard-image"),
1391 "webp" => Some("org.webmproject.webp"),
1392 "svg" => Some("public.svg-image"),
1393 "mp4" => Some("public.mpeg-4"),
1395 "mov" => Some("com.apple.quicktime-movie"),
1396 "avi" => Some("public.avi"),
1397 "mkv" => Some("public.mpeg-4"),
1398 "mp3" => Some("public.mp3"),
1400 "wav" => Some("com.microsoft.waveform-audio"),
1401 "aac" => Some("public.aac-audio"),
1402 "m4a" => Some("public.mpeg-4-audio"),
1403 "pdf" => Some("com.adobe.pdf"),
1405 "txt" => Some("public.plain-text"),
1406 "rtf" => Some("public.rtf"),
1407 "html" | "htm" => Some("public.html"),
1408 "json" => Some("public.json"),
1409 "xml" => Some("public.xml"),
1410 _ => None,
1411 }
1412}
1413
1414fn mime_type_to_uti(mime_type: &str) -> Option<&'static str> {
1416 match mime_type {
1417 "image/png" => Some("public.png"),
1418 "image/jpeg" | "image/jpg" => Some("public.jpeg"),
1419 "image/gif" => Some("com.compuserve.gif"),
1420 "image/bmp" => Some("com.microsoft.bmp"),
1421 "image/tiff" => Some("public.tiff"),
1422 "image/heic" | "image/heif" => Some("public.heif-standard-image"),
1423 "image/webp" => Some("org.webmproject.webp"),
1424 "image/svg+xml" => Some("public.svg-image"),
1425 mime if mime.starts_with("image/") => Some("public.image"),
1426 "video/mp4" => Some("public.mpeg-4"),
1427 "video/quicktime" => Some("com.apple.quicktime-movie"),
1428 "video/x-msvideo" => Some("public.avi"),
1429 mime if mime.starts_with("video/") => Some("public.movie"),
1430 "audio/mpeg" | "audio/mp3" => Some("public.mp3"),
1431 "audio/wav" | "audio/wave" => Some("com.microsoft.waveform-audio"),
1432 "audio/aac" => Some("public.aac-audio"),
1433 "audio/mp4" => Some("public.mpeg-4-audio"),
1434 mime if mime.starts_with("audio/") => Some("public.audio"),
1435 "application/pdf" => Some("com.adobe.pdf"),
1436 "text/plain" => Some("public.plain-text"),
1437 "text/rtf" => Some("public.rtf"),
1438 "text/html" => Some("public.html"),
1439 "application/json" => Some("public.json"),
1440 "application/xml" | "text/xml" => Some("public.xml"),
1441 _ => None,
1442 }
1443}
1444
1445#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1447#[cfg_attr(feature = "schema", derive(JsonSchema))]
1448#[serde(rename_all = "camelCase", deny_unknown_fields)]
1449pub struct DeepLinkProtocol {
1450 #[serde(default)]
1452 pub schemes: Vec<String>,
1453 #[serde(default)]
1461 pub domains: Vec<String>,
1462 pub name: Option<String>,
1464 #[serde(default)]
1466 pub role: BundleTypeRole,
1467}
1468
1469#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1472#[cfg_attr(feature = "schema", derive(JsonSchema))]
1473#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1474pub enum BundleResources {
1475 List(Vec<String>),
1477 Map(HashMap<String, String>),
1479}
1480
1481impl BundleResources {
1482 pub fn push(&mut self, path: impl Into<String>) {
1484 match self {
1485 Self::List(l) => l.push(path.into()),
1486 Self::Map(l) => {
1487 let path = path.into();
1488 l.insert(path.clone(), path);
1489 }
1490 }
1491 }
1492}
1493
1494#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1496#[cfg_attr(feature = "schema", derive(JsonSchema))]
1497#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)]
1498pub enum Updater {
1499 String(V1Compatible),
1501 Bool(bool),
1504}
1505
1506impl Default for Updater {
1507 fn default() -> Self {
1508 Self::Bool(false)
1509 }
1510}
1511
1512#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1514#[cfg_attr(feature = "schema", derive(JsonSchema))]
1515#[serde(rename_all = "camelCase", deny_unknown_fields)]
1516pub enum V1Compatible {
1517 V1Compatible,
1519}
1520
1521#[skip_serializing_none]
1525#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
1526#[cfg_attr(feature = "schema", derive(JsonSchema))]
1527#[serde(rename_all = "camelCase", deny_unknown_fields)]
1528pub struct BundleConfig {
1529 #[serde(default)]
1531 pub active: bool,
1532 #[serde(default)]
1534 pub targets: BundleTarget,
1535 #[serde(default)]
1536 pub create_updater_artifacts: Updater,
1538 pub publisher: Option<String>,
1543 pub homepage: Option<String>,
1548 #[serde(default)]
1550 pub icon: Vec<String>,
1551 pub resources: Option<BundleResources>,
1596 pub copyright: Option<String>,
1598 pub license: Option<String>,
1601 #[serde(alias = "license-file")]
1603 pub license_file: Option<PathBuf>,
1604 pub category: Option<String>,
1609 pub file_associations: Option<Vec<FileAssociation>>,
1611 #[serde(alias = "short-description")]
1613 pub short_description: Option<String>,
1614 #[serde(alias = "long-description")]
1616 pub long_description: Option<String>,
1617 #[serde(default, alias = "use-local-tools-dir")]
1625 pub use_local_tools_dir: bool,
1626 #[serde(alias = "external-bin")]
1638 pub external_bin: Option<Vec<String>>,
1639 #[serde(default)]
1641 pub windows: WindowsConfig,
1642 #[serde(default)]
1644 pub linux: LinuxConfig,
1645 #[serde(rename = "macOS", alias = "macos", default)]
1647 pub macos: MacConfig,
1648 #[serde(rename = "iOS", alias = "ios", default)]
1650 pub ios: IosConfig,
1651 #[serde(default)]
1653 pub android: AndroidConfig,
1654 #[serde(default)]
1656 pub cef: CefConfig,
1657}
1658
1659#[skip_serializing_none]
1664#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1665#[cfg_attr(feature = "schema", derive(JsonSchema))]
1666#[serde(rename_all = "camelCase", deny_unknown_fields)]
1667pub struct CefConfig {
1668 #[serde(default = "default_true")]
1679 pub embed: bool,
1680}
1681
1682impl Default for CefConfig {
1683 fn default() -> Self {
1684 Self { embed: true }
1685 }
1686}
1687
1688#[derive(Debug, PartialEq, Eq, Serialize, Default, Clone, Copy)]
1690#[cfg_attr(feature = "schema", derive(JsonSchema), schemars(with = "InnerColor"))]
1691#[serde(rename_all = "camelCase", deny_unknown_fields)]
1692pub struct Color(pub u8, pub u8, pub u8, pub u8);
1693
1694impl From<Color> for (u8, u8, u8, u8) {
1695 fn from(value: Color) -> Self {
1696 (value.0, value.1, value.2, value.3)
1697 }
1698}
1699
1700impl From<Color> for (u8, u8, u8) {
1701 fn from(value: Color) -> Self {
1702 (value.0, value.1, value.2)
1703 }
1704}
1705
1706impl From<(u8, u8, u8, u8)> for Color {
1707 fn from(value: (u8, u8, u8, u8)) -> Self {
1708 Color(value.0, value.1, value.2, value.3)
1709 }
1710}
1711
1712impl From<(u8, u8, u8)> for Color {
1713 fn from(value: (u8, u8, u8)) -> Self {
1714 Color(value.0, value.1, value.2, 255)
1715 }
1716}
1717
1718impl From<Color> for [u8; 4] {
1719 fn from(value: Color) -> Self {
1720 [value.0, value.1, value.2, value.3]
1721 }
1722}
1723
1724impl From<Color> for [u8; 3] {
1725 fn from(value: Color) -> Self {
1726 [value.0, value.1, value.2]
1727 }
1728}
1729
1730impl From<[u8; 4]> for Color {
1731 fn from(value: [u8; 4]) -> Self {
1732 Color(value[0], value[1], value[2], value[3])
1733 }
1734}
1735
1736impl From<[u8; 3]> for Color {
1737 fn from(value: [u8; 3]) -> Self {
1738 Color(value[0], value[1], value[2], 255)
1739 }
1740}
1741
1742impl FromStr for Color {
1743 type Err = String;
1744 fn from_str(mut color: &str) -> Result<Self, Self::Err> {
1745 color = color.trim().strip_prefix('#').unwrap_or(color);
1746 let color = match color.len() {
1747 3 => color.chars()
1748 .flat_map(|c| std::iter::repeat_n(c, 2))
1749 .chain(std::iter::repeat_n('f', 2))
1750 .collect(),
1751 6 => format!("{color}FF"),
1752 8 => color.to_string(),
1753 _ => return Err("Invalid hex color length, must be either 3, 6 or 8, for example: #fff, #ffffff, or #ffffffff".into()),
1754 };
1755
1756 let r = u8::from_str_radix(&color[0..2], 16).map_err(|e| e.to_string())?;
1757 let g = u8::from_str_radix(&color[2..4], 16).map_err(|e| e.to_string())?;
1758 let b = u8::from_str_radix(&color[4..6], 16).map_err(|e| e.to_string())?;
1759 let a = u8::from_str_radix(&color[6..8], 16).map_err(|e| e.to_string())?;
1760
1761 Ok(Color(r, g, b, a))
1762 }
1763}
1764
1765fn default_alpha() -> u8 {
1766 255
1767}
1768
1769#[derive(Deserialize)]
1770#[cfg_attr(feature = "schema", derive(JsonSchema))]
1771#[serde(untagged)]
1772enum InnerColor {
1773 String(
1775 #[cfg_attr(
1776 feature = "schema",
1777 schemars(pattern("^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$"))
1778 )]
1779 String,
1780 ),
1781 Rgb((u8, u8, u8)),
1783 Rgba((u8, u8, u8, u8)),
1785 RgbaObject {
1787 red: u8,
1788 green: u8,
1789 blue: u8,
1790 #[serde(default = "default_alpha")]
1791 alpha: u8,
1792 },
1793}
1794
1795impl<'de> Deserialize<'de> for Color {
1796 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1797 where
1798 D: Deserializer<'de>,
1799 {
1800 let color = InnerColor::deserialize(deserializer)?;
1801 let color = match color {
1802 InnerColor::String(string) => string.parse().map_err(serde::de::Error::custom)?,
1803 InnerColor::Rgb(rgb) => Color(rgb.0, rgb.1, rgb.2, 255),
1804 InnerColor::Rgba(rgb) => rgb.into(),
1805 InnerColor::RgbaObject {
1806 red,
1807 green,
1808 blue,
1809 alpha,
1810 } => Color(red, green, blue, alpha),
1811 };
1812
1813 Ok(color)
1814 }
1815}
1816
1817#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1819#[cfg_attr(feature = "schema", derive(JsonSchema))]
1820#[serde(rename_all = "camelCase", deny_unknown_fields)]
1821pub enum BackgroundThrottlingPolicy {
1822 Disabled,
1824 Suspend,
1826 Throttle,
1828}
1829
1830#[skip_serializing_none]
1832#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1833#[cfg_attr(feature = "schema", derive(JsonSchema))]
1834#[serde(rename_all = "camelCase", deny_unknown_fields)]
1835pub struct WindowEffectsConfig {
1836 pub effects: Vec<WindowEffect>,
1839 pub state: Option<WindowEffectState>,
1841 pub radius: Option<f64>,
1843 pub color: Option<Color>,
1846}
1847
1848#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
1851#[cfg_attr(feature = "schema", derive(JsonSchema))]
1852#[serde(rename_all = "camelCase", deny_unknown_fields)]
1853pub struct PreventOverflowMargin {
1854 pub width: u32,
1856 pub height: u32,
1858}
1859
1860#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1862#[cfg_attr(feature = "schema", derive(JsonSchema))]
1863#[serde(untagged)]
1864pub enum PreventOverflowConfig {
1865 Enable(bool),
1867 Margin(PreventOverflowMargin),
1870}
1871
1872#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
1878#[cfg_attr(feature = "schema", derive(JsonSchema))]
1879#[serde(rename_all = "camelCase", deny_unknown_fields)]
1880#[non_exhaustive]
1881pub enum ScrollBarStyle {
1882 #[default]
1883 Default,
1885
1886 FluentOverlay,
1891}
1892
1893#[skip_serializing_none]
1897#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
1898#[cfg_attr(feature = "schema", derive(JsonSchema))]
1899#[serde(rename_all = "camelCase", deny_unknown_fields)]
1900pub struct WindowConfig {
1901 #[serde(default = "default_window_label")]
1903 pub label: String,
1904 #[serde(default = "default_true")]
1919 pub create: bool,
1920 #[serde(default)]
1922 pub url: WebviewUrl,
1923 #[serde(alias = "user-agent")]
1925 pub user_agent: Option<String>,
1926 #[serde(default = "default_true", alias = "drag-drop-enabled")]
1936 pub drag_drop_enabled: bool,
1937 #[serde(default)]
1939 pub center: bool,
1940 pub x: Option<f64>,
1942 pub y: Option<f64>,
1944 #[serde(default = "default_width")]
1946 pub width: f64,
1947 #[serde(default = "default_height")]
1949 pub height: f64,
1950 #[serde(alias = "min-width")]
1952 pub min_width: Option<f64>,
1953 #[serde(alias = "min-height")]
1955 pub min_height: Option<f64>,
1956 #[serde(alias = "max-width")]
1958 pub max_width: Option<f64>,
1959 #[serde(alias = "max-height")]
1961 pub max_height: Option<f64>,
1962 #[serde(alias = "prevent-overflow")]
1968 pub prevent_overflow: Option<PreventOverflowConfig>,
1969 #[serde(default = "default_true")]
1971 pub resizable: bool,
1972 #[serde(default = "default_true")]
1980 pub maximizable: bool,
1981 #[serde(default = "default_true")]
1987 pub minimizable: bool,
1988 #[serde(default = "default_true")]
1996 pub closable: bool,
1997 #[serde(default = "default_title")]
1999 pub title: String,
2000 #[serde(default)]
2002 pub fullscreen: bool,
2003 #[serde(default = "default_true")]
2005 pub focus: bool,
2006 #[serde(default = "default_true")]
2008 pub focusable: bool,
2009 #[serde(default)]
2020 pub transparent: bool,
2021 #[serde(default)]
2023 pub maximized: bool,
2024 #[serde(default = "default_true")]
2026 pub visible: bool,
2027 #[serde(default = "default_true")]
2029 pub decorations: bool,
2030 #[serde(default, alias = "always-on-bottom")]
2032 pub always_on_bottom: bool,
2033 #[serde(default, alias = "always-on-top")]
2035 pub always_on_top: bool,
2036 #[serde(default, alias = "visible-on-all-workspaces")]
2042 pub visible_on_all_workspaces: bool,
2043 #[serde(default, alias = "content-protected")]
2045 pub content_protected: bool,
2046 #[serde(default, alias = "skip-taskbar")]
2048 pub skip_taskbar: bool,
2049 pub window_classname: Option<String>,
2051 #[serde(default, alias = "no-redirection-bitmap")]
2056 pub no_redirection_bitmap: bool,
2057 pub theme: Option<crate::Theme>,
2059 #[serde(default, alias = "title-bar-style")]
2061 pub title_bar_style: TitleBarStyle,
2062 #[serde(default, alias = "traffic-light-position")]
2066 pub traffic_light_position: Option<LogicalPosition>,
2067 #[serde(default, alias = "hidden-title")]
2069 pub hidden_title: bool,
2070 #[serde(default, alias = "accept-first-mouse")]
2078 pub accept_first_mouse: bool,
2079 #[serde(default, alias = "tabbing-identifier")]
2086 pub tabbing_identifier: Option<String>,
2087 #[serde(default, alias = "additional-browser-args")]
2101 pub additional_browser_args: Option<String>,
2102 #[serde(default = "default_true")]
2112 pub shadow: bool,
2113 #[serde(default, alias = "window-effects")]
2122 pub window_effects: Option<WindowEffectsConfig>,
2123 #[serde(default)]
2129 pub incognito: bool,
2130 pub parent: Option<String>,
2142 #[serde(alias = "proxy-url")]
2150 pub proxy_url: Option<Url>,
2151 #[serde(default, alias = "zoom-hotkeys-enabled")]
2161 pub zoom_hotkeys_enabled: bool,
2162 #[serde(default, alias = "browser-extensions-enabled")]
2170 pub browser_extensions_enabled: bool,
2171
2172 #[serde(default, alias = "use-https-scheme")]
2182 pub use_https_scheme: bool,
2183 pub devtools: Option<bool>,
2193
2194 #[serde(alias = "background-color")]
2202 pub background_color: Option<Color>,
2203
2204 #[serde(default, alias = "background-throttling")]
2220 pub background_throttling: Option<BackgroundThrottlingPolicy>,
2221 #[serde(default, alias = "javascript-disabled")]
2223 pub javascript_disabled: bool,
2224 #[serde(default = "default_true", alias = "allow-link-preview")]
2229 pub allow_link_preview: bool,
2230 #[serde(
2235 default,
2236 alias = "disable-input-accessory-view",
2237 alias = "disable_input_accessory_view"
2238 )]
2239 pub disable_input_accessory_view: bool,
2240 #[serde(default, alias = "data-directory")]
2251 pub data_directory: Option<PathBuf>,
2252 #[serde(default, alias = "data-store-identifier")]
2265 pub data_store_identifier: Option<[u8; 16]>,
2266
2267 #[serde(default, alias = "scroll-bar-style")]
2281 pub scroll_bar_style: ScrollBarStyle,
2282
2283 #[serde(default, alias = "limit-navigations-to-app-bound-domains")]
2333 pub limit_navigations_to_app_bound_domains: bool,
2334 #[serde(default, alias = "activity-name")]
2336 pub activity_name: Option<String>,
2337 #[serde(default, alias = "created-by-activity-name")]
2341 pub created_by_activity_name: Option<String>,
2342
2343 #[serde(default, alias = "requested-by-scene-identifier")]
2348 pub requested_by_scene_identifier: Option<String>,
2349 #[serde(default = "default_true", alias = "general-autofill-enabled")]
2368 pub general_autofill_enabled: bool,
2369}
2370
2371impl Default for WindowConfig {
2372 fn default() -> Self {
2373 Self {
2374 label: default_window_label(),
2375 url: WebviewUrl::default(),
2376 create: true,
2377 user_agent: None,
2378 drag_drop_enabled: true,
2379 center: false,
2380 x: None,
2381 y: None,
2382 width: default_width(),
2383 height: default_height(),
2384 min_width: None,
2385 min_height: None,
2386 max_width: None,
2387 max_height: None,
2388 prevent_overflow: None,
2389 resizable: true,
2390 maximizable: true,
2391 minimizable: true,
2392 closable: true,
2393 title: default_title(),
2394 fullscreen: false,
2395 focus: true,
2396 focusable: true,
2397 transparent: false,
2398 maximized: false,
2399 visible: true,
2400 decorations: true,
2401 always_on_bottom: false,
2402 always_on_top: false,
2403 visible_on_all_workspaces: false,
2404 content_protected: false,
2405 skip_taskbar: false,
2406 window_classname: None,
2407 no_redirection_bitmap: false,
2408 theme: None,
2409 title_bar_style: Default::default(),
2410 traffic_light_position: None,
2411 hidden_title: false,
2412 accept_first_mouse: false,
2413 tabbing_identifier: None,
2414 additional_browser_args: None,
2415 shadow: true,
2416 window_effects: None,
2417 incognito: false,
2418 parent: None,
2419 proxy_url: None,
2420 zoom_hotkeys_enabled: false,
2421 browser_extensions_enabled: false,
2422 use_https_scheme: false,
2423 devtools: None,
2424 background_color: None,
2425 background_throttling: None,
2426 javascript_disabled: false,
2427 allow_link_preview: true,
2428 disable_input_accessory_view: false,
2429 data_directory: None,
2430 data_store_identifier: None,
2431 scroll_bar_style: ScrollBarStyle::Default,
2432 limit_navigations_to_app_bound_domains: false,
2433 activity_name: None,
2434 created_by_activity_name: None,
2435 requested_by_scene_identifier: None,
2436 general_autofill_enabled: true,
2437 }
2438 }
2439}
2440
2441fn default_window_label() -> String {
2442 "main".to_string()
2443}
2444
2445fn default_width() -> f64 {
2446 800.
2447}
2448
2449fn default_height() -> f64 {
2450 600.
2451}
2452
2453fn default_title() -> String {
2454 "Tauri App".to_string()
2455}
2456
2457#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2460#[cfg_attr(feature = "schema", derive(JsonSchema))]
2461#[serde(rename_all = "camelCase", untagged)]
2462pub enum CspDirectiveSources {
2463 Inline(String),
2465 List(Vec<String>),
2467}
2468
2469impl Default for CspDirectiveSources {
2470 fn default() -> Self {
2471 Self::List(Vec::new())
2472 }
2473}
2474
2475impl From<CspDirectiveSources> for Vec<String> {
2476 fn from(sources: CspDirectiveSources) -> Self {
2477 match sources {
2478 CspDirectiveSources::Inline(source) => source.split(' ').map(|s| s.to_string()).collect(),
2479 CspDirectiveSources::List(l) => l,
2480 }
2481 }
2482}
2483
2484impl CspDirectiveSources {
2485 pub fn contains(&self, source: &str) -> bool {
2487 match self {
2488 Self::Inline(s) => s.contains(&format!("{source} ")) || s.contains(&format!(" {source}")),
2489 Self::List(l) => l.contains(&source.into()),
2490 }
2491 }
2492
2493 pub fn push<S: AsRef<str>>(&mut self, source: S) {
2495 match self {
2496 Self::Inline(s) => {
2497 s.push(' ');
2498 s.push_str(source.as_ref());
2499 }
2500 Self::List(l) => {
2501 l.push(source.as_ref().to_string());
2502 }
2503 }
2504 }
2505
2506 pub fn extend(&mut self, sources: Vec<String>) {
2508 for s in sources {
2509 self.push(s);
2510 }
2511 }
2512}
2513
2514#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
2517#[cfg_attr(feature = "schema", derive(JsonSchema))]
2518#[serde(rename_all = "camelCase", untagged)]
2519pub enum Csp {
2520 Policy(String),
2522 DirectiveMap(HashMap<String, CspDirectiveSources>),
2524}
2525
2526impl Serialize for Csp {
2527 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2528 where
2529 S: Serializer,
2530 {
2531 match self {
2532 Self::Policy(policy) => serializer.serialize_str(policy),
2533 Self::DirectiveMap(map) => {
2534 let btree_map: BTreeMap<_, _> = map.iter().collect();
2538 btree_map.serialize(serializer)
2539 }
2540 }
2541 }
2542}
2543
2544impl From<HashMap<String, CspDirectiveSources>> for Csp {
2545 fn from(map: HashMap<String, CspDirectiveSources>) -> Self {
2546 Self::DirectiveMap(map)
2547 }
2548}
2549
2550impl From<Csp> for HashMap<String, CspDirectiveSources> {
2551 fn from(csp: Csp) -> Self {
2552 match csp {
2553 Csp::Policy(policy) => {
2554 let mut map = HashMap::new();
2555 for directive in policy.split(';') {
2556 let mut tokens = directive.trim().split(' ');
2557 if let Some(directive) = tokens.next() {
2558 let sources = tokens.map(|s| s.to_string()).collect::<Vec<String>>();
2559 map.insert(directive.to_string(), CspDirectiveSources::List(sources));
2560 }
2561 }
2562 map
2563 }
2564 Csp::DirectiveMap(m) => m,
2565 }
2566 }
2567}
2568
2569impl Display for Csp {
2570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2571 match self {
2572 Self::Policy(s) => write!(f, "{s}"),
2573 Self::DirectiveMap(m) => {
2574 let len = m.len();
2575 let mut i = 0;
2576 for (directive, sources) in m {
2577 let sources: Vec<String> = sources.clone().into();
2578 write!(f, "{} {}", directive, sources.join(" "))?;
2579 i += 1;
2580 if i != len {
2581 write!(f, "; ")?;
2582 }
2583 }
2584 Ok(())
2585 }
2586 }
2587 }
2588}
2589
2590#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2592#[serde(untagged)]
2593#[cfg_attr(feature = "schema", derive(JsonSchema))]
2594pub enum DisabledCspModificationKind {
2595 Flag(bool),
2598 List(Vec<String>),
2600}
2601
2602impl DisabledCspModificationKind {
2603 pub fn can_modify(&self, directive: &str) -> bool {
2605 match self {
2606 Self::Flag(f) => !f,
2607 Self::List(l) => !l.contains(&directive.into()),
2608 }
2609 }
2610}
2611
2612impl Default for DisabledCspModificationKind {
2613 fn default() -> Self {
2614 Self::Flag(false)
2615 }
2616}
2617
2618#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2627#[serde(untagged)]
2628#[cfg_attr(feature = "schema", derive(JsonSchema))]
2629pub enum FsScope {
2630 AllowedPaths(Vec<PathBuf>),
2632 #[serde(rename_all = "camelCase")]
2634 Scope {
2635 #[serde(default)]
2637 allow: Vec<PathBuf>,
2638 #[serde(default)]
2641 deny: Vec<PathBuf>,
2642 #[serde(alias = "require-literal-leading-dot")]
2651 require_literal_leading_dot: Option<bool>,
2652 },
2653}
2654
2655impl Default for FsScope {
2656 fn default() -> Self {
2657 Self::AllowedPaths(Vec::new())
2658 }
2659}
2660
2661impl FsScope {
2662 pub fn allowed_paths(&self) -> &Vec<PathBuf> {
2664 match self {
2665 Self::AllowedPaths(p) => p,
2666 Self::Scope { allow, .. } => allow,
2667 }
2668 }
2669
2670 pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
2672 match self {
2673 Self::AllowedPaths(_) => None,
2674 Self::Scope { deny, .. } => Some(deny),
2675 }
2676 }
2677}
2678
2679#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2683#[cfg_attr(feature = "schema", derive(JsonSchema))]
2684#[serde(rename_all = "camelCase", deny_unknown_fields)]
2685pub struct AssetProtocolConfig {
2686 #[serde(default)]
2688 pub scope: FsScope,
2689 #[serde(default)]
2691 pub enable: bool,
2692}
2693
2694#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
2698#[cfg_attr(feature = "schema", derive(JsonSchema))]
2699#[serde(rename_all = "camelCase", untagged)]
2700pub enum HeaderSource {
2701 Inline(String),
2703 List(Vec<String>),
2705 Map(HashMap<String, String>),
2707}
2708
2709impl Serialize for HeaderSource {
2710 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
2711 where
2712 S: Serializer,
2713 {
2714 match self {
2715 Self::Inline(s) => serializer.serialize_str(s),
2716 Self::List(l) => l.serialize(serializer),
2717 Self::Map(m) => {
2718 let btree_map: BTreeMap<_, _> = m.iter().collect();
2722 btree_map.serialize(serializer)
2723 }
2724 }
2725 }
2726}
2727
2728impl Display for HeaderSource {
2729 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2730 match self {
2731 Self::Inline(s) => write!(f, "{s}"),
2732 Self::List(l) => write!(f, "{}", l.join(", ")),
2733 Self::Map(m) => {
2734 let map: BTreeMap<_, _> = m.iter().collect();
2738 let len = map.len();
2739 for (i, (key, value)) in map.into_iter().enumerate() {
2740 write!(f, "{key} {value}")?;
2741 if i + 1 != len {
2742 write!(f, "; ")?;
2743 }
2744 }
2745 Ok(())
2746 }
2747 }
2748 }
2749}
2750
2751pub trait HeaderAddition {
2755 fn add_configured_headers(self, headers: Option<&HeaderConfig>) -> http::response::Builder;
2757}
2758
2759impl HeaderAddition for http::response::Builder {
2760 fn add_configured_headers(mut self, headers: Option<&HeaderConfig>) -> http::response::Builder {
2764 if let Some(headers) = headers {
2765 if let Some(value) = &headers.access_control_allow_credentials {
2767 self = self.header("Access-Control-Allow-Credentials", value.to_string());
2768 };
2769
2770 if let Some(value) = &headers.access_control_allow_headers {
2772 self = self.header("Access-Control-Allow-Headers", value.to_string());
2773 };
2774
2775 if let Some(value) = &headers.access_control_allow_methods {
2777 self = self.header("Access-Control-Allow-Methods", value.to_string());
2778 };
2779
2780 if let Some(value) = &headers.access_control_expose_headers {
2782 self = self.header("Access-Control-Expose-Headers", value.to_string());
2783 };
2784
2785 if let Some(value) = &headers.access_control_max_age {
2787 self = self.header("Access-Control-Max-Age", value.to_string());
2788 };
2789
2790 if let Some(value) = &headers.cross_origin_embedder_policy {
2792 self = self.header("Cross-Origin-Embedder-Policy", value.to_string());
2793 };
2794
2795 if let Some(value) = &headers.cross_origin_opener_policy {
2797 self = self.header("Cross-Origin-Opener-Policy", value.to_string());
2798 };
2799
2800 if let Some(value) = &headers.cross_origin_resource_policy {
2802 self = self.header("Cross-Origin-Resource-Policy", value.to_string());
2803 };
2804
2805 if let Some(value) = &headers.permissions_policy {
2807 self = self.header("Permissions-Policy", value.to_string());
2808 };
2809
2810 if let Some(value) = &headers.service_worker_allowed {
2811 self = self.header("Service-Worker-Allowed", value.to_string());
2812 }
2813
2814 if let Some(value) = &headers.timing_allow_origin {
2816 self = self.header("Timing-Allow-Origin", value.to_string());
2817 };
2818
2819 if let Some(value) = &headers.x_content_type_options {
2821 self = self.header("X-Content-Type-Options", value.to_string());
2822 };
2823
2824 if let Some(value) = &headers.tauri_custom_header {
2826 self = self.header("Tauri-Custom-Header", value.to_string());
2828 };
2829 }
2830 self
2831 }
2832}
2833
2834#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
2886#[cfg_attr(feature = "schema", derive(JsonSchema))]
2887#[serde(deny_unknown_fields)]
2888pub struct HeaderConfig {
2889 #[serde(rename = "Access-Control-Allow-Credentials")]
2894 pub access_control_allow_credentials: Option<HeaderSource>,
2895 #[serde(rename = "Access-Control-Allow-Headers")]
2903 pub access_control_allow_headers: Option<HeaderSource>,
2904 #[serde(rename = "Access-Control-Allow-Methods")]
2909 pub access_control_allow_methods: Option<HeaderSource>,
2910 #[serde(rename = "Access-Control-Expose-Headers")]
2916 pub access_control_expose_headers: Option<HeaderSource>,
2917 #[serde(rename = "Access-Control-Max-Age")]
2924 pub access_control_max_age: Option<HeaderSource>,
2925 #[serde(rename = "Cross-Origin-Embedder-Policy")]
2930 pub cross_origin_embedder_policy: Option<HeaderSource>,
2931 #[serde(rename = "Cross-Origin-Opener-Policy")]
2938 pub cross_origin_opener_policy: Option<HeaderSource>,
2939 #[serde(rename = "Cross-Origin-Resource-Policy")]
2944 pub cross_origin_resource_policy: Option<HeaderSource>,
2945 #[serde(rename = "Permissions-Policy")]
2950 pub permissions_policy: Option<HeaderSource>,
2951 #[serde(rename = "Service-Worker-Allowed")]
2961 pub service_worker_allowed: Option<HeaderSource>,
2962 #[serde(rename = "Timing-Allow-Origin")]
2968 pub timing_allow_origin: Option<HeaderSource>,
2969 #[serde(rename = "X-Content-Type-Options")]
2976 pub x_content_type_options: Option<HeaderSource>,
2977 #[serde(rename = "Tauri-Custom-Header")]
2982 pub tauri_custom_header: Option<HeaderSource>,
2983}
2984
2985impl HeaderConfig {
2986 pub fn new() -> Self {
2988 HeaderConfig {
2989 access_control_allow_credentials: None,
2990 access_control_allow_methods: None,
2991 access_control_allow_headers: None,
2992 access_control_expose_headers: None,
2993 access_control_max_age: None,
2994 cross_origin_embedder_policy: None,
2995 cross_origin_opener_policy: None,
2996 cross_origin_resource_policy: None,
2997 permissions_policy: None,
2998 service_worker_allowed: None,
2999 timing_allow_origin: None,
3000 x_content_type_options: None,
3001 tauri_custom_header: None,
3002 }
3003 }
3004}
3005
3006#[skip_serializing_none]
3010#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3011#[cfg_attr(feature = "schema", derive(JsonSchema))]
3012#[serde(rename_all = "camelCase", deny_unknown_fields)]
3013pub struct SecurityConfig {
3014 pub csp: Option<Csp>,
3020 #[serde(alias = "dev-csp")]
3025 pub dev_csp: Option<Csp>,
3026 #[serde(default, alias = "freeze-prototype")]
3028 pub freeze_prototype: bool,
3029 #[serde(default, alias = "dangerous-disable-asset-csp-modification")]
3042 pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind,
3043 #[serde(default, alias = "asset-protocol")]
3045 pub asset_protocol: AssetProtocolConfig,
3046 #[serde(default)]
3048 pub pattern: PatternKind,
3049 #[serde(default)]
3072 pub capabilities: Vec<CapabilityEntry>,
3073 #[serde(default)]
3076 pub headers: Option<HeaderConfig>,
3077}
3078
3079#[derive(Debug, Clone, PartialEq, Serialize)]
3081#[cfg_attr(feature = "schema", derive(JsonSchema))]
3082#[serde(untagged)]
3083pub enum CapabilityEntry {
3084 Inlined(Capability),
3086 Reference(String),
3088}
3089
3090impl<'de> Deserialize<'de> for CapabilityEntry {
3091 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3092 where
3093 D: Deserializer<'de>,
3094 {
3095 UntaggedEnumVisitor::new()
3096 .string(|string| Ok(Self::Reference(string.to_owned())))
3097 .map(|map| map.deserialize::<Capability>().map(Self::Inlined))
3098 .deserialize(deserializer)
3099 }
3100}
3101
3102#[skip_serializing_none]
3104#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
3105#[serde(rename_all = "lowercase", tag = "use", content = "options")]
3106#[cfg_attr(feature = "schema", derive(JsonSchema))]
3107pub enum PatternKind {
3108 #[default]
3110 Brownfield,
3111 Isolation {
3113 dir: PathBuf,
3115 },
3116}
3117
3118#[skip_serializing_none]
3122#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3123#[cfg_attr(feature = "schema", derive(JsonSchema))]
3124#[serde(rename_all = "camelCase", deny_unknown_fields)]
3125pub struct AppConfig {
3126 #[serde(default)]
3181 pub windows: Vec<WindowConfig>,
3182 #[serde(default)]
3184 pub security: SecurityConfig,
3185 #[serde(alias = "tray-icon")]
3187 pub tray_icon: Option<TrayIconConfig>,
3188 #[serde(rename = "macOSPrivateApi", alias = "macos-private-api", default)]
3190 pub macos_private_api: bool,
3191 #[serde(default, alias = "with-global-tauri")]
3193 pub with_global_tauri: bool,
3194 #[serde(rename = "enableGTKAppId", alias = "enable-gtk-app-id", default)]
3196 pub enable_gtk_app_id: bool,
3197}
3198
3199impl AppConfig {
3200 pub fn all_features() -> Vec<&'static str> {
3202 vec![
3203 "tray-icon",
3204 "macos-private-api",
3205 "protocol-asset",
3206 "isolation",
3207 ]
3208 }
3209
3210 pub fn features(&self) -> Vec<&str> {
3212 let mut features = Vec::new();
3213 if self.tray_icon.is_some() {
3214 features.push("tray-icon");
3215 }
3216 if self.macos_private_api {
3217 features.push("macos-private-api");
3218 }
3219 if self.security.asset_protocol.enable {
3220 features.push("protocol-asset");
3221 }
3222
3223 if let PatternKind::Isolation { .. } = self.security.pattern {
3224 features.push("isolation");
3225 }
3226
3227 features.sort_unstable();
3228 features
3229 }
3230}
3231
3232#[skip_serializing_none]
3236#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
3237#[cfg_attr(feature = "schema", derive(JsonSchema))]
3238#[serde(rename_all = "camelCase", deny_unknown_fields)]
3239pub struct TrayIconConfig {
3240 pub id: Option<String>,
3242 #[serde(alias = "icon-path")]
3248 pub icon_path: PathBuf,
3249 #[serde(default, alias = "icon-as-template")]
3251 pub icon_as_template: bool,
3252 #[serde(default = "default_true", alias = "menu-on-left-click")]
3260 #[deprecated(
3261 since = "2.2.0",
3262 note = "No longer works, use `show_menu_on_left_click` instead."
3263 )]
3264 pub menu_on_left_click: bool,
3265 #[serde(default = "default_true", alias = "show-menu-on-left-click")]
3271 pub show_menu_on_left_click: bool,
3272 pub title: Option<String>,
3274 pub tooltip: Option<String>,
3276}
3277
3278#[skip_serializing_none]
3280#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3281#[cfg_attr(feature = "schema", derive(JsonSchema))]
3282#[serde(rename_all = "camelCase", deny_unknown_fields)]
3283pub struct IosConfig {
3284 pub template: Option<PathBuf>,
3288 pub frameworks: Option<Vec<String>>,
3292 #[serde(alias = "development-team")]
3295 pub development_team: Option<String>,
3296 #[serde(alias = "bundle-version")]
3300 pub bundle_version: Option<String>,
3301 #[serde(
3305 alias = "minimum-system-version",
3306 default = "ios_minimum_system_version"
3307 )]
3308 pub minimum_system_version: String,
3309 #[serde(alias = "info-plist")]
3313 pub info_plist: Option<PathBuf>,
3314}
3315
3316impl Default for IosConfig {
3317 fn default() -> Self {
3318 Self {
3319 template: None,
3320 frameworks: None,
3321 development_team: None,
3322 bundle_version: None,
3323 minimum_system_version: ios_minimum_system_version(),
3324 info_plist: None,
3325 }
3326 }
3327}
3328
3329#[skip_serializing_none]
3331#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3332#[cfg_attr(feature = "schema", derive(JsonSchema))]
3333#[serde(rename_all = "camelCase", deny_unknown_fields)]
3334pub struct AndroidConfig {
3335 #[serde(alias = "min-sdk-version", default = "default_min_sdk_version")]
3338 pub min_sdk_version: u32,
3339
3340 #[serde(alias = "version-code")]
3346 #[cfg_attr(feature = "schema", validate(range(min = 1, max = 2_100_000_000)))]
3347 pub version_code: Option<u32>,
3348
3349 #[serde(alias = "auto-increment-version-code", default)]
3357 pub auto_increment_version_code: bool,
3358
3359 #[serde(alias = "debug-application-id-suffix")]
3363 pub debug_application_id_suffix: Option<String>,
3364}
3365
3366impl Default for AndroidConfig {
3367 fn default() -> Self {
3368 Self {
3369 min_sdk_version: default_min_sdk_version(),
3370 version_code: None,
3371 auto_increment_version_code: false,
3372 debug_application_id_suffix: None,
3373 }
3374 }
3375}
3376
3377fn default_min_sdk_version() -> u32 {
3378 24
3379}
3380
3381#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3383#[cfg_attr(feature = "schema", derive(JsonSchema))]
3384#[serde(untagged, deny_unknown_fields)]
3385#[non_exhaustive]
3386pub enum FrontendDist {
3387 Url(Url),
3389 Directory(PathBuf),
3391 Files(Vec<PathBuf>),
3393}
3394
3395impl std::fmt::Display for FrontendDist {
3396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3397 match self {
3398 Self::Url(url) => write!(f, "{url}"),
3399 Self::Directory(p) => write!(f, "{}", p.display()),
3400 Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
3401 }
3402 }
3403}
3404
3405#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3407#[cfg_attr(feature = "schema", derive(JsonSchema))]
3408#[serde(rename_all = "camelCase", untagged)]
3409pub enum BeforeDevCommand {
3410 Script(String),
3412 ScriptWithOptions {
3414 script: String,
3416 cwd: Option<String>,
3418 #[serde(default)]
3420 wait: bool,
3421 },
3422}
3423
3424#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3426#[cfg_attr(feature = "schema", derive(JsonSchema))]
3427#[serde(rename_all = "camelCase", untagged)]
3428pub enum HookCommand {
3429 Script(String),
3431 ScriptWithOptions {
3433 script: String,
3435 cwd: Option<String>,
3437 },
3438}
3439
3440#[skip_serializing_none]
3442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3443#[cfg_attr(feature = "schema", derive(JsonSchema))]
3444#[serde(untagged)]
3445pub enum RunnerConfig {
3446 String(String),
3448 Object {
3450 cmd: String,
3452 cwd: Option<String>,
3454 args: Option<Vec<String>>,
3456 },
3457}
3458
3459impl Default for RunnerConfig {
3460 fn default() -> Self {
3461 RunnerConfig::String("cargo".to_string())
3462 }
3463}
3464
3465impl RunnerConfig {
3466 pub fn cmd(&self) -> &str {
3468 match self {
3469 RunnerConfig::String(cmd) => cmd,
3470 RunnerConfig::Object { cmd, .. } => cmd,
3471 }
3472 }
3473
3474 pub fn cwd(&self) -> Option<&str> {
3476 match self {
3477 RunnerConfig::String(_) => None,
3478 RunnerConfig::Object { cwd, .. } => cwd.as_deref(),
3479 }
3480 }
3481
3482 pub fn args(&self) -> Option<&[String]> {
3484 match self {
3485 RunnerConfig::String(_) => None,
3486 RunnerConfig::Object { args, .. } => args.as_deref(),
3487 }
3488 }
3489}
3490
3491impl std::str::FromStr for RunnerConfig {
3492 type Err = std::convert::Infallible;
3493
3494 fn from_str(s: &str) -> Result<Self, Self::Err> {
3495 Ok(RunnerConfig::String(s.to_string()))
3496 }
3497}
3498
3499impl From<&str> for RunnerConfig {
3500 fn from(s: &str) -> Self {
3501 RunnerConfig::String(s.to_string())
3502 }
3503}
3504
3505impl From<String> for RunnerConfig {
3506 fn from(s: String) -> Self {
3507 RunnerConfig::String(s)
3508 }
3509}
3510
3511#[skip_serializing_none]
3515#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
3516#[cfg_attr(feature = "schema", derive(JsonSchema))]
3517#[serde(rename_all = "camelCase", deny_unknown_fields)]
3518pub struct BuildConfig {
3519 pub runner: Option<RunnerConfig>,
3521 #[serde(alias = "dev-url")]
3529 pub dev_url: Option<Url>,
3530 #[serde(alias = "frontend-dist")]
3544 pub frontend_dist: Option<FrontendDist>,
3545 #[serde(alias = "before-dev-command")]
3549 pub before_dev_command: Option<BeforeDevCommand>,
3550 #[serde(alias = "before-build-command")]
3554 pub before_build_command: Option<HookCommand>,
3555 #[serde(alias = "before-bundle-command")]
3559 pub before_bundle_command: Option<HookCommand>,
3560 pub features: Option<Vec<String>>,
3562 #[serde(alias = "remove-unused-commands", default)]
3570 pub remove_unused_commands: bool,
3571 #[serde(
3573 alias = "additional-watch-folders",
3574 alias = "additional-watch-directories",
3575 default
3576 )]
3577 pub additional_watch_folders: Vec<PathBuf>,
3578 #[serde(default)]
3580 pub windows: WindowsBuildConfig,
3581}
3582
3583#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
3585#[cfg_attr(feature = "schema", derive(JsonSchema))]
3586#[serde(rename_all = "camelCase", deny_unknown_fields)]
3587pub struct WindowsBuildConfig {
3588 #[serde(
3590 default = "default_true",
3591 rename = "staticVCRuntime",
3592 alias = "static-vc-runtime",
3593 alias = "staticVcRuntime"
3594 )]
3595 pub static_vc_runtime: bool,
3596}
3597
3598impl Default for WindowsBuildConfig {
3599 fn default() -> Self {
3600 Self {
3601 static_vc_runtime: true,
3602 }
3603 }
3604}
3605
3606#[derive(Debug, PartialEq, Eq)]
3607struct PackageVersion(String);
3608
3609impl<'d> serde::Deserialize<'d> for PackageVersion {
3610 fn deserialize<D: Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
3611 struct PackageVersionVisitor;
3612
3613 impl Visitor<'_> for PackageVersionVisitor {
3614 type Value = PackageVersion;
3615
3616 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3617 write!(
3618 formatter,
3619 "a semver string or a path to a package.json file"
3620 )
3621 }
3622
3623 fn visit_str<E: DeError>(self, value: &str) -> Result<PackageVersion, E> {
3624 let path = PathBuf::from(value);
3625 if path.exists() {
3626 let json_str = read_to_string(&path)
3627 .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3628 let package_json: serde_json::Value = serde_json::from_str(&json_str)
3629 .map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
3630 if let Some(obj) = package_json.as_object() {
3631 let version = obj
3632 .get("version")
3633 .ok_or_else(|| DeError::custom("JSON must contain a `version` field"))?
3634 .as_str()
3635 .ok_or_else(|| {
3636 DeError::custom(format!("`{} > version` must be a string", path.display()))
3637 })?;
3638 Ok(PackageVersion(
3639 Version::from_str(version)
3640 .map_err(|_| {
3641 DeError::custom("`tauri.conf.json > version` must be a semver string")
3642 })?
3643 .to_string(),
3644 ))
3645 } else {
3646 Err(DeError::custom(
3647 "`tauri.conf.json > version` value is not a path to a JSON object",
3648 ))
3649 }
3650 } else {
3651 Ok(PackageVersion(
3652 Version::from_str(value)
3653 .map_err(|_| DeError::custom("`tauri.conf.json > version` must be a semver string"))?
3654 .to_string(),
3655 ))
3656 }
3657 }
3658 }
3659
3660 deserializer.deserialize_string(PackageVersionVisitor {})
3661 }
3662}
3663
3664fn version_deserializer<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
3665where
3666 D: Deserializer<'de>,
3667{
3668 Option::<PackageVersion>::deserialize(deserializer).map(|v| v.map(|v| v.0))
3669}
3670
3671#[skip_serializing_none]
3737#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
3738#[cfg_attr(feature = "schema", derive(JsonSchema))]
3739#[serde(rename_all = "camelCase", deny_unknown_fields)]
3740pub struct Config {
3741 #[serde(rename = "$schema")]
3743 pub schema: Option<String>,
3744 #[serde(alias = "product-name")]
3762 #[cfg_attr(feature = "schema", schemars(regex(pattern = "^[^/\\:*?\"<>|]+$")))]
3763 pub product_name: Option<String>,
3764 #[serde(alias = "main-binary-name")]
3778 pub main_binary_name: Option<String>,
3779 #[serde(deserialize_with = "version_deserializer", default)]
3795 pub version: Option<String>,
3796 pub identifier: String,
3804 #[serde(default)]
3806 pub app: AppConfig,
3807 #[serde(default)]
3809 pub build: BuildConfig,
3810 #[serde(default)]
3812 pub bundle: BundleConfig,
3813 #[serde(default)]
3815 pub plugins: PluginConfig,
3816}
3817
3818#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
3822#[cfg_attr(feature = "schema", derive(JsonSchema))]
3823pub struct PluginConfig(pub HashMap<String, JsonValue>);
3824
3825impl Serialize for PluginConfig {
3826 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
3827 where
3828 S: Serializer,
3829 {
3830 let btree_map: BTreeMap<_, _> = self.0.iter().collect();
3834 btree_map.serialize(serializer)
3835 }
3836}
3837
3838#[cfg(any(feature = "build", feature = "build-2"))]
3844mod build {
3845 use super::*;
3846 use crate::{literal_struct, tokens::*};
3847 use proc_macro2::TokenStream;
3848 use quote::{ToTokens, TokenStreamExt, quote};
3849 use std::convert::identity;
3850
3851 impl ToTokens for WebviewUrl {
3852 fn to_tokens(&self, tokens: &mut TokenStream) {
3853 let prefix = quote! { ::tauri::utils::config::WebviewUrl };
3854
3855 tokens.append_all(match self {
3856 Self::App(path) => {
3857 let path = path_buf_lit(path);
3858 quote! { #prefix::App(#path) }
3859 }
3860 Self::External(url) => {
3861 let url = url_lit(url);
3862 quote! { #prefix::External(#url) }
3863 }
3864 Self::CustomProtocol(url) => {
3865 let url = url_lit(url);
3866 quote! { #prefix::CustomProtocol(#url) }
3867 }
3868 })
3869 }
3870 }
3871
3872 impl ToTokens for BackgroundThrottlingPolicy {
3873 fn to_tokens(&self, tokens: &mut TokenStream) {
3874 let prefix = quote! { ::tauri::utils::config::BackgroundThrottlingPolicy };
3875 tokens.append_all(match self {
3876 Self::Disabled => quote! { #prefix::Disabled },
3877 Self::Throttle => quote! { #prefix::Throttle },
3878 Self::Suspend => quote! { #prefix::Suspend },
3879 })
3880 }
3881 }
3882
3883 impl ToTokens for crate::Theme {
3884 fn to_tokens(&self, tokens: &mut TokenStream) {
3885 let prefix = quote! { ::tauri::utils::Theme };
3886
3887 tokens.append_all(match self {
3888 Self::Light => quote! { #prefix::Light },
3889 Self::Dark => quote! { #prefix::Dark },
3890 })
3891 }
3892 }
3893
3894 impl ToTokens for Color {
3895 fn to_tokens(&self, tokens: &mut TokenStream) {
3896 let Color(r, g, b, a) = self;
3897 tokens.append_all(quote! {::tauri::utils::config::Color(#r,#g,#b,#a)});
3898 }
3899 }
3900 impl ToTokens for WindowEffectsConfig {
3901 fn to_tokens(&self, tokens: &mut TokenStream) {
3902 let effects = vec_lit(self.effects.clone(), |d| d);
3903 let state = opt_lit(self.state.as_ref());
3904 let radius = opt_lit(self.radius.as_ref());
3905 let color = opt_lit(self.color.as_ref());
3906
3907 literal_struct!(
3908 tokens,
3909 ::tauri::utils::config::WindowEffectsConfig,
3910 effects,
3911 state,
3912 radius,
3913 color
3914 )
3915 }
3916 }
3917
3918 impl ToTokens for crate::TitleBarStyle {
3919 fn to_tokens(&self, tokens: &mut TokenStream) {
3920 let prefix = quote! { ::tauri::utils::TitleBarStyle };
3921
3922 tokens.append_all(match self {
3923 Self::Visible => quote! { #prefix::Visible },
3924 Self::Transparent => quote! { #prefix::Transparent },
3925 Self::Overlay => quote! { #prefix::Overlay },
3926 })
3927 }
3928 }
3929
3930 impl ToTokens for LogicalPosition {
3931 fn to_tokens(&self, tokens: &mut TokenStream) {
3932 let LogicalPosition { x, y } = self;
3933 literal_struct!(tokens, ::tauri::utils::config::LogicalPosition, x, y)
3934 }
3935 }
3936
3937 impl ToTokens for crate::WindowEffect {
3938 fn to_tokens(&self, tokens: &mut TokenStream) {
3939 let prefix = quote! { ::tauri::utils::WindowEffect };
3940
3941 #[allow(deprecated)]
3942 tokens.append_all(match self {
3943 WindowEffect::AppearanceBased => quote! { #prefix::AppearanceBased},
3944 WindowEffect::Light => quote! { #prefix::Light},
3945 WindowEffect::Dark => quote! { #prefix::Dark},
3946 WindowEffect::MediumLight => quote! { #prefix::MediumLight},
3947 WindowEffect::UltraDark => quote! { #prefix::UltraDark},
3948 WindowEffect::Titlebar => quote! { #prefix::Titlebar},
3949 WindowEffect::Selection => quote! { #prefix::Selection},
3950 WindowEffect::Menu => quote! { #prefix::Menu},
3951 WindowEffect::Popover => quote! { #prefix::Popover},
3952 WindowEffect::Sidebar => quote! { #prefix::Sidebar},
3953 WindowEffect::HeaderView => quote! { #prefix::HeaderView},
3954 WindowEffect::Sheet => quote! { #prefix::Sheet},
3955 WindowEffect::WindowBackground => quote! { #prefix::WindowBackground},
3956 WindowEffect::HudWindow => quote! { #prefix::HudWindow},
3957 WindowEffect::FullScreenUI => quote! { #prefix::FullScreenUI},
3958 WindowEffect::Tooltip => quote! { #prefix::Tooltip},
3959 WindowEffect::ContentBackground => quote! { #prefix::ContentBackground},
3960 WindowEffect::UnderWindowBackground => quote! { #prefix::UnderWindowBackground},
3961 WindowEffect::UnderPageBackground => quote! { #prefix::UnderPageBackground},
3962 WindowEffect::Mica => quote! { #prefix::Mica},
3963 WindowEffect::MicaDark => quote! { #prefix::MicaDark},
3964 WindowEffect::MicaLight => quote! { #prefix::MicaLight},
3965 WindowEffect::Blur => quote! { #prefix::Blur},
3966 WindowEffect::Acrylic => quote! { #prefix::Acrylic},
3967 WindowEffect::Tabbed => quote! { #prefix::Tabbed },
3968 WindowEffect::TabbedDark => quote! { #prefix::TabbedDark },
3969 WindowEffect::TabbedLight => quote! { #prefix::TabbedLight },
3970 })
3971 }
3972 }
3973
3974 impl ToTokens for crate::WindowEffectState {
3975 fn to_tokens(&self, tokens: &mut TokenStream) {
3976 let prefix = quote! { ::tauri::utils::WindowEffectState };
3977
3978 #[allow(deprecated)]
3979 tokens.append_all(match self {
3980 WindowEffectState::Active => quote! { #prefix::Active},
3981 WindowEffectState::FollowsWindowActiveState => quote! { #prefix::FollowsWindowActiveState},
3982 WindowEffectState::Inactive => quote! { #prefix::Inactive},
3983 })
3984 }
3985 }
3986
3987 impl ToTokens for PreventOverflowMargin {
3988 fn to_tokens(&self, tokens: &mut TokenStream) {
3989 let width = self.width;
3990 let height = self.height;
3991
3992 literal_struct!(
3993 tokens,
3994 ::tauri::utils::config::PreventOverflowMargin,
3995 width,
3996 height
3997 )
3998 }
3999 }
4000
4001 impl ToTokens for PreventOverflowConfig {
4002 fn to_tokens(&self, tokens: &mut TokenStream) {
4003 let prefix = quote! { ::tauri::utils::config::PreventOverflowConfig };
4004
4005 #[allow(deprecated)]
4006 tokens.append_all(match self {
4007 Self::Enable(enable) => quote! { #prefix::Enable(#enable) },
4008 Self::Margin(margin) => quote! { #prefix::Margin(#margin) },
4009 })
4010 }
4011 }
4012
4013 impl ToTokens for ScrollBarStyle {
4014 fn to_tokens(&self, tokens: &mut TokenStream) {
4015 let prefix = quote! { ::tauri::utils::config::ScrollBarStyle };
4016
4017 tokens.append_all(match self {
4018 Self::Default => quote! { #prefix::Default },
4019 Self::FluentOverlay => quote! { #prefix::FluentOverlay },
4020 })
4021 }
4022 }
4023
4024 impl ToTokens for WindowConfig {
4025 fn to_tokens(&self, tokens: &mut TokenStream) {
4026 let label = str_lit(&self.label);
4027 let create = &self.create;
4028 let url = &self.url;
4029 let user_agent = opt_str_lit(self.user_agent.as_ref());
4030 let drag_drop_enabled = self.drag_drop_enabled;
4031 let center = self.center;
4032 let x = opt_lit(self.x.as_ref());
4033 let y = opt_lit(self.y.as_ref());
4034 let width = self.width;
4035 let height = self.height;
4036 let min_width = opt_lit(self.min_width.as_ref());
4037 let min_height = opt_lit(self.min_height.as_ref());
4038 let max_width = opt_lit(self.max_width.as_ref());
4039 let max_height = opt_lit(self.max_height.as_ref());
4040 let prevent_overflow = opt_lit(self.prevent_overflow.as_ref());
4041 let resizable = self.resizable;
4042 let maximizable = self.maximizable;
4043 let minimizable = self.minimizable;
4044 let closable = self.closable;
4045 let title = str_lit(&self.title);
4046 let proxy_url = opt_lit(self.proxy_url.as_ref().map(url_lit).as_ref());
4047 let fullscreen = self.fullscreen;
4048 let focus = self.focus;
4049 let focusable = self.focusable;
4050 let transparent = self.transparent;
4051 let maximized = self.maximized;
4052 let visible = self.visible;
4053 let decorations = self.decorations;
4054 let always_on_bottom = self.always_on_bottom;
4055 let always_on_top = self.always_on_top;
4056 let visible_on_all_workspaces = self.visible_on_all_workspaces;
4057 let content_protected = self.content_protected;
4058 let skip_taskbar = self.skip_taskbar;
4059 let window_classname = opt_str_lit(self.window_classname.as_ref());
4060 let no_redirection_bitmap = self.no_redirection_bitmap;
4061 let theme = opt_lit(self.theme.as_ref());
4062 let title_bar_style = &self.title_bar_style;
4063 let traffic_light_position = opt_lit(self.traffic_light_position.as_ref());
4064 let hidden_title = self.hidden_title;
4065 let accept_first_mouse = self.accept_first_mouse;
4066 let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
4067 let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref());
4068 let shadow = self.shadow;
4069 let window_effects = opt_lit(self.window_effects.as_ref());
4070 let incognito = self.incognito;
4071 let parent = opt_str_lit(self.parent.as_ref());
4072 let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled;
4073 let browser_extensions_enabled = self.browser_extensions_enabled;
4074 let use_https_scheme = self.use_https_scheme;
4075 let devtools = opt_lit(self.devtools.as_ref());
4076 let background_color = opt_lit(self.background_color.as_ref());
4077 let background_throttling = opt_lit(self.background_throttling.as_ref());
4078 let javascript_disabled = self.javascript_disabled;
4079 let allow_link_preview = self.allow_link_preview;
4080 let disable_input_accessory_view = self.disable_input_accessory_view;
4081 let data_directory = opt_lit(self.data_directory.as_ref().map(path_buf_lit).as_ref());
4082 let data_store_identifier = opt_vec_lit(self.data_store_identifier, identity);
4083 let scroll_bar_style = &self.scroll_bar_style;
4084 let limit_navigations_to_app_bound_domains = self.limit_navigations_to_app_bound_domains;
4085 let activity_name = opt_lit(self.activity_name.as_ref());
4086 let created_by_activity_name = opt_lit(self.created_by_activity_name.as_ref());
4087 let requested_by_scene_identifier = opt_lit(self.requested_by_scene_identifier.as_ref());
4088 let general_autofill_enabled = self.general_autofill_enabled;
4089
4090 literal_struct!(
4091 tokens,
4092 ::tauri::utils::config::WindowConfig,
4093 label,
4094 url,
4095 create,
4096 user_agent,
4097 drag_drop_enabled,
4098 center,
4099 x,
4100 y,
4101 width,
4102 height,
4103 min_width,
4104 min_height,
4105 max_width,
4106 max_height,
4107 prevent_overflow,
4108 resizable,
4109 maximizable,
4110 minimizable,
4111 closable,
4112 title,
4113 proxy_url,
4114 fullscreen,
4115 focus,
4116 focusable,
4117 transparent,
4118 maximized,
4119 visible,
4120 decorations,
4121 always_on_bottom,
4122 always_on_top,
4123 visible_on_all_workspaces,
4124 content_protected,
4125 skip_taskbar,
4126 window_classname,
4127 no_redirection_bitmap,
4128 theme,
4129 title_bar_style,
4130 traffic_light_position,
4131 hidden_title,
4132 accept_first_mouse,
4133 tabbing_identifier,
4134 additional_browser_args,
4135 shadow,
4136 window_effects,
4137 incognito,
4138 parent,
4139 zoom_hotkeys_enabled,
4140 browser_extensions_enabled,
4141 use_https_scheme,
4142 devtools,
4143 background_color,
4144 background_throttling,
4145 javascript_disabled,
4146 allow_link_preview,
4147 disable_input_accessory_view,
4148 data_directory,
4149 data_store_identifier,
4150 scroll_bar_style,
4151 limit_navigations_to_app_bound_domains,
4152 activity_name,
4153 created_by_activity_name,
4154 requested_by_scene_identifier,
4155 general_autofill_enabled
4156 );
4157 }
4158 }
4159
4160 impl ToTokens for PatternKind {
4161 fn to_tokens(&self, tokens: &mut TokenStream) {
4162 let prefix = quote! { ::tauri::utils::config::PatternKind };
4163
4164 tokens.append_all(match self {
4165 Self::Brownfield => quote! { #prefix::Brownfield },
4166 #[cfg(not(feature = "isolation"))]
4167 Self::Isolation { dir: _ } => quote! { #prefix::Brownfield },
4168 #[cfg(feature = "isolation")]
4169 Self::Isolation { dir } => {
4170 let dir = path_buf_lit(dir);
4171 quote! { #prefix::Isolation { dir: #dir } }
4172 }
4173 })
4174 }
4175 }
4176
4177 impl ToTokens for WebviewInstallMode {
4178 fn to_tokens(&self, tokens: &mut TokenStream) {
4179 let prefix = quote! { ::tauri::utils::config::WebviewInstallMode };
4180
4181 tokens.append_all(match self {
4182 Self::Skip => quote! { #prefix::Skip },
4183 Self::DownloadBootstrapper { silent } => {
4184 quote! { #prefix::DownloadBootstrapper { silent: #silent } }
4185 }
4186 Self::EmbedBootstrapper { silent } => {
4187 quote! { #prefix::EmbedBootstrapper { silent: #silent } }
4188 }
4189 Self::OfflineInstaller { silent } => {
4190 quote! { #prefix::OfflineInstaller { silent: #silent } }
4191 }
4192 Self::FixedRuntime { path } => {
4193 let path = path_buf_lit(path);
4194 quote! { #prefix::FixedRuntime { path: #path } }
4195 }
4196 })
4197 }
4198 }
4199
4200 impl ToTokens for WindowsConfig {
4201 fn to_tokens(&self, tokens: &mut TokenStream) {
4202 let webview_install_mode = &self.webview_install_mode;
4203 tokens.append_all(quote! { ::tauri::utils::config::WindowsConfig {
4204 webview_install_mode: #webview_install_mode,
4205 ..Default::default()
4206 }})
4207 }
4208 }
4209
4210 impl ToTokens for BundleResources {
4211 fn to_tokens(&self, tokens: &mut TokenStream) {
4212 let prefix = quote! { ::tauri::utils::config::BundleResources };
4213
4214 tokens.append_all(match self {
4215 Self::List(paths) => {
4216 let paths = vec_lit(paths, str_lit);
4217 quote! { #prefix::List(#paths) }
4218 }
4219 Self::Map(map) => {
4220 let map = map_lit(
4221 quote! { ::std::collections::HashMap },
4222 map,
4223 str_lit,
4224 str_lit,
4225 );
4226 quote! { #prefix::Map(#map) }
4227 }
4228 })
4229 }
4230 }
4231
4232 impl ToTokens for BundleConfig {
4233 fn to_tokens(&self, tokens: &mut TokenStream) {
4234 let publisher = quote!(None);
4235 let homepage = quote!(None);
4236 let icon = vec_lit(&self.icon, str_lit);
4237 let active = self.active;
4238 let targets = quote!(Default::default());
4239 let create_updater_artifacts = quote!(Default::default());
4240 let resources = opt_lit(self.resources.as_ref());
4241 let copyright = quote!(None);
4242 let category = quote!(None);
4243 let file_associations = quote!(None);
4244 let short_description = quote!(None);
4245 let long_description = quote!(None);
4246 let use_local_tools_dir = self.use_local_tools_dir;
4247 let external_bin = opt_vec_lit(self.external_bin.as_ref(), str_lit);
4248 let windows = &self.windows;
4249 let license = opt_str_lit(self.license.as_ref());
4250 let license_file = opt_lit(self.license_file.as_ref().map(path_buf_lit).as_ref());
4251 let linux = quote!(Default::default());
4252 let macos = quote!(Default::default());
4253 let ios = quote!(Default::default());
4254 let android = quote!(Default::default());
4255 let cef = quote!(Default::default());
4256
4257 literal_struct!(
4258 tokens,
4259 ::tauri::utils::config::BundleConfig,
4260 active,
4261 publisher,
4262 homepage,
4263 icon,
4264 targets,
4265 create_updater_artifacts,
4266 resources,
4267 copyright,
4268 category,
4269 license,
4270 license_file,
4271 file_associations,
4272 short_description,
4273 long_description,
4274 use_local_tools_dir,
4275 external_bin,
4276 windows,
4277 linux,
4278 macos,
4279 ios,
4280 android,
4281 cef
4282 );
4283 }
4284 }
4285
4286 impl ToTokens for FrontendDist {
4287 fn to_tokens(&self, tokens: &mut TokenStream) {
4288 let prefix = quote! { ::tauri::utils::config::FrontendDist };
4289
4290 tokens.append_all(match self {
4291 Self::Url(url) => {
4292 let url = url_lit(url);
4293 quote! { #prefix::Url(#url) }
4294 }
4295 Self::Directory(path) => {
4296 let path = path_buf_lit(path);
4297 quote! { #prefix::Directory(#path) }
4298 }
4299 Self::Files(files) => {
4300 let files = vec_lit(files, path_buf_lit);
4301 quote! { #prefix::Files(#files) }
4302 }
4303 })
4304 }
4305 }
4306
4307 impl ToTokens for RunnerConfig {
4308 fn to_tokens(&self, tokens: &mut TokenStream) {
4309 let prefix = quote! { ::tauri::utils::config::RunnerConfig };
4310
4311 tokens.append_all(match self {
4312 Self::String(cmd) => {
4313 let cmd = cmd.as_str();
4314 quote!(#prefix::String(#cmd.into()))
4315 }
4316 Self::Object { cmd, cwd, args } => {
4317 let cmd = cmd.as_str();
4318 let cwd = opt_str_lit(cwd.as_ref());
4319 let args = opt_lit(args.as_ref().map(|v| vec_lit(v, str_lit)).as_ref());
4320 quote!(#prefix::Object {
4321 cmd: #cmd.into(),
4322 cwd: #cwd,
4323 args: #args,
4324 })
4325 }
4326 })
4327 }
4328 }
4329
4330 impl ToTokens for BuildConfig {
4331 fn to_tokens(&self, tokens: &mut TokenStream) {
4332 let dev_url = opt_lit(self.dev_url.as_ref().map(url_lit).as_ref());
4333 let frontend_dist = opt_lit(self.frontend_dist.as_ref());
4334 let runner = opt_lit(self.runner.as_ref());
4335 let before_dev_command = quote!(None);
4336 let before_build_command = quote!(None);
4337 let before_bundle_command = quote!(None);
4338 let features = quote!(None);
4339 let remove_unused_commands = quote!(false);
4340 let additional_watch_folders = quote!(Vec::new());
4341 let windows = &self.windows;
4342
4343 literal_struct!(
4344 tokens,
4345 ::tauri::utils::config::BuildConfig,
4346 runner,
4347 dev_url,
4348 frontend_dist,
4349 before_dev_command,
4350 before_build_command,
4351 before_bundle_command,
4352 features,
4353 remove_unused_commands,
4354 additional_watch_folders,
4355 windows
4356 );
4357 }
4358 }
4359
4360 impl ToTokens for WindowsBuildConfig {
4361 fn to_tokens(&self, tokens: &mut TokenStream) {
4362 let static_vc_runtime = self.static_vc_runtime;
4363
4364 literal_struct!(
4365 tokens,
4366 ::tauri::utils::config::WindowsBuildConfig,
4367 static_vc_runtime
4368 );
4369 }
4370 }
4371
4372 impl ToTokens for CspDirectiveSources {
4373 fn to_tokens(&self, tokens: &mut TokenStream) {
4374 let prefix = quote! { ::tauri::utils::config::CspDirectiveSources };
4375
4376 tokens.append_all(match self {
4377 Self::Inline(sources) => {
4378 let sources = sources.as_str();
4379 quote!(#prefix::Inline(#sources.into()))
4380 }
4381 Self::List(list) => {
4382 let list = vec_lit(list, str_lit);
4383 quote!(#prefix::List(#list))
4384 }
4385 })
4386 }
4387 }
4388
4389 impl ToTokens for Csp {
4390 fn to_tokens(&self, tokens: &mut TokenStream) {
4391 let prefix = quote! { ::tauri::utils::config::Csp };
4392
4393 tokens.append_all(match self {
4394 Self::Policy(policy) => {
4395 let policy = policy.as_str();
4396 quote!(#prefix::Policy(#policy.into()))
4397 }
4398 Self::DirectiveMap(list) => {
4399 let mut sorted: Vec<_> = list.iter().collect();
4403 sorted.sort_by_key(|(k, _)| *k);
4404 let map = map_lit(
4405 quote! { ::std::collections::HashMap },
4406 sorted,
4407 str_lit,
4408 identity,
4409 );
4410 quote!(#prefix::DirectiveMap(#map))
4411 }
4412 })
4413 }
4414 }
4415
4416 impl ToTokens for DisabledCspModificationKind {
4417 fn to_tokens(&self, tokens: &mut TokenStream) {
4418 let prefix = quote! { ::tauri::utils::config::DisabledCspModificationKind };
4419
4420 tokens.append_all(match self {
4421 Self::Flag(flag) => {
4422 quote! { #prefix::Flag(#flag) }
4423 }
4424 Self::List(directives) => {
4425 let directives = vec_lit(directives, str_lit);
4426 quote! { #prefix::List(#directives) }
4427 }
4428 });
4429 }
4430 }
4431
4432 impl ToTokens for CapabilityEntry {
4433 fn to_tokens(&self, tokens: &mut TokenStream) {
4434 let prefix = quote! { ::tauri::utils::config::CapabilityEntry };
4435
4436 tokens.append_all(match self {
4437 Self::Inlined(capability) => {
4438 quote! { #prefix::Inlined(#capability) }
4439 }
4440 Self::Reference(id) => {
4441 let id = str_lit(id);
4442 quote! { #prefix::Reference(#id) }
4443 }
4444 });
4445 }
4446 }
4447
4448 impl ToTokens for HeaderSource {
4449 fn to_tokens(&self, tokens: &mut TokenStream) {
4450 let prefix = quote! { ::tauri::utils::config::HeaderSource };
4451
4452 tokens.append_all(match self {
4453 Self::Inline(s) => {
4454 let line = s.as_str();
4455 quote!(#prefix::Inline(#line.into()))
4456 }
4457 Self::List(l) => {
4458 let list = vec_lit(l, str_lit);
4459 quote!(#prefix::List(#list))
4460 }
4461 Self::Map(m) => {
4462 let mut sorted: Vec<_> = m.iter().collect();
4466 sorted.sort_by_key(|(k, _)| *k);
4467 let map = map_lit(
4468 quote! { ::std::collections::HashMap },
4469 sorted,
4470 str_lit,
4471 str_lit,
4472 );
4473 quote!(#prefix::Map(#map))
4474 }
4475 })
4476 }
4477 }
4478
4479 impl ToTokens for HeaderConfig {
4480 fn to_tokens(&self, tokens: &mut TokenStream) {
4481 let access_control_allow_credentials =
4482 opt_lit(self.access_control_allow_credentials.as_ref());
4483 let access_control_allow_headers = opt_lit(self.access_control_allow_headers.as_ref());
4484 let access_control_allow_methods = opt_lit(self.access_control_allow_methods.as_ref());
4485 let access_control_expose_headers = opt_lit(self.access_control_expose_headers.as_ref());
4486 let access_control_max_age = opt_lit(self.access_control_max_age.as_ref());
4487 let cross_origin_embedder_policy = opt_lit(self.cross_origin_embedder_policy.as_ref());
4488 let cross_origin_opener_policy = opt_lit(self.cross_origin_opener_policy.as_ref());
4489 let cross_origin_resource_policy = opt_lit(self.cross_origin_resource_policy.as_ref());
4490 let permissions_policy = opt_lit(self.permissions_policy.as_ref());
4491 let service_worker_allowed = opt_lit(self.service_worker_allowed.as_ref());
4492 let timing_allow_origin = opt_lit(self.timing_allow_origin.as_ref());
4493 let x_content_type_options = opt_lit(self.x_content_type_options.as_ref());
4494 let tauri_custom_header = opt_lit(self.tauri_custom_header.as_ref());
4495
4496 literal_struct!(
4497 tokens,
4498 ::tauri::utils::config::HeaderConfig,
4499 access_control_allow_credentials,
4500 access_control_allow_headers,
4501 access_control_allow_methods,
4502 access_control_expose_headers,
4503 access_control_max_age,
4504 cross_origin_embedder_policy,
4505 cross_origin_opener_policy,
4506 cross_origin_resource_policy,
4507 permissions_policy,
4508 service_worker_allowed,
4509 timing_allow_origin,
4510 x_content_type_options,
4511 tauri_custom_header
4512 );
4513 }
4514 }
4515
4516 impl ToTokens for SecurityConfig {
4517 fn to_tokens(&self, tokens: &mut TokenStream) {
4518 let csp = opt_lit(self.csp.as_ref());
4519 let dev_csp = opt_lit(self.dev_csp.as_ref());
4520 let freeze_prototype = self.freeze_prototype;
4521 let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification;
4522 let asset_protocol = &self.asset_protocol;
4523 let pattern = &self.pattern;
4524 let capabilities = vec_lit(&self.capabilities, identity);
4525 let headers = opt_lit(self.headers.as_ref());
4526
4527 literal_struct!(
4528 tokens,
4529 ::tauri::utils::config::SecurityConfig,
4530 csp,
4531 dev_csp,
4532 freeze_prototype,
4533 dangerous_disable_asset_csp_modification,
4534 asset_protocol,
4535 pattern,
4536 capabilities,
4537 headers
4538 );
4539 }
4540 }
4541
4542 impl ToTokens for TrayIconConfig {
4543 fn to_tokens(&self, tokens: &mut TokenStream) {
4544 tokens.append_all(quote!(#[allow(deprecated)]));
4546
4547 let id = opt_str_lit(self.id.as_ref());
4548 let icon_as_template = self.icon_as_template;
4549 #[allow(deprecated)]
4550 let menu_on_left_click = self.menu_on_left_click;
4551 let show_menu_on_left_click = self.show_menu_on_left_click;
4552 let icon_path = path_buf_lit(&self.icon_path);
4553 let title = opt_str_lit(self.title.as_ref());
4554 let tooltip = opt_str_lit(self.tooltip.as_ref());
4555 literal_struct!(
4556 tokens,
4557 ::tauri::utils::config::TrayIconConfig,
4558 id,
4559 icon_path,
4560 icon_as_template,
4561 menu_on_left_click,
4562 show_menu_on_left_click,
4563 title,
4564 tooltip
4565 );
4566 }
4567 }
4568
4569 impl ToTokens for FsScope {
4570 fn to_tokens(&self, tokens: &mut TokenStream) {
4571 let prefix = quote! { ::tauri::utils::config::FsScope };
4572
4573 tokens.append_all(match self {
4574 Self::AllowedPaths(allow) => {
4575 let allowed_paths = vec_lit(allow, path_buf_lit);
4576 quote! { #prefix::AllowedPaths(#allowed_paths) }
4577 }
4578 Self::Scope { allow, deny , require_literal_leading_dot} => {
4579 let allow = vec_lit(allow, path_buf_lit);
4580 let deny = vec_lit(deny, path_buf_lit);
4581 let require_literal_leading_dot = opt_lit(require_literal_leading_dot.as_ref());
4582 quote! { #prefix::Scope { allow: #allow, deny: #deny, require_literal_leading_dot: #require_literal_leading_dot } }
4583 }
4584 });
4585 }
4586 }
4587
4588 impl ToTokens for AssetProtocolConfig {
4589 fn to_tokens(&self, tokens: &mut TokenStream) {
4590 let scope = &self.scope;
4591 tokens.append_all(quote! { ::tauri::utils::config::AssetProtocolConfig { scope: #scope, ..Default::default() } })
4592 }
4593 }
4594
4595 impl ToTokens for AppConfig {
4596 fn to_tokens(&self, tokens: &mut TokenStream) {
4597 let windows = vec_lit(&self.windows, identity);
4598 let security = &self.security;
4599 let tray_icon = opt_lit(self.tray_icon.as_ref());
4600 let macos_private_api = self.macos_private_api;
4601 let with_global_tauri = self.with_global_tauri;
4602 let enable_gtk_app_id = self.enable_gtk_app_id;
4603
4604 literal_struct!(
4605 tokens,
4606 ::tauri::utils::config::AppConfig,
4607 windows,
4608 security,
4609 tray_icon,
4610 macos_private_api,
4611 with_global_tauri,
4612 enable_gtk_app_id
4613 );
4614 }
4615 }
4616
4617 impl ToTokens for PluginConfig {
4618 fn to_tokens(&self, tokens: &mut TokenStream) {
4619 let mut sorted: Vec<_> = self.0.iter().collect();
4623 sorted.sort_by_key(|(k, _)| *k);
4624 let config = map_lit(
4625 quote! { ::std::collections::HashMap },
4626 sorted,
4627 str_lit,
4628 json_value_lit,
4629 );
4630 tokens.append_all(quote! { ::tauri::utils::config::PluginConfig(#config) })
4631 }
4632 }
4633
4634 impl ToTokens for Config {
4635 fn to_tokens(&self, tokens: &mut TokenStream) {
4636 let schema = quote!(None);
4637 let product_name = opt_str_lit(self.product_name.as_ref());
4638 let main_binary_name = opt_str_lit(self.main_binary_name.as_ref());
4639 let version = opt_str_lit(self.version.as_ref());
4640 let identifier = str_lit(&self.identifier);
4641 let app = &self.app;
4642 let build = &self.build;
4643 let bundle = &self.bundle;
4644 let plugins = &self.plugins;
4645
4646 literal_struct!(
4647 tokens,
4648 ::tauri::utils::config::Config,
4649 schema,
4650 product_name,
4651 main_binary_name,
4652 version,
4653 identifier,
4654 app,
4655 build,
4656 bundle,
4657 plugins
4658 );
4659 }
4660 }
4661}
4662
4663#[cfg(test)]
4664mod test {
4665 use super::*;
4666
4667 #[test]
4670 fn test_defaults() {
4672 let a_config = AppConfig::default();
4674 let b_config = BuildConfig::default();
4676 let d_windows: Vec<WindowConfig> = vec![];
4678 let d_bundle = BundleConfig::default();
4680
4681 let app = AppConfig {
4683 windows: vec![],
4684 security: SecurityConfig {
4685 csp: None,
4686 dev_csp: None,
4687 freeze_prototype: false,
4688 dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false),
4689 asset_protocol: AssetProtocolConfig::default(),
4690 pattern: Default::default(),
4691 capabilities: Vec::new(),
4692 headers: None,
4693 },
4694 tray_icon: None,
4695 macos_private_api: false,
4696 with_global_tauri: false,
4697 enable_gtk_app_id: false,
4698 };
4699
4700 let build = BuildConfig {
4702 runner: None,
4703 dev_url: None,
4704 frontend_dist: None,
4705 before_dev_command: None,
4706 before_build_command: None,
4707 before_bundle_command: None,
4708 features: None,
4709 remove_unused_commands: false,
4710 additional_watch_folders: Vec::new(),
4711 windows: WindowsBuildConfig::default(),
4712 };
4713
4714 let bundle = BundleConfig {
4716 active: false,
4717 targets: Default::default(),
4718 create_updater_artifacts: Default::default(),
4719 publisher: None,
4720 homepage: None,
4721 icon: Vec::new(),
4722 resources: None,
4723 copyright: None,
4724 category: None,
4725 file_associations: None,
4726 short_description: None,
4727 long_description: None,
4728 use_local_tools_dir: false,
4729 license: None,
4730 license_file: None,
4731 linux: Default::default(),
4732 macos: Default::default(),
4733 external_bin: None,
4734 windows: Default::default(),
4735 ios: Default::default(),
4736 android: Default::default(),
4737 cef: Default::default(),
4738 };
4739
4740 assert_eq!(a_config, app);
4742 assert_eq!(b_config, build);
4743 assert_eq!(d_bundle, bundle);
4744 assert_eq!(d_windows, app.windows);
4745 }
4746
4747 #[test]
4748 fn parse_hex_color() {
4749 use super::Color;
4750
4751 assert_eq!(Color(255, 255, 255, 255), "fff".parse().unwrap());
4752 assert_eq!(Color(255, 255, 255, 255), "#fff".parse().unwrap());
4753 assert_eq!(Color(0, 0, 0, 255), "#000000".parse().unwrap());
4754 assert_eq!(Color(0, 0, 0, 255), "#000000ff".parse().unwrap());
4755 assert_eq!(Color(0, 255, 0, 255), "#00ff00ff".parse().unwrap());
4756 }
4757
4758 #[test]
4759 fn test_runner_config_string_format() {
4760 use super::RunnerConfig;
4761
4762 let json = r#""cargo""#;
4764 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4765
4766 assert_eq!(runner.cmd(), "cargo");
4767 assert_eq!(runner.cwd(), None);
4768 assert_eq!(runner.args(), None);
4769
4770 let serialized = serde_json::to_string(&runner).unwrap();
4772 assert_eq!(serialized, r#""cargo""#);
4773 }
4774
4775 #[test]
4776 fn test_runner_config_object_format_full() {
4777 use super::RunnerConfig;
4778
4779 let json = r#"{"cmd": "my_runner", "cwd": "/tmp/build", "args": ["--quiet", "--verbose"]}"#;
4781 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4782
4783 assert_eq!(runner.cmd(), "my_runner");
4784 assert_eq!(runner.cwd(), Some("/tmp/build"));
4785 assert_eq!(
4786 runner.args(),
4787 Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4788 );
4789
4790 let serialized = serde_json::to_string(&runner).unwrap();
4792 let deserialized: RunnerConfig = serde_json::from_str(&serialized).unwrap();
4793 assert_eq!(runner, deserialized);
4794 }
4795
4796 #[test]
4797 fn test_runner_config_object_format_minimal() {
4798 use super::RunnerConfig;
4799
4800 let json = r#"{"cmd": "cross"}"#;
4802 let runner: RunnerConfig = serde_json::from_str(json).unwrap();
4803
4804 assert_eq!(runner.cmd(), "cross");
4805 assert_eq!(runner.cwd(), None);
4806 assert_eq!(runner.args(), None);
4807 }
4808
4809 #[test]
4810 fn test_runner_config_default() {
4811 use super::RunnerConfig;
4812
4813 let default_runner = RunnerConfig::default();
4814 assert_eq!(default_runner.cmd(), "cargo");
4815 assert_eq!(default_runner.cwd(), None);
4816 assert_eq!(default_runner.args(), None);
4817 }
4818
4819 #[test]
4820 fn test_runner_config_from_str() {
4821 use super::RunnerConfig;
4822
4823 let runner: RunnerConfig = "my_runner".into();
4825 assert_eq!(runner.cmd(), "my_runner");
4826 assert_eq!(runner.cwd(), None);
4827 assert_eq!(runner.args(), None);
4828 }
4829
4830 #[test]
4831 fn test_runner_config_from_string() {
4832 use super::RunnerConfig;
4833
4834 let runner: RunnerConfig = "another_runner".to_string().into();
4836 assert_eq!(runner.cmd(), "another_runner");
4837 assert_eq!(runner.cwd(), None);
4838 assert_eq!(runner.args(), None);
4839 }
4840
4841 #[test]
4842 fn test_runner_config_from_str_parse() {
4843 use super::RunnerConfig;
4844 use std::str::FromStr;
4845
4846 let runner = RunnerConfig::from_str("parsed_runner").unwrap();
4848 assert_eq!(runner.cmd(), "parsed_runner");
4849 assert_eq!(runner.cwd(), None);
4850 assert_eq!(runner.args(), None);
4851 }
4852
4853 #[test]
4854 fn test_runner_config_in_build_config() {
4855 use super::BuildConfig;
4856
4857 let json = r#"{"runner": "cargo"}"#;
4859 let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4860
4861 let runner = build_config.runner.unwrap();
4862 assert_eq!(runner.cmd(), "cargo");
4863 assert_eq!(runner.cwd(), None);
4864 assert_eq!(runner.args(), None);
4865 }
4866
4867 #[test]
4868 fn test_runner_config_in_build_config_object() {
4869 use super::BuildConfig;
4870
4871 let json = r#"{"runner": {"cmd": "cross", "cwd": "/workspace", "args": ["--target", "x86_64-unknown-linux-gnu"]}}"#;
4873 let build_config: BuildConfig = serde_json::from_str(json).unwrap();
4874
4875 let runner = build_config.runner.unwrap();
4876 assert_eq!(runner.cmd(), "cross");
4877 assert_eq!(runner.cwd(), Some("/workspace"));
4878 assert_eq!(
4879 runner.args(),
4880 Some(
4881 &[
4882 "--target".to_string(),
4883 "x86_64-unknown-linux-gnu".to_string()
4884 ][..]
4885 )
4886 );
4887 }
4888
4889 #[test]
4890 fn test_runner_config_in_full_config() {
4891 use super::Config;
4892
4893 let json = r#"{
4895 "productName": "Test App",
4896 "version": "1.0.0",
4897 "identifier": "com.test.app",
4898 "build": {
4899 "runner": {
4900 "cmd": "my_custom_cargo",
4901 "cwd": "/tmp/build",
4902 "args": ["--quiet", "--verbose"]
4903 }
4904 }
4905 }"#;
4906
4907 let config: Config = serde_json::from_str(json).unwrap();
4908 let runner = config.build.runner.unwrap();
4909
4910 assert_eq!(runner.cmd(), "my_custom_cargo");
4911 assert_eq!(runner.cwd(), Some("/tmp/build"));
4912 assert_eq!(
4913 runner.args(),
4914 Some(&["--quiet".to_string(), "--verbose".to_string()][..])
4915 );
4916 }
4917
4918 #[test]
4919 fn test_runner_config_equality() {
4920 use super::RunnerConfig;
4921
4922 let runner1 = RunnerConfig::String("cargo".to_string());
4923 let runner2 = RunnerConfig::String("cargo".to_string());
4924 let runner3 = RunnerConfig::String("cross".to_string());
4925
4926 assert_eq!(runner1, runner2);
4927 assert_ne!(runner1, runner3);
4928
4929 let runner4 = RunnerConfig::Object {
4930 cmd: "cargo".to_string(),
4931 cwd: Some("/tmp".to_string()),
4932 args: Some(vec!["--quiet".to_string()]),
4933 };
4934 let runner5 = RunnerConfig::Object {
4935 cmd: "cargo".to_string(),
4936 cwd: Some("/tmp".to_string()),
4937 args: Some(vec!["--quiet".to_string()]),
4938 };
4939
4940 assert_eq!(runner4, runner5);
4941 assert_ne!(runner1, runner4);
4942 }
4943
4944 #[test]
4945 fn test_runner_config_untagged_serialization() {
4946 use super::RunnerConfig;
4947
4948 let string_runner = RunnerConfig::String("cargo".to_string());
4950 let string_json = serde_json::to_string(&string_runner).unwrap();
4951 assert_eq!(string_json, r#""cargo""#);
4952
4953 let object_runner = RunnerConfig::Object {
4955 cmd: "cross".to_string(),
4956 cwd: None,
4957 args: None,
4958 };
4959 let object_json = serde_json::to_string(&object_runner).unwrap();
4960 assert!(object_json.contains("\"cmd\":\"cross\""));
4961 assert!(object_json.contains("\"cwd\":null") || !object_json.contains("cwd"));
4963 assert!(object_json.contains("\"args\":null") || !object_json.contains("args"));
4964 }
4965
4966 #[test]
4967 fn header_source_map_display_is_deterministic() {
4968 let map = HashMap::from([
4969 ("key3".to_string(), "'value3'".to_string()),
4970 ("key1".to_string(), "'value1' 'value2'".to_string()),
4971 ("key2".to_string(), "'value4'".to_string()),
4972 ]);
4973
4974 assert_eq!(
4976 HeaderSource::Map(map.clone()).to_string(),
4977 "key1 'value1' 'value2'; key2 'value4'; key3 'value3'"
4978 );
4979
4980 let expected = HeaderSource::Map(map).to_string();
4981 for _ in 0..10 {
4982 let map = HashMap::from([
4983 ("key2".to_string(), "'value4'".to_string()),
4984 ("key3".to_string(), "'value3'".to_string()),
4985 ("key1".to_string(), "'value1' 'value2'".to_string()),
4986 ]);
4987 assert_eq!(HeaderSource::Map(map).to_string(), expected);
4988 }
4989
4990 let map = HashMap::from([
4992 ("b".to_string(), "2".to_string()),
4993 ("a".to_string(), "1".to_string()),
4994 ]);
4995 assert_eq!(
4996 serde_json::to_string(&HeaderSource::Map(map)).unwrap(),
4997 r#"{"a":"1","b":"2"}"#
4998 );
4999 }
5000
5001 #[test]
5002 fn header_source_display() {
5003 assert_eq!(
5004 HeaderSource::Inline("same-origin".into()).to_string(),
5005 "same-origin"
5006 );
5007 assert_eq!(
5008 HeaderSource::List(vec!["https://a.example".into(), "https://b.example".into()]).to_string(),
5009 "https://a.example, https://b.example"
5010 );
5011 }
5012
5013 #[test]
5014 fn window_config_default_same_as_deserialize() {
5015 let config_from_deserialization: WindowConfig = serde_json::from_str("{}").unwrap();
5016 let config_from_default: WindowConfig = WindowConfig::default();
5017
5018 assert_eq!(config_from_deserialization, config_from_default);
5019 }
5020}