惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
D
DataBreaches.Net
量子位
C
Cybersecurity and Infrastructure Security Agency CISA
G
Google Developers Blog
小众软件
小众软件
Application and Cybersecurity Blog
Application and Cybersecurity Blog
MyScale Blog
MyScale Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cisco Talos Blog
Cisco Talos Blog
P
Proofpoint News Feed
V
Vulnerabilities – Threatpost
Security Latest
Security Latest
T
Tenable Blog
Vercel News
Vercel News
AWS News Blog
AWS News Blog
Forbes - Security
Forbes - Security
M
MIT News - Artificial intelligence
S
Security Affairs
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hacker News - Newest:
Hacker News - Newest: "LLM"
美团技术团队
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
Last Week in AI
Last Week in AI
罗磊的独立博客
Project Zero
Project Zero
Cyberwarzone
Cyberwarzone
S
Schneier on Security
S
Secure Thoughts
T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
Y
Y Combinator Blog
F
Full Disclosure
N
Netflix TechBlog - Medium
博客园 - 叶小钗
A
Arctic Wolf
腾讯CDC
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
The Exploit Database - CXSecurity.com
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
C
Check Point Blog
C
CERT Recently Published Vulnerability Notes
www.infosecurity-magazine.com
www.infosecurity-magazine.com

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Меняем Punto Switcher на macOS: открытый код, ноль телеметрии и ноль зависимостей — в обоих смыслах
Rashns · 2026-06-22 · via Все публикации подряд на Хабре

Средний

8 мин

0

— Поставил 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 — проект живой, баг‑репорты реально читаются и чинятся.