Skip to content

Commit 554227b

Browse files
committed
feat: introducing alt-tab pro!
BREAKING CHANGE: announcement: #5533 * improved performance, especially switcher responsiveness * reduced battery usage even more * reduced ram usage (closes #5450, closes #5539, closes #5627) * reduced app size even more * polished many aspects of the ui; align more with liquid glass * better handle "ghost" windows (closes #5509) * fix issue with wrong window order (closes #5492) * escape closes the switcher on tahoe (closes #5585) * improve search matches (closes #5488) * localizations trimmed and reviewed entirely (closes #5583) * highlight matching app icons when searching, in addition to text * better settings import/export * reworked "send feedback" experience * reworked exceptions ui (closes #5482) * per-shortcut settings (closes #5313) * rework about window
1 parent 317a485 commit 554227b

1,893 files changed

Lines changed: 81585 additions & 65025 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
name: assets-optimization
3+
description: Audit and optimize every image asset shipped with AltTab. Apply the right format per asset class (PDF for vectors, HEIC for raster) and the right post-processing (strip Figma cruft from PDFs, extract SF Symbols as minimal vector PDFs, encode raster sources to HEIC at q50 with visual review). Use whenever new assets are added, when the bundle size needs shrinking, or whenever you want a full assets audit.
4+
---
5+
6+
# /assets-optimization — AltTab asset audit and optimization
7+
8+
## Goal
9+
10+
Every byte that ships in `AltTab.app/Contents/Resources/` should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.
11+
12+
This skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.
13+
14+
## When to use
15+
16+
- A designer drops new exports into `~/Desktop/` or `resources/`.
17+
- Someone asks "why is the bundle so big?".
18+
- After adding a new icon, illustration, app icon variant, or menubar variant.
19+
- Periodic audit when nothing else is broken.
20+
21+
## Step 1: Inventory
22+
23+
Run a one-shot enumeration so you know what you're working with:
24+
25+
```sh
26+
find resources -type f \( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \) \
27+
-exec ls -la {} \; | awk '{printf "%8d %s\n", $5, $9}' | sort -k2
28+
```
29+
30+
Group what you see by directory. For AltTab the relevant buckets are:
31+
32+
- `resources/icons/menubar/` — small template icons shown in the macOS menubar.
33+
- `resources/icons/tabs/` — Preferences sidebar icons (template, sized ~13pt).
34+
- `resources/icons/permission-window/` — first-launch permission window icons (~32pt).
35+
- `resources/icons/app/` — the macOS app icon (`.icns` + `.iconset/`). **Don't touch**`.icns` is required by the bundle and Apple's tooling produces near-optimal output already.
36+
- `resources/illustrations/` — the appearance-tab preview thumbnails. Raster (screenshots inside).
37+
38+
For each asset, decide what category it falls into:
39+
40+
| Source content | Right format | Why |
41+
|---|---|---|
42+
| Custom vector design (Figma/Sketch/Illustrator) | **PDF** | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. |
43+
| SF Symbol (Apple system glyph) | **Font glyph** in the bundled `SF-Pro-Text-Regular.otf` subset | Render via `NSImage.fromSymbol(.foo, pointSize:)` (or as text via the `Symbols` enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |
44+
| Photographic / screenshot-heavy | **HEIC** | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |
45+
| Tiny pixel-precise UI sprite | **PNG @2x** | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |
46+
| App icon (the macOS bundle one) | **`.icns`** | Required by `CFBundleIconFile`. |
47+
48+
If an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.
49+
50+
## Step 2: Vector PDFs — Figma exports
51+
52+
Figma's "Export → PDF" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel.
53+
54+
What Figma adds that AppKit doesn't need:
55+
56+
1. **Embedded ICC color profile** (`/ICCBased ...`, ~3.2 KB compressed). Replace every `[/ICCBased N R]` reference with `/DeviceRGB` (or `/DeviceGray` for monochrome). Patches needed in:
57+
- the page `Resources/ColorSpace` dict
58+
- every Form XObject's `Resources/ColorSpace` dict (these are streams, not plain dicts — pikepdf's `pdf.objects` will only catch them if you accept both `Dictionary` and `Stream`)
59+
- every Image XObject's direct `/ColorSpace` key
60+
- every Shading dict inside `Pattern` entries (Figma's color icons use 8+ patterns, each with its own `/ColorSpace N R` reference)
61+
2. **`/Metadata`** XMP packet (~830 B) — Figma's XML manifest.
62+
3. **`/StructTreeRoot`, `/ParentTree`, `/StructElem`** — accessibility tags ("Document" / "Part" structural roles). AppKit's PDF renderer ignores them.
63+
4. **`/Info` dict**`Producer="Figma"`, `Title="Menubar 22x22@1x white"`. In Figma's exports the Info dict sometimes lives **inline inside the Catalog** rather than at the trailer level, so deleting `pdf.docinfo` isn't enough — also `del root[Name('/Info')]`.
64+
5. **`/Lang`, `/MarkInfo`, `/Annots`, `/StructParents`, `/Tabs`** — empty or trivial page-level entries.
65+
6. **`/ProcSet [/PDF]`** — deprecated since PDF 1.4.
66+
67+
Use the script:
68+
69+
```sh
70+
python3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf
71+
```
72+
73+
It edits in place and prints the savings per file. After running, also pipe through `mutool clean -ggg -z` and `qpdf --object-streams=generate --recompress-flate --compression-level=9` for the final 1–2% squeeze.
74+
75+
Verify each file still renders by sips'ing it back to PNG and eyeballing:
76+
77+
```sh
78+
for f in resources/icons/menubar/*.pdf; do
79+
sips -s format png "$f" --out "/tmp/$(basename $f .pdf).png" -Z 300 >/dev/null 2>&1
80+
done
81+
```
82+
83+
Open the PNGs in Preview to confirm nothing visual changed.
84+
85+
## Step 3: SF Symbols via font subset
86+
87+
Every SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.
88+
89+
How it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's `SF-Pro-Text-Regular.otf` contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into `resources/SF-Pro-Text-Regular.otf`, and register it via `Info.plist:ATSApplicationFontsPath = ""`. At runtime, `NSFont(name: "SF Pro Text", size:)` resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.
90+
91+
To add a new SF Symbol:
92+
93+
1. Open [SF Symbols.app](https://developer.apple.com/sf-symbols/), search for the symbol, press **Cmd-C** to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses `uniXXXXXX.medium`-style names rather than semantic ones, so this manual lookup is the authoritative path.)
94+
2. Paste the character into a new case on the `Symbols` enum in [src/switcher/main-window/TileFontIconView.swift](src/switcher/main-window/TileFontIconView.swift) — e.g., `case foo = "􀝥" // SF Symbol name`.
95+
3. Paste the same character at the end of the `--text=` argument in [scripts/assets/subset_font.sh](scripts/assets/subset_font.sh).
96+
4. Run `bash scripts/assets/subset_font.sh`. It reads `/Library/Fonts/SF-Pro-Text-Regular.otf` (installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset to `resources/SF-Pro-Text-Regular.otf`. Picks up Apple's latest glyph refinements automatically.
97+
5. Use it in code: `NSImage.fromSymbol(.foo, pointSize: 14)` returns a template `NSImage`; or `TileFontIconView(symbol: .foo, ...)` for the cached-attributed-string path in the switcher hot loop.
98+
99+
The script runs `pyftsubset` via the project's pipenv environment. Warnings about `MERG`/`meta`/`trak` tables being dropped are normal — those tables aren't relevant to glyph rendering.
100+
101+
### Historical note: SF Symbols via PDF (deprecated, scripts removed)
102+
103+
A previous pipeline shipped each SF Symbol as a per-glyph PDF in `resources/icons/`. The pipeline lives only in git history now (`scripts/assets/export_sf_symbol_pdf.swift` was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):
104+
105+
- The naive route — `NSImage(systemSymbolName:).draw(in:)` against a PDF `CGContext` — produces a **black rectangle**, because Quartz emits `image-mask + fill-rectangle` operators where the rectangle paints over the mask. AppKit bug at the PDF emission level; `paletteColors` config does not fix it.
106+
- The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:
107+
`NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform("vectorGlyph") (CUINamedVectorGlyph) → .perform("CGPath") (real CGPath)`.
108+
- The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via `CGPath.applyWithBlock` and emit raw PDF operators (`m`, `l`, `c`, `h`, `f`). Non-zero winding fill. Fill with DeviceGray (`0 g`), **not** `NSColor.black.cgColor`, which drags in a ~3.4 KB ICC color profile.
109+
- Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no `/Info`, no `/Metadata`, no `/Resources/ColorSpace`. ~750–1800 bytes per icon.
110+
111+
Git: see commit `990c1e79` ("feat: pro improve assets") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.
112+
113+
## Step 4: Raster → HEIC at q50
114+
115+
For anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.
116+
117+
Pipeline (built-in to macOS via `sips`):
118+
119+
```sh
120+
sips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic
121+
```
122+
123+
- `-Z 1000` resizes the longest edge to 1000px **preserving aspect ratio**. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.
124+
- `formatOptions 50` is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.
125+
126+
Use the script for batch conversion:
127+
128+
```sh
129+
bash scripts/assets/encode_heic.sh ~/Desktop 1000 50
130+
```
131+
132+
That walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to `/tmp/heic-out/`.
133+
134+
### Visual review (mandatory)
135+
136+
Before swapping the new HEICs into `resources/`, **always** decode a representative sample back to PNG and visually compare against the source:
137+
138+
```sh
139+
sips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1
140+
open /tmp/sample-decoded.png /Users/you/Desktop/sample.png
141+
```
142+
143+
Pick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:
144+
145+
- No banding in flat color regions
146+
- Text edges still crisp at native display size
147+
- No haloing around anti-aliased edges
148+
- Color rendition matches
149+
150+
If anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.
151+
152+
### Bumping quality
153+
154+
If q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.
155+
156+
## Step 5: pbxproj registration
157+
158+
Whenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update [alt-tab-macos.xcodeproj/project.pbxproj](alt-tab-macos.xcodeproj/project.pbxproj). The places that need patching:
159+
160+
1. **PBXBuildFile section** — comment + the comment inside `fileRef = ... /* name.ext */`.
161+
2. **PBXFileReference section** — comment, `lastKnownFileType` (e.g. `image.pdf`, `image.heic`, `image.png`), and `path = "name.ext"`.
162+
3. **PBXGroup section** — the file's entry inside its parent group's `children`.
163+
4. **PBXResourcesBuildPhase section** — the entry in the main app target's `files`.
164+
165+
For pure extension swaps (no new files), `sed -i '' 's|old\.ext|new.ext|g'` plus a `lastKnownFileType` substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (`python3 -c "import secrets; print(secrets.token_hex(12).upper())"`) and insert in all four places.
166+
167+
If you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with "missing file" or ships dangling references.
168+
169+
## Step 6: Verify
170+
171+
```sh
172+
bash ai/build.sh # must show ** BUILD SUCCEEDED **
173+
bash ai/run.sh # launch the app and visually inspect every asset
174+
```
175+
176+
Walk through every UI surface that loads an asset:
177+
178+
- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)
179+
- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)
180+
- Permissions window — open by revoking a permission
181+
- Preferences → Appearance — illustration thumbnails change per show/hide row
182+
183+
Compare `git diff --stat` before committing. Asset replacements should net negative on bundle size.
184+
185+
## Reporting
186+
187+
After the run, report:
188+
189+
- File-by-file before/after sizes for everything that changed.
190+
- Total bundle delta in KB.
191+
- Any files left untouched and why (e.g., `app.icns` — bundle-required format).
192+
- The encoder settings used (especially HEIC quality if not q50, so the next person knows).
193+
- Anything the visual review revealed (e.g., "had to bump to q60 for `thumbnails_dark` because gradient banding at q50").
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
name: translate-missing-l10n
3+
description: Translate missing l10n strings for AltTab using Claude's multilingual ability. Refreshes the source `Localizable.strings` from current Swift code, finds keys missing in each `<lang>.lproj/Localizable.strings`, translates them, then writes them back via `scripts/l10n/apply_translations.ts`.
4+
---
5+
6+
# /translate-missing-l10n
7+
8+
## Project context
9+
10+
AltTab is a macOS app that helps switch between windows, similar to the Windows alt-tab experience. The strings to translate are mostly Settings UI — some tooltips, dialogs, and general user guidance.
11+
12+
## Target languages (20)
13+
14+
`de, ja, fr, es, zh-CN, pt-BR, nl, ko, it, pl, ar, zh-HK, vi, tr, sv, th, zh-TW, he, id, ru`
15+
16+
(`en` is the source — handled automatically by the apply step.)
17+
18+
## Translation guidelines
19+
20+
- **Format specifiers must be preserved exactly**: `%@`, `%d`, `%1$@`, `%2$@`, `\n`, `\t`. The number and order of specifiers in the translation must match the source. Positional and unindexed forms are interchangeable (`%@ %@``%1$@ %2$@`), but the count must match.
21+
- **Do not translate proper nouns**: "AltTab", "macOS", "Mission Control", "Spaces", "Dock".
22+
- **Do not translate modifier key names**: "Cmd", "Option", "Alt", "Shift", "Ctrl", "Control", "Fn", "Command".
23+
- **Use Apple's official platform terminology** for the target locale where one exists. For example, prefer the term Apple uses in macOS System Settings for that locale ("Réglages" vs "Préférences" in French) over a literal translation.
24+
- **Match the source brevity**. Settings strings are short; the translation should be short too. Prefer concise, idiomatic phrasing over literal grammatical completeness.
25+
- **Match macOS tone**: neutral, direct, no exclamation marks unless the source has them.
26+
- **Comments in the source file are engineer guidance, not user-visible text**. Use them to disambiguate meaning, but never include them in the translation.
27+
28+
## Workflow
29+
30+
1. **Refresh the source.** Run:
31+
```sh
32+
bash scripts/l10n/extract_l10n_strings.sh
33+
```
34+
This regenerates `resources/l10n/Localizable.strings` from the current Swift code via `genstrings`.
35+
36+
2. **Parse the source.** Read `resources/l10n/Localizable.strings`. Each entry has the shape:
37+
```
38+
/* engineer comment */
39+
"key" = "value";
40+
```
41+
Build the ordered list of `(comment, key, value)` triples. The `value` for the source is usually identical to `key`, but may include positional indices (`%1$@`, `%2$@`) when there are multiple specifiers.
42+
43+
3. **Compute missing keys per language.** For each of the 20 target languages, read `resources/l10n/<lang>.lproj/Localizable.strings`. Each line is `"key" = "translation";`. A key is **missing** if it's present in the source but either (a) absent from the target file, or (b) present with an empty or whitespace-only value. Treat (b) the same as (a) — produce a real translation.
44+
45+
4. **Stop if nothing is missing.** Report and exit.
46+
47+
5. **Translate.** For each language, produce a translation for each missing key, applying the guidelines above. Do not invent keys; only translate keys that appear in the source. Group your work into batches of 5–10 languages per call to keep individual outputs manageable.
48+
49+
6. **Apply each batch.** Write the batch to a fresh file under `/tmp` (not committed) with this shape:
50+
```json
51+
{
52+
"fr": { "About %@": "À propos de %@", "Quit": "Quitter" },
53+
"ja": { "About %@": "%@について" }
54+
}
55+
```
56+
Then run:
57+
```sh
58+
npx ts-node scripts/l10n/apply_translations.ts /tmp/batch-NN.json
59+
```
60+
The helper:
61+
- Validates format specifiers — translations whose specifier set doesn't match the source value are rejected with a clear error and **not merged**.
62+
- Always rewrites `en.lproj/Localizable.strings` from source **keys** (each entry written as `"key" = "key";` for symmetry) — no need to include `en` in your batch. This avoids leaking genstrings-rewritten values like `%1$@` into the English file when the original source key uses plain `%@`.
63+
- Rewrites each `<lang>.lproj/Localizable.strings` using source order: existing translations are preserved, new translations merged in, and keys no longer in the source are pruned.
64+
- Exits with code `2` if any entries were rejected.
65+
66+
7. **Handle rejections.** If `apply_translations.ts` reports format-specifier mismatches, fix those translations and re-run only the affected language(s) in a follow-up batch. Do not move on while rejections are outstanding.
67+
68+
## Reporting
69+
70+
After the run, report:
71+
72+
- Number of languages processed.
73+
- Total translations produced and merged.
74+
- Any entries you intentionally left untranslated (e.g., the source was already a proper noun) — list them so the user can decide.
75+
- Any format-specifier rejections that required manual fixes.

.github/FUNDING.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
github: lwouis
22
patreon: alt_tab_macos
3-
custom: ["paypal.com/donate/?hosted_button_id=ACYAC3UYCM7CN", "donate.stripe.com/4gw9D7bA3g3O0ikaEF"]
3+
ko_fi: alt_tab
4+
custom: ["paypal.com/donate/?hosted_button_id=ACYAC3UYCM7CN"]

.github/workflows/ci_cd.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ env:
1010
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
1111
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
1212
APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }}
13-
FEEDBACK_TOKEN: ${{ secrets.FEEDBACK_TOKEN }}
14-
CLOUDFLARE_WEBHOOK: ${{ secrets.CLOUDFLARE_WEBHOOK }}
1513
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
1614
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
1715
GITHUB_EVENT_AFTER: ${{ github.event.after }}
@@ -67,3 +65,5 @@ jobs:
6765
body: ${{ steps.set_release_info.outputs.body }}
6866
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
6967
- run: scripts/update_website.sh
68+
env:
69+
GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }}

.gitignore

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ IDEWorkspaceChecks.plist
77
/node_modules/
88
/DerivedData/
99
/build/
10+
/vendor/*/.build/
11+
/vendor/*/.swiftpm/
12+
/vendor/*/Package.resolved
1013
.DS_Store
11-
/docs/_site/
12-
/docs/vendor/
1314
codesign.conf
1415
codesign.crt
1516
codesign.key
1617
codesign.p12
17-
/.claude/worktrees/
18+
/.claude/*
19+
!/.claude/skills/
20+
/config/local.xcconfig
21+
/scripts/l10n/missing-translations.json
22+
/ai/output/

.periphery.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
project: alt-tab-macos.xcworkspace
1+
project: alt-tab-macos.xcodeproj
22
schemes:
33
- Release
44
index_exclude:
5-
- Pods/**
5+
- vendor/**
66
- src/api-wrappers/private-apis/**

.swiftformatignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# src/**/*.swift: Matches all .swift files in the src directory and all its subdirectories.
33
# !node_modules/**: Excludes all files in the node_modules directory and all its subdirectories.
44

5-
Pods/**
65
Generated/**
76

87
**/PrivateApis.swift

0 commit comments

Comments
 (0)