Как забрать текст из чужого буфера обмена и не быть замеченным
UTranslate работает в любой программе, потому что сам нажимает Ctrl+C за человека. Пять шагов, чтобы он этого не заметил, и что всё-таки заметно.

Содержание
Выделил абзац, нажал Ctrl+Alt+T, и перевод появился у курсора. Чтобы это работало в браузере, мессенджере и редакторе одинаково, UTranslate делает неприличную вещь: сам нажимает Ctrl+C за человека и лезет в буфер обмена, где лежит то, что человек скопировал минуту назад.
В требованиях для этого есть отдельная строка: буфер обмена после работы такой же, как до. Вот как это устроено и где оно до сих пор протекает.
Почему именно Ctrl+C
У Windows есть способ попросить текст вежливо: UI Automation. Он не работает в Qt и в части приложений на Chromium, то есть как раз там, где человек чаще всего читает. Старый QTranslate годами работал через эмуляцию Ctrl+C и работал везде. Я взял тот же приём, а UI Automation оставил резервом на случай, если найдётся программа, где Ctrl+C переназначен.
Пять шагов захвата
1. Снимок буфера
Программа сохраняет всё, что лежит в буфере и что можно прочитать. Форматов там бывает много: текст, картинка, файлы, чужие внутренние форматы.
2. Дождаться, пока человек отпустит клавиши
Пока зажаты Ctrl и Alt, синтетический Ctrl+C превратится в Ctrl+Alt+C. Сначала я отправлял принудительное отпускание, и оно гонялось с автоповтором: буква из сочетания падала прямо в чужое поле. Теперь программа просто ждёт, это настоящий код из репозитория:
fn wait_modifiers_released() -> Result<(), String> {
let start = Instant::now();
while modifiers_down() {
if start.elapsed() > Duration::from_millis(1500) {
return Err("Отпустите клавиши хоткея и повторите".into());
}
thread::sleep(Duration::from_millis(10));
}
Ok(())
}Полторы секунды потолка, и если человек так и держит клавиши, он получает текст, что ему сделать.
3. Поставить метку
В буфер кладётся короткая метка от самой программы. Без неё нельзя отличить «скопировала нужная программа» от «что-то записало в буфер в ту же секунду». Перед нажатием программа ещё раз проверяет окно, поле и последний ввод. Изменились, значит копирование отменяется и буфер возвращается.
4. Нажать и ждать ответ
Ctrl+C отправляется через SendInput, дальше опрос раз в 10 мс: сменился ли номер буфера и кто теперь его владелец. Обычное копирование занимает 50–150 мс, и опрос заканчивается на первой же смене. Потолок ожидания 0,7 секунды. В требованиях сначала стояло 300 мс, но к версии 0.4.2 их подняли.
Номер не сменился за это время, значит выделения не было, и попап откроется с полем ввода. Буфер записал кто-то третий или фокус ушёл в другое поле, значит захват отменяется, а не подхватывает чужой текст.
5. Вернуть всё на место
Текст уже снят, и перевод этого шага не ждёт. В коде это прямо так и записано: возврат буфера теперь вежливость, а не условие успеха. Если вернуть не вышло, перевод не теряется.
Что ломалось по дороге
Формат 49354. Некоторые программы кладут в буфер данные с отложенной отрисовкой: настоящие байты появляются только по запросу, и читать их можно не всегда. Один такой формат срывал весь захват, и человек видел пустой попап с ошибкой. Теперь снимок делается по возможности: нечитаемый формат пропускается, захват идёт дальше.
Прибор, который врал. Живые проверки я гоняю через стенд, который нажимает клавиши за меня. SendKeys из PowerShell не доходит до RegisterHotKey и теряет модификаторы, поэтому два теста подряд печатали букву в Блокнот вместо сочетания. Починилось переходом на SendInput со скан-кодами.
Замена текста: то же самое, но страшнее
По Ctrl+Alt+R выделенный текст заменяется переводом прямо в поле. Здесь цена ошибки выше: программа пишет в чужое поле.
Между захватом и вставкой лежит сетевой перевод, за это время человек мог уйти в другое окно. Поэтому перед вставкой программа захватывает выделение ещё раз и сравнивает его с исходным текстом. Окно, поле и текст совпали, значит вставляем. Время последнего ввода тут уже не сравнивается: оно растёт от простого движения мыши.
В живой проверке resilient в Блокноте стал устойчивый, а буфер обмена остался прежним. Для этой части кода есть юнит-тесты: метка, запись чужой программы, сбой чтения, тайм-аут. Код открыт, это файл capture.rs в репозитории UTranslate.
Где это всё-таки заметно
Заголовок немного врёт, и лучше сказать об этом прямо.
Правило из требований выполняется там, где это зависит от меня: буфер возвращается, фокус не трогается, а ошибка говорит словами, что случилось. Остальное написано в README, чтобы человек узнал об этом от меня, а не от журнала буфера.
Умар Турсунов
Собираю с нейросетями приложения, сайты, программы, ботов и драйверы. От идеи до работающего продукта.
Поделиться

UTranslate
Перевод у курсора за три клавиши
Выделил текст, нажал три клавиши, и перевод появился у курсора. Без вкладок, ключей и регистрации.
- 9
- 3
- 0


