sawfish_client/lib.rs
1// sawfish-client -- client library to communicate with Sawfish window manager
2// © 2025 by Michał Nazarewicz <mina86@mina86.com>
3//
4// sawfish-client is free software: you can redistribute it and/or modify it
5// under the terms of the GNU Lesser General Public License as published by the
6// Free Software Foundation; either version 3 of the License, or (at your
7// option) any later version.
8//
9// sawfish-client is distributed in the hope that it will be useful, but WITHOUT
10// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12// details.
13//
14// You should have received a copy of the GNU General Public License along with
15// sawfish-client. If not, see <http://www.gnu.org/licenses/>.
16
17#![cfg_attr(docsrs, feature(doc_cfg))]
18#![doc = include_str!("../README.md")]
19
20use std::borrow::Cow;
21
22#[cfg(feature = "async")]
23use futures_util::io::{AsyncRead, AsyncWrite};
24
25mod error;
26mod unix;
27#[cfg(feature = "experimental-xcb")]
28mod x11;
29
30pub use error::{ConnError, EvalError};
31
32/// A connection to the Sawfish window manager.
33pub struct Client(Inner);
34
35/// Result of a form evaluation.
36///
37/// If the form was successfully evaluated, the response from the server (with
38/// the value the form evaluated to) is represented by the `Ok` variant. If the
39/// form failed to evaluated (most likely due to syntax error), the error
40/// message is represented by the `Err` variant.
41pub type EvalResponse = Result<Vec<u8>, Vec<u8>>;
42
43enum Inner {
44 Unix(unix::Client),
45 X11(x11::Client),
46}
47
48impl Client {
49 /// Opens a connection to the Sawfish server.
50 ///
51 /// The `display` argument specifies an optional display string, (such as
52 /// `":0"`). If not provided, the `DISPLAY` environment variable is used.
53 ///
54 /// Tries to connect to the Unix socket of the Sawfish server. If that
55 /// fails and the `experimental-xcb` Cargo feature is enabled, tries using
56 /// X11 protocol to communicate with Sawfish.
57 pub fn open(display: Option<&str>) -> Result<Self, ConnError> {
58 let display = get_display(display)?;
59 match unix::Client::open(&display) {
60 Ok(client) => Ok(Self(Inner::Unix(client))),
61 Err(err) => x11::Client::fallback(&display, err)
62 .map(|client| Self(Inner::X11(client))),
63 }
64 }
65
66 /// Sends a Lisp `form` to the Sawfish server for evaluation and waits for
67 /// a reply.
68 ///
69 /// * If there’s an error sending the `form` to the server (e.g. an I/O
70 /// error), returns an `Err(error)` value.
71 /// * Otherwise, if the `form` has been successfully sent to the server but
72 /// evaluation failed, returns `Ok(Err(data))` value.
73 /// * Otherwise, if the `form` has been successfully executed by the server,
74 /// returns `Ok(Ok(data))` value.
75 ///
76 /// # Example
77 ///
78 /// ```no_run
79 /// let mut client = sawfish_client::Client::open(None).unwrap();
80 /// match client.eval("(system-name)") {
81 /// Ok(Ok(data)) => {
82 /// println!("Form evaluated to: {}",
83 /// String::from_utf8_lossy(&data))
84 /// }
85 /// Ok(Err(data)) => {
86 /// println!("Error evaluating form: {}",
87 /// String::from_utf8_lossy(&data))
88 /// }
89 /// Err(err) => println!("Communication error: {err}")
90 /// }
91 /// ```
92 pub fn eval(
93 &mut self,
94 form: impl AsRef<[u8]>,
95 ) -> Result<EvalResponse, EvalError> {
96 match &mut self.0 {
97 Inner::Unix(client) => client.eval(form.as_ref(), false),
98 Inner::X11(client) => client.eval(form.as_ref(), false),
99 }
100 }
101
102 /// Sends a Lisp `form` to the Sawfish server for evaluation but does not
103 /// wait for a reply.
104 ///
105 /// If there’s an error sending the `form` to the server (e.g. an I/O
106 /// error), returns an `Err(error)` value. Otherwise, so long as the `form`
107 /// was successfully sent, returns `Ok(())` even if evaluation on the server
108 /// side has changed (e.g. due to syntax error). Use [`Self::eval`] instead
109 /// to check whether evaluation succeeded.
110 ///
111 /// # Example
112 ///
113 /// ```no_run
114 /// let mut client = sawfish_client::Client::open(None).unwrap();
115 /// match client.send("(set-screen-viewport 0 0)") {
116 /// Ok(()) => println!("Form successfully sent"),
117 /// Err(err) => println!("Communication error: {err}")
118 /// }
119 /// ```
120 pub fn send(&mut self, form: impl AsRef<[u8]>) -> Result<(), EvalError> {
121 match &mut self.0 {
122 Inner::Unix(client) => client.eval(form.as_ref(), true).map(|_| ()),
123 Inner::X11(client) => client.eval(form.as_ref(), true).map(|_| ()),
124 }
125 }
126}
127
128/// Opens a connection to the Sawfish server.
129///
130/// This is a convenience alias for [`Client::open`].
131#[inline]
132pub fn open(display: Option<&str>) -> Result<Client, ConnError> {
133 Client::open(display)
134}
135
136
137/// A connection to the Sawfish window manager using asynchronous I/O.
138#[cfg(feature = "async")]
139pub struct AsyncClient<S>(unix::AsyncClient<S>);
140
141/// An alias for the [`AsyncClient`] which uses Tokio runtime Unix stream.
142///
143/// # Example
144///
145/// ```no_run
146/// use tokio_util::compat::TokioAsyncReadCompatExt;
147///
148/// async fn print_system_name() {
149/// let mut client = sawfish_client::open_tokio(None).await.unwrap();
150/// let sysname = client.eval("(system-name)").await.unwrap().unwrap();
151/// println!("{}", String::from_utf8_lossy(&sysname));
152/// }
153/// ```
154#[cfg(feature = "tokio")]
155pub type TokioClient =
156 AsyncClient<tokio_util::compat::Compat<tokio::net::UnixStream>>;
157
158#[cfg(feature = "tokio")]
159impl AsyncClient<tokio_util::compat::Compat<tokio::net::UnixStream>> {
160 /// Opens a connection to the Sawfish server using the Tokio runtime.
161 ///
162 /// The `display` argument specifies an optional display string, (such as
163 /// `":0"`). If not provided, the `DISPLAY` environment variable is used.
164 pub async fn open(display: Option<&str>) -> Result<Self, ConnError> {
165 let display = get_display(display)?;
166 unix::AsyncClient::open(&display).await.map(Self)
167 }
168}
169
170/// Opens a connection to the Sawfish server using the Tokio runtime.
171///
172/// This is a convenience alias for [`AsyncClient::open`] with the generic
173/// argument `S` set to Tokio Unix stream type.
174#[cfg(feature = "tokio")]
175#[inline]
176pub async fn open_tokio(
177 display: Option<&str>,
178) -> Result<TokioClient, ConnError> {
179 TokioClient::open(display).await
180}
181
182#[cfg(feature = "async")]
183impl<S: AsyncRead + AsyncWrite + Unpin> AsyncClient<S> {
184 /// Constructs a connection to the Sawfish server over an asynchronous Unix
185 /// socket.
186 ///
187 /// Because the creation of an asynchronous Unix socket depends on the async
188 /// runtime, responsibility to open the connection falls on the caller. Use
189 /// [`server_path`] to determine path to the Unix Socket the Sawfish server
190 /// is (supposed to be) listening on.
191 ///
192 /// # Example
193 ///
194 /// ```no_run
195 /// use tokio_util::compat::TokioAsyncReadCompatExt;
196 ///
197 /// type TokioClient = sawfish_client::AsyncClient<
198 /// tokio_util::compat::Compat<tokio::net::UnixStream>>;
199 ///
200 /// async fn open() -> TokioClient {
201 /// let path = sawfish_client::server_path(None).unwrap();
202 /// let sock = tokio::net::UnixStream::connect(path).await.unwrap();
203 /// sawfish_client::AsyncClient::new(sock.compat())
204 /// }
205 /// ```
206 pub fn new(socket: S) -> Self { Self(unix::AsyncClient(socket)) }
207
208 /// Sends a Lisp `form` to the Sawfish server for evaluation and waits for
209 /// a reply.
210 ///
211 /// * If there’s an error sending the `form` to the server (e.g. an I/O
212 /// error), returns an `Err(error)` value.
213 /// * Otherwise, if the `form` has been successfully sent to the server but
214 /// evaluation failed (e.g. due to syntax error), returns `Ok(Err(data))`
215 /// value.
216 /// * Otherwise, if the `form` has been successfully executed by the server,
217 /// returns `Ok(Ok(data))` value.
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// use futures_util::{AsyncRead, AsyncWrite};
223 ///
224 /// async fn system_name<S: AsyncRead + AsyncWrite + Unpin>(
225 /// client: &mut sawfish_client::AsyncClient<S>,
226 /// ) -> Option<String> {
227 /// match client.eval("(system-name)").await {
228 /// Ok(Ok(data)) => {
229 /// Some(String::from_utf8_lossy(&data).into_owned())
230 /// }
231 /// Ok(Err(data)) => {
232 /// println!("Error evaluating form: {}",
233 /// String::from_utf8_lossy(&data));
234 /// None
235 /// }
236 /// Err(err) => {
237 /// println!("Communication error: {err}");
238 /// None
239 /// }
240 /// }
241 /// }
242 /// ```
243 pub async fn eval(
244 &mut self,
245 form: impl AsRef<[u8]>,
246 ) -> Result<EvalResponse, EvalError> {
247 self.0.eval(form.as_ref(), false).await
248 }
249
250 /// Sends a Lisp `form` to the Sawfish server for evaluation but does not
251 /// wait for a reply.
252 ///
253 /// If there’s an error sending the `form` to the server (e.g. an I/O
254 /// error), returns an `Err(error)` value. Otherwise, so long as the `form`
255 /// was successfully sent, returns `Ok(())` even if evaluation on the server
256 /// side has changed (e.g. due to syntax error). Use [`Self::eval`] instead
257 /// to check whether evaluation succeeded.
258 ///
259 /// # Example
260 ///
261 /// ```
262 /// use futures_util::{AsyncRead, AsyncWrite};
263 ///
264 /// async fn set_screen_viewport<S: AsyncRead + AsyncWrite + Unpin>(
265 /// client: &mut sawfish_client::AsyncClient<S>,
266 /// x: u32,
267 /// y: u32,
268 /// ) {
269 /// let form = format!("(set-screen-viewport {x} {y})");
270 /// if let Err(err) = client.send(&form).await {
271 /// println!("Communication error: {err}");
272 /// }
273 /// }
274 /// ```
275 pub async fn send(
276 &mut self,
277 form: impl AsRef<[u8]>,
278 ) -> Result<(), EvalError> {
279 self.0.eval(form.as_ref(), true).await.map(|_| ())
280 }
281}
282
283
284/// Returns path of the Unix socket the Sawfish server is (or should be)
285/// listening on.
286///
287/// Does not verify that the socket exists or the Sawfish server is listening on
288/// it. This is used for opening connections with [`AsyncClient::new`].
289///
290/// The Unix socket is located in `/tmp/.sawfish-$LOGNAME` directory.
291#[cfg(feature = "async")]
292pub fn server_path(
293 display: Option<&str>,
294) -> Result<std::path::PathBuf, ConnError> {
295 get_display(display).and_then(|display| unix::server_path(&display))
296}
297
298
299/// Unwraps the option or returns value of $DISPLAY environment variable.
300fn get_display(
301 display: Option<&str>,
302) -> Result<std::borrow::Cow<'_, str>, ConnError> {
303 display
304 .map(Cow::Borrowed)
305 .or_else(|| std::env::var("DISPLAY").map(Cow::Owned).ok())
306 .filter(|display| !display.is_empty())
307 .ok_or(ConnError::NoDisplay)
308}
309
310
311#[cfg(not(feature = "experimental-xcb"))]
312mod x11 {
313 use super::*;
314
315 pub enum Client {}
316
317 impl Client {
318 pub fn fallback(
319 _display: &str,
320 err: ConnError,
321 ) -> Result<Self, ConnError> {
322 Err(err)
323 }
324
325 pub fn eval(
326 &mut self,
327 _form: &[u8],
328 _is_async: bool,
329 ) -> Result<EvalResponse, EvalError> {
330 match *self {}
331 }
332 }
333}