Restore rofi window picker for Hyprland
This commit is contained in:
@@ -423,6 +423,10 @@ bind = $mainMod SHIFT, Z, movewindow, mon:+1
|
|||||||
# WINDOW MANAGEMENT
|
# WINDOW MANAGEMENT
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bind = $mainMod, G, exec, hypr_rofi_window go
|
||||||
|
bind = $mainMod, B, exec, hypr_rofi_window bring
|
||||||
|
bind = $mainMod SHIFT, B, exec, hypr_rofi_window replace
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# MEDIA KEYS
|
# MEDIA KEYS
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1243,15 +1243,9 @@ bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"))
|
|||||||
bind(main_mod .. " + V", exec("wl-paste | xdotool type --file -"))
|
bind(main_mod .. " + V", exec("wl-paste | xdotool type --file -"))
|
||||||
bind(main_mod .. " + Tab", hyprexpo("toggle"))
|
bind(main_mod .. " + Tab", hyprexpo("toggle"))
|
||||||
bind(main_mod .. " + SHIFT + Tab", hyprexpo("bring"))
|
bind(main_mod .. " + SHIFT + Tab", hyprexpo("bring"))
|
||||||
bind(main_mod .. " + G", function()
|
bind(main_mod .. " + G", exec("hypr_rofi_window go"))
|
||||||
enter_window_picker("go")
|
bind(main_mod .. " + B", exec("hypr_rofi_window bring"))
|
||||||
end)
|
bind(main_mod .. " + SHIFT + B", exec("hypr_rofi_window replace"))
|
||||||
bind(main_mod .. " + B", function()
|
|
||||||
enter_window_picker("bring")
|
|
||||||
end)
|
|
||||||
bind(main_mod .. " + SHIFT + B", function()
|
|
||||||
enter_window_picker("replace")
|
|
||||||
end)
|
|
||||||
|
|
||||||
bind(main_mod .. " + W", function()
|
bind(main_mod .. " + W", function()
|
||||||
focus_direction("up")
|
focus_direction("up")
|
||||||
|
|||||||
254
dotfiles/lib/bin/hypr_rofi_window
Executable file
254
dotfiles/lib/bin/hypr_rofi_window
Executable file
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PROMPTS = {
|
||||||
|
"go": "Go to window",
|
||||||
|
"bring": "Bring window",
|
||||||
|
"replace": "Replace with",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_hyprland_instance():
|
||||||
|
if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
|
||||||
|
return
|
||||||
|
|
||||||
|
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"))
|
||||||
|
hypr_dir = runtime / "hypr"
|
||||||
|
if not hypr_dir.is_dir():
|
||||||
|
return
|
||||||
|
|
||||||
|
sockets = []
|
||||||
|
for socket in hypr_dir.glob("*/.socket.sock"):
|
||||||
|
try:
|
||||||
|
sockets.append((socket.stat().st_mtime, socket.parent.name))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sockets:
|
||||||
|
os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = max(sockets)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def run_json(*args):
|
||||||
|
result = subprocess.run(args, check=False, text=True, capture_output=True)
|
||||||
|
if result.returncode != 0 or not result.stdout.strip():
|
||||||
|
return None
|
||||||
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_lua(expression):
|
||||||
|
return subprocess.run(["hyprctl", "dispatch", expression], check=False).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_legacy(*args):
|
||||||
|
return subprocess.run(["hyprctl", "dispatch", *args], check=False).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def normal_workspace(window):
|
||||||
|
workspace = window.get("workspace") or {}
|
||||||
|
workspace_id = workspace.get("id")
|
||||||
|
return isinstance(workspace_id, int) and workspace_id >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def window_address(window):
|
||||||
|
address = window.get("address") or ""
|
||||||
|
return address if isinstance(address, str) and address else None
|
||||||
|
|
||||||
|
|
||||||
|
def clean_text(value):
|
||||||
|
return str(value or "").replace("\t", " ").replace("\n", " ").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def app_dirs():
|
||||||
|
seen = set()
|
||||||
|
for base in os.environ.get("XDG_DATA_DIRS", "/run/current-system/sw/share:/usr/share:/usr/local/share").split(":"):
|
||||||
|
path = Path(base) / "applications"
|
||||||
|
if path.is_dir() and path not in seen:
|
||||||
|
seen.add(path)
|
||||||
|
yield path
|
||||||
|
|
||||||
|
local = Path.home() / ".local/share/applications"
|
||||||
|
if local.is_dir() and local not in seen:
|
||||||
|
yield local
|
||||||
|
|
||||||
|
|
||||||
|
def parse_desktop_file(path):
|
||||||
|
icon = None
|
||||||
|
wm_class = None
|
||||||
|
try:
|
||||||
|
with path.open(errors="ignore") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not icon and line.startswith("Icon="):
|
||||||
|
icon = line.removeprefix("Icon=").strip()
|
||||||
|
elif not wm_class and line.startswith("StartupWMClass="):
|
||||||
|
wm_class = line.removeprefix("StartupWMClass=").strip()
|
||||||
|
if icon and wm_class:
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
return None, None
|
||||||
|
return icon, wm_class
|
||||||
|
|
||||||
|
|
||||||
|
def icon_map():
|
||||||
|
mapping = {}
|
||||||
|
for directory in app_dirs():
|
||||||
|
for desktop_file in directory.glob("*.desktop"):
|
||||||
|
icon, wm_class = parse_desktop_file(desktop_file)
|
||||||
|
if not icon:
|
||||||
|
continue
|
||||||
|
mapping.setdefault(desktop_file.stem.lower(), icon)
|
||||||
|
if wm_class:
|
||||||
|
mapping.setdefault(wm_class.lower(), icon)
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def icon_for(mapping, class_name):
|
||||||
|
class_key = clean_text(class_name).lower()
|
||||||
|
return mapping.get(class_key, class_key or "application-x-executable")
|
||||||
|
|
||||||
|
|
||||||
|
def rofi_index(entries, prompt):
|
||||||
|
menu = b"".join(entry.encode("utf-8", errors="replace") for entry in entries)
|
||||||
|
result = subprocess.run(
|
||||||
|
["rofi", "-dmenu", "-i", "-show-icons", "-p", prompt, "-format", "i"],
|
||||||
|
input=menu,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(result.stdout.decode("utf-8").strip())
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def candidates(mode, clients, active_workspace, active_window):
|
||||||
|
current_ws = (active_workspace or {}).get("id")
|
||||||
|
focused = window_address(active_window or {}) if active_window else None
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
for window in clients:
|
||||||
|
if not normal_workspace(window):
|
||||||
|
continue
|
||||||
|
|
||||||
|
address = window_address(window)
|
||||||
|
if not address:
|
||||||
|
continue
|
||||||
|
|
||||||
|
workspace_id = (window.get("workspace") or {}).get("id")
|
||||||
|
if mode == "bring" and workspace_id == current_ws:
|
||||||
|
continue
|
||||||
|
if mode == "replace" and address == focused:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered.append(window)
|
||||||
|
|
||||||
|
filtered.sort(
|
||||||
|
key=lambda window: (
|
||||||
|
(window.get("workspace") or {}).get("id", 9999),
|
||||||
|
window.get("focusHistoryID", 999999),
|
||||||
|
clean_text(window.get("class")).lower(),
|
||||||
|
clean_text(window.get("title")).lower(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def menu_entry(window, icons):
|
||||||
|
class_name = clean_text(window.get("class"))
|
||||||
|
title = clean_text(window.get("title"))
|
||||||
|
workspace_id = (window.get("workspace") or {}).get("id", "?")
|
||||||
|
label = f"{class_name[:24]:<24} {title} WS:{workspace_id}"
|
||||||
|
icon = icon_for(icons, class_name)
|
||||||
|
return f"{label}\0icon\x1f{icon}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def focus_window(address):
|
||||||
|
if dispatch_lua(f'hl.dsp.focus({{ window = "address:{address}" }})'):
|
||||||
|
return
|
||||||
|
dispatch_legacy("focuswindow", f"address:{address}")
|
||||||
|
|
||||||
|
|
||||||
|
def move_to_workspace(address, workspace_id):
|
||||||
|
workspace = str(workspace_id)
|
||||||
|
if dispatch_lua(f'hl.dsp.window.move({{ workspace = "{workspace}", window = "address:{address}" }})'):
|
||||||
|
return
|
||||||
|
dispatch_legacy("movetoworkspace", f"{workspace},address:{address}")
|
||||||
|
|
||||||
|
|
||||||
|
def swap_with_focused(target_address, focused_address):
|
||||||
|
if dispatch_lua(
|
||||||
|
f'hl.dsp.window.swap({{ target = "address:{target_address}", window = "address:{focused_address}" }})'
|
||||||
|
):
|
||||||
|
return
|
||||||
|
dispatch_legacy("hy3:movewindow", f"address:{target_address}")
|
||||||
|
|
||||||
|
|
||||||
|
def activate(mode, window, active_workspace, active_window):
|
||||||
|
address = window_address(window)
|
||||||
|
if not address:
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == "go":
|
||||||
|
focus_window(address)
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == "bring":
|
||||||
|
current_ws = (active_workspace or {}).get("id")
|
||||||
|
if current_ws is not None:
|
||||||
|
move_to_workspace(address, current_ws)
|
||||||
|
focus_window(address)
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == "replace":
|
||||||
|
focused = window_address(active_window or {})
|
||||||
|
if focused and focused != address:
|
||||||
|
swap_with_focused(address, focused)
|
||||||
|
focus_window(address)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Rofi window go/bring/replace for Hyprland.")
|
||||||
|
parser.add_argument("mode", choices=sorted(PROMPTS))
|
||||||
|
parser.add_argument("--print-candidates", action="store_true")
|
||||||
|
parser.add_argument("--select-index", type=int)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ensure_hyprland_instance()
|
||||||
|
|
||||||
|
clients = run_json("hyprctl", "clients", "-j") or []
|
||||||
|
active_workspace = run_json("hyprctl", "activeworkspace", "-j") or {}
|
||||||
|
active_window = run_json("hyprctl", "activewindow", "-j") or {}
|
||||||
|
windows = candidates(args.mode, clients, active_workspace, active_window)
|
||||||
|
|
||||||
|
if args.print_candidates:
|
||||||
|
print(json.dumps(windows, indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not windows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.select_index is None:
|
||||||
|
icons = icon_map()
|
||||||
|
entries = [menu_entry(window, icons) for window in windows]
|
||||||
|
index = rofi_index(entries, PROMPTS[args.mode])
|
||||||
|
else:
|
||||||
|
index = args.select_index
|
||||||
|
|
||||||
|
if index is None or index < 0 or index >= len(windows):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
activate(args.mode, windows[index], active_workspace, active_window)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user