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}