|
@@ -1,4 +1,5 @@
|
|
|
#!/usr/bin/env python3
|
|
#!/usr/bin/env python3
|
|
|
|
|
+from os.path import exists
|
|
|
from json import loads
|
|
from json import loads
|
|
|
from logging import NOTSET, basicConfig, debug, info
|
|
from logging import NOTSET, basicConfig, debug, info
|
|
|
from os import getenv
|
|
from os import getenv
|
|
@@ -58,7 +59,7 @@ class Space:
|
|
|
return self.index.__gt__(other.index)
|
|
return self.index.__gt__(other.index)
|
|
|
|
|
|
|
|
|
|
|
|
|
-class Display:
|
|
|
|
|
|
|
+class YabaiDisplay:
|
|
|
def __init__(self, data: dict[str, Any]):
|
|
def __init__(self, data: dict[str, Any]):
|
|
|
self.id: int = data["id"]
|
|
self.id: int = data["id"]
|
|
|
self.uuid: str = data["uuid"]
|
|
self.uuid: str = data["uuid"]
|
|
@@ -70,20 +71,90 @@ class Display:
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
def __repr__(self) -> str:
|
|
|
return (
|
|
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", {self.index}"
|
|
|
f", Frame ({self.frame})"
|
|
f", Frame ({self.frame})"
|
|
|
f"{', Focused' if self.has_focus else ''})"
|
|
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"])
|
|
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
|
|
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]]] = [
|
|
spaces: list[tuple[str, bool, list[str]]] = [
|
|
|
("Desktop", False, ["*"]),
|
|
("Desktop", False, ["*"]),
|
|
|
("Finder", False, ["Finder"]),
|
|
("Finder", False, ["Finder"]),
|
|
@@ -107,13 +178,6 @@ class Yabai:
|
|
|
if exc_type is None:
|
|
if exc_type is None:
|
|
|
debug(f"Executed successfully")
|
|
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]:
|
|
def get_windows(self) -> set[Window]:
|
|
|
return {
|
|
return {
|
|
|
Window(window) for window in loads(self.message(["query", "--windows"]))
|
|
Window(window) for window in loads(self.message(["query", "--windows"]))
|
|
@@ -122,12 +186,15 @@ class Yabai:
|
|
|
def get_spaces(self) -> set[Space]:
|
|
def get_spaces(self) -> set[Space]:
|
|
|
return {Space(space) for space in loads(self.message(["query", "--spaces"]))}
|
|
return {Space(space) for space in loads(self.message(["query", "--spaces"]))}
|
|
|
|
|
|
|
|
- def get_displays(self) -> set[Display]:
|
|
|
|
|
|
|
+ def get_displays(self) -> set[YabaiDisplay]:
|
|
|
return {
|
|
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]
|
|
return sorted(list(displays if displays != None else self.get_displays()))[-1]
|
|
|
|
|
|
|
|
def is_blank_space(self, space: Space) -> bool:
|
|
def is_blank_space(self, space: Space) -> bool:
|
|
@@ -144,8 +211,22 @@ class Yabai:
|
|
|
self.message(["display", display.index, "--label", "Main"])
|
|
self.message(["display", display.index, "--label", "Main"])
|
|
|
else:
|
|
else:
|
|
|
self.message(
|
|
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):
|
|
def manage_spaces(self):
|
|
|
initial_window = self.get_focused_window()
|
|
initial_window = self.get_focused_window()
|
|
@@ -195,7 +276,7 @@ class Yabai:
|
|
|
(main_display.frame["h"] - TARGET_DISPLAY_HEIGHT) / 2
|
|
(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])
|
|
self.message(["window", "--focus", initial_window])
|
|
|
# Sort the remaining spaces
|
|
# Sort the remaining spaces
|
|
|
for space_index, space_label in enumerate(self.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
|
|
# Almost certainly thrown because space is already in place, so no problem
|
|
|
pass
|
|
pass
|
|
|
# Return focus
|
|
# 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())}")
|
|
info(f"Spaces configured: {sorted(self.get_spaces())}")
|
|
|
|
|
|
|
|
def set_space_background(self, space: SpaceSel):
|
|
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__":
|
|
if __name__ == "__main__":
|