leftwm-core 0.4.1

A window manager for Adventurers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
use super::TagId;
use crate::{layouts::Layout, Window, Workspace};
use serde::{Deserialize, Serialize};

/// Wrapper struct holding all the tags.
/// This wrapper provides convenience methods to change the tag-list
/// during its lifetime, while ensuring that all tags are in correct order
/// and numbered accordingly.
///
/// Each Tag is stored in either the 'normal' or the 'hidden' list.
/// This is just an internal simplification for handling the different
/// kinds of Tags.
///
/// ## Normal Tags
/// Normal tags are the tags visible to the user, those are the tags
/// that can be labelled via the config file. Tags are identified by a
/// unique ID which is automatically assigned by `LeftWM`, those IDs start at 1
/// and increment by 1. You can always expect that the tags are ordered by their ID
/// and that there is no gap between the numbers. The largest tag ID is equal to the
/// amount of normal tags. This also means: tags with larger IDs are always to the right
/// and tags with smaller IDs are always to the left of the reference tag.
///
/// Usually the number of normal tags is equal to the number of tags configured by the user.
/// However, if there are more workspaces than there are tags, additional "unnamed" tags
/// will be created automatically and appended to the list.
///
/// ## Hidden Tags
/// A hidden tag is a tag that is invisible and unknown to the user.
/// Those tags are created in the source code and can be used for
/// various purposes. The Scratchpad (NSP) feature is an example which uses
/// a hidden Tag called "NSP" to hide away the scratchpad until its summoned again.
/// You can think of a hidden tag as an invisible window storage. It is not possible
/// for a user to display a hidden tag in a workspace.
///
/// Hidden tags also have a unique ID starting at `usize::MAX`, decrementing by 1.
/// This prevents conflicts of Tag IDs between normal and hidden tags and makes it easier
/// to dynamically change normal tags and their ID without affecting hidden tags and vice versa.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Tags {
    // holds all the 'normal' tags
    normal: Vec<Tag>,

    // holds the 'hidden' tags (like 'NSP')
    hidden: Vec<Tag>,
}

impl Tags {
    /// Create a new empty Taglist
    pub const fn new() -> Self {
        Self {
            normal: vec![],
            hidden: vec![],
        }
    }

    /// Create a new tag with the provided label and layout,
    /// and append it to the list of normal tags.
    /// The ID will be assigned automatically and returned.
    pub fn add_new(&mut self, label: &str, layout: Layout) -> TagId {
        let next_id = self.normal.len() + 1; // tag id starts at 1
        let tag = Tag::new(next_id, label, layout);
        let id = tag.id;
        self.normal.push(tag);
        id
    }

    /// Create a new tag with the provided layout, labelling it directly with its ID,
    /// and append it to the list of normal tags.
    /// The ID will be assigned automatically and returned.
    pub fn add_new_unlabeled(&mut self, layout: Layout) -> TagId {
        let next_id = self.normal.len() + 1; // tag id starts at 1
        self.add_new(next_id.to_string().as_str(), layout)
    }

    // todo: add_new_at(position, label, layout)
    // -> shifting all one to the right and re-number them (vec.insert)

    // todo: remove(id)
    // -> shifting all right of the removed tag one to the left and re-number them

    /// Create a new hidden tag with the provided label,
    /// and append it to the list of hidden tags.
    /// The ID will be assigned automatically and returned.
    ///
    /// ## Non unique label
    /// May not create a new tag if a hidden tag with the same label already exists,
    /// this is indicated by the return value of `None`.
    pub fn add_new_hidden(&mut self, label: &str) -> Option<TagId> {
        if self.get_hidden_by_label(label).is_none() {
            // hidden tags are numbered descending from the highest possible number
            // to prevent conflicts with IDs of normal tags
            let next_id = usize::MAX - self.hidden.len();
            let tag = Tag {
                id: next_id,
                label: label.to_string(),
                hidden: true,
                ..Tag::default()
            };
            let id = tag.id;
            self.hidden.push(tag);
            Some(id)
        } else {
            tracing::error!(
                "tried creating a hidden tag with label {}, but a hidden tag with the same label already exists",
                label
            );
            None
        }
    }

    /// Get all normal tags
    pub const fn normal(&self) -> &Vec<Tag> {
        &self.normal
    }

    /// Get a list of all tags, including hidden ones.
    /// The hidden tags are at the end of the list.
    pub fn all(&self) -> Vec<&Tag> {
        //&self.normal.append(&self.hidden)
        let mut result: Vec<&Tag> = vec![];
        self.normal.iter().for_each(|tag| result.push(tag));
        self.hidden.iter().for_each(|tag| result.push(tag));
        result
    }

    /// Get a list of all tags as mutable, including hidden ones.
    /// The hidden tags are at the end of the list
    pub fn all_mut(&mut self) -> Vec<&mut Tag> {
        //&self.normal.append(&self.hidden)
        let mut result: Vec<&mut Tag> = vec![];
        self.normal.iter_mut().for_each(|tag| result.push(tag));
        self.hidden.iter_mut().for_each(|tag| result.push(tag));
        result
    }

    /// Get a tag by its ID.
    /// This method returns normal, as well as hidden tags.
    pub fn get(&self, id: TagId) -> Option<&Tag> {
        self.normal
            .get(id - 1) // tag id starts at 1, but arrays at 0 :)
            .or_else(|| self.hidden.iter().find(|&hidden_tag| hidden_tag.id == id))
    }

    /// Get a tag by its ID as mutable
    /// This method returns normal, as well as hidden tags.
    pub fn get_mut(&mut self, id: TagId) -> Option<&mut Tag> {
        if let Some(normal) = self.normal.get_mut(id - 1) {
            return Some(normal);
        }
        return self
            .hidden
            .iter_mut()
            .find(|hidden_tag| hidden_tag.id == id);
    }

    /// Get a hidden tag by its label
    pub fn get_hidden_by_label(&self, label: &str) -> Option<&Tag> {
        self.hidden.iter().find(|tag| tag.label.eq(label))
    }

    /// Get the amount of 'normal' tags
    pub fn len_normal(&self) -> usize {
        self.normal.len()
    }
}

impl Default for Tags {
    fn default() -> Self {
        Self::new()
    }
}

/// A Tag is similar to a "Desktop".
/// Each Screen/Workspace will always display a certain Tag.
/// A Tag can not be displayed on more than one Workspace at a time.
///
/// Unlike in some other WMs (eg. `dwm`), in `LeftWM`
/// the same set of tags and windows are shared among
/// all Workspaces, this means there aren't multiple instances of
/// the same Tag on different Screens.
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Tag {
    /// Unique identifier for the tag,
    /// this is automatically assigned by `LeftWM`.
    /// IDs start at 1 for the first tag, and are
    /// incremented by 1 for each subsequent tag.
    pub id: TagId,

    /// Label of the tag, only used for display purposes.
    ///
    /// Labels of normal tags may not be unique,
    /// but labels of hidden tags must be unique.
    ///
    /// ## Hint
    /// Unlike in earlier versions of LeftWM,
    /// the label of a Tag is not something that
    /// actually identifies a Tag. Tags are always
    /// identified and referenced by their ID (ie. `[1, 2, 3, ...]`).
    ///
    /// This means, if the user configures the tags `["home", "chat", "surf", "code"]`
    /// and later removes the `chat` tag, leaving `["home", "surf", "code"]`, this does not mean
    /// that Tag 2 is removed, it just means that Tag 2 has the label "surf" now.
    /// What is actually removed in that case is Tag 4.
    pub label: String,

    /// Indicates whether the tag can be
    /// displayed in a Workspace or not.
    /// Hidden tags are internal only, and
    /// are unknown to other programs (eg. polybar)
    pub hidden: bool,

    /// The layout in which the windows
    /// on this Tag are arranged
    pub layout: Layout,

    /// The percentage of available space
    /// which is designated for the "main"
    /// column of the layout, compared
    /// to the secondary column(s).
    pub main_width_percentage: u8,

    pub flipped_horizontal: bool,
    pub flipped_vertical: bool,
    pub layout_rotation: usize,
}

impl Tag {
    #[must_use]
    pub fn new(id: TagId, label: &str, layout: Layout) -> Self {
        Self {
            id,
            label: label.to_owned(),
            hidden: false,
            layout,
            main_width_percentage: layout.main_width(),
            flipped_horizontal: false,
            flipped_vertical: false,
            layout_rotation: 0,
        }
    }

    pub fn update_windows(&self, windows: &mut [Window], workspace: &Workspace) {
        if let Some(window) = windows
            .iter_mut()
            .find(|w| w.has_tag(&self.id) && w.is_fullscreen())
        {
            window.set_visible(true);
            window.normal = workspace.xyhw;
            let handle = window.handle;
            windows
                .iter_mut()
                .filter(|w| {
                    w.has_tag(&self.id)
                        && w.transient.unwrap_or_else(|| 0.into()) == handle
                        && w.is_managed()
                })
                .for_each(|w| {
                    w.set_visible(true);
                });
        } else {
            // Don't bother updating the other windows when a window is fullscreen.
            // Mark all windows for this workspace as visible.
            let mut all_mine: Vec<&mut Window> =
                windows.iter_mut().filter(|w| w.has_tag(&self.id)).collect();
            all_mine.iter_mut().for_each(|w| w.set_visible(true));
            // Update the location of all non-floating windows.
            let mut managed_nonfloat: Vec<&mut Window> = windows
                .iter_mut()
                .filter(|w| w.has_tag(&self.id) && w.is_managed() && !w.floating())
                .collect();
            self.layout
                .update_windows(workspace, &mut managed_nonfloat, self);
            for w in &mut managed_nonfloat {
                w.container_size = Some(workspace.xyhw);
            }
            // Update the location of all floating windows.
            windows
                .iter_mut()
                .filter(|w| w.has_tag(&self.id) && w.is_managed() && w.floating())
                .for_each(|w| w.normal = workspace.xyhw);
        }
    }

    /// Changes the main width percentage by the provided delta.
    /// Result is sanitized, so the percentage can't go below 0 or above 100.
    ///
    /// ## Arguments
    /// * `delta` - increase/decrease main width percentage by this amount
    pub fn change_main_width(&mut self, delta: i8) {
        // not smaller than 0 and not larger than 100
        self.main_width_percentage = (self.main_width_percentage as i8 + delta).clamp(0, 100) as u8;
    }

    /// Sets the main width percentage
    ///
    /// ## Arguments
    /// * `val` - the new with percentage
    pub fn set_main_width(&mut self, val: u8) {
        self.main_width_percentage = val.min(100); // not larger than 100
    }

    #[must_use]
    pub fn main_width_percentage(&self) -> f32 {
        f32::from(self.main_width_percentage)
    }

    pub fn set_layout(&mut self, layout: Layout, main_width_percentage: u8) {
        self.layout = layout;
        self.set_main_width(main_width_percentage);
        self.layout_rotation = 0;
    }

    pub fn rotate_layout(&mut self) -> Option<()> {
        let rotations = self.layout.rotations();
        self.layout_rotation += 1;
        if self.layout_rotation >= rotations.len() {
            self.layout_rotation = 0;
        }
        let (horz, vert) = rotations.get(self.layout_rotation)?;
        self.flipped_horizontal = *horz;
        self.flipped_vertical = *vert;
        Some(())
    }
}

#[cfg(test)]
mod tests {
    use super::Tags;
    use crate::layouts::Layout;

    #[test]
    fn normal_tags_are_numbered_in_order() {
        let mut tags = Tags::new();
        let home_id = tags.add_new("home", Layout::default());
        let chat_id = tags.add_new("chat", Layout::default());
        let surf_id = tags.add_new("surf", Layout::default());
        let code_id = tags.add_new("code", Layout::default());
        assert_eq!(home_id, 1);
        assert_eq!(chat_id, 2);
        assert_eq!(surf_id, 3);
        assert_eq!(code_id, 4);
    }

    #[test]
    fn hidden_tags_are_numbered_in_order() {
        let mut tags = Tags::new();
        let nsp_id = tags.add_new_hidden("NSP");
        let whatver_id = tags.add_new_hidden("whatever");
        assert_eq!(nsp_id, Some(usize::MAX));
        assert_eq!(whatver_id, Some(usize::MAX - 1));
    }

    #[test]
    fn multiple_normal_tags_can_have_same_label() {
        let mut tags = Tags::new();
        let first_id = tags.add_new("home", Layout::default());
        let second_id = tags.add_new("home", Layout::default());
        assert_eq!(first_id, 1);
        assert_eq!(second_id, 2);
    }

    #[test]
    fn unlabelled_tags_are_automatically_labelled_with_their_id() {
        let mut tags = Tags::new();
        let first_tag = tags.add_new_unlabeled(Layout::default());
        let second_tag = tags.add_new_unlabeled(Layout::default());
        let first_label = tags.get(first_tag).map(|tag| tag.label.clone());
        let second_label = tags.get(second_tag).map(|tag| tag.label.clone());
        assert_eq!(first_label, Some(String::from("1")));
        assert_eq!(second_label, Some(String::from("2")));
    }

    #[test]
    fn hidden_tags_must_have_unique_label() {
        let mut tags = Tags::new();
        let first_tag = tags.add_new_hidden("NSP");
        let second_tag = tags.add_new_hidden("NSP");
        let third_tag = tags.add_new_hidden("something-unique");
        assert_eq!(first_tag, Some(usize::MAX));
        assert_eq!(second_tag, None);
        assert_eq!(third_tag, Some(usize::MAX - 1));
        assert_eq!(tags.all().len(), 2); // the second tag must not be created
    }

    #[test]
    fn must_be_able_to_only_get_normal_tags() {
        let mut tags = Tags::new();
        tags.add_new("home", Layout::default());
        tags.add_new("chat", Layout::default());
        tags.add_new("surf", Layout::default());
        tags.add_new("code", Layout::default());
        tags.add_new_hidden("NSP");

        assert_eq!(tags.len_normal(), 4);
        assert_eq!(tags.normal().len(), 4);
    }

    #[test]
    fn must_be_able_to_get_all_tags() {
        let mut tags = Tags::new();
        tags.add_new("home", Layout::default());
        tags.add_new("chat", Layout::default());
        tags.add_new("surf", Layout::default());
        tags.add_new("code", Layout::default());
        tags.add_new_hidden("NSP");

        assert_eq!(tags.all().len(), 5);
    }

    #[test]
    fn hidden_tags_must_be_retrievable_by_label() {
        let mut tags = Tags::new();
        tags.add_new("home", Layout::default());
        tags.add_new_hidden("NSP");
        tags.add_new_hidden("whatever");

        let nsp_tag = tags.get_hidden_by_label("NSP");
        let whatever_tag = tags.get_hidden_by_label("whatever");
        let inexistent_tag = tags.get_hidden_by_label("inexistent");

        assert!(nsp_tag.is_some());
        assert!(whatever_tag.is_some());
        assert!(inexistent_tag.is_none());
    }

    #[test]
    fn only_hidden_tags_can_be_retrieved_by_label() {
        let mut tags = Tags::new();
        tags.add_new("home", Layout::default());
        let tag = tags.get_hidden_by_label("home");
        assert!(tag.is_none());
    }

    #[test]
    fn tags_can_be_mutable() {
        let mut tags = Tags::new();
        tags.add_new("home", Layout::default());
        tags.add_new("chat", Layout::default());
        tags.add_new("surf", Layout::default());

        let first_retrieve = tags.get_mut(2).unwrap();
        assert_eq!(first_retrieve.label, String::from("chat"));
        first_retrieve.label = String::from("code");

        let second_retrieve = tags.get_mut(2).unwrap();
        assert_eq!(second_retrieve.label, String::from("code"));
    }
}