{"id":13683125,"url":"https://github.com/kriansa/wmcompanion","last_synced_at":"2025-04-23T22:01:25.664Z","repository":{"id":50227995,"uuid":"518620037","full_name":"kriansa/wmcompanion","owner":"kriansa","description":"Desktop event listener for minimal window manager users","archived":false,"fork":false,"pushed_at":"2023-05-03T19:35:30.000Z","size":214,"stargazers_count":42,"open_issues_count":1,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-30T03:51:12.226Z","etag":null,"topics":["bspwm","dwm","herbstluftwm","i3-config","i3wm","polybar","qtile","tiling-window-manager","xmonad"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kriansa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-07-27T21:46:12.000Z","updated_at":"2025-02-08T08:17:11.000Z","dependencies_parsed_at":"2024-01-14T16:08:31.005Z","dependency_job_id":"eac77db6-b6ab-4095-89f4-1ff06c01ea8e","html_url":"https://github.com/kriansa/wmcompanion","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kriansa%2Fwmcompanion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kriansa%2Fwmcompanion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kriansa%2Fwmcompanion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kriansa%2Fwmcompanion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kriansa","download_url":"https://codeload.github.com/kriansa/wmcompanion/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250522299,"owners_count":21444511,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bspwm","dwm","herbstluftwm","i3-config","i3wm","polybar","qtile","tiling-window-manager","xmonad"],"created_at":"2024-08-02T13:02:01.241Z","updated_at":"2025-04-23T22:01:25.456Z","avatar_url":"https://github.com/kriansa.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# wmcompanion\n\n### Build your own desktop environment using Python\n\nYou use a minimalist tiling window manager, yet you want to be able to tinker with your desktop more\neasily and implement features like the ones available in full blown desktop environments?\n\nMore specifically, you want to react to system events (such as returning from sleep, or wifi signal\nchange) and easily automate your workflow or power your desktop user experience using a consistent\nand centralized configuration so it is _actually_ easy to maintain?\n\n## Show me\n\nSee below small examples of the broad idea that is `wmcompanion` and what you can achieve with small\namounts of code.\n\n- Send a desktop notification and updates a given module on Polybar whenever a certain connection\n  managed by NetworkManager changes statuses:\n  ```python\n  from wmcompanion import use, on\n  from wmcompanion.modules.polybar import Polybar\n  from wmcompanion.modules.notifications import Notify\n  from wmcompanion.events.network import NetworkConnectionStatus\n\n  @on(NetworkConnectionStatus, connection_name=\"Wired-Network\")\n  @use(Polybar)\n  @use(Notify)\n  async def network_status(status: dict, polybar: Polybar, notify: Notify):\n      color = \"blue\" if status[\"connected\"] else \"gray\"\n      await polybar(\"eth\", polybar.fmt(\"eth\", color=color))\n\n      msg = \"connected\" if status[\"connected\"] else \"disconnected\"\n      await notify(f\"Hey, wired network is {msg}\")\n  ```\n\n- Add a microphone volume level to Polybar:\n  ```python\n  from wmcompanion import use, on\n  from wmcompanion.modules.polybar import Polybar\n  from wmcompanion.events.audio import MainVolumeLevel\n\n  @on(MainVolumeLevel)\n  @use(Polybar)\n  async def volume_level(volume: dict, polybar: Polybar):\n     if not volume[\"input\"][\"available\"]:\n         return await polybar(\"mic\", \"\")\n\n     if not volume[\"muted\"]:\n         level = int(volume['level'] * 100)\n         text = f\"[mic: {level}]\"\n         color = \"blue\"\n     else:\n         text = \"[mic: muted]\"\n         color = \"gray\"\n\n     await polybar(\"mic\", polybar.fmt(text, color=color))\n  ```\n\n- Set your monitor screen arrangement on plug/unplug events:\n  ```python\n  from wmcompanion import use, on\n  from wmcompanion.modules.notifications import Notify\n  from wmcompanion.events.x11 import DeviceState\n\n  @on(DeviceState)\n  @use(Notify)\n  async def configure_screens(status: dict, notify: Notify):\n      if status[\"event\"] == DeviceState.ChangeEvent.SCREEN_CHANGE:\n        await cmd(\"autorandr\")\n        await notify(\"Screen layout adjusted!\")\n  ```\n\n- A [more complex example][polybar-example] of Polybar widgets powered by wmcompanion, in less than\n  80 lines of code:\n\n  ![image](docs/example-bar.png)\n\n## Who is this for?\n\nIt is initially built for people using tiling window managers that don't have the many of the\nfeatures that a full `DE` provides, but still want some convenience and automation here and there\nwithout having to rely on lots of unorganized shell scripts running without supervision. Things like\nbluetooth status notifications, keyboard layout visualizer, volume, network manager status and so\non.\n\nIf you already have a desktop environment such as GNOME or KDE, this tool is probably not for you,\nas most of its features are already built-in on those. However, there's absolutely nothing stopping\nyou from using it, as it is so flexible you may find it useful for other purposes (such as\nnotifications, for instance).\n\n### Design rationale\n\nYou might want to ask: isn't most of that feature set already available on a status bar such as\nPolybar, for instance? And some of them aren't just a matter of writing a simple shell script?\n\nGenerally, yes, but then you will be limited by the features of that status bar and how they are\nimplemented internally, and have a small room for customization. Ever wanted to have microphone\nvolume on Polybar? Or a `kbdd` widget? Or a built-in `dunst` pause toggle? You may be well served\nwith the default option your status bar provides, but you also might want more out of it and they\ncan not be as easily customizable or integrate well with, let's say, notifications, for instance.\n\nMoreover, `wmcompanion` isn't designed to power status bars or simply serve as a notification\ndaemon. Instead it is modeled around listening to events and reacting to them. One of these\nreactions might be to update a status bar, of course. But it can also be to send a notification, or\nperhaps change a layout, update your monitor setup, etc. The important part is that it is meant to\nbe integrated and easily scriptable in a single service, and you won't have to maintain and manually\norchestrate several scripts to make your desktop experience more pleasant.\n\n## Usage\n\n### 1. Install\n\nCurrently it's available as an OS package for [Arch Linux on AUR][aur]. On other platforms, you can\npull this repository, install `poetry` and run `poetry run wmcompanion`.\n\n### 2. Configure\n\nFirst, you need to add a config file on `~/.config/wmcompanion/config.py`. For starters, you can use\nthe one below:\n\n```python\nfrom wmcompanion import use, on\nfrom wmcompanion.modules.notifications import Notify\nfrom wmcompanion.events.audio import MainVolumeLevel\n\n@on(MainVolumeLevel)\n@use(Notify)\nasync def volume_level(volume: dict, notify: Notify):\n    await notify(f\"Your volume levels: {volume=}\")\n```\n\nTake a look at [examples][examples] if you want to get inspired, and you can get really creative by\nreading the source files under `events` folder.\n\n### 3. Run\n\nYou can simply run `wmcompanion` as it's an executable installed on your system, or use `poetry run\nwmcompanion` in case you downloaded the codebase using git.\n\nMost people already have many user daemons running as part of their `.xinit` file, and that's a\nfine place for you to run it automatically on user login.\n\nA recommendation is to keep it under a `systemd` user unit, so it's separate from your window\nmanager and you can manage logs and failures a bit better.\n\n## Available event listeners\n\nBy default, `wmcompanion` is _accompanied_ by many _`EventListeners`_ already. An `EventListener` is\nthe heart of the application. Yet, they are simple Python classes that can listen to system events\nasynchronously and notify the user configured callbacks whenever there's a change in the state.\n\nCurrently there are the following event listeners available:\n\n* Main audio input/output volume level with WirePlumber (`events.audio.MainVolumeLevel`)\n* Bluetooth status (`events.bluetooth.BluetoothRadioStatus`)\n* [Kbdd][kbdd] currently selected layout (`events.keyboard.KbddChangeLayout`)\n* NetworkManager connection status (`events.network.NetworkConnectionStatus`)\n* NetworkManager Wi-Fi status/strength (`events.network.WifiStatus`)\n* Dunst notification pause status (`events.notifications.DunstPausedStatus`)\n* Power actions (`events.power.PowerActions`)\n* Logind Idle status (`events.power.LogindIdleStatus`)\n* X11 monitor and input device changes (`events.x11.DeviceState`) **[requires python-xcffib]**\n\nThe architecture allows for developing event listeners very easily and make them reusable by others,\neven if they are not integrated in this codebase -- they just need to be classes extending\n`wmcompanion.event_listening.EventListener` and you can even include them in your dotfiles.\n\n## Built-in modules\n\nModules are built-in integrations with the most common desktop tooling so that you don't need to\nreimplement them for your configurations. All you need is to inject them at runtime and they will be\navailable to you automatically, keeping your user configuration clean.\n\nFor instance, instead of playing with `notify-send` manually, there's a builtin module that you can\ninvoke from within Python script and it will work as you would expect.\n\n* Polybar IPC _(replaces `polybar-msg action`)_ (`modules.polybar.Polybar`)\n* Notifications _(replaces `notify-send`)_ (`modules.notifications.Notify`)\n\n### Polybar IPC integration\n\nIn order to use Polybar integration, you need to create a module on Polybar using `custom/ipc` as\nthe type and then add an initial hook to it so it reads from wmcompanion's module upon\ninitialization. Here's an example below:\n\n```ini\n[module/kbdd]\ntype = custom/ipc\nhook-0 = cat $XDG_RUNTIME_DIR/polybar/kbdd 2\u003e /dev/null\ninitial = 1\n```\n\nMind you that, for that example, `kbdd` must be the first string argument that you pass when\ncalling `polybar()` on a wmcompanion callback:\n\n```python\n@use(Polybar)\nasync def my_callback(status: dict, polybar: Polybar):\n    await polybar(\"kbdd\", \"any string that will show up on polybar\")\n```\n\n### Desktop notifications\n\nWe have a full implementation of the [desktop notifications spec][desktop-notifications], and it's\nsuper easy to use:\n\n```python\n@use(Notify)\nasync def my_callback(status: dict, notify: Notify):\n    await notify(\"Summary\", \"Body\")\n```\n\nIt also provides native support for Dunst-specific behaviors, such as progress bar and colors:\n\n```python\nawait notify(\"Volume level\", dunst_progress_bar: 20)\n```\n\n\nAs always, refer to the [source code][notifications.py] if you want more details.\n\n## Development\n\nIn order to run the daemon in development mode, just run:\n\n```sh\n$ poetry run wmcompanion\n```\n\n## Acknowledgements\n\n* Main design is inspired by Vincent Bernat's [great i3-companion][i3-companion] script.\n* The `DBusClient` util was partially extracted from [qtile utils][qtile-utils].\n* The `INotify` util was partially extracted from Chris Billington's\n  [inotify_simple][inotify-simple].\n\n[i3-companion]: https://github.com/vincentbernat/i3wm-configuration/blob/master/bin/i3-companion\n[qtile-utils]: https://github.com/qtile/qtile/blob/master/libqtile/utils.py\n[inotify-simple]: https://github.com/chrisjbillington/inotify_simple/blob/master/inotify_simple.py\n[kbdd]: https://github.com/qnikst/kbdd\n[aur]: https://aur.archlinux.org/packages/wmcompanion\n[desktop-notifications]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html\n[notifications.py]: ./wmcompanion/modules/notifications.py\n[examples]: ./examples\n[polybar-example]: ./examples/polybar_icons.py\n\n## License\n\nApache V2.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkriansa%2Fwmcompanion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkriansa%2Fwmcompanion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkriansa%2Fwmcompanion/lists"}