Всем привет! Мой первый пост, но не первый проект, хотелось бы собрать фидбек от опытных разрабов или проггеров. Каждый совет делает меня лучше:)
Начну с главного: «навайбкодил», и мне не стыдно (немного если только). Claude писал код, а я сидел и пил чай. На самом деле кучу времени потратил на тесты и несколько дней на составление плана: «что должно быть внутри» и «какой функционал».

Что получилось: пиксельная кошка для Windows 10/11. Живёт на краях экрана и на верхних рамках окон, ест, спит, обижается, смотрит с тобой кино и прячется на созвонах. Главное требование: питомец не должен мешать. Из него и выросла вся техническая часть.
Стек — Tauri 2: Rust для WinAPI, Svelte 5 внутри WebView2. Rust только собирает геометрию и флаги, а поведение кошки — чистые функции на TypeScript, поэтому почти всё покрыто тестами без запуска окна.

Вывод: прозрачный WebView ловит мышь всем прямоугольником
Кошка нарисована в квадрате 32×32 и занимает от силы половину, а клики глотает весь квадрат: у WebView2 прозрачны только пиксели, не hit‑test. SetWindowRgn и layered‑окна не подходят — WebView2 рисует сам. WM_NCHITTEST тоже: мышь получает дочернее окно WebView2, а не наше.
Сделал наоборот. Окно по умолчанию прозрачно для мыши (WS_EX_TRANSPARENT). Отдельный поток раз в 33 мс смотрит, над какой клеткой спрайта курсор, и снимает флаг, только пока курсор над нарисованным пикселем. Форму кадра фронт присылает маской из 32 u32:
let want = !masks.contains(&label, x, y) || alt; // row >> x & 1 if want != ignoring && window.set_ignore_cursor_events(want).is_ok() { ignoring = want; }
Грабли: окно нельзя сделать уже SM_CXMINTRACK (136 px при 100%), поэтому спрайт центрируется и координаты считаются от квадрата. Пока кнопка зажата, мышь не отпускаем, иначе перетаскивание рвётся. С Alt клик проходит насквозь всегда.
Опрос по таймеру — не самое красивое решение. Если знаете способ лучше для WebView2, жду в комментариях.
Фокус и Esc, который улетал в игру
Все окна неактивируемые (WS_EX_NOACTIVATE), иначе кошка свернёт игру. Но show() в Tauri вызывает ShowWindow(SW_SHOW), а тот активирует окно несмотря ни на что, поэтому меню показывается через SetWindowPos с SWP_NOACTIVATE.
Следом вылез баг: у меню нет фокуса, значит, клавиатура у игры. Нажал Esc, чтобы закрыть меню, — и в игре открылось главное меню. Пришлось ставить WH_KEYBOARD_LL, который съедает Esc, но только пока меню открыто. Важная тонкость: поток хука обязан разбирать сообщения (MsgWaitForMultipleObjects, а не sleep), иначе ввод всей системы ждёт его.
Всё остальное время хука нет, ввод читается через Raw Input в message‑only окно: он не встаёт в цепочку обработки нажатий и не добавляет задержку в играх.

Прогулки по рамкам: GetWindowRect цепляет невидимую рамку
EnumWindows отдаёт окна по z‑order, дальше фильтр, и каждое правило в нём — исправленный баг:
— GetWindowRect врёт. В Windows 10 у окна невидимые поля для ресайза, около 7 px, и кошка висела в воздухе. Видимую рамку отдаёт DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS).
— Невидимые видимые окна. Окна других рабочих столов и призраки UWP проходят IsWindowVisible. Отсекает DWMWA_CLOAKED.
— Оверлеи. NVIDIA и Steam держат прозрачные окна на весь экран поверх всего и «закрывают» все кромки. Признак — WS_EX_TRANSPARENT.
— Перекрытия. От кромки окна вычитаются куски, закрытые окнами выше. Это чистая функция на отрезках с тестами.
— Развёрнутое окно. Ходить можно по заголовку левее кнопок, их даёт DWMWA_CAPTION_BUTTON_BOUNDS, но относительно GetWindowRect, вместе с невидимыми полями.
— Полноэкранное видео. Windows не убирает панель задач из rcWork, и пол висел поверх фильма. Ещё Chrome на весь экран оставляет 1439 px из 1440, так что «на весь экран» считается с допуском.
Заголовки окон не читаются нигде: только рамка, стили, имя класса и exe.
Созвон без списка программ
Кошка на созвоне прячется. Список Zoom/Teams/Discord вечно отстаёт, поэтому я взял источник, откуда Windows берёт значок микрофона в трее:
HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone
У каждой программы там LastUsedTimeStart и LastUsedTimeStop. Пока устройство занято, Stop равен нулю. Meet в браузере, Telegram, что угодно — видно без списка.
На живой машине вылезли два исключения. Программа упала, не отпустив микрофон, — Stop остался нулём навсегда, поэтому занятость старше 12 часов не считается. А NVIDIA Broadcast, Voicemeeter и OBS держат микрофон всегда, их пришлось внести в список из четырёх строк. Наружу модуль отдаёт только да/нет, звук не пишется.
Месть, которую не видно на записи
Обидишь кошку — уйдёт за край, а вернувшись, оставит лужу на окне программы, где ты сегодня провёл больше всего времени. Лужа на чужом окне в скриншоте или на демонстрации экрана выглядит как настоящая порча, поэтому её окно всегда исключено из захвата:
SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE)
На мониторе видно, в OBS и Zoom — нет. Побочный эффект: для гифки лужу пришлось вклеивать при монтаже.

Цифры и неприятное
— установщик ~2,3 МБ, exe ~6 МБ;
— память — Tauri не волшебный: около 400 МБ частной памяти через 20 минут работы, exe плюс 14 процессов msedgewebview2. Каждое окно (кошка, миска, лежак, облачко, меню…) — свой WebView. Диспетчер задач показывает 8 МБ, но врёт. Ужать — следующая задача, советы приветствуются;
— процессор: 1–3% от восьми ядер;
— 762 теста vitest и 125 cargo;
— в сеть уходят только дата и язык — для новостей по выбранным темам.
Windows при установке скажет «Windows защитила ваш компьютер»: сертификат подписи стоит денег. Лицензия PolyForm Noncommercial — код открыт, продавать нельзя, так что это не open source в строгом смысле. Только Windows: под Linux почти ничего из этого не переносится.
Если поставите — напишите, что мешает. Именно «мешает» мне интереснее всего.
— исходники: Тык (гитхаб)
— сайт и установщик: Тык (тоже гитхаб)
— итч.ио: Тык
