
Несмотря на весь технический прогресс IT, мне за всё время так и не удалось повстречать убедительное решение проблемы ввода «ghbdtn» вместо «п��ивет» или «lf» вместо «да» — путаницы с раскладкой клавиатуры при наборе текста.
Какие решения мне знакомы:
стандартный системный индикатор на панели — он малозаметен, особенно на больших мониторах; его использование требует отдельный навык дисциплины — перед каждым прикосновением к клавиатуре искать глазами крохотный индикатор где‑то в углу экрана
вариант использовать в роли индикатора светодиод клавиши Caps Lock кажется мне нагляднее, но всё равно требует движений головы и глаз; также не подходит, если раскладок в системе больше двух
специально предназначенные программы типа Punto Switcher давно испортили себе репутацию, посеяв глубокие сомнения в вопросе безопасности их использования
программы, которые отображают текущую раскладку прямо около курсора ввода текста на экране — звучит здорово, но такое я находил только под Windows
Поэтому предлагаю свой вариант — менять в зависимости от раскладки цвет всей подсветки клавиатуры. С таким подходом куда бы вы ни смотрели перед компьютером, подсветка будет хорошо заметна периферийным зрением, и вы всегда будете знать какая раскладка выбрана.
Я опишу реализацию решения для среды рабочего стола GNOME, проверенное на дистрибутивах Fedora 43 и Ubuntu 24.04.
Нам потребуется три вещи:
узнать какие байты нужно отправлять клавиатуре для изменения подсветки
научиться слушать системные события смены раскладки
начать нужным образом реагировать на эти события
Примером клавиатуры послужит относительно популярная бюджетная механическая Redragon Anubis (K539-RGB), подключённая по USB‑кабелю. Почему именно по USB: ПО для настройки этой модели умеет работать только с проводным подключением, а мы будем использовать команды для смены подсветки именно от этого ПО.
Важное примечание: я не рекламирую эту клавиатуру — она здесь только потому, что имела неосторожность оказаться под рукой и поддерживать RGB‑подсветку, которой я нашёл своеобразное применение. Для недоверчивых скажу даже больше — я не рекомендую эту клавиатуру из‑за громкого (на мой взгляд) шума клавиш и скрипа некоторых кнопок. Возможно, этот недостаток поддаётся исправлению смазкой свитчей и дополнительной самодельной шумоизоляцией, но я не фанат такого хобби, из‑за чего моё общение с этой клавиатурой по итогу завершилось возвратом.
ЧАСТЬ I — Перехватываем байты
Первым делом нам надо научиться программно менять подсветку клавиатуры, например из Python‑скрипта.
Единства в отношении управления подсветкой у производителей клавиатур не наблюдается — каждый делает свои реализации. Некоторые устройства могут поддерживать OpenRGB или работать на прошивке QMK — здесь можно ожидать какое-то универсальное решение. Но это не наш случай, поэтому надо узнать конкретную последовательность байтов для отправки USB‑контроллеру устройства.
Эту информацию можно поискать в сети — возможно, для вашей клавиатуры кто‑то уже узнал нужные байты. Рекомендую смотреть не только по названию и модели, но и по идентификаторам вендора и продукта — их можно получить командой lsusb, найдя в выводе своё устройство:
…
Bus 001 Device 009: ID 258a:0049 BY Tech Gaming Keyboard
…Так система видит клавиатуру Anubis при подключении через USB‑кабель. Значения 258a и 0049 — это и есть Vendor ID и Product ID соответственно. Они нам ещё понадобятся.
Для своей клавиатуры нужных данных в интернете я не нашёл, поэтому пришлось добывать их самостоятельно. Идея проста: при помощи ПО от производителя для настройки клавиатуры меняем цвет подсветки, перехватывая отправляемый на устройство USB‑трафик при помощи Wireshark. Софт для клавиатуры доступен только под Windows, поэтому пришлось обзавестись соответствующей виртуальной машиной. Я использовал Virtual Machine Manager, но в данном случае выбор не принципиален, разве что не забудьте позаботиться о видимости USB‑устройства в конфигурации виртуальной машины.
Итак, устанавливаем на виртуалке софт для настройки клавиатуры с официального сайта производителя: раздел «Скачать» внизу страницы — «Драйвер для Redragon Anubis 70505, 70506». Также ставим Wireshark — не забудьте выбрать для дополнительной установки модуль USBPcap, при помощи которого можно прослушивать USB‑трафик. Процесс установки и запуска Wireshark можно посмотреть, например, в этом видео.
Спустя пару дней ковыряния трафика клавиатуры экспериментальным путём выяснилось, что Anubis для смены подсветки почему-то ожидает не одну команду, а сразу две — друг за другом, размером 1032 байт каждая.
Прослушивание USB-трафика в Wireshark (GIF 2.2 MB)

Структура первого пакета (без заполняющих нулевых байтов)
06 08 b8 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 __ __ __ 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff 00 ff 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff
Здесь метками __ __ __ обозначено место для RGB‑значений подсветки, например ff 00 00 для красного цвета. За что отвечают остальные байты мне неизвестно. Технически клавиатура умеет менять цвет каждой клавиши отдельно, но мне не удалось понять как это сделать.
Структура второго пакета
06 03 b6 00 00 00 00 00 00 00 00 00 00 00 5a a5 03 03 00 00 00 01 20 01 00 00 00 00 55 55 01 00 00 00 00 00 ff ff 00 __ 07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 00 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 5a a5 00 10 07 44 07 44 07 44 07 44 07 44 07 44 07 44 04 04 04 04 04 04 04 04 04 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 5a a5 03 03
Тут вместо __ ожидается значение уровня яркости подсветки:
30→ подсветка отключена31→ уровень 132→ уровень 233→ уровень 334→ уровень 4, максимальная яркость
Почему для уровней выбраны такие «неровные» значения, и за что отвечают оставшиеся во втором пакете байты мне тоже неизвестно. Но для наших целей полученных данных достаточно, поэтому движемся дальше.
ЧАСТЬ II — Учимся командовать
Попробуем отправлять найденные байты в клавиатуру из простого Python‑скрипта. Нам потребуется предварительная подготовка.
Настраиваем доступ к устройству
По умолчанию в Linux USB‑клавиатура появляется как устройство, которое принадлежит root и требует для работы соответствующие системные привилегии. Так как мы хотим запускать свой Python‑скрипт от имени обычного пользователя, нам понадобится добавить udev‑правило (udev — userspace device). Для этого создадим файл:
sudo nano /etc/udev/rules.d/99-keyboard.rulesИ добавим в него:
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="258a", ATTRS{idProduct}=="0049", MODE="0666"Здесь мы:
применяем правило к устройствам типа
hidraw, чем USB‑клавиатура и являетсяуточняем конкретные Vendor ID и Product ID нашей клавиатуры
выставляем права
rw-rw-rw-, разрешая управлять HID‑устройством без root
Применяем правило к системе и подключённым устройствам:
sudo udevadm control --reload-rules
sudo udevadm triggerГотовим среду для работы Python-скрипта
Для получения Python и создания виртуального окружения для проекта я рекомендую использовать менеджер версий pyenv — с его помощью можно ставить нужные версии Python рядом с системной и управлять виртуальными окружениями, не трогая глобальное.
Для подготовки pyenv нужно выполнить шаги A−D из инструкции по установке. На шаге A рекомендую использовать вариант «Automatic installer» — для его работы необходимы установленные curl и git.
В дополнение шага B советую выполнить действие «Add pyenv virtualenv‑init to your shell» из инструкции по установке pyenv‑virtualenv — это позволит наглядно видеть активированное окружение в терминале. Отдельно ставить pyenv‑virtualenv не нужно — он уже входит в состав установки pyenv при использовании «Automatic installer».
Я проверял работу своего решения на Python версии 3.13.9, установить которую можно командой:
pyenv install 3.13.9Теперь создадим отдельное виртуальное окружение для нашего проекта:
pyenv virtualenv 3.13.9 anubeam-3.13.9Как можно догадаться, здесь:
3.13.9— версия Python, которую мы будем использовать в новом виртуальном окруженииanubeam-3.13.9— название создаваемого окружения; если оно будет совпадать со строкой из файла.python-versionв корне проекта, pyenv автоматически активирует указанное окружение в терминале при входе в директорию проекта — чудовищно удобно
Создайте в удобном месте директорию для Python‑скрипта. У меня это будет ~/anubeam. Не откажите себе в удовольствии поместить туда тот самый файл .python-version с названием созданного виртуального окружения: anubeam-3.13.9. Зайдите в терминале в эту директорию — по изменению приглашения командной строки будет видно, что окружение активировалось. Дополнительно убедиться в этом можно, воспользовавшись командой pip list, которая покажет список установленных пакетов в текущем виртуальном окружении:
(anubeam-3.13.9) eshfield@fedora:~/anubeam$ pip list
Package Version
------- -------
pip 25.2Одинокий pip — признак чистого окружения. В глобальном мы бы увидели кучу установленных системой зависимостей.
Для отправки байтов в клавиатуру нам понадобится мультиплатформенная библиотека hid, которая даёт возможность взаимодействовать с USB‑устройствами. Добавим её в наш�� окружение:
(anubeam-3.13.9) eshfield@fedora:~/anubeam$ pip install hidДля работы пакета требуется библиотека hidapi. В Fedora 43 она уже установлена по умолчанию, а в Ubuntu 24.04 её надо поставить вручную:
sudo apt install libhidapi-hidraw0 -yПроводим пробные стрельбы
Создадим файл keyboard_controller.py, в котором опишем класс для работы с клавиатурой:
Содержимое файла keyboard_controller.py
from logging import Logger
import hid
from hid import HIDException
VENDOR_ID = 0x258a
PRODUCT_ID = 0x0049
USAGE_PAGE = 65280
PACKET_LENGTH = 1032
INTENSITY = b'\x31' # 30 → 0, 31 → 1, 32 → 2 etc.
class KeyboardController:
def __init__(self, logger: Logger):
self.device = None
self.logger = logger
def connect(self) -> bool:
for d in hid.enumerate(VENDOR_ID, PRODUCT_ID):
if d.get("usage_page") == USAGE_PAGE:
try:
self.device = hid.Device(path=d.get("path"))
except HIDException as e:
self.logger.error(e)
return False
break
if self.device is None:
self.logger.error("Keyboard not found")
return False
self.logger.info(f"Connected to {self.device.manufacturer} — {self.device.product}")
return True
def change_color(self, color: str) -> None:
packet1 = bytearray()
packet1.extend(bytes.fromhex(
"06 08 b8 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"))
packet1.extend(bytes.fromhex(color))
packet1.extend(bytes.fromhex(
"00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff "
"00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff "
"ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff "
"00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 "
"ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff "
"00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 "
"ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff "
"ff ff ff ff 00 ff 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 "
"00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 "
"ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff "
"ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 "
"ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff "
"00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 "
"00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff "
"ff 00 ff 00 ff 00 ff ff ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff "
"ff ff ff ff 00 00 00 00 ff 00 ff 00 ff ff 00 ff 00 ff 00 ff ff ff ff ff"))
packet2 = bytearray()
packet2.extend(bytes.fromhex(
"06 03 b6 00 00 00 00 00 00 00 00 00 00 00 5a a5 03 03 00 00 00 01 20 01 00 00 00 00 55 "
"55 01 00 00 00 00 00 ff ff 00"))
packet2.extend(INTENSITY)
packet2.extend(bytes.fromhex(
"07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 07 33 00 33 07 33 07 33 07 33 07 33 07 "
"33 07 33 07 33 07 33 07 33 5a a5 00 10 07 44 07 44 07 44 07 44 07 44 07 44 07 44 04 04 "
"04 04 04 04 04 04 04 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
"00 00 00 00 00 00 00 00 00 00 5a a5 03 03"))
try:
self.device.send_feature_report(_pad_packet(packet1))
self.device.send_feature_report(_pad_packet(packet2))
except Exception as e:
self.logger.error("Error sending packet:", e)
def close(self) -> None:
self.device.close()
def _pad_packet(data: bytes, length: int = PACKET_LENGTH) -> bytes:
result = bytearray(data)
if len(data) < length:
zeroes = b'\x00' * (length - len(data))
result.extend(zeroes)
return bytes(result)
Здесь:
VENDOR_IDиPRODUCT_ID— знакомые нам идентификаторы клавиатурыметод
connectищет нужный физический интерфейс устройства. Фильтр поUSAGE_PAGE=65280выбирает vendor‑defined интерфейс, который обычно отвечает за подсветку клавиатуры — так определяется нужный для подключенияpath, через который будут отправляться команды на управление. Посмотреть все доступные интерфейсы можно, выполнив файлlist_devices.py:
Содержимое файла list_devices.py
import hid
VENDOR_ID = 0x258a
PRODUCT_ID = 0x0049
def main():
for device in hid.enumerate(VENDOR_ID, PRODUCT_ID):
for k, v in device.items():
print(f"{k}: {v}")
print("\n" + "-" * 40 + "\n")
if __name__ == "__main__":
main()
Нужный интерфейс выглядит так:
path: b'/dev/hidraw2'
vendor_id: 9610
product_id: 73
serial_number:
release_number: 258
manufacturer_string: BY Tech
product_string: Gaming Keyboard
usage_page: 65280
usage: 1
interface_number: 1
bus_type: BusType.USBИнтерфейсы могут повторяться — нам подойдёт первый с нужным usage_page, так как нам в итоге важно получить path, который для повторных интерфейсов будет одинаков.
Далее:
метод
change_colorсобирает и отправляет два пакета для установки цвета и яркости подсветкифункция
_pad_packetдополняет пакет нулями до фиксированной длины, которую ожидает устройство
Создаём файл test.py, где подключаемся к клавиатуре и для проверки меняем цвет подсветки:
Содержимое файла test.py
import logging
from keyboard_controller import KeyboardController
RED = "FF0000"
logger = logging.getLogger("anubeam")
def main():
keyboard = KeyboardController(logger)
result = keyboard.connect()
if not result:
logger.error("Keyboard connection failure")
return
keyboard.change_color(RED)
if __name__ == "__main__":
main()
Запуск Python‑скрипта должен озарить нашу клавиатуру красным:
(anubeam-3.13.9) eshfield@fedora:~/anubeam$ python test.pyОтлично! Мы научились программно управлять подсветкой. Казалось бы, дело в шляпе — осталось только начать как‑то ловить системные события смены раскладки, чтобы реагировать на них отправкой нужных команд.
Однако мне не удалось найти ни одного актуального прямого способа из скрипта узнать о смене раскладки в системе. Похоже, об этом событии знает только среда рабочего стола — GNOME, и наружу эти данные не транслируются.
Здесь нам поможет D‑Bus (Desktop Bus) — стандартный механизм обмена сообщениями между приложениями и системными сервисами в Linux‑средах рабочего стола. Мы напишем своё расширение для GNOME, которое будет передавать события смены раскладки в кастомный D‑Bus интерфейс, откуда мы в Python‑скрипте без труда сможем эти данные слушать и реагировать на них.
ЧАСТЬ III — Пишем GNOME-расширение
Если вы не знали, основной язык логики и пользовательского интерфейса графической оболочки рабочего стола GNOME Shell — это JavaScript. Сама оболочка реализована как JS‑приложение, выполняющееся в среде GJS — JavaScript‑движке на базе SpiderMonkey (от Mozilla), который также используется в браузере Firefox.
Расширения для GNOME Shell также пишутся на JS. Наше — не исключение.
Создаём директорию под новое расширение:
mkdir -p ~/.local/share/gnome-shell/extensions/input-source-monitor@eshfield && cd $_Здесь:
~/.local/share/gnome-shell/extensions/— стандартное место для расширений уровня пользователя; возможно там у вас уже что‑то естьinput-source-monitor@eshfield— название расширения. Регламент требует две разделённые символом@части: собственно название и подконтрольное пространство имён, например, адрес email или сайта — для наших локальных целей можно ограничиться именем пользователя.конструкция
&& cd $_позволяет сразу же перейти в созданную директорию — параметр командной строки$_содержит последний аргумент предыдущей команды, то есть указанный путь
В директории расширения понадобится создать два обязательных файла:
Файл № 1: metadata.json
Тут указываем минимально необходимый набор полей с основной информацией о расширении:
Содержимое файла metadata.json
{
"uuid": "input-source-monitor@eshfield",
"name": "Keyboard Input Source Monitor",
"description": "Monitors input source changes and notifies external script via custom D-Bus interface",
"shell-version": ["46", "47", "48", "49"]
}Здесь:
поле идентификатора
uuidдолжно совпадать с полным названием расширения из созданной ранее директориив массиве
shell-versionуказываются поддерживаемые версии GNOME — в нашем случае они соответствуют диапазону от Ubuntu 24.04 (GNOME 46) до Fedora 43 (GNOME 49)
Подробнее о полях этого файла можно почитать в документации.
Файл № 2: extension.js
Здесь ожидается унаследованная от базового класса Extension реализация вида:
Шаблон класса расширения
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
export default class ExampleExtension extends Extension {
constructor(metadata) {
super(metadata);
// код инициализации
}
enable() {
// код включения
}
disable() {
// код отключения
}
}Подробнее про требования к содержимому этого файла — в документации.
Добавляем нашу реализацию:
Содержимое файла extension.js
import Gio from "gi://Gio";
import GLib from "gi://GLib";
import St from "gi://St";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js";
import * as Keyboard from "resource:///org/gnome/shell/ui/status/keyboard.js";
const ICON_NAME = "view-wrapped-symbolic";
const ICON_STYLE = "system-status-icon";
const DBUS_INTERFACE = `
<node>
<interface name="org.gnome.InputSourceMonitor">
<signal name="SourceChanged">
<arg type="s" name="source"/>
</signal>
</interface>
</node>
`;
const DBUS_NAME = "org.gnome.InputSourceMonitor";
const DBUS_PATH = "/org/gnome/InputSourceMonitor";
const DBUS_SIGNAL_NAME = "SourceChanged";
const MANAGER_SIGNAL_NAME = "current-source-changed";
export default class InputSourceMonitorExtension extends Extension {
constructor(metadata) {
super(metadata);
this._dbus = null;
this._indicator = null;
this._manager = Keyboard.getInputSourceManager();
this._ownerId = null;
this._signalId = null;
}
enable() {
// setup panel indicator
this._indicator = new PanelMenu.Button(0.0, this.metadata.name, false);
const icon = new St.Icon({
icon_name: ICON_NAME,
style_class: ICON_STYLE,
});
this._indicator.add_child(icon);
Main.panel.addToStatusArea(this.uuid, this._indicator);
// setup D-Bus interface
this._dbus = Gio.DBusExportedObject.wrapJSObject(
DBUS_INTERFACE,
this,
);
this._dbus.export(Gio.DBus.session, DBUS_PATH);
// reserve D-Bus name
this._ownerId = Gio.DBus.session.own_name(
DBUS_NAME,
Gio.BusNameOwnerFlags.NONE,
null,
null
);
// subscribe to input source changes
this._signalId = this._manager.connect(
MANAGER_SIGNAL_NAME,
() => {
const source = this._manager.currentSource;
this._dbus.emit_signal(
DBUS_SIGNAL_NAME,
GLib.Variant.new("(s)", [source.id])
);
},
);
}
disable() {
if (this._signalId) {
this._manager.disconnect(this._signalId);
this._signalId = null;
}
if (this._dbus) {
this._dbus.flush();
this._dbus.unexport();
this._dbus = null;
}
if (this._ownerId) {
Gio.DBus.session.unown_name(this._ownerId);
this._ownerId = null;
}
if (this._indicator) {
this._indicator.destroy();
this._indicator = null;
}
}
}
Здесь:
объявляем константы, среди которых
DBUS_INTERFACE, который содержит XML‑описание структуры создаваемого интерфейса D‑Bus — имя, сигнал и аргумент:
—type="s"— строковый тип (s— string)
—name="source"— имя аргумента, значением которого будет текущая раскладка:usилиruдалее в конструкторе инициируем нужные переменные
в методе
enable()указываем иконку для панели — я выбралview-wrapped-symbolic; при желании можете выбрать другую из директории/usr/share/icons/Adwaita/symbolic/status/или заморочиться добавлением своейсоздаём и регистрируем новый интерфейс D‑Bus по описанной в константе структуре
резервируем на D‑Bus уникальное имя, по которому внешние клиенты смогут найти и подключиться к нашему сервису
_manager— это объект, управляющий раскладками и источниками ввода клавиатуры; он знает текущую раскладку и при помощи методаconnect()даёт возможность подписаться на события её сменыemit_signal()отправляет в созданный интерфейс сообщения, передавая из переменнойsource.idстроковое значение текущей раскладки:usилиruв методе
disable()очищаем ресурсы
Осталось перезапустить GNOME — выйти из сессии и зайти заново. Теперь можно активировать расширение, выполнив команду (из любой директории):
gnome-extensions enable input-source-monitor@eshfieldРасширение будет автоматически запускаться со стартом системы. На верхней панели мы должны увидеть выбранную иконку:

Проверить корректность работы можно командой:
gdbus monitor --session --dest org.gnome.InputSourceMonitorПри смене раскладки будут видны соответствующие сообщения:

ЧАСТЬ IV — Собираем всё вместе
Теперь мы готовы написать основной скрипт, в котором будем реагировать на смену раскладки клавиатуры и включать гимн СССР при выборе кириллицы
Возвращаемся в директорию проекта ~/anubeam. Нам понадобится дополнительная Python‑зависимость в виртуальном окружении для работы с D‑Bus интерфейсами — dbus‑fast:
(anubeam-3.13.9) eshfield@fedora:~/anubeam$ pip install dbus-fastСоздаём основной файл main.py:
Содержимое файла main.py
import asyncio
import logging
import sys
from typing import Optional
from dbus_fast import DBusError
from dbus_fast.aio import MessageBus, ProxyInterface
from keyboard_controller import KeyboardController
START_TIMEOUT_SECONDS = 10
BUS_NAME = "org.gnome.InputSourceMonitor"
BUS_PATH = "/org/gnome/InputSourceMonitor"
RED = "FF0000"
WHITE = "FFFFFF"
COLORS = {
"us": WHITE,
"ru": RED,
}
logger = logging.getLogger("anubeam")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stdout,
)
async def wait_for_input_source_interface(bus: MessageBus) -> Optional[ProxyInterface]:
for i in range(1, START_TIMEOUT_SECONDS + 1):
try:
introspection = await bus.introspect(BUS_NAME, BUS_PATH)
proxy = bus.get_proxy_object(BUS_NAME, BUS_PATH, introspection)
logger.info("Connected to D-Bus")
return proxy.get_interface(BUS_NAME)
except DBusError:
logger.info(f"Try #{i}: DBus service is not ready, retrying")
await asyncio.sleep(1)
return None
async def main():
keyboard: Optional[KeyboardController] = None
bus: Optional[MessageBus] = None
try:
keyboard = KeyboardController(logger)
if not keyboard.connect():
logger.error("Keyboard connection failure")
return
bus = await MessageBus().connect()
interface = await wait_for_input_source_interface(bus)
if interface is None:
logger.error(f"D-Bus service is not available after {START_TIMEOUT_SECONDS} seconds")
return
last_source: dict[str, Optional[str]] = {"value": None}
def handle_source_change(source: str) -> None:
if source == last_source["value"]:
return
last_source["value"] = source
color = COLORS.get(source)
if not color:
logger.warning(f"Unexpected input source: {source}")
return
keyboard.change_color(color)
interface.on_source_changed(handle_source_change) # type: ignore
logger.info("Listening for input source changes. Press Ctrl+C to exit")
await asyncio.Future()
except DBusError as e:
logger.error(f"D-Bus error: {e}")
except asyncio.CancelledError:
logger.info("Shutdown requested")
except Exception as e:
logger.exception(f"Unexpected error: {e}")
finally:
if keyboard is not None:
keyboard.close()
if bus is not None:
bus.disconnect()
logger.info("Exit")
if __name__ == "__main__":
asyncio.run(main())
Здесь:
описаны значения цветов подсветки для раскладок: для
enя выбрал нейтрально белый цвет, дляru— идеологически красныйожидается готовность сервиса D‑Bus и его интерфейса, чтобы подписаться на события изменения раскладки: скрипт делает несколько попыток подключения в течение отведённого времени — это защита от падения при автозапуске, когда GNOME‑сессия и связанные сервисы D‑Bus ещё не полностью инициализированы системой при старте
при смене раскладки обработчик
handle_source_changeотправляет знакомую нам команду на смену цвета подсветки
Скрипт готов, осталось позаботиться о его автоматическом запуске при входе в систему. Для этого создадим файл .desktop в нужной директории:
mkdir -p ~/.config/autostart
nano ~/.config/autostart/anubeam.desktopСодержимое файла anubeam.desktop:
[Desktop Entry]
Type=Application
Name=Anubeam
Exec=systemd-cat -t anubeam <ABSOLUTE_PATH_TO_PYTHON_BIN> <ABSOLUTE_PATH_TO_MAIN_PY_FILE>
X-GNOME-Autostart-enabled=true
NoDisplay=false
Comment=Keyboard light controllerЗдесь:
TypeиName— стандартные обязательные поляExec— команда для выполнения: здесь мы будем запускать наш Python‑скриптsystemd-cat -t anubeam— это утилита, которая перенаправляетstdoutиstderrв журнал логированияjournal systemdс указанным тегом (-t anubeam), чтобы при необходимости иметь возможность посмотреть логи нашего скрипта командойjournalctl --user -t anubeam -f<ABSOLUTE_PATH_TO_PYTHON_BIN>— абсолютный путь к исполняемому файлу Python — можно узнать командойpyenv which python, выполненной из директории со скриптом<ABSOLUTE_PATH_TO_MAIN_PY_FILE>— абсолютный путь к главному файлу скрипта — можно собрать из вывода командыpwdтам же +/main.pyX-GNOME-Autostart-enabled=true— включает автозапуск в GNOMENoDisplay=false— так наш скрипт будет виден в настройках автозапуска, например, в приложении Tweaks (раздел Startup Applications)
Осталось ещё раз выйти из сессии GNOME и войти заново, чтобы получить готовое решение. Теперь смена раскладки будет сопровождаться соответствующей подсветкой клавиатуры, и вы всегда будете знать, какие символы набираете.
P. S.
Теперь от вас не скроются неожиданные смены раскладки, которыми втихаря балуются некоторые приложения. Посмотрите, например, что происходит с русской раскладкой при нажатии правой кнопкой мыши по сообщению в чате Telegram Desktop.
