pinnacle/api/rust/src/lib.rs

226 lines
7.3 KiB
Rust
Raw Normal View History

// 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/.
2024-03-15 20:51:12 -05:00
#![deny(elided_lifetimes_in_paths)]
#![warn(missing_docs)]
2023-10-19 20:19:00 -05:00
2024-01-21 23:45:09 -06:00
//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
//! configuration API.
//!
2024-01-22 00:09:11 -06:00
//! This library allows you to interface with the Pinnacle compositor and configure various aspects
2024-01-21 23:45:09 -06:00
//! 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 {
//! pinnacle,
//! process,
//! window,
//! input,
//! output,
//! tag,
//! } = modules;
//! }
//! ```
//!
//! ## 5. Begin crafting your config!
//! You can peruse the documentation for things to configure.
use std::{sync::OnceLock, time::Duration};
2024-01-21 23:45:09 -06:00
use futures::{future::BoxFuture, Future, StreamExt};
2024-01-21 23:45:09 -06:00
use input::Input;
2024-03-15 20:51:12 -05:00
use layout::Layout;
2024-01-21 23:45:09 -06:00
use output::Output;
use pinnacle::Pinnacle;
use process::Process;
2024-02-23 16:24:43 -06:00
use signal::SignalState;
2024-01-21 23:45:09 -06:00
use tag::Tag;
2024-02-23 16:24:43 -06:00
use tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver},
RwLock,
};
use tokio_stream::wrappers::UnboundedReceiverStream;
2024-01-21 23:45:09 -06:00
use tonic::transport::{Endpoint, Uri};
use tower::service_fn;
use window::Window;
pub mod input;
2024-03-15 20:51:12 -05:00
pub mod layout;
pub mod output;
2024-01-21 23:45:09 -06:00
pub mod pinnacle;
pub mod process;
2024-02-23 16:24:43 -06:00
pub mod signal;
pub mod tag;
2024-01-21 23:45:09 -06:00
pub mod util;
pub mod window;
2023-10-19 21:44:33 -05:00
2024-01-21 23:45:09 -06:00
pub use pinnacle_api_macros::config;
pub use tokio;
2023-10-19 21:44:33 -05:00
pub use xkbcommon;
2024-01-21 23:45:09 -06:00
static PINNACLE: OnceLock<Pinnacle> = OnceLock::new();
static PROCESS: OnceLock<Process> = OnceLock::new();
static WINDOW: OnceLock<Window> = OnceLock::new();
static INPUT: OnceLock<Input> = OnceLock::new();
static OUTPUT: OnceLock<Output> = OnceLock::new();
static TAG: OnceLock<Tag> = OnceLock::new();
2024-02-23 16:24:43 -06:00
static SIGNAL: OnceLock<RwLock<SignalState>> = OnceLock::new();
2024-03-15 20:51:12 -05:00
static LAYOUT: OnceLock<Layout> = OnceLock::new();
2024-01-21 23:45:09 -06:00
/// A struct containing static references to all of the configuration structs.
#[derive(Debug, Clone, Copy)]
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,
2024-03-15 20:51:12 -05:00
/// The [`Layout`] struct
pub layout: &'static Layout,
2023-10-19 17:43:37 -05:00
}
2023-10-18 23:05:07 -05:00
2024-01-21 23:45:09 -06:00
/// 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")?
2024-01-21 23:45:09 -06:00
.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?;
2024-03-15 20:51:12 -05:00
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
2024-01-21 23:45:09 -06:00
let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone()));
let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone()));
let window = WINDOW.get_or_init(|| Window::new(channel.clone()));
let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone()));
2024-02-23 16:24:43 -06:00
let tag = TAG.get_or_init(|| Tag::new(channel.clone()));
let output = OUTPUT.get_or_init(|| Output::new(channel.clone()));
2024-03-15 20:51:12 -05:00
let layout = LAYOUT.get_or_init(|| Layout::new(channel.clone()));
2024-02-23 16:24:43 -06:00
SIGNAL
.set(RwLock::new(SignalState::new(
channel.clone(),
fut_sender.clone(),
)))
.map_err(|_| "failed to create SIGNAL")?;
2024-01-21 23:45:09 -06:00
let modules = ApiModules {
pinnacle,
process,
window,
input,
output,
tag,
2024-03-15 20:51:12 -05:00
layout,
2023-10-19 17:43:37 -05:00
};
2024-01-21 23:45:09 -06:00
Ok((modules, fut_recv))
2023-10-18 23:05:07 -05:00
}
2023-10-20 17:56:34 -05:00
2024-01-21 23:45:09 -06:00
/// Listen to Pinnacle for incoming messages.
2023-10-20 17:56:34 -05:00
///
2024-01-21 23:45:09 -06:00
/// This will run all futures returned by configuration methods that take in callbacks in order to
/// call them.
2023-10-20 17:56:34 -05:00
///
2024-01-21 23:45:09 -06:00
/// This function is inserted at the end of your config through the [`config`] macro.
/// You should use the macro instead of this function directly.
2024-01-22 20:27:22 -06:00
pub async fn listen(fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
2024-02-23 16:30:59 -06:00
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
let pinnacle = PINNACLE.get().unwrap();
let keepalive = async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(err) = pinnacle.ping().await {
eprintln!("Failed to ping compositor: {err}");
std::process::exit(1);
}
2023-10-20 17:56:34 -05:00
}
};
tokio::spawn(keepalive);
while let Some(fut) = fut_recv.next().await {
tokio::spawn(fut);
2023-10-20 17:56:34 -05:00
}
}
/// 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)
})
}