— Поставил RuSwitcher. Пользуюсь четвёртый месяц. Люто, бешено доставляет. Зависимостей никаких. Рекомендую.

— НЕ СМЕШНО У МЕНЯ БРАТ УМЕР ОТ ЭТИХ ЗАВИСИМОСТЕЙ ТЫ ВРЁШЬ ЧТО ИХ НЕТ!!1

Если серьёзно — зависимостей у RuSwitcher действительно ноль: только системные фреймворки и чистый Swift, никакой телеметрии и ничего постороннего в Package.swift. Но начнём с боли.

Если вы пишете на двух языках, то знаете эту боль: набрал полстроки, поднял глаза — а там ghbdtn вместо «привет». На Windows эту проблему закрывает Punto Switcher. А на macOS? Его Mac‑версию Яндекс забросил ещё в 2017-м, да и у самого Punto хватает «сюрпризов»: встроенный кейлоггер‑«дневник», телеметрия, навязывание Яндекс‑сервисов и закрытый код. Мне хотелось простого, открытого и без слежки — поэтому я написал своё: RuSwitcher, лёгкий переключатель раскладки в меню‑баре. Open source (MIT), ноль зависимостей, ноль телеметрии.

В статье — как это устроено внутри: перехват клавиатуры через CGEventTap, динамический маппинг любых двух раскладок через UCKeyTranslate, и отдельно — раздел «грабли», включая историю про то, как я случайно выложил релиз, где DMG назывался 2.1.0, а внутри лежала сборка 2.0.3.

Чем не угодил Punto Switcher

Сразу оговорюсь: Punto тоже бесплатный, так что дело не в деньгах. ЭТО ДРУГОЕ;)

  • На macOS его фактически нет. Яндекс заморозил разработку Mac‑версии ещё в ноябре 2017-го, а последняя сборка вышла за ~15 месяцев до этого. То есть «Punto для Mac» — это заброшенный почти десять лет назад проект, без поддержки современных macOS.

  • Встроенный кейлоггер. В Punto есть «Дневник» — функция, которая пишет всё набранное с клавиатуры в файл. По умолчанию выключена и её можно запаролить — но по сути это штатный клавиатурный шпион внутри переключателя раскладки.

  • Телеметрия. По многочисленным сообщениям, программа отправляет данные о пользователе и конфигурации на серверы Яндекса — в том числе если при установке отказаться от обновлений.

  • Навязывание Яндекса. Установщик (на Windows) предлагает Яндекс.Браузер, меняет домашнюю страницу и поиск по умолчанию, ставит Яндекс‑расширения — фактически «ходячая реклама».

  • Закрытый код. Что именно программа делает с вашими нажатиями — проверить нельзя в принципе.

  • Буфер обмена. Пользователи годами жалуются: слежение за буфером отваливается (например, после гибернации), автозамена конфликтует с расширенным буфером Windows (Win+V) и вставляет чужой текст, а в Telegram/WhatsApp — стирает или перемешивает набранное. (Лично у меня Punto тоже периодически терял буфер обмена — это и стало последней каплей.)

Честно: «Дневник» по умолчанию выключен, а бандл с браузером — беда Windows‑инсталлятора. Но сам подход — закрытый код + телеметрия + встроенный логгер набора — для инструмента, который видит каждое ваше нажатие, мне не нравится категорически.

Чего хотел я:

Punto Switcher (macOS)

RuSwitcher

Поддержка macOS

заморожена с 2017

активная, нативная

Исходный код

закрытый

открытый, MIT

Телеметрия

есть (по сообщениям)

нет

Кейлоггер‑«дневник»

есть (опционально)

нет

Навязывание ПО

да (Яндекс)

нет

Зависимости

ноль

Цена

бесплатно

бесплатно

RuSwitcher — только macOS и нативно, открытый код (можно прочитать каждую строчку), ноль телеметрии, ничего не навязывает, а буфер обмена аккуратно сохраняется и восстанавливается (в 2.1.1 это отдельно закалили — см. раздел про грабли).

Что умеет

  • Одиночное нажатие Alt → конвертирует последнее слово (или выделенный текст) в другую раскладку и переключает её.

  • Повторный Alt → откатывает конвертацию обратно.

  • Работает с любой парой установленных раскладок, не только ru/en.

  • Запоминает раскладку по приложению (per‑app layout memory).

  • Меню‑бар утилита, без окна, автозапуск, 12 языков интерфейса.

  • Никакой телеметрии.

Работает почти везде, запоминает раскладу приложения
Работает почти везде, запоминает раскладу приложения

А теперь интересное — как оно работает.

1. Перехват клавиатуры: CGEventTap

Чтобы реагировать на Alt и считать набранное слово, нужно видеть все нажатия системно. В macOS для этого есть CGEventTap. Я слушаю keyDown и flagsChanged (модификаторы), в режиме listenOnly — мы не блокируем и не меняем события, только наблюдаем:

let mask: CGEventMask =
    (1 << CGEventType.keyDown.rawValue) |
    (1 << CGEventType.flagsChanged.rawValue)

guard let tap = CGEvent.tapCreate(
    tap: .cghidEventTap,
    place: .tailAppendEventTap,
    options: .listenOnly,
    eventsOfInterest: mask,
    callback: keyboardCallback,
    userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
    // нет разрешения Input Monitoring
    return false
}

Колбэк — это C‑функция (event tap не принимает замыкания с захватом), поэтому контекст прокидывается через userInfo и распаковывается обратно в объект:

let monitor = Unmanaged<KeyboardMonitor>
    .fromOpaque(userInfo).takeUnretainedValue()

Важная деталь: event tap система может отключить при таймауте или перегрузке (tapDisabledByTimeout). Это надо ловить и включать заново, иначе приложение тихо «оглохнет»:

if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
    CGEvent.tapEnable(tap: monitor.eventTap!, enable: true)
    return Unmanaged.passRetained(event)
}

2. Динамический маппинг раскладок через UCKeyTranslate

Самое частое, что видишь в подобных проектах — захардкоженная таблица «й→q, ц→w…». Это работает ровно для ЙЦУКЕН↔QWERTY и ломается на всём остальном (немецкая, французская, армянская…).

Я пошёл другим путём: спрашиваю у самой системы, какой символ даёт keycode в конкретной раскладке. За это отвечает Carbon‑функция UCKeyTranslate. Берём данные раскладки (kTISPropertyUnicodeKeyLayoutData) и прогоняем keycode:

let result = layoutData.withUnsafeBytes { raw -> OSStatus in
    let ptr = raw.baseAddress!.assumingMemoryBound(to: UCKeyboardLayout.self)
    return UCKeyTranslate(
        ptr, keycode, UInt16(kUCKeyActionDown),
        modifierKeyState, UInt32(LMGetKbdType()),
        UInt32(kUCKeyTranslateNoDeadKeysMask),
        &deadKeyState, chars.count, &length, &chars
    )
}

Дальше строю карту символ_в_раскладке_A → символ_в_раскладке_B, перебирая keycodes 0–50 с шифтом и без. Карта кэшируется по ключу "layoutID_A→layoutID_B". Итог: конвертация работает между любыми двумя раскладками, которые есть в системе, без единой захардкоженной таблицы.

3. Как подменяется текст (и почему это компромисс)

Окей, слово определили и сконвертировали. Как заменить его в чужом приложении, к которому у нас нет доступа к тексту?

Честный ответ: через эмуляцию буфера обмена. Алгоритм:

  1. Shift+Left × N — выделить N последних символов;

  2. Cmd+C — скопировать;

  3. прочитать NSPasteboard, сконвертировать строку;

  4. Cmd+V — вставить обратно.

Звучит просто, но дьявол в деталях. Главная проблема — мы затираем буфер обмена пользователя. Поэтому до начала делается снапшот всех типов данных пастборда, а после — восстановление через 2 секунды (с защитой на случай выхода из приложения):

savedClipboardItems = snapshotPasteboard(pasteboard)
// ... конвертация ...
scheduleClipboardRestore()   // вернуть оригинал через 2с

И да — это не самый изящный подход, и я честно об этом скажу в разделе планов. Но именно буфер обмена работает практически везде, в отличие от прямой записи через Accessibility API, которую половина приложений (Electron, веб‑вью) не поддерживает.

4. Как не зациклиться на собственных событиях

Мы и слушаем клавиатуру, и сами генерим нажатия (Shift+Left, Cmd+C, Cmd+V). Если не отфильтровать свои события — получим бесконечный цикл. Решение: помечаем синтезированные события маркером в userData источника:

let kRuSwitcherEventMarker: Int64 = 0x52555300  // "RUS\0" в ASCII

source?.userData = kRuSwitcherEventMarker

// в колбэке:
if event.getIntegerValueField(.eventSourceUserData) == kRuSwitcherEventMarker {
    return Unmanaged.passRetained(event)   // это мы сами — игнор
}

5. Грабли macOS

Самое полезное — то, на чём набил шишки.

Разрешения. Нужны и Accessibility, и Input Monitoring — два разных переключателя в System Settings. Хуже: при обновлении приложения macOS может сбросить разрешения (если изменилась подпись). Поэтому я держу флаг «разрешения были выданы» и при их пропаже после апдейта показываю пользователю объяснение и сбрасываю старые записи через tccutil reset.

Терминалы. В Terminal.app и iTerm2 конвертация не работает: там Cmd+C — это не «копировать», а SIGINT. Честно вынес в known limitations.

Тайминги. Между симулированными нажатиями нужны микро‑паузы (usleep), иначе приложение не успевает обработать выделение/вставку. Это хрупко и зависит от приложения — отдельная головная боль.

Подпись и нотаризация. Без нотаризации Gatekeeper выдаёт «Apple не может проверить, что приложение не содержит вредоносного ПО». Сборка подписывается Developer ID, отправляется на нотаризацию (notarytool submit --wait) и стейплится (stapler staple).

6. Честная история про релиз, который сломался

Тут расскажу, как сам себя подставил — Хабр такое любит.

В какой‑то момент пользователь написал: «через Homebrew ставится старая версия, а в DMG с именем 2.1.0 внутри лежит 2.0.3». И он был прав.

Причина оказалась в скрипте сборки DMG. Имя файла бралось из version.json (источник правды), а само приложение скрипт не пересобирал — просто паковал тот RuSwitcher.app, что лежал рядом в рабочей папке. А там осталась старая сборка. В итоге: DMG называется 2.1.0, а бинарь внутри — 2.0.3. И что хуже — авто‑апдейтер сравнивал заявленную 2.1.0 с зашитой 2.0.3 и предлагал «обновиться» по кругу.

Починка — сделать пайплайн самопроверяющимся. Теперь скрипт DMG:

  • всегда пересобирает приложение перед упаковкой;

  • жёстко падает, если версия в бандле не совпадает с version.json;

  • сам вписывает финальный sha256 и версию в version.json и в Homebrew‑cask, чтобы манифест не расходился с артефактом.

BUNDLE_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
    "$APP_PATH/Contents/Info.plist")
if [ "$BUNDLE_VERSION" != "$VERSION" ]; then
    echo "ERROR: bundle is $BUNDLE_VERSION but version.json is $VERSION"
    exit 1   # отказываемся паковать DMG с расхождением версии
fi

Мораль: если у вас есть «единый источник правды», убедитесь, что в него действительно всё упирается, а не «обычно упирается».

7. Безопасность авто‑обновления

Приложение, которое перехватывает клавиатуру и умеет само себя заменять скачанным DMG — это лакомый вектор. Поэтому авто‑апдейт закалён:

  • sha256 проверки обязательны (нет хэша — откат на загрузку в браузере);

  • перед заменой проверяется подпись Developer ID с пиннингом Team ID:

let requirement =
  "anchor apple generic and certificate leaf[subject.OU] = \"9GEWCZ59HK\""
// codesign --verify --deep --strict -R=<requirement> <app>
  • сверяется bundle id и версия смонтированного приложения.

То есть даже если кто‑то подменит и DMG, и хэш — без валидной подписи моей команды установка не пройдёт.

8. Чего пока нет и куда двигаюсь

Главный технический долг — тот самый копи‑пастный движок конверсии. В планах — гибрид:

  • хранить буфер последних нажатий (keycodes) и конвертировать их напрямую, без чтения поля и без Cmd+C;

  • вставлять через CGEvent.keyboardSetUnicodeString, не трогая буфер обмена вообще;

  • Accessibility API для уже выделенного мышью текста;

  • старый копи‑паст оставить последним фолбэком.

  • Телеметрия, навязывание софта, реклама, кейлоггер — шутка для внимательных и дочитавших до конца))

Это уберёт бóльшую часть «граблей» из раздела 5 для основного сценария.

Итог

RuSwitcher — это ~3500 строк чистого Swift 6, только системные фреймворки (AppKit, Carbon, CoreGraphics, ServiceManagement), меню‑бар, без сэндбокса (нужен для event tap), без телеметрии. Ставится через Homebrew или DMG.

«Основные» с выбором раскладок и опциями
«Основные» с выбором раскладок и опциями

Буду рад фидбеку, issue и PR — проект живой, баг‑репорты реально читаются и чинятся.

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


  1. Franky4F
    22.06.2026 10:58

    @Rashns ghbdtn!
    Завидую людям, у которых хватает свободного времени и упорства чинить любимые игрушки до блеска. У меня обычно всё заканчивается на стадии «бесит, но потерплю». Снимаю панамку :)


    1. Rashns Автор
      22.06.2026 10:58

      Спасибо )))


  1. zxweed
    22.06.2026 10:58

    так оно не переключает раскладку само, надо обязательно посмотреть и нажать alt?


    1. Rashns Автор
      22.06.2026 10:58

      Да, меня автопереключение всегда бесило в пунте, оно реально в обиходе мешает (мне) поэтому его и нет в РуСвитчере. Принимая во внимание концепцию мультиязычности - проблемно будет столько словарей держать


    1. savostin
      22.06.2026 10:58

      Ну, или хотя бы звук какой, когда "неправильная" раскладка...


      1. Rashns Автор
        22.06.2026 10:58

        Да, хорошая идея - записал, автозамена на подходе


  1. zxweed
    22.06.2026 10:58

    а можно повесить исправление на ту же кнопку, что и смену раскладки (Capslock или Cmd-space)?


    1. Rashns Автор
      22.06.2026 10:58

      Да, в ближайших планах сделать выбор сочетания.


  1. stalinets
    22.06.2026 10:58

    А подскажите, есть ли для винды простейшая прогаммка, чтоб биндить базовые вещи на нестандартные клавиши? У меня сейчас стоит старенькая утилитка MKey, в ней я настроил переключение языка на CapsLock, а ещё уменьшение/увеличение системной громкости на один шаг на F9/F10. Очень удобно. Но эта утилитка уже заброшена. И у неё есть ряд проблем, например, при переключении раскладки через неё не всегда меняется отображаемая раскладка в трее (написано EN, набирается кириллицей), или в некоторых текстбоксах переключение не происходит (в LibreOffice всё работает, а при переименовании папки/файла в окне "Сохранить как..." нет).


    1. domage
      22.06.2026 10:58

      Попробуй Microsoft Power Toys: https://github.com/microsoft/powertoys
      Там есть целый ряд утилит со схожим функционалом.


    1. Rashns Автор
      22.06.2026 10:58

      Спасибо за идею - изучу, обязательно применим


  1. Groosha
    22.06.2026 10:58

    Если вы пишете на двух языках, то знаете эту боль: набрал полстроки, поднял глаза — а там ghbdtn вместо «привет». На Windows эту проблему закрывает Punto Switcher. 

    В любой ОС это закрывается тем, что смотрят на экран, а не на клавиатуру. Да, занимает какое-то время, но результат гораздо лучше, чем если пользоваться Punto и его аналогами.

    Мнение комментатора является исключительно субъективным.


    1. Rashns Автор
      22.06.2026 10:58

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


  1. Jury_78
    22.06.2026 10:58

    Неужели в такой полезной вещи все держится на энтузиастах...

    Вот и на linux был X Neural Switcher и тоже заброшен...


    1. Rashns Автор
      22.06.2026 10:58

      я в целом думал что-то универсальное написать - чтобы и винду и линухи поддерживало, но т.к. с первыми давно не сталкивался, а со вторыми только в терминале и на английском - выбрал путь свифта и только под мак


  1. kolabaister
    22.06.2026 10:58

    А если уже ввел пробел после слова не в той раскладке - можно ли вернуться и перевести?


    1. Rashns Автор
      22.06.2026 10:58

      по замыслу пробел отсекает и конверсия на предыдущее слово не распространится - в этом случае (если текста набрано много) можно выделить его мышкой и через Альт конвертнуть, я подумаю над вашим запросом как такие моменты ловить чтобы они не ломали общий принцип


      1. kolabaister
        22.06.2026 10:58

        Для меня это типичная боль - начать вслепую набирать что то, смотря вообще в другое место, уже набрать несколько слов, и обнаружить не ту раскладку.


        1. Rashns Автор
          22.06.2026 10:58

          выделили, жамкнули Альт - все кувыркнулось в нужную раскладку


          1. kolabaister
            22.06.2026 10:58

            Прерваться, найти мышку, выделить, жмякнуть, вернуться обратно... Лишнее действие.

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


            1. Rashns Автор
              22.06.2026 10:58

              Автозамена на подходе ;)


              1. kolabaister
                22.06.2026 10:58

                Спасибо!

                Еще одна идейка, может будет полезна - автопереключение в зависимости от приложений. Например в терминале в 99% процентах случаев нужен только английский (не затрагиваем тему агентов), тем более что там все равно конвертация не работает.


                1. Rashns Автор
                  22.06.2026 10:58

                  а это "закрыто" памятью раскладки в приложении, но можно сделать принудительное включение, для первого открытия


            1. zapimir
              22.06.2026 10:58

              Прерваться, найти мышку, выделить, жмякнуть, вернуться обратно

              Так можно клавишами выделить. Ctrl + Shift + ⇐ (стрелка влево) выделяет по словам в винде, в маке по идее должен быть аналог


  1. Daemonic
    22.06.2026 10:58

    fixlayout() {
        en="qwertyuiop\[]asdfghjkl;'\zxcvbnm,.QWERTYUIOP{}ASDFGHJKL:\"|ZXCVBNM<>\@№%%^&*"
        ru="йцукенгшщз\хъфывапролджэёячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖ\ЭЁЯЧСМИТЬБЮ\"#$:,.;"
        pbpaste | sed y=$en$ru=$ru$en= | pbcopy
    }

    Для минималистов