Skip to content

feat(instance): register .mrpack file association for system-level op…#1621

Open
zaixiZaixiSJTU wants to merge 1 commit into
UNIkeEN:mainfrom
zaixiZaixiSJTU:feat/issue-1618-mrpack-file-association
Open

feat(instance): register .mrpack file association for system-level op…#1621
zaixiZaixiSJTU wants to merge 1 commit into
UNIkeEN:mainfrom
zaixiZaixiSJTU:feat/issue-1618-mrpack-file-association

Conversation

@zaixiZaixiSJTU
Copy link
Copy Markdown
Collaborator

@zaixiZaixiSJTU zaixiZaixiSJTU commented May 17, 2026

…en with

Add OS-level file handler for .mrpack (Modrinth modpack) files so that double-clicking a .mrpack file in the file manager launches SJMCL and opens the import-modpack modal.

  • Add bundle.fileAssociations in tauri.conf.json for .mrpack
  • Add UTExportedTypeDeclarations in Info.plist for macOS custom UTI
  • Handle cold start via std::env::args() (Windows/Linux) and RunEvent::Opened (macOS)
  • Handle warm start via single-instance plugin callback (Windows/Linux)
  • Add check_pending_modpack_import Tauri command for cold-start race condition
  • Add sjmcl://import-modpack deep link handler in GlobalEventHandler
  • Add Tauri event bridge (sjmcl://import) for Rust-to-frontend communication

Checklist

  • Changes have been tested locally (windows) and work as expected.

This PR is a ..

  • 🆕 New feature

Related Issues

Summary by Sourcery

Add OS-level .mrpack file association handling so opening Modrinth modpack files launches the app and opens the import-modpack flow across supported platforms.

New Features:

  • Support importing Modrinth .mrpack files via OS-level file associations and custom deep links, opening the import-modpack modal automatically.

Enhancements:

  • Wire Tauri backend events and a new check_pending_modpack_import command to notify the frontend about pending modpack imports on both cold and warm starts.
  • Register macOS document type and custom UTI for .mrpack files so the app can be set as the default handler.

…en with

Add OS-level file handler for .mrpack (Modrinth modpack) files so that
double-clicking a .mrpack file in the file manager launches SJMCL and
opens the import-modpack modal.

- Add bundle.fileAssociations in tauri.conf.json for .mrpack
- Add UTExportedTypeDeclarations in Info.plist for macOS custom UTI
- Handle cold start via std::env::args() (Windows/Linux) and RunEvent::Opened (macOS)
- Handle warm start via single-instance plugin callback (Windows/Linux)
- Add check_pending_modpack_import Tauri command for cold-start race condition
- Add sjmcl://import-modpack deep link handler in GlobalEventHandler
- Add Tauri event bridge (sjmcl://import) for Rust-to-frontend communication
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 17, 2026

Reviewer's Guide

Adds OS-level .mrpack (Modrinth modpack) file association support across platforms and implements the backend/frontend plumbing to open the import-modpack modal when a .mrpack file is opened, handling both cold and warm starts.

Sequence diagram for .mrpack file association handling (cold and warm starts)

sequenceDiagram
  actor User
  participant OS
  participant TauriBackend
  participant GlobalEventHandler

  User->>OS: Open .mrpack file
  OS->>TauriBackend: Launch SJMCL with .mrpack arg

  alt [cold start]
    TauriBackend->>TauriBackend: std::env::args
    TauriBackend->>TauriBackend: PENDING_MODPACK_IMPORT.lock
    TauriBackend->>TauriBackend: store sjmcl://import-modpack?path=...

    GlobalEventHandler->>TauriBackend: invoke check_pending_modpack_import
    TauriBackend-->>GlobalEventHandler: sjmcl://import-modpack?path=...
    GlobalEventHandler->>GlobalEventHandler: openModpackImportFromUrl
    GlobalEventHandler->>GlobalEventHandler: openSharedModal import-modpack
  else [warm start]
    TauriBackend->>TauriBackend: tauri_plugin_single_instance callback
    TauriBackend->>TauriBackend: emit sjmcl://import
    TauriBackend-->>GlobalEventHandler: sjmcl://import event
    GlobalEventHandler->>GlobalEventHandler: openModpackImportFromUrl
    GlobalEventHandler->>GlobalEventHandler: openSharedModal import-modpack
  end

  opt [macOS RunEvent::Opened]
    OS->>TauriBackend: RunEvent::Opened urls
    TauriBackend->>TauriBackend: update PENDING_MODPACK_IMPORT
    TauriBackend->>TauriBackend: emit sjmcl://import
    TauriBackend-->>GlobalEventHandler: sjmcl://import event
    GlobalEventHandler->>GlobalEventHandler: openModpackImportFromUrl
    GlobalEventHandler->>GlobalEventHandler: openSharedModal import-modpack
  end
Loading

File-Level Changes

Change Details Files
Frontend deep-link and event handling to open the import-modpack modal when triggered from backend or URL schemes.
  • Adds a deep-link trigger for the import-modpack path and handler that extracts the file path from the sjmcl://import-modpack URL and opens the import-modpack modal.
  • Introduces a reusable helper to open the import-modpack modal from a deep-link URL by parsing the path query parameter and deferring modal opening slightly with setTimeout.
  • Listens for sjmcl://import events from the Rust backend on warm starts and routes payload URLs to the import-modpack opener.
  • On app load, invokes a new Tauri command to check for any pending modpack import URL from a cold start and opens the import-modpack modal if present.
src/components/special/global-event-handler.tsx
Backend support for .mrpack file association handling, including cold/warm start flows and a Tauri command bridge.
  • Introduces a global mutex-backed PENDING_MODPACK_IMPORT store for pending deep-link URLs to be consumed by the frontend.
  • Extends the single-instance plugin callback to scan args for .mrpack files on warm start, encode the path into an sjmcl://import-modpack deep link, and emit it to the frontend via the sjmcl://import event.
  • In setup, inspects std::env::args() on cold start for .mrpack arguments, encodes the path into an sjmcl://import-modpack deep link, and stores it in PENDING_MODPACK_IMPORT for later retrieval.
  • Registers a new Tauri command check_pending_modpack_import that returns and clears any stored pending deep-link URL.
  • Updates the Tauri run loop to handle macOS RunEvent::Opened, converting opened .mrpack file URLs into deep-link strings, storing them in PENDING_MODPACK_IMPORT, and emitting sjmcl://import events.
  • Adjusts the run loop signature to accept app_handle and match on multiple RunEvent variants while preserving the existing Exit handling logic.
src-tauri/src/lib.rs
src-tauri/src/utils/commands.rs
macOS metadata to declare .mrpack as a handled document type and exported UTI.
  • Declares CFBundleDocumentTypes entry for "Modrinth Modpack" with default handler rank and binds it to the custom UTI com.modrinth.mrpack.
  • Adds UTExportedTypeDeclarations for com.modrinth.mrpack, specifying the mrpack filename extension, application/zip MIME type, and conformance to public.zip-archive.
src-tauri/Info.plist
Platform-level file association for .mrpack via Tauri configuration.
  • Configures bundle.fileAssociations in Tauri config (details not shown in diff) to associate the .mrpack extension with the app so opening a .mrpack file launches the launcher.
src-tauri/tauri.conf.json

Assessment against linked issues

Issue Objective Addressed Explanation
#1618 Register SJMCL as a system-level file handler for .mrpack (Modrinth modpack) files so that they can be opened via the OS (e.g., double-click or "Open with" on Windows/macOS/Linux).
#1618 When a .mrpack file is opened via file association or deep link / startup parameters, launch SJMCL (cold or warm start) and automatically open the import-modpack modal with the correct file path.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label May 17, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The frontend has two nearly identical code paths for opening the import-modpack modal (importModpackByDeeplink and openModpackImportFromUrl); consider consolidating them into a single helper to avoid divergence and make future changes easier.
  • The deep-link construction logic for .mrpack files in Rust (single-instance callback, cold-start setup, and macOS RunEvent::Opened) is duplicated; extracting a small helper that takes a path and returns/dispatches the sjmcl://import-modpack?path=... URL would reduce repetition and keep behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The frontend has two nearly identical code paths for opening the import-modpack modal (`importModpackByDeeplink` and `openModpackImportFromUrl`); consider consolidating them into a single helper to avoid divergence and make future changes easier.
- The deep-link construction logic for `.mrpack` files in Rust (single-instance callback, cold-start `setup`, and macOS `RunEvent::Opened`) is duplicated; extracting a small helper that takes a path and returns/dispatches the `sjmcl://import-modpack?path=...` URL would reduce repetition and keep behavior consistent.

## Individual Comments

### Comment 1
<location path="src-tauri/src/lib.rs" line_range="68-69" />
<code_context>
         let _ = main_window.set_focus();
+
+        // Handle .mrpack file association on warm re-launch
+        for arg in &args {
+          if arg.ends_with(".mrpack") {
+            let encoded = urlencoding::encode(arg);
+            let deep_link = format!("sjmcl://import-modpack?path={}", encoded);
</code_context>
<issue_to_address>
**suggestion:** Using `ends_with(".mrpack")` directly may miss valid files on case-insensitive platforms or with trailing spaces/quotes.

Consider normalizing each argument before checking: trim surrounding quotes/whitespace, parse as a `Path`, and compare the extension case-insensitively. That way variants like `PACK.MRPACK` or arguments with extra spaces/quotes are still correctly detected.

Suggested implementation:

```rust
        // Handle .mrpack file association on warm re-launch
        for arg in &args {
          // Normalize argument: trim whitespace and surrounding quotes
          let normalized = arg
            .trim()
            .trim_matches(|c| c == '"' || c == '\'');

          // Check extension case-insensitively using Path
          let has_mrpack_ext = std::path::Path::new(normalized)
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.eq_ignore_ascii_case("mrpack"))
            .unwrap_or(false);

          if has_mrpack_ext {
            let encoded = urlencoding::encode(normalized);
            let deep_link = format!("sjmcl://import-modpack?path={}", encoded);
            let _ = app.emit("sjmcl://import", deep_link);
          }
        }

```

If `std::path::Path` is not already in scope at the top of this file, you can optionally add:
- `use std::path::Path;`
and then replace `std::path::Path::new(...)` with `Path::new(...)` for readability.
</issue_to_address>

### Comment 2
<location path="src-tauri/src/lib.rs" line_range="365-373" />
<code_context>
-        if let tauri::RunEvent::Exit = event {
+      .run_return(|app_handle, event| match event {
+        #[cfg(target_os = "macos")]
+        tauri::RunEvent::Opened { urls } => {
+          for url in urls {
+            if let Ok(path) = url.to_file_path() {
+              if path.extension().map_or(false, |ext| ext == "mrpack") {
+                if let Some(path_str) = path.to_str() {
+                  let encoded = urlencoding::encode(path_str);
+                  let deep_link = format!("sjmcl://import-modpack?path={}", encoded);
+                  *PENDING_MODPACK_IMPORT.lock().unwrap() = Some(deep_link.clone());
+                  let _ = app_handle.emit("sjmcl://import", deep_link);
+                }
+              }
</code_context>
<issue_to_address>
**question (bug_risk):** Potential for the same modpack import to be processed twice on macOS (pending state + emitted event).

In `Opened`, you both set `PENDING_MODPACK_IMPORT` and emit `"sjmcl://import"`. On cold start the frontend also calls `check_pending_modpack_import`, so the same file can be handled via both the event and the pending check. If double-processing is an issue, make one path authoritative (e.g., only use pending state on cold start and skip the emit then, or have the renderer ignore the event if it already consumed a pending import).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src-tauri/src/lib.rs
Comment on lines +68 to +69
for arg in &args {
if arg.ends_with(".mrpack") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Using ends_with(".mrpack") directly may miss valid files on case-insensitive platforms or with trailing spaces/quotes.

Consider normalizing each argument before checking: trim surrounding quotes/whitespace, parse as a Path, and compare the extension case-insensitively. That way variants like PACK.MRPACK or arguments with extra spaces/quotes are still correctly detected.

Suggested implementation:

        // Handle .mrpack file association on warm re-launch
        for arg in &args {
          // Normalize argument: trim whitespace and surrounding quotes
          let normalized = arg
            .trim()
            .trim_matches(|c| c == '"' || c == '\'');

          // Check extension case-insensitively using Path
          let has_mrpack_ext = std::path::Path::new(normalized)
            .extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.eq_ignore_ascii_case("mrpack"))
            .unwrap_or(false);

          if has_mrpack_ext {
            let encoded = urlencoding::encode(normalized);
            let deep_link = format!("sjmcl://import-modpack?path={}", encoded);
            let _ = app.emit("sjmcl://import", deep_link);
          }
        }

If std::path::Path is not already in scope at the top of this file, you can optionally add:

  • use std::path::Path;
    and then replace std::path::Path::new(...) with Path::new(...) for readability.

Comment thread src-tauri/src/lib.rs
Comment on lines +365 to +373
tauri::RunEvent::Opened { urls } => {
for url in urls {
if let Ok(path) = url.to_file_path() {
if path.extension().map_or(false, |ext| ext == "mrpack") {
if let Some(path_str) = path.to_str() {
let encoded = urlencoding::encode(path_str);
let deep_link = format!("sjmcl://import-modpack?path={}", encoded);
*PENDING_MODPACK_IMPORT.lock().unwrap() = Some(deep_link.clone());
let _ = app_handle.emit("sjmcl://import", deep_link);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (bug_risk): Potential for the same modpack import to be processed twice on macOS (pending state + emitted event).

In Opened, you both set PENDING_MODPACK_IMPORT and emit "sjmcl://import". On cold start the frontend also calls check_pending_modpack_import, so the same file can be handled via both the event and the pending check. If double-processing is an issue, make one path authoritative (e.g., only use pending state on cold start and skip the emit then, or have the renderer ignore the event if it already consumed a pending import).

Copy link
Copy Markdown
Owner

@UNIkeEN UNIkeEN left a comment

Choose a reason for hiding this comment

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

如果 pending... command 是防止前一个请求未结束、弹出新的 shared import modal 覆盖,我觉得可以简化。覆盖行为和现在的 single instance 设计是一致的,只需要 sjmcl://import-modpack 一个 deeplink 即可;

如果是为了别的用处,请评论 ovo


#[tauri::command]
pub fn check_pending_modpack_import() -> SJMCLResult<Option<String>> {
let pending = crate::PENDING_MODPACK_IMPORT.lock().unwrap().take();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

我好像没看懂这个函数是干啥的喵

let unlisten: (() => void) | undefined;
(async () => {
unlisten = await listen<string>("sjmcl://import", (event) => {
openModpackImportFromUrl(event.payload);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这里手搓的 sjmcl://import 和上面的 importModpackTrigger 都是 deeplink,有什么区别呢

Comment thread src-tauri/src/lib.rs
if let tauri::RunEvent::Exit = event {
.run_return(|app_handle, event| match event {
#[cfg(target_os = "macos")]
tauri::RunEvent::Opened { urls } => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

这一段是为了照顾 macOS 的什么神秘事件呢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 支持将 SJMCL 注册为如 mrpack 格式的打开方式

2 participants