From 3d511bbd67060fbf4e1081dd8931a1dd121c96a7 Mon Sep 17 00:00:00 2001 From: Stefan Siegel Date: Thu, 18 Apr 2024 04:36:15 +0200 Subject: [PATCH] Accept drag and drop of URLs from browsers and plain text on X11 For filenames and urls an additional space is inserted after the last item to enable adding more files and urls with another drag-and-drop operation without the need to manually enter the space in between. --- wezterm-gui/src/termwindow/mod.rs | 25 ++++++++++++- window/examples/async.rs | 2 + window/src/lib.rs | 7 ++++ window/src/os/x11/connection.rs | 3 ++ window/src/os/x11/window.rs | 62 +++++++++++++++++++++++++++++-- 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index c92e5a27123..0ba025e422a 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -1011,6 +1011,28 @@ impl TermWindow { } Ok(true) } + WindowEvent::DroppedString(text) => { + let pane = match self.get_active_pane_or_overlay() { + Some(pane) => pane, + None => return Ok(true), + }; + pane.send_paste(text.as_str())?; + Ok(true) + } + WindowEvent::DroppedUrl(urls) => { + let pane = match self.get_active_pane_or_overlay() { + Some(pane) => pane, + None => return Ok(true), + }; + let urls = urls + .iter() + .map(|url| self.config.quote_dropped_files.escape(&url.to_string())) + .collect::>() + .join(" ") + + " "; + pane.send_paste(urls.as_str())?; + Ok(true) + } WindowEvent::DroppedFile(paths) => { let pane = match self.get_active_pane_or_overlay() { Some(pane) => pane, @@ -1024,7 +1046,8 @@ impl TermWindow { .escape(&path.to_string_lossy()) }) .collect::>() - .join(" "); + .join(" ") + + " "; pane.send_paste(&paths)?; Ok(true) } diff --git a/window/examples/async.rs b/window/examples/async.rs index a42b5040c6c..419e7a200ac 100644 --- a/window/examples/async.rs +++ b/window/examples/async.rs @@ -85,6 +85,8 @@ impl MyWindow { | WindowEvent::FocusChanged(_) | WindowEvent::DraggedFile(_) | WindowEvent::DroppedFile(_) + | WindowEvent::DroppedUrl(_) + | WindowEvent::DroppedString(_) | WindowEvent::PerformKeyAssignment(_) | WindowEvent::MouseLeave | WindowEvent::SetInnerSizeCompleted => {} diff --git a/window/src/lib.rs b/window/src/lib.rs index ac97cc1dcab..29c670b32de 100644 --- a/window/src/lib.rs +++ b/window/src/lib.rs @@ -7,6 +7,7 @@ use std::any::Any; use std::path::PathBuf; use std::rc::Rc; use thiserror::Error; +use url::Url; pub mod bitmaps; pub use wezterm_color_types as color; mod configuration; @@ -205,6 +206,12 @@ pub enum WindowEvent { // Called when the files are dropped into the window DroppedFile(Vec), + // Called when urls are dropped into the window + DroppedUrl(Vec), + + // Called when text is dropped into the window + DroppedString(String), + /// Called by menubar dispatching stuff on some systems PerformKeyAssignment(config::keyassignment::KeyAssignment), diff --git a/window/src/os/x11/connection.rs b/window/src/os/x11/connection.rs index 5e55ca98700..0a11d75d583 100644 --- a/window/src/os/x11/connection.rs +++ b/window/src/os/x11/connection.rs @@ -35,6 +35,7 @@ pub struct XConnection { pub atom_targets: Atom, pub atom_clipboard: Atom, pub atom_texturilist: Atom, + pub atom_xmozurl: Atom, pub atom_xdndaware: Atom, pub atom_xdndtypelist: Atom, pub atom_xdndselection: Atom, @@ -626,6 +627,7 @@ impl XConnection { let atom_targets = Self::intern_atom(&conn, "TARGETS")?; let atom_clipboard = Self::intern_atom(&conn, "CLIPBOARD")?; let atom_texturilist = Self::intern_atom(&conn, "text/uri-list")?; + let atom_xmozurl = Self::intern_atom(&conn, "text/x-moz-url")?; let atom_xdndaware = Self::intern_atom(&conn, "XdndAware")?; let atom_xdndtypelist = Self::intern_atom(&conn, "XdndTypeList")?; let atom_xdndselection = Self::intern_atom(&conn, "XdndSelection")?; @@ -762,6 +764,7 @@ impl XConnection { atom_protocols, atom_clipboard, atom_texturilist, + atom_xmozurl, atom_xdndaware, atom_xdndtypelist, atom_xdndselection, diff --git a/window/src/os/x11/window.rs b/window/src/os/x11/window.rs index 299415f4367..18024097a3a 100644 --- a/window/src/os/x11/window.rs +++ b/window/src/os/x11/window.rs @@ -576,10 +576,17 @@ impl XWindowInner { }; } self.drag_and_drop.target_type = xcb::x::ATOM_NONE; - for t in &self.drag_and_drop.src_types { - if *t == conn.atom_texturilist { - self.drag_and_drop.target_type = conn.atom_texturilist; + for t in [ + conn.atom_texturilist, + conn.atom_xmozurl, + conn.atom_utf8_string, + ] { + if self.drag_and_drop.src_types.contains(&t) { + self.drag_and_drop.target_type = t; + break; } + } + for t in &self.drag_and_drop.src_types { log::trace!("types offered: {}", conn.atom_name(*t)); } log::trace!( @@ -1105,7 +1112,54 @@ impl XWindowInner { long_length: u32::max_value(), }) { Ok(prop) => { - if selection.target() == conn.atom_texturilist { + if selection.target() == conn.atom_utf8_string { + let text = String::from_utf8_lossy(prop.value()).to_string(); + self.events.dispatch(WindowEvent::DroppedString(text)); + } else if selection.target() == conn.atom_xmozurl { + let raw = prop.value(); + let data; + if raw.len() >= 2 + && ((raw[0], raw[1]) == (0xfe, 0xff) + || (raw[0] != 0x00 && raw[1] == 0x00)) + { + data = String::from_utf16_lossy( + raw.chunks_exact(2) + .map(|x: &[u8]| u16::from(x[1]) << 8 | u16::from(x[0])) + .collect::>() + .as_slice(), + ); + } else if raw.len() >= 2 + && ((raw[0], raw[1]) == (0xff, 0xfe) + || (raw[0] == 0x00 && raw[1] != 0x00)) + { + data = String::from_utf16_lossy( + raw.chunks_exact(2) + .map(|x: &[u8]| u16::from(x[0]) << 8 | u16::from(x[1])) + .collect::>() + .as_slice(), + ); + } else { + data = String::from_utf8_lossy(prop.value()).to_string(); + } + use url::Url; + let urls = data + .lines() + .step_by(2) + .filter_map(|line| { + // the lines alternate between the urls and their titles + Url::parse(line) + .map_err(|err| { + log::error!( + "Error parsing dropped file line {} as url: {:#}", + line, + err + ); + }) + .ok() + }) + .collect::>(); + self.events.dispatch(WindowEvent::DroppedUrl(urls)); + } else if selection.target() == conn.atom_texturilist { let paths = String::from_utf8_lossy(prop.value()) .lines() .filter_map(|line| {