Несмотря на весь технический прогресс 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)
USB-рыбалка
USB-рыбалка
Структура первого пакета (без заполняющих нулевых байтов)

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 → уровень 1

  • 32 → уровень 2

  • 33 → уровень 3

  • 34 → уровень 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.py

  • X-GNOME-Autostart-enabled=true — включает автозапуск в GNOME

  • NoDisplay=false — так наш скрипт будет виден в настройках автозапуска, например, в приложении Tweaks (раздел Startup Applications)

Осталось ещё раз выйти из сессии GNOME и войти заново, чтобы получить готовое решение. Теперь смена раскладки будет сопровождаться соответствующей подсветкой клавиатуры, и вы всегда будете знать, какие символы набираете.

P. S.

Теперь от вас не скроются неожиданные смены раскладки, которыми втихаря балуются некоторые приложения. Посмотрите, например, что происходит с русской раскладкой при нажатии правой кнопкой мыши по сообщению в чате Telegram Desktop.

Репозиторий с кодом

Комментарии (47)


  1. MaxAkaAltmer
    24.12.2025 15:10

    А не проще просто присоединить две клавы и на одной русский настроить, а на второй английский?


    1. eshfield Автор
      24.12.2025 15:10

      И набирать текст так


    1. alcotel
      24.12.2025 15:10

      Больше клавиатур - богу клавиатур!


  1. alexzen
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

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


      1. Roman_Cherkasov
        24.12.2025 15:10

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


      1. nixtonixto
        24.12.2025 15:10

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


    1. BigBrother
      24.12.2025 15:10

      Я пользую Punto. Но не на автомате. Довольно часто у него на коротких словах и сокращениях случались ложные срабатывания. Пользуюсь только ручным исправлением, когда часть текста уже набрал в неправильной раскладке.
      Предложенный в статье вариант интересен. Жаль, что у меня клавиатура без подсветки и жаль, что у меня Windows.


      1. AquariusStar
        24.12.2025 15:10

        У меня наоборот. Не всегда срабатывает. Правда, только для одного механизма, переключение раскладки рус/лат. Повесил их на левый и правый ctrl. Остальное отключил. Отрабатывают механизм, как у советских клавиатур с отдельными клавишами смены раскладки. Штука удобная. В Linux что-то похожее есть, но полностью отключается механизм удержания Ctrl для комбинации.


      1. astenix
        24.12.2025 15:10

        Меня как-то программисты хотели избить — в веб-форме нашего сайта при переходе на следующее поле через Tab у меня ВНЕЗАПНО и постоянно перезагружалась страница и все данные из полей терялись. А у программистов баг не воспроизводился.

        Я пошел проверять этот баг на каждом компьютере на этаже, даже у бухгалтеров, и где-то он непредсказуемо воспроизводился, где-то нет. Маразм!

        Причиной бага был тогдашний Punto Switcher, который у кого-то был установлен, у кого-то нет… как молоды мы были, мда.


  1. Uolis
    24.12.2025 15:10

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


    1. AVX
      24.12.2025 15:10

      В кедах не надо ничего делать, там это штатно в настройках включается уже много лет. А гномоюзеры продолжают жрать кактус)

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


      1. eshfield Автор
        24.12.2025 15:10

        там это штатно в настройках включается уже много лет

        Вы уверены, что речь идёт именно об управлении цветом подсветки клавиатуры?


        1. AVX
          24.12.2025 15:10

          Ох, каюсь, пинайте меня семеро.

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


    1. jidckii
      24.12.2025 15:10

      1. Uolis
        24.12.2025 15:10

        Круто. :) Но я побоюсь использовать сгенерённое, слишком ответственная область, клавиатура это всё, пароли, и всё такое. А самому проверить пока времени нет.


        1. jidckii
          24.12.2025 15:10

          Ну также ИИ агентом проверь ))


  1. BigBrother
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

      Звучит как вполне посильная задача: подключить коротким шнуром к USB-порту (который может уже быть там же, на мониторе) простой контроллер со светодиодом. Должно быть несложно для знакомых с пайкой ребят.

      Программную часть можно адаптировать из статьи, в том числе и под Windows — думаю там будет намного проще подписаться на события смены раскладки, без дополнительных прослоек в виде расширения среды рабочего стола.


      1. Arhammon
        24.12.2025 15:10

        Должно быть несложно для знакомых с пайкой ребят.

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


    1. garm
      24.12.2025 15:10

      Идеальный вариант — это педаль.

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


  1. garrystalin
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

      Нет, не надо — цвет подсветки хорошо заметен периферийным зрением даже на минимальном значении яркости (для этой клавиатуры).

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

      Ну, это, конечно, справедливо, если вы клавиатуру перед монитором на столе держите, а не, например, на коленках. В последнем случае, правда, будут вопросы по эргономике.


  1. virst
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

      Обеими руками одобряю и поддерживаю слепой ввод текста десятипальцевым методом. Тем не менее, когда вы смотрите на монитор, клавиатура вполне себе остаётся в зоне периферийного зрения.

      Кстати, говорят, что у женщин оно развито лучше. Но мне и с моей мужской версией не потребовалось дополнительных усилий для считывания цвета текущей подсветки без специального взгляда на клавиатуру.


  1. Cdr80
    24.12.2025 15:10

    В идеале, чтобы подсвечивались только буквы текущей раскладки.

    Можно соответствующими цветами закрасить буквы ещё попробовать. Например, синим одну раскаладку а красным другую.


    1. eshfield Автор
      24.12.2025 15:10

      Отличная идея. Правда, реализовать такое можно разве что на уровне производителя клавиатур


      1. Kekero
        24.12.2025 15:10

        Есть клавиатуры со сменными кейкапами, можно на заказ напечатать только их и вставить светофильтры. Но это прям вариант для эстетов.


  1. feelamee
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

      Отличное превентивное решение, спасибо что поделились


  1. alcotel
    24.12.2025 15:10

    Удобно. Жалко, что вендоры не договорились, как управлять подсветкой.

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


    1. eshfield Автор
      24.12.2025 15:10

      Благодарю за комментарий

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


      1. alcotel
        24.12.2025 15:10

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


        1. eshfield Автор
          24.12.2025 15:10

          В смысле, через него родной софт может управлять подсветкой?

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


  1. frayvd7
    24.12.2025 15:10

    Ещё как идея менять цвет таскбара на винде или dock в линухе.


    1. eshfield Автор
      24.12.2025 15:10

      Бодрая мысль, спасибо


    1. 21224
      24.12.2025 15:10

      Так ещё под DOS раскладка переключалась с подсветкой периметра экрана.


      1. eshfield Автор
        24.12.2025 15:10

        Я начинал своё знакомство с компьютерами с Win 3.11. Моё почтение настоящим олдскулам IT :)

        Ну и, как мы видим, всё уже давно придумано до нас


  1. garm
    24.12.2025 15:10

    Любопытная статья. Надо будет попробовать. У меня как раз гном. И клавиатура на qmk.

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


  1. 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


  1. essamze
    24.12.2025 15:10

    Отличная работа! Моё уважение автору!


  1. svanichkin
    24.12.2025 15:10

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


    1. eshfield Автор
      24.12.2025 15:10

      Идея здравая

      Однако реализация под GNOME выглядит затруднительной:
      1. Курсор ввода текста (caret) — это не отдельный системный объект (который можно менять на ходу), а часть отрисовки GTK-виджетов
      2. Мне не удалось найти у GTK API для динамического изменения внешнего вида caret
      3. Стилизация caret через CSS возможна только статически и применяется целиком на приложение

      Выглядит так, что в GNOME такое архитектурно не предусмотрено и парочкой простых скриптов здесь не обойтись

      Кажется, в KDE ситуация по этому вопросу не проще, но лучше уточнить у более опытных пользователей этой DE


      1. svanichkin
        24.12.2025 15:10

        получается надо патч в GTK/QT делать? а есть вариант сделать какой то оверлей поверх всех окон, и рисовать поверх?


        1. eshfield Автор
          24.12.2025 15:10

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


  1. jidckii
    24.12.2025 15:10

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

    флаг
    флаг
    моно
    моно

    Код если что тут, думаю тоже статью напишу ))

    https://github.com/jidckii/kolor-keyboard


    1. eshfield Автор
      24.12.2025 15:10

      Впечатляющая работа, моё почтение!