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}