import Foundation /// What `issue list` is narrowed to. Renders itself to the flags the /// command accepts; an empty value sends no flag at all, since /// `--search ""` is a different query from no search. nonisolated struct IssueFilter: Equatable, Sendable { enum State: String, CaseIterable, Identifiable, Sendable { case open, closed, all var id: String { rawValue } var label: String { stateLabel(rawValue) } } var state: State = .open var search = "" var label: String? var assignee: String? var author: String? /// A milestone title, or the literal "none" for issues without one. var milestone: String? func flags() -> [String] { var flags = ["--state", state.rawValue] flags += filterFlag("--search", search) flags += filterFlag("--label", label) flags += filterFlag("--assignee", assignee) flags += filterFlag("--author", author) flags += filterFlag("--milestone", milestone) return flags } /// True when anything beyond the state picker is set, which is what /// decides whether the empty state blames the filters. var isNarrowed: Bool { flags().count > 2 } var emptyMessage: String { if isNarrowed { return state == .all ? "No issues match these filters." : "No \(state.rawValue) issues match these filters." } return state == .all ? "No issues." : "No \(state.rawValue) issues." } } /// One flag, or nothing when the value is absent or blank. Shared by /// both filters, so it is named for what it does rather than taking the /// bare name `flag` in the module namespace. nonisolated func filterFlag(_ name: String, _ value: String?) -> [String] { guard let value else { return [] } let trimmed = value.trimmingCharacters(in: .whitespaces) return trimmed.isEmpty ? [] : [name, trimmed] }