Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ jobs:
- name: Build, sign, notarize, staple, package, checksum, appcast
run: scripts/sign-and-notarize.sh

- name: Compose the release notes
run: |
set -euo pipefail
python3 scripts/release-notes.py "${{ steps.version.outputs.version }}" \
> build/release-notes.md
wc -l build/release-notes.md

- name: Release candidate artifacts
if: ${{ inputs.publish != true }}
uses: actions/upload-artifact@v4
Expand All @@ -154,6 +161,7 @@ jobs:
build/*.dmg
build/SHA256SUMS
build/appcast.xml
build/release-notes.md
build/notarization/*.json
retention-days: 3

Expand All @@ -164,7 +172,7 @@ jobs:
version="${{ steps.version.outputs.version }}"
gh release create "v${version}" \
--title "${version}" \
--notes-file <(python3 scripts/changelog-section.py "${version}") \
--notes-file build/release-notes.md \
--prerelease \
build/OpenDeviceHub-${version}.dmg build/SHA256SUMS
env:
Expand All @@ -179,6 +187,7 @@ jobs:
build/*.dmg
build/SHA256SUMS
build/appcast.xml
build/release-notes.md
retention-days: 90

- name: Clean up
Expand Down
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ First release.
you switch to another app.
- A window follows its device: it shows when the device has shut down, offers a reboot, and
reattaches on its own when the device comes back.
- A capture shows itself beside the window before it is filed, so you can open, copy, save or discard
it. Left alone it saves itself, to a folder you choose.
- Create a simulator without leaving the app: name, device type and OS version, where the two lists
follow each other so a device no installed runtime can run is never offered.
- Device controls beyond the buttons: restart, erase, text size, increased contrast, an iCloud sync,
and a location from a scenario or your own coordinates.
- Keyboard choices: send keys to the device or keep them, connect the Mac keyboard as the device's
hardware keyboard, and match the device's keyboard language to the Mac's.
- Settings for what closing a window does, what launching opens, where captures go, whether device
links open here, and updates.
- The menu bar follows the simulator it replaces: the same menus in the same order, the same
shortcuts, and the standard macOS ones that were missing. The zoom shortcuts are Apple's now,
⌘1 Physical Size, ⌘2 Point Accurate, ⌘3 Pixel Accurate, ⌘4 Fit Screen, which changes three of the
Expand All @@ -28,5 +38,3 @@ First release.
One menu item puts it on your PATH, without a password on most machines.
- If Xcode is missing or its simulator frameworks will not load, the app says what is missing and
what to do about it instead of failing silently.

Known gaps are listed in the README.
120 changes: 84 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
# OpenDeviceHub

**A macOS app that shows your iOS simulators in windows, one window per device, the way
Simulator.app did.**

Xcode 27 replaced Simulator.app with Device Hub, and a decade of habits did not come across with it.
OpenDeviceHub connects to the same simulators Xcode installs and brings back the familiar window,
scaling and shortcuts, with a command line tool for driving a device from a script.

> **Screenshot goes here.** Placeholder: replace this block with a screenshot or a short GIF of a
> device window before publishing.
<p align="center">
<img src="icons/icon-256.png" width="116" alt="">
</p>

<h1 align="center">OpenDeviceHub</h1>

<p align="center">
<strong>Your iOS simulators in windows again, one per device.</strong>
</p>

<p align="center">
Xcode 27 replaced Simulator.app with Device Hub, and a decade of habits did not come across with
it. OpenDeviceHub connects to the same simulators Xcode installs and brings back the familiar
window, scaling and shortcuts, with a command line tool for driving a device from a script.
</p>

<!-- PLACEHOLDER, deliberate: no screenshot has been captured yet. Replace this block with a
screenshot or a short GIF of a device window, and keep the width attribute. -->
<p align="center">
<em>A screenshot goes here.</em>
</p>

<p align="center">
<a href="https://github.com/Mastersam07/OpenDeviceHub/releases">
<img src="icons/download-button.png" width="332" alt="Download for macOS">
</a>
</p>

<p align="center">
<sub>Signed, notarized, and it updates itself.</sub>
</p>

## Install

Expand All @@ -25,24 +45,36 @@ simulators.

## Features

- **A window per device** — several simulators side by side, resizable from any edge or corner, full
- **A window per device**: several simulators side by side, resizable from any edge or corner, full
screen, keep on top, and each window remembers where you put it.
- **Precise scaling** — Point Accurate, Pixel Accurate, Physical Size or Fit, with the device body
- **Precise scaling**: Point Accurate, Pixel Accurate, Physical Size or Fit, with the device body
drawn around the screen or hidden.
- **Opens like an app** — click the icon for whatever is running, or pick a simulator from the Dock
- **Opens like an app**: click the icon for whatever is running, or pick a simulator from the Dock
icon or File → Open Simulator. A device booted anywhere else gets a window here too.
- **Familiar shortcuts** — the Simulator keys you already know.
- **Input** — click to tap and drag, pinch and rotate with the trackpad, type with your Mac
keyboard, and swipe up from the bottom edge for home or the app switcher.
- **Device controls** — Home, Lock, volume, rotation, and light or dark appearance, from the menu or
by clicking the buttons on the device body.
- **Screenshots and recordings** — PNGs and MP4s to the Desktop, or a screenshot to the clipboard.
- **Drag and drop** — drop a file or a link onto a device to open it there.
- **Follows the device** — shut a simulator down from anywhere and its window says so and offers to
- **Familiar shortcuts**: the Simulator keys you already know.
- **Input**: click to tap and drag, pinch and rotate with the trackpad, type with your Mac
keyboard, and swipe up from the bottom edge for home or the app switcher. Send keys to the device or
keep them, connect the Mac keyboard as the device's hardware keyboard, and match its language to
yours.
- **Device controls**: Home, Lock, volume, rotation, Siri, the Action button and light or dark
appearance, from the menu or by clicking the buttons on the device body. Restart or erase a device,
step its text size, turn on increased contrast, trigger an iCloud sync, and set a location from a
scenario or your own coordinates.
- **Create a simulator**: name it, pick a device type and an OS version, and it boots and opens. The
two lists follow each other, so a device an installed runtime cannot run is never offered.
- **Screenshots and recordings**: a capture appears beside the window first, where you can open,
copy, save it elsewhere, reveal it in the Finder or throw it away. Left alone it files itself. The
folder is yours to choose.
- **The clipboard stays in step**: copy on the Mac and it is on the device; copy on the device and it
is on the Mac when you switch to another app. Or do it by hand from Edit.
- **Settings**: whether closing a window shuts the device down, whether launching opens the
simulator you had last, where captures go, and which app opens device links.
- **Drag and drop**: drop a file or a link onto a device to open it there.
- **Follows the device**: shut a simulator down from anywhere and its window says so and offers to
start it again; boot it and the window reattaches on its own.
- **Debug helpers** — slow animations, shake, simulated memory warning, the system log and app data
- **Debug helpers**: slow animations, shake, simulated memory warning, the system log and app data
in the Finder, and a click to frame latency overlay.
- **A command line tool** — `odhub` drives a simulator from a script: tap, swipe, pinch, type,
- **A command line tool**: `odhub` drives a simulator from a script: tap, swipe, pinch, type,
buttons, rotation. One menu item puts it on your `PATH`.

<details>
Expand Down Expand Up @@ -94,28 +126,44 @@ options.
</details>

## Current limitations

- **Input can stop on a running simulator** — a booted device sometimes stops accepting taps and
keystrokes while it still renders and still reports itself booted, with no error anywhere. Not the
app window: a command line tap fails on the same device at the same moment, while a device booted
fresh right then is fine. Shut the simulator down and boot it again. Under investigation.
- **Copying from a device does nothing** on at least one machine, which traces to `simctl pbpaste`
returning nothing. Pasting to a device works.
- **The windows are invisible to accessibility** — VoiceOver, window managers and scripting tools see
- **The windows are invisible to accessibility**: VoiceOver, window managers and scripting tools see
the app and its menus but not its windows.
- **A device taller than your display** keeps its bottom edge off screen, where the pointer cannot
reach it. Use Fit.
- **Simulators shut themselves down when idle** on at least one machine. Not reproduced on demand,
and CoreSimulator has no setting for it.
- **The compact layout below 340 points wide** is written and unit tested but has never been used by
hand.
- **No Flutter tooling yet** — it is the reason this project exists, and none of it is built.
- **Other Apple platforms** — tvOS, watchOS and visionOS simulators are not supported.
- **Other Apple platforms**: tvOS, watchOS and visionOS simulators are not supported.

Want to help close one of these? Pull requests are welcome. [ROADMAP.md](ROADMAP.md) is where that
list is going: location, push, biometrics, proxy certificates, device management, and the project
aware tooling this exists for.

## Working with Device Hub

Xcode 27 opens Device Hub itself when you run an app on a simulator, and other tools may launch it
too. OpenDeviceHub shows the same devices, so both can be on screen at once. Two of Apple's own
settings make them share better.

Quitting Device Hub can shut down the simulators it started, which closes their windows here. To stop
that, quit Device Hub and run:

```sh
defaults write com.apple.dt.Devices shutdownStartedDevicesOnQuit -bool false
```

To stop Xcode opening Device Hub at all, quit Xcode and run:

```sh
defaults write com.apple.dt.Xcode DVTiPhoneSimulatorAlwaysLaunchInCoreSimulatorSession -bool true
```

Xcode then boots the simulator without a window of its own and yours is the one you use. With this
on, boot the device here before pressing Run, or Xcode may shut it down when you press Stop.

**OpenDeviceHub → Settings → Links** offers to open `devices://` links here instead. That covers
links only. Nothing can take over from a tool that launches Device Hub directly, because it is
launched by name rather than through the link.

## Requirements

macOS 14 or later, Apple silicon, and Xcode 26 or 27 with at least one iOS simulator runtime.
Expand Down
41 changes: 20 additions & 21 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,45 @@ mentioned.

Things the classic Simulator had, or that a simulator should simply be able to do.

- **Location** — presets, a search, GPX routes, and per-project saved places.
- **Push notifications** — send a payload to an app, with a saved library of them and templates for
- **Location**: a search, GPX routes, and per-project saved places. Scenarios, your own coordinates
and clearing the location already work.
- **Push notifications**: send a payload to an app, with a saved library of them and templates for
the fiddly parts.
- **Biometrics** — enrol, match and fail a match for Face ID and Touch ID.
- **Device and runtime management** — create, rename, erase and delete simulators, and install
runtimes, without going back to Xcode.
- **Proxy certificates** — drop a `.cer` onto a device, or install a debugging proxy's root
- **Biometrics**: enrol, match and fail a match for Face ID and Touch ID.
- **Device and runtime management**: rename and delete simulators, and install runtimes, without
going back to Xcode. Creating and erasing already work.
- **Proxy certificates**: drop a `.cer` onto a device, or install a debugging proxy's root
certificate on every booted device at once. Removed from Device Hub, and badly missed by anyone
who inspects traffic for a living.
- **Status bar presets** — 9:41, full battery, full signal, for clean App Store screenshots.
- **Locale, region and Dynamic Type** — switch them without digging through Settings.
- **Privacy permissions** — grant, revoke and reset per app, so the first-run flow can be tested more
- **Status bar presets**: 9:41, full battery, full signal, for clean App Store screenshots.
- **Locale and region**: switch them without digging through Settings. Text size and increased
contrast already work.
- **Privacy permissions**: grant, revoke and reset per app, so the first-run flow can be tested more
than once.

## Tooling for the app you are actually building

The reason this project exists, and none of it is built yet. The theme is everything a mobile
developer scripts by hand today, one click away, remembered per project.

- **Project awareness** — point it at a project and it learns the bundle identifiers, flavours and
- **Project awareness**: point it at a project and it learns the bundle identifiers, flavours and
entry points, then remembers your deep links, payloads, proxy settings and favourite devices
alongside them.
- **Run, hot reload and hot restart** — on any device, flavour aware, from the device window.
- **DevTools** — a link on the window once the app is attached, and attaching to an app that is
- **Run, hot reload and hot restart**: on any device, flavour aware, from the device window.
- **DevTools**: a link on the window once the app is attached, and attaching to an app that is
already running.
- **Deep links** — saved per app, custom schemes and universal links, one click to fire.
- **Screenshot sets** — the same screen captured across several devices in one action, named and
- **Deep links**: saved per app, custom schemes and universal links, one click to fire.
- **Screenshot sets**: the same screen captured across several devices in one action, named and
organised for the App Store.

## Harder, or not yet certain

Listed separately because the honest answer is that these might not work, or might not be worth what
they cost.

- **iPad pointer and mouse input** — the hardest item here, and the one most likely to slip.
- **Network conditioning presets** — 3G, lossy, offline. Apple's own conditioner is system wide on
- **iPad pointer and mouse input**: the hardest item here, and the one most likely to slip.
- **Network conditioning presets**: 3G, lossy, offline. Apple's own conditioner is system wide on
macOS rather than per simulator, so either a per-device approach turns up or this gets documented
as the compromise it is.
- **Audio routing and external displays** — wanted, not investigated.
- **Android emulators in the same window** — cross platform developers juggle both all day. Genuinely
- **Audio routing and external displays**: wanted, not investigated.
- **Android emulators in the same window**: cross platform developers juggle both all day. Genuinely
useful, a long way out, and not started.

## Not planned
Expand Down
2 changes: 2 additions & 0 deletions icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Every file here is generated, not hand drawn. `packaging/make-icon.swift` is the
xcrun swiftc -O -o /tmp/make-icon packaging/make-icon.swift
/tmp/make-icon icons --marketing # the PNGs below
/tmp/make-icon icons --social # social-preview.png
/tmp/make-icon icons --button # download-button.png
```

The app's own `.icns` is built from the same file by `scripts/build-app.sh`, so the icon on disk and
Expand All @@ -18,6 +19,7 @@ the icon in the app can never disagree.
| `icon-512.png`, `icon-1024.png` | store listings, anything print or retina |
| `icon-square-400.png`, `-512.png`, `-1024.png` | avatars: GitHub, X, Discord, anywhere the picture is cropped |
| `social-preview.png` | GitHub's repository social preview, 1280x640 |
| `download-button.png` | the download button on the README, shown at half its pixel width |
| `icon.svg` | vector, for any size that is not here |

The square files fill the frame instead of rounding their own corners, because an avatar is cropped
Expand Down
Binary file added icons/download-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions packaging/make-icon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,86 @@ func socialPreview(width: Int = 1280, height: Int = 640) -> Data {
return data
}

// The download button on the README. Drawn rather than written as SVG text, because a web font is
// not guaranteed where the README is rendered and clipped lettering is worse than a picture.
func downloadButton(scale: CGFloat = 2) -> Data {
let label = "Download for macOS"
let font = NSFont.systemFont(ofSize: 25 * scale, weight: .semibold)
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: NSColor.white]
let text = label.size(withAttributes: attributes)

let arrow = 22 * scale
let padding = 30 * scale
let gap = 14 * scale
// Sized to the text it was measured with, so a longer label cannot run off the end.
let width = Int((padding * 2 + arrow + gap + text.width).rounded())
let height = Int((text.height + 26 * scale).rounded())

guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else {
print("could not make the button bitmap")
exit(1)
}
bitmap.size = NSSize(width: width, height: height)

NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmap)

let frame = NSRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))
let pill = NSBezierPath(roundedRect: frame, xRadius: frame.height / 2, yRadius: frame.height / 2)
plateGradient.draw(in: pill, angle: -90)

let centre = frame.height / 2
let arrowX = padding + arrow / 2
let stem = NSBezierPath()
stem.move(to: NSPoint(x: arrowX, y: centre + arrow * 0.55))
stem.line(to: NSPoint(x: arrowX, y: centre - arrow * 0.15))
stem.lineWidth = 3 * scale
stem.lineCapStyle = .round
NSColor.white.setStroke()
stem.stroke()

let head = NSBezierPath()
head.move(to: NSPoint(x: arrowX - arrow * 0.42, y: centre - arrow * 0.02))
head.line(to: NSPoint(x: arrowX, y: centre - arrow * 0.5))
head.line(to: NSPoint(x: arrowX + arrow * 0.42, y: centre - arrow * 0.02))
head.lineWidth = 3 * scale
head.lineCapStyle = .round
head.lineJoinStyle = .round
head.stroke()

let tray = NSBezierPath()
tray.move(to: NSPoint(x: arrowX - arrow * 0.5, y: centre - arrow * 0.62))
tray.line(to: NSPoint(x: arrowX + arrow * 0.5, y: centre - arrow * 0.62))
tray.lineWidth = 3 * scale
tray.lineCapStyle = .round
tray.stroke()

label.draw(
at: NSPoint(x: padding + arrow + gap, y: (frame.height - text.height) / 2),
withAttributes: attributes
)

NSGraphicsContext.restoreGraphicsState()

guard let data = bitmap.representation(using: .png, properties: [:]) else {
print("could not encode the button")
exit(1)
}
return data
}

let outputDirectory = CommandLine.arguments[1]

if CommandLine.arguments.contains("--button") {
try downloadButton().write(to: URL(fileURLWithPath: "\(outputDirectory)/download-button.png"))
print("wrote the download button to \(outputDirectory)")
exit(0)
}

if CommandLine.arguments.contains("--social") {
try socialPreview().write(to: URL(fileURLWithPath: "\(outputDirectory)/social-preview.png"))
print("wrote the social preview to \(outputDirectory)")
Expand Down
Binary file not shown.
Loading
Loading