hyprland: match scratchpads by dedicated ghostty class, not title

This commit is contained in:
rope 2026-08-16 19:18:32 +01:00
parent e5566fc412
commit f82a6430ea

View file

@ -263,17 +263,25 @@ in
hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 }) hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 })
hl.layer_rule({ match = { namespace = "quickshell-osd" }, blur = true, ignore_alpha = 0.3 }) hl.layer_rule({ match = { namespace = "quickshell-osd" }, blur = true, ignore_alpha = 0.3 })
-- Scratchpad respawn: both special-workspace terminals are meant to -- Both special-workspace terminals get their own --class, which does
-- exist for the whole session, so SUPER+Q on them reads as "reset -- two things ghostty's --title cannot. First, gtk-single-instance
-- this thing", not "close it". Relaunch on close. -- means a second `ghostty` hands its request to the running instance
-- and exits; forwarded requests keep -e but DROP --title, so the
-- window ends up named by the shell and no title rule matches it.
-- A distinct class forces a separate instance. Second, the class is
-- fixed at surface creation, so the shell's own title escapes can't
-- desync the window rules or the respawn below.
--
-- Respawn: these are meant to exist for the whole session, so SUPER+Q
-- on them reads as "reset this thing", not "close it".
local scratchpads = { local scratchpads = {
["scratchpad"] = "ghostty --title=scratchpad", ["dev.fred.scratchpad"] = "ghostty --class=dev.fred.scratchpad --title=scratchpad",
["spotify-player"] = "ghostty --title=spotify-player -e spotify_player", ["dev.fred.spotify"] = "ghostty --class=dev.fred.spotify --title=spotify-player -e spotify_player",
} }
local shutting_down = false local shutting_down = false
hl.on("hyprland.shutdown", function() shutting_down = true end) hl.on("hyprland.shutdown", function() shutting_down = true end)
hl.on("window.close", function(win) hl.on("window.close", function(win)
local cmd = scratchpads[win.title] local cmd = scratchpads[win.class]
if cmd and not shutting_down then hl.exec_cmd(cmd) end if cmd and not shutting_down then hl.exec_cmd(cmd) end
end) end)
@ -295,11 +303,11 @@ in
-- Warms spotify-player's Spotify Connect device while you're -- Warms spotify-player's Spotify Connect device while you're
-- doing something else; the window rule above swallows it onto -- doing something else; the window rule above swallows it onto
-- special:music silently, so it never steals focus. -- special:music silently, so it never steals focus.
hl.exec_cmd("ghostty --title=spotify-player -e spotify_player") hl.exec_cmd(scratchpads["dev.fred.spotify"])
-- Scratchpad terminal, same trick: parked on special:scratch at -- Scratchpad terminal, same trick: parked on special:scratch at
-- login so SUPER+Return is always an instant toggle and the shell -- login so SUPER+Return is always an instant toggle and the shell
-- keeps its history/cwd between peeks. -- keeps its history/cwd between peeks.
hl.exec_cmd("ghostty --title=scratchpad") hl.exec_cmd(scratchpads["dev.fred.scratchpad"])
-- swayosd removed; volume/brightness OSD handled by quickshell -- swayosd removed; volume/brightness OSD handled by quickshell
${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''} ${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''}
end) end)
@ -343,38 +351,38 @@ in
-- Spotify's backend, and that cost is per *process*, so relaunching -- Spotify's backend, and that cost is per *process*, so relaunching
-- a terminal for it means waiting every time. Parking one instance -- a terminal for it means waiting every time. Parking one instance
-- here pays it once at login; SUPER+M slides it in and out. -- here pays it once at login; SUPER+M slides it in and out.
-- Matched on title, not class: gtk-single-instance means every -- Matched on the dedicated --class set above, not on title: a shell
-- ghostty window is the same app-id, and --title is authoritative -- prompt rewrites its window title, and forwarded single-instance
-- (ghostty ignores the program's own title escapes once it is set). -- launches never carry --title in the first place.
hl.window_rule({ hl.window_rule({
match = { title = "^spotify-player$" }, match = { class = "^dev.fred.spotify$" },
workspace = "special:music silent", workspace = "special:music silent",
}) })
-- Inset it rather than filling the screen, so the workspace -- Inset it rather than filling the screen, so the workspace
-- underneath still frames it. Floating is what makes a percentage -- underneath still frames it. Floating is what makes a percentage
-- size stick: a tiled window is sized by the layout, which would -- size stick: a tiled window is sized by the layout, which would
-- just expand it back to the full area. Matched on title for the -- just expand it back to the full area. Matched on class for the
-- same reason as above, and because matching on the special -- same reason as above, and because matching on the special
-- workspace would depend on the assignment rule running first. -- workspace would depend on the assignment rule running first.
hl.window_rule({ hl.window_rule({
name = "spotify-player-inset", name = "spotify-player-inset",
match = { title = "^spotify-player$" }, match = { class = "^dev.fred.spotify$" },
float = true, float = true,
size = "80% 80%", size = "80% 80%",
center = true, center = true,
}) })
-- Scratchpad terminal same title-match + float-to-inset pattern as -- Scratchpad terminal same class-match + float-to-inset pattern as
-- spotify-player above. -- spotify-player above.
hl.window_rule({ hl.window_rule({
match = { title = "^scratchpad$" }, match = { class = "^dev.fred.scratchpad$" },
workspace = "special:scratch silent", workspace = "special:scratch silent",
}) })
hl.window_rule({ hl.window_rule({
name = "scratchpad-inset", name = "scratchpad-inset",
match = { title = "^scratchpad$" }, match = { class = "^dev.fred.scratchpad$" },
float = true, float = true,
size = "80% 80%", size = "80% 80%",
center = true, center = true,