Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pyprconf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/__pycache__/
build/
*.egg-info/
*.env/
20 changes: 20 additions & 0 deletions pyprconf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# pyprconf

**templated configuration and event handling for Hyprland**

pyprconf is a front-end for Hyprland socket.sock and socket2.sock.
It is configured using a yaml file, and is intended to be exec'd in your
`hyprland.conf`.

## Installation

`python -m pip install .`

Modules will be installed to appropiate `site-packages` location, and a
command-line tool `pyprconf` in $HOME/.local/bin`.

### `hyprland.conf`

```
exec-once $HOME/.local/bin/pyprconf /path/to/conf.yaml
```
59 changes: 59 additions & 0 deletions pyprconf/doc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# this file documents the configuration keys for pyprconf
# and will only result in sadness if used as-is
pyprconf:
bindreload: SUPER+CTRL,C
bindunload: SUPER+ALT+CTRL,C

# a section named 'sample'
sample:
# tokens are available for substitution
# in most other keys. a special token {@}
# is always available and refers to the
# name of section.
tokens:
foo: bar
fiz: buz
# repeats the section for each element in
# the range. makes the token {#} available
# for use in the section. a 3-element array
# using python range syntax [start, stop (exclusive), step]
range: [1,10,1]
# binds are set each time the configuration
# is loaded, and unbound when the configuration
# is unloaded. an object where keys are MOD,Key
# and values are the dispatcher,args
bind:
SUPER,H: togglefloating,
SUPERSHIFT,H: pass,
SUPERALT,H: pass,
# keywords at this level are set each
# time the configuration is loaded.
# an array of strings
keyword:
- windowrule float,^({foo})$
- windowrulev2 pin,title:^()$
- general:col.active_border 0xff000000
# an array of event objects
events:
- event: createworkpace # (required) the event to handle
match: ^({@})$ # (required) a regex to match the event data
keyword: # (optional) keywords set when the event matches
- decorations:dim-active 1
- general:col.active_border 0xffffffff
dispatch: # (optional) dispatchers used when the event matches
- exec {foo}
- workspace {@}
# gates are used for making additional
# conditions on event handlers.
# in most uses one event handler
# will set a gate, and a different
# handler will check the gate and
# reset it. gate names are global
# and can be used across sections.
gate:
# set (True) a gate named 'mylatch'
set: mylatch
# reset (False) a gate named 'mylatch'
reset: mylatch
# only process the event handler if 'mylatch' is set
check: mylatch
Empty file added pyprconf/pyprconf/__init__.py
Empty file.
178 changes: 178 additions & 0 deletions pyprconf/pyprconf/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""pyrprconf configuration module"""

from collections import defaultdict
from dataclasses import dataclass
import os
import re
from typing import Any, Callable, Dict, List, Tuple, Union

COMMAND_TYPES = ["keyword", "dispatch"]


@dataclass
class EventHandler:
send: Union[str, None] = None
set: Union[str, None] = None
reset: Union[str, None] = None
check: Union[str, None] = None


HandlerDict = Dict[str, Dict[re.Pattern, List[EventHandler]]]
Loaders = List[str]
Unloaders = Loaders
Logger = Callable[[object], None]


@dataclass
class Configuration:
unloaders: Union[Unloaders, None]
handlers: Union[HandlerDict, None]


def batch(batch: List[str]) -> str:
if batch:
return f"[[BATCH]] {str.join(';', batch)}"
return None


def safe_format_map(format: str, map: Dict[str, Any]) -> str:
"""like str.format_map but ignores indexed positional format specifiers"""
safe_format = re.sub(r"{(\d+)}", lambda m: f"**]{m.group(1)}**]", format)
formatted = safe_format.format_map(map)
return re.sub(r"\*\*\](\d+)\*\*\]", lambda m: f"{{{m.group(1)}}}", formatted)


def _register_tokens(tokens: Dict[str, str], section: Dict[str, Any]) -> Dict[str, str]:
if "tokens" in section:
tokens.update(section["tokens"])


def _resolve_commands(
command: str, commands: Dict[str, Any], tokens: Dict[str, str]
) -> List[str]:
return list(
safe_format_map(f"/{command} {keyword}", tokens) for keyword in commands
)


def _resolve_binds(
binds: Dict[str, str], tokens: Dict[str, str]
) -> Tuple[List[str], List[str]]:
resolved = {
key.format_map(tokens): cmd.format_map(tokens) for (key, cmd) in binds.items()
}
return list(f"/keyword bind {key},{cmd}" for key, cmd in resolved.items()), list(
f"/keyword unbind {key}" for key in resolved
)


def _register_handlers(
log: Logger,
handlers: HandlerDict,
events: List[Dict[str, Any]],
tokens: Dict[str, str],
):
for event in events:
try:
commands = batch(
[
resolved
for command_type in COMMAND_TYPES
if command_type in event
for resolved in _resolve_commands(
command_type, event[command_type], tokens
)
]
)

set = None
reset = None
check = None

gate = event.get("gate", None)
if gate:
set = gate.get("set", None)
reset = gate.get("reset", None)
check = gate.get("check", None)

pattern = re.compile(event["match"].format_map(tokens))

handlers[event["event"]][pattern].append(
EventHandler(send=commands, set=set, reset=reset, check=check)
)

except KeyError as err:
log(f"Event in section '{tokens['@']}' missing required key: {err.args[0]}")
continue


def _parse_pyprconf(config: Dict[str, Any], pyrpconf: Dict) -> Tuple[Dict, List[str]]:
extensions = [name for name in config if name.startswith("x-")]
for name in extensions:
if name.startswith("x-"):
config.pop(name)

specialbinds = []

bindreload = pyrpconf.get("bindreload", None)
if bindreload:
cmd = f"/keyword bind {bindreload},exec,kill -USR1 {os.getpid()}"
specialbinds.append(cmd)

bindunload = pyrpconf.get("bindunload", None)
if bindunload:
cmd = f"/keyword bind {bindunload},exec,kill -USR2 {os.getpid()}"
specialbinds.append(cmd)

return config, specialbinds


def _parse_range(log: Logger, rangedef: Union[Dict[str, Any], None]) -> List[int]:
if rangedef:
if len(rangedef) < 3:
log(f"range must be defined as: [start, stop, step]")
return [0]

return range(rangedef[0], rangedef[1], rangedef[2])

return [0]


def parse(configdict: Dict[str, Any], log: Logger) -> Tuple[Loaders, Configuration]:
loaders = list()
unloaders = list()
handlers = defaultdict(lambda: defaultdict(list))

pyrpconf: Union[Dict, None] = configdict.pop("pyprconf", None)
if pyrpconf:
configdict, specialbinds = _parse_pyprconf(configdict, pyrpconf)
if specialbinds:
loaders.append(specialbinds)

for name, section in configdict.items():
log(f"processing section: {name}")

rangedef = section.get("range", None)
range_tokens = _parse_range(log, rangedef)

for range_token in range_tokens:
tokens = {"@": name, "#": range_token}
_register_tokens(tokens, section)

for command_type in COMMAND_TYPES:
if command_type in section:
loaders.append(
_resolve_commands(command_type, section[command_type], tokens)
)

if "bind" in section:
binds, unbinds = _resolve_binds(section["bind"], tokens)
loaders.append(binds)
unloaders.append(unbinds)
if "events" in section:
_register_handlers(log, handlers, section["events"], tokens)

handlers.default_factory = None
return [batch(loader) for loader in loaders], Configuration(
[batch(unloader) for unloader in unloaders], handlers
)
Loading