Punto Switcher для Mac заморожен с ноября 2017 года. Я печатаю на русском и английском целый день, и «ghbdtn» вместо «привет» выскакивает у меня раз двадцать в сутки. В июне я решил, что хватит, и начал писать свой свитчер. Альтернативы есть, ниже я их честно перечислю. Мне хотелось двух вещей: исправление прямо во время набора, без горячей клавиши, и ноль сети. А ещё было интересно проверить на себе: вытянет ли ИИ-агент системное приложение для macOS, где нет фреймворка, который сделает за тебя сложную часть.
Получилось 184 коммита, 18 290 строк Swift в приложении и 12 592 в тестах. Три пользователя: я, друг и моя мама. Приложение бесплатное, код открыт под MIT. В этом тексте расскажу, как оно устроено, где я облажался и что из этого вынес про работу с агентом.
Что уже есть на рынке
Чтобы не было вопросов «а зачем, когда есть X»:
Caramba Switcher. От автора оригинального Punto. Классическая версия бесплатная, Pro за $4.99 в год. Работает на macOS 26 и Apple Silicon. Закрытый код.
Keyboop. Открытый код, исправляет автоматически, словарный. Появился в августе 2026.
RuSwitcher. Открытый код, 3,5 тысячи строк Swift, про него на Хабре уже была статья. Конвертирует последнее слово или выделение по нажатию Alt, сам раскладку не переключает.
Мой вариант отличается тем, что работает на двух уровнях сразу: мгновенное исправление, пока слово ещё набирается, и проверка на границе слова. Плюс он учится на ваших словах и не ходит в сеть вообще, кроме опциональной проверки обновлений.
Что умеет
Фоновое приложение в строке меню, без иконки в Dock. Следит за клавиатурой через CGEventTap, понимает, на каком языке вы хотели напечатать слово, стирает его и перепечатывает в нужной раскладке.

Горячие клавиши: одиночный Shift переключает раскладку, двойной Shift конвертирует последнее слово или выделение, левый плюс правый Shift включают и выключают автопереключение, Cmd+Option+Z отменяет последнее исправление. Есть профили по приложениям: в терминале или в игре автоисправление можно отключить, и есть пауза на 15, 60 или 120 минут.

Всё, что приложение узнаёт о вашем наборе, остаётся на Mac. Сеть используется только если вы сами включите проверку обновлений: раз в день скачивается один JSON с моего сайта, и ничего о вас не отправляется.
Как принимается решение о слове
Три уровня, по убыванию веса.
Первый: словарь. Объединённый список примерно из 697 тысяч английских и русских слов. Сначала отвечает фильтр Блума (FNV-1a, двойное хеширование), потом каждое попадание подтверждается точным бинарным поиском по отсортированному списку. Почему двухступенчато, расскажу ниже, это стоило мне трёх испорченных слов.
Второй: биграммы. У каждого языка есть сочетания букв, которые в нём невозможны. Если прочтение слова в текущей раскладке даёт такое сочетание, а прочтение в другой раскладке даёт чистое слово, это сильный сигнал.
Третий: частотность слова и контекст. Предыдущее слово тянет за собой язык следующего. Числа измерены: смещение контекста 5, зазор при коллизии 10, зазор для текущей раскладки 25. Каждая константа менялась только после прогона на стенде с реальными словами из моего лога.
Перепечатка идёт через TISSelectInputSource и UCKeyTranslate, по одному символу на CGEvent. Если отправить пачкой, Electron-приложения (Slack, Discord, VS Code, Telegram) молча теряют нажатия. Это первое, что я узнал о macOS в этом проекте, и далеко не последнее.
Главное правило, к которому я пришёл после пары недель чтения логов: экран решает, сколько символов стирать, клавиши решают, что печатать. Внутренняя модель набранного слова дрейфует, поэтому перед конвертацией двойным Shift приложение сверяет её с реальным текстом под курсором через Accessibility API. Если расхождение, доверяем экрану. В терминалах и Electron экрана нет, там остаётся только модель, и единственная защита там: не терять нажатия.
Где я облажался
Все пять историй найдены одинаково: подробный лог включён, я печатаю как обычно, вечером смотрю, что пошло не так. Ни одна не была найдена агентом по коду. Все пять нашёл я, просто печатая.
Фильтр Блума портил русские слова. У него 0,5 % ложных срабатываний, и первые три месяца он был единственным словарём на горячем пути. За полтора суток лога три русские опечатки превратились в латинскую кашу: «оптимизированнро» стало «jgnbvbpbhjdfyyhj». Я портировал фильтр на Python, скормил ему живой кэш из приложения и получил true ровно на эти три строки. Починка: Блум остаётся только предфильтром, каждое попадание подтверждается точным поиском.
Игровой режим навсегда записал браузер в игры. Режим придуман для игр: там WASD и автоповторы, а исправление слов превращает игру в ад. Вердикт сохранялся на диск. Одна веб-игра в Brave, и с этого момента каждая активация Brave глушила все исправления. В логе за полтора суток 17 «мёртвых» двойных Shift, без единой строки о причине. Теперь поведенческий вердикт на диск не пишется никогда. Выход из игрового режима происходит по прозе: два словарных слова от четырёх букв за 30 секунд.
Звук заморозил клавиатуру на десять секунд. 26 сентября девять исправлений из девяти заняли от 10,1 до 10,3 секунды, и после каждого macOS отключала event tap по таймауту. Виноват NSSound на главном потоке: CoreAudio задумался, а вместе с ним встал обработчик клавиш. Клавиши, нажатые в эти десять секунд, шли мимо приложения и ложились в раскладку, на которую оно уже переключилось. Целые фразы не в той раскладке, и ничего не исправлено. Звук теперь живёт в отдельной очереди и никогда не блокирует главный поток.
Три копии приложения одновременно. 23 сентября после одной из установок клавиатуру слушали три копии, и каждая исправляла текст по-своему. Быстрее всего это диагностируется через CGGetEventTapList: один tap от приложения, значит здоровое. Починка: flock на файл в Application Support, вторая копия выходит сразу.
Запятая превращалась в точку. 7 октября из 206 запятых 20 пришли как точка, а умный регистр после 16 из них ещё и поставил заглавную букву следующему слову. Причина не в приложении: на русской раскладке запятая это Shift плюс та же клавиша, что и точка, и при быстром наборе клавиша прилетает раньше Shift. Разница от 2 до 105 миллисекунд, у восьми случаев из двадцати меньше десяти. Теперь приложение узнаёт этот паттерн (точка, потом голый Shift в пределах 120 мс, между ними ничего) и чинит запятую обратно.
Как это делалось с агентом
Основной код писал Claude Code, я ставил задачи, читал диффы и логи. Пара вещей, которые я бы сказал себе в июне.
Тесты живут в самом бинарнике. Отдельного тестового таргета нет: в debug-сборке приложение запускается с флагом --test и прогоняет 1542 теста за три минуты. Часть из них читает собственный исходник: проверяет, что на горячем пути нет синхронных вызовов Accessibility, звука и обращений к NSWorkspace. Если кто-то (я или агент) вернёт туда тяжёлую операцию, тест упадёт раньше, чем я это почувствую пальцами.
Зелёные тесты ничего не доказывают. Перед релизом 0.11.6 я отдал дифф на независимое ревью второй модели (Codex). Два круга, пять реальных дефектов, три из них я пропустил при построчном чтении, и все они жили между словами: отложенное восстановление раскладки срабатывало на следующем слове, бэкап настроек терял метку выученного слова. Мои тесты проверяли одно слово за раз. С тех пор независимое ревью перед релизом у меня обязательный шаг.
Git stash с агентами запрещён. 5 августа один git stash снёс 3399 строк в 29 файлах, пока параллельные агенты продолжали писать поверх дерева двухдневной давности. Теперь правило простое: грязное дерево это нормально, коммит перед каждой делегацией, у каждого агента свой список файлов.
Самое ценное в проекте: лог. Подробный лог с кодами клавиш (выключен по умолчанию, файл только для владельца) и скрипты на Python, которые превращают сутки набора в таблицу: сколько исправлений, сколько откатов, где раскладка застряла. Без этого ни одна из пяти историй выше не была бы найдена.
Чего нет, и я об этом знаю
Сборка подписана самоподписанным сертификатом, поэтому первый запуск macOS встречает предупреждением. На macOS 15 и новее: закрыть его, открыть Системные настройки, Конфиденциальность и безопасность, внизу нажать «Открыть всё равно». Нотаризация отложена сознательно: смена подписи сбрасывает выданные разрешения у всех, кто уже поставил приложение, и делать это надо один раз, когда автообновление доедет до всех.
Мгновенное исправление в терминалах не выключено, выключен только агрессивный режим «мусор в слово». Хочу сначала измерить долю ложных срабатываний, потом решать.
Выученное слово может перебить настоящее на границе слова: если вы научили приложение английскому «hub», русское «руб» однажды проиграет. Это известно, решение за мной.
Словари чистились только до пяти букв. Длиннее шести, имена собственные и заимствования ещё не разбирались.
Про деньги
Сначала у приложения была подписка: триал, ключи, сервер лицензий, даже оплата криптой через Telegram. В сентябре я всё это выбросил. Три пользователя, один из которых мама, плохо сочетаются со словом «подписка», а сервер лицензий был единственным сетевым вызовом в приложении, которое я продавал как «ничего не отправляет». Теперь честно: бесплатно, офлайн, ключей нет. В окне «Об авторе» есть ссылки на мои проекты, этого достаточно.
Где взять
DMG: https://shulgin.is-a.dev/store/downloads/QwertySwitcher-0.11.6.dmg (универсальная сборка, 5 МБ, macOS 13 и новее).
Код: https://github.com/Sanexxxx777/QwertySwitcher (MIT, Swift Package Manager, без Xcode-проекта).
Если поставите и оно испортит вам слово, которое портить было не надо: включите подробный лог в настройках, нажмите «Собрать отчёт» (строки с кодами клавиш из отчёта вырезаются) и пришлите в issue. Именно так были найдены все баги выше, и мне интереснее чужие раскладки и чужие привычки набора, чем свои.
А у вас какой свитчер стоит на Mac и что он делает не так? Напишите в комментариях.

