Skip to content

Commit 3d6868d

Browse files
authored
feat(cli): UTExportedTypeDeclarations support for file associations (#14128)
* feat(cli): UTExportedTypeDeclarations support for file associations closes #13314 * update example * update readme
1 parent cc8c0b5 commit 3d6868d

File tree

10 files changed

+230
-16
lines changed

10 files changed

+230
-16
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-cli": minor:feat
3+
"@tauri-apps/cli": minor:feat
4+
---
5+
6+
Added support to defining the content type of the declared file association on macOS (maps to LSItemContentTypes property).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-cli": minor:feat
3+
"@tauri-apps/cli": minor:feat
4+
---
5+
6+
Added support to defining the metadata for custom types declared in `tauri.conf.json > bundle > fileAssociations > exportedType` via the `UTExportedTypeDeclarations` Info.plist property.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-utils": minor:feat
3+
---
4+
5+
Added `FileAssociation::exported_type` and `FileAssociation::content_types` for better support to defining custom types on macOS.

crates/tauri-bundler/src/bundle/macos/app.rs

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -268,23 +268,83 @@ fn create_info_plist(
268268
}
269269

270270
if let Some(associations) = settings.file_associations() {
271+
let exported_associations = associations
272+
.iter()
273+
.filter_map(|association| {
274+
association.exported_type.as_ref().map(|exported_type| {
275+
let mut dict = plist::Dictionary::new();
276+
277+
dict.insert(
278+
"UTTypeIdentifier".into(),
279+
exported_type.identifier.clone().into(),
280+
);
281+
if let Some(description) = &association.description {
282+
dict.insert("UTTypeDescription".into(), description.clone().into());
283+
}
284+
if let Some(conforms_to) = &exported_type.conforms_to {
285+
dict.insert(
286+
"UTTypeConformsTo".into(),
287+
plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
288+
);
289+
}
290+
291+
let mut specification = plist::Dictionary::new();
292+
specification.insert(
293+
"public.filename-extension".into(),
294+
plist::Value::Array(
295+
association
296+
.ext
297+
.iter()
298+
.map(|s| s.to_string().into())
299+
.collect(),
300+
),
301+
);
302+
if let Some(mime_type) = &association.mime_type {
303+
specification.insert("public.mime-type".into(), mime_type.clone().into());
304+
}
305+
306+
dict.insert("UTTypeTagSpecification".into(), specification.into());
307+
308+
plist::Value::Dictionary(dict)
309+
})
310+
})
311+
.collect::<Vec<_>>();
312+
313+
if !exported_associations.is_empty() {
314+
plist.insert(
315+
"UTExportedTypeDeclarations".into(),
316+
plist::Value::Array(exported_associations),
317+
);
318+
}
319+
271320
plist.insert(
272321
"CFBundleDocumentTypes".into(),
273322
plist::Value::Array(
274323
associations
275324
.iter()
276325
.map(|association| {
277326
let mut dict = plist::Dictionary::new();
278-
dict.insert(
279-
"CFBundleTypeExtensions".into(),
280-
plist::Value::Array(
281-
association
282-
.ext
283-
.iter()
284-
.map(|ext| ext.to_string().into())
285-
.collect(),
286-
),
287-
);
327+
328+
if !association.ext.is_empty() {
329+
dict.insert(
330+
"CFBundleTypeExtensions".into(),
331+
plist::Value::Array(
332+
association
333+
.ext
334+
.iter()
335+
.map(|ext| ext.to_string().into())
336+
.collect(),
337+
),
338+
);
339+
}
340+
341+
if let Some(content_types) = &association.content_types {
342+
dict.insert(
343+
"LSItemContentTypes".into(),
344+
plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()),
345+
);
346+
}
347+
288348
dict.insert(
289349
"CFBundleTypeName".into(),
290350
association

crates/tauri-cli/config.schema.json

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,7 @@
21592159
]
21602160
},
21612161
"fileAssociations": {
2162-
"description": "File associations to application.",
2162+
"description": "File types to associate with the application.",
21632163
"type": [
21642164
"array",
21652165
"null"
@@ -2433,6 +2433,16 @@
24332433
"$ref": "#/definitions/AssociationExt"
24342434
}
24352435
},
2436+
"contentTypes": {
2437+
"description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].",
2438+
"type": [
2439+
"array",
2440+
"null"
2441+
],
2442+
"items": {
2443+
"type": "string"
2444+
}
2445+
},
24362446
"name": {
24372447
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
24382448
"type": [
@@ -2471,6 +2481,17 @@
24712481
"$ref": "#/definitions/HandlerRank"
24722482
}
24732483
]
2484+
},
2485+
"exportedType": {
2486+
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.\n\n You should define this if the associated file is a custom file type defined by your application.",
2487+
"anyOf": [
2488+
{
2489+
"$ref": "#/definitions/ExportedFileAssociation"
2490+
},
2491+
{
2492+
"type": "null"
2493+
}
2494+
]
24742495
}
24752496
},
24762497
"additionalProperties": false
@@ -2552,6 +2573,30 @@
25522573
}
25532574
]
25542575
},
2576+
"ExportedFileAssociation": {
2577+
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.",
2578+
"type": "object",
2579+
"required": [
2580+
"identifier"
2581+
],
2582+
"properties": {
2583+
"identifier": {
2584+
"description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.",
2585+
"type": "string"
2586+
},
2587+
"conformsTo": {
2588+
"description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.json` and `public.database`.",
2589+
"type": [
2590+
"array",
2591+
"null"
2592+
],
2593+
"items": {
2594+
"type": "string"
2595+
}
2596+
}
2597+
},
2598+
"additionalProperties": false
2599+
},
25552600
"WindowsConfig": {
25562601
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
25572602
"type": "object",

crates/tauri-schema-generator/schemas/config.schema.json

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,7 @@
21592159
]
21602160
},
21612161
"fileAssociations": {
2162-
"description": "File associations to application.",
2162+
"description": "File types to associate with the application.",
21632163
"type": [
21642164
"array",
21652165
"null"
@@ -2433,6 +2433,16 @@
24332433
"$ref": "#/definitions/AssociationExt"
24342434
}
24352435
},
2436+
"contentTypes": {
2437+
"description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].",
2438+
"type": [
2439+
"array",
2440+
"null"
2441+
],
2442+
"items": {
2443+
"type": "string"
2444+
}
2445+
},
24362446
"name": {
24372447
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
24382448
"type": [
@@ -2471,6 +2481,17 @@
24712481
"$ref": "#/definitions/HandlerRank"
24722482
}
24732483
]
2484+
},
2485+
"exportedType": {
2486+
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.\n\n You should define this if the associated file is a custom file type defined by your application.",
2487+
"anyOf": [
2488+
{
2489+
"$ref": "#/definitions/ExportedFileAssociation"
2490+
},
2491+
{
2492+
"type": "null"
2493+
}
2494+
]
24742495
}
24752496
},
24762497
"additionalProperties": false
@@ -2552,6 +2573,30 @@
25522573
}
25532574
]
25542575
},
2576+
"ExportedFileAssociation": {
2577+
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.",
2578+
"type": "object",
2579+
"required": [
2580+
"identifier"
2581+
],
2582+
"properties": {
2583+
"identifier": {
2584+
"description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.",
2585+
"type": "string"
2586+
},
2587+
"conformsTo": {
2588+
"description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.json` and `public.database`.",
2589+
"type": [
2590+
"array",
2591+
"null"
2592+
],
2593+
"items": {
2594+
"type": "string"
2595+
}
2596+
}
2597+
},
2598+
"additionalProperties": false
2599+
},
25552600
"WindowsConfig": {
25562601
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
25572602
"type": "object",

crates/tauri-utils/src/config.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,12 @@ impl<'d> serde::Deserialize<'d> for AssociationExt {
11771177
pub struct FileAssociation {
11781178
/// File extensions to associate with this app. e.g. 'png'
11791179
pub ext: Vec<AssociationExt>,
1180+
/// Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.
1181+
///
1182+
/// This allows supporting any file format declared by another application that conforms to this type.
1183+
/// Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].
1184+
#[serde(alias = "content-types")]
1185+
pub content_types: Option<Vec<String>>,
11801186
/// The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`
11811187
pub name: Option<String>,
11821188
/// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.
@@ -1190,6 +1196,24 @@ pub struct FileAssociation {
11901196
/// The ranking of this app among apps that declare themselves as editors or viewers of the given file type. Maps to `LSHandlerRank` on macOS.
11911197
#[serde(default)]
11921198
pub rank: HandlerRank,
1199+
/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
1200+
///
1201+
/// You should define this if the associated file is a custom file type defined by your application.
1202+
pub exported_type: Option<ExportedFileAssociation>,
1203+
}
1204+
1205+
/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
1206+
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
1207+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
1208+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
1209+
pub struct ExportedFileAssociation {
1210+
/// The unique identifier for the exported type. Maps to `UTTypeIdentifier`.
1211+
pub identifier: String,
1212+
/// The types that this type conforms to. Maps to `UTTypeConformsTo`.
1213+
///
1214+
/// Examples are `public.data`, `public.image`, `public.json` and `public.database`.
1215+
#[serde(alias = "conforms-to")]
1216+
pub conforms_to: Option<Vec<String>>,
11931217
}
11941218

11951219
/// Deep link protocol configuration.
@@ -1356,7 +1380,7 @@ pub struct BundleConfig {
13561380
/// Should be one of the following:
13571381
/// Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather.
13581382
pub category: Option<String>,
1359-
/// File associations to application.
1383+
/// File types to associate with the application.
13601384
pub file_associations: Option<Vec<FileAssociation>>,
13611385
/// A short description of your application.
13621386
#[serde(alias = "short-description")]

examples/file-associations/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ This feature is commonly used for functionality such as previewing or editing fi
1111
```
1212
cargo build --features tauri/protocol-asset
1313
```
14+
15+
## Associations
16+
17+
This example creates associations with PNG, JPG, JPEG and GIF files.
18+
19+
Additionally, it defines two new extensions - `taurid` (derives from a raw data file) and `taurijson` (derives from JSON). They have special treatment on macOS (see `exportedType` in `src-tauri/tauri.conf.json`).

examples/file-associations/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ tauri-build = { path = "../../../crates/tauri-build", features = ["codegen"] }
1111
[dependencies]
1212
serde_json = "1"
1313
serde = { version = "1", features = ["derive"] }
14-
tauri = { path = "../../../crates/tauri", features = [] }
14+
tauri = { path = "../../../crates/tauri", features = ["protocol-asset"] }
1515
url = "2"

examples/file-associations/src-tauri/tauri.conf.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
{
2-
"$schema": "../../../crates/tauri-cli/schema.json",
2+
"$schema": "../../../crates/tauri-cli/config.schema.json",
33
"identifier": "com.tauri.dev-file-associations-demo",
44
"build": {
55
"frontendDist": ["../index.html"]
66
},
77
"app": {
88
"security": {
9-
"csp": "default-src 'self'"
9+
"csp": "default-src 'self'",
10+
"assetProtocol": {
11+
"enable": true
12+
}
1013
}
1114
},
1215
"bundle": {
@@ -34,6 +37,20 @@
3437
"ext": ["gif"],
3538
"mimeType": "image/gif",
3639
"rank": "Owner"
40+
},
41+
{
42+
"ext": ["taurijson"],
43+
"exportedType": {
44+
"identifier": "com.tauri.dev-file-associations-demo.taurijson",
45+
"conformsTo": ["public.json"]
46+
}
47+
},
48+
{
49+
"ext": ["taurid"],
50+
"exportedType": {
51+
"identifier": "com.tauri.dev-file-associations-demo.tauridata",
52+
"conformsTo": ["public.data"]
53+
}
3754
}
3855
]
3956
}

0 commit comments

Comments
 (0)