diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..9792a1ab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "libcosmic" +version = "0.1.0" +edition = "2021" + +[dependencies] +cascade = "1" +gdk4 = "0.3.1" +gdk4-x11 = "0.3.0" +gtk4 = "0.3.1" +x11 = { version = "2", features = ["xlib"] } +# examples/launcher +pop-launcher = "1.0.3" +serde_json = "1.0.70" diff --git a/examples/launcher/ipc.rs b/examples/launcher/ipc.rs new file mode 100644 index 00000000..c1ce7576 --- /dev/null +++ b/examples/launcher/ipc.rs @@ -0,0 +1,76 @@ +use pop_launcher::{Request, Response}; +use std::process; +use std::io::{self, BufRead, Write}; + +pub struct LauncherIpc { + child: process::Child, + stdin: process::ChildStdin, + stdout: io::BufReader, + exited: bool, +} + +impl LauncherIpc { + pub fn new() -> io::Result { + let mut child = process::Command::new("pop-launcher") + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.take().ok_or( + io::Error::new(io::ErrorKind::Other, "failed to find child stdin") + )?; + + let stdout = io::BufReader::new(child.stdout.take().ok_or( + io::Error::new(io::ErrorKind::Other, "failed to find child stdout") + )?); + + Ok(Self { + child, + stdin, + stdout, + exited: false, + }) + } + + fn send_request(&mut self, request: Request) -> io::Result<()> { + let mut request_json = serde_json::to_string(&request).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidInput, err) + })?; + request_json.push('\n'); + self.stdin.write_all(request_json.as_bytes()) + } + + fn recv_response(&mut self) -> io::Result { + let mut response_json = String::new(); + self.stdout.read_line(&mut response_json)?; + serde_json::from_str(&response_json).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, err) + }) + } + + pub fn request(&mut self, request: Request) -> io::Result { + self.send_request(request)?; + self.recv_response() + } + + //TODO: better exit implementation + pub fn exit(&mut self) -> io::Result> { + if ! self.exited { + self.send_request(Request::Exit)?; + let status = self.child.wait()?; + self.exited = true; + Ok(Some(status)) + } else { + Ok(None) + } + } +} + +impl Drop for LauncherIpc { + fn drop(&mut self) { + match self.exit() { + Ok(_) => (), + Err(err) => eprintln!("LauncherIpc::drop failed: {}", err), + } + } +} diff --git a/examples/launcher/main.rs b/examples/launcher/main.rs new file mode 100644 index 00000000..08de7d30 --- /dev/null +++ b/examples/launcher/main.rs @@ -0,0 +1,83 @@ +use gdk4 as gdk; +use gtk4 as gtk; + +use gdk::prelude::*; +use gtk::prelude::*; +use libcosmic::x; +use std::{ + cell::RefCell, + rc::Rc, +}; + +use self::ipc::LauncherIpc; +mod ipc; + +fn main() { + let launcher = Rc::new(RefCell::new( + LauncherIpc::new().expect("failed to connect to launcher service") + )); + + let app = gtk::Application::builder() + .application_id("com.system76.Launcher") + .build(); + + app.connect_activate(move |app| { + let display = gdk::Display::default().unwrap(); + let monitors = display.monitors().unwrap(); + + for i in 0..monitors.n_items() { + let monitor: gdk::Monitor = monitors.item(i).unwrap().downcast().unwrap(); + let rect = monitor.geometry(); + //TODO: get monitor with mouse cursor + if i == 0 { + let window = gtk::ApplicationWindow::builder() + .application(app) + .default_width(480) + .default_height(440) + .title("Launcher") + .build(); + + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 4); + window.set_child(Some(&vbox)); + + let search = gtk::Entry::new(); + vbox.append(&search); + + { + let launcher = launcher.clone(); + search.connect_changed(move |search| { + let response = launcher.borrow_mut().request( + pop_launcher::Request::Search(search.text().to_string()) + ).expect("failed to access launcher service"); + + println!("{:?}", response); + }); + } + + window.show(); + + if let Some((display, surface)) = x::get_window_x11(&window) { + unsafe { + x::change_property( + &display, + &surface, + "_NET_WM_WINDOW_TYPE", + x::PropMode::Replace, + &[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DIALOG").unwrap()], + ); + x::set_position( + &display, + &surface, + rect.x + (rect.width - 480) / 2, + rect.y + (rect.height - 440) / 2 + ); + } + } else { + println!("Failed to get X11 window"); + } + } + } + }); + + app.run(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..7a788f24 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod x; diff --git a/src/x.rs b/src/x.rs new file mode 100644 index 00000000..f535c5ca --- /dev/null +++ b/src/x.rs @@ -0,0 +1,189 @@ +use cascade::cascade; +use gdk4::prelude::*; +use gdk4_x11::x11::xlib; +use glib::translate::ToGlibPtr; +use gtk4::{glib, prelude::*}; +use std::{ + ffi::{CString, NulError}, + os::raw::c_long, + ptr, +}; + +pub use std::os::raw::{c_int, c_uchar, c_ulong, c_ushort}; + +pub fn get_window_x11>( + window: &T, +) -> Option<(gdk4_x11::X11Display, gdk4_x11::X11Surface)> { + let surface = window + .upcast_ref() + .surface()? + .downcast::() + .ok()?; + let display = surface.display()?.downcast::().ok()?; + Some((display, surface)) +} + +#[repr(transparent)] +pub struct Atom(xlib::Atom); + +impl Atom { + pub fn new(display: &gdk4_x11::X11Display, prop: &str) -> Result { + unsafe { + let prop = CString::new(prop)?; + Ok(Self(gdk4_x11::ffi::gdk_x11_get_xatom_by_name_for_display( + display.to_glib_none().0, + prop.as_ptr(), + ))) + } + } +} + +pub unsafe trait XElement { + const TYPE: xlib::Atom; + const SIZE: c_int; +} + +unsafe impl XElement for c_uchar { + const TYPE: xlib::Atom = xlib::XA_CARDINAL; + const SIZE: c_int = 8; +} + +unsafe impl XElement for c_ushort { + const TYPE: xlib::Atom = xlib::XA_CARDINAL; + const SIZE: c_int = 16; +} + +unsafe impl XElement for c_ulong { + const TYPE: xlib::Atom = xlib::XA_CARDINAL; + const SIZE: c_int = 32; +} + +unsafe impl XElement for Atom { + const TYPE: xlib::Atom = xlib::XA_ATOM; + const SIZE: c_int = 32; +} + +pub unsafe trait XProp { + const TYPE: xlib::Atom; + const SIZE: c_int; + + fn ptr(&self) -> *const u8; + + fn nelements(&self) -> c_int; +} + +unsafe impl XProp for &[T; LEN] { + const TYPE: xlib::Atom = T::TYPE; + const SIZE: c_int = T::SIZE; + + fn ptr(&self) -> *const u8 { + self.as_ptr() as _ + } + + fn nelements(&self) -> c_int { + LEN as c_int + } +} + +unsafe impl XProp for &[T] { + const TYPE: xlib::Atom = T::TYPE; + const SIZE: c_int = T::SIZE; + + fn ptr(&self) -> *const u8 { + self.as_ptr() as _ + } + + fn nelements(&self) -> c_int { + self.len() as c_int + } +} + +unsafe impl XProp for &str { + const TYPE: xlib::Atom = xlib::XA_STRING; + const SIZE: c_int = 8; + + fn ptr(&self) -> *const u8 { + self.as_ptr() + } + + fn nelements(&self) -> c_int { + self.len() as c_int + } +} + +#[allow(dead_code)] +pub enum PropMode { + Replace, + Prepend, + Append, +} + +pub unsafe fn change_property( + display: &gdk4_x11::X11Display, + surface: &gdk4_x11::X11Surface, + prop: &str, + mode: PropMode, + value: T, +) { + // TODO check error return value + let mode = match mode { + PropMode::Replace => xlib::PropModeReplace, + PropMode::Prepend => xlib::PropModePrepend, + PropMode::Append => xlib::PropModeAppend, + }; + xlib::XChangeProperty( + display.xdisplay(), + surface.xid(), + Atom::new(display, prop).unwrap().0, + T::TYPE, + T::SIZE, + mode, + value.ptr(), + value.nelements(), + ); +} + +pub unsafe fn set_position( + display: &gdk4_x11::X11Display, + surface: &gdk4_x11::X11Surface, + x: c_int, + y: c_int, +) { + // XXX check error return value + xlib::XMoveWindow(display.xdisplay(), surface.xid(), x, y); +} + +pub unsafe fn wm_state_add( + display: &gdk4_x11::X11Display, + surface: &gdk4_x11::X11Surface, + state: &str, +) { + const _NET_WM_STATE_ADD: c_long = 1; + // XXX check error return value + let mut event = xlib::XEvent { + client_message: xlib::XClientMessageEvent { + type_: xlib::ClientMessage, + serial: 0, + send_event: 0, + display: ptr::null_mut(), + window: surface.xid(), + message_type: Atom::new(display, "_NET_WM_STATE").unwrap().0, + format: 32, + data: cascade! { + xlib::ClientMessageData::new(); + ..set_long(0, _NET_WM_STATE_ADD); + ..set_long(1, Atom::new(display, state).unwrap().0 as _); + ..set_long(2, Atom::new(display, "").unwrap().0 as _); + ..set_long(3, 1); + ..set_long(3, 0); + }, + }, + }; + xlib::XSendEvent( + display.xdisplay(), + display.xrootwindow(), + 0, + xlib::SubstructureRedirectMask | xlib::SubstructureNotifyMask, + &mut event, + ); +}