You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Repository conformance record at .github/conformance.yml, assessed against
version 1.5.1 of the trsdn Repository Quality Standard, with the reasoning
behind every result in docs/self-assessment.md and the badge rendered from
the record into .github/badges/conformance.svg. A scheduled workflow
re-validates both, so a record that drifts from its badge, or an assessment
that ages past the review cadence, turns the check red on its own.
scripts/check.sh, the single command that validates a change before it is
proposed: build with warnings as errors, tests, markdown lint, and the bundle
metadata checks below. It needs no signing identity, no permissions, and no
network, so it behaves the same on a laptop, in CI, and for an agent.
Drift guards for the three facts nothing used to compare. The version in Info.plist is checked against the newest release heading in this file, and
the minimum macOS version is checked across Package.swift, scripts/build-app.sh, and the README badge. All of them had been maintained
by hand in parallel.
Product identity in the bundle: repository URL, issue tracker URL, licence
identifier, and copyright holder as Info.plist keys, plus the licence text
copied into Contents/Resources. The About tab now reads its links back out
of the bundle rather than hardcoding them a second time, so the shipped
metadata and what the user sees cannot disagree.
Issue forms for bug reports and proposals, a CODEOWNERS file, a Dependabot
configuration for the pinned action versions, .github/github-app.yml, and a
markdown lint workflow.
.gitattributes, marking Resources/AppIcon.icns as generated. It is
rewritten by every build, so hand-editing it accomplishes nothing.
README sections stating the cases that were previously only implicit: the app
collects nothing and opens no outbound connection, preferences live in UserDefaults under com.openswitchr.app and how to read or delete them,
the project is English-only, and which accessibility limitations are known.
AGENTS.md now names the operations an agent must not perform, the paths that
are generated, and the review expectation for agent-authored changes.
WindowFilter: one pure value type describing which windows a surface wants
and in what order, applied by both frontends. Four axes — application scope,
minimized handling, display scope, and order — with the switcher's
configurable in Settings and the Dock preview's fixed and permissive, because
the pointer already chose the application. Defaults reproduce the previous
behaviour exactly.
openswitchr-diag --filters, which applies the profiles to the windows
actually open and checks the one axis unit tests cannot judge: that scoping
by display leaves no window claimed by no display.
Dock previews can switch instantly while one is already open, so the open
delay applies to the first preview only and moving along the Dock does not
wait again. On by default. A hover that resolves before the index has caught
up is retried once the rebuild lands, rather than leaving the pointer on an
icon with nothing shown.
A per-application rule table (AppRule, AppRuleTable), keyed by
bundle-identifier prefix, with two independent axes: hide a window outright
(never / always / when its title matches), and stand aside for it — swallow
nothing, raise nothing — while it is frontmost and full screen. Ships with
verified defaults for the known correctness case: a remote desktop, screen
share, or virtual machine running full screen now gets the switcher hotkey
itself rather than an overlay raised over it. Full screen is detected once
per rebuild, from the window's frame against the display it covers, and
carried on WindowInfo.isFullScreen so the event tap never has to ask. openswitchr-diag --filters reports both against the windows actually open.
A minimized window keeps its last good thumbnail. ScreenCaptureKit cannot
capture a window in the Dock, so that image is the only preview there will
be; the store now exempts it from the refresh age limit (which it could never
satisfy), evicts it after every live entry when the byte budget is exceeded,
and drops it when the window is restored so the first frame after restoring
is not stale. The tile already dims a minimized preview and marks it as such.
Icon-and-title tile mode, chosen deliberately instead of by accident. A
"Tiles show" setting picks previews or icons only, and previews switch to
icons on their own when Screen Recording is not granted (every capture would
fail) or when a surface has more than twelve windows (each preview too small
to identify). The threshold is a named constant per surface. In icon mode
nothing calls ThumbnailProvider.prefetch, so a busy Space costs zero
captures; the mode is decided when a panel opens and kept for that session,
and the cache is left valid rather than cleared. For scale, eight cold
parallel captures measured ~340 ms with openswitchr-diag --bench --capture.
Switcher tiles can shrink so every window fits without scrolling
("Shrink switcher tiles so every window fits", on by default). The configured
width is the upper limit and is never exceeded: with a few windows nothing
changes, and with more than fit in the overlay's three visible rows the tile
steps down in twenty point steps until they do. Sizes are quantised so the
thumbnail cache keeps hitting and the step is what is captured, and below the
legible floor the switcher uses icon tiles instead of a smaller image. The
layout maths is TileSizing, pure and unit tested.
Thumbnail captures are bounded, prioritised, and cancellable
(CaptureLimiter, unit tested). At most four run at once instead of one per
tile, the selected tile is asked for first and jumps the queue, and dismissing
a panel cancels every capture that has not started, so they do not complete
into a cache nobody will read. A cancelled request records nothing, so a later
one simply tries again.
Changed
main is protected: pull requests are required, the three CI checks must
pass, and force pushes and branch deletion are blocked. Secret scanning, push
protection, Dependabot security updates, and private vulnerability reporting
are enabled.
The repository quality section of AGENTS.md was reassessed. It had described
a default branch holding two files, which stopped being true when the work was
merged, and it assessed against version 1.3.3 of a standard now at 1.5.1.
Reassessed against version 1.11.1 of the standard (previously 1.5.1), the
latest version actually tagged in trsdn/.github — the reusable conformance
workflow resolves the standard at its published tag, and the untagged
1.12.0 bump on its main is not yet consumable. No criterion regressed; the
ten added since 1.5.1 were assessed for the first time and surfaced one real
gap, S12: actions/checkout@v4 and the reusable conformance workflow
pinned to @main can both change underneath the repository. docs/self-assessment.md and the badge are updated to match.
The rule deciding whether a CoreGraphics window with no accessibility
counterpart belongs in the index moved out of WindowIndex.apply into WindowAdmission.admits, where it is documented and unit tested. Behaviour
is unchanged.
Fixed
In-app updates from GitHub Releases (#30), through AppUpdater 4.1.2 pinned exact: with Package.resolved committed, the same setup as the sibling apps. This is the
app's first third-party dependency and its first network connection, so the
README's Privacy section, the project page and Settings now say so: one daily
request to GitHub, no identifier, and "Check for Updates Automatically" turns
it off entirely. The menu gains Check for Updates…, an update that is ready
offers "Install and Restart", and the app stops its event tap, Dock observer and
panels before the bundle is replaced. The schedule (UpdateSchedule: a daily
check that wakes hourly so a slept-through deadline catches up, and survives a
clock that moved back) and the state rules (UpdateState: a failed background
check stays silent, a manual one always answers) are pure and unit tested. THIRD_PARTY_NOTICES.txt carries AppUpdater's (Unlicense) and its dependency
Version's (Apache-2.0) licenses verbatim and is copied into the bundle. The
broker side (an OpenSwitchr-<semver>.dmg asset, the dependency lock, the
resource bundle) landed in trsdn/macos-notarization-broker#56, but no release
with the updater has been built, and a real update has never been run, both
recorded in RELEASE_CHECKLIST.md.
Two German layout and wording fixes found by rendering the Settings views
offscreen in German for the first time (#57): the "Präfix der Bundle-Kennung"
label wrapped onto two lines and squeezed its own text field, so it is now the
field's prompt; and the update explanation switched to informal "du" while every
other string is impersonal. The other tabs, including the two labels #5 warned
about, fit without truncation.
The interface is localized, with German as the first additional language
(#5). Two String Catalogs, Localizable.xcstrings for the app and UI.xcstrings
for the shared views, are compiled by SwiftPM and copied into the app bundle by scripts/build-app.sh; counts use plural variants; the product name and the
modifier symbols are never translated. LocalizationCatalogTests fails on an
untranslated entry, a dropped placeholder, an incomplete plural, a lost product
name, or a plain literal that never reached a catalog (mutation-tested). The
German was written by an AI assistant and has not been reviewed by a native
speaker. The release broker's openswitchr adapter now copies the .lproj
directories too (trsdn/macos-notarization-broker#56), but no release has been
built since, and a per-release check is in RELEASE_CHECKLIST.md. Info.plist gains CFBundleDevelopmentRegion and CFBundleLocalizations.
The switcher can list running applications that have no open windows (off by
default, "Applications with no windows"). They come after the windows, and
choosing one activates the application and asks it to open a window by
launching it again, which sends the reopen event a Dock click sends; not every
application makes a window on that, and some make an unexpected one, which the
setting says. Built when the overlay opens and only if the setting is on, from
a list of running applications read once (10-18 ms measured here), so the
index and its rebuild path keep resolving only processes that own a window. An
entry is a WindowInfo marked isApplicationOnly with a synthetic id above a
reserved base, so both frontends, the thumbnail cache and the actions already
understand it, and the few that must differ (no capture, no raise) can ask. WindowlessApplications and the filter axis are unit tested, and openswitchr-diag --filters reports them against the real running applications.
Optional scroll-to-cycle on a Dock icon (off by default): scrolling with the
pointer on an application's icon focuses its next or previous window without
opening a preview. It is the version the deferral said was the only one worth
building: the scroll tap exists disabled, is enabled on hover-enter and disabled
on hover-leave, and its callback also checks the event's location against the
hovered icon, so a missed leave cannot leave scrolling swallowed anywhere else.
Step accumulation (ScrollStepper: one step per event at most, rate-limited,
reversal discards) and which window is next (WindowCycle: a stable order, not
recency, so repeated steps visit every window instead of ping-ponging) are pure
and unit tested. The callback's per-event work measured about 105 ns in a debug
build.
An optional second switcher hotkey (off by default): the hold modifier with the
backtick key opens the switcher for the current application's windows only, the
question that is awkward to express as a search. The profile is fixed and
inherits every other axis from the configured filter (SwitcherProfile, unit
tested), so it is one toggle rather than a second copy of the settings, and
which key opens which profile is a tested lookup, so the tap callback stays
trivial. The tap now remembers the key code it swallowed rather than assuming
Tab.
The per-application rules are editable: a new Apps tab lists them, adds one
by bundle identifier prefix or from a running application, and restores the
shipped defaults. Each hiding rule shows how many windows it removes right now
(AppRuleTable.hiddenCount), so a rule that empties the switcher is noticed
where it was set. Stored as JSON under appRules; nothing stored, or data that
no longer parses, yields the defaults rather than an empty table, which would
silently switch off the stand-aside protection. Editing a rule recomputes
whether the hotkey stands aside immediately.
A project page at docs/index.html, served by GitHub Pages: what it is, who it
is for, status and version, how to get it, the privacy statement, and the
repository, license, security and support links. One page, no scripts, no
third-party resources, no cookies, styled with Instrument Workshop v1.5.1
vendored unmodified with its version recorded in docs/assets/VERSION. The
Published Sites criteria (W01-W08) are now assessed and pass; the network
review behind W07 is in docs/self-assessment.md.
openswitchr-diag --check-budgets turns the measured performance numbers
into thresholds that can fail: cold and warm index rebuild, cold thumbnails,
and cache hits, checked against budgets kept in one file next to the harness.
It exits non-zero when a budget is exceeded or could not be measured (a check
that skipped what it could not measure would go green the day the measurement
broke). Wall-clock and machine-dependent by nature, so it is a step in RELEASE_CHECKLIST.md, not a hosted CI gate. The comparison is PerformanceBudget.evaluate, pure and unit tested.
The Dock preview is placed from where the Dock item actually is
(DockPanelPlacement, unit tested). A left or right Dock was misdetected as a
bottom one, because the edge check required the icon to be within 4 pt of the
screen edge and a Dock icon sits inside the Dock's own padding; the edge is now
the nearest one. The screen is the one containing the item's centre instead of NSScreen.main, the panel is clamped inside that screen's visible frame, and a
side Dock gets a scrolling column rather than a row that runs off the screen.
The Dock preview's lifetime is one state machine (DockPanelLifecycle, unit
tested) instead of a scheduled hide plus a fire-time check. The Dock item and
the panel are one hover region: moving between them cancels a pending hide,
and a stale timer does nothing. A panel with no pointer movement and no
interaction for ten seconds now closes itself; the timer exists only while a
panel is on screen and hide() cancels it on every exit.
openswitchr-diag --probe-app decides whether focus moved by the frontmost
window's CGWindowID instead of by a rendered "app — title" string, which
read a correct switch as a failure whenever two windows shared a title. The
Dock preview's "hidden on exit" check now polls up to three seconds instead of
sleeping a fixed 900 ms, which reported the preview still visible once and
never again.
Dependabot now watches the Swift package as well as the workflows. AppUpdater is
pinned exact:, which does not move by itself, so an upstream fix would never
arrive; the code that downloads and installs updates should not age quietly.
The two sibling ### Added headings under 0.1.0 in this file are merged
into one, which is both a lint failure and genuinely confusing to read.
actions/checkout and the reusable trsdn/.github conformance workflow are
pinned to a full commit SHA instead of @v7 and @main. Both were mutable
references that could change underneath this repository without a commit
here — the S12 gap the last reassessment found. S12 moves to pass.
A modifier release landing in the gap between the hotkey opening the
overlay and the overlay reporting itself visible no longer strands the
overlay on screen. HotkeySessionGate tracks the open request separately
from confirmed visibility, so a fast tap of the hotkey still commits.
The switcher overlay now picks up an index rebuild that lands after it
opened, instead of showing a stale list — including an empty one under a
restrictive filter — for the rest of the session. Selection is preserved by
window identity across the refresh, not by position.