hypr: reassert joplin workspace on window open
Joplin (Wayland-native Electron) reasserts its own window right after it maps, escaping the map-time `silent` workspace rule, so it opened on the focused workspace instead of its app-home (ws 4 / HDMI-A-2). Add a data-driven `reassert` placement flag: rules.lua emits a window.open handler that re-applies the assignment by address (follow = false, silent) for flagged apps. Only Joplin opts in; the static rule stays as a fallback and the other app-homes are untouched. Document the flag in CLAUDE.md and AGENTS.md.
This commit is contained in:
@@ -37,7 +37,7 @@ The data model separates concerns:
|
||||
- **`compositor`** — string, `"hyprland"` or `"niri"`. Branch with `eq .chezmoi.config.data.compositor "hyprland"`.
|
||||
- **`apps`** — list of optional programs present on this machine (e.g. `["steam", "spotify", "jellyfin"]`). Gate with `has "steam" .chezmoi.config.data.apps`.
|
||||
- **`data.monitors`** — array (size varies per machine). Each has `name`, optional `primary = true`, and a `role` (`"primary"`/`"left"`/`"right"`) used by placement.
|
||||
- **`data.placement`** — array of app-home workspaces: `id`, `name`, `role`, `apps` (window classes routed here), `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`. A `role` resolves to the monitor carrying it, falling back to the primary monitor when absent (single-monitor machines "just work"). `requires` drops the entry unless that program is in `apps`. Consumed by `hypr/hyprland.d.lua/workspaces.lua.tmpl` (workspace_rule) and `rules.lua.tmpl` (window_rule).
|
||||
- **`data.placement`** — array of app-home workspaces: `id`, `name`, `role`, `apps` (window classes routed here), `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`/`reassert`. A `role` resolves to the monitor carrying it, falling back to the primary monitor when absent (single-monitor machines "just work"). `requires` drops the entry unless that program is in `apps`. `reassert` re-applies the workspace assignment by address via a `window.open` handler in `rules.lua.tmpl`, for apps (e.g. Joplin) whose window escapes the map-time silent rule and opens on the focused workspace. Consumed by `hypr/hyprland.d.lua/workspaces.lua.tmpl` (workspace_rule) and `rules.lua.tmpl` (window_rule).
|
||||
- **`data.microphones`** — array; only meaningful when `pipewire = true`.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -13,7 +13,7 @@ Hyprland configuration uses the v0.55+ **Lua** config: entry point `hyprland.lua
|
||||
## Chezmoi Config (chezmoi.toml)
|
||||
- Source of truth: `~/.config/chezmoi/chezmoi.toml` (not tracked here).
|
||||
- Data model: `tags` (booleans: `laptop`, `touchscreen`, `pipewire`, `dev`, `bluetooth` — no `desktop`, use `not laptop`), `compositor` (`"hyprland"`/`"niri"`), `apps` (optional programs, gate with `has "x" .apps`), `data.monitors` (array; each `name` + optional `primary` + `role`), `data.placement` (app-home workspaces), `data.microphones`.
|
||||
- **Workspace placement is data-driven.** `data.placement[]` entries define the app-home workspaces (`id`, `name`, monitor `role`, `apps` = window classes routed there, `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`). `workspaces.lua.tmpl` turns each into a `workspace_rule` (resolving `role` → monitor name, falling back to the `primary` monitor when that role is absent), and `rules.lua.tmpl` turns each `apps` class into a `window_rule` routing to that workspace. To move an app or add a new app-home, edit `data.placement` — do **not** hardcode monitor names in the templates.
|
||||
- **Workspace placement is data-driven.** `data.placement[]` entries define the app-home workspaces (`id`, `name`, monitor `role`, `apps` = window classes routed there, `silent`, optional `layout`/`layoutopts`/`on_created_empty`/`requires`/`reassert`). `workspaces.lua.tmpl` turns each into a `workspace_rule` (resolving `role` → monitor name, falling back to the `primary` monitor when that role is absent), and `rules.lua.tmpl` turns each `apps` class into a `window_rule` routing to that workspace. `reassert = true` additionally emits a `window.open` handler that re-applies the assignment by address, for apps (e.g. Joplin) that ignore the map-time silent rule and open on the focused workspace. To move an app or add a new app-home, edit `data.placement` — do **not** hardcode monitor names in the templates.
|
||||
- Reference structure (current machine):
|
||||
```toml
|
||||
[data]
|
||||
|
||||
@@ -59,6 +59,26 @@ hl.window_rule({ match = { class = "^({{ join "|" $classes }})$" }, workspace =
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
-- App-home reassertion: some apps (Joplin) ignore the static silent workspace
|
||||
-- rule at map time and open on the focused workspace instead. For placements
|
||||
-- flagged `reassert`, re-apply the assignment by address when the window opens.
|
||||
local reassert_homes = {
|
||||
{{- range $p := .placement }}
|
||||
{{- if and (index $p "reassert" | default false) (or (not (hasKey $p "requires")) (has $p.requires $apps)) }}
|
||||
{{- range $c := $p.apps }}
|
||||
["{{ $c }}"] = "{{ $p.id }}",
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
hl.on("window.open", function(w)
|
||||
if not (w and w.class and w.address) then return end
|
||||
local ws = reassert_homes[w.class]
|
||||
if ws then
|
||||
hl.dispatch(hl.dsp.window.move({ workspace = ws, window = "address:" .. w.address, follow = false }))
|
||||
end
|
||||
end)
|
||||
|
||||
-- Game Content Bypass (Option A - per-monitor CTM bypass)
|
||||
-- Automatically mark Steam games as "game" content type
|
||||
hl.window_rule({ match = { class = "^steam_app_%d+$" }, content = "game" })
|
||||
|
||||
Reference in New Issue
Block a user