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