├── requirements.txt ├── src └── easyland │ ├── __init__.py │ ├── log.py │ ├── main.py │ ├── command.py │ ├── idle.py │ └── daemon.py ├── config_examples ├── Listener.py ├── Sway.py └── Hyprland.py ├── .gitignore ├── LICENSE.txt ├── pyproject.toml ├── CHANGELOG.md └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.16.0 2 | pycparser==2.22 3 | pywayland==0.4.17 4 | -------------------------------------------------------------------------------- /src/easyland/__init__.py: -------------------------------------------------------------------------------- 1 | # from .libs.Log import logger 2 | # print(logger) 3 | # from . import libs 4 | from .log import logger 5 | from .command import Command 6 | 7 | command = Command() 8 | -------------------------------------------------------------------------------- /src/easyland/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging 4 | logger.basicConfig( 5 | level=logging.INFO, 6 | format="%(asctime)s [%(levelname)s] %(message)s", 7 | handlers=[ 8 | logging.FileHandler("easyland.log"), 9 | logging.StreamHandler() 10 | ] 11 | ) 12 | 13 | -------------------------------------------------------------------------------- /config_examples/Listener.py: -------------------------------------------------------------------------------- 1 | from easyland import logger 2 | 3 | listeners = { 4 | "hyprland": {}, 5 | 'systemd_logind': {}, 6 | 'idle': {} 7 | } 8 | 9 | def idle_config(): 10 | return [ 11 | [5, ['echo "Entering Idle"'], ['echo "Resuming Idle"']], 12 | ] 13 | 14 | def on_hyprland_event(event, argument): 15 | logger.info("Hyprland: Receveid '"+event +"' with argument "+argument.strip()) 16 | 17 | 18 | def on_systemd_event(sender, signal, payload): 19 | logger.info("Systemd: Received from '"+sender+"': "+ signal +' with payload: '+payload) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pyland.log 2 | easyland.log 3 | venv/ 4 | __pycache__/ 5 | .Python 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | downloads/ 10 | eggs/ 11 | .eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | wheels/ 18 | share/python-wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # PyBuilder 25 | .pybuilder/ 26 | target/ 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | .tox/ 31 | .nox/ 32 | .coverage 33 | .coverage.* 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | *.cover 38 | *.py,cover 39 | .hypothesis/ 40 | .pytest_cache/ 41 | cover/ 42 | 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "easyland" 3 | version = "0.7.6" 4 | authors = [ 5 | { name="Julien Pro", email="contact@julienpro.com"} 6 | ] 7 | description = "A python swiss-knife to manage wayand compositors like Hyprland and Sway" 8 | readme = "README.md" 9 | requires-python = ">=3.11" 10 | dependencies = [ 11 | "pywayland>=0.4.17" 12 | ] 13 | classifiers = [ 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.11", 18 | "Operating System :: POSIX :: Linux", 19 | "Topic :: Software Development :: Libraries", 20 | "Topic :: System :: Operating System", 21 | "Topic :: System :: Systems Administration", 22 | "Topic :: Desktop Environment" 23 | ] 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/juienpro/easyland" 27 | Issues = "https://github.com/juienpro/easyland/issues" 28 | 29 | [project.scripts] 30 | easyland = "easyland.main:main" 31 | 32 | [build-system] 33 | requires = ["setuptools>=61.0"] 34 | build-backend = "setuptools.build_meta" 35 | 36 | -------------------------------------------------------------------------------- /src/easyland/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import importlib 4 | import importlib.util 5 | import importlib.machinery 6 | import sys 7 | import time 8 | import os 9 | from easyland.daemon import Daemon 10 | 11 | version = '0.7.6' 12 | 13 | def import_from_path(path): 14 | module_name = os.path.basename(path).replace('-', '_').replace('.py', '') 15 | spec = importlib.util.spec_from_loader(module_name, importlib.machinery.SourceFileLoader(module_name, path)) 16 | module = importlib.util.module_from_spec(spec) 17 | spec.loader.exec_module(module) 18 | sys.modules[module_name] = module 19 | return module 20 | 21 | def main(): 22 | 23 | parser = argparse.ArgumentParser(description="Easyland - A python swiss-knife to manage Wayland compositors like Hyprland and Sway") 24 | parser.add_argument("-c", "--config", help="Path to your config file") 25 | parser.add_argument("-v", "--version", action="store_true", help="Show the version") 26 | args = parser.parse_args() 27 | 28 | if args.version: 29 | print('Easyland version: ' + version) 30 | sys.exit() 31 | 32 | if not args.config: 33 | print('Please provide a config file with the option -c') 34 | sys.exit(1) 35 | 36 | config = import_from_path(args.config) 37 | daemon = Daemon(config) 38 | while True: 39 | time.sleep(1) 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /config_examples/Sway.py: -------------------------------------------------------------------------------- 1 | from easyland import logger, command 2 | 3 | listeners = { 4 | "sway": { 5 | "event_types": ['workspace', 'window'] 6 | }, 7 | 'systemd_logind': {}, 8 | 'idle': {} 9 | } 10 | 11 | ############################################################################### 12 | # Idle configuration 13 | # Format: [timeout in seconds, [commands to run], [commands to run on resume]] 14 | ############################################################################### 15 | 16 | def idle_config(): 17 | return [ 18 | [5, ['echo "Idle for 5 seconds"'], ['echo "Resumed"']], 19 | [150, ['brightnessctl -s set 0'], ['brightnessctl -r']], 20 | [600, ['pidof hyprlock || hyprlock']], 21 | [720, ['hyprctl dispatch dpms off'], ['hyprctl dispatch dpms on']] 22 | ] 23 | 24 | 25 | ############################################################################### 26 | # Handler of Sway IPC events 27 | # List of events: https://man.archlinux.org/man/sway-ipc.7.en 28 | ############################################################################### 29 | def on_sway_event_workspace(payload): 30 | logger.info('Handling Sway workspace event: ' + payload['change']) 31 | 32 | def on_sway_event_window(payload): 33 | logger.info('Handling Sway window event: ' + payload['change']) 34 | 35 | ############################################################################### 36 | # Handlers of Systemd logind eventz 37 | ############################################################################### 38 | 39 | def on_PrepareForSleep(payload): 40 | if 'true' in payload: 41 | logger.info("Locking the screen before suspend") 42 | command.exec("pidof swaylock || swaylock", True) 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## v0.7.6 (2024-05-15) 3 | 4 | - Major fix: Idle thread launched several times leading to unexpected events 5 | - Minor fix: Improvement of the Hyprland config example to restart waybar and wpaperd each time the monitors change 6 | - Minor fix: Versionning 7 | 8 | ## v0.7.5 (2024-05-13) 9 | 10 | - Fix Hyprland socket path (support of Hyprland v0.4) 11 | - Better errors management in Hyprland IPC thread 12 | 13 | ## v0.7.4 (2024-05-13) 14 | 15 | - Fix Wayland dispatcher 16 | 17 | ## v0.7.3 (2024-05-13) 18 | 19 | - Fix listeners of Hyprland.py 20 | 21 | ## v0.7.2 (2024-05-03) 22 | 23 | - Added the possibility to change the path of the Hyprland socket 24 | - Better parameters definition for Listeners 25 | - Minor changes 26 | 27 | ## v0.7.1 (2024-05-02) 28 | 29 | - Minor changes (docs and config examples) 30 | 31 | ## v0.7.0 (2024-05-02) 32 | 33 | - Full rewrite 34 | - Implementation of a real Wayland Idle system 35 | 36 | ## v0.6 (2024-04-26) 37 | 38 | - Added support for background shell commands by adding & at the end of the command. Should fix the issue with hyprlock that blocks a thread. 39 | - Rename CHANGELOG.txt to CHANGELOG.md 40 | 41 | ## v0.5 (2024-04-26) 42 | 43 | - Added the parent class `Config.py` to each user-config class, to make user-defined class easier 44 | - Added the possibility to configure idle actions easily with `set_idle_config` and `do_idle_with_config` methods. 45 | 46 | ## v0.4 (2024-04-26) 47 | 48 | - Added Changelog.txt 49 | 50 | ## v0.3 (2024-04-26) 51 | 52 | - DBUS monitoring is made with `gdbus` instead of `dbus-monitor`. Indeed, `gdbus` displays received messages on a single line, including the payload, which ease the parsing. 53 | - Includes the payload in Systemd handler functions 54 | - Added version number to the script 55 | 56 | ## v0.2 (2024-04-26) 57 | 58 | - Updated myConfig to fix a bug in the Idle handler 59 | 60 | ## v0.1 (2024-04-25) 61 | 62 | - Initial Release 63 | -------------------------------------------------------------------------------- /src/easyland/command.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import json 3 | import os 4 | import time 5 | import threading 6 | from easyland.log import logger 7 | 8 | class Command(): 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def sway_get_all_monitors(self): 14 | monitors = self.exec("swaymsg -t get_outputs", decode_json = True) 15 | return monitors 16 | 17 | def sway_get_monitor(self, name = None, make = None, model = None): 18 | monitors = self.sway_get_all_monitors() 19 | for monitor in monitors: 20 | if name is not None: 21 | if name in monitor['name']: 22 | return monitor 23 | 24 | if make is not None: 25 | if make in monitor['make']: 26 | return monitor 27 | 28 | if model is not None: 29 | if model in monitor['model']: 30 | return monitor 31 | return None 32 | 33 | def hyprland_get_all_monitors(self): 34 | monitors = self.exec("hyprctl -j monitors", decode_json = True) 35 | return monitors 36 | 37 | def hyprland_get_monitor(self, name = None, description = None, make = None, model = None): 38 | monitors = self.hyprland_get_all_monitors() 39 | 40 | for monitor in monitors: 41 | if name is not None: 42 | if name in monitor['name']: 43 | return monitor 44 | 45 | if description is not None: 46 | if description in monitor['description']: 47 | return monitor 48 | 49 | if make is not None: 50 | if make in monitor['make']: 51 | return monitor 52 | 53 | if model is not None: 54 | if model in monitor['model']: 55 | return monitor 56 | return None 57 | 58 | def exec(self, command, background = False, decode_json = False): 59 | if background: 60 | logger.info("Executing background command: "+command) 61 | with open(os.devnull, 'w') as fp: 62 | subprocess.Popen(command, shell=True, stdout=fp) 63 | return True 64 | else: 65 | logger.info("Executing command: "+command) 66 | output = subprocess.check_output(command, shell=True) 67 | decoded_output = output.decode("utf-8") 68 | if decode_json: 69 | try: 70 | json_output = json.loads(decoded_output) 71 | except json.decoder.JSONDecodeError: 72 | return None 73 | return json_output 74 | else: 75 | return decoded_output 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/easyland/idle.py: -------------------------------------------------------------------------------- 1 | # import .command as Command 2 | from . import command 3 | from . import logger 4 | import subprocess 5 | import threading 6 | import os 7 | from pywayland.client import Display 8 | from pywayland.protocol.wayland.wl_seat import WlSeat 9 | from pywayland.protocol.ext_idle_notify_v1 import ( 10 | ExtIdleNotificationV1, 11 | ExtIdleNotifierV1 12 | ) 13 | 14 | class Idle(): 15 | def __init__(self, config): 16 | # self.command = Command.Command() 17 | self.command = command 18 | self._config = config 19 | self._display = Display() 20 | self._display.connect() 21 | self._idle_notifier = None 22 | self._seat = None 23 | self._notifications = [] 24 | self._notifier_set = False 25 | 26 | # def _global_handler_init(self, reg, id_num, iface_name, version): 27 | # if iface_name == 'wl_seat': 28 | # self._seat = reg.bind(id_num, WlSeat, version) 29 | 30 | def _global_handler(self, reg, id_num, iface_name, version): 31 | if iface_name == 'wl_seat': 32 | self._seat = reg.bind(id_num, WlSeat, version) 33 | if iface_name == "ext_idle_notifier_v1": 34 | self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version) 35 | 36 | if self._idle_notifier and self._seat and not self._notifier_set: 37 | self._notifier_set = True 38 | for idx, c in enumerate(self._config): 39 | self._notifications.append(None) 40 | logger.info("Setting idle notifier for " + str(c[0]) + " seconds") 41 | self._notifications[idx] = self._idle_notifier.get_idle_notification(c[0] * 1000, self._seat) 42 | self._notifications[idx]._index = idx 43 | self._notifications[idx].dispatcher['idled'] = self._idle_notifier_handler 44 | self._notifications[idx].dispatcher['resumed'] = self._idle_notifier_resume_handler 45 | 46 | def _idle_notifier_handler(self, notification): 47 | for command in self._config[notification._index][1]: 48 | logger.info('Idle - Running command: ' + command) 49 | with open(os.devnull, 'w') as fp: 50 | subprocess.Popen(command, shell=True, stdout=fp) 51 | 52 | def _idle_notifier_resume_handler(self, notification): 53 | if len(self._config[notification._index]) > 2: 54 | for command in self._config[notification._index][2]: 55 | logger.info('Idle - Resuming: Running command: ' + command) 56 | with open(os.devnull, 'w') as fp: 57 | subprocess.Popen(command, shell=True, stdout=fp) 58 | 59 | def setup(self): 60 | reg = self._display.get_registry() 61 | reg.dispatcher['global'] = self._global_handler 62 | while True: 63 | self._display.dispatch(block=True) 64 | 65 | -------------------------------------------------------------------------------- /config_examples/Hyprland.py: -------------------------------------------------------------------------------- 1 | from easyland import logger, command 2 | 3 | ############################################################################### 4 | # Set active listeners 5 | ############################################################################### 6 | 7 | listeners = { 8 | "hyprland": { 9 | # "socket_path": "/tmp/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" (For hyprland < 0.4) 10 | }, 11 | 'systemd_logind': {}, 12 | 'idle': {} 13 | } 14 | 15 | def init(): 16 | set_monitors() 17 | 18 | ############################################################################### 19 | # Idle configuration 20 | # Format: [timeout in seconds, [commands to run], [commands to run on resume]] 21 | ############################################################################### 22 | 23 | def idle_config(): 24 | return [ 25 | [150, ['brightnessctl -s set 0'], ['brightnessctl -r']], 26 | [600, ['pidof hyprlock || hyprlock']], 27 | [720, ['hyprctl dispatch dpms off'], ['hyprctl dispatch dpms on']] 28 | ] 29 | 30 | ############################################################################### 31 | # Handler of Hyprland IPC events 32 | # List of events: https://wiki.hyprland.org/IPC/ 33 | ############################################################################### 34 | 35 | def on_hyprland_event(event, argument): 36 | if event in [ "monitoradded", "monitorremoved", "configreloaded" ]: 37 | logger.info('Handling hyprland event: ' + event) 38 | set_monitors() 39 | 40 | ## When disconnecting my laptop, a new workspace is created. We switch back to a default workspace 41 | if event == 'monitorremoved': 42 | command.exec("hyprctl dispatch workspace 5", True) 43 | 44 | ## Sometimes, Waybar or wpaperd crashes 45 | if event in ['monitoradded', 'monitorremoved']: 46 | command.exec("pkill waybar || true && waybar", background = True) 47 | command.exec("pkill wpaperd || true && wpaperd -d", background = True) 48 | 49 | 50 | 51 | ############################################################################### 52 | # Handlers of Systemd logind events 53 | ############################################################################### 54 | 55 | def on_PrepareForSleep(payload): 56 | if 'true' in payload: 57 | logger.info("Locking the screen before suspend") 58 | command.exec("pidof hyprlock || hyprlock", True) 59 | 60 | # To use this handler, you need to launch your locker (hyprlock or swaylock) like this: hyprlock && loginctl unlock-session 61 | # def on_Unlock(): 62 | # logger.info("Unlocking the screen") 63 | 64 | # To use this handler, you need to launch your locker like this: loginctl lock-session 65 | # def on_lock(): 66 | # logger.info("Locking the screen") 67 | 68 | ############################################################################### 69 | # Various methods 70 | ############################################################################### 71 | 72 | def set_monitors(): 73 | logger.info('Setting monitors') 74 | if command.hyprland_get_monitor(description="HP 22es") is not None: 75 | # command.exec('hyprctl keyword monitor "eDP-1,preferred,auto,2"') 76 | command.exec('hyprctl keyword monitor "eDP-1,disable"') 77 | else: 78 | command.exec('hyprctl keyword monitor "eDP-1,preferred,auto,2"') 79 | # command.exec("brightnessctl -s set 0") 80 | 81 | -------------------------------------------------------------------------------- /src/easyland/daemon.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | import re 4 | import time 5 | import json 6 | import sys 7 | from easyland.log import logger 8 | from easyland.idle import Idle 9 | 10 | class Daemon(): 11 | 12 | def __init__(self, config): 13 | self.config = config 14 | self.listeners = self.get_listeners() 15 | listener_names = self.listeners.keys() 16 | 17 | logger.info('Starting easyland daemon') 18 | 19 | if 'hyprland' in listener_names: 20 | hyprland_thread = threading.Thread(target=self.launch_hyprland_daemon) 21 | hyprland_thread.daemon = True 22 | hyprland_thread.start() 23 | 24 | if 'sway' in listener_names: 25 | if not 'event_types' in self.listeners['sway']: 26 | logger.error('No sway event types defined for Sway listeners in the config file') 27 | sys.exit(1) 28 | existing_types = ['workspace', 'window', 'output', 'mode', 'barconfig_update', 'binding', 'shutdown', 'tick', 'bar_state_update', 'input'] 29 | sway_threads = [None] * len(self.listeners['sway']['event_types']) 30 | for idx, event_type in enumerate(self.listeners['sway']['event_types']): 31 | if event_type not in existing_types: 32 | logger.error('Sway - Invalid event type: ' + event_type) 33 | sys.exit(1) 34 | sway_threads[idx] = threading.Thread(target=self.launch_sway_daemon, args=(event_type,)) 35 | sway_threads[idx].daemon = True 36 | sway_threads[idx].start() 37 | 38 | if 'systemd_logind' in listener_names: 39 | systemd_thread = threading.Thread(target=self.launch_systemd_login_daemon) 40 | systemd_thread.daemon = True 41 | systemd_thread.start() 42 | 43 | if 'idle' in listener_names: 44 | if callable(getattr(self.config, 'idle_config', None)): 45 | idle_thread = threading.Thread(target=self.launch_idle_daemon) 46 | idle_thread.daemon = True 47 | idle_thread.start() 48 | 49 | self.call_handler('init') 50 | 51 | def get_listeners(self): 52 | if hasattr(self.config, 'listeners'): 53 | return self.config.listeners 54 | else: 55 | logger.error('No listeners defined in the config file') 56 | sys.exit(1) 57 | 58 | 59 | def call_handler(self, handler, *argv): 60 | func = getattr(self.config, handler, None) 61 | if callable(func): 62 | func(*argv) 63 | 64 | def launch_idle_daemon(self): 65 | idle_config = self.config.idle_config() 66 | idle = Idle(idle_config) 67 | idle.setup() 68 | 69 | def launch_hyprland_daemon(self): 70 | logger.info('Launching hyprland daemon') 71 | socket = self.listeners['hyprland'].get('socket_path', '$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock') 72 | cmd = "socat -U - UNIX-CONNECT:"+socket 73 | # ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) 74 | ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 75 | while True: 76 | if ps.returncode != 0 and ps.returncode is not None: 77 | err = ps.stderr.read().decode("utf-8") 78 | logger.error("Error while listening to Hyprland socket: " + err) 79 | sys.exit(1) 80 | for line in iter(ps.stdout.readline, ""): 81 | self.last_event_time = time.time() 82 | decoded_line = line.decode("utf-8") 83 | logger.debug(decoded_line.strip()) 84 | if '>>' in decoded_line: 85 | data = decoded_line.split('>>') 86 | self.call_handler('on_hyprland_event', data[0], data[1]) 87 | 88 | def launch_sway_daemon(self, event_type): 89 | logger.info('Launching Sway daemon for event type: ' + event_type) 90 | cmd = "swaymsg -m -r -t subscribe '[\"" + event_type + "\"]'" 91 | ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) 92 | while True: 93 | for line in iter(ps.stdout.readline, ""): 94 | decoded_line = line.decode("utf-8").strip() 95 | try: 96 | json_output = json.loads(decoded_line) 97 | self.call_handler('on_sway_event_' + event_type, json_output) 98 | except json.decoder.JSONDecodeError: 99 | logger.error('Sway daemon: Invalid JSON: '+ decoded_line) 100 | logger.error('Sway daemon: Exiting') 101 | return 102 | 103 | def launch_systemd_login_daemon(self): 104 | logger.info('Launching systemd daemon') 105 | cmd = "gdbus monitor --system --dest org.freedesktop.login1" 106 | ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) 107 | while True: 108 | for line in iter(ps.stdout.readline, ""): 109 | decoded_line = line.decode("utf-8").strip() 110 | res = re.match(r'(.+?): ([^\s]+?) \((.*?)\)$', decoded_line) 111 | if res: 112 | sender = res.group(1) 113 | name = res.group(2) 114 | payload = res.group(3) 115 | 116 | if 'Properties' not in name: 117 | signal_name = name.split('.')[-1] 118 | f = 'on_' + signal_name 119 | self.call_handler(f, payload) 120 | self.call_handler('on_systemd_event', sender, signal_name, payload) 121 | 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easyland 2 | 3 | **Easyland** is a Python framework to manage your wayland compositor (Hyprland, Sway) configuration by reacting to events. With **Easyland**, you can dismiss many side tools like Kanshi, hypridle, swayidle, etc. and script your environment according to your preferences. 4 | 5 | 6 | ## Available listeners 7 | 8 | - [Hyprland IPC](https://wiki.hyprland.org/IPC/) event list 9 | - [Sway IPC]( https://man.archlinux.org/man/sway-ipc.7.en) 10 | - Systemd signals 11 | - Native Wayland Idle system (ext_idle_notify_v1) 12 | 13 | The tool allows to listen for these events and to execute commands in response. 14 | 15 | ## Why this tool? 16 | 17 | Good question. 18 | 19 | Initially, I was a bit stressed by the number of tools needed with Hyprland (Kanshi & hypridle notably), and also by the number of bugs despite the awesome efforts of the developers. 20 | 21 | I wanted to have a deeper control on my system, and to be able to script it as I wanted. 22 | 23 | To give an example, my laptop screen brightness was always at 100% when I undock it, and Kanshi does not allow to add shell commands. This is only one small example of the numerous limitations I met during my setup of Hyprland. 24 | 25 | By scripting my Desktop in Python, I have more control to implement what I want. 26 | 27 | ## Installation 28 | 29 | 1. Install Easyland in a python environment 30 | 31 | ``` 32 | pip3 -i easyland 33 | ``` 34 | 35 | 2. Copy an example of configuration files from [here](https://github.com/juienpro/easyland/tree/main/config_examples) 36 | 37 | 3. Modify it according to your needs 38 | 39 | 4. Launch `easyland -c