Запись 53. Тайна проглоченной буквы: как переключение раскладки съедало первый символ и как мы научили терминал слышать с полуслова
⏱️ Время: 30 сентября 2026, 08:20 — 08:45 (~25 минут)1. Повод: когда первую букву приходится нажимать дважды
Каждый, кто регулярно работает в консоли и переключается между русским и английским языками, знает эту раздражающую микро-паузу: нажал Alt + Shift, хочешь набрать команду ls или написать комментарий на русском — жмёшь первую букву, а в терминале… тишина. Жмёшь её второй раз — и только тогда она нехотя появляется на экране.
Поначалу на это можно махнуть рукой: показалось, не дожал клавишу, случайная задержка. Но когда дефект воспроизводится абсолютно стабильно при каждом переключении раскладки, ощущение рабочего комфорта испаряется. В эмуляторе терминала отзывчивость обязана быть образцовой: если палец коснулся клавиши, символ должен оказаться в буфере без каких-либо заминок.
Мы решили докопаться до фундаментальной причины: почему теряется именно первый символ и почему для его ввода обязательно требуется ровно два нажатия?
2. Подводные камни: три заговора против первого нажатия
Когда мы разложили цепочку обработки клавиш на составляющие, наружу выплыл целый комплекс скрытых механизмов Win32 API и внутреннего состояния Flutter Engine:
Коварство Win32 и меню по Alt (SC_KEYMENU)
В классической архитектуре Windows одиночное нажатие и отпускание клавиши Alt переводит окно в режим системного меню (WM_SYSCOMMAND с параметром SC_KEYMENU). В этом режиме операционная система считает, что следующее нажатие клавиши — это выбор пункта меню (мнемоника).
Когда пользователь переключал раскладку по Alt + Shift, Windows успевала активировать этот режим. Первое последующее нажатие любой буквы операционная система перехватывала на уровне оконной процедуры DefWindowProc, пыталась найти соответствующий пункт меню, не находила его, молча закрывала режим меню и проглатывала нажатие. До приложения символ даже не долетал!
Фантомный Alt в памяти Flutter (HardwareKeyboard)
Поскольку системные комбинации переключения языка (Alt + Shift, Ctrl + Shift, Win + Space) перехватываются операционной системой глобально на уровне ядра ввода, Windows очень часто не отправляет сообщение WM_KEYUP для клавиши-модификатора в очередь конкретного окна.
В итоге в оперативной памяти Flutter подсистема HardwareKeyboard продолжала считать, что Alt (или Ctrl) всё ещё физически зажат (isAltPressed == true).
Преждевременная маршрутизация шорткатов
В нашем терминальном экране стоял предохранительный фильтр: если зажат Ctrl или Alt, событие отдавалось встроенным шорткатам xterm (KeyEventResult.ignored), чтобы не мешать комбинациям вроде Ctrl+C или Alt+F.
Видя залипший флаг isAlt == true, наш обработчик ошибочно решал, что пользователь нажал комбинацию Alt + [Буква], и полностью блокировал прямой ввод текста!
Почему же срабатывало второе нажатие?
Во время первого «холостого» нажатия происходили два события: Windows выходила из режима системного меню, а Flutter Engine сверял битовую маску реального состояния клавиатуры и сбрасывал фантомный флаг модификатора. Ко второму нажатию система возвращалась в чистое состояние — и символ наконец-то попадал в терминал.
3. Исправление и стабилизация (BUG-045)
Для надежного устранения проблемы мы применили двухфакторную защиту: на уровне C++ раннера Windows и на уровне логики терминального экрана в Dart:
1. Блокировка SC_KEYMENU в нативном окне (win32_window.cpp)
В оконной процедуре Win32Window::MessageHandler мы перехватили системное сообщение WM_SYSCOMMAND:
case WM_SYSCOMMAND: // Блокируем вход в модальный цикл системного меню по Alt (SC_KEYMENU), // чтобы Windows не перехватывала последующий ввод символов if (wparam == SC_KEYMENU) { return 0; } break;Возврат 0 сообщает Windows, что системное меню обрабатывать не нужно. Это стандартный отраслевой прием для профессиональных эмуляторов терминалов (Windows Terminal, Alacritty) и игр.
2. Приоритет печатных символов над фантомными модификаторами (terminal_screen.dart)
В обработчике _handleTerminalKeyEvent мы выстроили строгую иерархию:
- Специальные навигационные клавиши (
Enter,Tab,Backspace, стрелки,Delete) безусловно передаются в движок xterm. - Если событие содержит валидный печатный символ (руны $\ge 32$ и $\ne 127$), мы отдаем безусловный приоритет печати текста:
- Любой не-ASCII символ (например, русские буквы
п,фпосле смены языка) гарантированно является текстом, так как в терминалах в принципе не существует кириллических управляющих кодов. - Если активен
AltбезCtrl— в WindowsAltв одиночку никогда не производит печатных символов. Наличие символа однозначно указывает на то, чтоAltуже отпущен, а флаг в памяти — фантом. - При этом классические сочетания (
Ctrl+C,Ctrl+D,Ctrl+Z,Ctrl+L) и европейский AltGr (@,€,~) продолжают работать штатно.
- Любой не-ASCII символ (например, русские буквы
4. Регрессионные тесты и верификация
В тестовый набор terminal_screen_test.dart добавлены юнит-тесты, моделирующие залипание модификаторов altLeft и controlLeft при смене языка. Тесты подтверждают, что и латиница ('a'), и кириллица ('п', 'ф') передаются в сессию терминала с самого первого нажатия.
- Зарегистрирован и проверен дефект
BUG-045(P1 /VERIFIED). - Все 128 из 128 тестов пакета
terminal_uiуспешно пройдены. - Статический анализ
flutter analyzeподтвердил 0 замечаний.
Теперь переключение раскладки в Shellit работает абсолютно незаметно: терминал откликается на первое же касание клавиши, на каком бы языке вы ни начали набирать команду.