Skip to content

Commit

Permalink
feat(widget/glazewm): GlazeWM support
Browse files Browse the repository at this point in the history
- Added GlazeWM Workspaces and Tiling Direction widgets.
- Updated readme and added docs.
  • Loading branch information
Video-Nomad committed Jan 24, 2025
1 parent a137057 commit 510cb51
Show file tree
Hide file tree
Showing 8 changed files with 608 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ for more themes visit [yasb-themes](https://github.com/amnweb/yasb-themes)
- **[Clock](https://github.com/amnweb/yasb/wiki/(Widget)-Clock)**: Displays the current time and date.
- **[Custom](https://github.com/amnweb/yasb/wiki/(Widget)-Custom)**: Create a custom widget.
- **[Github](https://github.com/amnweb/yasb/wiki/(Widget)-Github)**: Shows notifications from GitHub.
- **[GlazeWM Workspaces](https://github.com/amnweb/yasb/wiki/(Widget)-GlazeWM-Workspaces)**: GlazeWM workspaces widget.
- **[GlazeWM Tiling Direction](https://github.com/amnweb/yasb/wiki/(Widget)-GlazeWM-Tiling-Direction)**: GlazeWM tiling direction widget.
- **[Home](https://github.com/amnweb/yasb/wiki/(Widget)-Home)**: A customizable home widget menu.
- **[Disk](https://github.com/amnweb/yasb/wiki/(Widget)-Disk)**: Displays disk usage information.
- **[Language](https://github.com/amnweb/yasb/wiki/(Widget)-Language)**: Shows the current input language.
Expand Down
48 changes: 48 additions & 0 deletions docs/widgets/(Widget)-GlazeWM-Tiling-Direction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# GlazeWM Tiling Direction Widget
| Option | Type | Default | Description |
|----------------------|--------|-------------------------|-------------------------------------------------|
| `horizontal_label` | string | `'\udb81\udce1'` | The label used for horizontal tiling direction. |
| `vertical_label` | string | `'\udb81\udce2'` | Optional label for populated workspaces. |
| `glazewm_server_uri` | string | `'ws://localhost:6123'` | Optional GlazeWM server uri. |

## Example Configuration

```yaml
glazewm_tiling_direction:
type: "glazewm.tiling_direction.GlazewmTilingDirectionWidget"
options:
horizontal_label: "\udb81\udce1"
vertical_label: "\udb81\udce2"
```
## Description of Options
- **horizontal_label:** Label used for horizontal tiling direction.
- **vertical_label:** Label for vertical tiling direction.
- **glazewm_server_uri:** Optional GlazeWM server uri if it ever changes on GlazeWM side.
## Style
```css
.glazewm-tiling-direction {} /*Style for widget.*/
.glazewm-tiling-direction .btn {} /*Style for tiling direction button.*/
```

## Example CSS
```css
.glazewm-tiling-direction {
background-color: transparent;
padding: 0;
margin: 0;
}

.glazewm-tiling-direction .btn {
font-size: 18px;
width: 14px;
padding: 0 4px 0 4px;
color: #CDD6F4;
border: none;
}

.glazewm-tiling-direction .btn:hover {
background-color: #727272;
}
```
99 changes: 99 additions & 0 deletions docs/widgets/(Widget)-GlazeWM-Workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# GlazeWM Workspaces Widget
| Option | Type | Default | Description |
|-------------------------|---------|-------------------------|----------------------------------------------------------|
| `offline_label` | string | `'GlazeWM Offline'` | The label to display when GlazeWM is offline. |
| `populated_label` | string | `'{index}'` | Optional label for populated workspaces. |
| `empty_label` | string | `'{index}'` | Optional label for empty workspaces. |
| `hide_empty_workspaces` | boolean | `true` | Whether to hide empty workspaces. |
| `hide_if_offline` | boolean | `false` | Whether to hide workspaces widget if GlazeWM is offline. |
| `glazewm_server_uri` | string | `'ws://localhost:6123'` | Optional GlazeWM server uri. |


## Example Configuration

```yaml
glazewm_workspaces:
type: "glazewm.workspaces.GlazewmWorkspacesWidget"
options:
offline_label: "GlazeWM Offline"
hide_empty_workspaces: true
hide_if_offline: false

# By default workspace names are fetched from GlazeWM and "display_name" option takes priority over "name".
# However, you can customize populated and empty labels here using {name} and {display_name} placeholders if needed.
# {name} will be replaced with workspace name (index) from GlazeWM.
# {display_name} will be replaced with workspace display_name from GlazeWM.

# populated_label: "{name} {display_name} \uebb4"e
# empty_label: "{name} {display_name} \uebb5"
```

## Description of Options
- **offline_label:** The label to display when GlazeWM is offline.
- **populated_label:** Optional label for populated workspaces. If not set, name or display_name from GlazeWM will be used.
- **empty_label:** Optional label for empty workspaces. If not set, name or display_name from GlazeWM will be used.
- **hide_empty_workspaces:** Whether to hide empty workspaces.
- **hide_if_offline:** Whether to hide workspaces widget if GlazeWM is offline.
- **glazewm_server_uri:** Optional GlazeWM server uri if it ever changes on GlazeWM side.

## Important Note
In GlazeWM config use "1", "2", "3" for workspace "name" and NOT some custom string. This will ensure proper sorting of workspaces.

If you need a custom name for each workspace - use "display_name".

**Example:**

```yaml
workspaces:
- name: "1"
display_name: "Work" # Optional
- name: "2"
display_name: "Browser" # Optional
- name: "3"
display_name: "Music" # Optional
# and so on...
```

## Style
```css
.glazewm-workspaces {} /*Style for widget.*/
.glazewm-workspaces .btn {} /*Style for workspace buttons.*/
.glazewm-workspaces .btn.active {} /*Style for active workspace button.*/
.glazewm-workspaces .btn.populated {} /*Style for populated workspace button.*/
.glazewm-workspaces .btn.empty {} /*Style for empty workspace button.*/
.glazewm-workspaces .offline-status {} /*Style for offline status label.*/
```

## Example CSS
```css
.glazewm-workspaces {
margin: 0;
}

.glazewm-workspaces .ws-btn {
font-size: 14px;
background-color: transparent;
border: none;
padding: 0px 4px 0px 4px;
margin: 0 2px 0 2px;
color: #CDD6F4;
}

.glazewm-workspaces .ws-btn.active {
background-color: #727272;
}

.glazewm-workspaces .ws-btn.populated {
color: #C2DAF7;
}

.glazewm-workspaces .ws-btn.empty {
color: #7D8B9D;
}

.glazewm-workspaces .ws-btn:hover,
.glazewm-workspaces .ws-btn.populated:hover,
.glazewm-workspaces .ws-btn.empty:hover {
background-color: #727272;
}
```
144 changes: 144 additions & 0 deletions src/core/utils/glazewm/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
import logging
from dataclasses import dataclass
from enum import StrEnum, auto
from typing import Any

from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSignal
from PyQt6.QtNetwork import QAbstractSocket
from PyQt6.QtWebSockets import QWebSocket

from settings import DEBUG

logger = logging.getLogger("glazewm_client")

if DEBUG:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.CRITICAL)


@dataclass
class Workspace:
name: str
display_name: str
focus: bool = False
is_displayed: bool = False
num_windows: int = 0


@dataclass
class Monitor:
name: str
hwnd: int
workspaces: list[Workspace]


class MessageType(StrEnum):
EVENT_SUBSCRIPTION = auto()
CLIENT_RESPONSE = auto()


class QueryType(StrEnum):
MONITORS = "query monitors"
TILING_DIRECTION = "query tiling-direction"


class TilingDirection(StrEnum):
HORIZONTAL = auto()
VERTICAL = auto()


class GlazewmClient(QObject):
workspaces_data_processed = pyqtSignal(list)
tiling_direction_processed = pyqtSignal(str)
glazewm_connection_status = pyqtSignal(bool)

def __init__(self, uri: str, initial_messages: list[str] | None = None, reconnect_interval=4000) -> None:
super().__init__()
self.initial_messages = initial_messages if initial_messages else []

self._uri = QUrl(uri)
self._websocket = QWebSocket()
self._websocket.connected.connect(self._on_connected)
self._websocket.textMessageReceived.connect(self._handle_message)
self._websocket.stateChanged.connect(self._on_state_changed)
self._websocket.errorOccurred.connect(self._on_error)

self._reconnect_timer = QTimer()
self._reconnect_timer.setInterval(reconnect_interval)
self._reconnect_timer.timeout.connect(self.connect)

def activate_workspace(self, workspace_name: str):
self._websocket.sendTextMessage(f"command focus --workspace {workspace_name}")

def toggle_tiling_direction(self):
self._websocket.sendTextMessage("command toggle-tiling-direction")

def connect(self):
logger.debug(f"Connecting to {self._uri}...")
self._websocket.open(self._uri)

def _on_connected(self) -> None:
logger.debug(f"Connected to {self._uri}")
for message in self.initial_messages:
logger.debug(f"Sent initial message: {message}")
self._websocket.sendTextMessage(message)

# Stop reconnect timer
self._reconnect_timer.stop()

def _on_state_changed(self, state: QAbstractSocket.SocketState):
logger.debug(f"WebSocket state changed: {state}")
self.glazewm_connection_status.emit(state == QAbstractSocket.SocketState.ConnectedState)

def _on_error(self, error: QAbstractSocket.SocketError) -> None:
logger.warning(f"WebSocket error: {error}\nReconnecting...")
self._reconnect_timer.start()

def _handle_message(self, message: str):
try:
response = json.loads(message)
except json.JSONDecodeError:
logger.warning("Received invalid JSON data.")
return

if response.get("messageType") == MessageType.EVENT_SUBSCRIPTION:
self._websocket.sendTextMessage(QueryType.MONITORS)
self._websocket.sendTextMessage(QueryType.TILING_DIRECTION)
elif response.get("messageType") == MessageType.CLIENT_RESPONSE:
data = response.get("data", {})
if response.get("clientMessage") == QueryType.MONITORS:
monitors = data.get("monitors", [])
self.workspaces_data_processed.emit(self._process_workspaces(monitors))
elif response.get("clientMessage") == QueryType.TILING_DIRECTION:
tiling_direction = TilingDirection(data.get("tilingDirection", TilingDirection.HORIZONTAL))
self.tiling_direction_processed.emit(tiling_direction)

def _process_workspaces(self, data: list[dict[str, Any]]) -> list[Monitor]:
monitors: list[Monitor] = []
for mon in data:
monitor_name: str | None = mon.get("hardwareId")
handle: int | None = mon.get("handle")
if not monitor_name or not handle:
logger.warning("Monitor name or hwnd not found")
continue
workspaces_data = [
Workspace(
name=child.get("name", ""),
display_name=child.get("displayName", ""),
is_displayed=child.get("isDisplayed", False),
focus=child.get("hasFocus", False),
num_windows=len(child.get("children", [])),
)
for child in mon.get("children", [])
if child.get("type") == "workspace"
]
monitors.append(
Monitor(
name=monitor_name,
hwnd=handle,
workspaces=workspaces_data,
)
)
return monitors
21 changes: 21 additions & 0 deletions src/core/validation/widgets/glazewm/tiling_direction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
DEFAULTS = {
"horizontal_label": "\udb81\udce1",
"vertical_label": "\udb81\udce2",
"glazewm_server_uri": "ws://localhost:6123",
}


VALIDATION_SCHEMA = {
"glazewm_server_uri": {
"type": "string",
"default": DEFAULTS["glazewm_server_uri"],
},
"horizontal_label": {
"type": "string",
"default": DEFAULTS["horizontal_label"]
},
"vertical_label": {
"type": "string",
"default": DEFAULTS["vertical_label"]
},
}
37 changes: 37 additions & 0 deletions src/core/validation/widgets/glazewm/workspaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
DEFAULTS = {
"offline_label": "GlazeWM Offline",
"populated_label": None,
"empty_label": None,
"hide_empty_workspaces": True,
"hide_if_offline": False,
"glazewm_server_uri": "ws://localhost:6123",
}

VALIDATION_SCHEMA = {
"offline_label": {
"type": "string",
"default": DEFAULTS["offline_label"],
},
"populated_label": {
"type": "string",
"default": DEFAULTS["populated_label"],
"nullable": True,
},
"empty_label": {
"type": "string",
"default": DEFAULTS["empty_label"],
"nullable": True,
},
"hide_empty_workspaces": {
"type": "boolean",
"default": DEFAULTS["hide_empty_workspaces"],
},
"hide_if_offline": {
"type": "boolean",
"default": DEFAULTS["hide_if_offline"],
},
"glazewm_server_uri": {
"type": "string",
"default": DEFAULTS["glazewm_server_uri"],
},
}
Loading

0 comments on commit 510cb51

Please # to comment.