|
@@ -58,6 +58,7 @@ class Space:
|
|
|
f"Space({self.label if self.label and len(self.label) > 0 else '<NoLabel>'}"
|
|
f"Space({self.label if self.label and len(self.label) > 0 else '<NoLabel>'}"
|
|
|
f", {self.index}"
|
|
f", {self.index}"
|
|
|
f"{', Fullscreen' if self.is_native_fullscreen else ''}"
|
|
f"{', Fullscreen' if self.is_native_fullscreen else ''}"
|
|
|
|
|
+ f", {self.display}"
|
|
|
f"{', Focused' if self.has_focus else ''})"
|
|
f"{', Focused' if self.has_focus else ''})"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -105,15 +106,18 @@ class CLIWrapper:
|
|
|
class Yabai(CLIWrapper):
|
|
class Yabai(CLIWrapper):
|
|
|
_base_args: list[str] = ["yabai", "-m"]
|
|
_base_args: list[str] = ["yabai", "-m"]
|
|
|
|
|
|
|
|
|
|
+ # Label, Full-Page, Apps, Main Display
|
|
|
spaces: list[tuple[str, bool, list[str], bool]] = [
|
|
spaces: list[tuple[str, bool, list[str], bool]] = [
|
|
|
("Desktop", False, ["*"], True),
|
|
("Desktop", False, ["*"], True),
|
|
|
- ("Finder", False, ["Finder"], False),
|
|
|
|
|
- ("Terminal", True, ["Alacritty"], False),
|
|
|
|
|
- ("Browser", True, ["Firefox"], False),
|
|
|
|
|
|
|
+ ("Finder", False, ["Finder"], True),
|
|
|
|
|
+ ("Terminal", True, ["Alacritty"], True),
|
|
|
|
|
+ ("Browser", True, ["Firefox"], True),
|
|
|
]
|
|
]
|
|
|
_initial_window: Window | None = None
|
|
_initial_window: Window | None = None
|
|
|
_dual_display: None | Literal[True] | Literal[False] = None
|
|
_dual_display: None | Literal[True] | Literal[False] = None
|
|
|
_exit_with_rule_apply: bool = False
|
|
_exit_with_rule_apply: bool = False
|
|
|
|
|
+ _exit_with_refocus: bool = False
|
|
|
|
|
+ _display_labels: tuple[str, str] = ("Main", "Secondary")
|
|
|
|
|
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
|
self._dual_display = len(self.get_displays()) > 1
|
|
self._dual_display = len(self.get_displays()) > 1
|
|
@@ -123,7 +127,7 @@ class Yabai(CLIWrapper):
|
|
|
"Communication",
|
|
"Communication",
|
|
|
False,
|
|
False,
|
|
|
["Slack", "Signal", "Spotify"],
|
|
["Slack", "Signal", "Spotify"],
|
|
|
- True,
|
|
|
|
|
|
|
+ False,
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
for application in [
|
|
for application in [
|
|
@@ -139,7 +143,7 @@ class Yabai(CLIWrapper):
|
|
|
application,
|
|
application,
|
|
|
True,
|
|
True,
|
|
|
[application],
|
|
[application],
|
|
|
- True,
|
|
|
|
|
|
|
+ False,
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
else:
|
|
else:
|
|
@@ -162,6 +166,7 @@ class Yabai(CLIWrapper):
|
|
|
debug(f"Exited with {exc_type} {exc_value}")
|
|
debug(f"Exited with {exc_type} {exc_value}")
|
|
|
if self._exit_with_rule_apply:
|
|
if self._exit_with_rule_apply:
|
|
|
self.message(["rule", "--apply"])
|
|
self.message(["rule", "--apply"])
|
|
|
|
|
+ if self._exit_with_refocus or self._exit_with_rule_apply:
|
|
|
if self._initial_window is not None:
|
|
if self._initial_window is not None:
|
|
|
self.message(["window", "--focus", self._initial_window.id])
|
|
self.message(["window", "--focus", self._initial_window.id])
|
|
|
if exc_type is None:
|
|
if exc_type is None:
|
|
@@ -195,11 +200,17 @@ class Yabai(CLIWrapper):
|
|
|
def manage_displays(self):
|
|
def manage_displays(self):
|
|
|
displays = self.get_displays()
|
|
displays = self.get_displays()
|
|
|
main_display = self.get_main_display(displays)
|
|
main_display = self.get_main_display(displays)
|
|
|
|
|
+ secondary_display = None
|
|
|
for display in displays:
|
|
for display in displays:
|
|
|
if display.index == main_display.index:
|
|
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"])
|
|
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ ["display", display.index, "--label", self._display_labels[0]]
|
|
|
|
|
+ )
|
|
|
|
|
+ elif secondary_display == None:
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ ["display", display.index, "--label", self._display_labels[1]]
|
|
|
|
|
+ )
|
|
|
|
|
+ secondary_display = display
|
|
|
else:
|
|
else:
|
|
|
self.message(
|
|
self.message(
|
|
|
[
|
|
[
|
|
@@ -423,16 +434,7 @@ class Yabai(CLIWrapper):
|
|
|
|
|
|
|
|
# Rules that differ for one or multiple displays
|
|
# Rules that differ for one or multiple displays
|
|
|
if self._dual_display:
|
|
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",
|
|
|
|
|
- ]
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ pass
|
|
|
else:
|
|
else:
|
|
|
# Google Meet and Slack Huddles should be "sticky"
|
|
# Google Meet and Slack Huddles should be "sticky"
|
|
|
for app, title in (("Google Meet", ".*"), ("Slack", "Huddle.*")):
|
|
for app, title in (("Google Meet", ".*"), ("Slack", "Huddle.*")):
|
|
@@ -461,21 +463,23 @@ class Yabai(CLIWrapper):
|
|
|
]
|
|
]
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- def move_spaces(self):
|
|
|
|
|
- initial_window = self.get_focused_window()
|
|
|
|
|
- final_window = self.get_focused_window()
|
|
|
|
|
-
|
|
|
|
|
|
|
+ def move_spaces_to_displays(self):
|
|
|
|
|
+ if not self._dual_display:
|
|
|
|
|
+ return
|
|
|
|
|
+ current_spaces = self.get_spaces()
|
|
|
displays = self.get_displays()
|
|
displays = self.get_displays()
|
|
|
- main_display = self.get_main_display(displays)
|
|
|
|
|
|
|
+ main_display = next(
|
|
|
|
|
+ (d for d in displays if d.label == self._display_labels[0]), None
|
|
|
|
|
+ )
|
|
|
secondary_display = next(
|
|
secondary_display = next(
|
|
|
- (d for d in displays if not d.id == main_display.id), None
|
|
|
|
|
|
|
+ (d for d in displays if d.label == self._display_labels[1]), None
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- current_spaces = self.get_spaces()
|
|
|
|
|
|
|
+ if main_display is None or secondary_display is None:
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
incorrect_main_spaces: set[Space] = set()
|
|
incorrect_main_spaces: set[Space] = set()
|
|
|
incorrect_secondary_spaces: set[Space] = set()
|
|
incorrect_secondary_spaces: set[Space] = set()
|
|
|
- for space_label, _, _, is_secondary_space in self.spaces:
|
|
|
|
|
|
|
+ for space_label, _, _, is_main_display_space in self.spaces:
|
|
|
mapped_space = next(
|
|
mapped_space = next(
|
|
|
(s for s in current_spaces if s.label == space_label), None
|
|
(s for s in current_spaces if s.label == space_label), None
|
|
|
)
|
|
)
|
|
@@ -484,21 +488,27 @@ class Yabai(CLIWrapper):
|
|
|
|
|
|
|
|
if (
|
|
if (
|
|
|
mapped_space.display == main_display.index
|
|
mapped_space.display == main_display.index
|
|
|
- and is_secondary_space
|
|
|
|
|
|
|
+ and not is_main_display_space
|
|
|
and secondary_display
|
|
and secondary_display
|
|
|
):
|
|
):
|
|
|
incorrect_secondary_spaces.add(mapped_space)
|
|
incorrect_secondary_spaces.add(mapped_space)
|
|
|
- if mapped_space.display != main_display.index and not is_secondary_space:
|
|
|
|
|
|
|
+ elif mapped_space.display != main_display.index and is_main_display_space:
|
|
|
incorrect_main_spaces.add(mapped_space)
|
|
incorrect_main_spaces.add(mapped_space)
|
|
|
|
|
+ last_focus = next((s.label for s in current_spaces if s.has_focus), None)
|
|
|
while len(incorrect_main_spaces) > 0 and len(incorrect_secondary_spaces) > 0:
|
|
while len(incorrect_main_spaces) > 0 and len(incorrect_secondary_spaces) > 0:
|
|
|
|
|
+ from_space = incorrect_main_spaces.pop()
|
|
|
|
|
+ to_space = incorrect_secondary_spaces.pop()
|
|
|
|
|
+ if to_space.label == last_focus:
|
|
|
|
|
+ from_space, to_space = to_space, from_space
|
|
|
self.message(
|
|
self.message(
|
|
|
[
|
|
[
|
|
|
"space",
|
|
"space",
|
|
|
- incorrect_main_spaces.pop().label,
|
|
|
|
|
|
|
+ from_space.label,
|
|
|
"--switch",
|
|
"--switch",
|
|
|
- incorrect_secondary_spaces.pop().label,
|
|
|
|
|
|
|
+ to_space.label,
|
|
|
]
|
|
]
|
|
|
)
|
|
)
|
|
|
|
|
+ last_focus = to_space.label
|
|
|
for spaces, secondary in (
|
|
for spaces, secondary in (
|
|
|
(incorrect_main_spaces, False),
|
|
(incorrect_main_spaces, False),
|
|
|
(incorrect_secondary_spaces, True),
|
|
(incorrect_secondary_spaces, True),
|
|
@@ -515,12 +525,24 @@ class Yabai(CLIWrapper):
|
|
|
]
|
|
]
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- 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 sort_spaces(self):
|
|
|
|
|
+ all_spaces = [s.label for s in sorted(self.get_spaces())]
|
|
|
|
|
+ for is_main in (True, False) if self._dual_display else [None]:
|
|
|
|
|
+ order = [
|
|
|
|
|
+ s[0]
|
|
|
|
|
+ for s in self.spaces
|
|
|
|
|
+ if s[0] in all_spaces and (s[3] == is_main) or is_main is None
|
|
|
|
|
+ ]
|
|
|
|
|
+ spaces = [s for s in all_spaces if s in order]
|
|
|
|
|
+
|
|
|
|
|
+ while tuple(spaces) != tuple(order) and len(spaces) == len(order):
|
|
|
|
|
+ for index in range(1, len(spaces)):
|
|
|
|
|
+ if order.index(spaces[index]) < order.index(spaces[index - 1]):
|
|
|
|
|
+ self.message(["space", spaces[index], "--move", "prev"])
|
|
|
|
|
+ spaces[index], spaces[index - 1] = (
|
|
|
|
|
+ spaces[index - 1],
|
|
|
|
|
+ spaces[index],
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
def get_focused_window(self) -> Window | None:
|
|
def get_focused_window(self) -> Window | None:
|
|
|
windows = [
|
|
windows = [
|
|
@@ -534,8 +556,54 @@ class Yabai(CLIWrapper):
|
|
|
return windows.pop()
|
|
return windows.pop()
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
+ def invert_displays(self) -> None:
|
|
|
|
|
+ if not self._dual_display:
|
|
|
|
|
+ return
|
|
|
|
|
+ displays = self.get_displays()
|
|
|
|
|
+ for display in displays:
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ [
|
|
|
|
|
+ "display",
|
|
|
|
|
+ display.index,
|
|
|
|
|
+ "--label",
|
|
|
|
|
+ f"TEMP{display.label}",
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ for display in displays:
|
|
|
|
|
+ if display.label == self._display_labels[0]:
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ [
|
|
|
|
|
+ "display",
|
|
|
|
|
+ display.index,
|
|
|
|
|
+ "--label",
|
|
|
|
|
+ self._display_labels[1],
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ elif display.label == self._display_labels[1]:
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ [
|
|
|
|
|
+ "display",
|
|
|
|
|
+ display.index,
|
|
|
|
|
+ "--label",
|
|
|
|
|
+ self._display_labels[0],
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.message(
|
|
|
|
|
+ [
|
|
|
|
|
+ "display",
|
|
|
|
|
+ display.index,
|
|
|
|
|
+ "--label",
|
|
|
|
|
+ display.label,
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
def enable_exit_with_rule_apply(self):
|
|
def enable_exit_with_rule_apply(self):
|
|
|
self._exit_with_rule_apply = True
|
|
self._exit_with_rule_apply = True
|
|
|
|
|
+ self.enable_exit_with_refocus()
|
|
|
|
|
+
|
|
|
|
|
+ def enable_exit_with_refocus(self):
|
|
|
|
|
+ self._exit_with_refocus = True
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
@@ -548,5 +616,16 @@ if __name__ == "__main__":
|
|
|
yabai.manage_spaces()
|
|
yabai.manage_spaces()
|
|
|
if argv[1] == "initialize":
|
|
if argv[1] == "initialize":
|
|
|
yabai.set_rules_and_signals()
|
|
yabai.set_rules_and_signals()
|
|
|
- if argv[1] == "move" or argv[1] == "manage" or argv[1] == "initialize":
|
|
|
|
|
- yabai.move_spaces()
|
|
|
|
|
|
|
+ if argv[1] == "invert":
|
|
|
|
|
+ yabai.invert_displays()
|
|
|
|
|
+ yabai.enable_exit_with_rule_apply()
|
|
|
|
|
+ if (
|
|
|
|
|
+ argv[1] == "move"
|
|
|
|
|
+ or argv[1] == "manage"
|
|
|
|
|
+ or argv[1] == "initialize"
|
|
|
|
|
+ or argv[1] == "invert"
|
|
|
|
|
+ ):
|
|
|
|
|
+ yabai.move_spaces_to_displays()
|
|
|
|
|
+ # Disabled until moving spaces no longer triggers mission control
|
|
|
|
|
+ # yabai.sort_spaces()
|
|
|
|
|
+ yabai.enable_exit_with_refocus()
|