Merge pull request #6 from vndangkhoa/devin/1782522350-clipboard-and-cicd
Debounce the clipboard restore so the user's clipboard is never written back while a just-pasted Vietnamese word may still be read by the target app, which caused old clipboard content to appear in the text mid-typing. Applied to both vietc-uinputd and the in-process UinputInjector. Add a GitHub Actions workflow that builds the .deb and AppImage on the runner (artifacts on push/PR, GitHub Release on v* tags), and include vietc-uinputd + vietc-xrecord in the .deb. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: vndangkhoa <vonguyendangkhoa@gmail.com>
This commit is contained in:
commit
fcd7b4e61f
5 changed files with 349 additions and 63 deletions
107
.github/workflows/build.yml
vendored
Normal file
107
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
name: Build & Release
|
||||
|
||||
# Builds the .deb and AppImage on the CI runner so artifacts are produced
|
||||
# without compiling on a local machine:
|
||||
# - every push to main / pull request -> packages uploaded as workflow artifacts
|
||||
# - pushing a `v*` tag -> a GitHub Release with the .deb + AppImage
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Build & test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libdbus-1-dev libx11-dev libxtst-dev libxext-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build workspace
|
||||
run: cargo build --release --features "x11,wayland"
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --release
|
||||
|
||||
package:
|
||||
name: Build packages
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libdbus-1-dev libx11-dev libxtst-dev libxext-dev \
|
||||
fakeroot dpkg-dev xclip xdotool desktop-file-utils file curl
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Determine version
|
||||
id: ver
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
else
|
||||
BASE=$(grep '^version' engine/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
VERSION="$BASE"
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
echo "Building version $VERSION"
|
||||
|
||||
- name: Fetch appimagetool
|
||||
run: |
|
||||
curl -fsSL -o packaging/appimage/appimagetool \
|
||||
https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x packaging/appimage/appimagetool
|
||||
|
||||
- name: Build .deb
|
||||
run: bash packaging/deb/build-deb.sh "${{ steps.ver.outputs.version }}"
|
||||
|
||||
- name: Build AppImage
|
||||
# appimagetool is invoked with --appimage-extract-and-run by the build
|
||||
# script, so no FUSE is required on the runner.
|
||||
run: bash packaging/appimage/build-appimage.sh "${{ steps.ver.outputs.version }}"
|
||||
|
||||
- name: Collect artifacts
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp packaging/deb/*.deb dist/
|
||||
cp packaging/appimage/*.AppImage dist/
|
||||
ls -la dist
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vietc-packages-${{ steps.ver.outputs.version }}-${{ steps.ver.outputs.short_sha }}
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*
|
||||
generate_release_notes: true
|
||||
|
|
@ -179,14 +179,16 @@ else
|
|||
SVGEOF
|
||||
fi
|
||||
|
||||
# Convert SVG to PNG for appimagetool (it prefers PNG for the root icon)
|
||||
# Convert SVG to PNG for appimagetool (it prefers PNG for the root icon).
|
||||
# This is best-effort: if no converter works, appimagetool falls back to the
|
||||
# SVG, so a conversion failure must never abort the build.
|
||||
if [ -f "$APPDIR/vietc.svg" ] && ! [ -f "$APPDIR/vietc.png" ]; then
|
||||
if command -v rsvg-convert &>/dev/null; then
|
||||
rsvg-convert -w 256 -h 256 "$APPDIR/vietc.svg" -o "$APPDIR/vietc.png"
|
||||
rsvg-convert -w 256 -h 256 "$APPDIR/vietc.svg" -o "$APPDIR/vietc.png" || true
|
||||
elif command -v inkscape &>/dev/null; then
|
||||
inkscape -w 256 -h 256 "$APPDIR/vietc.svg" --export-filename="$APPDIR/vietc.png" 2>/dev/null
|
||||
inkscape -w 256 -h 256 "$APPDIR/vietc.svg" --export-filename="$APPDIR/vietc.png" 2>/dev/null || true
|
||||
elif command -v convert &>/dev/null; then
|
||||
convert -background none "$APPDIR/vietc.svg" -resize 256x256 "$APPDIR/vietc.png" 2>/dev/null
|
||||
convert -background none "$APPDIR/vietc.svg" -resize 256x256 "$APPDIR/vietc.png" 2>/dev/null || true
|
||||
elif command -v python3 &>/dev/null; then
|
||||
python3 -c "
|
||||
import subprocess, sys
|
||||
|
|
@ -194,7 +196,7 @@ try:
|
|||
subprocess.check_call(['rsvg-convert', '-w', '256', '-h', '256', '$APPDIR/vietc.svg', '-o', '$APPDIR/vietc.png'])
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null
|
||||
" 2>/dev/null || true
|
||||
fi
|
||||
# If no converter, appimagetool can use SVG directly
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -31,8 +31,19 @@ mkdir -p "$STAGING/usr/share/metainfo"
|
|||
echo "[3/5] Installing binaries..."
|
||||
cp "$PROJECT_ROOT/target/release/vietc" "$STAGING/usr/bin/"
|
||||
cp "$PROJECT_ROOT/target/release/vietc-cli" "$STAGING/usr/bin/"
|
||||
# Privileged uinput injection daemon — required for Unicode (Vietnamese) output.
|
||||
cp "$PROJECT_ROOT/target/release/vietc-uinputd" "$STAGING/usr/bin/"
|
||||
[ -f "$PROJECT_ROOT/ui/target/release/vietc-tray" ] && cp "$PROJECT_ROOT/ui/target/release/vietc-tray" "$STAGING/usr/bin/"
|
||||
|
||||
# Compile and bundle vietc-xrecord (C helper for X11 XRecord keyboard capture)
|
||||
if command -v gcc &>/dev/null; then
|
||||
gcc -O2 -o "$STAGING/usr/bin/vietc-xrecord" "$PROJECT_ROOT/packaging/appimage/vietc-xrecord.c" -lX11 -lXtst \
|
||||
&& echo " vietc-xrecord compiled" \
|
||||
|| echo " WARNING: vietc-xrecord compile failed (libX11/libXtst dev headers missing)"
|
||||
else
|
||||
echo " WARNING: no gcc, vietc-xrecord not bundled"
|
||||
fi
|
||||
|
||||
# Desktop file
|
||||
cp "$PROJECT_ROOT/packaging/appimage/vietc.desktop" "$STAGING/usr/share/applications/"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
use std::fs::{File, OpenOptions};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::inject::{InjectResult, KeyInjector};
|
||||
|
||||
/// How long to wait after the last Unicode paste before restoring the user's
|
||||
/// real clipboard. Each paste pushes this deadline back, so a burst of typing
|
||||
/// only triggers a single restore once the user pauses — the user's clipboard
|
||||
/// is never pasted into the text while the target app might still be reading
|
||||
/// our freshly pasted word.
|
||||
const RESTORE_DEBOUNCE: Duration = Duration::from_millis(600);
|
||||
|
||||
const UINPUT_MAX_NAME_SIZE: usize = 80;
|
||||
const UI_SET_EVBIT: u64 = 0x40045564;
|
||||
const UI_SET_KEYBIT: u64 = 0x40045565;
|
||||
|
|
@ -16,14 +25,31 @@ const EV_KEY: u16 = 0x01;
|
|||
const EV_ABS: u16 = 0x03;
|
||||
const KEY_MAX: u32 = 0x1ff;
|
||||
|
||||
pub struct UinputInjector {
|
||||
file: File,
|
||||
/// Shared clipboard bookkeeping between the injection path and the background
|
||||
/// restorer thread.
|
||||
struct ClipInner {
|
||||
/// The user's real clipboard contents, saved before we overwrite the
|
||||
/// clipboard to inject Unicode text, so we can restore it afterwards.
|
||||
saved_clipboard: std::sync::Mutex<Option<String>>,
|
||||
/// The last text we injected via the clipboard. Used to tell our own
|
||||
/// injected text apart from text the user copied with Ctrl+C.
|
||||
last_injected: std::sync::Mutex<Option<String>>,
|
||||
saved_clipboard: Option<String>,
|
||||
/// The last text we wrote to the clipboard ourselves (an injected word or
|
||||
/// the restored user content). Used to tell our own writes apart from text
|
||||
/// the user copied with Ctrl+C.
|
||||
last_injected: Option<String>,
|
||||
/// When set, the restorer thread should rewrite the user's clipboard at
|
||||
/// this instant. `None` means no restore is pending.
|
||||
restore_due: Option<Instant>,
|
||||
/// Set on shutdown so the restorer thread can exit.
|
||||
shutdown: bool,
|
||||
}
|
||||
|
||||
struct ClipState {
|
||||
inner: Mutex<ClipInner>,
|
||||
cv: Condvar,
|
||||
}
|
||||
|
||||
pub struct UinputInjector {
|
||||
file: File,
|
||||
clip: Arc<ClipState>,
|
||||
}
|
||||
|
||||
unsafe impl Send for UinputInjector {}
|
||||
|
|
@ -78,11 +104,21 @@ impl UinputInjector {
|
|||
// Small delay for device to be ready
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
saved_clipboard: std::sync::Mutex::new(None),
|
||||
last_injected: std::sync::Mutex::new(None),
|
||||
})
|
||||
let clip = Arc::new(ClipState {
|
||||
inner: Mutex::new(ClipInner {
|
||||
saved_clipboard: None,
|
||||
last_injected: None,
|
||||
restore_due: None,
|
||||
shutdown: false,
|
||||
}),
|
||||
cv: Condvar::new(),
|
||||
});
|
||||
{
|
||||
let clip = Arc::clone(&clip);
|
||||
std::thread::spawn(move || run_restorer(clip));
|
||||
}
|
||||
|
||||
Ok(Self { file, clip })
|
||||
}
|
||||
|
||||
fn send_uinput_event(&self, type_: u16, code: u16, value: i32) {
|
||||
|
|
@ -380,7 +416,7 @@ impl UinputInjector {
|
|||
|
||||
/// Read the user's current clipboard contents (wl-paste on Wayland, xclip
|
||||
/// on X11). Returns None if no clipboard tool is available or it is empty.
|
||||
fn read_clipboard(&self) -> Option<String> {
|
||||
fn read_clipboard() -> Option<String> {
|
||||
let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
|
||||
let (prog, args): (&str, &[&str]) = if is_wayland {
|
||||
("wl-paste", &["-n"])
|
||||
|
|
@ -403,34 +439,42 @@ impl UinputInjector {
|
|||
///
|
||||
/// Returns whether the text was successfully copied to the clipboard.
|
||||
fn paste_via_clipboard(&self, text: &str, use_x11_paste: bool) -> bool {
|
||||
// Snapshot the clipboard. If it differs from what we last injected, the
|
||||
// user changed it themselves (a real Ctrl+C), so remember it to restore.
|
||||
let current = self.read_clipboard();
|
||||
// Critical section: snapshot the clipboard, decide what to preserve,
|
||||
// cancel any pending restore so the restorer cannot fire while we
|
||||
// paste, and put our word on the clipboard. The read and write happen
|
||||
// under the lock so they can never interleave with the restorer.
|
||||
{
|
||||
let last = self.last_injected.lock().unwrap();
|
||||
let is_our_injection = matches!((¤t, &*last), (Some(c), Some(l)) if c == l);
|
||||
if !is_our_injection {
|
||||
*self.saved_clipboard.lock().unwrap() = current;
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
let current = Self::read_clipboard();
|
||||
let is_our_write =
|
||||
matches!((¤t, &st.last_injected), (Some(c), Some(l)) if c == l);
|
||||
if !is_our_write {
|
||||
st.saved_clipboard = current;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.copy_to_clipboard(text) {
|
||||
st.restore_due = None;
|
||||
if !Self::copy_to_clipboard(text) {
|
||||
return false;
|
||||
}
|
||||
st.last_injected = Some(text.to_string());
|
||||
}
|
||||
|
||||
// Give the selection owner a moment to take ownership before pasting.
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
|
||||
if use_x11_paste {
|
||||
self.send_ctrl_v_x11();
|
||||
} else {
|
||||
self.send_ctrl_v();
|
||||
}
|
||||
|
||||
// Restore the user's clipboard once the paste has been consumed. The
|
||||
// extra delay gives the target application time to read our text from
|
||||
// the clipboard before we overwrite it again.
|
||||
std::thread::sleep(std::time::Duration::from_millis(40));
|
||||
let saved = self.saved_clipboard.lock().unwrap().clone();
|
||||
let restored = saved.unwrap_or_default();
|
||||
let _ = self.copy_to_clipboard(&restored);
|
||||
*self.last_injected.lock().unwrap() = Some(restored);
|
||||
// Schedule a debounced restore. While the user keeps typing this gets
|
||||
// pushed back, so the user's clipboard is only restored once typing
|
||||
// settles — never overwriting our freshly pasted word mid-stream.
|
||||
{
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
st.restore_due = Some(Instant::now() + RESTORE_DEBOUNCE);
|
||||
}
|
||||
self.clip.cv.notify_all();
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -461,7 +505,7 @@ impl UinputInjector {
|
|||
}
|
||||
|
||||
// Clipboard fallback: copy + paste via our uinput device
|
||||
let copied = self.copy_to_clipboard(s);
|
||||
let copied = Self::copy_to_clipboard(s);
|
||||
if copied {
|
||||
eprintln!("[vietc] paste_string: clipboard OK, sending Ctrl+V");
|
||||
self.send_ctrl_v();
|
||||
|
|
@ -510,7 +554,7 @@ impl UinputInjector {
|
|||
}
|
||||
|
||||
/// Copy text to clipboard using wl-copy (Wayland) or xclip (X11).
|
||||
fn copy_to_clipboard(&self, s: &str) -> bool {
|
||||
fn copy_to_clipboard(s: &str) -> bool {
|
||||
// Try wl-copy (Wayland) via user_cmd
|
||||
{
|
||||
let mut cmd = Self::user_cmd("wl-copy");
|
||||
|
|
@ -623,10 +667,47 @@ impl UinputInjector {
|
|||
|
||||
impl Drop for UinputInjector {
|
||||
fn drop(&mut self) {
|
||||
{
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
st.shutdown = true;
|
||||
}
|
||||
self.clip.cv.notify_all();
|
||||
let _ = ioctl(self.file.as_raw_fd(), UI_DEV_DESTROY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Background thread: once no Unicode paste has happened for `RESTORE_DEBOUNCE`,
|
||||
/// rewrite the user's real clipboard so Ctrl+V keeps working.
|
||||
fn run_restorer(state: Arc<ClipState>) {
|
||||
loop {
|
||||
let mut st = state.inner.lock().unwrap();
|
||||
loop {
|
||||
if st.shutdown {
|
||||
return;
|
||||
}
|
||||
match st.restore_due {
|
||||
None => {
|
||||
st = state.cv.wait(st).unwrap();
|
||||
}
|
||||
Some(due) => {
|
||||
let now = Instant::now();
|
||||
if now >= due {
|
||||
break;
|
||||
}
|
||||
let (guard, _) = state.cv.wait_timeout(st, due - now).unwrap();
|
||||
st = guard;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deadline reached. Restore under the lock so the write cannot
|
||||
// interleave with a concurrent paste's clipboard write.
|
||||
let restored = st.saved_clipboard.clone().unwrap_or_default();
|
||||
let _ = UinputInjector::copy_to_clipboard(&restored);
|
||||
st.last_injected = Some(restored);
|
||||
st.restore_due = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_vn_diacritic(ch: char) -> char {
|
||||
match ch {
|
||||
'à' | 'á' | 'ả' | 'ã' | 'ạ' | 'ă' | 'ằ' | 'ắ' | 'ẳ' | 'ẵ' | 'ặ' | 'â' | 'ầ' | 'ấ' | 'ẩ' | 'ẫ' | 'ậ' => 'a',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@ use std::os::unix::net::{UnixListener, UnixStream};
|
|||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// How long to wait after the last Unicode paste before restoring the user's
|
||||
/// real clipboard. Each paste pushes this deadline back, so a burst of typing
|
||||
/// only triggers a single restore once the user pauses. This is what keeps the
|
||||
/// user's clipboard from being pasted into the text mid-typing: we never
|
||||
/// overwrite our just-pasted word with the user's clipboard while the target
|
||||
/// app might still be reading it.
|
||||
const RESTORE_DEBOUNCE: Duration = Duration::from_millis(600);
|
||||
|
||||
const UINPUT_MAX_NAME_SIZE: usize = 80;
|
||||
const UI_SET_EVBIT: u64 = 0x40045564;
|
||||
|
|
@ -45,14 +55,31 @@ struct input_id {
|
|||
version: u16,
|
||||
}
|
||||
|
||||
struct UinputDevice {
|
||||
fd: i32,
|
||||
/// Shared clipboard bookkeeping between the command handler and the background
|
||||
/// restorer thread.
|
||||
struct ClipInner {
|
||||
/// The user's real clipboard contents, saved before we overwrite the
|
||||
/// clipboard to paste Unicode text, so we can restore it afterwards.
|
||||
saved_clipboard: std::sync::Mutex<Option<String>>,
|
||||
/// The last text we injected via the clipboard, used to distinguish our
|
||||
/// own paste content from text the user copied with Ctrl+C.
|
||||
last_injected: std::sync::Mutex<Option<String>>,
|
||||
saved_clipboard: Option<String>,
|
||||
/// The last text we wrote to the clipboard ourselves (an injected word or
|
||||
/// the restored user content). Used to distinguish our own writes from
|
||||
/// text the user copied with Ctrl+C.
|
||||
last_injected: Option<String>,
|
||||
/// When set, the restorer thread should rewrite the user's clipboard at
|
||||
/// this instant. `None` means no restore is pending.
|
||||
restore_due: Option<Instant>,
|
||||
/// Set on shutdown so the restorer thread can exit.
|
||||
shutdown: bool,
|
||||
}
|
||||
|
||||
struct ClipState {
|
||||
inner: Mutex<ClipInner>,
|
||||
cv: Condvar,
|
||||
}
|
||||
|
||||
struct UinputDevice {
|
||||
fd: i32,
|
||||
clip: Arc<ClipState>,
|
||||
}
|
||||
|
||||
impl UinputDevice {
|
||||
|
|
@ -88,12 +115,22 @@ impl UinputDevice {
|
|||
std::mem::forget(file);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
let clip = Arc::new(ClipState {
|
||||
inner: Mutex::new(ClipInner {
|
||||
saved_clipboard: None,
|
||||
last_injected: None,
|
||||
restore_due: None,
|
||||
shutdown: false,
|
||||
}),
|
||||
cv: Condvar::new(),
|
||||
});
|
||||
{
|
||||
let clip = Arc::clone(&clip);
|
||||
std::thread::spawn(move || run_restorer(clip));
|
||||
}
|
||||
|
||||
eprintln!("[vietc-uinputd] Device '{}' created", name);
|
||||
Ok(Self {
|
||||
fd,
|
||||
saved_clipboard: std::sync::Mutex::new(None),
|
||||
last_injected: std::sync::Mutex::new(None),
|
||||
})
|
||||
Ok(Self { fd, clip })
|
||||
}
|
||||
|
||||
fn send_event(&self, type_: u16, code: u16, value: i32) {
|
||||
|
|
@ -163,20 +200,29 @@ impl UinputDevice {
|
|||
}
|
||||
|
||||
fn paste_unicode(&self, text: &str) {
|
||||
// Save the user's clipboard before we clobber it, unless what is on the
|
||||
// clipboard is our own previously-injected text. This keeps Ctrl+C /
|
||||
// Ctrl+V working: every Vietnamese word is pasted via the clipboard, so
|
||||
// without restoring it the user's copied content would be lost.
|
||||
let current = read_clipboard();
|
||||
// Critical section: snapshot the clipboard, decide what to preserve,
|
||||
// cancel any pending restore so the restorer cannot fire while we are
|
||||
// pasting, and put our word on the clipboard. The read and write happen
|
||||
// under the lock so they can never interleave with the restorer.
|
||||
{
|
||||
let last = self.last_injected.lock().unwrap();
|
||||
let is_our_injection = matches!((¤t, &*last), (Some(c), Some(l)) if c == l);
|
||||
if !is_our_injection {
|
||||
*self.saved_clipboard.lock().unwrap() = current;
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
let current = read_clipboard();
|
||||
let is_our_write =
|
||||
matches!((¤t, &st.last_injected), (Some(c), Some(l)) if c == l);
|
||||
if !is_our_write {
|
||||
// The user changed the clipboard themselves (a real Ctrl+C).
|
||||
st.saved_clipboard = current;
|
||||
}
|
||||
// Cancel any pending restore; the restorer parks until we schedule
|
||||
// a new one after the paste.
|
||||
st.restore_due = None;
|
||||
copy_to_clipboard(text);
|
||||
st.last_injected = Some(text.to_string());
|
||||
}
|
||||
|
||||
copy_to_clipboard(text);
|
||||
// Give the selection owner a moment to take ownership before pasting.
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
|
||||
self.send_key(29, 1);
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
self.send_key(47, 1);
|
||||
|
|
@ -184,17 +230,56 @@ impl UinputDevice {
|
|||
self.send_key(29, 0);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
// Restore the user's clipboard after the paste has been consumed.
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
let saved = self.saved_clipboard.lock().unwrap().clone();
|
||||
let restored = saved.unwrap_or_default();
|
||||
// Schedule a debounced restore. While the user keeps typing this gets
|
||||
// pushed back, so the user's clipboard is only restored once typing
|
||||
// settles — never overwriting our freshly pasted word mid-stream.
|
||||
{
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
st.restore_due = Some(Instant::now() + RESTORE_DEBOUNCE);
|
||||
}
|
||||
self.clip.cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Background thread: once no Unicode paste has happened for `RESTORE_DEBOUNCE`,
|
||||
/// rewrite the user's real clipboard so Ctrl+V keeps working.
|
||||
fn run_restorer(state: Arc<ClipState>) {
|
||||
loop {
|
||||
let mut st = state.inner.lock().unwrap();
|
||||
loop {
|
||||
if st.shutdown {
|
||||
return;
|
||||
}
|
||||
match st.restore_due {
|
||||
None => {
|
||||
st = state.cv.wait(st).unwrap();
|
||||
}
|
||||
Some(due) => {
|
||||
let now = Instant::now();
|
||||
if now >= due {
|
||||
break;
|
||||
}
|
||||
let (guard, _) = state.cv.wait_timeout(st, due - now).unwrap();
|
||||
st = guard;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deadline reached. Restore under the lock so the write cannot
|
||||
// interleave with a concurrent paste's clipboard write.
|
||||
let restored = st.saved_clipboard.clone().unwrap_or_default();
|
||||
copy_to_clipboard(&restored);
|
||||
*self.last_injected.lock().unwrap() = Some(restored);
|
||||
st.last_injected = Some(restored);
|
||||
st.restore_due = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UinputDevice {
|
||||
fn drop(&mut self) {
|
||||
{
|
||||
let mut st = self.clip.inner.lock().unwrap();
|
||||
st.shutdown = true;
|
||||
}
|
||||
self.clip.cv.notify_all();
|
||||
let _ = unsafe { libc::ioctl(self.fd, UI_DEV_DESTROY, 0) };
|
||||
let _ = unsafe { libc::close(self.fd) };
|
||||
eprintln!("[vietc-uinputd] Device destroyed");
|
||||
|
|
|
|||
Loading…
Reference in a new issue