|
|
@@ -1,6 +1,6 @@
|
|
|
#!/usr/bin/env python3
|
|
|
from json import loads
|
|
|
-from logging import NOTSET, basicConfig, critical, debug, error, info, warning
|
|
|
+from logging import NOTSET, basicConfig, debug, info
|
|
|
from os import getenv
|
|
|
from subprocess import CalledProcessError, check_output
|
|
|
from sys import argv
|
|
|
@@ -11,6 +11,30 @@ XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME")
|
|
|
TARGET_DISPLAY_WIDTH = 1680
|
|
|
TARGET_DISPLAY_HEIGHT = 1050
|
|
|
|
|
|
+class Window:
|
|
|
+ def __init__(self, data: dict):
|
|
|
+ self.id: int = data["id"]
|
|
|
+ self.app: str = data["app"]
|
|
|
+ self.space: str = data["space"]
|
|
|
+ self.title: str = data["title"]
|
|
|
+ self.has_focus: bool = data["has-focus"]
|
|
|
+ self.frame_w: int = data["frame"]["w"]
|
|
|
+ self.frame_h: int = data["frame"]["h"]
|
|
|
+
|
|
|
+ @property
|
|
|
+ def size(self) -> int:
|
|
|
+ return self.frame_w * self.frame_h
|
|
|
+
|
|
|
+ def __repr__(self) -> str:
|
|
|
+ return (
|
|
|
+ f"Window({self.title} | {self.app}"
|
|
|
+ f", {self.id}"
|
|
|
+ f"{', Focused' if self.has_focus else ''})"
|
|
|
+ )
|
|
|
+
|
|
|
+ def __gt__(self, other: "Window"):
|
|
|
+ return self.id.__gt__(other.id)
|
|
|
+
|
|
|
|
|
|
class Space:
|
|
|
def __init__(self, data: dict):
|
|
|
@@ -88,13 +112,14 @@ class Yabai:
|
|
|
def message(self, args: list[str | int]) -> str:
|
|
|
return self.execute(["yabai", "-m"] + [str(arg) for arg in args]).decode("utf8")
|
|
|
|
|
|
+ def get_windows(self) -> set[Window]:
|
|
|
+ return { Window(window) for window in loads(self.message(["query", "--windows"])) }
|
|
|
+
|
|
|
def get_spaces(self) -> set[Space]:
|
|
|
return {Space(space) for space in loads(self.message(["query", "--spaces"]))}
|
|
|
|
|
|
def get_displays(self) -> set[Display]:
|
|
|
- return {
|
|
|
- Display(display) for display in loads(self.message(["query", "--displays"]))
|
|
|
- }
|
|
|
+ return { Display(display) for display in loads(self.message(["query", "--displays"])) }
|
|
|
|
|
|
def get_main_display(self, displays: set[Display] | None = None) -> Display:
|
|
|
return sorted(list(displays if displays != None else self.get_displays()))[-1]
|
|
|
@@ -238,9 +263,9 @@ class Yabai:
|
|
|
[
|
|
|
"rule",
|
|
|
"--add",
|
|
|
- f"label=DefaultDesktopRule",
|
|
|
- f"subrole=AXStandardWindow",
|
|
|
- f"space=^Desktop",
|
|
|
+ "label=DefaultDesktopRule",
|
|
|
+ "subrole=AXStandardWindow",
|
|
|
+ "space=^Desktop",
|
|
|
]
|
|
|
)
|
|
|
# Rules for applications that get their own spaces
|
|
|
@@ -257,32 +282,21 @@ class Yabai:
|
|
|
f"space=^{label}",
|
|
|
]
|
|
|
)
|
|
|
- # Google Meet and Slack Huddles should be "sticky"
|
|
|
- self.message(
|
|
|
- [
|
|
|
- "rule",
|
|
|
- "--add",
|
|
|
- "label=GoogleMeetFloatingWindowRule",
|
|
|
- "app=Google Meet",
|
|
|
- "sticky=on",
|
|
|
- "manage=on",
|
|
|
- "opacity=0.9",
|
|
|
- "grid=10:10:6:6:4:4",
|
|
|
- ]
|
|
|
- )
|
|
|
- self.message(
|
|
|
- [
|
|
|
- "rule",
|
|
|
- "--add",
|
|
|
- "label=SlackHuddleFloatingWindowRule",
|
|
|
- "app=Slack",
|
|
|
- "title=Huddle.*",
|
|
|
- "sticky=on",
|
|
|
- "manage=on",
|
|
|
- "opacity=0.9",
|
|
|
- "grid=10:10:6:6:4:4",
|
|
|
- ]
|
|
|
- )
|
|
|
+ # Safari and Slack Huddles should be "sticky"
|
|
|
+ for app, title in (("Safari", ".*"), ("Slack", "Huddle.*")):
|
|
|
+ self.message(
|
|
|
+ [
|
|
|
+ "rule",
|
|
|
+ "--add",
|
|
|
+ f"label={app}VideoCallFloatingWindowRule",
|
|
|
+ f"app={app}",
|
|
|
+ f"title={title}",
|
|
|
+ "sticky=on",
|
|
|
+ "manage=on",
|
|
|
+ "opacity=0.9",
|
|
|
+ "grid=10:10:6:6:4:4",
|
|
|
+ ]
|
|
|
+ )
|
|
|
# Compile SurfingKeys configuration when Firefox is launched
|
|
|
self.message(
|
|
|
[
|
|
|
@@ -305,6 +319,31 @@ class Yabai:
|
|
|
f"action=/bin/zsh {HOME}/.scripts/lightmode.zsh",
|
|
|
]
|
|
|
)
|
|
|
+ # When focusing an that may share a space with at least two others, rotate the
|
|
|
+ # space's windows to ensure that the focused window is the largest.
|
|
|
+ for label, _, apps in self.spaces:
|
|
|
+ if len(apps) < 3:
|
|
|
+ continue
|
|
|
+ for app in apps:
|
|
|
+ self.message(
|
|
|
+ [
|
|
|
+ "signal",
|
|
|
+ "--add",
|
|
|
+ "event=window_focused",
|
|
|
+ f"app={app}",
|
|
|
+ f"label={app}RotateToFocus",
|
|
|
+ f"action=python3 {XDG_CONFIG_HOME}/yabai/yabai.py rotate",
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+ def rotate(self) -> None:
|
|
|
+ windows = self.get_windows()
|
|
|
+ focus_window = next(w for w in windows if w.has_focus)
|
|
|
+ same_space_windows = sorted((w for w in windows if w.space == focus_window.space), key = lambda w: w.size)
|
|
|
+ if same_space_windows[-1].has_focus:
|
|
|
+ return
|
|
|
+ self.message(["window", focus_window.id, "--swap", same_space_windows[-1].id])
|
|
|
+ return
|
|
|
|
|
|
def get_focused_window(self) -> int:
|
|
|
return [
|
|
|
@@ -317,11 +356,11 @@ class Yabai:
|
|
|
if __name__ == "__main__":
|
|
|
basicConfig(level=NOTSET)
|
|
|
debug(f"Called with parameters {argv}")
|
|
|
- with Yabai() as yabai:
|
|
|
- if argv[1] == "manage" or argv[1] == "initialize":
|
|
|
+ if argv[1] == "manage" or argv[1] == "initialize":
|
|
|
+ with Yabai() as yabai:
|
|
|
yabai.manage_displays()
|
|
|
yabai.manage_spaces()
|
|
|
if argv[1] == "initialize":
|
|
|
yabai.set_rules_and_signals()
|
|
|
- else:
|
|
|
- raise Exception(argv)
|
|
|
+ if argv[1] == "rotate":
|
|
|
+ Yabai().rotate()
|