|
|
@@ -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()
|