hyprshell_core_lib/config/
binds.rs

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        // add index keys launcher run
227        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    // if poisoned lock
297    keyword_list.push((
298        "bind",
299        "ctrl, k, exec, pkill hyprshell; hyprctl dispatch submap reset".to_string(),
300    ));
301
302    // restart demon (like config reload or monitor change)
303    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    // second keybinding to close of mod + reverse mod is released
431    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    // if poisoned lock
509    keyword_list.push((
510        "bind",
511        "ctrl, k, exec, pkill hyprshell; hyprctl dispatch submap reset".to_string(),
512    ));
513
514    // restart demon (like config reload or monitor change)
515    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}