pinnacle_api/
tag.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//! Tag management.
6//!
7//! This module allows you to interact with Pinnacle's tag system.
8//!
9//! # The Tag System
10//! Many Wayland compositors use workspaces for window management.
11//! Each window is assigned to a workspace and will only show up if that workspace is being
12//! viewed. This is a find way to manage windows, but it's not that powerful.
13//!
14//! Instead, Pinnacle works with a tag system similar to window managers like [dwm](https://dwm.suckless.org/)
15//! and, the window manager Pinnacle takes inspiration from, [awesome](https://awesomewm.org/).
16//!
17//! In a tag system, there are no workspaces. Instead, each window can be tagged with zero or more
18//! tags, and zero or more tags can be displayed on a monitor at once. This allows you to, for
19//! example, bring in your browsers on the same screen as your IDE by toggling the "Browser" tag.
20//!
21//! Workspaces can be emulated by only displaying one tag at a time. Combining this feature with
22//! the ability to tag windows with multiple tags allows you to have one window show up on multiple
23//! different "workspaces". As you can see, this system is much more powerful than workspaces
24//! alone.
25
26use futures::FutureExt;
27use pinnacle_api_defs::pinnacle::{
28    tag::v1::{
29        AddRequest, GetActiveRequest, GetNameRequest, GetOutputNameRequest, GetRequest,
30        RemoveRequest, SetActiveRequest, SwitchToRequest,
31    },
32    util::v1::SetOrToggle,
33};
34
35use crate::{
36    BlockOnTokio,
37    client::Client,
38    output::OutputHandle,
39    signal::{SignalHandle, TagSignal},
40    util::Batch,
41    window::WindowHandle,
42};
43
44/// Adds tags to the specified output.
45///
46/// This will add tags with the given names to `output` and return [`TagHandle`]s to all of
47/// them.
48///
49/// # Examples
50///
51/// ```no_run
52/// # use pinnacle_api::output;
53/// # use pinnacle_api::tag;
54/// // Add tags 1-5 to the focused output
55/// if let Some(op) = output::get_focused() {
56///     let tags = tag::add(&op, ["1", "2", "3", "4", "5"]);
57/// }
58/// ```
59pub fn add<I, T>(output: &OutputHandle, tag_names: I) -> impl Iterator<Item = TagHandle> + use<I, T>
60where
61    I: IntoIterator<Item = T>,
62    T: ToString,
63{
64    let output_name = output.name();
65    let tag_names = tag_names.into_iter().map(|name| name.to_string()).collect();
66
67    Client::tag()
68        .add(AddRequest {
69            output_name,
70            tag_names,
71        })
72        .block_on_tokio()
73        .unwrap()
74        .into_inner()
75        .tag_ids
76        .into_iter()
77        .map(|id| TagHandle { id })
78}
79
80/// Gets handles to all tags across all outputs.
81///
82/// # Examples
83///
84/// ```no_run
85/// # use pinnacle_api::tag;
86/// for tag in tag::get_all() {
87///     println!("{}", tag.name());
88/// }
89/// ```
90pub fn get_all() -> impl Iterator<Item = TagHandle> {
91    get_all_async().block_on_tokio()
92}
93
94/// Async impl for [`get_all_async`].
95pub async fn get_all_async() -> impl Iterator<Item = TagHandle> {
96    Client::tag()
97        .get(GetRequest {})
98        .await
99        .unwrap()
100        .into_inner()
101        .tag_ids
102        .into_iter()
103        .map(|id| TagHandle { id })
104}
105
106/// Gets a handle to the first tag with the given `name` on the focused output.
107///
108/// To get the first tag with the given `name` on a specific output, see
109/// [`get_on_output`].
110///
111/// # Examples
112///
113/// ```no_run
114/// # use pinnacle_api::tag;
115/// # || {
116/// let tag = tag::get("2")?;
117/// # Some(())
118/// # };
119/// ```
120pub fn get(name: impl ToString) -> Option<TagHandle> {
121    get_async(name).block_on_tokio()
122}
123
124/// Async impl for [`get`].
125pub async fn get_async(name: impl ToString) -> Option<TagHandle> {
126    let name = name.to_string();
127    let focused_op = crate::output::get_focused_async().await?;
128
129    get_on_output_async(name, &focused_op).await
130}
131
132/// Gets a handle to the first tag with the given `name` on `output`.
133///
134/// For a simpler way to get a tag on the focused output, see [`get`].
135///
136/// # Examples
137///
138/// ```no_run
139/// # use pinnacle_api::output;
140/// # use pinnacle_api::tag;
141/// # || {
142/// let output = output::get_by_name("eDP-1")?;
143/// let tag = tag::get_on_output("2", &output)?;
144/// # Some(())
145/// # };
146/// ```
147pub fn get_on_output(name: impl ToString, output: &OutputHandle) -> Option<TagHandle> {
148    get_on_output_async(name, output).block_on_tokio()
149}
150
151/// Async impl for [`get_on_output`].
152pub async fn get_on_output_async(name: impl ToString, output: &OutputHandle) -> Option<TagHandle> {
153    let name = name.to_string();
154    let output = output.clone();
155    get_all_async().await.batch_find(
156        |tag| async { (tag.name_async().await, tag.output_async().await) }.boxed(),
157        |(n, op)| *n == name && *op == output,
158    )
159}
160
161/// Removes the given tags from their outputs.
162///
163/// # Examples
164///
165/// ```no_run
166/// # use pinnacle_api::tag;
167/// # use pinnacle_api::output;
168/// # || {
169/// let tags = tag::add(&output::get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]);
170///
171/// tag::remove(tags); // "DP-1" no longer has any tags
172/// # Some(())
173/// # };
174/// ```
175pub fn remove(tags: impl IntoIterator<Item = TagHandle>) {
176    let tag_ids = tags.into_iter().map(|handle| handle.id).collect::<Vec<_>>();
177
178    Client::tag()
179        .remove(RemoveRequest { tag_ids })
180        .block_on_tokio()
181        .unwrap();
182}
183
184/// Connects to a [`TagSignal`].
185///
186/// # Examples
187///
188/// ```no_run
189/// # use pinnacle_api::tag;
190/// # use pinnacle_api::signal::TagSignal;
191/// tag::connect_signal(TagSignal::Active(Box::new(|tag, active| {
192///     println!("Tag is active = {active}");
193/// })));
194/// ```
195pub fn connect_signal(signal: TagSignal) -> SignalHandle {
196    let mut signal_state = Client::signal_state();
197
198    match signal {
199        TagSignal::Active(f) => signal_state.tag_active.add_callback(f),
200        TagSignal::Created(f) => signal_state.tag_created.add_callback(f),
201        TagSignal::Removed(f) => signal_state.tag_removed.add_callback(f),
202    }
203}
204
205/// A handle to a tag.
206///
207/// This handle allows you to do things like switch to tags and get their properties.
208#[derive(Debug, Clone, PartialEq, Eq, Hash)]
209pub struct TagHandle {
210    pub(crate) id: u32,
211}
212
213impl TagHandle {
214    /// Creates a tag handle from a numeric id.
215    pub fn from_id(id: u32) -> Self {
216        Self { id }
217    }
218
219    /// Activates this tag and deactivates all other ones on the same output.
220    ///
221    /// This emulates what a traditional workspace is.
222    ///
223    /// # Examples
224    ///
225    /// ```no_run
226    /// # use pinnacle_api::tag;
227    /// // Assume the focused output has the following inactive tags and windows:
228    /// // "1": Alacritty
229    /// // "2": Firefox, Discord
230    /// // "3": Steam
231    /// # || {
232    /// tag::get("2")?.switch_to(); // Displays Firefox and Discord
233    /// tag::get("3")?.switch_to(); // Displays Steam
234    /// # Some(())
235    /// # };
236    /// ```
237    pub fn switch_to(&self) {
238        let tag_id = self.id;
239
240        Client::tag()
241            .switch_to(SwitchToRequest { tag_id })
242            .block_on_tokio()
243            .unwrap();
244    }
245
246    /// Sets this tag to active or not.
247    ///
248    /// While active, windows with this tag will be displayed.
249    ///
250    /// While inactive, windows with this tag will not be displayed unless they have other active
251    /// tags.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// # use pinnacle_api::tag;
257    /// // Assume the focused output has the following inactive tags and windows:
258    /// // "1": Alacritty
259    /// // "2": Firefox, Discord
260    /// // "3": Steam
261    /// # || {
262    /// tag::get("2")?.set_active(true);  // Displays Firefox and Discord
263    /// tag::get("3")?.set_active(true);  // Displays Firefox, Discord, and Steam
264    /// tag::get("2")?.set_active(false); // Displays Steam
265    /// # Some(())
266    /// # };
267    /// ```
268    pub fn set_active(&self, set: bool) {
269        let tag_id = self.id;
270
271        Client::tag()
272            .set_active(SetActiveRequest {
273                tag_id,
274                set_or_toggle: match set {
275                    true => SetOrToggle::Set,
276                    false => SetOrToggle::Unset,
277                }
278                .into(),
279            })
280            .block_on_tokio()
281            .unwrap();
282    }
283
284    /// Toggles this tag between active and inactive.
285    ///
286    /// While active, windows with this tag will be displayed.
287    ///
288    /// While inactive, windows with this tag will not be displayed unless they have other active
289    /// tags.
290    ///
291    /// # Examples
292    ///
293    /// ```no_run
294    /// # use pinnacle_api::tag;
295    /// // Assume the focused output has the following inactive tags and windows:
296    /// // "1": Alacritty
297    /// // "2": Firefox, Discord
298    /// // "3": Steam
299    /// # || {
300    /// tag::get("2")?.toggle_active(); // Displays Firefox and Discord
301    /// tag::get("3")?.toggle_active(); // Displays Firefox, Discord, and Steam
302    /// tag::get("3")?.toggle_active(); // Displays Firefox, Discord
303    /// tag::get("2")?.toggle_active(); // Displays nothing
304    /// # Some(())
305    /// # };
306    /// ```
307    pub fn toggle_active(&self) {
308        let tag_id = self.id;
309
310        Client::tag()
311            .set_active(SetActiveRequest {
312                tag_id,
313                set_or_toggle: SetOrToggle::Toggle.into(),
314            })
315            .block_on_tokio()
316            .unwrap();
317    }
318
319    /// Removes this tag from its output.
320    ///
321    /// # Examples
322    ///
323    /// ```no_run
324    /// # use pinnacle_api::tag;
325    /// # use pinnacle_api::output;
326    /// # || {
327    /// let tags =
328    ///     tag::add(&output::get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]).collect::<Vec<_>>();
329    ///
330    /// tags[1].remove();
331    /// tags[3].remove();
332    /// # Some(())
333    /// # };
334    /// // "DP-1" now only has tags "1" and "Buckle"
335    /// ```
336    pub fn remove(&self) {
337        let tag_id = self.id;
338
339        Client::tag()
340            .remove(RemoveRequest {
341                tag_ids: vec![tag_id],
342            })
343            .block_on_tokio()
344            .unwrap();
345    }
346
347    /// Gets whether or not this tag is active.
348    pub fn active(&self) -> bool {
349        self.active_async().block_on_tokio()
350    }
351
352    /// Async impl for [`Self::active`].
353    pub async fn active_async(&self) -> bool {
354        let tag_id = self.id;
355
356        Client::tag()
357            .get_active(GetActiveRequest { tag_id })
358            .await
359            .unwrap()
360            .into_inner()
361            .active
362    }
363
364    /// Gets this tag's name.
365    pub fn name(&self) -> String {
366        self.name_async().block_on_tokio()
367    }
368
369    /// Async impl for [`Self::name`].
370    pub async fn name_async(&self) -> String {
371        let tag_id = self.id;
372
373        Client::tag()
374            .get_name(GetNameRequest { tag_id })
375            .await
376            .unwrap()
377            .into_inner()
378            .name
379    }
380
381    /// Gets a handle to the output this tag is on.
382    pub fn output(&self) -> OutputHandle {
383        self.output_async().block_on_tokio()
384    }
385
386    /// Async impl for [`Self::output`].
387    pub async fn output_async(&self) -> OutputHandle {
388        let tag_id = self.id;
389
390        let name = Client::tag()
391            .get_output_name(GetOutputNameRequest { tag_id })
392            .await
393            .unwrap()
394            .into_inner()
395            .output_name;
396        OutputHandle { name }
397    }
398
399    /// Gets all windows with this tag.
400    pub fn windows(&self) -> impl Iterator<Item = WindowHandle> + use<> {
401        self.windows_async().block_on_tokio()
402    }
403
404    /// Async impl for [`Self::windows`].
405    pub async fn windows_async(&self) -> impl Iterator<Item = WindowHandle> + use<> {
406        let windows = crate::window::get_all_async().await;
407        let this = self.clone();
408        windows.batch_filter(
409            |win| win.tags_async().boxed(),
410            move |mut tags| tags.any(|tag| tag == this),
411        )
412    }
413
414    /// Gets this tag's raw compositor id.
415    pub fn id(&self) -> u32 {
416        self.id
417    }
418}