153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
|
|||
|
|
import subprocess
|
|||
|
|
import json
|
|||
|
|
import sys
|
|||
|
|
import shutil
|
|||
|
|
import time # <--- 新增:用于等待窗口关闭生效
|
|||
|
|
|
|||
|
|
# ================= Configuration =================
|
|||
|
|
EXCLUDE_APPS = ["fuzzel", "quick-switch", "niri-quick-switch"]
|
|||
|
|
FUZZEL_ARGS = [
|
|||
|
|
"--dmenu",
|
|||
|
|
"--index",
|
|||
|
|
"--width", "60",
|
|||
|
|
"--lines", "15",
|
|||
|
|
"--prompt", "Switch: ",
|
|||
|
|
# 修改了这里:提示 Ctrl+L 跳转,Ctrl+H 关闭
|
|||
|
|
"--placeholder", "Search... [Ctrl+J/K: Select | Ctrl+L: Switch | Ctrl+H: Close]"
|
|||
|
|
]
|
|||
|
|
# =================================================
|
|||
|
|
|
|||
|
|
def run_cmd(cmd):
|
|||
|
|
try:
|
|||
|
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|||
|
|
if result.returncode != 0: return None
|
|||
|
|
if "-j" in cmd:
|
|||
|
|
return json.loads(result.stdout)
|
|||
|
|
return result.stdout
|
|||
|
|
except Exception: return None
|
|||
|
|
|
|||
|
|
def get_active_output_workspace_ids():
|
|||
|
|
"""
|
|||
|
|
获取当前活动显示器(output)上的所有工作区 ID
|
|||
|
|
"""
|
|||
|
|
workspaces = run_cmd("niri msg -j workspaces")
|
|||
|
|
if not workspaces: return set()
|
|||
|
|
|
|||
|
|
# 1. 找到当前聚焦的工作区所在的显示器名称
|
|||
|
|
active_output = None
|
|||
|
|
for ws in workspaces:
|
|||
|
|
if ws.get("is_focused"):
|
|||
|
|
active_output = ws.get("output")
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not active_output: return set()
|
|||
|
|
|
|||
|
|
# 2. 收集该显示器上的所有工作区 ID
|
|||
|
|
valid_ws_ids = set()
|
|||
|
|
for ws in workspaces:
|
|||
|
|
if ws.get("output") == active_output:
|
|||
|
|
valid_ws_ids.add(ws.get("id"))
|
|||
|
|
|
|||
|
|
return valid_ws_ids
|
|||
|
|
|
|||
|
|
def get_window_sort_key(w):
|
|||
|
|
# 将 workspace_id 作为第一排序优先级,确保不同工作区的窗口按组排列
|
|||
|
|
ws_id = w.get("workspace_id", 0)
|
|||
|
|
|
|||
|
|
if w.get("is_floating"):
|
|||
|
|
return (ws_id, 99999, 0, w.get("id"))
|
|||
|
|
try:
|
|||
|
|
layout = w.get("layout", {})
|
|||
|
|
if not layout: return (ws_id, 9999, 0, w.get("id"))
|
|||
|
|
pos = layout.get("pos_in_scrolling_layout")
|
|||
|
|
if pos and isinstance(pos, list) and len(pos) >= 2:
|
|||
|
|
return (ws_id, pos[0], pos[1], w.get("id"))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return (ws_id, 9999, 0, w.get("id"))
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
if not shutil.which("fuzzel"):
|
|||
|
|
print("Error: Fuzzel not found")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# === 核心改动:开启死循环 ===
|
|||
|
|
while True:
|
|||
|
|
# 1. 每次循环重新获取当前显示器上的所有 Workspace ID
|
|||
|
|
valid_ws_ids = get_active_output_workspace_ids()
|
|||
|
|
if not valid_ws_ids: break
|
|||
|
|
|
|||
|
|
# 2. 每次循环都重新获取最新的窗口列表
|
|||
|
|
windows = run_cmd("niri msg -j windows")
|
|||
|
|
if not windows: break
|
|||
|
|
|
|||
|
|
current_windows = []
|
|||
|
|
for w in windows:
|
|||
|
|
# 判断窗口是否在允许的工作区集合内
|
|||
|
|
if w.get("workspace_id") not in valid_ws_ids: continue
|
|||
|
|
app_id = w.get("app_id") or ""
|
|||
|
|
if app_id in EXCLUDE_APPS: continue
|
|||
|
|
current_windows.append(w)
|
|||
|
|
|
|||
|
|
# 如果没有窗口了,直接退出
|
|||
|
|
if not current_windows: break
|
|||
|
|
|
|||
|
|
current_windows.sort(key=get_window_sort_key)
|
|||
|
|
|
|||
|
|
input_str = ""
|
|||
|
|
for w in current_windows:
|
|||
|
|
app_id = w.get("app_id") or "Wayland"
|
|||
|
|
title = w.get("title", "No Title").replace("\n", " ")
|
|||
|
|
display_str = f"[{app_id}] {title}"
|
|||
|
|
line = f"{display_str}\0icon\x1f{app_id}"
|
|||
|
|
input_str += f"{line}\n"
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 3. 启动 Fuzzel
|
|||
|
|
proc = subprocess.Popen(
|
|||
|
|
["fuzzel"] + FUZZEL_ARGS,
|
|||
|
|
stdin=subprocess.PIPE,
|
|||
|
|
stdout=subprocess.PIPE,
|
|||
|
|
text=True
|
|||
|
|
)
|
|||
|
|
stdout, _ = proc.communicate(input=input_str)
|
|||
|
|
|
|||
|
|
return_code = proc.returncode
|
|||
|
|
raw_output = stdout.strip()
|
|||
|
|
|
|||
|
|
# 情况 A: 用户按 ESC 取消 -> 退出循环
|
|||
|
|
if return_code not in [0, 10] or not raw_output:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
selected_idx = int(raw_output)
|
|||
|
|
except ValueError:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if 0 <= selected_idx < len(current_windows):
|
|||
|
|
target_window = current_windows[selected_idx]
|
|||
|
|
target_id = target_window.get("id")
|
|||
|
|
|
|||
|
|
if return_code == 0:
|
|||
|
|
# 动作: 切换窗口 -> 任务完成,退出循环
|
|||
|
|
subprocess.run(["niri", "msg", "action", "focus-window", "--id", str(target_id)])
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
elif return_code == 10:
|
|||
|
|
# 动作: 关闭窗口 -> 执行关闭,然后 CONTINUE (继续循环)
|
|||
|
|
subprocess.run(["niri", "msg", "action", "close-window", "--id", str(target_id)])
|
|||
|
|
|
|||
|
|
# 关键:稍微等一下,让 niri 有时间处理关闭动作,
|
|||
|
|
# 否则立刻刷新列表可能还会看到那个已经被杀死的窗口
|
|||
|
|
time.sleep(0.1)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"Error: {e}")
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|