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_payload → wm_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
- 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).
- 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.
- 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.
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_windowsframe off a running daemon:{"window_id": "37794", "label": "Firefox", "icon": null}layouts.macos/firefox.yamlcarriesdisplay_name: Firefoxandicon: {source: simple-icons, name: firefox};icon: nullproves the row took the default-fallback path inlabel_for_window/icon_for_window, not the matched-layout path.Cause
Two identity sources with different casing, and an exact-match comparison between them:
osascriptprocess name (MacFocusBackend.get_active_app)firefoxkCGWindowOwnerName(_window_info_from_cg_payload→wm_class)FirefoxLayout.matches_identity(daemon/deckd/layouts.py) is an exact membership test:Layout tokens are lowercase process names because they were written for the focus path, so
Firefoxnever matchesfirefox. 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
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 Linuxwm_classvalues are conventionally case-sensitive (Navigatorvsnavigator).wm_classin_window_info_from_cg_payload, keepingapp_nameas-is for the label. Contained blast radius, but leaves the general mismatch class unsolved and makeswm_classmean something slightly different per platform.Option 1 or 2 is the real choice; flagging it as a decision rather than a cleanup because it touches shared matching semantics.
Acceptance
running_windowsrow for an app with a matching layout carries that layout'sdisplay_nameandiconon macOS.test_layouts.pymatcher tests stay green).WindowInfo(wm_class="Firefox")against a layout withmatch: [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.