241 lines
14 KiB
Markdown
241 lines
14 KiB
Markdown
# Feature Specification: Mac App Store mode
|
|
|
|
**Feature Branch**: `003-mac-app-store-mode`, this spec's original name
|
|
|
|
**Created**: 2026-09-27
|
|
|
|
**Status**: Implemented
|
|
|
|
**Input**: User description: "For the Mac side, can we add an option when it determines that its
|
|
running in the MacOS container (when ran from mac app store) that it will instead of running the
|
|
daemon separate, run it as part of the GUI app and have it change the function to where it hides
|
|
its app icon from the doc and moves to the status menu? Also there are no menu items, for this I
|
|
think it would change cmd-q to quit to status menu, and cmd-shift-q to quit entirely. For on boot,
|
|
instead of launchd it'll ask for a login item to be created and remember the state of the gui
|
|
being open or closed when it was quit. The status menu would allow choosing quit or open GUI, when
|
|
the gui is up it should restore the dock icon."
|
|
|
|
Midi Harbor on macOS is a daemon registered with launchd plus a window that is one of its clients.
|
|
An app distributed through the Mac App Store runs inside the App Sandbox, where an app cannot
|
|
register its own launchd agent. This feature gives that build its own way of running: the app
|
|
starts and stops the daemon itself, the app lives in the menu bar while its window is closed, and
|
|
it starts at login as a login item. Everything in specs 001 to 013 applies unchanged to the direct-download
|
|
build and to Linux and Windows.
|
|
|
|
**Decided 2026-09-27**: the mode is for the App Store build only, and is chosen by the app
|
|
detecting that it runs sandboxed, not by a setting. The App Store build requires macOS 13 or
|
|
newer, the first release where a sandboxed app can register itself as a login item without a
|
|
helper app; the direct-download build keeps macOS 11. The project adds a sandboxed build variant,
|
|
signed ad hoc, so the mode can be run and tested before the owner has App Store signing.
|
|
|
|
**Decided 2026-09-27, in implementation**: quitting entirely is Command-Option-Q, not
|
|
Command-Shift-Q as first asked. Command-Shift-Q is macOS's own Log Out shortcut in the Apple menu,
|
|
which takes precedence over any app's menu (research R-098).
|
|
|
|
**Decided 2026-09-27, in planning**: the daemon stays a separate process, which the app starts
|
|
from a copy of the program bundled for it, rather than running inside the app's own process. The
|
|
owner first asked for it inside the app; a crash in the window would then drop every connection,
|
|
which Principle II forbids, and the sandbox refuses to start the app's own executable as a second
|
|
process, so the copy is what makes a separate daemon possible (research R-095).
|
|
|
|
## User Scenarios & Testing *(mandatory)*
|
|
|
|
### User Story 1 - Connections keep running from the menu bar (Priority: P1)
|
|
|
|
A musician installs Midi Harbor from the App Store, opens it, sets up a virtual port and a network
|
|
session, and closes the window. The ports and sessions keep working, Midi Harbor's icon leaves the
|
|
Dock, and a Midi Harbor item stays in the menu bar. Choosing "Open Midi Harbor" from it brings the
|
|
window and the Dock icon back.
|
|
|
|
**Why this priority**: With no launchd service, the running app is what keeps the daemon alive.
|
|
If closing the window stopped MIDI, the product would lose the property it exists for.
|
|
|
|
**Independent Test**: Launch the sandboxed build, create a virtual port routed to a network
|
|
session with another machine, close the window, and play into the port from another application:
|
|
the far machine keeps receiving, the Dock shows no Midi Harbor icon, and the menu bar item reopens
|
|
the window with its Dock icon.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** the sandboxed app is running with its window open, **When** the user closes the
|
|
window, **Then** every endpoint and route keeps running, the Dock icon disappears, and the menu
|
|
bar item remains.
|
|
2. **Given** the window is closed, **When** the user chooses "Open Midi Harbor" from the menu bar
|
|
item, **Then** the window opens showing current state and the Dock icon returns.
|
|
3. **Given** the window is open, **When** the user chooses "Quit" from the menu bar item, **Then**
|
|
the app stops its daemon through its graceful shutdown, releasing sounding notes, and exits.
|
|
4. **Given** the sandboxed app starts, **When** it checks for a daemon, **Then** it starts its own,
|
|
or uses one already running from before, and never offers to install a launchd service.
|
|
|
|
---
|
|
|
|
### User Story 2 - Quitting the window versus quitting Midi Harbor (Priority: P1)
|
|
|
|
The app has a standard menu bar menu. Command-Q closes the window and leaves Midi Harbor in the
|
|
menu bar with every connection running. Command-Option-Q quits Midi Harbor entirely.
|
|
|
|
**Why this priority**: Command-Q is the reflex for "I'm done with this window". Quitting the
|
|
whole app on it would drop every connection by accident.
|
|
|
|
**Independent Test**: With the window focused, press Command-Q and confirm connections still pass
|
|
MIDI and the menu bar item remains; reopen, press Command-Option-Q, and confirm the process has
|
|
exited and its ports are gone from other applications.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** the window is focused, **When** the user presses Command-Q, **Then** the window
|
|
closes, the Dock icon disappears, and connections keep running.
|
|
2. **Given** the window is focused, **When** the user presses Command-Option-Q, **Then** Midi
|
|
Harbor quits entirely, stopping its daemon.
|
|
3. **Given** the window is focused, **When** the user opens the app menu, **Then** it lists a
|
|
close-to-menu-bar item with Command-Q, a quit item with Command-Option-Q, and the standard Hide
|
|
and window items, each with its shortcut shown.
|
|
4. **Given** macOS asks the app to quit, at logout, restart or shutdown, **When** the request
|
|
arrives, **Then** Midi Harbor quits entirely rather than hiding.
|
|
|
|
---
|
|
|
|
### User Story 3 - Starting at login (Priority: P2)
|
|
|
|
The user turns on "Start at login". After the next login Midi Harbor starts in the menu bar with
|
|
its connections restored, and opens its window only if the window was open when Midi Harbor last
|
|
quit.
|
|
|
|
**Why this priority**: A studio machine should bring its MIDI setup back without anyone opening
|
|
an app. It comes after P1 because the app is useful, if less convenient, without it.
|
|
|
|
**Independent Test**: Turn on "Start at login", quit with the window open, log out and in: the
|
|
window is open. Quit with the window closed, log out and in: only the menu bar item appears. In
|
|
both cases the ports and sessions come back.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** the sandboxed app is running and not registered to start at login, **When** the user
|
|
opens it for the first time, **Then** it offers once to start at login, saying what that does.
|
|
2. **Given** the user turns on "Start at login", **When** macOS needs the user's approval for it,
|
|
**Then** the app says so and where to approve it, and shows the setting as waiting.
|
|
3. **Given** Midi Harbor quit with its window open, **When** it starts at login, **Then** it opens
|
|
the window.
|
|
4. **Given** Midi Harbor quit with its window closed, **When** it starts at login, **Then** it
|
|
shows only the menu bar item.
|
|
5. **Given** the user starts Midi Harbor by hand from Finder or Launchpad, **When** it starts,
|
|
**Then** it opens the window, whatever state it quit in.
|
|
6. **Given** "Start at login" is on, **When** the user turns it off, **Then** Midi Harbor no
|
|
longer starts at login.
|
|
|
|
---
|
|
|
|
### User Story 4 - A sandboxed build to test with (Priority: P2)
|
|
|
|
A developer builds the App Store variant on a Mac, signed ad hoc, and runs it to exercise
|
|
everything above before any App Store signing exists.
|
|
|
|
**Why this priority**: The mode cannot be run, and so cannot be proved, without a sandboxed
|
|
build.
|
|
|
|
**Independent Test**: Build the variant, confirm the bundle runs sandboxed, and run the checks in
|
|
User Stories 1 to 3 against it.
|
|
|
|
**Acceptance Scenarios**:
|
|
|
|
1. **Given** a Mac with the build tools, **When** the developer builds the App Store variant,
|
|
**Then** it produces an app bundle that runs sandboxed, declaring only the access Midi Harbor
|
|
uses: network client and server, Bluetooth, and USB devices.
|
|
2. **Given** the variant runs sandboxed, **When** the user creates virtual ports, joins network
|
|
sessions, uses hardware and scans for Bluetooth devices, **Then** each works as in the
|
|
direct-download build, or is reported unavailable with the reason.
|
|
|
|
### Edge Cases
|
|
|
|
- The daemon cannot start, because its configuration is from a newer build or its socket cannot
|
|
be bound: the window says why, and "Quit" still works.
|
|
- A second copy of the app is launched while one runs: the running one opens its window and the
|
|
second exits, so two daemons never run in one sandbox.
|
|
- The direct-download build's launchd daemon is also running on the same Mac: both run with
|
|
separate setups and the user sees two sets of ports; neither can manage the other.
|
|
- The window is closed while a dialog is open or an action is in flight: the action finishes, and
|
|
the window opens on the current state next time.
|
|
- The user denies the login item in System Settings: the setting shows it as off, with the
|
|
reason.
|
|
- macOS sleeps with the window closed: the daemon handles sleep and wake as it does under
|
|
launchd.
|
|
- A capability the sandbox withholds, such as the MIDI server recovery that needs another
|
|
process: it is reported unavailable with the reason, and the rest runs.
|
|
- The configuration is kept inside the app's container, so it is not the file the
|
|
direct-download build uses, and `midi-harbor config path` run from the app reports where it is.
|
|
- The window crashes: the daemon keeps every connection running, and the next launch finds it
|
|
and shows its state. The daemon crashes: the app starts it again, as launchd would.
|
|
- The MIDI server dies: the daemon replaces itself as it does under launchd (008-resilience), and the
|
|
window shows it as unreachable for those seconds.
|
|
|
|
## Requirements *(mandatory)*
|
|
|
|
### Functional Requirements
|
|
|
|
- **FR-A01**: The app MUST decide at launch whether it runs sandboxed, and MUST use App Store mode
|
|
exactly when it does. The direct-download build, Linux and Windows MUST behave as before.
|
|
- **FR-A02**: In App Store mode the app MUST start the daemon as a separate process of its own,
|
|
restart it after a failure, and stop it when Midi Harbor quits entirely. The daemon serves the
|
|
same contract on its socket inside the app's container, and the window remains a client of it.
|
|
A daemon already running when the app starts MUST be used rather than a second one started.
|
|
- **FR-A03**: In App Store mode the app MUST NOT register, start, or offer a launchd service, and
|
|
`service` commands MUST say that the App Store build starts through a login item instead, but
|
|
for `service stop`, which stops the running daemon as the app's Quit does.
|
|
- **FR-A04**: The app MUST show a menu bar item while it runs in App Store mode, with "Open Midi
|
|
Harbor" and "Quit".
|
|
- **FR-A05**: The app MUST show its Dock icon while its window is open and hide it while the
|
|
window is closed.
|
|
- **FR-A06**: Closing the window, by its close button or Command-Q, MUST leave every endpoint and
|
|
route running.
|
|
- **FR-A07**: The app MUST provide a menu bar menu with a close-to-menu-bar item on Command-Q and
|
|
a quit item on Command-Option-Q, plus the standard Hide and window items.
|
|
- **FR-A08**: Quitting entirely, from the menu bar item, Command-Option-Q, or a logout, restart or
|
|
shutdown, MUST stop the daemon through its graceful shutdown, including releasing sounding notes
|
|
(008-resilience, FR-026), before the app exits.
|
|
- **FR-A09**: The app MUST let the user turn starting at login on and off, MUST offer it once on
|
|
first launch, and MUST report when macOS is waiting for the user's approval.
|
|
- **FR-A10**: The app MUST remember whether its window was open when it last quit, and when
|
|
started at login MUST open the window only if it was; started any other way, it MUST open the
|
|
window.
|
|
- **FR-A11**: Only one copy of the app MUST run at a time; a second launch MUST bring the first
|
|
one's window forward and exit.
|
|
- **FR-A12**: A capability the sandbox withholds MUST be reported unavailable with the reason, as
|
|
any other unavailable capability is (002-configuration, Principle IV).
|
|
- **FR-A13**: The project MUST build a sandboxed App Store variant of the app bundle, signed ad hoc
|
|
by default, declaring network client and server, Bluetooth and USB device access and nothing
|
|
more, and requiring macOS 13 or newer.
|
|
- **FR-A14**: A crash of the window MUST NOT disturb any connection, and relaunching the app MUST
|
|
show the daemon's current state (Principle II).
|
|
|
|
### Key Entities
|
|
|
|
- **Window state at quit**: whether the window was open when Midi Harbor last quit, kept per user
|
|
inside the app's container and read at the next launch.
|
|
- **Login item**: the app's registration to start at login, owned by macOS and read back from it
|
|
rather than stored by Midi Harbor.
|
|
|
|
## Success Criteria *(mandatory)*
|
|
|
|
### Measurable Outcomes
|
|
|
|
- **SC-A01**: With the window closed for 10 minutes, MIDI sent into a virtual port keeps reaching
|
|
a network peer with no message lost that the network did not lose.
|
|
- **SC-A02**: The window opens from the menu bar item within 1 second, showing current state.
|
|
- **SC-A03**: After a login with "Start at login" on, the ports and sessions are back as fast as
|
|
the direct-download build's launchd daemon brings them back, and the window is open exactly when
|
|
it was open at quit, in every combination tried.
|
|
- **SC-A04**: Command-Q never stops a connection, and Command-Option-Q always ends the process, in
|
|
every trial.
|
|
- **SC-A05**: The direct-download build passes its existing gates unchanged.
|
|
|
|
## Assumptions
|
|
|
|
- The App Store build is signed and provisioned by the owner later; this feature proves the mode
|
|
with an ad hoc signed sandboxed build.
|
|
- Clicking the window's close button behaves like Command-Q, closing to the menu bar.
|
|
- Settings gains the "Start at login" switch in App Store mode, in place of the service offer the
|
|
direct-download build shows when no daemon runs.
|
|
- Command-line use against the App Store build goes through the app's own executable, which runs
|
|
in the same sandbox and so reaches the same socket; the plan verifies this.
|
|
- The menu bar item uses the app icon drawn as a template image, so it follows the menu bar's
|
|
light and dark appearance.
|