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}