diff --git a/Cargo.toml b/Cargo.toml index 8472e7e..447a180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "apps/magpie", "apps/music-player", + "apps/notifications", "apps/sandbox", "crates/script", "crates/textwrap", diff --git a/apps/magpie/src/protocol.rs b/apps/magpie/src/protocol.rs index 5aedc03..9e80597 100644 --- a/apps/magpie/src/protocol.rs +++ b/apps/magpie/src/protocol.rs @@ -4,7 +4,7 @@ use std::collections::VecDeque; use std::io::{Read, Write}; use std::marker::PhantomData; -use std::path::PathBuf; +use std::path::{PathBuf, Path}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -26,6 +26,7 @@ pub struct CreatePanel { pub id: PanelId, pub protocol: String, pub script: PathBuf, + pub init_msg: Vec, } /// Sends a panel a message. @@ -210,6 +211,16 @@ impl Messenger { } } +/// Acquires the path to the Magpie socket. +/// +/// Currently only joins XDG_RUNTIME_DIR with [MAGPIE_SOCK]. +pub fn find_socket() -> PathBuf { + let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); + let sock_dir = Path::new(&sock_dir); + let sock_path = sock_dir.join(MAGPIE_SOCK); + sock_path +} + #[cfg(feature = "async")] mod async_messages { use super::*; diff --git a/apps/magpie/src/service/ipc.rs b/apps/magpie/src/service/ipc.rs index 0da73cb..6ca7484 100644 --- a/apps/magpie/src/service/ipc.rs +++ b/apps/magpie/src/service/ipc.rs @@ -130,6 +130,7 @@ impl Client { id, protocol, script, + init_msg, }) => { let mut data = self.data.write(); @@ -146,6 +147,7 @@ impl Client { id: window, protocol, script, + init_msg, }; let _ = self.window_sender.send_event(msg); } diff --git a/apps/magpie/src/service/window.rs b/apps/magpie/src/service/window.rs index 0e9229e..5d3a6de 100644 --- a/apps/magpie/src/service/window.rs +++ b/apps/magpie/src/service/window.rs @@ -21,6 +21,7 @@ pub enum WindowMessage { id: usize, protocol: String, script: PathBuf, + init_msg: Vec, }, CloseWindow { id: usize, @@ -194,13 +195,14 @@ impl WindowStore { id, protocol, script, + init_msg, } => { log::debug!("Opening window {} with script {:?}...", id, script); let start = std::time::Instant::now(); let module = std::fs::read(script)?; let mut script = self.runtime.load_module(&module)?; log::debug!("Instantiated window {} script in {:?}", id, start.elapsed()); - let panel = script.create_panel(&protocol, vec![])?; + let panel = script.create_panel(&protocol, init_msg)?; log::debug!("Created window {} panel in {:?}", id, start.elapsed()); let window = Window::new(self.ipc_sender.to_owned(), id, panel, &event_loop)?; let window_id = window.get_id(); diff --git a/apps/music-player/src/main.rs b/apps/music-player/src/main.rs index fb647ee..5719cc6 100644 --- a/apps/music-player/src/main.rs +++ b/apps/music-player/src/main.rs @@ -196,9 +196,7 @@ fn main() { .to_owned(); smol::block_on(async { - let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); - let sock_dir = Path::new(&sock_dir); - let sock_path = sock_dir.join(MAGPIE_SOCK); + let sock_path = canary_magpie::protocol::find_socket(); let socket = UnixStream::connect(sock_path).await.unwrap(); let mut magpie = MagpieClient::new(socket); let protocol = "tebibyte-media.desktop.music-player-controller".to_string(); @@ -207,6 +205,7 @@ fn main() { id: 0, protocol, script, + init_msg: vec![], }; let msg = MagpieServerMsg::CreatePanel(msg); diff --git a/apps/notifications/Cargo.toml b/apps/notifications/Cargo.toml new file mode 100644 index 0000000..615470b --- /dev/null +++ b/apps/notifications/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "canary-notifications" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "canary-notifications" +path = "src/main.rs" +required-features = ["bin"] + +[dependencies] +canary-magpie = { path = "../magpie", optional = true, features = ["async"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +smol = { version = "1.2", optional = true } +zbus = { version = "3.5", optional = true } + +[features] +bin = ["dep:canary-magpie", "dep:smol", "dep:zbus"] diff --git a/apps/notifications/src/lib.rs b/apps/notifications/src/lib.rs new file mode 100644 index 0000000..fb0ac41 --- /dev/null +++ b/apps/notifications/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + +use serde::{Deserialize, Serialize}; + +pub use serde; +pub use serde_json; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Contents { + /// The optional name of the application sending the notification. + pub app_name: Option, + + /// The summary text briefly describing the notification. + pub summary: String, + + /// The optional detailed body text. + pub body: Option, + + /// The timeout time in milliseconds since the display of the notification + /// at which the notification should automatically close. + pub timeout: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum OutMsg {} diff --git a/apps/notifications/src/main.rs b/apps/notifications/src/main.rs new file mode 100644 index 0000000..6a7122a --- /dev/null +++ b/apps/notifications/src/main.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; +use std::future::pending; +use std::path::PathBuf; + +use canary_magpie::protocol::*; +use canary_notifications::Contents; +use smol::net::unix::UnixStream; +use zbus::{dbus_interface, zvariant::Value, ConnectionBuilder, SignalContext}; + +pub type MagpieClient = ClientMessenger; + +pub struct Notifications { + module_path: PathBuf, + magpie: MagpieClient, + next_id: u32, +} + +#[dbus_interface(name = "org.freedesktop.Notifications")] +impl Notifications { + fn get_capabilities(&self) -> Vec { + vec!["body", "body-markup", "actions", "icon-static"] + .into_iter() + .map(ToString::to_string) + .collect() + } + + #[dbus_interface(out_args("name", "vendor", "version", "spec_version"))] + fn get_server_information(&self) -> zbus::fdo::Result<(String, String, String, String)> { + Ok(( + "canary-notifications".to_string(), + "Canary Development Team".to_string(), + "0.1.0".to_string(), + "1.2".to_string(), + )) + } + + async fn notify( + &mut self, + app_name: String, + replaces_id: u32, + app_icon: String, + summary: String, + body: String, + actions: Vec, + hints: HashMap>, + timeout: i32, + ) -> u32 { + let contents = Contents { + app_name: Some(app_name).filter(|s| !s.is_empty()), + summary, + body: Some(body).filter(|s| !s.is_empty()), + timeout: Some(timeout).filter(|t| *t == 0), + }; + + let id = self.next_id; + self.next_id += 1; + + let msg = CreatePanel { + id, + protocol: "tebibyte-media.desktop.notification".to_string(), + script: self.module_path.to_owned(), + init_msg: serde_json::to_vec(&contents).unwrap(), + }; + + self.magpie + .send_async(&MagpieServerMsg::CreatePanel(msg)) + .await + .unwrap(); + + id + } + + fn close_notification(&self, id: u32) {} + + #[dbus_interface(signal)] + async fn notification_closed(ctx: &SignalContext<'_>, id: u32, reason: u32) + -> zbus::Result<()>; + + #[dbus_interface(signal)] + async fn action_invoked( + ctx: &SignalContext<'_>, + id: u32, + action_key: String, + ) -> zbus::Result<()>; +} + +pub fn main() { + let args: Vec = std::env::args().collect(); + let module_path = args + .get(1) + .expect("Please pass a path to a Canary script!") + .to_owned() + .into(); + + smol::block_on(async { + let sock_path = find_socket(); + let socket = UnixStream::connect(sock_path).await.unwrap(); + let magpie = MagpieClient::new(socket); + + let notifications = Notifications { + magpie, + next_id: 0, + module_path, + }; + + let _ = ConnectionBuilder::session() + .unwrap() + .name("org.freedesktop.Notifications") + .unwrap() + .serve_at("/org/freedesktop/Notifications", notifications) + .unwrap() + .build() + .await + .unwrap(); + + pending::<()>().await; + }); +} diff --git a/scripts/sao-ui/Cargo.toml b/scripts/sao-ui/Cargo.toml index 2a09cd0..798db04 100644 --- a/scripts/sao-ui/Cargo.toml +++ b/scripts/sao-ui/Cargo.toml @@ -11,7 +11,9 @@ crate-type = ["cdylib"] glam = "^0.21" keyframe = "1" canary-music-player = { path = "../../apps/music-player" } +canary-notifications = { path = "../../apps/notifications" } canary-script = { path = "../../crates/script" } +canary-textwrap = { path = "../../crates/textwrap" } serde = { version = "1", features = ["derive"] } serde_json = "1" wee_alloc = "^0.4" diff --git a/scripts/sao-ui/src/lib.rs b/scripts/sao-ui/src/lib.rs index 948c2e9..f379d19 100644 --- a/scripts/sao-ui/src/lib.rs +++ b/scripts/sao-ui/src/lib.rs @@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub mod anim; pub mod main_menu; pub mod music_player; +pub mod notifications; pub mod style; pub mod widgets; @@ -14,6 +15,7 @@ use api::*; use canary_script::*; use main_menu::MainMenuPanel; use music_player::MusicPlayerPanel; +use notifications::NotificationPanel; use widgets::Widget; export_abi!(bind_panel_impl); @@ -24,6 +26,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box MusicPlayerPanel::bind(panel, msg), + "tebibyte-media.desktop.notification" => NotificationPanel::bind(panel, msg), "wip-dialog" => ConfirmationDialogPanel::bind(panel, msg), _ => MainMenuPanel::bind(panel, msg), } diff --git a/scripts/sao-ui/src/notifications.rs b/scripts/sao-ui/src/notifications.rs new file mode 100644 index 0000000..4860f93 --- /dev/null +++ b/scripts/sao-ui/src/notifications.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Marceline Crmaer +// SPDX-License-Identifier: AGPL-3.0-or-later + +use super::widgets::prelude::*; +use api::*; +use canary_script::*; +use canary_textwrap::{Content, Layout, TextCache}; + +use shell::Offset; +use text::{Label, LabelText}; + +pub struct NotificationPanel { + panel: Panel, + summary: Offset