From 4d28f1408c94db8c233d7ebcf4278c869f481646 Mon Sep 17 00:00:00 2001 From: Codefarmer Date: Thu, 24 Sep 2026 23:15:24 +0100 Subject: [PATCH 1/3] docs: compose the release notes from the changelog and the readme The release body carried only the changelog section. It now also states the requirements, the known gaps, how to verify the download, who to credit and where to report a bug, with the gaps read from the readme so they cannot drift. Composing them is its own step so that a failure stops the job before a tag or a release exists, rather than inside a process substitution whose exit status nothing checks. --- .github/workflows/release.yml | 11 +- CHANGELOG.md | 12 ++- README.md | 22 ++-- .../changelog-section.cpython-314.pyc | Bin 0 -> 2442 bytes scripts/release-notes.py | 102 ++++++++++++++++++ 5 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 scripts/__pycache__/changelog-section.cpython-314.pyc create mode 100755 scripts/release-notes.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29ebae0..56bc881 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -154,6 +161,7 @@ jobs: build/*.dmg build/SHA256SUMS build/appcast.xml + build/release-notes.md build/notarization/*.json retention-days: 3 @@ -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: @@ -179,6 +187,7 @@ jobs: build/*.dmg build/SHA256SUMS build/appcast.xml + build/release-notes.md retention-days: 90 - name: Clean up diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b2a3c..9325310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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. diff --git a/README.md b/README.md index 8946451..c63c3f4 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,22 @@ simulators. 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. + 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. @@ -99,8 +111,6 @@ options. 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 app and its menus but not its windows. - **A device taller than your display** keeps its bottom edge off screen, where the pointer cannot diff --git a/scripts/__pycache__/changelog-section.cpython-314.pyc b/scripts/__pycache__/changelog-section.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e32d3f8f0919f3c6334729235d45d5c2be5a96b1 GIT binary patch literal 2442 zcmai0O>7fK6rS~t?PP7oiwFeMkZh8c$SRF#fhHgnS`i2&G;v+9Xlt-G-ifoY-rde_ z0*O@BQaL0lRRXO z&zpJseQ*4FOGH2*(Vu?4GJz5Ln{_;azAIZFfhwXDve9Lf;?8n5=W3trOZnPRif==j zX!9uyw%@h!C4VX~a1kM;1)+9yDbR&3;VyKzy5V82VoY%PP5~4^sqW~5)3hsy{L(kOY962Ca26GAaHABkDI&2~-RGqXX z*_5dbp)Xi+o*a{`992!*O5~w+edermyn2U0VJye4TD>j`fX%~i>o_z; zlt!0*12-T^;A=?^8nXd9Z}w98BhCaefJA= zqW4D9IORLAx4uErU$;HWi<%62p1Mx)UaT}Xa0brg8y!atk@9Y&(GS7LC!6c*JS&a8 z^_^%`L#P>bpcGDqyz#nD*CM^_jDQvY={dfqM>_V5rDkl*;bt68hzA_OGBwrKRGnC0 z+W>Ftnrcrue!z}uIsvV!WfQQVIQWX8>Ty41ABeK7I6}_QG{#Xq@8AhTnRT$c8ynoe zUy>xsF6acS%%CyoxzP9wj3O%U*j*ec@7_}!DvPamqPL0m1FysC%)NpzvFI4DHdOi9bAbW{BdU~cI20~mDoG;BWvN-r=eX7$I4-G zadcsH89xc{+l-AZ( z;p$yYU%epY0R)YiOF;n&0}<0)7`=rX^6`J!1Dw?JpZ0k-hP)z1O&37`9z3aPWG>wJ z%*_SI45=50I~VD#J%y?+#Ul={5gkhWtmR;t=4Twg$(~Oqn9V9iE}L}%maPy8o_ZP*VfUrra&T$(+sFg;!N(6 +""" +import pathlib +import sys + +import importlib.util + +ROOT = pathlib.Path(__file__).resolve().parent.parent +REPOSITORY = "https://github.com/Mastersam07/OpenDeviceHub" + + +def changelog_section(version: str) -> str: + spec = importlib.util.spec_from_file_location( + "changelog_section", ROOT / "scripts" / "changelog-section.py" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.section((ROOT / "CHANGELOG.md").read_text(), version) + + +def readme_section(heading: str) -> str: + collecting = False + body: list[str] = [] + for line in (ROOT / "README.md").read_text().splitlines(): + if line.startswith("## "): + if collecting: + break + collecting = line[3:].strip() == heading + continue + if collecting: + body.append(line) + return "\n".join(body).strip() + + +def main() -> int: + if len(sys.argv) != 2: + print(__doc__, file=sys.stderr) + return 2 + version = sys.argv[1] + + changes = changelog_section(version) + if not changes: + print(f"No '## {version}' section in CHANGELOG.md", file=sys.stderr) + return 1 + + requirements = readme_section("Requirements") + limitations = readme_section("Current limitations") + if not requirements or not limitations: + print("README.md is missing Requirements or Current limitations", file=sys.stderr) + return 1 + # The README's closing invitation belongs on its own page, not in release notes. + limitations = limitations.split("Want to help close one of these?")[0].strip() + + print(f"""{changes} + +## Requirements + +{requirements} + +## Known gaps + +This is a pre-release. These are the ones worth knowing before you install it: + +{limitations} + +## Verifying the download + +The app is signed with a Developer ID certificate, notarized by Apple and stapled, so it launches +without a Gatekeeper warning. `SHA256SUMS` is attached; to check the DMG against it: + +```sh +shasum -a 256 -c SHA256SUMS +``` + +## Credits + +Built after studying two MIT licensed projects, and better for both: +[Siniulator](https://github.com/kmagiera/Siniulator) by Krzysztof Magiera, and +[idb / FBSimulatorControl](https://github.com/facebook/idb) by Meta. What was adapted from each, down +to the individual constants, is listed in +[THIRD_PARTY_NOTICES.md]({REPOSITORY}/blob/dev/THIRD_PARTY_NOTICES.md). + +## Tell me what breaks + +This is the first release and it has been used by one person on one Mac. If something does not work, +please [open an issue]({REPOSITORY}/issues/new/choose): `odhub doctor` prints almost +everything the bug form asks for.""") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 864827a4ac273f9a74273c4ab3cd0a3b163b7248 Mon Sep 17 00:00:00 2001 From: Codefarmer Date: Fri, 25 Sep 2026 00:03:35 +0100 Subject: [PATCH 2/3] docs: give the readme a front page The icon, one line saying what it is, a place for a screenshot and a download button, following the shape of the prior art's own front page. Adds a section on sharing a machine with Device Hub, including the two Apple preferences that stop it opening and stop it taking simulators down when it quits, and says plainly that nothing can take over the app Xcode launches. Also replaces every em dash in the readme and the roadmap, which no tracked file should carry. --- README.md | 104 +++++++++++++++++++++++++++----------- ROADMAP.md | 34 ++++++------- icons/README.md | 2 + icons/download-button.png | Bin 0 -> 25123 bytes packaging/make-icon.swift | 78 ++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 46 deletions(-) create mode 100644 icons/download-button.png diff --git a/README.md b/README.md index c63c3f4..cdffb2a 100644 --- a/README.md +++ b/README.md @@ -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. +

+ +

+ +

OpenDeviceHub

+ +

+ Your iOS simulators in windows again, one per device. +

+ +

+ 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. +

+ + +

+ A screenshot goes here. +

+ +

+ + Download for macOS + +

+ +

+ Signed, notarized, and it updates itself. +

## Install @@ -25,36 +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 +- **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 +- **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 +- **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, +- **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 +- **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 +- **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 +- **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`.
@@ -107,11 +127,11 @@ options. ## Current limitations -- **Input can stop on a running simulator** — a booted device sometimes stops accepting taps and +- **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. -- **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. @@ -119,13 +139,39 @@ options. 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. +- **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. 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. diff --git a/ROADMAP.md b/ROADMAP.md index 79df955..2d768be 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,18 +11,18 @@ 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**: 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 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 +- **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 +- **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, 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 than once. ## Tooling for the app you are actually building @@ -30,14 +30,14 @@ Things the classic Simulator had, or that a simulator should simply be able to d 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 @@ -45,12 +45,12 @@ developer scripts by hand today, one click away, remembered per project. 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 diff --git a/icons/README.md b/icons/README.md index 2ac0e8a..21517a6 100644 --- a/icons/README.md +++ b/icons/README.md @@ -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 @@ -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 diff --git a/icons/download-button.png b/icons/download-button.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a704e8db2ebb4fef66819aa4823c1052ed99e1 GIT binary patch literal 25123 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 Jt5CA~_&;Bud8z;a literal 0 HcmV?d00001 diff --git a/packaging/make-icon.swift b/packaging/make-icon.swift index ff272b2..42fb3f6 100644 --- a/packaging/make-icon.swift +++ b/packaging/make-icon.swift @@ -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)") From 2a78e1cd059f326822466e3fac5a785ab2b6c5b3 Mon Sep 17 00:00:00 2001 From: Codefarmer Date: Fri, 25 Sep 2026 00:06:46 +0100 Subject: [PATCH 3/3] docs: correct the roadmap where the work has landed Location scenarios, custom coordinates, creating and erasing a simulator, text size and increased contrast all ship now, so the roadmap was listing them as missing. Each entry keeps only what is genuinely left. --- README.md | 8 -------- ROADMAP.md | 13 ++++++------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cdffb2a..45515ac 100644 --- a/README.md +++ b/README.md @@ -126,20 +126,12 @@ options.
## 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. - **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. Want to help close one of these? Pull requests are welcome. [ROADMAP.md](ROADMAP.md) is where that diff --git a/ROADMAP.md b/ROADMAP.md index 2d768be..4eec055 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,25 +11,24 @@ 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. +- **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. +- **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. +- **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 entry points, then remembers your deep links, payloads, proxy settings and favourite devices alongside them.