pinnacle_api/lib.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5#![warn(missing_docs)]
6
7//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
8//! configuration API.
9//!
10//! This library allows you to interface with the Pinnacle compositor and configure various aspects
11//! like input and the tag system.
12//!
13//! # Configuration
14//!
15//! ## With the config generation CLI
16//!
17//! To create a Rust config using the config generation CLI, run
18//!
19//! ```sh
20//! pinnacle config gen
21//! ```
22//!
23//! and step through the interactive generator (be sure to select Rust as the language).
24//! This will create the default config in the specified directory.
25//!
26//! ## Manually
27//!
28//! ### 1. Create a Cargo project
29//!
30//! Create a Cargo project in your config directory with `cargo init`.
31//!
32//! ### 2. Create `pinnacle.toml`
33//!
34//! `pinnacle.toml` is what tells Pinnacle what command is used to start the config.
35//!
36//! Create `pinnacle.toml` at the root of the cargo project and add the following to it:
37//! ```toml
38//! run = ["cargo", "run"]
39//! ```
40//!
41//! Pinnacle will now use `cargo run` to start your config.
42//!
43//! ## 3. Set up dependencies
44//!
45//! In your `Cargo.toml`, add `pinnacle-api` as a dependency:
46//!
47//! ```toml
48//! [dependencies]
49//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" }
50//! ```
51//!
52//! ## 4. Set up the main function
53//!
54//! In `main.rs`, remove the main function and create an `async` one. This is where your config
55//! will start from. Then, call the [`main`] macro, which will create a `tokio` main function
56//! that will perform the necessary setup and call your async function.
57//!
58//! ```
59//! async fn config() {
60//! // Your config here
61//! }
62//!
63//! pinnacle_api::main!(config);
64//! ```
65//!
66//! ## 5. Begin crafting your config!
67//!
68//! Take a look at the default config or browse the docs to see what you can do.
69
70use client::Client;
71use futures::{Future, StreamExt};
72use hyper_util::rt::TokioIo;
73use tonic::transport::{Endpoint, Uri};
74use tower::service_fn;
75
76pub mod debug;
77pub mod input;
78pub mod layout;
79pub mod output;
80pub mod pinnacle;
81pub mod process;
82pub mod render;
83pub mod signal;
84#[cfg(feature = "snowcap")]
85pub mod snowcap;
86pub mod tag;
87pub mod util;
88pub mod window;
89
90mod client;
91
92pub use tokio;
93pub use xkbcommon::xkb::Keysym;
94
95const SOCKET_PATH: &str = "PINNACLE_GRPC_SOCKET";
96
97/// Connects to Pinnacle.
98///
99/// This function is called by the [`main`] and [`config`] macros.
100/// You'll only need to use this if you aren't using them.
101pub async fn connect() -> Result<(), Box<dyn std::error::Error>> {
102 // port doesn't matter, we use a unix socket
103 let channel = Endpoint::try_from("http://[::]:50051")?
104 .connect_with_connector(service_fn(|_: Uri| async {
105 let path = std::env::var(SOCKET_PATH)
106 .expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?");
107
108 Ok::<_, std::io::Error>(TokioIo::new(tokio::net::UnixStream::connect(path).await?))
109 }))
110 .await
111 .unwrap();
112
113 let socket_path = std::env::var(SOCKET_PATH).unwrap();
114 println!("Connected to {socket_path}");
115
116 Client::init(channel.clone());
117
118 #[cfg(feature = "snowcap")]
119 snowcap_api::connect().await.unwrap();
120
121 Ok(())
122}
123
124/// Blocks until Pinnacle exits.
125///
126/// This function is called by the [`main`] and [`config`] macros.
127/// You'll only need to use this if you aren't using them.
128pub async fn block() {
129 let (_sender, mut keepalive_stream) = crate::pinnacle::keepalive().await;
130
131 // This will trigger either when the compositor sends the shutdown signal
132 // or when it exits (in which case the stream receives an error)
133 keepalive_stream.next().await;
134
135 Client::signal_state().shutdown();
136}
137
138trait BlockOnTokio {
139 type Output;
140
141 fn block_on_tokio(self) -> Self::Output;
142}
143
144impl<F: Future> BlockOnTokio for F {
145 type Output = F::Output;
146
147 /// Blocks on a future using the current Tokio runtime.
148 fn block_on_tokio(self) -> Self::Output {
149 tokio::task::block_in_place(|| {
150 let handle = tokio::runtime::Handle::current();
151 handle.block_on(self)
152 })
153 }
154}
155
156/// Defines the config's main entry point.
157///
158/// This macro creates a `main` function annotated with
159/// `#[pinnacle_api::tokio::main]` that performs necessary setup
160/// and calls the provided async function.
161///
162/// # Examples
163///
164/// ```no_run
165/// async fn config() {}
166///
167/// pinnacle_api::main!(config);
168/// ```
169#[macro_export]
170macro_rules! main {
171 ($func:ident) => {
172 #[$crate::tokio::main(crate = "pinnacle_api::tokio")]
173 async fn main() {
174 $crate::config!($func);
175 }
176 };
177}
178
179/// Connects to Pinnacle before calling the provided async function,
180/// then blocks until Pinnacle exits.
181///
182/// This macro is called by [`main`]. It is exposed for use in case you
183/// need to change the generated main function.
184///
185/// # Examples
186///
187/// ```no_run
188/// async fn config() {}
189///
190/// #[pinnacle_api::tokio::main(worker_threads = 8)]
191/// async fn main() {
192/// pinnacle_api::config!(config);
193/// }
194/// ```
195#[macro_export]
196macro_rules! config {
197 ($func:ident) => {{
198 let (send, mut recv) = $crate::tokio::sync::mpsc::unbounded_channel();
199
200 let hook = ::std::panic::take_hook();
201 ::std::panic::set_hook(::std::boxed::Box::new(move |info| {
202 hook(info);
203 let backtrace = ::std::backtrace::Backtrace::force_capture();
204 let error_msg = format!("{info}\n{backtrace}");
205 let _ = send.send(error_msg);
206 }));
207
208 $crate::connect().await.unwrap();
209 $func().await;
210 $crate::tokio::select! {
211 Some(error) = recv.recv() => {
212 $crate::pinnacle::set_last_error(error);
213 }
214 _ = $crate::block() => (),
215 }
216 }};
217}