Skip to content

Commit 4010f9b

Browse files
Spanscreen 1.0: use your iPhone as a second Mac display
Mac menu bar app creates a virtual display, captures it with ScreenCaptureKit, encodes HEVC with VideoToolbox and streams it over Wi-Fi, peer-to-peer or USB (usbmuxd). The iOS app decodes in hardware, sends touch, trackpad and keyboard input back and follows fold, unfold and rotation on iPhone Duo. Devices pair with an X25519 key exchange and authenticate every reconnect with HMAC proofs. GitHub Actions build, sign, notarize and upload both apps.
0 parents  commit 4010f9b

90 files changed

Lines changed: 7346 additions & 0 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.

‎.github/workflows/ci.yml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
test:
14+
name: Protocol tests
15+
runs-on: macos-26
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: maxim-lobanov/setup-xcode@v1
19+
with:
20+
xcode-version: latest-stable
21+
- run: swift test --package-path Packages/SpanscreenKit
22+
23+
build:
24+
name: Build ${{ matrix.name }}
25+
runs-on: macos-26
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
include:
30+
- name: macOS
31+
scheme: SpanscreenMac
32+
destination: generic/platform=macOS
33+
- name: iOS
34+
scheme: SpanscreenIOS
35+
destination: generic/platform=iOS Simulator
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: maxim-lobanov/setup-xcode@v1
39+
with:
40+
xcode-version: latest-stable
41+
- name: Build
42+
run: |
43+
xcodebuild build \
44+
-project Spanscreen.xcodeproj \
45+
-scheme "${{ matrix.scheme }}" \
46+
-destination "${{ matrix.destination }}" \
47+
-derivedDataPath build \
48+
CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions || exit ${PIPESTATUS[0]}

‎.github/workflows/release.yml‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ['v*']
6+
workflow_dispatch:
7+
inputs:
8+
mac:
9+
description: Build, sign and notarize the Mac app
10+
type: boolean
11+
default: true
12+
ios:
13+
description: Build the iOS app and upload it to App Store Connect
14+
type: boolean
15+
default: true
16+
17+
permissions:
18+
contents: write
19+
20+
env:
21+
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
22+
23+
jobs:
24+
mac:
25+
name: Mac app (Developer ID)
26+
if: github.event_name == 'push' || inputs.mac
27+
runs-on: macos-26
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: maxim-lobanov/setup-xcode@v1
31+
with:
32+
xcode-version: latest-stable
33+
34+
- name: Version
35+
id: version
36+
run: |
37+
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then v="${GITHUB_REF#refs/tags/v}"; else v="$(grep -m1 'MARKETING_VERSION' project.yml | awk '{print $2}')"; fi
38+
echo "version=$v" >> "$GITHUB_OUTPUT"
39+
echo "build=$((GITHUB_RUN_NUMBER + 100))" >> "$GITHUB_OUTPUT"
40+
41+
- name: Import Developer ID certificate
42+
env:
43+
P12: ${{ secrets.MAC_CERTIFICATE_P12 }}
44+
P12_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
45+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
46+
run: scripts/ci-keychain.sh "$P12" "$P12_PASSWORD" "$KEYCHAIN_PASSWORD"
47+
48+
- name: Store App Store Connect key
49+
env:
50+
KEY: ${{ secrets.ASC_KEY_P8 }}
51+
run: |
52+
mkdir -p ~/.appstoreconnect/private_keys
53+
echo "$KEY" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.ASC_KEY_ID }}.p8
54+
55+
- name: Archive
56+
run: |
57+
xcodebuild archive \
58+
-project Spanscreen.xcodeproj \
59+
-scheme SpanscreenMac \
60+
-destination generic/platform=macOS \
61+
-archivePath build/Spanscreen-mac.xcarchive \
62+
MARKETING_VERSION=${{ steps.version.outputs.version }} \
63+
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }} \
64+
CODE_SIGN_STYLE=Manual \
65+
CODE_SIGN_IDENTITY="Developer ID Application" \
66+
DEVELOPMENT_TEAM=$TEAM_ID \
67+
OTHER_CODE_SIGN_FLAGS="--timestamp" | xcbeautify --renderer github-actions || exit ${PIPESTATUS[0]}
68+
69+
- name: Package, notarize and staple
70+
env:
71+
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
72+
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
73+
run: scripts/package-mac.sh build/Spanscreen-mac.xcarchive/Products/Applications/Spanscreen.app build/Spanscreen.dmg
74+
75+
- uses: actions/upload-artifact@v4
76+
with:
77+
name: Spanscreen-mac
78+
path: build/Spanscreen.dmg
79+
80+
- name: Publish release
81+
if: startsWith(github.ref, 'refs/tags/v')
82+
env:
83+
GH_TOKEN: ${{ github.token }}
84+
run: |
85+
tag="${GITHUB_REF#refs/tags/}"
86+
gh release view "$tag" >/dev/null 2>&1 || gh release create "$tag" --title "Spanscreen ${tag#v}" --generate-notes
87+
gh release upload "$tag" build/Spanscreen.dmg --clobber
88+
89+
ios:
90+
name: iOS app (App Store)
91+
if: github.event_name == 'push' || inputs.ios
92+
runs-on: macos-26
93+
steps:
94+
- uses: actions/checkout@v4
95+
- uses: maxim-lobanov/setup-xcode@v1
96+
with:
97+
xcode-version: latest-stable
98+
99+
- name: Version
100+
id: version
101+
run: |
102+
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then v="${GITHUB_REF#refs/tags/v}"; else v="$(grep -m1 'MARKETING_VERSION' project.yml | awk '{print $2}')"; fi
103+
echo "version=$v" >> "$GITHUB_OUTPUT"
104+
echo "build=$((GITHUB_RUN_NUMBER + 100))" >> "$GITHUB_OUTPUT"
105+
106+
- name: Import distribution certificate and profile
107+
env:
108+
P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
109+
P12_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
110+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
111+
PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
112+
run: |
113+
scripts/ci-keychain.sh "$P12" "$P12_PASSWORD" "$KEYCHAIN_PASSWORD"
114+
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
115+
echo "$PROFILE" | base64 --decode > profile.mobileprovision
116+
uuid=$(security cms -D -i profile.mobileprovision | plutil -extract UUID raw -)
117+
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
118+
echo "PROFILE_UUID=$uuid" >> "$GITHUB_ENV"
119+
120+
- name: Store App Store Connect key
121+
env:
122+
KEY: ${{ secrets.ASC_KEY_P8 }}
123+
run: |
124+
mkdir -p ~/.appstoreconnect/private_keys
125+
echo "$KEY" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.ASC_KEY_ID }}.p8
126+
127+
- name: Archive
128+
run: |
129+
xcodebuild archive \
130+
-project Spanscreen.xcodeproj \
131+
-scheme SpanscreenIOS \
132+
-destination generic/platform=iOS \
133+
-archivePath build/Spanscreen-ios.xcarchive \
134+
MARKETING_VERSION=${{ steps.version.outputs.version }} \
135+
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }} \
136+
CODE_SIGN_STYLE=Manual \
137+
CODE_SIGN_IDENTITY="iPhone Distribution" \
138+
PROVISIONING_PROFILE_SPECIFIER=$PROFILE_UUID \
139+
DEVELOPMENT_TEAM=$TEAM_ID | xcbeautify --renderer github-actions || exit ${PIPESTATUS[0]}
140+
141+
- name: Export
142+
run: |
143+
sed -e "s/__TEAM__/$TEAM_ID/" -e "s/__PROFILE__/$PROFILE_UUID/" scripts/ExportOptions-ios.plist > build/ExportOptions.plist
144+
xcodebuild -exportArchive -archivePath build/Spanscreen-ios.xcarchive -exportPath build/ios -exportOptionsPlist build/ExportOptions.plist
145+
146+
- uses: actions/upload-artifact@v4
147+
with:
148+
name: Spanscreen-ios
149+
path: build/ios/*.ipa
150+
151+
- name: Upload to App Store Connect
152+
run: |
153+
xcrun altool --upload-app -f build/ios/Spanscreen.ipa -t ios \
154+
--apiKey "${{ secrets.ASC_KEY_ID }}" --apiIssuer "${{ secrets.ASC_ISSUER_ID }}"

‎.gitignore‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
build/
3+
DerivedData/
4+
*.xcuserstate
5+
xcuserdata/
6+
.swiftpm/
7+
Packages/SpanscreenKit/.build/
8+
*.ipa
9+
*.dmg
10+
*.p12
11+
*.mobileprovision
12+
*.p8
13+
.omc/

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Codext GmbH
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors": [
3+
{
4+
"idiom": "universal",
5+
"color": {
6+
"color-space": "srgb",
7+
"components": {
8+
"red": "0.180",
9+
"green": "0.420",
10+
"blue": "1.000",
11+
"alpha": "1.000"
12+
}
13+
}
14+
}
15+
],
16+
"info": {
17+
"version": 1,
18+
"author": "xcode"
19+
}
20+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"images": [
3+
{
4+
"size": "16x16",
5+
"idiom": "mac",
6+
"filename": "icon_16x16@1x.png",
7+
"scale": "1x"
8+
},
9+
{
10+
"size": "16x16",
11+
"idiom": "mac",
12+
"filename": "icon_16x16@2x.png",
13+
"scale": "2x"
14+
},
15+
{
16+
"size": "32x32",
17+
"idiom": "mac",
18+
"filename": "icon_32x32@1x.png",
19+
"scale": "1x"
20+
},
21+
{
22+
"size": "32x32",
23+
"idiom": "mac",
24+
"filename": "icon_32x32@2x.png",
25+
"scale": "2x"
26+
},
27+
{
28+
"size": "128x128",
29+
"idiom": "mac",
30+
"filename": "icon_128x128@1x.png",
31+
"scale": "1x"
32+
},
33+
{
34+
"size": "128x128",
35+
"idiom": "mac",
36+
"filename": "icon_128x128@2x.png",
37+
"scale": "2x"
38+
},
39+
{
40+
"size": "256x256",
41+
"idiom": "mac",
42+
"filename": "icon_256x256@1x.png",
43+
"scale": "1x"
44+
},
45+
{
46+
"size": "256x256",
47+
"idiom": "mac",
48+
"filename": "icon_256x256@2x.png",
49+
"scale": "2x"
50+
},
51+
{
52+
"size": "512x512",
53+
"idiom": "mac",
54+
"filename": "icon_512x512@1x.png",
55+
"scale": "1x"
56+
},
57+
{
58+
"size": "512x512",
59+
"idiom": "mac",
60+
"filename": "icon_512x512@2x.png",
61+
"scale": "2x"
62+
}
63+
],
64+
"info": {
65+
"version": 1,
66+
"author": "xcode"
67+
}
68+
}
5.21 KB
Loading
10.7 KB
Loading
984 Bytes
Loading
1.53 KB
Loading

0 commit comments

Comments
 (0)