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        MoveToOutputRequest, RemoveRequest, SetActiveRequest, SwitchToRequest,
31        move_to_output_response::error::Kind,
32    },
33    util::v1::SetOrToggle,
34};
35
36use crate::{
37    BlockOnTokio,
38    client::Client,
39    output::OutputHandle,
40    signal::{SignalHandle, TagSignal},
41    util::Batch,
42    window::WindowHandle,
43};
44
45/// Adds tags to the specified output.
46///
47/// This will add tags with the given names to `output` and return [`TagHandle`]s to all of
48/// them.
49///
50/// # Examples
51///
52/// ```no_run
53/// # use pinnacle_api::output;
54/// # use pinnacle_api::tag;
55/// // Add tags 1-5 to the focused output
56/// if let Some(op) = output::get_focused() {
57///     let tags = tag::add(&op, ["1", "2", "3", "4", "5"]);
58/// }
59/// ```
60pub fn add<I, T>(output: &OutputHandle, tag_names: I) -> impl Iterator<Item = TagHandle> + use<I, T>
61where
62    I: IntoIterator<Item = T>,
63    T: ToString,
64{
65    let output_name = output.name();
66    let tag_names = tag_names.into_iter().map(|name| name.to_string()).collect();
67
68    Client::tag()
69        .add(AddRequest {
70            output_name,
71            tag_names,
72        })
73        .block_on_tokio()
74        .unwrap()
75        .into_inner()
76        .tag_ids
77        .into_iter()
78        .map(|id| TagHandle { id })
79}
80
81/// Gets handles to all tags across all outputs.
82///
83/// # Examples
84///
85/// ```no_run
86/// # use pinnacle_api::tag;
87/// for tag in tag::get_all() {
88///     println!("{}", tag.name());
89/// }
90/// ```
91pub fn get_all() -> impl Iterator<Item = TagHandle> {
92    get_all_async().block_on_tokio()
93}
94
95/// Async impl for [`get_all_async`].
96pub async fn get_all_async() -> impl Iterator<Item = TagHandle> {
97    Client::tag()
98        .get(GetRequest {})
99        .await
100        .unwrap()
101        .into_inner()
102        .tag_ids
103        .into_iter()
104        .map(|id| TagHandle { id })
105}
106
107/// Gets a handle to the first tag with the given `name` on the focused output.
108///
109/// To get the first tag with the given `name` on a specific output, see
110/// [`get_on_output`].
111///
112/// # Examples
113///
114/// ```no_run
115/// # use pinnacle_api::tag;
116/// # || {
117/// let tag = tag::get("2")?;
118/// # Some(())
119/// # };
120/// ```
121pub fn get(name: impl ToString) -> Option<TagHandle> {
122    get_async(name).block_on_tokio()
123}
124
125/// Async impl for [`get`].
126pub async fn get_async(name: impl ToString) -> Option<TagHandle> {
127    let name = name.to_string();
128    let focused_op = crate::output::get_focused_async().await?;
129
130    get_on_output_async(name, &focused_op).await
131}
132
133/// Gets a handle to the first tag with the given `name` on `output`.
134///
135/// For a simpler way to get a tag on the focused output, see [`get`].
136///
137/// # Examples
138///
139/// ```no_run
140/// # use pinnacle_api::output;
141/// # use pinnacle_api::tag;
142/// # || {
143/// let output = output::get_by_name("eDP-1")?;
144/// let tag = tag::get_on_output("2", &output)?;
145/// # Some(())
146/// # };
147/// ```
148pub fn get_on_output(name: impl ToString, output: &OutputHandle) -> Option<TagHandle> {
149    get_on_output_async(name, output).block_on_tokio()
150}
151
152/// Async impl for [`get_on_output`].
153pub async fn get_on_output_async(name: impl ToString, output: &OutputHandle) -> Option<TagHandle> {
154    let name = name.to_string();
155    let output = output.clone();
156    get_all_async().await.batch_find(
157        |tag| async { (tag.name_async().await, tag.output_async().await) }.boxed(),
158        |(n, op)| *n == name && *op == output,
159    )
160}
161
162/// Removes the given tags from their outputs.
163///
164/// # Examples
165///
166/// ```no_run
167/// # use pinnacle_api::tag;
168/// # use pinnacle_api::output;
169/// # || {
170/// let tags = tag::add(&output::get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]);
171///
172/// tag::remove(tags); // "DP-1" no longer has any tags
173/// # Some(())
174/// # };
175/// ```
176pub fn remove(tags: impl IntoIterator<Item = TagHandle>) {
177    let tag_ids = tags.into_iter().map(|handle| handle.id).collect::<Vec<_>>();
178
179    Client::tag()
180        .remove(RemoveRequest { tag_ids })
181        .block_on_tokio()
182        .unwrap();
183}
184
185/// Error that happens when moving tags to a different output.
186#[derive(Debug, PartialEq, Clone)]
187pub enum MoveToOutputError {
188    /// The requested output to move the tag to, does not exist
189    OutputDoesNotExist,
190
191    /// Moving the Tag to another output would result in having the same window in multiple tags.
192    /// It contains a list of windows that would be on multiple outputs.
193    SameWindowOnTwoOutputs(Vec<WindowHandle>),
194}
195
196/// Moves existing tags to the specified output.
197///
198/// # Examples
199///
200/// ```no_run
201/// # || {
202/// # use pinnacle_api::output;
203/// # use pinnacle_api::tag;
204/// let output = output::get_by_name("eDP-1")?;
205/// let tag_to_move = tag::get("1")?;
206/// tag::move_to_output(&output, [tag_to_move]);
207/// # Some(())
208/// # };
209/// ```
210pub fn move_to_output<I>(output: &OutputHandle, tag_handles: I) -> Result<(), MoveToOutputError>
211where
212    I: IntoIterator<Item = TagHandle>,
213{
214    let output_name = output.name();
215    let tag_ids = tag_handles.into_iter().map(|h| h.id).collect();
216
217    let error = Client::tag()
218        .move_to_output(MoveToOutputRequest {
219            output_name,
220            tag_ids,
221        })
222        .block_on_tokio()
223        .unwrap()
224        .into_inner()
225        .error
226        .and_then(|error| error.kind);
227
228    match error {
229        None => Ok(()),
230        Some(Kind::OutputDoesNotExist(_)) => Err(MoveToOutputError::OutputDoesNotExist),
231        Some(Kind::SameWindowOnTwoOutputs(windows)) => {
232            Err(MoveToOutputError::SameWindowOnTwoOutputs(
233                windows
234                    .window_ids
235                    .into_iter()
236                    .map(WindowHandle::from_id)
237                    .collect(),
238            ))
239        }
240    }
241}
242
243/// Connects to a [`TagSignal`].
244///
245/// # Examples
246///
247/// ```no_run
248/// # use pinnacle_api::tag;
249/// # use pinnacle_api::signal::TagSignal;
250/// tag::connect_signal(TagSignal::Active(Box::new(|tag, active| {
251///     println!("Tag is active = {active}");
252/// })));
253/// ```
254pub fn connect_signal(signal: TagSignal) -> SignalHandle {
255    let mut signal_state = Client::signal_state();
256
257    match signal {
258        TagSignal::Active(f) => signal_state.tag_active.add_callback(f),
259        TagSignal::Created(f) => signal_state.tag_created.add_callback(f),
260        TagSignal::Removed(f) => signal_state.tag_removed.add_callback(f),
261    }
262}
263
264/// A handle to a tag.
265///
266/// This handle allows you to do things like switch to tags and get their properties.
267#[derive(Debug, Clone, PartialEq, Eq, Hash)]
268pub struct TagHandle {
269    pub(crate) id: u32,
270}
271
272impl TagHandle {
273    /// Creates a tag handle from a numeric id.
274    pub fn from_id(id: u32) -> Self {
275        Self { id }
276    }
277
278    /// Activates this tag and deactivates all other ones on the same output.
279    ///
280    /// This emulates what a traditional workspace is.
281    ///
282    /// # Examples
283    ///
284    /// ```no_run
285    /// # use pinnacle_api::tag;
286    /// // Assume the focused output has the following inactive tags and windows:
287    /// // "1": Alacritty
288    /// // "2": Firefox, Discord
289    /// // "3": Steam
290    /// # || {
291    /// tag::get("2")?.switch_to(); // Displays Firefox and Discord
292    /// tag::get("3")?.switch_to(); // Displays Steam
293    /// # Some(())
294    /// # };
295    /// ```
296    pub fn switch_to(&self) {
297        let tag_id = self.id;
298
299        Client::tag()
300            .switch_to(SwitchToRequest { tag_id })
301            .block_on_tokio()
302            .unwrap();
303    }
304
305    /// Sets this tag to active or not.
306    ///
307    /// While active, windows with this tag will be displayed.
308    ///
309    /// While inactive, windows with this tag will not be displayed unless they have other active
310    /// tags.
311    ///
312    /// # Examples
313    ///
314    /// ```no_run
315    /// # use pinnacle_api::tag;
316    /// // Assume the focused output has the following inactive tags and windows:
317    /// // "1": Alacritty
318    /// // "2": Firefox, Discord
319    /// // "3": Steam
320    /// # || {
321    /// tag::get("2")?.set_active(true);  // Displays Firefox and Discord
322    /// tag::get("3")?.set_active(true);  // Displays Firefox, Discord, and Steam
323    /// tag::get("2")?.set_active(false); // Displays Steam
324    /// # Some(())
325    /// # };
326    /// ```
327    pub fn set_active(&self, set: bool) {
328        let tag_id = self.id;
329
330        Client::tag()
331            .set_active(SetActiveRequest {
332                tag_id,
333                set_or_toggle: match set {
334                    true => SetOrToggle::Set,
335                    false => SetOrToggle::Unset,
336                }
337                .into(),
338            })
339            .block_on_tokio()
340            .unwrap();
341    }
342
343    /// Toggles this tag between active and inactive.
344    ///
345    /// While active, windows with this tag will be displayed.
346    ///
347    /// While inactive, windows with this tag will not be displayed unless they have other active
348    /// tags.
349    ///
350    /// # Examples
351    ///
352    /// ```no_run
353    /// # use pinnacle_api::tag;
354    /// // Assume the focused output has the following inactive tags and windows:
355    /// // "1": Alacritty
356    /// // "2": Firefox, Discord
357    /// // "3": Steam
358    /// # || {
359    /// tag::get("2")?.toggle_active(); // Displays Firefox and Discord
360    /// tag::get("3")?.toggle_active(); // Displays Firefox, Discord, and Steam
361    /// tag::get("3")?.toggle_active(); // Displays Firefox, Discord
362    /// tag::get("2")?.toggle_active(); // Displays nothing
363    /// # Some(())
364    /// # };
365    /// ```
366    pub fn toggle_active(&self) {
367        let tag_id = self.id;
368
369        Client::tag()
370            .set_active(SetActiveRequest {
371                tag_id,
372                set_or_toggle: SetOrToggle::Toggle.into(),
373            })
374            .block_on_tokio()
375            .unwrap();
376    }
377
378    /// Moves this tag to the specified output.
379    ///
380    /// See [tag::move_to_output][crate::tag::move_to_output] for more information.
381    pub fn move_to_output(&self, output: &OutputHandle) -> Result<(), MoveToOutputError> {
382        move_to_output(output, [self.clone()])
383    }
384
385    /// Removes this tag from its output.
386    ///
387    /// # Examples
388    ///
389    /// ```no_run
390    /// # use pinnacle_api::tag;
391    /// # use pinnacle_api::output;
392    /// # || {
393    /// let tags =
394    ///     tag::add(&output::get_by_name("DP-1")?, ["1", "2", "Buckle", "Shoe"]).collect::<Vec<_>>();
395    ///
396    /// tags[1].remove();
397    /// tags[3].remove();
398    /// # Some(())
399    /// # };
400    /// // "DP-1" now only has tags "1" and "Buckle"
401    /// ```
402    pub fn remove(&self) {
403        let tag_id = self.id;
404
405        Client::tag()
406            .remove(RemoveRequest {
407                tag_ids: vec![tag_id],
408            })
409            .block_on_tokio()
410            .unwrap();
411    }
412
413    /// Gets whether or not this tag is active.
414    pub fn active(&self) -> bool {
415        self.active_async().block_on_tokio()
416    }
417
418    /// Async impl for [`Self::active`].
419    pub async fn active_async(&self) -> bool {
420        let tag_id = self.id;
421
422        Client::tag()
423            .get_active(GetActiveRequest { tag_id })
424            .await
425            .unwrap()
426            .into_inner()
427            .active
428    }
429
430    /// Gets this tag's name.
431    pub fn name(&self) -> String {
432        self.name_async().block_on_tokio()
433    }
434
435    /// Async impl for [`Self::name`].
436    pub async fn name_async(&self) -> String {
437        let tag_id = self.id;
438
439        Client::tag()
440            .get_name(GetNameRequest { tag_id })
441            .await
442            .unwrap()
443            .into_inner()
444            .name
445    }
446
447    /// Gets a handle to the output this tag is on.
448    pub fn output(&self) -> OutputHandle {
449        self.output_async().block_on_tokio()
450    }
451
452    /// Async impl for [`Self::output`].
453    pub async fn output_async(&self) -> OutputHandle {
454        let tag_id = self.id;
455
456        let name = Client::tag()
457            .get_output_name(GetOutputNameRequest { tag_id })
458            .await
459            .unwrap()
460            .into_inner()
461            .output_name;
462        OutputHandle { name }
463    }
464
465    /// Gets all windows with this tag.
466    pub fn windows(&self) -> impl Iterator<Item = WindowHandle> + use<> {
467        self.windows_async().block_on_tokio()
468    }
469
470    /// Async impl for [`Self::windows`].
471    pub async fn windows_async(&self) -> impl Iterator<Item = WindowHandle> + use<> {
472        let windows = crate::window::get_all_async().await;
473        let this = self.clone();
474        windows.batch_filter(
475            |win| win.tags_async().boxed(),
476            move |mut tags| tags.any(|tag| tag == this),
477        )
478    }
479
480    /// Gets this tag's raw compositor id.
481    pub fn id(&self) -> u32 {
482        self.id
483    }
484}