1use crate::config::structs::{Config, FilterBy, Overview, Reverse, Switch, ToKey};
2use crate::config::Launcher;
3use crate::transfer::{
4 Direction, OpenOverview, OpenSwitch, ReturnConfig, SwitchConfig, TransferType,
5};
6use crate::util::{get_daemon_socket_path_buff, LAUNCHER_NAMESPACE, OVERVIEW_NAMESPACE};
7use anyhow::Context;
8use ron::extensions::Extensions;
9use std::env;
10use std::path::PathBuf;
11use tracing::{span, Level};
12
13pub fn create_binds_and_submaps<'a>(config: &Config) -> anyhow::Result<Vec<(&'a str, String)>> {
14 let _span = span!(Level::DEBUG, "create_binds_and_submaps").entered();
15 let ron_options = ron::Options::default()
16 .with_default_extension(Extensions::IMPLICIT_SOME)
17 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
18 .with_default_extension(Extensions::EXPLICIT_STRUCT_NAMES);
19
20 let socat_path = get_socat_path()?;
21
22 let mut keyword_list = Vec::<(&str, String)>::new();
23
24 if config.layerrules {
25 keyword_list.push(("layerrule", format!("noanim, {LAUNCHER_NAMESPACE}")));
26 keyword_list.push(("layerrule", format!("noanim, {OVERVIEW_NAMESPACE}")));
27 keyword_list.push(("layerrule", format!("dimaround, {OVERVIEW_NAMESPACE}")));
28 }
29
30 if let Some(windows) = &config.windows {
31 if let Some(overview) = &windows.overview {
32 let workspaces_per_row = windows.workspaces_per_row;
33 let submap_name = "hyprshell-overview";
34 generate_overview(
35 &mut keyword_list,
36 &ron_options,
37 overview,
38 submap_name,
39 workspaces_per_row,
40 &config.launcher,
41 &socat_path,
42 )
43 .context("Failed to generate overview")?;
44 }
45 if let Some(switch) = &windows.switch {
46 let workspaces_per_row = windows.workspaces_per_row;
47 let submap_name = "hyprshell-switch";
48 generate_switch(
49 &mut keyword_list,
50 &ron_options,
51 switch,
52 submap_name,
53 workspaces_per_row,
54 &socat_path,
55 )
56 .context("Failed to generate overview")?;
57 }
58 }
59
60 Ok(keyword_list)
61}
62
63fn get_socat_path() -> anyhow::Result<String> {
64 env::var("HYPRSHELL_SOCAT_PATH")
65 .or_else(|_| which::which("socat").map(|path| path.to_string_lossy().to_string()))
66 .context("`socat` command not found. Please ensure it is installed and available in PATH.")
67}
68
69fn generate_socat(echo: &str, path: PathBuf, socat_path: &str) -> String {
70 format!(
71 r#"echo '{}' | {} - UNIX-CONNECT:{}"#,
72 echo,
73 socat_path,
74 path.as_path().to_string_lossy()
75 )
76}
77
78fn generate_close(ron_options: &ron::Options, socat_path: &str) -> anyhow::Result<String> {
79 let config = TransferType::Close;
80 let config_str = ron_options
81 .to_string(&config)
82 .context("Failed to serialize config")?;
83 Ok(generate_socat(
84 &config_str,
85 get_daemon_socket_path_buff(),
86 socat_path,
87 ))
88}
89
90fn generate_restart(ron_options: &ron::Options, socat_path: &str) -> anyhow::Result<String> {
91 let config = TransferType::Restart;
92 let config_str = ron_options
93 .to_string(&config)
94 .context("Failed to serialize config")?;
95 Ok(generate_socat(
96 &config_str,
97 get_daemon_socket_path_buff(),
98 socat_path,
99 ))
100}
101
102fn generate_return(
103 ron_options: &ron::Options,
104 offset: u8,
105 socat_path: &str,
106) -> anyhow::Result<String> {
107 let config = TransferType::Return(ReturnConfig { offset });
108 let config_str = ron_options
109 .to_string(&config)
110 .context("Failed to serialize config")?;
111 Ok(generate_socat(
112 &config_str,
113 get_daemon_socket_path_buff(),
114 socat_path,
115 ))
116}
117
118fn generate_switch_press(
119 ron_options: &ron::Options,
120 direction: Direction,
121 workspace: bool,
122 socat_path: &str,
123) -> anyhow::Result<String> {
124 let config = TransferType::Switch(SwitchConfig {
125 direction,
126 workspace,
127 });
128 let config_str = ron_options
129 .to_string(&config)
130 .context("Failed to serialize config")?;
131 Ok(generate_socat(
132 &config_str,
133 get_daemon_socket_path_buff(),
134 socat_path,
135 ))
136}
137
138fn generate_overview_open(
139 ron_options: &ron::Options,
140 submap_name: &str,
141 overview: &Overview,
142 workspaces_per_row: u8,
143 socat_path: &str,
144) -> anyhow::Result<String> {
145 let config = TransferType::OpenOverview(OpenOverview {
146 hide_filtered: overview.other.hide_filtered,
147 filter_current_workspace: overview
148 .other
149 .filter_by
150 .iter()
151 .any(|f| f == &FilterBy::CurrentWorkspace),
152 filter_current_monitor: overview
153 .other
154 .filter_by
155 .iter()
156 .any(|f| f == &FilterBy::CurrentMonitor),
157 filter_same_class: overview
158 .other
159 .filter_by
160 .iter()
161 .any(|f| f == &FilterBy::SameClass),
162 submap_name: submap_name.to_string(),
163 workspaces_per_row,
164 });
165 let config_str = ron_options
166 .to_string(&config)
167 .context("Failed to serialize config")?;
168 Ok(generate_socat(
169 &config_str,
170 get_daemon_socket_path_buff(),
171 socat_path,
172 ))
173}
174
175fn generate_overview(
176 keyword_list: &mut Vec<(&str, String)>,
177 ron_options: &ron::Options,
178 overview: &Overview,
179 submap_name: &str,
180 workspaces_per_row: u8,
181 launcher: &Option<Launcher>,
182 socat_path: &str,
183) -> anyhow::Result<()> {
184 keyword_list.push((
185 "bind",
186 format!(
187 "{}, {}, exec, {}",
188 overview.open.modifier,
189 overview.open.key.to_key(),
190 generate_overview_open(
191 ron_options,
192 submap_name,
193 overview,
194 workspaces_per_row,
195 socat_path
196 )?,
197 ),
198 ));
199
200 keyword_list.push(("submap", submap_name.to_string()));
201 keyword_list.push((
202 "bind",
203 format!(
204 ", escape, exec, {}",
205 generate_close(ron_options, socat_path)?
206 ),
207 ));
208 keyword_list.push((
209 "bind",
210 format!(
211 "{}, {}, exec, {}",
212 overview.open.modifier,
213 overview.open.key.to_key(),
214 generate_close(ron_options, socat_path)?
215 ),
216 ));
217 keyword_list.push((
218 "bind",
219 format!(
220 ", return, exec, {}",
221 generate_return(ron_options, 0, socat_path)?
222 ),
223 ));
224
225 if let Some(_launcher) = launcher {
226 for i in 1..=9 {
228 keyword_list.push((
229 "bind",
230 format!(
231 "ctrl, {}, exec, {}",
232 i,
233 generate_return(ron_options, i, socat_path)?
234 ),
235 ));
236 }
237 }
238
239 keyword_list.push((
240 "binde",
241 format!(
242 ", right, exec, {}",
243 generate_switch_press(ron_options, Direction::Right, true, socat_path)?
244 ),
245 ));
246 keyword_list.push((
247 "binde",
248 format!(
249 ", left, exec, {}",
250 generate_switch_press(ron_options, Direction::Left, true, socat_path)?
251 ),
252 ));
253 keyword_list.push((
254 "binde",
255 format!(
256 ", down, exec, {}",
257 generate_switch_press(ron_options, Direction::Down, true, socat_path)?
258 ),
259 ));
260 keyword_list.push((
261 "binde",
262 format!(
263 ", up, exec, {}",
264 generate_switch_press(ron_options, Direction::Up, true, socat_path)?
265 ),
266 ));
267
268 keyword_list.push((
269 "binde",
270 format!(
271 ", {}, exec, {}",
272 overview.navigate.forward,
273 generate_switch_press(ron_options, Direction::Right, false, socat_path)?
274 ),
275 ));
276 match &overview.navigate.reverse {
277 Reverse::Key(key) => keyword_list.push((
278 "binde",
279 format!(
280 ", {}, exec, {}",
281 key,
282 generate_switch_press(ron_options, Direction::Left, false, socat_path)?
283 ),
284 )),
285 Reverse::Mod(modk) => keyword_list.push((
286 "binde",
287 format!(
288 "{}, {}, exec, {}",
289 modk,
290 overview.navigate.forward,
291 generate_switch_press(ron_options, Direction::Left, false, socat_path)?
292 ),
293 )),
294 }
295
296 keyword_list.push((
298 "bind",
299 "ctrl, k, exec, pkill hyprshell; hyprctl dispatch submap reset".to_string(),
300 ));
301
302 keyword_list.push((
304 "bind",
305 format!(
306 "ctrl, r, exec, {}",
307 generate_restart(ron_options, socat_path)?
308 ),
309 ));
310 keyword_list.push(("submap", "reset".to_string()));
311 Ok(())
312}
313
314fn generate_switch_open(
315 ron_options: &ron::Options,
316 submap_name: &str,
317 switch: &Switch,
318 workspaces_per_row: u8,
319 direction: Direction,
320 socat_path: &str,
321) -> anyhow::Result<String> {
322 let config = TransferType::OpenSwitch(OpenSwitch {
323 hide_filtered: switch.other.hide_filtered,
324 filter_current_workspace: switch
325 .other
326 .filter_by
327 .iter()
328 .any(|f| f == &FilterBy::CurrentWorkspace),
329 filter_current_monitor: switch
330 .other
331 .filter_by
332 .iter()
333 .any(|f| f == &FilterBy::CurrentMonitor),
334 filter_same_class: switch
335 .other
336 .filter_by
337 .iter()
338 .any(|f| f == &FilterBy::SameClass),
339 submap_name: submap_name.to_string(),
340 workspaces_per_row,
341 direction,
342 });
343 let config_str = ron_options
344 .to_string(&config)
345 .context("Failed to serialize config")?;
346 Ok(generate_socat(
347 &config_str,
348 get_daemon_socket_path_buff(),
349 socat_path,
350 ))
351}
352
353fn generate_switch(
354 keyword_list: &mut Vec<(&str, String)>,
355 ron_options: &ron::Options,
356 switch: &Switch,
357 submap_name: &str,
358 workspaces_per_row: u8,
359 socat_path: &str,
360) -> anyhow::Result<()> {
361 keyword_list.push((
362 "bind",
363 format!(
364 "{}, {}, exec, {}",
365 switch.open.modifier,
366 switch.navigate.forward,
367 generate_switch_open(
368 ron_options,
369 submap_name,
370 switch,
371 workspaces_per_row,
372 Direction::Right,
373 socat_path
374 )?,
375 ),
376 ));
377 match &switch.navigate.reverse {
378 Reverse::Key(key) => keyword_list.push((
379 "bind",
380 format!(
381 "{}, {}, exec, {}",
382 switch.open.modifier,
383 key,
384 generate_switch_open(
385 ron_options,
386 submap_name,
387 switch,
388 workspaces_per_row,
389 Direction::Left,
390 socat_path
391 )?,
392 ),
393 )),
394 Reverse::Mod(modk) => keyword_list.push((
395 "bind",
396 format!(
397 "{} {}, {}, exec, {}",
398 switch.open.modifier,
399 modk,
400 switch.navigate.forward,
401 generate_switch_open(
402 ron_options,
403 submap_name,
404 switch,
405 workspaces_per_row,
406 Direction::Left,
407 socat_path
408 )?,
409 ),
410 )),
411 }
412
413 keyword_list.push(("submap", submap_name.to_string()));
414 keyword_list.push((
415 "bind",
416 format!(
417 ", escape, exec, {}",
418 generate_close(ron_options, socat_path)?
419 ),
420 ));
421 keyword_list.push((
422 "bindrt",
423 format!(
424 "{}, {}, exec, {}",
425 switch.open.modifier,
426 switch.open.modifier.to_key(),
427 generate_return(ron_options, 0, socat_path)?
428 ),
429 ));
430 if let Reverse::Mod(modk) = &switch.navigate.reverse {
432 keyword_list.push((
433 "bindrt",
434 format!(
435 "{} {}, {}, exec, {}",
436 switch.open.modifier,
437 modk,
438 switch.open.modifier.to_key(),
439 generate_return(ron_options, 0, socat_path)?,
440 ),
441 ));
442 }
443
444 keyword_list.push((
445 "bind",
446 format!(
447 "{}, right, exec, {}",
448 switch.open.modifier,
449 generate_switch_press(ron_options, Direction::Right, false, socat_path)?
450 ),
451 ));
452 keyword_list.push((
453 "binde",
454 format!(
455 "{}, left, exec, {}",
456 switch.open.modifier,
457 generate_switch_press(ron_options, Direction::Left, false, socat_path)?
458 ),
459 ));
460 keyword_list.push((
461 "binde",
462 format!(
463 "{}, down, exec, {}",
464 switch.open.modifier,
465 generate_switch_press(ron_options, Direction::Down, false, socat_path)?
466 ),
467 ));
468 keyword_list.push((
469 "binde",
470 format!(
471 "{}, up, exec, {}",
472 switch.open.modifier,
473 generate_switch_press(ron_options, Direction::Up, false, socat_path)?
474 ),
475 ));
476
477 keyword_list.push((
478 "binde",
479 format!(
480 "{}, {}, exec, {}",
481 switch.open.modifier,
482 switch.navigate.forward,
483 generate_switch_press(ron_options, Direction::Right, false, socat_path)?
484 ),
485 ));
486 match &switch.navigate.reverse {
487 Reverse::Key(key) => keyword_list.push((
488 "binde",
489 format!(
490 "{}, {}, exec, {}",
491 switch.open.modifier,
492 key,
493 generate_switch_press(ron_options, Direction::Left, false, socat_path)?
494 ),
495 )),
496 Reverse::Mod(modk) => keyword_list.push((
497 "binde",
498 format!(
499 "{} {}, {}, exec, {}",
500 switch.open.modifier,
501 modk,
502 switch.navigate.forward,
503 generate_switch_press(ron_options, Direction::Left, false, socat_path)?
504 ),
505 )),
506 }
507
508 keyword_list.push((
510 "bind",
511 "ctrl, k, exec, pkill hyprshell; hyprctl dispatch submap reset".to_string(),
512 ));
513
514 keyword_list.push((
516 "bind",
517 format!(
518 "ctrl, r, exec, {}",
519 generate_restart(ron_options, socat_path)?
520 ),
521 ));
522 keyword_list.push(("submap", "reset".to_string()));
523 Ok(())
524}