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
8 changes: 6 additions & 2 deletions MonitorLizard/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ enum Constants {

// UI constants
static let menuMaxHeightMultiplier = 0.7
static let settingsWindowWidth = 450.0
static let settingsWindowHeight = 500.0
static let settingsWindowWidth = 580.0
static let settingsWindowHeight = 550.0

// Quiet hours defaults
static let defaultQuietHoursStart = 20 // 20:00
static let defaultQuietHoursEnd = 9 // 09:00

// Voice announcement
static let defaultVoiceAnnouncementText = "Build ready for Q A"
Expand Down
59 changes: 59 additions & 0 deletions MonitorLizard/Models/MonitoredUser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation

struct IgnoredCheckRule: Codable, Equatable, Identifiable {
var id: UUID = UUID()
var pattern: String // glob: "codecov/*", "DCO"
var repository: String // "owner/repo" or "*" for all repos

func matches(checkName: String, repo: String) -> Bool {
let repoMatches = repository == "*" || repository == repo
guard repoMatches else { return false }
return globMatch(pattern: pattern, string: checkName)
}
}

struct MonitoredUser: Codable, Identifiable, Equatable {
var id: UUID
var username: String // "@me" or any GitHub username
var displayName: String? // optional label for segment control
var ignoredRepos: [String] // ["owner/repo", ...]
var ignoredChecks: [IgnoredCheckRule]

var label: String {
displayName ?? (username == "@me" ? "@me" : username)
}

var isMe: Bool { username == "@me" }

static func defaultMe() -> MonitoredUser {
MonitoredUser(
id: UUID(),
username: "@me",
displayName: nil,
ignoredRepos: [],
ignoredChecks: []
)
}
}

/// Simple glob matching supporting only `*` wildcard.
/// `*` matches any sequence of characters.
/// Example: "codecov/*" matches "codecov/patch", "codecov/project".
func globMatch(pattern: String, string: String) -> Bool {
let parts = pattern.split(separator: "*", omittingEmptySubsequences: false).map(String.init)
if parts.count == 1 {
return pattern == string
}

var remaining = string[...]
for (i, part) in parts.enumerated() {
if part.isEmpty { continue }
guard let range = remaining.range(of: part) else { return false }
if i == 0 && range.lowerBound != remaining.startIndex { return false }
remaining = remaining[range.upperBound...]
}
if let last = parts.last, !last.isEmpty {
return string.hasSuffix(last)
}
return true
}
144 changes: 139 additions & 5 deletions MonitorLizard/Models/PullRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,24 @@ enum PRType: String, Codable, Hashable {
func displayTitle(count: Int) -> String {
pluralizes && count != 1 ? sectionTitle + "s" : sectionTitle
}

func displayTitle(count: Int, username: String?) -> String {
switch self {
case .reviewing:
return "Awaiting My Review"
case .other:
return count != 1 ? "Other PRs" : "Other PR"
case .authored:
if let username, username != "@me" {
let base = "\(username)'s PR"
return count != 1 ? base + "s" : base
}
return count != 1 ? "My PRs" : "My PR"
}
}
}

struct PullRequest: Identifiable, Hashable {
struct PullRequest: Identifiable, Hashable, Codable {
let number: Int
let title: String
let repository: RepositoryInfo
Expand All @@ -69,10 +84,11 @@ struct PullRequest: Identifiable, Hashable {
let labels: [Label]
let type: PRType
let isDraft: Bool
let statusChecks: [StatusCheck]
var statusChecks: [StatusCheck]
var reviewDecision: ReviewDecision?
let host: String // GitHub host (e.g. "github.com" or enterprise hostname)
var customName: String? // nil = use GitHub title
var ignoredCheckCount: Int = 0 // Number of ignored failing checks

var displayTitle: String { customName ?? title }

Expand All @@ -84,16 +100,16 @@ struct PullRequest: Identifiable, Hashable {
!statusChecks.isEmpty
}

struct RepositoryInfo: Hashable {
struct RepositoryInfo: Hashable, Codable {
let name: String
let nameWithOwner: String
}

struct Author: Hashable {
struct Author: Hashable, Codable {
let login: String
}

struct Label: Hashable, Identifiable {
struct Label: Hashable, Identifiable, Codable {
let id: String
let name: String
let color: String
Expand Down Expand Up @@ -176,6 +192,11 @@ struct GHPRDetailResponse: Codable {

struct ReviewRequest: Codable {
let login: String? // nil for team review requests

// Support both REST and GraphQL formats
enum CodingKeys: String, CodingKey {
case login
}
}

struct StatusCheck: Codable {
Expand All @@ -193,3 +214,116 @@ struct GHPRDetailResponse: Codable {
}
}
}

// MARK: - GraphQL Response Models

struct GraphQLSearchResponse: Codable {
let data: GraphQLData?
let errors: [GraphQLError]?
}

struct GraphQLError: Codable {
let message: String
}

struct GraphQLData: Codable {
let search: GraphQLSearch
}

struct GraphQLSearch: Codable {
let nodes: [GraphQLPRNode]
}

struct GraphQLPRNode: Codable {
let number: Int
let title: String
let url: String
let isDraft: Bool
let headRefName: String
let updatedAt: String
let mergeable: String?
let reviewDecision: String?
let author: GraphQLAuthor?
let repository: GraphQLRepository
let labels: GraphQLLabels?
let commits: GraphQLCommits?
let latestReviews: GraphQLReviews?
let reviewRequests: GraphQLReviewRequests?
}

struct GraphQLAuthor: Codable {
let login: String
}

struct GraphQLRepository: Codable {
let name: String
let nameWithOwner: String
}

struct GraphQLLabels: Codable {
let nodes: [GraphQLLabel]?
}

struct GraphQLLabel: Codable {
let id: String
let name: String
let color: String
}

struct GraphQLCommits: Codable {
let nodes: [GraphQLCommitNode]?
}

struct GraphQLCommitNode: Codable {
let commit: GraphQLCommit
}

struct GraphQLCommit: Codable {
let statusCheckRollup: GraphQLStatusCheckRollup?
}

struct GraphQLStatusCheckRollup: Codable {
let contexts: GraphQLContexts
}

struct GraphQLContexts: Codable {
let nodes: [GraphQLCheckContext]
}

struct GraphQLCheckContext: Codable {
let __typename: String
// CheckRun fields
let name: String?
let status: String?
let conclusion: String?
let detailsUrl: String?
// StatusContext fields
let context: String?
let state: String?
let targetUrl: String?

private enum CodingKeys: String, CodingKey {
case __typename, name, status, conclusion, detailsUrl, context, state, targetUrl
}
}

struct GraphQLReviews: Codable {
let nodes: [GraphQLReviewNode]?
}

struct GraphQLReviewNode: Codable {
let author: GraphQLAuthor?
let state: String
}

struct GraphQLReviewRequests: Codable {
let nodes: [GraphQLReviewRequestNode]?
}

struct GraphQLReviewRequestNode: Codable {
let requestedReviewer: GraphQLRequestedReviewer?
}

struct GraphQLRequestedReviewer: Codable {
let login: String?
}
16 changes: 16 additions & 0 deletions MonitorLizard/MonitorLizard.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
AA0001112F103E3200F0ABCD /* OtherPRsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0002222F103E3200F0ABCD /* OtherPRsService.swift */; };
AA0003332F103E3200F0ABCD /* AddPRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004442F103E3200F0ABCD /* AddPRView.swift */; };
BB0001112F103E3200F0ABCD /* CustomNamesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0002222F103E3200F0ABCD /* CustomNamesService.swift */; };
CC0001112F103E3200F0ABCD /* MonitoredUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0002222F103E3200F0ABCD /* MonitoredUser.swift */; };
CC0003332F103E3200F0ABCD /* MonitoredUsersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0004442F103E3200F0ABCD /* MonitoredUsersService.swift */; };
DD0001112F103E3200F0ABCD /* PRCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0002222F103E3200F0ABCD /* PRCacheService.swift */; };
EE0001112F103E3200F0ABCD /* RefreshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0002222F103E3200F0ABCD /* RefreshLogger.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -78,6 +82,10 @@
AA0002222F103E3200F0ABCD /* OtherPRsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherPRsService.swift; sourceTree = "<group>"; };
AA0004442F103E3200F0ABCD /* AddPRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPRView.swift; sourceTree = "<group>"; };
BB0002222F103E3200F0ABCD /* CustomNamesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNamesService.swift; sourceTree = "<group>"; };
CC0002222F103E3200F0ABCD /* MonitoredUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitoredUser.swift; sourceTree = "<group>"; };
CC0004442F103E3200F0ABCD /* MonitoredUsersService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitoredUsersService.swift; sourceTree = "<group>"; };
DD0002222F103E3200F0ABCD /* PRCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PRCacheService.swift; sourceTree = "<group>"; };
EE0002222F103E3200F0ABCD /* RefreshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshLogger.swift; sourceTree = "<group>"; };
F1EE91F749D44258AE659F6C /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -141,6 +149,7 @@
329D9EDE2F103E3200F0E6EA /* BuildStatus.swift */,
329D9EDF2F103E3200F0E6EA /* PullRequest.swift */,
32DEMO9E2F103E3200F0E6EA /* DemoData.swift */,
CC0002222F103E3200F0ABCD /* MonitoredUser.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -164,6 +173,9 @@
324376102F5DB275009B7E2D /* UpdateService.swift */,
AA0002222F103E3200F0ABCD /* OtherPRsService.swift */,
BB0002222F103E3200F0ABCD /* CustomNamesService.swift */,
CC0004442F103E3200F0ABCD /* MonitoredUsersService.swift */,
DD0002222F103E3200F0ABCD /* PRCacheService.swift */,
EE0002222F103E3200F0ABCD /* RefreshLogger.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -357,6 +369,10 @@
AA0001112F103E3200F0ABCD /* OtherPRsService.swift in Sources */,
BB0001112F103E3200F0ABCD /* CustomNamesService.swift in Sources */,
AA0003332F103E3200F0ABCD /* AddPRView.swift in Sources */,
CC0001112F103E3200F0ABCD /* MonitoredUser.swift in Sources */,
CC0003332F103E3200F0ABCD /* MonitoredUsersService.swift in Sources */,
DD0001112F103E3200F0ABCD /* PRCacheService.swift in Sources */,
EE0001112F103E3200F0ABCD /* RefreshLogger.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
6 changes: 6 additions & 0 deletions MonitorLizard/MonitorLizardApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct MonitorLizardApp: App {
let isDemoMode = CommandLine.arguments.contains("--demo-mode")
return PRMonitorViewModel(isDemoMode: isDemoMode)
}()
@State private var didRestoreFloatingWindowAtLaunch = false
private let updateService = UpdateService.shared

var body: some Scene {
Expand All @@ -14,6 +15,11 @@ struct MonitorLizardApp: App {
.environmentObject(viewModel)
} label: {
MenuBarLabel(showWarningIcon: viewModel.showWarningIcon)
.task {
guard !didRestoreFloatingWindowAtLaunch else { return }
didRestoreFloatingWindowAtLaunch = true
WindowManager.shared.restoreFloatingWindowIfNeeded(viewModel: viewModel)
}
}
.menuBarExtraStyle(.window)

Expand Down
Loading