Add Swift app for iPod contacts and calendar export

Replaces the bash/AppleScript/Python app bundle with a SwiftUI app backed
by a testable library target.

The app now reads Contacts and EventKit directly rather than sanitizing
files someone exported by hand, so the properties that break click-wheel
firmware are never written instead of being stripped afterwards: no PHOTO,
no X-, no VALARM, no VTIMEZONE, no TZID. Recurring events arrive from
EventKit already expanded into occurrences, so no RRULE is emitted either
- the open question from the previous version no longer applies.

Sanitizing existing .vcf/.ics files is kept as a second path, and gains
handling the Python lacked: vCard 2.1 quoted-printable soft line breaks,
Apple's item1. property groups, nested STANDARD/DAYLIGHT components inside
VTIMEZONE, and UTC times converted to local floating times.

Output is laid out as Contacts/ and Calendars/ subfolders mirroring the
device, one file per contact and one per calendar.

Built via SwiftPM plus Scripts/bundle.sh, which assembles and ad-hoc signs
the bundle. Ad-hoc signing is required rather than cosmetic: Contacts and
Calendars key permissions off the code signature.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C5X1tYo9oxAfvoiFMh1QQr
This commit is contained in:
2026-08-08 20:21:09 -03:00
co-authored by Claude Opus 5
commit 7b9d16e442
23 changed files with 2712 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
#
# Assembles build/iPod Contacts and Calendar Sync.app from the SwiftPM executable.
#
# SwiftPM builds a bare binary; macOS needs it inside a bundle with an
# Info.plist before Contacts and EventKit will even prompt for access, so the
# bundle is put together here rather than by an Xcode target.
#
# ./Scripts/bundle.sh # native architecture
# ./Scripts/bundle.sh --universal # arm64 + x86_64
#
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
APP_NAME="iPod Contacts and Calendar Sync"
BUNDLE_ID="ca.unsupervised.ipodsync"
VERSION="2.0"
MIN_MACOS="14.0"
BUILD_DIR="$ROOT/build"
APP="$BUILD_DIR/$APP_NAME.app"
# Bash 3.2 ships with macOS and treats an empty array as unbound under
# `set -u`, hence the ${ARCH_FLAGS[@]+…} guard at each use site.
ARCH_FLAGS=()
if [ "${1:-}" = "--universal" ]; then
ARCH_FLAGS=(--arch arm64 --arch x86_64)
fi
echo "Building…"
swift build --package-path "$ROOT" -c release ${ARCH_FLAGS[@]+"${ARCH_FLAGS[@]}"}
BIN_PATH="$(swift build --package-path "$ROOT" -c release ${ARCH_FLAGS[@]+"${ARCH_FLAGS[@]}"} --show-bin-path)"
echo "Assembling $APP_NAME.app…"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "$BIN_PATH/iPodSyncApp" "$APP/Contents/MacOS/$APP_NAME"
cat > "$APP/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>$APP_NAME</string>
<key>CFBundleDisplayName</key>
<string>$APP_NAME</string>
<key>CFBundleIdentifier</key>
<string>$BUNDLE_ID</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>CFBundleShortVersionString</key>
<string>$VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>$APP_NAME</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSMinimumSystemVersion</key>
<string>$MIN_MACOS</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSContactsUsageDescription</key>
<string>iPod Contacts and Calendar Sync reads your contacts so it can write one iPod-compatible vCard file per person.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>iPod Contacts and Calendar Sync reads your calendars so it can write iPod-compatible calendar files.</string>
<key>NSCalendarsUsageDescription</key>
<string>iPod Contacts and Calendar Sync reads your calendars so it can write iPod-compatible calendar files.</string>
</dict>
PLIST
echo "</plist>" >> "$APP/Contents/Info.plist"
# Ad-hoc signature. Contacts and Calendars identify an app by its code
# signature, so an unsigned bundle either re-prompts constantly or is denied
# outright. This is not a Developer ID signature — Gatekeeper still requires
# right-click → Open the first time.
echo "Signing (ad-hoc)"
codesign --force --sign - --timestamp=none "$APP" >/dev/null 2>&1
echo "Built $APP"