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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ jobs:
-destination 'generic/platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
build

- name: Test
run: |
xcodebuild -project ActionBar.xcodeproj -scheme ActionBar -configuration Debug \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
test
118 changes: 116 additions & 2 deletions ActionBar.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
A10001000000000000000019 /* UpdaterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000019 /* UpdaterService.swift */; };
A10001000000000000000021 /* GitHubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000021 /* GitHubModels.swift */; };
A10001000000000000000022 /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000022 /* Components.swift */; };
A10001000000000000000023 /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000023 /* SettingsStore.swift */; };
A10001000000000000000030 /* TestRuns.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000030 /* TestRuns.swift */; };
A10001000000000000000036 /* FetchFailureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000036 /* FetchFailureTests.swift */; };
A10001000000000000000031 /* RunStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000031 /* RunStatusTests.swift */; };
A10001000000000000000032 /* WorkflowRunListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000032 /* WorkflowRunListTests.swift */; };
A10001000000000000000033 /* RelativeTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000033 /* RelativeTimeTests.swift */; };
A10001000000000000000034 /* ModelDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000034 /* ModelDecodingTests.swift */; };
A10001000000000000000035 /* SettingsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000035 /* SettingsStoreTests.swift */; };
A10001000000000000000040 /* WorkflowRun.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000003 /* WorkflowRun.swift */; };
A10001000000000000000041 /* GitHubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000021 /* GitHubModels.swift */; };
A10001000000000000000042 /* RelativeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000014 /* RelativeTime.swift */; };
A10001000000000000000043 /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000023 /* SettingsStore.swift */; };
/* End PBXBuildFile section */


Expand Down Expand Up @@ -56,7 +68,16 @@
A10002000000000000000019 /* UpdaterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterService.swift; sourceTree = "<group>"; };
A10002000000000000000021 /* GitHubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubModels.swift; sourceTree = "<group>"; };
A10002000000000000000022 /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = "<group>"; };
A10002000000000000000023 /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = "<group>"; };
A10002000000000000000020 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A10002000000000000000030 /* TestRuns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRuns.swift; sourceTree = "<group>"; };
A10002000000000000000036 /* FetchFailureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFailureTests.swift; sourceTree = "<group>"; };
A10002000000000000000031 /* RunStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunStatusTests.swift; sourceTree = "<group>"; };
A10002000000000000000032 /* WorkflowRunListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowRunListTests.swift; sourceTree = "<group>"; };
A10002000000000000000033 /* RelativeTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeTimeTests.swift; sourceTree = "<group>"; };
A10002000000000000000034 /* ModelDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelDecodingTests.swift; sourceTree = "<group>"; };
A10002000000000000000035 /* SettingsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStoreTests.swift; sourceTree = "<group>"; };
A10003000000000000000002 /* ActionBarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ActionBarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -68,6 +89,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A10004000000000000000002 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
Expand All @@ -77,6 +105,7 @@
A10002000000000000000018 /* Base.xcconfig */,
A10005000000000000000003 /* Sources */,
A10005000000000000000008 /* Resources */,
A10005000000000000000010 /* ActionBarTests */,
A10005000000000000000002 /* Products */,
);
sourceTree = "<group>";
Expand All @@ -85,6 +114,7 @@
isa = PBXGroup;
children = (
A10003000000000000000001 /* ActionBar.app */,
A10003000000000000000002 /* ActionBarTests.xctest */,
);
name = Products;
sourceTree = "<group>";
Expand Down Expand Up @@ -117,6 +147,7 @@
A10002000000000000000003 /* WorkflowRun.swift */,
A10002000000000000000004 /* AppState.swift */,
A10002000000000000000021 /* GitHubModels.swift */,
A10002000000000000000023 /* SettingsStore.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -166,6 +197,20 @@
path = Utilities;
sourceTree = "<group>";
};
A10005000000000000000010 /* ActionBarTests */ = {
isa = PBXGroup;
children = (
A10002000000000000000030 /* TestRuns.swift */,
A10002000000000000000036 /* FetchFailureTests.swift */,
A10002000000000000000031 /* RunStatusTests.swift */,
A10002000000000000000032 /* WorkflowRunListTests.swift */,
A10002000000000000000033 /* RelativeTimeTests.swift */,
A10002000000000000000034 /* ModelDecodingTests.swift */,
A10002000000000000000035 /* SettingsStoreTests.swift */,
);
path = Tests/ActionBarTests;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand All @@ -189,6 +234,22 @@
productReference = A10003000000000000000001 /* ActionBar.app */;
productType = "com.apple.product-type.application";
};
A10006000000000000000002 /* ActionBarTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = A10008000000000000000004 /* Build configuration list for PBXNativeTarget "ActionBarTests" */;
buildPhases = (
A10007000000000000000003 /* Sources */,
A10004000000000000000002 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = ActionBarTests;
productName = ActionBarTests;
productReference = A10003000000000000000002 /* ActionBarTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
Expand Down Expand Up @@ -216,6 +277,7 @@
projectRoot = "";
targets = (
A10006000000000000000001 /* ActionBar */,
A10006000000000000000002 /* ActionBarTests */,
);
};
/* End PBXProject section */
Expand Down Expand Up @@ -256,6 +318,25 @@
A10001000000000000000017 /* SettingsView.swift in Sources */,
A10001000000000000000021 /* GitHubModels.swift in Sources */,
A10001000000000000000022 /* Components.swift in Sources */,
A10001000000000000000023 /* SettingsStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A10007000000000000000003 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A10001000000000000000030 /* TestRuns.swift in Sources */,
A10001000000000000000036 /* FetchFailureTests.swift in Sources */,
A10001000000000000000031 /* RunStatusTests.swift in Sources */,
A10001000000000000000032 /* WorkflowRunListTests.swift in Sources */,
A10001000000000000000033 /* RelativeTimeTests.swift in Sources */,
A10001000000000000000034 /* ModelDecodingTests.swift in Sources */,
A10001000000000000000035 /* SettingsStoreTests.swift in Sources */,
A10001000000000000000040 /* WorkflowRun.swift in Sources */,
A10001000000000000000041 /* GitHubModels.swift in Sources */,
A10001000000000000000042 /* RelativeTime.swift in Sources */,
A10001000000000000000043 /* SettingsStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -389,7 +470,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
Expand Down Expand Up @@ -419,7 +500,31 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
};
name = Release;
};
A1000A000000000000000005 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
GENERATE_INFOPLIST_FILE = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.ptrchm.actionbar.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 6.0;
};
name = Debug;
};
A1000A000000000000000006 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
GENERATE_INFOPLIST_FILE = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.ptrchm.actionbar.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 6.0;
};
name = Release;
};
Expand All @@ -444,6 +549,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A10008000000000000000004 /* Build configuration list for PBXNativeTarget "ActionBarTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A1000A000000000000000005 /* Debug */,
A1000A000000000000000006 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
Expand Down
26 changes: 26 additions & 0 deletions ActionBar.xcodeproj/xcshareddata/xcschemes/ActionBar.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
ReferencedContainer = "container:ActionBar.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A10006000000000000000002"
BuildableName = "ActionBarTests.xctest"
BlueprintName = "ActionBarTests"
ReferencedContainer = "container:ActionBar.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand All @@ -29,6 +43,18 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A10006000000000000000002"
BuildableName = "ActionBarTests.xctest"
BlueprintName = "ActionBarTests"
ReferencedContainer = "container:ActionBar.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SCHEME = $(APP_NAME)
BUILD_DIR = $(shell xcodebuild -project $(PROJECT) -scheme $(SCHEME) -configuration Release -showBuildSettings 2>/dev/null | grep -m1 '^\s*BUILT_PRODUCTS_DIR' | sed 's/.*= //')
APP_BUNDLE = $(BUILD_DIR)/$(APP_NAME).app

.PHONY: build run clean
.PHONY: build run test clean

build:
xcodebuild -project $(PROJECT) -scheme $(SCHEME) -configuration Release build
Expand All @@ -14,6 +14,10 @@ run: build
sleep 0.5
open "$(APP_BUNDLE)"

# The tests don't launch the app, so they never touch the Keychain.
test:
xcodebuild -project $(PROJECT) -scheme $(SCHEME) -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO test

clean:
xcodebuild -project $(PROJECT) -scheme $(SCHEME) clean
rm -rf ~/Library/Developer/Xcode/DerivedData/$(APP_NAME)-*
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Requirements: macOS 14+ and a recent version of Xcode.
2. Copy `Config/Local.xcconfig.example` to `Config/Local.xcconfig` and set your Apple Developer Team ID and a bundle identifier registered to your team.
3. Open `ActionBar.xcodeproj` and run the `ActionBar` scheme, or use `make run`.

Run the unit tests with `make test` or the scheme's Test action. They cover the app's model logic and don't launch the app.

The app authenticates through the official ActionBar GitHub App. If you're shipping your own fork, register your own [GitHub App](https://docs.github.com/en/apps/creating-github-apps) with device flow enabled and **Actions (read)** permission, then update `GitHubApp.clientID` and `GitHubApp.installationURL` in `Sources/ActionBar/Services/OAuthService.swift`. You'll also need your own Sparkle key pair and feed URL in `Resources/Info.plist` (see below).

## Releasing
Expand Down
7 changes: 4 additions & 3 deletions Sources/ActionBar/App/ActionBarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import SwiftUI
@main
struct ActionBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@StateObject private var appState = AppState()
@State private var appState = AppState()
@StateObject private var updater = UpdaterService()

var body: some Scene {
MenuBarExtra {
MenuContentView()
.environmentObject(appState)
.environment(appState)
.environmentObject(updater)
} label: {
MenuBarLabel(appState: appState)
// onAppear can fire more than once; start() only runs the first time.
.onAppear { appState.start() }
}
.menuBarExtraStyle(.window)
}
}

private struct MenuBarLabel: View {
@ObservedObject var appState: AppState
let appState: AppState

var body: some View {
Image(nsImage: MenuBarIcon.image(statusDotColor: statusDotColor))
Expand Down
34 changes: 20 additions & 14 deletions Sources/ActionBar/App/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import AppKit
import UserNotifications

@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
private var windowObserver: NSObjectProtocol?

func applicationDidFinishLaunching(_ notification: Notification) {
UNUserNotificationCenter.current().delegate = self
NotificationService.requestPermission()

// Make the MenuBarExtra panel immune to "Show Desktop" / Exposé
windowObserver = NotificationCenter.default.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: nil,
queue: .main
) { notification in
let behavior: NSWindow.CollectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
guard let panel = notification.object as? NSPanel, !(panel is NSOpenPanel),
!panel.collectionBehavior.contains(behavior) else { return }
panel.collectionBehavior.insert(behavior)
}
NotificationCenter.default.addObserver(
self,
selector: #selector(windowDidBecomeKey(_:)),
name: NSWindow.didBecomeKeyNotification,
object: nil
)
}

// Windows become key on the main thread, so this is called there.
@objc private func windowDidBecomeKey(_ notification: Notification) {
let behavior: NSWindow.CollectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
guard let panel = notification.object as? NSPanel, !(panel is NSOpenPanel),
!panel.collectionBehavior.contains(behavior) else { return }
panel.collectionBehavior.insert(behavior)
}

// UserNotifications doesn't promise to call its delegate on the main thread, so these are
// nonisolated rather than inheriting the app delegate's main actor isolation.

// Show notifications even when the app is in the foreground
func userNotificationCenter(
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
Expand All @@ -31,7 +37,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}

// Open the workflow run in the browser when the notification is clicked
func userNotificationCenter(
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
Expand Down
Loading
Loading