Skip to content

macOS: enumerated window rows never match their layout (case-sensitive identity matching) #140

Description

@jonocodes

Summary

On macOS, rows in the running-windows switcher never resolve to their layout. The list shows the bare app name with no brand icon and no display_name, even when a matching layout exists — while the same app's layout switches correctly on focus.

Verified live on macOS 15.6.1 / Apple Silicon (2026-08-06) by reading a running_windows frame off a running daemon:

{"window_id": "37794", "label": "Firefox", "icon": null}

layouts.macos/firefox.yaml carries display_name: Firefox and icon: {source: simple-icons, name: firefox}; icon: null proves the row took the default-fallback path in label_for_window / icon_for_window, not the matched-layout path.

Cause

Two identity sources with different casing, and an exact-match comparison between them:

path identity source value
focus osascript process name (MacFocusBackend.get_active_app) firefox
enumeration kCGWindowOwnerName (_window_info_from_cg_payloadwm_class) Firefox

Layout.matches_identity (daemon/deckd/layouts.py) is an exact membership test:

return any(token in (app.app_id, app.wm_class) for token in self.match)

Layout tokens are lowercase process names because they were written for the focus path, so Firefox never matches firefox. Focus-driven layout switching is unaffected — this only hits _window_to_app() consumers, i.e. the switcher rows (#119 / #120 / #126).

Not macOS-specific in principle: any backend whose enumeration identity differs in case from its focus identity would hit the same wall. macOS is just where the two sources genuinely disagree today.

Options

  1. Case-insensitive matches_identity — one-line fix (casefold() both sides), fixes every backend at once. Changes cross-platform matching semantics: two layouts differing only by case would collide, and Linux wm_class values are conventionally case-sensitive (Navigator vs navigator).
  2. Normalise in the macOS backend — map the owner name to a lowercase wm_class in _window_info_from_cg_payload, keeping app_name as-is for the label. Contained blast radius, but leaves the general mismatch class unsolved and makes wm_class mean something slightly different per platform.
  3. Match on a normalised identity tuple — introduce an explicit "identity key" normalisation used by both the matcher and the backends. Most correct, most work.

Option 1 or 2 is the real choice; flagging it as a decision rather than a cleanup because it touches shared matching semantics.

Acceptance

  • A running_windows row for an app with a matching layout carries that layout's display_name and icon on macOS.
  • Focus-driven layout resolution is unchanged (existing test_layouts.py matcher tests stay green).
  • A regression test pins the casing case — e.g. a WindowInfo(wm_class="Firefox") against a layout with match: [firefox].

Context

Found during a macOS upkeep pass; documented in docs/PLATFORM-PARITY.md (row "Window row → layout match" plus the macOS backend note) and the README macOS table.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfocus-watcherActive-window / app-awareness detectionhuman-verification-requiredCode is complete; a human must verify on real hardware / a live session before closing

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions