Skip to main content

Overview

Default Tamer’s routing engine evaluates rules in a specific order to determine which browser should open a URL. The router is a pure decision engine that takes a URL, optional source application, and your rule configuration, then returns an action.

Routing Flow

When a URL is opened, the router follows this decision flow:
1

Check if app is enabled

If Default Tamer is disabled in settings, immediately route to the fallback browser.
guard settings.enabled else {
    return .openInFallback
}
2

Check modifier keys

If the Option key is held while opening a URL, show the browser chooser dialog instead of evaluating rules.
if let flags = modifierFlags, flags.contains(.option) {
    return .showChooser(url: url)
}
3

Evaluate rules top-to-bottom

Rules are evaluated in the order they appear in your configuration. First match wins - as soon as a rule matches, that browser is used and no further rules are checked.
for (index, rule) in rules.enumerated() where rule.enabled {
    if let action = evaluateRule(rule, url: url, sourceApp: sourceApp) {
        return action  // First match wins!
    }
}
4

Use fallback browser

If no rules match, open the URL in your configured fallback browser.
return .openInFallback

Route Actions

The router returns one of three possible actions:
case openInBrowser(bundleId: String, matchedRule: Rule?)
// Opens URL in the specified browser
// Includes reference to the rule that matched

Rule Evaluation Order

Order matters! Rules are evaluated from top to bottom, and the first matching rule determines the browser. Place more specific rules before general ones.

Example: Correct Rule Order

# ✅ CORRECT: Specific rules first
1. Source App: Slack → Chrome
2. Domain: github.com (exact) → Arc
3. Domain: .atlassian.net (suffix) → Firefox
4. URL Pattern: /admin (contains) → Chrome

Example: Incorrect Rule Order

# ❌ INCORRECT: General rule blocks specific ones
1. Domain: .com (suffix) → Safari
2. Domain: github.com (exact) → Arc      # Never reached!
3. Source App: Slack → Chrome            # Never reached!
In the incorrect example, the first rule matches all .com domains, so rules 2 and 3 never get evaluated.

Rule Evaluation Logic

Each rule type has specific matching logic:
Matches based on the bundle identifier of the app that opened the URL.
// Router.swift:73-92
private static func evaluateSourceAppRule(
    _ rule: Rule, 
    sourceApp: String?
) -> RouteAction? {
    guard let ruleAppBundleId = rule.sourceAppBundleId else {
        return nil
    }
    
    guard let sourceApp = sourceApp else {
        return nil
    }
    
    if sourceApp == ruleAppBundleId {
        return .openInBrowser(
            bundleId: rule.targetBrowserId, 
            matchedRule: rule
        )
    }
    
    return nil
}
Source app matching is an exact match of bundle identifiers (e.g., com.tinyspeck.slackmacgap for Slack).

Routing Examples

Example 1: Source App Rule

Type: Source App
From: Slack (com.tinyspeck.slackmacgap)
To: Chrome

Example 2: Domain Rule with Exact Match

Type: Domain
Pattern: github.com
Match Type: Exact
To: Arc

Example 3: Domain Rule with Suffix Match

Type: Domain
Pattern: .atlassian.net
Match Type: Suffix
To: Firefox

Example 4: URL Pattern with Contains

Type: URL Pattern
Contains: /admin
To: Chrome

Example 5: URL Pattern with Regex

Type: URL Pattern
Regex: /pull/[0-9]+
To: Arc

Debugging Rules

Default Tamer logs detailed routing decisions. Use these logs to understand why a URL was routed to a specific browser:
🔀 Routing URL: https://github.com/user/repo
🔀 Source App: com.tinyspeck.slackmacgap
🔀 Enabled Rules: 5/7
🔀 Evaluating rule #1: Source App
   Source app rule: source=com.tinyspeck.slackmacgap, rule=com.tinyspeck.slackmacgap
   ✅ Source app rule MATCHED!
Enable debug logging in Default Tamer’s settings to see detailed rule evaluation logs in Console.app.

Rule Types

Learn about all available rule types and their matching behavior

Browser Detection

How Default Tamer discovers and manages browsers