mirror of
https://github.com/lwouis/alt-tab-macos.git
synced 2026-05-24 11:20:36 +00:00
9147a4a864
BREAKING CHANGE: announcement: https://github.com/lwouis/alt-tab-macos/discussions/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
277 lines
15 KiB
Python
Executable File
277 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Regenerate docs/readme/main.svg, the consolidated SVG that drives README.md.
|
|
|
|
The README is a single dark, on-brand image (hero + stats + CTAs + screenshot)
|
|
that hands GitHub visitors off to https://alt-tab.app/. To stay seamless across
|
|
GitHub light & dark themes, every visible element is baked into one SVG —
|
|
including the hero screenshot, embedded as a base64 JPG.
|
|
|
|
What this script does:
|
|
1. Reads a source screenshot (default: docs/readme/screenshot-source.webp).
|
|
2. Re-encodes it to a 1800-wide quality-86 JPG via ImageMagick.
|
|
3. Embeds it as base64 in the SVG template defined below.
|
|
4. Writes the result to docs/readme/main.svg.
|
|
|
|
This script is intentionally NOT wired into CI. Run it manually after updating
|
|
the source screenshot or the SVG layout. Stat values (downloads / GitHub stars)
|
|
are NOT regenerated here — they live behind <!--downloads--> / <!--stars-->
|
|
markers in main.svg and are refreshed on every release by
|
|
scripts/update_readme_and_website.sh.
|
|
|
|
Usage:
|
|
scripts/build_readme_svg.py [PATH_TO_SCREENSHOT]
|
|
|
|
Without an argument, reads docs/readme/screenshot-source.webp.
|
|
Pass a path to override (any format ImageMagick can read: webp, png, jpg).
|
|
|
|
Requires: python3, ImageMagick (`magick`).
|
|
"""
|
|
|
|
import base64
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
|
|
DEFAULT_SOURCE = os.path.join(REPO_ROOT, 'docs', 'readme', 'screenshot-source.webp')
|
|
OUTPUT_PATH = os.path.join(REPO_ROOT, 'docs', 'readme', 'main.svg')
|
|
|
|
|
|
def encode_screenshot(input_path: str) -> str:
|
|
"""Re-encode the source image to 1800-wide quality-86 JPG, return base64."""
|
|
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
|
|
tmp_path = tmp.name
|
|
try:
|
|
subprocess.run(
|
|
['magick', input_path, '-resize', '1800x>', '-quality', '86', tmp_path],
|
|
check=True,
|
|
)
|
|
with open(tmp_path, 'rb') as f:
|
|
return base64.b64encode(f.read()).decode('ascii')
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def build_svg(screenshot_b64: str) -> str:
|
|
"""
|
|
Build the SVG. Layout (y coordinates inside a 900x1040 viewBox):
|
|
50 - 194 : icon (144x144, vertically centered with the title+tagline stack)
|
|
120 : "AltTab Pro" title baseline
|
|
158 : tagline baseline
|
|
220 - 360 : stats container (deco 140x140 with number + label inside)
|
|
365 - 421 : CTA #1
|
|
455 - 916 : screenshot (820x461, rounded corners)
|
|
950 - 1006: CTA #2
|
|
|
|
Gradients (matching the website's _buttons.scss exactly):
|
|
- Pro badge: 36deg, 60px period, animated 8s (one full horizontal period)
|
|
- CTA pill : 36deg, 150px period, animated 14s (one full horizontal period)
|
|
|
|
Stat values (7.4M, 15K) are placeholders — they sit behind XML comment
|
|
markers (<!--downloads-->, <!--stars-->) so update_readme_and_website.sh
|
|
can replace them via sed without re-running this script.
|
|
"""
|
|
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!--
|
|
AUTOGENERATED by scripts/build_readme_svg.py — do not edit by hand.
|
|
Stat values are refreshed by scripts/update_readme_and_website.sh.
|
|
-->
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1040" viewBox="0 0 900 1040">
|
|
<defs>
|
|
<linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" stop-color="#13132a"/>
|
|
<stop offset="40%" stop-color="#0f0f24"/>
|
|
<stop offset="100%" stop-color="#0a0a1a"/>
|
|
</linearGradient>
|
|
<radialGradient id="glow1" cx="50%" cy="6%" r="40%">
|
|
<stop offset="0%" stop-color="#4488ff" stop-opacity="0.10"/>
|
|
<stop offset="100%" stop-color="#0a0a1a" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<radialGradient id="glow2" cx="50%" cy="40%" r="30%">
|
|
<stop offset="0%" stop-color="#8F47EB" stop-opacity="0.08"/>
|
|
<stop offset="100%" stop-color="#0a0a1a" stop-opacity="0"/>
|
|
</radialGradient>
|
|
|
|
<linearGradient id="proGrad"
|
|
x1="0" y1="48.541" x2="35.267" y2="0"
|
|
gradientUnits="userSpaceOnUse"
|
|
spreadMethod="repeat">
|
|
<stop offset="0" stop-color="#FF4488"/>
|
|
<stop offset="0.333" stop-color="#4488ff"/>
|
|
<stop offset="0.667" stop-color="#66CCFF"/>
|
|
<stop offset="1" stop-color="#FF4488"/>
|
|
<animateTransform attributeName="gradientTransform" type="translate"
|
|
values="0,0; -102.107,0" dur="8s" repeatCount="indefinite"/>
|
|
</linearGradient>
|
|
|
|
<linearGradient id="ctaGrad"
|
|
x1="0" y1="121.353" x2="88.167" y2="0"
|
|
gradientUnits="userSpaceOnUse"
|
|
spreadMethod="repeat">
|
|
<stop offset="0" stop-color="#FF4488"/>
|
|
<stop offset="0.333" stop-color="#4488ff"/>
|
|
<stop offset="0.667" stop-color="#66CCFF"/>
|
|
<stop offset="1" stop-color="#FF4488"/>
|
|
<animateTransform attributeName="gradientTransform" type="translate"
|
|
values="0,0; -255.267,0" dur="14s" repeatCount="indefinite"/>
|
|
</linearGradient>
|
|
|
|
<filter id="ctaShadow" x="-10%" y="-30%" width="120%" height="160%">
|
|
<feDropShadow dx="0" dy="1" stdDeviation="1" flood-color="#000000" flood-opacity="0.25"/>
|
|
</filter>
|
|
|
|
<linearGradient id="iconCardA" x1="50%" x2="50%" y1="0%" y2="100%">
|
|
<stop stop-color="#d619ac"/>
|
|
<stop offset="1" stop-color="#d0224c"/>
|
|
</linearGradient>
|
|
<linearGradient id="iconCardB" x1="50%" x2="50%" y1="0%" y2="100%">
|
|
<stop stop-color="#1ddfdf"/>
|
|
<stop offset="1" stop-color="#1d8cdc"/>
|
|
</linearGradient>
|
|
<linearGradient id="iconCardC" x1="50%" x2="50%" y1="0%" y2="100%">
|
|
<stop stop-color="#161386"/>
|
|
<stop offset="1" stop-color="#0a093d"/>
|
|
</linearGradient>
|
|
<clipPath id="screenshotClip">
|
|
<rect x="40" y="455" width="820" height="461" rx="14"/>
|
|
</clipPath>
|
|
</defs>
|
|
|
|
<rect width="900" height="1040" fill="url(#bg)"/>
|
|
<rect width="900" height="1040" fill="url(#glow1)"/>
|
|
<rect width="900" height="1040" fill="url(#glow2)"/>
|
|
|
|
<g fill="#ffffff">
|
|
<circle cx="60" cy="40" r="0.9" opacity="0.55"/>
|
|
<circle cx="135" cy="80" r="0.6" opacity="0.35"/>
|
|
<circle cx="200" cy="30" r="1.1" opacity="0.45"/>
|
|
<circle cx="42" cy="160" r="0.5" opacity="0.3"/>
|
|
<circle cx="170" cy="180" r="1" opacity="0.5"/>
|
|
<circle cx="50" cy="280" r="0.6" opacity="0.4"/>
|
|
<circle cx="120" cy="340" r="0.7" opacity="0.45"/>
|
|
<circle cx="40" cy="430" r="0.8" opacity="0.4"/>
|
|
<circle cx="160" cy="380" r="0.5" opacity="0.3"/>
|
|
<circle cx="80" cy="430" r="0.9" opacity="0.5"/>
|
|
<circle cx="700" cy="35" r="0.6" opacity="0.4"/>
|
|
<circle cx="780" cy="80" r="1" opacity="0.5"/>
|
|
<circle cx="840" cy="40" r="0.7" opacity="0.45"/>
|
|
<circle cx="860" cy="170" r="0.6" opacity="0.35"/>
|
|
<circle cx="730" cy="220" r="0.9" opacity="0.5"/>
|
|
<circle cx="820" cy="320" r="0.5" opacity="0.3"/>
|
|
<circle cx="870" cy="430" r="0.7" opacity="0.45"/>
|
|
<circle cx="730" cy="380" r="0.5" opacity="0.3"/>
|
|
<circle cx="820" cy="430" r="0.6" opacity="0.35"/>
|
|
<circle cx="450" cy="22" r="0.5" opacity="0.3"/>
|
|
<circle cx="60" cy="940" r="0.8" opacity="0.45"/>
|
|
<circle cx="120" cy="1000" r="0.5" opacity="0.3"/>
|
|
<circle cx="180" cy="980" r="0.7" opacity="0.4"/>
|
|
<circle cx="780" cy="930" r="0.6" opacity="0.4"/>
|
|
<circle cx="840" cy="1000" r="0.7" opacity="0.45"/>
|
|
<circle cx="730" cy="980" r="0.5" opacity="0.3"/>
|
|
<circle cx="40" cy="1010" r="0.6" opacity="0.35"/>
|
|
<circle cx="860" cy="940" r="0.5" opacity="0.3"/>
|
|
</g>
|
|
|
|
<g transform="translate(166 50)">
|
|
<svg width="144" height="144" viewBox="0 0 55 55">
|
|
<g><rect width="43.171" height="36.695" x="1.843" y="15.476" fill="url(#iconCardA)" rx="1.125" transform="rotate(-9 1.843 15.476)"/><rect width="43.171" height="36.695" x="5.771" y="9.315" fill="url(#iconCardB)" rx="1.125" transform="rotate(-9 5.771 9.315)"/><path fill="url(#iconCardC)" d="M6.481 13.787 43.49 7.926a1.125 1.125 0 0 1 1.287.935l4.856 30.658-37.009 5.862a1.125 1.125 0 0 1-1.287-.935z"/><path fill="#fff" fill-rule="evenodd" d="m43.728 21.934-9.955 8.01-.639-4.033-6.662 1.055-.714-4.51L32.42 21.4l-.635-4.009z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="m12.892 34.939 11.943 4.542-.639-4.033 6.663-1.055-.714-4.51-6.663 1.055-.635-4.01z" clip-rule="evenodd"/><circle cx="12.121" cy="18.181" r="1.923" fill="#fff" transform="rotate(-9 12.121 18.18)"/><circle cx="17.55" cy="17.322" r="1.923" fill="#fff" transform="rotate(-9 17.55 17.322)"/><circle cx="23.036" cy="16.452" r="1.923" fill="#fff" transform="rotate(-9 23.036 16.452)"/></g>
|
|
</svg>
|
|
</g>
|
|
|
|
<text x="330" y="120" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="700" font-size="56" fill="#ffffff" letter-spacing="-1.5">AltTab <tspan fill="url(#proGrad)">Pro</tspan></text>
|
|
|
|
<text x="330" y="158" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="400" font-size="22" fill="#8888a8">See every window. Switch in an instant.</text>
|
|
|
|
<g transform="translate(170 220)">
|
|
<g transform="translate(80 0)" opacity="0.85">
|
|
<svg width="140" height="140" viewBox="0 0 120 120">
|
|
<g fill="none" stroke="#7EB6FF" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 16v10 M18 22l4 4 4-4" stroke-width="2" opacity=".6"/>
|
|
<path d="M45 8v8 M42 13l3 3 3-3" stroke-width="1.5" opacity=".35"/>
|
|
<path d="M78 10v8 M75 15l3 3 3-3" stroke-width="1.5" opacity=".4"/>
|
|
<path d="M100 18v10 M96 24l4 4 4-4" stroke-width="2" opacity=".55"/>
|
|
<path d="M12 52v8 M9 57l3 3 3-3" stroke-width="1.5" opacity=".3"/>
|
|
<path d="M60 30v14 M54 39l6 5 6-5" stroke-width="2.5" opacity=".7"/>
|
|
<path d="M108 50v8 M105 55l3 3 3-3" stroke-width="1.5" opacity=".3"/>
|
|
<path d="M30 86v10 M26 92l4 4 4-4" stroke-width="2" opacity=".55"/>
|
|
<path d="M58 96v8 M55 101l3 3 3-3" stroke-width="1.5" opacity=".45"/>
|
|
<path d="M90 88v10 M86 94l4 4 4-4" stroke-width="2" opacity=".5"/>
|
|
</g>
|
|
<g fill="#7EB6FF">
|
|
<circle cx="38" cy="52" r="1" opacity=".3"/>
|
|
<circle cx="82" cy="48" r="1" opacity=".3"/>
|
|
<circle cx="50" cy="72" r=".8" opacity=".25"/>
|
|
<circle cx="72" cy="78" r=".8" opacity=".25"/>
|
|
</g>
|
|
</svg>
|
|
</g>
|
|
<text x="150" y="78" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="700" font-size="42" fill="#ffffff" letter-spacing="-0.5"><!--downloads-->7.4M</text>
|
|
<text x="150" y="115" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="500" font-size="14" fill="#8888a8" letter-spacing="2.4">DOWNLOADS</text>
|
|
</g>
|
|
|
|
<g transform="translate(450 220)">
|
|
<g transform="translate(80 0)" opacity="0.85">
|
|
<svg width="140" height="140" viewBox="0 0 120 120">
|
|
<g fill="#F5D97E">
|
|
<path d="m22 28 2-6 2 6 6 2-6 2-2 6-2-6-6-2Z" opacity=".7"/>
|
|
<path d="m90 20 1.5-5 1.5 5 5 1.5-5 1.5-1.5 5-1.5-5-5-1.5Z" opacity=".5"/>
|
|
<path d="m15 70 1.5-5 1.5 5 5 1.5-5 1.5-1.5 5-1.5-5-5-1.5Z" opacity=".45"/>
|
|
<path d="m98 72 2-6 2 6 6 2-6 2-2 6-2-6-6-2Z" opacity=".6"/>
|
|
<path d="m50 10 1-3 1 3 3 1-3 1-1 3-1-3-3-1Z" opacity=".35"/>
|
|
<path d="m70 8 1-3 1 3 3 1-3 1-1 3-1-3-3-1Zm-40 92 1.5-5 1.5 5 5 1.5-5 1.5-1.5 5-1.5-5-5-1.5Z" opacity=".45"/>
|
|
<path d="m85 98 1.5-5 1.5 5 5 1.5-5 1.5-1.5 5-1.5-5-5-1.5Z" opacity=".5"/>
|
|
<circle cx="40" cy="18" r="1" opacity=".3"/>
|
|
<circle cx="80" cy="15" r=".8" opacity=".25"/>
|
|
<circle cx="12" cy="50" r=".8" opacity=".25"/>
|
|
<circle cx="108" cy="50" r="1" opacity=".3"/>
|
|
<circle cx="60" cy="105" r=".8" opacity=".25"/>
|
|
</g>
|
|
</svg>
|
|
</g>
|
|
<text x="150" y="78" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="700" font-size="42" fill="#ffffff" letter-spacing="-0.5"><!--stars-->15K</text>
|
|
<g transform="translate(150 110)">
|
|
<g transform="translate(-79 -7) scale(0.143)" opacity="0.7">
|
|
<path fill="#8888a8" fill-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a47 47 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0" clip-rule="evenodd"/>
|
|
</g>
|
|
<text x="-58" y="5" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="500" font-size="14" fill="#8888a8" letter-spacing="2.4">GITHUB STARS</text>
|
|
</g>
|
|
</g>
|
|
|
|
<g transform="translate(372 365)">
|
|
<rect width="156" height="56" rx="28" fill="url(#ctaGrad)"/>
|
|
<text x="78" y="36" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="700" font-size="22" fill="#ffffff" filter="url(#ctaShadow)">Get AltTab</text>
|
|
</g>
|
|
|
|
<image href="data:image/jpeg;base64,{screenshot_b64}" x="40" y="455" width="820" height="461" clip-path="url(#screenshotClip)" preserveAspectRatio="xMidYMid slice"/>
|
|
|
|
<g transform="translate(372 950)">
|
|
<rect width="156" height="56" rx="28" fill="url(#ctaGrad)"/>
|
|
<text x="78" y="36" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif" font-weight="700" font-size="22" fill="#ffffff" filter="url(#ctaShadow)">Get AltTab</text>
|
|
</g>
|
|
</svg>
|
|
'''
|
|
|
|
|
|
def main() -> None:
|
|
src = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_SOURCE
|
|
|
|
if not os.path.exists(src):
|
|
print(f'error: screenshot not found at {src}', file=sys.stderr)
|
|
print(f'pass a path as the first argument, or place the file at {DEFAULT_SOURCE}', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f'reading screenshot: {src}')
|
|
b64 = encode_screenshot(src)
|
|
svg = build_svg(b64)
|
|
|
|
with open(OUTPUT_PATH, 'w') as f:
|
|
f.write(svg)
|
|
|
|
print(f'wrote {os.path.getsize(OUTPUT_PATH):,} bytes to {OUTPUT_PATH}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|