diff --git a/Cargo.toml b/Cargo.toml index 75182ce4..e3679e2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,13 @@ gdk4-x11 = "0.3.0" gio = "0.14.8" gtk4 = "0.3.1" x11 = { version = "2", features = ["xlib"] } -# examples/launcher -pop-launcher = "1.0.3" -serde_json = "1.0.70" + +[dev-dependencies] +pop-launcher = { git = "https://github.com/pop-os/launcher", branch = "client" } +pop-launcher-service = { git = "https://github.com/pop-os/launcher", branch = "client" } +postage = "0.4.1" +futures = "0.3.18" +glib = "0.14.8" [[example]] -name = "launcher" # examples/launcher/main.rs \ No newline at end of file +name = "launcher" # examples/launcher/main.rs diff --git a/examples/launcher/ipc.rs b/examples/launcher/ipc.rs deleted file mode 100644 index 42a1c6c5..00000000 --- a/examples/launcher/ipc.rs +++ /dev/null @@ -1,76 +0,0 @@ -use pop_launcher::{Request, Response}; -use std::io::{self, BufRead, Write}; -use std::process; - -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 index ddbef6a2..8e530b04 100644 --- a/examples/launcher/main.rs +++ b/examples/launcher/main.rs @@ -1,13 +1,11 @@ use gtk4 as gtk; use gtk::prelude::*; -use gtk4::Application; use libcosmic::x; -use std::{cell::RefCell, rc::Rc}; -use gtk4::glib::Type; -use self::ipc::LauncherIpc; -mod ipc; +use pop_launcher_service::IpcClient; +use postage::mpsc::Sender; +use postage::prelude::*; fn icon_source(icon: >k::Image, source: &Option) { match source { @@ -23,124 +21,152 @@ fn icon_source(icon: >k::Image, source: &Option) { } } + +enum Event { + Response(pop_launcher::Response), + Search(String), +} + +fn spawn_launcher(mut tx: Sender) -> IpcClient { + let (launcher, responses) = + pop_launcher_service::IpcClient::new().expect("failed to connect to launcher service"); + + glib::MainContext::default().spawn_local(async move { + use futures::StreamExt; + futures::pin_mut!(responses); + while let Some(event) = responses.next().await { + let _ = tx.send(Event::Response(event)).await; + } + }); + + launcher +} + fn main() { let app = gtk::Application::builder() .application_id("com.system76.Launcher") .build(); - app.connect_activate(move |app| build_ui(app)); + app.connect_activate(move |app| { + let (tx, mut rx) = postage::mpsc::channel(1); + let mut launcher = spawn_launcher(tx.clone()); + + let window = gtk::ApplicationWindow::builder() + .application(app) + .decorated(false) + .default_width(480) + .default_height(440) + .title("Launcher") + .build(); + + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 16); + vbox.set_margin_start(16); + vbox.set_margin_end(16); + vbox.set_margin_top(16); + vbox.set_margin_bottom(16); + window.set_child(Some(&vbox)); + + let search = gtk::SearchEntry::new(); + search.set_placeholder_text(Some(" Type to search apps, or type '?' for more options.")); + vbox.append(&search); + + let listbox = gtk::ListBox::new(); + vbox.append(&listbox); + + { + let search_changed = glib::clone!(@strong tx => move |search: >k::SearchEntry| { + let search = search.text().to_string(); + + let mut tx = tx.clone(); + glib::MainContext::default().spawn_local(async move { + let _ = tx.send(Event::Search(search)).await; + }); + }); + + search_changed(&search); + search.connect_changed(search_changed); + } + + // Setting the window to dialog type must happen between realize and show. Dialog windows + // show up centered on the display with the cursor, so we do not have to set position + window.connect_realize(move |window| { + 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()], + ); + } + } + }); + + window.show(); + + glib::MainContext::default().spawn_local(async move { + while let Some(event) = rx.recv().await { + match event { + Event::Search(search) => { + //TODO: is this the best way to clear a listbox? + while let Some(child) = listbox.last_child() { + listbox.remove(&child); + } + + let _ = launcher.send(pop_launcher::Request::Search(search)).await; + } + + Event::Response(event) => { + if let pop_launcher::Response::Update(results) = event { + for (i, result) in results.iter().enumerate() { + // Limit to 9 results + if i >= 9 { + continue; + } + + let row = gtk::ListBoxRow::new(); + listbox.append(&row); + + // Select first row + if i == 0 { + listbox.select_row(Some(&row)); + } + + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + hbox.set_margin_top(8); + hbox.set_margin_bottom(8); + row.set_child(Some(&hbox)); + + let category_icon = gtk::Image::new(); + category_icon.set_pixel_size(16); + icon_source(&category_icon, &result.category_icon); + hbox.append(&category_icon); + + let icon = gtk::Image::new(); + icon.set_pixel_size(32); + icon_source(&icon, &result.icon); + hbox.append(&icon); + + let labels = gtk::Box::new(gtk::Orientation::Vertical, 4); + hbox.append(&labels); + + let name = gtk::Label::new(Some(&result.name)); + name.set_halign(gtk::Align::Start); + labels.append(&name); + + let description = gtk::Label::new(Some(&result.description)); + description.set_halign(gtk::Align::Start); + labels.append(&description); + } + } + } + } + } + }); + }); app.run(); } - -fn build_ui(app: &Application) { - let launcher = Rc::new(RefCell::new( - LauncherIpc::new().expect("failed to connect to launcher service"), - )); - - let window = gtk::ApplicationWindow::builder() - .application(app) - .decorated(false) - .default_width(590) - .default_height(150) - .title("Launcher") - .build(); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 16); - vbox.set_margin_start(16); - vbox.set_margin_end(16); - vbox.set_margin_top(16); - vbox.set_margin_bottom(16); - window.set_child(Some(&vbox)); - - let search = gtk::SearchEntry::new(); - search.set_placeholder_text(Some(" Type to search apps, or type '?' for more options.")); - vbox.append(&search); - { - let launcher = launcher.clone(); - let search_changed = move |search: >k::SearchEntry| { - let listbox_widget = vbox.last_child().unwrap(); - let gtk_list_box = Type::from_name("GtkListBox"); - if gtk_list_box.is_some() && listbox_widget.type_() == gtk_list_box.unwrap() { - vbox.remove(&listbox_widget); - } - let listbox = gtk::ListBox::new(); - vbox.append(&listbox); - - let response_res = launcher - .borrow_mut() - .request(pop_launcher::Request::Search(search.text().to_string())); - - // println!("{:#?}", response_res); - - if let Ok(pop_launcher::Response::Update(results)) = response_res { - for (i, result) in results.iter().enumerate() { - // Limit to 9 results - if i >= 9 { - continue; - } - - let row = gtk::ListBoxRow::new(); - listbox.append(&row); - - // Select first row - if i == 0 { - listbox.select_row(Some(&row)); - } - - let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8); - hbox.set_margin_start(8); - hbox.set_margin_end(8); - hbox.set_margin_top(8); - hbox.set_margin_bottom(8); - row.set_child(Some(&hbox)); - - let category_icon = gtk::Image::new(); - category_icon.set_pixel_size(16); - icon_source(&category_icon, &result.category_icon); - hbox.append(&category_icon); - - let icon = gtk::Image::new(); - icon.set_pixel_size(32); - icon_source(&icon, &result.icon); - hbox.append(&icon); - - let labels = gtk::Box::new(gtk::Orientation::Vertical, 4); - hbox.append(&labels); - - let name = gtk::Label::new(Some(&result.name)); - name.set_halign(gtk::Align::Start); - labels.append(&name); - - let description = gtk::Label::new(Some(&result.description)); - description.set_halign(gtk::Align::Start); - labels.append(&description); - } - } - }; - - search.connect_changed(search_changed.clone()); - - search_changed(&search); - } - - // Setting the window to dialog type must happen between realize and show. Dialog windows - // show up centered on the display with the cursor, so we do not have to set position - window.connect_realize(move |window| { - 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()], - ); - } - } else { - println!("failed to get X11 window"); - } - }); - - window.show(); -}