Skip to content

Core Components

This document details the core components of Pyprland's architecture.

Entry Points

The application can run in two modes: daemon (background service) or client (send commands to running daemon).

Entry PointFilePurpose
pyprcommand.pyMain CLI entry (daemon or client mode)
Daemon modepypr_daemon.pyStart the background daemon
Client modeclient.pySend command to running daemon

Manager

The Pyprland class is the core orchestrator, responsible for:

ResponsibilityMethod/Attribute
Plugin loading_load_plugins()
Event dispatching_run_event()
Command handlinghandle_command()
Server lifecyclerun(), serve()
Configurationload_config(), config
Shared statestate: SharedState

Key Design Patterns:

  • Per-plugin async task queues (queues: dict[str, asyncio.Queue]) - ensures plugin isolation
  • Deduplication via @remove_duplicate decorator - prevents rapid duplicate events
  • Plugin isolation - each plugin processes events independently

Plugin System

Base Class

All plugins inherit from the Plugin base class:

python
class Plugin:
    name: str                    # Plugin identifier
    config: Configuration        # Plugin-specific config section
    state: SharedState           # Shared application state
    backend: EnvironmentBackend  # WM abstraction layer
    log: Logger                  # Plugin-specific logger
    
    # Lifecycle hooks
    async def init() -> None           # Called once at startup
    async def on_reload() -> None      # Called on init and config reload
    async def exit() -> None           # Called on shutdown
    
    # Config validation
    config_schema: ClassVar[list[ConfigField]]
    def validate_config() -> list[str]

Event Handler Protocol

Plugins implement handlers by naming convention. See protocols.py for the full protocol definitions:

python
# Hyprland events: event_<eventname>
async def event_openwindow(self, params: str) -> None: ...
async def event_closewindow(self, addr: str) -> None: ...
async def event_workspace(self, workspace: str) -> None: ...

# Commands: run_<command>
async def run_toggle(self, name: str) -> str | None: ...

# Niri events: niri_<eventtype>
async def niri_outputschanged(self, data: dict) -> None: ...

Plugin Lifecycle

Built-in Plugins

PluginSourceDescription
pyprland (core)plugins/pyprland/Internal state management
scratchpadsplugins/scratchpads/Dropdown/scratchpad windows
monitorsplugins/monitors/Monitor layout management
wallpapersplugins/wallpapers/Wallpaper cycling, color schemes
exposeplugins/expose.pyWindow overview
magnifyplugins/magnify.pyZoom functionality
layout_centerplugins/layout_center.pyCentered layout mode
fetch_client_menuplugins/fetch_client_menu.pyMenu-based window switching
shortcuts_menuplugins/shortcuts_menu.pyShortcut launcher
toggle_dpmsplugins/toggle_dpms.pyScreen power toggle
toggle_specialplugins/toggle_special.pySpecial workspace toggle
system_notifierplugins/system_notifier.pySystem notifications
lost_windowsplugins/lost_windows.pyRecover lost windows
shift_monitorsplugins/shift_monitors.pyShift windows between monitors
workspaces_follow_focusplugins/workspaces_follow_focus.pyWorkspace follows focus
fcitx5_switcherplugins/fcitx5_switcher.pyInput method switching
menubarplugins/menubar.pyMenu bar integration

Backend Adapter Layer

The adapter layer abstracts differences between window managers. See adapters/ for the full implementation.

ClassSource
EnvironmentBackendadapters/backend.py
HyprlandBackendadapters/hyprland.py
NiriBackendadapters/niri.py

The backend is selected automatically based on environment:

  • If NIRI_SOCKET is set -> NiriBackend
  • Otherwise -> HyprlandBackend

IPC Layer

Low-level socket communication with the window manager is handled in ipc.py:

FunctionPurpose
hyprctl_connection()Context manager for Hyprland command socket
niri_connection()Context manager for Niri socket
get_response()Send command, receive JSON response
get_event_stream()Subscribe to WM event stream
niri_request()Send Niri-specific request
@retry_on_resetDecorator for automatic connection retry

Socket Paths:

SocketPath
Hyprland commands$XDG_RUNTIME_DIR/hypr/$SIGNATURE/.socket.sock
Hyprland events$XDG_RUNTIME_DIR/hypr/$SIGNATURE/.socket2.sock
Niri$NIRI_SOCKET
Pyprland (Hyprland)$XDG_RUNTIME_DIR/hypr/$SIGNATURE/.pyprland.sock
Pyprland (Niri)dirname($NIRI_SOCKET)/.pyprland.sock
Pyprland (standalone)$XDG_DATA_HOME/.pyprland.sock

Pyprland Socket Protocol

The daemon exposes a Unix domain socket for client-daemon communication. This simple text-based protocol allows any language to implement a client.

Socket Path

The socket location depends on the environment:

EnvironmentSocket Path
Hyprland$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.pyprland.sock
Niridirname($NIRI_SOCKET)/.pyprland.sock
Standalone$XDG_DATA_HOME/.pyprland.sock (defaults to ~/.local/share/.pyprland.sock)

If the Hyprland path exceeds 107 characters, a shortened path is used:

/tmp/.pypr-$HYPRLAND_INSTANCE_SIGNATURE/.pyprland.sock

Protocol

DirectionFormat
Request<command> [args...]\n (newline-terminated, then EOF)
ResponseOK [output] or ERROR: <message> or raw text (legacy)

Response Prefixes:

PrefixMeaningExit Code
OKCommand succeeded0
OK <output>Command succeeded with output0
ERROR: <msg>Command failed4
(raw text)Legacy response (help, version, dumpjson)0

Exit Codes:

CodeNameDescription
0SUCCESSCommand completed successfully
1USAGE_ERRORNo command provided or invalid arguments
2ENV_ERRORMissing environment variables
3CONNECTION_ERRORCannot connect to daemon
4COMMAND_ERRORCommand execution failed

See models.py for ExitCode and ResponsePrefix definitions.

pypr-client

For performance-critical use cases (e.g., keybindings), pypr-client is a lightweight C client available as an alternative to pypr. It supports all commands except validate and edit (which require Python).

FileDescription
client/pypr-client.cC implementation of the pypr client

Build:

bash
cd client
gcc -O2 -o pypr-client pypr-client.c

Features:

  • Minimal dependencies (libc only)
  • Fast startup (~1ms vs ~50ms for Python)
  • Same protocol as Python client
  • Proper exit codes for scripting

Comparison:

Aspectpyprpypr-client
Startup time~50ms~1ms
DependenciesPython 3.11+libc
Daemon modeYesNo
CommandsAllAll except validate, edit
Best forInteractive use, daemonKeybindings
Sourceclient.pypypr-client.c

Configuration System

Configuration is stored in TOML format at ~/.config/hypr/pyprland.toml:

toml
[pyprland]
plugins = ["scratchpads", "monitors", "magnify"]

[scratchpads.term]
command = "kitty --class scratchpad"
position = "50% 50%"
size = "80% 80%"

[monitors]
unknown = "extend"
ComponentSourceDescription
Configurationconfig.pyDict wrapper with typed accessors
ConfigValidatorvalidation.pySchema-based validation
ConfigFieldvalidation.pyField definition (name, type, required, default)

Shared State

The SharedState dataclass maintains commonly needed information:

python
@dataclass
class SharedState:
    active_workspace: str    # Current workspace name
    active_monitor: str      # Current monitor name  
    active_window: str       # Current window address
    environment: str         # "hyprland" or "niri"
    variables: dict          # User-defined variables
    monitors: list[str]      # All monitor names
    hyprland_version: VersionInfo

Data Models

TypedDict definitions in models.py ensure type safety:

python
class ClientInfo(TypedDict):
    address: str
    mapped: bool
    hidden: bool
    workspace: WorkspaceInfo
    class_: str  # aliased from "class"
    title: str
    # ... more fields

class MonitorInfo(TypedDict):
    name: str
    width: int
    height: int
    x: int
    y: int
    focused: bool
    transform: int
    # ... more fields