Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions Browserino.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
AA00010B2F0C700100FC6D37 /* ChromeProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010A2F0C700100FC6D37 /* ChromeProfile.swift */; };
AA00010D2F0C700100FC6D37 /* ProfilesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010C2F0C700100FC6D37 /* ProfilesTab.swift */; };
664FDF222CE4AA6B009766AA /* BrowserUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664FDF212CE4AA6B009766AA /* BrowserUtil.swift */; };
669247902CEB48A5006A8C8B /* LocationsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6692478F2CEB48A5006A8C8B /* LocationsTab.swift */; };
9830F0A62C119A3000D69C88 /* BrowserinoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9830F0A52C119A3000D69C88 /* BrowserinoApp.swift */; };
Expand Down Expand Up @@ -40,6 +42,8 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
AA00010A2F0C700100FC6D37 /* ChromeProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromeProfile.swift; sourceTree = "<group>"; };
AA00010C2F0C700100FC6D37 /* ProfilesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesTab.swift; sourceTree = "<group>"; };
664FDF212CE4AA6B009766AA /* BrowserUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserUtil.swift; sourceTree = "<group>"; };
6692478F2CEB48A5006A8C8B /* LocationsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsTab.swift; sourceTree = "<group>"; };
9830F0A22C119A3000D69C88 /* Browserino.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Browserino.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -165,6 +169,7 @@
986AC6262CECDD2500884C40 /* Models */ = {
isa = PBXGroup;
children = (
AA00010A2F0C700100FC6D37 /* ChromeProfile.swift */,
986DC8082CFE42EA00447DC7 /* Rule.swift */,
664FDF212CE4AA6B009766AA /* BrowserUtil.swift */,
);
Expand All @@ -189,6 +194,7 @@
9830F0BA2C11DE8F00D69C88 /* PreferencesView.swift */,
989B17FC2C17400A0039441B /* EditAppForm.swift */,
989B17F22C16FC550039441B /* BrowsersTab.swift */,
AA00010C2F0C700100FC6D37 /* ProfilesTab.swift */,
989B17FA2C1710D70039441B /* AppsTab.swift */,
989B17F62C16FD260039441B /* GeneralTab.swift */,
989B17F82C170C160039441B /* AboutTab.swift */,
Expand Down Expand Up @@ -268,6 +274,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AA00010B2F0C700100FC6D37 /* ChromeProfile.swift in Sources */,
AA00010D2F0C700100FC6D37 /* ProfilesTab.swift in Sources */,
9830F0BB2C11DE8F00D69C88 /* PreferencesView.swift in Sources */,
9830F0A82C119A3000D69C88 /* PromptView.swift in Sources */,
9830F0A62C119A3000D69C88 /* BrowserinoApp.swift in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion Browserino/BrowserinoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
BrowserUtil.openURL(
processedUrls,
app: rule.app,
isIncognito: false
isIncognito: false,
profileDirectory: rule.profileDirectory
)
return
}
Expand Down
8 changes: 1 addition & 7 deletions Browserino/Extensions/View+ScrollEdgeDisabled.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import SwiftUI
extension View {
@ViewBuilder
func scrollEdgeEffectDisabledCompat() -> some View {
if #available(macOS 26.0, *) {
self
.scrollEdgeEffectStyle(.soft, for: .all)
.scrollEdgeEffectDisabled(true, for: .all)
} else {
self
}
self
}
}
23 changes: 17 additions & 6 deletions Browserino/Models/BrowserUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,31 @@ class BrowserUtil {
return filteredUrlsForApplications
}

static func openURL(_ urls: [URL], app: URL, isIncognito: Bool) {
static func openURL(_ urls: [URL], app: URL, isIncognito: Bool, profileDirectory: String? = nil) {
guard let bundle = Bundle(url: app) else {
return
}

let configuration = NSWorkspace.OpenConfiguration()

let isChrome = bundle.bundleIdentifier == ChromeProfileUtil.chromeBundleID

var args: [String] = []

if let profileDirectory, isChrome {
args.append("--profile-directory=\(profileDirectory)")
}

if isIncognito, let privateArg = privateArgs[bundle.bundleIdentifier!] {
args.append(privateArg)
}

if !args.isEmpty {
configuration.createsNewApplicationInstance = true
configuration.arguments = [privateArg] + urls.map(\.absoluteString)
configuration.arguments = args + urls.map(\.absoluteString)
}

NSWorkspace.shared.open(
isIncognito ? [] : urls,
args.isEmpty ? urls : [],
withApplicationAt: app,
configuration: configuration
)
Expand Down
48 changes: 48 additions & 0 deletions Browserino/Models/ChromeProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// ChromeProfile.swift
// Browserino
//

import AppKit
import Foundation

struct ChromeProfile: Codable, Hashable {
var directoryName: String
var displayName: String
var isHidden: Bool = false
}

class ChromeProfileUtil {
static let chromeBundleID = "com.google.Chrome"

static func chromeURL() -> URL? {
NSWorkspace.shared.urlForApplication(withBundleIdentifier: chromeBundleID)
}

static func detectProfiles() -> [ChromeProfile] {
let localStatePath = NSString("~/Library/Application Support/Google/Chrome/Local State")
.expandingTildeInPath

guard let data = FileManager.default.contents(atPath: localStatePath),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let profileInfo = json["profile"] as? [String: Any],
let infoCache = profileInfo["info_cache"] as? [String: Any]
else {
return []
}

return infoCache.compactMap { (dirName, value) in
guard let profileDict = value as? [String: Any],
let name = profileDict["name"] as? String
else {
return nil
}

return ChromeProfile(
directoryName: dirName,
displayName: name
)
}
.sorted { $0.directoryName < $1.directoryName }
}
}
1 change: 1 addition & 0 deletions Browserino/Models/Rule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import Foundation
struct Rule: Hashable, Codable {
var regex: String
var app: URL
var profileDirectory: String? = nil
}
29 changes: 24 additions & 5 deletions Browserino/Views/Preferences/EditRuleForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,19 @@ struct NewRuleForm: View {

struct RuleForm: View {
var rule: Rule?

var onCancel: () -> Void
var onSave: (Rule) -> Void
var onDelete: () -> Void

@AppStorage("chromeProfiles") private var chromeProfiles: [ChromeProfile] = []

@State private var openWithPresented = false

@State private var regex: String = ""
@State private var testUrls: String = "https://github.com/AlexStrNik/Browserino\nhttps://x.com/alexstrnik"
@State private var url: URL?
@State private var profileDirectory: String? = nil

private var compiledRegex: Regex<AnyRegexOutput>? {
return try? Regex(regex).ignoresCase()
Expand Down Expand Up @@ -137,7 +140,21 @@ struct RuleForm: View {
.foregroundStyle(.secondary)
}
}


if let bundle = url.flatMap({ Bundle(url: $0) }),
bundle.bundleIdentifier == ChromeProfileUtil.chromeBundleID,
!chromeProfiles.isEmpty {
LabeledContent("Profile:") {
Picker("", selection: $profileDirectory) {
Text("None").tag(nil as String?)
ForEach(chromeProfiles, id: \.directoryName) { profile in
Text(profile.displayName).tag(profile.directoryName as String?)
}
}
.frame(width: 200)
}
}

Spacer()
.frame(height: 32)

Expand All @@ -158,11 +175,12 @@ struct RuleForm: View {
guard let url else {
return
}

onSave(
Rule(
regex: regex,
app: url
app: url,
profileDirectory: profileDirectory
)
)
}) {
Expand All @@ -177,6 +195,7 @@ struct RuleForm: View {
.onAppear {
regex = rule?.regex ?? ""
url = rule?.app
profileDirectory = rule?.profileDirectory
}
}
}
20 changes: 13 additions & 7 deletions Browserino/Views/Preferences/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,36 @@ struct PreferencesView: View {
Label("Browsers", systemImage: "gear")
}
.tag(1)


ProfilesTab()
.tabItem {
Label("Profiles", systemImage: "gear")
}
.tag(2)

AppsTab()
.tabItem {
Label("Apps", systemImage: "gear")
}
.tag(2)
.tag(3)

RulesTab()
.tabItem {
Label("Rules", systemImage: "gear")
}
.tag(3)
.tag(4)

BrowserSearchLocationsTab()
.tabItem {
Label("Locations", systemImage: "gear")
}
.tag(4)
.tag(5)

AboutTab()
.tabItem {
Label("About", systemImage: "gear")
}
.tag(5)
.tag(6)
}
.frame(minWidth: 700, minHeight: 500)
}
Expand Down
123 changes: 123 additions & 0 deletions Browserino/Views/Preferences/ProfilesTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// ProfilesTab.swift
// Browserino
//

import SwiftUI

struct ProfilesTab: View {
@AppStorage("chromeProfiles") private var chromeProfiles: [ChromeProfile] = []
@AppStorage("chromeProfilesEnabled") private var chromeProfilesEnabled: Bool = true

@State private var hasDetected = false

private var chromeInstalled: Bool {
ChromeProfileUtil.chromeURL() != nil
}

private func detectProfiles() {
let detected = ChromeProfileUtil.detectProfiles()

var merged: [ChromeProfile] = []
for profile in detected {
if let existing = chromeProfiles.first(where: { $0.directoryName == profile.directoryName }) {
merged.append(ChromeProfile(
directoryName: existing.directoryName,
displayName: existing.displayName,
isHidden: existing.isHidden
))
} else {
merged.append(profile)
}
}

chromeProfiles = merged
hasDetected = true
}

private func displayName(at index: Int) -> Binding<String> {
Binding(
get: { chromeProfiles[index].displayName },
set: { chromeProfiles[index].displayName = $0 }
)
}

var body: some View {
VStack(alignment: .leading) {
if !chromeInstalled {
Spacer()
Text("Google Chrome is not installed.")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
Spacer()
} else {
HStack(spacing: 16) {
Toggle(isOn: $chromeProfilesEnabled) {
Text("Show Chrome profiles as separate items in the picker")
.font(.callout)
}

Spacer()

Button(action: detectProfiles) {
Text("Detect Profiles")
}
}
.padding(.horizontal, 20)
.padding(.top, 12)

List {
ForEach(Array(chromeProfiles.enumerated()), id: \.element.directoryName) { index, profile in
HStack {
TextField("Display name", text: displayName(at: index))
.font(.system(size: 14))
.frame(maxWidth: 200)

Spacer()
.frame(width: 16)

Text(profile.directoryName)
.font(.system(size: 12).monospaced())
.foregroundStyle(.secondary)

Spacer()

ShortcutButton(
browserId: "\(ChromeProfileUtil.chromeBundleID)::\(profile.directoryName)"
)

Spacer()
.frame(width: 8)

Button(action: {
chromeProfiles[index].isHidden.toggle()
}) {
Image(
systemName: profile.isHidden
? "eye.slash.fill" : "eye.fill"
)
}
.buttonStyle(.plain)
}
.padding(10)
}
}

Text("Detect Chrome profiles and show them as separate picker items. Assign shortcuts and hide profiles you don't use.")
.font(.subheadline)
.foregroundStyle(.primary.opacity(0.5))
.frame(maxWidth: .infinity)
}
}
.padding(.bottom, 20)
.onAppear {
if !hasDetected && chromeInstalled && chromeProfiles.isEmpty {
detectProfiles()
}
}
}
}

#Preview {
ProfilesTab()
}
Loading