Open links in a specific Chromium profile - #65
Open
matijazezelj wants to merge 8 commits into
Open
matijazezelj wants to merge 8 commits into
matijazezelj wants to merge 8 commits into
Conversation
Bundle(url:) returns nil once an app is moved, renamed or uninstalled, so force-unwrapping it in RuleItem.body trapped the moment the Rules tab laid out its List — making a stale rule impossible to reach and delete. Rows for unresolved apps now render in a degraded state instead of being force-unwrapped or hidden, so they can still be edited or removed. Apply the same treatment to CFBundleName and bundleIdentifier, which are both optional in practice, and to the equivalent sites in the Apps tab, Browsers tab and the picker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
Unify MACOSX_DEPLOYMENT_TARGET on 14.0 — it read 14.4 at project level but 13.0 at target level, so the effective minimum was silently 13.0 — and move the target to Swift 6 language mode. With 13.0 gone the two availability shims are dead weight: call onKeyPress and focusEffectDisabled directly and delete View+OnKeyPress.swift and View+FocusEffectDisabled.swift. Adopt the two-parameter onChange. Replace two overrides declared in extensions of ObjC classes, which is undefined behaviour in Swift and picks a winner arbitrarily when more than one exists. canBecomeKey belongs on BrowserinoWindow, the only panel that needs it, rather than on every NSPanel including the open and save panels. The NSTableView background hack becomes scrollContentBackground(.hidden) on the four Lists it was there to serve. AppDelegate is @mainactor; observeValue stays nonisolated because defaults written by another process arrive off the main thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
The symbol survives in SwiftUI.tbd but is absent from the swiftinterface of both the macOS 26.5 and 27.0 SDKs, so the call no longer compiles against any current SDK. scrollEdgeEffectHidden(_:for:) carries the same @available(macOS 26.0, *) and slots straight into the existing guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
Errors and warnings that only surface in a full compile, not in -typecheck. BrowserUtil's static @AppStorage properties are nonisolated global mutable state; the type is only ever touched from the main actor, so isolate it there. SettingsDocument holds [String: Any] while FileDocument requires Sendable. Serialize on the way in and carry Data instead — the exporter wrote the same pretty-printed JSON either way. setDefaultApplication's completion handler is a Sendable closure and cannot touch @State or call a main-actor method, so hop before updating isDefault. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
A browser was identified only by its .app bundle URL, so one with several profiles could appear just once and a rule could never say which to use. Introduce BrowserTarget — an app plus an optional Chromium profile directory — and thread it through discovery, the picker, rules and site mappings. A browser with two or more profiles is now represented by its profiles rather than by itself, since opening it plainly would be an extra row that does nothing distinct; single-profile browsers are unchanged. Profiles are read from the browser's own user data directory, located via CrProductDirName in its Info.plist and falling back to a small table of known bundle identifiers. Anything that yields neither is not a Chromium browser, so Safari and Firefox are excluded without special-casing and unlisted forks and beta channels work on their own. Notable details: Migration. browsers and hiddenBrowsers were arrays of bare URL strings, and Array+RawRepresentable turns any decode failure into an empty default with nothing shown, so BrowserTarget decodes both shapes. Shortcuts keep the bare bundle identifier when there is no profile, so existing ones still resolve, and the private argument stays keyed by bundle identifier because incognito belongs to the browser rather than one profile. The old ordering comparator returned true when neither entry had a known position, which is not a strict weak ordering and left sort() undefined. Replaced with a stable merge that keeps the arrangement, puts profiles where the plain row used to sit, and is idempotent — which is what makes it safe for the Browsers tab to refresh on appear rather than only on rescan. An unrecognised --profile-directory makes Chromium create an empty profile, so a stale one is dropped and the browser launched normally instead. Fixes two existing defects met on the way: a link was silently discarded when incognito was requested without a private argument configured, and the keyboard cursor could address rows that render nothing because a bundle no longer resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
macOS refuses to let one app read another's data under Application Support, so Local State comes back as "Operation not permitted" and no profiles are found. The refusal is invisible from a terminal-launched build, which inherits the terminal's own permissions — it only appears once the app runs under its own identity. Add a per-browser grant: an "Enable profiles" button opens a folder picker at the browser's user data directory, and the security-scoped bookmark from that choice carries the permission across launches. Reads are wrapped in that scope, including the avatar images, which live in the same protected directory. The button appears only for a Chromium browser whose profiles could not be read, so it disappears once granted and never shows for Safari or Firefox. Without a grant the read is still attempted, since a browser that does not protect its data, or an app holding wider access, works unaided. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
Rules and site mappings opened a Finder dialog over every application on the machine, which is an odd way to answer "which browser", and it forced a second control beside it once profiles existed. Both forms now offer the browsers already configured under Browsers, profiles included, so choosing "Chrome — Work" sets the app and the profile at once and the separate profile dropdown is gone. Anything outside that list is still reachable through "Other Application", which keeps the old behaviour for apps that are not registered browsers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
hasAccess was written alongside requestAccess but never called — the Browsers tab decides from whether the profile list came back, not from whether a bookmark exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWQa3xpkwVRtrtVh8VkQQo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets a link open in a specific Chromium profile, both from the picker and from a rule.
Today a browser is identified only by its
.appbundle URL —browsers: [URL],Rule.app: URL,App.app: URL— so a browser with several profiles can only appear once, and a rule can say "open in Chrome" but never which Chrome.What it looks like
A browser with two or more profiles is represented by its profiles instead of by itself:
Opening the plain row would just be a fourth row that does nothing distinct, so it is replaced rather than kept alongside. Single-profile browsers are untouched, and the rows carry the profile's picture as a badge on the browser icon so they stay distinguishable at a glance.
Rules and site mappings now pick from that same list rather than opening a file dialog over every application on the machine, which also means a profile needs no second control. "Other Application…" is still there for apps that are not registered browsers.
Discovery
Profiles come from the browser's own user data directory, located by reading
CrProductDirNamefrom itsInfo.plistand falling back to a small table of known bundle identifiers. Brave ships that key, stable Chrome does not.Anything that yields neither is simply not a Chromium browser — so Safari and Firefox are excluded with no special-casing, and unlisted forks and beta channels work without being added here. Discovery is driven by installed apps rather than by the presence of a data directory, because those outlive uninstallation and would otherwise invent browsers that are not there.
The part worth knowing about: macOS blocks the read
macOS refuses to let one app read another's data under
~/Library/Application Support.Local Statecomes back asOperation not permitted, andcontentsOfDirectoryon the profile folder fails too.This is invisible from a terminal-launched build, which inherits the terminal's permissions — it only appears once the app runs under its own identity. Two traps I walked into:
FileManager.isReadableFilereturnstruewhile the read still fails. It stats rather than opens.fileExistson a profile subdirectory also returnstrue. Only opening is refused.So there is a per-browser grant: an "Enable profiles" button opens a folder picker at the browser's data directory, and the security-scoped bookmark from that choice carries the permission across launches. Reads run inside that scope, avatars included. The button shows only for a Chromium browser whose profiles could not be read, so it disappears once granted and never appears for Safari or Firefox. Without a grant the read is still attempted, since a browser that does not protect its data works unaided.
If you would rather not have a permission prompt at all, the fallback is probing
fileExistsforDefault/Profile N, which works with no permission but yields directory names instead of the names the user chose. Happy to switch if you prefer that.Migration
browsersandhiddenBrowserswere arrays of bare URL strings, andArray+RawRepresentableturns any decode failure into an empty default with nothing shown — so a naive type change would silently wipe the user's browser order and hidden set.BrowserTargettherefore decodes both the old bare-string shape and the new one, and I verified that against a real installed configuration before changing anything.Shortcuts keep the bare bundle identifier when there is no profile, so existing ones still resolve. The private argument stays keyed by bundle identifier and lives on the plain row, because incognito belongs to the browser rather than to one profile.
One asymmetry worth documenting:
rulesandappsstay readable by older builds, since the newprofilekey is omitted when nil.browsersdoes not — an older build fails to decode the object form and empties the list, requiring a rescan.Fixes met on the way
truewhen neither entry had a known position, soa < bandb < awere both true. That is not a strict weak ordering and leftsort()undefined. Replaced with a stable merge that keeps the user's arrangement, puts profiles where the plain row used to sit, and is idempotent — which is what makes it safe for the Browsers tab to refresh on appear instead of only on an explicit rescan.ifwas skipped yet the URLs were still replaced with[].--profile-directorymakes Chromium create a fresh empty profile, so a stale one is dropped and the browser launched normally rather than littering its profile list.Verification
Compiles with 0 errors and 0 warnings in Swift 6 against the macOS 26.5 SDK targeting macOS 14.0. Beyond that:
--profile-directorywas confirmed to reach an already-running browser through Chromium's process singleton — the whole feature rests on that, so it was tested before anything was written.Disclosure
Written with AI assistance (Claude Code), same as #63 and #64. Everything above was compiled and exercised in a running build before submitting. This is a large and fairly opinionated change, so treat it as a proposal — I am happy to cut it down, split it, or change any of the choices called out above.