How to Automatically Open Downloaded Files Using Tauri: A Complete Guide

Use Tauri's WebviewBuilder::on_download callback to capture the DownloadEvent::Finished event, then invoke app_handle.shell().open() from the Shell plugin to launch the file with the system's default application.

The lencx/ChatGPT desktop application demonstrates this pattern to provide a seamless native experience when users download files from the embedded webview. By handling download events in the Tauri backend, you can automatically open downloaded files using Tauri without requiring manual user navigation to the downloads folder.

Understanding Tauri's Download Event Lifecycle

Tauri's webview emits a sequence of events during the download process. To automatically open downloaded files, you must intercept these events and persist the target file path until the download completes.

Capturing Download Requests with WebviewBuilder

The WebviewBuilder::on_download method registers a closure that receives every DownloadEvent. When a DownloadEvent::Requested occurs, you should resolve the user's download directory and store the destination path.

In the ChatGPT application, this logic resides in src-tauri/src/core/setup.rs:

// src-tauri/src/core/setup.rs
.on_download({
    let app_handle = app_handle.clone();
    let download_path = Arc::new(Mutex::new(PathBuf::new()));
    
    move |_webview, event| {
        match event {
            DownloadEvent::Requested { destination, .. } => {
                let download_dir = app_handle
                    .path()
                    .download_dir()
                    .expect("Failed to get download dir");
                let mut locked = download_path.lock().expect("Lock failed");
                *locked = download_dir.join(&destination);
                *destination = locked.clone();
            }
            // ...
        }
        true
    }
})

Handling the DownloadEvent::Finished Callback

When the DownloadEvent::Finished event fires with success: true, you retrieve the stored path and invoke the Shell plugin's open method. This is the critical step that triggers the operating system to launch the file with its associated default application.

From src-tauri/src/core/setup.rs:81-85:

// src-tauri/src/core/setup.rs:81‑85
if success {
    app_handle
        .shell()
        .open(final_path.to_string_lossy(), None)
        .expect("[view:download] Failed to open file");
}

Implementing Automatic File Opening in ChatGPT Desktop

The ChatGPT desktop application (lencx/ChatGPT) implements this pattern to handle downloads from the ChatGPT web interface. The implementation requires the Tauri Shell plugin and careful management of the download state.

The Setup Logic in src-tauri/src/core/setup.rs

The core implementation resides in the setup function within src-tauri/src/core/setup.rs. This file initializes the main webview and configures the download handler:

  • Lines 56-90: Register the on_download callback during webview construction
  • Path resolution: Uses app_handle.path().download_dir() to determine the user's default downloads folder
  • State management: Uses Arc<Mutex<PathBuf>> to share the download path between the request and finish events

Using the Shell Plugin to Open Files

The Shell plugin (tauri_plugin_shell) provides the ShellExt trait, which adds the shell() method to AppHandle. This method returns a Shell instance with an open method that accepts:

  • Path: The file path as a string (using to_string_lossy() for cross-platform compatibility)
  • OpenOptions: Optional configuration (pass None for default behavior)

This abstraction works across macOS, Windows, and Linux, automatically invoking the appropriate system command (open on macOS, start on Windows, xdg-open on Linux).

Complete Code Example for Tauri Auto-Open Downloads

Here is the complete implementation pattern based on the ChatGPT desktop application source code:

use tauri::{
    webview::{DownloadEvent, WebviewBuilder, WebviewUrl},
    Manager, AppHandle,
};
use tauri_plugin_shell::ShellExt;
use std::sync::{Arc, Mutex};
use std::path::PathBuf;

fn setup_webview(app_handle: AppHandle) {
    // Shared state to store the download path between events
    let download_path = Arc::new(Mutex::new(PathBuf::new()));
    
    let main_view = WebviewBuilder::new("main", WebviewUrl::App("https://example.com".into()))
        .auto_resize()
        .on_download({
            let app_handle = app_handle.clone();
            let download_path = download_path.clone();
            
            move |_webview, event| {
                match event {
                    DownloadEvent::Requested { destination, .. } => {
                        // Resolve the user's download directory
                        let download_dir = app_handle
                            .path()
                            .download_dir()
                            .expect("Failed to get download dir");
                        
                        // Store the full path for later use
                        let mut locked = download_path.lock().expect("Lock failed");
                        *locked = download_dir.join(&destination);
                        *destination = locked.clone();
                    }
                    DownloadEvent::Finished { success, .. } => {
                        if success {
                            // Retrieve the stored path
                            let path = {
                                let lock = download_path.lock().expect("Lock failed");
                                lock.clone()
                            };
                            
                            // Open the file with the default application
                            app_handle
                                .shell()
                                .open(path.to_string_lossy(), None)
                                .expect("Failed to open the downloaded file");
                        }
                    }
                    _ => {}
                }
                true // Keep the download handler active
            }
        })
        .build()
        .expect("Failed to build webview");
}

Cross-Platform Considerations

The Shell plugin abstracts platform differences, but you should consider these implementation details:

  • Path handling: Use to_string_lossy() on PathBuf when passing to shell().open() to handle non-UTF8 paths gracefully on Windows.
  • Download directory: Always resolve app_handle.path().download_dir() rather than hardcoding paths, as this respects the user's system preferences on all platforms.
  • Error handling: The expect calls in the example should be replaced with proper error logging in production applications to prevent panics.

Summary

  • Tauri's on_download callback captures download lifecycle events from the embedded webview.
  • DownloadEvent::Requested allows you to intercept the destination path and redirect it to the user's download directory.
  • DownloadEvent::Finished signals completion, providing the final file path and success status.
  • The Shell plugin's open method (app_handle.shell().open()) launches the file with the system's default application.
  • This pattern is implemented in src-tauri/src/core/setup.rs within the lencx/ChatGPT repository.

Frequently Asked Questions

How does Tauri detect when a file download completes?

Tauri emits a DownloadEvent::Finished event through the WebviewBuilder::on_download callback. This event includes a success boolean and the final file path. In the ChatGPT desktop app, this is handled in src-tauri/src/core/setup.rs to trigger the file opening logic.

What Tauri plugin is required to open files with default applications?

You need the Tauri Shell plugin (tauri_plugin_shell). It provides the ShellExt trait which adds the shell() method to AppHandle, allowing you to call open(path, None) to launch files with their associated default programs on macOS, Windows, and Linux.

Can I customize which application opens the downloaded file?

Yes. While the example uses None for default application behavior, the open method accepts an OpenOptions parameter where you can specify a custom program. However, for cross-platform compatibility, relying on the system's default association via None is generally preferred unless you have specific application requirements.

Is this automatic file opening behavior available on all platforms?

Yes. The implementation works on macOS, Windows, and Linux because the Shell plugin abstracts the platform-specific commands (open on macOS, start on Windows, xdg-open on Linux). The code in src-tauri/src/core/setup.rs uses standard Tauri APIs that are fully cross-platform.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →