Split open-wrapper interception into its own setting

This commit is contained in:
Lawrence Chen 2026-02-22 18:26:07 -08:00
parent 2428ae5dbd
commit 0046b674aa
5 changed files with 94 additions and 13 deletions

View file

@ -169,7 +169,11 @@ fi
# Respect the same settings used for terminal link clicks.
if [[ -n "$settings_domain" ]]; then
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserOpenTerminalLinksInCmuxBrowser 2>/dev/null || true)"
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserInterceptTerminalOpenCommandInCmuxBrowser 2>/dev/null || true)"
if [[ -z "$open_in_cmux" ]]; then
# Backward compatibility for installs that predate the dedicated open-wrapper toggle.
open_in_cmux="$("$DEFAULTS_BIN" read "$settings_domain" browserOpenTerminalLinksInCmuxBrowser 2>/dev/null || true)"
fi
case "${open_in_cmux,,}" in
0|false|no)
system_open "$@"

View file

@ -127,6 +127,9 @@ enum BrowserLinkOpenSettings {
static let openTerminalLinksInCmuxBrowserKey = "browserOpenTerminalLinksInCmuxBrowser"
static let defaultOpenTerminalLinksInCmuxBrowser: Bool = true
static let interceptTerminalOpenCommandInCmuxBrowserKey = "browserInterceptTerminalOpenCommandInCmuxBrowser"
static let defaultInterceptTerminalOpenCommandInCmuxBrowser: Bool = true
static let browserHostWhitelistKey = "browserHostWhitelist"
static let defaultBrowserHostWhitelist: String = ""
@ -137,6 +140,19 @@ enum BrowserLinkOpenSettings {
return defaults.bool(forKey: openTerminalLinksInCmuxBrowserKey)
}
static func interceptTerminalOpenCommandInCmuxBrowser(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: interceptTerminalOpenCommandInCmuxBrowserKey) != nil {
return defaults.bool(forKey: interceptTerminalOpenCommandInCmuxBrowserKey)
}
// Migrate existing behavior for users who only had the link-click toggle.
if defaults.object(forKey: openTerminalLinksInCmuxBrowserKey) != nil {
return defaults.bool(forKey: openTerminalLinksInCmuxBrowserKey)
}
return defaultInterceptTerminalOpenCommandInCmuxBrowser
}
static func hostWhitelist(defaults: UserDefaults = .standard) -> [String] {
let raw = defaults.string(forKey: browserHostWhitelistKey) ?? defaultBrowserHostWhitelist
return raw

View file

@ -2559,6 +2559,8 @@ struct SettingsView: View {
@AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
@AppStorage(BrowserThemeSettings.modeKey) private var browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
@AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
@AppStorage(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey)
private var interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser
@AppStorage(BrowserLinkOpenSettings.browserHostWhitelistKey) private var browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist
@AppStorage(BrowserInsecureHTTPSettings.allowlistKey) private var browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText
@AppStorage(NotificationBadgeSettings.dockBadgeEnabledKey) private var notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled
@ -3023,13 +3025,24 @@ struct SettingsView: View {
.controlSize(.small)
}
if openTerminalLinksInCmuxBrowser {
SettingsCardDivider()
SettingsCardRow(
"Intercept open http(s) in Terminal",
subtitle: "When off, `open https://...` and `open http://...` always use your default browser."
) {
Toggle("", isOn: $interceptTerminalOpenCommandInCmuxBrowser)
.labelsHidden()
.controlSize(.small)
}
if openTerminalLinksInCmuxBrowser || interceptTerminalOpenCommandInCmuxBrowser {
SettingsCardDivider()
VStack(alignment: .leading, spacing: 6) {
SettingsCardRow(
"Hosts to Open in Embedded Browser",
subtitle: "When you click links in terminal output, only these hosts open in cmux. Other hosts open in your default browser. One host or wildcard per line (for example: example.com, *.internal.example). Leave empty to open all links in cmux."
subtitle: "Applies to terminal link clicks and intercepted `open https://...` calls. Only these hosts open in cmux. Others open in your default browser. One host or wildcard per line (for example: example.com, *.internal.example). Leave empty to open all hosts in cmux."
) {
EmptyView()
}
@ -3291,6 +3304,7 @@ struct SettingsView: View {
browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser
interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser
browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist
browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText
browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText

View file

@ -4140,6 +4140,26 @@ final class BrowserLinkOpenSettingsTests: XCTestCase {
defaults.set(true, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey)
XCTAssertTrue(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser(defaults: defaults))
}
func testOpenCommandInterceptionDefaultsToCmuxBrowser() {
XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults))
}
func testOpenCommandInterceptionUsesStoredValue() {
defaults.set(false, forKey: BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey)
XCTAssertFalse(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults))
defaults.set(true, forKey: BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey)
XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults))
}
func testOpenCommandInterceptionFallsBackToLegacyLinkToggleWhenUnset() {
defaults.set(false, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey)
XCTAssertFalse(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults))
defaults.set(true, forKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey)
XCTAssertTrue(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowser(defaults: defaults))
}
}
final class TerminalOpenURLTargetResolutionTests: XCTestCase {

View file

@ -30,7 +30,8 @@ def read_log(path: Path) -> list[str]:
def run_wrapper(
*,
args: list[str],
open_setting: str | None,
intercept_setting: str | None,
legacy_open_setting: str | None = None,
whitelist: str | None,
fail_urls: list[str] | None = None,
) -> tuple[list[str], list[str], int, str]:
@ -63,9 +64,16 @@ if [[ "${1:-}" != "read" ]]; then
fi
key="${3:-}"
case "$key" in
browserInterceptTerminalOpenCommandInCmuxBrowser)
if [[ -v FAKE_DEFAULTS_INTERCEPT_OPEN ]]; then
printf '%s\\n' "$FAKE_DEFAULTS_INTERCEPT_OPEN"
exit 0
fi
exit 1
;;
browserOpenTerminalLinksInCmuxBrowser)
if [[ -v FAKE_DEFAULTS_OPEN ]]; then
printf '%s\\n' "$FAKE_DEFAULTS_OPEN"
if [[ -v FAKE_DEFAULTS_LEGACY_OPEN ]]; then
printf '%s\\n' "$FAKE_DEFAULTS_LEGACY_OPEN"
exit 0
fi
exit 1
@ -110,10 +118,15 @@ exit 0
env["FAKE_OPEN_LOG"] = str(open_log)
env["FAKE_CMUX_LOG"] = str(cmux_log)
if open_setting is None:
env.pop("FAKE_DEFAULTS_OPEN", None)
if intercept_setting is None:
env.pop("FAKE_DEFAULTS_INTERCEPT_OPEN", None)
else:
env["FAKE_DEFAULTS_OPEN"] = open_setting
env["FAKE_DEFAULTS_INTERCEPT_OPEN"] = intercept_setting
if legacy_open_setting is None:
env.pop("FAKE_DEFAULTS_LEGACY_OPEN", None)
else:
env["FAKE_DEFAULTS_LEGACY_OPEN"] = legacy_open_setting
if whitelist is None:
env.pop("FAKE_DEFAULTS_WHITELIST", None)
@ -145,7 +158,7 @@ def test_toggle_disabled_passthrough(failures: list[str]) -> None:
url = "https://example.com"
open_log, cmux_log, code, stderr = run_wrapper(
args=[url],
open_setting="0",
intercept_setting="0",
whitelist="",
)
expect(code == 0, f"toggle off: wrapper exited {code}: {stderr}", failures)
@ -157,7 +170,7 @@ def test_whitelist_miss_passthrough(failures: list[str]) -> None:
url = "https://example.com"
open_log, cmux_log, code, stderr = run_wrapper(
args=[url],
open_setting="1",
intercept_setting="1",
whitelist="localhost\n127.0.0.1",
)
expect(code == 0, f"whitelist miss: wrapper exited {code}: {stderr}", failures)
@ -169,7 +182,7 @@ def test_whitelist_match_routes_to_cmux(failures: list[str]) -> None:
url = "https://api.example.com/path?q=1"
open_log, cmux_log, code, stderr = run_wrapper(
args=[url],
open_setting="1",
intercept_setting="1",
whitelist="*.example.com",
)
expect(code == 0, f"whitelist match: wrapper exited {code}: {stderr}", failures)
@ -183,7 +196,7 @@ def test_partial_failures_only_fallback_failed_urls(failures: list[str]) -> None
external = "https://outside.test"
open_log, cmux_log, code, stderr = run_wrapper(
args=[good, failed, external],
open_setting="1",
intercept_setting="1",
whitelist="*.example.com",
fail_urls=[failed],
)
@ -200,12 +213,26 @@ def test_partial_failures_only_fallback_failed_urls(failures: list[str]) -> None
)
def test_legacy_toggle_fallback_passthrough(failures: list[str]) -> None:
url = "https://example.com"
open_log, cmux_log, code, stderr = run_wrapper(
args=[url],
intercept_setting=None,
legacy_open_setting="0",
whitelist="",
)
expect(code == 0, f"legacy fallback: wrapper exited {code}: {stderr}", failures)
expect(cmux_log == [], f"legacy fallback: cmux should not be called, got {cmux_log}", failures)
expect(open_log == [url], f"legacy fallback: expected system open [{url}], got {open_log}", failures)
def main() -> int:
failures: list[str] = []
test_toggle_disabled_passthrough(failures)
test_whitelist_miss_passthrough(failures)
test_whitelist_match_routes_to_cmux(failures)
test_partial_failures_only_fallback_failed_urls(failures)
test_legacy_toggle_fallback_passthrough(failures)
if failures:
print("open wrapper regression tests failed:")