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