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 8398555ebb9..0a11d75d583 100644 --- a/window/src/os/x11/connection.rs +++ b/window/src/os/x11/connection.rs @@ -34,6 +34,22 @@ pub struct XConnection { pub atom_xsel_data: Atom, 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, + pub atom_xdndenter: Atom, + pub atom_xdndposition: Atom, + pub atom_xdndstatus: Atom, + pub atom_xdndleave: Atom, + pub atom_xdnddrop: Atom, + pub atom_xdndfinished: Atom, + pub atom_xdndactioncopy: Atom, + pub atom_xdndactionmove: Atom, + pub atom_xdndactionlink: Atom, + pub atom_xdndactionask: Atom, + pub atom_xdndactionprivate: Atom, pub atom_gtk_edge_constraints: Atom, pub atom_xsettings_selection: Atom, pub atom_xsettings_settings: Atom, @@ -610,6 +626,22 @@ impl XConnection { let atom_xsel_data = Self::intern_atom(&conn, "XSEL_DATA")?; 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")?; + let atom_xdndenter = Self::intern_atom(&conn, "XdndEnter")?; + let atom_xdndposition = Self::intern_atom(&conn, "XdndPosition")?; + let atom_xdndstatus = Self::intern_atom(&conn, "XdndStatus")?; + let atom_xdndleave = Self::intern_atom(&conn, "XdndLeave")?; + let atom_xdnddrop = Self::intern_atom(&conn, "XdndDrop")?; + let atom_xdndfinished = Self::intern_atom(&conn, "XdndFinished")?; + let atom_xdndactioncopy = Self::intern_atom(&conn, "XdndActionCopy")?; + let atom_xdndactionmove = Self::intern_atom(&conn, "XdndActionMove")?; + let atom_xdndactionlink = Self::intern_atom(&conn, "XdndActionLink")?; + let atom_xdndactionask = Self::intern_atom(&conn, "XdndActionAsk")?; + let atom_xdndactionprivate = Self::intern_atom(&conn, "XdndActionPrivate")?; let atom_gtk_edge_constraints = Self::intern_atom(&conn, "_GTK_EDGE_CONSTRAINTS")?; let atom_xsettings_selection = Self::intern_atom(&conn, &format!("_XSETTINGS_S{}", screen_num))?; @@ -731,6 +763,22 @@ impl XConnection { xrm: RefCell::new(xrm), atom_protocols, atom_clipboard, + atom_texturilist, + atom_xmozurl, + atom_xdndaware, + atom_xdndtypelist, + atom_xdndselection, + atom_xdndenter, + atom_xdndposition, + atom_xdndstatus, + atom_xdndleave, + atom_xdnddrop, + atom_xdndfinished, + atom_xdndactioncopy, + atom_xdndactionmove, + atom_xdndactionlink, + atom_xdndactionask, + atom_xdndactionprivate, atom_gtk_edge_constraints, atom_xsettings_selection, atom_xsettings_settings, diff --git a/window/src/os/x11/window.rs b/window/src/os/x11/window.rs index b66da14fa82..18024097a3a 100644 --- a/window/src/os/x11/window.rs +++ b/window/src/os/x11/window.rs @@ -57,6 +57,28 @@ impl CopyAndPaste { } } +struct DragAndDrop { + src_window: Option, + src_types: Vec, + src_action: Atom, + time: u32, + target_type: Atom, + target_action: Atom, +} + +impl Default for DragAndDrop { + fn default() -> DragAndDrop { + DragAndDrop { + src_window: None, + src_types: Vec::new(), + src_action: xcb::x::ATOM_NONE, + time: 0, + target_type: xcb::x::ATOM_NONE, + target_action: xcb::x::ATOM_NONE, + } + } +} + pub(crate) struct XWindowInner { pub window_id: xcb::x::Window, conn: Weak, @@ -67,6 +89,7 @@ pub(crate) struct XWindowInner { dpi: f64, cursors: CursorInfo, copy_and_paste: CopyAndPaste, + drag_and_drop: DragAndDrop, config: ConfigHandle, appearance: Appearance, title: String, @@ -516,6 +539,128 @@ impl XWindowInner { Ok(()) } + fn xdnd_event(&mut self, msgtype: Atom, data: &[u32]) -> anyhow::Result<()> { + use xcb::XidNew; + let conn = self.conn(); + let msgtype_name = conn.atom_name(msgtype); + let srcwin = unsafe { xcb::x::Window::new(data[0]) }; + if msgtype == conn.atom_xdndenter { + self.drag_and_drop.src_window = Some(srcwin); + let moretypes = data[1] & 0x01 != 0; + let xdndversion = data[1] >> 24 as u8; + log::trace!("ClientMessage {msgtype_name}, Version {xdndversion}, more than 3 types: {moretypes}"); + if !moretypes { + self.drag_and_drop.src_types = data[2..] + .into_iter() + .filter(|&&x| x != 0) + .map(|&x| unsafe { Atom::new(x) }) + .collect(); + } else { + self.drag_and_drop.src_types = + match conn.send_and_wait_request(&xcb::x::GetProperty { + delete: false, + window: srcwin, + property: conn.atom_xdndtypelist, + r#type: xcb::x::ATOM_ATOM, + long_offset: 0, + long_length: u32::max_value(), + }) { + Ok(prop) => prop.value::().to_vec(), + Err(err) => { + log::error!( + "xdnd: unable to get type list from source window: {:?}", + err + ); + Vec::::new() + } + }; + } + self.drag_and_drop.target_type = xcb::x::ATOM_NONE; + 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!( + "selected: {}", + conn.atom_name(self.drag_and_drop.target_type) + ); + } else if self.drag_and_drop.src_window != Some(srcwin) { + log::error!("ClientMessage {msgtype_name} received, but no Xdnd in progress or source window mismatch"); + } else if msgtype == conn.atom_xdndposition { + self.drag_and_drop.time = data[3]; + let (x, y) = (data[2] >> 16 as u16, data[2] as u16); + self.drag_and_drop.src_action = unsafe { Atom::new(data[4]) }; + self.drag_and_drop.target_action = conn.atom_xdndactioncopy; + log::trace!( + "ClientMessage {msgtype_name}, ({x}, {y}), timestamp: {}, action: {}", + self.drag_and_drop.time, + conn.atom_name(self.drag_and_drop.src_action) + ); + conn.send_request_no_reply_log(&xcb::x::SendEvent { + propagate: false, + destination: xcb::x::SendEventDest::Window(srcwin), + event_mask: xcb::x::EventMask::empty(), + event: &xcb::x::ClientMessageEvent::new( + srcwin, + conn.atom_xdndstatus, + xcb::x::ClientMessageData::Data32([ + self.window_id.resource_id(), + 2 | (self.drag_and_drop.target_type != xcb::x::ATOM_NONE) as u32, + 0, + 0, + self.drag_and_drop.target_action.resource_id(), + ]), + ), + }); + } else if msgtype == conn.atom_xdndleave { + self.drag_and_drop.src_window = None; + log::trace!("ClientMessage {msgtype_name}"); + } else if msgtype == conn.atom_xdnddrop { + self.drag_and_drop.time = data[2]; + log::trace!( + "ClientMessage {msgtype_name}, timestamp: {}", + self.drag_and_drop.time + ); + if self.drag_and_drop.target_type != xcb::x::ATOM_NONE { + conn.send_request_no_reply_log(&xcb::x::ConvertSelection { + requestor: self.window_id, + selection: conn.atom_xdndselection, + target: self.drag_and_drop.target_type, + property: conn.atom_xsel_data, + time: self.drag_and_drop.time, + }); + } else { + log::warn!("XdndDrop received, but no target type selected. Ignoring."); + conn.send_request_no_reply_log(&xcb::x::SendEvent { + propagate: false, + destination: xcb::x::SendEventDest::Window(srcwin), + event_mask: xcb::x::EventMask::empty(), + event: &xcb::x::ClientMessageEvent::new( + srcwin, + conn.atom_xdndfinished, + xcb::x::ClientMessageData::Data32([ + self.window_id.resource_id(), + 0, + 0, + 0, + 0, + ]), + ), + }); + } + } + return Ok(()); + } + pub fn dispatch_event(&mut self, event: &Event) -> anyhow::Result<()> { let conn = self.conn(); match event { @@ -589,14 +734,36 @@ impl XWindowInner { )?; } Event::X(xcb::x::Event::ClientMessage(msg)) => { + let type_atom_name = conn.atom_name(msg.r#type()); use xcb::x::ClientMessageData; - match msg.data() { - ClientMessageData::Data32(data) => { - if data[0] == conn.atom_delete().resource_id() { + use xcb::XidNew; + let xdnd_msgtype_atoms = [ + conn.atom_xdndenter, + conn.atom_xdndposition, + conn.atom_xdndstatus, + conn.atom_xdndleave, + conn.atom_xdnddrop, + conn.atom_xdndfinished, + ]; + if xdnd_msgtype_atoms.contains(&msg.r#type()) { + if let ClientMessageData::Data32(data) = msg.data() { + self.xdnd_event(msg.r#type(), &data)?; + } else { + log::warn!("Received ClientMessage {type_atom_name} with wrong format"); + } + } else if msg.r#type() == conn.atom_protocols { + if let ClientMessageData::Data32(data) = msg.data() { + let protocol_atom = unsafe { Atom::new(data[0]) }; + log::trace!( + "ClientMessage {type_atom_name}/{}", + conn.atom_name(protocol_atom) + ); + if protocol_atom == conn.atom_delete { self.events.dispatch(WindowEvent::CloseRequested); } + } else { + log::warn!("Received ClientMessage {type_atom_name} with wrong format"); } - ClientMessageData::Data8(_) | ClientMessageData::Data16(_) => {} } } Event::X(xcb::x::Event::DestroyNotify(_)) => { @@ -932,6 +1099,122 @@ impl XWindowInner { } } } + } else if selection.selection() == conn.atom_xdndselection + && selection.property() == conn.atom_xsel_data + { + if let Some(srcwin) = self.drag_and_drop.src_window { + match conn.send_and_wait_request(&xcb::x::GetProperty { + delete: true, + window: selection.requestor(), + property: selection.property(), + r#type: selection.target(), + long_offset: 0, + long_length: u32::max_value(), + }) { + Ok(prop) => { + 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| { + if line.starts_with('#') || line.trim().is_empty() { + // text/uri-list: Any lines beginning with the '#' character + // are comment lines and are ignored during processing + return None; + } + use url::Url; + let url = Url::parse(line) + .map_err(|err| { + log::error!( + "Error parsing dropped file line {} as url: {:#}", + line, + err + ); + }) + .ok()?; + url.to_file_path() + .map_err(|_| { + log::error!( + "Error converting url {} from line {} to pathbuf", + url, + line + ); + }) + .ok() + }) + .collect::>(); + self.events.dispatch(WindowEvent::DroppedFile(paths)); + } + } + Err(err) => { + log::error!("clipboard: err while getting clipboard property: {:?}", err); + } + } + conn.send_request_no_reply_log(&xcb::x::SendEvent { + propagate: false, + destination: xcb::x::SendEventDest::Window(srcwin), + event_mask: xcb::x::EventMask::empty(), + event: &xcb::x::ClientMessageEvent::new( + srcwin, + conn.atom_xdndfinished, + xcb::x::ClientMessageData::Data32([ + window_id.resource_id(), + 1, + self.drag_and_drop.target_action.resource_id(), + 0, + 0, + ]), + ), + }); + } else { + log::warn!("No Xdnd in progress, but received Xdnd selection. Ignoring."); + } } else { log::trace!("SEL: window_id={window_id:?} unknown selection {selection_name}"); } @@ -1204,6 +1487,7 @@ impl XWindow { height: height.try_into()?, dpi: conn.default_dpi(), copy_and_paste: CopyAndPaste::default(), + drag_and_drop: DragAndDrop::default(), cursors: CursorInfo::new(&config, &conn), config: config.clone(), has_focus: None, @@ -1253,6 +1537,14 @@ impl XWindow { data: &[conn.atom_delete], })?; + conn.send_request_no_reply(&xcb::x::ChangeProperty { + mode: PropMode::Replace, + window: window_id, + property: conn.atom_xdndaware, + r#type: xcb::x::ATOM_ATOM, + data: &[5u32], + })?; + window .lock() .unwrap()