pinnacle/api/rust/src/lib.rs
Ottatop 43809d4210 Remove statics from Rust API
And a bunch of other things, this commit doesn't build
2024-04-16 11:10:47 -05:00

238 lines
7.7 KiB
Rust

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
#![deny(elided_lifetimes_in_paths)]
#![warn(missing_docs)]
//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
//! configuration API.
//!
//! This library allows you to interface with the Pinnacle compositor and configure various aspects
//! like input and the tag system.
//!
//! # Configuration
//!
//! ## 1. Create a cargo project
//! To create your own Rust config, create a Cargo project in `~/.config/pinnacle`.
//!
//! ## 2. Create `metaconfig.toml`
//! Then, create a file named `metaconfig.toml`. This is the file Pinnacle will use to determine
//! what to run, kill and reload-config keybinds, an optional socket directory, and any environment
//! variables to give the config client.
//!
//! In `metaconfig.toml`, put the following:
//! ```toml
//! # `command` will tell Pinnacle to run `cargo run` in your config directory.
//! # You can add stuff like "--release" here if you want to.
//! command = ["cargo", "run"]
//!
//! # You must define a keybind to reload your config if it crashes, otherwise you'll get stuck if
//! # the Lua config doesn't kick in properly.
//! reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" }
//!
//! # Similarly, you must define a keybind to kill Pinnacle.
//! kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" }
//!
//! # You can specify an optional socket directory if you need to place the socket Pinnacle will
//! # use for configuration in a different place.
//! # socket_dir = "your/dir/here"
//!
//! # If you need to set any environment variables for the config process, you can do so here if
//! # you don't want to do it in the config itself.
//! [envs]
//! # key = "value"
//! ```
//!
//! ## 3. Set up dependencies
//! In your `Cargo.toml`, add a dependency to `pinnacle-api`:
//!
//! ```toml
//! # Cargo.toml
//!
//! [dependencies]
//! pinnacle-api = { git = "https://github.com/pinnacle-comp/pinnacle" }
//! ```
//!
//! ## 4. Set up the main function
//! In `main.rs`, change `fn main()` to `async fn main()` and annotate it with the
//! [`pinnacle_api::config`][`crate::config`] macro. Pass in the identifier you want to bind the
//! config modules to:
//!
//! ```
//! use pinnacle_api::ApiModules;
//!
//! #[pinnacle_api::config(modules)]
//! async fn main() {
//! // `modules` is now available in the function body.
//! // You can deconstruct `ApiModules` to get all the config structs.
//! let ApiModules {
//! ..
//! } = modules;
//! }
//! ```
//!
//! ## 5. Begin crafting your config!
//! You can peruse the documentation for things to configure.
use std::{sync::Arc, time::Duration};
use futures::{future::BoxFuture, Future, StreamExt};
use input::Input;
use layout::Layout;
use output::Output;
use pinnacle::Pinnacle;
use process::Process;
use render::Render;
use signal::SignalState;
use tag::Tag;
use tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver},
RwLock,
};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tonic::transport::{Endpoint, Uri};
use tower::service_fn;
use window::Window;
pub mod input;
pub mod layout;
pub mod output;
pub mod pinnacle;
pub mod process;
pub mod render;
pub mod signal;
pub mod tag;
pub mod util;
pub mod window;
pub use pinnacle_api_macros::config;
pub use tokio;
pub use xkbcommon;
/// A struct containing static references to all of the configuration structs.
#[non_exhaustive]
#[derive(Clone)]
pub struct ApiModules {
/// The [`Pinnacle`] struct
pub pinnacle: &'static Pinnacle,
/// The [`Process`] struct
pub process: &'static Process,
/// The [`Window`] struct
pub window: &'static Window,
/// The [`Input`] struct
pub input: &'static Input,
/// The [`Output`] struct
pub output: &'static Output,
/// The [`Tag`] struct
pub tag: &'static Tag,
/// The [`Layout`] struct
pub layout: &'static Layout,
/// The [`Render`] struct
pub render: &'static Render,
signal: Arc<RwLock<SignalState>>,
}
impl std::fmt::Debug for ApiModules {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiModules")
.field("pinnacle", &self.pinnacle)
.field("process", &self.process)
.field("window", &self.window)
.field("input", &self.input)
.field("output", &self.output)
.field("tag", &self.tag)
.field("layout", &self.layout)
.field("render", &self.render)
.field("signal", &"...")
.finish()
}
}
/// Connects to Pinnacle and builds the configuration structs.
///
/// This function is inserted at the top of your config through the [`config`] macro.
/// You should use that macro instead of this function directly.
pub async fn connect(
) -> Result<(ApiModules, UnboundedReceiver<BoxFuture<'static, ()>>), Box<dyn std::error::Error>> {
// port doesn't matter, we use a unix socket
let channel = Endpoint::try_from("http://[::]:50051")?
.connect_with_connector(service_fn(|_: Uri| {
tokio::net::UnixStream::connect(
std::env::var("PINNACLE_GRPC_SOCKET")
.expect("PINNACLE_GRPC_SOCKET was not set; is Pinnacle running?"),
)
}))
.await?;
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
let signal = Arc::new(RwLock::new(SignalState::new(
channel.clone(),
fut_sender.clone(),
)));
let pinnacle = Box::leak(Box::new(Pinnacle::new(channel.clone())));
let process = Box::leak(Box::new(Process::new(channel.clone(), fut_sender.clone())));
let window = Box::leak(Box::new(Window::new(channel.clone())));
let input = Box::leak(Box::new(Input::new(channel.clone(), fut_sender.clone())));
let output = Box::leak(Box::new(Output::new(channel.clone())));
let tag = Box::leak(Box::new(Tag::new(channel.clone())));
let render = Box::leak(Box::new(Render::new(channel.clone())));
let layout = Box::leak(Box::new(Layout::new(channel.clone())));
let modules = ApiModules {
pinnacle,
process,
window,
input,
output,
tag,
layout,
render,
signal: signal.clone(),
};
window.finish_init(modules.clone());
output.finish_init(modules.clone());
tag.finish_init(modules.clone());
layout.finish_init(modules.clone());
signal.read().await.finish_init(modules.clone());
Ok((modules, fut_recv))
}
/// Listen to Pinnacle for incoming messages.
///
/// This will run all futures returned by configuration methods that take in callbacks in order to
/// call them.
///
/// This function is inserted at the end of your config through the [`config`] macro.
/// You should use the macro instead of this function directly.
pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
let keepalive = async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(err) = api.pinnacle.ping().await {
eprintln!("Failed to ping compositor: {err}");
std::process::exit(1);
}
}
};
tokio::spawn(keepalive);
while let Some(fut) = fut_recv.next().await {
tokio::spawn(fut);
}
}
/// Block on a future using the current Tokio runtime.
pub(crate) fn block_on_tokio<F: Future>(future: F) -> F::Output {
tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(future)
})
}