feat: implement Backspace-Replay pattern for perfect engine sync

- Add Engine::replay_keystrokes() — creates fresh engine and replays
  all keystrokes to compute correct screen output from scratch
- Add Daemon::replay_and_inject() — tracks keystroke history and
  screen output, computes diff (backspaces + new text) on each keypress
- Add Daemon::replay_backspace() — pops from history, replays, diffs
- Handle flush chars (space, period, etc.) separately — commit word,
  type char, clear history
- Add FocusIn/FocusOut detection for engine reset on focus loss
- Add CPU pinning (P-cores 0-3) + nice(-10) priority boost
- Clean up 9 dead code warnings (unused fields, constants, types)
- Add replay_keystrokes tests for Telex, VNI, and backspace
- 255 tests pass (was 252)
This commit is contained in:
Khoa Vo 2026-06-26 08:40:38 +07:00
parent bb0847a38f
commit 3858aa955c
4 changed files with 338 additions and 39 deletions

View file

@ -8,6 +8,31 @@ use std::time::Duration;
use vietc_engine::{Engine, EngineEvent, InputMethod}; use vietc_engine::{Engine, EngineEvent, InputMethod};
/// Pin current thread to performance cores (0-3) and boost priority.
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
fn boost_thread_priority() {
unsafe {
// Set nice value to -10 (higher priority than normal)
libc::setpriority(libc::PRIO_PROCESS, 0, -10);
// Try to pin to P-cores (cores 0-3 on Intel hybrid)
#[cfg(target_os = "linux")]
{
let mut cpuset: libc::cpu_set_t = std::mem::zeroed();
// Pin to cores 0-3 (P-cores on Intel 12th gen+)
for i in 0..4 {
libc::CPU_SET(i, &mut cpuset);
}
let ret = libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &cpuset);
if ret == 0 {
eprintln!("[vietc] Pinned to P-cores 0-3, nice=-10");
} else {
eprintln!("[vietc] CPU pinning failed ({}), nice=-10 still set", ret);
}
}
}
}
mod app_state; mod app_state;
mod config; mod config;
mod display; mod display;
@ -84,6 +109,13 @@ struct Daemon {
app_state: AppStateManager, app_state: AppStateManager,
engine_enabled: Arc<AtomicBool>, engine_enabled: Arc<AtomicBool>,
grab_enabled: bool, grab_enabled: bool,
/// Backspace-Replay: all keystrokes in the current word being composed.
/// On each keypress, we replay the entire history through a fresh engine
/// to compute the correct screen output, eliminating state desync.
keystroke_history: Vec<char>,
/// What's currently displayed on screen for the current word.
/// Used to calculate how many backspaces we need before retyping.
screen_output: String,
} }
impl Daemon { impl Daemon {
@ -120,6 +152,8 @@ impl Daemon {
config_modified, config_modified,
app_state, app_state,
engine_enabled, engine_enabled,
keystroke_history: Vec::new(),
screen_output: String::new(),
} }
} }
@ -307,6 +341,130 @@ impl Daemon {
self.app_state.is_current_app_bypassed() self.app_state.is_current_app_bypassed()
} }
/// Backspace-Replay: replay the entire keystroke history through a fresh
/// engine, compute what should be on screen, and return the commands
/// (backspaces to erase old + new text to type).
fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
let mut commands = Vec::new();
// Flush characters: commit current word, type the character, clear state
if is_flush_char(ch) {
if !self.screen_output.is_empty() {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.screen_output.clone()));
}
// Type the flush character itself
commands.push(OutputCommand::Type(ch.to_string()));
self.keystroke_history.clear();
self.screen_output.clear();
return commands;
}
// Add the new keystroke to history
self.keystroke_history.push(ch);
// Replay through fresh engine
let method = match self.config.input_method.as_str() {
"vni" => InputMethod::Vni,
_ => InputMethod::Telex,
};
let (new_output, did_flush) = Engine::replay_keystrokes(
method,
&self.config.macros,
&self.keystroke_history,
);
log_info(&format!(
"[vietc] replay: history_len={} old_screen='{}' new_output='{}' flush={}",
self.keystroke_history.len(),
self.screen_output,
new_output,
did_flush
));
if did_flush {
// Engine flushed a word — commit it and clear state
// The flush char (space/period/etc) was NOT in history, so we need to
// type whatever was on screen + the flush char
if !self.screen_output.is_empty() {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.screen_output.clone()));
}
self.keystroke_history.clear();
self.screen_output.clear();
return commands;
}
if new_output != self.screen_output {
let backspaces = self.screen_output.chars().count();
if backspaces > 0 {
commands.push(OutputCommand::Backspace(backspaces));
}
if !new_output.is_empty() {
commands.push(OutputCommand::Type(new_output.clone()));
}
self.screen_output = new_output;
}
commands
}
/// Backspace-Replay: pop from history, replay, and return commands to fix screen.
fn replay_backspace(&mut self) -> Vec<OutputCommand> {
let mut commands = Vec::new();
if self.keystroke_history.is_empty() {
// Nothing in history — just forward the backspace
commands.push(OutputCommand::Backspace(1));
return commands;
}
// Remove last keystroke from history
self.keystroke_history.pop();
// Replay through fresh engine
let method = match self.config.input_method.as_str() {
"vni" => InputMethod::Vni,
_ => InputMethod::Telex,
};
let (new_output, _) = if self.keystroke_history.is_empty() {
(String::new(), false)
} else {
Engine::replay_keystrokes(
method,
&self.config.macros,
&self.keystroke_history,
)
};
log_info(&format!(
"[vietc] replay_backspace: history_len={} old_screen='{}' new_output='{}'",
self.keystroke_history.len(),
self.screen_output,
new_output
));
// Calculate diff
let backspaces = self.screen_output.chars().count();
if backspaces > 0 {
commands.push(OutputCommand::Backspace(backspaces));
}
if !new_output.is_empty() {
commands.push(OutputCommand::Type(new_output.clone()));
}
self.screen_output = new_output;
commands
}
/// Reset the replay state (on flush, focus loss, modifier key, etc.)
fn replay_reset(&mut self) {
self.keystroke_history.clear();
self.screen_output.clear();
}
fn check_app_change_with(&mut self, new_class: String) { fn check_app_change_with(&mut self, new_class: String) {
if let Some(should_enable) = self.app_state.update_with_app(new_class) { if let Some(should_enable) = self.app_state.update_with_app(new_class) {
self.engine.set_enabled(should_enable); self.engine.set_enabled(should_enable);
@ -321,6 +479,11 @@ enum OutputCommand {
Backspace(usize), Backspace(usize),
} }
/// Characters that flush the current word and start a new one.
fn is_flush_char(ch: char) -> bool {
matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n')
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let config_path = config::find_config_path(); let config_path = config::find_config_path();
let config = Config::load()?; let config = Config::load()?;
@ -353,6 +516,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
)); ));
// Boost thread priority for low-latency input (VMK technique)
boost_thread_priority();
// Spawn background monitor for active window, config changes, and status changes // Spawn background monitor for active window, config changes, and status changes
let shared_active_window = Arc::new(Mutex::new(String::new())); let shared_active_window = Arc::new(Mutex::new(String::new()));
let config_changed = Arc::new(AtomicBool::new(false)); let config_changed = Arc::new(AtomicBool::new(false));
@ -560,7 +726,7 @@ fn run_with_x11(
if active_window != last_active_window { if active_window != last_active_window {
log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window)); log_info(&format!("[vietc] Window changed: '{}' -> '{}'", last_active_window, active_window));
last_active_window = active_window.clone(); last_active_window = active_window.clone();
daemon.engine.reset(); daemon.replay_reset();
} }
} }
@ -569,9 +735,17 @@ fn run_with_x11(
daemon.check_app_change_with(active_window); daemon.check_app_change_with(active_window);
} }
// Reset on focus loss (VMK technique)
if capture.focus_lost {
eprintln!("[vietc] Focus lost — resetting engine state");
daemon.replay_reset();
pressed_keys.clear();
capture.focus_lost = false;
}
while let Some(event) = capture.next_event() { while let Some(event) = capture.next_event() {
if event.pressed { if event.pressed {
// Skip autorepeat — key is already tracked as held // Skip autorepeat
if !pressed_keys.insert(event.keycode) { if !pressed_keys.insert(event.keycode) {
continue; continue;
} }
@ -580,54 +754,53 @@ fn run_with_x11(
if let Some(' ') = event.ch { if let Some(' ') = event.ch {
if (event.state & 4) != 0 { if (event.state & 4) != 0 {
pressed_keys.remove(&event.keycode); pressed_keys.remove(&event.keycode);
daemon.replay_reset();
daemon.toggle(); daemon.toggle();
continue; continue;
} }
} }
// Modifier or non-character key → forward press only // Modifier or non-character key → forward press only, reset replay
if capture.is_modifier_pressed(event.state) || event.ch.is_none() { if capture.is_modifier_pressed(event.state) || event.ch.is_none() {
daemon.engine.reset(); daemon.replay_reset();
capture.without_grab(|| { capture.without_grab(|| {
let _ = injector.send_key_event(event.keycode as u16, 1); let _ = injector.send_key_event(event.keycode as u16, 1);
}); });
continue; continue;
} }
// Character key // Character key — use Backspace-Replay
if let Some(ch) = event.ch { if let Some(ch) = event.ch {
match ch { match ch {
'\x08' => { '\x08' => {
daemon.engine.process_key('\x08'); // Backspace: replay pattern pops from history
let commands = daemon.replay_backspace();
pressed_keys.remove(&event.keycode);
capture.without_grab(|| {
execute_commands(&*injector, &commands, true);
});
// If history is empty and commands only had a bare backspace,
// we need to actually send it
if daemon.keystroke_history.is_empty() && commands.is_empty() {
capture.without_grab(|| { capture.without_grab(|| {
let _ = injector.send_backspace(); let _ = injector.send_backspace();
}); });
// Keep in pressed_keys so release is forwarded }
} }
'\n' => { '\n' => {
pressed_keys.remove(&event.keycode); pressed_keys.remove(&event.keycode);
daemon.engine.reset(); daemon.replay_reset();
capture.without_grab(|| { capture.without_grab(|| {
let _ = injector.send_key_event(event.keycode as u16, 1); let _ = injector.send_key_event(event.keycode as u16, 1);
let _ = injector.send_key_event(event.keycode as u16, 0); let _ = injector.send_key_event(event.keycode as u16, 0);
}); });
} }
_ => { _ => {
let commands = daemon.process_key(ch); let commands = daemon.replay_and_inject(ch);
if !commands.is_empty() {
// Engine consumed the key; remove from tracking
pressed_keys.remove(&event.keycode); pressed_keys.remove(&event.keycode);
capture.without_grab(|| { capture.without_grab(|| {
execute_commands(&*injector, &commands, true); execute_commands(&*injector, &commands, true);
}); });
} else {
// Engine started composing; forward press+release immediately
pressed_keys.remove(&event.keycode);
capture.without_grab(|| {
let _ = injector.send_key_event(event.keycode as u16, 1);
let _ = injector.send_key_event(event.keycode as u16, 0);
});
}
} }
} }
} }

View file

@ -92,6 +92,72 @@ impl Engine {
event event
} }
/// Replay a sequence of keystrokes through a fresh engine and return the
/// final screen output. This is the core of the Backspace-Replay pattern:
/// instead of tracking incremental state, we always recompute from scratch.
/// Returns (output_on_screen, did_flush).
/// `did_flush` means the engine processed a word boundary and the cursor
/// is now at a clean position — caller should clear keystroke history.
pub fn replay_keystrokes(
method: InputMethod,
macros: &std::collections::HashMap<String, String>,
keystrokes: &[char],
) -> (String, bool) {
let mut engine = Engine::new(method);
for (shortcut, expansion) in macros {
engine.add_macro(shortcut.clone(), expansion.clone());
}
let mut last_output = String::new();
let mut did_flush = false;
for &ch in keystrokes {
if let Some(event) = engine.process_key(ch) {
match event {
EngineEvent::Replace { insert, .. } => {
last_output = insert;
}
EngineEvent::Flush(_word) => {
// Word was flushed. The flush char is NOT part of the word.
// The word is committed; clear tracking for current composing.
last_output.clear();
did_flush = true;
}
EngineEvent::Insert(text) => {
last_output = text;
}
EngineEvent::UndoTones { restored, .. } => {
last_output = restored;
}
EngineEvent::Paste(text) => {
last_output = text;
}
EngineEvent::AutoRestore(word) => {
last_output = word;
}
}
} else {
// Key consumed but no screen change — buffer is building
let buf = engine.buffer().to_string();
if !buf.is_empty() {
last_output = buf;
}
}
}
// If the engine has a buffer that hasn't been flushed, that's on screen
let buf = engine.buffer().to_string();
if !buf.is_empty() {
last_output = buf;
did_flush = false; // Still composing
} else if did_flush {
// After flush, nothing is on screen for the composing word
last_output.clear();
}
(last_output, did_flush)
}
/// Update buffer with pasted text for subsequent edit operations (delete/backspace) /// Update buffer with pasted text for subsequent edit operations (delete/backspace)
pub fn update_with_pasted_text(&mut self, text: &str) { pub fn update_with_pasted_text(&mut self, text: &str) {
self.raw_buffer.clear(); self.raw_buffer.clear();
@ -497,4 +563,63 @@ mod tests {
panic!("Expected Replace event, got {:?}", event2); panic!("Expected Replace event, got {:?}", event2);
} }
} }
#[test]
fn test_replay_keystrokes_telex() {
let macros = std::collections::HashMap::new();
// Replay "chao" -> should produce "chao" (no tone yet)
let (output, flush) = Engine::replay_keystrokes(
InputMethod::Telex,
&macros,
&['c', 'h', 'a', 'o'],
);
assert_eq!(output, "chao");
assert!(!flush);
// Replay "chaos" -> s adds acute accent: "cháo"
let (output, flush) = Engine::replay_keystrokes(
InputMethod::Telex,
&macros,
&['c', 'h', 'a', 'o', 's'],
);
assert_eq!(output, "cháo");
assert!(!flush);
// Replay "chaof" -> f adds grave accent: "chào"
let (output, flush) = Engine::replay_keystrokes(
InputMethod::Telex,
&macros,
&['c', 'h', 'a', 'o', 'f'],
);
assert_eq!(output, "chào");
assert!(!flush);
}
#[test]
fn test_replay_keystrokes_backspace() {
let macros = std::collections::HashMap::new();
// Replay "chaos" then backspace -> engine pops 'o' from "cháo" → "chá"
let (output, _) = Engine::replay_keystrokes(
InputMethod::Telex,
&macros,
&['c', 'h', 'a', 'o', 's', '\x08'],
);
assert_eq!(output, "chá");
}
#[test]
fn test_replay_keystrokes_vni() {
let macros = std::collections::HashMap::new();
// VNI: "chao1" → acute accent on last vowel
let (output, _) = Engine::replay_keystrokes(
InputMethod::Vni,
&macros,
&['c', 'h', 'a', 'o', '1'],
);
// Verify it produces accented output (engine applies tone to last vowel)
assert!(output.contains('á') || output.contains('ó'), "Expected toned output, got: {}", output);
}
} }

View file

@ -2,15 +2,15 @@ use std::ffi::{c_char, c_int, c_void};
type Display = c_void; type Display = c_void;
type Window = u64; type Window = u64;
type XID = u64;
type Time = u64; type Time = u64;
// X11 event types // X11 event types
const KEY_PRESS: c_int = 2; const KEY_PRESS: c_int = 2;
const KEY_RELEASE: c_int = 3; const KEY_RELEASE: c_int = 3;
const FOCUS_IN: c_int = 9;
const FOCUS_OUT: c_int = 10;
// X11 modifier masks // X11 modifier masks
const SHIFT_MASK: c_int = 1;
const CONTROL_MASK: c_int = 4; const CONTROL_MASK: c_int = 4;
const MOD1_MASK: c_int = 8; // Alt const MOD1_MASK: c_int = 8; // Alt
const MOD4_MASK: c_int = 64; // Super/Win const MOD4_MASK: c_int = 64; // Super/Win
@ -33,7 +33,6 @@ struct X11Lib {
x_ungrab_keyboard: unsafe extern "C" fn(*mut Display, Time) -> c_int, x_ungrab_keyboard: unsafe extern "C" fn(*mut Display, Time) -> c_int,
x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent), x_next_event: unsafe extern "C" fn(*mut Display, *mut XEvent),
x_lookup_string: unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int, x_lookup_string: unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int,
x_keysym_to_keycode: unsafe extern "C" fn(*mut Display, KeySym) -> u32,
x_utf8_lookup_string: Option<unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>, x_utf8_lookup_string: Option<unsafe extern "C" fn(*mut XKeyEvent, *mut c_char, c_int, *mut KeySym, *mut c_int) -> c_int>,
x_flush: unsafe extern "C" fn(*mut Display) -> c_int, x_flush: unsafe extern "C" fn(*mut Display) -> c_int,
} }
@ -69,7 +68,6 @@ impl X11Lib {
let x_ungrab_keyboard = sym!("XUngrabKeyboard"); let x_ungrab_keyboard = sym!("XUngrabKeyboard");
let x_next_event = sym!("XNextEvent"); let x_next_event = sym!("XNextEvent");
let x_lookup_string = sym!("XLookupString"); let x_lookup_string = sym!("XLookupString");
let x_keysym_to_keycode = sym!("XKeysymToKeycode");
let x_utf8_lookup_string = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char); let x_utf8_lookup_string = dlsym(handle, b"Xutf8LookupString\0".as_ptr() as *const c_char);
let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() { let x_utf8_lookup_string = if x_utf8_lookup_string.is_null() {
None None
@ -87,7 +85,6 @@ impl X11Lib {
x_ungrab_keyboard, x_ungrab_keyboard,
x_next_event, x_next_event,
x_lookup_string, x_lookup_string,
x_keysym_to_keycode,
x_utf8_lookup_string, x_utf8_lookup_string,
x_flush, x_flush,
}) })
@ -149,7 +146,8 @@ pub struct X11Capture {
display: *mut Display, display: *mut Display,
root: Window, root: Window,
grabbed: bool, grabbed: bool,
event_buf: Vec<u8>, /// Set to true when FocusOut is received — caller should reset engine state
pub focus_lost: bool,
} }
unsafe impl Send for X11Capture {} unsafe impl Send for X11Capture {}
@ -178,7 +176,7 @@ impl X11Capture {
display, display,
root, root,
grabbed: false, grabbed: false,
event_buf: Vec::new(), focus_lost: false,
}) })
} }
} }
@ -225,6 +223,17 @@ impl X11Capture {
} }
let _type = event._type; let _type = event._type;
// Handle FocusIn/FocusOut — reset engine state when focus changes
if _type == FOCUS_OUT {
self.focus_lost = true;
return self.next_event();
}
if _type == FOCUS_IN {
self.focus_lost = false;
return self.next_event();
}
if _type != KEY_PRESS && _type != KEY_RELEASE { if _type != KEY_PRESS && _type != KEY_RELEASE {
return self.next_event(); return self.next_event();
} }

View file

@ -16,7 +16,6 @@ extern "C" {
const CURRENT_TIME: Time = 0; const CURRENT_TIME: Time = 0;
const PROP_MODE_REPLACE: c_int = 0; const PROP_MODE_REPLACE: c_int = 0;
const NO_EVENT_MASK: i64 = 0; const NO_EVENT_MASK: i64 = 0;
const INPUT_OUTPUT: c_int = 1;
const COPY_FROM_PARENT: Window = 0; const COPY_FROM_PARENT: Window = 0;
const SELECTION_REQUEST: c_int = 30; const SELECTION_REQUEST: c_int = 30;
@ -34,7 +33,6 @@ struct X11Lib {
x_intern_atom: unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> Atom, x_intern_atom: unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> Atom,
x_set_selection_owner: unsafe extern "C" fn(*mut Display, Atom, Window, Time) -> c_int, x_set_selection_owner: unsafe extern "C" fn(*mut Display, Atom, Window, Time) -> c_int,
x_change_property: unsafe extern "C" fn(*mut Display, Window, Atom, Atom, c_int, c_int, *const c_void, c_int) -> c_int, x_change_property: unsafe extern "C" fn(*mut Display, Window, Atom, Atom, c_int, c_int, *const c_void, c_int) -> c_int,
x_get_selection_owner: unsafe extern "C" fn(*mut Display, Atom) -> Window,
x_send_event: unsafe extern "C" fn(*mut Display, Window, c_int, i64, *const c_void) -> c_int, x_send_event: unsafe extern "C" fn(*mut Display, Window, c_int, i64, *const c_void) -> c_int,
x_create_simple_window: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, c_int, c_int, Atom, Atom) -> Window, x_create_simple_window: unsafe extern "C" fn(*mut Display, Window, c_int, c_int, c_int, c_int, c_int, Atom, Atom) -> Window,
x_map_window: unsafe extern "C" fn(*mut Display, Window) -> c_int, x_map_window: unsafe extern "C" fn(*mut Display, Window) -> c_int,
@ -90,7 +88,6 @@ impl X11Lib {
let x_intern_atom = sym!(x11_handle, "XInternAtom"); let x_intern_atom = sym!(x11_handle, "XInternAtom");
let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner"); let x_set_selection_owner = sym!(x11_handle, "XSetSelectionOwner");
let x_change_property = sym!(x11_handle, "XChangeProperty"); let x_change_property = sym!(x11_handle, "XChangeProperty");
let x_get_selection_owner = sym!(x11_handle, "XGetSelectionOwner");
let x_send_event = sym!(x11_handle, "XSendEvent"); let x_send_event = sym!(x11_handle, "XSendEvent");
let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow"); let x_create_simple_window = sym!(x11_handle, "XCreateSimpleWindow");
let x_map_window = sym!(x11_handle, "XMapWindow"); let x_map_window = sym!(x11_handle, "XMapWindow");
@ -110,7 +107,6 @@ impl X11Lib {
x_intern_atom, x_intern_atom,
x_set_selection_owner, x_set_selection_owner,
x_change_property, x_change_property,
x_get_selection_owner,
x_send_event, x_send_event,
x_create_simple_window, x_create_simple_window,
x_map_window, x_map_window,
@ -131,8 +127,6 @@ impl Drop for X11Lib {
} }
} }
const X11_KEYCODE_OFFSET: u32 = 8;
fn char_to_keycode(ch: char) -> Option<(u32, bool)> { fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
match ch { match ch {
'a' => Some((30, false)), 'a' => Some((30, false)),
@ -215,7 +209,6 @@ struct XEvent {
pub struct X11Injector { pub struct X11Injector {
lib: X11Lib, lib: X11Lib,
display: *mut Display, display: *mut Display,
root: Window,
clipboard_window: Window, clipboard_window: Window,
atom_clipboard: Atom, atom_clipboard: Atom,
atom_utf8: Atom, atom_utf8: Atom,
@ -251,7 +244,6 @@ impl X11Injector {
Ok(Self { Ok(Self {
lib, lib,
display, display,
root,
clipboard_window, clipboard_window,
atom_clipboard, atom_clipboard,
atom_utf8, atom_utf8,