Browse Source

feat(yabai): use BetterDisplay (if available) to control display brightness)

Joe 1 year ago
parent
commit
3798787394
1 changed files with 106 additions and 23 deletions
  1. 106 23
      .config/yabai/yabai.py

+ 106 - 23
.config/yabai/yabai.py

@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+from os.path import exists
 from json import loads
 from logging import NOTSET, basicConfig, debug, info
 from os import getenv
@@ -58,7 +59,7 @@ class Space:
         return self.index.__gt__(other.index)
 
 
-class Display:
+class YabaiDisplay:
     def __init__(self, data: dict[str, Any]):
         self.id: int = data["id"]
         self.uuid: str = data["uuid"]
@@ -70,20 +71,90 @@ class Display:
 
     def __repr__(self) -> str:
         return (
-            f"Space({self.label if self.label and len(self.label) > 0 else '<NoLabel>'}"
+            f"Display({self.label if self.label and len(self.label) > 0 else '<NoLabel>'}"
             f", {self.index}"
             f", Frame ({self.frame})"
             f"{', Focused' if self.has_focus else ''})"
         )
 
-    def __gt__(self, other: "Display"):
+    def __gt__(self, other: "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
 
 
-class Yabai:
+class CLIWrapper:
+    _base_args: list[str] = []
+
+    def message(self, args: list[str | int]) -> str:
+        return self.execute(self._base_args + [str(arg) for arg in args]).decode("utf8")
+
+    def execute(self, *args: Any, **kwargs: Any) -> Any:
+        debug(f"Executing: ({args}), ({kwargs})")
+        return check_output(*args, **kwargs)
+
+
+class BetterDisplay(CLIWrapper):
+    _available: bool = False
+    _base_args: list[str] = [
+        "/Applications/BetterDisplay.app/Contents/MacOS/BetterDisplay"
+    ]
+
+    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"]) + "]"
+            )
+        ]
+
+
+class Yabai(CLIWrapper):
+    _base_args: list[str] = ["yabai", "-m"]
     spaces: list[tuple[str, bool, list[str]]] = [
         ("Desktop", False, ["*"]),
         ("Finder", False, ["Finder"]),
@@ -107,13 +178,6 @@ class Yabai:
         if exc_type is None:
             debug(f"Executed successfully")
 
-    def execute(self, *args: Any, **kwargs: Any) -> Any:
-        debug(f"Executing: ({args}), ({kwargs})")
-        return check_output(*args, **kwargs)
-
-    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"]))
@@ -122,12 +186,15 @@ class Yabai:
     def get_spaces(self) -> set[Space]:
         return {Space(space) for space in loads(self.message(["query", "--spaces"]))}
 
-    def get_displays(self) -> set[Display]:
+    def get_displays(self) -> set[YabaiDisplay]:
         return {
-            Display(display) for display in loads(self.message(["query", "--displays"]))
+            YabaiDisplay(display)
+            for display in loads(self.message(["query", "--displays"]))
         }
 
-    def get_main_display(self, displays: set[Display] | None = None) -> Display:
+    def get_main_display(
+        self, displays: set[YabaiDisplay] | None = None
+    ) -> YabaiDisplay:
         return sorted(list(displays if displays != None else self.get_displays()))[-1]
 
     def is_blank_space(self, space: Space) -> bool:
@@ -144,8 +211,22 @@ class Yabai:
                 self.message(["display", display.index, "--label", "Main"])
             else:
                 self.message(
-                    ["display", display.index, "--label", f"Display {display.index}"]
+                    [
+                        "display",
+                        display.index,
+                        "--label",
+                        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)
 
     def manage_spaces(self):
         initial_window = self.get_focused_window()
@@ -195,7 +276,7 @@ class Yabai:
                     (main_display.frame["h"] - TARGET_DISPLAY_HEIGHT) / 2
                 ),
             )
-        if len(created_space_labels) > 0:
+        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):
@@ -205,7 +286,8 @@ class Yabai:
                 # Almost certainly thrown because space is already in place, so no problem
                 pass
         # Return focus
-        self.message(["window", "--focus", initial_window])
+        if initial_window is not None:
+            self.message(["window", "--focus", initial_window])
         info(f"Spaces configured: {sorted(self.get_spaces())}")
 
     def set_space_background(self, space: SpaceSel):
@@ -330,12 +412,13 @@ class Yabai:
             ]
         )
 
-    def get_focused_window(self) -> int:
-        return [
-            window
-            for window in loads(self.message(["query", "--windows"]))
-            if window["has-focus"]
-        ].pop()["id"]
+    def get_focused_window(self) -> Window | None:
+        windows = [
+            window for window in [Window(window) for window in loads(self.message(["query", "--windows"]))] if window.has_focus
+        ]
+        if len(windows) > 0:
+            return windows.pop()
+        return None
 
 
 if __name__ == "__main__":