diff --git a/README.md b/README.md index 9e5be38e..dc78e275 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 4eac132c..5ff4473f 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -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) diff --git a/docs/widgets/(Widget)-Lock-Keys.md b/docs/widgets/(Widget)-Lock-Keys.md new file mode 100644 index 00000000..76e53dde --- /dev/null +++ b/docs/widgets/(Widget)-Lock-Keys.md @@ -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; +} +``` diff --git a/src/core/validation/widgets/yasb/lock_keys.py b/src/core/validation/widgets/yasb/lock_keys.py new file mode 100644 index 00000000..5d939cce --- /dev/null +++ b/src/core/validation/widgets/yasb/lock_keys.py @@ -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() diff --git a/src/core/widgets/yasb/lock_keys.py b/src/core/widgets/yasb/lock_keys.py new file mode 100644 index 00000000..ba285690 --- /dev/null +++ b/src/core/widgets/yasb/lock_keys.py @@ -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("(.*?)", 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)