Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/file-association-content-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
---

Added support to defining the content type of the declared file association on macOS (maps to LSItemContentTypes property).
6 changes: 6 additions & 0 deletions .changes/file-association-exported-type-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
---

Added support to defining the metadata for custom types declared in `tauri.conf.json > bundle > fileAssociations > exportedType` via the `UTExportedTypeDeclarations` Info.plist property.
5 changes: 5 additions & 0 deletions .changes/file-association-exported-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri-utils": minor:feat
---

Added `FileAssociation::exported_type` and `FileAssociation::content_types` for better support to defining custom types on macOS.
82 changes: 71 additions & 11 deletions crates/tauri-bundler/src/bundle/macos/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,29 +268,89 @@ fn create_info_plist(
}

if let Some(associations) = settings.file_associations() {
let exported_associations = associations
.iter()
.filter_map(|association| {
association.exported_type.as_ref().map(|exported_type| {
let mut dict = plist::Dictionary::new();

dict.insert(
"UTTypeIdentifier".into(),
exported_type.identifier.clone().into(),
);
if let Some(description) = &association.description {
dict.insert("UTTypeDescription".into(), description.clone().into());
}
if let Some(content_types) = &association.content_types {
dict.insert(
"UTTypeConformsTo".into(),
plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
);
}
Comment on lines +284 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Wrong field used for UTTypeConformsTo.

association.content_types is meant for LSItemContentTypes in CFBundleDocumentTypes, not for UTTypeConformsTo in UTExportedTypeDeclarations. The ExportedFileAssociation struct has its own conforms_to field that should be used here.

🐛 Proposed fix
-          if let Some(content_types) = &association.content_types {
+          if let Some(conforms_to) = &exported_type.conforms_to {
             dict.insert(
               "UTTypeConformsTo".into(),
-              plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
+              plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
             );
           }
🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 284 - 289, The
code incorrectly uses association.content_types when setting the
"UTTypeConformsTo" plist key; instead use the ExportedFileAssociation's
conforms_to field. Update the dict.insert call that creates the
"UTTypeConformsTo" entry (currently mapping content_types ->
plist::Value::Array(...)) to read from association.conforms_to, mapping each
string into plist values as before; ensure you only set this key when
association.conforms_to.is_some() and preserve the same Array construction
logic.


let mut specification = plist::Dictionary::new();
specification.insert(
"public.filename-extension".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|s| s.to_string().into())
.collect(),
),
);
if let Some(mime_type) = &association.mime_type {
specification.insert("public.mime-type".into(), mime_type.clone().into());
}

dict.insert("UTTypeTagSpecification".into(), specification.into());

plist::Value::Dictionary(dict)
})
})
.collect::<Vec<_>>();

if !exported_associations.is_empty() {
plist.insert(
"UTExportedTypeDeclarations".into(),
plist::Value::Array(exported_associations),
);
}

plist.insert(
"CFBundleDocumentTypes".into(),
plist::Value::Array(
associations
.iter()
.map(|association| {
let mut dict = plist::Dictionary::new();
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);

if association.ext.is_empty() {
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
}
Comment on lines +328 to +339

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Inverted condition inserts empty array instead of actual extensions.

The condition if association.ext.is_empty() will insert CFBundleTypeExtensions only when there are no extensions, resulting in an empty array. The logic should be inverted to insert extensions when they exist.

🐛 Proposed fix
-            if association.ext.is_empty() {
+            if !association.ext.is_empty() {
               dict.insert(
                 "CFBundleTypeExtensions".into(),
                 plist::Value::Array(
                   association
                     .ext
                     .iter()
                     .map(|ext| ext.to_string().into())
                     .collect(),
                 ),
               );
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if association.ext.is_empty() {
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
}
if !association.ext.is_empty() {
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
}
🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 328 - 339, The
current check uses if association.ext.is_empty() which inserts
CFBundleTypeExtensions when there are no extensions; invert the condition to
only call dict.insert for "CFBundleTypeExtensions" when association.ext is
non-empty (e.g., if !association.ext.is_empty()), leaving the existing mapping
logic (association.ext.iter().map(|ext| ext.to_string().into()).collect())
unchanged so the actual extensions are inserted into dict rather than an empty
array.


if let Some(content_types) = &association.content_types {
dict.insert(
"LSItemContentTypes".into(),
plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()),
);
}

dict.insert(
"CFBundleTypeName".into(),
association
.name
.as_ref()
.unwrap_or(&association.ext[0].0)
.expect("File association must have a name")
.to_string()
.into(),
);
Expand Down
47 changes: 46 additions & 1 deletion crates/tauri-cli/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,7 @@
]
},
"fileAssociations": {
"description": "File associations to application.",
"description": "File types to associate with the application.",
"type": [
"array",
"null"
Expand Down Expand Up @@ -2433,6 +2433,16 @@
"$ref": "#/definitions/AssociationExt"
}
},
"contentTypes": {
"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`].",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"name": {
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
"type": [
Expand Down Expand Up @@ -2471,6 +2481,17 @@
"$ref": "#/definitions/HandlerRank"
}
]
},
"exportedType": {
"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.",
"anyOf": [
{
"$ref": "#/definitions/ExportedFileAssociation"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
Expand Down Expand Up @@ -2552,6 +2573,30 @@
}
]
},
"ExportedFileAssociation": {
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"identifier": {
"description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.",
"type": "string"
},
"conformsTo": {
"description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.json` and `public.database`.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"WindowsConfig": {
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
"type": "object",
Expand Down
47 changes: 46 additions & 1 deletion crates/tauri-schema-generator/schemas/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,7 @@
]
},
"fileAssociations": {
"description": "File associations to application.",
"description": "File types to associate with the application.",
"type": [
"array",
"null"
Expand Down Expand Up @@ -2433,6 +2433,16 @@
"$ref": "#/definitions/AssociationExt"
}
},
"contentTypes": {
"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`].",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"name": {
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
"type": [
Expand Down Expand Up @@ -2471,6 +2481,17 @@
"$ref": "#/definitions/HandlerRank"
}
]
},
"exportedType": {
"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.",
"anyOf": [
{
"$ref": "#/definitions/ExportedFileAssociation"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
Expand Down Expand Up @@ -2552,6 +2573,30 @@
}
]
},
"ExportedFileAssociation": {
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.",
"type": "object",
"required": [
"identifier"
],
"properties": {
"identifier": {
"description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.",
"type": "string"
},
"conformsTo": {
"description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.json` and `public.database`.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"WindowsConfig": {
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
"type": "object",
Expand Down
26 changes: 25 additions & 1 deletion crates/tauri-utils/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,12 @@ impl<'d> serde::Deserialize<'d> for AssociationExt {
pub struct FileAssociation {
/// File extensions to associate with this app. e.g. 'png'
pub ext: Vec<AssociationExt>,
/// Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.
///
/// This allows supporting any file format declared by another application that conforms to this type.
/// Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].
#[serde(alias = "content-types")]
pub content_types: Option<Vec<String>>,
/// The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`
pub name: Option<String>,
/// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.
Expand All @@ -1190,6 +1196,24 @@ pub struct FileAssociation {
/// The ranking of this app among apps that declare themselves as editors or viewers of the given file type. Maps to `LSHandlerRank` on macOS.
#[serde(default)]
pub rank: HandlerRank,
/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
///
/// You should define this if the associated file is a custom file type defined by your application.
pub exported_type: Option<ExportedFileAssociation>,
}

/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ExportedFileAssociation {
/// The unique identifier for the exported type. Maps to `UTTypeIdentifier`.
pub identifier: String,
/// The types that this type conforms to. Maps to `UTTypeConformsTo`.
///
/// Examples are `public.data`, `public.image`, `public.json` and `public.database`.
#[serde(alias = "conforms-to")]
pub conforms_to: Option<Vec<String>>,
}

/// Deep link protocol configuration.
Expand Down Expand Up @@ -1356,7 +1380,7 @@ pub struct BundleConfig {
/// Should be one of the following:
/// 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.
pub category: Option<String>,
/// File associations to application.
/// File types to associate with the application.
pub file_associations: Option<Vec<FileAssociation>>,
/// A short description of your application.
#[serde(alias = "short-description")]
Expand Down
6 changes: 6 additions & 0 deletions examples/file-associations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ This feature is commonly used for functionality such as previewing or editing fi
```
cargo build --features tauri/protocol-asset
```

## Associations

This example creates associations with PNG, JPG, JPEG and GIF files.

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`).
2 changes: 1 addition & 1 deletion examples/file-associations/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ tauri-build = { path = "../../../crates/tauri-build", features = ["codegen"] }
[dependencies]
serde_json = "1"
serde = { version = "1", features = ["derive"] }
tauri = { path = "../../../crates/tauri", features = [] }
tauri = { path = "../../../crates/tauri", features = ["protocol-asset"] }
url = "2"
21 changes: 19 additions & 2 deletions examples/file-associations/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"$schema": "../../../crates/tauri-cli/schema.json",
"$schema": "../../../crates/tauri-cli/config.schema.json",
"identifier": "com.tauri.dev-file-associations-demo",
"build": {
"frontendDist": ["../index.html"]
},
"app": {
"security": {
"csp": "default-src 'self'"
"csp": "default-src 'self'",
"assetProtocol": {
"enable": true
}
}
},
"bundle": {
Expand Down Expand Up @@ -34,6 +37,20 @@
"ext": ["gif"],
"mimeType": "image/gif",
"rank": "Owner"
},
{
"ext": ["taurijson"],
"exportedType": {
"identifier": "com.tauri.dev-file-associations-demo.taurijson",
"conformsTo": ["public.json"]
}
},
{
"ext": ["taurid"],
"exportedType": {
"identifier": "com.tauri.dev-file-associations-demo.tauridata",
"conformsTo": ["public.data"]
}
}
]
}
Expand Down