From beb607186d1466e84c0b22a71ffdc018ada37cfd Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 1 Aug 2023 18:24:59 +0200 Subject: [PATCH 01/27] Version 1.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e1ddb73..29eb7fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyprland" -version = "1.3.1" +version = "1.4.0" description = "An hyperland plugin system" authors = ["fdev31 "] license = "MIT" From 895e24b368fb064790e4190a16348da79b91beaf Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 1 Aug 2023 18:26:24 +0200 Subject: [PATCH 02/27] Mark release in Changelog --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0ca08c..a4851a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Host process for multiple Hyprland plugins. Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more information. -# Changelog +# 1.4.0 - Add [expose](https://github.com/hyprland-community/pyprland/wiki/Plugins#expose) addon - scratchpad: add [lazy](https://github.com/hyprland-community/pyprland/wiki/Plugins#lazy-optional) option From 2eafea986255b303df28673252a4be54dfd6217d Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 1 Aug 2023 22:34:46 +0200 Subject: [PATCH 03/27] sorted imports --- pyprland/command.py | 15 +++++++-------- pyprland/common.py | 2 +- pyprland/ipc.py | 6 +++--- pyprland/plugins/expose.py | 4 ++-- pyprland/plugins/interface.py | 1 + pyprland/plugins/lost_windows.py | 4 ++-- pyprland/plugins/magnify.py | 3 +-- pyprland/plugins/monitors.py | 2 +- pyprland/plugins/scratchpads.py | 10 +++------- pyprland/plugins/shift_monitors.py | 4 ++-- pyprland/plugins/toggle_dpms.py | 4 ++-- pyprland/plugins/workspaces_follow_focus.py | 4 ++-- 12 files changed, 27 insertions(+), 32 deletions(-) diff --git a/pyprland/command.py b/pyprland/command.py index 9ca0320..6f4df52 100755 --- a/pyprland/command.py +++ b/pyprland/command.py @@ -1,16 +1,15 @@ #!/bin/env python -" Pyprland - an Hyprland companion app " import asyncio -from typing import cast -import json -import sys -import os import importlib import itertools +import json +import os +import sys +from typing import cast - -from .ipc import get_event_stream, init as ipc_init -from .common import init_logger, get_logger, PyprError +from .common import PyprError, get_logger, init_logger +from .ipc import get_event_stream +from .ipc import init as ipc_init from .plugins.interface import Plugin CONTROL = f'/tmp/hypr/{ os.environ["HYPRLAND_INSTANCE_SIGNATURE"] }/.pyprland.sock' diff --git a/pyprland/common.py b/pyprland/common.py index 8cd2ed3..8c5947f 100644 --- a/pyprland/common.py +++ b/pyprland/common.py @@ -1,6 +1,6 @@ """ Shared utilities: logging """ -import os import logging +import os __all__ = ["DEBUG", "get_logger", "init_logger"] diff --git a/pyprland/ipc.py b/pyprland/ipc.py index e2e3208..ea269dd 100644 --- a/pyprland/ipc.py +++ b/pyprland/ipc.py @@ -1,12 +1,12 @@ #!/bin/env python """ Interact with hyprland using sockets """ import asyncio -from logging import Logger -from typing import Any import json import os +from logging import Logger +from typing import Any -from .common import get_logger, PyprError +from .common import PyprError, get_logger log: Logger | None = None diff --git a/pyprland/plugins/expose.py b/pyprland/plugins/expose.py index 32213a5..78f8e81 100644 --- a/pyprland/plugins/expose.py +++ b/pyprland/plugins/expose.py @@ -2,9 +2,9 @@ toggle_minimized allows having an "expose" like selection of minimized windows """ from typing import Any, cast -from .interface import Plugin -from ..ipc import hyprctlJSON, hyprctl +from ..ipc import hyprctl, hyprctlJSON +from .interface import Plugin class Extension(Plugin): # pylint: disable=missing-class-docstring diff --git a/pyprland/plugins/interface.py b/pyprland/plugins/interface.py index b48dc19..4613695 100644 --- a/pyprland/plugins/interface.py +++ b/pyprland/plugins/interface.py @@ -1,5 +1,6 @@ " Common plugin interface " from typing import Any + from ..common import get_logger diff --git a/pyprland/plugins/lost_windows.py b/pyprland/plugins/lost_windows.py index 3b7c133..4556dfa 100644 --- a/pyprland/plugins/lost_windows.py +++ b/pyprland/plugins/lost_windows.py @@ -1,8 +1,8 @@ " Moves unreachable client windows to the currently focused workspace" from typing import Any, cast -from .interface import Plugin -from ..ipc import hyprctlJSON, hyprctl +from ..ipc import hyprctl, hyprctlJSON +from .interface import Plugin def contains(monitor, window): diff --git a/pyprland/plugins/magnify.py b/pyprland/plugins/magnify.py index b3d9cc2..fe3a296 100644 --- a/pyprland/plugins/magnify.py +++ b/pyprland/plugins/magnify.py @@ -1,7 +1,6 @@ " Toggles workspace zooming " -from .interface import Plugin - from ..ipc import hyprctl +from .interface import Plugin class Extension(Plugin): # pylint: disable=missing-class-docstring diff --git a/pyprland/plugins/monitors.py b/pyprland/plugins/monitors.py index da4821d..a047132 100644 --- a/pyprland/plugins/monitors.py +++ b/pyprland/plugins/monitors.py @@ -1,9 +1,9 @@ " The monitors plugin " import subprocess from typing import Any, cast -from .interface import Plugin from ..ipc import hyprctlJSON +from .interface import Plugin def configure_monitors(monitors, screenid: str, pos_x: int, pos_y: int) -> None: diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index e4da67f..e7b37f3 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -1,15 +1,10 @@ " Scratchpads addon " -import os import asyncio +import os import subprocess from typing import Any, cast -from ..ipc import ( - hyprctl, - hyprctlJSON, - get_focused_monitor_props, -) - +from ..ipc import get_focused_monitor_props, hyprctl, hyprctlJSON from .interface import Plugin DEFAULT_MARGIN = 60 @@ -230,6 +225,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring if not class_lookup_hack: return False self.log.debug("Lookup hack triggered") + # hack to update the client info from the provided class for client in await hyprctlJSON("clients"): assert isinstance(client, dict) for pending_scratch in class_lookup_hack: diff --git a/pyprland/plugins/shift_monitors.py b/pyprland/plugins/shift_monitors.py index 29990da..2e6d91c 100644 --- a/pyprland/plugins/shift_monitors.py +++ b/pyprland/plugins/shift_monitors.py @@ -1,8 +1,8 @@ " shift workspaces across monitors " from typing import cast -from .interface import Plugin -from ..ipc import hyprctlJSON, hyprctl +from ..ipc import hyprctl, hyprctlJSON +from .interface import Plugin class Extension(Plugin): # pylint: disable=missing-class-docstring diff --git a/pyprland/plugins/toggle_dpms.py b/pyprland/plugins/toggle_dpms.py index 86c6074..a395519 100644 --- a/pyprland/plugins/toggle_dpms.py +++ b/pyprland/plugins/toggle_dpms.py @@ -1,8 +1,8 @@ " Toggle monitors on or off " from typing import Any, cast -from .interface import Plugin -from ..ipc import hyprctlJSON, hyprctl +from ..ipc import hyprctl, hyprctlJSON +from .interface import Plugin class Extension(Plugin): # pylint: disable=missing-class-docstring diff --git a/pyprland/plugins/workspaces_follow_focus.py b/pyprland/plugins/workspaces_follow_focus.py index 29e2872..24efe8b 100644 --- a/pyprland/plugins/workspaces_follow_focus.py +++ b/pyprland/plugins/workspaces_follow_focus.py @@ -1,8 +1,8 @@ """ Force workspaces to follow the focus / mouse """ from typing import cast -from .interface import Plugin -from ..ipc import hyprctlJSON, hyprctl +from ..ipc import hyprctl, hyprctlJSON +from .interface import Plugin class Extension(Plugin): # pylint: disable=missing-class-docstring From c710e7691203fe4d7e310a1e5b56020734913390 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 1 Aug 2023 23:29:00 +0200 Subject: [PATCH 04/27] restore the module doc --- pyprland/command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyprland/command.py b/pyprland/command.py index 6f4df52..95ed3ee 100755 --- a/pyprland/command.py +++ b/pyprland/command.py @@ -1,4 +1,5 @@ #!/bin/env python +""" Pyprland - an Hyprland companion app (cli client & daemon) """ import asyncio import importlib import itertools From 5197977a26f03a5fe72c98f47078d4171723bc57 Mon Sep 17 00:00:00 2001 From: iliayar Date: Fri, 4 Aug 2023 02:32:14 +0300 Subject: [PATCH 05/27] feat nix: Add nix flake --- .gitignore | 2 ++ flake.lock | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 27 ++++++++++++++++++++++++ poetry.lock | 8 +++++++ 4 files changed, 98 insertions(+) create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e8862c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Nix +result diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8d25f52 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1685573264, + "narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "380be19fbd2d9079f677978361792cb25e8a3635", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-22.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ee0ead0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,27 @@ +{ + description = "pyprland"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05"; + }; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + packages = rec { + pyprland = pkgs.poetry2nix.mkPoetryApplication { + projectDir = ./.; + python = pkgs.python310; + }; + default = pyprland; + }; + } + ); +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2ddd539 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,8 @@ +package = [] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" + +[metadata.files] From ecfade18ab0e94a85b554f06536f833057d2b777 Mon Sep 17 00:00:00 2001 From: iliayar Date: Fri, 4 Aug 2023 20:17:47 +0300 Subject: [PATCH 06/27] feat scratchpads: adjust size, position for monitor --- pyprland/plugins/scratchpads.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index e7b37f3..65242a8 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -381,6 +381,9 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring wrkspc = monitor["activeWorkspace"]["id"] + size = item.conf.get("size") + position = item.conf.get("position") + self.transitioning_scratches.add(uid) await hyprctl(f"moveworkspacetomonitor special:scratch_{uid} {monitor['name']}") await hyprctl(f"movetoworkspacesilent {wrkspc},{addr}") @@ -390,5 +393,23 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring await fn(monitor, item.client_info, addr, margin) await hyprctl(f"focuswindow {addr}") + + if size: + # NOTE: Format for size is "X_SIZE Y_SIZE" + # X_SIZE, Y_SIZE is percentage of monitor size + x_size_p, y_size_p = map(int, size.split()) + x_size, y_size = int(monitor["width"] * x_size_p / 100), int(monitor["height"] * y_size_p / 100) + + await hyprctl(f"resizewindowpixel exact {x_size} {y_size},{addr}") + + if position: + # NOTE: Format for position is "X_POS Y_POS" + # X_POS, Y_POS is percentage of monitor size from top left corner + x_pos_p, y_pos_p = map(int, position.split()) + x_pos, y_pos = int(monitor["width"] * x_pos_p / 100), int(monitor["height"] * y_pos_p / 100) + x_pos_abs, y_pos_abs = x_pos + monitor["x"], y_pos + monitor["y"] + + await hyprctl(f"movewindowpixel exact {x_pos_abs} {y_pos_abs},{addr}") + await asyncio.sleep(0.2) # ensure some time for events to propagate self.transitioning_scratches.discard(uid) From d0fcbf123fd0ce40f995bebde7108300a5fe38c8 Mon Sep 17 00:00:00 2001 From: iliayar Date: Sun, 6 Aug 2023 17:09:50 +0300 Subject: [PATCH 07/27] refactor scratchpads: size, position percentage --- pyprland/plugins/scratchpads.py | 47 ++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index 65242a8..c203b4b 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -381,9 +381,6 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring wrkspc = monitor["activeWorkspace"]["id"] - size = item.conf.get("size") - position = item.conf.get("position") - self.transitioning_scratches.add(uid) await hyprctl(f"moveworkspacetomonitor special:scratch_{uid} {monitor['name']}") await hyprctl(f"movetoworkspacesilent {wrkspc},{addr}") @@ -394,22 +391,46 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring await hyprctl(f"focuswindow {addr}") + size = self._convert_coords(item.conf.get("size"), monitor) if size: - # NOTE: Format for size is "X_SIZE Y_SIZE" - # X_SIZE, Y_SIZE is percentage of monitor size - x_size_p, y_size_p = map(int, size.split()) - x_size, y_size = int(monitor["width"] * x_size_p / 100), int(monitor["height"] * y_size_p / 100) - + x_size, y_size = size await hyprctl(f"resizewindowpixel exact {x_size} {y_size},{addr}") + position = self._convert_coords(item.conf.get("position"), monitor) if position: - # NOTE: Format for position is "X_POS Y_POS" - # X_POS, Y_POS is percentage of monitor size from top left corner - x_pos_p, y_pos_p = map(int, position.split()) - x_pos, y_pos = int(monitor["width"] * x_pos_p / 100), int(monitor["height"] * y_pos_p / 100) + x_pos, y_pos = position x_pos_abs, y_pos_abs = x_pos + monitor["x"], y_pos + monitor["y"] - await hyprctl(f"movewindowpixel exact {x_pos_abs} {y_pos_abs},{addr}") await asyncio.sleep(0.2) # ensure some time for events to propagate self.transitioning_scratches.discard(uid) + + def _convert_coords(self, coords, monitor): + """ + Converts a string like "X Y" to coordinates relative to monitor + Supported formats for X, Y: + - Percentage: "V%". V in [0; 100] + + Example: + "10% 20%", monitor 800x600 => 80, 120 + """ + + if not coords: + return None + + def convert(s, dim): + if s[-1] == "%": + p = int(s[:-1]) + if p < 0 or p > 100: + raise Exception(f"Percentage must be in range [0; 100], got {p}") + return int(monitor[dim] * p / 100) + else: + raise Exception(f"Unsupported format for dimension {dim} size, got {s}") + + try: + x_str, y_str = coords.split() + + return convert(x_str, "width"), convert(y_str, "height") + except Exception as e: + self.log.error(f"Failed to read coordinates: {e}") + return None From 43619bc1ca01c788bab88d39ef60a8630ba724a5 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Thu, 14 Sep 2023 18:39:21 +0200 Subject: [PATCH 08/27] make some operations more robust --- pyprland/command.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyprland/command.py b/pyprland/command.py index 95ed3ee..9196718 100755 --- a/pyprland/command.py +++ b/pyprland/command.py @@ -82,11 +82,15 @@ class Pyprland: async def read_events_loop(self): "Consumes the event loop and calls corresponding handlers" while not self.stopped: - data = (await self.event_reader.readline()).decode() + try: + data = (await self.event_reader.readline()).decode() + except UnicodeDecodeError: + self.log.error("Invalid unicode while reading events") + continue if not data: self.log.critical("Reader starved") return - cmd, params = data.split(">>") + cmd, params = data.split(">>", 1) full_name = f"event_{cmd}" await self._callHandler(full_name, params) From 19b9741ec33eee2d9f436efda05b8e9a6a240af4 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Thu, 14 Sep 2023 18:42:17 +0200 Subject: [PATCH 09/27] add some log in case of unexpected error --- pyprland/plugins/ironbar.py | 27 +++++++++++++++++++++++++++ pyprland/plugins/scratchpads.py | 11 ++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 pyprland/plugins/ironbar.py diff --git a/pyprland/plugins/ironbar.py b/pyprland/plugins/ironbar.py new file mode 100644 index 0000000..219d0e8 --- /dev/null +++ b/pyprland/plugins/ironbar.py @@ -0,0 +1,27 @@ +" Ironbar Plugin " +import os +import json +import asyncio + +from .interface import Plugin + +SOCKET = f"/run/user/{os.getuid()}/ironbar-ipc.sock" + + +async def ipcCall(**params): + ctl_reader, ctl_writer = await asyncio.open_unix_connection(SOCKET) + ctl_writer.write(json.dumps(params).encode("utf-8")) + await ctl_writer.drain() + ret = await ctl_reader.read() + ctl_writer.close() + await ctl_writer.wait_closed() + return json.loads(ret) + + +class Extension(Plugin): + "Toggles ironbar on/off" + is_visible = True + + async def run_toggle_ironbar(self, bar_name: str): + self.is_visible = not self.is_visible + await ipcCall(type="set_visible", visible=self.is_visible, bar_name=bar_name) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index e7b37f3..43ff936 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -3,6 +3,7 @@ import asyncio import os import subprocess from typing import Any, cast +import logging from ..ipc import get_focused_monitor_props, hyprctl, hyprctlJSON from .interface import Plugin @@ -82,6 +83,7 @@ class Animations: class Scratch: "A scratchpad state including configuration & client state" + log = logging.getLogger("scratch") def __init__(self, uid, opts): self.uid = uid @@ -118,7 +120,14 @@ class Scratch: "update the internal client info property, if not provided, refresh based on the current address" if client_info is None: client_info = await get_client_props_by_address("0x" + self.address) - assert isinstance(client_info, dict) + try: + assert isinstance(client_info, dict) + except AssertionError as e: + self.log.error( + f"client_info of {self.address} must be a dict: {client_info}" + ) + raise AssertionError(e) from e + self.client_info.update(client_info) def __str__(self): From 968c2241a064dcf4b44bd2e06016ec99b6b67e8e Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 19 Sep 2023 18:26:15 +0200 Subject: [PATCH 10/27] 1.4.1 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a4851a2..6bfc4d4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Host process for multiple Hyprland plugins. Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more information. +# 1.4.1 + +- minor bugfixes + # 1.4.0 - Add [expose](https://github.com/hyprland-community/pyprland/wiki/Plugins#expose) addon From ea39db64db7563037aabb9503ca3ea9395240a39 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Tue, 19 Sep 2023 18:26:24 +0200 Subject: [PATCH 11/27] Version 1.4.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 29eb7fe..a6adc29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyprland" -version = "1.4.0" +version = "1.4.1" description = "An hyperland plugin system" authors = ["fdev31 "] license = "MIT" From 121c11c26bf6e5dba97711049c3e32fcff42ab21 Mon Sep 17 00:00:00 2001 From: iliayar Date: Sun, 24 Sep 2023 14:44:10 +0300 Subject: [PATCH 12/27] feat scratchpads: Support monitor scale --- pyprland/plugins/scratchpads.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index c203b4b..ddcb6f1 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -423,7 +423,8 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring p = int(s[:-1]) if p < 0 or p > 100: raise Exception(f"Percentage must be in range [0; 100], got {p}") - return int(monitor[dim] * p / 100) + scale = float(monitor["scale"]) + return int(monitor[dim] / scale * p / 100) else: raise Exception(f"Unsupported format for dimension {dim} size, got {s}") From 2b722a39b098c99180c4e99031cc2aae459c7ec6 Mon Sep 17 00:00:00 2001 From: iliayar Date: Sun, 24 Sep 2023 14:48:06 +0300 Subject: [PATCH 13/27] feat nix: Update nixpkgs to 23.05 --- flake.lock | 8 ++++---- flake.nix | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 8d25f52..6067f6e 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1685573264, - "narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=", + "lastModified": 1695416179, + "narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "380be19fbd2d9079f677978361792cb25e8a3635", + "rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-22.05", + "ref": "nixos-23.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index ee0ead0..b60c2b8 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { flake-utils.url = "github:numtide/flake-utils"; - nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05"; }; outputs = { self, nixpkgs, flake-utils, ... }: From e7acf8f5382ee7c0ef8701f14504b477d8fd0d81 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Sun, 15 Oct 2023 22:49:29 +0200 Subject: [PATCH 14/27] Add poetry.lock, closes #16 --- poetry.lock | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..022bbde --- /dev/null +++ b/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" From 9b8ba82d545c4d8c3162228861703f7a44359aa4 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Sun, 15 Oct 2023 22:50:54 +0200 Subject: [PATCH 15/27] monitors plugin: don't fail if monitor is unknown --- pyprland/plugins/monitors.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyprland/plugins/monitors.py b/pyprland/plugins/monitors.py index a047132..d40b753 100644 --- a/pyprland/plugins/monitors.py +++ b/pyprland/plugins/monitors.py @@ -82,7 +82,10 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring for mon_pattern, conf in self.config["placement"].items(): if mon_pattern in mon_description: for placement, other_mon_description in conf.items(): - ref = mon_by_name[other_mon_description] + try: + ref = mon_by_name[other_mon_description] + except KeyError: + continue if ref: place = placement.lower() x: int = 0 From 07e27e225dc7421551083df2ee58f737587aa0d9 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Sun, 15 Oct 2023 23:16:40 +0200 Subject: [PATCH 16/27] Add a nicer assertion for addresses --- pyprland/plugins/scratchpads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index 43ff936..f05d6de 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -13,6 +13,7 @@ DEFAULT_MARGIN = 60 async def get_client_props_by_address(addr: str): "Returns client properties given its address" + assert len(addr) > 2, "Client address is invalid" for client in await hyprctlJSON("clients"): assert isinstance(client, dict) if client.get("address") == addr: From 4c8c570c7b941f5800d422937c2fa315451d710a Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 16 Oct 2023 00:15:10 +0200 Subject: [PATCH 17/27] Don't assume clients will be moved as expected, closes #17 --- pyprland/plugins/scratchpads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index f05d6de..bf2a4d7 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -248,7 +248,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring async def event_openwindow(self, params) -> None: "open windows hook" addr, wrkspc, _kls, _title = params.split(",", 3) - if wrkspc.startswith("special"): + if self._respawned_scratches: item = self.scratches_by_address.get(addr) if not item and self._respawned_scratches: # hack for windows which aren't related to the process (see #8) From f4597f4fd46c8dc1b4baeeb71188703469ca1f5b Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 16 Oct 2023 00:19:25 +0200 Subject: [PATCH 18/27] add couple of logs & asserts Also invert the zombie process logic --- pyprland/command.py | 5 +++++ pyprland/plugins/scratchpads.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyprland/command.py b/pyprland/command.py index 9196718..17ffd7c 100755 --- a/pyprland/command.py +++ b/pyprland/command.py @@ -73,6 +73,11 @@ class Pyprland: self.log.debug("%s.%s%s", plugin.name, full_name, params) try: await getattr(plugin, full_name)(*params) + except AssertionError as e: + self.log.error( + "Bug detected, please report on https://github.com/fdev31/pyprland/issues" + ) + self.log.exception(e) except Exception as e: # pylint: disable=W0718 self.log.warning( "%s::%s(%s) failed:", plugin.name, full_name, params diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index bf2a4d7..841a9a8 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -102,7 +102,7 @@ class Scratch: for line in f.readlines(): if line.startswith("State"): state = line.split()[1] - return state in "RSDTt" # not "Z (zombie)"or "X (dead)" + return state not in "ZX" # not "Z (zombie)"or "X (dead)" return False def reset(self, pid: int) -> None: @@ -199,7 +199,8 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring self.procs[name] = proc pid = proc.pid self.scratches[name].reset(pid) - self.scratches_by_pid[proc.pid] = scratch + self.scratches_by_pid[pid] = scratch + self.log.info(f"scratch {scratch.uid} has pid {pid}") if old_pid and old_pid in self.scratches_by_pid: del self.scratches_by_pid[old_pid] @@ -375,9 +376,13 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring del self.scratches_by_pid[item.pid] if item.address in self.scratches_by_address: del self.scratches_by_address[item.address] + self.log.info(f"starting {uid}") await self.start_scratch_command(uid) + self.log.info(f"{uid} started") + self.log.info("==> Wait for spawning") while uid in self._respawned_scratches: await asyncio.sleep(0.05) + self.log.info("<== spawned!") item.visible = True monitor = await get_focused_monitor_props() @@ -385,6 +390,8 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring await self.updateScratchInfo(item) + assert item.address, "No address !" + addr = "address:0x" + item.address animation_type = item.conf.get("animation", "").lower() From 8c7ad933ab112729a3334db96249551ed2d8387b Mon Sep 17 00:00:00 2001 From: iliayar Date: Tue, 17 Oct 2023 03:50:03 +0300 Subject: [PATCH 19/27] fix scratchapds: rework _convert_coords --- pyprland/plugins/scratchpads.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index ddcb6f1..5c8ba0b 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -391,14 +391,14 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring await hyprctl(f"focuswindow {addr}") - size = self._convert_coords(item.conf.get("size"), monitor) + size = item.conf.get("size") if size: - x_size, y_size = size + x_size, y_size = self._convert_coords(size, monitor) await hyprctl(f"resizewindowpixel exact {x_size} {y_size},{addr}") - position = self._convert_coords(item.conf.get("position"), monitor) + position = item.conf.get("position") if position: - x_pos, y_pos = position + x_pos, y_pos = self._convert_coords(position, monitor) x_pos_abs, y_pos_abs = x_pos + monitor["x"], y_pos + monitor["y"] await hyprctl(f"movewindowpixel exact {x_pos_abs} {y_pos_abs},{addr}") @@ -415,8 +415,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring "10% 20%", monitor 800x600 => 80, 120 """ - if not coords: - return None + assert coords, "coords must be non null" def convert(s, dim): if s[-1] == "%": @@ -434,4 +433,4 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring return convert(x_str, "width"), convert(y_str, "height") except Exception as e: self.log.error(f"Failed to read coordinates: {e}") - return None + raise e From 2074554268098f951359fbba9075b9a10771a741 Mon Sep 17 00:00:00 2001 From: iliayar Date: Tue, 17 Oct 2023 03:53:46 +0300 Subject: [PATCH 20/27] fix: remove poetry.lock --- poetry.lock | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 2ddd539..0000000 --- a/poetry.lock +++ /dev/null @@ -1,8 +0,0 @@ -package = [] - -[metadata] -lock-version = "1.1" -python-versions = "^3.10" -content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" - -[metadata.files] From 8993a6214963f0e38364a6b131dcf3942fccc747 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 23 Oct 2023 15:24:43 +0200 Subject: [PATCH 21/27] monitors: add relayout command --- pyprland/plugins/monitors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyprland/plugins/monitors.py b/pyprland/plugins/monitors.py index d40b753..18556ed 100644 --- a/pyprland/plugins/monitors.py +++ b/pyprland/plugins/monitors.py @@ -40,6 +40,9 @@ def configure_monitors(monitors, screenid: str, pos_x: int, pos_y: int) -> None: class Extension(Plugin): # pylint: disable=missing-class-docstring async def load_config(self, config) -> None: await super().load_config(config) + await self.run_relayout() + + async def run_relayout(self): monitors = cast(list[dict], await hyprctlJSON("monitors")) for monitor in monitors: await self.event_monitoradded( From 2a09103f1087e632d271514df44163a8f6ac238e Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 23 Oct 2023 20:30:21 +0200 Subject: [PATCH 22/27] more informative log --- pyprland/plugins/scratchpads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index e788771..df3a96f 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -382,7 +382,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring self.log.info("==> Wait for spawning") while uid in self._respawned_scratches: await asyncio.sleep(0.05) - self.log.info("<== spawned!") + self.log.info(f"=> spawned {uid} as proc {item.pid}") item.visible = True monitor = await get_focused_monitor_props() From 8d443a541fdc1439079d8d26b9a1c01afdd1b2bd Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 23 Oct 2023 20:30:29 +0200 Subject: [PATCH 23/27] README: add WIP items --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6bfc4d4..3de406c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Host process for multiple Hyprland plugins. Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more information. +# 1.4.2 (WIP) + +- [two new options](https://github.com/hyprland-community/pyprland/wiki/Plugins#size-optional) for scratchpads: `position` and `size` +- bugfixes + # 1.4.1 - minor bugfixes From efebc234b557d80876091f398ba8cda210a61c06 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Mon, 23 Oct 2023 20:39:26 +0200 Subject: [PATCH 24/27] add a couple of logs --- pyprland/plugins/scratchpads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index df3a96f..d6a7775 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -303,6 +303,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring """Update every scratchpads information if no `scratch` given, else update a specific scratchpad info""" if scratch is None: + self.log.info("update from None") for client in await hyprctlJSON("clients"): assert isinstance(client, dict) scratch = self.scratches_by_address.get(client["address"][2:]) @@ -316,6 +317,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring add_to_address_book = ("address" not in scratch.client_info) or ( scratch.address not in self.scratches_by_address ) + self.log.info(f"update from something, adding: {add_to_address_book}") await scratch.updateClientInfo() if add_to_address_book: self.scratches_by_address[scratch.client_info["address"][2:]] = scratch From 02de5fbc7643b52369775060e697c75730448dd7 Mon Sep 17 00:00:00 2001 From: Fabien Devaux Date: Mon, 23 Oct 2023 20:45:33 +0200 Subject: [PATCH 25/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3de406c..4b168d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Check the [wiki](https://github.com/hyprland-community/pyprland/wiki) for more i # 1.4.2 (WIP) -- [two new options](https://github.com/hyprland-community/pyprland/wiki/Plugins#size-optional) for scratchpads: `position` and `size` +- [two new options](https://github.com/hyprland-community/pyprland/wiki/Plugins#size-optional) for scratchpads: `position` and `size` - from @iliayar - bugfixes # 1.4.1 From 1653d383d363062f49769f67d0dc27c3b3b1c735 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Wed, 25 Oct 2023 19:44:04 +0200 Subject: [PATCH 26/27] Experimental logic chnge --- pyprland/plugins/scratchpads.py | 76 +++++++++++++++++---------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/pyprland/plugins/scratchpads.py b/pyprland/plugins/scratchpads.py index d6a7775..df1d466 100644 --- a/pyprland/plugins/scratchpads.py +++ b/pyprland/plugins/scratchpads.py @@ -1,6 +1,7 @@ " Scratchpads addon " import asyncio import os +from itertools import count import subprocess from typing import Any, cast import logging @@ -217,6 +218,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring scratch.just_created = False else: for uid, scratch in self.scratches.items(): + self.log.info((scratch.address, addr)) if scratch.client_info and scratch.address != addr: if ( scratch.visible @@ -299,28 +301,21 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring return # abort sequence await asyncio.sleep(0.2) # await for animation to finish - async def updateScratchInfo(self, scratch: Scratch | None = None) -> None: + async def updateScratchInfo(self, orig_scratch: Scratch | None = None) -> None: """Update every scratchpads information if no `scratch` given, else update a specific scratchpad info""" - if scratch is None: - self.log.info("update from None") - for client in await hyprctlJSON("clients"): - assert isinstance(client, dict) - scratch = self.scratches_by_address.get(client["address"][2:]) - if not scratch: - scratch = self.scratches_by_pid.get(client["pid"]) - if scratch: - self.scratches_by_address[client["address"][2:]] = scratch + pid = orig_scratch.pid if orig_scratch else None + for client in await hyprctlJSON("clients"): + assert isinstance(client, dict) + if pid and pid != client["pid"]: + continue + scratch = self.scratches_by_address.get(client["address"][2:]) + if not scratch: + scratch = self.scratches_by_pid.get(client["pid"]) if scratch: - await scratch.updateClientInfo(client) - else: - add_to_address_book = ("address" not in scratch.client_info) or ( - scratch.address not in self.scratches_by_address - ) - self.log.info(f"update from something, adding: {add_to_address_book}") - await scratch.updateClientInfo() - if add_to_address_book: - self.scratches_by_address[scratch.client_info["address"][2:]] = scratch + self.scratches_by_address[client["address"][2:]] = scratch + if scratch: + await scratch.updateClientInfo(client) async def run_hide(self, uid: str, force=False, autohide=False) -> None: """ hides scratchpad "name" """ @@ -332,8 +327,11 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring if not scratch.visible and not force: self.log.warning("%s is already hidden", uid) return - self.log.info("Hiding %s", uid) scratch.visible = False + if not scratch.isAlive(): + await self.run_show(uid, force=True) + return + self.log.info("Hiding %s", uid) addr = "address:0x" + scratch.address animation_type: str = scratch.conf.get("animation", "").lower() if animation_type: @@ -351,6 +349,27 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring ) del self.focused_window_tracking[uid] + async def ensure_alive(self, uid, item=None): + if item is None: + item = self.scratches.get(uid) + + if not item.isAlive(): + self.log.info("%s is not running, restarting...", uid) + if uid in self.procs: + self.procs[uid].kill() + if item.pid in self.scratches_by_pid: + del self.scratches_by_pid[item.pid] + if item.address in self.scratches_by_address: + del self.scratches_by_address[item.address] + self.log.info(f"starting {uid}") + await self.start_scratch_command(uid) + self.log.info(f"{uid} started") + self.log.info("==> Wait for spawning") + loop_count = count() + while uid in self._respawned_scratches and next(loop_count) < 10: + await asyncio.sleep(0.05) + self.log.info(f"=> spawned {uid} as proc {item.pid}") + async def run_show(self, uid, force=False) -> None: """ shows scratchpad "name" """ uid = uid.strip() @@ -369,22 +388,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring return self.log.info("Showing %s", uid) - - if not item.isAlive(): - self.log.info("%s is not running, restarting...", uid) - if uid in self.procs: - self.procs[uid].kill() - if item.pid in self.scratches_by_pid: - del self.scratches_by_pid[item.pid] - if item.address in self.scratches_by_address: - del self.scratches_by_address[item.address] - self.log.info(f"starting {uid}") - await self.start_scratch_command(uid) - self.log.info(f"{uid} started") - self.log.info("==> Wait for spawning") - while uid in self._respawned_scratches: - await asyncio.sleep(0.05) - self.log.info(f"=> spawned {uid} as proc {item.pid}") + await self.ensure_alive(uid, item) item.visible = True monitor = await get_focused_monitor_props() From fff23c250e6d701029fd7491c8373e52d73fd5c2 Mon Sep 17 00:00:00 2001 From: Hydroxycarbamide Date: Sun, 29 Oct 2023 20:23:10 +0100 Subject: [PATCH 27/27] expose: disable togglespecialworkspace when hiding the workspace because movetoworkspacesilent is already doing it --- pyprland/plugins/expose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyprland/plugins/expose.py b/pyprland/plugins/expose.py index 78f8e81..ed01925 100644 --- a/pyprland/plugins/expose.py +++ b/pyprland/plugins/expose.py @@ -42,7 +42,7 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring await hyprctl( f"movetoworkspacesilent {client['workspace']['id']},address:{client['address']}" ) - await hyprctl("togglespecialworkspace exposed") + # await hyprctl("togglespecialworkspace exposed") await hyprctl(f"focuswindow address:{focused_addr}") self.exposed = [] else: