swayr/
layout.rs

1// Copyright (C) 2021-2023  Tassilo Horn <tsdh@gnu.org>
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU General Public License as published by the Free
5// Software Foundation, either version 3 of the License, or (at your option)
6// any later version.
7//
8// This program is distributed in the hope that it will be useful, but WITHOUT
9// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
11// more details.
12//
13// You should have received a copy of the GNU General Public License along with
14// this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Functions and data structures of the swayrd daemon.
17
18use crate::config;
19use crate::shared::ipc;
20use crate::shared::ipc::NodeMethods;
21use std::collections::HashMap;
22use swayipc as s;
23
24pub fn auto_tile(res_to_min_width: &HashMap<i32, i32>) {
25    if let Ok(mut con) = s::Connection::new() {
26        if let Ok(tree) = con.get_tree() {
27            for output in &tree.nodes {
28                log::debug!("output: {:?}", output.name);
29
30                // Assert our assumption that all children of the tree's root
31                // must be outputs.
32                if output.node_type != s::NodeType::Output {
33                    panic!(
34                        "Child of Root is no Output but a {:?}",
35                        output.node_type
36                    );
37                }
38
39                let output_width = output.rect.width;
40                let min_window_width = &res_to_min_width.get(&output_width);
41
42                if let Some(min_window_width) = min_window_width {
43                    for container in output.iter().filter(|n| {
44                        let t = n.get_type();
45                        t == ipc::Type::Workspace || t == ipc::Type::Container
46                    }) {
47                        if container.is_scratchpad() {
48                            log::debug!("  Skipping scratchpad");
49                            continue;
50                        }
51                        log::debug!(
52                            "  container: {:?}, layout {:?}, {} nodes",
53                            container.node_type,
54                            container.layout,
55                            container.nodes.len(),
56                        );
57                        for child_win in container
58                            .nodes
59                            .iter()
60                            .filter(|n| n.get_type() == ipc::Type::Window)
61                        {
62                            // Width if we'd split once more.
63                            let estimated_width =
64                                child_win.rect.width as f32 / 2.0;
65                            log::debug!(
66                                "    child_win: {:?}, estimated width after splith {} px",
67                                child_win.app_id, estimated_width
68                            );
69                            let split = if container.layout
70                                == s::NodeLayout::SplitH
71                                && estimated_width <= **min_window_width as f32
72                            {
73                                Some("splitv")
74                            } else if container.layout == s::NodeLayout::SplitV
75                                && estimated_width > **min_window_width as f32
76                            {
77                                Some("splith")
78                            } else {
79                                None
80                            };
81
82                            if let Some(split) = split {
83                                log::debug!(
84                                    "Auto-tiling performing {} on window {} \
85                                     because estimated width after another \
86                                     split is {} and the minimum window width \
87                                     is {} on this output.",
88                                    split,
89                                    child_win.id,
90                                    estimated_width,
91                                    min_window_width
92                                );
93                                match con.run_command(format!(
94                                    "[con_id={}] {}",
95                                    child_win.id, split
96                                )) {
97                                    Ok(_) => (),
98                                    Err(e) => log::error!(
99                                        "Couldn't set {} on con {}: {:?}",
100                                        split,
101                                        child_win.id,
102                                        e
103                                    ),
104                                }
105                            }
106                        }
107                    }
108                } else {
109                    log::error!("No layout.auto_tile_min_window_width_per_output_width \
110                               setting for output_width {}", output_width);
111                }
112            }
113        } else {
114            log::error!("Couldn't call get_tree during auto_tile.");
115        }
116    } else {
117        log::error!("Couldn't get connection for auto_tile");
118    }
119}
120
121pub fn maybe_auto_tile() {
122    config::with_config(|cfg| {
123        if cfg.is_layout_auto_tile() {
124            log::debug!("auto_tile: start");
125            auto_tile(
126                &cfg.get_layout_auto_tile_min_window_width_per_output_width_as_map(),
127            );
128            log::debug!("auto_tile: end");
129        }
130    })
131}
132
133const SWAYR_TMP_WORKSPACE: &str = "✨";
134
135pub fn relayout_current_workspace<F>(
136    include_floating: bool,
137    insert_win_fn: F,
138) -> Result<String, String>
139where
140    F: Fn(&mut [&s::Node], &mut s::Connection) -> s::Fallible<()>,
141{
142    let root = ipc::get_root_node(false);
143    let workspaces: Vec<&s::Node> = root
144        .iter()
145        .filter(|n| n.get_type() == ipc::Type::Workspace)
146        .collect();
147    match workspaces.iter().find(|ws| ws.is_current()) {
148        Some(cur_ws) => match s::Connection::new() {
149            Ok(mut con) => {
150                let mut moved_wins: Vec<&s::Node> = vec![];
151                let mut focused_win = None;
152                for win in
153                    cur_ws.iter().filter(|n| n.get_type() == ipc::Type::Window)
154                {
155                    if win.focused {
156                        focused_win = Some(win);
157                    }
158                    if !include_floating && win.is_floating() {
159                        continue;
160                    }
161                    moved_wins.push(win);
162                    con.run_command(format!(
163                        "[con_id={}] move to workspace {}",
164                        win.id, SWAYR_TMP_WORKSPACE
165                    ))
166                    .map_err(|err| err.to_string())?;
167                }
168
169                insert_win_fn(moved_wins.as_mut_slice(), &mut con)
170                    .map_err(|err| err.to_string())?;
171                std::thread::sleep(std::time::Duration::from_millis(25));
172
173                if let Some(win) = focused_win {
174                    con.run_command(format!("[con_id={}] focus", win.id))
175                        .map_err(|err| err.to_string())?;
176                }
177                Ok(format!(
178                    "Re-layouted current workspace {}.",
179                    cur_ws.get_name()
180                ))
181            }
182            Err(err) => Err(err.to_string()),
183        },
184        None => Err("No workspace is focused.".to_string()),
185    }
186}