
Несмотря на весь технический прогресс 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.
Комментарии (47)

alexzen
24.12.2025 15:10Для Win есть же Caramba Switcher и Punto Switcher (который после покупки Яндексом собирает кучу аналитики). У Linux тоже своих решений хватает. Цвета, конечно, это красиво, но все-таки практичнее автоматика.

eshfield Автор
24.12.2025 15:10Кстати, в таких программах как-то обрабатывается сценарий ввода пароля? Может ли раскладка при вводе английских символов переключиться на русский, тем самым сломав ввод? Что при плохом раскладе может привести к блокировке за неудачные попытки входа, например в интернет-банкинг

Roman_Cherkasov
24.12.2025 15:10Когда я пользовался - ни как не обрабатывался, из-за чего я долго не мог понять почему я не могу ввести пароль на роутере с телефона, который сам же только что и создал, написав его дважды.
У меня кстати довольно длительное время, была проблема подобная вашей. Но она решилась тем что я начал просто смотреть на экран (видимо я освоил слепую печать пока играл в MOBA). Так я стал быстро понимать, что я не в той раскладке. Но переключаться было все ещё не удобно. А потом кто-то посоветовал поставить смену раскладки на Caps. После этого надобность в автосвитчерах отпала.

nixtonixto
24.12.2025 15:10В Punto можно временно (до пробела) запретить переключение раскладки, если перед этим были нажаты (выбирается чекбоксами в настройках) стрелки, Backspace, Delete или пользователь руками переключил раскладку. Так же можно первые символы пароля (или вообще весь, если от его утечки ничего не ухудшится) добавить в список автозамены и тогда программа будет принудительно переключать раскладку на правильную и снимать Caps Lock если он включён.

BigBrother
24.12.2025 15:10Я пользую Punto. Но не на автомате. Довольно часто у него на коротких словах и сокращениях случались ложные срабатывания. Пользуюсь только ручным исправлением, когда часть текста уже набрал в неправильной раскладке.
Предложенный в статье вариант интересен. Жаль, что у меня клавиатура без подсветки и жаль, что у меня Windows.
AquariusStar
24.12.2025 15:10У меня наоборот. Не всегда срабатывает. Правда, только для одного механизма, переключение раскладки рус/лат. Повесил их на левый и правый ctrl. Остальное отключил. Отрабатывают механизм, как у советских клавиатур с отдельными клавишами смены раскладки. Штука удобная. В Linux что-то похожее есть, но полностью отключается механизм удержания Ctrl для комбинации.

astenix
24.12.2025 15:10Меня как-то программисты хотели избить — в веб-форме нашего сайта при переходе на следующее поле через Tab у меня ВНЕЗАПНО и постоянно перезагружалась страница и все данные из полей терялись. А у программистов баг не воспроизводился.
Я пошел проверять этот баг на каждом компьютере на этаже, даже у бухгалтеров, и где-то он непредсказуемо воспроизводился, где-то нет. Маразм!
Причиной бага был тогдашний Punto Switcher, который у кого-то был установлен, у кого-то нет… как молоды мы были, мда.

Uolis
24.12.2025 15:10Гениально! Почему я сам не подумал о таком применении подсветки? Сделаю себе под КДЕ.

AVX
24.12.2025 15:10В кедах не надо ничего делать, там это штатно в настройках включается уже много лет. А гномоюзеры продолжают жрать кактус)
Я всегда так настраивал, вместе с переключением раскладки по правому контролу, но тут дело вкуса. Правда, использовал индикатор scroll lock, потому что он нафиг не нужен мне для чего-то ещë. Правда, в ноутбуках придëтся всë же на капс настраивать, там обычно нет лишних индикаторов. Правда, на ноутбуке и клава рядом с экраном (если конечно не используется внешний экран вместо ноутбучного).

eshfield Автор
24.12.2025 15:10там это штатно в настройках включается уже много лет
Вы уверены, что речь идёт именно об управлении цветом подсветки клавиатуры?

AVX
24.12.2025 15:10Ох, каюсь, пинайте меня семеро.
Не понял сразу, что тут про подсветку, а не индикатор. Если так, то ещë лучше решение, вообще смотреть не надо, фон подсветки краем глаза увидишь. Было бы круто это внести как штатную функцию (хоть в кеды, хоть в гном, правда, гном от этого я не стану любить).

BigBrother
24.12.2025 15:10Для меня идеалом был бы не слишком яркий светодиод, размещаемый где-то под монитором. Я чаще печатаю на русском: значит при Ru раскладке он должен быть в положении off, А на английской раскладке должен гореть.

eshfield Автор
24.12.2025 15:10Звучит как вполне посильная задача: подключить коротким шнуром к USB-порту (который может уже быть там же, на мониторе) простой контроллер со светодиодом. Должно быть несложно для знакомых с пайкой ребят.
Программную часть можно адаптировать из статьи, в том числе и под Windows — думаю там будет намного проще подписаться на события смены раскладки, без дополнительных прослоек в виде расширения среды рабочего стола.

Arhammon
24.12.2025 15:10Должно быть несложно для знакомых с пайкой ребят.
"Ардуино" индустрия позаботилась что давно ничего паять не надо - новая на Ренесасе имеет встроенную матрицу(адресных??) диодов. Есть Вавешаровская ЕСП с встроенным экраном, можно им светить причем без проводов... И скорее всего дофигища других вариантов.

garm
24.12.2025 15:10Идеальный вариант — это педаль.
Педаль нажата — одна раскладка, педаль отпущена — другая. Об этом ещё Джеф Раскин писал, в своей книге про интерфейс.

garrystalin
24.12.2025 15:10Это что, ещё и на клавиатуру смотреть что-ли надо?

eshfield Автор
24.12.2025 15:10Нет, не надо — цвет подсветки хорошо заметен периферийным зрением даже на минимальном значении яркости (для этой клавиатуры).
По сути, при использовании двух раскладок (en и ru), вам нужно контролировать лишь один цвет. Например, для меня раскладка по умолчанию это en, а при активации ru загорается заметный красный цвет — именно он и ловится вниманием.
Ну, это, конечно, справедливо, если вы клавиатуру перед монитором на столе держите, а не, например, на коленках. В последнем случае, правда, будут вопросы по эргономике.

virst
24.12.2025 15:10Я во время печати смотрю на вводимый текст, поэтому могу не заметить цвет клавиатуры. Но уже привык ориентироваться по буквам которые отображаются на экране.

eshfield Автор
24.12.2025 15:10Обеими руками одобряю и поддерживаю слепой ввод текста десятипальцевым методом. Тем не менее, когда вы смотрите на монитор, клавиатура вполне себе остаётся в зоне периферийного зрения.
Кстати, говорят, что у женщин оно развито лучше. Но мне и с моей мужской версией не потребовалось дополнительных усилий для считывания цвета текущей подсветки без специального взгляда на клавиатуру.

Cdr80
24.12.2025 15:10В идеале, чтобы подсвечивались только буквы текущей раскладки.
Можно соответствующими цветами закрасить буквы ещё попробовать. Например, синим одну раскаладку а красным другую.

feelamee
24.12.2025 15:10Мне кажется у вас подход неэффективый.
Как по мне, если вам нужно подумать и принять решение о переключении, то это уже медленно + лень думать каждый раз.
Я использую разные клавиши для переключение между раскладками - на английский LAlt, русский RAlt. Заметил как вошло в привычку нажимать соответствующую перед началом ввода. Думать не нужно + эти клавиши очень близко к толстым пальцам обеих рук

alcotel
24.12.2025 15:10Удобно. Жалко, что вендоры не договорились, как управлять подсветкой.
В /dev/hidraw можно, кстати, писать напрямую, не обвешиваясь библиотеками. Хоть через python write, хоть через bash echo. И вроде даже через bluetooth тот же интерфейс должен работать, но конкретно это я не пробовал.

eshfield Автор
24.12.2025 15:10Благодарю за комментарий
Я, кстати, пробовал отправлять те же пакеты в устройство 2.4 GHz ресивера при подключении клавиатуры через USB-донгл — сигналы остались без внимания. По Bluetooth не пытался.

alcotel
24.12.2025 15:10А USB-донгл двунаправленный? В смысле, через него родной софт может управлять подсветкой? Ну и он для этой клавиатуры не bluetooth, вроде.

eshfield Автор
24.12.2025 15:10В смысле, через него родной софт может управлять подсветкой?
Родной софт видит клавиатуру только при проводном подключении. При подключении через USB-донгл устройство софтом не определяется. Так что, видимо, тут система однонаправленная. По Bluetooth после этого я даже не пытался.

garm
24.12.2025 15:10Любопытная статья. Надо будет попробовать. У меня как раз гном. И клавиатура на qmk.
А с раскладкой я когда-то пытался решать обратную задачу: менять раскладку автоматически из скрипта. Тоже пытался слать какие-то сообщения через dbus. Но ничего у меня не получилось.

funca
24.12.2025 15:10Вместо pyenv и pip, используйте uv: `uv run keyboard_controller.py` - больше ни чего не нужно. Список зависимостей и версию интерпретора лучше указать непосредственно в скрипте. uv сам создаст виртуальное окружение и установит все необходимое https://docs.astral.sh/uv/guides/scripts/#creating-a-python-script

svanichkin
24.12.2025 15:10а может кто то подскажет, можно ли системный курсор текста менать на свой? например цвет ему разный давать, синий например это английская раскладка... а красная например русская? или например если цвет нельзя, то хотя бы заменить курсор на E для английского и R для русского? Такое было в ZX Spectrum и это было довольно удобно

eshfield Автор
24.12.2025 15:10Идея здравая
Однако реализация под GNOME выглядит затруднительной:
1. Курсор ввода текста (caret) — это не отдельный системный объект (который можно менять на ходу), а часть отрисовки GTK-виджетов
2. Мне не удалось найти у GTK API для динамического изменения внешнего вида caret
3. Стилизация caret через CSS возможна только статически и применяется целиком на приложениеВыглядит так, что в GNOME такое архитектурно не предусмотрено и парочкой простых скриптов здесь не обойтись
Кажется, в KDE ситуация по этому вопросу не проще, но лучше уточнить у более опытных пользователей этой DE
svanichkin
24.12.2025 15:10получается надо патч в GTK/QT делать? а есть вариант сделать какой то оверлей поверх всех окон, и рисовать поверх?

eshfield Автор
24.12.2025 15:10Как будто думать стоит именно в эту сторону, но есть вероятность, что Wayland намеренно не позволит сделать такое: он запрещает инжектиться в процессы, вмешиваться в чужой рендер и рисовать поверх окон

jidckii
24.12.2025 15:10Автору огромный респект, это так гениально, и я так вдохновился, что за 2 дня навайбкордил тоже самое для KDE но только чуть замороченее, рисую прям флаг на клаве относительно лейаута.

флаг 
моно Код если что тут, думаю тоже статью напишу ))
https://github.com/jidckii/kolor-keyboard
MaxAkaAltmer
А не проще просто присоединить две клавы и на одной русский настроить, а на второй английский?
eshfield Автор
И набирать текст так
alcotel
Больше клавиатур - богу клавиатур!