Skip to content

v0.2.0

Choose a tag to compare

@trsdn trsdn released this 19 Sep 16:36
· 17 commits to main since this release
6a35cf1

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.