From 152418a3288475e278964163b1154d0bce9e8ae9 Mon Sep 17 00:00:00 2001 From: Tan Date: Fri, 6 Mar 2026 14:45:27 -0500 Subject: [PATCH 1/2] improve: build tooling, CI, and documentation - Add GitHub Actions CI workflow for macOS (tests + app bundle verification) - Use architecture-agnostic build paths via swift build --show-bin-path - Consolidate bundle scripts: create-app-bundle.sh delegates to build-app.sh - Remove redundant run-lucid.sh (use 'make run' instead) - Improve ProcessMonitor: defer pattern for isRefreshing, 2s poll interval - Exclude Info.plist and entitlements from SPM resources - Update README with build/CI improvements (remove 'abandoned' language) - Fix CI: only target existing branches (main, master) Co-authored-by: Agent --- .github/workflows/ci.yml | 27 ++++ Lucid/Lucid/Services/ProcessMonitor.swift | 6 +- Lucid/Makefile | 25 ++-- Lucid/Package.swift | 4 + Lucid/build-app.sh | 153 ++++++---------------- Lucid/create-app-bundle.sh | 26 +--- Lucid/run-lucid.sh | 51 -------- README.md | 36 ++++- 8 files changed, 130 insertions(+), 198 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100755 Lucid/run-lucid.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4831c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: ["main", "master"] + pull_request: + +jobs: + test-and-build: + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Show Swift version + run: swift --version + + - name: Run unit tests + run: | + cd Lucid + swift test + + - name: Verify app bundle script + run: | + cd Lucid + ./build-app.sh debug diff --git a/Lucid/Lucid/Services/ProcessMonitor.swift b/Lucid/Lucid/Services/ProcessMonitor.swift index b471553..2b06219 100644 --- a/Lucid/Lucid/Services/ProcessMonitor.swift +++ b/Lucid/Lucid/Services/ProcessMonitor.swift @@ -31,7 +31,7 @@ final class ProcessMonitor { private var previousCPUTimes: [pid_t: UInt64] = [:] private var previousCPUHistory: [Double] = [] private var previousMemoryHistory: [Double] = [] - private let pollInterval: TimeInterval = 3.0 + private let pollInterval: TimeInterval = 2.0 private let logger = Logger(subsystem: "com.tan.lucid", category: "ProcessMonitor") private let timerQueue = DispatchQueue(label: "com.tan.lucid.timer", qos: .userInitiated) @@ -87,6 +87,9 @@ final class ProcessMonitor { guard let self else { return } guard !self.isRefreshing else { return } self.isRefreshing = true + defer { + self.isRefreshing = false + } // Refresh NSWorkspace app names every other cycle to reduce MainActor blocking self.shouldRefreshAppNames.toggle() @@ -215,7 +218,6 @@ final class ProcessMonitor { self.filterCounts = counts self.activePorts = ports self.updateSystemStats() - self.isRefreshing = false } } } diff --git a/Lucid/Makefile b/Lucid/Makefile index de44e8f..49edf00 100644 --- a/Lucid/Makefile +++ b/Lucid/Makefile @@ -1,19 +1,26 @@ -.PHONY: all build clean app run +.PHONY: all build test app run clean install + +CONFIG ?= debug all: app build: - swift build + swift build --configuration $(CONFIG) -app: build - ./build-app.sh +test: + swift test -clean: - swift package clean - rm -rf .build +app: build + ./build-app.sh $(CONFIG) run: app - open .build/arm64-apple-macosx/debug/Lucid.app + @BIN_PATH=$$(swift build --configuration $(CONFIG) --show-bin-path); \ + open "$$BIN_PATH/Lucid.app" install: app - cp -R .build/arm64-apple-macosx/debug/Lucid.app /Applications/ + BIN_PATH="$$(swift build --configuration $(CONFIG) --show-bin-path)" && \ + cp -R "$$BIN_PATH/Lucid.app" /Applications/ + +clean: + swift package clean + rm -rf .build diff --git a/Lucid/Package.swift b/Lucid/Package.swift index 7be3c45..df2e547 100644 --- a/Lucid/Package.swift +++ b/Lucid/Package.swift @@ -18,6 +18,10 @@ let package = Package( name: "Lucid", dependencies: [], path: "Lucid", + exclude: [ + "Info.plist", + "Lucid.entitlements" + ], resources: [ .copy("Assets.xcassets") ] diff --git a/Lucid/build-app.sh b/Lucid/build-app.sh index 38dcf25..2e871f2 100755 --- a/Lucid/build-app.sh +++ b/Lucid/build-app.sh @@ -1,125 +1,56 @@ -#!/bin/bash -# Build script to create a proper macOS app bundle with icon +#!/usr/bin/env bash +set -euo pipefail -set -e - -# Configuration APP_NAME="Lucid" -BUILD_DIR=".build/arm64-apple-macosx/debug" -APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" +CONFIGURATION="${1:-debug}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Error: $APP_NAME.app bundles can only be built on macOS." >&2 + exit 1 +fi + +if [[ "$CONFIGURATION" != "debug" && "$CONFIGURATION" != "release" ]]; then + echo "Usage: $0 [debug|release]" >&2 + exit 1 +fi + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT_DIR" + +echo "Building $APP_NAME ($CONFIGURATION)..." +swift build --configuration "$CONFIGURATION" + +BIN_PATH="$(swift build --configuration "$CONFIGURATION" --show-bin-path)" +EXECUTABLE="$BIN_PATH/$APP_NAME" +APP_BUNDLE="$BIN_PATH/$APP_NAME.app" CONTENTS="$APP_BUNDLE/Contents" MACOS="$CONTENTS/MacOS" RESOURCES="$CONTENTS/Resources" -EXECUTABLE="$BUILD_DIR/$APP_NAME" +ICONSET_SOURCE="Lucid/Assets.xcassets/AppIcon.appiconset" +ICONSET="$RESOURCES/AppIcon.iconset" -echo "Building $APP_NAME.app bundle..." +if [[ ! -f "$EXECUTABLE" ]]; then + echo "Error: compiled executable not found at $EXECUTABLE" >&2 + exit 1 +fi -# Clean previous build +echo "Creating app bundle at: $APP_BUNDLE" rm -rf "$APP_BUNDLE" +mkdir -p "$MACOS" "$RESOURCES" -# Create app bundle structure -mkdir -p "$MACOS" -mkdir -p "$RESOURCES" - -# Copy executable -cp "$EXECUTABLE" "$MACOS/" - -# Copy Info.plist +cp "$EXECUTABLE" "$MACOS/$APP_NAME" +chmod +x "$MACOS/$APP_NAME" cp "Lucid/Info.plist" "$CONTENTS/Info.plist" -# Copy Assets.xcassets to Resources -cp -R "Lucid/Assets.xcassets" "$RESOURCES/" - -# Create icon set from Assets.xcassets for macOS to recognize -ICONSET="$RESOURCES/AppIcon.iconset" -mkdir -p "$ICONSET" - -# Copy icon files from Assets.xcassets to a location macOS recognizes -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_16x16.png "$ICONSET/icon_16x16.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png "$ICONSET/icon_16x16@2x.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_32x32.png "$ICONSET/icon_32x32.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png "$ICONSET/icon_32x32@2x.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_128x128.png "$ICONSET/icon_128x128.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_256x256.png "$ICONSET/icon_256x256.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_256x256.png "$ICONSET/icon_128x128@2x.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_512x512.png "$ICONSET/icon_256x256@2x.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_512x512.png "$ICONSET/icon_512x512.png" -cp Lucid/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png "$ICONSET/icon_512x512@2x.png" +if [[ -d "$ICONSET_SOURCE" ]]; then + cp -R "$ICONSET_SOURCE" "$ICONSET" -# Create iconset Contents.json -cat > "$ICONSET/Contents.json" << 'EOF' -{ - "images" : [ - { - "filename" : "icon_16x16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "icon_16x16@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "icon_32x32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "icon_32x32@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "icon_128x128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "icon_256x256.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "icon_256x256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "icon_512x512.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "icon_512x512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "icon_1024x1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF + if command -v iconutil >/dev/null 2>&1; then + iconutil -c icns "$ICONSET" -o "$RESOURCES/AppIcon.icns" + fi -# Update Info.plist to reference the icon -/usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string AppIcon" "$CONTENTS/Info.plist" 2>/dev/null || true + /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile AppIcon" "$CONTENTS/Info.plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string AppIcon" "$CONTENTS/Info.plist" +fi -echo "✓ $APP_NAME.app bundle created successfully" -echo " Location: $APP_BUNDLE" +echo "✅ Created $APP_BUNDLE" diff --git a/Lucid/create-app-bundle.sh b/Lucid/create-app-bundle.sh index 2cf40ac..5a798a0 100755 --- a/Lucid/create-app-bundle.sh +++ b/Lucid/create-app-bundle.sh @@ -1,24 +1,4 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail -APP_NAME="Lucid" -BUILD_DIR=".build/arm64-apple-macosx/debug" -BUNDLE_DIR="$BUILD_DIR/$APP_NAME.app" - -# Clean old bundle -rm -rf "$BUNDLE_DIR" - -# Create app bundle structure -mkdir -p "$BUNDLE_DIR/Contents/MacOS" -mkdir -p "$BUNDLE_DIR/Contents/Resources" - -# Copy executable -cp "$BUILD_DIR/$APP_NAME" "$BUNDLE_DIR/Contents/MacOS/$APP_NAME" - -# Copy Info.plist -cp "$APP_NAME/Info.plist" "$BUNDLE_DIR/Contents/Info.plist" - -# Set executable permissions -chmod +x "$BUNDLE_DIR/Contents/MacOS/$APP_NAME" - -echo "App bundle created at: $BUNDLE_DIR" +"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/build-app.sh" "${1:-debug}" diff --git a/Lucid/run-lucid.sh b/Lucid/run-lucid.sh deleted file mode 100755 index cae25bc..0000000 --- a/Lucid/run-lucid.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -e - -echo "Building Lucid..." -swift build - -APP_NAME="Lucid" -BUILD_DIR=".build/arm64-apple-macosx/debug" -BUNDLE_DIR="$BUILD_DIR/$APP_NAME.app" - -echo "Creating app bundle..." -rm -rf "$BUNDLE_DIR" -mkdir -p "$BUNDLE_DIR/Contents/MacOS" -mkdir -p "$BUNDLE_DIR/Contents/Resources" - -cp "$BUILD_DIR/$APP_NAME" "$BUNDLE_DIR/Contents/MacOS/$APP_NAME" -chmod +x "$BUNDLE_DIR/Contents/MacOS/$APP_NAME" - -cat > "$BUNDLE_DIR/Contents/Info.plist" << 'PLIST' - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - Lucid - CFBundleIdentifier - com.tan.lucid - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Lucid - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSMinimumSystemVersion - 14.0 - NSHumanReadableCopyright - Copyright © 2026. All rights reserved. - NSPrincipalClass - NSApplication - - -PLIST - -echo "Launching Lucid..." -open "$BUNDLE_DIR" diff --git a/README.md b/README.md index 717de10..84e60e2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ A plain-English activity monitor for macOS built with native SwiftUI. Lucid tran ![Lucid Screenshot](Resources/app-screenshot.png) +## Build & CI Improvements + +This repository has been updated with improved build tooling and continuous integration: + +- Build tooling is now architecture-agnostic (`swift build --show-bin-path`) supporting both Intel and Apple Silicon Macs. +- Legacy duplicate bundle scripts consolidated into one maintained workflow. +- A GitHub Actions workflow now verifies tests and app-bundle creation on macOS for every push/PR. + +### Development Branches + +The primary development branch is `master`. Additional feature branches may exist for specific improvements. + ## Features - **Plain-English descriptions** — 250+ macOS processes mapped to readable names and explanations @@ -33,9 +45,20 @@ A plain-English activity monitor for macOS built with native SwiftUI. Lucid tran ## Getting Started -1. Open `Lucid/Lucid.xcodeproj` in Xcode +### Xcode + +1. Open `Lucid/Package.swift` in Xcode 2. Build and run (⌘R) +### Command line (macOS) + +```bash +cd Lucid +make app # builds executable + Lucid.app bundle +make run # builds and launches Lucid.app +make test # runs unit tests +``` + Lucid disables App Sandbox to access process information. Development builds sign automatically; distribution requires a Developer ID certificate. ## Architecture @@ -68,7 +91,7 @@ Lucid/ **Key architectural patterns:** - **State Management**: `@Observable` ProcessMonitor as single source of truth, injected via `@Environment` -- **Timer Loop**: ProcessMonitor polls every 3 seconds, coordinating all services +- **Timer Loop**: ProcessMonitor polls every 2 seconds, coordinating all services - **Data Flow**: Darwin APIs → ProcessMonitor → @Observable state → SwiftUI views - **Service Integration**: PortScanner (lsof), LLMService (actor), ProcessDictionary (250+ mappings) @@ -112,6 +135,15 @@ Lucid/ 3. Notarize with Apple 4. Distribute as DMG or ZIP +## Continuous Integration + +GitHub Actions runs on macOS for each push and pull request: + +1. `swift test` +2. `./build-app.sh debug` + +Workflow file: `.github/workflows/ci.yml`. + ## License MIT From 833d00ac3f3b93f68d13ded528134d4c2f45ba7b Mon Sep 17 00:00:00 2001 From: Tan Date: Fri, 6 Mar 2026 17:59:25 -0500 Subject: [PATCH 2/2] fix: replace count(where:) with filter for Swift 5.10 compatibility The count(where:) method was introduced in Swift 6.0, but CI runners on macOS-14 use Swift 5.10. Replace with filter { }.count pattern which works on both Swift 5.10 and Swift 6.x. --- Lucid/Lucid/Services/ProcessMonitor.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lucid/Lucid/Services/ProcessMonitor.swift b/Lucid/Lucid/Services/ProcessMonitor.swift index 2b06219..797e50f 100644 --- a/Lucid/Lucid/Services/ProcessMonitor.swift +++ b/Lucid/Lucid/Services/ProcessMonitor.swift @@ -205,9 +205,9 @@ final class ProcessMonitor { let finalCPUTimes = currentCPUTimes let counts = FilterCounts( total: newProcesses.count, - system: newProcesses.count(where: { $0.safety == .system }), - user: newProcesses.count(where: { $0.safety == .user }), - unknown: newProcesses.count(where: { $0.safety == .unknown }) + system: newProcesses.filter { $0.safety == .system }.count, + user: newProcesses.filter { $0.safety == .user }.count, + unknown: newProcesses.filter { $0.safety == .unknown }.count ) let ports = Array(Set(newProcesses.flatMap(\.ports))).sorted()