johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import Foundation
struct Person: Decodable, Hashable, Identifiable {
let did: String
let handle: String
var avatar: String?
var id: String { did }
}
/// A page of accounts: who follows someone, or whom they follow.
struct ActorPage: Decodable, Sendable {
let actors: [Person]
var cursor: String?
}
/// A repository as the feed and notifications name it.
struct RepoRef: Decodable, Hashable {
let did: String
let handle: String
let name: String
var uri: String { "at://\(did)/dev.knotless.repo/\(name)" }
var label: String { "\(handle)/\(name)" }
}
struct Repo: Decodable, Identifiable, Hashable {
let uri: String
let owner: Person
let name: String
var description: String?
var starCount: Int?
var forkCount: Int?
var `private`: Bool?
var language: Language?
var id: String { uri }
var ref: RepoRef { RepoRef(did: owner.did, handle: owner.handle, name: name) }
}
struct Language: Decodable, Hashable {
let name: String
var color: String?
}
/// A link a repository or an account shows, by its title or else its host.
struct WebLink: Decodable, Hashable {
let url: String
var title: String?
var label: String { title ?? URL(string: url)?.host() ?? url }
}
struct RepoList: Decodable {
var repos: [Repo] = []
}
struct RepoDetail: Decodable {
let uri: String
let owner: Person
let name: String
var description: String?
let defaultBranch: String
let openPulls: Int
let openIssues: Int
var starCount: Int?
var `private`: Bool?
var links: [WebLink]?
var forkOf: Basic?
var releases: Releases?
var viewer: Viewing?
struct Viewing: Decodable {
/// The asker's star of it, if they starred it.
var star: String?
/// What they chose to hear of it: `all`, `releases` or `ignore`.
var subscription: String?
}
struct Basic: Decodable, Hashable {
let did: String
let handle: String
let name: String
var ref: RepoRef { RepoRef(did: did, handle: handle, name: name) }
}
struct Releases: Decodable {
let count: Int
var release: Latest?
struct Latest: Decodable {
let package: String
let version: String
var latest: Bool?
var createdAt: String?
}
}
}
struct RepoMade: Decodable, Sendable {
let uri: String
let name: String
}
struct Branch: Decodable, Identifiable, Hashable {
let name: String
var id: String { name }
}
struct BranchList: Decodable {
var branches: [Branch] = []
}
struct CommitList: Decodable {
var commits: [Commit] = []
var cursor: String?
}
struct Release: Decodable, Identifiable, Hashable {
let uri: String
let package: String
let version: String
var latest: Bool?
var notes: String?
var html: String?
var files: [File]?
var createdAt: String?
var id: String { uri }
struct File: Decodable, Hashable {
let name: String
var size: Int?
}
}
struct ReleaseList: Decodable {
var releases: [Release] = []
var cursor: String?
}
struct Run: Decodable, Identifiable, Hashable {
let check: Check
let ref: String
var pull: Pull?
var batch: Int?
var subject: String?
var id: String { check.id }
struct Pull: Decodable, Hashable { var title: String? }
}
struct RunList: Decodable {
var runs: [Run] = []
var cursor: String?
var workflows: [Workflow] = []
}
/// A workflow on the default branch.
struct Workflow: Decodable, Hashable {
let name: String
/// It runs only when asked for by name.
var asked: Bool?
/// The first sentence of the comment its file opens with.
var about: String?
}
/// One run, as its runner filed it: each step, and the end of what they printed.
struct RunDetail: Decodable, Hashable {
let check: Check
let ref: String
var pull: String?
let runner: Person
var steps: [Step] = []
/// Each step's after a line `==> <name>`.
var log: String?
}
struct Step: Decodable, Hashable {
let name: String
/// `success`, `failure`, `skipped` or `cancelled`; `running` only while it runs.
let status: String
/// Absent when it was killed.
var exitCode: Int?
var durationMs: Int?
}
/// The workflows at a ref, and what runs each there.
struct WorkflowList: Decodable, Hashable {
/// In full.
let ref: String
var workflows: [Entry] = []
struct Entry: Decodable, Hashable {
let name: String
let push: Bool
let pull: Bool
/// Its owner may ask for it at the ref by name.
let asked: Bool
}
}
struct CheckImage: Decodable, Hashable {
let alt: String
/// On this server, from its root.
let url: String
}
struct ItemLabel: Decodable, Hashable {
let name: String
var color: String?
}
struct Item: Decodable, Identifiable, Hashable {
let uri: String
let kind: String
let title: String
let state: String
let author: Person
let createdAt: String
let comments: Int
var draft: Bool?
var labels: [ItemLabel]?
var id: String { uri }
}
struct ItemList: Decodable {
var items: [Item] = []
var openCount: Int?
var closedCount: Int?
var cursor: String?
}
struct DiffRow: Decodable, Hashable {
let kind: String
let line: Bool
var old: Int?
var new: Int?
let text: String
}
struct DiffFile: Decodable, Identifiable, Hashable {
let old: String
let new: String
let status: String
let added: Int
let removed: Int
var note: String?
var ranges: String?
var rows: [DiffRow] = []
var rowCount: Int?
var id: String { old + "\u{0}" + new }
var path: String { new.isEmpty ? old : new }
enum CodingKeys: String, CodingKey { case old, new, status, added, removed, note, ranges, rows, rowCount }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
old = try c.decode(String.self, forKey: .old)
new = try c.decode(String.self, forKey: .new)
status = try c.decode(String.self, forKey: .status)
added = try c.decode(Int.self, forKey: .added)
removed = try c.decode(Int.self, forKey: .removed)
note = try c.decodeIfPresent(String.self, forKey: .note)
ranges = try c.decodeIfPresent(String.self, forKey: .ranges)
rows = try c.decodeIfPresent([DiffRow].self, forKey: .rows) ?? []
rowCount = try c.decodeIfPresent(Int.self, forKey: .rowCount)
}
}
struct CommitAuthor: Decodable, Hashable {
var name: String?
var actor: Person?
/// The coding agent whose address it is.
var agent: String?
var label: String { actor?.handle ?? name ?? "" }
/// The agent's own face, where the app has it.
var face: String? { agent == "claude" ? "agent-claude" : nil }
}
struct Commit: Decodable, Identifiable, Hashable {
let sha: String
let message: String
var author: CommitAuthor?
var coauthors: [CommitAuthor]?
var committer: CommitAuthor?
var parents: [Parent]?
var date: String?
var committedAt: String?
var pull: Pull?
var id: String { sha }
var subject: String { message.split(separator: "\n", maxSplits: 1).first.map(String.init) ?? message }
/// What its message says after its subject.
var details: String {
message.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false).dropFirst().first
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? ""
}
var when: String? { committedAt ?? date }
var authors: [CommitAuthor] { (author.map { [$0] } ?? []) + (coauthors ?? []) }
/// Who committed it, where that's someone other than those who wrote it.
var otherCommitter: CommitAuthor? {
guard let committer, !authors.contains(where: { $0.label == committer.label }) else { return nil }
return committer
}
struct Parent: Decodable, Hashable { let sha: String }
struct Pull: Decodable, Hashable {
let uri: String
var title: String?
}
}
/// What was searched for, on this phone, newest first, as one string to keep.
enum RecentSearches {
static let key = "recentSearches"
private static let most = 10
static func decode(_ text: String) -> [String] { text.split(separator: "\n").map(String.init) }
static func adding(_ query: String, to text: String) -> String {
let clean = query.replacing("\n", with: " ").trimmingCharacters(in: .whitespaces)
guard !clean.isEmpty else { return text }
return ([clean] + decode(text).filter { $0 != clean }).prefix(most).joined(separator: "\n")
}
}
/// `listActorItems`: pulls or issues, each with the repository it's on.
struct ActorItems: Decodable, Sendable {
let items: [ActorItem]
var cursor: String?
}
struct ActorItem: Decodable, Identifiable, Hashable, Sendable {
let item: Item
let repo: RepoRef
var id: String { item.uri }
}
/// `searchCode`.
struct CodeSearch: Decodable, Sendable {
var files: [File]?
var cursor: String?
struct File: Decodable, Identifiable, Hashable, Sendable {
let repo: RepoHit
let path: String
var language: String?
var chunks: [Chunk]?
var id: String { repo.did + "/" + repo.name + "/" + path }
}
struct Chunk: Decodable, Hashable, Sendable {
let startLine: Int
let content: String
var ranges: [Match]?
var highlights: String?
}
struct Match: Decodable, Hashable, Sendable {
let line: Int
let byteStart: Int
let byteEnd: Int
}
}
/// A repository in `listRepos` or `searchCode`, whose owner may not be named.
struct RepoHit: Decodable, Hashable, Sendable {
let did: String
let name: String
var description: String?
var owner: Person?
var `private`: Bool?
var starCount: Int?
var language: Language?
var ref: RepoRef { RepoRef(did: did, handle: owner?.handle ?? did, name: name) }
var repo: Repo {
Repo(
uri: "at://\(did)/dev.knotless.repo/\(name)",
owner: owner ?? Person(did: did, handle: did),
name: name, description: description, starCount: starCount, private: `private`, language: language)
}
}
struct RepoSearch: Decodable, Sendable {
let repos: [RepoHit]
var cursor: String?
}
struct OwnerSearch: Decodable, Sendable {
let owners: [Found]
struct Found: Decodable, Hashable, Sendable {
let owner: Person
let repoCount: Int
}
}
struct CommitAnswer: Decodable {
let commit: Commit
var files: [DiffFile] = []
}
struct Changes: Decodable {
/// Where its commits can be read, which for an open pull from a fork is the fork.
var repo: RepoDetail.Basic?
var commits: [Commit]?
var files: [DiffFile]?
}
struct Comment: Decodable, Identifiable, Hashable {
var uri: String?
let author: Person
let createdAt: String
var body: String?
var html: String?
var verdict: String?
/// The lines of a pull's diff it's on, if it's on any.
var line: Lines?
/// The comment starting the thread on those lines that this answers.
var reply: String?
var resolved: Bool?
var reactions: [ReactionCount]?
var id: String { uri ?? (author.did + createdAt) }
struct Lines: Decodable, Hashable {
let path: String
var side: String?
let line: Int
var startLine: Int?
let outdated: Bool
/// As they were when it was written; absent when that can't be read.
var lines: [Text]?
struct Text: Decodable, Hashable {
let number: Int
let text: String
}
}
/// Each thread on lines together: answers straight after what they answer,
/// wherever they fell in time, as the pages show them.
static func threaded(_ comments: [Comment]) -> [Comment] {
let starts = Set(comments.compactMap(\.uri))
var answers: [String: [Comment]] = [:]
for comment in comments {
if let root = comment.reply, starts.contains(root) { answers[root, default: []].append(comment) }
}
return comments.flatMap { comment -> [Comment] in
if let root = comment.reply, starts.contains(root) { return [] }
return [comment] + (comment.uri.flatMap { answers[$0] } ?? [])
}
}
}
struct Check: Decodable, Identifiable, Hashable {
let uri: String
let workflow: String
let status: String
/// Why it was skipped, when it was.
var skipped: String?
var startedAt: String?
var finishedAt: String?
var commit: String?
/// Why it couldn't run.
var error: String?
var images: [CheckImage]?
var id: String { uri }
}
struct Review: Decodable, Hashable {
let reviewer: Person
var verdict: String?
var outdated: Bool?
}
struct PullHead: Decodable, Hashable {
var branch: String?
}
struct PullThread: Decodable {
let item: Item
var body: String?
var html: String?
var base: String?
var head: PullHead?
var merged: String?
var comments: [Comment]?
var reviews: [Review]?
var checks: [Check]?
var changes: Changes?
var reactions: [ReactionCount]?
var viewer: ThreadViewer?
var queue: QueueInfo?
}
struct ReactionCount: Decodable, Hashable {
let content: String
var count: Int
/// The asker's own reaction of this content, if they reacted so.
var reaction: String?
}
struct ThreadViewer: Decodable {
var setState: Bool?
}
/// Where a pull stands in the merge queue, if it was queued or ejected.
struct QueueInfo: Decodable, Hashable {
let state: String
/// Where it stands, in the server's words.
let said: String
var reason: String?
}
/// Whether a pull can merge, and what the asker may do about it.
struct MergeStatus: Decodable {
let state: String
var checks: String?
var queueOnly: Bool?
var queueBlocked: String?
var reviews: String?
var reviewsMissing: [Note]?
var may: May?
struct Note: Decodable {
let text: String
}
struct May: Decodable {
var merge: Bool?
var queue: Bool?
}
}
struct DiffAnswer: Decodable {
var files: [DiffFile] = []
}
struct TreeEntry: Decodable, Identifiable, Hashable {
let name: String
let kind: String
var size: Int?
var id: String { name }
}
struct Tree: Decodable {
var ref: String?
var path: String?
var lastCommit: Commit?
var entries: [TreeEntry]?
var readme: Readme?
var checks: [Check]?
struct Readme: Decodable { var name: String? }
}
struct FileAnswer: Decodable {
var ref: String?
var path: String?
var kind: String?
var size: Int?
var lines: Int?
var text: String?
var ranges: String?
/// A Markdown file, made into HTML.
var html: String?
}
struct Org: Decodable, Hashable { let actor: Person }
struct Profile: Decodable {
let did: String
let handle: String
var displayName: String?
var description: String?
var pronouns: String?
var avatar: String?
var followersCount: Int?
var followsCount: Int?
var links: [WebLink]?
var viewer: Viewing?
var orgs: [Org]?
struct Viewing: Decodable {
/// The asker's follow of them, if they follow them.
var following: String?
/// The asker's block of them from all its repositories, if it has one.
var blocking: String?
}
}
/// What knotless data the asking account holds, before it deletes it.
struct AccountData: Decodable, Sendable {
let did: String
let collections: [Count]
let repos: [Held]
let spaces: Int
struct Count: Decodable, Hashable, Sendable {
let collection: String
let count: Int
}
struct Held: Decodable, Hashable, Sendable {
let name: String
let `private`: Bool
let collaborators: Int
var members: Int?
/// How many other accounts use it.
var sharing: Int { max(collaborators, members ?? 0) }
}
}
/// One part of deleting an account's data.
struct DeleteStep: Decodable, Sendable {
let deleted: Int
let done: Bool
}
/// The accounts the asker blocks, the latest first.
struct BlockList: Decodable, Sendable {
let blocks: [Block]
struct Block: Decodable, Identifiable, Sendable {
let uri: String
let subject: Person
/// The one repository it's blocked from, or from all of them with none.
var repo: From?
let createdAt: String
var id: String { uri }
struct From: Decodable, Sendable {
let did: String
let name: String
var uri: String { "at://\(did)/dev.knotless.repo/\(name)" }
}
}
}
struct InboxItem: Decodable, Identifiable, Hashable {
let uri: String
let author: Person
let reason: String
var reasonSubject: String?
var repo: RepoRef?
var title: String?
var verdict: String?
var text: String?
var subjectState: String?
let isRead: Bool
let createdAt: String
var id: String { uri + reason }
}
struct NotificationList: Decodable {
var notifications: [InboxItem] = []
var cursor: String?
}
struct FeedEvent: Decodable, Identifiable, Hashable {
let createdAt: String
let actor: Person
let repo: RepoRef
let action: Action
/// The pulls and issues among `events`, each once, by what last happened.
static func recent(_ events: [FeedEvent], most: Int = 8) -> [FeedEvent] {
var seen = Set<String>()
return Array(events.filter { event in
guard let subject = event.action.subject,
subject.contains("/dev.knotless.pull/") || subject.contains("/dev.knotless.issue/") else { return false }
return seen.insert(subject).inserted
}.prefix(most))
}
var id: String { createdAt + actor.did + repo.label + action.summary }
/// An open union: a kind this version doesn't know still shows as an event.
struct Action: Decodable, Hashable {
let type: String
var subject: String?
var title: String?
var state: String?
var subjectState: String?
var text: String?
var verdict: String?
var ref: String?
var count: Int?
var forkOf: RepoRef?
enum CodingKeys: String, CodingKey {
case type = "$type", subject, title, state, subjectState, text, verdict, ref, count, forkOf
}
var kind: String { type.split(separator: "#").last.map(String.init) ?? type }
/// Where the subject stands now, or as this event left it on an older server.
var standing: String? { subjectState ?? state }
/// What the account whose feed this is did, in a line of its own.
var yours: String {
switch kind {
// A queue's runner merges what you opened, so no event of yours says so.
case "opened":
switch subjectState {
case "merged": "Merged"
case "closed": "Closed"
default: "You opened"
}
case "changed": "You \(state == "open" ? "reopened" : state ?? "changed")"
case "commented":
switch verdict {
case "approve": "You approved"
case "changes": "You asked for changes"
default: "You commented"
}
default: "You updated"
}
}
var branch: String? { ref?.replacingOccurrences(of: "refs/heads/", with: "") }
var summary: String {
switch kind {
case "created": forkOf.map { "forked \($0.label)" } ?? "created this repository"
case "pushed":
if let tag = ref?.replacing("refs/tags/", with: ""), ref?.hasPrefix("refs/tags/") == true {
"pushed tag \(tag)"
} else if (count ?? 0) == 0 {
"pushed to \(branch ?? "a branch")"
} else {
count == 1 ? "pushed 1 commit to \(branch ?? "a branch")" : "pushed \(count ?? 0) commits to \(branch ?? "a branch")"
}
case "deleted": "deleted \(branch ?? "a branch")"
case "opened": "opened"
case "commented":
switch verdict {
case "approve": "approved"
case "changes": "asked for changes on"
default: "commented on"
}
case "changed":
switch state {
case "merged": "merged"
case "closed": "closed"
case "open": "reopened"
default: "changed"
}
case "starred": "starred this repository"
default: "did something new"
}
}
}
}
struct Viewer: Decodable {
var actor: Person?
var reauth: Bool?
}
/// What the server asks of clients, of which the app reads its own part.
struct ServerDescription: Decodable {
struct IOS: Decodable {
var minimumBuild: Int
var reason: String?
}
var ios: IOS?
}
struct Feed: Decodable {
var events: [FeedEvent] = []
var cursor: String?
}
/// A `dev.knotless.theme#palette` as the account keeps it.
struct PaletteRecord: Codable, Equatable, Sendable {
struct Syntax: Codable, Equatable, Sendable {
var comments, strings, keywords, numbers, functions, types, tags: String?
}
var colorScheme: String?
var background, panel, soft, border, text, textMuted, textFaint: String?
var primary, onPrimary, accent, success, danger, done, attention, neutral: String?
var syntax: Syntax?
}
/// A theme the app is drawn in, as `appearancePref` and its `night` name one:
/// one the pages offer by name, or `custom` with its palette.
struct ThemeHalf: Codable, Equatable, Sendable {
var theme: String?
var palette: PaletteRecord?
}
/// How the app is drawn: in one theme, or in a day one while the system is
/// light and a night one while it is dark.
struct ThemeChoice: Codable, Equatable, Sendable {
var day: ThemeHalf
var night: ThemeHalf?
init(day: ThemeHalf, night: ThemeHalf? = nil) {
self.day = day
self.night = night
}
}
/// A theme of the account's own: one it published, or one it added, as Appearance
/// on the pages lists them.
struct OwnTheme: Codable, Equatable, Identifiable, Sendable {
let uri: String
let name: String
let palette: PaletteRecord
/// Who made it, when that's someone else.
var maker: String?
/// When it was made or added, which orders them, the latest first.
var at: String?
var id: String { uri }
}
/// What `getPreferences` answers, of the kinds the app acts on.
struct Preferences: Decodable, Sendable {
let notifications: NotificationChoices?
/// The theme the pages are drawn in, which the app is drawn in too.
let appearance: ThemeChoice?
/// The themes people made that the account added to its own, `themesPref`.
let themes: [OwnTheme]
private struct Added: Decodable {
let uri: String
let name: String?
let palette: PaletteRecord
let addedAt: String?
}
private struct Kind: Decodable {
let choices: NotificationChoices?
let appearance: ThemeChoice?
let themes: [OwnTheme]?
private enum Keys: String, CodingKey { case type = "$type", theme, palette, night, themes }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: Keys.self)
let type = try c.decodeIfPresent(String.self, forKey: .type)
choices = try type == NotificationChoices.type ? NotificationChoices(from: decoder) : nil
if type == "dev.knotless.getPreferences#appearancePref" {
let day = ThemeHalf(
theme: try? c.decodeIfPresent(String.self, forKey: .theme),
palette: try? c.decodeIfPresent(PaletteRecord.self, forKey: .palette)
)
let night = try? c.decodeIfPresent(ThemeHalf.self, forKey: .night)
appearance = ThemeChoice(day: day, night: night)
} else {
appearance = nil
}
if type == "dev.knotless.getPreferences#themesPref" {
let added = (try? c.decode([Added].self, forKey: .themes)) ?? []
themes = added.map { OwnTheme(uri: $0.uri, name: $0.name ?? "Untitled", palette: $0.palette, at: $0.addedAt) }
} else {
themes = nil
}
}
}
private enum Keys: String, CodingKey { case preferences }
init(from decoder: Decoder) throws {
let kinds = try decoder.container(keyedBy: Keys.self).decode([Kind].self, forKey: .preferences)
notifications = kinds.lazy.compactMap(\.choices).first
appearance = kinds.lazy.compactMap(\.appearance).first
themes = kinds.lazy.compactMap(\.themes).first ?? []
}
}
/// Which notifications the account hears of: each reason the server gives, on or
/// off, and whether only of what accounts it follows did.
struct NotificationChoices: Decodable, Equatable, Sendable {
static let type = "dev.knotless.getPreferences#notificationPref"
struct Reason: Decodable, Equatable, Sendable, Identifiable {
let reason: String
var enabled: Bool
var id: String { reason }
}
var reasons: [Reason]
var followedOnly: Bool?
/// As `putPreferences` is given it.
var written: [String: Any] {
[
"$type": Self.type,
"reasons": reasons.map { ["reason": $0.reason, "enabled": $0.enabled] as [String: Any] },
"followedOnly": followedOnly ?? false,
]
}
}