From 95fc1dd5b960bb6e29b24fa3ea102c5b9a438129 Mon Sep 17 00:00:00 2001 From: myortv Date: Sat, 17 Jan 2026 08:38:19 +0300 Subject: rename to summoner --- watcher.py | 581 ------------------------------------------------------------- 1 file changed, 581 deletions(-) delete mode 100644 watcher.py (limited to 'watcher.py') diff --git a/watcher.py b/watcher.py deleted file mode 100644 index 6a8e889..0000000 --- a/watcher.py +++ /dev/null @@ -1,581 +0,0 @@ -import logging -import subprocess -import time -import i3ipc -import argparse - - -from typing import Dict, Optional, List, Any -from functools import reduce -from itertools import accumulate -from enum import Enum -from time import sleep - - -from dataclasses import dataclass, field - - -@dataclass -class Window: - run: list | None # command to run (list of arguments) or None - geometry: Dict[str, int] # dict like {"x": 0, "y": 0, "w": 200, "h": 200} - window_name: str # name of the window spawned from run command - workspace: Optional[str | int] = "w_hidden" # home workspace - skip_spawn: Optional[bool] = False # do not spawn this window. Skips spawn step - skip_init: Optional[bool] = False # do not init this window (initial state snapshot, position, set mode, etc). Skips init step - steal_focus: Optional[bool] = False # should this window steal focus. If multiple set in one layout, last one will steal focus - restore_to_initial_state: Optional[bool] = False # if set, window will catch it current state and try to restore to this state instead of hiding - - _initial_state_snapshot: Optional[Any] = None # internal variable for initial window state to return to - - def __hash__(self): - return hash(self.window_name) - - def __eq__(self, o): - if not isinstance(o, Window): - raise ValueError("compare only with window") - return self.window_name == o.window_name - - -@dataclass -class Layout: - windows: List[Window] - close_layout_on: Optional[List[str]] = field(default_factory=list()) - # layout_command: str - - -# Ideally, first layout, which used to showup on empty workspaces -# should not contain any focus stealing windows -# because default layout is shown on workspace with floating windows as well -# and stealing focus in this case can be annoying -# on the other hand, sub-layouts should have focus stealing on apps, that -# actually interactable, because it is usually expected behaviour - -LAYOUTS = { - 'show_partial_overlay': Layout( - windows=[ - Window( - run=["alacritty", "-T", "cmatrix2", "-e", "cmatrix"], - geometry={"x": 350, "y": 150, "w": 250, "h": 500}, - window_name="cmatrix2", - ), - Window( - run=["alacritty", "-T", "cmatrix3", "-e", "cmatrix"], - geometry={"x": 900, "y": 300, "w": 250, "h": 500}, - window_name="cmatrix3", - ), - ], - close_layout_on=[ - 'exec --no-startup-id echo "show_partial_overlay"', - ] - ), - 'exec --no-startup-id echo "show_overlay_1"': Layout( - windows=[ - Window( - run=["alacritty", "-T", "cmatrix", "-e", "cmatrix"], - geometry={"x": 50, "y": 50, "w": 250, "h": 500}, - window_name="cmatrix", - ), - # Window( - # run=["alacritty", "-T", "bigtime", "-e", "bigtime"], - # geometry={"x": 350, "y": 50, "w": 1800, "h": 250}, - # window_name="bigtime", - # steal_focus=True, - # ), - Window( - run=None, - geometry={"x": 350, "y": 50, "w": 1800, "h": 250}, - window_name="special", - skip_spawn=True, - restore_to_initial_state=True, - workspace="21" - ), - ], - close_layout_on=[ - 'exec --no-startup-id echo "show_partial_overlay"', - 'exec --no-startup-id echo "show_overlay_1"', - ] - ), -} - -CURRENT_LAYOUT: None | Layout = None -HIDE_ON_WORKSPACE: None | str = None - - -i3 = i3ipc.Connection() - - -def move_window_to_workspace( - i3_container: i3ipc.Con, - workspace: Optional[str] = None, -): - if workspace == "scratchpad": - i3.command(f"[con_id={i3_container.id}] move to scratchpad") - else: - i3.command(f"[con_id={i3_container.id}] move to workspace {workspace}") - - -def get_dimensions_on_workspace( - geometry: Dict[str, int], - workspace: i3ipc.Con, -): - max_x = workspace.rect.x + workspace.rect.width - desired_x = geometry['x'] + workspace.rect.x - - if desired_x >= max_x: - return - - max_y = workspace.rect.y + workspace.rect.height - desired_y = geometry['y'] + workspace.rect.y - - if desired_y >= max_y: - return - - width = min( - geometry['w'], - max_x - desired_x - ) - height = min( - geometry['h'], - max_y - desired_y, - ) - - if width < 100 or height < 100: - return - - - return { - "x": desired_x, - "y": desired_y, - "w": width, - "h": height, - } - - -def place_window( - i3_container: i3ipc.Con, - x, - y, -): - win_id = i3_container.id - i3.command(f"[con_id={win_id}] move position {x} {y}") - -def resize_window( - i3_container: i3ipc.Con, - w, - h, -): - win_id = i3_container.id - i3.command(f"[con_id={win_id}] resize set {w} {h}") - - -def float_window( - i3_container: i3ipc.Con, -): - i3.command(f"[con_id={i3_container.id}] floating enable") - - -def focus_window( - i3_container: i3ipc.Con, -): - i3.command(f"[con_id={i3_container.id}] focus") - - -def snapshot_Window( - i3_container: i3ipc.Con, -): - rect = i3_container.rect - - return { - "con_id": i3_container.id, - "workspace": i3_container.workspace().name, - "floating": i3_container.floating, - "fullscreen": i3_container.fullscreen_mode, - "rect": { - "x": rect.x, - "y": rect.y, - "w": rect.width, - "h": rect.height, - }, - } - - -def restore_Window( - window: Window, -): - if not window.restore_to_initial_state: - return - - state = window._initial_state_snapshot - - i3.command( - f"[con_id={state['con_id']}] move container to workspace {state['workspace']}" - ) - - if state["floating"] not in {"user_off", "auto_off"}: - r = state["rect"] - i3.command(f"[con_id={state['con_id']}] floating enable") - i3.command(f"[con_id={state['con_id']}] move position {r['x']} px {r['y']} px") - i3.command(f"[con_id={state['con_id']}] resize set {r['w']} px {r['h']} px") - else: - i3.command(f"[con_id={state['con_id']}] floating disable") - - if state["fullscreen"]: - i3.command(f"[con_id={state['con_id']}] fullscreen enable") - - -def init_Layout(layout: Layout): - """set windows initial positions and sizes""" - tree = i3.get_tree() - for win in layout.windows: - if win.skip_init: - continue - i3_container = find_window_container(tree, win) - if not i3_container: - logging.debug(f'Container for window (window_name={win.window_name}) not found, trying to spawn it') - continue - if win.restore_to_initial_state: - win._initial_state_snapshot = snapshot_Window(i3_container) - restore_Window(win) - continue - resize_window(i3_container, win.geometry['w'], win.geometry['h']) - move_window_to_workspace( - i3_container, - win.workspace, - ) - - -def spawn_Layout(layout: Layout): - tree = i3.get_tree() - for win in layout.windows: - if win.skip_spawn: - continue - i3_container = find_window_container(tree, win) - if i3_container: - continue - spawn([win]) - i3_container = find_window_container(tree, win) - if not i3_container: - logging.debug(f'Container for window (window_name={win.window_name}) either not spawned properly or disabled for spawning. Either way, it is not moved to desired workspace') - - -def close_Layout(layout: Optional[Layout] = None): - global CURRENT_LAYOUT - global HIDE_ON_WORKSPACE - - if not layout: - layout = CURRENT_LAYOUT - - if not layout: - return - - tree = i3.get_tree() - for win in layout.windows: - i3_container = find_window_container(tree, win) - if not i3_container: - logging.debug(f'Container for window (window_name={win.window_name}) not found') - continue - if win.restore_to_initial_state: - restore_Window(win) - continue - move_window_to_workspace( - i3_container, - win.workspace, - ) - - CURRENT_LAYOUT = None - - -def open_Layout( - layout: Layout, -): - global CURRENT_LAYOUT - global HIDE_ON_WORKSPACE - - tree = i3.get_tree() - ws = tree.find_focused().workspace() - windows_data = list() - for win in layout.windows: - i3_container = find_window_container(tree, win) - if not i3_container: - logging.debug(f'Container for window (window_name={win.window_name}) not found') - continue - float_window(i3_container) - move_window_to_workspace( - i3_container, - ws.name, - ) - dimensions = get_dimensions_on_workspace( - win.geometry, - ws, - ) - if not dimensions: - logging.debug(f'Container for window (window_name={win.window_name}) can not be fitted in workspace, hiding it') - move_window_to_workspace( - i3_container, - win.workspace, - ) - continue - place_window(i3_container, dimensions["x"], dimensions["y"]) - if win.steal_focus: - focus_window(i3_container) - windows_data.append( - { - "win": win, - "i3_container": i3_container, - "x": dimensions['x'], - "y": dimensions["y"], - "w": dimensions["w"], - "h": dimensions["h"], - } - ) - - sleep(0.005) - for win in windows_data: - resize_window( - win["i3_container"], - win['w'], - win['h'] - ) - - CURRENT_LAYOUT = layout - HIDE_ON_WORKSPACE = None - - -def workspace_empty(ws: i3ipc.Con): - """checks if all workspace empty, or have only 'floating' windows in it""" - for w in ws.leaves(): - if w.floating in {"auto_off", "user_off",}: - return False - return True - - -def spawn(targets: List[Window]): - tree = i3.get_tree() - for win in filter(lambda target: not find_window_container(tree, target), targets): - res = subprocess.Popen(win.run) - logging.debug(f"window spawned: {res}") - - -def find_window_container(tree: i3ipc.Con, win: Window) -> i3ipc.Con | None: - try: - return next( - filter( - lambda i3_con: i3_con.name - and i3_con.name == win.window_name, - tree.leaves(), - ) - ) - except StopIteration: - return None - - -def get_layouts_window_titles() -> List[str]: - all_titles = set() - for layout in LAYOUTS.values(): - for win in layout.windows: - all_titles.add(win.window_name) - - return list(all_titles) - - - -def get_spawn_targets( - layouts: List[Layout] -) -> List[Window]: - """returns filtered out unique spawn targets""" - all_windows = reduce( - lambda a, b: a.windows + b.windows, - layouts, - ) - return list(set(all_windows)) - - -def on_binding(i3conn, event: i3ipc.events.BindingEvent): - """check binding sended and try to showup windows""" - global CURRENT_LAYOUT - global HIDE_ON_WORKSPACE - - tree = i3conn.get_tree() - - layout_to_switch_to = LAYOUTS.get(event.binding.command) - if not layout_to_switch_to: - return - - - if CURRENT_LAYOUT is None: - close_Layout() - spawn_Layout(layout_to_switch_to) - open_Layout(layout_to_switch_to) - return - - if CURRENT_LAYOUT == layout_to_switch_to: - close_Layout() - HIDE_ON_WORKSPACE = tree.find_focused().workspace().name - return - - if event.binding.command in CURRENT_LAYOUT.close_layout_on: - close_Layout() - HIDE_ON_WORKSPACE = tree.find_focused().workspace().name - return - - close_Layout() - open_Layout(layout_to_switch_to) - - -i3.on("binding", on_binding) - - -def on_workspace_focus(i3conn: i3ipc.Connection, event: i3ipc.events.IpcBaseEvent): - global HIDE_ON_WORKSPACE - - tree = i3conn.get_tree() - focus_workspace = tree.find_focused().workspace() - - current_layout_workspace_is_empty = ( - CURRENT_LAYOUT - and next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None) - and workspace_empty(next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None).workspace()) - ) - if current_layout_workspace_is_empty and not workspace_empty(focus_workspace): - return - - focus_on_same_workspace_as_current_layout = ( - CURRENT_LAYOUT - and next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None) - and focus_workspace.name == next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None).workspace().name - ) - if focus_on_same_workspace_as_current_layout: - # skip re-focus on the current layout - return - - hide_button_pressed_on_current_workspace = focus_workspace.name == HIDE_ON_WORKSPACE - if hide_button_pressed_on_current_workspace: - return - - if workspace_empty(focus_workspace): - close_Layout() - default_layout = list(LAYOUTS.values())[0] - spawn_Layout(default_layout) - open_Layout(default_layout) - return - - close_Layout() - -i3.on("workspace::focus", on_workspace_focus) - - -def default_behavior(i3conn, event: i3ipc.events.IpcBaseEvent): - tree = i3conn.get_tree() - focus_workspace = tree.find_focused().workspace() - - if event.container.window_title in get_layouts_window_titles(): - return - focus_on_same_workspace_as_current_layout = ( - CURRENT_LAYOUT - and next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None) - and focus_workspace.name == next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None).workspace().name - ) - if focus_on_same_workspace_as_current_layout and workspace_empty(focus_workspace): - return - elif focus_on_same_workspace_as_current_layout and not workspace_empty(focus_workspace): - close_Layout() - return - - if workspace_empty(focus_workspace): - default_layout = list(LAYOUTS.values())[0] - spawn_Layout(default_layout) - open_Layout(default_layout) - else: - close_Layout() - - -i3.on("window::new", default_behavior) -i3.on("window::floating", default_behavior) - - -def on_close(i3conn, event: i3ipc.events.WindowEvent): - closed_window = event.container - tree = i3conn.get_tree() - focus_workspace = tree.find_focused().workspace() - - focus_on_same_workspace_as_current_layout = ( - CURRENT_LAYOUT - and next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None) - and focus_workspace.name == next((find_window_container(tree, win) for win in CURRENT_LAYOUT.windows), None).workspace().name - ) - if focus_on_same_workspace_as_current_layout and workspace_empty(focus_workspace): - return - - if closed_window.window_title in get_layouts_window_titles(): - return - if workspace_empty(focus_workspace): - default_layout = list(LAYOUTS.values())[0] - spawn_Layout(default_layout) - open_Layout(default_layout) - else: - close_Layout() - -i3.on("window::close", on_close) - - - -def parse_loglevel(loglevel: str): - map = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - } - return map.get(loglevel, logging.INFO) - - -def get_cli_args(): - parser = argparse.ArgumentParser( - prog="Manage and use floating sub-layouts with i3.", - description=( - "Manage floating overlay layouts in i3wm. " - "The script listens to i3 IPC events and keybindings to automatically " - "spawn, position, float, hide, and restore predefined windows. " - "Layouts appear on empty workspaces or when triggered, and disappear " - "when focus or workspace state changes." - ) - ) - - parser.add_argument( - "--log-level", - dest="log_level", - choices=[ - "debug", - "info", - "warning", - "error", - ], - default="warning", - ) - - return parser.parse_args() - - -def main(): - args = get_cli_args() - - logging.basicConfig(level=parse_loglevel(args.log_level)) - - spawn( - get_spawn_targets( - LAYOUTS.values(), - ) - ) - - sleep(1) - - for layout in LAYOUTS.values(): - spawn_Layout(layout) - init_Layout(layout) - - i3.main() - - -if __name__ == "__main__": - main() -- cgit v1.3.1