Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create labelled spaces and move windows on startup #541

Closed
Just-Insane opened this issue May 28, 2020 · 7 comments
Closed

Create labelled spaces and move windows on startup #541

Just-Insane opened this issue May 28, 2020 · 7 comments
Labels
help wanted Community help appreciated question Request for information or help, not an issue

Comments

@Just-Insane
Copy link

Is there any way to create labelled spaces and move windows to those spaces on startup?

I have a multiple monitor setup and would like to be able to log in and have my apps all open in the correct spaces on the correct monitors.

Currently, I have this in my config file:

yabai -m space --create
yabai -m space 1 --label main
yabai -m space --create
yabai -m space 2 --label code
yabai -m space --create
yabai -m space 3 --label term
yabai -m space --create
yabai -m space 4 --label chat

yabai -m space main --display 1
yabai -m space chat --display 2
yabai -m space code --display 3
yabai -m space term --display 3

yabai -m rule --add app=iTerm space=term
yabai -m rule --add app=Code space=code
yabai -m rule --add app=Safari space=main
yabai -m rule --add app=Firefox space=main
yabai -m rule --add app="News Explorer" space=news
yabai -m rule --add app=Discord space=chat
yabai -m rule --add app=Franz space=chat
yabai -m rule --add app=Ferdi space=chat

I then have another script that runs on space change to close unfocused spaces without windows, which doesn't really do what I want:

#! /usr/bin/env zsh

yabai -m query --spaces --display | jq -re 'map(select(."native-fullscreen" == 0)) | length > 1' && zsh <(yabai -m query --spaces | jq -re 'map(select(."windows" == [] and ."focused" == 0 and ."label" == "" )) | .[]| "yabai -m space \(.index|@sh) --destroy"')

The goal is to create specific spaces on specific monitors and move applications to those spaces.

@johnallen3d
Copy link

Sorry, I don't have an answer to your question (though I'm curious, how do you first ensure there aren't any spaces?) but just installed Ferdi based on your config. Thanks for that!

@Just-Insane
Copy link
Author

@johnallen3d That's half the problem... I'm not really sure how to ensure there are no spaces (or label the spaces that are there), which is a bit painful because a bunch of spaces get made and then labelled and moved, and then any extra are removed by the script.

Ferdi is pretty awesome, it's the free version of Franz.

@koekeishiya
Copy link
Owner

The way macOS work you will always have at least one space per monitor.
You can query the space information from yabai in your config and act accordingly, e.g: yabai -m query --spaces will give you information about all existing spaces.

Maybe the solution in #365 could be of some help?

@koekeishiya koekeishiya added help wanted Community help appreciated question Request for information or help, not an issue labels May 29, 2020
@Just-Insane
Copy link
Author

Just-Insane commented Jun 2, 2020

I was able to (seemingly) get this working by doing the following:

python3 ./.config/yabai/yabaictl.py update-spaces

yabai -m space 1 --label main
yabai -m space 2 --label code
yabai -m space 3 --label term
yabai -m space 4 --label chat
yabai -m space 5 --label spotify 

yabai -m space main --display 1
yabai -m space chat --display 2
yabai -m space code --display 3
yabai -m space term --display 3
yabai -m space term --move next
yabai -m space spotify --display 3
yabai -m space spotify --move next
yabai -m space spotify --move next

It's not ideal but seems to work decently. Moving the spaces using next is clunky though. Probably a better way to go about that. This is needed is due to the python script randomly distributing the spaces, and then moving the spaces to the monitor. I didn't see a better way of ordering spaces after a quick search.

@nkezhaya
Copy link

@Just-Insane could you share the contents of yabaictl.py?

@Just-Insane
Copy link
Author

@whitepaperclip sure, it's here:

#!/usr/local/bin/python3
import json
import subprocess

import click

# these are used to determine display order
setups = {
    "home": [
        "9D6F6199-E480-45B6-B5B5-F6C1D3C40171",
        "F4300722-B985-E97A-DAC9-BC8415D223C6",
        "D8BFD1B0-C53A-5A22-141F-5F889FB85E01",
    ],
    "laptop": [
        "731FF600-A54A-B317-CF0B-C767EC3D5EB2",
    ],
}

ignore_messages = [
    "acting space is already located on the given display.",
    "cannot focus an already focused space.",
]
# TODO: need to handle following errors:
"acting space is the last user-space on the source display and cannot be destroyed."
"acting space is the last user-space on the source display and cannot be moved."


def yabai_message(*msg):
    ret = subprocess.run(["yabai", "-m", *msg], capture_output=True)

    if ret.returncode:
        err_msg = ret.stderr.decode()

        if err_msg.strip() not in ignore_messages:
            raise Exception(err_msg)
        else:
            print(f"While running {msg} we received error: {err_msg}")

    return ret.stdout.decode()


def yabai_query(domain):
    return json.loads(yabai_message("query", "--{}".format(domain)))


class WindowManager:
    spaces = []
    displays = []
    display_order = []
    NUM_SPACES = 10

    def __init__(self):
        self.refresh_state()

    @property
    def num_displays(self):
        return len(self.displays)

    @property
    def num_spaces(self):
        return len(self.spaces)

    @property
    def unlabled_spaces(self):
        return [space for space in self.spaces if space["label"] == ""]

    @property
    def visible_spaces(self):
        return [space for space in self.spaces if space["visible"] > 0]

    def refresh_state(self):
        self.spaces = yabai_query("spaces")
        self.displays = yabai_query("displays")

        for setup in setups.values():
            if set([display["uuid"] for display in self.displays]) == set(setup):
                self.display_order = setup
        if self.display_order == []:
            print("unidentified setup")

    def find_display_index(self, display):
        uuid = self.display_order[display]

        return next(
            display["index"] for display in self.displays if display["uuid"] == uuid
        )

    def find_space_index(self, space):
        return next(
            space["index"] for space in self.spaces if space["label"] == f"s{space}"
        )

    def get_display_for_space(self, space):
        return space % self.num_displays - 1

    def focus_space(self, space):
        yabai_message("space", "--focus", f"s{space}")

    def move_space_to_display(self, space, display):
        display_index = self.find_display_index(display)

        yabai_message(
            "space", f"s{space}", "--display", f"{display_index}",
        )

    def remove_unnecessary_spaces(self):
        if self.num_spaces > self.NUM_SPACES:
            for unlabled_space in self.unlabled_spaces:
                yabai_message("space", f"{unlabled_space['index']}", "--destroy")

    def ensure_spaces(self):
        if self.num_spaces < self.NUM_SPACES:
            for i in range(self.num_spaces, self.NUM_SPACES):
                yabai_message("space", "--create")

    def ensure_labels(self):
        wanted_labels = set(f"s{i}" for i in range(1, self.NUM_SPACES + 1))
        existing_labels = set(space["label"] for space in self.spaces)

        for ix, missing_label in enumerate(sorted(wanted_labels - existing_labels)):
            yabai_message(
                "space",
                f"{self.unlabled_spaces[ix]['index']}",
                "--label",
                missing_label,
            )

    def reorganize_spaces(self):
        focused_spaces = self.visible_spaces

        for space_index in range(1, self.NUM_SPACES + 1):
            self.move_space_to_display(
                space_index, self.get_display_for_space(space_index),
            )

        for space in focused_spaces[: min(self.num_displays, len(focused_spaces))]:
            self.focus_space(space["label"].strip("s"))

    def update_spaces(self):
        self.ensure_spaces()
        self.refresh_state()

        self.ensure_labels()
        self.refresh_state()

        self.reorganize_spaces()

        self.remove_unnecessary_spaces()
        self.refresh_state()


@click.group()
@click.pass_context
def cli(ctx):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below
    ctx.ensure_object(dict)

    ctx.obj["wm"] = WindowManager()


@cli.command()
@click.pass_context
def update_spaces(ctx):
    ctx.obj["wm"].update_spaces()


@cli.command()
@click.argument("space")
@click.pass_context
def focus_space(ctx, space):
    ctx.obj["wm"].focus_space(space)


if __name__ == "__main__":
    cli(obj={})% 

It doesn't really work for my use cases due to how it checks for unused spaces and removes them, and how it sends spaces to specific monitors (balanced).

@nkezhaya
Copy link

nkezhaya commented Jun 11, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Community help appreciated question Request for information or help, not an issue
Projects
None yet
Development

No branches or pull requests

4 participants