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}