libawm/core/manager/
clients.rs

1//! State and management of clients being managed by Penrose.
2use crate::{
3    core::{
4        client::Client,
5        data_types::Region,
6        hooks::HookName,
7        layout::LayoutConf,
8        manager::{event::EventAction, util::pad_region},
9        ring::Selector,
10        workspace::ArrangeActions,
11        xconnection::{
12            Atom, ClientMessageKind, Prop, XClientConfig, XClientHandler, XClientProperties,
13            XEventHandler, XState, Xid,
14        },
15    },
16    draw::Color,
17    Result,
18};
19use std::collections::HashMap;
20use tracing::{trace, warn};
21
22#[derive(Debug)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24pub(super) struct Clients {
25    inner: HashMap<Xid, Client>,
26    focused_client_id: Option<Xid>,
27    focused_border: Color,
28    unfocused_border: Color,
29}
30
31impl Clients {
32    pub fn new(focused_border: impl Into<Color>, unfocused_border: impl Into<Color>) -> Self {
33        Self {
34            inner: HashMap::new(),
35            focused_client_id: None,
36            focused_border: focused_border.into(),
37            unfocused_border: unfocused_border.into(),
38        }
39    }
40
41    pub fn is_known(&self, id: Xid) -> bool {
42        self.inner.contains_key(&id)
43    }
44
45    pub fn focused_client_id(&self) -> Option<Xid> {
46        self.focused_client_id
47    }
48
49    pub fn focused_client(&self) -> Option<&Client> {
50        self.focused_client_id.and_then(|id| self.inner.get(&id))
51    }
52
53    pub fn focused_client_mut(&mut self) -> Option<&mut Client> {
54        self.focused_client_id
55            .and_then(move |id| self.inner.get_mut(&id))
56    }
57
58    pub fn client(&self, selector: &Selector<'_, Client>) -> Option<&Client> {
59        match selector {
60            Selector::Focused | Selector::Any => self.focused_client(),
61            Selector::WinId(id) => self.inner.get(&id),
62            Selector::Condition(f) => self.inner.iter().find(|(_, v)| f(v)).map(|(_, v)| v),
63            Selector::Index(i) => self.inner.iter().nth(*i).map(|(_, c)| c),
64        }
65    }
66
67    pub fn client_mut(&mut self, selector: &Selector<'_, Client>) -> Option<&mut Client> {
68        match selector {
69            Selector::Focused | Selector::Any => self.focused_client_mut(),
70            Selector::WinId(id) => self.inner.get_mut(&id),
71            Selector::Condition(f) => self.inner.iter_mut().find(|(_, v)| f(v)).map(|(_, v)| v),
72            Selector::Index(i) => self.inner.iter_mut().nth(*i).map(|(_, c)| c),
73        }
74    }
75
76    pub fn matching_clients(&self, selector: &Selector<'_, Client>) -> Vec<&Client> {
77        let mut clients: Vec<&Client> = match selector {
78            Selector::Any => self.inner.values().collect(),
79            Selector::Focused => self.focused_client().into_iter().collect(),
80            Selector::WinId(id) => self.inner.get(&id).into_iter().collect(),
81            Selector::Condition(f) => self.inner.values().filter(|v| f(v)).collect(),
82            _ => self.client(selector).into_iter().collect(),
83        };
84
85        clients.sort_unstable_by_key(|c| c.id());
86        clients
87    }
88
89    pub fn matching_clients_mut(&mut self, selector: &Selector<'_, Client>) -> Vec<&mut Client> {
90        let mut clients: Vec<&mut Client> = match selector {
91            Selector::Any => self.inner.values_mut().collect(),
92            Selector::Focused => self.focused_client_mut().into_iter().collect(),
93            Selector::WinId(id) => self.inner.get_mut(&id).into_iter().collect(),
94            Selector::Condition(f) => self.inner.values_mut().filter(|v| f(v)).collect(),
95            _ => self.client_mut(selector).into_iter().collect(),
96        };
97
98        clients.sort_unstable_by_key(|c| c.id());
99        clients
100    }
101
102    pub fn set_focused<X>(&mut self, id: Xid, conn: &X) -> Option<Xid>
103    where
104        X: XClientConfig,
105    {
106        let prev = self.focused_client_id;
107        self.focused_client_id = Some(id);
108
109        if let Some(prev_id) = prev {
110            if id != prev_id {
111                self.client_lost_focus(prev_id, conn);
112            }
113        }
114
115        prev
116    }
117
118    #[allow(dead_code)]
119    pub fn clear_focused(&mut self) {
120        self.focused_client_id = None
121    }
122
123    pub fn insert(&mut self, id: Xid, c: Client) -> Option<Client> {
124        self.inner.insert(id, c)
125    }
126
127    pub fn remove(&mut self, id: Xid) -> Option<Client> {
128        if self.focused_client_id == Some(id) {
129            self.focused_client_id = None;
130        }
131
132        self.inner.remove(&id)
133    }
134
135    pub fn get(&self, id: Xid) -> Option<&Client> {
136        self.inner.get(&id)
137    }
138
139    pub fn get_mut(&mut self, id: Xid) -> Option<&mut Client> {
140        self.inner.get_mut(&id)
141    }
142
143    pub fn set_client_workspace(&mut self, id: Xid, wix: usize) {
144        self.inner.entry(id).and_modify(|c| c.set_workspace(wix));
145    }
146
147    pub fn map_if_needed<X>(&mut self, id: Xid, conn: &X) -> Result<()>
148    where
149        X: XClientHandler,
150    {
151        Ok(conn.map_client_if_needed(self.inner.get_mut(&id))?)
152    }
153
154    pub fn unmap_if_needed<X>(&mut self, id: Xid, conn: &X) -> Result<()>
155    where
156        X: XClientHandler,
157    {
158        Ok(conn.unmap_client_if_needed(self.inner.get_mut(&id))?)
159    }
160
161    // The index of the [Workspace] holding the requested X window ID. This can return None if
162    // the id does not map to a [WindowManager] managed [Client] which happens if the window
163    // is unmanaged (e.g. a dock or toolbar) or if a client [Hook] has requested ownership
164    // of that particular [Client].
165    pub fn workspace_index_for_client(&self, id: Xid) -> Option<usize> {
166        self.inner.get(&id).map(|c| c.workspace())
167    }
168
169    pub fn clients_for_workspace(&self, wix: usize) -> Vec<&Client> {
170        self.matching_clients(&Selector::Condition(&|c: &Client| c.workspace == wix))
171    }
172
173    pub fn clients_for_ids(&self, ids: &[Xid]) -> Vec<&Client> {
174        ids.iter().map(|i| &self.inner[i]).collect()
175    }
176
177    pub fn all_known_ids(&self) -> Vec<Xid> {
178        self.inner.keys().copied().collect()
179    }
180
181    pub fn modify(&mut self, id: Xid, f: impl Fn(&mut Client)) {
182        self.inner.entry(id).and_modify(f);
183    }
184
185    // Set X focus to the requested client if it accepts focus, otherwise send a
186    // 'take focus' event for the client to process
187    pub fn set_x_focus<X>(&self, id: Xid, accepts_focus: bool, conn: &X) -> Result<()>
188    where
189        X: XState + XEventHandler + XClientConfig + XClientHandler + XClientProperties,
190    {
191        trace!(id, accepts_focus, "setting focus");
192        if accepts_focus {
193            if let Err(e) = conn.focus_client(id) {
194                warn!("unable to focus client {}: {}", id, e);
195            }
196            conn.change_prop(
197                conn.root(),
198                Atom::NetActiveWindow.as_ref(),
199                Prop::Window(vec![id]),
200            )?;
201            let fb = self.focused_border;
202            if let Err(e) = conn.set_client_border_color(id, fb) {
203                warn!("unable to set client border color for {}: {}", id, e);
204            }
205        } else {
206            let msg = ClientMessageKind::TakeFocus(id).as_message(conn)?;
207            conn.send_client_event(msg)?;
208        }
209
210        // TODO: should this be running the FocusChange hook?
211        Ok(())
212    }
213
214    pub fn focus_in<X>(&self, id: Xid, conn: &X) -> Result<()>
215    where
216        X: XState + XEventHandler + XClientConfig + XClientHandler + XClientProperties,
217    {
218        let accepts_focus = match self.inner.get(&id) {
219            Some(client) => client.accepts_focus,
220            None => conn.client_accepts_focus(id),
221        };
222
223        self.set_x_focus(id, accepts_focus, conn)
224    }
225
226    // The given X window ID lost focus according to the X server
227    #[tracing::instrument(level = "trace", skip(self, conn))]
228    pub fn client_lost_focus<X>(&mut self, id: Xid, conn: &X)
229    where
230        X: XClientConfig,
231    {
232        if self.focused_client_id == Some(id) {
233            self.focused_client_id = None;
234        }
235
236        if self.inner.contains_key(&id) {
237            let ub = self.unfocused_border;
238            // The target window may have lost focus because it has just been closed and
239            // we have not yet updated our state.
240            conn.set_client_border_color(id, ub).unwrap_or(());
241        }
242    }
243
244    // The given window ID has had its EWMH name updated by something
245    pub fn client_name_changed<X>(
246        &mut self,
247        id: Xid,
248        is_root: bool,
249        conn: &X,
250    ) -> Result<EventAction>
251    where
252        X: XClientProperties,
253    {
254        let name = conn.client_name(id)?;
255        if !is_root {
256            if let Some(c) = self.inner.get_mut(&id) {
257                c.set_name(&name)
258            }
259        }
260
261        Ok(EventAction::RunHook(HookName::ClientNameUpdated(
262            id, name, is_root,
263        )))
264    }
265
266    pub fn apply_arrange_actions<X>(
267        &mut self,
268        actions: ArrangeActions,
269        lc: &LayoutConf,
270        border_px: u32,
271        gap_px: u32,
272        conn: &X,
273    ) -> Result<()>
274    where
275        X: XClientHandler + XClientConfig,
276    {
277        // Tile first then place floating clients on top
278        for (id, region) in actions.actions {
279            trace!(id, ?region, "positioning client");
280            if let Some(region) = region {
281                let reg = pad_region(&region, lc.gapless, gap_px, border_px);
282                conn.position_client(id, reg, border_px, false)?;
283                self.map_if_needed(id, conn)?;
284            } else {
285                self.unmap_if_needed(id, conn)?;
286            }
287        }
288
289        for id in actions.floating {
290            debug!(id, "mapping floating client above tiled");
291            conn.raise_client(id)?;
292        }
293
294        Ok(())
295    }
296
297    pub fn toggle_fullscreen<X>(
298        &mut self,
299        id: Xid,
300        wix: usize,
301        workspace_clients: &[Xid],
302        screen_size: Region,
303        conn: &X,
304    ) -> Result<Vec<EventAction>>
305    where
306        X: XClientHandler + XClientProperties + XClientConfig,
307    {
308        let client_currently_fullscreen = match self.get(id) {
309            Some(c) => c.fullscreen,
310            None => {
311                warn!(id, "attempt to make unknown client fullscreen");
312                return Ok(vec![]);
313            }
314        };
315
316        conn.toggle_client_fullscreen(id, client_currently_fullscreen)?;
317
318        for &i in workspace_clients.iter() {
319            if client_currently_fullscreen {
320                if i == id {
321                    self.inner.entry(id).and_modify(|c| c.fullscreen = false);
322                } else {
323                    self.map_if_needed(i, conn)?;
324                }
325            // client was not fullscreen
326            } else if i == id {
327                conn.position_client(id, screen_size, 0, false)?;
328                let is_known = self.is_known(id);
329                if is_known {
330                    self.map_if_needed(id, conn)?;
331                    self.modify(id, |c| c.fullscreen = true);
332                }
333            } else {
334                self.unmap_if_needed(i, conn)?;
335            }
336        }
337
338        Ok(if client_currently_fullscreen {
339            vec![EventAction::LayoutWorkspace(wix)]
340        } else {
341            vec![]
342        })
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::core::xconnection::{self, *};
350    use std::cell::Cell;
351
352    #[test]
353    fn client_lost_focus_on_focused_clears_focused_client_id() {
354        let conn = MockXConn::new(vec![], vec![], vec![]);
355        let mut clients = Clients::new(0xffffff, 0x000000);
356
357        clients.focused_client_id = Some(42);
358        clients.client_lost_focus(42, &conn);
359        assert!(clients.focused_client_id.is_none());
360    }
361
362    struct RecordingXConn {
363        positions: Cell<Vec<(Xid, Region)>>,
364        maps: Cell<Vec<Xid>>,
365        unmaps: Cell<Vec<Xid>>,
366    }
367
368    impl RecordingXConn {
369        fn init() -> Self {
370            Self {
371                positions: Cell::new(Vec::new()),
372                maps: Cell::new(Vec::new()),
373                unmaps: Cell::new(Vec::new()),
374            }
375        }
376    }
377
378    impl StubXClientProperties for RecordingXConn {}
379
380    impl StubXClientHandler for RecordingXConn {
381        fn mock_map_client(&self, id: Xid) -> xconnection::Result<()> {
382            let mut v = self.maps.take();
383            v.push(id);
384            self.maps.set(v);
385            Ok(())
386        }
387
388        fn mock_unmap_client(&self, id: Xid) -> xconnection::Result<()> {
389            let mut v = self.unmaps.take();
390            v.push(id);
391            self.unmaps.set(v);
392            Ok(())
393        }
394    }
395
396    impl StubXClientConfig for RecordingXConn {
397        fn mock_position_client(
398            &self,
399            id: Xid,
400            r: Region,
401            _: u32,
402            _: bool,
403        ) -> xconnection::Result<()> {
404            let mut v = self.positions.take();
405            v.push((id, r));
406            self.positions.set(v);
407            Ok(())
408        }
409    }
410
411    test_cases! {
412        toggle_fullscreen;
413        args: (
414            n_clients: u32,
415            fullscreen: Option<Xid>,
416            target: Xid,
417            unmapped: &[Xid],
418            should_apply_layout: bool,
419            expected_positions: Vec<Xid>,
420            expected_maps: Vec<Xid>,
421            expected_unmaps: Vec<Xid>,
422        );
423
424        case: single_client_on => (1, None, 0, &[], false, vec![0], vec![], vec![]);
425        case: single_client_off => (1, Some(0), 0, &[], true, vec![], vec![], vec![]);
426        case: multiple_clients_on => (4, None, 1, &[], false, vec![1], vec![], vec![0, 2, 3]);
427        case: multiple_clients_off => (4, Some(1), 1, &[0, 2, 3], true, vec![], vec![0, 2, 3], vec![]);
428
429        body: {
430            let conn = RecordingXConn::init();
431            let ids: Vec<Xid> = (0..n_clients).collect();
432
433            let mut clients = Clients {
434                inner: ids.iter()
435                .map(|&id| {
436                    let mut client = Client::new(&conn, id, 0, &[]);
437                    client.mapped = true;
438                    (id, client)
439                })
440                .collect(),
441                focused_client_id: None,
442                focused_border: 0xffffff.into(),
443                unfocused_border: 0x000000.into(),
444            };
445
446            let r = Region::new(0, 0, 1000, 800);
447            let expected_positions: Vec<_> = expected_positions.iter().map(|id| (*id, r)).collect();
448
449            for id in unmapped {
450                clients.modify(*id, |c| c.mapped = false);
451            }
452
453            if let Some(id) = fullscreen {
454                clients.modify(id, |c| c.fullscreen = true);
455            }
456
457            let events = clients.toggle_fullscreen(target, 42, &ids, r, &conn).unwrap();
458
459            assert_eq!(!events.is_empty(), should_apply_layout);
460            assert_eq!(conn.positions.take(), expected_positions);
461            assert_eq!(conn.maps.take(), expected_maps);
462            assert_eq!(conn.unmaps.take(), expected_unmaps);
463        }
464    }
465}