БлогСтатья

Как забрать текст из чужого буфера обмена и не быть замеченным

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

Планшет с чистым листом на оранжевом фоне. Стальной пинцет вытягивает из-под зажима узкую полоску бумаги.
Содержание
  1. Почему именно Ctrl+C
  2. Пять шагов захвата
  3. 1. Снимок буфера
  4. 2. Дождаться, пока человек отпустит клавиши
  5. 3. Поставить метку
  6. 4. Нажать и ждать ответ
  7. 5. Вернуть всё на место
  8. Что ломалось по дороге
  9. Замена текста: то же самое, но страшнее
  10. Где это всё-таки заметно

Выделил абзац, нажал Ctrl+Alt+T, и перевод появился у курсора. Чтобы это работало в браузере, мессенджере и редакторе одинаково, UTranslate делает неприличную вещь: сам нажимает Ctrl+C за человека и лезет в буфер обмена, где лежит то, что человек скопировал минуту назад.

В требованиях для этого есть отдельная строка: буфер обмена после работы такой же, как до. Вот как это устроено и где оно до сих пор протекает.

Перевод у курсора
0:00 /
Настоящая запись программы: выделение, сочетание клавиш, пилюля, карточка с переводом.

Почему именно Ctrl+C

У Windows есть способ попросить текст вежливо: UI Automation. Он не работает в Qt и в части приложений на Chromium, то есть как раз там, где человек чаще всего читает. Старый QTranslate годами работал через эмуляцию Ctrl+C и работал везде. Я взял тот же приём, а UI Automation оставил резервом на случай, если найдётся программа, где Ctrl+C переназначен.

Пять шагов захвата

1. Снимок буфера

Программа сохраняет всё, что лежит в буфере и что можно прочитать. Форматов там бывает много: текст, картинка, файлы, чужие внутренние форматы.

2. Дождаться, пока человек отпустит клавиши

Пока зажаты Ctrl и Alt, синтетический Ctrl+C превратится в Ctrl+Alt+C. Сначала я отправлял принудительное отпускание, и оно гонялось с автоповтором: буква из сочетания падала прямо в чужое поле. Теперь программа просто ждёт, это настоящий код из репозитория:

Rust
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 выделенный текст заменяется переводом прямо в поле. Здесь цена ошибки выше: программа пишет в чужое поле.

Замена текста в поле
0:00 /
Настоящая запись: фраза заменилась, и подпись у курсора показывает, что именно произошло.

Между захватом и вставкой лежит сетевой перевод, за это время человек мог уйти в другое окно. Поэтому перед вставкой программа захватывает выделение ещё раз и сравнивает его с исходным текстом. Окно, поле и текст совпали, значит вставляем. Время последнего ввода тут уже не сравнивается: оно растёт от простого движения мыши.

В живой проверке resilient в Блокноте стал устойчивый, а буфер обмена остался прежним. Для этой части кода есть юнит-тесты: метка, запись чужой программы, сбой чтения, тайм-аут. Код открыт, это файл capture.rs в репозитории UTranslate.

Где это всё-таки заметно

Заголовок немного врёт, и лучше сказать об этом прямо.

Правило из требований выполняется там, где это зависит от меня: буфер возвращается, фокус не трогается, а ошибка говорит словами, что случилось. Остальное написано в README, чтобы человек узнал об этом от меня, а не от журнала буфера.

Умар Турсунов

Нейрокреатор

Собираю с нейросетями приложения, сайты, программы, ботов и драйверы. От идеи до работающего продукта.

Обсудить задачуОбо мне

UTranslate

Программа для Windows, открытый код

Перевод у курсора за три клавиши

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

  • Ctrl + Alt + T
  • Windows
  • Без ключей
  • 9релизов за 4 дня
  • 3движка с автопереходом
  • 0телеметрии и ключей

Похожие по темам