Создаём быстрый конвертер раскладки клавиатуры на Go для Linux

Создаём быстрый конвертер раскладки клавиатуры на Go для Linux

Каждый, кто работает с двумя раскладками клавиатуры, знаком с этой проблемой: набрал текст в неправильной раскладке. Вместо `привет` получилось `ghbdtn`, и приходится удалять и набирать заново. В Windows есть Punto Switcher, но в Linux хороших решений мало.

Я решил написать своё — простое, быстрое и надёжное.

Первая попытка: GNOME Shell расширение

Изначально я пытался создать расширение для GNOME Shell на TypeScript. Казалось логичным — нативная интеграция, доступ к системным API, горячие клавиши из коробки.

Но реальность оказалась суровой:

- Проблемы с компиляцией: TypeScript генерировал CommonJS вместо ES modules
- Конфликты импортов: `St` (Shell Toolkit) не загружался до инициализации
- GSettings схемы: нужна компиляция XML в бинарный формат
- Отладка: логи не попадали в `journalctl`, расширение молча падало в ERROR

После нескольких часов борьбы с `metadata.json`, `gschemas.compiled` и загадочными ошибками загрузки модулей, я решил: это слишком сложно для такой простой задачи.

Второй подход: bash-скрипт с xdotool

Решение стало очевидным — использовать стандартные утилиты Linux:

- xdotool — симуляция нажатий клавиш
- xclip — работа с буфером обмена
- Bash для склейки всего вместе

#!/bin/bash

# Копируем выделенный текст
xdotool key ctrl+c
sleep 0.3

# Получаем из буфера
text=$(xclip -o -selection clipboard)

# Конвертируем через case
result=""
while IFS= read -r -n1 char; do
    case "$char" in
        q) result+="й";;
        w) result+="ц";;
        # ... 100+ строк
    esac
done <<< "$text"

# Вставляем обратно
echo -n "$result" | xclip -selection clipboard
xdotool key ctrl+v

Проблемы:

  • Медленно: цикл `while read -n1` обрабатывает символы по одному
  • Сложный маппинг: 100+ строк `case` для всех символов
  • Задержки: `sleep 0.3` — костыль, чтобы текст успел попасть в буфер

Но главное — оно работало! Теперь оставалось только оптимизировать.

Третья попытка: Python

Python быстрее bash для обработки строк:

#!/usr/bin/env python3
import subprocess
import time

en_ru = {
    'q':'й','w':'ц','e':'у', # ...
}
ru_en = {v:k for k,v in en_ru.items()}

time.sleep(0.1)
subprocess.run(['xdotool', 'key', 'ctrl+c'])
time.sleep(0.4)

text = subprocess.run(['xclip', '-o', '-selection', 'clipboard'], 
                      capture_output=True, text=True).stdout

result = ''.join(en_ru.get(c, ru_en.get(c, c)) for c in text)

subprocess.run(['xclip', '-selection', 'clipboard'], input=result, text=True)
subprocess.run(['xdotool', 'key', 'ctrl+v'])

Плюсы:

  • Быстрее bash
  • Читаемый код
  • Простой маппинг через словарь

Минусы:

  • Всё ещё ~300-400ms задержка
  • Зависимость от Python runtime
  • Холодный старт интерпретатора

Финальное решение: Go

Go даёт нам скорость нативного кода и простоту высокоуровневого языка:

package main

import (
	"os/exec"
	"strings"
	"time"
)

var enRu = map[rune]rune{
	'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е',
	'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з',
	'[': 'х', ']': 'ъ', 'a': 'ф', 's': 'ы', 'd': 'в',
	'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л',
	'l': 'д', ';': 'ж', '\'': 'э', 'z': 'я', 'x': 'ч',
	'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь',
	',': 'б', '.': 'ю', '/': '.', '`': 'ё',
	// Заглавные буквы
	'Q': 'Й', 'W': 'Ц', 'E': 'У', // ...
}

var ruEn map[rune]rune

func init() {
	ruEn = make(map[rune]rune, len(enRu))
	for k, v := range enRu {
		ruEn[v] = k
	}
}

func main() {
	time.Sleep(80 * time.Millisecond)

	// Сохраняем старый буфер
	oldOut, _ := exec.Command("xclip", "-o", "-selection", "clipboard").Output()

	// Очищаем буфер
	clearCmd := exec.Command("xclip", "-selection", "clipboard")
	clearCmd.Stdin = strings.NewReader("")
	clearCmd.Run()

	// Копируем выделенное
	exec.Command("xdotool", "key", "ctrl+c").Run()
	time.Sleep(250 * time.Millisecond)

	// Получаем текст
	out, err := exec.Command("xclip", "-o", "-selection", "clipboard").Output()
	if err != nil || len(out) == 0 {
		// Восстанавливаем старый буфер
		restoreCmd := exec.Command("xclip", "-selection", "clipboard")
		restoreCmd.Stdin = strings.NewReader(string(oldOut))
		restoreCmd.Run()
		return
	}

	// Конвертируем (in-place для скорости)
	runes := []rune(string(out))
	for i, char := range runes {
		if mapped, ok := enRu[char]; ok {
			runes[i] = mapped
		} else if mapped, ok := ruEn[char]; ok {
			runes[i] = mapped
		}
	}

	// Вставляем
	setCmd := exec.Command("xclip", "-selection", "clipboard")
	setCmd.Stdin = strings.NewReader(string(runes))
	setCmd.Run()
	
	exec.Command("xdotool", "key", "ctrl+v").Run()
}

Оптимизации

1. In-place конвертация: изменяем массив рун напрямую, без создания промежуточных строк
2. Предварительная инициализация: обратный маппинг `ruEn` создаётся один раз в `init()`
3. Компиляция с оптимизацией: флаги `-ldflags="-s -w"` убирают отладочную информацию
4. Минимальные задержки: 80ms + 250ms = 330ms (оптимальный баланс скорости и надёжности)

Установка

# Установка зависимостей
sudo apt install xdotool xclip golang-go

# Сборка
mkdir -p ~/bin
cd ~/bin
# Скачайте layout-convert.go
go build -ldflags="-s -w" -o layout-convert layout-convert.go

# Настройка горячей клавиши в GNOME
gnome-control-center keyboard
# Добавьте Custom Shortcut:
# Command: /home/YOUR_USERNAME/bin/layout-convert
# Shortcut: Super+Space

Использование

  1. Выделите текст: `ghbdtn`
  2. Нажмите `Super+Space`
  3. Текст заменится: `привет`

Работает в обе стороны автоматически!

Подводные камни

Проблема 1: xdotool не копирует текст

Симптом: скрипт конвертирует старый буфер обмена вместо выделенного текста.

Решение: добавить задержку перед `ctrl+c` и очистку буфера:

time.Sleep(80 * time.Millisecond) // даём фокусу вернуться к окну
// Очищаем буфер
clearCmd := exec.Command("xclip", "-selection", "clipboard")
clearCmd.Stdin = strings.NewReader("")
clearCmd.Run()

Проблема 3: Менеджеры буфера обмена

Diodon и подобные могут конфликтовать. Убедитесь, что скрипт правильно очищает и восстанавливает буфер.

Выводы

Иногда простое решение — лучшее решение. Вместо борьбы с GNOME Shell API я получил:

- ✅ Кросс-платформенность (работает на любом DE с X11)
- ✅ Простоту отладки (один бинарник, никаких зависимостей)
- ✅ Высокую производительность (~330ms)
- ✅ Надёжность (не ломается при обновлении GNOME Shell)

Исходный код: github.com/semelyanov86/layout-converter

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js, Wordpress. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235