Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,117 @@ struct CMUXCLI {
#endif
}

private static let browserDisabledDefaultsKey = "browserDisabledOverride"
private static let defaultBrowserSettingsDomain = "com.cmuxterm.app"

private static func currentExecutableURL() -> URL? {
var size: UInt32 = 0
_ = _NSGetExecutablePath(nil, &size)
guard size > 0 else {
return Bundle.main.executableURL?.standardizedFileURL
}

var buffer = Array<CChar>(repeating: 0, count: Int(size))
guard _NSGetExecutablePath(&buffer, &size) == 0 else {
return Bundle.main.executableURL?.standardizedFileURL
}
return URL(fileURLWithPath: String(cString: buffer)).standardizedFileURL
}

private static func containingAppBundleIdentifier() -> String? {
guard let executableURL = currentExecutableURL() else { return nil }
var current = executableURL.deletingLastPathComponent().standardizedFileURL
while current.path != "/" {
if current.pathExtension == "app",
let bundle = Bundle(url: current),
let bundleIdentifier = normalizedEnvValue(bundle.bundleIdentifier) {
return bundleIdentifier
}

if current.lastPathComponent == "Contents" {
let appURL = current.deletingLastPathComponent().standardizedFileURL
if appURL.pathExtension == "app",
let bundle = Bundle(url: appURL),
let bundleIdentifier = normalizedEnvValue(bundle.bundleIdentifier) {
return bundleIdentifier
}
}

let parent = current.deletingLastPathComponent().standardizedFileURL
guard parent.path != current.path else { break }
current = parent
}
return nil
}

private static func browserSettingsDomain(environment: [String: String]) -> String {
normalizedEnvValue(environment["CMUX_BUNDLE_ID"])
?? containingAppBundleIdentifier()
?? defaultBrowserSettingsDomain
}

private func runBrowserAvailabilityCommand(
command: String,
commandArgs: [String],
jsonOutput globalJSONOutput: Bool,
environment: [String: String]
) throws {
var effectiveJSONOutput = globalJSONOutput
var args = commandArgs
if let jsonIndex = args.firstIndex(of: "--json") {
effectiveJSONOutput = true
args.remove(at: jsonIndex)
}

let action: String
if command == "browser" {
guard let first = args.first?.lowercased() else {
throw CLIError(message: "browser requires a subcommand")
}
action = first
args = Array(args.dropFirst())
} else {
action = command
}

guard args.isEmpty else {
throw CLIError(message: "Unexpected argument: \(args[0])")
}

let domain = Self.browserSettingsDomain(environment: environment)
let defaults = UserDefaults(suiteName: domain) ?? .standard

switch action {
case "disable", "disable-browser":
defaults.set(true, forKey: Self.browserDisabledDefaultsKey)
defaults.synchronize()
case "enable", "enable-browser":
defaults.set(false, forKey: Self.browserDisabledDefaultsKey)
defaults.synchronize()
case "status", "browser-status":
break
default:
throw CLIError(message: "Unknown browser availability command: \(action)")
}

let disabled = defaults.object(forKey: Self.browserDisabledDefaultsKey) == nil
? false
: defaults.bool(forKey: Self.browserDisabledDefaultsKey)
let payload: [String: Any] = [
"enabled": !disabled,
"disabled": disabled,
"domain": domain,
"key": Self.browserDisabledDefaultsKey
]
if effectiveJSONOutput {
print(jsonString(payload))
} else if action == "status" || action == "browser-status" {
print(disabled ? "disabled" : "enabled")
} else {
print(disabled ? "cmux browser disabled" : "cmux browser enabled")
}
}

func run() throws {
let processEnv = ProcessInfo.processInfo.environment
let envSocketPath: String? = {
Expand Down Expand Up @@ -2084,6 +2195,20 @@ struct CMUXCLI {
return
}

let browserAvailabilityArgs = commandArgs.filter { $0 != "--json" }
if command == "disable-browser" ||
command == "enable-browser" ||
command == "browser-status" ||
(command == "browser" && ["disable", "enable", "status"].contains(browserAvailabilityArgs.first?.lowercased() ?? "")) {
try runBrowserAvailabilityCommand(
command: command,
commandArgs: commandArgs,
jsonOutput: jsonOutput,
environment: processEnv
)
return
}

let client = SocketClient(path: resolvedSocketPath)
if resolvedSocketPath != socketPath {
cliTelemetry.breadcrumb(
Expand Down Expand Up @@ -8344,6 +8469,25 @@ struct CMUXCLI {

Open the Settings window to Keyboard Shortcuts.
"""
case "disable-browser":
return """
Usage: cmux disable-browser [--json]

Disable cmux browser creation and link interception. This overrides
browser settings from settings.json until re-enabled.
"""
case "enable-browser":
return """
Usage: cmux enable-browser [--json]

Re-enable cmux browser creation and link interception.
"""
case "browser-status":
return """
Usage: cmux browser-status [--json]

Print whether cmux browser creation and link interception are enabled.
"""
case "restore-session":
return """
Usage: cmux restore-session
Expand Down Expand Up @@ -9556,6 +9700,7 @@ struct CMUXCLI {
Subcommands:
open|open-split|new [url] [--workspace <id|ref|index>] [--window <id|ref|index>]
open/open-split/new default to $CMUX_WORKSPACE_ID when --workspace is omitted and --window is not set
disable | enable | status
goto|navigate <url> [--snapshot-after]
back|forward|reload [--snapshot-after]
url|get-url
Expand Down Expand Up @@ -17826,6 +17971,7 @@ export default CMUXSessionRestore;
Commands:
welcome
shortcuts
disable-browser | enable-browser | browser-status
restore-session
feedback [--email <email> --body <text> [--image <path> ...]]
themes [list|set|clear]
Expand Down Expand Up @@ -17915,6 +18061,7 @@ export default CMUXSessionRestore;
markdown [open] <path> (open markdown file in formatted viewer panel with live reload)

browser [--surface <id|ref|index> | <surface>] <subcommand> ...
browser disable | enable | status
browser open [url] (create browser split in caller's workspace; if surface supplied, behaves like navigate)
browser open-split [url]
browser goto|navigate <url> [--snapshot-after]
Expand Down
102 changes: 102 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -19681,6 +19681,40 @@
}
}
},
"command.browserAvailability.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Browser"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ブラウザ"
}
}
}
},
"command.disableBrowser.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Disable cmux Browser"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmuxブラウザを無効にする"
}
}
}
},
"command.disableMinimalMode.title": {
"extractionState": "manual",
"localizations": {
Expand Down Expand Up @@ -19710,6 +19744,23 @@
}
}
},
"command.enableBrowser.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Enable cmux Browser"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmuxブラウザを有効にする"
}
}
}
},
"command.installCLI.subtitle": {
"extractionState": "manual",
"localizations": {
Expand Down Expand Up @@ -63024,6 +63075,57 @@
}
}
},
"settings.browser.enabled": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Enable cmux Browser"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmuxブラウザを有効にする"
}
}
}
},
"settings.browser.enabled.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Browser tabs and link interception are disabled. Links open in your default browser."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ブラウザタブとリンクのインターセプトは無効です。リンクはデフォルトブラウザで開きます。"
}
}
}
},
"settings.browser.enabled.subtitleOn": {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Browser tabs, terminal link clicks, and intercepted open commands can use the embedded browser."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ブラウザタブ、ターミナルのリンククリック、インターセプトされたopenコマンドで内蔵ブラウザを使用できます。"
}
}
}
},
"settings.browser.interceptOpen": {
"extractionState": "manual",
"localizations": {
Expand Down
16 changes: 16 additions & 0 deletions Resources/bin/open
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ is_false_setting() {
return 1
}

is_true_setting() {
local normalized
normalized="$(normalize_boolean "$1")"
case "$normalized" in
1|true|yes|on)
return 0
;;
esac
return 1
}

canonicalize_idn_host() {
local value="$1"
[[ -z "$PYTHON3_BIN" ]] && {
Expand Down Expand Up @@ -424,6 +435,11 @@ fi

# Respect the same settings used for terminal link clicks.
if [[ -n "$settings_domain" ]]; then
browser_disabled="$("$DEFAULTS_BIN" read "$settings_domain" browserDisabledOverride 2>/dev/null || true)"
if is_true_setting "$browser_disabled"; then
system_open "$@"
fi
Comment thread
lawrencecchen marked this conversation as resolved.
Resolved

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.
Expand Down
17 changes: 17 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11019,6 +11019,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

@discardableResult
func openBrowserAndFocusAddressBar(url: URL? = nil, insertAtEnd: Bool = false) -> UUID? {
guard BrowserAvailabilitySettings.isEnabled() else {
#if DEBUG
cmuxDebugLog(
"browser.focus.openAndFocus result=blocked_browser_disabled " +
"insertAtEnd=\(insertAtEnd ? 1 : 0) url=\(redactedDebugURL(url))"
)
#endif
return nil
}

let preferredProfileID =
tabManager?.focusedBrowserPanel?.profileID
?? tabManager?.selectedWorkspace?.preferredBrowserProfileID
Expand Down Expand Up @@ -11511,6 +11521,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent

@discardableResult
func performBrowserSplitShortcut(direction: SplitDirection) -> Bool {
guard BrowserAvailabilitySettings.isEnabled() else {
#if DEBUG
cmuxDebugLog("split.browser.shortcut blocked reason=browser_disabled")
#endif
return false
}

_ = synchronizeActiveMainWindowContext(preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow)

#if DEBUG
Expand Down
Loading
Loading