swayfx-dots/config/sway/.swaymonad/swaymonad.py
2025-05-06 13:24:30 +02:00

156 lines
4.9 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
from collections.abc import Callable, Iterator
import functools
import itertools
import logging
import shlex
import sys
import traceback
import time
try:
from typing import Concatenate, ParamSpec
except ImportError:
from typing_extensions import Concatenate, ParamSpec
import i3ipc
import common
import cycle_windows
import layout
import master_operations
import n_col
import nop_layout
import transformations
argparser = argparse.ArgumentParser(description='An xmonad-like auto-tiler for sway.')
argparser.add_argument('--default-layout', default="tall",
help="Layout to use for workspaces where the layout has not been manually set."
"Valid options are 'tall', '3_col', and 'nop'.")
argparser.add_argument('--verbose', "-v", action="count", help="Enable debug logging.")
argparser.add_argument('--log-file', help="Log file path, defaults to stderr.")
argparser.add_argument('--delay', default=0.0, type=float,
help=("Sleep for n seconds before sending every command to sway, "
"allowing a human to observe intermediate state,"))
args = argparser.parse_args()
P = ParamSpec("P")
Command = Callable[Concatenate[i3ipc.Connection, i3ipc.Event, P], None]
COMMANDS: dict[str, Command] = {
"promote_window": master_operations.promote_window,
"focus_master": master_operations.focus_master,
"resize_master": master_operations.resize_master,
"reflectx": layout.reflectx_dispatcher,
"reflecty": layout.reflecty_dispatcher,
"transpose": layout.transpose_dispatcher,
"focus_next_window": cycle_windows.focus_next_window,
"focus_prev_window": cycle_windows.focus_prev_window,
"swap_with_next_window": cycle_windows.swap_with_next_window,
"swap_with_prev_window": cycle_windows.swap_with_prev_window,
"set_layout": layout.set_layout,
"increment_masters": layout.increment_masters_dispatcher,
"decrement_masters": layout.decrement_masters_dispatcher,
"move": layout.move_dispatcher,
"fullscreen": layout.fullscreen_dispatcher,
}
def parse_binding(event: i3ipc.Event) -> Iterator[list[str]]:
logging.debug(f"Parsing command: {event.binding.command}")
split_commands = shlex.split(event.binding.command)
delims = ';,'
for _, group in itertools.groupby(split_commands, key=lambda s: s in delims):
group = list(group)
if group[0] == 'nop':
yield group[1:]
def command_dispatcher(i3: i3ipc.Connection, event: i3ipc.Event):
logging.debug(f"Receved command event: {event.ipc_data}")
commands = list(parse_binding(event))
logging.debug(f"Parsed commands: {commands}")
if not commands:
return
try:
i3.enable_command_buffering()
for command in commands:
COMMANDS.get(command[0], lambda i3, event, *args: None)(i3, event, *command[1:])
i3.disable_command_buffering()
except Exception as ex:
traceback.print_exc()
layout.LAYOUTS.update({
"tall": functools.partial(n_col.NCol, n_columns=2),
"3_col": functools.partial(n_col.NCol, n_columns=3),
"nop": nop_layout.Nop,
})
class Connection(i3ipc.Connection):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.buffering_commands = False
self.command_buffer: list[str] = []
def command(self, payload: str) -> list[i3ipc.CommandReply]:
if self.buffering_commands:
logging.debug(f"Buffering command: {payload}", stacklevel=2)
self.command_buffer.append(payload)
return []
logging.debug(f"Executing command: {payload}", stacklevel=2)
time.sleep(args.delay)
return super().command(payload)
def enable_command_buffering(self) -> None:
self.buffering_commands = True
def disable_command_buffering(self) -> list[i3ipc.CommandReply]:
self.buffering_commands = False
if not self.command_buffer:
return []
command = ";".join(self.command_buffer)
self.command_buffer = []
return self.command(command)
def get_tree(self) -> i3ipc.Con:
# TODO: handle returned errors
self.disable_command_buffering()
tree = super().get_tree()
self.enable_command_buffering()
return tree
def get_workspaces(self) -> list[i3ipc.replies.WorkspaceReply]:
# TODO: handle returned errors
self.disable_command_buffering()
workspaces = super().get_workspaces()
self.enable_command_buffering()
return workspaces
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING,
filename=args.log_file,
format='%(asctime)s, %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')
layout.DEFAULT_LAYOUT = args.default_layout
i3 = Connection()
i3.on(i3ipc.Event.BINDING, command_dispatcher)
i3.on(i3ipc.Event.WINDOW_NEW, layout.layout_dispatcher)
i3.on(i3ipc.Event.WINDOW_CLOSE, layout.layout_dispatcher)
i3.on(i3ipc.Event.WINDOW_MOVE, layout.layout_dispatcher)
i3.main()