Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9d1cdbf
fix(gui): open minimized honours the setting
Behnam-RK Aug 21, 2026
b6564b3
fix(gui): the login agent must not spawn a second app, or re-arm itself
Behnam-RK Aug 21, 2026
ff66ba9
fix(gui): hold a lock, not an opinion about who launched first
Behnam-RK Aug 21, 2026
b9f674b
fix(gui): retract what is registered, not only what is enabled
Behnam-RK Aug 21, 2026
d80606a
fix(gui): make the migration's promised retry actually reachable
Behnam-RK Aug 21, 2026
38b78c7
fix(gui): the migration must not carry forward an off it was never to…
Behnam-RK Aug 21, 2026
51c1e0e
fix(gui): answer the hand-off once, and off the main thread
Behnam-RK Aug 21, 2026
e65a193
fix(gui): make the hand-off a claim, not a read
Behnam-RK Aug 21, 2026
74c2cbf
fix(gui): debounce the hand-off effect instead of over-refusing signals
Behnam-RK Aug 21, 2026
1aaa095
fix(gui): serialize login-item mutations, and stop over-refusing
Behnam-RK Aug 21, 2026
d16bd83
fix: stop writing a root marker to a guessable path, and report every…
Behnam-RK Aug 21, 2026
3bc8ba9
fix: drop the age cutoff the discard already covers
Behnam-RK Aug 21, 2026
c0d471c
fix(gui): retract a dormant legacy login item instead of leaving it a…
Behnam-RK Aug 21, 2026
e75d0ca
fix: read the home from /Search, and stop claiming refusals that neve…
Behnam-RK Aug 21, 2026
08b72f0
fix(gui): make both migration flags equally durable, and refuse coexi…
Behnam-RK Aug 21, 2026
c283fcc
fix(gui): split the two stuck states, which are opposite facts
Behnam-RK Aug 21, 2026
af418dd
fix(gui): stop gating the hand-off on a preference the reopen path ig…
Behnam-RK Aug 21, 2026
8813abf
fix: parse dscl as a plist, and accept subfolders of /Applications
Behnam-RK Aug 22, 2026
f6c20ed
fix: look for the bundle the app was allowed to register from
Behnam-RK Aug 22, 2026
e54fe27
fix: undo a rename that reached a shipped changelog entry
Behnam-RK Aug 22, 2026
9aac813
fix(gui): the Settings switch must respect the install location too
Behnam-RK Aug 22, 2026
c0f34d3
fix(gui): a drift check that could not fail, and a flag written too late
Behnam-RK Aug 22, 2026
cfe94b2
fix(gui): persist the user's "off" before retracting anything
Behnam-RK Aug 22, 2026
e4444ae
fix(gui): answer from live state, not from the branch you are in
Behnam-RK Aug 22, 2026
54bd686
fix(gui): keep the explanation for a refusal the user has to act on
Behnam-RK Aug 22, 2026
0dd71af
fix(gui): the live state does not say which way the switch was moving
Behnam-RK Aug 22, 2026
848d437
fix(gui): the switch cannot show "waiting for approval", so the line …
Behnam-RK Aug 22, 2026
4088ce4
fix(gui): .notFound means "not registered", so stop reading it as "ca…
Behnam-RK Aug 22, 2026
6055408
fix(gui): identify hand-off requests instead of timing them
Behnam-RK Aug 22, 2026
5b9472e
fix(gui): one status read per decision, and claim by rename
Behnam-RK Aug 22, 2026
1a15035
fix(gui): retry every post-unregister read, not two of the four
Behnam-RK Aug 22, 2026
82d3068
fix(gui): settle the register-side read, and stop trusting a leftover…
Behnam-RK Aug 22, 2026
7ab0a42
fix(gui): remember several answered hand-offs, and stop find truncati…
Behnam-RK Aug 22, 2026
1649a69
fix: uninstall every install found, and yield only to a live incumbent
Behnam-RK Aug 22, 2026
6e8a1b5
fix(gui): decide a stale lock by where it lives, not by a timer
Behnam-RK Aug 22, 2026
ffb2805
fix(gui): a failed enable must move the switch back, and prune the ap…
Behnam-RK Aug 22, 2026
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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,54 @@ current as you land changes.

## [Unreleased]

### Fixed

- **"Open minimized" now actually decides whether the window opens.** The app
used to infer a login launch from `NSApplication.launchIsDefaultUserInfoKey`,
which reported wrong in both directions — the window appeared at login with
the setting on, and stayed hidden on a manual launch. The login item is now a
LaunchAgent shipped inside the bundle that passes `--background`, so the app
reads the launch kind instead of guessing it
([ADR-0014](docs/adr/0014-login-item-launch-marker.md)). The main window also
opted out of AppKit state restoration, which could reopen it at launch without
consulting the setting at all. Existing installs are migrated once, on first
launch; if you had login-at-launch switched off, it stays off.

Three consequences of the mechanism, handled in the same change. A launchd
agent starts the moment it is registered and — unlike the LaunchServices login
item it replaces — does not check whether the app is already running, so
turning "Open this app at login" on could leave you with two menubar icons; a
duplicate copy now exits at startup, and if you started it yourself it brings
the running copy forward with its window open rather than appearing to do
nothing. A launchd registration also does not disappear with the app bundle
the way a login item did, so `uninstall.sh` now has the app retract it before
deleting anything, instead of leaving an entry that fails to load at every
subsequent login; the Login Items entry reads "Dezhban" now instead of a raw
job label. And the login toggle says what actually happened — including when
macOS is holding the registration for your approval, and when it refuses to
remove the old login item and only you can clear it in System Settings; either
state can still be switched back off, which an earlier build could not do.

Dezhban also stops relying on macOS's "Reopen windows when logging back in" to
leave it alone: that path relaunches the app at login without the marker, so it
is now opted out of explicitly, leaving the login item as the only thing that
starts the app at login. And clicking the login switch acts on the state you
clicked, not on a re-read a moment later — with the Settings window open,
removing the login item in System Settings and coming back used to make the
next click turn login-at-launch *on*. If you had switched Dezhban off under
System Settings → General → Login Items, upgrading leaves it off; and a copy of
the app run from somewhere other than `/Applications` no longer claims the
login item for a location it is about to be moved out of — and neither does the
Settings switch, which now says so rather than registering a login item that would
break the moment the copy moves. The login switch also
no longer freezes the Settings window while macOS thinks about it, and reads OFF
rather than ON when the item you see is one you had already switched off in
System Settings. Uninstalling clears Dezhban's saved app preferences too —
leaving them behind meant a later install silently skipped the login-item
migration. If macOS refuses to retract the login item — or the app bundle was
already in the Trash, so nothing can — the uninstaller now says so instead of
reporting a clean removal.

## [0.11.0] - 2026-08-21

### Added
Expand Down
501 changes: 501 additions & 0 deletions docs/adr/0014-login-item-launch-marker.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ New records use [template.md](template.md) and take the next free number.
| [0011](0011-biometric-enrollment-requires-a-signed-build.md) | Biometric token enrollment requires a signed build, so unsigned builds must refuse it | accepted, implemented (Alternative 3 superseded by 0012) |
| [0012](0012-app-checked-biometrics-on-unsigned-builds.md) | App-checked biometrics on unsigned builds, rather than no biometrics | accepted, implemented |
| [0013](0013-geo-provider-pass-opt-out.md) | The geo-provider pass gets an opt-out, not a redesign | accepted, implemented |
| [0014](0014-login-item-launch-marker.md) | The login item carries an explicit launch marker | accepted, implemented |

> **0006 is the one to read first if you are touching the geo lookup.** It records why
> the obvious implementation silently defeats the exit-country check, and it exists
Expand Down
27 changes: 27 additions & 0 deletions docs/concepts/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ available, root-only, and independent of the socket.
root, **with no daemon running**. Deliberately not a socket operation, because the escape
hatch must never depend on the thing it is escaping from.

**Launch marker** — the `--background` argument the macOS app's login LaunchAgent
passes and nothing else does, so the app knows macOS started it at login rather
than the user starting it. What the "Open minimized" setting decides on; before
it, the app inferred the launch kind from an AppKit key that read wrong in both
directions ([ADR-0014](../adr/0014-login-item-launch-marker.md)).

**Login agent** — `Contents/Library/LaunchAgents/com.behnam-rk.dezhban.app.login.plist`
inside `Dezhban.app`, registered with `SMAppService.agent(plistName:)`. It is what
starts the app at login, and it exists in place of `SMAppService.mainApp` solely
because a LaunchAgent can pass the **launch marker**. Unlike the login item it
replaced it does not disappear with the bundle, so `uninstall.sh` has the app
retract it.

**Session lock** — an exclusive `flock` the **macOS app** holds for its lifetime,
one per install, so a second copy of the same bundle exits at startup instead of
running a second menubar item, Dock tile and state-file timer. Needed because
registering the login agent starts the app immediately and launchd, unlike
LaunchServices, does not care that it is already running. Distinct from the
**single-instance lock** below, which is the daemon's and guards `Backend.Apply`;
this one guards nothing but the app's own uniqueness.

**Hand-off request** — a file beside the **session lock** by which a copy of the
app that is exiting asks the copy that owns the session to show its window, so a
launch the user performed is never a silent no-op. A file rather than only a
notification because the notification is never queued and the owner may not be
observing yet.

**Single-instance lock** — an exclusive lock `run` holds over the state directory
for its entire lifetime, so a second `run` — with or without `--no-daemon` —
refuses outright instead of racing the first to call `Backend.Apply`. Released
Expand Down
Loading