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:
Check if app is enabled
If Default Tamer is disabled in settings, immediately route to the fallback browser. guard settings.enabled else {
return . openInFallback
}
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)
}
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!
}
}
Use fallback browser
If no rules match, open the URL in your configured fallback browser.
Route Actions
The router returns one of three possible actions:
Open in Browser
Show Chooser
Open in Fallback
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:
Source App
Domain
URL Pattern
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).
Matches based on the URL’s host/domain with three match types: exact, suffix, or contains. // Router.swift:95-136
private static func evaluateDomainRule (
_ rule : Rule,
url : URL
) -> RouteAction ? {
guard let host = url.host ? . lowercased () else {
return nil
}
guard let pattern = rule.domainPattern ? . lowercased () else {
return nil
}
let matches: Bool
switch matchType {
case . exact :
// Normalizes www. prefix for cleaner UX
let normalizedHost = host. hasPrefix ( "www." )
? String (host. dropFirst ( 4 ))
: host
let normalizedPattern = pattern. hasPrefix ( "www." )
? String (pattern. dropFirst ( 4 ))
: pattern
matches = normalizedHost == normalizedPattern
case . suffix :
// Pattern like ".atlassian.net" or "atlassian.net"
let suffixPattern = pattern. hasPrefix ( "." )
? pattern
: "." + pattern
matches = host. hasSuffix (suffixPattern) || host == pattern
case . contains :
matches = host. contains (pattern)
}
if matches {
return . openInBrowser (
bundleId : rule. targetBrowserId ,
matchedRule : rule
)
}
return nil
}
Matches based on the full URL string using either substring contains or regex. // Router.swift:139-163
private static func evaluateURLPatternRule (
_ rule : Rule,
url : URL
) -> RouteAction ? {
let urlString = url. absoluteString . lowercased ()
// Contains matching
if let contains = rule.urlContains ? . lowercased () {
if urlString. contains (contains) {
return . openInBrowser (
bundleId : rule. targetBrowserId ,
matchedRule : rule
)
}
}
// Regex matching
if let regexPattern = rule.urlRegex {
do {
let regex = try NSRegularExpression (
pattern : regexPattern,
options : [. caseInsensitive ]
)
let range = NSRange (urlString. startIndex ... , in : urlString)
if regex. firstMatch ( in : urlString, options : [], range : range) != nil {
return . openInBrowser (
bundleId : rule. targetBrowserId ,
matchedRule : rule
)
}
} catch {
// Invalid regex pattern - rule doesn't match
}
}
return nil
}
URL pattern matching is case-insensitive and evaluates against the full URL string (including protocol and query parameters).
Routing Examples
Example 1: Source App Rule
Rule Configuration
Evaluation
Type : Source App
From : Slack (com.tinyspeck.slackmacgap)
To : Chrome
Example 2: Domain Rule with Exact Match
Rule Configuration
Evaluation
Type : Domain
Pattern : github.com
Match Type : Exact
To : Arc
Example 3: Domain Rule with Suffix Match
Rule Configuration
Evaluation
Type : Domain
Pattern : .atlassian.net
Match Type : Suffix
To : Firefox
Example 4: URL Pattern with Contains
Rule Configuration
Evaluation
Type : URL Pattern
Contains : /admin
To : Chrome
Example 5: URL Pattern with Regex
Rule Configuration
Evaluation
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