Skip to content

Commit e639951

Browse files
mathix420claude
andauthored
refactor(clipboard): drop marker dance, patch egui-winit instead (#16)
egui-winit on Linux eats Ctrl+V and reads text/plain only, so an image-only clipboard made the keystroke vanish before the PTY (and thus Claude Code) ever saw it. The earlier fix worked around this with a clipboard-polling marker swap that mutated the user's clipboard 6x/sec; this matches alacritty's actual practice instead, where bare Ctrl+V is just forwarded to the PTY as the raw 0x16 byte and the inner TUI does its own wl-paste/xclip image read. - Vendor egui-winit 0.31.1 under egui-winit/ with a one-liner that falls through to a Key event when the clipboard read yields no text, so input.rs's existing Ctrl+letter -> control-byte path kicks in. - Wire it via [patch.crates-io] alongside the existing x11-clipboard pin. - Strip pending_image / IMAGE_PASTE_MARKER / stash_image_and_mark / restore_image / process_clipboard_image_paste and the 150ms poll loop. Net: -110 lines in alacritree/, +3 effective lines of patch in egui-winit/, plus the vendored crate. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0ad8ffc commit e639951

10 files changed

Lines changed: 2632 additions & 115 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ toml_edit = "0.24.0"
2424
# TODO: Validation of fix for #6978. Remove before next release and use released `x11-clipboard`.
2525
[patch.crates-io]
2626
x11-clipboard = { git = "https://github.com/quininer/x11-clipboard.git", rev = "19ab2163cf0bd0db607e827a5214571990307866" }
27+
# Vendored egui-winit with a one-liner so Ctrl+V falls through to a Key event
28+
# when the clipboard isn't text (e.g. holds an image) — matches alacritty's
29+
# raw 0x16 forwarding and lets TUIs like Claude Code do their own wl-paste.
30+
egui-winit = { path = "egui-winit" }

alacritree/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ toml = { workspace = true }
2121
xdg = "3.0.0"
2222
home = "0.5.5"
2323
fontdb = { version = "0.23", default-features = false, features = ["fontconfig", "memmap"] }
24-
arboard = { version = "3", default-features = false, features = ["wayland-data-control", "image-data"] }
24+
arboard = { version = "3", default-features = false, features = ["wayland-data-control"] }
2525
png = "0.17"
2626
# Desktop notifications when a background session rings the bell.
2727
notify-rust = "4"

alacritree/src/app.rs

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,6 @@ pub struct AlacritreeApp {
120120
quit_dialog_open: bool,
121121
pending_delete: Option<DeleteRequest>,
122122
pending_create: Option<CreateState>,
123-
pending_image: Option<clipboard::PendingImage>,
124-
last_clipboard_poll: Option<std::time::Instant>,
125123
notify_rx: Receiver<WorkspaceKey>,
126124
/// Shared across sessions; auto-invalidated when cell size changes.
127125
builtin_glyphs: crate::builtin_font::BuiltinGlyphCache,
@@ -228,8 +226,6 @@ impl AlacritreeApp {
228226
quit_dialog_open: false,
229227
pending_delete: None,
230228
pending_create: None,
231-
pending_image: None,
232-
last_clipboard_poll: None,
233229
notify_rx,
234230
builtin_glyphs: crate::builtin_font::BuiltinGlyphCache::new(),
235231
};
@@ -484,60 +480,6 @@ impl AlacritreeApp {
484480
}
485481
}
486482

487-
/// Two-phase shim around egui_winit's text-only Ctrl+V: while focused we
488-
/// poll the clipboard and swap any image for a sentinel text; on the
489-
/// matching `Event::Paste` we put the image back and write raw 0x16 to the
490-
/// PTY so the inner TUI does its own `wl-paste` read (same effect as
491-
/// alacritty's bare Ctrl+V). Polling rather than reacting to Ctrl-down
492-
/// avoids the compositor-propagation race: smithay-clipboard reads
493-
/// happen-before our new selection is observable otherwise.
494-
fn process_clipboard_image_paste(&mut self, ctx: &Context) {
495-
let (focus_lost, marker_pasted, focused) = ctx.input_mut(|i| {
496-
let mut focus_lost = false;
497-
let mut marker = false;
498-
i.events.retain(|ev| match ev {
499-
egui::Event::WindowFocused(false) => {
500-
focus_lost = true;
501-
true
502-
},
503-
egui::Event::Paste(s) if s == clipboard::IMAGE_PASTE_MARKER => {
504-
marker = true;
505-
false
506-
},
507-
_ => true,
508-
});
509-
(focus_lost, marker, i.viewport().focused.unwrap_or(false))
510-
});
511-
512-
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(150);
513-
if focused {
514-
let now = std::time::Instant::now();
515-
let due =
516-
self.last_clipboard_poll.is_none_or(|t| now.duration_since(t) >= POLL_INTERVAL);
517-
if due {
518-
if let Some(image) = clipboard::stash_image_and_mark() {
519-
self.pending_image = Some(image);
520-
}
521-
self.last_clipboard_poll = Some(now);
522-
}
523-
ctx.request_repaint_after(POLL_INTERVAL);
524-
}
525-
526-
if marker_pasted {
527-
if let Some(image) = self.pending_image.take() {
528-
clipboard::restore_image(&image);
529-
}
530-
if let Some(idx) = self.active_session_index() {
531-
self.sessions[idx].write(vec![0x16]);
532-
}
533-
}
534-
if focus_lost {
535-
if let Some(image) = self.pending_image.take() {
536-
clipboard::restore_image(&image);
537-
}
538-
}
539-
}
540-
541483
fn dispatch_action(&mut self, ctx: &Context, action: BindingAction) {
542484
match action {
543485
BindingAction::Chars(bytes) => {
@@ -1885,7 +1827,6 @@ impl eframe::App for AlacritreeApp {
18851827
}
18861828

18871829
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
1888-
self.process_clipboard_image_paste(ctx);
18891830
let modal_open = self.is_modal_open();
18901831
if !modal_open {
18911832
self.handle_shortcuts(ctx);

alacritree/src/clipboard.rs

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
#[cfg(target_os = "linux")]
88
use arboard::{GetExtLinux, LinuxClipboardKind, SetExtLinux};
99

10-
/// Sentinel text that smuggles a Ctrl+V keystroke past egui_winit's
11-
/// text-only paste handler when the real clipboard holds an image.
12-
pub const IMAGE_PASTE_MARKER: &str = "\u{1}alacritree-image-paste\u{1}";
13-
1410
#[derive(Copy, Clone, Debug)]
1511
pub enum Target {
1612
/// `Ctrl+V` clipboard.
@@ -67,52 +63,3 @@ pub fn read(target: Target) -> Option<String> {
6763
},
6864
}
6965
}
70-
71-
#[derive(Clone)]
72-
pub struct PendingImage {
73-
pub width: usize,
74-
pub height: usize,
75-
pub bytes: Vec<u8>,
76-
}
77-
78-
pub fn stash_image_and_mark() -> Option<PendingImage> {
79-
let mut clip = arboard::Clipboard::new().ok()?;
80-
if let Ok(text) = clip.get_text() {
81-
if !text.is_empty() {
82-
return None;
83-
}
84-
}
85-
let image = match clip.get_image() {
86-
Ok(img) => img,
87-
Err(arboard::Error::ContentNotAvailable) => return None,
88-
Err(e) => {
89-
log::warn!("clipboard image read failed: {e}");
90-
return None;
91-
},
92-
};
93-
let stash =
94-
PendingImage { width: image.width, height: image.height, bytes: image.bytes.into_owned() };
95-
if let Err(e) = clip.set_text(IMAGE_PASTE_MARKER.to_owned()) {
96-
log::warn!("clipboard marker write failed: {e}");
97-
return None;
98-
}
99-
Some(stash)
100-
}
101-
102-
pub fn restore_image(image: &PendingImage) {
103-
let mut clip = match arboard::Clipboard::new() {
104-
Ok(c) => c,
105-
Err(e) => {
106-
log::warn!("clipboard unavailable for image restore: {e}");
107-
return;
108-
},
109-
};
110-
let res = clip.set_image(arboard::ImageData {
111-
width: image.width,
112-
height: image.height,
113-
bytes: std::borrow::Cow::Borrowed(&image.bytes),
114-
});
115-
if let Err(e) = res {
116-
log::warn!("clipboard image restore failed: {e}");
117-
}
118-
}

0 commit comments

Comments
 (0)