diff --git a/api/rust/Cargo.toml b/api/rust/Cargo.toml index d48b147..bf259ba 100644 --- a/api/rust/Cargo.toml +++ b/api/rust/Cargo.toml @@ -11,3 +11,4 @@ rmp = { version = "0.8.12" } rmp-serde = { version = "1.1.2" } anyhow = { version = "1.0.75", features = ["backtrace"] } lazy_static = "1.4.0" +xkbcommon = "0.7.0" diff --git a/api/rust/examples/example_config.rs b/api/rust/examples/example_config.rs index d9371eb..2fcd05f 100644 --- a/api/rust/examples/example_config.rs +++ b/api/rust/examples/example_config.rs @@ -1,6 +1,19 @@ +use pinnacle_api::{Modifier, MouseButton, MouseEdge}; + fn main() { pinnacle_api::setup(|pinnacle| { pinnacle.process.spawn(vec!["alacritty"]).unwrap(); + + pinnacle.input.keybind(&[Modifier::Ctrl], 'a', move || { + pinnacle.process.spawn(vec!["alacritty"]).unwrap(); + }); + + pinnacle.input.mousebind( + &[Modifier::Ctrl], + MouseButton::Left, + MouseEdge::Press, + || {}, + ); }) .unwrap(); } diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs new file mode 100644 index 0000000..1ea8ea4 --- /dev/null +++ b/api/rust/src/input.rs @@ -0,0 +1,99 @@ +use xkbcommon::xkb::Keysym; + +use crate::{ + msg::{Args, CallbackId, KeyIntOrString, Modifier, MouseEdge, Msg}, + send_msg, CALLBACK_VEC, +}; + +pub struct Input; + +impl Input { + /// Set a keybind. + /// + /// This function takes in three parameters: + /// - `modifiers`: A slice of the modifiers you want held for the keybind to trigger. + /// - `key`: The key that needs to be pressed. This takes `impl Into` and can + /// take the following three types: + /// - [`char`]: A character of the key you want. This can be `a`, `~`, `@`, and so on. + /// - [`u32`]: The key in numeric form. You can use the keys defined in + /// [`xkbcommon::xkb::keysyms`] for this. + /// - [`Keysym`]: The key in `Keysym` form, from [xkbcommon::xkb::Keysym]. + pub fn keybind(&self, modifiers: &[Modifier], key: impl Into, mut action: F) + where + F: FnMut() + Send + 'static, + { + let args_callback = move |_: Option| { + action(); + }; + + let mut callback_vec = CALLBACK_VEC.lock().unwrap(); + let len = callback_vec.len(); + callback_vec.push(Box::new(args_callback)); + + let key = key.into(); + + let msg = Msg::SetKeybind { + key, + modifiers: modifiers.to_vec(), + callback_id: CallbackId(len as u32), + }; + + send_msg(msg).unwrap(); + } + + pub fn mousebind( + &self, + modifiers: &[Modifier], + button: MouseButton, + edge: MouseEdge, + mut action: F, + ) where + F: FnMut() + Send + 'static, + { + let args_callback = move |_: Option| { + action(); + }; + + let mut callback_vec = CALLBACK_VEC.lock().unwrap(); + let len = callback_vec.len(); + callback_vec.push(Box::new(args_callback)); + + let msg = Msg::SetMousebind { + modifiers: modifiers.to_vec(), + button: button as u32, + edge, + callback_id: CallbackId(len as u32), + }; + + send_msg(msg).unwrap(); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MouseButton { + Left = 0x110, + Right, + Middle, + Side, + Extra, + Forward, + Back, +} + +impl From for KeyIntOrString { + fn from(value: char) -> Self { + Self::String(value.to_string()) + } +} + +impl From for KeyIntOrString { + fn from(value: u32) -> Self { + Self::Int(value) + } +} + +impl From for KeyIntOrString { + fn from(value: Keysym) -> Self { + Self::Int(value.raw()) + } +} diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index fab42c0..1160e93 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -1,20 +1,40 @@ +mod input; mod msg; mod process; +mod window; + +use input::Input; +pub use input::MouseButton; +pub use msg::Modifier; +pub use msg::MouseEdge; +use window::Window; +pub use xkbcommon::xkb::keysyms; +pub use xkbcommon::xkb::Keysym; use std::{ - io::Write, + collections::HashMap, + io::{Read, Write}, os::unix::net::UnixStream, path::PathBuf, - sync::{Mutex, OnceLock}, + sync::{atomic::AtomicU32, Mutex, OnceLock}, }; -use msg::{Args, Msg, Request}; +use msg::{Args, CallbackId, IncomingMsg, Msg, Request, RequestResponse}; use process::Process; +use crate::msg::RequestId; + static STREAM: OnceLock> = OnceLock::new(); #[allow(clippy::type_complexity)] -static CALLBACK_VEC: Mutex>> = Mutex::new(Vec::new()); +static CALLBACK_VEC: Mutex) + Send>>> = Mutex::new(Vec::new()); +lazy_static::lazy_static! { + static ref UNREAD_CALLBACK_MSGS: Mutex> = Mutex::new(HashMap::new()); + static ref UNREAD_REQUEST_MSGS: Mutex> = Mutex::new(HashMap::new()); +} +static REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0); + +/// Setup Pinnacle. pub fn setup(config_func: impl FnOnce(Pinnacle)) -> anyhow::Result<()> { STREAM .set(Mutex::new(UnixStream::connect(PathBuf::from( @@ -22,11 +42,46 @@ pub fn setup(config_func: impl FnOnce(Pinnacle)) -> anyhow::Result<()> { ))?)) .unwrap(); - let pinnacle = Pinnacle { process: Process }; + let pinnacle = Pinnacle { + process: Process, + input: Input, + window: Window, + }; config_func(pinnacle); - Ok(()) + loop { + let mut unread_callback_msgs = UNREAD_CALLBACK_MSGS.lock().unwrap(); + let mut callback_vec = CALLBACK_VEC.lock().unwrap(); + let mut to_remove = vec![]; + for (cb_id, incoming_msg) in unread_callback_msgs.iter() { + let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else { + continue; + }; + let Some(f) = callback_vec.get_mut(callback_id.0 as usize) else { + continue; + }; + f(args.clone()); + to_remove.push(*cb_id); + } + for id in to_remove { + unread_callback_msgs.remove(&id); + } + + let incoming_msg = read_msg(None); + + assert!(matches!(incoming_msg, IncomingMsg::CallCallback { .. })); + + let IncomingMsg::CallCallback { callback_id, args } = incoming_msg else { + unreachable!() + }; + + let Some(f) = callback_vec.get_mut(callback_id.0 as usize) else { + continue; + }; + + f(args); + } } fn send_msg(msg: Msg) -> anyhow::Result<()> { @@ -41,16 +96,78 @@ fn send_msg(msg: Msg) -> anyhow::Result<()> { Ok(()) } -fn read_msg() { - todo!() +fn read_msg(request_id: Option) -> IncomingMsg { + loop { + if let Some(request_id) = request_id { + if let Some(msg) = UNREAD_REQUEST_MSGS.lock().unwrap().remove(&request_id) { + return msg; + } + } + + let mut stream = STREAM.get().unwrap().lock().unwrap(); + let mut msg_len_bytes = [0u8; 4]; + stream.read_exact(msg_len_bytes.as_mut_slice()).unwrap(); + + let msg_len = u32::from_ne_bytes(msg_len_bytes); + let mut msg_bytes = vec![0u8; msg_len as usize]; + stream.read_exact(msg_bytes.as_mut_slice()).unwrap(); + + let incoming_msg: IncomingMsg = rmp_serde::from_slice(msg_bytes.as_slice()).unwrap(); + + if let Some(request_id) = request_id { + match &incoming_msg { + IncomingMsg::CallCallback { + callback_id, + args: _, + } => { + UNREAD_CALLBACK_MSGS + .lock() + .unwrap() + .insert(*callback_id, incoming_msg); + } + IncomingMsg::RequestResponse { + request_id: req_id, + response: _, + } => { + if req_id != &request_id { + UNREAD_REQUEST_MSGS + .lock() + .unwrap() + .insert(*req_id, incoming_msg); + } else { + return incoming_msg; + } + } + } + } else { + return incoming_msg; + } + } } -fn request(request: Request) { - // +fn request(request: Request) -> RequestResponse { + use std::sync::atomic::Ordering; + let request_id = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + + let msg = Msg::Request { + request_id: RequestId(request_id), + request, + }; + send_msg(msg).unwrap(); // TODO: propogate + + let IncomingMsg::RequestResponse { + request_id: _, + response, + } = read_msg(Some(RequestId(request_id))) + else { + unreachable!() + }; + + response } pub struct Pinnacle { pub process: Process, + pub window: Window, + pub input: Input, } - -impl Pinnacle {} diff --git a/api/rust/src/msg.rs b/api/rust/src/msg.rs index b26735d..ac05dc9 100644 --- a/api/rust/src/msg.rs +++ b/api/rust/src/msg.rs @@ -1,6 +1,6 @@ use std::num::NonZeroU32; -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] +#[derive(Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, Clone, Copy)] pub struct CallbackId(pub u32); #[derive(Debug, Hash, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] @@ -88,8 +88,8 @@ pub enum LibinputSetting { TapEnabled(bool), } -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct RequestId(u32); +#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RequestId(pub u32); #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WindowRuleCondition { @@ -116,7 +116,7 @@ pub enum FloatingOrTiled { Tiled, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum FullscreenOrMaximized { Neither, Fullscreen, @@ -300,7 +300,7 @@ pub enum Modifier { Super, } -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] pub enum Args { /// Send a message with lines from the spawned process. Spawn { @@ -317,3 +317,66 @@ pub enum Args { output_name: String, }, } + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum IncomingMsg { + CallCallback { + callback_id: CallbackId, + #[serde(default)] + args: Option, + }, + RequestResponse { + request_id: RequestId, + response: RequestResponse, + }, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum RequestResponse { + Window { + window_id: Option, + }, + Windows { + window_ids: Vec, + }, + WindowProps { + size: Option<(i32, i32)>, + loc: Option<(i32, i32)>, + class: Option, + title: Option, + focused: Option, + floating: Option, + fullscreen_or_maximized: Option, + }, + Output { + output_name: Option, + }, + Outputs { + output_names: Vec, + }, + OutputProps { + /// The make of the output. + make: Option, + /// The model of the output. + model: Option, + /// The location of the output in the space. + loc: Option<(i32, i32)>, + /// The resolution of the output. + res: Option<(i32, i32)>, + /// The refresh rate of the output. + refresh_rate: Option, + /// The size of the output, in millimeters. + physical_size: Option<(i32, i32)>, + /// Whether the output is focused or not. + focused: Option, + tag_ids: Option>, + }, + Tags { + tag_ids: Vec, + }, + TagProps { + active: Option, + name: Option, + output_name: Option, + }, +} diff --git a/api/rust/src/process.rs b/api/rust/src/process.rs index 066ae82..83d222d 100644 --- a/api/rust/src/process.rs +++ b/api/rust/src/process.rs @@ -19,13 +19,13 @@ impl Process { where F: FnMut(Option, Option, Option, Option) + Send + 'static, { - let args_callback = move |args: Args| { - if let Args::Spawn { + let args_callback = move |args: Option| { + if let Some(Args::Spawn { stdout, stderr, exit_code, exit_msg, - } = args + }) = args { callback(stdout, stderr, exit_code, exit_msg); } diff --git a/api/rust/src/window.rs b/api/rust/src/window.rs new file mode 100644 index 0000000..1c814e2 --- /dev/null +++ b/api/rust/src/window.rs @@ -0,0 +1,143 @@ +use crate::{ + msg::{FullscreenOrMaximized, Msg, Request, RequestResponse, WindowId}, + request, send_msg, +}; + +pub struct Window; + +impl Window { + pub fn get_by_class<'a>(&self, class: &'a str) -> impl Iterator + 'a { + self.get_all() + .filter(|win| win.class().as_deref() == Some(class)) + } + + pub fn get_focused(&self) -> Option { + self.get_all() + .find(|win| win.focused().is_some_and(|focused| focused)) + } + + pub fn get_all(&self) -> impl Iterator { + let RequestResponse::Windows { window_ids } = request(Request::GetWindows) else { + unreachable!() + }; + + window_ids.into_iter().map(WindowHandle) + } +} + +pub struct WindowHandle(WindowId); + +impl WindowHandle { + pub fn toggle_floating(&self) { + send_msg(Msg::ToggleFloating { window_id: self.0 }).unwrap(); + } + + pub fn toggle_fullscreen(&self) { + send_msg(Msg::ToggleFullscreen { window_id: self.0 }).unwrap(); + } + + pub fn toggle_maximized(&self) { + send_msg(Msg::ToggleMaximized { window_id: self.0 }).unwrap(); + } + + pub fn set_size(&self, width: Option, height: Option) { + send_msg(Msg::SetWindowSize { + window_id: self.0, + width, + height, + }) + .unwrap(); + } + + pub fn close(&self) { + send_msg(Msg::CloseWindow { window_id: self.0 }).unwrap(); + } + + pub fn size(&self) -> Option<(i32, i32)> { + let RequestResponse::WindowProps { size, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + size + } + + pub fn loc(&self) -> Option<(i32, i32)> { + let RequestResponse::WindowProps { loc, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + loc + } + + pub fn class(&self) -> Option { + let RequestResponse::WindowProps { class, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + class + } + + pub fn title(&self) -> Option { + let RequestResponse::WindowProps { title, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + title + } + + pub fn floating(&self) -> Option { + let RequestResponse::WindowProps { floating, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + floating + } + + pub fn maximized(&self) -> Option { + let RequestResponse::WindowProps { + fullscreen_or_maximized, + .. + } = request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + fullscreen_or_maximized.map(|fullscreen_or_maximized| { + matches!(fullscreen_or_maximized, FullscreenOrMaximized::Maximized) + }) + } + + pub fn fullscreen(&self) -> Option { + let RequestResponse::WindowProps { + fullscreen_or_maximized, + .. + } = request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + fullscreen_or_maximized.map(|fullscreen_or_maximized| { + matches!(fullscreen_or_maximized, FullscreenOrMaximized::Fullscreen) + }) + } + + pub fn focused(&self) -> Option { + let RequestResponse::WindowProps { focused, .. } = + request(Request::GetWindowProps { window_id: self.0 }) + else { + unreachable!() + }; + + focused + } +}