From dabf62e3c725500467d17a09c3d0372962ea3bee Mon Sep 17 00:00:00 2001 From: Alexandre Lemieux Date: Wed, 22 Jul 2026 20:07:40 -0400 Subject: [PATCH 1/5] Buttons improvements. --- .gitignore | 4 ++++ Makefile | 4 ++-- Sources/Views/MenuBarView.swift | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3563a1e..e83713c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ project.xcworkspace # Swift Package Manager .build/ +build/ Packages/ Package.pins Package.resolved @@ -43,6 +44,9 @@ fastlane/test_output # Code Injection iOSInjectionProject/ +# VS Code +.vscode/ + # macOS .DS_Store .AppleDouble diff --git a/Makefile b/Makefile index 554af62..5a40347 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ build: # Build and run run: build - .build/debug/MacRunner + .build/debug/mac-runner # Clean build artifacts clean: @@ -22,7 +22,7 @@ app: release @echo "Creating app bundle..." @mkdir -p build/MacRunner.app/Contents/MacOS @mkdir -p build/MacRunner.app/Contents/Resources - @cp .build/release/MacRunner build/MacRunner.app/Contents/MacOS/ + @cp .build/apple/Products/Release/mac-runner build/MacRunner.app/Contents/MacOS/MacRunner @./scripts/generate-info-plist.sh > build/MacRunner.app/Contents/Info.plist @echo "App bundle created at build/MacRunner.app" diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index ff6f740..f4f1f72 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -356,7 +356,11 @@ struct RunnerRow: View { } }) { Image(systemName: runner.status == .running ? "stop.fill" : "play.fill") - .foregroundColor(runner.status == .running ? .red : .green) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.primary) + .frame(width: 26, height: 26) + .background(Circle().fill(Color.primary.opacity(0.08))) + .overlay(Circle().strokeBorder(Color.primary.opacity(0.2), lineWidth: 1)) } .buttonStyle(.plain) .help(runner.status == .running ? "Stop" : "Start") From 7cfbe593d12cd6156bc62b7b5d67a783a55ac42a Mon Sep 17 00:00:00 2001 From: Alexandre Lemieux Date: Wed, 22 Jul 2026 20:16:51 -0400 Subject: [PATCH 2/5] No breaks on labels. --- Sources/Views/MenuBarView.swift | 92 +++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index f4f1f72..23d71d9 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -290,48 +290,60 @@ struct RunnerRow: View { .help("Open current GitHub Actions run") } - HStack(spacing: 4) { - // Isolation mode indicator - if let mode = runner.isolationMode { - Text("\(mode.icon) \(mode.displayName)") - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.purple.opacity(0.2)) - .cornerRadius(4) - } else { - Text("🌐 Global") - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + // Isolation mode indicator + if let mode = runner.isolationMode { + Text("\(mode.icon) \(mode.displayName)") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.purple.opacity(0.2)) + .cornerRadius(4) + } else { + Text("🌐 Global") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } - // GUI access indicator - if runner.enableGUI { - Text("🖥️ GUI") - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.green.opacity(0.2)) - .cornerRadius(4) - } else { - Text("⚫ Headless") - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } + // GUI access indicator + if runner.enableGUI { + Text("🖥️ GUI") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.green.opacity(0.2)) + .cornerRadius(4) + } else { + Text("⚫ Headless") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } - ForEach(runner.labels, id: \.self) { label in - Text(label) - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.blue.opacity(0.2)) - .cornerRadius(4) + ForEach(runner.labels, id: \.self) { label in + Text(label) + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.blue.opacity(0.2)) + .cornerRadius(4) + } } } From 861a8516110e47088b262ff1b512577afcd3d2ed Mon Sep 17 00:00:00 2001 From: Alexandre Lemieux Date: Wed, 22 Jul 2026 20:56:24 -0400 Subject: [PATCH 3/5] Missing label. --- Sources/Views/MenuBarView.swift | 150 ++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 54 deletions(-) diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index 23d71d9..40e494b 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -290,60 +290,58 @@ struct RunnerRow: View { .help("Open current GitHub Actions run") } - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 4) { - // Isolation mode indicator - if let mode = runner.isolationMode { - Text("\(mode.icon) \(mode.displayName)") - .font(.caption2) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.purple.opacity(0.2)) - .cornerRadius(4) - } else { - Text("🌐 Global") - .font(.caption2) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } + FlowLayout(spacing: 4) { + // Isolation mode indicator + if let mode = runner.isolationMode { + Text("\(mode.icon) \(mode.displayName)") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.purple.opacity(0.2)) + .cornerRadius(4) + } else { + Text("🌐 Global") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } - // GUI access indicator - if runner.enableGUI { - Text("🖥️ GUI") - .font(.caption2) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.green.opacity(0.2)) - .cornerRadius(4) - } else { - Text("⚫ Headless") - .font(.caption2) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } + // GUI access indicator + if runner.enableGUI { + Text("🖥️ GUI") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.green.opacity(0.2)) + .cornerRadius(4) + } else { + Text("⚫ Headless") + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } - ForEach(runner.labels, id: \.self) { label in - Text(label) - .font(.caption2) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.blue.opacity(0.2)) - .cornerRadius(4) - } + ForEach(runner.labels, id: \.self) { label in + Text(label) + .font(.caption2) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.blue.opacity(0.2)) + .cornerRadius(4) } } @@ -354,8 +352,7 @@ struct RunnerRow: View { .lineLimit(2) } } - - Spacer() + .frame(maxWidth: .infinity, alignment: .leading) // Inline start/stop button Button(action: { @@ -668,3 +665,48 @@ struct SettingsView: View { MenuBarView() .environmentObject(RunnerManager()) } + +/// Lays out subviews left-to-right, wrapping onto additional lines when a row would exceed the available width. +struct FlowLayout: Layout { + var spacing: CGFloat = 4 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxWidth = proposal.width ?? .infinity + var origin = CGPoint.zero + var rowHeight: CGFloat = 0 + var totalHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if origin.x > 0, origin.x + size.width > maxWidth { + origin.x = 0 + origin.y += rowHeight + spacing + totalHeight += rowHeight + spacing + rowHeight = 0 + } + origin.x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + totalHeight += rowHeight + + return CGSize(width: maxWidth.isFinite ? maxWidth : origin.x, height: totalHeight) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let maxWidth = bounds.width + var origin = CGPoint(x: bounds.minX, y: bounds.minY) + var rowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if origin.x > bounds.minX, origin.x - bounds.minX + size.width > maxWidth { + origin.x = bounds.minX + origin.y += rowHeight + spacing + rowHeight = 0 + } + subview.place(at: origin, proposal: .unspecified) + origin.x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + } +} From 8de76ecd10575445519d0a0158885ba05339f25a Mon Sep 17 00:00:00 2001 From: Alexandre Lemieux Date: Wed, 22 Jul 2026 21:01:10 -0400 Subject: [PATCH 4/5] Collapse folders. --- Sources/Views/MenuBarView.swift | 62 ++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index 40e494b..f948591 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -3,6 +3,7 @@ import SwiftUI struct MenuBarView: View { @EnvironmentObject var runnerManager: RunnerManager @State private var showAddRunner = false + @State private var collapsedTargets: Set = [] var body: some View { VStack(spacing: 0) { @@ -83,32 +84,53 @@ struct MenuBarView: View { ScrollView { VStack(alignment: .leading, spacing: 4) { ForEach(groupedRunners, id: \.target) { group in + let isCollapsed = collapsedTargets.contains(group.target) + // Section header — org-level groups use a building icon to distinguish - // them from repo-level groups at a glance. - HStack(spacing: 4) { - Image(systemName: group.target.scope == .org ? "building.2" : "folder") - .font(.caption2) - .foregroundColor(.secondary) - Text(group.target.displayName) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - Spacer() - Text("\(group.runners.count)") - .font(.caption2) - .foregroundColor(.secondary) - .padding(.horizontal, 6) - .padding(.vertical, 1) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) + // them from repo-level groups at a glance. Tapping toggles the group. + Button(action: { + withAnimation(.easeInOut(duration: 0.15)) { + if isCollapsed { + collapsedTargets.remove(group.target) + } else { + collapsedTargets.insert(group.target) + } + } + }) { + HStack(spacing: 4) { + Image(systemName: "chevron.right") + .font(.caption2) + .foregroundColor(.secondary) + .rotationEffect(.degrees(isCollapsed ? 0 : 90)) + + Image(systemName: group.target.scope == .org ? "building.2" : "folder") + .font(.caption2) + .foregroundColor(.secondary) + Text(group.target.displayName) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + Spacer() + Text("\(group.runners.count)") + .font(.caption2) + .foregroundColor(.secondary) + .padding(.horizontal, 6) + .padding(.vertical, 1) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } + .contentShape(Rectangle()) } + .buttonStyle(.plain) .padding(.horizontal, 12) .padding(.top, 8) .padding(.bottom, 2) - ForEach(group.runners) { runner in - RunnerRow(runner: runner) - .environmentObject(runnerManager) + if !isCollapsed { + ForEach(group.runners) { runner in + RunnerRow(runner: runner) + .environmentObject(runnerManager) + } } } } From 9ff0c187197b322abd0dce75af1a220943c35c0d Mon Sep 17 00:00:00 2001 From: Alexandre Lemieux Date: Wed, 22 Jul 2026 21:32:57 -0400 Subject: [PATCH 5/5] Ability to dismiss the update notification. --- Sources/Views/MenuBarView.swift | 65 ++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index f948591..23d4f0a 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -4,6 +4,7 @@ struct MenuBarView: View { @EnvironmentObject var runnerManager: RunnerManager @State private var showAddRunner = false @State private var collapsedTargets: Set = [] + @State private var dismissedUpdateVersion: String? var body: some View { VStack(spacing: 0) { @@ -173,38 +174,50 @@ struct MenuBarView: View { .frame(maxWidth: .infinity, alignment: .leading) } - if let update = runnerManager.availableUpdate { - Button(action: { - Task { - await runnerManager.performAvailableUpdate() - } - }) { - HStack(spacing: 10) { - if runnerManager.isInstallingUpdate { - ProgressView() - .controlSize(.small) - } else { - Image(systemName: "arrow.down.circle.fill") - .foregroundColor(.accentColor) + if let update = runnerManager.availableUpdate, update.latestVersion != dismissedUpdateVersion { + HStack(spacing: 10) { + Button(action: { + Task { + await runnerManager.performAvailableUpdate() } + }) { + HStack(spacing: 10) { + if runnerManager.isInstallingUpdate { + ProgressView() + .controlSize(.small) + } else { + Image(systemName: "arrow.down.circle.fill") + .foregroundColor(.accentColor) + } - VStack(alignment: .leading, spacing: 2) { - Text(update.actionTitle) - .font(.caption) - .fontWeight(.semibold) - Text(update.detailText) - .font(.caption2) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 2) { + Text(update.actionTitle) + .font(.caption) + .fontWeight(.semibold) + Text(update.detailText) + .font(.caption2) + .foregroundColor(.secondary) + } } + } + .buttonStyle(.plain) + .disabled(runnerManager.isInstallingUpdate) - Spacer() + Spacer() + + Button(action: { + dismissedUpdateVersion = update.latestVersion + }) { + Image(systemName: "xmark") + .font(.caption2) + .foregroundColor(.secondary) } - .padding(10) - .background(Color.accentColor.opacity(0.1)) - .cornerRadius(8) + .buttonStyle(.plain) + .help("Dismiss") } - .buttonStyle(.plain) - .disabled(runnerManager.isInstallingUpdate) + .padding(10) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(8) } HStack {