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
20 changes: 18 additions & 2 deletions Option1/BundleIds.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
// todo Android Studio
// todo Jetbrain IDE's
// todo Microsoft Office Products: Word, Excel, ...
// todo Text Edit
class BundleIds {

static let Xcode = "com.apple.dt.Xcode"
static let IntelliJ = "com.jetbrains.intellij"
static let Finder = "com.apple.finder"
static let Xcode = "com.apple.dt.Xcode"
// JetBrains
static let IntelliJ = "com.jetbrains.intellij"
// Microsoft
static let MicrosoftWord = "com.microsoft.Word"

static func isOpenByShellNoNewWindow(_ bundle: String) -> Bool {
[Finder, MicrosoftWord].contains(bundle)
}

static func isOpenByShellWithNewWindow(_ bundle: String) -> Bool {
[IntelliJ].contains(bundle)
}
}
6 changes: 3 additions & 3 deletions Option1/CachedWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ struct CachedWindow: Hashable {
let axuiElementId: AXUIElementID
let title: String
let appBundle: String
let ideaProject: String?
let shellWithNewWindow: String?

static func addByAxuiElement(
nsRunningApplication: NSRunningApplication,
axuiElement: AXUIElement,
ideaProject: String? = nil,
shellWithNewWindow: String? = nil,
) throws {
if
let pid = try axuiElement.pid(),
Expand All @@ -29,7 +29,7 @@ struct CachedWindow: Hashable {
axuiElementId: axuiElementId,
title: title,
appBundle: bundleIdentifier,
ideaProject: ideaProject ?? oldCachedWindow?.ideaProject,
shellWithNewWindow: shellWithNewWindow ?? oldCachedWindow?.shellWithNewWindow,
)
}
}
Expand Down
29 changes: 20 additions & 9 deletions Option1/HotKeysUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,30 @@ private func handleSpecial(
return false
}

if bindDb.bundle == BundleIds.IntelliJ {
if BundleIds.isOpenByShellNoNewWindow(bindDb.bundle) {
let bundle = bindDb.bundle
let fileManager = FileManager.default
let project = bindDb.substring
if project.first == "/",
fileManager.fileExists(atPath: project) {
let path = bindDb.substring
if isFileExists(path) {
let result = shell("open", "-b", bundle, path)
// No sense to update cachedWindows
return result == 0
}
return false
}

if BundleIds.isOpenByShellWithNewWindow(bindDb.bundle) {
let bundle = bindDb.bundle
let path = bindDb.substring
if isFileExists(path) {

// При вызове NSWorkspace.shared.openApplication() с createsNewApplicationInstance
// macOS начинает анимацию запуска приложения в Dock, хотя по факту открывается еще
// одно окно а не всё приложение. Каждый раз смотреть эти подпрыгивания не охото,
// по этому если уже есть окно с этим ideaProject - то его запуск.
// по этому если уже есть окно с этим путем - то его запуск.
CachedWindow.cleanClosed__slow()
if let cachedProject = cachedWindows.first(where: { $0.value.ideaProject == project }) {
if let cachedProject = cachedWindows.first(
where: { $0.value.appBundle == bundle && $0.value.shellWithNewWindow == path }
) {
try? focusAxuiElement(cachedProject.value.axuiElement)
return true
}
Expand All @@ -123,7 +134,7 @@ private func handleSpecial(
return false
}
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = [project]
configuration.arguments = [path]
configuration.createsNewApplicationInstance = true
NSWorkspace.shared.openApplication(at: url, configuration: configuration, completionHandler: { _, _ in
// Почему-то у объекта приложения из completionHandler .bundleIdentifier всегда nil,
Expand All @@ -138,7 +149,7 @@ private func handleSpecial(
try? CachedWindow.addByAxuiElement(
nsRunningApplication: nsApp,
axuiElement: focused,
ideaProject: project,
shellWithNewWindow: path,
)
}
})
Expand Down
108 changes: 100 additions & 8 deletions Option1/UI/Screens/Workspace/WorkspaceBindView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ struct WorkspaceBindView: View {
@State private var appsUi: [AppUi]
@State private var formUi: FormUi

@State private var isTitleInfoPresented = false

private var selectedAppName: String? {
appsUi.first(where: { $0.bundle == formUi.bundle })?.title
}

@State private var isAnyFilePickerPresented = false
@State private var isAnyFilePickerInfoPresented = false

// Т.к. одновременно данное View отображается 10 раз а в формировании
// списка много внутренней логики нужно давать хотябы 2 секунды.
private let updateAppsUiTimer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
Expand Down Expand Up @@ -56,7 +65,7 @@ struct WorkspaceBindView: View {
if formUi.bundle != nil {

if formUi.bundle == BundleIds.Xcode {
ProjectPickerView(
FileTypeView(
path: formUi.substring,
pickerButtonText: "Select Xcode Project File or Folder",
fileTypes: [.data, .directory],
Expand All @@ -65,18 +74,89 @@ struct WorkspaceBindView: View {
},
)
} else if formUi.bundle == BundleIds.IntelliJ {
ProjectPickerView(
FileTypeView(
path: formUi.substring,
pickerButtonText: "Select IDEA Project Folder",
fileTypes: [.directory],
onPathChanged: { path in
formUi.substring = path
},
)
} else if formUi.bundle == BundleIds.MicrosoftWord {
FileTypeView(
path: formUi.substring,
pickerButtonText: "Select Word Document",
fileTypes: [.data],
onPathChanged: { path in
formUi.substring = path
},
)
} else if isFileExists(formUi.substring) {
FileTypeView(
path: formUi.substring,
pickerButtonText: "---", // Impossible to show because of .isFileExists()
fileTypes: [.data, .directory],
onPathChanged: { path in
formUi.substring = path
},
)
} else {
TextField("Part of title (optional)", text: $formUi.substring)

TextField("Window title (optional)", text: $formUi.substring)
.autocorrectionDisabled()
.frame(width: 200)
.frame(width: 180)

Button(
action: {
isAnyFilePickerInfoPresented = true
},
label: {
Image(systemName: "folder")
.font(.system(size: fontSize, weight: .regular))
.foregroundColor(.secondary)
},
)
.buttonStyle(.borderless)
.padding(.leading, 12)
.confirmationDialog(
"",
isPresented: $isAnyFilePickerInfoPresented,
) {
Button("Select File or Folder") {
isAnyFilePickerPresented = true
}
.keyboardShortcut(.defaultAction)

Button("Cancel", role: .cancel) {
}
} message: {
Text("If the app supports opening files or folders, select the one you want to open.")
}
.fileImporter(
isPresented: $isAnyFilePickerPresented,
allowedContentTypes: [.data, .directory],
onCompletion: { result in
switch result {
case .success(let url):
formUi.substring = url.relativePath
case .failure:
break
}
}
)

Button(
action: {
isTitleInfoPresented = true
},
label: {
Image(systemName: "info.circle")
.font(.system(size: fontSize, weight: .regular))
.foregroundColor(.secondary)
},
)
.buttonStyle(.borderless)
.padding(.leading, 8)
}
} else if let sharedOverride = sharedOverride {
HStack(spacing: 0) {
Expand Down Expand Up @@ -120,10 +200,18 @@ struct WorkspaceBindView: View {
}
}
}
.alert(
"",
isPresented: $isTitleInfoPresented,
actions: {},
message: { Text("If you have multiple \(selectedAppName ?? "app") windows open, enter the window title for window you want to open.\n\nYou can enter part of title as well.") }
)
}
}

private struct ProjectPickerView: View {
private let userRelativePathRegex = /^\/Users\/(.*?)\/\b/

private struct FileTypeView: View {

let path: String
let pickerButtonText: String
Expand All @@ -134,6 +222,10 @@ private struct ProjectPickerView: View {

@State private var isFilePickerPresented = false

private var validatedPath: String {
path.replacing(userRelativePathRegex, with: "~/")
}

var body: some View {
HStack(spacing: 4) {
if path.isEmpty {
Expand All @@ -146,9 +238,9 @@ private struct ProjectPickerView: View {
},
)
} else {
Text(path)
Text(validatedPath)
.padding(.vertical, 8)
.foregroundColor(.green)
.foregroundColor(.blue)
.font(.system(size: fontSize, weight: .regular))
.onTapGesture {
isFilePickerPresented = true
Expand All @@ -160,7 +252,7 @@ private struct ProjectPickerView: View {
},
label: {
Image(systemName: "xmark.circle")
.font(.system(size: fontSize, weight: .medium))
.font(.system(size: fontSize, weight: .regular))
},
)
.buttonStyle(.borderless)
Expand Down
2 changes: 1 addition & 1 deletion Option1/UI/Screens/Workspace/WorkspaceScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct WorkspaceScreen: View {
Divider()
.padding()

Text("Open Window Titles")
Text("Window Titles")
.font(.system(size: 20, weight: .semibold))
.padding(.horizontal)
.textAlign(.leading)
Expand Down