1 use anyhow::{Context, Result};
2 use log::{debug, info, warn};
3 use std::collections::HashMap;
4 use std::iter::Iterator;
5 use std::time::Duration;
6 use structopt::clap::crate_name;
7 use xkbcommon::xkb;
8 
9 mod args;
10 mod utils;
11 
12 #[cfg(feature = "i3")]
13 mod wm_i3;
14 
15 #[cfg(feature = "i3")]
16 use crate::wm_i3 as wm;
17 
18 #[derive(Debug)]
19 pub struct DesktopWindow {
20     id: i64,
21     x_window_id: Option<i64>,
22     title: String,
23     pos: (i32, i32),
24     size: (i32, i32),
25 }
26 
27 #[derive(Debug)]
28 pub struct RenderWindow<'a> {
29     desktop_window: &'a DesktopWindow,
30     cairo_context: cairo::Context,
31     draw_pos: (f64, f64),
32     rect: (i32, i32, i32, i32),
33 }
34 
35 #[cfg(any(feature = "i3", feature = "add_some_other_wm_here"))]
main() -> Result<()>36 fn main() -> Result<()> {
37     pretty_env_logger::init();
38     let app_config = args::parse_args();
39 
40     // Get the windows from each specific window manager implementation.
41     let desktop_windows_raw = wm::get_windows().context("Couldn't get desktop windows")?;
42 
43     // Sort by position to make hint position more deterministic.
44     let desktop_windows = utils::sort_by_pos(desktop_windows_raw);
45 
46     let (conn, screen_num) = xcb::Connection::connect(None).context("No Xorg connection")?;
47     let setup = conn.get_setup();
48     let screen = setup
49         .roots()
50         .nth(screen_num as usize)
51         .context("Couldn't get screen")?;
52 
53     let values = [
54         (xcb::CW_BACK_PIXEL, screen.black_pixel()),
55         (
56             xcb::CW_EVENT_MASK,
57             xcb::EVENT_MASK_EXPOSURE
58                 | xcb::EVENT_MASK_KEY_PRESS
59                 | xcb::EVENT_MASK_BUTTON_PRESS
60                 | xcb::EVENT_MASK_BUTTON_RELEASE,
61         ),
62         (xcb::CW_OVERRIDE_REDIRECT, 1),
63     ];
64 
65     let mut render_windows = HashMap::new();
66     for desktop_window in &desktop_windows {
67         // We need to estimate the font size before rendering because we want the window to only be
68         // the size of the font.
69         let hint = utils::get_next_hint(
70             render_windows.keys().collect(),
71             &app_config.hint_chars,
72             desktop_windows.len(),
73         )
74         .context("Couldn't get next hint")?;
75 
76         // Figure out how large the window actually needs to be.
77         let text_extents = utils::extents_for_text(
78             &hint,
79             &app_config.font.font_family,
80             app_config.font.font_size,
81         )
82         .context("Couldn't create extents for text")?;
83         let (width, height, margin_width, margin_height) = if app_config.fill {
84             (
85                 desktop_window.size.0 as u16,
86                 desktop_window.size.1 as u16,
87                 (f64::from(desktop_window.size.0) - text_extents.width) / 2.0,
88                 (f64::from(desktop_window.size.1) - text_extents.height) / 2.0,
89             )
90         } else {
91             let margin_factor = 1.0 + 0.2;
92             (
93                 (text_extents.width * margin_factor).round() as u16,
94                 (text_extents.height * margin_factor).round() as u16,
95                 ((text_extents.width * margin_factor) - text_extents.width) / 2.0,
96                 ((text_extents.height * margin_factor) - text_extents.height) / 2.0,
97             )
98         };
99 
100         // Due to the way cairo lays out text, we'll have to calculate the actual coordinates to
101         // put the cursor. See:
102         // https://www.cairographics.org/samples/text_align_center/
103         // https://www.cairographics.org/samples/text_extents/
104         // https://www.cairographics.org/tutorial/#L1understandingtext
105         let draw_pos = (
106             margin_width - text_extents.x_bearing,
107             text_extents.height + margin_height - (text_extents.height + text_extents.y_bearing),
108         );
109 
110         debug!(
111             "Spawning RenderWindow for this DesktopWindow: {:?}",
112             desktop_window
113         );
114 
115         let x_offset = app_config.offset.x;
116         let mut x = match app_config.horizontal_align {
117             args::HorizontalAlign::Left => (desktop_window.pos.0 + x_offset) as i16,
118             args::HorizontalAlign::Center => {
119                 (desktop_window.pos.0 + desktop_window.size.0 / 2 - i32::from(width) / 2) as i16
120             }
121             args::HorizontalAlign::Right => {
122                 (desktop_window.pos.0 + desktop_window.size.0 - i32::from(width) - x_offset) as i16
123             }
124         };
125 
126         let y_offset = app_config.offset.y;
127         let y = match app_config.vertical_align {
128             args::VerticalAlign::Top => (desktop_window.pos.1 + y_offset) as i16,
129             args::VerticalAlign::Center => {
130                 (desktop_window.pos.1 + desktop_window.size.1 / 2 - i32::from(height) / 2) as i16
131             }
132             args::VerticalAlign::Bottom => {
133                 (desktop_window.pos.1 + desktop_window.size.1 - i32::from(height) - y_offset) as i16
134             }
135         };
136 
137         // If this is overlapping then we'll nudge the new RenderWindow a little bit out of the
138         // way.
139         let mut overlaps = utils::find_overlaps(
140             render_windows.values().collect(),
141             (x.into(), y.into(), width.into(), height.into()),
142         );
143         while !overlaps.is_empty() {
144             x += overlaps.pop().unwrap().2 as i16;
145             overlaps = utils::find_overlaps(
146                 render_windows.values().collect(),
147                 (x.into(), y.into(), width.into(), height.into()),
148             );
149         }
150 
151         let xcb_window_id = conn.generate_id();
152 
153         // Create the actual window.
154         xcb::create_window(
155             &conn,
156             xcb::COPY_FROM_PARENT as u8,
157             xcb_window_id,
158             screen.root(),
159             x,
160             y,
161             width,
162             height,
163             0,
164             xcb::WINDOW_CLASS_INPUT_OUTPUT as u16,
165             screen.root_visual(),
166             &values,
167         );
168 
169         xcb::map_window(&conn, xcb_window_id);
170 
171         // Set title.
172         let title = crate_name!();
173         xcb::change_property(
174             &conn,
175             xcb::PROP_MODE_REPLACE as u8,
176             xcb_window_id,
177             xcb::ATOM_WM_NAME,
178             xcb::ATOM_STRING,
179             8,
180             title.as_bytes(),
181         );
182 
183         // Set transparency.
184         let opacity_atom = xcb::intern_atom(&conn, false, "_NET_WM_WINDOW_OPACITY")
185             .get_reply()
186             .context("Couldn't create atom _NET_WM_WINDOW_OPACITY")?
187             .atom();
188         let opacity = (0xFFFFFFFFu64 as f64 * app_config.bg_color.3) as u64;
189         xcb::change_property(
190             &conn,
191             xcb::PROP_MODE_REPLACE as u8,
192             xcb_window_id,
193             opacity_atom,
194             xcb::ATOM_CARDINAL,
195             32,
196             &[opacity],
197         );
198 
199         conn.flush();
200 
201         let mut visual =
202             utils::find_visual(&conn, screen.root_visual()).context("Couldn't find visual")?;
203         let cairo_xcb_conn = unsafe {
204             cairo::XCBConnection::from_raw_none(
205                 conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t
206             )
207         };
208         let cairo_xcb_drawable = cairo::XCBDrawable(xcb_window_id);
209         let raw_visualtype = &mut visual.base as *mut xcb::ffi::xcb_visualtype_t;
210         let cairo_xcb_visual = unsafe {
211             cairo::XCBVisualType::from_raw_none(raw_visualtype as *mut cairo_sys::xcb_visualtype_t)
212         };
213         let surface = cairo::XCBSurface::create(
214             &cairo_xcb_conn,
215             &cairo_xcb_drawable,
216             &cairo_xcb_visual,
217             width.into(),
218             height.into(),
219         )
220         .context("Couldn't create Cairo Surface")?;
221         let cairo_context =
222             cairo::Context::new(&surface).context("Couldn't create Cairo Context")?;
223 
224         let render_window = RenderWindow {
225             desktop_window,
226             cairo_context,
227             draw_pos,
228             rect: (x.into(), y.into(), width.into(), height.into()),
229         };
230 
231         render_windows.insert(hint, render_window);
232     }
233 
234     // Receive keyboard events.
235     utils::snatch_keyboard(&conn, &screen, Duration::from_secs(1))?;
236 
237     // Receive mouse events.
238     utils::snatch_mouse(&conn, &screen, Duration::from_secs(1))?;
239 
240     // Since we might have lots of windows on the desktop, it might be required
241     // to enter a sequence in order to get to the correct window.
242     // We'll have to track the keys pressed so far.
243     let mut pressed_keys = String::default();
244     let mut sequence = utils::Sequence::new(None);
245 
246     let mut closed = false;
247     while !closed {
248         let event = conn.wait_for_event();
249         match event {
250             None => {
251                 closed = true;
252             }
253             Some(event) => {
254                 let r = event.response_type();
255                 match r {
256                     xcb::EXPOSE => {
257                         for (hint, rw) in &render_windows {
258                             utils::draw_hint_text(rw, &app_config, hint, &pressed_keys)
259                                 .context("Couldn't draw hint text")?;
260                             conn.flush();
261                         }
262                     }
263                     xcb::BUTTON_PRESS => {
264                         closed = true;
265                     }
266                     xcb::KEY_RELEASE => {
267                         let ksym = utils::get_pressed_symbol(&conn, &event);
268                         let kstr = utils::convert_to_string(ksym)
269                             .context("Couldn't convert ksym to string")?;
270                         sequence.remove(kstr);
271                     }
272                     xcb::KEY_PRESS => {
273                         let ksym = utils::get_pressed_symbol(&conn, &event);
274                         let kstr = utils::convert_to_string(ksym)
275                             .context("Couldn't convert ksym to string")?;
276 
277                         sequence.push(kstr.to_owned());
278 
279                         if app_config.hint_chars.contains(kstr) {
280                             info!("Adding '{}' to key sequence", kstr);
281                             pressed_keys.push_str(kstr);
282                         } else {
283                             warn!("Pressed key '{}' is not a valid hint characters", kstr);
284                         }
285 
286                         info!("Current key sequence: '{}'", pressed_keys);
287 
288                         if ksym == xkb::KEY_Escape || app_config.exit_keys.contains(&sequence) {
289                             info!("{:?} is exit sequence", sequence);
290                             closed = true;
291                             continue;
292                         }
293 
294                         // Attempt to match the current sequence of keys as a string to the window
295                         // hints shown.
296                         // If there is an exact match, we're done. We'll then focus the window
297                         // and exit. However, we also want to check whether there is still any
298                         // chance to focus any windows from the current key sequence. If there
299                         // is not then we will also just exit and focus no new window.
300                         // If there still is a chance we might find a window then we'll just
301                         // keep going for now.
302                         if sequence.is_started() {
303                             utils::remove_last_key(&mut pressed_keys, kstr);
304                         } else if let Some(rw) = &render_windows.get(&pressed_keys) {
305                             info!("Found matching window, focusing");
306                             if app_config.print_only {
307                                 println!("0x{:x}", rw.desktop_window.x_window_id.unwrap_or(0));
308                             } else {
309                                 wm::focus_window(rw.desktop_window)
310                                     .context("Couldn't focus window")?;
311                             }
312                             closed = true;
313                         } else if !pressed_keys.is_empty()
314                             && render_windows.keys().any(|k| k.starts_with(&pressed_keys))
315                         {
316                             for (hint, rw) in &render_windows {
317                                 utils::draw_hint_text(rw, &app_config, hint, &pressed_keys)
318                                     .context("Couldn't draw hint text")?;
319                                 conn.flush();
320                             }
321                             continue;
322                         } else {
323                             warn!("No more matches possible with current key sequence");
324                             closed = app_config.exit_keys.is_empty();
325                             utils::remove_last_key(&mut pressed_keys, kstr);
326                         }
327                     }
328                     _ => {}
329                 }
330             }
331         }
332     }
333 
334     Ok(())
335 }
336 
337 #[cfg(not(any(feature = "i3", feature = "add_some_other_wm_here")))]
main() -> Result<()>338 fn main() -> Result<()> {
339     eprintln!(
340         "You need to enable support for at least one window manager.\n
341 Currently supported:
342     --features i3"
343     );
344 
345     Ok(())
346 }
347