Full Code of hyprland-community/pyprland for AI

main c5e15d52405e cached
794 files
2.6 MB
716.4k tokens
1870 symbols
1 requests
Download .txt
Showing preview only (2,862K chars total). Download the full file or copy to clipboard to get everything.
Repository: hyprland-community/pyprland
Branch: main
Commit: c5e15d52405e
Files: 794
Total size: 2.6 MB

Directory structure:
gitextract_3e73ij71/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── wiki_improvement.md
│   ├── PULL_REQUEST_TEMPLATE/
│   │   └── pull_request_template.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── aur.yml
│       ├── ci.yml
│       ├── nix-setup.yml
│       ├── nix.yml
│       ├── site.yml
│       └── uv-install.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── client/
│   ├── pypr-client.c
│   ├── pypr-client.rs
│   └── pypr-rs/
│       └── Cargo.toml
├── default.nix
├── done.rst
├── examples/
│   ├── README.md
│   └── copy_conf.sh
├── flake.nix
├── hatch_build.py
├── justfile
├── package.json
├── pyprland/
│   ├── __init__.py
│   ├── adapters/
│   │   ├── __init__.py
│   │   ├── backend.py
│   │   ├── colors.py
│   │   ├── fallback.py
│   │   ├── hyprland.py
│   │   ├── menus.py
│   │   ├── niri.py
│   │   ├── proxy.py
│   │   ├── units.py
│   │   ├── wayland.py
│   │   └── xorg.py
│   ├── aioops.py
│   ├── ansi.py
│   ├── client.py
│   ├── command.py
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── discovery.py
│   │   ├── models.py
│   │   ├── parsing.py
│   │   └── tree.py
│   ├── common.py
│   ├── completions/
│   │   ├── __init__.py
│   │   ├── discovery.py
│   │   ├── generators/
│   │   │   ├── __init__.py
│   │   │   ├── bash.py
│   │   │   ├── fish.py
│   │   │   └── zsh.py
│   │   ├── handlers.py
│   │   └── models.py
│   ├── config.py
│   ├── config_loader.py
│   ├── constants.py
│   ├── debug.py
│   ├── doc.py
│   ├── gui/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── api.py
│   │   ├── frontend/
│   │   │   ├── .gitignore
│   │   │   ├── .vscode/
│   │   │   │   └── extensions.json
│   │   │   ├── README.md
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── App.vue
│   │   │   │   ├── components/
│   │   │   │   │   ├── DictEditor.vue
│   │   │   │   │   ├── FieldInput.vue
│   │   │   │   │   └── PluginEditor.vue
│   │   │   │   ├── composables/
│   │   │   │   │   ├── useLocalCopy.js
│   │   │   │   │   └── useToggleMap.js
│   │   │   │   ├── main.js
│   │   │   │   ├── style.css
│   │   │   │   └── utils.js
│   │   │   └── vite.config.js
│   │   ├── server.py
│   │   └── static/
│   │       ├── assets/
│   │       │   ├── index-CX03GsX-.js
│   │       │   └── index-Dpu0NgRN.css
│   │       └── index.html
│   ├── help.py
│   ├── httpclient.py
│   ├── ipc.py
│   ├── ipc_paths.py
│   ├── logging_setup.py
│   ├── manager.py
│   ├── models.py
│   ├── plugins/
│   │   ├── __init__.py
│   │   ├── experimental.py
│   │   ├── expose.py
│   │   ├── fcitx5_switcher.py
│   │   ├── fetch_client_menu.py
│   │   ├── gamemode.py
│   │   ├── interface.py
│   │   ├── layout_center.py
│   │   ├── lost_windows.py
│   │   ├── magnify.py
│   │   ├── menubar.py
│   │   ├── mixins.py
│   │   ├── monitors/
│   │   │   ├── __init__.py
│   │   │   ├── commands.py
│   │   │   ├── layout.py
│   │   │   ├── resolution.py
│   │   │   └── schema.py
│   │   ├── protocols.py
│   │   ├── pyprland/
│   │   │   ├── __init__.py
│   │   │   ├── hyprland_core.py
│   │   │   ├── niri_core.py
│   │   │   └── schema.py
│   │   ├── scratchpads/
│   │   │   ├── __init__.py
│   │   │   ├── animations.py
│   │   │   ├── common.py
│   │   │   ├── events.py
│   │   │   ├── helpers.py
│   │   │   ├── lifecycle.py
│   │   │   ├── lookup.py
│   │   │   ├── objects.py
│   │   │   ├── schema.py
│   │   │   ├── transitions.py
│   │   │   └── windowruleset.py
│   │   ├── shift_monitors.py
│   │   ├── shortcuts_menu.py
│   │   ├── stash.py
│   │   ├── system_notifier.py
│   │   ├── toggle_dpms.py
│   │   ├── toggle_special.py
│   │   ├── wallpapers/
│   │   │   ├── __init__.py
│   │   │   ├── cache.py
│   │   │   ├── colorutils.py
│   │   │   ├── hyprpaper.py
│   │   │   ├── imageutils.py
│   │   │   ├── models.py
│   │   │   ├── online/
│   │   │   │   ├── __init__.py
│   │   │   │   └── backends/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── base.py
│   │   │   │       ├── bing.py
│   │   │   │       ├── picsum.py
│   │   │   │       ├── reddit.py
│   │   │   │       ├── unsplash.py
│   │   │   │       └── wallhaven.py
│   │   │   ├── palette.py
│   │   │   ├── templates.py
│   │   │   └── theme.py
│   │   └── workspaces_follow_focus.py
│   ├── process.py
│   ├── pypr_daemon.py
│   ├── quickstart/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── discovery.py
│   │   ├── generator.py
│   │   ├── helpers/
│   │   │   ├── __init__.py
│   │   │   ├── monitors.py
│   │   │   └── scratchpads.py
│   │   ├── questions.py
│   │   └── wizard.py
│   ├── state.py
│   ├── terminal.py
│   ├── utils.py
│   ├── validate_cli.py
│   ├── validation.py
│   └── version.py
├── pyproject.toml
├── sample_extension/
│   ├── README.md
│   ├── pypr_examples/
│   │   ├── __init__.py
│   │   └── focus_counter.py
│   └── pyproject.toml
├── scripts/
│   ├── backquote_as_links.py
│   ├── check_plugin_docs.py
│   ├── completions/
│   │   ├── README.md
│   │   ├── pypr.bash
│   │   └── pypr.zsh
│   ├── generate_codebase_overview.py
│   ├── generate_monitor_diagrams.py
│   ├── generate_plugin_docs.py
│   ├── get-pypr
│   ├── make_release
│   ├── plugin_metadata.toml
│   ├── pypr.py
│   ├── pypr.sh
│   ├── title
│   ├── update_get-pypr.sh
│   ├── update_version
│   └── v_whitelist.py
├── site/
│   ├── .vitepress/
│   │   ├── config.mjs
│   │   └── theme/
│   │       ├── custom.css
│   │       └── index.js
│   ├── Architecture.md
│   ├── Architecture_core.md
│   ├── Architecture_overview.md
│   ├── Commands.md
│   ├── Configuration.md
│   ├── Development.md
│   ├── Examples.md
│   ├── Getting-started.md
│   ├── InstallVirtualEnvironment.md
│   ├── Menu.md
│   ├── MultipleConfigurationFiles.md
│   ├── Nix.md
│   ├── Optimizations.md
│   ├── Plugins.md
│   ├── Troubleshooting.md
│   ├── Variables.md
│   ├── components/
│   │   ├── CommandList.vue
│   │   ├── ConfigBadges.vue
│   │   ├── ConfigTable.vue
│   │   ├── EngineDefaults.vue
│   │   ├── EngineList.vue
│   │   ├── PluginCommands.vue
│   │   ├── PluginConfig.vue
│   │   ├── PluginList.vue
│   │   ├── configHelpers.js
│   │   ├── jsonLoader.js
│   │   └── usePluginData.js
│   ├── expose.md
│   ├── fcitx5_switcher.md
│   ├── fetch_client_menu.md
│   ├── filters.md
│   ├── gamemode.md
│   ├── generated/
│   │   └── generted_files.keep_me
│   ├── index.md
│   ├── layout_center.md
│   ├── lost_windows.md
│   ├── magnify.md
│   ├── make_version.sh
│   ├── menubar.md
│   ├── monitors.md
│   ├── scratchpads.md
│   ├── scratchpads_advanced.md
│   ├── scratchpads_nonstandard.md
│   ├── shift_monitors.md
│   ├── shortcuts_menu.md
│   ├── sidebar.json
│   ├── stash.md
│   ├── system_notifier.md
│   ├── toggle_dpms.md
│   ├── toggle_special.md
│   ├── versions/
│   │   ├── 2.3.5/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.3.6,7/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.3.8/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.0/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.1+/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.6/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.7/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.5.x/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.6.2/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 3.0.0/
│   │   │   ├── Architecture.md
│   │   │   ├── Architecture_core.md
│   │   │   ├── Architecture_overview.md
│   │   │   ├── Commands.md
│   │   │   ├── Configuration.md
│   │   │   ├── Development.md
│   │   │   ├── Examples.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   ├── ConfigBadges.vue
│   │   │   │   ├── ConfigTable.vue
│   │   │   │   ├── EngineDefaults.vue
│   │   │   │   ├── PluginCommands.vue
│   │   │   │   ├── PluginConfig.vue
│   │   │   │   ├── PluginList.vue
│   │   │   │   ├── configHelpers.js
│   │   │   │   └── usePluginData.js
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── generated/
│   │   │   │   ├── expose.json
│   │   │   │   ├── fcitx5_switcher.json
│   │   │   │   ├── fetch_client_menu.json
│   │   │   │   ├── index.json
│   │   │   │   ├── layout_center.json
│   │   │   │   ├── lost_windows.json
│   │   │   │   ├── magnify.json
│   │   │   │   ├── menu.json
│   │   │   │   ├── menubar.json
│   │   │   │   ├── monitors.json
│   │   │   │   ├── pyprland.json
│   │   │   │   ├── scratchpads.json
│   │   │   │   ├── shift_monitors.json
│   │   │   │   ├── shortcuts_menu.json
│   │   │   │   ├── system_notifier.json
│   │   │   │   ├── toggle_dpms.json
│   │   │   │   ├── toggle_special.json
│   │   │   │   ├── wallpapers.json
│   │   │   │   └── workspaces_follow_focus.json
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   ├── wallpapers_online.md
│   │   │   ├── wallpapers_templates.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 3.1.1/
│   │   │   ├── Architecture.md
│   │   │   ├── Architecture_core.md
│   │   │   ├── Architecture_overview.md
│   │   │   ├── Commands.md
│   │   │   ├── Configuration.md
│   │   │   ├── Development.md
│   │   │   ├── Examples.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   ├── ConfigBadges.vue
│   │   │   │   ├── ConfigTable.vue
│   │   │   │   ├── EngineDefaults.vue
│   │   │   │   ├── EngineList.vue
│   │   │   │   ├── PluginCommands.vue
│   │   │   │   ├── PluginConfig.vue
│   │   │   │   ├── PluginList.vue
│   │   │   │   ├── configHelpers.js
│   │   │   │   ├── jsonLoader.js
│   │   │   │   └── usePluginData.js
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gamemode.md
│   │   │   ├── generated/
│   │   │   │   ├── expose.json
│   │   │   │   ├── fcitx5_switcher.json
│   │   │   │   ├── fetch_client_menu.json
│   │   │   │   ├── gamemode.json
│   │   │   │   ├── index.json
│   │   │   │   ├── layout_center.json
│   │   │   │   ├── lost_windows.json
│   │   │   │   ├── magnify.json
│   │   │   │   ├── menu.json
│   │   │   │   ├── menubar.json
│   │   │   │   ├── monitors.json
│   │   │   │   ├── pyprland.json
│   │   │   │   ├── scratchpads.json
│   │   │   │   ├── shift_monitors.json
│   │   │   │   ├── shortcuts_menu.json
│   │   │   │   ├── system_notifier.json
│   │   │   │   ├── toggle_dpms.json
│   │   │   │   ├── toggle_special.json
│   │   │   │   ├── wallpapers.json
│   │   │   │   └── workspaces_follow_focus.json
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   ├── wallpapers_online.md
│   │   │   ├── wallpapers_templates.md
│   │   │   └── workspaces_follow_focus.md
│   │   └── 3.2.1/
│   │       ├── Architecture.md
│   │       ├── Architecture_core.md
│   │       ├── Architecture_overview.md
│   │       ├── Commands.md
│   │       ├── Configuration.md
│   │       ├── Development.md
│   │       ├── Examples.md
│   │       ├── Getting-started.md
│   │       ├── InstallVirtualEnvironment.md
│   │       ├── Menu.md
│   │       ├── MultipleConfigurationFiles.md
│   │       ├── Nix.md
│   │       ├── Optimizations.md
│   │       ├── Plugins.md
│   │       ├── Troubleshooting.md
│   │       ├── Variables.md
│   │       ├── components/
│   │       │   ├── CommandList.vue
│   │       │   ├── ConfigBadges.vue
│   │       │   ├── ConfigTable.vue
│   │       │   ├── EngineDefaults.vue
│   │       │   ├── EngineList.vue
│   │       │   ├── PluginCommands.vue
│   │       │   ├── PluginConfig.vue
│   │       │   ├── PluginList.vue
│   │       │   ├── configHelpers.js
│   │       │   ├── jsonLoader.js
│   │       │   └── usePluginData.js
│   │       ├── expose.md
│   │       ├── fcitx5_switcher.md
│   │       ├── fetch_client_menu.md
│   │       ├── filters.md
│   │       ├── gamemode.md
│   │       ├── generated/
│   │       │   ├── expose.json
│   │       │   ├── fcitx5_switcher.json
│   │       │   ├── fetch_client_menu.json
│   │       │   ├── gamemode.json
│   │       │   ├── index.json
│   │       │   ├── layout_center.json
│   │       │   ├── lost_windows.json
│   │       │   ├── magnify.json
│   │       │   ├── menu.json
│   │       │   ├── menubar.json
│   │       │   ├── monitors.json
│   │       │   ├── pyprland.json
│   │       │   ├── scratchpads.json
│   │       │   ├── shift_monitors.json
│   │       │   ├── shortcuts_menu.json
│   │       │   ├── stash.json
│   │       │   ├── system_notifier.json
│   │       │   ├── toggle_dpms.json
│   │       │   ├── toggle_special.json
│   │       │   ├── wallpapers.json
│   │       │   └── workspaces_follow_focus.json
│   │       ├── index.md
│   │       ├── layout_center.md
│   │       ├── lost_windows.md
│   │       ├── magnify.md
│   │       ├── menubar.md
│   │       ├── monitors.md
│   │       ├── scratchpads.md
│   │       ├── scratchpads_advanced.md
│   │       ├── scratchpads_nonstandard.md
│   │       ├── shift_monitors.md
│   │       ├── shortcuts_menu.md
│   │       ├── sidebar.json
│   │       ├── stash.md
│   │       ├── system_notifier.md
│   │       ├── toggle_dpms.md
│   │       ├── toggle_special.md
│   │       ├── wallpapers.md
│   │       ├── wallpapers_online.md
│   │       ├── wallpapers_templates.md
│   │       └── workspaces_follow_focus.md
│   ├── wallpapers.md
│   ├── wallpapers_online.md
│   ├── wallpapers_templates.md
│   └── workspaces_follow_focus.md
├── systemd-unit/
│   └── pyprland.service
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── sample_config.toml
│   ├── test_adapters_fallback.py
│   ├── test_ansi.py
│   ├── test_command.py
│   ├── test_command_registry.py
│   ├── test_common_types.py
│   ├── test_common_utils.py
│   ├── test_completions.py
│   ├── test_config.py
│   ├── test_event_signatures.py
│   ├── test_external_plugins.py
│   ├── test_http.py
│   ├── test_interface.py
│   ├── test_ipc.py
│   ├── test_load_all.py
│   ├── test_monitors_commands.py
│   ├── test_monitors_layout.py
│   ├── test_monitors_resolution.py
│   ├── test_plugin_expose.py
│   ├── test_plugin_fetch_client_menu.py
│   ├── test_plugin_layout_center.py
│   ├── test_plugin_lost_windows.py
│   ├── test_plugin_magnify.py
│   ├── test_plugin_menubar.py
│   ├── test_plugin_monitor.py
│   ├── test_plugin_scratchpads.py
│   ├── test_plugin_shift_monitors.py
│   ├── test_plugin_shortcuts_menu.py
│   ├── test_plugin_stash.py
│   ├── test_plugin_system_notifier.py
│   ├── test_plugin_toggle_dpms.py
│   ├── test_plugin_toggle_special.py
│   ├── test_plugin_wallpapers.py
│   ├── test_plugin_workspaces_follow_focus.py
│   ├── test_process.py
│   ├── test_pyprland.py
│   ├── test_scratchpad_vulnerabilities.py
│   ├── test_string_template.py
│   ├── test_wallpapers_cache.py
│   ├── test_wallpapers_colors.py
│   ├── test_wallpapers_imageutils.py
│   ├── testtools.py
│   └── vreg/
│       ├── 01_client_id_change.py
│       └── run_tests.sh
├── tickets.rst
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Enter a short bug description here"
labels: bug
assignees: fdev31

---

**Pyprland version**
Which version did you use? (copy & paste the string returned by `pypr version`)

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Configuration (provide following files/samples when relevant):**
- pyprland.toml
- hyprland.conf

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT] Description of the feature"
labels: enhancement
assignees: fdev31

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/wiki_improvement.md
================================================
---
name: Wiki improvement
about: Suggest a fix or improvement in the documentation
title: "[WIKI] Description of the problem"
labels: documentation
assignees: fdev31

---

**Is your feature request related to a problem or an improvement? Please describe.**
A clear and concise description of what the problem is. Ex. There is a link broken in...

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
================================================
---
name: Default PR template
about: Improve the code
title: "Change description here"
---

# Description of the pull request content & goal

In order to ... I have implemented ...

# Relevant wiki content to be added/updated

## In "Wiki page XXX"

Add configuration details for ...eg:
...


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly
      time: "01:00"
    open-pull-requests-limit: 10
    reviewers:
      - NotAShelf
      - fdev31
    assignees:
      - NotAShelf
      - fdev31


================================================
FILE: .github/workflows/aur.yml
================================================
name: AUR Package Validation

on:
  workflow_dispatch:
  pull_request:
    paths:
      - "pyprland/**"
      - "pyproject.toml"
      - "uv.lock"
      - "client/**"
      - ".github/workflows/aur.yml"
  push:
    paths:
      - "pyprland/**"
      - "pyproject.toml"
      - "uv.lock"
      - "client/**"
      - ".github/workflows/aur.yml"

jobs:
  makepkg:
    runs-on: ubuntu-latest
    container:
      image: archlinux:latest
    steps:
      - name: Install base dependencies
        run: pacman -Syu --noconfirm base-devel git python python-build python-installer python-hatchling python-aiofiles python-aiohttp python-pillow gcc

      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Fetch PKGBUILD from AUR
        run: |
          mkdir -p /tmp/build
          curl -sL "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=pyprland-git" -o /tmp/build/PKGBUILD

      - name: Patch PKGBUILD to use local source
        run: |
          # Point the PKGBUILD source at the checked-out repo instead of remote
          sed -i "s|source=(git+https://github.com/fdev31/pyprland#branch=main)|source=(\"git+file://${GITHUB_WORKSPACE}\")|" /tmp/build/PKGBUILD
          # Show the patched PKGBUILD for debugging
          cat /tmp/build/PKGBUILD

      - name: Build and install with makepkg
        run: |
          # makepkg refuses to run as root, create a builder user
          useradd -m builder
          chown -R builder:builder /tmp/build
          # Allow builder to install packages via pacman
          echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
          # git needs to trust the workspace
          git config --global --add safe.directory "${GITHUB_WORKSPACE}"
          su builder -c "git config --global --add safe.directory '${GITHUB_WORKSPACE}'"
          cd /tmp/build
          su builder -c "makepkg -si --noconfirm"

      - name: Smoke test
        run: |
          which pypr
          which pypr-client
          mkdir -p ~/.config/hypr
          echo -e '[pyprland]\nplugins = []' > ~/.config/hypr/pyprland.toml
          pypr validate


================================================
FILE: .github/workflows/ci.yml
================================================
---
name: CI

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@v6
      - name: Set up uv
        uses: astral-sh/setup-uv@v7
        with:
          python-version: ${{ matrix.python-version }}
      - name: Finish environment setup
        run: |
          mkdir ~/.config/hypr/
          touch ~/.config/hypr/pyprland.toml
      - name: Install dependencies
        run: uv sync --all-groups
      - name: Run tests
        run: |
          uv run ./scripts/generate_plugin_docs.py
          uv run pytest -q tests
      - name: Check wiki docs
        if: matrix.python-version == '3.14'
        run: |
          uv run ./scripts/generate_plugin_docs.py
          uv run ./scripts/check_plugin_docs.py


================================================
FILE: .github/workflows/nix-setup.yml
================================================
# This is a re-usable workflow that is used by .github/workflows/check.yml to handle necessary setup
# before running Nix commands. E.g. this will install Nix and set up Magic Nix Cache
name: "Nix Setup"

on:
  workflow_call:
    inputs:
      command:
        required: false
        type: string
      platform:
        default: "ubuntu"
        required: false
        type: string
    secrets:
      GH_TOKEN:
        required: true

jobs:
  nix:
    runs-on: "${{ inputs.platform }}-latest"
    steps:
      - name: "Set default git branch (to reduce log spam)"
        run: git config --global init.defaultBranch main

      - name: "Checkout"
        uses: actions/checkout@v6
        with:
          token: "${{ secrets.GH_TOKEN }}"

      - name: "Set up QEMU support"
        uses: docker/setup-qemu-action@v4
        with:
          platforms: arm64

      - name: "Install nix"
        uses: cachix/install-nix-action@master
        with:
          install_url: https://nixos.org/nix/install
          extra_nix_config: |
            experimental-features = nix-command flakes fetch-tree
            allow-import-from-derivation = false
            extra-platforms = aarch64-linux

      - name: "Cachix Setup"
        uses: cachix/cachix-action@v17
        with:
          authToken: ${{ secrets.CACHIX_TOKEN }}
          name: hyprland-community

      - name: "Nix Magic Cache"
        uses: DeterminateSystems/magic-nix-cache-action@main

      - name: "Run Input: ${{ inputs.command }}"
        run: "${{ inputs.command }}"


================================================
FILE: .github/workflows/nix.yml
================================================
name: Nix Flake Validation

on:
  workflow_dispatch:
  pull_request:
    paths:
      - "pyprland/**"
      - "**.nix"
      - "**.lock"
      - ".github/workflows/check.yml"
  push:
    paths:
      - "pyprland/**"
      - "**.nix"
      - "**.lock"
      - ".github/workflows/check.yml"

jobs:
  check:
    uses: ./.github/workflows/nix-setup.yml
    secrets:
      GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
    with:
      command: nix flake check --accept-flake-config --print-build-logs

  build:
    needs: [check]
    uses: ./.github/workflows/nix-setup.yml
    strategy:
      matrix:
        package:
          - pyprland
    secrets:
      GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
    with:
      command: nix build .#${{ matrix.package }} --print-build-logs


================================================
FILE: .github/workflows/site.yml
================================================
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Website deployment

on:
  # Runs on pushes targeting the `main` branch. Change this to `master` if you're
  # using the `master` branch as the default branch.
  push:
    branches: [main]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0 # Not needed if lastUpdated is not enabled
      # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
      # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: 20
          cache: npm # or pnpm / yarn
      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.14'
      - name: Install Python dependencies
        run: pip install .
      - name: Setup Pages
        uses: actions/configure-pages@v6
      - name: Install dependencies
        run: npm ci # or pnpm install / yarn install / bun install
      - name: Generate plugin documentation JSON
        run: python scripts/generate_plugin_docs.py
      - name: Build with VitePress
        run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: site/.vitepress/dist

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v5


================================================
FILE: .github/workflows/uv-install.yml
================================================
name: uv tool install Validation

on:
  workflow_dispatch:
  pull_request:
    paths:
      - "pyprland/**"
      - "pyproject.toml"
      - "uv.lock"
      - ".github/workflows/uv-install.yml"
  push:
    paths:
      - "pyprland/**"
      - "pyproject.toml"
      - "uv.lock"
      - ".github/workflows/uv-install.yml"

jobs:
  install:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13", "3.14"]
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up uv
        uses: astral-sh/setup-uv@v7
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install with uv tool install
        run: uv tool install .

      - name: Smoke test
        run: |
          which pypr
          mkdir -p ~/.config/hypr
          echo -e '[pyprland]\nplugins = []' > ~/.config/hypr/pyprland.toml
          pypr validate


================================================
FILE: .gitignore
================================================
RELEASE_NOTES.md

# Byte-compiled / optimized / DLL files
site/.vitepress/cache/
site/.vitepress/dist/
node_modules/
__pycache__/
*.py[cod]
*$py.class

# Generated documentation JSON (regenerated at build time)
site/generated/*.json

# stale merges
*.orig

# random crap
*.out

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# Nix
result

# Local development files
OLD/

# AI assistant / editor tool files
.opencode/
CODEBASE_OVERVIEW.md
DEVELOPMENT_GUIDELINES.md

# One-time migration scripts
site/migration/


================================================
FILE: .pre-commit-config.yaml
================================================
---
repos:
  - repo: local
    hooks:
      - id: versionMgmt
        name: Increment the version number
        entry: ./scripts/update_version
        language: system
        pass_filenames: false
      - id: wikiDocGen
        name: Generate wiki docs
        entry: ./scripts/generate_plugin_docs.py
        language: system
        pass_filenames: false
      - id: wikiDocCheck
        name: Check wiki docs coverage
        entry: python scripts/check_plugin_docs.py
        language: system
        pass_filenames: false
  - repo: https://github.com/astral-sh/ruff-pre-commit
    # Ruff version.
    rev: "v0.14.14"
    hooks:
      # Run the linter.
      - id: ruff-check
      - id: ruff-format
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: "v6.0.0"
    hooks:
      - id: check-yaml
      - id: check-json
      - id: pretty-format-json
        args: ['--autofix', '--no-sort-keys']
  - repo: https://github.com/PyCQA/flake8
    rev: 7.3.0
    hooks:
      - id: flake8
        files: ^pyprland/
        args: [--max-line-length=140]
  - repo: https://github.com/lovesegfault/beautysh
    rev: "v6.4.2"
    hooks:
      - id: beautysh
  - repo: https://github.com/adrienverge/yamllint
    rev: "v1.38.0"
    hooks:
      - id: yamllint

  - repo: local
    hooks:
      - id: runtests
        name: Run pytest
        entry: uv run pytest tests
        pass_filenames: false
        language: system
        stages: [pre-push]

#        types: [python]
#        pass_filenames: false


================================================
FILE: .pylintrc
================================================
[MASTER]

# Specify a configuration file.
#rcfile=

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=


# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=.hg

# Pickle collected data for later comparisons.
persistent=yes

# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=


[MESSAGES CONTROL]

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=

# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=R0903,W0603,yield-inside-async-function,unnecessary-ellipsis


[REPORTS]

# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
# Tells whether to display a full report or only the messages
reports=yes

# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (R0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)



[BASIC]


# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$

# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log(_.*)?)$

# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$

# Regular expression which should only match correct function names
function-rgx=[a-z_][a-zA-Z0-9_]{2,30}$

# Regular expression which should only match correct method names
method-rgx=[a-z_][a-zA-Z0-9_]{2,30}$

# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$

# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$

# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{,30}$

# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$

# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_

# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata,pdb

# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__


[FORMAT]

# Maximum number of characters on a single line.
max-line-length=140

# Maximum number of lines in a module
max-module-lines=1000

# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string='    '


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO


[SIMILARITIES]

# Minimum lines number of a similarity.
min-similarity-lines=4

# Ignore comments when computing similarities.
ignore-comments=yes

# Ignore docstrings when computing similarities.
ignore-docstrings=yes


[TYPECHECK]

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes

# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject


# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed.
generated-members=REQUEST,acl_users,aq_parent


[VARIABLES]

# Tells whether we should check for unused import in __init__ files.
init-import=no

# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy

# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
#additional-builtins=
additional-builtins = _,DBG


[CLASSES]

# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp


[DESIGN]

# Maximum number of arguments for function / method
max-args=7

# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*

# Maximum number of locals for function / method body
max-locals=15

# Maximum number of return / yield for function / method body
max-returns=6

# Maximum number of statements in function / method body
max-statements=50

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of attributes for a class (see R0902).
max-attributes=10

# Minimum number of public methods for a class (see R0903).
min-public-methods=2

# Maximum number of public methods for a class (see R0904).
max-public-methods=24


[IMPORTS]

# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec

# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=

# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=

# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
fdev31@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


================================================
FILE: CONTRIBUTING.md
================================================
- ensure everything works when you run `tox`
- use `ruff format` to format the code
- provide documentation to be added to the wiki when relevant
- provide some *tests* when possible
- ensure you read https://github.com/hyprland-community/pyprland/wiki/Development


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Fabien Devaux

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:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

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.


================================================
FILE: README.md
================================================
![rect](https://github.com/hyprland-community/pyprland/assets/238622/3fab93b6-6445-4e7b-b757-035095b5c8e8)

[![Hyprland](https://img.shields.io/badge/Made%20for-Hyprland-blue)](https://github.com/hyprwm/Hyprland)
[![Discord](https://img.shields.io/discord/1055990214411169892?label=discord)](https://discord.com/channels/1458202721294356522/1458202722892386519)

[![Documentation](https://img.shields.io/badge/Documentation-Read%20Now-brightgreen?style=for-the-badge)](https://hyprland-community.github.io/pyprland)

[Discussions](https://github.com/hyprland-community/pyprland/discussions) • [Plugins](https://hyprland-community.github.io/pyprland/Plugins.html) • [Dotfiles](https://github.com/fdev31/dotfiles) • [Changes History](https://github.com/hyprland-community/pyprland/releases) • [Share](https://github.com/hyprland-community/pyprland/discussions/46)

## Power up your desktop

A plugin system that extends your graphical environment with features like scratchpads, dynamic popup nested menus, custom notifications, easy monitor settings and more.

Think of it as a *Gnome tweak tool* for Hyprland, with options that can run on any desktop. With a fully plugin-based architecture, it's lightweight and easy to customize.

Contributions, suggestions, bug reports and comments are welcome.

<details>
<summary>
About Pyprland (latest stable is: <b>3.3.1</b>)
</summary>

[![Packaging Status](https://repology.org/badge/vertical-allrepos/pyprland.svg)](https://repology.org/project/pyprland/versions)

🎉 Hear what others are saying:

- [Elsa in Mac](https://elsainmac.tistory.com/915) some tutorial article for fedora in Korean with a nice short demo video
- [Archlinux Hyprland dotfiles](https://github.com/DinDotDout/.dotfiles/blob/main/conf-hyprland/.config/hypr/pyprland.toml) + [video](https://www.youtube.com/watch?v=jHuzcjf-FGM)
- ["It just works very very well" - The Linux Cast (video)](https://youtu.be/Cjn0SFyyucY?si=hGb0TM9IDvlbcD6A&t=131) - February 2024
- [You NEED This in your Hyprland Config - LibrePhoenix (video)](https://www.youtube.com/watch?v=CwGlm-rpok4) - October 2023 (*Now [TOML](https://toml.io/en/) format is preferred over [JSON](https://www.w3schools.com/js/js_json_intro.asp))

</details>

<details>

<summary>
Contributing
</summary>

Check out the [creating a pull request](https://docs.github.com/fr/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) document for guidance.

- Report bugs or propose features [here](https://github.com/hyprland-community/pyprland/issues)
- Improve our [wiki](https://hyprland-community.github.io/pyprland/)
- Read the [internal ticket list](https://github.com/hyprland-community/pyprland/blob/main/tickets.rst) for some PR ideas

and if you have coding skills you can also

- Enhance the coverage of our [tests](https://github.com/hyprland-community/pyprland/tree/main/tests)
- Propose & write new plugins or enhancements

</details>

<details>
<summary>
Dependencies
</summary>

- **Python** >= 3.11
    - **aiofiles** (optional but recommended)
    - **pillow** (optional, required for rounded borders in `wallpapers`)
</details>

<details>
<summary>
Latest major changes
</summary>

Check the [Releases change log](https://github.com/hyprland-community/pyprland/releases) for more information

### 3.0.0

- Dynamic shell completions
- Better error handling and configuration validation
- Removed hard dependency on Hyprland
- General polish including a couple ofbreaking changes
  - remove old or broken options
  - fixes

### 2.5

- wallpapers plugin refactored, supports rounded corners and pause
- fcitx5 switcher plugin (appeared in late 2.4)

### 2.4

- Scratchpads are now pinned by default (set `pinned = false` for the old behavior)
- Version >=2.4.4 is required for Hyprland 0.48.0
- A snappier `pypr-client` command is available, meant to be used in the keyboard bindings (NOT to start pypr on startup!), eg:
```sh
$pypr = uwsm-app -- pypr-client
bind = $mainMod SHIFT, Z, exec, $pypr zoom ++0.5
 ```

### 2.3

- Supports *Hyprland > 0.40.0*
- Improved code kwaleetee
- [monitors](https://hyprland-community.github.io/pyprland/monitors) allows general monitor settings
- [scratchpads](https://hyprland-community.github.io/pyprland/scratchpads)
  - better multi-window support
  - better `preserve_aspect` implementation (i3 "compatibility")

### 2.2

- Added [wallpapers](https://hyprland-community.github.io/pyprland/wallpapers) and [system_notifier](https://hyprland-community.github.io/pyprland/system_notifier) plugins.
- Deprecated [class_match](https://hyprland-community.github.io/pyprland/scratchpads_nonstandard) in [scratchpads](https://hyprland-community.github.io/pyprland/scratchpads)
- Added [gbar](https://hyprland-community.github.io/pyprland/gbar) in 2.2.6
- [scratchpads](https://hyprland-community.github.io/pyprland/scratchpads) supports multiple client windows (using 2.2.19 is recommended)
- [monitors](https://hyprland-community.github.io/pyprland/monitors) and [scratchpads](https://hyprland-community.github.io/pyprland/scratchpads) supports rotation in 2.2.13
- Improve [Nix support](https://hyprland-community.github.io/pyprland/Nix)

### 2.1

- Requires Hyprland >= 0.37
- [Monitors](https://hyprland-community.github.io/pyprland/monitors) plugin improvements.

### 2.0

- New dependency: [aiofiles](https://pypi.org/project/aiofiles/)
- Added [hysteresis](https://hyprland-community.github.io/pyprland/scratchpads#hysteresis-optional) support for [scratchpads](https://hyprland-community.github.io/pyprland/scratchpads).

### 1.10

- New [fetch_client_menu](https://hyprland-community.github.io/pyprland/fetch_client_menu) and [shortcuts_menu](https://hyprland-community.github.io/pyprland/shortcuts_menu) plugins.

### 1.9

- Introduced [shortcuts_menu](https://hyprland-community.github.io/pyprland/shortcuts_menu) plugin.

### 1.8

- Requires Hyprland >= 0.30
- Added [layout_center](https://hyprland-community.github.io/pyprland/layout_center) plugin.

</details>

<a href="https://star-history.com/#fdev31/pyprland&Date">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=fdev31/pyprland&type=Timeline&theme=dark" />
    <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=fdev31/pyprland&type=Timeline" />
    <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=fdev31/pyprland&type=Timeline" />
  </picture>
</a>


================================================
FILE: client/pypr-client.c
================================================
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <libgen.h>

// Exit codes matching pyprland/models.py ExitCode
#define EXIT_SUCCESS_CODE 0
#define EXIT_USAGE_ERROR 1
#define EXIT_ENV_ERROR 2
#define EXIT_CONNECTION_ERROR 3
#define EXIT_COMMAND_ERROR 4

// Response prefixes
#define RESPONSE_OK "OK"
#define RESPONSE_ERROR "ERROR"

int main(int argc, char *argv[]) {
    // If no argument passed, show usage
    if (argc < 2) {
        fprintf(stderr, "No command provided.\n");
        fprintf(stderr, "Usage: pypr <command> [args...]\n");
        fprintf(stderr, "Try 'pypr help' for available commands.\n");
        exit(EXIT_USAGE_ERROR);
    }

    // Get environment variables for socket path detection
    const char *runtimeDir = getenv("XDG_RUNTIME_DIR");
    const char *signature = getenv("HYPRLAND_INSTANCE_SIGNATURE");
    const char *niriSocket = getenv("NIRI_SOCKET");
    const char *dataHome = getenv("XDG_DATA_HOME");
    const char *home = getenv("HOME");

    // Construct the socket path based on environment priority: Hyprland > Niri > Standalone
    char socketPath[256];
    int pathLen;

    if (signature != NULL && runtimeDir != NULL) {
        // Hyprland environment
        pathLen = snprintf(socketPath, sizeof(socketPath), "%s/hypr/%s/.pyprland.sock", runtimeDir, signature);
    } else if (niriSocket != NULL) {
        // Niri environment - use dirname of NIRI_SOCKET
        char *niriSocketCopy = strdup(niriSocket);
        if (niriSocketCopy == NULL) {
            fprintf(stderr, "Error: Memory allocation failed.\n");
            exit(EXIT_ENV_ERROR);
        }
        char *niriDir = dirname(niriSocketCopy);
        pathLen = snprintf(socketPath, sizeof(socketPath), "%s/.pyprland.sock", niriDir);
        free(niriSocketCopy);
    } else {
        // Standalone fallback - use XDG_DATA_HOME or ~/.local/share
        if (dataHome != NULL) {
            pathLen = snprintf(socketPath, sizeof(socketPath), "%s/.pyprland.sock", dataHome);
        } else if (home != NULL) {
            pathLen = snprintf(socketPath, sizeof(socketPath), "%s/.local/share/.pyprland.sock", home);
        } else {
            fprintf(stderr, "Error: Cannot determine socket path. HOME not set.\n");
            exit(EXIT_ENV_ERROR);
        }
    }

    if (pathLen >= (int)sizeof(socketPath)) {
        fprintf(stderr, "Error: Socket path too long (max %zu characters).\n", sizeof(socketPath) - 1);
        exit(EXIT_ENV_ERROR);
    }

    // Connect to the Unix socket
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0) {
        fprintf(stderr, "Error: Failed to create socket.\n");
        exit(EXIT_CONNECTION_ERROR);
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, socketPath, sizeof(addr.sun_path) - 1);

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        fprintf(stderr, "Cannot connect to pyprland daemon at %s.\n", socketPath);
        fprintf(stderr, "Is the daemon running? Start it with: pypr (no arguments)\n");
        close(sockfd);
        exit(EXIT_CONNECTION_ERROR);
    }

    // Concatenate all command-line arguments with spaces, plus newline
    char message[1024] = {0};
    int offset = 0;
    for (int i = 1; i < argc; i++) {
        int remaining = sizeof(message) - offset - 2; // Reserve space for \n and \0
        if (remaining <= 0) {
            fprintf(stderr, "Error: Command too long (max %zu characters).\n", sizeof(message) - 2);
            close(sockfd);
            exit(EXIT_USAGE_ERROR);
        }
        int written = snprintf(message + offset, remaining + 1, "%s", argv[i]);
        if (written > remaining) {
            fprintf(stderr, "Error: Command too long (max %zu characters).\n", sizeof(message) - 2);
            close(sockfd);
            exit(EXIT_USAGE_ERROR);
        }
        offset += written;
        if (i < argc - 1) {
            if (offset < (int)sizeof(message) - 2) {
                message[offset++] = ' ';
            }
        }
    }
    // Add newline for protocol
    message[offset++] = '\n';
    message[offset] = '\0';

    // Send the message to the socket
    if (write(sockfd, message, strlen(message)) < 0) {
        fprintf(stderr, "Error: Failed to send command to daemon.\n");
        close(sockfd);
        exit(EXIT_CONNECTION_ERROR);
    }

    // send EOF to indicate end of message
    if (shutdown(sockfd, SHUT_WR) < 0) {
        fprintf(stderr, "Error: Failed to complete command transmission.\n");
        close(sockfd);
        exit(EXIT_CONNECTION_ERROR);
    }

    // Read the response from the socket until EOF
    char buffer[4096];
    char response[65536] = {0};
    size_t totalRead = 0;
    ssize_t bytesRead;

    while ((bytesRead = read(sockfd, buffer, sizeof(buffer) - 1)) > 0) {
        if (totalRead + bytesRead >= sizeof(response) - 1) {
            // Response too large, just print what we have
            buffer[bytesRead] = '\0';
            printf("%s", buffer);
        } else {
            memcpy(response + totalRead, buffer, bytesRead);
            totalRead += bytesRead;
        }
    }
    response[totalRead] = '\0';

    close(sockfd);

    // Parse response and determine exit code
    int exitCode = EXIT_SUCCESS_CODE;

    if (strncmp(response, RESPONSE_ERROR ":", strlen(RESPONSE_ERROR ":")) == 0) {
        // Error response - extract message after "ERROR: "
        const char *errorMsg = response + strlen(RESPONSE_ERROR ": ");
        // Trim trailing whitespace
        size_t len = strlen(errorMsg);
        while (len > 0 && (errorMsg[len-1] == '\n' || errorMsg[len-1] == ' ')) {
            len--;
        }
        fprintf(stderr, "Error: %.*s\n", (int)len, errorMsg);
        exitCode = EXIT_COMMAND_ERROR;
    } else if (strncmp(response, RESPONSE_OK, strlen(RESPONSE_OK)) == 0) {
        // OK response - check for additional output after "OK"
        const char *remaining = response + strlen(RESPONSE_OK);
        // Skip whitespace/newlines
        while (*remaining == ' ' || *remaining == '\n') {
            remaining++;
        }
        if (*remaining != '\0') {
            printf("%s", remaining);
        }
        exitCode = EXIT_SUCCESS_CODE;
    } else if (totalRead > 0) {
        // Legacy response (version, help, dumpjson) - print as-is
        // Trim trailing newlines for cleaner output
        while (totalRead > 0 && response[totalRead-1] == '\n') {
            totalRead--;
        }
        response[totalRead] = '\0';
        if (totalRead > 0) {
            printf("%s\n", response);
        }
        exitCode = EXIT_SUCCESS_CODE;
    }

    return exitCode;
}


================================================
FILE: client/pypr-client.rs
================================================
use std::env;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::process::exit;

// Exit codes matching pyprland/models.py ExitCode
const EXIT_SUCCESS: i32 = 0;
const EXIT_USAGE_ERROR: i32 = 1;
const EXIT_ENV_ERROR: i32 = 2;
const EXIT_CONNECTION_ERROR: i32 = 3;
const EXIT_COMMAND_ERROR: i32 = 4;

fn run() -> Result<(), i32> {
    // Collect arguments (skip program name)
    let args: Vec<String> = env::args().skip(1).collect();

    if args.is_empty() {
        eprintln!("No command provided.");
        eprintln!("Usage: pypr <command> [args...]");
        eprintln!("Try 'pypr help' for available commands.");
        return Err(EXIT_USAGE_ERROR);
    }

    // Build command message
    let message = format!("{}\n", args.join(" "));

    if message.len() > 1024 {
        eprintln!("Error: Command too long (max 1022 characters).");
        return Err(EXIT_USAGE_ERROR);
    }

    // Get socket path from environment
    let runtime_dir = env::var("XDG_RUNTIME_DIR").map_err(|_| {
        eprintln!("Environment error: XDG_RUNTIME_DIR or HYPRLAND_INSTANCE_SIGNATURE not set.");
        eprintln!("Are you running under Hyprland?");
        EXIT_ENV_ERROR
    })?;

    let signature = env::var("HYPRLAND_INSTANCE_SIGNATURE").map_err(|_| {
        eprintln!("Environment error: XDG_RUNTIME_DIR or HYPRLAND_INSTANCE_SIGNATURE not set.");
        eprintln!("Are you running under Hyprland?");
        EXIT_ENV_ERROR
    })?;

    let socket_path = format!("{}/hypr/{}/.pyprland.sock", runtime_dir, signature);

    if socket_path.len() >= 256 {
        eprintln!("Error: Socket path too long (max 255 characters).");
        return Err(EXIT_ENV_ERROR);
    }

    // Connect to Unix socket
    let mut stream = UnixStream::connect(&socket_path).map_err(|_| {
        eprintln!("Cannot connect to pyprland daemon at {}.", socket_path);
        eprintln!("Is the daemon running? Start it with: pypr (no arguments)");
        EXIT_CONNECTION_ERROR
    })?;

    // Send command
    stream.write_all(message.as_bytes()).map_err(|_| {
        eprintln!("Error: Failed to send command to daemon.");
        EXIT_CONNECTION_ERROR
    })?;

    // Signal end of message
    stream.shutdown(std::net::Shutdown::Write).map_err(|_| {
        eprintln!("Error: Failed to complete command transmission.");
        EXIT_CONNECTION_ERROR
    })?;

    // Read response
    let mut response = String::new();
    stream.read_to_string(&mut response).map_err(|_| {
        eprintln!("Error: Failed to read response from daemon.");
        EXIT_CONNECTION_ERROR
    })?;

    // Parse response and determine exit code
    if let Some(error_msg) = response.strip_prefix("ERROR: ") {
        eprintln!("Error: {}", error_msg.trim_end());
        Err(EXIT_COMMAND_ERROR)
    } else if let Some(rest) = response.strip_prefix("OK") {
        // Print any content after "OK" (skip leading whitespace/newlines)
        let output = rest.trim_start();
        if !output.is_empty() {
            print!("{}", output);
        }
        Ok(())
    } else if !response.is_empty() {
        // Legacy response (version, help, dumpjson) - print as-is
        println!("{}", response.trim_end_matches('\n'));
        Ok(())
    } else {
        Ok(())
    }
}

fn main() {
    exit(run().err().unwrap_or(EXIT_SUCCESS));
}


================================================
FILE: client/pypr-rs/Cargo.toml
================================================
[package]
name = "pypr-client"
version = "0.1.0"
edition = "2021"

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true


================================================
FILE: default.nix
================================================
(
  import
  (
    let
      lock = builtins.fromJSON (builtins.readFile ./flake.lock);
      nodeName = lock.nodes.root.inputs.flake-compat;
    in
      fetchTarball {
        url = lock.nodes.${nodeName}.locked.url or "https://github.com/hyprland-community/pyprland/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
        sha256 = lock.nodes.${nodeName}.locked.narHash;
      }
  )
  {src = ./.;}
)


================================================
FILE: done.rst
================================================
scratchpads: attach / detach only attaches !
============================================

:bugid: 56
:created: 2026-1-11T22:10:17
:fixed: 2026-01-19T22:03:48
:priority: 0

--------------------------------------------------------------------------------

Hyprpaper integration
=====================

:bugid: 56
:created: 2025-12-16T21:08:03
:fixed: 2025-12-16T21:44:08
:priority: 0

Use the socket $XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.hyprpaper.sock directly if no command is provided in "wallpapers"

--------------------------------------------------------------------------------

Wiki: remove optional and add mandatory (to the titles for the configuration options)
=====================================================================================

:bugid: 52
:created: 2024-07-08T21:51:28
:fixed: 2025-08-07T21:27:38
:priority: 0

--------------------------------------------------------------------------------

improve groups support
======================

:bugid: 37
:created: 2024-04-15T00:27:52
:fixed: 2024-07-08T21:54:09
:priority: 0

Instead of making it in "layout_center" by lack of choice, refactor:

- make run_command return a code compatible with shell (0 = success, < 0 = error)
- by default it returns 0

else: Add it to "layout_center" overriding prev & next

if grouped, toggle over groups, when at the limit, really changes the focus

Option: think about a "chaining" in handlers, (eg: "pypr groups prev OR layout_center prev") in case of a separate plugin called "groups"

--------------------------------------------------------------------------------

Add a fallback to aiofiles (removes one dependency)
===================================================

:bugid: 49
:created: 2024-06-02T01:41:33
:fixed: 2024-07-08T21:51:40
:priority: 0

--------------------------------------------------------------------------------

monitors: allow "screen descr".transform = 3
============================================

:bugid: 48
:created: 2024-05-07T00:50:34
:fixed: 2024-05-23T20:56:53
:priority: 0

also allow `.scale = <something>`

--------------------------------------------------------------------------------

review CanceledError handling
=============================

:bugid: 38
:created: 2024-04-17T23:24:13
:fixed: 2024-05-16T20:38:37
:priority: 0
:timer: 20

--------------------------------------------------------------------------------

Add "satellite" scratchpads
===========================

:bugid: 36
:created: 2024-04-08T23:42:26
:fixed: 2024-05-16T20:38:18
:priority: 0

- add a "scratch" command that sets the focused window into the currently focused scratchpad window

Eg: open a terminal, hover it + "scratch" it while a scratchpad is open.
Behind the hood, it creates attached "ghost scratchpads" for each attached window. They use "perserve_aspect" by default.

**Alternative**

Move focused client into the named scratchpad's special workspace.
Rework pyprland's scratchpad to keep track of every window added to the special workspace and attach it to the last used scratch then hide it if the scratchpad is hidden.
If called on a scratchpad window, will "de-attach" this window.

Every attached window should be synchronized with the main one.


**Option**

Prepare / Simplify this dev by adding support for "ScratchGroups" (contains multiple Scratches which are synchronized).
Would generalize the current feature: passing multiple scratches to the toggle command.

--------------------------------------------------------------------------------

offset & margin: support % and px units
=======================================

:bugid: 33
:created: 2024-03-08T00:07:02
:fixed: 2024-05-16T20:38:09
:priority: 0

--------------------------------------------------------------------------------

scratchpads: experiment handling manual scratchpad workspace change
===================================================================

:bugid: 47
:created: 2024-05-01T23:38:51
:fixed: 2024-05-16T20:37:54
:priority: 0

--------------------------------------------------------------------------------

Check behavior of monitors when no match is found
=================================================

:bugid: 42
:created: 2024-04-26T00:26:22
:fixed: 2024-05-16T20:37:32
:priority: 0

Should ignore applying any rule

--------------------------------------------------------------------------------

CHECK / fix multi-monitor & attach command
==========================================

:bugid: 40
:created: 2024-04-23T22:01:39
:fixed: 2024-05-16T20:36:40
:priority: 0

--------------------------------------------------------------------------------

Review smart_focus when toggling on a special workspace
=======================================================

:bugid: 43
:created: 2024-04-27T18:25:47
:fixed: 2024-05-16T20:36:26
:priority: 0
:timer: 20

--------------------------------------------------------------------------------

Re-introduce focus tracking with a twist
========================================

:bugid: 41
:created: 2024-04-25T23:54:53
:fixed: 2024-05-01T23:42:00
:priority: 0

Only enable it if the focuse changed the active workspace

--------------------------------------------------------------------------------

TESTS: ensure commands are completed (push the proper events in the queue)
==========================================================================

:bugid: 27
:created: 2024-02-29T23:30:02
:fixed: 2024-05-01T23:40:05
:priority: 0

--------------------------------------------------------------------------------

Add a command to update config
==============================

:bugid: 22
:created: 2024-02-18T17:53:17
:fixed: 2024-05-01T23:39:56
:priority: 0

cfg_set and cfg_toggle commands
eg::

  pypr cfg_toggle scratchpads.term.unfocus (toggles will toggle strings to "" and back - keeping a memory)

--------------------------------------------------------------------------------

Rework focus
============

:bugid: 45
:created: 2024-04-29T00:01:27
:fixed: 2024-05-01T23:39:44
:priority: 0


Save workspace before hide,
when hide is done, after processing some events (use a task), focus the workspace again

--------------------------------------------------------------------------------

AUR: add zsh completion file
============================

:bugid: 44
:created: 2024-04-27T23:54:28
:fixed: 2024-05-01T23:38:57
:priority: 0

--------------------------------------------------------------------------------

2.1 ?
=====

:bugid: 35
:created: 2024-03-08T00:22:35
:fixed: 2024-04-09T21:28:26
:priority: 0

- lazy = true
- positions in % and px (defaults to px if no unit is provided)
- #34 done
- #33 done
- VISUAL REGRESSION TESTS

--------------------------------------------------------------------------------

Make an "system_notifier" plugin
================================

:bugid: 21
:created: 2024-02-16T00:16:11
:fixed: 2024-04-08T19:58:46
:priority: 0

Reads journalctl -fxn and notifies some errors,
user can use some patterns to match additional errors
and create their own notifications

> Started, works but better examples are needed


.. code:: toml

    [system_notifier]
    builtin_rules = true

    [[system_notifier.source]]
    name = "kernel"
    source = "sudo journalctl -fkn"
    duration = 10
    rules = [
        {match="xxx", filter=["s/foobar//", "s/meow/plop/g"], message="bad"},
        {contains="xxx", filter=["s/foobar//", "s/meow/plop/g"], message="xxx happened [orig] [filtered]"},
    ]

    [[system_notifier.source]]
    name = "user journal"
    source = "journalctl -fxn --user"
    rules = [
        {match="Consumed \d+.?\d*s CPU time", filter="s/.*: //", message="[filtered]"},
        {match="xxx", filter=["s/foobar//", "s/meow/plop/g"], message="bad"},
        {contains="xxx", filter=["s/foobar//", "s/meow/plop/g"], message="xxx happened [orig] [filtered]"},
    ]

    [[system_notifier.source]]
    name = "user journal"
    source = "sudo journalctl -fxn"
    rules = [
        {match="systemd-networkd\[\d+\]: ([a-z0-9]+): Link (DOWN|UP)", filter="s/.*: ([a-z0-9]+): Link (DOWN|UP)/\1 \2/", message="[filtered]"}
        {match="wireplumber[1831]: Failure in Bluetooth audio transport "}
        {match="usb 7-1: Product: USB2.0 Hub", message="detected"}
        {match="févr. 02 17:30:24 gamix systemd-coredump[11872]: [🡕] Process 11801 (tracker-extract) of user 1000 dumped core."}
    ]

    [[system_notifier.source]]
    name = "Hyprland"
    source = "/tmp/pypr.log"
    duration = 10
    rules = [
        {message="[orig]"},
    ]

    [[system_notifier.source]]
    name = "networkd"

--------------------------------------------------------------------------------

preserve_aspect to manage multi-screen setups
=============================================

:bugid: 30
:created: 2024-03-04T22:21:41
:fixed: 2024-04-08T19:58:23
:priority: 0

--------------------------------------------------------------------------------

offset computation (hide anim) rework
=====================================

:bugid: 34
:created: 2024-03-08T00:11:31
:fixed: 2024-03-08T21:35:57
:priority: 0

use animation type + margin to do a reverse computation of the placement (out of screen)

--------------------------------------------------------------------------------

set preserve_aspect=true by default
===================================

:bugid: 32
:created: 2024-03-06T23:28:41
:fixed: 2024-03-06T23:29:33
:priority: 0

also add a command "scratch reset <uid>" to set active scratch position and size according to the rules.
Can support ommitting the <uid>, requires tracking of the currently active scratch (or just check focused window)

--------------------------------------------------------------------------------

preserve_aspect should adapt to screen changes
==============================================

:bugid: 29
:created: 2024-03-03T01:56:28
:fixed: 2024-03-06T23:29:32
:priority: 0

--------------------------------------------------------------------------------

BUG: preserve_aspect + offset = KO
==================================

:bugid: 31
:created: 2024-03-05T00:22:34
:fixed: 2024-03-06T23:29:21
:priority: 0
:tags: #bug

tested on "term"

--------------------------------------------------------------------------------

scratchpad: per monitor overrides
=================================

:bugid: 9
:created: 2023-12-02T21:53:48
:fixed: 2024-03-02T15:30:25
:priority: 10

--------------------------------------------------------------------------------

Check for types (schema?) in the config
=======================================

:bugid: 24
:created: 2024-02-21T00:50:34
:fixed: 2024-03-02T15:30:15
:priority: 0

notify an error in case type isn't matching

--------------------------------------------------------------------------------

Make "replace links" script
===========================

:bugid: 23
:created: 2024-02-20T00:15:31
:fixed: 2024-03-02T15:29:57
:priority: 0

Reads a file (like RELEASE NOTES) and replace `links` with something in the wiki
uses difflib to make the job

--------------------------------------------------------------------------------

Hide / Show ALL command
=======================

:bugid: 10
:created: 2023-12-26T21:48:36
:fixed: 2024-02-29T23:30:14
:priority: 10

hide all command for scratchpad

--------------------------------------------------------------------------------

Make a get_bool() util function
===============================

:bugid: 25
:created: 2024-02-21T23:34:39
:fixed: 2024-02-29T23:30:12
:priority: 10


Should detect "no", "False", etc.. (strings) as being false

makes a notification warning, that it has been automatically fixed to `False`

--------------------------------------------------------------------------------

Add "exit" command that exits cleanly (& removing the socket)
=============================================================

:bugid: 20
:created: 2024-02-15T19:29:48
:fixed: 2024-02-29T22:38:15
:priority: 0

--------------------------------------------------------------------------------

scratchpads: autofloat=True
===========================

:bugid: 26
:created: 2024-02-28T19:40:02
:fixed: 2024-02-29T22:37:52
:priority: 0


Allows to disable the automatic float toggle when the scratch is opened


================================================
FILE: examples/README.md
================================================
# Contribute your configuration

[Dotfiles](https://github.com/fdev31/dotfiles)
[![Discord](https://img.shields.io/discord/1055990214411169892?label=discord)](https://discord.com/channels/1458202721294356522/1458202722892386519)

Make a [pull request](https://github.com/hyprland-community/pyprland/compare) with your files or a link to your dotfiles, you can use `copy_conf.sh` to get a starting point.



================================================
FILE: examples/copy_conf.sh
================================================
#!/bin/sh
if [ -z "$1" ]; then
    echo -n "config name: "
    read name
else
    name=$1
fi
[ -d $name/hypr/ ] || mkdir $name/hypr/
FILENAMES=("hyprland.conf" "pyprland.toml")
for fname in ${FILENAMES[@]}; do
    install -T ~/.config/hypr/$fname $name/hypr/$fname
done

# recursively install the ~/config/hypr/pyprland.d folder into $name/hypr/pyprland.d
# cp -r ~/.config/hypr/pyprland.d $name/hypr/

# for fname in "config" "style.scss" ; do
#     install -T ~/.config/gBar/$fname $name/hypr/gBar/$fname
# done


================================================
FILE: flake.nix
================================================
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";

    # <https://github.com/pyproject-nix/pyproject.nix>
    pyproject-nix = {
      url = "github:nix-community/pyproject.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # <https://github.com/nix-systems/nix-systems>
    systems.url = "github:nix-systems/default-linux";

    # <https://github.com/edolstra/flake-compat>
    flake-compat = {
      url = "github:edolstra/flake-compat";
      flake = false;
    };
  };

  outputs =
    {
      self,
      nixpkgs,
      pyproject-nix,
      systems,
      ...
    }:
    let
      eachSystem = nixpkgs.lib.genAttrs (import systems);
      pkgsFor = eachSystem (system: nixpkgs.legacyPackages.${system});
      project = pyproject-nix.lib.project.loadPyproject {
        projectRoot = ./.;
      };
    in
    {
      packages = eachSystem (
        system:
        let
          pkgs = pkgsFor.${system};
          python = pkgs.python3;

          attrs = project.renderers.buildPythonPackage { inherit python; };
        in
        {
          default = self.packages.${system}.pyprland;
          pyprland = python.pkgs.buildPythonPackage (
            attrs
            // {
              nativeBuildInputs = (attrs.nativeBuildInputs or [ ]) ++ [
                python.pkgs.hatchling
                pkgs.stdenv.cc
              ];
              postInstall = ''
                $CC -O2 -o $out/bin/pypr-client $src/client/pypr-client.c
              '';
            }
          );
        }
      );

      devShells = eachSystem (
        system:
        let
          pkgs = pkgsFor.${system};
          python = pkgs.python3;

          getDependencies = project.renderers.withPackages { inherit python; };
          pythonWithPackages = python.withPackages getDependencies;
        in
        {
          default = self.devShells.${system}.pyprland;
          pyprland = pkgs.mkShell {
            packages = [
              pythonWithPackages
              pkgs.uv
            ];

          };
        }
      );
    };

  nixConfig = {
    extra-substituters = [ "https://hyprland-community.cachix.org" ];
    extra-trusted-public-keys = [
      "hyprland-community.cachix.org-1:5dTHY+TjAJjnQs23X+vwMQG4va7j+zmvkTKoYuSXnmE="
    ];
  };
}


================================================
FILE: hatch_build.py
================================================
"""Custom hatch build hook to compile the optional C client.

When the PYPRLAND_BUILD_NATIVE environment variable is set to "1", compiles
client/pypr-client.c into a statically-linked native binary and includes it
in a platform-specific wheel tagged for manylinux_2_17_x86_64.

Without the env var the hook does nothing and the wheel stays pure-python.
"""

import os
import shutil
import subprocess
import tempfile
from pathlib import Path

from hatchling.builders.hooks.plugin.interface import BuildHookInterface

# The manylinux tag to use for the platform-specific wheel.
# 2.17 corresponds to glibc 2.17 (CentOS 7) — effectively all modern Linux.
# With static linking the binary has *no* glibc dependency, so this is safe.
MANYLINUX_TAG = "cp3-none-manylinux_2_17_x86_64"


def _find_compiler() -> str:
    """Find a C compiler from CC env var or common names.

    Returns:
        The compiler command string, or empty string if none found.
    """
    cc = os.environ.get("CC", "")
    if cc:
        return cc
    for candidate in ("cc", "gcc", "clang"):
        if shutil.which(candidate):
            return candidate
    return ""


def _try_compile(cc: str, source: Path, *, static: bool = False) -> tuple[Path | None, str]:
    """Attempt to compile the C client.

    Args:
        cc: C compiler command.
        source: Path to the C source file.
        static: Whether to produce a statically-linked binary.

    Returns:
        Tuple of (output_path or None, warning message if failed).
    """
    tmpdir = Path(tempfile.mkdtemp(prefix="pypr-build-"))
    output = tmpdir / "pypr-client"
    cmd = [cc, "-O2"]
    if static:
        cmd.append("-static")
    cmd.extend(["-o", str(output), str(source)])

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, check=False)  # noqa: S603
    except FileNotFoundError:
        return None, f"C compiler '{cc}' not found. Skipping native client build."
    except subprocess.TimeoutExpired:
        return None, "C client compilation timed out. Skipping native client build."

    if result.returncode != 0:
        return None, (
            f"C client compilation failed (exit {result.returncode}). Skipping native client build.\nstderr: {result.stderr.strip()}"
        )

    if not output.exists():
        return None, "Compiled binary not found after build. Skipping native client."

    output.chmod(0o755)  # noqa: S103
    return output, ""


class NativeClientBuildHook(BuildHookInterface):
    """Build hook that compiles the native C client."""

    PLUGIN_NAME = "native-client"

    def initialize(self, version: str, build_data: dict) -> None:  # noqa: ARG002
        """Compile the C client and include it in the wheel if successful.

        Only runs when PYPRLAND_BUILD_NATIVE=1 is set and the build target
        is a wheel.  The resulting wheel is tagged as manylinux so it can be
        uploaded to PyPI.
        """
        if self.target_name != "wheel":
            return

        if os.environ.get("PYPRLAND_BUILD_NATIVE") != "1":
            return

        source = Path(self.root) / "client" / "pypr-client.c"
        if not source.exists():
            self.app.display_warning("C client source not found, skipping native client build")
            return

        cc = _find_compiler()
        if not cc:
            self.app.display_warning("No C compiler found (set CC env var or install gcc/clang). Skipping native pypr-client build.")
            return

        self.app.display_info(f"Compiling native client with {cc} (static)")
        output, warning = _try_compile(cc, source, static=True)

        if output is None:
            self.app.display_warning(warning)
            return

        self.app.display_success("Native pypr-client compiled successfully")

        # Use shared_scripts so hatchling generates the correct
        # {name}-{version}.data/scripts/ path in the wheel (PEP 427).
        build_data["shared_scripts"][str(output)] = "pypr-client"

        # Mark the wheel as platform-specific with an explicit manylinux tag.
        build_data["pure_python"] = False
        build_data["tag"] = MANYLINUX_TAG


================================================
FILE: justfile
================================================
# Run tests quickly
quicktest:
    uv run pytest -q tests

# Run pytest with optional parameters
debug *params='tests':
    uv run pytest --pdb -s {{params}}

# Start the documentation website in dev mode
website: gendoc
    npm i
    npm run docs:dev

# Run linting and dead code detection
lint:
    uv run mypy --install-types --non-interactive --check-untyped-defs pyprland
    uv run ruff format pyprland
    uv run ruff check --fix pyprland
    uv run pylint -E pyprland
    uv run flake8 pyprland
    uv run vulture --ignore-names 'event_*,run_*,fromtop,frombottom,fromleft,fromright,instance' pyprland scripts/v_whitelist.py

# Run version registry checks
vreg:
    uv run --group vreg ./tests/vreg/run_tests.sh

# Build documentation
doc:
    uv run pdoc --docformat google ./pyprland

# Generate wiki pages
wiki:
    ./scripts/generate_plugin_docs.py
    ./scripts/check_plugin_docs.py

# Generate plugin documentation from source
gendoc:
    python scripts/generate_plugin_docs.py

# Generate codebase overview from module docstrings
overview:
    python scripts/generate_codebase_overview.py

# Archive documentation for a specific version (creates static snapshot)
archive-docs version:
    cd site && ./make_version.sh {{version}}

# Create a new release
release:
    uv lock --upgrade
    git add uv.lock
    ./scripts/make_release

# Generate and open HTML coverage report
htmlcov:
    uv run coverage run --source=pyprland -m pytest tests -q
    uv run coverage html
    uv run coverage report
    xdg-open ./htmlcov/index.html

# Run mypy type checks on pyprland
types:
    uv run mypy --check-untyped-defs pyprland

# Build C client - release (~17K)
compile-c-client:
    gcc -O2 -o client/pypr-client client/pypr-client.c

# Build C client - debug with symbols
compile-c-client-debug:
    gcc -g -O0 -o client/pypr-client client/pypr-client.c

# Build Rust client via Cargo - release with LTO (~312K)
compile-rust-client:
    cargo build --release --manifest-path client/pypr-rs/Cargo.toml
    cp client/pypr-rs/target/release/pypr-client client/pypr-client

# Build Rust client via Cargo - debug
compile-rust-client-debug:
    cargo build --manifest-path client/pypr-rs/Cargo.toml
    cp client/pypr-rs/target/debug/pypr-client client/pypr-client

# Build Rust client via rustc - release (~375K)
compile-rust-client-simple:
    rustc -C opt-level=3 -C strip=symbols client/pypr-client.rs -o client/pypr-client

# Build Rust client via rustc - debug
compile-rust-client-simple-debug:
    rustc client/pypr-client.rs -o client/pypr-client

# Build GUI frontend static assets
gui-build:
    cd pyprland/gui/frontend && npm install && npm run build

# Start GUI frontend dev server (hot-reload) + backend API server
gui-dev:
    cd pyprland/gui/frontend && npm install && npm run dev &
    uv run pypr-gui --port 8099 --no-browser

# Launch the GUI (production mode — serves pre-built frontend)
gui *args='':
    uv run pypr-gui {{args}}


================================================
FILE: package.json
================================================
{
  "scripts": {
    "docs:dev": "vitepress dev site",
    "docs:build": "vitepress build site",
    "docs:preview": "vitepress preview site"
  },
  "devDependencies": {
    "mermaid": "^11.12.2",
    "vitepress": "^1.6.4",
    "vitepress-plugin-mermaid": "^2.0.17"
  },
  "dependencies": {
    "markdown-it": "^14.1.0"
  }
}


================================================
FILE: pyprland/__init__.py
================================================
"""Pyprland - a companion application for Hyprland and other Wayland compositors.

Provides a plugin-based architecture for extending window manager functionality
with features like scratchpads, workspace management, monitor control, and more.
The daemon runs as an asyncio service, communicating via Unix sockets.
"""


================================================
FILE: pyprland/adapters/__init__.py
================================================
"""Backend adapters for compositor abstraction.

This package provides the EnvironmentBackend abstraction layer that allows
Pyprland to work with multiple compositors (Hyprland, Niri) and fallback
environments (generic Wayland via wlr-randr, X11 via xrandr).
"""

from .proxy import BackendProxy

__all__ = ["BackendProxy"]


================================================
FILE: pyprland/adapters/backend.py
================================================
"""Abstract base class defining the compositor backend interface.

EnvironmentBackend defines the contract for all compositor backends:
- Window operations (get_clients, focus, move, resize, close)
- Monitor queries (get_monitors, get_monitor_props)
- Command execution (execute, execute_batch, execute_json)
- Notifications (notify, notify_info, notify_error)
- Event parsing (parse_event)

All methods accept a 'log' parameter for traceability via BackendProxy.
"""

from abc import ABC, abstractmethod
from collections.abc import Callable
from logging import Logger
from typing import Any

from ..common import MINIMUM_ADDR_LEN, SharedState
from ..models import ClientInfo, MonitorInfo


class EnvironmentBackend(ABC):
    """Abstract base class for environment backends (Hyprland, Niri, etc).

    All methods that perform logging require a `log` parameter to be passed.
    This allows the calling code (via BackendProxy) to inject the appropriate
    logger for traceability.
    """

    def __init__(self, state: SharedState) -> None:
        """Initialize the backend.

        Args:
            state: Shared state object
        """
        self.state = state

    @abstractmethod
    async def get_clients(
        self,
        mapped: bool = True,
        workspace: str | None = None,
        workspace_bl: str | None = None,
        *,
        log: Logger,
    ) -> list[ClientInfo]:
        """Return the list of clients, optionally filtered.

        Args:
            mapped: If True, only return mapped clients
            workspace: Filter to this workspace name
            workspace_bl: Blacklist this workspace name
            log: Logger to use for this operation
        """

    @abstractmethod
    async def get_monitors(self, *, log: Logger, include_disabled: bool = False) -> list[MonitorInfo]:
        """Return the list of monitors.

        Args:
            log: Logger to use for this operation
            include_disabled: If True, include disabled monitors (Hyprland only)
        """

    @abstractmethod
    def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any] | None:
        """Parse a raw event string into (event_name, event_data).

        Args:
            raw_data: Raw event string from the compositor
            log: Logger to use for this operation
        """

    async def get_monitor_props(
        self,
        name: str | None = None,
        include_disabled: bool = False,
        *,
        log: Logger,
    ) -> MonitorInfo:
        """Return focused monitor data if `name` is not defined, else use monitor's name.

        Args:
            name: Monitor name to look for, or None for focused monitor
            include_disabled: If True, include disabled monitors in search
            log: Logger to use for this operation
        """
        monitors = await self.get_monitors(log=log, include_disabled=include_disabled)
        if name:
            for mon in monitors:
                if mon["name"] == name:
                    return mon
        else:
            for mon in monitors:
                if mon.get("focused"):
                    return mon
        msg = "no focused monitor"
        raise RuntimeError(msg)

    @abstractmethod
    async def execute(self, command: str | list | dict, *, log: Logger, **kwargs: Any) -> bool:
        """Execute a command (or list of commands).

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments (base_command, weak, etc.)
        """

    @abstractmethod
    async def execute_json(self, command: str, *, log: Logger, **kwargs: Any) -> Any:
        """Execute a command and return the JSON result.

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments
        """

    @abstractmethod
    async def execute_batch(self, commands: list[str], *, log: Logger) -> None:
        """Execute a batch of commands.

        Args:
            commands: List of commands to execute
            log: Logger to use for this operation
        """

    @abstractmethod
    async def notify(self, message: str, duration: int, color: str, *, log: Logger) -> None:
        """Send a notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            color: Hex color code
            log: Logger to use for this operation
        """

    async def notify_info(self, message: str, duration: int = 5000, *, log: Logger) -> None:
        """Send an info notification (default: blue color).

        Args:
            message: The notification message
            duration: Duration in milliseconds
            log: Logger to use for this operation
        """
        await self.notify(message, duration, "0000ff", log=log)

    async def notify_error(self, message: str, duration: int = 5000, *, log: Logger) -> None:
        """Send an error notification (default: red color).

        Args:
            message: The notification message
            duration: Duration in milliseconds
            log: Logger to use for this operation
        """
        await self.notify(message, duration, "ff0000", log=log)

    async def get_client_props(
        self,
        match_fn: Callable[[Any, Any], bool] | None = None,
        clients: list[ClientInfo] | None = None,
        *,
        log: Logger,
        **kw: Any,
    ) -> ClientInfo | None:
        """Return the properties of a client matching the given criteria.

        Args:
            match_fn: Custom match function (defaults to equality)
            clients: Optional pre-fetched client list
            log: Logger to use for this operation
            **kw: Property to match (addr, cls, etc.)
        """
        if match_fn is None:

            def default_match_fn(value1: Any, value2: Any) -> bool:
                return bool(value1 == value2)

            match_fn = default_match_fn

        assert kw

        addr = kw.get("addr")
        klass = kw.get("cls")

        if addr:
            assert len(addr) > MINIMUM_ADDR_LEN, "Client address is invalid"
            prop_name = "address"
            prop_value = addr
        elif klass:
            prop_name = "class"
            prop_value = klass
        else:
            prop_name, prop_value = next(iter(kw.items()))

        clients_list = clients or await self.get_clients(mapped=False, log=log)

        for client in clients_list:
            assert isinstance(client, dict)
            val = client.get(prop_name)
            if match_fn(val, prop_value):
                return client
        return None

    # ─── Window Operation Helpers ─────────────────────────────────────────────

    async def focus_window(self, address: str, *, log: Logger) -> bool:
        """Focus a window by address.

        Args:
            address: Window address (without 'address:' prefix)
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"focuswindow address:{address}", log=log)

    async def move_window_to_workspace(
        self,
        address: str,
        workspace: str,
        *,
        silent: bool = True,
        log: Logger,
    ) -> bool:
        """Move a window to a workspace.

        Args:
            address: Window address (without 'address:' prefix)
            workspace: Target workspace name or ID
            silent: If True, don't follow the window (default: True)
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        cmd = "movetoworkspacesilent" if silent else "movetoworkspace"
        return await self.execute(f"{cmd} {workspace},address:{address}", log=log)

    async def pin_window(self, address: str, *, log: Logger) -> bool:
        """Toggle pin state of a window.

        Args:
            address: Window address (without 'address:' prefix)
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"pin address:{address}", log=log)

    async def close_window(self, address: str, *, silent: bool = True, log: Logger) -> bool:
        """Close a window.

        Args:
            address: Window address (without 'address:' prefix)
            silent: Accepted for API consistency (currently unused for close)
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"closewindow address:{address}", log=log)

    async def resize_window(self, address: str, width: int, height: int, *, log: Logger) -> bool:
        """Resize a window to exact pixel dimensions.

        Args:
            address: Window address (without 'address:' prefix)
            width: Target width in pixels
            height: Target height in pixels
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"resizewindowpixel exact {width} {height},address:{address}", log=log)

    async def move_window(self, address: str, x: int, y: int, *, log: Logger) -> bool:  # pylint: disable=invalid-name
        """Move a window to exact pixel position.

        Args:
            address: Window address (without 'address:' prefix)
            x: Target x position in pixels
            y: Target y position in pixels
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"movewindowpixel exact {x} {y},address:{address}", log=log)

    async def toggle_floating(self, address: str, *, log: Logger) -> bool:
        """Toggle floating state of a window.

        Args:
            address: Window address (without 'address:' prefix)
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(f"togglefloating address:{address}", log=log)

    async def set_keyword(self, keyword_command: str, *, log: Logger) -> bool:
        """Execute a keyword/config command.

        Args:
            keyword_command: The keyword command string (e.g., "general:gaps_out 10")
            log: Logger to use for this operation

        Returns:
            True if command succeeded
        """
        return await self.execute(keyword_command, log=log, base_command="keyword")


================================================
FILE: pyprland/adapters/colors.py
================================================
"""Color conversion & misc color related helpers."""


def convert_color(description: str) -> str:
    """Get a color description and returns the 6 HEX digits as string.

    Args:
        description: Color description (e.g. "#FF0000" or "rgb(255, 0, 0)")
    """
    if description[0] == "#":
        return description[1:]
    if description.startswith("rgb("):
        return "".join([f"{int(i):02x}" for i in description[4:-1].split(", ")])
    return description


================================================
FILE: pyprland/adapters/fallback.py
================================================
"""Fallback backend base class for limited functionality environments."""

import asyncio
from abc import abstractmethod
from collections.abc import Callable
from logging import Logger
from typing import Any

from ..constants import DEFAULT_NOTIFICATION_DURATION_MS, DEFAULT_REFRESH_RATE_HZ
from ..models import ClientInfo, MonitorInfo
from .backend import EnvironmentBackend


def make_monitor_info(  # noqa: PLR0913  # pylint: disable=too-many-arguments,too-many-positional-arguments
    index: int,
    name: str,
    width: int,
    height: int,
    pos_x: int = 0,
    pos_y: int = 0,
    scale: float = 1.0,
    transform: int = 0,
    refresh_rate: float = DEFAULT_REFRESH_RATE_HZ,
    enabled: bool = True,
    description: str = "",
) -> MonitorInfo:
    """Create a MonitorInfo dict with default values for fallback backends.

    Args:
        index: Monitor index
        name: Monitor name (e.g., "DP-1")
        width: Monitor width in pixels
        height: Monitor height in pixels
        pos_x: X position
        pos_y: Y position
        scale: Scale factor
        transform: Transform value (0-7)
        refresh_rate: Refresh rate in Hz
        enabled: Whether the monitor is enabled
        description: Monitor description

    Returns:
        MonitorInfo dict with all required fields
    """
    return MonitorInfo(
        id=index,
        name=name,
        description=description or name,
        make="",
        model="",
        serial="",
        width=width,
        height=height,
        refreshRate=refresh_rate,
        x=pos_x,
        y=pos_y,
        activeWorkspace={"id": 0, "name": ""},
        specialWorkspace={"id": 0, "name": ""},
        reserved=[0, 0, 0, 0],
        scale=scale,
        transform=transform,
        focused=index == 0,
        dpmsStatus=enabled,
        vrr=False,
        activelyTearing=False,
        disabled=not enabled,
        currentFormat="",
        availableModes=[],
        to_disable=False,
    )


class FallbackBackend(EnvironmentBackend):
    """Base class for fallback backends (X11, generic Wayland).

    Provides minimal functionality - only get_monitors() is implemented
    by subclasses. Other methods are stubs that log warnings or no-op.

    These backends provide monitor information for plugins like wallpapers
    but do not support compositor-specific features like window management
    or event handling.
    """

    async def get_clients(
        self,
        mapped: bool = True,
        workspace: str | None = None,
        workspace_bl: str | None = None,
        *,
        log: Logger,
    ) -> list[ClientInfo]:
        """Not supported in fallback mode.

        Args:
            mapped: Ignored
            workspace: Ignored
            workspace_bl: Ignored
            log: Logger to use for this operation

        Returns:
            Empty list
        """
        log.debug("get_clients() not supported in fallback backend")
        return []

    def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any] | None:
        """No event support in fallback mode.

        Args:
            raw_data: Ignored
            log: Logger to use for this operation

        Returns:
            None (no events)
        """
        return None

    async def execute(self, command: str | list | dict, *, log: Logger, **kwargs: Any) -> bool:
        """Not supported in fallback mode.

        Args:
            command: Ignored
            log: Logger to use for this operation
            **kwargs: Ignored

        Returns:
            False (command not executed)
        """
        log.debug("execute() not supported in fallback backend")
        return False

    async def execute_batch(self, commands: list[str], *, log: Logger) -> None:
        """Not supported in fallback mode.

        Args:
            commands: Ignored
            log: Logger to use for this operation
        """
        log.debug("execute_batch() not supported in fallback backend")

    async def execute_json(self, command: str, *, log: Logger, **kwargs: Any) -> Any:
        """Not supported in fallback mode.

        Args:
            command: Ignored
            log: Logger to use for this operation
            **kwargs: Ignored

        Returns:
            Empty dict
        """
        log.debug("execute_json() not supported in fallback backend")
        return {}

    async def notify(
        self,
        message: str,
        duration: int = DEFAULT_NOTIFICATION_DURATION_MS,
        color: str = "ff0000",
        *,
        log: Logger,
    ) -> None:
        """Send notification via notify-send.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            color: Ignored (notify-send doesn't support colors)
            log: Logger to use for this operation
        """
        log.info("Notification: %s", message)
        try:
            # Convert duration from ms to ms (notify-send uses ms)
            proc = await asyncio.create_subprocess_shell(
                f'notify-send -t {duration} "Pyprland" "{message}"',
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.DEVNULL,
            )
            await proc.wait()
        except OSError as e:
            log.debug("notify-send failed: %s", e)

    @classmethod
    @abstractmethod
    async def is_available(cls) -> bool:
        """Check if this backend's required tool is available.

        Subclasses must implement this to check for their required
        tool (e.g., xrandr, wlr-randr).

        Returns:
            True if the backend can be used
        """

    @classmethod
    async def _check_command(cls, command: str) -> bool:
        """Check if a command is available and works.

        Args:
            command: The command to test

        Returns:
            True if command executed successfully
        """
        try:
            proc = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.DEVNULL,
            )
            return await proc.wait() == 0
        except OSError:
            return False

    async def _run_monitor_command(
        self,
        command: str,
        tool_name: str,
        parser: Callable[[str, bool, Logger], list[MonitorInfo]],
        *,
        include_disabled: bool,
        log: Logger,
    ) -> list[MonitorInfo]:
        """Run a command and parse its output for monitor information.

        This is a shared helper for wayland/xorg backends to reduce duplication.

        Args:
            command: Shell command to execute
            tool_name: Name of the tool for error messages
            parser: Function to parse the command output
            include_disabled: Whether to include disabled monitors
            log: Logger instance

        Returns:
            List of MonitorInfo dicts, empty on failure
        """
        try:
            proc = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await proc.communicate()

            if proc.returncode != 0:
                log.error("%s failed: %s", tool_name, stderr.decode())
                return []

            return parser(stdout.decode(), include_disabled, log)

        except OSError as e:
            log.warning("Failed to get monitors from %s: %s", tool_name, e)
            return []


================================================
FILE: pyprland/adapters/hyprland.py
================================================
"""Hyprland compositor backend implementation.

Primary backend for Hyprland, using its Unix socket IPC protocol.
Provides full functionality including batched commands, JSON queries,
native notifications, and Hyprland-specific event parsing.
"""

from logging import Logger
from typing import Any, cast

from ..constants import DEFAULT_NOTIFICATION_DURATION_MS
from ..ipc import get_response, hyprctl_connection, retry_on_reset
from ..models import ClientInfo, MonitorInfo
from .backend import EnvironmentBackend


class HyprlandBackend(EnvironmentBackend):
    """Hyprland backend implementation."""

    def _format_command(self, command_list: list[str] | list[list[str]], default_base_command: str) -> list[str]:
        """Format a list of commands to be sent to Hyprland."""
        result = []
        for command in command_list:
            if isinstance(command, str):
                result.append(f"{default_base_command} {command}")
            else:
                result.append(f"{command[1]} {command[0]}")
        return result

    @retry_on_reset
    async def execute(self, command: str | list | dict, *, log: Logger, **kwargs: Any) -> bool:
        """Execute a command (or list of commands).

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments (base_command, weak, etc.)
        """
        base_command = kwargs.get("base_command", "dispatch")
        weak = kwargs.get("weak", False)

        if not command:
            log.warning("%s triggered without a command!", base_command)
            return False
        log.debug("%s %s", base_command, command)

        async with hyprctl_connection(log) as (ctl_reader, ctl_writer):
            if isinstance(command, list):
                nb_cmds = len(command)
                ctl_writer.write(f"[[BATCH]] {' ; '.join(self._format_command(command, base_command))}".encode())
            else:
                nb_cmds = 1
                ctl_writer.write(f"/{base_command} {command}".encode())
            await ctl_writer.drain()
            resp = await ctl_reader.read(100)

        # remove "\n" from the response
        resp = b"".join(resp.split(b"\n"))

        r: bool = resp == b"ok" * nb_cmds
        if not r:
            if weak:
                log.warning("FAILED %s", resp)
            else:
                log.error("FAILED %s", resp)
        return r

    @retry_on_reset
    async def execute_json(self, command: str, *, log: Logger, **kwargs: Any) -> Any:
        """Execute a command and return the JSON result.

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments
        """
        ret = await get_response(f"-j/{command}".encode(), log)
        assert isinstance(ret, list | dict)
        return ret

    async def get_clients(
        self,
        mapped: bool = True,
        workspace: str | None = None,
        workspace_bl: str | None = None,
        *,
        log: Logger,
    ) -> list[ClientInfo]:
        """Return the list of clients, optionally filtered.

        Args:
            mapped: If True, only return mapped clients
            workspace: Filter to this workspace name
            workspace_bl: Blacklist this workspace name
            log: Logger to use for this operation
        """
        return [
            client
            for client in cast("list[ClientInfo]", await self.execute_json("clients", log=log))
            if (not mapped or client["mapped"])
            and (workspace is None or client["workspace"]["name"] == workspace)
            and (workspace_bl is None or client["workspace"]["name"] != workspace_bl)
        ]

    async def get_monitors(self, *, log: Logger, include_disabled: bool = False) -> list[MonitorInfo]:
        """Return the list of monitors.

        Args:
            log: Logger to use for this operation
            include_disabled: If True, include disabled monitors
        """
        cmd = "monitors all" if include_disabled else "monitors"
        return cast("list[MonitorInfo]", await self.execute_json(cmd, log=log))

    async def execute_batch(self, commands: list[str], *, log: Logger) -> None:
        """Execute a batch of commands.

        Args:
            commands: List of commands to execute
            log: Logger to use for this operation
        """
        if not commands:
            return

        log.debug("Batch %s", commands)

        # Format commands for batch execution
        # Based on ipc.py _format_command implementation
        formatted_cmds = [f"dispatch {command}" for command in commands]

        async with hyprctl_connection(log) as (_, ctl_writer):
            ctl_writer.write(f"[[BATCH]] {' ; '.join(formatted_cmds)}".encode())
            await ctl_writer.drain()
            # We assume it worked, similar to current implementation
            # detailed error checking for batch is limited in current ipc.py implementation

    def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any] | None:
        """Parse a raw event string into (event_name, event_data).

        Args:
            raw_data: Raw event string from the compositor
            log: Logger to use for this operation (unused in Hyprland - simple parsing)
        """
        if ">>" not in raw_data:
            return None
        cmd, params = raw_data.split(">>", 1)
        return f"event_{cmd}", params.rstrip("\n")

    async def notify(self, message: str, duration: int = DEFAULT_NOTIFICATION_DURATION_MS, color: str = "ff1010", *, log: Logger) -> None:
        """Send a notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            color: Hex color code
            log: Logger to use for this operation
        """
        # Using icon -1 for default/generic
        await self._notify_impl(message, duration, color, -1, log=log)

    async def notify_info(self, message: str, duration: int = DEFAULT_NOTIFICATION_DURATION_MS, *, log: Logger) -> None:
        """Send an info notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            log: Logger to use for this operation
        """
        # Using icon 1 for info
        await self._notify_impl(message, duration, "1010ff", 1, log=log)

    async def notify_error(self, message: str, duration: int = DEFAULT_NOTIFICATION_DURATION_MS, *, log: Logger) -> None:
        """Send an error notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            log: Logger to use for this operation
        """
        # Using icon 0 for error
        await self._notify_impl(message, duration, "ff1010", 0, log=log)

    async def _notify_impl(self, text: str, duration: int, color: str, icon: int, *, log: Logger) -> None:
        """Internal notify implementation.

        Args:
            text: The notification text
            duration: Duration in milliseconds
            color: Hex color code
            icon: Icon code (-1 default, 0 error, 1 info)
            log: Logger to use for this operation
        """
        # This mirrors ipc.notify logic for Hyprland
        await self.execute(f"{icon} {duration} rgb({color})  {text}", log=log, base_command="notify")


================================================
FILE: pyprland/adapters/menus.py
================================================
"""Menu engine adapter."""

import asyncio
import subprocess
from collections.abc import Iterable
from logging import Logger
from typing import TYPE_CHECKING, ClassVar

from ..common import apply_variables, get_logger
from ..models import PyprError, ReloadReason
from ..validation import ConfigField, ConfigItems

if TYPE_CHECKING:
    from ..config import Configuration

__all__ = ["MenuEngine", "MenuMixin"]

menu_logger = get_logger("menus adapter")


class MenuEngine:
    """Menu backend interface."""

    proc_name: str
    " process name for this engine "
    proc_extra_parameters: str = ""
    " process parameters to use for this engine "
    proc_detect_parameters: ClassVar[list[str]] = ["--help"]
    " process parameters used to check if the engine can run "

    def __init__(self, extra_parameters: str) -> None:
        """Initialize the engine with extra parameters.

        Args:
            extra_parameters: extra parameters to pass to the program
        """
        if extra_parameters:
            self.proc_extra_parameters = extra_parameters

    @classmethod
    def is_available(cls) -> bool:
        """Check engine availability."""
        try:
            subprocess.call([cls.proc_name, *cls.proc_detect_parameters], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except FileNotFoundError:
            return False
        return True

    async def run(self, choices: Iterable[str], prompt: str = "") -> str:
        """Run the engine and get the response for the proposed `choices`.

        Args:
            choices: options to chose from
            prompt: prompt replacement variable (passed in `apply_variables`)

        Returns:
            The choice which have been selected by the user, or an empty string
        """
        menu_text = "\n".join(choices)
        if not menu_text.strip():
            return ""
        command = apply_variables(
            f"{self.proc_name} {self.proc_extra_parameters}",
            {"prompt": f"{prompt}:  "} if prompt else {"prompt": ""},
        )
        menu_logger.debug(command)
        proc = await asyncio.create_subprocess_shell(
            command,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
        )
        assert proc.stdin
        assert proc.stdout

        proc.stdin.write(menu_text.encode())
        # flush program execution
        await proc.stdin.drain()
        proc.stdin.close()
        await proc.wait()

        return (await proc.stdout.read()).decode().strip()


def _menu(proc: str, params: str) -> type[MenuEngine]:
    """Create a menu engine class.

    Args:
        proc: process name for this engine
        params: default parameters to pass to the process

    Returns:
        A MenuEngine subclass configured for the specified menu program
    """
    return type(
        f"{proc.title()}Menu",
        (MenuEngine,),
        {"proc_name": proc, "proc_extra_parameters": params, "__doc__": f"A {proc} based menu."},
    )


TofiMenu = _menu("tofi", "--prompt-text '[prompt]'")
RofiMenu = _menu("rofi", "-dmenu -i -p '[prompt]'")
WofiMenu = _menu("wofi", "-dmenu -i -p '[prompt]'")
DmenuMenu = _menu("dmenu", "-i")
BemenuMenu = _menu("bemenu", "-c")
FuzzelMenu = _menu("fuzzel", "--match-mode=fuzzy -d -p '[prompt]'")
WalkerMenu = _menu("walker", "-d -k -p '[prompt]'")
AnyrunMenu = _menu("anyrun", "--plugins libstdin.so --show-results-immediately true")
VicinaeMenu = _menu("vicinae", "dmenu --no-quick-look")

every_menu_engine = [FuzzelMenu, TofiMenu, RofiMenu, WofiMenu, BemenuMenu, DmenuMenu, AnyrunMenu, WalkerMenu, VicinaeMenu]

MENU_ENGINE_CHOICES: list[str] = [engine.proc_name for engine in every_menu_engine]
"""List of available menu engine names, derived from every_menu_engine."""


async def init(force_engine: str | None = None, extra_parameters: str = "") -> MenuEngine:
    """Initialize the module.

    Args:
        force_engine: Name of the engine to force use of
        extra_parameters: Extra parameters to pass to the engine
    """
    try:
        engines = [next(e for e in every_menu_engine if e.proc_name == force_engine)] if force_engine else every_menu_engine
    except StopIteration:
        engines = []

    if force_engine and engines:
        return engines[0](extra_parameters)

    # detect engine
    for engine in engines:
        if engine.is_available():
            return engine(extra_parameters)

    # fallback if not found but forced
    if force_engine:
        # Attempt to use the user-supplied command
        me = MenuEngine(extra_parameters)
        me.proc_name = force_engine
        return me

    msg = "No engine found"
    raise PyprError(msg)


class MenuMixin:
    """An extension mixin supporting 'engine' and 'parameters' config options to show a menu."""

    menu_config_schema = ConfigItems(
        ConfigField(
            "engine",
            str,
            description="Menu engine to use",
            choices=MENU_ENGINE_CHOICES,
            category="menu",
        ),
        ConfigField(
            "parameters",
            str,
            description="Extra parameters for the menu engine command",
            category="menu",
        ),
    )
    """Schema for menu configuration fields. Plugins using MenuMixin should include this in their config_schema."""

    _menu_configured = False
    menu: MenuEngine
    """ provided `MenuEngine` """
    config: "Configuration"
    " used by the mixin but provided by `pyprland.plugins.interface.Plugin` "
    log: Logger

    " used by the mixin but provided by `pyprland.plugins.interface.Plugin` "

    async def ensure_menu_configured(self) -> None:
        """If not configured, init the menu system."""
        if not self._menu_configured:
            self.menu = await init(self.config.get_str("engine") or None, self.config.get_str("parameters"))
            self.log.info("Using %s engine", self.menu.proc_name)
            self._menu_configured = True

    async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) -> None:
        """Reset the configuration status."""
        _ = reason  # unused
        self._menu_configured = False


================================================
FILE: pyprland/adapters/niri.py
================================================
"""Niri compositor backend implementation.

Backend for Niri compositor using its JSON-based IPC protocol.
Maps Niri's window/output data structures to Hyprland-compatible formats.
Some operations (pin, resize, move) are unavailable due to Niri's tiling nature.
"""

import json
from logging import Logger
from typing import Any, cast

from ..common import notify_send
from ..constants import DEFAULT_NOTIFICATION_DURATION_MS, DEFAULT_REFRESH_RATE_HZ
from ..ipc import niri_request
from ..models import ClientInfo, MonitorInfo
from .backend import EnvironmentBackend

# Niri transform string to Hyprland-compatible integer mapping
# Keys are lowercase for case-insensitive lookup
NIRI_TRANSFORM_MAP: dict[str, int] = {
    "normal": 0,
    "90": 1,
    "180": 2,
    "270": 3,
    "flipped": 4,
    "flipped90": 5,
    "flipped-90": 5,
    "flipped180": 6,
    "flipped-180": 6,
    "flipped270": 7,
    "flipped-270": 7,
}


def get_niri_transform(value: str, default: int = 0) -> int:
    """Get transform integer from Niri transform string (case-insensitive).

    Args:
        value: Transform string like "Normal", "90", "Flipped-90", etc.
        default: Value to return if not found

    Returns:
        Integer transform value (0-7)
    """
    return NIRI_TRANSFORM_MAP.get(value.lower(), default)


def niri_output_to_monitor_info(name: str, data: dict[str, Any]) -> MonitorInfo:
    """Convert Niri output data to MonitorInfo.

    Handles both Niri output formats:
    - Format A: Uses "logical" object with nested x, y, scale, transform
    - Format B: Uses "logical_position", "logical_size", "scale" at root level

    Args:
        name: Output name (e.g., "DP-1")
        data: Niri output data dictionary

    Returns:
        MonitorInfo TypedDict with normalized fields
    """
    # Try format A first (more detailed - has modes, logical object)
    logical = data.get("logical") or {}
    mode: dict[str, Any] = next((m for m in data.get("modes", []) if m.get("is_active")), {})

    # Fall back to format B for position/size if format A fields missing
    x = logical.get("x") if logical else data.get("logical_position", {}).get("x", 0)
    y = logical.get("y") if logical else data.get("logical_position", {}).get("y", 0)
    scale = logical.get("scale") if logical else data.get("scale", 1.0)

    # Width/height: prefer active mode, fall back to logical_size
    width = mode.get("width") if mode else data.get("logical_size", {}).get("width", 0)
    height = mode.get("height") if mode else data.get("logical_size", {}).get("height", 0)

    # Refresh rate from mode (in mHz), default to 60Hz
    refresh_rate = mode.get("refresh_rate", DEFAULT_REFRESH_RATE_HZ * 1000) / 1000.0 if mode else DEFAULT_REFRESH_RATE_HZ

    # Transform from logical object
    transform_str = logical.get("transform", "Normal") if logical else "Normal"

    # Build description from make/model if available
    make = data.get("make", "")
    model = data.get("model", "")
    description = f"{make} {model}".strip() if make or model else ""

    return cast(
        "MonitorInfo",
        {
            "name": name,
            "description": description,
            "make": make,
            "model": model,
            "serial": data.get("serial", ""),
            "width": width,
            "height": height,
            "refreshRate": refresh_rate,
            "x": x if x is not None else 0,
            "y": y if y is not None else 0,
            "scale": scale if scale is not None else 1.0,
            "transform": get_niri_transform(transform_str),
            "focused": data.get("is_focused", False),
            # Fields not available in Niri - provide sensible defaults
            "id": -1,
            "activeWorkspace": {"id": -1, "name": ""},
            "specialWorkspace": {"id": -1, "name": ""},
            "reserved": [],
            "dpmsStatus": True,
            "vrr": False,
            "activelyTearing": False,
            "disabled": False,
            "currentFormat": "",
            "availableModes": [],
            "to_disable": False,
        },
    )


class NiriBackend(EnvironmentBackend):
    """Niri backend implementation."""

    def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any] | None:
        """Parse a raw event string into (event_name, event_data).

        Args:
            raw_data: Raw event string from the compositor
            log: Logger to use for this operation
        """
        if not raw_data.strip().startswith("{"):
            return None
        try:
            event = json.loads(raw_data)
        except json.JSONDecodeError:
            log.exception("Invalid JSON event: %s", raw_data)
            return None

        if "Variant" in event:
            type_name = event["Variant"]["type"]
            data = event["Variant"]
            return f"niri_{type_name.lower()}", data
        return None

    async def execute(self, command: str | list | dict, *, log: Logger, **kwargs: Any) -> bool:
        """Execute a command (or list of commands).

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments (weak, etc.)
        """
        weak = kwargs.get("weak", False)
        # Niri commands are typically lists of strings or objects, not a single string command line
        # If we receive a string, we might need to wrap it.
        # But looking at existing usage, nirictl expects list or dict.

        # If we receive a list of strings from execute(), it might be multiple commands?
        # Niri socket protocol is request-response JSON.

        try:
            ret = await niri_request(command, log)
            if isinstance(ret, dict) and "Ok" in ret:
                return True
        except (OSError, ConnectionError, json.JSONDecodeError) as e:
            log.warning("Niri command failed: %s", e)
            return False

        if weak:
            log.warning("Niri command failed: %s", ret)
        else:
            log.error("Niri command failed: %s", ret)
        return False

    async def execute_json(self, command: str, *, log: Logger, **kwargs: Any) -> Any:
        """Execute a command and return the JSON result.

        Args:
            command: The command to execute
            log: Logger to use for this operation
            **kwargs: Additional arguments
        """
        ret = await niri_request(command, log)
        if isinstance(ret, dict) and "Ok" in ret:
            return ret["Ok"]
        msg = f"Niri command failed: {ret}"
        raise RuntimeError(msg)

    async def get_clients(
        self,
        mapped: bool = True,
        workspace: str | None = None,
        workspace_bl: str | None = None,
        *,
        log: Logger,
    ) -> list[ClientInfo]:
        """Return the list of clients, optionally filtered.

        Args:
            mapped: If True, only return mapped clients
            workspace: Filter to this workspace name
            workspace_bl: Blacklist this workspace name
            log: Logger to use for this operation
        """
        return [
            self._map_niri_client(client)
            for client in cast("list[dict]", await self.execute_json("windows", log=log))
            if (not mapped or client.get("is_mapped", True))
            and (workspace is None or str(client.get("workspace_id")) == workspace)
            and (workspace_bl is None or str(client.get("workspace_id")) != workspace_bl)
        ]

    def _map_niri_client(self, niri_client: dict[str, Any]) -> ClientInfo:
        """Helper to map Niri window dict to ClientInfo TypedDict."""
        return cast(
            "ClientInfo",
            {
                "address": str(niri_client.get("id")),
                "class": niri_client.get("app_id"),
                "title": niri_client.get("title"),
                "workspace": {"name": str(niri_client.get("workspace_id"))},
                "pid": -1,
                "mapped": niri_client.get("is_mapped", True),
                "hidden": False,
                "at": (0, 0),
                "size": (0, 0),
                "floating": False,
                "monitor": -1,
                "initialClass": niri_client.get("app_id"),
                "initialTitle": niri_client.get("title"),
                "xwayland": False,
                "pinned": False,
                "fullscreen": False,
                "fullscreenMode": 0,
                "fakeFullscreen": False,
                "grouped": [],
                "swallowing": "",
                "focusHistoryID": 0,
            },
        )

    async def get_monitors(self, *, log: Logger, include_disabled: bool = False) -> list[MonitorInfo]:
        """Return the list of monitors.

        Args:
            log: Logger to use for this operation
            include_disabled: Ignored for Niri (no concept of disabled monitors)
        """
        outputs = await self.execute_json("outputs", log=log)
        return [niri_output_to_monitor_info(name, output) for name, output in outputs.items()]

    async def execute_batch(self, commands: list[str], *, log: Logger) -> None:
        """Execute a batch of commands.

        Args:
            commands: List of commands to execute
            log: Logger to use for this operation
        """
        # Niri doesn't support batching in the same way, so we iterate
        for cmd in commands:
            # We need to parse the command string into an action
            # This is a bit tricky as niri commands are structured objects/lists
            # For now, let's assume 'action' is a command to be sent via nirictl
            # But wait, execute_batch typically receives "dispatch <cmd>" type strings for Hyprland.
            # We need to adapt this.

            # Simple adaptation: if it's a known string command, we try to map it or just send it if niri accepts string commands
            # (it mostly uses 'action' msg)
            # This part requires more knowledge of how commands are passed.
            # In current Pyprland, nirictl takes a list or dict.

            # Placeholder implementation:
            await self.execute(["action", cmd], log=log)

    async def notify(
        self,
        message: str,
        duration: int = DEFAULT_NOTIFICATION_DURATION_MS,
        color: str = "ff0000",
        *,
        log: Logger,
    ) -> None:
        """Send a notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            color: Hex color code
            log: Logger to use for this operation (unused - notify_send doesn't log)
        """
        # Niri doesn't have a built-in notification system exposed via IPC like Hyprland's `notify`
        # We rely on `notify-send` via the common utility

        await notify_send(message, duration, color)

    # ─── Window Operation Helpers (Niri overrides) ────────────────────────────

    async def focus_window(self, address: str, *, log: Logger) -> bool:
        """Focus a window by ID.

        Args:
            address: Window ID
            log: Logger to use for this operation
        """
        return await self.execute({"Action": {"FocusWindow": {"id": int(address)}}}, log=log)

    async def move_window_to_workspace(
        self,
        address: str,
        workspace: str,
        *,
        silent: bool = True,
        log: Logger,
    ) -> bool:
        """Move a window to a workspace (silent parameter ignored in Niri).

        Args:
            address: Window ID
            workspace: Target workspace ID
            silent: Ignored in Niri
            log: Logger to use for this operation
        """
        return await self.execute(
            {"Action": {"MoveWindowToWorkspace": {"window_id": int(address), "reference": {"Id": int(workspace)}}}},
            log=log,
        )

    async def pin_window(self, address: str, *, log: Logger) -> bool:
        """Toggle pin state - not available in Niri.

        Args:
            address: Window ID (unused)
            log: Logger to use for this operation
        """
        log.debug("pin_window: not available in Niri")
        return False

    async def close_window(self, address: str, *, silent: bool = True, log: Logger) -> bool:
        """Close a window by ID.

        Args:
            address: Window ID
            silent: Accepted for API consistency (currently unused for close)
            log: Logger to use for this operation
        """
        return await self.execute({"Action": {"CloseWindow": {"id": int(address)}}}, log=log)

    async def resize_window(self, address: str, width: int, height: int, *, log: Logger) -> bool:
        """Resize a window - not available in Niri (tiling WM).

        Args:
            address: Window ID (unused)
            width: Target width (unused)
            height: Target height (unused)
            log: Logger to use for this operation
        """
        log.debug("resize_window: not available in Niri")
        return False

    async def move_window(self, address: str, x: int, y: int, *, log: Logger) -> bool:
        """Move a window to exact position - not available in Niri (tiling WM).

        Args:
            address: Window ID (unused)
            x: Target x position (unused)
            y: Target y position (unused)
            log: Logger to use for this operation
        """
        log.debug("move_window: not available in Niri")
        return False

    async def toggle_floating(self, address: str, *, log: Logger) -> bool:
        """Toggle floating state - not available in Niri.

        Args:
            address: Window ID (unused)
            log: Logger to use for this operation
        """
        log.debug("toggle_floating: not available in Niri")
        return False

    async def set_keyword(self, keyword_command: str, *, log: Logger) -> bool:
        """Execute a keyword command - not available in Niri.

        Args:
            keyword_command: The keyword command (unused)
            log: Logger to use for this operation
        """
        log.debug("set_keyword: not available in Niri")
        return False


================================================
FILE: pyprland/adapters/proxy.py
================================================
"""Backend proxy that injects plugin logger into all calls.

This module provides a BackendProxy class that wraps an EnvironmentBackend
and automatically passes the plugin's logger to all backend method calls.
This allows backend operations to be logged under the calling plugin's
logger for better traceability.
"""

from collections.abc import Callable
from logging import Logger
from typing import TYPE_CHECKING, Any

from ..models import ClientInfo, MonitorInfo

if TYPE_CHECKING:
    from .backend import EnvironmentBackend


class BackendProxy:
    """Proxy that injects the plugin logger into all backend calls.

    This allows backend operations to be logged under the calling plugin's
    logger for better traceability. Each plugin gets its own BackendProxy
    instance with its own logger, while sharing the underlying backend.

    Attributes:
        log: The logger to use for all backend operations
        state: Reference to the shared state (from the underlying backend)
    """

    def __init__(self, backend: "EnvironmentBackend", log: Logger) -> None:
        """Initialize the proxy.

        Args:
            backend: The underlying backend to delegate calls to
            log: The logger to inject into all backend calls
        """
        self._backend = backend
        self.log = log
        self.state = backend.state

    # === Core execution methods ===

    async def execute(self, command: str | list | dict, **kwargs: Any) -> bool:
        """Execute a command (or list of commands).

        Args:
            command: The command to execute
            **kwargs: Additional arguments (base_command, weak, etc.)

        Returns:
            True if command succeeded
        """
        return await self._backend.execute(command, log=self.log, **kwargs)

    async def execute_json(self, command: str, **kwargs: Any) -> Any:
        """Execute a command and return the JSON result.

        Args:
            command: The command to execute
            **kwargs: Additional arguments

        Returns:
            The JSON response
        """
        return await self._backend.execute_json(command, log=self.log, **kwargs)

    async def execute_batch(self, commands: list[str]) -> None:
        """Execute a batch of commands.

        Args:
            commands: List of commands to execute
        """
        return await self._backend.execute_batch(commands, log=self.log)

    # === Query methods ===

    async def get_clients(
        self,
        mapped: bool = True,
        workspace: str | None = None,
        workspace_bl: str | None = None,
    ) -> list[ClientInfo]:
        """Return the list of clients, optionally filtered.

        Args:
            mapped: If True, only return mapped clients
            workspace: Filter to this workspace name
            workspace_bl: Blacklist this workspace name

        Returns:
            List of matching clients
        """
        return await self._backend.get_clients(mapped, workspace, workspace_bl, log=self.log)

    async def get_monitors(self, include_disabled: bool = False) -> list[MonitorInfo]:
        """Return the list of monitors.

        Args:
            include_disabled: If True, include disabled monitors

        Returns:
            List of monitors
        """
        return await self._backend.get_monitors(log=self.log, include_disabled=include_disabled)

    async def get_monitor_props(
        self,
        name: str | None = None,
        include_disabled: bool = False,
    ) -> MonitorInfo:
        """Return focused monitor data if name is not defined, else use monitor's name.

        Args:
            name: Monitor name to look for, or None for focused monitor
            include_disabled: If True, include disabled monitors in search

        Returns:
            Monitor info dict
        """
        return await self._backend.get_monitor_props(name, include_disabled, log=self.log)

    async def get_client_props(
        self,
        match_fn: Callable[[Any, Any], bool] | None = None,
        clients: list[ClientInfo] | None = None,
        **kw: Any,
    ) -> ClientInfo | None:
        """Return the properties of a client matching the given criteria.

        Args:
            match_fn: Custom match function (defaults to equality)
            clients: Optional pre-fetched client list
            **kw: Property to match (addr, cls, etc.)

        Returns:
            Matching client info or None
        """
        return await self._backend.get_client_props(match_fn, clients, log=self.log, **kw)

    # === Notification methods ===

    async def notify(self, message: str, duration: int = 5000, color: str = "ff0000") -> None:
        """Send a notification.

        Args:
            message: The notification message
            duration: Duration in milliseconds
            color: Hex color code
        """
        return await self._backend.notify(message, duration, color, log=self.log)

    async def notify_info(self, message: str, duration: int = 5000) -> None:
        """Send an info notification (blue color).

        Args:
            message: The notification message
            duration: Duration in milliseconds
        """
        return await self._backend.notify_info(message, duration, log=self.log)

    async def notify_error(self, message: str, duration: int = 5000) -> None:
        """Send an error notification (red color).

        Args:
            message: The notification message
            duration: Duration in milliseconds
        """
        return await self._backend.notify_error(message, duration, log=self.log)

    # === Window operation helpers ===

    async def focus_window(self, address: str) -> bool:
        """Focus a window by address.

        Args:
            address: Window address (without 'address:' prefix)

        Returns:
            True if command succeeded
        """
        return await self._backend.focus_window(address, log=self.log)

    async def move_window_to_workspace(
        self,
        address: str,
        workspace: str,
        *,
        silent: bool = True,
    ) -> bool:
        """Move a window to a workspace.

        Args:
            address: Window address (without 'address:' prefix)
            workspace: Target workspace name or ID
            silent: If True, don't follow the window

        Returns:
            True if command succeeded
        """
        return await self._backend.move_window_to_workspace(address, workspace, silent=silent, log=self.log)

    async def pin_window(self, address: str) -> bool:
        """Toggle pin state of a window.

        Args:
            address: Window address (without 'address:' prefix)

        Returns:
            True if command succeeded
        """
        return await self._backend.pin_window(address, log=self.log)

    async def close_window(self, address: str, silent: bool = True) -> bool:
        """Close a window.

        Args:
            address: Window address (without 'address:' prefix)
            silent: If True, don't shift focus (default: True)

        Returns:
            True if command succeeded
        """
        return await self._backend.close_window(address, silent=silent, log=self.log)

    async def resize_window(self, address: str, width: int, height: int) -> bool:
        """Resize a window to exact pixel dimensions.

        Args:
            address: Window address (without 'address:' prefix)
            width: Target width in pixels
            height: Target height in pixels

        Returns:
            True if command succeeded
        """
        return await self._backend.resize_window(address, width, height, log=self.log)

    async def move_window(self, address: str, x: int, y: int) -> bool:  # pylint: disable=invalid-name
        """Move a window to exact pixel position.

        Args:
            address: Window address (without 'address:' prefix)
            x: Target x position in pixels
            y: Target y position in pixels

        Returns:
            True if command succeeded
        """
        return await self._backend.move_window(address, x, y, log=self.log)

    async def toggle_floating(self, address: str) -> bool:
        """Toggle floating state of a window.

        Args:
            address: Window address (without 'address:' prefix)

        Returns:
            True if command succeeded
        """
        return await self._backend.toggle_floating(address, log=self.log)

    async def set_keyword(self, keyword_command: str) -> bool:
        """Execute a keyword/config command.

        Args:
            keyword_command: The keyword command string

        Returns:
            True if command succeeded
        """
        return await self._backend.set_keyword(keyword_command, log=self.log)

    # === Event parsing ===

    def parse_event(self, raw_data: str) -> tuple[str, Any] | None:
        """Parse a raw event string into (event_name, event_data).

        Args:
            raw_data: Raw event string from the compositor

        Returns:
            Tuple of (event_name, event_data) or None if parsing failed
        """
        return self._backend.parse_event(raw_data, log=self.log)


================================================
FILE: pyprland/adapters/units.py
================================================
"""Conversion functions for units used in Pyprland & plugins."""

from typing import Literal

from ..common import is_rotated
from ..models import MonitorInfo

MonitorDimension = Literal["width", "height"]


def convert_monitor_dimension(size: int | str, ref_value: int, monitor: MonitorInfo) -> int:
    """Convert `size` into pixels (given a reference value applied to a `monitor`).

    if size is an integer, assumed pixels & return it
    if size is a string, expects a "%" or "px" suffix
    else throws an error

    Args:
        size: The size to convert (int or string with unit)
        ref_value: Reference value for percentage calculations
        monitor: Monitor information
    """
    if isinstance(size, int):
        return size

    if isinstance(size, str):
        if size.endswith("%"):
            p = int(size[:-1])
            return int(ref_value / monitor["scale"] * p / 100)
        if size.endswith("px"):
            return int(size[:-2])

    msg = f"Unsupported format: {size} (applied to {ref_value})"
    raise ValueError(msg)


def convert_coords(coords: str, monitor: MonitorInfo) -> list[int]:
    """Convert a string like "X Y" to coordinates relative to monitor.

    Supported formats for X, Y:
    - Percentage: "V%". V in [0; 100]
    - Pixels: "Vpx". V should fit in your screen and not be zero

    Example:
    "10% 20%", monitor 800x600 => 80, 120

    Args:
        coords: Coordinates string "X Y"
        monitor: Monitor information
    """
    coord_list = [coord.strip() for coord in coords.split()]
    refs: tuple[MonitorDimension, MonitorDimension] = ("height", "width") if is_rotated(monitor) else ("width", "height")
    return [convert_monitor_dimension(name, monitor[ref], monitor) for (name, ref) in zip(coord_list, refs, strict=False)]


================================================
FILE: pyprland/adapters/wayland.py
================================================
"""Generic Wayland backend using wlr-randr for monitor detection."""

import re
from logging import Logger

from ..models import MonitorInfo
from .fallback import FallbackBackend, make_monitor_info
from .niri import NIRI_TRANSFORM_MAP


class WaylandBackend(FallbackBackend):
    """Generic Wayland backend using wlr-randr for monitor information.

    Provides monitor detection for the wallpapers plugin on wlroots-based
    compositors (Sway, etc.). Does not support window management, events,
    or other compositor features.
    """

    @classmethod
    async def is_available(cls) -> bool:
        """Check if wlr-randr is available.

        Returns:
            True if wlr-randr command works
        """
        return await cls._check_command("wlr-randr")

    async def get_monitors(self, *, log: Logger, include_disabled: bool = False) -> list[MonitorInfo]:
        """Get monitor information from wlr-randr.

        Args:
            log: Logger to use for this operation
            include_disabled: If True, include disabled monitors

        Returns:
            List of MonitorInfo dicts
        """
        return await self._run_monitor_command(
            "wlr-randr",
            "wlr-randr",
            self._parse_wlr_randr_output,
            include_disabled=include_disabled,
            log=log,
        )

    def _parse_wlr_randr_output(self, output: str, include_disabled: bool, log: Logger) -> list[MonitorInfo]:
        """Parse wlr-randr output to extract monitor information.

        Example wlr-randr output:
            DP-1 "Dell Inc. DELL U2415 ABC123"
              Enabled: yes
              Modes:
                1920x1200 px, 59.950 Hz (preferred, current)
                1920x1080 px, 60.000 Hz
              Position: 0,0
              Transform: normal
              Scale: 1.000000
            HDMI-A-1 "Some Monitor"
              Enabled: no
              ...

        Args:
            output: Raw wlr-randr output
            include_disabled: Whether to include disabled outputs
            log: Logger for debug output

        Returns:
            List of MonitorInfo dicts
        """
        monitors: list[MonitorInfo] = []

        # Split into sections per output (each starts with output name at column 0)
        sections = re.split(r"^(?=\S)", output, flags=re.MULTILINE)

        for raw_section in sections:
            section = raw_section.strip()
            if not section:
                continue

            monitor = self._parse_output_section(section, len(monitors), log)
            if monitor is None:
                continue

            # Skip disabled unless requested
            if monitor.get("disabled") and not include_disabled:
                continue

            monitors.append(monitor)

        return monitors

    def _parse_output_section(  # noqa: C901  # pylint: disable=too-many-locals
        self, section: str, index: int, log: Logger
    ) -> MonitorInfo | None:
        """Parse a single output section from wlr-randr.

        Args:
            section: Section text for one output
            index: Index for this monitor
            log: Logger for debug output

        Returns:
            MonitorInfo dict or None if parsing failed
        """
        lines = section.splitlines()
        if not lines:
            return None

        # First line: output name and description
        # Format: "DP-1 "Dell Inc. DELL U2415 ABC123""
        header_match = re.match(r'^(\S+)\s*(?:"(.+)")?', lines[0])
        if not header_match:
            return None

        name = header_match.group(1)
        description = header_match.group(2) or name

        # Parse properties
        enabled = True
        width, height = 0, 0
        x, y = 0, 0
        scale = 1.0
        transform = 0
        refresh_rate = 60.0

        for raw_line in lines[1:]:
            line = raw_line.strip()

            # Enabled: yes/no
            if line.startswith("Enabled:"):
                enabled = "yes" in line.lower()

            # Position: x,y
            elif line.startswith("Position:"):
                pos_match = re.search(r"(\d+),\s*(\d+)", line)
                if pos_match:
                    x, y = int(pos_match.group(1)), int(pos_match.group(2))

            # Transform: normal/90/180/270/flipped/etc
            elif line.startswith("Transform:"):
                transform_str = line.split(":", 1)[1].strip()
                transform = NIRI_TRANSFORM_MAP.get(transform_str, 0)

            # Scale: 1.000000
            elif line.startswith("Scale:"):
                try:
                    scale = float(line.split(":", 1)[1].strip())
                except ValueError:
                    scale = 1.0

            # Mode line with "current": 1920x1200 px, 59.950 Hz (preferred, current)
            elif "current" in line.lower() and "x" in line:
                mode_match = re.match(r"(\d+)x(\d+)\s*px,\s*([\d.]+)\s*Hz", line)
                if mode_match:
                    width = int(mode_match.group(1))
                    height = int(mode_match.group(2))
                    refresh_rate = float(mode_match.group(3))

        # Skip outputs without resolution
        if width == 0 or height == 0:
            log.debug("wlr-randr: skipping %s (no active mode)", name)
            return None

        log.debug("wlr-randr monitor: %s %dx%d+%d+%d scale=%.2f transform=%d", name, width, height, x, y, scale, transform)

        return make_monitor_info(
            index=index,
            name=name,
            width=width,
            height=height,
            pos_x=x,
            pos_y=y,
            scale=scale,
            transform=transform,
            refresh_rate=refresh_rate,
            enabled=enabled,
            description=description,
        )


================================================
FILE: pyprland/adapters/xorg.py
================================================
"""X11/Xorg backend using xrandr for monitor detection."""
# pylint: disable=duplicate-code  # make_monitor_info calls share parameter patterns

import re
from logging import Logger

from ..models import MonitorInfo
from .fallback import FallbackBackend, make_monitor_info

# Map xrandr rotation names to transform integers
# 0=normal, 1=90° (left), 2=180° (inverted), 3=270° (right)
TRANSFORM_MAP = {
    "normal": 0,
    "left": 1,
    "inverted": 2,
    "right": 3,
}


class XorgBackend(FallbackBackend):
    """X11/Xorg backend using xrandr for monitor information.

    Provides monitor detection for the wallpapers plugin on X11 systems.
    Does not support window management, events, or other compositor features.
    """

    @classmethod
    async def is_available(cls) -> bool:
        """Check if xrandr is available.

        Returns:
            True if xrandr command works
        """
        return await cls._check_command("xrandr --version")

    async def get_monitors(self, *, log: Logger, include_disabled: bool = False) -> list[MonitorInfo]:
        """Get monitor information from xrandr.

        Args:
            log: Logger to use for this operation
            include_disabled: If True, include disconnected monitors

        Returns:
            List of MonitorInfo dicts
        """
        return await self._run_monitor_command(
            "xrandr --query",
            "xrandr",
            self._parse_xrandr_output,
            include_disabled=include_disabled,
            log=log,
        )

    def _parse_xrandr_output(  # pylint: disable=too-many-locals
        self, output: str, include_disabled: bool, log: Logger
    ) -> list[MonitorInfo]:
        """Parse xrandr --query output to extract monitor information.

        Example xrandr output:
            DP-1 connected primary 1920x1080+0+0 left (normal left inverted right x axis y axis) 527mm x 296mm
               1920x1080     60.00*+
            HDMI-1 connected 2560x1440+1920+0 (normal left inverted right x axis y axis) 597mm x 336mm
               2560x1440     59.95*+
            VGA-1 disconnected (normal left inverted right x axis y axis)

        Args:
            output: Raw xrandr output
            include_disabled: Whether to include disconnected outputs
            log: Logger for debug output

        Returns:
            List of MonitorInfo dicts
        """
        monitors: list[MonitorInfo] = []

        # Pattern to match connected outputs with resolution
        # Groups: name, primary?, resolution+position, transform?
        # Example: "DP-1 connected primary 1920x1080+0+0 left"
        pattern = re.compile(
            r"^(\S+)\s+(connected|disconnected)"  # name, status
            r"(?:\s+primary)?"  # optional primary
            r"(?:\s+(\d+)x(\d+)\+(\d+)\+(\d+))?"  # optional WxH+X+Y
            r"(?:\s+(normal|left|inverted|right))?"  # optional transform
        )

        for line in output.splitlines():
            match = pattern.match(line)
            if not match:
                continue

            name = match.group(1)
            connected = match.group(2) == "connected"
            width = int(match.group(3)) if match.group(3) else 0
            height = int(match.group(4)) if match.group(4) else 0
            x = int(match.group(5)) if match.group(5) else 0
            y = int(match.group(6)) if match.group(6) else 0
            transform_str = match.group(7) or "normal"

            # Skip disconnected unless requested
            if not connected and not include_disabled:
                continue

            # Skip outputs without resolution (not active)
            if (width == 0 or height == 0) and not include_disabled:
                continue

            transform = TRANSFORM_MAP.get(transform_str, 0)

            log.debug("xrandr monitor: %s %dx%d+%d+%d transform=%d", name, width, height, x, y, transform)

            # Build MonitorInfo - X11 doesn't have fractional scaling via xrandr
            monitor = make_monitor_info(
                index=len(monitors),
                name=name,
                width=width,
                height=height,
                pos_x=x,
                pos_y=y,
                transform=transform,
                enabled=connected,
            )
            monitors.append(monitor)

        return monitors


================================================
FILE: pyprland/aioops.py
================================================
"""Async operation utilities.

Provides fallback sync methods if aiofiles is not installed,
plus async task management utilities.
"""

__all__ = [
    "DebouncedTask",
    "TaskManager",
    "aiexists",
    "aiisdir",
    "aiisfile",
    "ailistdir",
    "aiopen",
    "airmdir",
    "airmtree",
    "aiunlink",
    "graceful_cancel_tasks",
    "is_process_running",
]

import asyncio
import contextlib
import io
import os
import shutil
from collections.abc import AsyncIterator, Callable, Coroutine
from types import TracebackType
from typing import Any, Self

try:
    import aiofiles.os
    from aiofiles import open as aiopen
    from aiofiles.os import listdir as ailistdir
    from aiofiles.os import unlink as aiunlink

    aiexists = aiofiles.os.path.exists
    aiisdir = aiofiles.os.path.isdir
    aiisfile = aiofiles.os.path.isfile
except ImportError:

    class AsyncFile:
        """Async file wrapper.

        Args:
            file: The file object to wrap
        """

        def __init__(self, file: io.TextIOWrapper) -> None:
            self.file = file

        async def readlines(self) -> list[str]:
            """Read lines."""
            return self.file.readlines()

        async def read(self) -> str:
            """Read lines."""
            return self.file.read()

        async def __aenter__(self) -> Self:
            return self

        async def __aexit__(
            self,
            exc_type: type[BaseException] | None,
            exc_val: BaseException | None,
            exc_tb: TracebackType | None,
        ) -> None:
            self.file.close()

    @contextlib.asynccontextmanager  # type: ignore[no-redef, unused-ignore]
    async def aiopen(*args, **kwargs) -> AsyncIterator[AsyncFile]:
        """Async > sync wrapper."""
        with open(*args, **kwargs) as f:  # noqa: ASYNC230, PTH123  # pylint: disable=unspecified-encoding
            yield AsyncFile(f)

    async def aiexists(*args, **kwargs) -> bool:
        """Async > sync wrapper."""
        return os.path.exists(*args, **kwargs)  # noqa: ASYNC240

    async def aiisdir(*args, **kwargs) -> bool:
        """Async > sync wrapper."""
        return os.path.isdir(*args, **kwargs)  # noqa: ASYNC240

    async def aiisfile(*args, **kwargs) -> bool:
        """Async > sync wrapper."""
        return await asyncio.to_thread(os.path.isfile, *args, **kwargs)

    async def ailistdir(*args, **kwargs) -> list[str]:  # type: ignore[no-redef, unused-ignore]
        """Async > sync wrapper."""
        return await asyncio.to_thread(os.listdir, *args, **kwargs)

    async def aiunlink(*args, **kwargs) -> None:  # type: ignore[no-redef, misc, unused-ignore]
        """Async > sync wrapper."""
        await asyncio.to_thread(os.unlink, *args, **kwargs)


async def airmtree(path: str) -> None:
    """Async wrapper for shutil.rmtree.

    Removes a directory tree recursively.

    Args:
        path: Directory to remove recursively.
    """
    await asyncio.to_thread(shutil.rmtree, path)


async def airmdir(path: str) -> None:
    """Async wrapper for os.rmdir.

    Removes an empty directory.

    Args:
        path: Empty directory to remove.
    """
    await asyncio.to_thread(os.rmdir, path)


async def is_process_running(name: str) -> bool:
    """Check if a process with the given name is running.

    Uses /proc filesystem to check process names (Linux only).

    Args:
        name: The process name to search for (matches /proc/<pid>/comm)

    Returns:
        True if a process with that name is running, False otherwise.
    """
    for pid in await ailistdir("/proc"):
        if pid.isdigit():
            try:
                async with aiopen(f"/proc/{pid}/comm") as f:
                    if (await f.read()).strip() == name:
                        return True
            except OSError:
                pass  # Process may have exited
    return False


async def graceful_cancel_tasks(
    tasks: list[asyncio.Task],
    timeout: float = 1.0,  # noqa: ASYNC109
) -> None:
    """Cancel tasks with graceful timeout, then force cancel remaining.

    This is the standard shutdown pattern for async tasks:
    1. Wait up to `timeout` seconds for tasks to complete gracefully
    2. Force cancel any tasks still running
    3. Await all cancelled tasks to ensure cleanup

    Args:
        tasks: List of tasks to cancel (filters out already-done tasks)
        timeout: Seconds to wait for graceful completion (default: 1.0)
    """
    pending = [t for t in tasks if not t.done()]
    if not pending:
        return

    # Wait for graceful completion
    _, still_pending = await asyncio.wait(
        pending,
        timeout=timeout,
        return_when=asyncio.ALL_COMPLETED,
    )

    # Force cancel remaining
    for task in still_pending:
        task.cancel()

    # Await all cancelled tasks
    for task in still_pending:
        with contextlib.suppress(asyncio.CancelledError):
            await task


class DebouncedTask:
    """A debounced async task with ignore window support.

    Useful for plugins that react to events they can also trigger themselves.
    The ignore window prevents reacting to self-triggered events.

    Usage:
        # Create instance (typically in on_reload)
        self._relayout_debouncer = DebouncedTask(ignore_window=3.0)

        # In event handler - schedule with delay
        self._relayout_debouncer.schedule(self._delayed_relayout, delay=1.0)

        # Before self-triggering actions - set ignore window
        self._relayout_debouncer.set_ignore_window()
        await self.backend.execute(cmd, base_command="keyword")
    """

    def __init__(self, ignore_window: float = 3.0) -> None:
        """Initialize the debounced task.

        Args:
            ignore_window: Duration in seconds to ignore schedule() calls
                          after set_ignore_window() is called.
        """
        self._task: asyncio.Task[None] | None = None
        self._ignore_window = ignore_window
        self._ignore_until: float = 0

    def schedule(self, coro_func: Callable[[], Coroutine[Any, Any, Any]], delay: float = 0) -> bool:
        """Schedule or reschedule the task.

        Cancels any pending task before scheduling. If within the ignore window,
        the task is not scheduled.

        Args:
            coro_func: Async function to call (no arguments)
            delay: Delay in seconds before executing

        Returns:
            True if scheduled, False if in ignore window
        """
        if asyncio.get_event_loop().time() < self._ignore_until:
            return False

        self.cancel()

        async def _run() -> None:
            try:
                if delay > 0:
                    await asyncio.sleep(delay)
                await coro_func()
            except asyncio.CancelledError:
                pass

        self._task = asyncio.create_task(_run())
        return True

    def set_ignore_window(self) -> None:
        """Start the ignore window and cancel any pending task.

        Calls to schedule() will be ignored until the window expires.
        """
        self._ignore_until = asyncio.get_event_loop().time() + self._ignore_window
        self.cancel()

    def cancel(self) -> None:
        """Cancel any pending task."""
        if self._task and not self._task.done():
            self._task.cancel()
            self._task = None


class TaskManager:
    """Manages async tasks with proper lifecycle handling.

    Provides consistent start/stop behavior with graceful shutdown:
    1. Set running=False and signal stop event (graceful)
    2. Wait with timeout for tasks to complete
    3. Cancel remaining tasks if still alive
    4. Always await to completion

    Similar to ManagedProcess but for asyncio Tasks instead of subprocesses.

    Usage:
        # Single background loop
        self._tasks = TaskManager()

        async def on_reload(self):
            self._tasks.start()
            self._tasks.create(self._main_loop())

        async def _main_loop(self):
            while self._tasks.running:
                await self.do_work()
                if await self._tasks.sleep(60):
                    break  # Stop requested

        async def exit(self):
            await self._tasks.stop()

    Keyed tasks (for per-item tracking like scratchpad hysteresis):
        self._tasks.create(self._delayed_hide(uid), key=uid)
        self._tasks.cancel_keyed(uid)  # Cancel specific task
    """

    def __init__(
        self,
        graceful_timeout: float = 1.0,
        on_error: Callable[[asyncio.Task, BaseException], Coroutine[Any, Any, None]] | None = None,
    ) -> None:
        """Initialize.

        Args:
            graceful_timeout: Seconds to wait for graceful stop before force cancel
            on_error: Async callback when a task fails (receives task and exception)
        """
        self._tasks: list[asyncio.Task] = []
        self._keyed_tasks: dict[str, asyncio.Task] = {}
        self._running: bool = False
        self._stop_event: asyncio.Event | None = None
        self._graceful_timeout = graceful_timeout
        self._on_error = on_error

    @property
    def running(self) -> bool:
        """Check if manager is running (tasks should continue)."""
        return self._running

    def start(self) -> None:
        """Mark manager as running. Call before creating tasks."""
        self._running = True
        self._stop_event = asyncio.Event()

    def create(self, coro: Coroutine[Any, Any, Any], *, key: str | None = None) -> asyncio.Task:
        """Create and track a task.

        Args:
            coro: Coroutine to run
            key: Optional key for keyed task (replaces existing task with same key)

        Returns:
            The created task
        """
        if key is not None:
            self.cancel_keyed(key)
            task = asyncio.create_task(self._wrap_task(coro))
            self._keyed_tasks[key] = task
        else:
            task = asyncio.create_task(self._wrap_task(coro))
            self._tasks.append(task)
        return task

    async def _wrap_task(self, coro: Coroutine[Any, Any, Any]) -> None:
        """Wrap a coroutine to handle errors via callback."""
        try:
            await coro
        except asyncio.CancelledError:
            raise
        except BaseException as e:  # pylint: disable=broad-exception-caught
            if self._on_error:
                task = asyncio.current_task()
                assert task is not None
                await self._on_error(task, e)
            else:
                raise

    def cancel_keyed(self, key: str) -> bool:
        """Cancel a keyed task immediately.

        Args:
            key: The task key

        Returns:
            True if task existed and was cancelled
        """
        task = self._keyed_tasks.pop(key, None)
        if task and not task.done():
            task.cancel()
            return True
        return False

    async def sleep(self, duration: float) -> bool:
        """Interruptible sleep that respects stop signal.

        Use this instead of asyncio.sleep() in loops.

        Args:
            duration: Sleep duration in seconds

        Returns:
            True if interrupted (should exit loop), False if completed normally
        """
        if self._stop_event is None:
            await asyncio.sleep(duration)
            return not self._running
        try:
            await asyncio.wait_for(self._stop_event.wait(), timeout=duration)
        except TimeoutError:
            return False  # Sleep completed normally
        return True  # Stop event was set

    async def stop(self) -> None:
        """Stop all tasks with graceful timeout.

        Shutdown sequence (mirrors ManagedProcess):
        1. Set running=False and signal stop event (graceful)
        2. Wait up to graceful_timeout for tasks to complete
        3. Cancel remaining tasks if still alive
        4. Await all tasks to completion
        """
        self._running = False
        if self._stop_event:
            self._stop_event.set()

        all_tasks = self._tasks + list(self._keyed_tasks.values())
        await graceful_cancel_tasks(all_tasks, timeout=self._graceful_timeout)

        self._tasks.clear()
        self._keyed_tasks.clear()
        self._stop_event = None


================================================
FILE: pyprland/ansi.py
================================================
"""ANSI terminal color utilities.

Provides constants and helpers for terminal coloring with proper
NO_COLOR environment variable support and TTY detection.
"""

import os
import sys
from typing import TextIO

__all__ = [
    "BLACK",
    "BLUE",
    "BOLD",
    "CYAN",
    "DIM",
    "GREEN",
    "RED",
    "RESET",
    "YELLOW",
    "HandlerStyles",
    "LogStyles",
    "colorize",
    "make_style",
    "should_colorize",
]

# ANSI escape sequence prefix
_ESC = "\x1b["

# Reset all attributes
RESET = f"{_ESC}0m"

# Style codes
BOLD = "1"
DIM = "2"

# Foreground color codes
BLACK = "30"
RED = "31"
GREEN = "32"
YELLOW = "33"
BLUE = "34"
CYAN = "36"


def should_colorize(stream: TextIO | None = None) -> bool:
    """Determine if ANSI colors should be used for the given stream.

    Respects:
    - NO_COLOR environment variable (disables colors)
    - FORCE_COLOR environment variable (forces colors)
    - TTY detection (disables colors when piping)

    Args:
        stream: The output stream to check. Defaults to sys.stderr.

    Returns:
        True if colors should be used, False otherwise.
    """
    if os.environ.get("NO_COLOR"):
        return False
    if os.environ.get("FORCE_COLOR"):
        return True
    if stream is None:
        stream = sys.stderr
    return hasattr(stream, "isatty") and stream.isatty()


def colorize(text: str, *codes: str) -> str:
    """Wrap text in ANSI color codes.

    Args:
        text: The text to colorize.
        *codes: ANSI codes to apply (e.g., RED, BOLD).

    Returns:
        The text wrapped in ANSI escape sequences.
    """
    if not codes:
        return text
    return f"{_ESC}{';'.join(codes)}m{text}{RESET}"


def make_style(*codes: str) -> tuple[str, str]:
    """Create a style prefix and suffix pair.

    Args:
        *codes: ANSI codes to apply.

    Returns:
        Tuple of (prefix, suffix) strings for use in formatters.
    """
    if not codes:
        return ("", RESET)
    return (f"{_ESC}{';'.join(codes)}m", RESET)


class LogStyles:
    """Pre-built styles for log levels."""

    WARNING = (YELLOW, DIM)
    ERROR = (RED, DIM)
    CRITICAL = (RED, BOLD)


class HandlerStyles:
    """Pre-built styles for handler logging."""

    COMMAND = (YELLOW, BOLD)  # run_* methods
    EVENT = (BLACK, BOLD)  # event_* methods


================================================
FILE: pyprland/client.py
================================================
"""Client-side functions for pyprland CLI."""

import asyncio
import contextlib
import os
import sys

from . import constants as pyprland_constants
from .commands.parsing import normalize_command_name
from .common import get_logger, notify_send, run_interactive_program
from .models import ExitCode, ResponsePrefix

__all__ = ["run_client"]


def _get_config_file_path() -> str:
    """Get the config file path, checking new location then legacy fallback.

    Returns:
        Path to the config file as a string.
    """
    config_path = pyprland_constants.CONFIG_FILE
    legacy_path = pyprland_constants.LEGACY_CONFIG_FILE
    if config_path.exists():
        return str(config_path)
    if legacy_path.exists():
        return str(legacy_path)
    # Default to new path (will be created by user)
    return str(config_path)


async def run_client() -> None:
    """Run the client (CLI)."""
    log = get_logger("client")

    if sys.argv[1] == "edit":
        editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
        filename = _get_config_file_path()
        run_interactive_program(f'{editor} "{filename}"')
        sys.argv[1] = "reload"

    elif sys.argv[1] == "validate":
        # Validate doesn't require daemon - run locally and exit
        from .validate_cli import run_validate  # noqa: PLC0415

        run_validate()
        return

    elif sys.argv[1] in {"--help", "-h"}:
        sys.argv[1] = "help"

    try:
        reader, writer = await asyncio.open_unix_connection(pyprland_constants.CONTROL)
    except (ConnectionRefusedError, FileNotFoundError):
        log.critical(
            "Cannot connect to pyprland daemon at %s.\nIs the daemon running? Start it with: pypr (no arguments)",
            pyprland_constants.CONTROL,
        )
        with contextlib.suppress(Exception):
            await notify_send("Pypr can't connect. Is daemon running?", icon="dialog-error")
        sys.exit(ExitCode.CONNECTION_ERROR)

    args = sys.argv[1:]
    args[0] = normalize_command_name(args[0])
    writer.write((" ".join(args) + "\n").encode())
    writer.write_eof()
    await writer.drain()
    return_value = (await reader.read()).decode("utf-8")
    writer.close()
    await writer.wait_closed()

    # Parse response and set exit code
    if return_value.startswith(f"{ResponsePrefix.ERROR}:"):
        # Extract error message (skip "ERROR: " prefix)
        error_msg = return_value[len(ResponsePrefix.ERROR) + 2 :].strip()
        print(f"Error: {error_msg}", file=sys.stderr)
        sys.exit(ExitCode.COMMAND_ERROR)
    elif return_value.startswith(f"{ResponsePrefix.OK}"):
        # Command succeeded, check for additional output after OK
        remaining = return_value[len(ResponsePrefix.OK) :]
        if remaining.startswith(": "):
            # Success message format: "OK: message\n"
            print(remaining[2:].strip())
        elif remaining.startswith("\n"):
            # Content format: "OK\n<content>"
            content = remaining[1:].rstrip("\n")
            if content:
                print(content)
        # "OK\n" with no content: print nothing
        sys.exit(ExitCode.SUCCESS)
    else:
        # Legacy response (version, help, dumpjson) - print as-is
        print(return_value.rstrip())
        sys.exit(ExitCode.SUCCESS)


================================================
FILE: pyprland/command.py
================================================
"""Pyprland - an Hyprland companion app (cli client & daemon)."""

import asyncio
import json
import sys
from pathlib import Path
from typing import Literal, overload

from . import constants as pyprland_constants
from .common import get_logger, init_logger
from .constants import CONTROL
from .ipc import init as ipc_init
from .models import PyprError

__all__: list[str] = ["main"]


@overload
def use_param(txt: str, optional_value: Literal[False] = ...) -> str: ...


@overload
def use_param(txt: str, optional_value: Literal[True]) -> str | bool: ...


def use_param(txt: str, optional_value: bool = False) -> str | bool:
    """Check if parameter `txt` is in sys.argv.

    If found, removes it from sys.argv & returns the argument value.
    If optional_value is True, the parameter value is optional.

    Args:
        txt: Parameter name to look for
        optional_value: If True, value after parameter is optional

    Returns:
        - "" if parameter not present
        - True if parameter present but no value (only when optional_value=True)
        - The value string if parameter present with value
    """
    if txt not in sys.argv:
        return ""
    i = sys.argv.index(txt)
    # Check if there's a next arg and it's not a flag
    if optional_value and (i + 1 >= len(sys.argv) or sys.argv[i + 1].startswith("-")):
        del sys.argv[i]
        return True
    v = sys.argv[i + 1]
    del sys.argv[i : i + 2]
    return v


def _run(invoke_daemon: bool) -> None:
    """Run the daemon or client, handling errors."""
    log = get_logger("startup")
    try:
        if invoke_daemon:
            from .pypr_daemon import run_daemon  # noqa: PLC0415

            asyncio.run(run_daemon())
        else:
            from .client import run_client  # noqa: PLC0415

            asyncio.run(run_client())
    except KeyboardInterrupt:
        pass
    except PyprError:
        log.critical("Command failed.")
    except json.decoder.JSONDecodeError as e:
        log.critical("Invalid JSON syntax in the config file: %s", e.args[0])
    except Exception:  # pylint: disable=W0718
        log.critical("Unhandled exception:", exc_info=True)
    finally:
        if invoke_daemon and Path(CONTROL).exists():
            Path(CONTROL).unlink()


def main() -> None:
    """Run the command."""
    debug_flag = use_param("--debug", optional_value=True)
    if debug_flag:
        filename = debug_flag if isinstance(debug_flag, str) else None
        init_logger(filename=filename, force_debug=True)
    else:
        init_logger()
    ipc_init()

    config_override = use_param("--config")
    if config_override:
        pyprland_constants.CONFIG_FILE = Path(config_override)

    invoke_daemon = len(sys.argv) <= 1
    if invoke_daemon and Path(CONTROL).exists():
        get_logger("startup").critical(
            """%s exists,
is pypr already running ?
If that's not the case, delete this file and run again.""",
            CONTROL,
        )
    else:
        _run(invoke_daemon)


if __name__ == "__main__":
    main()


================================================
FILE: pyprland/commands/__init__.py
================================================
"""Command handling utilities for pyprland.

This package provides:
- models: Data structures (CommandArg, CommandInfo, CommandNode, CLIENT_COMMANDS)
- parsing: Docstring and command name parsing
- discovery: Command extraction from plugins
- tree: Hierarchical command tree building
"""


================================================
FILE: pyprland/commands/discovery.py
================================================
"""Command extraction and discovery from plugins."""

from __future__ import annotations

import inspect
from typing import TYPE_CHECKING

from .models import CLIENT_COMMANDS, CommandInfo
from .parsing import parse_docstring

if TYPE_CHECKING:
    from ..manager import Pyprland

__all__ = ["extract_commands_from_object", "get_all_commands", "get_client_commands"]


def extract_commands_from_object(obj: object, source: str) -> list[CommandInfo]:
    """Extract commands from a plugin class or instance.

    Works with both classes (for docs generation) and instances (runtime).
    Looks for methods starting with "run_" and extracts their docstrings.

    Args:
        obj: A plugin class or instance
        source: The source identifier (plugin name, "built-in", or "client")

    Returns:
        List of CommandInfo objects
    """
    commands: list[CommandInfo] = []

    for name in dir(obj):
        if not name.startswith("run_"):
            continue

        method = getattr(obj, name)
        if not callable(method):
            continue

        command_name = name[4:]  # Remove 'run_' prefix
        docstring = inspect.getdoc(method) or ""

        args, short_desc, full_desc = parse_docstring(docstring)

        commands.append(
            CommandInfo(
                name=command_name,
                args=args,
                short_description=short_desc,
                full_description=full_desc,
                source=source,
            )
        )

    return commands


def get_client_commands() -> list[CommandInfo]:
    """Get client-only commands (edit, validate).

    These commands run on the client side and don't go through the daemon.

    Returns:
        List of CommandInfo for client-only commands
    """
    commands: list[CommandInfo] = []
    for name, doc in CLIENT_COMMANDS.items():
        args, short_desc, full_desc = parse_docstring(doc)
        commands.append(
            CommandInfo(
                name=name,
                args=args,
                short_description=short_desc,
                full_description=full_desc,
                source="client",
            )
        )
    return commands


def get_all_commands(manager: Pyprland) -> dict[str, CommandInfo]:
    """Get all commands from plugins and client.

    Args:
        manager: The Pyprland manager instance with loaded plugins

    Returns:
        Dict mapping command name to CommandInfo
    """
    commands: dict[str, CommandInfo] = {}

    # Extract from all plugins
    for plugin in manager.plugins.values():
        source = "built-in" if plugin.name == "pyprland" else plugin.name
        for cmd in extract_commands_from_object(plugin, source):
            commands[cmd.name] = cmd

    # Add client-only commands
    for cmd in get_client_commands():
        commands[cmd.name] = cmd

    return commands


================================================
FILE: pyprland/commands/models.py
================================================
"""Data models for command handling."""

from __future__ import annotations

from dataclasses import dataclass, field

__all__ = ["CLIENT_COMMANDS", "CommandArg", "CommandInfo", "CommandNode"]

# Client-only commands with their docstrings (not sent to daemon)
CLIENT_COMMANDS: dict[str, str] = {
    "edit": """Open the configuration file in $EDITOR, then reload.

Opens pyprland.toml in your preferred editor (EDITOR or VISUAL env var,
defaults to vi). After the editor closes, the configuration is reloaded.""",
    "validate": """Validate the configuration file.

Checks the configuration file for syntax errors and validates plugin
configurations against their schemas. Does not require the daemon.""",
}


@dataclass
class CommandArg:
    """An argument parsed from a command's docstring."""

    value: str  # e.g., "next|pause|clear" or "name"
    required: bool  # True for <arg>, False for [arg]


@dataclass
class CommandInfo:
    """Complete information about a command."""

    name: str
    args: list[CommandArg]
    short_description: str
    full_description: str
    source: str  # "built-in", plugin name, or "client"


@dataclass
class CommandNode:
    """A node in the command hierarchy.

    Used to represent commands with subcommands (e.g., "wall next", "wall pause").
    A node can have both its own handler (info) and children subcommands.
    """

    name: str  # The segment name (e.g., "wall" or "next")
    full_name: str  # Full command name (e.g., "wall" or "wall_next")
    info: CommandInfo | None = None  # Command info if this node is callable
    children: dict[str, CommandNode] = field(default_factory=dict)


================================================
FILE: pyprland/commands/parsing.py
================================================
"""Docstring and command name parsing utilities."""

from __future__ import annotations

import re

from .models import CommandArg

__all__ = ["normalize_command_name", "parse_docstring"]

# Regex pattern to match args: <required> or [optional]
_ARG_PATTERN = re.compile(r"([<\[])([^>\]]+)([>\]])")


def normalize_command_name(cmd: str) -> str:
    """Normalize a user-typed command to internal format.

    Converts spaces and hyphens to underscores.
    E.g., "wall rm" -> "wall_rm", "toggle-special" -> "toggle_special"

    Args:
        cmd: User-typed command string

    Returns:
        Normalized command name with underscores
    """
    return cmd.replace("-", "_").replace(" ", "_")


def parse_docstring(docstring: str) -> tuple[list[CommandArg], str, str]:
    """Parse a docstring to extract arguments and descriptions.

    The first line may contain arguments like:
    "<arg> Short description" or "[optional_arg] Short description"

    Args:
        docstring: The raw docstring to parse

    Returns:
        Tuple of (args, short_description, full_description)
        - args: List of CommandArg objects
        - short_description: Text after arguments on first line
        - full_description: Complete docstring
    """
    if not docstring:
        return [], "No description available.", ""

    full_description = docstring.strip()
    lines = full_description.split("\n")
    first_line = lines[0].strip()

    args: list[CommandArg] = []
    last_end = 0

    # Find all args at the start of the line
    for match in _ARG_PATTERN.finditer(first_line):
        # Check if this match is at the expected position (start or after whitespace)
        if match.start() != last_end and first_line[last_end : match.start()].strip():
            # There's non-whitespace before this match, stop parsing args
            break

        bracket_open = match.group(1)
        content = match.group(2)
        required = bracket_open == "<"
        args.append(CommandArg(value=content, required=required))
        last_end = match.end()

        # Skip any whitespace after the arg
        while last_end < len(first_line) and first_line[last_end] == " ":
            last_end += 1

    # The short description is what comes after the args
    if args:
        short_description = first_line[last_end:].strip()
        if not short_description:
            short_description = first_line
    else:
        short_description = first_line

    return args, short_description, full_description


================================================
FILE: pyprland/commands/tree.py
================================================
"""Hierarchical command tree building and display name utilities."""

from __future__ import annotations

from typing import TYPE_CHECKING

from .models import CommandInfo, CommandNode
from .parsing import normalize_command_name

if TYPE_CHECKING:
    from collections.abc import Iterable

__all__ = ["build_command_tree", "get_display_name", "get_parent_prefixes"]


def get_parent_prefixes(commands: dict[str, str] | Iterable[str]) -> set[str]:
    """Identify prefixes that have multiple child commands from the same source.

    A prefix becomes a parent node when more than one command from the
    SAME source/plugin shares it. This prevents unrelated commands like
    toggle_special and toggle_dpms (from different plugins) from being grouped.

    Args:
        commands: Either a dict mapping command name -> source/plugin name,
                  or an iterable of command names (legacy, no source filtering)

    Returns:
        Set of prefixes that should become parent nodes
    """
    # Handle legacy call with just command names (no source info)
    if not isinstance(commands, dict):
        commands = dict.fromkeys(commands, "")

    # Group commands by (prefix, source) to find true hierarchies
    prefix_source_counts: dict[tuple[str, str], int] = {}
    for name, source in commands.items():
        parts = name.split("_")
        for i in range(1, len(parts)):
            prefix = "_".join(parts[:i])
            key = (prefix, source)
            prefix_source_counts[key] = prefix_source_counts.get(key, 0) + 1

    # A prefix is a parent only if multiple commands from same source share it
    return {prefix for (prefix, _source), count in prefix_source_counts.items() if count > 1}


def get_display_name(cmd_name: str, parent_prefixes: set[str]) -> str:
    """Get the user-facing display name for a command.

    Converts underscore-separated hierarchical commands to space-separated.
    E.g., "wall_rm" -> "wall rm" if "wall" is a parent prefix.
    Non-hierarchical commands stay unchanged: "shift_monitors" -> "shift_monitors"

    Args:
        cmd_name: The internal command name (underscore-separated)
        parent_prefixes: Set of prefixes that have multiple children

    Returns:
        The display name (space-separated for hierarchical commands)
    """
    parts = cmd_name.split("_")
    for i in range(1, len(parts)):
        prefix = "_".join(parts[:i])
        if prefix in parent_prefixes:
            subcommand = "_".join(parts[i:])
            return f"{prefix} {subcommand}"
    return cmd_name


def build_command_tree(commands: dict[str, CommandInfo]) -> dict[str, CommandNode]:
    """Build hierarchical command tree from flat command names.

    Groups commands with shared prefixes into a tree structure.
    For example, wall_next, wall_pause, wall_clear become children of "wall".

    Only creates hierarchy when multiple commands share a prefix:
    - wall_next + wall_pause -> wall: {next, pause}
    - layout_center (alone) -> layout_center (no split)

    Accepts command names in both formats:
    - Internal format: wall_next (underscore-separated)
    - Display format: wall next (space-separated)

    Args:
        commands: Dict mapping command name to CommandInfo

    Returns:
        Dict mapping root command names to CommandNode trees
    """
    # Normalize names to internal format (underscore) for tree building
    normalized_commands = {normalize_command_name(name): info for name, info in commands.items()}
    parent_prefixes = get_parent_prefixes({name: info.source for name, info in normalized_commands.items()})

    # Build the tree
    roots: dict[str, CommandNode] = {}

    for name, info in sorted(normalized_commands.items()):
        parts = name.split("_")

        # Find the longest parent prefix for this command
        parent_depth = 0
        for i in range(1, len(parts)):
            prefix = "_".join(parts[:i])
            if prefix in parent_prefixes:
                parent_depth = i

        if parent_depth == 0:
            # No parent prefix - this is a root command
            if name not in roots:
                roots[name] = CommandNode(name=name, full_name=name, info=info)
            else:
                roots[name].info = info
        else:
            # Has a parent prefix - add to tree
            root_name = "_".join(parts[:parent_depth])

            # Ensure root node exists
            if root_name not in roots:
                # Check if root itself is a command
                root_info = normalized_commands.get(root_name)
                roots[root_name] = CommandNode(name=root_name, full_name=root_name, info=root_info)

            # Add this command as a child
            if name != root_name:
                child_name = "_".join(parts[parent_depth:])
                roots[root_name].children[child_name] = CommandNode(
                    name=child_name,
                    full_name=name,
                    info=info,
                )

    return roots


================================================
FILE: pyprland/common.py
================================================
"""Shared utilities - re-exports from focused modules for backward compatibility.

This module aggregates exports from specialized modules (debug, ipc_paths,
logging_setup, state, terminal, utils) providing a single import point
for commonly used functions and classes.

Note: For new code, prefer importing directly from the specific modules.
"""

# Re-export from focused modules
from .debug import DEBUG, is_debug, set_debug
from .ipc_paths import (
    HYPRLAND_INSTANCE_SIGNATURE,
    IPC_FOLDER,
    MINIMUM_ADDR_LEN,
    MINIMUM_FULL_ADDR_LEN,
    init_ipc_folder,
)
from .logging_setup import LogObjects, get_logger, init_logger
from .state import SharedState
from .terminal import run_interactive_program, set_raw_mode, set_terminal_size
from .utils import apply_filter, apply_variables, is_rotated, merge, notify_send

__all__ = [
    "DEBUG",
    "HYPRLAND_INSTANCE_SIGNATURE",
    "IPC_FOLDER",
    "MINIMUM_ADDR_LEN",
    "MINIMUM_FULL_ADDR_LEN",
    "LogObjects",
    "SharedState",
    "apply_filter",
    "apply_variables",
    "get_logger",
    "init_ipc_folder",
    "init_logger",
    "is_debug",
    "is_rotated",
    "merge",
    "notify_send",
    "run_interactive_program",
    "set_debug",
    "set_raw_mode",
    "set_terminal_size",
]


================================================
FILE: pyprland/completions/__init__.py
================================================
"""Shell completion generators for pyprland.

Generates dynamic shell completions based on loaded plugins and configuration.
Supports positional argument awareness with type-specific completions.

This package provides:
- Command completion discovery from loaded plugins
- Shell-specific completion script generators (bash, zsh, fish)
- CLI handler for the `pypr compgen` command
"""

from __future__ import annotations

from .discovery import get_command_completions
from .generators import GENERATORS
from .handlers import get_default_path, handle_compgen
from .models import CommandCompletion, CompletionArg

__all__ = [
    "GENERATORS",
    "CommandCompletion",
    "CompletionArg",
    "get_command_completions",
    "get_default_path",
    "handle_compgen",
]


================================================
FILE: pyprland/completions/discovery.py
================================================
"""Command completion discovery.

Extracts structured completion data from loaded plugins and configuration.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from ..commands.discovery import get_all_commands
from ..commands.tree import build_command_tree
from ..constants import SUPPORTED_SHELLS
from .models import (
    HINT_ARGS,
    KNOWN_COMPLETIONS,
    SCRATCHPAD_COMMANDS,
    CommandCompletion,
    CompletionArg,
)

if TYPE_CHECKING:
    from ..commands.models import CommandInfo, CommandNode
    from ..manager import Pyprland

__all__ = ["get_command_completions"]


def _classify_arg(
    arg_value: str,
    cmd_name: str,
    scratchpad_names: list[str],
) -> tuple[str, list[str]]:
    """Classify an argument and determine its completion type and values.

    Args:
        arg_value: The argument value from docstring (e.g., "next|pause|clear")
        cmd_name: The command name (for context-specific handling)
        scratchpad_names: Available scratchpad names from config

    Returns:
        Tuple of (completion_type, values)
    """
    # Check for pipe-separated choices
    if "|" in arg_value:
        return ("choices", arg_value.split("|"))

    # Check for scratchpad commands with "name" arg
    if arg_value == "name" and cmd_name in SCRATCHPAD_COMMANDS:
        return ("dynamic", scratchpad_names)

    # Check for known completions
    if arg_value in KNOWN_COMPLETIONS:
        return ("choices", KNOWN_COMPLETIONS[arg_value])

    # Check for literal values (like "json")
    if arg_value == "json":
        return ("literal", [arg_value])

    # Check for hint args
    if arg_value in HINT_ARGS:
        return ("hint", [HINT_ARGS[arg_value]])

    # Default: no completion, show arg name as hint
    return ("hint", [arg_value])


def _build_completion_args(
    cmd_name: str,
    cmd_info: CommandInfo | None,
    scratchpad_names: list[str],
) -> list[CompletionArg]:
    """Build completion args from a CommandInfo."""
    if cmd_info is None:
        return []
    completion_args: list[CompletionArg] = []
    for pos, arg in enumerate(cmd_info.args, start=1):
        comp_type, values = _classify_arg(arg.value, cmd_name, scratchpad_names)
        completion_args.append(
            CompletionArg(
                position=pos,
                completion_type=comp_type,
                values=values,
                required=arg.required,
                description=arg.value,
            )
        )
    return completion_args


def _build_command_from_node(
    root_name: str,
    node: CommandNode,
    scratchpad_names: list[str],
) -> CommandCompletion:
    """Build a CommandCompletion from a CommandNode."""
    # Build subcommands dict
    subcommands: dict[str, CommandCompletion] = {}
    for child_name, child_node in node.children.items():
        if child_node.info:
            subcommands[child_name] = CommandCompletion(
                name=child_name,
                args=_build_completion_args(child_node.full_name, child_node.info, scratchpad_names),
                description=child_node.info.short_description,
            )

    # Build root command completion
    root_args: list[CompletionArg] = []
    root_desc = ""
    if node.info:
        root_args = _build_completion_args(root_name, node.info, scratchpad_names)
        root_desc = node.info.short_description

    return CommandCompletion(
        name=root_name,
        args=root_args,
        description=root_desc,
        subcommands=subcommands,
    )


def _apply_command_overrides(commands: dict[str, CommandCompletion], manager: Pyprland) -> None:
    """Apply special overrides for built-in commands (help, compgen, doc)."""
    all_cmd_names = sorted(commands.keys())

    # Build subcommand completions for help
    help_subcommands: dict[str, CommandCompletion] = {}
    for cmd_name, cmd in commands.items():
        if cmd.subcommands:
            help_subcommands[cmd_name] = CommandCompletion(
                name=cmd_name,
                args=[
                    CompletionArg(
                        position=1,
                        completion_type="choices",
                        values=sorted(cmd.subcommands.keys()),
                        required=False,
                        description="subcommand",
                    )
                ],
                description=f"Subcommands of {cmd_name}",
            )

    if "help" in commands:
        commands["help"] = CommandCompletion(
            name="help",
            args=[
                CompletionArg(
                    position=1,
                    completion_type="choices",
                    values=all_cmd_names,
                    required=False,
                    description="command",
                )
            ],
            description=commands["help"].description or "Show available commands or detailed help",
            subcommands=help_subcommands,
        )

    if "compgen" in commands:
        commands["compgen"] = CommandCompletion(
            name="compgen",
            args=[
                CompletionArg(
                    position=1,
                    completion_type="choices",
                    values=list(SUPPORTED_SHELLS),
                    required=True,
                    description="shell",
                ),
                CompletionArg(
                    position=2,
                    completion_type="choices",
                    values=["default"],
                    required=False,
                    description="path",
                ),
            ],
            description=commands["compgen"].description or "Generate shell completions",
        )

    if "doc" in commands:
        plugin_names = [p for p in manager.plugins if p != "pyprland"]
        commands["doc"] = CommandCompletion(
            name="doc",
            args=[
                CompletionArg(
                    position=1,
                    completion_type="choices",
                    values=sorted(plugin_names),
                    required=False,
                    description="plugin",
                )
            ],
            description=commands["doc"].description or "Show plugin documentation",
        )


def get_command_completions(manager: Pyprland) -> dict[str, CommandCompletion]:
    """Extract structured completion data from loaded plugins.

    Args:
        manager: The Pyprland manager instance with loaded plugins

    Returns:
        Dict mapping command name -> CommandCompletion (with subcommands for hierarchical commands)
    """
    scratchpad_names: list[str] = list(manager.config.get("scratchpads", {}).keys())
    command_tree = build_command_tree(get_all_commands(manager))

    commands: dict[str, CommandCompletion] = {}
    for root_name, node in command_tree.items():
        commands[root_name] = _build_command_from_node(root_name, node, scratchpad_names)

    _apply_command_overrides(commands, manager)

    return commands


================================================
FILE: pyprland/completions/generators/__init__.py
================================================
"""Shell completion generators.

Provides generator functions for each supported shell.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from .bash import generate_bash
from .fish import generate_fish
from .zsh import generate_zsh

if TYPE_CHECKING:
    from collections.abc import Callable

    from ..models import CommandCompletion

__all__ = ["GENERATORS", "generate_bash", "generate_fish", "generate_zsh"]

GENERATORS: dict[str, Callable[[dict[str, CommandCompletion]], str]] = {
    "bash": generate_bash,
    "zsh": generate_zsh,
    "fish": generate_fish,
}


================================================
FILE: pyprland/completions/generators/bash.py
================================================
"""Bash shell completion generator."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import CommandCompletion

__all__ = ["generate_bash"]


def _build_subcommand_case(cmd_name: str, cmd: CommandCompletion) -> str:
    """Build bash case statement for a command with subcommands."""
    subcmd_list = " ".join(sorted(cmd.subcommands.keys()))
    # Position 1 is subcommand selection
    subcmd_cases: list[str] = [f'                1) COMPREPLY=($(compgen -W "{subcmd_list}" -- "$cur"));;']

    # Build per-subcommand argument completions at position 2+
    for subcmd_name, subcmd in sorted(cmd.subcommands.items()):
        for arg in subcmd.args:
            if arg.completion_type in ("choices", "dynamic", "literal"):
                values_str = " ".join(arg.values)
                # Subcommand args start at position 2 (pos 1 is the subcommand itself)
                subcmd_cases.append(
                    f'                *) [[ "${{COMP_WORDS[2]}}" == "{subcmd_name}" ]] && '
                    f"[[ $pos -eq {arg.position + 1} ]] && "
                    f'COMPREPLY=($(compgen -W "{values_str}" -- "$cur"));;'
                )

    pos_block = "\n".join(subcmd_cases)
    return f"""            {cmd_name})
                case $pos in
{pos_block}
                esac
                ;;"""


def _build_help_case(cmd: CommandCompletion, all_commands: str) -> str:
    """Build bash case for help command with hierarchical completion.

    Position 1: complete with all commands
    Position 2+: complete subcommands based on the command at position 1
    """
    # Build case statements for commands that have subcommands
    subcmd_cases: list[str] = []
    for parent_name, parent_cmd in sorted(cmd.subcommands.items()):
        if parent_cmd.args:
            subcmds_str = " ".join(parent_cmd.args[0].values)
            subcmd_cases.append(f'                    {parent_name}) COMPREPLY=($(compgen -W "{subcmds_str}" -- "$cur"));;')

    subcmd_block = "\n".join(subcmd_cases) if subcmd_cases else "                    *) ;;"

    return f"""            help)
                if [[ $pos -eq 1 ]]; then
                    # Position 1: complete with all commands
                    COMPREPLY=($(compgen -W "{all_commands}" -- "$cur"))
                else
                    # Position 2+: complete subcommands based on COMP_WORDS[2]
                    case "${{COMP_WORDS[2]}}" in
{subcmd_block}
                    esac
                fi
                ;;"""


def _build_args_case(cmd_name: str, cmd: CommandCompletion) -> str | None:
    """Build bash case statement for a command with positional args."""
    pos_cases: list[str] = []
    for arg in cmd.args:
        if arg.completion_type in ("choices", "dynamic", "literal"):
            values_str = " ".join(arg.values)
            pos_cases.append(f'                {arg.position}) COMPREPLY=($(compgen -W "{values_str}" -- "$cur"));;')
        elif arg.completion_type == "file":
            pos_cases.append(f'                {arg.position}) COMPREPLY=($(compgen -f -- "$cur"));;')
        # hint and none types: no completion

    if not pos_cases:
        return None

    pos_block = "\n".join(pos_cases)
    return f"""            {cmd_name})
                case $pos in
{pos_block}
                esac
                ;;"""


def generate_bash(commands: dict[str, CommandCompletion]) -> str:
    """Generate bash completion script content.

    Args:
        commands: Dict mapping command name -> CommandCompletion

    Returns:
        The bash completion script content
    """
    cmd_list = " ".join(sorted(commands.keys()))

    # Build case statements for each command
    case_statements: list[str] = []
    for cmd_name, cmd in sorted(commands.items()):
        if cmd_name == "help":
            case_statements.append(_build_help_case(cmd, cmd_list))
        elif cmd.subcommands:
            case_statements.append(_build_subcommand_case(cmd_name, cmd))
        elif cmd.args:
            case_stmt = _build_args_case(cmd_name, cmd)
            if case_stmt:
                case_statements.append(case_stmt)

    case_block = "\n".join(case_statements) if case_statements else "            *) ;;"

    return f"""# Bash completion for pypr
# Generated by: pypr compgen bash

_pypr() {{
    local cur="${{COMP_WORDS[COMP_CWORD]}}"
    local cmd="${{COMP_WORDS[1]}}"
    local pos=$((COMP_CWORD - 1))

    if [[ $COMP_CWORD -eq 1 ]]; then
        COMPREPLY=($(compgen -W "{cmd_list}" -- "$cur"))
        return
    fi

    case "$cmd" in
{case_block}
    esac
}}

complete -F _pypr pypr
"""


================================================
FILE: pyprland/completions/generators/fish.py
================================================
"""Fish shell completion generator."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import CommandCompletion

__all__ = ["generate_fish"]

_HEADER = """# Fish completion for pypr
# Generated by: pypr compgen fish

# Disable default file completions for pypr
complete -c pypr -f

# Helper function to count args after command
function __pypr_arg_count
    set -l cmd (commandline -opc)
    math (count $cmd) - 1
end

# Main commands"""


def _build_main_commands(commands: dict[str, CommandCompletion]) -> list[str]:
    """Build main command completion lines."""
    lines: list[str] = []
    for cmd_name, cmd in sorted(commands.items()):
        desc = cmd.description.replace('"', '\\"') if cmd.description else ""
        # Add subcommand hint if applicable
        if cmd.subcommands and not cmd.args and not desc:
            subcmds = "|".join(sorted(cmd.subcommands.keys()))
            desc = f"<{subcmds}>"
        if desc:
            lines.append(f'complete -c pypr -n "__fish_use_subcommand" -a "{cmd_name}" -d "{desc}"')
        else:
            lines.append(f'complete -c pypr -n "__fish_use_subcommand" -a "{cmd_name}"')
    return lines


def _build_subcommand_completions(cmd_name: str, cmd: CommandCompletion) -> list[str]:
    """Build fish completions for a command's subcommands."""
    lines: list[str] = []
    for subcmd_name, subcmd in sorted(cmd.subcommands.items()):
        subdesc = subcmd.description.replace('"', '\\"') if subcmd.description else ""
        if subdesc:
            lines.append(
                f'complete -c pypr -n "__fish_seen_subcommand_from {cmd_name}; '
                f'and test (__pypr_arg_count) -eq 1" -a "{subcmd_name}" -d "{subdesc}"'
            )
        else:
            lines.append(
                f'complete -c pypr -n "__fish_seen_subcommand_from {cmd_name}; and test (__pypr_arg_count) -eq 1" -a "{subcmd_name}"'
            )
    return lines


def _build_help_completions(cmd: CommandCompletion, all_commands: list[str]) -> list[str]:
    """Build fish completions for help command with hierarchical completion.

    Position 1: complete with all commands
    Position 2+: complete subcommands based on the command at position 1
    """
    lines: list[str] = []

    # Position 1: complete with all command names
    all_cmds_str = " ".join(all_commands)
    lines.append(f'complete -c pypr -n "__fish_seen_subcommand_from help; and test (__pypr_arg_count) -eq 1" -a "{all_cmds_str}"')

    # Position 2+: complete subcommands for each parent command
    for parent_name, parent_cmd in sorted(cmd.subcommands.items()):
        if parent_cmd.args:
            subcmds_str = " ".join(parent_cmd.args[0].values)
            lines.append(
                f'complete -c pypr -n "__fish_seen_subcommand_from help; '
                f"and contains {parent_name} (commandline -opc); "
                f'and test (__pypr_arg_count) -eq 2" -a "{subcmds_str}"'
            )

    return lines


def _build_args_completions(cmd_name: str, cmd: CommandCompletion) -> list[str]:
    """Build fish completions for a command's positional args."""
    lines: list[str] = []
    for arg in cmd.args:
        if arg.completion_type in ("choices", "dynamic", "literal"):
            values_str = " ".join(arg.values)
            lines.append(
                f'complete -c pypr -n "__fish_seen_subcommand_from {cmd_name}; '
                f'and test (__pypr_arg_count) -eq {arg.position}" -a "{values_str}"'
            )
        elif arg.completion_type == "file":
            lines.append(f'complete -c pypr -n "__fish_seen_subcommand_from {cmd_name}; and test (__pypr_arg_count) -eq {arg.position}" -F')
        # hint type: no completion added
    return lines


def generate_fish(commands: dict[str, CommandCompletion]) -> str:
    """Generate fish completion script content.

    Args:
        commands: Dict mapping command name -> CommandCompletion

    Returns:
        The fish completion script content
    """
    lines = [_HEADER]
    lines.extend(_build_main_commands(commands))

    lines.append("")
    lines.append("# Subcommand and positional argument completions")

    all_cmd_names = sorted(commands.keys())
    for cmd_name, cmd in sorted(commands.items()):
        if cmd_name == "help":
            lines.extend(_build_help_completions(cmd, all_cmd_names))
        elif cmd.subcommands:
            lines.extend(_build_subcommand_completions(cmd_name, cmd))
        elif cmd.args:
            lines.extend(_build_args_completions(cmd_name, cmd))

    return "\n".join(lines) + "\n"


================================================
FILE: pyprland/completions/generators/zsh.py
================================================
"""Zsh shell completion generator."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import CommandCompletion

__all__ = ["generate_zsh"]


def _build_command_descriptions(commands: dict[str, CommandCompletion]) -> str:
    """Build the command descriptions block for zsh."""
    cmd_descs: list[str] = []
    for cmd_name, cmd in sorted(commands.items()):
        desc = cmd.description.replace("'", "'\\''") if cmd.description else cmd_name
        # Add subcommand hint if applicable
        if cmd.subcommands and not cmd.args:
            subcmds = "|".join(sorted(cmd.subcommands.keys()))
            desc = f"<{subcmds}> {desc}" if desc else f"<{subcmds}>"
        cmd_descs.append(f"        '{cmd_name}:{desc}'")
    return "\n".join(cmd_descs)


def _build_subcommand_case(cmd_name: str, cmd: CommandCompletion) -> str:
    """Build zsh case statement for a command with subcommands."""
    subcmd_descs: list[str] = []
    for subcmd_name, subcmd in sorted(cmd.subcommands.items()):
        subdesc = subcmd.description.replace("'", "'\\''") if subcmd.description else subcmd_name
        subcmd_descs.append(f"'{subcmd_name}:{subdesc}'")
    subcmd_desc_str = " ".join(subcmd_descs)

    return f"""                {cmd_name})
                    local -a subcmds=({subcmd_desc_str})
                    if [[ $CURRENT -eq 2 ]]; then
                        _describe 'subcommand' subcmds
                    fi
                    ;;"""


def _build_help_case(cmd: CommandCompletion) -> str:
    """Build zsh case for help command with hierarchical completion.

    Position 1: complete with all commands (reuses the commands array)
    Position 2+: complete subcommands based on the command at position 1
    """
    # Build case statements for commands that have subcommands
    subcmd_cases: list[str] = []
    for parent_name, parent_cmd in sorted(cmd.subcommands.items()):
        if parent_cmd.args:
            # Build subcommand descriptions for this parent
            subcmds_str = " ".join(f"'{val}'" for val in parent_cmd.args[0].values)
            subcmd_cases.append(f"                            {parent_name}) compadd {subcmds_str} ;;")

    subcmd_block = "\n".join(subcmd_cases) if subcmd_cases else "                            *) ;;"

    return f"""                help)
                    if [[ $CURRENT -eq 2 ]]; then
                        # Position 1: complete with all commands
                        _describe 'command' commands
                    else
                        # Position 2+: complete subcommands based on previous word
                        case $words[2] in
{subcmd_block}
                        esac
                    fi
                    ;;"""


def _build_args_case(cmd_name: str, cmd: CommandCompletion) -> str | None:
    """Build zsh case statement for a command with positional args."""
    arg_specs: list[str] = []
    for arg in cmd.args:
        pos = arg.position
        desc = arg.description.replace("'", "'\\''")

        if arg.completion_type in ("choices", "dynamic", "literal"):
            values_str = " ".join(arg.values)
            arg_specs.append(f"'{pos}:{desc}:({values_str})'")
        elif arg.completion_type == "file":
            arg_specs.append(f"'{pos}:{desc}:_files'")
        elif arg.completion_type == "hint":
            # Show description but no actual completions
            hint = arg.values[0] if arg.values else desc
            arg_specs.append(f"'{pos}:{hint}:'")

    if not arg_specs:
        return None

    args_line = " \\\n                        ".join(arg_specs)
    return f"""                {cmd_name})
 
Download .txt
gitextract_3e73ij71/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── wiki_improvement.md
│   ├── PULL_REQUEST_TEMPLATE/
│   │   └── pull_request_template.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── aur.yml
│       ├── ci.yml
│       ├── nix-setup.yml
│       ├── nix.yml
│       ├── site.yml
│       └── uv-install.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── client/
│   ├── pypr-client.c
│   ├── pypr-client.rs
│   └── pypr-rs/
│       └── Cargo.toml
├── default.nix
├── done.rst
├── examples/
│   ├── README.md
│   └── copy_conf.sh
├── flake.nix
├── hatch_build.py
├── justfile
├── package.json
├── pyprland/
│   ├── __init__.py
│   ├── adapters/
│   │   ├── __init__.py
│   │   ├── backend.py
│   │   ├── colors.py
│   │   ├── fallback.py
│   │   ├── hyprland.py
│   │   ├── menus.py
│   │   ├── niri.py
│   │   ├── proxy.py
│   │   ├── units.py
│   │   ├── wayland.py
│   │   └── xorg.py
│   ├── aioops.py
│   ├── ansi.py
│   ├── client.py
│   ├── command.py
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── discovery.py
│   │   ├── models.py
│   │   ├── parsing.py
│   │   └── tree.py
│   ├── common.py
│   ├── completions/
│   │   ├── __init__.py
│   │   ├── discovery.py
│   │   ├── generators/
│   │   │   ├── __init__.py
│   │   │   ├── bash.py
│   │   │   ├── fish.py
│   │   │   └── zsh.py
│   │   ├── handlers.py
│   │   └── models.py
│   ├── config.py
│   ├── config_loader.py
│   ├── constants.py
│   ├── debug.py
│   ├── doc.py
│   ├── gui/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── api.py
│   │   ├── frontend/
│   │   │   ├── .gitignore
│   │   │   ├── .vscode/
│   │   │   │   └── extensions.json
│   │   │   ├── README.md
│   │   │   ├── index.html
│   │   │   ├── package.json
│   │   │   ├── src/
│   │   │   │   ├── App.vue
│   │   │   │   ├── components/
│   │   │   │   │   ├── DictEditor.vue
│   │   │   │   │   ├── FieldInput.vue
│   │   │   │   │   └── PluginEditor.vue
│   │   │   │   ├── composables/
│   │   │   │   │   ├── useLocalCopy.js
│   │   │   │   │   └── useToggleMap.js
│   │   │   │   ├── main.js
│   │   │   │   ├── style.css
│   │   │   │   └── utils.js
│   │   │   └── vite.config.js
│   │   ├── server.py
│   │   └── static/
│   │       ├── assets/
│   │       │   ├── index-CX03GsX-.js
│   │       │   └── index-Dpu0NgRN.css
│   │       └── index.html
│   ├── help.py
│   ├── httpclient.py
│   ├── ipc.py
│   ├── ipc_paths.py
│   ├── logging_setup.py
│   ├── manager.py
│   ├── models.py
│   ├── plugins/
│   │   ├── __init__.py
│   │   ├── experimental.py
│   │   ├── expose.py
│   │   ├── fcitx5_switcher.py
│   │   ├── fetch_client_menu.py
│   │   ├── gamemode.py
│   │   ├── interface.py
│   │   ├── layout_center.py
│   │   ├── lost_windows.py
│   │   ├── magnify.py
│   │   ├── menubar.py
│   │   ├── mixins.py
│   │   ├── monitors/
│   │   │   ├── __init__.py
│   │   │   ├── commands.py
│   │   │   ├── layout.py
│   │   │   ├── resolution.py
│   │   │   └── schema.py
│   │   ├── protocols.py
│   │   ├── pyprland/
│   │   │   ├── __init__.py
│   │   │   ├── hyprland_core.py
│   │   │   ├── niri_core.py
│   │   │   └── schema.py
│   │   ├── scratchpads/
│   │   │   ├── __init__.py
│   │   │   ├── animations.py
│   │   │   ├── common.py
│   │   │   ├── events.py
│   │   │   ├── helpers.py
│   │   │   ├── lifecycle.py
│   │   │   ├── lookup.py
│   │   │   ├── objects.py
│   │   │   ├── schema.py
│   │   │   ├── transitions.py
│   │   │   └── windowruleset.py
│   │   ├── shift_monitors.py
│   │   ├── shortcuts_menu.py
│   │   ├── stash.py
│   │   ├── system_notifier.py
│   │   ├── toggle_dpms.py
│   │   ├── toggle_special.py
│   │   ├── wallpapers/
│   │   │   ├── __init__.py
│   │   │   ├── cache.py
│   │   │   ├── colorutils.py
│   │   │   ├── hyprpaper.py
│   │   │   ├── imageutils.py
│   │   │   ├── models.py
│   │   │   ├── online/
│   │   │   │   ├── __init__.py
│   │   │   │   └── backends/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── base.py
│   │   │   │       ├── bing.py
│   │   │   │       ├── picsum.py
│   │   │   │       ├── reddit.py
│   │   │   │       ├── unsplash.py
│   │   │   │       └── wallhaven.py
│   │   │   ├── palette.py
│   │   │   ├── templates.py
│   │   │   └── theme.py
│   │   └── workspaces_follow_focus.py
│   ├── process.py
│   ├── pypr_daemon.py
│   ├── quickstart/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── discovery.py
│   │   ├── generator.py
│   │   ├── helpers/
│   │   │   ├── __init__.py
│   │   │   ├── monitors.py
│   │   │   └── scratchpads.py
│   │   ├── questions.py
│   │   └── wizard.py
│   ├── state.py
│   ├── terminal.py
│   ├── utils.py
│   ├── validate_cli.py
│   ├── validation.py
│   └── version.py
├── pyproject.toml
├── sample_extension/
│   ├── README.md
│   ├── pypr_examples/
│   │   ├── __init__.py
│   │   └── focus_counter.py
│   └── pyproject.toml
├── scripts/
│   ├── backquote_as_links.py
│   ├── check_plugin_docs.py
│   ├── completions/
│   │   ├── README.md
│   │   ├── pypr.bash
│   │   └── pypr.zsh
│   ├── generate_codebase_overview.py
│   ├── generate_monitor_diagrams.py
│   ├── generate_plugin_docs.py
│   ├── get-pypr
│   ├── make_release
│   ├── plugin_metadata.toml
│   ├── pypr.py
│   ├── pypr.sh
│   ├── title
│   ├── update_get-pypr.sh
│   ├── update_version
│   └── v_whitelist.py
├── site/
│   ├── .vitepress/
│   │   ├── config.mjs
│   │   └── theme/
│   │       ├── custom.css
│   │       └── index.js
│   ├── Architecture.md
│   ├── Architecture_core.md
│   ├── Architecture_overview.md
│   ├── Commands.md
│   ├── Configuration.md
│   ├── Development.md
│   ├── Examples.md
│   ├── Getting-started.md
│   ├── InstallVirtualEnvironment.md
│   ├── Menu.md
│   ├── MultipleConfigurationFiles.md
│   ├── Nix.md
│   ├── Optimizations.md
│   ├── Plugins.md
│   ├── Troubleshooting.md
│   ├── Variables.md
│   ├── components/
│   │   ├── CommandList.vue
│   │   ├── ConfigBadges.vue
│   │   ├── ConfigTable.vue
│   │   ├── EngineDefaults.vue
│   │   ├── EngineList.vue
│   │   ├── PluginCommands.vue
│   │   ├── PluginConfig.vue
│   │   ├── PluginList.vue
│   │   ├── configHelpers.js
│   │   ├── jsonLoader.js
│   │   └── usePluginData.js
│   ├── expose.md
│   ├── fcitx5_switcher.md
│   ├── fetch_client_menu.md
│   ├── filters.md
│   ├── gamemode.md
│   ├── generated/
│   │   └── generted_files.keep_me
│   ├── index.md
│   ├── layout_center.md
│   ├── lost_windows.md
│   ├── magnify.md
│   ├── make_version.sh
│   ├── menubar.md
│   ├── monitors.md
│   ├── scratchpads.md
│   ├── scratchpads_advanced.md
│   ├── scratchpads_nonstandard.md
│   ├── shift_monitors.md
│   ├── shortcuts_menu.md
│   ├── sidebar.json
│   ├── stash.md
│   ├── system_notifier.md
│   ├── toggle_dpms.md
│   ├── toggle_special.md
│   ├── versions/
│   │   ├── 2.3.5/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.3.6,7/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.3.8/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.0/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.1+/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gbar.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.6/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.4.7/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.5.x/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── bar.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 2.6.2/
│   │   │   ├── Development.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   └── PluginList.vue
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 3.0.0/
│   │   │   ├── Architecture.md
│   │   │   ├── Architecture_core.md
│   │   │   ├── Architecture_overview.md
│   │   │   ├── Commands.md
│   │   │   ├── Configuration.md
│   │   │   ├── Development.md
│   │   │   ├── Examples.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   ├── ConfigBadges.vue
│   │   │   │   ├── ConfigTable.vue
│   │   │   │   ├── EngineDefaults.vue
│   │   │   │   ├── PluginCommands.vue
│   │   │   │   ├── PluginConfig.vue
│   │   │   │   ├── PluginList.vue
│   │   │   │   ├── configHelpers.js
│   │   │   │   └── usePluginData.js
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── generated/
│   │   │   │   ├── expose.json
│   │   │   │   ├── fcitx5_switcher.json
│   │   │   │   ├── fetch_client_menu.json
│   │   │   │   ├── index.json
│   │   │   │   ├── layout_center.json
│   │   │   │   ├── lost_windows.json
│   │   │   │   ├── magnify.json
│   │   │   │   ├── menu.json
│   │   │   │   ├── menubar.json
│   │   │   │   ├── monitors.json
│   │   │   │   ├── pyprland.json
│   │   │   │   ├── scratchpads.json
│   │   │   │   ├── shift_monitors.json
│   │   │   │   ├── shortcuts_menu.json
│   │   │   │   ├── system_notifier.json
│   │   │   │   ├── toggle_dpms.json
│   │   │   │   ├── toggle_special.json
│   │   │   │   ├── wallpapers.json
│   │   │   │   └── workspaces_follow_focus.json
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   ├── wallpapers_online.md
│   │   │   ├── wallpapers_templates.md
│   │   │   └── workspaces_follow_focus.md
│   │   ├── 3.1.1/
│   │   │   ├── Architecture.md
│   │   │   ├── Architecture_core.md
│   │   │   ├── Architecture_overview.md
│   │   │   ├── Commands.md
│   │   │   ├── Configuration.md
│   │   │   ├── Development.md
│   │   │   ├── Examples.md
│   │   │   ├── Getting-started.md
│   │   │   ├── InstallVirtualEnvironment.md
│   │   │   ├── Menu.md
│   │   │   ├── MultipleConfigurationFiles.md
│   │   │   ├── Nix.md
│   │   │   ├── Optimizations.md
│   │   │   ├── Plugins.md
│   │   │   ├── Troubleshooting.md
│   │   │   ├── Variables.md
│   │   │   ├── components/
│   │   │   │   ├── CommandList.vue
│   │   │   │   ├── ConfigBadges.vue
│   │   │   │   ├── ConfigTable.vue
│   │   │   │   ├── EngineDefaults.vue
│   │   │   │   ├── EngineList.vue
│   │   │   │   ├── PluginCommands.vue
│   │   │   │   ├── PluginConfig.vue
│   │   │   │   ├── PluginList.vue
│   │   │   │   ├── configHelpers.js
│   │   │   │   ├── jsonLoader.js
│   │   │   │   └── usePluginData.js
│   │   │   ├── expose.md
│   │   │   ├── fcitx5_switcher.md
│   │   │   ├── fetch_client_menu.md
│   │   │   ├── filters.md
│   │   │   ├── gamemode.md
│   │   │   ├── generated/
│   │   │   │   ├── expose.json
│   │   │   │   ├── fcitx5_switcher.json
│   │   │   │   ├── fetch_client_menu.json
│   │   │   │   ├── gamemode.json
│   │   │   │   ├── index.json
│   │   │   │   ├── layout_center.json
│   │   │   │   ├── lost_windows.json
│   │   │   │   ├── magnify.json
│   │   │   │   ├── menu.json
│   │   │   │   ├── menubar.json
│   │   │   │   ├── monitors.json
│   │   │   │   ├── pyprland.json
│   │   │   │   ├── scratchpads.json
│   │   │   │   ├── shift_monitors.json
│   │   │   │   ├── shortcuts_menu.json
│   │   │   │   ├── system_notifier.json
│   │   │   │   ├── toggle_dpms.json
│   │   │   │   ├── toggle_special.json
│   │   │   │   ├── wallpapers.json
│   │   │   │   └── workspaces_follow_focus.json
│   │   │   ├── index.md
│   │   │   ├── layout_center.md
│   │   │   ├── lost_windows.md
│   │   │   ├── magnify.md
│   │   │   ├── menubar.md
│   │   │   ├── monitors.md
│   │   │   ├── scratchpads.md
│   │   │   ├── scratchpads_advanced.md
│   │   │   ├── scratchpads_nonstandard.md
│   │   │   ├── shift_monitors.md
│   │   │   ├── shortcuts_menu.md
│   │   │   ├── sidebar.json
│   │   │   ├── system_notifier.md
│   │   │   ├── toggle_dpms.md
│   │   │   ├── toggle_special.md
│   │   │   ├── wallpapers.md
│   │   │   ├── wallpapers_online.md
│   │   │   ├── wallpapers_templates.md
│   │   │   └── workspaces_follow_focus.md
│   │   └── 3.2.1/
│   │       ├── Architecture.md
│   │       ├── Architecture_core.md
│   │       ├── Architecture_overview.md
│   │       ├── Commands.md
│   │       ├── Configuration.md
│   │       ├── Development.md
│   │       ├── Examples.md
│   │       ├── Getting-started.md
│   │       ├── InstallVirtualEnvironment.md
│   │       ├── Menu.md
│   │       ├── MultipleConfigurationFiles.md
│   │       ├── Nix.md
│   │       ├── Optimizations.md
│   │       ├── Plugins.md
│   │       ├── Troubleshooting.md
│   │       ├── Variables.md
│   │       ├── components/
│   │       │   ├── CommandList.vue
│   │       │   ├── ConfigBadges.vue
│   │       │   ├── ConfigTable.vue
│   │       │   ├── EngineDefaults.vue
│   │       │   ├── EngineList.vue
│   │       │   ├── PluginCommands.vue
│   │       │   ├── PluginConfig.vue
│   │       │   ├── PluginList.vue
│   │       │   ├── configHelpers.js
│   │       │   ├── jsonLoader.js
│   │       │   └── usePluginData.js
│   │       ├── expose.md
│   │       ├── fcitx5_switcher.md
│   │       ├── fetch_client_menu.md
│   │       ├── filters.md
│   │       ├── gamemode.md
│   │       ├── generated/
│   │       │   ├── expose.json
│   │       │   ├── fcitx5_switcher.json
│   │       │   ├── fetch_client_menu.json
│   │       │   ├── gamemode.json
│   │       │   ├── index.json
│   │       │   ├── layout_center.json
│   │       │   ├── lost_windows.json
│   │       │   ├── magnify.json
│   │       │   ├── menu.json
│   │       │   ├── menubar.json
│   │       │   ├── monitors.json
│   │       │   ├── pyprland.json
│   │       │   ├── scratchpads.json
│   │       │   ├── shift_monitors.json
│   │       │   ├── shortcuts_menu.json
│   │       │   ├── stash.json
│   │       │   ├── system_notifier.json
│   │       │   ├── toggle_dpms.json
│   │       │   ├── toggle_special.json
│   │       │   ├── wallpapers.json
│   │       │   └── workspaces_follow_focus.json
│   │       ├── index.md
│   │       ├── layout_center.md
│   │       ├── lost_windows.md
│   │       ├── magnify.md
│   │       ├── menubar.md
│   │       ├── monitors.md
│   │       ├── scratchpads.md
│   │       ├── scratchpads_advanced.md
│   │       ├── scratchpads_nonstandard.md
│   │       ├── shift_monitors.md
│   │       ├── shortcuts_menu.md
│   │       ├── sidebar.json
│   │       ├── stash.md
│   │       ├── system_notifier.md
│   │       ├── toggle_dpms.md
│   │       ├── toggle_special.md
│   │       ├── wallpapers.md
│   │       ├── wallpapers_online.md
│   │       ├── wallpapers_templates.md
│   │       └── workspaces_follow_focus.md
│   ├── wallpapers.md
│   ├── wallpapers_online.md
│   ├── wallpapers_templates.md
│   └── workspaces_follow_focus.md
├── systemd-unit/
│   └── pyprland.service
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── sample_config.toml
│   ├── test_adapters_fallback.py
│   ├── test_ansi.py
│   ├── test_command.py
│   ├── test_command_registry.py
│   ├── test_common_types.py
│   ├── test_common_utils.py
│   ├── test_completions.py
│   ├── test_config.py
│   ├── test_event_signatures.py
│   ├── test_external_plugins.py
│   ├── test_http.py
│   ├── test_interface.py
│   ├── test_ipc.py
│   ├── test_load_all.py
│   ├── test_monitors_commands.py
│   ├── test_monitors_layout.py
│   ├── test_monitors_resolution.py
│   ├── test_plugin_expose.py
│   ├── test_plugin_fetch_client_menu.py
│   ├── test_plugin_layout_center.py
│   ├── test_plugin_lost_windows.py
│   ├── test_plugin_magnify.py
│   ├── test_plugin_menubar.py
│   ├── test_plugin_monitor.py
│   ├── test_plugin_scratchpads.py
│   ├── test_plugin_shift_monitors.py
│   ├── test_plugin_shortcuts_menu.py
│   ├── test_plugin_stash.py
│   ├── test_plugin_system_notifier.py
│   ├── test_plugin_toggle_dpms.py
│   ├── test_plugin_toggle_special.py
│   ├── test_plugin_wallpapers.py
│   ├── test_plugin_workspaces_follow_focus.py
│   ├── test_process.py
│   ├── test_pyprland.py
│   ├── test_scratchpad_vulnerabilities.py
│   ├── test_string_template.py
│   ├── test_wallpapers_cache.py
│   ├── test_wallpapers_colors.py
│   ├── test_wallpapers_imageutils.py
│   ├── testtools.py
│   └── vreg/
│       ├── 01_client_id_change.py
│       └── run_tests.sh
├── tickets.rst
└── tox.ini
Download .txt
SYMBOL INDEX (1870 symbols across 178 files)

FILE: client/pypr-client.c
  function main (line 20) | int main(int argc, char *argv[]) {

FILE: client/pypr-client.rs
  constant EXIT_SUCCESS (line 7) | const EXIT_SUCCESS: i32 = 0;
  constant EXIT_USAGE_ERROR (line 8) | const EXIT_USAGE_ERROR: i32 = 1;
  constant EXIT_ENV_ERROR (line 9) | const EXIT_ENV_ERROR: i32 = 2;
  constant EXIT_CONNECTION_ERROR (line 10) | const EXIT_CONNECTION_ERROR: i32 = 3;
  constant EXIT_COMMAND_ERROR (line 11) | const EXIT_COMMAND_ERROR: i32 = 4;
  function run (line 13) | fn run() -> Result<(), i32> {
  function main (line 98) | fn main() {

FILE: hatch_build.py
  function _find_compiler (line 24) | def _find_compiler() -> str:
  function _try_compile (line 39) | def _try_compile(cc: str, source: Path, *, static: bool = False) -> tupl...
  class NativeClientBuildHook (line 76) | class NativeClientBuildHook(BuildHookInterface):
    method initialize (line 81) | def initialize(self, version: str, build_data: dict) -> None:  # noqa:...

FILE: pyprland/adapters/backend.py
  class EnvironmentBackend (line 22) | class EnvironmentBackend(ABC):
    method __init__ (line 30) | def __init__(self, state: SharedState) -> None:
    method get_clients (line 39) | async def get_clients(
    method get_monitors (line 57) | async def get_monitors(self, *, log: Logger, include_disabled: bool = ...
    method parse_event (line 66) | def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any...
    method get_monitor_props (line 74) | async def get_monitor_props(
    method execute (line 101) | async def execute(self, command: str | list | dict, *, log: Logger, **...
    method execute_json (line 111) | async def execute_json(self, command: str, *, log: Logger, **kwargs: A...
    method execute_batch (line 121) | async def execute_batch(self, commands: list[str], *, log: Logger) -> ...
    method notify (line 130) | async def notify(self, message: str, duration: int, color: str, *, log...
    method notify_info (line 140) | async def notify_info(self, message: str, duration: int = 5000, *, log...
    method notify_error (line 150) | async def notify_error(self, message: str, duration: int = 5000, *, lo...
    method get_client_props (line 160) | async def get_client_props(
    method focus_window (line 209) | async def focus_window(self, address: str, *, log: Logger) -> bool:
    method move_window_to_workspace (line 221) | async def move_window_to_workspace(
    method pin_window (line 243) | async def pin_window(self, address: str, *, log: Logger) -> bool:
    method close_window (line 255) | async def close_window(self, address: str, *, silent: bool = True, log...
    method resize_window (line 268) | async def resize_window(self, address: str, width: int, height: int, *...
    method move_window (line 282) | async def move_window(self, address: str, x: int, y: int, *, log: Logg...
    method toggle_floating (line 296) | async def toggle_floating(self, address: str, *, log: Logger) -> bool:
    method set_keyword (line 308) | async def set_keyword(self, keyword_command: str, *, log: Logger) -> b...

FILE: pyprland/adapters/colors.py
  function convert_color (line 4) | def convert_color(description: str) -> str:

FILE: pyprland/adapters/fallback.py
  function make_monitor_info (line 14) | def make_monitor_info(  # noqa: PLR0913  # pylint: disable=too-many-argu...
  class FallbackBackend (line 73) | class FallbackBackend(EnvironmentBackend):
    method get_clients (line 84) | async def get_clients(
    method parse_event (line 106) | def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any...
    method execute (line 118) | async def execute(self, command: str | list | dict, *, log: Logger, **...
    method execute_batch (line 132) | async def execute_batch(self, commands: list[str], *, log: Logger) -> ...
    method execute_json (line 141) | async def execute_json(self, command: str, *, log: Logger, **kwargs: A...
    method notify (line 155) | async def notify(
    method is_available (line 185) | async def is_available(cls) -> bool:
    method _check_command (line 196) | async def _check_command(cls, command: str) -> bool:
    method _run_monitor_command (line 215) | async def _run_monitor_command(

FILE: pyprland/adapters/hyprland.py
  class HyprlandBackend (line 17) | class HyprlandBackend(EnvironmentBackend):
    method _format_command (line 20) | def _format_command(self, command_list: list[str] | list[list[str]], d...
    method execute (line 31) | async def execute(self, command: str | list | dict, *, log: Logger, **...
    method execute_json (line 69) | async def execute_json(self, command: str, *, log: Logger, **kwargs: A...
    method get_clients (line 81) | async def get_clients(
    method get_monitors (line 105) | async def get_monitors(self, *, log: Logger, include_disabled: bool = ...
    method execute_batch (line 115) | async def execute_batch(self, commands: list[str], *, log: Logger) -> ...
    method parse_event (line 137) | def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any...
    method notify (line 149) | async def notify(self, message: str, duration: int = DEFAULT_NOTIFICAT...
    method notify_info (line 161) | async def notify_info(self, message: str, duration: int = DEFAULT_NOTI...
    method notify_error (line 172) | async def notify_error(self, message: str, duration: int = DEFAULT_NOT...
    method _notify_impl (line 183) | async def _notify_impl(self, text: str, duration: int, color: str, ico...

FILE: pyprland/adapters/menus.py
  class MenuEngine (line 21) | class MenuEngine:
    method __init__ (line 31) | def __init__(self, extra_parameters: str) -> None:
    method is_available (line 41) | def is_available(cls) -> bool:
    method run (line 49) | async def run(self, choices: Iterable[str], prompt: str = "") -> str:
  function _menu (line 84) | def _menu(proc: str, params: str) -> type[MenuEngine]:
  function init (line 117) | async def init(force_engine: str | None = None, extra_parameters: str = ...
  class MenuMixin (line 148) | class MenuMixin:
    method ensure_menu_configured (line 177) | async def ensure_menu_configured(self) -> None:
    method on_reload (line 184) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...

FILE: pyprland/adapters/niri.py
  function get_niri_transform (line 35) | def get_niri_transform(value: str, default: int = 0) -> int:
  function niri_output_to_monitor_info (line 48) | def niri_output_to_monitor_info(name: str, data: dict[str, Any]) -> Moni...
  class NiriBackend (line 118) | class NiriBackend(EnvironmentBackend):
    method parse_event (line 121) | def parse_event(self, raw_data: str, *, log: Logger) -> tuple[str, Any...
    method execute (line 142) | async def execute(self, command: str | list | dict, *, log: Logger, **...
    method execute_json (line 172) | async def execute_json(self, command: str, *, log: Logger, **kwargs: A...
    method get_clients (line 186) | async def get_clients(
    method _map_niri_client (line 210) | def _map_niri_client(self, niri_client: dict[str, Any]) -> ClientInfo:
    method get_monitors (line 239) | async def get_monitors(self, *, log: Logger, include_disabled: bool = ...
    method execute_batch (line 249) | async def execute_batch(self, commands: list[str], *, log: Logger) -> ...
    method notify (line 272) | async def notify(
    method focus_window (line 295) | async def focus_window(self, address: str, *, log: Logger) -> bool:
    method move_window_to_workspace (line 304) | async def move_window_to_workspace(
    method pin_window (line 325) | async def pin_window(self, address: str, *, log: Logger) -> bool:
    method close_window (line 335) | async def close_window(self, address: str, *, silent: bool = True, log...
    method resize_window (line 345) | async def resize_window(self, address: str, width: int, height: int, *...
    method move_window (line 357) | async def move_window(self, address: str, x: int, y: int, *, log: Logg...
    method toggle_floating (line 369) | async def toggle_floating(self, address: str, *, log: Logger) -> bool:
    method set_keyword (line 379) | async def set_keyword(self, keyword_command: str, *, log: Logger) -> b...

FILE: pyprland/adapters/proxy.py
  class BackendProxy (line 19) | class BackendProxy:
    method __init__ (line 31) | def __init__(self, backend: "EnvironmentBackend", log: Logger) -> None:
    method execute (line 44) | async def execute(self, command: str | list | dict, **kwargs: Any) -> ...
    method execute_json (line 56) | async def execute_json(self, command: str, **kwargs: Any) -> Any:
    method execute_batch (line 68) | async def execute_batch(self, commands: list[str]) -> None:
    method get_clients (line 78) | async def get_clients(
    method get_monitors (line 96) | async def get_monitors(self, include_disabled: bool = False) -> list[M...
    method get_monitor_props (line 107) | async def get_monitor_props(
    method get_client_props (line 123) | async def get_client_props(
    method notify (line 143) | async def notify(self, message: str, duration: int = 5000, color: str ...
    method notify_info (line 153) | async def notify_info(self, message: str, duration: int = 5000) -> None:
    method notify_error (line 162) | async def notify_error(self, message: str, duration: int = 5000) -> None:
    method focus_window (line 173) | async def focus_window(self, address: str) -> bool:
    method move_window_to_workspace (line 184) | async def move_window_to_workspace(
    method pin_window (line 203) | async def pin_window(self, address: str) -> bool:
    method close_window (line 214) | async def close_window(self, address: str, silent: bool = True) -> bool:
    method resize_window (line 226) | async def resize_window(self, address: str, width: int, height: int) -...
    method move_window (line 239) | async def move_window(self, address: str, x: int, y: int) -> bool:  # ...
    method toggle_floating (line 252) | async def toggle_floating(self, address: str) -> bool:
    method set_keyword (line 263) | async def set_keyword(self, keyword_command: str) -> bool:
    method parse_event (line 276) | def parse_event(self, raw_data: str) -> tuple[str, Any] | None:

FILE: pyprland/adapters/units.py
  function convert_monitor_dimension (line 11) | def convert_monitor_dimension(size: int | str, ref_value: int, monitor: ...
  function convert_coords (line 37) | def convert_coords(coords: str, monitor: MonitorInfo) -> list[int]:

FILE: pyprland/adapters/wayland.py
  class WaylandBackend (line 11) | class WaylandBackend(FallbackBackend):
    method is_available (line 20) | async def is_available(cls) -> bool:
    method get_monitors (line 28) | async def get_monitors(self, *, log: Logger, include_disabled: bool = ...
    method _parse_wlr_randr_output (line 46) | def _parse_wlr_randr_output(self, output: str, include_disabled: bool,...
    method _parse_output_section (line 92) | def _parse_output_section(  # noqa: C901  # pylint: disable=too-many-l...

FILE: pyprland/adapters/xorg.py
  class XorgBackend (line 20) | class XorgBackend(FallbackBackend):
    method is_available (line 28) | async def is_available(cls) -> bool:
    method get_monitors (line 36) | async def get_monitors(self, *, log: Logger, include_disabled: bool = ...
    method _parse_xrandr_output (line 54) | def _parse_xrandr_output(  # pylint: disable=too-many-locals

FILE: pyprland/aioops.py
  class AsyncFile (line 42) | class AsyncFile:
    method __init__ (line 49) | def __init__(self, file: io.TextIOWrapper) -> None:
    method readlines (line 52) | async def readlines(self) -> list[str]:
    method read (line 56) | async def read(self) -> str:
    method __aenter__ (line 60) | async def __aenter__(self) -> Self:
    method __aexit__ (line 63) | async def __aexit__(
  function aiopen (line 72) | async def aiopen(*args, **kwargs) -> AsyncIterator[AsyncFile]:
  function aiexists (line 77) | async def aiexists(*args, **kwargs) -> bool:
  function aiisdir (line 81) | async def aiisdir(*args, **kwargs) -> bool:
  function aiisfile (line 85) | async def aiisfile(*args, **kwargs) -> bool:
  function ailistdir (line 89) | async def ailistdir(*args, **kwargs) -> list[str]:  # type: ignore[no-re...
  function aiunlink (line 93) | async def aiunlink(*args, **kwargs) -> None:  # type: ignore[no-redef, m...
  function airmtree (line 98) | async def airmtree(path: str) -> None:
  function airmdir (line 109) | async def airmdir(path: str) -> None:
  function is_process_running (line 120) | async def is_process_running(name: str) -> bool:
  function graceful_cancel_tasks (line 142) | async def graceful_cancel_tasks(
  class DebouncedTask (line 178) | class DebouncedTask:
    method __init__ (line 196) | def __init__(self, ignore_window: float = 3.0) -> None:
    method schedule (line 207) | def schedule(self, coro_func: Callable[[], Coroutine[Any, Any, Any]], ...
    method set_ignore_window (line 236) | def set_ignore_window(self) -> None:
    method cancel (line 244) | def cancel(self) -> None:
  class TaskManager (line 251) | class TaskManager:
    method __init__ (line 284) | def __init__(
    method running (line 303) | def running(self) -> bool:
    method start (line 307) | def start(self) -> None:
    method create (line 312) | def create(self, coro: Coroutine[Any, Any, Any], *, key: str | None = ...
    method _wrap_task (line 331) | async def _wrap_task(self, coro: Coroutine[Any, Any, Any]) -> None:
    method cancel_keyed (line 345) | def cancel_keyed(self, key: str) -> bool:
    method sleep (line 360) | async def sleep(self, duration: float) -> bool:
    method stop (line 380) | async def stop(self) -> None:

FILE: pyprland/ansi.py
  function should_colorize (line 47) | def should_colorize(stream: TextIO | None = None) -> bool:
  function colorize (line 70) | def colorize(text: str, *codes: str) -> str:
  function make_style (line 85) | def make_style(*codes: str) -> tuple[str, str]:
  class LogStyles (line 99) | class LogStyles:
  class HandlerStyles (line 107) | class HandlerStyles:

FILE: pyprland/client.py
  function _get_config_file_path (line 16) | def _get_config_file_path() -> str:
  function run_client (line 32) | async def run_client() -> None:

FILE: pyprland/command.py
  function use_param (line 19) | def use_param(txt: str, optional_value: Literal[False] = ...) -> str: ...
  function use_param (line 23) | def use_param(txt: str, optional_value: Literal[True]) -> str | bool: ...
  function use_param (line 26) | def use_param(txt: str, optional_value: bool = False) -> str | bool:
  function _run (line 53) | def _run(invoke_daemon: bool) -> None:
  function main (line 78) | def main() -> None:

FILE: pyprland/commands/discovery.py
  function extract_commands_from_object (line 17) | def extract_commands_from_object(obj: object, source: str) -> list[Comma...
  function get_client_commands (line 58) | def get_client_commands() -> list[CommandInfo]:
  function get_all_commands (line 81) | def get_all_commands(manager: Pyprland) -> dict[str, CommandInfo]:

FILE: pyprland/commands/models.py
  class CommandArg (line 23) | class CommandArg:
  class CommandInfo (line 31) | class CommandInfo:
  class CommandNode (line 42) | class CommandNode:

FILE: pyprland/commands/parsing.py
  function normalize_command_name (line 15) | def normalize_command_name(cmd: str) -> str:
  function parse_docstring (line 30) | def parse_docstring(docstring: str) -> tuple[list[CommandArg], str, str]:

FILE: pyprland/commands/tree.py
  function get_parent_prefixes (line 16) | def get_parent_prefixes(commands: dict[str, str] | Iterable[str]) -> set...
  function get_display_name (line 47) | def get_display_name(cmd_name: str, parent_prefixes: set[str]) -> str:
  function build_command_tree (line 70) | def build_command_tree(commands: dict[str, CommandInfo]) -> dict[str, Co...

FILE: pyprland/completions/discovery.py
  function _classify_arg (line 28) | def _classify_arg(
  function _build_completion_args (line 67) | def _build_completion_args(
  function _build_command_from_node (line 90) | def _build_command_from_node(
  function _apply_command_overrides (line 121) | def _apply_command_overrides(commands: dict[str, CommandCompletion], man...
  function get_command_completions (line 198) | def get_command_completions(manager: Pyprland) -> dict[str, CommandCompl...

FILE: pyprland/completions/generators/bash.py
  function _build_subcommand_case (line 13) | def _build_subcommand_case(cmd_name: str, cmd: CommandCompletion) -> str:
  function _build_help_case (line 39) | def _build_help_case(cmd: CommandCompletion, all_commands: str) -> str:
  function _build_args_case (line 67) | def _build_args_case(cmd_name: str, cmd: CommandCompletion) -> str | None:
  function generate_bash (line 89) | def generate_bash(commands: dict[str, CommandCompletion]) -> str:

FILE: pyprland/completions/generators/fish.py
  function _build_main_commands (line 27) | def _build_main_commands(commands: dict[str, CommandCompletion]) -> list...
  function _build_subcommand_completions (line 43) | def _build_subcommand_completions(cmd_name: str, cmd: CommandCompletion)...
  function _build_help_completions (line 60) | def _build_help_completions(cmd: CommandCompletion, all_commands: list[s...
  function _build_args_completions (line 85) | def _build_args_completions(cmd_name: str, cmd: CommandCompletion) -> li...
  function generate_fish (line 101) | def generate_fish(commands: dict[str, CommandCompletion]) -> str:

FILE: pyprland/completions/generators/zsh.py
  function _build_command_descriptions (line 13) | def _build_command_descriptions(commands: dict[str, CommandCompletion]) ...
  function _build_subcommand_case (line 26) | def _build_subcommand_case(cmd_name: str, cmd: CommandCompletion) -> str:
  function _build_help_case (line 42) | def _build_help_case(cmd: CommandCompletion) -> str:
  function _build_args_case (line 71) | def _build_args_case(cmd_name: str, cmd: CommandCompletion) -> str | None:
  function generate_zsh (line 98) | def generate_zsh(commands: dict[str, CommandCompletion]) -> str:

FILE: pyprland/completions/handlers.py
  function get_default_path (line 23) | def get_default_path(shell: str) -> str:
  function _get_success_message (line 35) | def _get_success_message(shell: str, output_path: str, used_default: boo...
  function _parse_compgen_args (line 70) | def _parse_compgen_args(args: str) -> tuple[bool, str, str | None]:
  function handle_compgen (line 97) | def handle_compgen(manager: Pyprland, args: str) -> tuple[bool, str]:

FILE: pyprland/completions/models.py
  class CompletionArg (line 47) | class CompletionArg:
  class CommandCompletion (line 58) | class CommandCompletion:

FILE: pyprland/config.py
  function coerce_to_bool (line 33) | def coerce_to_bool(value: ConfigValueType | None, default: bool = False)...
  class SchemaAwareMixin (line 59) | class SchemaAwareMixin:
    method __init_schema__ (line 75) | def __init_schema__(self) -> None:
    method set_schema (line 79) | def set_schema(self, schema: ConfigItems) -> None:
    method _get_raw (line 87) | def _get_raw(self, name: str) -> ConfigValueType:
    method get (line 95) | def get(self, name: str) -> ConfigValueType | None: ...
    method get (line 98) | def get(self, name: str, default: None) -> ConfigValueType | None: ...
    method get (line 101) | def get(self, name: str, default: ConfigValueType) -> ConfigValueType:...
    method get (line 103) | def get(self, name: str, default: ConfigValueType | None = None) -> Co...
    method get_bool (line 120) | def get_bool(self, name: str, default: bool = False) -> bool:
    method get_int (line 139) | def get_int(self, name: str, default: int = 0) -> int:
    method get_float (line 158) | def get_float(self, name: str, default: float = 0.0) -> float:
    method get_str (line 177) | def get_str(self, name: str, default: str = "") -> str:
    method has_explicit (line 192) | def has_explicit(self, name: str) -> bool:
  class Configuration (line 208) | class Configuration(SchemaAwareMixin, dict):
    method __init__ (line 214) | def __init__(
    method _get_raw (line 235) | def _get_raw(self, name: str) -> ConfigValueType:
    method get (line 241) | def get(self, name: str, default: ConfigValueType | None = None) -> Co...
    method iter_subsections (line 253) | def iter_subsections(self) -> Iterator[tuple[str, dict[str, Any]]]:

FILE: pyprland/config_loader.py
  function resolve_config_path (line 41) | def resolve_config_path(config_filename: str) -> Path:
  function load_toml (line 53) | def load_toml(path: Path) -> dict[str, Any]:
  function load_toml_directory (line 67) | def load_toml_directory(directory: Path) -> dict[str, Any]:
  function load_config (line 77) | def load_config(config_filename: str | None = None) -> dict[str, Any]:
  class ConfigLoader (line 110) | class ConfigLoader:
    method __init__ (line 120) | def __init__(self, log: logging.Logger) -> None:
    method config (line 131) | def config(self) -> dict[str, Any]:
    method load (line 135) | async def load(self, config_filename: str = "") -> dict[str, Any]:
    method _open_config (line 152) | async def _open_config(self, config_filename: str = "") -> dict[str, A...
    method _load_config_directory (line 208) | def _load_config_directory(self, directory: Path) -> dict[str, Any]:
    method _load_config_file (line 221) | def _load_config_file(self, fname: Path) -> dict[str, Any]:

FILE: pyprland/debug.py
  class _DebugState (line 12) | class _DebugState:
  function is_debug (line 21) | def is_debug() -> bool:
  function set_debug (line 26) | def set_debug(value: bool) -> None:

FILE: pyprland/doc.py
  function _c (line 23) | def _c(text: str, *codes: str) -> str:
  function _format_default (line 30) | def _format_default(value: Any) -> str:
  function _get_plugin_description (line 43) | def _get_plugin_description(plugin: Plugin) -> str:
  function format_plugin_list (line 51) | def format_plugin_list(plugins: dict[str, Plugin]) -> str:
  function format_plugin_doc (line 82) | def format_plugin_doc(
  function _format_config_section (line 137) | def _format_config_section(schema: ConfigItems) -> list[str]:
  function _format_field_brief (line 161) | def _format_field_brief(field: ConfigField) -> list[str]:
  function _format_field_status (line 194) | def _format_field_status(field: ConfigField) -> str:
  function _format_field_choices (line 203) | def _format_field_choices(field: ConfigField) -> list[str]:
  function _format_field_children (line 214) | def _format_field_children(field: ConfigField) -> list[str]:
  function format_config_field_doc (line 226) | def format_config_field_doc(plugin_name: str, field: ConfigField) -> str:

FILE: pyprland/gui/__init__.py
  function _find_free_port (line 47) | def _find_free_port() -> int:
  function _write_lock (line 54) | def _write_lock(port: int) -> None:
  function _remove_lock (line 60) | def _remove_lock() -> None:
  function _check_existing_instance (line 71) | def _check_existing_instance() -> int | None:
  function _wait_for_server (line 104) | def _wait_for_server(port: int) -> bool:
  function _start_server (line 120) | def _start_server(port: int) -> None:
  function _daemonize_server (line 132) | def _daemonize_server(port: int) -> bool:
  function main (line 157) | def main() -> None:

FILE: pyprland/gui/api.py
  function _field_to_dict (line 58) | def _field_to_dict(field: ConfigField) -> dict[str, Any]:
  function _schema_to_list (line 82) | def _schema_to_list(schema: ConfigItems | None) -> list[dict[str, Any]]:
  function _plugin_to_dict (line 89) | def _plugin_to_dict(plugin: PluginInfo) -> dict[str, Any]:
  function get_plugins_schema (line 110) | def get_plugins_schema() -> list[dict[str, Any]]:
  function get_config (line 121) | def get_config() -> dict[str, Any]:
  function validate_config (line 144) | def validate_config(config: dict[str, Any]) -> list[str]:
  function _toml_key (line 195) | def _toml_key(key: str) -> str:
  function _generate_plugin_toml (line 204) | def _generate_plugin_toml(plugin_name: str, plugin_data: dict[str, Any])...
  function _write_subtable (line 250) | def _write_subtable(lines: list[str], prefix: str, data: dict[str, Any])...
  function _generate_variables_toml (line 269) | def _generate_variables_toml(variables: dict[str, Any]) -> str:
  function _generate_main_toml (line 283) | def _generate_main_toml(
  function _backup_conf_d_file (line 325) | def _backup_conf_d_file(path: Path) -> Path | None:
  function _find_conf_d (line 335) | def _find_conf_d(config_path: Path) -> tuple[list[str], Path | None]:
  function _write_confd_plugins (line 351) | def _write_confd_plugins(
  function _cleanup_composite_files (line 392) | def _cleanup_composite_files(
  function save_config (line 411) | def save_config(config: dict[str, Any]) -> dict[str, Any]:
  function _save_result (line 469) | def _save_result(
  function _generate_plugin_toml_raw (line 487) | def _generate_plugin_toml_raw(existing_data: dict[str, Any]) -> str:
  function _send_ipc_command (line 523) | async def _send_ipc_command(command: str) -> str:
  function apply_config (line 542) | async def apply_config(config: dict[str, Any]) -> dict[str, Any]:

FILE: pyprland/gui/frontend/src/composables/useLocalCopy.js
  function useLocalCopy (line 12) | function useLocalCopy(propGetter, emit, eventName) {

FILE: pyprland/gui/frontend/src/composables/useToggleMap.js
  function useToggleMap (line 8) | function useToggleMap() {

FILE: pyprland/gui/frontend/src/utils.js
  function tryParseJson (line 11) | function tryParseJson(raw, fallback) {
  function formatValue (line 19) | function formatValue(val) {
  function groupFields (line 30) | function groupFields(fields) {

FILE: pyprland/gui/server.py
  function handle_get_plugins (line 27) | async def handle_get_plugins(_request: web.Request) -> web.Response:
  function handle_get_config (line 33) | async def handle_get_config(_request: web.Request) -> web.Response:
  function handle_validate (line 39) | async def handle_validate(request: web.Request) -> web.Response:
  function handle_save (line 47) | async def handle_save(request: web.Request) -> web.Response:
  function handle_apply (line 55) | async def handle_apply(request: web.Request) -> web.Response:
  function handle_spa_fallback (line 71) | async def handle_spa_fallback(request: web.Request) -> web.FileResponse:
  function create_app (line 96) | def create_app() -> web.Application:

FILE: pyprland/gui/static/assets/index-CX03GsX-.js
  function t (line 1) | function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.r...
  function n (line 1) | function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}
  function e (line 1) | function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1...
  function ue (line 1) | function ue(e){if(d(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],...
  function me (line 1) | function me(e){let t={};return e.replace(pe,``).split(de).forEach(e=>{if...
  function k (line 1) | function k(e){let t=``;if(g(e))t=e;else if(d(e))for(let n=0;n<e.length;n...
  function _e (line 1) | function _e(e){return!!e||e===``}
  function ve (line 1) | function ve(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&...
  function ye (line 1) | function ye(e,t){if(e===t)return!0;let n=m(e),r=m(t);if(n||r)return n&&r...
  function be (line 1) | function be(e,t){return e.findIndex(e=>ye(e,t))}
  method constructor (line 1) | constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effect...
  method active (line 1) | get active(){return this._active}
  method pause (line 1) | pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=...
  method resume (line 1) | resume(){if(this._active&&this._isPaused){this._isPaused=!1;let e,t;if(t...
  method run (line 1) | run(e){if(this._active){let t=j;try{return j=this,e()}finally{j=t}}}
  method on (line 1) | on(){++this._on===1&&(this.prevScope=j,j=this)}
  method off (line 1) | off(){this._on>0&&--this._on===0&&(j=this.prevScope,this.prevScope=void 0)}
  method stop (line 1) | stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects....
  function Te (line 1) | function Te(){return j}
  method constructor (line 1) | constructor(e){this.fn=e,this.deps=void 0,this.depsTail=void 0,this.flag...
  method pause (line 1) | pause(){this.flags|=64}
  method resume (line 1) | resume(){this.flags&64&&(this.flags&=-65,Ee.has(this)&&(Ee.delete(this),...
  method notify (line 1) | notify(){this.flags&2&&!(this.flags&32)||this.flags&8||je(this)}
  method run (line 1) | run(){if(!(this.flags&1))return this.fn();this.flags|=2,We(this),Pe(this...
  method stop (line 1) | stop(){if(this.flags&1){for(let e=this.deps;e;e=e.nextDep)Re(e);this.dep...
  method trigger (line 1) | trigger(){this.flags&64?Ee.add(this):this.scheduler?this.scheduler():thi...
  method runIfDirty (line 1) | runIfDirty(){Ie(this)&&this.run()}
  method dirty (line 1) | get dirty(){return Ie(this)}
  function je (line 1) | function je(e,t=!1){if(e.flags|=8,t){e.next=Ae,Ae=e;return}e.next=ke,ke=e}
  function Me (line 1) | function Me(){Oe++}
  function Ne (line 1) | function Ne(){if(--Oe>0)return;if(Ae){let e=Ae;for(Ae=void 0;e;){let t=e...
  function Pe (line 1) | function Pe(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveL...
  function Fe (line 1) | function Fe(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version...
  function Ie (line 1) | function Ie(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.versi...
  function Le (line 1) | function Le(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersio...
  function Re (line 1) | function Re(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,...
  function ze (line 1) | function ze(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void...
  function He (line 1) | function He(){Ve.push(Be),Be=!1}
  function Ue (line 1) | function Ue(){let e=Ve.pop();Be=e===void 0?!0:e}
  function We (line 1) | function We(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=M;M=void 0;...
  method constructor (line 1) | constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextD...
  method constructor (line 1) | constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,thi...
  method track (line 1) | track(e){if(!M||!Be||M===this.computed)return;let t=this.activeLink;if(t...
  method trigger (line 1) | trigger(e){this.version++,Ge++,this.notify(e)}
  method notify (line 1) | notify(e){Me();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.s...
  function Je (line 1) | function Je(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!...
  function N (line 1) | function N(e,t,n){if(Be&&M){let t=Ye.get(e);t||Ye.set(e,t=new Map);let r...
  function $e (line 1) | function $e(e,t,n,r,i,a){let o=Ye.get(e);if(!o){Ge++;return}let s=e=>{e&...
  function et (line 1) | function et(e){let t=F(e);return t===e?t:(N(t,`iterate`,Qe),P(e)?t:t.map...
  function tt (line 1) | function tt(e){return N(e=F(e),`iterate`,Qe),e}
  function nt (line 1) | function nt(e,t){return zt(e)?Ht(Rt(e)?I(t):t):I(t)}
  method [Symbol.iterator] (line 1) | [Symbol.iterator](){return it(this,Symbol.iterator,e=>nt(this,e))}
  method concat (line 1) | concat(...e){return et(this).concat(...e.map(e=>d(e)?et(e):e))}
  method entries (line 1) | entries(){return it(this,`entries`,e=>(e[1]=nt(this,e[1]),e))}
  method every (line 1) | every(e,t){return ot(this,`every`,e,t,void 0,arguments)}
  method filter (line 1) | filter(e,t){return ot(this,`filter`,e,t,e=>e.map(e=>nt(this,e)),arguments)}
  method find (line 1) | find(e,t){return ot(this,`find`,e,t,e=>nt(this,e),arguments)}
  method findIndex (line 1) | findIndex(e,t){return ot(this,`findIndex`,e,t,void 0,arguments)}
  method findLast (line 1) | findLast(e,t){return ot(this,`findLast`,e,t,e=>nt(this,e),arguments)}
  method findLastIndex (line 1) | findLastIndex(e,t){return ot(this,`findLastIndex`,e,t,void 0,arguments)}
  method forEach (line 1) | forEach(e,t){return ot(this,`forEach`,e,t,void 0,arguments)}
  method includes (line 1) | includes(...e){return ct(this,`includes`,e)}
  method indexOf (line 1) | indexOf(...e){return ct(this,`indexOf`,e)}
  method join (line 1) | join(e){return et(this).join(e)}
  method lastIndexOf (line 1) | lastIndexOf(...e){return ct(this,`lastIndexOf`,e)}
  method map (line 1) | map(e,t){return ot(this,`map`,e,t,void 0,arguments)}
  method pop (line 1) | pop(){return lt(this,`pop`)}
  method push (line 1) | push(...e){return lt(this,`push`,e)}
  method reduce (line 1) | reduce(e,...t){return st(this,`reduce`,e,t)}
  method reduceRight (line 1) | reduceRight(e,...t){return st(this,`reduceRight`,e,t)}
  method shift (line 1) | shift(){return lt(this,`shift`)}
  method some (line 1) | some(e,t){return ot(this,`some`,e,t,void 0,arguments)}
  method splice (line 1) | splice(...e){return lt(this,`splice`,e)}
  method toReversed (line 1) | toReversed(){return et(this).toReversed()}
  method toSorted (line 1) | toSorted(e){return et(this).toSorted(e)}
  method toSpliced (line 1) | toSpliced(...e){return et(this).toSpliced(...e)}
  method unshift (line 1) | unshift(...e){return lt(this,`unshift`,e)}
  method values (line 1) | values(){return it(this,`values`,e=>nt(this,e))}
  function it (line 1) | function it(e,t,n){let r=tt(e),i=r[t]();return r!==e&&!P(e)&&(i._next=i....
  function ot (line 1) | function ot(e,t,n,r,i,a){let o=tt(e),s=o!==e&&!P(e),c=o[t];if(c!==at[t])...
  function st (line 1) | function st(e,t,n,r){let i=tt(e),a=i!==e&&!P(e),o=n,s=!1;i!==e&&(a?(s=r....
  function ct (line 1) | function ct(e,t,n){let r=F(e);N(r,`iterate`,Qe);let i=r[t](...n);return(...
  function lt (line 1) | function lt(e,t,n=[]){He(),Me();let r=F(e)[t].apply(e,n);return Ne(),Ue(...
  function ft (line 1) | function ft(e){_(e)||(e=String(e));let t=F(this);return N(t,`has`,e),t.h...
  method constructor (line 1) | constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}
  method get (line 1) | get(e,t,n){if(t===`__v_skip`)return e.__v_skip;let r=this._isReadonly,i=...
  method constructor (line 1) | constructor(e=!1){super(!1,e)}
  method set (line 1) | set(e,t,n,r){let i=e[t],a=d(e)&&w(t);if(!this._isShallow){let e=zt(i);if...
  method deleteProperty (line 1) | deleteProperty(e,t){let n=u(e,t),r=e[t],i=Reflect.deleteProperty(e,t);re...
  method has (line 1) | has(e,t){let n=Reflect.has(e,t);return(!_(t)||!dt.has(t))&&N(e,`has`,t),n}
  method ownKeys (line 1) | ownKeys(e){return N(e,`iterate`,d(e)?`length`:Xe),Reflect.ownKeys(e)}
  method constructor (line 1) | constructor(e=!1){super(!0,e)}
  method set (line 1) | set(e,t){return!0}
  method deleteProperty (line 1) | deleteProperty(e,t){return!0}
  function xt (line 1) | function xt(e,t,n){return function(...r){let i=this.__v_raw,a=F(i),o=f(a...
  function St (line 1) | function St(e){return function(...t){return e===`delete`?!1:e===`clear`?...
  function Ct (line 1) | function Ct(e,t){let n={get(n){let r=this.__v_raw,i=F(r),a=F(n);e||(D(n,...
  function wt (line 1) | function wt(e,t){let n=Ct(e,t);return(t,r,i)=>r===`__v_isReactive`?!e:r=...
  function Mt (line 1) | function Mt(e){switch(e){case`Object`:case`Array`:return 1;case`Map`:cas...
  function Nt (line 1) | function Nt(e){return e.__v_skip||!Object.isExtensible(e)?0:Mt(S(e))}
  function Pt (line 1) | function Pt(e){return zt(e)?e:Lt(e,!1,gt,Tt,Ot)}
  function Ft (line 1) | function Ft(e){return Lt(e,!1,vt,Et,kt)}
  function It (line 1) | function It(e){return Lt(e,!0,_t,Dt,At)}
  function Lt (line 1) | function Lt(e,t,n,r,i){if(!v(e)||e.__v_raw&&!(t&&e.__v_isReactive))retur...
  function Rt (line 1) | function Rt(e){return zt(e)?Rt(e.__v_raw):!!(e&&e.__v_isReactive)}
  function zt (line 1) | function zt(e){return!!(e&&e.__v_isReadonly)}
  function P (line 1) | function P(e){return!!(e&&e.__v_isShallow)}
  function Bt (line 1) | function Bt(e){return e?!!e.__v_raw:!1}
  function F (line 1) | function F(e){let t=e&&e.__v_raw;return t?F(t):e}
  function Vt (line 1) | function Vt(e){return!u(e,`__v_skip`)&&Object.isExtensible(e)&&O(e,`__v_...
  function L (line 1) | function L(e){return e?e.__v_isRef===!0:!1}
  function R (line 1) | function R(e){return Ut(e,!1)}
  function Ut (line 1) | function Ut(e,t){return L(e)?e:new Wt(e,t)}
  method constructor (line 1) | constructor(e,t){this.dep=new qe,this.__v_isRef=!0,this.__v_isShallow=!1...
  method value (line 1) | get value(){return this.dep.track(),this._value}
  method value (line 1) | set value(e){let t=this._rawValue,n=this.__v_isShallow||P(e)||zt(e);e=n?...
  function z (line 1) | function z(e){return L(e)?e.value:e}
  function Kt (line 1) | function Kt(e){return Rt(e)?e:new Proxy(e,Gt)}
  method constructor (line 1) | constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=n...
  method notify (line 1) | notify(){if(this.flags|=16,!(this.flags&8)&&M!==this)return je(this,!0),!0}
  method value (line 1) | get value(){let e=this.dep.track();return Le(this),e&&(e.version=this.de...
  method value (line 1) | set value(e){this.setter&&this.setter(e)}
  function Jt (line 1) | function Jt(e,t,n=!1){let r,i;return h(e)?r=e:(r=e.get,i=e.set),new qt(r...
  function Qt (line 1) | function Qt(e,t=!1,n=Zt){if(n){let t=Xt.get(n);t||Xt.set(n,t=[]),t.push(...
  function $t (line 1) | function $t(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJo...
  function en (line 1) | function en(e,t=1/0,n){if(t<=0||!v(e)||e.__v_skip||(n||=new Map,(n.get(e...
  function tn (line 1) | function tn(e,t,n,r){try{return r?e(...r):e()}catch(e){rn(e,t,n)}}
  function nn (line 1) | function nn(e,t,n,r){if(h(e)){let i=tn(e,t,n,r);return i&&y(i)&&i.catch(...
  function rn (line 1) | function rn(e,n,r,i=!0){let a=n?n.vnode:null,{errorHandler:o,throwUnhand...
  function an (line 1) | function an(e,t,n,r=!0,i=!1){if(i)throw e;console.error(e)}
  function fn (line 1) | function fn(e){let t=dn||un;return e?t.then(this?e.bind(this):e):t}
  function pn (line 1) | function pn(e){let t=on+1,n=B.length;for(;t<n;){let r=t+n>>>1,i=B[r],a=y...
  function mn (line 1) | function mn(e){if(!(e.flags&1)){let t=yn(e),n=B[B.length-1];!n||!(e.flag...
  function hn (line 1) | function hn(){dn||=un.then(bn)}
  function gn (line 1) | function gn(e){d(e)?sn.push(...e):cn&&e.id===-1?cn.splice(ln+1,0,e):e.fl...
  function _n (line 1) | function _n(e,t,n=on+1){for(;n<B.length;n++){let t=B[n];if(t&&t.flags&2)...
  function vn (line 1) | function vn(e){if(sn.length){let e=[...new Set(sn)].sort((e,t)=>yn(e)-yn...
  function bn (line 1) | function bn(e){try{for(on=0;on<B.length;on++){let e=B[on];e&&!(e.flags&8...
  function Sn (line 1) | function Sn(e){let t=V;return V=e,xn=e&&e.type.__scopeId||null,t}
  function Cn (line 1) | function Cn(e,t=V,n){if(!t||e._n)return e;let r=(...n)=>{r._d&&ki(-1);le...
  function wn (line 1) | function wn(e,n){if(V===null)return e;let r=la(V),i=e.dirs||=[];for(let ...
  function Tn (line 1) | function Tn(e,t,n,r){let i=e.dirs,a=t&&t.dirs;for(let o=0;o<i.length;o++...
  function En (line 1) | function En(e,t){if(Q){let n=Q.provides,r=Q.parent&&Q.parent.provides;r=...
  function Dn (line 1) | function Dn(e,t,n=!1){let r=Ji();if(r||Pr){let i=Pr?Pr._context.provides...
  function An (line 1) | function An(e,t,n){return jn(e,t,n)}
  function jn (line 1) | function jn(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),...
  function Mn (line 1) | function Mn(e,t,n){let r=this.proxy,i=g(e)?e.includes(`.`)?Nn(r,e):()=>r...
  function Nn (line 1) | function Nn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;e<n.l...
  function Ln (line 1) | function Ln(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Ln(e.compone...
  function Rn (line 1) | function Rn(e){e.ids=[e.ids[0]+ e.ids[2]+++`-`,0,0]}
  function zn (line 1) | function zn(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))...
  function Vn (line 1) | function Vn(e,n,r,a,o=!1){if(d(e)){e.forEach((e,t)=>Vn(e,n&&(d(n)?n[t]:n...
  function Hn (line 1) | function Hn(e){let t=Bn.get(e);t&&(t.flags|=8,Bn.delete(e))}
  function Gn (line 1) | function Gn(e,t){qn(e,`a`,t)}
  function Kn (line 1) | function Kn(e,t){qn(e,`da`,t)}
  function qn (line 1) | function qn(e,t,n=Q){let r=e.__wdc||=()=>{let t=n;for(;t;){if(t.isDeacti...
  function Jn (line 1) | function Jn(e,t,n,r){let i=Yn(t,e,r,!0);nr(()=>{c(r[t],i)},n)}
  function Yn (line 1) | function Yn(e,t,n=Q,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||=(...r)...
  function or (line 1) | function or(e,t=Q){Yn(`ec`,e,t)}
  function cr (line 1) | function cr(e,t){return ur(sr,e,!0,t)||e}
  function ur (line 1) | function ur(e,t,n=!0,r=!1){let i=V||Q;if(i){let n=i.type;if(e===sr){let ...
  function dr (line 1) | function dr(e,t){return e&&(e[t]||e[T(t)]||e[ie(T(t))])}
  function H (line 1) | function H(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||g(e)){let n=o&&Rt(e),r=...
  method get (line 1) | get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,pr...
  method set (line 1) | set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return mr(a,n)?(a[n]=r,!...
  method has (line 1) | has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,typ...
  method defineProperty (line 1) | defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.va...
  function gr (line 1) | function gr(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}
  function vr (line 1) | function vr(e){let t=Sr(e),n=e.proxy,i=e.ctx;_r=!1,t.beforeCreate&&br(t....
  function yr (line 1) | function yr(e,t,n=r){d(e)&&(e=Dr(e));for(let n in e){let r=e[n],i;i=v(r)...
  function br (line 1) | function br(e,t,n){nn(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}
  function xr (line 1) | function xr(e,t,n,r){let i=r.includes(`.`)?Nn(n,r):()=>n[r];if(g(e)){let...
  function Sr (line 1) | function Sr(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCach...
  function Cr (line 1) | function Cr(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&Cr(e,a,n,!0),i&&i.f...
  function Tr (line 1) | function Tr(e,t){return t?e?function(){return s(h(e)?e.call(this,this):e...
  function Er (line 1) | function Er(e,t){return Or(Dr(e),Dr(t))}
  function Dr (line 1) | function Dr(e){if(d(e)){let t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n]...
  function U (line 1) | function U(e,t){return e?[...new Set([].concat(e,t))]:t}
  function Or (line 1) | function Or(e,t){return e?s(Object.create(null),e,t):t}
  function kr (line 1) | function kr(e,t){return e?d(e)&&d(t)?[...new Set([...e,...t])]:s(Object....
  function Ar (line 1) | function Ar(e,t){if(!e)return t;if(!t)return e;let n=s(Object.create(nul...
  function jr (line 1) | function jr(){return{app:null,config:{isNativeTag:i,performance:!1,globa...
  function Nr (line 1) | function Nr(e,t){return function(n,r=null){h(n)||(n=s({},n)),r!=null&&!v...
  function Ir (line 1) | function Ir(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r...
  function Rr (line 1) | function Rr(e,t,n=!1){let r=n?Lr:t.emitsCache,i=r.get(e);if(i!==void 0)r...
  function zr (line 1) | function zr(e,t){return!e||!a(t)?!1:(t=t.slice(2).replace(/Once$/,``),u(...
  function Br (line 1) | function Br(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],s...
  function Ur (line 1) | function Ur(e,t,n){let{props:r,children:i,component:a}=e,{props:o,childr...
  function Wr (line 1) | function Wr(e,t,n){let r=Object.keys(t);if(r.length!==Object.keys(e).len...
  function Gr (line 1) | function Gr(e,t,n){let r=e[n],i=t[n];return n===`style`&&v(r)&&v(i)?!ye(...
  function Kr (line 1) | function Kr({vnode:e,parent:t,suspense:n},r){for(;t;){let n=t.subTree;if...
  function Xr (line 1) | function Xr(e,t,n,r=!1){let i={},a=Jr();e.propsDefaults=Object.create(nu...
  function Zr (line 1) | function Zr(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=F(i),[...
  function Qr (line 1) | function Qr(e,n,r,i){let[a,o]=e.propsOptions,s=!1,c;if(n)for(let t in n)...
  function $r (line 1) | function $r(e,t,n,r,i,a){let o=e[n];if(o!=null){let e=u(o,`default`);if(...
  function ti (line 1) | function ti(e,r,i=!1){let a=i?ei:r.propsCache,o=a.get(e);if(o)return o;l...
  function ni (line 1) | function ni(e){return e[0]!==`$`&&!ee(e)}
  function di (line 1) | function di(e){return fi(e)}
  function fi (line 1) | function fi(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp...
  function pi (line 1) | function pi({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n...
  function mi (line 1) | function mi({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33...
  function hi (line 1) | function hi(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}
  function gi (line 1) | function gi(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(le...
  function _i (line 1) | function _i(e){let t=e.slice(),n=[0],r,i,a,o,s,c=e.length;for(r=0;r<c;r+...
  function vi (line 1) | function vi(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyn...
  function yi (line 1) | function yi(e){if(e)for(let t=0;t<e.length;t++)e[t].flags|=8}
  function bi (line 1) | function bi(e){if(e.placeholder)return e.placeholder;let t=e.component;r...
  function Si (line 1) | function Si(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects....
  function q (line 1) | function q(e=!1){Ei.push(K=e?null:[])}
  function Di (line 1) | function Di(){Ei.pop(),K=Ei[Ei.length-1]||null}
  function ki (line 1) | function ki(e,t=!1){Oi+=e,e<0&&K&&t&&(K.hasOnce=!0)}
  function Ai (line 1) | function Ai(e){return e.dynamicChildren=Oi>0?K||n:null,Di(),Oi>0&&K&&K.p...
  function J (line 1) | function J(e,t,n,r,i,a){return Ai(Y(e,t,n,r,i,a,!0))}
  function ji (line 1) | function ji(e,t,n,r,i){return Ai(X(e,t,n,r,i,!0))}
  function Mi (line 1) | function Mi(e){return e?e.__v_isVNode===!0:!1}
  function Ni (line 1) | function Ni(e,t){return e.type===t.type&&e.key===t.key}
  function Y (line 1) | function Y(e,t=null,n=null,r=0,i=null,a=e===G?0:1,o=!1,s=!1){let c={__v_...
  function Ii (line 1) | function Ii(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===lr)&&(e=wi),Mi(...
  function Li (line 1) | function Li(e){return e?Bt(e)||Yr(e)?s({},e):e:null}
  function Ri (line 1) | function Ri(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,tran...
  function zi (line 1) | function zi(e=` `,t=0){return X(Ci,null,e,t)}
  function Z (line 1) | function Z(e=``,t=!1){return t?(q(),ji(wi,null,e)):X(wi,null,e)}
  function Bi (line 1) | function Bi(e){return e==null||typeof e==`boolean`?X(wi):d(e)?X(G,null,e...
  function Vi (line 1) | function Vi(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ri(e)}
  function Hi (line 1) | function Hi(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))...
  function Ui (line 1) | function Ui(...e){let t={};for(let n=0;n<e.length;n++){let r=e[n];for(le...
  function Wi (line 1) | function Wi(e,t,n,r=null){nn(e,t,7,[n,r])}
  function qi (line 1) | function qi(e,n,r){let i=e.type,a=(n?n.appContext:e.appContext)||Gi,o={u...
  function $i (line 1) | function $i(e){return e.vnode.shapeFlag&4}
  function ta (line 1) | function ta(e,t=!1,n=!1){t&&Xi(t);let{props:r,children:i}=e.vnode,a=$i(e...
  function na (line 1) | function na(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=...
  function ra (line 1) | function ra(e,t,n){h(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=...
  function oa (line 1) | function oa(e,t,n){let i=e.type;if(!e.render){if(!t&&ia&&!i.render){let ...
  method get (line 1) | get(e,t){return N(e,`get`,``),e[t]}
  function ca (line 1) | function ca(e){return{attrs:new Proxy(e.attrs,sa),slots:e.slots,emit:e.e...
  function la (line 1) | function la(e){return e.exposed?e.exposeProxy||=new Proxy(Kt(Vt(e.expose...
  function ua (line 1) | function ua(e,t=!0){return h(e)?e.displayName||e.name:e.name||t&&e.__name}
  function da (line 1) | function da(e){return h(e)&&`__vccOpts`in e}
  method setScopeId (line 1) | setScopeId(e,t){e.setAttribute(t,``)}
  method insertStaticContent (line 1) | insertStaticContent(e,t,n,r,i,a){let o=n?n.previousSibling:t.lastChild;i...
  function Sa (line 1) | function Sa(e,t,n){let r=e[xa];r&&(t=(t?[t,...r]:[...r]).join(` `)),t==n...
  function Da (line 1) | function Da(e,t,n){let r=e.style,i=g(n),a=!1;if(n&&!i){if(t)if(g(t))for(...
  function ka (line 1) | function ka(e,t,n){if(d(n))n.forEach(n=>ka(e,t,n));else if(n??=``,t.star...
  function Ma (line 1) | function Ma(e,t){let n=ja[t];if(n)return n;let r=T(t);if(r!==`filter`&&r...
  function Pa (line 1) | function Pa(e,t,n,r,i,a=ge(t)){r&&t.startsWith(`xlink:`)?n==null?e.remov...
  function Fa (line 1) | function Fa(e,t,n,r,i){if(t===`innerHTML`||t===`textContent`){n!=null&&(...
  function Ia (line 1) | function Ia(e,t,n,r){e.addEventListener(t,n,r)}
  function La (line 1) | function La(e,t,n,r){e.removeEventListener(t,n,r)}
  function za (line 1) | function za(e,t,n,r,i=null){let a=e[Ra]||(e[Ra]={}),o=a[t];if(r&&o)o.val...
  function Va (line 1) | function Va(e){let t;if(Ba.test(e)){t={};let n;for(;n=e.match(Ba);)e=e.s...
  function Ga (line 1) | function Ga(e,t){let n=e=>{if(!e._vts)e._vts=Date.now();else if(e._vts<=...
  function Ka (line 1) | function Ka(e,t){if(d(t)){let n=e.stopImmediatePropagation;return e.stop...
  function Ya (line 1) | function Ya(e,t,n,r){if(r)return!!(t===`innerHTML`||t===`textContent`||t...
  function Xa (line 1) | function Xa(e,t){let n=e._def.props;if(!n)return!1;let r=T(t);return Arr...
  function Qa (line 1) | function Qa(e){e.target.composing=!0}
  function $a (line 1) | function $a(e){let t=e.target;t.composing&&(t.composing=!1,t.dispatchEve...
  function to (line 1) | function to(e,t,n){return t&&(e=e.trim()),n&&(e=se(e)),e}
  method created (line 1) | created(e,{modifiers:{lazy:t,trim:n,number:r}},i){e[eo]=Za(i);let a=r||i...
  method mounted (line 1) | mounted(e,{value:t}){e.value=t??``}
  method beforeUpdate (line 1) | beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:i,number:a}},o...
  method created (line 1) | created(e,{value:t,modifiers:{number:n}},r){let i=p(t);Ia(e,`change`,()=...
  method mounted (line 1) | mounted(e,{value:t}){io(e,t)}
  method beforeUpdate (line 1) | beforeUpdate(e,t,n){e[eo]=Za(n)}
  method updated (line 1) | updated(e,{value:t}){e._assigning||io(e,t)}
  function io (line 1) | function io(e,t){let n=e.multiple,r=d(t);if(!(n&&!r&&!p(t))){for(let i=0...
  function ao (line 1) | function ao(e){return`_value`in e?e._value:e.value}
  function mo (line 1) | function mo(){return po||=di(fo)}
  function go (line 1) | function go(e){if(e instanceof SVGElement)return`svg`;if(typeof MathMLEl...
  function _o (line 1) | function _o(e){return g(e)?document.querySelector(e):e}
  function vo (line 1) | function vo(e,t,n){let r=R({...e()}),i=!1;return An(e,e=>{i=!0,r.value={...
  function yo (line 1) | function yo(){let e=Pt({});return{state:e,toggle:t=>{e[t]=!e[t]}}}
  function Co (line 1) | function Co(e,t){try{return JSON.parse(e)}catch{return t===void 0?e:t}}
  function wo (line 1) | function wo(e){return e==null?``:typeof e==`object`?JSON.stringify(e):St...
  function To (line 1) | function To(e){let t={};for(let n of e){let e=n.category||`general`;t[e]...
  method setup (line 1) | setup(e,{emit:t}){let n=e,r=vo(()=>n.value,t,`update:value`),{state:i,to...
  method setup (line 4) | setup(e,{emit:t}){let n=e,r=t,i=$(()=>(n.field.type||`str`).toLowerCase(...
  method setup (line 4) | setup(e,{emit:t}){let n=e,r=vo(()=>n.config,t,`update:config`),{state:i,...
  method setup (line 4) | setup(e){let t=R(!0),n=R(!1),r=R(``),i=R(`success`),a=R([]),o=R([]),s=R(...

FILE: pyprland/help.py
  function get_commands_help (line 17) | def get_commands_help(manager: Pyprland) -> dict[str, tuple[str, str]]:
  function get_help (line 55) | def get_help(manager: Pyprland) -> str:
  function get_command_help (line 100) | def get_command_help(manager: Pyprland, command: str) -> str:

FILE: pyprland/httpclient.py
  class FallbackClientError (line 54) | class FallbackClientError(Exception):
  class FallbackClientTimeout (line 58) | class FallbackClientTimeout:
    method __init__ (line 61) | def __init__(self, total: float = 30) -> None:
  class FallbackResponse (line 70) | class FallbackResponse:
    method __init__ (line 73) | def __init__(self, status: int, url: str, data: bytes) -> None:
    method json (line 85) | async def json(self) -> Any:
    method read (line 93) | async def read(self) -> bytes:
    method raise_for_status (line 101) | def raise_for_status(self) -> None:
  class _AsyncRequestContext (line 108) | class _AsyncRequestContext:
    method __init__ (line 111) | def __init__(self, coro_fn: Callable[[], Coroutine[Any, Any, FallbackR...
    method __aenter__ (line 119) | async def __aenter__(self) -> FallbackResponse:
    method __aexit__ (line 123) | async def __aexit__(self, *args: object) -> None:
  class FallbackClientSession (line 127) | class FallbackClientSession:
    method __init__ (line 130) | def __init__(
    method get (line 154) | def get(
    method close (line 209) | async def close(self) -> None:
    method __aenter__ (line 213) | async def __aenter__(self) -> Self:
    method __aexit__ (line 217) | async def __aexit__(self, *args: object) -> None:
  function reset_fallback_warning (line 222) | def reset_fallback_warning() -> None:

FILE: pyprland/ipc.py
  class _IpcState (line 35) | class _IpcState:
  function hyprctl_connection (line 50) | async def hyprctl_connection(logger: Logger) -> AsyncGenerator[tuple[asy...
  function niri_connection (line 70) | async def niri_connection(logger: Logger) -> AsyncGenerator[tuple[asynci...
  function set_notify_method (line 93) | def set_notify_method(method: str) -> None:
  function get_event_stream (line 102) | async def get_event_stream() -> tuple[asyncio.StreamReader, asyncio.Stre...
  function retry_on_reset (line 122) | def retry_on_reset(func: Callable) -> Callable:
  function niri_request (line 153) | async def niri_request(payload: str | dict | list, logger: Logger) -> JS...
  function get_response (line 165) | async def get_response(command: bytes, logger: Logger) -> JSONResponse:
  function init (line 181) | def init() -> None:

FILE: pyprland/ipc_paths.py
  function init_ipc_folder (line 55) | def init_ipc_folder() -> None:

FILE: pyprland/logging_setup.py
  class LogObjects (line 16) | class LogObjects:
  function init_logger (line 22) | def init_logger(filename: str | None = None, force_debug: bool = False) ...
  function get_logger (line 71) | def get_logger(name: str = "pypr", level: int | None = None) -> logging....

FILE: pyprland/manager.py
  function remove_duplicate (line 49) | def remove_duplicate(names: list[str]) -> Callable:
  class Pyprland (line 75) | class Pyprland:  # pylint: disable=too-many-instance-attributes
    method __init__ (line 91) | def __init__(self) -> None:
    method initialize (line 125) | async def initialize(self) -> None:
    method _select_fallback_backend (line 140) | async def _select_fallback_backend(self) -> None:
    method _load_single_plugin (line 164) | async def _load_single_plugin(self, name: str, init: bool) -> bool:
    method _init_plugin (line 203) | async def _init_plugin(self, name: str) -> None:
    method __load_plugins_config (line 227) | async def __load_plugins_config(self, init: bool = True) -> None:
    method load_config (line 257) | async def load_config(self, init: bool = True) -> None:
    method plain_log_handler (line 290) | def plain_log_handler(self, plugin: Plugin, name: str, params: tuple[s...
    method colored_log_handler (line 300) | def colored_log_handler(self, plugin: Plugin, name: str, params: tuple...
    method _run_plugin_handler (line 311) | async def _run_plugin_handler(self, plugin: Plugin, full_name: str, pa...
    method _run_plugin_handler_with_result (line 347) | async def _run_plugin_handler_with_result(
    method _dispatch_to_plugin (line 366) | async def _dispatch_to_plugin(self, plugin: Plugin, full_name: str, pa...
    method _handle_single_plugin (line 397) | async def _handle_single_plugin(self, plugin: Plugin, full_name: str, ...
    method _call_handler (line 417) | async def _call_handler(self, full_name: str, *params: str, notify: st...
    method read_events_loop (line 453) | async def read_events_loop(self) -> None:
    method exit_plugins (line 474) | async def exit_plugins(self) -> None:
    method _abort_plugins (line 479) | async def _abort_plugins(self, writer: asyncio.StreamWriter) -> None:
    method _has_handler (line 503) | def _has_handler(self, handler_name: str) -> bool:
    method _resolve_handler (line 514) | def _resolve_handler(self, tokens: list[str]) -> tuple[str, list[str]]:
    method _process_plugin_command (line 538) | async def _process_plugin_command(self, data: str) -> str:
    method _handle_exit_cleanup (line 577) | async def _handle_exit_cleanup(self, writer: asyncio.StreamWriter) -> ...
    method read_command (line 588) | async def read_command(self, reader: asyncio.StreamReader, writer: asy...
    method serve (line 613) | async def serve(self) -> None:
    method _execute_queued_task (line 618) | async def _execute_queued_task(self, name: str, task: partial) -> None:
    method _plugin_runner_loop (line 636) | async def _plugin_runner_loop(self, name: str) -> None:
    method plugins_runner (line 661) | async def plugins_runner(self) -> None:
    method run (line 668) | async def run(self) -> None:

FILE: pyprland/models.py
  class RetensionTimes (line 24) | class RetensionTimes(float, Enum):
  class WorkspaceDf (line 31) | class WorkspaceDf(TypedDict):
  class MonitorInfo (line 67) | class MonitorInfo(TypedDict):
  class VersionInfo (line 98) | class VersionInfo:
  class PyprError (line 106) | class PyprError(BaseException):
  class ExitCode (line 111) | class ExitCode(IntEnum):
  class ResponsePrefix (line 122) | class ResponsePrefix(StrEnum):
  class ReloadReason (line 129) | class ReloadReason(Enum):
  class Environment (line 141) | class Environment(StrEnum):

FILE: pyprland/plugins/experimental.py
  class Extension (line 7) | class Extension(Plugin, environments=[Environment.HYPRLAND]):

FILE: pyprland/plugins/expose.py
  class Extension (line 8) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method on_reload (line 17) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method exposed_clients (line 23) | def exposed_clients(self) -> list[ClientInfo]:
    method run_expose (line 29) | async def run_expose(self) -> None:

FILE: pyprland/plugins/fcitx5_switcher.py
  class Extension (line 8) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method event_activewindowv2 (line 22) | async def event_activewindowv2(self, _addr: str) -> None:

FILE: pyprland/plugins/fetch_client_menu.py
  class Extension (line 10) | class Extension(MenuMixin, Plugin, environments=[Environment.HYPRLAND]):
    method on_reload (line 30) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method _center_window_on_monitor (line 35) | async def _center_window_on_monitor(self, address: str) -> None:
    method run_unfetch_client (line 109) | async def run_unfetch_client(self) -> None:
    method run_fetch_client_menu (line 119) | async def run_fetch_client_menu(self) -> None:

FILE: pyprland/plugins/gamemode.py
  class Extension (line 17) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method __init__ (line 48) | def __init__(self, name: str) -> None:
    method _matches_pattern (line 55) | def _matches_pattern(self, window_class: str) -> bool:
    method exit (line 67) | async def exit(self) -> None:
    method _update_gamemode (line 71) | async def _update_gamemode(self) -> None:
    method _enable_gamemode (line 100) | async def _enable_gamemode(self, notify: bool = True) -> None:
    method _disable_gamemode (line 128) | async def _disable_gamemode(self, notify: bool = True) -> None:
    method on_reload (line 143) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method run_gamemode (line 167) | async def run_gamemode(self) -> None:
    method event_openwindow (line 174) | async def event_openwindow(self, params: str) -> None:
    method event_closewindow (line 191) | async def event_closewindow(self, addr: str) -> None:

FILE: pyprland/plugins/interface.py
  class PluginContext (line 36) | class PluginContext:
  class Plugin (line 48) | class Plugin:
    method __init_subclass__ (line 82) | def __init_subclass__(cls, environments: Environments | None = None, *...
    method get_config (line 93) | def get_config(self, name: str) -> ConfigValue:
    method get_config_str (line 120) | def get_config_str(self, name: str) -> str:
    method get_config_int (line 124) | def get_config_int(self, name: str) -> int:
    method get_config_float (line 140) | def get_config_float(self, name: str) -> float:
    method get_config_bool (line 156) | def get_config_bool(self, name: str) -> bool:
    method get_config_list (line 164) | def get_config_list(self, name: str) -> list[Any]:
    method get_config_dict (line 170) | def get_config_dict(self, name: str) -> dict[str, Any]:
    method __init__ (line 182) | def __init__(self, name: str) -> None:
    method init (line 195) | async def init(self) -> None:
    method on_reload (line 201) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method exit (line 220) | async def exit(self) -> None:
    method load_config (line 225) | async def load_config(self, config: dict[str, Any]) -> None:
    method validate_config (line 234) | def validate_config(self) -> list[str]:
    method validate_config_static (line 252) | def validate_config_static(cls, plugin_name: str, config: dict) -> lis...
    method get_clients (line 271) | async def get_clients(
    method get_focused_monitor_or_warn (line 286) | async def get_focused_monitor_or_warn(self, context: str = "") -> Moni...

FILE: pyprland/plugins/layout_center.py
  class Extension (line 21) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method init (line 49) | async def init(self) -> None:
    method event_openwindow (line 62) | async def event_openwindow(self, windescr: str) -> None:
    method event_activewindowv2 (line 98) | async def event_activewindowv2(self, _: str) -> None:
    method event_closewindow (line 114) | async def event_closewindow(self, addr: str) -> None:
    method run_layout_center (line 130) | async def run_layout_center(self, what: str) -> None:
    method on_reload (line 145) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method get_clients (line 157) | async def get_clients(
    method unprepare_window (line 169) | async def unprepare_window(self, clients: list[ClientInfo] | None = No...
    method prepare_window (line 185) | async def prepare_window(self, clients: list[ClientInfo] | None = None...
    method _calculate_centered_geometry (line 209) | async def _calculate_centered_geometry(
    method _sanity_check (line 239) | async def _sanity_check(self, clients: list[ClientInfo] | None = None)...
    method _run_changefocus (line 253) | async def _run_changefocus(self, direction: int, default_override: str...
    method _run_toggle (line 283) | async def _run_toggle(self) -> None:
    method offset (line 297) | def offset(self) -> tuple[int, int]:
    method margin (line 306) | def margin(self) -> int:
    method enabled (line 312) | def enabled(self) -> bool:
    method enabled (line 317) | def enabled(self, value: bool) -> None:
    method main_window_addr (line 324) | def main_window_addr(self) -> str:
    method main_window_addr (line 329) | def main_window_addr(self, value: str) -> None:

FILE: pyprland/plugins/lost_windows.py
  function contains (line 7) | def contains(monitor: MonitorInfo, window: ClientInfo) -> bool:
  class Extension (line 19) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method run_attract_lost (line 22) | async def run_attract_lost(self) -> None:

FILE: pyprland/plugins/magnify.py
  class Extension (line 11) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method on_reload (line 25) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method ease_out_quad (line 33) | def ease_out_quad(self, step: float, start: int, end: int, duration: i...
    method animated_eased_zoom (line 45) | def animated_eased_zoom(self, start: int, end: int, duration: int) -> ...
    method run_zoom (line 58) | async def run_zoom(self, *args) -> None:

FILE: pyprland/plugins/menubar.py
  function get_pid_from_layers_hyprland (line 23) | def get_pid_from_layers_hyprland(layers: dict) -> bool | int:
  function is_bar_in_layers_niri (line 41) | def is_bar_in_layers_niri(layers: list) -> bool:
  function is_bar_alive (line 53) | async def is_bar_alive(
  class Extension (line 89) | class Extension(Plugin, environments=[Environment.HYPRLAND, Environment....
    method __init__ (line 110) | def __init__(self, name: str) -> None:
    method on_reload (line 115) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method _run_program (line 122) | def _run_program(self) -> None:
    method is_running (line 173) | def is_running(self) -> bool:
    method run_bar (line 177) | async def run_bar(self, args: str) -> None:
    method set_best_monitor (line 198) | async def set_best_monitor(self) -> None:
    method get_best_monitor (line 208) | async def get_best_monitor(self) -> str:
    method event_monitoradded (line 226) | async def event_monitoradded(self, monitor: str) -> None:
    method niri_outputschanged (line 242) | async def niri_outputschanged(self, _data: dict) -> None:
    method exit (line 268) | async def exit(self) -> None:
    method stop (line 272) | async def stop(self) -> None:

FILE: pyprland/plugins/mixins.py
  class MonitorListDescriptor (line 13) | class MonitorListDescriptor:
    method __get__ (line 25) | def __get__(self, obj: object, _objtype: type | None = None) -> list[s...
    method __set__ (line 37) | def __set__(self, obj: object, value: list[str]) -> None:
  class MonitorTrackingMixin (line 49) | class MonitorTrackingMixin:
    method event_monitoradded (line 75) | async def event_monitoradded(self, name: str) -> None:
    method event_monitorremoved (line 83) | async def event_monitorremoved(self, name: str) -> None:

FILE: pyprland/plugins/monitors/__init__.py
  class Extension (line 28) | class Extension(Plugin, environments=[Environment.HYPRLAND, Environment....
    method on_reload (line 68) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method event_configreloaded (line 88) | async def event_configreloaded(self, _: str = "") -> None:
    method _delayed_relayout (line 94) | async def _delayed_relayout(self) -> None:
    method event_monitoradded (line 100) | async def event_monitoradded(self, name: str) -> None:
    method niri_outputschanged (line 116) | async def niri_outputschanged(self, _data: dict) -> None:
    method run_relayout (line 126) | async def run_relayout(self) -> bool:
    method _run_relayout (line 130) | async def _run_relayout(self, monitors: list[MonitorInfo] | None = Non...
    method _run_relayout_niri (line 166) | async def _run_relayout_niri(self) -> bool:
    method _mark_disabled_monitors (line 194) | def _mark_disabled_monitors(
    method _build_graph (line 219) | def _build_graph(
    method _compute_positions (line 242) | def _compute_positions(
    method _apply_layout (line 269) | async def _apply_layout(
    method _apply_niri_layout (line 317) | async def _apply_niri_layout(
    method _resolve_names (line 354) | def _resolve_names(self, monitors: list[MonitorInfo]) -> dict[str, Any]:
    method _hotplug_command (line 366) | async def _hotplug_command(self, monitors: list[MonitorInfo], name: st...
    method _clear_mon_by_pat_cache (line 384) | def _clear_mon_by_pat_cache(self) -> None:

FILE: pyprland/plugins/monitors/commands.py
  function build_hyprland_command (line 19) | def build_hyprland_command(monitor: MonitorInfo, config: dict[str, Any])...
  function build_niri_position_action (line 40) | def build_niri_position_action(name: str, x_pos: int, y_pos: int) -> dict:
  function build_niri_scale_action (line 54) | def build_niri_scale_action(name: str, scale: float) -> dict:
  function build_niri_transform_action (line 67) | def build_niri_transform_action(name: str, transform: int | str) -> dict:
  function build_niri_disable_action (line 84) | def build_niri_disable_action(name: str) -> dict:

FILE: pyprland/plugins/monitors/layout.py
  function get_dims (line 12) | def get_dims(mon: MonitorInfo, config: dict[str, Any] | None = None) -> ...
  function _place_left (line 47) | def _place_left(ref_rect: tuple[int, int, int, int], mon_dim: tuple[int,...
  function _place_right (line 69) | def _place_right(ref_rect: tuple[int, int, int, int], mon_dim: tuple[int...
  function _place_top (line 91) | def _place_top(ref_rect: tuple[int, int, int, int], mon_dim: tuple[int, ...
  function _place_bottom (line 113) | def _place_bottom(ref_rect: tuple[int, int, int, int], mon_dim: tuple[in...
  function compute_xy (line 135) | def compute_xy(
  function build_graph (line 167) | def build_graph(
  function compute_positions (line 204) | def compute_positions(
  function find_cycle_path (line 258) | def find_cycle_path(config: dict[str, Any], unprocessed: list[str]) -> str:

FILE: pyprland/plugins/monitors/resolution.py
  function get_monitor_by_pattern (line 9) | def get_monitor_by_pattern(
  function resolve_placement_config (line 47) | def resolve_placement_config(

FILE: pyprland/plugins/monitors/schema.py
  function validate_placement_keys (line 39) | def validate_placement_keys(value: dict[str, Any]) -> list[str]:

FILE: pyprland/plugins/protocols.py
  class HyprlandEvents (line 23) | class HyprlandEvents(Protocol):
    method event_activewindowv2 (line 32) | async def event_activewindowv2(self, addr: str) -> None:
    method event_changefloatingmode (line 40) | async def event_changefloatingmode(self, args: str) -> None:
    method event_closewindow (line 48) | async def event_closewindow(self, addr: str) -> None:
    method event_configreloaded (line 56) | async def event_configreloaded(self, data: str = "") -> None:
    method event_focusedmon (line 64) | async def event_focusedmon(self, mon: str) -> None:
    method event_monitoradded (line 72) | async def event_monitoradded(self, name: str) -> None:
    method event_monitorremoved (line 80) | async def event_monitorremoved(self, name: str) -> None:
    method event_openwindow (line 88) | async def event_openwindow(self, params: str) -> None:
    method event_workspace (line 96) | async def event_workspace(self, workspace: str) -> None:
  class NiriEvents (line 106) | class NiriEvents(Protocol):
    method niri_outputschanged (line 112) | async def niri_outputschanged(self, data: dict[str, Any]) -> None:

FILE: pyprland/plugins/pyprland/__init__.py
  class Extension (line 32) | class Extension(HyprlandStateMixin, NiriStateMixin, Plugin):
    method init (line 38) | async def init(self) -> None:
    method on_reload (line 47) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method run_version (line 55) | def run_version(self) -> str:
    method run_dumpjson (line 59) | def run_dumpjson(self) -> str:
    method run_help (line 63) | def run_help(self, command: str = "") -> str:
    method run_doc (line 72) | def run_doc(self, args: str = "") -> str:
    method _get_option_doc (line 118) | def _get_option_doc(self, plugin_name: str, plugin: Plugin, option_nam...
    method run_reload (line 159) | async def run_reload(self) -> None:
    method run_compgen (line 167) | def run_compgen(self, args: str = "") -> str:
    method run_exit (line 185) | def run_exit(self) -> None:
    method _parse_config_path (line 189) | def _parse_config_path(self, path: str) -> tuple[str, list[str]]:
    method _get_nested_value (line 208) | def _get_nested_value(self, data: dict, keys: list[str]) -> Any:
    method _set_nested_value (line 229) | def _set_nested_value(self, data: dict, keys: list[str], value: Any) -...
    method _delete_nested_value (line 244) | def _delete_nested_value(self, data: dict, keys: list[str]) -> bool:
    method _get_field_schema (line 264) | def _get_field_schema(self, plugin_name: str, keys: list[str]) -> Conf...
    method _coerce_value (line 297) | def _coerce_value(self, value_str: str, field: ConfigField | None, cur...
    method _get_target_type (line 321) | def _get_target_type(self, field: ConfigField | None, current_value: A...
    method _coerce_to_type (line 330) | def _coerce_to_type(self, value_str: str, target_type: type) -> Any:  ...
    method _parse_bool (line 353) | def _parse_bool(self, value_str: str) -> bool:
    method _parse_list (line 363) | def _parse_list(self, value_str: str) -> list:
    method run_get (line 374) | def run_get(self, path: str) -> str:
    method run_set (line 411) | async def run_set(self, args: str) -> str:  # noqa: C901

FILE: pyprland/plugins/pyprland/hyprland_core.py
  class HyprlandStateMixin (line 12) | class HyprlandStateMixin(StateMonitorTrackingMixin):
    method _init_hyprland (line 25) | async def _init_hyprland(self) -> None:
    method event_configreloaded (line 77) | async def event_configreloaded(self, _: str = "") -> None:
    method event_activewindowv2 (line 90) | async def event_activewindowv2(self, addr: str) -> None:
    method event_workspace (line 103) | async def event_workspace(self, workspace: str) -> None:
    method event_focusedmon (line 112) | async def event_focusedmon(self, mon: str) -> None:
    method _set_hyprland_version (line 121) | def _set_hyprland_version(self, version_str: str, auto_increment: bool...

FILE: pyprland/plugins/pyprland/niri_core.py
  class NiriStateMixin (line 10) | class NiriStateMixin:
    method _init_niri (line 22) | async def _init_niri(self) -> None:
    method niri_outputschanged (line 43) | async def niri_outputschanged(self, _data: dict) -> None:

FILE: pyprland/plugins/scratchpads/__init__.py
  class Extension (line 31) | class Extension(LifecycleMixin, EventsMixin, TransitionsMixin, Plugin, e...
    method __init__ (line 45) | def __init__(self, name: str) -> None:
    method exit (line 53) | async def exit(self) -> None:
    method validate_config (line 78) | def validate_config(self) -> list[str]:
    method validate_config_static (line 94) | def validate_config_static(cls, _plugin_name: str, config: dict) -> li...
    method on_reload (line 106) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method _unset_windowrules (line 172) | async def _unset_windowrules(self, scratch: Scratch) -> None:
    method _configure_windowrules (line 188) | async def _configure_windowrules(self, scratch: Scratch) -> None:
    method cancel_task (line 240) | def cancel_task(self, uid: str) -> bool:
    method _detach_window (line 254) | async def _detach_window(self, scratch: Scratch, address: str) -> None:
    method _attach_window (line 265) | async def _attach_window(self, scratch: Scratch, address: str) -> None:
    method run_attach (line 285) | async def run_attach(self) -> None:
    method run_toggle (line 306) | async def run_toggle(self, uid_or_uids: str) -> None:
    method run_show (line 361) | async def run_show(self, uid: str) -> None:
    method _hide_excluded (line 409) | async def _hide_excluded(self, scratch: Scratch, excluded_ids: list[st...
    method _handle_multiwindow (line 422) | async def _handle_multiwindow(self, scratch: Scratch, clients: list[Cl...
    method run_hide (line 449) | async def run_hide(self, uid: str, flavor: HideFlavors = HideFlavors.N...
    method _hide_scratch (line 484) | async def _hide_scratch(self, scratch: Scratch, active_window: str, ac...
    method _save_multiwindow_state (line 537) | async def _save_multiwindow_state(self, scratch: Scratch, clients: lis...
    method _move_clients_out (line 556) | async def _move_clients_out(self, scratch: Scratch, monitor: MonitorIn...
    method _handle_focus_tracking (line 591) | async def _handle_focus_tracking(self, scratch: Scratch, active_window...

FILE: pyprland/plugins/scratchpads/animations.py
  class AnimationTarget (line 13) | class AnimationTarget(enum.Enum):
  class Placement (line 21) | class Placement:  # {{{
    method get (line 26) | def get(animation_type: str, monitor: MonitorInfo, client: ClientInfo,...
    method get_offscreen (line 38) | def get_offscreen(animation_type: str, monitor: MonitorInfo, client: C...
    method fromtop (line 66) | def fromtop(monitor: MonitorInfo, client: ClientInfo, margin: int) -> ...
    method frombottom (line 86) | def frombottom(monitor: MonitorInfo, client: ClientInfo, margin: int) ...
    method fromleft (line 107) | def fromleft(monitor: MonitorInfo, client: ClientInfo, margin: int) ->...
    method fromright (line 127) | def fromright(monitor: MonitorInfo, client: ClientInfo, margin: int) -...

FILE: pyprland/plugins/scratchpads/common.py
  class HideFlavors (line 9) | class HideFlavors(Flag):
  class FocusTracker (line 19) | class FocusTracker:
    method clear (line 25) | def clear(self) -> None:

FILE: pyprland/plugins/scratchpads/events.py
  class EventsMixin (line 28) | class EventsMixin:
    method cancel_task (line 44) | def cancel_task(self, uid: str) -> bool:
    method run_hide (line 49) | async def run_hide(self, uid: str, flavor: HideFlavors = HideFlavors.N...
    method update_scratch_info (line 53) | async def update_scratch_info(self, orig_scratch: Scratch | None = Non...
    method _handle_multiwindow (line 57) | async def _handle_multiwindow(self, scratch: Scratch, clients: list[Cl...
    method event_changefloatingmode (line 62) | async def event_changefloatingmode(self, args: str) -> None:
    method event_workspace (line 74) | async def event_workspace(self, name: str) -> None:
    method event_closewindow (line 85) | async def event_closewindow(self, addr: str) -> None:
    method event_monitorremoved (line 104) | async def event_monitorremoved(self, monitor_name: str) -> None:
    method event_monitoradded (line 127) | async def event_monitoradded(self, monitor_name: str) -> None:
    method event_activewindowv2 (line 177) | async def event_activewindowv2(self, addr: str) -> None:
    method _hysteresis_handling (line 203) | async def _hysteresis_handling(self, scratch: Scratch) -> None:
    method _alternative_lookup (line 223) | async def _alternative_lookup(self) -> bool:
    method event_openwindow (line 240) | async def event_openwindow(self, params: str) -> None:

FILE: pyprland/plugins/scratchpads/helpers.py
  function mk_scratch_name (line 24) | def mk_scratch_name(uid: str) -> str:
  function compute_offset (line 34) | def compute_offset(pos1: tuple[int, int] | None, pos2: tuple[int, int] |...
  function apply_offset (line 46) | def apply_offset(pos: tuple[int, int], offset: tuple[int, int]) -> tuple...
  function get_size (line 56) | def get_size(monitor: MonitorInfo) -> tuple[int, int]:
  function get_active_space_identifier (line 69) | def get_active_space_identifier(state: SharedState) -> tuple[str, str]:
  function get_all_space_identifiers (line 78) | async def get_all_space_identifiers(monitors: list[MonitorInfo]) -> list...
  function get_match_fn (line 90) | def get_match_fn(prop_name: str, prop_value: float | bool | str | list) ...
  class DynMonitorConfig (line 111) | class DynMonitorConfig(SchemaAwareMixin):
    method __init__ (line 114) | def __init__(
    method __setitem__ (line 140) | def __setitem__(self, name: str, value: ConfigValueType) -> None:
    method update (line 143) | def update(self, other: dict[str, ConfigValueType]) -> None:
    method _get_raw (line 151) | def _get_raw(self, name: str) -> ConfigValueType:
    method __getitem__ (line 160) | def __getitem__(self, name: str) -> ConfigValueType:
    method __contains__ (line 164) | def __contains__(self, name: object) -> bool:
    method __str__ (line 169) | def __str__(self) -> str:

FILE: pyprland/plugins/scratchpads/lifecycle.py
  class LifecycleMixin (line 24) | class LifecycleMixin:
    method _configure_windowrules (line 38) | async def _configure_windowrules(self, scratch: Scratch) -> None:
    method _unset_windowrules (line 42) | async def _unset_windowrules(self, scratch: Scratch) -> None:
    method __wait_for_client (line 46) | async def __wait_for_client(self, scratch: Scratch, use_proc: bool = T...
    method _start_scratch_nopid (line 79) | async def _start_scratch_nopid(self, scratch: Scratch) -> bool:
    method _start_scratch (line 97) | async def _start_scratch(self, scratch: Scratch) -> bool:
    method ensure_alive (line 124) | async def ensure_alive(self, uid: str) -> bool:
    method start_scratch_command (line 150) | async def start_scratch_command(self, name: str) -> None:
    method update_scratch_info (line 170) | async def update_scratch_info(self, orig_scratch: Scratch | None = Non...

FILE: pyprland/plugins/scratchpads/lookup.py
  class ScratchDB (line 10) | class ScratchDB:  # {{{
    method __init__ (line 18) | def __init__(self) -> None:
    method get_by_state (line 25) | def get_by_state(self, status: str) -> set[Scratch]:
    method has_state (line 33) | def has_state(self, scratch: Scratch, status: str) -> bool:
    method set_state (line 42) | def set_state(self, scratch: Scratch, status: str) -> None:
    method clear_state (line 51) | def clear_state(self, scratch: Scratch, status: str) -> None:
    method __iter__ (line 63) | def __iter__(self) -> Iterator[str]:
    method values (line 67) | def values(self) -> Iterable[Scratch]:
    method items (line 71) | def items(self) -> Iterable[tuple[str, Scratch]]:
    method reset (line 77) | def reset(self, scratch: Scratch) -> None:
    method clear (line 88) | def clear(self, name: str | None = None, pid: int | None = None, addr:...
    method register (line 108) | def register(self, scratch: Scratch) -> None: ...
    method register (line 111) | def register(self, scratch: Scratch, name: str) -> None: ...
    method register (line 114) | def register(self, scratch: Scratch, *, pid: int) -> None: ...
    method register (line 117) | def register(self, scratch: Scratch, *, addr: str) -> None: ...
    method register (line 119) | def register(self, scratch: Scratch, name: str | None = None, pid: int...
    method get (line 151) | def get(self, name: str) -> Scratch | None: ...
    method get (line 154) | def get(self, *, pid: int) -> Scratch | None: ...
    method get (line 157) | def get(self, *, addr: str) -> Scratch | None: ...
    method get (line 159) | def get(self, name: str | None = None, pid: int | None = None, addr: s...

FILE: pyprland/plugins/scratchpads/objects.py
  class ClientPropGetter (line 22) | class ClientPropGetter(Protocol):
    method __call__ (line 25) | async def __call__(
  class MetaInfo (line 38) | class MetaInfo:
  class Scratch (line 51) | class Scratch:  # {{{
    method __init__ (line 62) | def __init__(self, uid: str, full_config: dict[str, Any], plugin: "Ext...
    method forced_monitor (line 79) | def forced_monitor(self) -> str | None:
    method animation_type (line 87) | def animation_type(self) -> str:
    method _make_initial_config (line 91) | def _make_initial_config(self, config: dict) -> dict:
    method set_config (line 113) | def set_config(self, full_config: dict[str, Any]) -> None:
    method have_address (line 138) | def have_address(self, addr: str) -> bool:
    method have_command (line 147) | def have_command(self) -> bool:
    method initialize (line 151) | async def initialize(self, ex: "_scratchpads_extension_m.Extension") -...
    method is_alive (line 171) | async def is_alive(self) -> bool:
    method fetch_matching_client (line 190) | async def fetch_matching_client(self, clients: list[ClientInfo] | None...
    method get_match_props (line 203) | def get_match_props(self) -> tuple[str, str | float]:
    method reset (line 214) | def reset(self, pid: int) -> None:
    method client_ready (line 228) | def client_ready(self) -> bool:
    method address (line 233) | def address(self) -> str:
    method full_address (line 240) | def full_address(self) -> str:
    method update_client_info (line 246) | async def update_client_info(
    method event_workspace (line 275) | def event_workspace(self, name: str) -> None:
    method __str__ (line 284) | def __str__(self) -> str:

FILE: pyprland/plugins/scratchpads/schema.py
  function _validate_against_schema (line 12) | def _validate_against_schema(config: dict, prefix: str, schema: ConfigIt...
  function _validate_animation (line 29) | def _validate_animation(value: str) -> list[str]:
  function _validate_monitor_overrides (line 125) | def _validate_monitor_overrides(name: str, scratch_config: dict, errors:...
  function get_template_names (line 140) | def get_template_names(config: dict) -> set[str]:
  function is_pure_template (line 165) | def is_pure_template(name: str, config: dict, template_names: set[str]) ...
  function validate_scratchpad_config (line 183) | def validate_scratchpad_config(name: str, scratch_config: dict, *, is_te...

FILE: pyprland/plugins/scratchpads/transitions.py
  class TransitionsMixin (line 26) | class TransitionsMixin:
    method _handle_multiwindow (line 40) | async def _handle_multiwindow(self, scratch: Scratch, clients: list[Cl...
    method update_scratch_info (line 45) | async def update_scratch_info(self, orig_scratch: Scratch | None = Non...
    method get_offsets (line 49) | async def get_offsets(self, scratch: Scratch, monitor: MonitorInfo | N...
    method _hide_transition (line 82) | async def _hide_transition(self, scratch: Scratch, monitor: MonitorInf...
    method _slide_animation (line 100) | async def _slide_animation(
    method _show_transition (line 137) | async def _show_transition(self, scratch: Scratch, monitor: MonitorInf...
    method _pin_scratch (line 240) | async def _pin_scratch(self, scratch: Scratch) -> None:
    method _update_infos (line 252) | async def _update_infos(self, scratch: Scratch, clients: list[ClientIn...
    method _animate_show (line 277) | async def _animate_show(self, scratch: Scratch, monitor: MonitorInfo, ...
    method _fix_size (line 317) | async def _fix_size(self, scratch: Scratch, monitor: MonitorInfo) -> N...
    method _fix_position (line 334) | async def _fix_position(self, scratch: Scratch, monitor: MonitorInfo) ...

FILE: pyprland/plugins/scratchpads/windowruleset.py
  class WindowRuleSet (line 11) | class WindowRuleSet:
    method __init__ (line 14) | def __init__(self, state: SharedState) -> None:
    method set_class (line 20) | def set_class(self, value: str) -> None:
    method set_name (line 28) | def set_name(self, value: str) -> None:
    method set (line 36) | def set(self, param: str, value: str) -> None:
    method _get_content (line 45) | def _get_content(self) -> Iterable[str]:
    method get_content (line 63) | def get_content(self) -> list[str]:

FILE: pyprland/plugins/shift_monitors.py
  class Extension (line 10) | class Extension(MonitorTrackingMixin, Plugin, environments=[Environment....
    method init (line 15) | async def init(self) -> None:
    method niri_outputschanged (line 23) | async def niri_outputschanged(self, _data: dict) -> None:
    method run_shift_monitors (line 35) | async def run_shift_monitors(self, arg: str) -> None:

FILE: pyprland/plugins/shortcuts_menu.py
  class Extension (line 13) | class Extension(MenuMixin, Plugin):
    method run_menu (line 29) | async def run_menu(self, name: str = "") -> None:
    method _handle_chain (line 75) | async def _handle_chain(self, options: list[str | dict]) -> None:
    method _run_command (line 108) | async def _run_command(self, command: str, variables: dict[str, str]) ...

FILE: pyprland/plugins/stash.py
  class Extension (line 14) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method __init__ (line 21) | def __init__(self, name: str) -> None:
    method on_reload (line 27) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method run_stash (line 38) | async def run_stash(self, name: str = "default") -> None:
    method _restore_floating (line 81) | async def _restore_floating(self, addr: str) -> None:
    method event_closewindow (line 89) | async def event_closewindow(self, addr: str) -> None:
    method run_stash_toggle (line 105) | async def run_stash_toggle(self, name: str = "default") -> None:
    method _show_stash (line 119) | async def _show_stash(self, name: str) -> None:
    method _hide_stash (line 135) | async def _hide_stash(self, name: str) -> None:

FILE: pyprland/plugins/system_notifier.py
  class Extension (line 41) | class Extension(Plugin):
    method __init__ (line 92) | def __init__(self, name: str) -> None:
    method on_reload (line 99) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method exit (line 116) | async def exit(self) -> None:
    method start_source (line 123) | async def start_source(self, props: dict[str, str]) -> None:
    method start_parser (line 148) | async def start_parser(self, name: str, props: list) -> None:

FILE: pyprland/plugins/toggle_dpms.py
  class Extension (line 7) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method run_toggle_dpms (line 10) | async def run_toggle_dpms(self) -> None:

FILE: pyprland/plugins/toggle_special.py
  class Extension (line 10) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method run_toggle_special (line 17) | async def run_toggle_special(self, special_workspace: str = "minimized...

FILE: pyprland/plugins/wallpapers/__init__.py
  class OnlineState (line 51) | class OnlineState:
  function fetch_monitors (line 61) | async def fetch_monitors(extension: "Extension") -> list[MonitorInfo]:
  class Extension (line 79) | class Extension(Plugin):
    method __init__ (line 147) | def __init__(self, name: str) -> None:
    method on_reload (line 153) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method _create_cache (line 230) | def _create_cache(self, cache_dir: Path) -> ImageCache:
    method _setup_online_fetching (line 241) | async def _setup_online_fetching(
    method _cleanup_caches (line 304) | async def _cleanup_caches(self) -> None:
    method _get_local_paths (line 322) | def _get_local_paths(self) -> list[Path]:
    method _warn_no_images (line 333) | async def _warn_no_images(self) -> None:
    method exit (line 342) | async def exit(self) -> None:
    method event_monitoradded (line 352) | async def event_monitoradded(self, _: str) -> None:
    method niri_outputschanged (line 356) | async def niri_outputschanged(self, _: dict) -> None:
    method select_next_image (line 360) | async def select_next_image(self) -> str:
    method _fetch_online_image (line 387) | async def _fetch_online_image(self) -> str:
    method _prefetch_online_image (line 444) | async def _prefetch_online_image(self) -> None:
    method run_wall_rm (line 474) | async def run_wall_rm(self) -> None:
    method run_wall_cleanup (line 526) | async def run_wall_cleanup(self, arg: str = "") -> str:
    method _clear_rounded_cache (line 556) | async def _clear_rounded_cache(self) -> int:
    method _cleanup_orphaned_rounded (line 573) | async def _cleanup_orphaned_rounded(self) -> tuple[int, int]:
    method run_wall_info (line 613) | async def run_wall_info(self, arg: str = "") -> str:
    method _prepare_wallpaper (line 691) | async def _prepare_wallpaper(self, monitor: MonitorInfo, img_path: str...
    method _run_one (line 697) | async def _run_one(self, template: str, values: dict[str, str]) -> None:
    method _generate_templates (line 705) | async def _generate_templates(self, img_path: str, color: str | None =...
    method update_vars (line 747) | async def update_vars(self, variables: dict[str, Any], monitor: Monito...
    method _iter_one (line 755) | async def _iter_one(self, variables: dict[str, Any]) -> None:
    method main_loop (line 803) | async def main_loop(self) -> None:
    method terminate (line 827) | async def terminate(self) -> None:
    method run_wall_next (line 833) | async def run_wall_next(self) -> None:
    method run_wall_pause (line 838) | async def run_wall_pause(self) -> None:
    method run_wall_clear (line 842) | async def run_wall_clear(self) -> None:
    method run_color (line 853) | async def run_color(self, arg: str) -> None:
    method run_palette (line 871) | async def run_palette(self, arg: str = "") -> str:

FILE: pyprland/plugins/wallpapers/cache.py
  class ImageCache (line 15) | class ImageCache:
    method __init__ (line 25) | def __init__(
    method _hash_key (line 46) | def _hash_key(self, key: str) -> str:
    method get_path (line 63) | def get_path(self, key: str, extension: str = "jpg") -> Path:
    method is_valid (line 76) | def is_valid(self, path: Path) -> bool:
    method get (line 95) | def get(self, key: str, extension: str = "jpg") -> Path | None:
    method store (line 110) | async def store(self, key: str, data: bytes, extension: str = "jpg") -...
    method _get_cache_size (line 131) | def _get_cache_size(self) -> int:
    method _get_cache_count (line 143) | def _get_cache_count(self) -> int:
    method _is_under_limits (line 151) | def _is_under_limits(self, current_size: int, current_count: int) -> b...
    method _auto_cleanup (line 165) | def _auto_cleanup(self) -> None:
    method cleanup (line 191) | def cleanup(self, max_age: int | None = None) -> int:
    method clear (line 218) | def clear(self) -> int:

FILE: pyprland/plugins/wallpapers/colorutils.py
  function _build_hue_histogram (line 28) | def _build_hue_histogram(hsv_pixels: list[tuple[int, int, int]]) -> tupl...
  function _smooth_histogram (line 48) | def _smooth_histogram(hue_weights: list[float]) -> list[float]:
  function _find_peaks (line 67) | def _find_peaks(smoothed_weights: list[float]) -> list[tuple[float, int]]:
  function _get_best_pixel_for_hue (line 85) | def _get_best_pixel_for_hue(
  function _calculate_hue_diff (line 119) | def _calculate_hue_diff(hue1: int, hue2: int) -> int:
  function _select_colors_from_peaks (line 132) | def _select_colors_from_peaks(
  function get_dominant_colors (line 190) | def get_dominant_colors(img_path: str) -> list[tuple[int, int, int]]:
  function nicify_oklab (line 229) | def nicify_oklab(

FILE: pyprland/plugins/wallpapers/hyprpaper.py
  class HyprpaperManager (line 19) | class HyprpaperManager:
    method __init__ (line 22) | def __init__(self, log: "logging.Logger") -> None:
    method ensure_running (line 30) | async def ensure_running(self) -> bool:
    method set_wallpaper (line 52) | async def set_wallpaper(self, commands: list[str], backend: "BackendPr...
    method stop (line 70) | async def stop(self) -> None:

FILE: pyprland/plugins/wallpapers/imageutils.py
  function expand_path (line 23) | def expand_path(path: str) -> str:
  function get_files_with_ext (line 32) | async def get_files_with_ext(
  class MonitorInfo (line 58) | class MonitorInfo:
  function get_effective_dimensions (line 68) | def get_effective_dimensions(monitor: MonitorInfo) -> tuple[int, int]:
  class RoundedImageManager (line 86) | class RoundedImageManager:
    method __init__ (line 89) | def __init__(self, radius: int, cache: ImageCache) -> None:
    method hash_source (line 99) | def hash_source(self, image_path: str) -> str:
    method _hash_settings (line 110) | def _hash_settings(self, monitor: MonitorInfo) -> str:
    method build_key (line 122) | def build_key(self, monitor: MonitorInfo, image_path: str) -> str:
    method scale_and_round (line 139) | def scale_and_round(self, src: str, monitor: MonitorInfo) -> str:
    method _create_rounded_mask (line 176) | def _create_rounded_mask(self, width: int, height: int, scale: int, re...
  function to_hex (line 192) | def to_hex(red: int, green: int, blue: int) -> str:
  function to_rgb (line 203) | def to_rgb(red: int, green: int, blue: int) -> str:
  function to_rgba (line 214) | def to_rgba(red: int, green: int, blue: int) -> str:
  function get_variant_color (line 225) | def get_variant_color(hue: float, saturation: float, lightness: float) -...

FILE: pyprland/plugins/wallpapers/models.py
  class Theme (line 10) | class Theme(StrEnum):
  class ColorScheme (line 21) | class ColorScheme(StrEnum):
  class MaterialColors (line 38) | class MaterialColors:
  class ColorVariant (line 47) | class ColorVariant:
  class VariantConfig (line 55) | class VariantConfig:

FILE: pyprland/plugins/wallpapers/online/__init__.py
  class NoBackendAvailableError (line 51) | class NoBackendAvailableError(Exception):
    method __init__ (line 54) | def __init__(self, message: str = "No backends available", tried: list...
  class OnlineFetcher (line 65) | class OnlineFetcher:
    method __init__ (line 76) | def __init__(
    method backends (line 117) | def backends(self) -> list[str]:
    method available_backends (line 122) | def available_backends(self) -> list[str]:
    method _get_session (line 126) | async def _get_session(self) -> Any:
    method close (line 139) | async def close(self) -> None:
    method __aenter__ (line 145) | async def __aenter__(self) -> Self:
    method __aexit__ (line 149) | async def __aexit__(self, *args: object) -> None:
    method _select_backends (line 153) | def _select_backends(self, backend: str | None) -> list[str]:
    method _try_backend (line 175) | async def _try_backend(
    method get_image (line 222) | async def get_image(
    method _download_image (line 276) | async def _download_image(self, session: Any, url: str) -> bytes:

FILE: pyprland/plugins/wallpapers/online/backends/__init__.py
  function register_backend (line 31) | def register_backend(cls: type[Backend]) -> type[Backend]:
  function get_backend (line 44) | def get_backend(name: str) -> Backend:
  function get_available_backends (line 63) | def get_available_backends() -> list[str]:

FILE: pyprland/plugins/wallpapers/online/backends/base.py
  class ImageInfo (line 24) | class ImageInfo:
  class Backend (line 46) | class Backend(ABC):
    method fetch_image_info (line 63) | async def fetch_image_info(
  class BackendError (line 87) | class BackendError(Exception):
    method __init__ (line 90) | def __init__(self, backend: str, message: str) -> None:
  function fetch_redirect_image (line 102) | async def fetch_redirect_image(

FILE: pyprland/plugins/wallpapers/online/backends/bing.py
  class BingBackend (line 20) | class BingBackend(Backend):
    method fetch_image_info (line 34) | async def fetch_image_info(

FILE: pyprland/plugins/wallpapers/online/backends/picsum.py
  class PicsumBackend (line 16) | class PicsumBackend(Backend):
    method _extract_id (line 30) | def _extract_id(url: str) -> str:
    method fetch_image_info (line 45) | async def fetch_image_info(

FILE: pyprland/plugins/wallpapers/online/backends/reddit.py
  class RedditBackend (line 37) | class RedditBackend(Backend):
    method fetch_image_info (line 50) | async def fetch_image_info(
    method _get_subreddits (line 106) | def _get_subreddits(self, keywords: list[str] | None) -> list[str]:
    method _filter_posts (line 129) | def _filter_posts(
    method _is_image_url (line 172) | def _is_image_url(self, url: str) -> bool:
    method _post_to_image_info (line 187) | def _post_to_image_info(self, post: dict) -> ImageInfo:

FILE: pyprland/plugins/wallpapers/online/backends/unsplash.py
  class UnsplashBackend (line 14) | class UnsplashBackend(Backend):
    method fetch_image_info (line 27) | async def fetch_image_info(
    method _extract_id (line 68) | def _extract_id(url: str) -> str:

FILE: pyprland/plugins/wallpapers/online/backends/wallhaven.py
  class WallhavenBackend (line 16) | class WallhavenBackend(Backend):
    method fetch_image_info (line 29) | async def fetch_image_info(

FILE: pyprland/plugins/wallpapers/palette.py
  function hex_to_rgb (line 33) | def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
  function _categorize_palette (line 46) | def _categorize_palette(palette: dict[str, str]) -> dict[str, list[str]]:
  function palette_to_json (line 72) | def palette_to_json(palette: dict[str, str]) -> str:
  function palette_to_terminal (line 104) | def palette_to_terminal(palette: dict[str, str]) -> str:  # pylint: disa...
  function generate_sample_palette (line 156) | def generate_sample_palette(

FILE: pyprland/plugins/wallpapers/templates.py
  function _set_alpha (line 15) | def _set_alpha(color: str, alpha: str) -> str:
  function _set_lightness (line 33) | def _set_lightness(hex_color: str, amount: str) -> str:
  function _apply_filters (line 57) | async def _apply_filters(content: str, replacements: dict[str, str]) -> ...
  class TemplateEngine (line 94) | class TemplateEngine:
    method __init__ (line 97) | def __init__(self, log: logging.Logger) -> None:
    method process_single_template (line 105) | async def process_single_template(

FILE: pyprland/plugins/wallpapers/theme.py
  function detect_theme (line 18) | async def detect_theme(logger: logging.Logger) -> Theme:
  function get_color_scheme_props (line 61) | def get_color_scheme_props(color_scheme: ColorScheme | str) -> dict[str,...
  function _get_rgb_for_variant (line 124) | def _get_rgb_for_variant(
  function _get_base_hs (line 144) | def _get_base_hs(
  function _populate_colors (line 171) | def _populate_colors(
  function _process_material_variant (line 216) | def _process_material_variant(
  function generate_palette (line 246) | def generate_palette(

FILE: pyprland/plugins/workspaces_follow_focus.py
  class Extension (line 11) | class Extension(Plugin, environments=[Environment.HYPRLAND]):
    method on_reload (line 20) | async def on_reload(self, reason: ReloadReason = ReloadReason.RELOAD) ...
    method event_focusedmon (line 25) | async def event_focusedmon(self, screenid_name: str) -> None:
    method run_change_workspace (line 45) | async def run_change_workspace(self, direction: str) -> None:

FILE: pyprland/process.py
  function create_subprocess (line 22) | async def create_subprocess(
  class ManagedProcess (line 51) | class ManagedProcess:
    method __init__ (line 75) | def __init__(self, graceful_timeout: float = 1.0) -> None:
    method pid (line 87) | def pid(self) -> int | None:
    method returncode (line 92) | def returncode(self) -> int | None:
    method is_alive (line 97) | def is_alive(self) -> bool:
    method process (line 102) | def process(self) -> asyncio.subprocess.Process | None:
    method start (line 106) | async def start(
    method stop (line 124) | async def stop(self) -> int | None:
    method restart (line 158) | async def restart(self) -> None:
    method wait (line 169) | async def wait(self) -> int:
    method iter_lines (line 180) | async def iter_lines(self) -> AsyncIterator[str]:
  class SupervisedProcess (line 202) | class SupervisedProcess(ManagedProcess):
    method __init__ (line 225) | def __init__(
    method is_supervised (line 249) | def is_supervised(self) -> bool:
    method start (line 253) | async def start(
    method _supervise (line 276) | async def _supervise(self) -> None:
    method stop (line 308) | async def stop(self) -> int | None:

FILE: pyprland/pypr_daemon.py
  function get_event_stream_with_retry (line 15) | async def get_event_stream_with_retry(
  function run_daemon (line 36) | async def run_daemon() -> None:

FILE: pyprland/quickstart/__init__.py
  class Args (line 11) | class Args:
  function parse_args (line 19) | def parse_args(argv: list[str]) -> Args:
  function print_help (line 46) | def print_help() -> None:
  function main (line 59) | def main() -> None:

FILE: pyprland/quickstart/discovery.py
  class PluginInfo (line 18) | class PluginInfo:
  function discover_plugins (line 27) | def discover_plugins() -> list[PluginInfo]:
  function load_plugin_info (line 56) | def load_plugin_info(name: str) -> PluginInfo | None:
  function filter_by_environment (line 87) | def filter_by_environment(plugins: list[PluginInfo], environment: str) -...

FILE: pyprland/quickstart/generator.py
  function get_config_path (line 21) | def get_config_path() -> Path:
  function backup_config (line 32) | def backup_config(config_path: Path) -> Path | None:
  function format_toml_value (line 51) | def format_toml_value(value: Any, indent: int = 0) -> str:  # noqa: PLR0911
  function generate_toml (line 98) | def generate_toml(config: dict) -> str:  # noqa: C901
  function _is_inline_dict (line 166) | def _is_inline_dict(data: dict) -> bool:
  function write_config (line 182) | def write_config(
  function merge_config (line 208) | def merge_config(existing: dict, new: dict) -> dict:
  function load_existing_config (line 229) | def load_existing_config(path: Path | None = None) -> dict | None:

FILE: pyprland/quickstart/helpers/__init__.py
  function detect_app (line 23) | def detect_app(candidates: list[str]) -> str | None:
  function detect_terminal (line 38) | def detect_terminal() -> str | None:
  function get_terminal_command (line 47) | def get_terminal_command(terminal: str, class_name: str) -> str:
  function detect_running_environment (line 61) | def detect_running_environment() -> Environment | None:

FILE: pyprland/quickstart/helpers/monitors.py
  class DetectedMonitor (line 18) | class DetectedMonitor:
  function detect_monitors_hyprland (line 27) | def detect_monitors_hyprland() -> list[DetectedMonitor]:
  function detect_monitors_niri (line 57) | def detect_monitors_niri() -> list[DetectedMonitor]:
  function detect_monitors (line 93) | def detect_monitors(environment: str) -> list[DetectedMonitor]:
  function ask_monitor_layout (line 109) | def ask_monitor_layout(monitors: list[DetectedMonitor]) -> dict:

FILE: pyprland/quickstart/helpers/scratchpads.py
  class ScratchpadConfig (line 90) | class ScratchpadConfig:
  function detect_available_presets (line 101) | def detect_available_presets() -> list[tuple[str, str, str]]:
  function build_command (line 115) | def build_command(
  function create_preset_config (line 149) | def create_preset_config(
  function ask_scratchpads (line 183) | def ask_scratchpads() -> list[ScratchpadConfig]:
  function scratchpad_to_dict (line 237) | def scratchpad_to_dict(config: ScratchpadConfig) -> dict:

FILE: pyprland/quickstart/questions.py
  function _is_path_type (line 15) | def _is_path_type(field_type: type | tuple) -> bool:
  function _is_path_list_type (line 24) | def _is_path_list_type(field_type: type | tuple) -> bool:
  function field_to_question (line 33) | def field_to_question(field: ConfigField, current_value: Any = None) -> ...
  function _ask_choice (line 77) | def _ask_choice(question: str, choices: list, default: Any) -> Any | None:
  function _ask_bool (line 89) | def _ask_bool(question: str, default: Any) -> bool | None:
  function _ask_int (line 98) | def _ask_int(question: str, default: Any) -> int | None:
  function _ask_float (line 120) | def _ask_float(question: str, default: Any) -> float | None:
  function _ask_text (line 142) | def _ask_text(question: str, default: Any) -> str | None:
  function _ask_list (line 149) | def _ask_list(question: str, default: Any) -> list | None:
  function _ask_path (line 168) | def _ask_path(question: str, default: Any, only_directories: bool = Fals...
  function _ask_path_list (line 179) | def _ask_path_list(question: str, default: Any, only_directories: bool =...
  function ask_plugin_options (line 216) | def ask_plugin_options(  # noqa: C901
  function _process_field (line 272) | def _process_field(

FILE: pyprland/quickstart/wizard.py
  function print_banner (line 31) | def print_banner() -> None:
  function ask_environment (line 38) | def ask_environment() -> str | None:
  function ask_plugins (line 74) | def ask_plugins(plugins: list[PluginInfo]) -> list[PluginInfo]:
  function configure_scratchpads (line 113) | def configure_scratchpads(plugin: PluginInfo, environment: str) -> dict:
  function configure_monitors (line 139) | def configure_monitors(plugin: PluginInfo, environment: str) -> dict:
  function configure_plugin (line 165) | def configure_plugin(plugin: PluginInfo, environment: str) -> dict:
  function handle_existing_config (line 186) | def handle_existing_config(config_path: Path) -> bool:
  function build_config (line 223) | def build_config(
  function run_wizard (line 250) | def run_wizard(
  function _show_keybind_hints (line 313) | def _show_keybind_hints(scratchpads_config: dict, environment: str) -> N...

FILE: pyprland/state.py
  class SharedState (line 22) | class SharedState:
    method active_monitors (line 35) | def active_monitors(self) -> list[str]:
    method set_disabled_monitors (line 39) | def set_disabled_monitors(self, disabled: set[str]) -> None:

FILE: pyprland/terminal.py
  function set_terminal_size (line 19) | def set_terminal_size(descriptor: int, rows: int, cols: int) -> None:
  function set_raw_mode (line 30) | def set_raw_mode(descriptor: int) -> None:
  function run_interactive_program (line 44) | def run_interactive_program(command: str) -> None:

FILE: pyprland/utils.py
  function merge (line 27) | def merge(merged: dict[str, Any], obj2: dict[str, Any], replace: bool = ...
  function apply_variables (line 64) | def apply_variables(template: str, variables: dict[str, str]) -> str:
  function apply_filter (line 83) | def apply_filter(text: str, filt_cmd: str) -> str:
  function is_rotated (line 111) | def is_rotated(monitor: MonitorInfo) -> bool:
  function notify_send (line 123) | async def notify_send(text: str, duration: int = 3000, color: str | None...

FILE: pyprland/validate_cli.py
  function _load_plugin_module (line 21) | def _load_plugin_module(name: str) -> type[Plugin] | None:
  function _load_validate_config (line 39) | def _load_validate_config(log: logging.Logger) -> dict:
  function _validate_plugin (line 75) | def _validate_plugin(plugin_name: str, config: dict) -> tuple[int, int]:
  function run_validate (line 125) | def run_validate() -> None:

FILE: pyprland/validation.py
  class ConfigField (line 29) | class ConfigField:  # pylint: disable=too-many-instance-attributes
    method type_name (line 63) | def type_name(self) -> str:
  class ConfigItems (line 81) | class ConfigItems(list):
    method __init__ (line 84) | def __init__(self, *args: ConfigField) -> None:
    method get (line 88) | def get(self, name: str) -> ConfigField | None:
  function _find_similar_key (line 107) | def _find_similar_key(unknown_key: str, known_keys: list[str]) -> str | ...
  function format_config_error (line 123) | def format_config_error(plugin: str, field: str, message: str, suggestio...
  class ConfigValidator (line 141) | class ConfigValidator:
    method __init__ (line 144) | def __init__(self, config: dict, plugin_name: str, logger: logging.Log...
    method validate (line 156) | def validate(self, schema: ConfigItems) -> list[str]:
    method _check_type (line 217) | def _check_type(self, field_def: ConfigField, value: Any) -> str | None:
    method _check_union_type (line 248) | def _check_union_type(
    method _check_bool (line 265) | def _check_bool(self, field_def: ConfigField, value: Any) -> str | None:
    method _check_numeric (line 278) | def _check_numeric(self, field_def: ConfigField, value: Any) -> str | ...
    method _check_str (line 295) | def _check_str(self, field_def: ConfigField, value: Any) -> str | None:
    method _check_list (line 306) | def _check_list(self, field_def: ConfigField, value: Any) -> str | None:
    method _check_dict (line 317) | def _check_dict(self, field_def: ConfigField, value: Any) -> str | None:
    method _validate_dict_children (line 334) | def _validate_dict_children(
    method _get_required_suggestion (line 370) | def _get_required_suggestion(self, field_def: ConfigField) -> str:
    method warn_unknown_keys (line 398) | def warn_unknown_keys(self, schema: ConfigItems) -> list[str]:

FILE: sample_extension/pypr_examples/focus_counter.py
  class Extension (line 12) | class Extension(Plugin):
    method run_counter (line 23) | async def run_counter(self, args: str = "") -> None:
    method event_activewindowv2 (line 33) | async def event_activewindowv2(self, _addr: str) -> None:

FILE: scripts/backquote_as_links.py
  function replace_links (line 6) | def replace_links(match):
  function main (line 14) | def main(filename):

FILE: scripts/check_plugin_docs.py
  class PluginCoverage (line 36) | class PluginCoverage:
  function load_plugin_json (line 49) | def load_plugin_json(plugin_name: str) -> dict | None:
  function extract_option_name (line 58) | def extract_option_name(full_name: str) -> str:
  function find_plugin_pages (line 67) | def find_plugin_pages(plugin_name: str) -> list[Path]:
  function parse_filter_from_component (line 86) | def parse_filter_from_component(content: str, plugin_name: str) -> tuple...
  function check_commands_component (line 119) | def check_commands_component(content: str, plugin_name: str) -> bool:
  function analyze_plugin (line 125) | def analyze_plugin(plugin_name: str) -> PluginCoverage:
  function discover_plugins (line 174) | def discover_plugins() -> list[str]:
  function main (line 186) | def main() -> int:

FILE: scripts/generate_codebase_overview.py
  class ModuleInfo (line 31) | class ModuleInfo:
    method has_good_docstring (line 41) | def has_good_docstring(self) -> bool:
    method docstring_status (line 46) | def docstring_status(self) -> str:
  function parse_module (line 149) | def parse_module(path: Path) -> ModuleInfo:
  function collect_modules (line 193) | def collect_modules() -> dict[str, list[ModuleInfo]]:
  function format_module_row (line 219) | def format_module_row(mod: ModuleInfo) -> str:
  function generate_section (line 234) | def generate_section(title: str, modules: list[ModuleInfo], groupings: d...
  function generate_coverage_report (line 272) | def generate_coverage_report(all_modules: list[ModuleInfo]) -> list[str]:
  function main (line 304) | def main() -> int:

FILE: scripts/generate_monitor_diagrams.py
  class Monitor (line 9) | class Monitor:
    method effective_size (line 19) | def effective_size(self, base_unit: float = 0.1) -> tuple[float, float]:
  class DiagramConfig (line 37) | class DiagramConfig:
  function generate_svg (line 70) | def generate_svg(config: DiagramConfig) -> str:
  function basic_top_of (line 111) | def basic_top_of() -> DiagramConfig:
  function basic_bottom_of (line 119) | def basic_bottom_of() -> DiagramConfig:
  function basic_left_of (line 127) | def basic_left_of() -> DiagramConfig:
  function basic_right_of (line 135) | def basic_right_of() -> DiagramConfig:
  function align_left_start (line 143) | def align_left_start() -> DiagramConfig:
  function align_left_center (line 151) | def align_left_center() -> DiagramConfig:
  function align_left_end (line 161) | def align_left_end() -> DiagramConfig:
  function align_top_start (line 171) | def align_top_start() -> DiagramConfig:
  function align_top_center (line 179) | def align_top_center() -> DiagramConfig:
  function align_top_end (line 189) | def align_top_end() -> DiagramConfig:
  function setup_dual (line 199) | def setup_dual() -> DiagramConfig:
  function setup_triple (line 207) | def setup_triple() -> DiagramConfig:
  function setup_stacked (line 217) | def setup_stacked() -> DiagramConfig:
  function real_world_l_shape (line 227) | def real_world_l_shape() -> DiagramConfig:
  function main (line 281) | def main() -> None:

FILE: scripts/generate_plugin_docs.py
  class ConfigItem (line 41) | class ConfigItem:
  class CommandItem (line 57) | class CommandItem:
  class PluginDoc (line 67) | class PluginDoc:
  function extract_config_from_schema (line 77) | def extract_config_from_schema(schema: list) -> list[ConfigItem]:
  function extract_commands (line 115) | def extract_commands(extension_class: type) -> list[CommandItem]:
  function get_plugin_description (line 137) | def get_plugin_description(extension_class: type) -> str:
  function check_menu_mixin (line 146) | def check_menu_mixin(extension_class: type) -> list:
  function load_scratchpads_schema (line 165) | def load_scratchpads_schema() -> list:
  function discover_plugins (line 172) | def discover_plugins() -> list[str]:
  function load_plugin (line 195) | def load_plugin(plugin_name: str) -> PluginDoc | None:
  function load_metadata (line 281) | def load_metadata() -> dict[str, dict]:
  function generate_plugin_json (line 289) | def generate_plugin_json(plugin_doc: PluginDoc) -> dict:
  function generate_menu_json (line 300) | def generate_menu_json() -> dict:
  function generate_index_json (line 323) | def generate_index_json(plugin_docs: list[PluginDoc], metadata: dict) ->...
  function main (line 353) | def main():

FILE: scripts/pypr.py
  function get_parser (line 29) | def get_parser():

FILE: site/.vitepress/config.mjs
  function loadSidebar (line 25) | function loadSidebar(dir) {
  function buildVersionedConfig (line 45) | function buildVersionedConfig() {
  method _render (line 113) | _render(src, env, md) {

FILE: site/.vitepress/theme/index.js
  method enhanceApp (line 16) | enhanceApp({ app, router }) {

FILE: site/components/configHelpers.js
  function hasChildren (line 14) | function hasChildren(item) {
  function hasDefault (line 23) | function hasDefault(value) {
  function formatDefault (line 36) | function formatDefault(value) {
  function renderDescription (line 55) | function renderDescription(text) {

FILE: site/components/jsonLoader.js
  function getPluginData (line 19) | function getPluginData(name, version = null) {

FILE: site/components/usePluginData.js
  function usePluginData (line 31) | function usePluginData(loader) {

FILE: site/versions/3.0.0/components/configHelpers.js
  function hasChildren (line 14) | function hasChildren(item) {
  function hasDefault (line 23) | function hasDefault(value) {
  function formatDefault (line 36) | function formatDefault(value) {
  function renderDescription (line 55) | function renderDescription(text) {

FILE: site/versions/3.0.0/components/usePluginData.js
  function usePluginData (line 31) | function usePluginData(loader) {

FILE: site/versions/3.1.1/components/configHelpers.js
  function hasChildren (line 14) | function hasChildren(item) {
  function hasDefault (line 23) | function hasDefault(value) {
  function formatDefault (line 36) | function formatDefault(value) {
  function renderDescription (line 55) | function renderDescription(text) {

FILE: site/versions/3.1.1/components/jsonLoader.js
  function getPluginData (line 19) | function getPluginData(name, version = null) {

FILE: site/versions/3.1.1/components/usePluginData.js
  function usePluginData (line 31) | function usePluginData(loader) {

FILE: site/versions/3.2.1/components/configHelpers.js
  function hasChildren (line 14) | function hasChildren(item) {
  function hasDefault (line 23) | function hasDefault(value) {
  function formatDefault (line 36) | function formatDefault(value) {
  function renderDescription (line 55) | function renderDescription(text) {

FILE: site/versions/3.2.1/components/jsonLoader.js
  function getPluginData (line 19) | function getPluginData(name, version = null) {

FILE: site/versions/3.2.1/components/usePluginData.js
  function usePluginData (line 31) | function usePluginData(loader) {

FILE: tests/conftest.py
  function test_logger (line 30) | def test_logger():
  function pytest_configure (line 58) | def pytest_configure():
  function _contains_error (line 66) | def _contains_error(text: str) -> str | None:
  function pytest_runtest_makereport (line 80) | def pytest_runtest_makereport(item, call):
  class GlobalMocks (line 101) | class GlobalMocks:
    method reset (line 114) | def reset(self):
    method pypr (line 119) | async def pypr(self, cmd):
    method wait_queues (line 125) | async def wait_queues(self):
    method send_event (line 140) | async def send_event(self, cmd):
  function make_extension (line 149) | def make_extension(plugin_class, name: str | None = None, *, logger=None...
  function mocked_unix_server (line 231) | async def mocked_unix_server(command_reader, *_):
  function mocked_unix_connection (line 238) | async def mocked_unix_connection(path):
  function empty_config (line 246) | async def empty_config(monkeypatch):
  function third_monitor (line 253) | async def third_monitor(monkeypatch):
  function sample1_config (line 261) | async def sample1_config(monkeypatch):
  function mocked_hyprctl_json (line 267) | async def mocked_hyprctl_json(self, command, *, log=None, **kwargs):
  function subprocess_shell_mock (line 289) | def subprocess_shell_mock(mocker):
  function server_fixture (line 305) | async def server_fixture(monkeypatch, mocker):

FILE: tests/test_adapters_fallback.py
  function test_log (line 11) | def test_log():
  function mock_state (line 21) | def mock_state():
  class TestWaylandBackend (line 26) | class TestWaylandBackend:
    method test_parse_single_monitor (line 29) | def test_parse_single_monitor(self, test_log, mock_state):
    method test_parse_multiple_monitors (line 53) | def test_parse_multiple_monitors(self, test_log, mock_state):
    method test_parse_rotated_monitor (line 82) | def test_parse_rotated_monitor(self, test_log, mock_state):
    method test_parse_disabled_monitor_excluded (line 98) | def test_parse_disabled_monitor_excluded(self, test_log, mock_state):
    method test_parse_disabled_monitor_included (line 122) | def test_parse_disabled_monitor_included(self, test_log, mock_state):
    method test_parse_no_mode_skipped (line 145) | def test_parse_no_mode_skipped(self, test_log, mock_state):
    method test_parse_empty_output (line 161) | def test_parse_empty_output(self, test_log, mock_state):
  class TestXorgBackend (line 169) | class TestXorgBackend:
    method test_parse_single_monitor (line 172) | def test_parse_single_monitor(self, test_log, mock_state):
    method test_parse_multiple_monitors (line 190) | def test_parse_multiple_monitors(self, test_log, mock_state):
    method test_parse_rotated_monitor (line 209) | def test_parse_rotated_monitor(self, test_log, mock_state):
    method test_parse_inverted_monitor (line 221) | def test_parse_inverted_monitor(self, test_log, mock_state):
    method test_parse_disconnected_excluded (line 233) | def test_parse_disconnected_excluded(self, test_log, mock_state):
    method test_parse_disconnected_included (line 246) | def test_parse_disconnected_included(self, test_log, mock_state):
    method test_parse_empty_output (line 260) | def test_parse_empty_output(self, test_log, mock_state):
    method test_parse_connected_no_mode (line 267) | def test_parse_connected_no_mode(self, test_log, mock_state):

FILE: tests/test_ansi.py
  function test_colorize_single_code (line 21) | def test_colorize_single_code():
  function test_colorize_multiple_codes (line 27) | def test_colorize_multiple_codes():
  function test_colorize_no_codes (line 33) | def test_colorize_no_codes():
  function test_make_style (line 39) | def test_make_style():
  function test_make_style_no_codes (line 46) | def test_make_style_no_codes():
  function test_should_colorize_respects_no_color (line 53) | def test_should_colorize_respects_no_color():
  function test_should_colorize_respects_force_color (line 59) | def test_should_colorize_respects_force_color():
  function test_should_colorize_non_tty (line 67) | def test_should_colorize_non_tty():
  function test_log_styles (line 74) | def test_log_styles():
  function test_handler_styles (line 81) | def test_handler_styles():
  function test_constants (line 87) | def test_constants():

FILE: tests/test_command.py
  function pyprland_app (line 14) | def pyprland_app():
  function test_load_config_toml (line 25) | async def test_load_config_toml(pyprland_app):
  function test_load_config_toml_with_notify (line 59) | async def test_load_config_toml_with_notify(pyprland_app):
  function test_load_config_json_fallback (line 89) | async def test_load_config_json_fallback(pyprland_app):
  function test_load_config_missing (line 126) | async def test_load_config_missing(pyprland_app):
  function test_run_plugin_handler_success (line 137) | async def test_run_plugin_handler_success(pyprland_app):
  function test_run_plugin_handler_exception (line 152) | async def test_run_plugin_handler_exception(pyprland_app, monkeypatch):
  function test_call_handler_dispatch (line 172) | async def test_call_handler_dispatch(pyprland_app):
  function test_call_handler_dispatch_with_wait (line 205) | async def test_call_handler_dispatch_with_wait(pyprland_app):
  function test_call_handler_unknown_command (line 245) | async def test_call_handler_unknown_command(pyprland_app):
  function test_read_command_socket (line 259) | async def test_read_command_socket(pyprland_app):
  function test_read_command_socket_error (line 282) | async def test_read_command_socket_error(pyprland_app):
  function test_read_command_socket_unknown (line 303) | async def test_read_command_socket_unknown(pyprland_app):
  function test_read_command_exit (line 324) | async def test_read_command_exit(pyprland_app):
  function test_load_plugin_module_builtin (line 350) | def test_load_plugin_module_builtin():
  function test_load_plugin_module_not_found (line 357) | def test_load_plugin_module_not_found():
  function test_run_validate_valid_config (line 363) | def test_run_validate_valid_config():
  function test_run_validate_missing_required_field (line 392) | def test_run_validate_missing_required_field():
  function test_run_validate_config_not_found (line 421) | def test_run_validate_config_not_found():

FILE: tests/test_command_registry.py
  class TestParseDocstring (line 11) | class TestParseDocstring:
    method test_required_arg (line 14) | def test_required_arg(self):
    method test_optional_arg (line 23) | def test_optional_arg(self):
    method test_mixed_args (line 31) | def test_mixed_args(self):
    method test_no_args (line 41) | def test_no_args(self):
    method test_pipe_choices (line 48) | def test_pipe_choices(self):
    method test_empty_docstring (line 56) | def test_empty_docstring(self):
    method test_multiline_docstring (line 63) | def test_multiline_docstring(self):
    method test_arg_only_no_description (line 75) | def test_arg_only_no_description(self):
    method test_multiple_optional_args (line 82) | def test_multiple_optional_args(self):
  class TestExtractCommandsFromObject (line 90) | class TestExtractCommandsFromObject:
    method test_extract_from_class (line 93) | def test_extract_from_class(self):
    method test_extract_from_instance (line 111) | def test_extract_from_instance(self):
    method test_source_preserved (line 124) | def test_source_preserved(self):
    method test_args_extracted (line 134) | def test_args_extracted(self):
    method test_no_commands (line 146) | def test_no_commands(self):
    method test_method_without_docstring (line 156) | def test_method_without_docstring(self):
  class TestGetClientCommands (line 168) | class TestGetClientCommands:
    method test_returns_edit_and_validate (line 171) | def test_returns_edit_and_validate(self):
    method test_source_is_client (line 178) | def test_source_is_client(self):
    method test_has_descriptions (line 184) | def test_has_descriptions(self):
  class TestCommandInfoDataclass (line 192) | class TestCommandInfoDataclass:
    method test_create_command_info (line 195) | def test_create_command_info(self):
  class TestCommandArgDataclass (line 209) | class TestCommandArgDataclass:
    method test_create_required_arg (line 212) | def test_create_required_arg(self):
    method test_create_optional_arg (line 218) | def test_create_optional_arg(self):
  function _make_cmd (line 225) | def _make_cmd(name: str, source: str) -> CommandInfo:
  class TestGetParentPrefixes (line 236) | class TestGetParentPrefixes:
    method test_same_source_groups (line 239) | def test_same_source_groups(self):
    method test_different_sources_not_grouped (line 249) | def test_different_sources_not_grouped(self):
    method test_mixed_same_and_different_sources (line 259) | def test_mixed_same_and_different_sources(self):
    method test_single_command_no_grouping (line 271) | def test_single_command_no_grouping(self):
    method test_legacy_iterable_groups_all (line 279) | def test_legacy_iterable_groups_all(self):
  class TestBuildCommandTree (line 287) | class TestBuildCommandTree:
    method test_cross_plugin_commands_stay_flat (line 290) | def test_cross_plugin_commands_stay_flat(self):
    method test_same_plugin_commands_grouped (line 309) | def test_same_plugin_commands_grouped(self):
    method test_mixed_plugins_correct_grouping (line 329) | def test_mixed_plugins_correct_grouping(self):
    method test_root_command_with_same_source_children (line 349) | def test_root_command_with_same_source_children(self):
  class TestGetDisplayName (line 366) | class TestGetDisplayName:
    method test_hierarchical_command (line 369) | def test_hierarchical_command(self):
    method test_non_hierarchical_command (line 374) | def test_non_hierarchical_command(self):
    method test_no_parent_prefixes (line 379) | def test_no_parent_prefixes(self):

FILE: tests/test_common_types.py
  function test_version_info_init (line 4) | def test_version_info_init():
  function test_version_info_defaults (line 11) | def test_version_info_defaults():
  function test_version_info_compare_major (line 18) | def test_version_info_compare_major():
  function test_version_info_compare_minor (line 25) | def test_version_info_compare_minor():
  function test_version_info_compare_micro (line 32) | def test_version_info_compare_micro():
  function test_version_info_equality (line 39) | def test_version_info_equality():

FILE: tests/test_common_utils.py
  function test_merge_dicts (line 5) | def test_merge_dicts():
  function test_merge_lists (line 12) | def test_merge_lists():
  function test_merge_overwrite (line 19) | def test_merge_overwrite():
  function test_apply_filter_empty (line 26) | def test_apply_filter_empty():
  function test_apply_filter_substitute (line 30) | def test_apply_filter_substitute():
  function test_apply_filter_substitute_global (line 35) | def test_apply_filter_substitute_global():
  function test_apply_filter_malformed (line 40) | def test_apply_filter_malformed():
  function test_is_rotated (line 46) | def test_is_rotated():

FILE: tests/test_completions.py
  function _load_commands_from_json (line 27) | def _load_commands_from_json() -> dict[str, CommandInfo]:
  function _build_completions_from_json (line 51) | def _build_completions_from_json() -> dict[str, CommandCompletion]:
  function commands_from_json (line 100) | def commands_from_json() -> dict[str, CommandCompletion]:
  function zsh_script (line 106) | def zsh_script(commands_from_json: dict[str, CommandCompletion]) -> str:
  function bash_script (line 112) | def bash_script(commands_from_json: dict[str, CommandCompletion]) -> str:
  function fish_script (line 118) | def fish_script(commands_from_json: dict[str, CommandCompletion]) -> str:
  class TestZshSyntax (line 127) | class TestZshSyntax:
    method test_syntax_valid (line 130) | def test_syntax_valid(self, zsh_script: str) -> None:
  class TestBashSyntax (line 141) | class TestBashSyntax:
    method test_syntax_valid (line 144) | def test_syntax_valid(self, bash_script: str) -> None:
  class TestFishSyntax (line 155) | class TestFishSyntax:
    method test_syntax_valid (line 158) | def test_syntax_valid(self, fish_script: str, tmp_path: Path) -> None:
  class TestZshCommandPresence (line 173) | class TestZshCommandPresence:
    method test_all_commands_present (line 176) | def test_all_commands_present(self, zsh_script: str, commands_from_jso...
    method test_help_case_exists (line 181) | def test_help_case_exists(self, zsh_script: str) -> None:
    method test_help_uses_commands_array (line 185) | def test_help_uses_commands_array(self, zsh_script: str) -> None:
  class TestBashCommandPresence (line 190) | class TestBashCommandPresence:
    method test_all_commands_present (line 193) | def test_all_commands_present(self, bash_script: str, commands_from_js...
    method test_help_case_exists (line 198) | def test_help_case_exists(self, bash_script: str) -> None:
  class TestFishCommandPresence (line 203) | class TestFishCommandPresence:
    method test_all_commands_present (line 206) | def test_all_commands_present(self, fish_script: str, commands_from_js...
    method test_help_completion_exists (line 211) | def test_help_completion_exists(self, fish_script: str) -> None:
  class TestSubcommandCompletions (line 219) | class TestSubcommandCompletions:
    method test_zsh_wall_subcommands (line 222) | def test_zsh_wall_subcommands(self, zsh_script: str) -> None:
    method test_bash_wall_subcommands (line 229) | def test_bash_wall_subcommands(self, bash_script: str) -> None:
    method test_fish_wall_subcommands (line 235) | def test_fish_wall_subcommands(self, fish_script: str) -> None:
    method test_zsh_help_wall_subcommands (line 240) | def test_zsh_help_wall_subcommands(self, zsh_script: str) -> None:
    method test_bash_help_wall_subcommands (line 245) | def test_bash_help_wall_subcommands(self, bash_script: str) -> None:
    method test_fish_help_wall_subcommands (line 250) | def test_fish_help_wall_subcommands(self, fish_script: str) -> None:

FILE: tests/test_config.py
  function test_config_access (line 5) | def test_config_access(test_logger):
  function test_get_bool (line 12) | def test_get_bool(test_logger):
  function test_get_int (line 53) | def test_get_int(test_logger):
  function test_get_float (line 61) | def test_get_float(test_logger):
  function test_get_str (line 69) | def test_get_str(test_logger):
  function test_iter_subsections (line 76) | def test_iter_subsections(test_logger):
  function test_config_field_defaults (line 102) | def test_config_field_defaults():
  function test_config_field_with_values (line 113) | def test_config_field_with_values():
  function test_find_similar_key (line 131) | def test_find_similar_key():
  function test_format_config_error (line 147) | def test_format_config_error():
  function test_config_validator_required_fields (line 158) | def test_config_validator_required_fields(test_logger):
  function test_config_validator_type_checking (line 178) | def test_config_validator_type_checking(test_logger):
  function test_config_validator_choices (line 217) | def test_config_validator_choices(test_logger):
  function test_config_validator_unknown_keys (line 236) | def test_config_validator_unknown_keys(test_logger):
  function test_config_validator_numeric_strings (line 260) | def test_config_validator_numeric_strings(test_logger):
  function test_config_validator_optional_fields (line 274) | def test_config_validator_optional_fields(test_logger):
  function test_config_validator_union_types (line 287) | def test_config_validator_union_types(test_logger):
  function test_config_validator_children_schema (line 327) | def test_config_validator_children_schema(test_logger):
  function test_config_validator_children_type_errors (line 347) | def test_config_validator_children_type_errors(test_logger):
  function test_config_validator_children_unknown_keys (line 367) | def test_config_validator_children_unknown_keys(test_logger):
  function test_config_validator_children_collects_all_errors (line 387) | def test_config_validator_children_collects_all_errors(test_logger):
  function test_config_validator_children_non_dict_value (line 418) | def test_config_validator_children_non_dict_value(test_logger):
  function test_config_validator_nested_children_schema (line 438) | def test_config_validator_nested_children_schema(test_logger):
  function test_get_template_names_single_use (line 484) | def test_get_template_names_single_use():
  function test_get_template_names_list_use (line 492) | def test_get_template_names_list_use():
  function test_get_template_names_no_use (line 501) | def test_get_template_names_no_use():
  function test_get_template_names_skips_non_dict_and_dotted (line 509) | def test_get_template_names_skips_non_dict_and_dotted():
  function test_is_pure_template_true (line 519) | def test_is_pure_template_true():
  function test_is_pure_template_false_when_has_command (line 528) | def test_is_pure_template_false_when_has_command():
  function test_is_pure_template_false_when_not_referenced (line 538) | def test_is_pure_template_false_when_not_referenced():
  function test_is_pure_template_false_for_nonexistent (line 548) | def test_is_pure_template_false_for_nonexistent():
  function test_validate_pure_template_no_errors (line 553) | def test_validate_pure_template_no_errors():
  function test_validate_real_scratchpad_without_command_errors (line 566) | def test_validate_real_scratchpad_without_command_errors():
  function test_validate_static_with_templates (line 579) | def test_validate_static_with_templates():

FILE: tests/test_event_signatures.py
  function get_protocol_signatures (line 31) | def get_protocol_signatures(protocol_cls: type) -> dict[str, inspect.Sig...
  function signatures_compatible (line 40) | def signatures_compatible(actual: inspect.Signature, expected: inspect.S...
  function test_hyprland_event_signatures (line 76) | def test_hyprland_event_signatures():
  function test_niri_event_signatures (line 120) | def test_niri_event_signatures():
  function test_protocol_methods_documented (line 162) | def test_protocol_methods_documented():

FILE: tests/test_external_plugins.py
  function external_plugin_config (line 13) | async def external_plugin_config(monkeypatch):
  function test_ext_plugin (line 25) | async def test_ext_plugin():

FILE: tests/test_http.py
  class TestFallbackClientTimeout (line 30) | class TestFallbackClientTimeout:
    method test_default_timeout (line 33) | def test_default_timeout(self) -> None:
    method test_custom_timeout (line 38) | def test_custom_timeout(self) -> None:
  class TestFallbackResponse (line 44) | class TestFallbackResponse:
    method test_status_and_url (line 47) | def test_status_and_url(self) -> None:
    method test_json (line 54) | async def test_json(self) -> None:
    method test_read (line 62) | async def test_read(self) -> None:
    method test_raise_for_status_ok (line 69) | def test_raise_for_status_ok(self) -> None:
    method test_raise_for_status_redirect (line 74) | def test_raise_for_status_redirect(self) -> None:
    method test_raise_for_status_client_error (line 79) | def test_raise_for_status_client_error(self) -> None:
    method test_raise_for_status_server_error (line 85) | def test_raise_for_status_server_error(self) -> None:
  class TestFallbackClientSession (line 92) | class TestFallbackClientSession:
    method reset_warning (line 96) | def reset_warning(self) -> None:
    method _mock_response (line 100) | def _mock_response(self, data: bytes, status: int = HTTP_OK, url: str ...
    method test_get_simple (line 109) | async def test_get_simple(self) -> None:
    method test_get_with_params (line 121) | async def test_get_with_params(self) -> None:
    method test_get_with_headers (line 137) | async def test_get_with_headers(self) -> None:
    method test_get_with_timeout (line 153) | async def test_get_with_timeout(self) -> None:
    method test_http_error_returns_response (line 167) | async def test_http_error_returns_response(self) -> None:
    method test_network_error_raises_client_error (line 184) | async def test_network_error_raises_client_error(self) -> None:
    method test_timeout_raises_client_error (line 195) | async def test_timeout_raises_client_error(self) -> None:
    method test_session_context_manager (line 206) | async def test_session_context_manager(self) -> None:
    method test_close (line 219) | async def test_close(self) -> None:
    method test_warns_on_first_use (line 226) | def test_warns_on_first_use(self) -> None:
    method test_warns_only_once (line 231) | def test_warns_only_once(self) -> None:
  class TestFallbackClientError (line 242) | class TestFallbackClientError:
    method test_is_exception (line 245) | def test_is_exception(self) -> None:
    method test_message (line 249) | def test_message(self) -> None:

FILE: tests/test_interface.py
  class ConcretePlugin (line 7) | class ConcretePlugin(Plugin):
  function plugin (line 14) | def plugin():
  function test_plugin_init (line 31) | async def test_plugin_init(plugin):
  function test_load_config (line 41) | async def test_load_config(plugin):
  function test_get_clients_filter (line 52) | async def test_get_clients_filter(plugin):

FILE: tests/test_ipc.py
  function test_log (line 12) | def test_log():
  function mock_open_connection (line 22) | def mock_open_connection(mocker):
  function test_hyprctl_connection_context_manager (line 34) | async def test_hyprctl_connection_context_manager(mock_open_connection):
  function test_hyprctl_connection_error (line 47) | async def test_hyprctl_connection_error(mocker):
  function test_get_response (line 59) | async def test_get_response(mock_open_connection):
  function test_hyprland_backend_execute_success (line 72) | async def test_hyprland_backend_execute_success(mock_open_connection, te...
  function test_hyprland_backend_execute_failure (line 86) | async def test_hyprland_backend_execute_failure(mock_open_connection, te...
  function test_hyprland_backend_execute_batch (line 100) | async def test_hyprland_backend_execute_batch(mock_open_connection, test...
  function test_backend_get_client_props (line 119) | async def test_backend_get_client_props(mock_open_connection, test_log):

FILE: tests/test_load_all.py
  function load_all_config (line 7) | async def load_all_config(monkeypatch):
  function test_load_all (line 33) | async def test_load_all(subprocess_shell_mock):

FILE: tests/test_monitors_commands.py
  class TestBuildHyprlandCommand (line 15) | class TestBuildHyprlandCommand:
    method test_basic_command (line 18) | def test_basic_command(self):
    method test_with_custom_resolution_string (line 36) | def test_with_custom_resolution_string(self):
    method test_with_custom_resolution_list (line 54) | def test_with_custom_resolution_list(self):
    method test_with_custom_scale (line 72) | def test_with_custom_scale(self):
    method test_with_custom_rate (line 90) | def test_with_custom_rate(self):
    method test_with_transform (line 108) | def test_with_transform(self):
  class TestBuildNiriPositionAction (line 127) | class TestBuildNiriPositionAction:
    method test_basic_position (line 130) | def test_basic_position(self):
    method test_zero_position (line 141) | def test_zero_position(self):
  class TestBuildNiriScaleAction (line 149) | class TestBuildNiriScaleAction:
    method test_scale_1 (line 152) | def test_scale_1(self):
    method test_scale_2 (line 158) | def test_scale_2(self):
    method test_scale_fractional (line 164) | def test_scale_fractional(self):
  class TestBuildNiriTransformAction (line 171) | class TestBuildNiriTransformAction:
    method test_transform_0 (line 174) | def test_transform_0(self):
    method test_transform_1 (line 185) | def test_transform_1(self):
    method test_transform_4 (line 191) | def test_transform_4(self):
    method test_transform_string (line 197) | def test_transform_string(self):
    method test_transform_out_of_range (line 203) | def test_transform_out_of_range(self):
  class TestBuildNiriDisableAction (line 210) | class TestBuildNiriDisableAction:
    method test_disable (line 213) | def test_disable(self):
  class TestNiriTransformNames (line 220) | class TestNiriTransformNames:
    method test_length (line 223) | def test_length(self):
    method test_values (line 227) | def test_values(self):

FILE: tests/test_monitors_layout.py
  function make_monitor (line 16) | def make_monitor(name, width=1920, height=1080, x=0, y=0, scale=1.0, tra...
  class TestGetDims (line 31) | class TestGetDims:
    method test_basic_dimensions (line 34) | def test_basic_dimensions(self):
    method test_with_scale (line 43) | def test_with_scale(self):
    method test_with_config_scale (line 52) | def test_with_config_scale(self):
    method test_with_config_resolution_string (line 62) | def test_with_config_resolution_string(self):
    method test_with_config_resolution_list (line 72) | def test_with_config_resolution_list(self):
    method test_with_transform_90 (line 82) | def test_with_transform_90(self):
    method test_with_transform_270 (line 91) | def test_with_transform_270(self):
    method test_with_transform_180 (line 100) | def test_with_transform_180(self):
  class TestComputeXy (line 110) | class TestComputeXy:
    method test_left (line 113) | def test_left(self):
    method test_right (line 123) | def test_right(self):
    method test_top (line 133) | def test_top(self):
    method test_bottom (line 143) | def test_bottom(self):
    method test_left_center (line 153) | def test_left_center(self):
    method test_right_center (line 163) | def test_right_center(self):
    method test_top_center (line 173) | def test_top_center(self):
    method test_bottom_end (line 183) | def test_bottom_end(self):
    method test_unknown_rule_returns_ref_position (line 193) | def test_unknown_rule_returns_ref_position(self):
  class TestBuildGraph (line 204) | class TestBuildGraph:
    method test_basic_graph (line 207) | def test_basic_graph(self):
    method test_multiple_targets_reported (line 224) | def test_multiple_targets_reported(self):
    method test_ignores_monitor_props (line 243) | def test_ignores_monitor_props(self):
    method test_ignores_disables (line 258) | def test_ignores_disables(self):
  class TestComputePositions (line 274) | class TestComputePositions:
    method test_chain_layout (line 277) | def test_chain_layout(self):
    method test_circular_dependency (line 297) | def test_circular_dependency(self):
    method test_anchor_monitor (line 314) | def test_anchor_monitor(self):
  class TestFindCyclePath (line 332) | class TestFindCyclePath:
    method test_simple_cycle (line 335) | def test_simple_cycle(self):
    method test_three_way_cycle (line 349) | def test_three_way_cycle(self):
    method test_no_clear_cycle (line 364) | def test_no_clear_cycle(self):
  class TestConstants (line 379) | class TestConstants:
    method test_monitor_props (line 382) | def test_monitor_props(self):
    method test_max_cycle_path_length (line 389) | def test_max_cycle_path_length(self):

FILE: tests/test_monitors_resolution.py
  function make_monitor (line 11) | def make_monitor(name, description=None, width=1920, height=1080, x=0, y...
  class TestGetMonitorByPattern (line 26) | class TestGetMonitorByPattern:
    method test_match_by_name (line 29) | def test_match_by_name(self):
    method test_match_by_description_substring (line 43) | def test_match_by_description_substring(self):
    method test_no_match (line 57) | def test_no_match(self):
    method test_cache_hit (line 69) | def test_cache_hit(self):
    method test_cache_populated (line 82) | def test_cache_populated(self):
    method test_name_takes_precedence (line 97) | def test_name_takes_precedence(self):
  class TestResolvePlacementConfig (line 112) | class TestResolvePlacementConfig:
    method test_basic_resolution (line 115) | def test_basic_resolution(self):
    method test_description_pattern_resolution (line 130) | def test_description_pattern_resolution(self):
    method test_preserves_monitor_props (line 145) | def test_preserves_monitor_props(self):
    method test_multiple_targets_as_list (line 166) | def test_multiple_targets_as_list(self):
    method test_unmatched_pattern_ignored (line 181) | def test_unmatched_pattern_ignored(self):
    method test_unmatched_target_ignored (line 196) | def test_unmatched_target_ignored(self):
    method test_uses_provided_cache (line 210) | def test_uses_provided_cache(self):
    method test_empty_config (line 226) | def test_empty_config(self):
    method test_empty_monitors (line 237) | def test_empty_monitors(self):
    method test_mixed_props_and_rules (line 248) | def test_mixed_props_and_rules(self):

FILE: tests/test_plugin_expose.py
  function sample_clients (line 8) | def sample_clients():
  function extension (line 17) | def extension():
  function test_exposed_clients_filtering (line 24) | async def test_exposed_clients_filtering(extension, sample_clients):
  function test_run_expose_enable (line 39) | async def test_run_expose_enable(extension, sample_clients):
  function test_run_expose_disable (line 63) | async def test_run_expose_disable(extension, sample_clients):
  function test_run_expose_empty_workspace (line 87) | async def test_run_expose_empty_workspace(extension):

FILE: tests/test_plugin_fetch_client_menu.py
  function extension (line 9) | def extension():
  function test_run_unfetch_client_success (line 24) | async def test_run_unfetch_client_success(extension):
  function test_run_unfetch_client_unknown (line 33) | async def test_run_unfetch_client_unknown(extension):
  function test_run_fetch_client_menu (line 43) | async def test_run_fetch_client_menu(extension):
  function test_run_fetch_client_menu_cancel (line 67) | async def test_run_fetch_client_menu_cancel(extension):
  function test_center_window_on_monitor_floats_and_centers (line 77) | async def test_center_window_on_monitor_floats_and_centers(extension):
  function test_center_window_already_floating (line 95) | async def test_center_window_already_floating(extension):
  function test_center_window_resizes_if_too_large (line 113) | async def test_center_window_resizes_if_too_large(extension):
  function test_center_window_with_rotated_monitor (line 132) | async def test_center_window_with_rotated_monitor(extension):
  function test_center_window_disabled (line 153) | async def test_center_window_disabled(extension):
  function test_center_window_with_scaled_monitor (line 171) | async def test_center_window_with_scaled_monitor(extension):

FILE: tests/test_plugin_layout_center.py
  function extension (line 8) | def extension():
  function test_sanity_check_fails (line 19) | async def test_sanity_check_fails(extension):
  function test_sanity_check_passes (line 29) | async def test_sanity_check_passes(extension):
  function test_calculate_geometry (line 37) | async def test_calculate_geometry(extension):
  function test_change_focus_next (line 55) | async def test_change_focus_next(extension):
  function test_change_focus_prev_wrap (line 70) | async def test_change_focus_prev_wrap(extension):

FILE: tests/test_plugin_lost_windows.py
  function extension (line 7) | def extension():
  function test_contains (line 11) | def test_contains():
  function test_run_attract_lost (line 27) | async def test_run_attract_lost(extension):

FILE: tests/test_plugin_magnify.py
  function magnify_config (line 9) | async def magnify_config(monkeypatch):
  function test_magnify (line 16) | async def test_magnify(magnify_config, server_fixture):

FILE: tests/test_plugin_menubar.py
  function test_get_pid_from_layers_hyprland (line 7) | def test_get_pid_from_layers_hyprland():
  function test_is_bar_in_layers_niri (line 34) | def test_is_bar_in_layers_niri():
  function test_is_bar_alive_hyprland (line 54) | async def test_is_bar_alive_hyprland():
  function test_is_bar_alive_niri (line 76) | async def test_is_bar_alive_niri():
  function extension (line 99) | def extension():
  function test_get_best_monitor_hyprland (line 114) | async def test_get_best_monitor_hyprland(extension):
  function test_get_best_monitor_niri (line 141) | async def test_get_best_monitor_niri(extension):
  function test_set_best_monitor (line 174) | async def test_set_best_monitor(extension):
  function test_event_monitoradded (line 187) | async def test_event_monitoradded(extension):
  function test_run_bar (line 203) | async def test_run_bar(extension):

FILE: tests/test_plugin_monitor.py
  function shapeL_config (line 10) | async def shapeL_config(monkeypatch):
  function flipped_shapeL_config (line 29) | async def flipped_shapeL_config(monkeypatch):
  function descr_config (line 48) | async def descr_config(monkeypatch):
  function topdown_config (line 67) | async def topdown_config(monkeypatch):
  function bottomup_config (line 86) | async def bottomup_config(monkeypatch):
  function get_xrandr_calls (line 104) | def get_xrandr_calls(mock):
  function reversed_config (line 109) | async def reversed_config(monkeypatch):
  function assert_modes (line 127) | def assert_modes(call_list, expected=None, allow_empty=False):
  function test_relayout (line 140) | async def test_relayout():
  function test_3screens_relayout (line 154) | async def test_3screens_relayout():
  function test_3screens_relayout_b (line 169) | async def test_3screens_relayout_b():
  function test_shape_l (line 184) | async def test_shape_l():
  function test_flipped_shape_l (line 199) | async def test_flipped_shape_l():
  function test_3screens_rev_relayout (line 214) | async def test_3screens_rev_relayout():
  function test_events (line 229) | async def test_events():
  function test_events_d (line 243) | async def test_events_d():
  function test_events2 (line 258) | async def test_events2():
  function test_events3 (line 274) | async def test_events3():
  function test_events3b (line 290) | async def test_events3b():
  function test_events4 (line 306) | async def test_events4():
  function test_nothing (line 322) | async def test_nothing():
  function disables_config (line 339) | async def disables_config(monkeypatch):
  function disables_list_config (line 358) | async def disables_list_config(monkeypatch):
  function assert_modes (line 376) | def assert_modes(call_list, expected=None, allow_empty=False):
  function test_disables_monitor (line 389) | async def test_disables_monitor():
  function test_disables_monitor_list (line 404) | async def test_disables_monitor_list():

FILE: tests/test_plugin_scratchpads.py
  function scratchpads (line 13) | def scratchpads(monkeypatch, mocker):
  function animated_scratchpads (line 27) | def animated_scratchpads(request, monkeypatch, mocker):
  function no_proc_scratchpads (line 44) | def no_proc_scratchpads(request, monkeypatch, mocker):
  function test_not_found (line 61) | async def test_not_found(scratchpads, subprocess_shell_mock, server_fixt...
  function gen_call_set (line 74) | def gen_call_set(call_list: list) -> set[str]:
  function _send_window_events (line 85) | async def _send_window_events(address="12345677890", klass="kitty-dropte...
  function test_std (line 92) | async def test_std(scratchpads, subprocess_shell_mock, server_fixture):
  function test_animated (line 130) | async def test_animated(animated_scratchpads, subprocess_shell_mock, ser...
  function test_no_proc (line 166) | async def test_no_proc(no_proc_scratchpads, subprocess_shell_mock, serve...
  function test_attach_sanity_checks (line 190) | async def test_attach_sanity_checks(scratchpads, subprocess_shell_mock, ...
  function test_attach_workspace_sanity (line 233) | async def test_attach_workspace_sanity(scratchpads, subprocess_shell_moc...
  function exclude_scratchpads (line 330) | def exclude_scratchpads(monkeypatch, mocker):
  function test_excluded_scratches_isolation (line 353) | async def test_excluded_scratches_isolation(exclude_scratchpads, subproc...
  function exclude_wildcard_scratchpads (line 389) | def exclude_wildcard_scratchpads(monkeypatch, mocker):
  function test_excluded_wildcard_list (line 412) | async def test_excluded_wildcard_list(exclude_wildcard_scratchpads, subp...
  function test_command_serialization (line 446) | async def test_command_serialization(scratchpads, subprocess_shell_mock,...

FILE: tests/test_plugin_shift_monitors.py
  function extension (line 10) | def extension():
  function test_init (line 18) | async def test_init(extension):
  function test_shift_positive (line 28) | async def test_shift_positive(extension):
  function test_shift_negative (line 41) | async def test_shift_negative(extension):
  function test_monitor_events (line 53) | async def test_monitor_events(extension):

FILE: tests/test_plugin_shortcuts_menu.py
  function extension (line 9) | def extension(test_logger):
  function test_run_menu_simple_command (line 27) | async def test_run_menu_simple_command(extension):
  function test_run_menu_nested (line 43) | async def test_run_menu_nested(extension):
  function test_run_menu_cancellation (line 60) | async def test_run_menu_cancellation(extension):
  function test_run_menu_with_skip_single (line 70) | async def test_run_menu_with_skip_single(extension):
  function test_run_menu_formatting (line 94) | async def test_run_menu_formatting(extension):

FILE: tests/test_plugin_stash.py
  function extension (line 9) | def extension():
  function styled_extension (line 14) | def styled_extension():
  function test_stash_moves_window_to_special_workspace (line 22) | async def test_stash_moves_window_to_special_workspace(extension):
  function test_stash_custom_name (line 38) | async def test_stash_custom_name(extension):
  function test_stash_already_floating_no_toggle (line 52) | async def test_stash_already_floating_no_toggle(extension):
  function test_unstash_moves_window_back (line 71) | async def test_unstash_moves_window_back(extension):
  function test_unstash_from_different_stash (line 88) | async def test_unstash_from_different_stash(extension):
  function test_unstash_originally_floating_stays_floating (line 104) | async def test_unstash_originally_floating_stays_floating(extension):
  function test_stash_no_active_window (line 122) | async def test_stash_no_active_window(extension):
  function test_stash_on_shown_window_removes_from_stash (line 136) | async def test_stash_on_shown_window_removes_from_stash(extension):
  function test_stash_on_shown_window_originally_floating_no_toggle (line 167) | async def test_stash_on_shown_window_originally_floating_no_toggle(exten...
  function test_stash_on_last_shown_window_clears_visibility (line 188) | async def test_stash_on_last_shown_window_clears_visibility(extension):
  function test_stash_toggle_show_moves_windows_to_active_workspace (line 213) | async def test_stash_toggle_show_moves_windows_to_active_workspace(exten...
  function test_stash_toggle_show_all_moves_are_silent (line 228) | async def test_stash_toggle_show_all_moves_are_silent(extension):
  function test_stash_toggle_show_does_not_toggle_floating (line 246) | async def test_stash_toggle_show_does_not_toggle_floating(extension):
  function test_stash_toggle_show_no_windows_is_noop (line 258) | async def test_stash_toggle_show_no_windows_is_noop(extension):
  function test_stash_toggle_show_custom_name (line 268) | async def test_stash_toggle_show_custom_name(extension):
  function test_stash_toggle_hide_moves_windows_back (line 281) | async def test_stash_toggle_hide_moves_windows_back(extension):
  function test_stash_toggle_hide_clears_tracking (line 300) | async def test_stash_toggle_hide_clears_tracking(extension):
  function test_on_reload_clears_old_rules_and_registers_new (line 318) | async def test_on_reload_clears_old_rules_and_registers_new(styled_exten...
  function test_on_reload_clears_rules_even_when_style_empty (line 329) | async def test_on_reload_clears_rules_even_when_style_empty(extension):
  function test_stash_tags_window_when_style_configured (line 338) | async def test_stash_tags_window_when_style_configured(styled_extension):
  function test_stash_does_not_tag_without_style (line 353) | async def test_stash_does_not_tag_without_style(extension):
  function test_unstash_untags_window_when_style_configured (line 368) | async def test_unstash_untags_window_when_style_configured(styled_extens...
  function test_stash_on_shown_window_untags_when_style_configured (line 382) | async def test_stash_on_shown_window_untags_when_style_configured(styled...
  function test_closewindow_removes_from_was_floating (line 406) | async def test_closewindow_removes_from_was_floating(extension):
  function test_closewindow_removes_from_shown_addresses (line 416) | async def test_closewindow_removes_from_shown_addresses(extension):
  function test_closewindow_clears_group_when_last_shown_window_closed (line 429) | async def test_closewindow_clears_group_when_last_shown_window_closed(ex...
  function test_closewindow_noop_for_unknown_window (line 441) | async def test_closewindow_noop_for_unknown_window(extension):
  function test_closewindow_cleans_both_was_floating_and_shown (line 455) | async def test_closewindow_cleans_both_was_floating_and_shown(extension):

FILE: tests/test_plugin_system_notifier.py
  function extension (line 10) | async def extension():
  function test_initialization (line 20) | async def test_initialization(extension):
  function test_on_reload_builtin_parser (line 27) | async def test_on_reload_builtin_parser(extension):
  function test_on_reload_custom_parser (line 35) | async def test_on_reload_custom_parser(extension):
  function test_parser_matching (line 43) | async def test_parser_matching(extension):
  function test_notify_send_option (line 79) | async def test_notify_send_option(extension):
  function test_exit_cleanup (line 113) | async def test_exit_cleanup(extension):

FILE: tests/test_plugin_toggle_dpms.py
  function extension (line 10) | def extension():
  function test_run_toggle_dpms_off (line 18) | async def test_run_toggle_dpms_off(extension):
  function test_run_toggle_dpms_on (line 26) | async def test_run_toggle_dpms_on(extension):

FILE: tests/test_plugin_toggle_special.py
  function extension (line 9) | def extension():
  function test_run_toggle_special_minimize (line 14) | async def test_run_toggle_special_minimize(extension):
  function test_run_toggle_special_restore (line 25) | async def test_run_toggle_special_restore(extension):

FILE: tests/test_plugin_wallpapers.py
  function extension (line 14) | def extension(mocker, test_logger):
  function test_on_reload (line 26) | async def test_on_reload(extension, mocker):
  function test_select_next_image (line 50) | async def test_select_next_image(extension):
  function test_run_wall_next (line 66) | async def test_run_wall_next(extension):
  function test_detect_theme (line 77) | async def test_detect_theme(mocker, test_logger):
  function test_material_palette_generation (line 92) | async def test_material_palette_generation():
  function test_run_palette_terminal (line 118) | async def test_run_palette_terminal(extension, mocker):
  function test_run_palette_json (line 141) | async def test_run_palette_json(extension, mocker):
  function test_run_palette_default_color (line 165) | async def test_run_palette_default_color(extension, mocker):
  function test_run_color (line 191) | async def test_run_color(extension, mocker):
  function test_run_color_with_scheme (line 202) | async def test_run_color_with_scheme(extension, mocker):
  function online_extension (line 217) | def online_extension(mocker, test_logger):
  function test_fetch_online_image_uses_prefetched (line 242) | async def test_fetch_online_image_uses_prefetched(online_extension, mock...
  function test_fetch_online_image_prefetched_missing (line 260) | async def test_fetch_online_image_prefetched_missing(online_extension, m...
  function test_prefetch_online_image_success (line 282) | async def test_prefetch_online_image_success(online_extension, mocker):
  function test_prefetch_online_image_retry (line 302) | async def test_prefetch_online_image_retry(online_extension, mocker):

FILE: tests/test_plugin_workspaces_follow_focus.py
  function layout_config (line 72) | async def layout_config(monkeypatch):
  function test_layout_center (line 81) | async def test_layout_center():

FILE: tests/test_process.py
  class TestManagedProcess (line 10) | class TestManagedProcess:
    method test_start_and_stop (line 14) | async def test_start_and_stop(self):
    method test_stop_not_started (line 30) | async def test_stop_not_started(self):
    method test_stop_already_exited (line 37) | async def test_stop_already_exited(self):
    method test_start_stops_existing (line 47) | async def test_start_stops_existing(self):
    method test_restart (line 60) | async def test_restart(self):
    method test_restart_without_start_raises (line 74) | async def test_restart_without_start_raises(self):
    method test_wait (line 81) | async def test_wait(self):
    method test_wait_without_start_raises (line 91) | async def test_wait_without_start_raises(self):
    method test_returncode (line 98) | async def test_returncode(self):
    method test_iter_lines (line 108) | async def test_iter_lines(self):
    method test_iter_lines_no_stdout_raises (line 117) | async def test_iter_lines_no_stdout_raises(self):
    method test_iter_lines_not_started_raises (line 129) | async def test_iter_lines_not_started_raises(self):
    method test_graceful_timeout (line 137) | async def test_graceful_timeout(self):
    method test_process_property (line 149) | async def test_process_property(self):
  class TestSupervisedProcess (line 161) | class TestSupervisedProcess:
    method test_start_and_stop (line 165) | async def test_start_and_stop(self):
    method test_auto_restart (line 181) | async def test_auto_restart(self):
    method test_cooldown (line 207) | async def test_cooldown(self):
    method test_on_crash_receives_returncode (line 233) | async def test_on_crash_receives_returncode(self):
    method test_stop_cancels_supervision (line 257) | async def test_stop_cancels_supervision(self):
    method test_start_stops_previous (line 271) | async def test_start_stops_previous(self):
    method test_no_on_crash_callback (line 286) | async def test_no_on_crash_callback(self):

FILE: tests/test_pyprland.py
  function test_reload (line 15) | async def test_reload(monkeypatch):

FILE: tests/test_scratchpad_vulnerabilities.py
  function multi_scratchpads (line 11) | def multi_scratchpads(monkeypatch, mocker):
  function subprocess_shell_mock (line 33) | def subprocess_shell_mock(mocker):
  function mock_aioops (line 61) | def mock_aioops(mocker):
  function gen_call_set (line 113) | def gen_call_set(call_list: list) -> set[str]:
  function _send_window_events (line 124) | async def _send_window_events(address="12345677890", klass="scratch-term...
  function test_shared_custody_conflict (line 177) | async def test_shared_custody_conflict(multi_scratchpads, subprocess_she...
  function test_zombie_process_recovery (line 281) | async def test_zombie_process_recovery(multi_scratchpads, subprocess_she...

FILE: tests/test_string_template.py
  function test_templates (line 1) | def test_templates():

FILE: tests/test_wallpapers_cache.py
  function test_cache_get_path (line 11) | def test_cache_get_path(tmp_path):
  function test_cache_get_path_different_keys (line 20) | def test_cache_get_path_different_keys(tmp_path):
  function test_cache_is_valid_no_ttl (line 28) | def test_cache_is_valid_no_ttl(tmp_path):
  function test_cache_is_valid_nonexistent (line 36) | def test_cache_is_valid_nonexistent(tmp_path):
  function test_cache_is_valid_with_ttl_fresh (line 43) | def test_cache_is_valid_with_ttl_fresh(tmp_path):
  function test_cache_is_valid_with_ttl_expired (line 51) | def test_cache_is_valid_with_ttl_expired(tmp_path):
  function test_cache_get_hit (line 61) | def test_cache_get_hit(tmp_path):
  function test_cache_get_miss (line 69) | def test_cache_get_miss(tmp_path):
  function test_cache_get_expired (line 75) | def test_cache_get_expired(tmp_path):
  function test_cache_store (line 86) | async def test_cache_store(tmp_path):
  function test_cache_store_overwrite (line 95) | async def test_cache_store_overwrite(tmp_path):
  function test_cache_cleanup_with_ttl (line 104) | def test_cache_cleanup_with_ttl(tmp_path):
  function test_cache_cleanup_no_ttl (line 122) | def test_cache_cleanup_no_ttl(tmp_path):
  function test_cache_cleanup_with_custom_max_age (line 135) | def test_cache_cleanup_with_custom_max_age(tmp_path):
  function test_cache_auto_cleanup_max_size (line 149) | def test_cache_auto_cleanup_max_size(tmp_path):
  function test_cache_auto_cleanup_max_count (line 169) | def test_cache_auto_cleanup_max_count(tmp_path):
  function test_cache_auto_cleanup_under_limits (line 187) | def test_cache_auto_cleanup_under_limits(tmp_path):
  function test_cache_auto_cleanup_no_limits (line 204) | def test_cache_auto_cleanup_no_limits(tmp_path):
  function test_cache_clear (line 216) | def test_cache_clear(tmp_path):
  function test_cache_clear_empty (line 232) | def test_cache_clear_empty(tmp_path):
  function test_cache_hash_key (line 239) | def test_cache_hash_key(tmp_path):
  function test_cache_get_cache_size (line 256) | def test_cache_get_cache_size(tmp_path):
  function test_cache_get_cache_count (line 267) | def test_cache_get_cache_count(tmp_path):

FILE: tests/test_wallpapers_colors.py
  function test_build_hue_histogram (line 33) | def test_build_hue_histogram():
  function test_smooth_histogram (line 57) | def test_smooth_histogram():
  function test_find_peaks (line 80) | def test_find_peaks():
  function test_calculate_hue_diff (line 98) | def test_calculate_hue_diff():
  function test_get_best_pixel_for_hue (line 113) | def test_get_best_pixel_for_hue():
  function test_select_colors_from_peaks (line 136) | def test_select_colors_from_peaks():
  function test_get_dominant_colors_integration (line 156) | def test_get_dominant_colors_integration():
  function test_nicify_oklab (line 194) | def test_nicify_oklab():
  function _oklab_hue (line 217) | def _oklab_hue(rgb: tuple[int, int, int]) -> float:
  function _hls_saturation (line 243) | def _hls_saturation(rgb: tuple[int, int, int]) -> float:
  function _hue_distance (line 249) | def _hue_distance(h1: float, h2: float) -> float:
  function test_nicify_oklab_hue_preservation (line 255) | def test_nicify_oklab_hue_preservation():
  function test_nicify_oklab_distinct_outputs_for_distinct_inputs (line 282) | def test_nicify_oklab_distinct_outputs_for_distinct_inputs():
  function test_nicify_oklab_muted_input_not_oversaturated (line 301) | def test_nicify_oklab_muted_input_not_oversaturated():
  function test_nicify_oklab_saturation_params_are_effective (line 315) | def test_nicify_oklab_saturation_params_are_effective():
  function test_nicify_oklab_chroma_not_always_capped (line 337) | def test_nicify_oklab_chroma_not_always_capped():
  function test_nicify_oklab_user_reported_bug (line 359) | def test_nicify_oklab_user_reported_bug():
  function wallpaper_plugin (line 387) | def wallpaper_plugin():
  function test_color_scheme_props (line 391) | def test_color_scheme_props(wallpaper_plugin):
  function test_color_scheme_props_default (line 442) | def test_color_scheme_props_default(wallpaper_plugin):
  function test_generate_palette_basic (line 447) | def test_generate_palette_basic(wallpaper_plugin):
  function test_generate_palette_islands (line 462) | def test_generate_palette_islands(wallpaper_plugin):
  function test_set_alpha (line 493) | def test_set_alpha(wallpaper_plugin):
  function test_set_lightness (line 502) | def test_set_lightness(wallpaper_plugin):
  function test_apply_filters (line 517) | async def test_apply_filters(wallpaper_plugin):
  function test_color_scheme_effect_on_saturation (line 546) | def test_color_scheme_effect_on_saturation(wallpaper_plugin):
  function test_hex_to_rgb (line 591) | def test_hex_to_rgb():
  function test_generate_sample_palette (line 607) | def test_generate_sample_palette():
  function test_generate_sample_palette_light_theme (line 636) | def test_generate_sample_palette_light_theme():
  function test_categorize_palette (line 646) | def test_categorize_palette():
  function test_palette_to_json (line 669) | def test_palette_to_json():
  function test_palette_to_terminal (line 703) | def test_palette_to_terminal():

FILE: tests/test_wallpapers_imageutils.py
  function test_expand_path (line 21) | def test_expand_path():
  function test_get_files_with_ext (line 34) | async def test_get_files_with_ext():
  function test_color_conversions (line 74) | def test_color_conversions():
  function test_get_variant_color (line 83) | def test_get_variant_color():
  function test_rounded_image_manager_paths (line 99) | def test_rounded_image_manager_paths(tmp_path):
  function test_get_effective_dimensions_no_rotation (line 123) | def test_get_effective_dimensions_no_rotation():
  function test_get_effective_dimensions_rotated (line 131) | def test_get_effective_dimensions_rotated():
  function test_rounded_image_manager_processing (line 139) | def test_rounded_image_manager_processing(tmp_path):

FILE: tests/testtools.py
  function wait_called (line 5) | async def wait_called(fn, timeout=1.0, count=1):
  function get_executed_commands (line 18) | def get_executed_commands(mock):
  class MockReader (line 41) | class MockReader:
    method __init__ (line 44) | def __init__(self):
    method readline (line 47) | async def readline(self, *a):
  class MockWriter (line 53) | class MockWriter:
    method __init__ (line 56) | def __init__(self):

FILE: tests/vreg/01_client_id_change.py
  function show_window (line 10) | def show_window():
Condensed preview — 794 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,910K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 704,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG] Enter a short bug description here\"\nlabels:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 641,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FEAT] Description of the feature\"\nlabels: enh"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/wiki_improvement.md",
    "chars": 675,
    "preview": "---\nname: Wiki improvement\nabout: Suggest a fix or improvement in the documentation\ntitle: \"[WIKI] Description of the pr"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md",
    "chars": 291,
    "preview": "---\nname: Default PR template\nabout: Improve the code\ntitle: \"Change description here\"\n---\n\n# Description of the pull re"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 263,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: weekly\n      "
  },
  {
    "path": ".github/workflows/aur.yml",
    "chars": 2139,
    "preview": "name: AUR Package Validation\n\non:\n  workflow_dispatch:\n  pull_request:\n    paths:\n      - \"pyprland/**\"\n      - \"pyproje"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 837,
    "preview": "---\nname: CI\n\non: [push]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: ["
  },
  {
    "path": ".github/workflows/nix-setup.yml",
    "chars": 1541,
    "preview": "# This is a re-usable workflow that is used by .github/workflows/check.yml to handle necessary setup\n# before running Ni"
  },
  {
    "path": ".github/workflows/nix.yml",
    "chars": 766,
    "preview": "name: Nix Flake Validation\n\non:\n  workflow_dispatch:\n  pull_request:\n    paths:\n      - \"pyprland/**\"\n      - \"**.nix\"\n "
  },
  {
    "path": ".github/workflows/site.yml",
    "chars": 2266,
    "preview": "# Sample workflow for building and deploying a VitePress site to GitHub Pages\n#\nname: Website deployment\n\non:\n  # Runs o"
  },
  {
    "path": ".github/workflows/uv-install.yml",
    "chars": 920,
    "preview": "name: uv tool install Validation\n\non:\n  workflow_dispatch:\n  pull_request:\n    paths:\n      - \"pyprland/**\"\n      - \"pyp"
  },
  {
    "path": ".gitignore",
    "chars": 3058,
    "preview": "RELEASE_NOTES.md\n\n# Byte-compiled / optimized / DLL files\nsite/.vitepress/cache/\nsite/.vitepress/dist/\nnode_modules/\n__p"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1517,
    "preview": "---\nrepos:\n  - repo: local\n    hooks:\n      - id: versionMgmt\n        name: Increment the version number\n        entry: "
  },
  {
    "path": ".pylintrc",
    "chars": 5919,
    "preview": "[MASTER]\n\n# Specify a configuration file.\n#rcfile=\n\n# Python code to execute, usually for sys.path manipulation such as\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5218,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 265,
    "preview": "- ensure everything works when you run `tox`\n- use `ruff format` to format the code\n- provide documentation to be added "
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2023 Fabien Devaux\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 6529,
    "preview": "![rect](https://github.com/hyprland-community/pyprland/assets/238622/3fab93b6-6445-4e7b-b757-035095b5c8e8)\n\n[![Hyprland]"
  },
  {
    "path": "client/pypr-client.c",
    "chars": 6766,
    "preview": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n#include <sys/socket.h>\n#include <sys/un."
  },
  {
    "path": "client/pypr-client.rs",
    "chars": 3320,
    "preview": "use std::env;\nuse std::io::{Read, Write};\nuse std::os::unix::net::UnixStream;\nuse std::process::exit;\n\n// Exit codes mat"
  },
  {
    "path": "client/pypr-rs/Cargo.toml",
    "chars": 159,
    "preview": "[package]\nname = \"pypr-client\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[profile.release]\nopt-level = \"z\"\nlto = true\ncodegen-"
  },
  {
    "path": "default.nix",
    "chars": 409,
    "preview": "(\n  import\n  (\n    let\n      lock = builtins.fromJSON (builtins.readFile ./flake.lock);\n      nodeName = lock.nodes.root"
  },
  {
    "path": "done.rst",
    "chars": 12221,
    "preview": "scratchpads: attach / detach only attaches !\n============================================\n\n:bugid: 56\n:created: 2026-1-1"
  },
  {
    "path": "examples/README.md",
    "chars": 405,
    "preview": "# Contribute your configuration\n\n[Dotfiles](https://github.com/fdev31/dotfiles)\n[![Discord](https://img.shields.io/disco"
  },
  {
    "path": "examples/copy_conf.sh",
    "chars": 514,
    "preview": "#!/bin/sh\nif [ -z \"$1\" ]; then\n    echo -n \"config name: \"\n    read name\nelse\n    name=$1\nfi\n[ -d $name/hypr/ ] || mkdir"
  },
  {
    "path": "flake.nix",
    "chars": 2292,
    "preview": "{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable-small\";\n\n    # <https://github.com/pyproject-nix/p"
  },
  {
    "path": "hatch_build.py",
    "chars": 4175,
    "preview": "\"\"\"Custom hatch build hook to compile the optional C client.\n\nWhen the PYPRLAND_BUILD_NATIVE environment variable is set"
  },
  {
    "path": "justfile",
    "chars": 2954,
    "preview": "# Run tests quickly\nquicktest:\n    uv run pytest -q tests\n\n# Run pytest with optional parameters\ndebug *params='tests':\n"
  },
  {
    "path": "package.json",
    "chars": 326,
    "preview": "{\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev site\",\n    \"docs:build\": \"vitepress build site\",\n    \"docs:preview\": \"vi"
  },
  {
    "path": "pyprland/__init__.py",
    "chars": 319,
    "preview": "\"\"\"Pyprland - a companion application for Hyprland and other Wayland compositors.\n\nProvides a plugin-based architecture "
  },
  {
    "path": "pyprland/adapters/__init__.py",
    "chars": 324,
    "preview": "\"\"\"Backend adapters for compositor abstraction.\n\nThis package provides the EnvironmentBackend abstraction layer that all"
  },
  {
    "path": "pyprland/adapters/backend.py",
    "chars": 10655,
    "preview": "\"\"\"Abstract base class defining the compositor backend interface.\n\nEnvironmentBackend defines the contract for all compo"
  },
  {
    "path": "pyprland/adapters/colors.py",
    "chars": 469,
    "preview": "\"\"\"Color conversion & misc color related helpers.\"\"\"\n\n\ndef convert_color(description: str) -> str:\n    \"\"\"Get a color de"
  },
  {
    "path": "pyprland/adapters/fallback.py",
    "chars": 7601,
    "preview": "\"\"\"Fallback backend base class for limited functionality environments.\"\"\"\n\nimport asyncio\nfrom abc import abstractmethod"
  },
  {
    "path": "pyprland/adapters/hyprland.py",
    "chars": 7456,
    "preview": "\"\"\"Hyprland compositor backend implementation.\n\nPrimary backend for Hyprland, using its Unix socket IPC protocol.\nProvid"
  },
  {
    "path": "pyprland/adapters/menus.py",
    "chars": 6180,
    "preview": "\"\"\"Menu engine adapter.\"\"\"\n\nimport asyncio\nimport subprocess\nfrom collections.abc import Iterable\nfrom logging import Lo"
  },
  {
    "path": "pyprland/adapters/niri.py",
    "chars": 14227,
    "preview": "\"\"\"Niri compositor backend implementation.\n\nBackend for Niri compositor using its JSON-based IPC protocol.\nMaps Niri's w"
  },
  {
    "path": "pyprland/adapters/proxy.py",
    "chars": 9228,
    "preview": "\"\"\"Backend proxy that injects plugin logger into all calls.\n\nThis module provides a BackendProxy class that wraps an Env"
  },
  {
    "path": "pyprland/adapters/units.py",
    "chars": 1798,
    "preview": "\"\"\"Conversion functions for units used in Pyprland & plugins.\"\"\"\n\nfrom typing import Literal\n\nfrom ..common import is_ro"
  },
  {
    "path": "pyprland/adapters/wayland.py",
    "chars": 5813,
    "preview": "\"\"\"Generic Wayland backend using wlr-randr for monitor detection.\"\"\"\n\nimport re\nfrom logging import Logger\n\nfrom ..model"
  },
  {
    "path": "pyprland/adapters/xorg.py",
    "chars": 4346,
    "preview": "\"\"\"X11/Xorg backend using xrandr for monitor detection.\"\"\"\n# pylint: disable=duplicate-code  # make_monitor_info calls s"
  },
  {
    "path": "pyprland/aioops.py",
    "chars": 12350,
    "preview": "\"\"\"Async operation utilities.\n\nProvides fallback sync methods if aiofiles is not installed,\nplus async task management u"
  },
  {
    "path": "pyprland/ansi.py",
    "chars": 2318,
    "preview": "\"\"\"ANSI terminal color utilities.\n\nProvides constants and helpers for terminal coloring with proper\nNO_COLOR environment"
  },
  {
    "path": "pyprland/client.py",
    "chars": 3305,
    "preview": "\"\"\"Client-side functions for pyprland CLI.\"\"\"\n\nimport asyncio\nimport contextlib\nimport os\nimport sys\n\nfrom . import cons"
  },
  {
    "path": "pyprland/command.py",
    "chars": 3050,
    "preview": "\"\"\"Pyprland - an Hyprland companion app (cli client & daemon).\"\"\"\n\nimport asyncio\nimport json\nimport sys\nfrom pathlib im"
  },
  {
    "path": "pyprland/commands/__init__.py",
    "chars": 288,
    "preview": "\"\"\"Command handling utilities for pyprland.\n\nThis package provides:\n- models: Data structures (CommandArg, CommandInfo, "
  },
  {
    "path": "pyprland/commands/discovery.py",
    "chars": 2858,
    "preview": "\"\"\"Command extraction and discovery from plugins.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom typing imp"
  },
  {
    "path": "pyprland/commands/models.py",
    "chars": 1648,
    "preview": "\"\"\"Data models for command handling.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\n_"
  },
  {
    "path": "pyprland/commands/parsing.py",
    "chars": 2511,
    "preview": "\"\"\"Docstring and command name parsing utilities.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\nfrom .models import "
  },
  {
    "path": "pyprland/commands/tree.py",
    "chars": 5011,
    "preview": "\"\"\"Hierarchical command tree building and display name utilities.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing im"
  },
  {
    "path": "pyprland/common.py",
    "chars": 1261,
    "preview": "\"\"\"Shared utilities - re-exports from focused modules for backward compatibility.\n\nThis module aggregates exports from s"
  },
  {
    "path": "pyprland/completions/__init__.py",
    "chars": 767,
    "preview": "\"\"\"Shell completion generators for pyprland.\n\nGenerates dynamic shell completions based on loaded plugins and configurat"
  },
  {
    "path": "pyprland/completions/discovery.py",
    "chars": 6999,
    "preview": "\"\"\"Command completion discovery.\n\nExtracts structured completion data from loaded plugins and configuration.\n\"\"\"\n\nfrom _"
  },
  {
    "path": "pyprland/completions/generators/__init__.py",
    "chars": 592,
    "preview": "\"\"\"Shell completion generators.\n\nProvides generator functions for each supported shell.\n\"\"\"\n\nfrom __future__ import anno"
  },
  {
    "path": "pyprland/completions/generators/bash.py",
    "chars": 4662,
    "preview": "\"\"\"Bash shell completion generator.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CH"
  },
  {
    "path": "pyprland/completions/generators/fish.py",
    "chars": 4625,
    "preview": "\"\"\"Fish shell completion generator.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CH"
  },
  {
    "path": "pyprland/completions/generators/zsh.py",
    "chars": 5115,
    "preview": "\"\"\"Zsh shell completion generator.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHE"
  },
  {
    "path": "pyprland/completions/handlers.py",
    "chars": 4539,
    "preview": "\"\"\"CLI handlers for shell completion commands.\n\nProvides the handle_compgen function used by the pyprland plugin\nto gene"
  },
  {
    "path": "pyprland/completions/models.py",
    "chars": 1900,
    "preview": "\"\"\"Data models and constants for shell completions.\n\nContains the data structures used to represent command completions\n"
  },
  {
    "path": "pyprland/config.py",
    "chars": 8295,
    "preview": "\"\"\"Configuration wrapper with typed accessors and schema-aware defaults.\n\nThe Configuration class extends dict with:\n- T"
  },
  {
    "path": "pyprland/config_loader.py",
    "chars": 8837,
    "preview": "\"\"\"Configuration file loading utilities.\n\nThis module handles loading, parsing, and merging TOML/JSON configuration file"
  },
  {
    "path": "pyprland/constants.py",
    "chars": 2306,
    "preview": "\"\"\"Shared constants and configuration defaults for Pyprland.\n\nIncludes:\n- Config file paths (CONFIG_FILE, LEGACY_CONFIG_"
  },
  {
    "path": "pyprland/debug.py",
    "chars": 636,
    "preview": "\"\"\"Debug mode state management.\"\"\"\n\nimport os\n\n__all__ = [\n    \"DEBUG\",\n    \"is_debug\",\n    \"set_debug\",\n]\n\n\nclass _Debu"
  },
  {
    "path": "pyprland/doc.py",
    "chars": 7917,
    "preview": "\"\"\"Documentation formatting for pypr doc command.\n\nFormats plugin and configuration documentation for terminal display\nw"
  },
  {
    "path": "pyprland/gui/__init__.py",
    "chars": 6375,
    "preview": "\"\"\"Web-based configuration editor for pyprland.\n\nProvides a local web server with a Vue.js frontend for viewing and edit"
  },
  {
    "path": "pyprland/gui/__main__.py",
    "chars": 98,
    "preview": "\"\"\"Allow running pyprland.gui as a module: python -m pyprland.gui.\"\"\"\n\nfrom . import main\n\nmain()\n"
  },
  {
    "path": "pyprland/gui/api.py",
    "chars": 18573,
    "preview": "\"\"\"API logic for the pyprland GUI: schema serialization, config I/O, validation.\n\nBridges the existing pyprland infrastr"
  },
  {
    "path": "pyprland/gui/frontend/.gitignore",
    "chars": 253,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "pyprland/gui/frontend/.vscode/extensions.json",
    "chars": 47,
    "preview": "{\n  \"recommendations\": [\n    \"Vue.volar\"\n  ]\n}\n"
  },
  {
    "path": "pyprland/gui/frontend/README.md",
    "chars": 385,
    "preview": "# Vue 3 + Vite\n\nThis template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<scrip"
  },
  {
    "path": "pyprland/gui/frontend/index.html",
    "chars": 351,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "pyprland/gui/frontend/package.json",
    "chars": 313,
    "preview": "{\n  \"name\": \"frontend\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n "
  },
  {
    "path": "pyprland/gui/frontend/src/App.vue",
    "chars": 7616,
    "preview": "<template>\n  <div v-if=\"loading\" class=\"loading\">Loading...</div>\n  <template v-else>\n    <header class=\"app-header\">\n  "
  },
  {
    "path": "pyprland/gui/frontend/src/components/DictEditor.vue",
    "chars": 9275,
    "preview": "<template>\n  <div class=\"dict-editor\" :class=\"{ nested: depth > 0 }\">\n    <div v-for=\"key in sortedKeys\" :key=\"key\" clas"
  },
  {
    "path": "pyprland/gui/frontend/src/components/FieldInput.vue",
    "chars": 5490,
    "preview": "<template>\n  <div class=\"field-row\">\n    <div class=\"field-label\">\n      <div class=\"name\">\n        {{ field.name }}\n   "
  },
  {
    "path": "pyprland/gui/frontend/src/components/PluginEditor.vue",
    "chars": 9666,
    "preview": "<template>\n  <div class=\"plugin-header\">\n    <h2>{{ plugin.name }} <a :href=\"docsBase + plugin.name + '.html'\" target=\"_"
  },
  {
    "path": "pyprland/gui/frontend/src/composables/useLocalCopy.js",
    "chars": 809,
    "preview": "import { ref, watch, nextTick } from 'vue'\n\n/**\n * Two-way sync a local copy of an object prop with a guard to prevent\n "
  },
  {
    "path": "pyprland/gui/frontend/src/composables/useToggleMap.js",
    "chars": 330,
    "preview": "import { reactive } from 'vue'\n\n/**\n * A reactive map of boolean toggle states, keyed by string.\n *\n * @returns {{ state"
  },
  {
    "path": "pyprland/gui/frontend/src/main.js",
    "chars": 111,
    "preview": "import { createApp } from 'vue'\nimport './style.css'\nimport App from './App.vue'\n\ncreateApp(App).mount('#app')\n"
  },
  {
    "path": "pyprland/gui/frontend/src/style.css",
    "chars": 11806,
    "preview": "/* pypr-gui global styles — dark theme inspired by Hyprland aesthetics */\n\n:root {\n  --bg-primary: #1a1b26;\n  --bg-secon"
  },
  {
    "path": "pyprland/gui/frontend/src/utils.js",
    "chars": 1159,
    "preview": "/** Type-checking helpers */\nexport const isString = (v) => typeof v === 'string'\nexport const isArray = (v) => Array.is"
  },
  {
    "path": "pyprland/gui/frontend/vite.config.js",
    "chars": 267,
    "preview": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\nexport default defineConfig({\n  plugins: [vue("
  },
  {
    "path": "pyprland/gui/server.py",
    "chars": 3954,
    "preview": "\"\"\"aiohttp.web server for the pyprland GUI.\n\nDefines HTTP routes that serve the Vue.js frontend and expose the\nJSON API "
  },
  {
    "path": "pyprland/gui/static/assets/index-CX03GsX-.js",
    "chars": 84266,
    "preview": "(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e "
  },
  {
    "path": "pyprland/gui/static/assets/index-Dpu0NgRN.css",
    "chars": 9037,
    "preview": ":root{--bg-primary:#1a1b26;--bg-secondary:#24283b;--bg-tertiary:#2f3348;--bg-hover:#3b3f57;--text-primary:#c0caf5;--text"
  },
  {
    "path": "pyprland/gui/static/index.html",
    "chars": 450,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "pyprland/help.py",
    "chars": 5824,
    "preview": "\"\"\"Help and documentation functions for pyprland commands.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TY"
  },
  {
    "path": "pyprland/httpclient.py",
    "chars": 7466,
    "preview": "\"\"\"HTTP client abstraction with aiohttp fallback to urllib.\n\nThis module provides a unified HTTP client interface that u"
  },
  {
    "path": "pyprland/ipc.py",
    "chars": 5956,
    "preview": "\"\"\"IPC communication with Hyprland and Niri compositors.\n\nProvides async context managers for socket connections and req"
  },
  {
    "path": "pyprland/ipc_paths.py",
    "chars": 2236,
    "preview": "\"\"\"IPC path management and constants.\"\"\"\n\nimport contextlib\nimport os\nfrom pathlib import Path\n\n__all__ = [\n    \"HYPRLAN"
  },
  {
    "path": "pyprland/logging_setup.py",
    "chars": 2998,
    "preview": "\"\"\"Logging setup and utilities.\"\"\"\n\nimport logging\nfrom typing import ClassVar\n\nfrom .ansi import LogStyles, make_style,"
  },
  {
    "path": "pyprland/manager.py",
    "chars": 27418,
    "preview": "\"\"\"Core daemon orchestrator for Pyprland.\n\nThe Pyprland class manages:\n- Plugin lifecycle (loading, initialization, conf"
  },
  {
    "path": "pyprland/models.py",
    "chars": 3696,
    "preview": "\"\"\"Type definitions and data models for Pyprland.\n\nProvides TypedDict definitions matching Hyprland's JSON API responses"
  },
  {
    "path": "pyprland/plugins/__init__.py",
    "chars": 279,
    "preview": "\"\"\"Built-in plugins for Pyprland.\n\nThis package contains all bundled plugins. Each plugin module exports an\nExtension cl"
  },
  {
    "path": "pyprland/plugins/experimental.py",
    "chars": 185,
    "preview": "\"\"\"Plugin template.\"\"\"\n\nfrom ..models import Environment\nfrom .interface import Plugin\n\n\nclass Extension(Plugin, environ"
  },
  {
    "path": "pyprland/plugins/expose.py",
    "chars": 2077,
    "preview": "\"\"\"expose Brings every client window to screen for selection.\"\"\"\n\nfrom ..models import ClientInfo, Environment, ReloadRe"
  },
  {
    "path": "pyprland/plugins/fcitx5_switcher.py",
    "chars": 1955,
    "preview": "\"\"\"A plugin to auto-switch Fcitx5 input method status by window class/title.\"\"\"\n\nfrom ..models import Environment\nfrom ."
  },
  {
    "path": "pyprland/plugins/fetch_client_menu.py",
    "chars": 5169,
    "preview": "\"\"\"Select a client window and move it to the active workspace.\"\"\"\n\nfrom ..adapters.menus import MenuMixin\nfrom ..common "
  },
  {
    "path": "pyprland/plugins/gamemode.py",
    "chars": 7425,
    "preview": "\"\"\"Gamemode plugin - toggle performance mode for gaming.\n\nProvides manual toggle and automatic detection of game windows"
  },
  {
    "path": "pyprland/plugins/interface.py",
    "chars": 10731,
    "preview": "\"\"\"Base Plugin class and infrastructure for Pyprland plugins.\n\nThe Plugin class provides:\n- Typed configuration accessor"
  },
  {
    "path": "pyprland/plugins/layout_center.py",
    "chars": 13074,
    "preview": "\"\"\"Implements a \"Centered\" layout.\n\n- windows are normally tiled but one\n- the active window is floating and centered\n- "
  },
  {
    "path": "pyprland/plugins/lost_windows.py",
    "chars": 1808,
    "preview": "\"\"\"Moves unreachable client windows to the currently focused workspace.\"\"\"\n\nfrom ..models import ClientInfo, Environment"
  },
  {
    "path": "pyprland/plugins/magnify.py",
    "chars": 3549,
    "preview": "\"\"\"Toggles workspace zooming.\"\"\"\n\nimport asyncio\nfrom collections.abc import Iterable\n\nfrom ..models import Environment,"
  },
  {
    "path": "pyprland/plugins/menubar.py",
    "chars": 9858,
    "preview": "\"\"\"Run a bar.\"\"\"\n\nimport contextlib\nfrom time import time\nfrom typing import TYPE_CHECKING, cast\n\nfrom ..aioops import T"
  },
  {
    "path": "pyprland/plugins/mixins.py",
    "chars": 3343,
    "preview": "\"\"\"Reusable mixins for common plugin functionality.\n\nMonitorTrackingMixin:\n    Automatically tracks monitor add/remove e"
  },
  {
    "path": "pyprland/plugins/monitors/__init__.py",
    "chars": 14190,
    "preview": "\"\"\"The monitors plugin.\"\"\"\n\nimport asyncio\nfrom typing import Any\n\nfrom ...adapters.niri import niri_output_to_monitor_i"
  },
  {
    "path": "pyprland/plugins/monitors/commands.py",
    "chars": 2561,
    "preview": "\"\"\"Command building for Hyprland and Niri backends.\"\"\"\n\nfrom typing import Any\n\nfrom ...models import MonitorInfo\n\nNIRI_"
  },
  {
    "path": "pyprland/plugins/monitors/layout.py",
    "chars": 9749,
    "preview": "\"\"\"Layout positioning logic.\"\"\"\n\nfrom collections import defaultdict\nfrom typing import Any\n\nfrom ...models import Monit"
  },
  {
    "path": "pyprland/plugins/monitors/resolution.py",
    "chars": 2969,
    "preview": "\"\"\"Monitor pattern matching and name resolution.\"\"\"\n\nfrom typing import Any\n\nfrom ...models import MonitorInfo\nfrom .sch"
  },
  {
    "path": "pyprland/plugins/monitors/schema.py",
    "chars": 2589,
    "preview": "\"\"\"Configuration schema for monitors plugin.\"\"\"\n\nfrom typing import Any\n\nfrom ...validation import ConfigField, ConfigIt"
  },
  {
    "path": "pyprland/plugins/protocols.py",
    "chars": 3140,
    "preview": "\"\"\"Protocol definitions for plugin event handlers.\n\nThis module provides Protocol classes that document the expected sig"
  },
  {
    "path": "pyprland/plugins/pyprland/__init__.py",
    "chars": 17024,
    "preview": "\"\"\"Core plugin for state management.\n\nThis plugin is not a real plugin - it provides core features and caching\nof common"
  },
  {
    "path": "pyprland/plugins/pyprland/hyprland_core.py",
    "chars": 5154,
    "preview": "\"\"\"Hyprland-specific state management.\"\"\"\n\nimport json\nfrom typing import Any, cast\n\nfrom ...models import PyprError, Ve"
  },
  {
    "path": "pyprland/plugins/pyprland/niri_core.py",
    "chars": 2485,
    "preview": "\"\"\"Niri-specific state management.\"\"\"\n\nfrom typing import Any\n\nfrom ...models import Environment, PyprError, VersionInfo"
  },
  {
    "path": "pyprland/plugins/pyprland/schema.py",
    "chars": 1825,
    "preview": "\"\"\"Configuration schema for the pyprland core plugin.\n\nThis module is separate to allow manager.py to import the schema\n"
  },
  {
    "path": "pyprland/plugins/scratchpads/__init__.py",
    "chars": 26621,
    "preview": "\"\"\"Scratchpads addon.\"\"\"\n\nimport asyncio\nimport contextlib\nfrom functools import partial\nfrom typing import cast\n\nfrom ."
  },
  {
    "path": "pyprland/plugins/scratchpads/animations.py",
    "chars": 4956,
    "preview": "\"\"\"Placement for absolute window positioning.\"\"\"\n\n__all__ = [\"Placement\"]\n\nimport enum\nfrom typing import cast\n\nfrom ..."
  },
  {
    "path": "pyprland/plugins/scratchpads/common.py",
    "chars": 623,
    "preview": "\"\"\"Common types for scratchpads.\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import Flag, auto\n\nONE_FRAME = 1 / 50  "
  },
  {
    "path": "pyprland/plugins/scratchpads/events.py",
    "chars": 10853,
    "preview": "\"\"\"Scratchpad event handlers mixin.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport time\nfrom typing impor"
  },
  {
    "path": "pyprland/plugins/scratchpads/helpers.py",
    "chars": 5335,
    "preview": "\"\"\"Helper functions for the scratchpads plugin.\"\"\"\n\n__all__ = [\n    \"DynMonitorConfig\",\n    \"apply_offset\",\n    \"compute"
  },
  {
    "path": "pyprland/plugins/scratchpads/lifecycle.py",
    "chars": 7112,
    "preview": "\"\"\"Scratchpad lifecycle management mixin.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport contextlib\nfrom "
  },
  {
    "path": "pyprland/plugins/scratchpads/lookup.py",
    "chars": 5581,
    "preview": "\"\"\"Lookup & update API for Scratch objects.\"\"\"\n\nfrom collections import defaultdict\nfrom collections.abc import Iterable"
  },
  {
    "path": "pyprland/plugins/scratchpads/objects.py",
    "chars": 10156,
    "preview": "\"\"\"Scratchpad object definition.\"\"\"\n\n__all__ = [\"Scratch\"]\n\nimport asyncio\nfrom dataclasses import dataclass, field\nfrom"
  },
  {
    "path": "pyprland/plugins/scratchpads/schema.py",
    "chars": 9632,
    "preview": "\"\"\"Configuration schema for scratchpads plugin.\"\"\"\n\nimport logging\n\nfrom pyprland.validation import ConfigField, ConfigI"
  },
  {
    "path": "pyprland/plugins/scratchpads/transitions.py",
    "chars": 13888,
    "preview": "\"\"\"Scratchpad transitions mixin.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport time\nfrom typing import T"
  },
  {
    "path": "pyprland/plugins/scratchpads/windowruleset.py",
    "chars": 2006,
    "preview": "\"\"\"WindowRuleSet builder for Hyprland windowrules.\"\"\"\n\n__all__ = [\"WindowRuleSet\"]\n\nfrom collections.abc import Iterable"
  },
  {
    "path": "pyprland/plugins/shift_monitors.py",
    "chars": 5052,
    "preview": "\"\"\"Shift workspaces across monitors.\"\"\"\n\nfrom ..models import Environment\nfrom .interface import Plugin\nfrom .mixins imp"
  },
  {
    "path": "pyprland/plugins/shortcuts_menu.py",
    "chars": 5468,
    "preview": "\"\"\"Shortcuts menu.\"\"\"\n\nimport asyncio\nfrom typing import cast\n\nfrom ..adapters.menus import MenuMixin\nfrom ..common impo"
  },
  {
    "path": "pyprland/plugins/stash.py",
    "chars": 5695,
    "preview": "\"\"\"stash allows stashing and showing windows in named groups.\"\"\"\n\nimport asyncio\nfrom typing import cast\n\nfrom ..models "
  },
  {
    "path": "pyprland/plugins/system_notifier.py",
    "chars": 6789,
    "preview": "\"\"\"Add system notifications based on journal logs.\"\"\"\n\nimport asyncio\nimport re\nfrom copy import deepcopy\nfrom typing im"
  },
  {
    "path": "pyprland/plugins/toggle_dpms.py",
    "chars": 576,
    "preview": "\"\"\"Toggle monitors on or off.\"\"\"\n\nfrom ..models import Environment\nfrom .interface import Plugin\n\n\nclass Extension(Plugi"
  },
  {
    "path": "pyprland/plugins/toggle_special.py",
    "chars": 1392,
    "preview": "\"\"\"toggle_special allows having an \"expose\" like selection of windows in a special group.\"\"\"\n\nfrom typing import cast\n\nf"
  },
  {
    "path": "pyprland/plugins/wallpapers/__init__.py",
    "chars": 36905,
    "preview": "\"\"\"Plugin template.\"\"\"\n\nimport asyncio\nimport colorsys\nimport contextlib\nimport json\nimport random\nfrom dataclasses impo"
  },
  {
    "path": "pyprland/plugins/wallpapers/cache.py",
    "chars": 7085,
    "preview": "\"\"\"File-based image cache with TTL support.\"\"\"\n\nimport hashlib\nimport time\nfrom pathlib import Path\n\nfrom ...aioops impo"
  },
  {
    "path": "pyprland/plugins/wallpapers/colorutils.py",
    "chars": 10326,
    "preview": "\"\"\"Utils for the wallpaper plugin.\"\"\"\n\nimport math\n\ntry:\n    # pylint: disable=unused-import\n    from PIL import Image, "
  },
  {
    "path": "pyprland/plugins/wallpapers/hyprpaper.py",
    "chars": 2158,
    "preview": "\"\"\"Hyprpaper integration for the wallpapers plugin.\"\"\"\n\nimport asyncio\nfrom typing import TYPE_CHECKING\n\nfrom ...aioops "
  },
  {
    "path": "pyprland/plugins/wallpapers/imageutils.py",
    "chars": 7237,
    "preview": "\"\"\"Image utilities for the wallpapers plugin.\"\"\"\n\nfrom __future__ import annotations\n\nimport colorsys\nimport hashlib\nimp"
  },
  {
    "path": "pyprland/plugins/wallpapers/models.py",
    "chars": 4396,
    "preview": "\"\"\"Models for color variants and configurations.\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import StrEnum\n\nHEX_LEN"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/__init__.py",
    "chars": 9327,
    "preview": "\"\"\"Online wallpaper fetcher with multiple backend support.\n\nThis module provides an async interface to fetch wallpaper i"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/__init__.py",
    "chars": 1727,
    "preview": "\"\"\"Backend registry for online wallpaper sources.\n\nThis module provides the registry for wallpaper backends and re-expor"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/base.py",
    "chars": 4576,
    "preview": "\"\"\"Base classes and types for wallpaper backends.\n\nThis module contains the abstract base class, data types, and helper "
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/bing.py",
    "chars": 3961,
    "preview": "\"\"\"Bing Daily Wallpaper backend.\"\"\"\n\nimport random\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom pyprland.httpclient "
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/picsum.py",
    "chars": 2278,
    "preview": "\"\"\"Picsum Photos backend for random images.\"\"\"\n\n# pylint: disable=duplicate-code  # Uses shared fetch_redirect_image pat"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/reddit.py",
    "chars": 7059,
    "preview": "\"\"\"Reddit JSON API backend for wallpaper images.\"\"\"\n\nimport random\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom pypr"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/unsplash.py",
    "chars": 2488,
    "preview": "\"\"\"Unsplash Source backend for random images.\"\"\"\n\nimport random\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom . impor"
  },
  {
    "path": "pyprland/plugins/wallpapers/online/backends/wallhaven.py",
    "chars": 3244,
    "preview": "\"\"\"Wallhaven API backend for wallpaper images.\"\"\"\n\nimport random\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom pyprla"
  },
  {
    "path": "pyprland/plugins/wallpapers/palette.py",
    "chars": 5994,
    "preview": "\"\"\"Palette display utilities for wallpapers plugin.\"\"\"\n\nimport colorsys\nimport json\n\nfrom .models import Theme\nfrom .the"
  },
  {
    "path": "pyprland/plugins/wallpapers/templates.py",
    "chars": 4825,
    "preview": "\"\"\"Template processing for wallpapers plugin.\"\"\"\n\nimport colorsys\nimport contextlib\nimport logging\nimport re\nfrom typing"
  },
  {
    "path": "pyprland/plugins/wallpapers/theme.py",
    "chars": 9362,
    "preview": "\"\"\"Theme detection and palette generation logic.\"\"\"\n\nimport asyncio\nimport colorsys\nimport logging\nfrom collections.abc "
  },
  {
    "path": "pyprland/plugins/workspaces_follow_focus.py",
    "chars": 2955,
    "preview": "\"\"\"Force workspaces to follow the focus / mouse.\"\"\"\n\nimport asyncio\nfrom typing import cast\n\nfrom ..models import Enviro"
  },
  {
    "path": "pyprland/process.py",
    "chars": 10541,
    "preview": "\"\"\"Process lifecycle management utilities for spawning subprocesses.\n\nManagedProcess:\n    Manages a subprocess with prop"
  },
  {
    "path": "pyprland/pypr_daemon.py",
    "chars": 2358,
    "preview": "\"\"\"Daemon startup functions for pyprland.\"\"\"\n\nimport asyncio\nimport itertools\nfrom pathlib import Path\n\nfrom pyprland.co"
  },
  {
    "path": "pyprland/quickstart/__init__.py",
    "chars": 2436,
    "preview": "\"\"\"Pyprland quickstart configuration wizard - standalone script.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom"
  },
  {
    "path": "pyprland/quickstart/__main__.py",
    "chars": 132,
    "preview": "\"\"\"Allow running the quickstart wizard as a module.\"\"\"\n\nfrom pyprland.quickstart import main\n\nif __name__ == \"__main__\":"
  },
  {
    "path": "pyprland/quickstart/discovery.py",
    "chars": 3240,
    "preview": "\"\"\"Standalone plugin discovery for the quickstart wizard.\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib\nfrom "
  },
  {
    "path": "pyprland/quickstart/generator.py",
    "chars": 6956,
    "preview": "\"\"\"TOML configuration generator and file handling.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimpor"
  },
  {
    "path": "pyprland/quickstart/helpers/__init__.py",
    "chars": 2530,
    "preview": "\"\"\"Shared helper utilities for the quickstart wizard.\"\"\"\n\nfrom __future__ import annotations\n\nimport shutil\nimport subpr"
  },
  {
    "path": "pyprland/quickstart/helpers/monitors.py",
    "chars": 5143,
    "preview": "\"\"\"Monitor detection and layout wizard.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nfrom datac"
  },
  {
    "path": "pyprland/quickstart/helpers/scratchpads.py",
    "chars": 7205,
    "preview": "\"\"\"Scratchpad presets and configuration wizard.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclas"
  },
  {
    "path": "pyprland/quickstart/questions.py",
    "chars": 8875,
    "preview": "\"\"\"Convert ConfigField schema to questionary questions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path"
  },
  {
    "path": "pyprland/quickstart/wizard.py",
    "chars": 9785,
    "preview": "\"\"\"Main wizard flow for pypr-quickstart.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimpor"
  },
  {
    "path": "pyprland/state.py",
    "chars": 1476,
    "preview": "\"\"\"Shared state management for cross-plugin coordination.\n\nSharedState is a dataclass holding commonly-accessed mutable "
  },
  {
    "path": "pyprland/terminal.py",
    "chars": 2734,
    "preview": "\"\"\"Terminal handling utilities for interactive programs.\"\"\"\n\nimport fcntl\nimport os\nimport pty\nimport select\nimport stru"
  },
  {
    "path": "pyprland/utils.py",
    "chars": 4523,
    "preview": "\"\"\"General utility functions for Pyprland.\n\nProvides:\n- merge(): Deep dict merging with optional replace mode\n- apply_va"
  },
  {
    "path": "pyprland/validate_cli.py",
    "chars": 5645,
    "preview": "\"\"\"CLI validation entry point for pyprland configuration.\"\"\"\n\nimport importlib\nimport json\nimport logging\nimport os\nimpo"
  },
  {
    "path": "pyprland/validation.py",
    "chars": 14854,
    "preview": "\"\"\"Configuration validation framework with schema definitions.\n\nProvides declarative schema definitions (ConfigField, Co"
  },
  {
    "path": "pyprland/version.py",
    "chars": 44,
    "preview": "\"\"\"Package version.\"\"\"\n\nVERSION = \"3.3.1-1\"\n"
  },
  {
    "path": "pyproject.toml",
    "chars": 2675,
    "preview": "[project]\nname = \"pyprland\"\nversion = \"3.3.1\"\ndescription = \"A companion for your desktop UX\"\nauthors = [\n    { name = \""
  },
  {
    "path": "sample_extension/README.md",
    "chars": 126,
    "preview": "# Sample external Pyprland plugin\n\nNeeds to be installed with the same python environment as Pyprland\n\nEg: `pip install "
  },
  {
    "path": "sample_extension/pypr_examples/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "sample_extension/pypr_examples/focus_counter.py",
    "chars": 1307,
    "preview": "\"\"\"Sample plugin demonstrating pyprland plugin development.\n\nExposes a \"counter\" command: `pypr counter` showing focus c"
  },
  {
    "path": "sample_extension/pyproject.toml",
    "chars": 359,
    "preview": "[tool.poetry]\nname = \"pypr_examples\"\nversion = \"0.0.1\"\ndescription = \"Example pyprland plugin counting focus changes\"\nau"
  },
  {
    "path": "scripts/backquote_as_links.py",
    "chars": 725,
    "preview": "#!/bin/env python\nimport os\nimport re\n\n\ndef replace_links(match):\n    \"\"\"Substitution handler for regex, replaces backqu"
  },
  {
    "path": "scripts/check_plugin_docs.py",
    "chars": 8709,
    "preview": "#!/usr/bin/env python\n\"\"\"Check that all plugin config options and commands are documented.\n\nThis script verifies that:\n1"
  },
  {
    "path": "scripts/completions/README.md",
    "chars": 115,
    "preview": "Installs in:\n\n- /usr/share/zsh/site-functions/_pypr for zsh\n- /usr/share/bash-completion/completions/pypr for bash\n"
  },
  {
    "path": "scripts/completions/pypr.bash",
    "chars": 8963,
    "preview": "# AUTOMATICALLY GENERATED by `shtab`\n\n_shtab_pypr_subparsers=('dumpjson' 'edit' 'exit' 'help' 'version' 'reload' 'attach"
  },
  {
    "path": "scripts/completions/pypr.zsh",
    "chars": 9824,
    "preview": "#compdef pypr\n\n# AUTOMATICALLY GENERATED by `shtab`\n\n\n_shtab_pypr_commands() {\n    local _commands=(\n        \"attach:\"\n "
  },
  {
    "path": "scripts/generate_codebase_overview.py",
    "chars": 9818,
    "preview": "#!/usr/bin/env python3\n\"\"\"Generate CODEBASE_OVERVIEW.md from module docstrings.\n\nParses all Python files in the pyprland"
  },
  {
    "path": "scripts/generate_monitor_diagrams.py",
    "chars": 10001,
    "preview": "#!/usr/bin/env python3\n\"\"\"Generate SVG diagrams for monitor placement documentation.\"\"\"\n\nfrom dataclasses import datacla"
  },
  {
    "path": "scripts/generate_plugin_docs.py",
    "chars": 13324,
    "preview": "#!/usr/bin/env python\n\"\"\"Generate JSON documentation for pyprland plugins.\n\nThis script extracts documentation from plug"
  },
  {
    "path": "scripts/get-pypr",
    "chars": 839,
    "preview": "#!/bin/sh\nset -e\nURL=\"https://files.pythonhosted.org/packages/58/6b/a8c8d8fbfb7ece2046b9a1ce811b53ba00e528fa3e45779acc24"
  },
  {
    "path": "scripts/make_release",
    "chars": 2064,
    "preview": "#!/bin/bash\n\ncd $(git rev-parse --show-toplevel)\n\n./scripts/backquote_as_links.py\n\nglow RELEASE_NOTES.md || exit \"Can't "
  },
  {
    "path": "scripts/plugin_metadata.toml",
    "chars": 664,
    "preview": "\n\n[scratchpads]\nstars = 3\ndemoVideoId = \"ZOhv59VYqkc\"\n\n[stash]\nstars = 2\n\n[magnify]\nstars = 3\ndemoVideoId = \"yN-mhh9aDuo"
  },
  {
    "path": "scripts/pypr.py",
    "chars": 4980,
    "preview": "\"\"\"Fake pypr CLI to generate auto-completion scripts.\"\"\"\n\nimport argparse\nimport os\nimport pathlib\n\nimport shtab\n\nTOML_F"
  },
  {
    "path": "scripts/pypr.sh",
    "chars": 110,
    "preview": "#!/bin/sh\nsocat - \"UNIX-CONNECT:${XDG_RUNTIME_DIR}/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.pyprland.sock\" <<< $@\n"
  },
  {
    "path": "scripts/title",
    "chars": 125,
    "preview": "#!/bin/sh\necho -e \"\\e[1;30;43m                                        \"$@\"                                        \\e[1;0"
  },
  {
    "path": "scripts/update_get-pypr.sh",
    "chars": 141,
    "preview": "#!/bin/sh\nset -e\nurl=$(curl https://pypi.org/pypi/pyprland/json | jq '.urls[] |.url' |grep 'whl\"$')\n\nsed -i \"s#^URL=.*#U"
  },
  {
    "path": "scripts/update_version",
    "chars": 509,
    "preview": "#!/bin/sh\n\n# Change directory to the root of the Git repository\ncd \"$(git rev-parse --show-toplevel)\"\n\n# Get the version"
  },
  {
    "path": "scripts/v_whitelist.py",
    "chars": 3429,
    "preview": "_.execute_batch  # unused method (pyprland/adapters/backend.py:66)\n_.execute_batch  # unused method (pyprland/adapters/h"
  },
  {
    "path": "site/.vitepress/config.mjs",
    "chars": 4055,
    "preview": "/**\n * VitePress configuration with dynamic version discovery.\n *\n * Sidebar configurations are loaded from sidebar.json"
  },
  {
    "path": "site/.vitepress/theme/custom.css",
    "chars": 7227,
    "preview": "summary {\n    cursor: help;\n}\n\nsummary:hover {\n    text-decoration: underline;\n}\n\ndetails {\n    border: solid 1px #777;\n"
  },
  {
    "path": "site/.vitepress/theme/index.js",
    "chars": 2921,
    "preview": "// .vitepress/theme/index.js\nimport DefaultTheme from 'vitepress/theme'\n\nimport CommandList from \"/components/CommandLis"
  },
  {
    "path": "site/Architecture.md",
    "chars": 2303,
    "preview": "# Architecture\n\nThis section provides a comprehensive overview of Pyprland's internal architecture, designed for develop"
  },
  {
    "path": "site/Architecture_core.md",
    "chars": 15275,
    "preview": "# Core Components\n\nThis document details the core components of Pyprland's architecture.\n\n## Entry Points\n\nThe applicati"
  },
  {
    "path": "site/Architecture_overview.md",
    "chars": 6950,
    "preview": "# Architecture Overview\n\nThis document provides a high-level overview of Pyprland's architecture, data flow, and design "
  },
  {
    "path": "site/Commands.md",
    "chars": 5309,
    "preview": "# Commands\n\n<script setup>\nimport PluginCommands from './components/PluginCommands.vue'\n</script>\n\nThis page covers the "
  },
  {
    "path": "site/Configuration.md",
    "chars": 3493,
    "preview": "# Configuration\n\nThis page covers the configuration file format and available options.\n\n## File Location\n\nThe default co"
  },
  {
    "path": "site/Development.md",
    "chars": 10313,
    "preview": "# Development\n\nIt's easy to write your own plugin by making a Python package and then indicating its name as the plugin "
  },
  {
    "path": "site/Examples.md",
    "chars": 4144,
    "preview": "# Examples\n\nThis page provides complete configuration examples to help you get started.\n\n## Basic Setup\n\nA minimal confi"
  },
  {
    "path": "site/Getting-started.md",
    "chars": 3052,
    "preview": "# Getting Started\n\nPypr consists of two things:\n\n- **A tool**: `pypr` which runs the daemon (service) and allows you to "
  }
]

// ... and 594 more files (download for full content)

About this extraction

This page contains the full source code of the hyprland-community/pyprland GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 794 files (2.6 MB), approximately 716.4k tokens, and a symbol index with 1870 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!