Skip to content

Add bindings for next/prev workspace in vswitch#3041

Open
rpaciorek wants to merge 1 commit into
WayfireWM:masterfrom
rpaciorek:vswitch_next_prev
Open

Add bindings for next/prev workspace in vswitch#3041
rpaciorek wants to merge 1 commit into
WayfireWM:masterfrom
rpaciorek:vswitch_next_prev

Conversation

@rpaciorek

Copy link
Copy Markdown

Similar to left/right but based on workspace number (instead of location), so jump to next row instead stay on last / wrap to first column in current row.

Similar to left/right but jump to next row instead wrap to first column.
@ammen99

ammen99 commented Jun 12, 2026

Copy link
Copy Markdown
Member

I wonder whether we really want to add even more bindings to vswitch. This functionality for example could easily be implemented via an IPC script. AI-generated, human-reviewed example:

#!/usr/bin/python3

import argparse
import signal
import sys
from typing import Any

from wayfire import WayfireSocket


DEFAULT_BINDINGS = {
    "next": "<super> <alt> KEY_PAGEUP",
    "prev": "<super> <alt> KEY_PAGEDOWN",
    "with_win_next": "<super> <alt> <shift> KEY_PAGEUP",
    "with_win_prev": "<super> <alt> <shift> KEY_PAGEDOWN",
    "send_win_next": "",
    "send_win_prev": "",
}


def option_value(response: Any, default: Any = None) -> Any:
    if isinstance(response, dict):
        return response.get("value", default)

    return default


def is_enabled(value: Any) -> bool:
    if isinstance(value, bool):
        return value

    if isinstance(value, str):
        return value.lower() in {"1", "true", "yes", "on"}

    return bool(value)


def focused_view_id(sock: WayfireSocket) -> int | None:
    view = sock.get_focused_view()
    if not isinstance(view, dict):
        return None

    view_id = view.get("id")
    if view_id is None:
        return None

    return int(view_id)


def target_workspace(sock: WayfireSocket, direction: int) -> tuple[int, int] | None:
    output = sock.get_focused_output()
    workspace = output["workspace"]

    grid_width = int(workspace["grid_width"])
    grid_height = int(workspace["grid_height"])
    current_x = int(workspace["x"])
    current_y = int(workspace["y"])

    current_index = current_y * grid_width + current_x
    target_index = current_index + direction
    workspace_count = grid_width * grid_height

    wraparound = is_enabled(option_value(sock.get_option_value("vswitch/wraparound"), False))
    if wraparound:
        target_index %= workspace_count
    elif target_index < 0 or target_index >= workspace_count:
        return None

    return target_index % grid_width, target_index // grid_width


def switch_linear(sock: WayfireSocket, direction: int, with_window: bool, send_only: bool) -> None:
    target = target_workspace(sock, direction)
    if target is None:
        return

    target_x, target_y = target

    if with_window or send_only:
        view_id = focused_view_id(sock)
        if view_id is None:
            return

        if send_only:
            sock.send_view_to_workspace(view_id, target_x, target_y)
            return

    output = sock.get_focused_output()
    sock.set_workspace(
        target_x,
        target_y,
        view_id=view_id if with_window else None,
        output_id=output["id"],
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Register IPC bindings for numeric next/previous vswitch workspaces."
    )
    parser.add_argument("--next", default=DEFAULT_BINDINGS["next"], help="binding for next workspace")
    parser.add_argument("--prev", default=DEFAULT_BINDINGS["prev"], help="binding for previous workspace")
    parser.add_argument(
        "--with-win-next",
        default=DEFAULT_BINDINGS["with_win_next"],
        help="binding for next workspace with focused window",
    )
    parser.add_argument(
        "--with-win-prev",
        default=DEFAULT_BINDINGS["with_win_prev"],
        help="binding for previous workspace with focused window",
    )
    parser.add_argument(
        "--send-win-next",
        default=DEFAULT_BINDINGS["send_win_next"],
        help="binding for sending focused window to next workspace without switching",
    )
    parser.add_argument(
        "--send-win-prev",
        default=DEFAULT_BINDINGS["send_win_prev"],
        help="binding for sending focused window to previous workspace without switching",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    sock = WayfireSocket()

    actions = {
        "next": (args.next, 1, False, False),
        "prev": (args.prev, -1, False, False),
        "with_win_next": (args.with_win_next, 1, True, False),
        "with_win_prev": (args.with_win_prev, -1, True, False),
        "send_win_next": (args.send_win_next, 1, True, True),
        "send_win_prev": (args.send_win_prev, -1, True, True),
    }

    registered = {}
    for name, (binding, direction, with_window, send_only) in actions.items():
        if not binding:
            continue

        response = sock.register_binding(binding, mode="press", exec_always=True)
        binding_id = response["binding-id"]
        registered[binding_id] = (name, direction, with_window, send_only)
        print(f"registered {name}: {binding} ({binding_id})")

    def unregister_and_exit(signum, frame):
        for binding_id in registered:
            sock.unregister_binding(binding_id)
        raise SystemExit(0)

    signal.signal(signal.SIGINT, unregister_and_exit)
    signal.signal(signal.SIGTERM, unregister_and_exit)

    while True:
        msg = sock.read_next_event()
        if msg.get("event") != "command-binding":
            continue

        action = registered.get(msg.get("binding-id"))
        if action is None:
            continue

        name, direction, with_window, send_only = action
        try:
            switch_linear(sock, direction, with_window, send_only)
        except Exception as error:
            print(f"{name}: {error}", file=sys.stderr)


if __name__ == "__main__":
    raise SystemExit(main())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants