diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index 8dd648a..fb1f93a 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -14,7 +14,7 @@ if [[ -z "$staged" ]]; then
fi
if ! command -v xcrun >/dev/null 2>&1; then
- red "xcrun not available — install Xcode command-line tools."
+ red "xcrun not available; install Xcode command-line tools."
exit 1
fi
diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml
index 33a9e5e..ff07f75 100644
--- a/.github/workflows/auto-tag.yml
+++ b/.github/workflows/auto-tag.yml
@@ -9,7 +9,7 @@ name: Auto-tag
#
# For non-patch bumps (minor / major), use the manual "Bump & Release"
# workflow instead. For docs-only / dependabot / hooks-only commits, the
-# paths filter below means this workflow doesn't even start — no tag, no
+# paths filter below means this workflow doesn't even start, no tag, no
# release, no inflation.
on:
@@ -17,7 +17,7 @@ on:
branches: [main]
paths:
# Only files that affect the produced binary trigger an auto-tag.
- # Workflow / CI changes do *not* — that was the source of the
+ # Workflow / CI changes do *not*, that was the source of the
# version-inflation we got while iterating on the workflows themselves.
- "Sources/**"
- "Tests/**"
@@ -43,7 +43,7 @@ jobs:
- name: Skip if HEAD already has a v* tag
# If the commit is already tagged (e.g. the Bump & Release workflow
- # just put one here) there's nothing to do — and re-tagging would
+ # just put one here) there's nothing to do, and re-tagging would
# collide. Sets `skip=true` for downstream conditions.
id: skip
run: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4c0b85c..64e1d67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,7 @@ name: CI
on:
# Only fires the actual job when a PR carries the `run-ci` label, or when
- # someone manually clicks "Run workflow" — keeps macOS minutes from being
+ # someone manually clicks "Run workflow", keeps macOS minutes from being
# burned on every push.
pull_request:
types: [opened, synchronize, reopened, labeled]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2365946..6ef46fe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,7 +1,7 @@
# Contributing to YouMenuTube
Thanks for considering a contribution! YouMenuTube is a small SwiftUI
-menu-bar app and the codebase is intentionally compact — read the README
+menu-bar app and the codebase is intentionally compact; read the README
first to get a feel for what the app does and the constraints it operates
under (it's an unofficial client wrapping YouTube's internal **InnerTube**
API; see the disclaimer in the README).
@@ -21,15 +21,15 @@ Then ⌘R in Xcode. Requires **macOS 15 Sequoia** and **Xcode 16+**.
See the "Project layout" section of the [README](README.md). High-level:
-- `Sources/App/` — `@main` entry, scenes, root view.
-- `Sources/Services/` — `YouTubeService` (the only network surface),
+- `Sources/App/`: `@main` entry, scenes, root view.
+- `Sources/Services/`: `YouTubeService` (the only network surface),
`PlayerController`, `RefreshTrigger`, `Keychain`.
-- `Sources/Views/` — one SwiftUI view per tab, plus the player and sign-in
+- `Sources/Views/`: one SwiftUI view per tab, plus the player and sign-in
windows. `VideoFeedList` / `VideoList` / `ErrorInline` are the shared
building blocks.
-- `Sources/Models/` — `VideoEntry`, `PlaylistEntry`.
-- `Sources/Utilities/` — `WindowID`, `UserAgent`, `BuiltInPlaylist`.
-- `Tests/` — Swift Testing target.
+- `Sources/Models/`: `VideoEntry`, `PlaylistEntry`.
+- `Sources/Utilities/`: `WindowID`, `UserAgent`, `BuiltInPlaylist`.
+- `Tests/`: Swift Testing target.
## Format, lint, test
@@ -64,7 +64,7 @@ Configuration lives in [`.swift-format`](.swift-format) (line length 120,
## CI
-GitHub Actions on every PR — but the actual `build-test` job runs **only**
+GitHub Actions run on every PR, but the actual `build-test` job runs **only**
when the PR carries the `run-ci` label, or when triggered manually from the
Actions tab. This keeps macOS minutes from being burned on every push. A
maintainer will usually add the label after a quick eyeball.
@@ -75,11 +75,11 @@ maintainer will usually add the label after a quick eyeball.
- Match the existing style: terse comments, no hypothetical generality, no
dead code, identifiers should carry meaning instead of comments explaining
what code does. (See [`CLAUDE.md` in the system prompt of the maintainers'
- AI tooling](.) — same principles apply to humans.)
+ AI tooling](.); same principles apply to humans.)
- Don't add code-level documentation that just narrates the next line. Do
add a short comment when there's a non-obvious *why* (a workaround, a
hidden constraint, a quirk of YouTubeKit / Google fingerprinting, etc.).
-- Run `swift-format format -i -r ...` before committing — the pre-commit
+- Run `swift-format format -i -r ...` before committing; the pre-commit
hook will reject unformatted files.
- Open an issue first for anything that touches the auth flow, the InnerTube
cookie filter, the browser cookie readers, or the player window's WKWebView
@@ -92,7 +92,7 @@ maintainer will usually add the label after a quick eyeball.
- Imperative mood: "Add Home tab", not "Added" or "Adds".
- Subject ≤ 72 characters; explain *why* in the body, not *what*.
-- One topic per commit. We don't squash on merge — keep history readable.
+- One topic per commit. We don't squash on merge; keep history readable.
## Reporting bugs
@@ -106,7 +106,7 @@ soon). For now include:
- Relevant log output:
`log stream --predicate 'subsystem == "app.youmenutube"' --level debug`
-For **security** issues, see [`SECURITY.md`](SECURITY.md) — please don't
+For **security** issues, see [`SECURITY.md`](SECURITY.md); please don't
file them publicly.
## License
diff --git a/NOTICE b/NOTICE
index 24292f0..3a1a7d4 100644
--- a/NOTICE
+++ b/NOTICE
@@ -5,5 +5,5 @@ Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
This product bundles the following third-party software:
- YouTubeKit (https://github.com/b5i/YouTubeKit) — MIT License
+ YouTubeKit (https://github.com/b5i/YouTubeKit): MIT License
Copyright 2023 - 2025 Antoine Bollengier
diff --git a/README.md b/README.md
index d40f1e4..17f8e34 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Website:
A macOS menu-bar app that puts YouTube in your menubar:
-- **Home** (default tab) — YouTube's main recommendations feed
+- **Home** (default tab): YouTube's main recommendations feed
- Latest videos from your **Subscriptions**
- Browse your **Playlists** (including **Watch Later** and **Liked Videos**)
- **Search** YouTube
@@ -20,14 +20,14 @@ A macOS menu-bar app that puts YouTube in your menubar:
- In-app **update check** against the latest GitHub release
Built with SwiftUI `MenuBarExtra`, targeting **macOS 15 Sequoia or later**.
-Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit) — talks to
+Powered by [YouTubeKit](https://github.com/b5i/YouTubeKit). It talks to
YouTube's own internal "InnerTube" API directly. **No Google Cloud project,
no API key, no OAuth client to set up.** Sign into youtube.com in your
-usual browser — passkeys, password managers, all of it — and YouMenuTube
+usual browser (passkeys, password managers, all of it) and YouMenuTube
imports the session. Supports Safari, Chrome, Edge, Arc, Brave, Vivaldi,
Opera, Helium, Firefox and Zen.
-> ## ⚠️ Important — read before installing
+> ## ⚠️ Important: read before installing
>
> **YouMenuTube is unofficial and not affiliated with, endorsed by, or
> sponsored by YouTube, Google, or Alphabet.** All trademarks belong to
@@ -38,7 +38,7 @@ Opera, Helium, Firefox and Zen.
> YouTube's internal **InnerTube** API via [YouTubeKit](https://github.com/b5i/YouTubeKit).
> This is the same surface YouTube's own website uses, but it is **not a
> public API** and using it is **arguably outside YouTube's Terms of
-> Service**. The API can — and occasionally does — change or break without
+> Service**. The API can (and occasionally does) change or break without
> notice.
>
> **Use at your own risk.** Recommendations:
@@ -55,7 +55,7 @@ Opera, Helium, Firefox and Zen.
(that link permanently redirects to the newest published version, so you
can bookmark it).
2. Open the DMG and drag `YouMenuTube.app` into `/Applications`.
-3. **First launch** — because the build is ad-hoc signed (no paid Apple
+3. **First launch**: because the build is ad-hoc signed (no paid Apple
Developer ID), Gatekeeper will refuse it on a normal double-click. Either:
- Right-click the app → **Open** → **Open** again in the prompt, *or*
- System Settings → Privacy & Security → "YouMenuTube was blocked …" →
@@ -129,17 +129,17 @@ YouMenuTube/
## Setup
```sh
-brew install xcodegen # (if missing — bootstrap.sh will offer to do this for you)
+brew install xcodegen # (if missing, bootstrap.sh will offer to install it for you)
./bootstrap.sh
```
This generates `YouMenuTube.xcodeproj`, resolves `YouTubeKit` via SPM, and
opens Xcode. In Xcode:
-1. Pick a signing team (target → **Signing & Capabilities → Team**) — a free
+1. Pick a signing team (target → **Signing & Capabilities → Team**); a free
personal team works.
2. Build & Run (⌘R).
-3. There's no Dock icon (by design — `LSUIElement`). Look for the ▶️ icon in
+3. There's no Dock icon (by design: `LSUIElement`). Look for the ▶️ icon in
the menu bar.
4. Click the icon → **Sign in** → pick the browser where you're already
signed in to youtube.com. First import may ask for a one-time permission
@@ -153,7 +153,7 @@ opens Xcode. In Xcode:
The project ships a second scheme, **YouMenuTube Demo**, which launches the
app with `-demo-mode`. In that mode `YouTubeService` swaps every network
call for the hardcoded fixtures in
-[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift) —
+[`Sources/Services/DemoData.swift`](Sources/Services/DemoData.swift):
popular real video IDs, so thumbnails render normally and clicking a row
plays the real video in the embedded player. No sign-in, no keychain, no
InnerTube traffic. Edit `DemoData.swift` to change what the screenshots
@@ -184,7 +184,7 @@ YMT_DEMO_MODE=1 ./path/to/YouMenuTube.app/Contents/MacOS/YouMenuTube
YouMenuTube doesn't host a sign-in UI of its own. Instead it imports
`youtube.com` cookies directly out of your browser's cookie store, so the
sign-in itself happens wherever your passkeys / password manager already
-work — your normal browser. Three formats covered:
+work: in your normal browser. Three formats covered:
| Browser family | Storage | What macOS will ask |
|---|---|---|
@@ -192,7 +192,7 @@ work — your normal browser. Three formats covered:
| **Chrome / Edge / Arc / Brave / Vivaldi / Opera / Helium** (Chromium family) | SQLite + AES-128-CBC with a key in the login Keychain | A standard "YouMenuTube wants to use confidential information stored in 'Chrome Safe Storage' …" Keychain prompt. Click **Always Allow** once per browser. (Helium's Keychain entry is `Helium Storage Key` rather than the usual `… Safe Storage`, but the flow is identical.) |
| **Firefox / Zen** (Firefox family) | Plain SQLite, unencrypted | No prompt. |
-The importer filters strictly to `*.youtube.com`-scoped rows — mixing in
+The importer filters strictly to `*.youtube.com`-scoped rows; mixing in
`.google.com` or `accounts.google.com` cookies makes InnerTube respond
with `loggedOut=true`. The resulting blob is persisted in the macOS
Keychain and handed to YouTubeKit via `YouTubeModel.cookies`.
@@ -204,14 +204,14 @@ once signed in and click **Import**.
The Now Playing window wraps `youtube.com/embed/` in a tiny HTML page
loaded with `baseURL = https://youmenutube.local/`. The fake-but-real-looking
-parent origin is what makes YouTube's IFrame player initialize — loading
+parent origin is what makes YouTube's IFrame player initialize. Loading
the embed URL top-level returns error 153, and `baseURL = youtube.com`
(same-origin parent) returns 152-4.
The window itself uses `.windowStyle(.plain)` for a fully chrome-less look,
locked to a 16:9 aspect ratio via `NSWindow.aspectRatio`. Because plain
(borderless) windows can't normally become key or be resized, the underlying
-`NSWindow`'s styleMask is patched to add `.resizable` — which restores both
+`NSWindow`'s styleMask is patched to add `.resizable`, which restores both
edge-resize handles and Cmd+W. A 28pt strip at the top of the player fades
in on hover, hosting a small ✕ close button and a `performDrag(with:)`-backed
draggable region (since the WKWebView itself swallows mouse events). By
@@ -234,19 +234,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for PR conventions and detail.
Releases are fully automated. The pipeline:
-1. **Auto-tag** (`.github/workflows/auto-tag.yml`) — every push to `main` that
+1. **Auto-tag** (`.github/workflows/auto-tag.yml`): every push to `main` that
touches `Sources/`, `Tests/`, `project.yml`, or `bootstrap.sh` gets a patch
bump (e.g. `v0.1.5 → v0.1.6`). Docs / dependabot / hook / format-config
pushes don't trigger it.
-2. **Release** (`.github/workflows/release.yml`) — fires on `v*` tag pushes,
+2. **Release** (`.github/workflows/release.yml`): fires on `v*` tag pushes,
builds the `.app` (Release config), ad-hoc signs it, packages it as a
`.dmg` with `create-dmg`, and publishes a GitHub Release.
-3. **Bump & Release** (`.github/workflows/bump-release.yml`) — manual entry
+3. **Bump & Release** (`.github/workflows/bump-release.yml`): manual entry
point (Actions tab → Run workflow) for `minor` / `major` bumps. Patches
are handled by Auto-tag.
Versioning: `CFBundleShortVersionString` is derived from `git describe`
-(post-build script in `project.yml`) — `0.1.0` on a tag, `0.1.0+N` past it.
+(post-build script in `project.yml`): `0.1.0` on a tag, `0.1.0+N` past it.
`CFBundleVersion` is `git rev-list --count HEAD` (monotonic, kept for
macOS update bookkeeping even though it isn't shown in UI). `GitCommit`
holds the short SHA (with a `-dirty` suffix when the working tree has
@@ -255,28 +255,28 @@ commit, e.g. `0.1.0 · 19d5410`.
## Troubleshooting
-- **Import says "Safari's cookies live inside a protected container"** —
+- **Import says "Safari's cookies live inside a protected container"**:
macOS requires **Full Disk Access** to read `~/Library/Containers/com.apple.Safari/…`.
System Settings → Privacy & Security → Full Disk Access → add YouMenuTube
(the Import window has a shortcut button for this). Re-run Import.
- **Import says "Couldn't read [Browser]'s cookie-encryption key from the
- Keychain"** — you clicked Deny on the macOS Keychain prompt. Open
+ Keychain"**: you clicked Deny on the macOS Keychain prompt. Open
Keychain Access, search for "Safe Storage" for that browser, open the
entry → **Access Control** tab → remove YouMenuTube from the deny list
(or delete the ACL), then re-run Import and click **Always Allow**.
-- **Import says "[Browser] isn't signed in to YouTube"** — the browser
+- **Import says "[Browser] isn't signed in to YouTube"**: the browser
you picked doesn't have a valid YouTube session. Click the
"Sign in to YouTube in [Browser]" button, complete sign-in there, then
re-run Import.
-- **Subscriptions / playlists empty after signing in** — enable verbose
+- **Subscriptions / playlists empty after signing in**: enable verbose
logging to see what the import produced:
`log stream --predicate 'subsystem == "app.youmenutube"' --level debug`.
If session markers (SAPISID, SID, LOGIN_INFO, __Secure-3PSIDTS) are all
present but InnerTube still says `loggedOut=true`, YouTubeKit may be out
- of date — check for a newer release.
-- **Embed player shows error 150 / 101** — the uploader has disabled
+ of date; check for a newer release.
+- **Embed player shows error 150 / 101**: the uploader has disabled
embedding for that specific video; it can only be played on youtube.com.
-- **Embed player shows error 153 / 152-4** — should not happen with the
+- **Embed player shows error 153 / 152-4**: should not happen with the
current wrapper; if it does, check `Sources/Views/PlayerWindow.swift` for
changes to `baseURL` or the iframe `origin` query parameter.
@@ -290,7 +290,7 @@ network only for:
- `api.github.com/repos/gek0z/YouMenuTube/releases/latest` once per launch
to render the update-available link in Settings → About
-Sign-in itself happens in your browser, not in YouMenuTube — the app
+Sign-in itself happens in your browser, not in YouMenuTube; the app
doesn't make network requests to Google / YouTube for authentication. It
reads `youtube.com` cookies out of your browser's on-disk cookie store
(filtered to `*.youtube.com` only), stores them in the macOS Keychain
diff --git a/SECURITY.md b/SECURITY.md
index 8d7f9ad..05ab76f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -13,7 +13,7 @@ Please **do not** open a public GitHub issue for security problems.
Use one of the following private channels:
- GitHub's [private vulnerability reporting](https://github.com/gek0z/YouMenuTube/security/advisories/new)
- (preferred — gives us a private discussion thread + draft advisory).
+ (preferred: gives us a private discussion thread + draft advisory).
- Email the maintainer (see the GitHub profile of [@gek0z](https://github.com/gek0z)).
Please include:
@@ -25,12 +25,12 @@ Please include:
4. Whether you'd like to be credited in the advisory.
We aim to acknowledge reports within 7 days. Because this is a hobby project,
-fix turnaround depends on availability — critical issues will be prioritised.
+fix turnaround depends on availability; critical issues will be prioritised.
## Supported versions
Only `main` is supported. Older tagged releases will not receive backported
-fixes — upgrade to the latest release.
+fixes; upgrade to the latest release.
## Scope
@@ -43,7 +43,7 @@ In scope:
Out of scope:
- Vulnerabilities in YouTube, Google, or any first-party Apple framework.
-- Vulnerabilities in third-party dependencies (report those upstream — currently
+- Vulnerabilities in third-party dependencies (report those upstream; currently
only [YouTubeKit](https://github.com/b5i/YouTubeKit)).
- The fact that YouMenuTube uses YouTube's internal **InnerTube** API. This is
documented in the README and is a deliberate design choice, not a security
@@ -67,7 +67,7 @@ of the selected browser's on-disk cookie store:
- **Safari** requires the user to grant YouMenuTube **Full Disk Access**
(System Settings → Privacy & Security → Full Disk Access) because the
Safari cookie file lives inside a protected container. Without this grant
- the reader fails cleanly — it cannot silently read Safari cookies.
+ the reader fails cleanly; it cannot silently read Safari cookies.
- **Chromium-family browsers** (Chrome, Edge, Arc, Brave, Vivaldi, Opera,
Helium) encrypt their cookie values with an AES key stored in the macOS
login Keychain under e.g. `Chrome Safe Storage` (or `Helium Storage Key`
@@ -89,4 +89,4 @@ present. No other domain's cookies are ever persisted or sent.
InnerTube endpoints.
- No background activity when the menu bar popover is closed.
-If you find evidence to the contrary, that's a bug — please report it.
+If you find evidence to the contrary, that's a bug; please report it.
diff --git a/Sources/Services/BrowserCookieImport/Browser.swift b/Sources/Services/BrowserCookieImport/Browser.swift
index ef4c689..69f205d 100644
--- a/Sources/Services/BrowserCookieImport/Browser.swift
+++ b/Sources/Services/BrowserCookieImport/Browser.swift
@@ -94,7 +94,7 @@ enum Browser: String, CaseIterable, Identifiable {
/// as a `kSecClassGenericPassword` entry in the login keychain.
///
/// Most Chromium forks use " Safe Storage" with account "",
- /// but Helium uses "Helium Storage Key" / "Helium" — so this is handled
+ /// but Helium uses "Helium Storage Key" / "Helium", so this is handled
/// per-case rather than synthesised from `displayName`.
var chromiumSafeStorageService: String? {
switch self {
diff --git a/Sources/Services/BrowserCookieImport/BrowserDetector.swift b/Sources/Services/BrowserCookieImport/BrowserDetector.swift
index 0acebea..1900f43 100644
--- a/Sources/Services/BrowserCookieImport/BrowserDetector.swift
+++ b/Sources/Services/BrowserCookieImport/BrowserDetector.swift
@@ -2,7 +2,7 @@ import Foundation
/// Finds which browsers the user actually has a cookie store for on this
/// machine, so the import UI only offers real choices. We don't use
-/// `NSWorkspace.urlForApplication(withBundleIdentifier:)` — an app being
+/// `NSWorkspace.urlForApplication(withBundleIdentifier:)`, an app being
/// installed doesn't mean it has ever been run, and without a cookie store
/// there's nothing to import. Presence of the data directory is the real
/// signal.
@@ -16,7 +16,7 @@ enum BrowserDetector {
// We *can't* stat the Safari container without Full Disk Access
// (the stat itself is gated by TCC on macOS 13+). Always list
// Safari and let the reader surface the TCC error if FDA hasn't
- // been granted — that's a clearer UX than silently hiding it.
+ // been granted, that's a clearer UX than silently hiding it.
return true
case .chromium:
guard let root = browser.userDataRoot(home: home) else { return false }
diff --git a/Sources/Services/BrowserCookieImport/ChromiumCookies.swift b/Sources/Services/BrowserCookieImport/ChromiumCookies.swift
index 17ae9ec..a2d7732 100644
--- a/Sources/Services/BrowserCookieImport/ChromiumCookies.swift
+++ b/Sources/Services/BrowserCookieImport/ChromiumCookies.swift
@@ -61,7 +61,7 @@ enum ChromiumCookies {
if let decrypted = Self.decrypt(data, key: key, hostKey: host) {
value = decrypted
} else {
- // Value was encrypted but we couldn't decrypt it —
+ // Value was encrypted but we couldn't decrypt it,
// skip rather than ship a corrupt cookie.
continue
}
@@ -89,7 +89,7 @@ enum ChromiumCookies {
private static func fetchSafeStoragePassword(service: String, account: String, browser: Browser) throws -> String {
// Passing an LAContext lets the system upgrade the prompt to Touch
- // ID on Macs that have it enrolled — but only if the keychain
+ // ID on Macs that have it enrolled, but only if the keychain
// item's ACL was created with biometric-compatible flags. Chromium
// browsers don't do that when they add their Safe Storage key, so
// in practice the OS still falls back to a password prompt. Harmless
diff --git a/Sources/Services/BrowserCookieImport/FirefoxCookies.swift b/Sources/Services/BrowserCookieImport/FirefoxCookies.swift
index b7895df..a5761d2 100644
--- a/Sources/Services/BrowserCookieImport/FirefoxCookies.swift
+++ b/Sources/Services/BrowserCookieImport/FirefoxCookies.swift
@@ -2,7 +2,7 @@ import Foundation
import SQLite3
/// Reads cookies out of Firefox's `cookies.sqlite`. Firefox stores cookie
-/// values in plaintext, so no key derivation or decryption is needed — just
+/// values in plaintext, so no key derivation or decryption is needed, just
/// SQLite.
///
/// The file can be opened even while Firefox is running because we open it
diff --git a/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift b/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift
index e378fd7..9a6f5ce 100644
--- a/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift
+++ b/Sources/Services/BrowserCookieImport/SafariBinaryCookies.swift
@@ -14,7 +14,7 @@ import Foundation
/// 4B BE number of pages N
/// N×4B BE page sizes
/// pages concatenated
-/// [checksum / footer — ignored]
+/// [checksum / footer, ignored]
///
/// Page (little-endian after the 4-byte BE page tag):
/// 4B 0x00000100 page tag
@@ -33,7 +33,7 @@ import Foundation
/// 4B path offset
/// 4B value offset
/// 8B unused
-/// 8B expiry (CFAbsoluteTime — seconds since 2001-01-01 UTC)
+/// 8B expiry (CFAbsoluteTime, seconds since 2001-01-01 UTC)
/// 8B creation (CFAbsoluteTime)
/// NUL-terminated strings at the declared offsets
enum SafariBinaryCookies {
@@ -52,7 +52,7 @@ enum SafariBinaryCookies {
}
// macOS returns "operation not permitted" for TCC-blocked reads,
// which Foundation surfaces as generic; treat any read failure
- // on a path that exists as TCC-denied — user can retry after
+ // on a path that exists as TCC-denied, user can retry after
// granting Full Disk Access.
throw BrowserCookieError.tccDenied
}
@@ -90,7 +90,7 @@ enum SafariBinaryCookies {
private static func parsePage(_ page: Data, into cookies: inout [HTTPCookie], domainSuffix: String) throws {
guard page.count >= 12 else { return }
- // Page tag is 4 bytes but we don't validate it strictly — some
+ // Page tag is 4 bytes but we don't validate it strictly, some
// Safari builds have varied the first byte.
let cookieCount = Int(page.readUInt32LE(at: 4))
guard cookieCount > 0, page.count >= 8 + cookieCount * 4 + 4 else { return }
@@ -127,7 +127,7 @@ enum SafariBinaryCookies {
let value = rec.readCString(at: valueOff)
else { return nil }
- // Early domain filter — 99% of cookies won't match, and this avoids
+ // Early domain filter, 99% of cookies won't match, and this avoids
// building HTTPCookie objects we'd drop anyway.
guard domain.hasSuffix(domainSuffix) else { return nil }
diff --git a/Sources/Services/DemoData.swift b/Sources/Services/DemoData.swift
index 903a545..f6a3a8f 100644
--- a/Sources/Services/DemoData.swift
+++ b/Sources/Services/DemoData.swift
@@ -4,7 +4,7 @@ import Foundation
/// is launched in demo mode (`-demo-mode` / `YMT_DEMO_MODE=1`). Populated
/// with real, famous YouTube video IDs so thumbnails render from
/// `i.ytimg.com` and clicking a row still plays the real video in the
-/// embedded player — no network call needed to produce the row itself.
+/// embedded player, no network call needed to produce the row itself.
///
/// To change the look of the screenshots, edit the arrays below.
enum DemoData {
@@ -12,7 +12,7 @@ enum DemoData {
// MARK: - Videos
/// Builds a `VideoEntry` using `https://i.ytimg.com/vi//hqdefault.jpg`
- /// for the thumbnail — which reliably exists for any real video ID.
+ /// for the thumbnail, which reliably exists for any real video ID.
private static func video(
_ id: String,
_ title: String,
@@ -33,7 +33,7 @@ enum DemoData {
)
}
- /// What the Home tab shows in demo mode. Classic memes / viral videos —
+ /// What the Home tab shows in demo mode. Classic memes / viral videos,
/// thumbnails all come from `i.ytimg.com` for these real IDs. Order is
/// curated so the most-recently-added entries land above the fold of
/// the 420×560 popover (~6 rows visible without scrolling).
@@ -47,7 +47,7 @@ enum DemoData {
channel: "Protectstar Inc.", time: "15 years ago",
views: "14M views", duration: "1:20:16"),
video(
- "92fLApYaCGI", "Michael Jordan \"The Last Shot\" – #NBATogetherLive Classic Game",
+ "92fLApYaCGI", "Michael Jordan \"The Last Shot\", #NBATogetherLive Classic Game",
channel: "NBA", time: "6 years ago",
views: "32M views", duration: "2:33"),
video(
@@ -178,7 +178,7 @@ enum DemoData {
}
}
- /// Seed set for the "is this in my Watch Later?" cache — drives the
+ /// Seed set for the "is this in my Watch Later?" cache, drives the
/// clock badge state on Home rows on first launch.
static let watchLaterIds: Set = ["dQw4w9WgXcQ", "tzD9OxAHtzU", "XnygT6ANLzQ", "dMH0bHeiRNg"]
@@ -186,8 +186,8 @@ enum DemoData {
/// Very cheap substring match so typing in the search box in demo mode
/// actually narrows a visible list. Falls back to everything when the
- /// query is empty (shouldn't happen — the view short-circuits empty
- /// queries — but harmless).
+ /// query is empty (shouldn't happen, the view short-circuits empty
+ /// queries, but harmless).
static func search(_ query: String) -> [VideoEntry] {
let q = query.lowercased().trimmingCharacters(in: .whitespaces)
guard !q.isEmpty else { return homeFeed }
diff --git a/Sources/Services/DockPresence.swift b/Sources/Services/DockPresence.swift
index de776d9..c041f16 100644
--- a/Sources/Services/DockPresence.swift
+++ b/Sources/Services/DockPresence.swift
@@ -4,7 +4,7 @@ import Observation
/// Tracks which "dockable" windows (player, sign-in) are currently on
/// screen and flips `NSApp`'s activation policy accordingly. The app
/// launches as `.accessory` (menubar-only, LSUIElement=true) and
-/// switches to `.regular` while any real window is visible — giving the
+/// switches to `.regular` while any real window is visible, giving the
/// user a Dock icon to switch to, a standard application menu, and a
/// Cmd+Tab slot.
///
diff --git a/Sources/Services/PlayerController.swift b/Sources/Services/PlayerController.swift
index 2451238..b2bc4c7 100644
--- a/Sources/Services/PlayerController.swift
+++ b/Sources/Services/PlayerController.swift
@@ -12,7 +12,7 @@ final class PlayerController {
self.videoId = videoId
self.title = title
Self.closeMenuBarPopover()
- // Activation happens in PlayerWindow.onAppear / onChange — calling
+ // Activation happens in PlayerWindow.onAppear / onChange, calling
// NSApp.activate here is a no-op while the app is still .accessory
// (the policy flip only lands once the window has appeared).
}
diff --git a/Sources/Services/UpdateChecker.swift b/Sources/Services/UpdateChecker.swift
index 35ac6dd..f3f1105 100644
--- a/Sources/Services/UpdateChecker.swift
+++ b/Sources/Services/UpdateChecker.swift
@@ -31,7 +31,7 @@ final class UpdateChecker {
self.session = session
}
- /// Bundle's `CFBundleShortVersionString` — the value the comparison is
+ /// Bundle's `CFBundleShortVersionString`, the value the comparison is
/// made against. Anything non-semver (e.g. "main-abc1234" from a nightly)
/// is treated as "older than any tagged release", which is the right
/// behaviour for users on the rolling channel.
diff --git a/Sources/Services/YouTubeService.swift b/Sources/Services/YouTubeService.swift
index c7c26d9..c79015d 100644
--- a/Sources/Services/YouTubeService.swift
+++ b/Sources/Services/YouTubeService.swift
@@ -7,8 +7,8 @@ private let log = Logger(subsystem: "app.youmenutube", category: "yt-service")
/// Single auth + API surface for the app. Uses YouTubeKit (InnerTube) for
/// everything, signed in via cookies imported from the user's own browser
-/// (Safari / Chrome / Firefox / etc — see `BrowserCookieImporter`). No
-/// Google Cloud project, no API key, no OAuth — just a YouTube login
+/// (Safari / Chrome / Firefox / etc.; see `BrowserCookieImporter`). No
+/// Google Cloud project, no API key, no OAuth; just a YouTube login
/// handed to us by the browser the user already uses.
///
/// Trade-off: InnerTube is YouTube's *internal* API. It can break and is
@@ -20,7 +20,7 @@ final class YouTubeService {
var lastError: String?
/// Video IDs known to be in the user's Watch Later. Populated lazily on
- /// sign-in (first page only — covers the most-recent items) and updated
+ /// sign-in (first page only, covers the most-recent items) and updated
/// optimistically on add/remove. Reads are local; only the toggle action
/// itself hits the network.
private(set) var watchLaterIds: Set = []
@@ -37,7 +37,7 @@ final class YouTubeService {
// Pretend we're signed in with a pre-seeded Watch Later so every
// tab has content and the "Sign in" chrome is hidden. No keychain
// read, no network.
- log.notice("demo mode active — serving fixtures from DemoData.swift")
+ log.notice("demo mode active, serving fixtures from DemoData.swift")
isSignedIn = true
watchLaterIds = DemoData.watchLaterIds
return
@@ -76,13 +76,13 @@ final class YouTubeService {
lastError = nil
}
- /// Called when any authenticated endpoint reports `isDisconnected` —
+ /// Called when any authenticated endpoint reports `isDisconnected`,
/// YouTube rejected our cookies despite them being present locally.
/// Wipes the stale session so every tab (Home included) reflects the
/// signed-out state consistently, instead of Home silently falling
/// through to the public feed while other tabs surface an error.
private func handleDisconnected() {
- log.notice("server reported disconnected session — wiping local cookies")
+ log.notice("server reported disconnected session, wiping local cookies")
lastError = YTServiceError.disconnected.errorDescription
signOut()
}
@@ -101,7 +101,7 @@ final class YouTubeService {
guard !relevant.isEmpty else { return false }
// A browser stores cookies keyed by (name, domain, path) and sends
- // only the ones whose path prefixes the request path — one value
+ // only the ones whose path prefixes the request path, one value
// per name, with the longest matching path winning. If we concatenate
// every row from the store we can end up with e.g. three
// `VISITOR_INFO1_LIVE=…` values in the header (paths `/`, `/embed`,
@@ -250,7 +250,7 @@ final class YouTubeService {
}
}
- /// Reload the Watch Later membership cache. Only fetches the first page —
+ /// Reload the Watch Later membership cache. Only fetches the first page,
/// far-back items won't show as "saved" until the user toggles them, but
/// the optimistic local cache keeps recent toggles accurate.
func refreshWatchLaterIds() {
@@ -326,7 +326,7 @@ enum YTServiceError: LocalizedError {
case .notSignedIn: return "Sign in to YouTube first."
case .disconnected:
return
- "YouTube rejected the session. Sign out and re-import from your browser — make sure the browser is signed in to youtube.com, not just accounts.google.com."
+ "YouTube rejected the session. Sign out and re-import from your browser. Make sure the browser is signed in to youtube.com, not just accounts.google.com."
case .actionFailed:
return "YouTube rejected the action."
}
diff --git a/Sources/Utilities/Collections.swift b/Sources/Utilities/Collections.swift
index 24c8d81..9bd354c 100644
--- a/Sources/Utilities/Collections.swift
+++ b/Sources/Utilities/Collections.swift
@@ -3,7 +3,7 @@ import Foundation
extension Array {
/// Returns elements with unique values at `keyPath`, keeping the first
/// occurrence. Used to normalise model arrays before handing them to
- /// `ForEach` — YouTubeKit occasionally returns the same video id twice
+ /// `ForEach`, YouTubeKit occasionally returns the same video id twice
/// in one response (e.g. a shelf item that also appears inline), which
/// triggers SwiftUI runtime warnings about duplicate ids.
func uniqued(by keyPath: KeyPath) -> [Element] {
diff --git a/Sources/Views/ImportSessionWindow.swift b/Sources/Views/ImportSessionWindow.swift
index a5aa098..2f9236e 100644
--- a/Sources/Views/ImportSessionWindow.swift
+++ b/Sources/Views/ImportSessionWindow.swift
@@ -5,8 +5,8 @@ import SwiftUI
private let importLog = Logger(subsystem: "app.youmenutube", category: "import-session")
/// Replacement for the old `YouTubeSignInWindow`. The user signs in to
-/// YouTube *in their own browser* — where passkeys and password managers
-/// all work natively — and we import their `youtube.com` cookies from that
+/// YouTube *in their own browser*, where passkeys and password managers
+/// all work natively, and we import their `youtube.com` cookies from that
/// browser's on-disk cookie store.
///
/// See `BrowserCookieImporter` for the mechanics, and issue #8 for why the
@@ -56,7 +56,7 @@ struct ImportSessionWindow: View {
Text("Import your YouTube session")
.font(.title2).bold()
Text(
- "Sign in to YouTube in your browser — passkeys, password managers, and all. Then pick that browser below and we'll copy the session into YouMenuTube."
+ "Sign in to YouTube in your browser (passkeys, password managers, and all). Then pick that browser below and we'll copy the session into YouMenuTube."
)
.font(.callout)
.foregroundStyle(.secondary)
@@ -117,7 +117,7 @@ struct ImportSessionWindow: View {
Banner(
icon: "lock.shield",
text:
- "macOS will ask for your login password once — that's the standard Keychain prompt that lets YouMenuTube read \(browser.displayName)'s cookie-encryption key. Click **Always Allow** to skip it on later imports. Touch ID may appear on Macs that support it."
+ "macOS will ask for your login password once. That's the standard Keychain prompt that lets YouMenuTube read \(browser.displayName)'s cookie-encryption key. Click **Always Allow** to skip it on later imports. Touch ID may appear on Macs that support it."
)
case .safari:
Banner(
@@ -241,14 +241,14 @@ struct ImportSessionWindow: View {
}
private func bringToFront() {
- // `NSApp.activate(ignoringOtherApps:)` was softened in macOS 14 — it
+ // `NSApp.activate(ignoringOtherApps:)` was softened in macOS 14, it
// only activates if the caller was recently user-facing, which a
// MenuBarExtra popover is not once it closes. `activate()` (no arg)
// is the replacement and works reliably for LSUIElement apps.
NSApp.activate()
// The NSWindow usually isn't in `NSApp.windows` yet when `onAppear`
// fires on first open. Poll a few times instead of assuming one
- // runloop is enough — fixes the "Sign in button did nothing"
+ // runloop is enough, fixes the "Sign in button did nothing"
// symptom when the host app is an LSUIElement + MenuBarExtra.
Task { @MainActor in
for _ in 0..<10 {
diff --git a/Sources/Views/PlayerWindow.swift b/Sources/Views/PlayerWindow.swift
index e19b3ac..898ea58 100644
--- a/Sources/Views/PlayerWindow.swift
+++ b/Sources/Views/PlayerWindow.swift
@@ -53,7 +53,7 @@ struct PlayerWindow: View {
}
.onChange(of: player.videoId) { _, newId in
// Playing a fresh video while the window is already open
- // should still pull focus — otherwise the menubar click
+ // should still pull focus, otherwise the menubar click
// swaps the video silently in the background.
if newId != nil { bringToFront() }
}
diff --git a/Sources/Views/SettingsView.swift b/Sources/Views/SettingsView.swift
index d93a55d..61428dd 100644
--- a/Sources/Views/SettingsView.swift
+++ b/Sources/Views/SettingsView.swift
@@ -37,7 +37,7 @@ struct SettingsView: View {
}
}
Text(
- "Uses YouTube's own internal API via your browser session — no Google Cloud setup needed. Personal use only."
+ "Uses YouTube's own internal API via your browser session. Personal use only."
)
.font(.caption2).foregroundStyle(.secondary)
}
@@ -133,7 +133,7 @@ struct SettingsView: View {
if let commit {
return "\(head) · \(commit)"
}
- return "\(head) — \(build)"
+ return "\(head) · \(build)"
}
@ViewBuilder
diff --git a/Sources/Views/VideoRow.swift b/Sources/Views/VideoRow.swift
index fac5739..d3c5774 100644
--- a/Sources/Views/VideoRow.swift
+++ b/Sources/Views/VideoRow.swift
@@ -113,7 +113,7 @@ struct ThumbnailView: View {
Color.gray.opacity(0.15)
}
}
- // Disable the implicit cross-fade — its layout pass jitters LazyVStack
+ // Disable the implicit cross-fade, its layout pass jitters LazyVStack
// when many rows finish loading in quick succession during fast scrolls.
.transaction { $0.animation = nil }
}
diff --git a/bootstrap.sh b/bootstrap.sh
index 09aee42..f884a23 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
-# YouMenuTube — one-shot bootstrap
+# YouMenuTube: one-shot bootstrap
#
# Usage:
# ./bootstrap.sh # install prereqs, generate project, open Xcode
@@ -45,7 +45,7 @@ xcode_ver=$(awk 'NR==1 { print $2 }' <<<"$xcodebuild_out")
xcode_ver=${xcode_ver:-0}
xcode_major=${xcode_ver%%.*}
if ! [[ "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 16 )); then
- yellow " Xcode $xcode_ver detected — this project targets macOS 15 and expects Xcode 16+."
+ yellow " Xcode $xcode_ver detected; this project targets macOS 15 and expects Xcode 16+."
fi
green " ✓ macOS + Xcode $xcode_ver"
diff --git a/project.yml b/project.yml
index ece8f32..6475c82 100644
--- a/project.yml
+++ b/project.yml
@@ -103,7 +103,7 @@ targets:
AHEAD=$(echo "$DESC" | sed -nE 's/.*-([0-9]+)-g[0-9a-f]+$/\1/p')
IS_DEV="dev+${AHEAD}"
else
- SHORT="" # no tags — keep whatever project.yml put in there
+ SHORT="" # no tags, keep whatever project.yml put in there
IS_DEV="dev"
fi
[ -n "$SHORT" ] && /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT" "$INFO_PLIST"
@@ -136,7 +136,7 @@ targets:
schemes:
YouMenuTube:
- # Explicit so it coexists with the Demo scheme below — defining any
+ # Explicit so it coexists with the Demo scheme below, defining any
# top-level schemes disables xcodegen's autogeneration for the target.
build:
targets:
diff --git a/tools/gen_app_icon.swift b/tools/gen_app_icon.swift
index 6bf807a..720a740 100644
--- a/tools/gen_app_icon.swift
+++ b/tools/gen_app_icon.swift
@@ -35,7 +35,7 @@ func render(sizePx: Int) -> Data {
cornerHeight: cornerRadius,
transform: nil)
- // Background gradient — bright red at top, deeper red at bottom.
+ // Background gradient, bright red at top, deeper red at bottom.
ctx.saveGState()
ctx.addPath(path)
ctx.clip()
@@ -53,7 +53,7 @@ func render(sizePx: Int) -> Data {
options: [])
ctx.restoreGState()
- // Top highlight — subtle glassy sheen.
+ // Top highlight, subtle glassy sheen.
ctx.saveGState()
ctx.addPath(path)
ctx.clip()
@@ -71,10 +71,10 @@ func render(sizePx: Int) -> Data {
options: [])
ctx.restoreGState()
- // Play triangle — equilateral pointing right, centroid at icon centre.
+ // Play triangle, equilateral pointing right, centroid at icon centre.
// The vertices (r, 0), (-r/2, ±r·√3/2) average to (0, 0), so placing
- // them relative to (cx, cy) puts the centroid — i.e. the visual
- // balance point — exactly at the icon's centre. The tip extends
+ // them relative to (cx, cy) puts the centroid, i.e. the visual
+ // balance point, exactly at the icon's centre. The tip extends
// further right than the back extends left, which is the "play
// button" look people expect (see SF Symbols' play.fill).
let cx = s / 2