Compare commits

...

21 commits
1.4.1 ... main

Author SHA1 Message Date
Hydroxycarbamide
fff23c250e expose: disable togglespecialworkspace when hiding the workspace because movetoworkspacesilent is already doing it 2023-10-29 20:23:10 +01:00
fdev31
1653d383d3 Experimental logic chnge 2023-10-25 20:16:55 +02:00
Fabien Devaux
02de5fbc76
Update README.md 2023-10-23 20:45:33 +02:00
fdev31
efebc234b5 add a couple of logs 2023-10-23 20:40:34 +02:00
fdev31
8d443a541f README: add WIP items 2023-10-23 20:40:34 +02:00
fdev31
2a09103f10 more informative log 2023-10-23 20:40:34 +02:00
Fabien Devaux
cfe4995e32
Merge pull request #15 from iliayar/main
Scratchpads: Dynamic position and size, depending on monitor
2023-10-23 19:26:00 +02:00
fdev31
8993a62149 monitors: add relayout command 2023-10-23 15:24:43 +02:00
iliayar
2074554268
fix: remove poetry.lock 2023-10-17 03:53:46 +03:00
iliayar
8c7ad933ab
fix scratchapds: rework _convert_coords 2023-10-17 03:50:03 +03:00
fdev31
f4597f4fd4 add couple of logs & asserts
Also invert the zombie process logic
2023-10-16 00:19:25 +02:00
fdev31
4c8c570c7b Don't assume clients will be moved as expected, closes #17 2023-10-16 00:15:10 +02:00
fdev31
07e27e225d Add a nicer assertion for addresses 2023-10-15 23:16:40 +02:00
fdev31
9b8ba82d54 monitors plugin: don't fail if monitor is unknown 2023-10-15 22:50:54 +02:00
fdev31
e7acf8f538 Add poetry.lock, closes #16 2023-10-15 22:49:29 +02:00
iliayar
2b722a39b0
feat nix: Update nixpkgs to 23.05 2023-09-24 14:48:06 +03:00
iliayar
121c11c26b
feat scratchpads: Support monitor scale 2023-09-24 14:44:10 +03:00
fdev31
ea39db64db Version 1.4.1 2023-09-19 18:26:24 +02:00
iliayar
d0fcbf123f
refactor scratchpads: size, position percentage 2023-08-06 17:09:50 +03:00
iliayar
ecfade18ab
feat scratchpads: adjust size, position for monitor 2023-08-04 20:17:47 +03:00
iliayar
5197977a26
feat nix: Add nix flake 2023-08-04 02:32:14 +03:00
10 changed files with 205 additions and 36 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Nix
result

View file

@ -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` - from @iliayar
- bugfixes
# 1.4.1
- minor bugfixes

61
flake.lock generated Normal file
View file

@ -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": 1695416179,
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-23.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
}

27
flake.nix Normal file
View file

@ -0,0 +1,27 @@
{
description = "pyprland";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.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;
};
}
);
}

7
poetry.lock generated Normal file
View file

@ -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"

View file

@ -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

View file

@ -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:

View file

@ -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(
@ -82,7 +85,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

View file

@ -1,6 +1,7 @@
" Scratchpads addon "
import asyncio
import os
from itertools import count
import subprocess
from typing import Any, cast
import logging
@ -13,6 +14,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:
@ -101,7 +103,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:
@ -198,7 +200,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]
@ -215,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
@ -247,7 +251,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)
@ -297,26 +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:
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
)
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:
"""<name> hides scratchpad "name" """
@ -328,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:
@ -347,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:
"""<name> shows scratchpad "name" """
uid = uid.strip()
@ -365,18 +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]
await self.start_scratch_command(uid)
while uid in self._respawned_scratches:
await asyncio.sleep(0.05)
await self.ensure_alive(uid, item)
item.visible = True
monitor = await get_focused_monitor_props()
@ -384,6 +396,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()
@ -399,5 +413,47 @@ class Extension(Plugin): # pylint: disable=missing-class-docstring
await fn(monitor, item.client_info, addr, margin)
await hyprctl(f"focuswindow {addr}")
size = item.conf.get("size")
if size:
x_size, y_size = self._convert_coords(size, monitor)
await hyprctl(f"resizewindowpixel exact {x_size} {y_size},{addr}")
position = item.conf.get("position")
if 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}")
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
"""
assert coords, "coords must be non null"
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}")
scale = float(monitor["scale"])
return int(monitor[dim] / scale * 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}")
raise e

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "pyprland"
version = "1.4.0"
version = "1.4.1"
description = "An hyperland plugin system"
authors = ["fdev31 <fdev31@gmail.com>"]
license = "MIT"