Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ choco install yasb
| [Language](https://github.com/amnweb/yasb/wiki/(Widget)-Language) | Shows the current input language and allows switching between languages. |
| [Launchpad](https://github.com/amnweb/yasb/wiki/(Widget)-Launchpad) | A customizable launchpad for quick access to applications. |
| [Libre Hardware Monitor](https://github.com/amnweb/yasb/wiki/(Widget)-Libre-HW-Monitor) | Connects to Libre Hardware Monitor to get sensor data. |
| [Lock Keys](https://github.com/amnweb/yasb/wiki/(Widget)-Lock-Keys) | Displays the current Caps Lock and Num Lock states. |
| [Media](https://github.com/amnweb/yasb/wiki/(Widget)-Media) | Displays media controls and information. |
| [Media Lite](https://github.com/amnweb/yasb/wiki/(Widget)-Media-Lite) | A vertical and minimal album-style media widget. |
| [Memory](https://github.com/amnweb/yasb/wiki/(Widget)-Memory) | Shows current memory usage and information. |
Expand Down
1 change: 1 addition & 0 deletions docs/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- [Language](./(Widget)-Language)
- [Launchpad](./(Widget)-Launchpad)
- [Libre Hardware Monitor](./(Widget)-Libre-HW-Monitor)
- [Lock Keys](./(Widget)-Lock-Keys)
- [Media](./(Widget)-Media)
- [Media Lite](./(Widget)-Media-Lite)
- [Memory](./(Widget)-Memory)
Expand Down
70 changes: 70 additions & 0 deletions docs/widgets/(Widget)-Lock-Keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Lock Keys Widget Configuration

Displays the current Caps Lock and Num Lock states on the bar. The widget checks the Windows keyboard toggle state at a short interval and updates only when a state changes.

## Options

| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `label` | string | `"{caps_lock} {num_lock}"` | Primary label. Supports `{caps_lock}` and `{num_lock}`. |
| `label_alt` | string | `"Caps: {caps_lock} Num: {num_lock}"` | Alternate label shown by the `toggle_label` callback. |
| `update_interval` | integer | `200` | How often to check the states, in milliseconds. Allowed range: 50–5000. |
| `class_name` | string | `""` | Additional CSS class for this widget. |
| `state_labels` | dictionary | See below | Text displayed for each on/off state. |
| `callbacks` | dictionary | `on_left: toggle_label` | Mouse callbacks for the widget. |

### State labels

| Option | Default |
| :--- | :--- |
| `caps_lock_on` | `"CAPS"` |
| `caps_lock_off` | `""` |
| `num_lock_on` | `"NUM"` |
| `num_lock_off` | `""` |

The state labels may contain text, symbols, or icon-font characters. Empty off-state labels make the indicator visible only while the corresponding lock is enabled.

## Example configuration

```yaml
lock_keys:
type: "yasb.lock_keys.LockKeysWidget"
options:
label: "{caps_lock} {num_lock}"
label_alt: "Caps: {caps_lock} | Num: {num_lock}"
update_interval: 200
state_labels:
caps_lock_on: "CAPS ON"
caps_lock_off: "caps off"
num_lock_on: "NUM ON"
num_lock_off: "num off"
callbacks:
on_left: "toggle_label"
on_middle: "do_nothing"
on_right: "do_nothing"
```

## Style

The widget container always has one Caps Lock class and one Num Lock class:

- `caps-lock-on` or `caps-lock-off`
- `num-lock-on` or `num-lock-off`

```css
.lock-keys-widget {
padding: 0 8px;
}

.lock-keys-widget .label {
color: #888888;
}

.lock-keys-widget .widget-container.caps-lock-on .label {
color: #f9e2af;
}

.lock-keys-widget .widget-container.num-lock-on .label {
font-weight: 700;
}
```
28 changes: 28 additions & 0 deletions src/core/validation/widgets/yasb/lock_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from pydantic import Field

from core.validation.widgets.base_model import (
CallbacksConfig,
CustomBaseModel,
KeybindingConfig,
)


class LockKeysStateLabelsConfig(CustomBaseModel):
caps_lock_on: str = "CAPS"
caps_lock_off: str = ""
num_lock_on: str = "NUM"
num_lock_off: str = ""


class LockKeysCallbacksConfig(CallbacksConfig):
on_left: str = "toggle_label"


class LockKeysConfig(CustomBaseModel):
label: str = "{caps_lock} {num_lock}"
label_alt: str = "Caps: {caps_lock} Num: {num_lock}"
update_interval: int = Field(default=200, ge=50, le=5000)
class_name: str = ""
state_labels: LockKeysStateLabelsConfig = LockKeysStateLabelsConfig()
keybindings: list[KeybindingConfig] = []
callbacks: LockKeysCallbacksConfig = LockKeysCallbacksConfig()
90 changes: 90 additions & 0 deletions src/core/widgets/yasb/lock_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import re

from PyQt6.QtWidgets import QLabel

from core.utils.utilities import refresh_widget_style
from core.utils.win32.bindings import user32
from core.validation.widgets.yasb.lock_keys import LockKeysConfig
from core.widgets.base import BaseWidget

VK_CAPITAL = 0x14
VK_NUMLOCK = 0x90


class LockKeysWidget(BaseWidget):
validation_schema = LockKeysConfig

def __init__(self, config: LockKeysConfig) -> None:
super().__init__(config.update_interval, class_name=f"lock-keys-widget {config.class_name}")
self.config = config
self._show_alt_label = False
self._caps_lock_active: bool | None = None
self._num_lock_active: bool | None = None

self._init_container()
self.build_widget_label(self.config.label, self.config.label_alt)

self.register_callback("toggle_label", self._toggle_label)
self.register_callback("update_state", self._update_state)

self.callback_left = self.config.callbacks.on_left
self.callback_middle = self.config.callbacks.on_middle
self.callback_right = self.config.callbacks.on_right
self.callback_timer = "update_state"

self.start_timer()

def _toggle_label(self) -> None:
self._show_alt_label = not self._show_alt_label
for widget in self._widgets:
widget.setVisible(not self._show_alt_label)
for widget in self._widgets_alt:
widget.setVisible(self._show_alt_label)
self._update_label()

def _update_state(self) -> None:
caps_lock_active = bool(user32.GetKeyState(VK_CAPITAL) & 0x0001)
num_lock_active = bool(user32.GetKeyState(VK_NUMLOCK) & 0x0001)

if caps_lock_active == self._caps_lock_active and num_lock_active == self._num_lock_active:
return

self._caps_lock_active = caps_lock_active
self._num_lock_active = num_lock_active
self._update_label()
self._update_state_classes()

def _update_label(self) -> None:
active_widgets = self._widgets_alt if self._show_alt_label else self._widgets
active_label_content = self.config.label_alt if self._show_alt_label else self.config.label
label_parts = re.split("(<span.*?>.*?</span>)", active_label_content)
label_parts = [part for part in label_parts if part]

state_labels = self.config.state_labels
label_options = {
"{caps_lock}": state_labels.caps_lock_on if self._caps_lock_active else state_labels.caps_lock_off,
"{num_lock}": state_labels.num_lock_on if self._num_lock_active else state_labels.num_lock_off,
}

widget_index = 0
for part in label_parts:
part = part.strip()
if not part:
continue

formatted_text = part
for placeholder, value in label_options.items():
formatted_text = formatted_text.replace(placeholder, value)

if widget_index < len(active_widgets) and isinstance(active_widgets[widget_index], QLabel):
active_widgets[widget_index].setText(formatted_text)
widget_index += 1

def _update_state_classes(self) -> None:
caps_class = "caps-lock-on" if self._caps_lock_active else "caps-lock-off"
num_class = "num-lock-on" if self._num_lock_active else "num-lock-off"
target_class = f"widget-container {caps_class} {num_class}"

if self._widget_container.property("class") != target_class:
self._widget_container.setProperty("class", target_class)
refresh_widget_style(self._widget_container, *self._widgets, *self._widgets_alt)