Skip to content

Commit db7a0a6

Browse files
committed
feat(language): add language widget
This commit introduces a new Language Widget that displays the current keyboard language and allows toggling between short and full language names. Additionally, it removes the deprecated lang widget configuration from the YAML file and updates the README to reflect these changes.
1 parent 181a684 commit db7a0a6

File tree

5 files changed

+211
-15
lines changed

5 files changed

+211
-15
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
- Look for the latest release version, which will typically be listed at the top.
1717
- Under the "Assets" section of the release, you’ll find various files. Click on the installer file to download it.
1818

19-
> [!IMPORTANT]
20-
> **Currently unstable:** The installer may crash when reloading configuration.
2119

2220
### Using Python
2321
- Install Python 3.12
@@ -58,4 +56,5 @@
5856
.power-menu-popup > .button > .label {} -> Styles for power buttons icons and labels inside the popup
5957
.media-widget {} -> Styles specific to the media widget
6058
.github-widget {} -> Styles specific to the github widget
59+
.language-widget {} -> Styles specific to the language widget
6160
```

src/config.yaml

+12-13
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ bars:
5252
"media",
5353
"wifi",
5454
#"battery",
55-
#"lang",
55+
"language",
5656
#"ip_info",
5757
#"traffic",
5858
"cpu",
@@ -143,6 +143,17 @@ widgets:
143143
callbacks:
144144
on_right: "exec cmd /c Taskmgr"
145145

146+
147+
language:
148+
type: "yasb.language.LanguageWidget"
149+
options:
150+
label: "<span>\uf1ab</span> {lang[short]}"
151+
label_alt: " {lang[full]}"
152+
update_interval: 5 # 5 seconds
153+
callbacks:
154+
on_left: "toggle_label"
155+
156+
146157
komorebi_workspaces:
147158
type: "komorebi.workspaces.WorkspaceWidget"
148159
options:
@@ -239,18 +250,6 @@ widgets:
239250
# https://www.weatherapi.com/docs/
240251
# Usage {temp_c}, {min_temp_c}, {max_temp_c}, {temp_f}, {min_temp_f}, {max_temp_f}, {location}, {humidity}, {icon}, {conditions}
241252

242-
lang:
243-
type: "yasb.custom.CustomWidget"
244-
options:
245-
label: "<span>\uf1ab</span>{data}"
246-
label_alt: "{data}"
247-
class_name: "lang-widget"
248-
exec_options:
249-
run_cmd: "powershell Add-Type -AssemblyName System.Windows.Forms;([System.Windows.Forms.InputLanguage]::CurrentInputLanguage).Culture.Name"
250-
# run every 10 sec
251-
run_interval: 10000
252-
return_format: "string"
253-
254253
volume:
255254
type: "yasb.volume.VolumeWidget"
256255
options:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
DEFAULTS = {
2+
'label': "{lang[short]}",
3+
'label_alt': "{lang[full]}",
4+
'update_interval': 5,
5+
'callbacks': {
6+
'on_left': 'toggle_label',
7+
'on_middle': 'do_nothing',
8+
'on_right': 'do_nothing'
9+
}
10+
}
11+
12+
VALIDATION_SCHEMA = {
13+
'label': {
14+
'type': 'string',
15+
'default': DEFAULTS['label']
16+
},
17+
'label_alt': {
18+
'type': 'string',
19+
'default': DEFAULTS['label_alt']
20+
},
21+
'update_interval': {
22+
'type': 'integer',
23+
'default': DEFAULTS['update_interval'],
24+
'min': 1,
25+
'max': 3600
26+
},
27+
'callbacks': {
28+
'type': 'dict',
29+
'schema': {
30+
'on_left': {
31+
'type': 'string',
32+
'default': DEFAULTS['callbacks']['on_left'],
33+
},
34+
'on_middle': {
35+
'type': 'string',
36+
'default': DEFAULTS['callbacks']['on_middle'],
37+
},
38+
'on_right': {
39+
'type': 'string',
40+
'default': DEFAULTS['callbacks']['on_right'],
41+
}
42+
},
43+
'default': DEFAULTS['callbacks']
44+
}
45+
}

src/core/widgets/yasb/language.py

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import re
2+
from core.widgets.base import BaseWidget
3+
from core.validation.widgets.yasb.language import VALIDATION_SCHEMA
4+
from PyQt6.QtWidgets import QLabel, QHBoxLayout, QWidget
5+
from PyQt6.QtCore import Qt, QTimer
6+
import ctypes
7+
import ctypes
8+
from ctypes import wintypes
9+
10+
# Constants
11+
LOCALE_NAME_MAX_LENGTH = 85
12+
LOCALE_SISO639LANGNAME = 0x59
13+
LOCALE_SISO3166CTRYNAME = 0x5A
14+
LOCALE_SLANGUAGE = 0x2
15+
LOCALE_SCOUNTRY = 0x6
16+
17+
# Define necessary ctypes structures and functions
18+
user32 = ctypes.WinDLL('user32', use_last_error=True)
19+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
20+
21+
class LanguageWidget(BaseWidget):
22+
validation_schema = VALIDATION_SCHEMA
23+
24+
def __init__(
25+
self,
26+
label: str,
27+
label_alt: str,
28+
update_interval: int,
29+
callbacks: dict[str, str],
30+
):
31+
super().__init__(int(update_interval * 1000), class_name="language-widget")
32+
33+
self._show_alt_label = False
34+
self._label_content = label
35+
self._label_alt_content = label_alt
36+
37+
# Construct container
38+
self._widget_container_layout: QHBoxLayout = QHBoxLayout()
39+
self._widget_container_layout.setSpacing(0)
40+
self._widget_container_layout.setContentsMargins(0, 0, 0, 0)
41+
# Initialize container
42+
self._widget_container: QWidget = QWidget()
43+
self._widget_container.setLayout(self._widget_container_layout)
44+
self._widget_container.setProperty("class", "widget-container")
45+
# Add the container to the main widget layout
46+
self.widget_layout.addWidget(self._widget_container)
47+
48+
self._create_dynamically_label(self._label_content, self._label_alt_content)
49+
50+
self.register_callback("toggle_label", self._toggle_label)
51+
self.register_callback("update_label", self._update_label)
52+
53+
self.callback_left = callbacks["on_left"]
54+
self.callback_right = callbacks["on_right"]
55+
self.callback_middle = callbacks["on_middle"]
56+
self.callback_timer = "update_label"
57+
58+
self.start_timer()
59+
60+
def _toggle_label(self):
61+
self._show_alt_label = not self._show_alt_label
62+
for widget in self._widgets:
63+
widget.setVisible(not self._show_alt_label)
64+
for widget in self._widgets_alt:
65+
widget.setVisible(self._show_alt_label)
66+
self._update_label()
67+
68+
69+
def _create_dynamically_label(self, content: str, content_alt: str):
70+
def process_content(content, is_alt=False):
71+
label_parts = re.split('(<span.*?>.*?</span>)', content) #Filters out empty parts before entering the loop
72+
label_parts = [part for part in label_parts if part]
73+
widgets = []
74+
for part in label_parts:
75+
part = part.strip() # Remove any leading/trailing whitespace
76+
if not part:
77+
continue
78+
if '<span' in part and '</span>' in part:
79+
class_name = re.search(r'class=(["\'])([^"\']+?)\1', part)
80+
class_result = class_name.group(2) if class_name else 'icon'
81+
icon = re.sub(r'<span.*?>|</span>', '', part).strip()
82+
label = QLabel(icon)
83+
label.setProperty("class", class_result)
84+
else:
85+
label = QLabel(part)
86+
label.setProperty("class", "label")
87+
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
88+
self._widget_container_layout.addWidget(label)
89+
widgets.append(label)
90+
if is_alt:
91+
label.setProperty("class", "label alt")
92+
label.hide()
93+
else:
94+
label.show()
95+
return widgets
96+
self._widgets = process_content(content)
97+
self._widgets_alt = process_content(content_alt, is_alt=True)
98+
99+
100+
def _update_label(self):
101+
active_widgets = self._widgets_alt if self._show_alt_label else self._widgets
102+
active_label_content = self._label_alt_content if self._show_alt_label else self._label_content
103+
label_parts = re.split('(<span.*?>.*?</span>)', active_label_content)
104+
label_parts = [part for part in label_parts if part]
105+
widget_index = 0
106+
try:
107+
lang = self._get_current_keyboard_language()
108+
except:
109+
lang = None
110+
111+
for part in label_parts:
112+
part = part.strip()
113+
if part and widget_index < len(active_widgets) and isinstance(active_widgets[widget_index], QLabel):
114+
if '<span' in part and '</span>' in part:
115+
# Ensure the icon is correctly set
116+
icon = re.sub(r'<span.*?>|</span>', '', part).strip()
117+
active_widgets[widget_index].setText(icon)
118+
else:
119+
# Update label with formatted content
120+
formatted_text = part.format(lang=lang) if lang else part
121+
active_widgets[widget_index].setText(formatted_text)
122+
widget_index += 1
123+
124+
125+
# Get the current keyboard layout
126+
def _get_current_keyboard_language(self):
127+
# Get the foreground window (the active window)
128+
hwnd = user32.GetForegroundWindow()
129+
# Get the thread id of the foreground window
130+
thread_id = user32.GetWindowThreadProcessId(hwnd, None)
131+
# Get the keyboard layout for the thread
132+
hkl = user32.GetKeyboardLayout(thread_id)
133+
# Get the language identifier from the HKL
134+
lang_id = hkl & 0xFFFF
135+
# Buffers for the language and country names
136+
lang_name = ctypes.create_unicode_buffer(LOCALE_NAME_MAX_LENGTH)
137+
country_name = ctypes.create_unicode_buffer(LOCALE_NAME_MAX_LENGTH)
138+
full_lang_name = ctypes.create_unicode_buffer(LOCALE_NAME_MAX_LENGTH)
139+
full_country_name = ctypes.create_unicode_buffer(LOCALE_NAME_MAX_LENGTH)
140+
# Get the ISO language name
141+
kernel32.GetLocaleInfoW(lang_id, LOCALE_SISO639LANGNAME, lang_name, LOCALE_NAME_MAX_LENGTH)
142+
# Get the ISO country name
143+
kernel32.GetLocaleInfoW(lang_id, LOCALE_SISO3166CTRYNAME, country_name, LOCALE_NAME_MAX_LENGTH)
144+
# Get the full language name
145+
kernel32.GetLocaleInfoW(lang_id, LOCALE_SLANGUAGE, full_lang_name, LOCALE_NAME_MAX_LENGTH)
146+
# Get the full country name
147+
kernel32.GetLocaleInfoW(lang_id, LOCALE_SCOUNTRY, full_country_name, LOCALE_NAME_MAX_LENGTH)
148+
149+
short_code = f"{lang_name.value}-{country_name.value}"
150+
full_name = f"{full_lang_name.value}"
151+
return {'short': short_code, 'full': full_name}
152+

src/styles.css

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
.power-menu-popup > .button > .label {} -> Styles for power buttons icons and labels inside the popup
1717
.media-widget {} -> Styles specific to the media widget
1818
.github-widget {} -> Styles specific to the github widget
19+
.language-widget {} -> Styles specific to the language widget
1920
*/
2021
* {
2122
font-size: 12px;

0 commit comments

Comments
 (0)