소스 검색

feat(yabai): add support for smaller second monitor

Joe 1 년 전
부모
커밋
d4b4b7a923
2개의 변경된 파일188개의 추가작업 그리고 122개의 파일을 삭제
  1. 175 119
      .config/yabai/yabai.py
  2. 13 3
      .scripts/open_application.zsh

+ 175 - 119
.config/yabai/yabai.py

@@ -46,7 +46,9 @@ class Space:
         self.id: int = data["id"]
         self.index: int = data["index"]
         self.label: str = data["label"]
+        self.display: int = data["display"]
         self.windows: list[int] = data["label"]
+        self.is_visible: str = data["is-visible"]
         self.has_focus: bool = data["has-focus"]
         self.is_native_fullscreen: bool = data["is-native-fullscreen"]
 
@@ -86,34 +88,6 @@ class YabaiDisplay:
         return self.frame["w"].__gt__(other.frame["w"])
 
 
-class BetterDisplayDisplay:
-    def __init__(self, data: dict[str, Any]):
-        self.alphanumeric_serial: str = data["alphanumericSerial"]
-        self.device_type: str = data["deviceType"]
-        self.display_id: str = data["displayID"]
-        self.model: str = data["model"]
-        self.name: str = data["name"]
-        self.original_name: str = data["originalName"]
-        self.product_name: str = data["productName"]
-        self.registry_location: str = data["registryLocation"]
-        self.serial: str = data["serial"]
-        self.tagID: str = data["tagID"]
-        self.uuid: str = data["UUID"]
-        self.vendor: str = data["vendor"]
-        self.manufacture_week: str = data["weekOfManufacture"]
-        self.manufacture_year: str = data["yearOfManufacture"]
-
-    def __repr__(self) -> str:
-        return f"BetterDisplayDisplay({self.name})"
-
-    def __gt__(self, other: "BetterDisplayDisplay"):
-        return self.display_id.__gt__(other.display_id)
-
-    @property
-    def built_in(self) -> bool:
-        return self.name == "Built-in Display"
-
-
 SpaceSel: TypeAlias = int | str
 
 
@@ -125,50 +99,59 @@ class CLIWrapper:
 
     def execute(self, *args: Any, **kwargs: Any) -> Any:
         debug(f"Executing: ({args}), ({kwargs})")
-        return check_output(*args, **kwargs)
+        return cast(str, check_output(*args, **kwargs))
+
 
+class Yabai(CLIWrapper):
+    _base_args: list[str] = ["yabai", "-m"]
 
-class BetterDisplay(CLIWrapper):
-    _available: bool = False
-    _base_args: list[str] = [
-        "/Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay"
+    spaces: list[tuple[str, bool, list[str], bool]] = [
+        ("Desktop", False, ["*"], True),
+        ("Finder", False, ["Finder"], False),
+        ("Terminal", True, ["Alacritty"], False),
+        ("Browser", True, ["Firefox"], False),
     ]
+    _initial_window: Window | None = None
+    _dual_display: None | Literal[True] | Literal[False] = None
+    _exit_with_rule_apply: bool = False
 
     def __init__(self):
-        if exists(self._base_args[0]) and self.execute(
-            self._base_args + ["help"], timeout=0.5
-        ):
-            self._available = True
-
-    @property
-    def available(self) -> bool:
-        return self._available
-
-    def set_brightness(self, display: BetterDisplayDisplay, value: float):
-        self.message(
-            ["set", f"-uuid={display.uuid}", "-feature=brightness", f"-value={value}"]
-        )
-
-    def get_displays(self) -> list[BetterDisplayDisplay]:
-        return [
-            BetterDisplayDisplay(display)
-            for display in loads(
-                "[" + self.message(["get", "-feature=identifiers"]) + "]"
+        self._dual_display = len(self.get_displays()) > 1
+        if self._dual_display:
+            self.spaces.append(
+                (
+                    "Communication",
+                    False,
+                    ["Slack", "Signal", "Spotify"],
+                    True,
+                )
             )
-        ]
-
-
-class Yabai(CLIWrapper):
-    _base_args: list[str] = ["yabai", "-m"]
-    spaces: list[tuple[str, bool, list[str]]] = [
-        ("Desktop", False, ["*"]),
-        ("Finder", False, ["Finder"]),
-        ("Terminal", True, ["Alacritty"]),
-        ("Browser", True, ["Firefox"]),
-        ("Communication", False, ["Slack", "Signal", "Spotify", "Notion"]),
-        ("Notetaking", True, ["Obsidian", "Asana"]),
-    ]
-    _initial_window: int | None = None
+            for application in [
+                "Google Meet",
+                "Obsidian",
+                "Asana",
+                "Notion",
+                "Netflix",
+                "YouTube",
+            ]:
+                self.spaces.append(
+                    (
+                        application,
+                        True,
+                        [application],
+                        True,
+                    )
+                )
+        else:
+            self.spaces.append(
+                (
+                    "Communication",
+                    False,
+                    ["Slack", "Signal", "Spotify", "Notion"],
+                    False,
+                )
+            )
+            self.spaces.append(("Notetaking", True, ["Obsidian", "Asana"], False))
 
     def __enter__(self):
         self._initial_window = self.get_focused_window()
@@ -177,9 +160,10 @@ class Yabai(CLIWrapper):
     def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
         if exc_type is not None:
             debug(f"Exited with {exc_type} {exc_value}")
-        self.message(["rule", "--apply"])
-        if self._initial_window is not None:
-            self.message(["window", "--focus", self._initial_window.id])
+        if self._exit_with_rule_apply:
+            self.message(["rule", "--apply"])
+            if self._initial_window is not None:
+                self.message(["window", "--focus", self._initial_window.id])
         if exc_type is None:
             debug(f"Executed successfully")
 
@@ -214,6 +198,8 @@ class Yabai(CLIWrapper):
         for display in displays:
             if display.index == main_display.index:
                 self.message(["display", display.index, "--label", "Main"])
+            elif display.index == main_display.index + 1:
+                self.message(["display", display.index, "--label", f"Secondary"])
             else:
                 self.message(
                     [
@@ -223,19 +209,6 @@ class Yabai(CLIWrapper):
                         f"YabaiDisplay {display.index}",
                     ]
                 )
-        better_display = BetterDisplay()
-        if better_display.available:
-            displays = better_display.get_displays()
-            if len(displays) > 1:
-                for display in displays:
-                    if display.built_in:
-                        better_display.set_brightness(display, 0)
-                    else:
-                        better_display.set_brightness(display, 1)
-            else:
-                display = displays.pop()
-                if display:
-                    better_display.set_brightness(display, 1)
 
     def manage_spaces(self):
         initial_window = self.get_focused_window()
@@ -255,7 +228,7 @@ class Yabai(CLIWrapper):
         spaces = self.get_spaces()
         blank_spaces = {space for space in spaces if self.is_blank_space(space)}
         created_space_labels: set[tuple[str, bool]] = set()
-        for space_label, space_fullscreen, _ in self.spaces:
+        for space_label, space_fullscreen, _, _ in self.spaces:
             if any(space.label == space_label for space in spaces):
                 continue
             space = blank_spaces.pop()
@@ -274,7 +247,7 @@ class Yabai(CLIWrapper):
         main_display = self.get_main_display()
         for label, fullscreen in created_space_labels:
             self.set_space_background(label)
-        for label, fullscreen, _ in self.spaces:
+        for label, fullscreen, _, _ in self.spaces:
             self.set_config(
                 label,
                 fullscreen=fullscreen,
@@ -286,14 +259,7 @@ class Yabai(CLIWrapper):
                 ),
             )
         if len(created_space_labels) > 0 and initial_window is not None:
-            self.message(["window", "--focus", initial_window])
-        # Sort the remaining spaces
-        for space_index, space_label in enumerate(self.spaces):
-            try:
-                self.message(["space", space_label[0], "--move", space_index + 1])
-            except CalledProcessError:
-                # Almost certainly thrown because space is already in place, so no problem
-                pass
+            self.message(["window", "--focus", initial_window.id])
         # Return focus
         if initial_window is not None:
             self.message(["window", "--focus", initial_window.id])
@@ -370,7 +336,7 @@ class Yabai(CLIWrapper):
             ]
         )
         # Rules for applications that get their own spaces
-        for label, _, apps in self.spaces:
+        for label, _, apps, _ in self.spaces:
             for app in apps:
                 if app == "*":
                     continue
@@ -383,32 +349,7 @@ class Yabai(CLIWrapper):
                         f"space=^{label}",
                     ]
                 )
-        # Google Meet and Slack Huddles should be "sticky"
-        for app, title in (("Google Meet", ".*"), ("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",
-                ]
-            )
-        # Tiny streaming player
-        self.message(
-            [
-                "rule",
-                "--add",
-                f"label=NetflixFloatingWindowRule",
-                f"app=Netflix",
-                "sticky=on",
-                "manage=on",
-            ]
-        )
+
         # Compile SurfingKeys configuration when Firefox is launched
         self.message(
             [
@@ -443,6 +384,115 @@ class Yabai(CLIWrapper):
             ]
         )
 
+        for trigger in [
+            "display_added",
+            "display_removed",
+            "display_resized",
+            "system_woke",
+        ]:
+            self.message(
+                [
+                    "signal",
+                    "--add",
+                    f"event={trigger}",
+                    f"label=DisplayChange{trigger}",
+                    f"action=/usr/bin/env python3 {HOME}/.config/yabai.py manage",
+                ]
+            )
+
+        # Rules that differ for one or multiple displays
+        if self._dual_display:
+            # space
+            self.message(
+                [
+                    "signal",
+                    "--add",
+                    "event=space_changed",
+                    "label=SpaceChanged",
+                    f"action=/usr/bin/env python3 {HOME}/.config/yabai.py move",
+                ]
+            )
+        else:
+            # Google Meet and Slack Huddles should be "sticky"
+            for app, title in (("Google Meet", ".*"), ("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",
+                    ]
+                )
+            # Tiny streaming player
+            self.message(
+                [
+                    "rule",
+                    "--add",
+                    f"label=NetflixFloatingWindowRule",
+                    f"app=Netflix",
+                    "sticky=on",
+                    "manage=on",
+                ]
+            )
+
+    def move_spaces(self):
+        initial_window = self.get_focused_window()
+        final_window = self.get_focused_window()
+
+        displays = self.get_displays()
+        main_display = self.get_main_display(displays)
+        secondary_display = next(d for d in displays if not d.id == main_display.id)
+
+        current_spaces = self.get_spaces()
+
+        incorrect_main_spaces: set[Space] = set()
+        incorrect_secondary_spaces: set[Space] = set()
+        for space_label, _, _, is_secondary_space in self.spaces:
+            mapped_space = next(
+                (s for s in current_spaces if s.label == space_label), None
+            )
+            if mapped_space is None:
+                continue
+
+            if mapped_space.display == main_display.index and is_secondary_space:
+                incorrect_secondary_spaces.add(mapped_space)
+            if mapped_space.display != main_display.index and not is_secondary_space:
+                incorrect_main_spaces.add(mapped_space)
+        while len(incorrect_main_spaces) > 0 and len(incorrect_secondary_spaces) > 0:
+            self.message(
+                [
+                    "space",
+                    incorrect_main_spaces.pop().label,
+                    "--switch",
+                    incorrect_secondary_spaces.pop().label,
+                ]
+            )
+        for spaces, secondary in (
+            (incorrect_main_spaces, False),
+            (incorrect_secondary_spaces, True),
+        ):
+            for space in spaces:
+                self.message(
+                    [
+                        "space",
+                        space.label,
+                        "--display",
+                        secondary_display.label if secondary else main_display.label,
+                    ]
+                )
+
+        if (
+            final_window is not None
+            and initial_window is not None
+            and final_window.id != initial_window.id
+        ):
+            self.message(["window", "--focus", initial_window.id])
+
     def get_focused_window(self) -> Window | None:
         windows = [
             window
@@ -455,13 +505,19 @@ class Yabai(CLIWrapper):
             return windows.pop()
         return None
 
+    def enable_exit_with_rule_apply(self):
+        self._exit_with_rule_apply = True
+
 
 if __name__ == "__main__":
     basicConfig(level=NOTSET)
     debug(f"Called with parameters {argv}")
     with Yabai() as yabai:
         if argv[1] == "manage" or argv[1] == "initialize":
+            yabai.enable_exit_with_rule_apply()
             yabai.manage_displays()
             yabai.manage_spaces()
         if argv[1] == "initialize":
             yabai.set_rules_and_signals()
+        if argv[1] == "move" or argv[1] == "manage" or argv[1] == "initialize":
+            yabai.move_spaces()

+ 13 - 3
.scripts/open_application.zsh

@@ -38,16 +38,26 @@ case $1 in
 esac
 
 if [ "$valid_selection" = true ] ; then
-    # If there are 3+ windows in the space, then make selected the largest window
     windows=$(yabai -m query --windows)
-    active_space=$(jq 'map(select(."has-focus"==true)) | map(.space) | first' <<< $windows)
+    spaces=$(yabai -m query --spaces)
+    displays=$(yabai -m query --displays)
+    active_display=$(jq 'map(select(."has-focus"==true)) | map(.display) | first' <<< $spaces)
+    active_space=$(jq 'map(select(."has-focus"==true)) | map(.index) | first' <<< $spaces)
+    other_space=$(jq 'map(select(."has-focus"==false and ."is-visible"==true)) | map(.index) | first' <<< $spaces)
+    # If the selected window is on the secondary monitor, swap it to the main one
+    if [ "$active_display" != "1" ]; then
+        yabai -m space $active_space --switch $other_space
+        yabai -m space $active_space --focus
+    fi
+    # If there are 3+ windows in the space, then make selected the largest window
     shared_windows=$(jq --argjson space "$active_space" 'map(select(.space==$space)) | map( { ("size"): (.frame.w * .frame.h), "id": .id, "focus": ."has-focus", "title": .title } ) | sort_by(.size)' <<< $windows)
+    focused_window=$(jq 'map(select(.focus)) | first | .id' <<< $shared_windows)
     if [ $(jq 'length' <<< $shared_windows ) -gt 2 ]; then
-        focused_window=$(jq 'map(select(.focus)) | first | .id' <<< $shared_windows)
         largest_window=$(jq 'last | .id' <<< $shared_windows)
         if [ "$focused_window" != "$largest_window" ]; then
             yabai -m window $focused_window --swap $largest_window
         fi
     fi
+    yabai -m window $focused_window --focus
 fi