A native macOS client for Tangled.
0

Configure Feed

Select the types of activity you want to include in your feed.

Swift 72.8%
Shell 1.9%
Other 25.3%
4 1 0

Clone this repository

https://git.vm.fail/j4ck.xyz/tangled-mac https://git.vm.fail/did:plc:c7uuuqawqpeasasd2axhbf3l
ssh://git@knot1.tangled.sh:2222/j4ck.xyz/tangled-mac ssh://git@knot1.tangled.sh:2222/did:plc:c7uuuqawqpeasasd2axhbf3l

For self-hosted knots, clone URLs may differ based on your setup.


README.md

Tangled for Mac#

A native SwiftUI macOS client for Tangled. It browses Tangled’s public Bobbin AppView and writes narrowly authorized Tangled records directly to the user’s AT Protocol PDS.

Current features#

  • Home, Explore, Following, and My Repositories navigation through https://api.tangled.org
  • Cursor-backed incremental loading for repositories, issues, pull requests, and discussion comments
  • Trending and recently discovered repository overviews
  • Browse repository metadata, source trees, files, issues, and pull requests
  • Copy correct hosted or self-hosted SSH/HTTPS clone commands (⌘⇧C copies SSH)
  • Resolve handles through HTTPS or DNS and discover any user’s PDS
  • Sign in with AT Protocol OAuth using PKCE, PAR, DPoP, and issuer/state/subject validation
  • Persist refresh tokens and DPoP keys in the macOS Keychain
  • Star repositories and follow their owners
  • Open and close issues, open and close pull requests, and comment on either
  • Open a new issue from the app
  • Native three-column navigation, Mac menu commands (⌘1⌘4), keyboard-friendly settings, and Sparkle update UI
  • macOS 26 Liquid Glass for the navigation and control layer, with native scroll-edge effects and accessible opaque fallbacks
  • Partial-data handling when an individual PDS or repository Knot is temporarily unavailable

Forking currently opens the selected repository on Tangled. A fully native fork is intentionally deferred because it needs both a sh.tangled.repo record and a delegated sh.tangled.repo.create call to the repository’s Knot. The app will not request that RPC permission before it implements and verifies the full service-auth flow.

OAuth permissions#

The app deliberately requests only:

atproto
repo:sh.tangled.feed.star?action=create&action=delete
repo:sh.tangled.graph.follow?action=create&action=delete
repo:sh.tangled.feed.comment?action=create&action=delete
repo:sh.tangled.repo.issue?action=create
repo:sh.tangled.repo.issue.state?action=create
repo:sh.tangled.repo.pull.status?action=create

Every writable permission names a sh.tangled.* collection and the actions the app performs. It never requests transition:generic, any other transition scope, or access to a Bluesky collection. Bobbin reads are public and do not require an OAuth permission.

The supplied repository export and the include/defer rationale are documented in docs/LexiconScope.md.

Debug builds use a localhost client ID and the fixed redirect URI http://127.0.0.1:43717/oauth/callback. The listener binds to 127.0.0.1, not all interfaces. Release builds use the client metadata document in oauth-client-metadata.json.

Build#

Requirements:

  • macOS 15 or newer
  • Xcode 26 or Swift 6.3 Command Line Tools with the macOS 26 SDK
  • XcodeGen

The app builds against the macOS 26 SDK so standard navigation, search, toolbars, controls, and settings adopt the current system design automatically. Its deployment target remains macOS 15; macOS 26-only effects are availability-gated and use native material fallbacks on older systems.

Generate the project and run its tests:

xcodegen generate
xcodebuild -project Tangled.xcodeproj -scheme Tangled -configuration Debug build
xcodebuild -project Tangled.xcodeproj -scheme Tangled test

swift test is also useful for package-level checks when a full Xcode 26 toolchain is selected; the standalone Command Line Tools installation can build the app but does not include Apple’s test frameworks.

Continuous integration and releases#

Builds run on GitHub-hosted macOS runners, not on Tangled. Tangled's CI (Spindle) executes each step in Linux containers or microVMs and has no macOS/Xcode toolchain, so it cannot compile this app. The workflows here build on macOS and then publish the result to both GitHub Releases and Tangled.

  • .github/workflows/ci.yml — builds and runs the test suite on every push and pull request to main. This is what verifies the app still compiles on each change.
  • .github/workflows/release.yml — triggered by pushing an annotated v* tag (or run manually). It builds a Release .dmg, signs it for Sparkle, creates a GitHub Release with the .dmg and appcast.xml, commits the updated appcast.xml back to main, and mirrors the .dmg to Tangled as a sh.tangled.repo.artifact record so it is downloadable from the Tangled repo page.

Both runners must have Xcode 26 (macOS 26 SDK); the workflows select the newest Xcode installed on the runner image.

Cutting a release#

Tangled artifact records key off the annotated tag object, so the tag must be annotated and pushed to both remotes:

git tag -a v0.2.0 -m "v0.2.0"
git push origin v0.2.0     # GitHub — triggers the release workflow
git push tangled v0.2.0    # Tangled — so the artifact record has a tag to attach to

Required GitHub configuration#

Secrets (Settings → Secrets and variables → Actions):

  • SPARKLE_PRIVATE_KEY — the Sparkle EdDSA private key. The matching public key is already embedded as SUPublicEDKey in TangledMac/Resources/Info.plist. Re-export it from your login Keychain with .build/artifacts/sparkle/Sparkle/bin/generate_keys -x <file>.
  • ATP_APP_PASSWORD — an app password for the PDS account that owns the Tangled repo. Used to upload the .dmg blob and create the artifact record. If unset, the Tangled step is skipped.

Optional repository variables (defaults shown, override only if they change):

  • ATP_PDS (https://eurosky.social)
  • ATP_IDENTIFIER (j4ck.xyz)
  • TANGLED_REPO_URL (https://tangled.org/j4ck.xyz/tangled-mac)

Signing, notarization, and the update feed#

The build is ad-hoc signed and neither Developer ID signed nor notarized (no Apple Developer Program membership required). Sparkle's EdDSA signature — not Apple code signing — is the update trust mechanism: the private key signs each .dmg and the embedded SUPublicEDKey verifies it. The trade-off is first-run trust: an unsigned, unnotarized download is treated as coming from an unidentified developer, so users must allow it in System Settings → Privacy & Security. Developer ID signing and notarization are the route to a normal double-click install.

SUFeedURL points at appcast.xml on the GitHub main branch. While the GitHub repository is private, that raw URL (and GitHub Release assets) require authentication, so auto-update will only reach the public once the repository is public or the appcast is hosted on a public URL. Downloads from a public Tangled repo are unaffected.

Also host oauth-client-metadata.json at its exact client_id URL, served as JSON over HTTPS, before distributing a release.

Public native AT Protocol clients can keep a user signed in across app launches by rotating the refresh token and retaining the DPoP key, as this app does. The authorization server still controls the maximum refresh-session lifetime; extending that beyond the public-client policy requires a confidential server component.