diff --git a/Packages/Sources/RxCodeCore/Utilities/GitURLHelpers.swift b/Packages/Sources/RxCodeCore/Utilities/GitURLHelpers.swift index d6b05378..42229b71 100644 --- a/Packages/Sources/RxCodeCore/Utilities/GitURLHelpers.swift +++ b/Packages/Sources/RxCodeCore/Utilities/GitURLHelpers.swift @@ -16,3 +16,33 @@ public func parseGitHubOwnerRepo(from urlString: String) -> String? { guard parts.count >= 2 else { return nil } return "\(parts[0])/\(parts[1])" } + +public func gitHubWebURL(forOwnerRepo ownerRepo: String) -> URL? { + URL(string: "https://github.com/\(ownerRepo)") +} + +public func detectGitHubOwnerRepo(at path: String) -> String? { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/git") + process.arguments = ["remote", "get-url", "origin"] + process.currentDirectoryURL = URL(fileURLWithPath: path) + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = FileHandle.nullDevice + + do { + try process.run() + process.waitUntilExit() + } catch { + return nil + } + + guard process.terminationStatus == 0 else { return nil } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let urlString = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), + !urlString.isEmpty else { return nil } + + return parseGitHubOwnerRepo(from: urlString) +} diff --git a/Packages/Tests/RxCodeCoreTests/GitURLHelpersTests.swift b/Packages/Tests/RxCodeCoreTests/GitURLHelpersTests.swift index d4b10d15..16db57fc 100644 --- a/Packages/Tests/RxCodeCoreTests/GitURLHelpersTests.swift +++ b/Packages/Tests/RxCodeCoreTests/GitURLHelpersTests.swift @@ -43,4 +43,9 @@ struct GitURLHelpersTests { func oneComponent() { #expect(parseGitHubOwnerRepo(from: "https://github.com/owner") == nil) } + + @Test("Owner repo builds GitHub web URL") + func ownerRepoBuildsWebURL() { + #expect(gitHubWebURL(forOwnerRepo: "owner/repo")?.absoluteString == "https://github.com/owner/repo") + } } diff --git a/RxCode.xcodeproj/project.pbxproj b/RxCode.xcodeproj/project.pbxproj index ecb46a96..a4612c02 100644 --- a/RxCode.xcodeproj/project.pbxproj +++ b/RxCode.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ DCA8CF4A05C959C4A6EB391F /* RxCodeCore in Frameworks */ = {isa = PBXBuildFile; productRef = CCDF9594876099576D4FD46E /* RxCodeCore */; }; DF06CCD12FB4CAB5005991E1 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = DF06CCD02FB4CAB5005991E1 /* ViewInspector */; }; DF06DCC72FB8552B005991E1 /* UnitTestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = DF06DCC62FB8552B005991E1 /* UnitTestPlan.xctestplan */; }; + DF23F7362FB8C3EC008929A6 /* icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = DF23F7352FB8C3EC008929A6 /* icon.icon */; }; DFA0CCD12FB4CC01005991E1 /* PlanDecisionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA0CCC02FB4CC01005991E1 /* PlanDecisionTests.swift */; }; DFA0CCD22FB4CC01005991E1 /* PlanCardViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA0CCC12FB4CC01005991E1 /* PlanCardViewTests.swift */; }; DFA0CCD42FB4CC01005991E1 /* RxCodeChatKit in Frameworks */ = {isa = PBXBuildFile; productRef = DFA0CCC32FB4CC01005991E1 /* RxCodeChatKit */; }; @@ -48,6 +49,7 @@ 7321B5E8B81AAB1A2DC0593B /* RxCodeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RxCodeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A9993BB72A5307039A88B729 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; DF06DCC62FB8552B005991E1 /* UnitTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTestPlan.xctestplan; sourceTree = ""; }; + DF23F7352FB8C3EC008929A6 /* icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = icon.icon; sourceTree = ""; }; DFA0CCC02FB4CC01005991E1 /* PlanDecisionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PlanDecisionTests.swift; sourceTree = ""; }; DFA0CCC12FB4CC01005991E1 /* PlanCardViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PlanCardViewTests.swift; sourceTree = ""; }; DFA0CCD52FB4CC02005991E1 /* HistoryListArchiveFilterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryListArchiveFilterTests.swift; sourceTree = ""; }; @@ -133,6 +135,7 @@ E673352F2F7356F600FD26C7 = { isa = PBXGroup; children = ( + DF23F7352FB8C3EC008929A6 /* icon.icon */, DF06DCC62FB8552B005991E1 /* UnitTestPlan.xctestplan */, 6E17B00D2FC8000100A10001 /* UITestplan.xctestplan */, E673353A2F7356F600FD26C7 /* RxCode */, @@ -288,6 +291,7 @@ buildActionMask = 2147483647; files = ( DF06DCC72FB8552B005991E1 /* UnitTestPlan.xctestplan in Resources */, + DF23F7362FB8C3EC008929A6 /* icon.icon in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -517,7 +521,7 @@ E67335442F7356F700FD26C7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = icon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; @@ -551,7 +555,7 @@ E67335452F7356F700FD26C7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = icon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; diff --git a/RxCode/App/AppState.swift b/RxCode/App/AppState.swift index 03e2d121..b0f3a2a6 100644 --- a/RxCode/App/AppState.swift +++ b/RxCode/App/AppState.swift @@ -3407,36 +3407,10 @@ final class AppState { func addProjectFromFolder(_ url: URL, in window: WindowState) async { let isGitRepo = FileManager.default.fileExists(atPath: url.appendingPathComponent(".git").path) - let gitHubRepo = isGitRepo ? detectGitHubRepo(at: url.path) : nil + let gitHubRepo = isGitRepo ? detectGitHubOwnerRepo(at: url.path) : nil await addAndSelectProject(name: url.lastPathComponent, path: url.path, gitHubRepo: gitHubRepo, in: window) } - private nonisolated func detectGitHubRepo(at path: String) -> String? { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/git") - process.arguments = ["remote", "get-url", "origin"] - process.currentDirectoryURL = URL(fileURLWithPath: path) - - let pipe = Pipe() - process.standardOutput = pipe - process.standardError = FileHandle.nullDevice - - do { - try process.run() - process.waitUntilExit() - } catch { - return nil - } - - guard process.terminationStatus == 0 else { return nil } - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let urlString = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), - !urlString.isEmpty else { return nil } - - return parseGitHubOwnerRepo(from: urlString) - } - private func addAndSelectProject(name: String, path: String, gitHubRepo: String? = nil, in window: WindowState) async { if let existing = projects.first(where: { $0.path == path }) { selectProject(existing, in: window) diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/1024-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/1024-mac.png deleted file mode 100644 index 2acf3465..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/1024-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/128-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/128-mac.png deleted file mode 100644 index 18255de8..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/128-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/16-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/16-mac.png deleted file mode 100644 index f2c1d35c..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/16-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/256-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/256-mac.png deleted file mode 100644 index 4ab52df3..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/256-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/32-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/32-mac.png deleted file mode 100644 index 70ca14d4..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/32-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/512-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/512-mac.png deleted file mode 100644 index 51361521..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/512-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/64-mac.png b/RxCode/Assets.xcassets/AppIcon.appiconset/64-mac.png deleted file mode 100644 index b06c147c..00000000 Binary files a/RxCode/Assets.xcassets/AppIcon.appiconset/64-mac.png and /dev/null differ diff --git a/RxCode/Assets.xcassets/AppIcon.appiconset/Contents.json b/RxCode/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 5177872a..00000000 --- a/RxCode/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "images" : [ - { - "filename" : "1024-mac.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - }, - { - "filename" : "16-mac.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "32-mac.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "32-mac.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "64-mac.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "128-mac.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "256-mac.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "256-mac.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "512-mac.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "512-mac.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "1024-mac.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/RxCode/Views/Toolbar/ExternalEditorMenu.swift b/RxCode/Views/Toolbar/ExternalEditorMenu.swift index bc4323cb..f4764432 100644 --- a/RxCode/Views/Toolbar/ExternalEditorMenu.swift +++ b/RxCode/Views/Toolbar/ExternalEditorMenu.swift @@ -84,6 +84,12 @@ final class ExternalEditorService { NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: path)]) } + func gitHubURL(for project: Project) -> URL? { + let ownerRepo = project.gitHubRepo ?? detectGitHubOwnerRepo(at: project.path) + guard let ownerRepo else { return nil } + return gitHubWebURL(forOwnerRepo: ownerRepo) + } + private func which(_ command: String) -> String? { let candidates = [ "/opt/homebrew/bin/\(command)", @@ -135,6 +141,14 @@ struct ExternalEditorMenu: View { } } Divider() + if let project = windowState.selectedProject, + let gitHubURL = ExternalEditorService.shared.gitHubURL(for: project) { + Button { + NSWorkspace.shared.open(gitHubURL) + } label: { + Label("Open in GitHub", systemImage: "globe") + } + } Button { if let path = windowState.selectedProject?.path { ExternalEditorService.shared.revealInFinder(path) diff --git a/icon.icon/Assets/image.png b/icon.icon/Assets/image.png new file mode 100644 index 00000000..0af94506 Binary files /dev/null and b/icon.icon/Assets/image.png differ diff --git a/icon.icon/icon.json b/icon.icon/icon.json new file mode 100644 index 00000000..4290945a --- /dev/null +++ b/icon.icon/icon.json @@ -0,0 +1,29 @@ +{ + "fill" : { + "solid" : "display-p3:0.00057,0.16509,1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "image.png", + "name" : "image" + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file