1use std::{cmp::Ordering, sync::Mutex};
17
18use crate::ipc;
19use niri_ipc::{Action, Request, Response, Window};
20use regex::Regex;
21use serde::{Deserialize, Serialize};
22
23#[derive(clap::Parser, PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
24pub enum NiriusCmd {
25 FocusOrSpawn {
26 #[clap(flatten)]
27 match_opts: MatchOptions,
28 command: Vec<String>,
29 },
30 Nop,
31}
32
33#[derive(clap::Parser, PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
34pub struct MatchOptions {
35 #[clap(short = 'a', long, help = "Matches window app-ids")]
36 app_id: Option<String>,
37
38 #[clap(short = 't', long, help = "Matches window titles")]
39 title: Option<String>,
40}
41
42static FOCUSED_WIN_IDS: Mutex<Vec<u64>> = Mutex::new(vec![]);
43
44pub fn exec_nirius_cmd(cmd: NiriusCmd) -> Result<String, String> {
45 let mut clear_focused_win_ids = true;
46
47 let result = match &cmd {
48 NiriusCmd::Nop => Ok("Nothing done".to_string()),
49 NiriusCmd::FocusOrSpawn {
50 match_opts,
51 command,
52 } => {
53 clear_focused_win_ids = false;
54 focus_or_spawn(match_opts, command)
55 }
56 };
57
58 if clear_focused_win_ids {
59 FOCUSED_WIN_IDS
60 .lock()
61 .expect("Could not lock mutex")
62 .clear()
63 }
64
65 result
66}
67
68fn focus_or_spawn(
69 match_opts: &MatchOptions,
70 command: &[String],
71) -> Result<String, String> {
72 match ipc::query_niri(Request::Windows)? {
73 Response::Windows(mut wins) => {
74 let mut ids = FOCUSED_WIN_IDS.lock().expect("Could not lock mutex");
75 wins.retain(|w| window_matches(w, match_opts));
76 if wins.iter().all(|w| ids.contains(&w.id)) {
77 ids.clear();
78 }
79 wins.sort_by(|a, b| {
80 if a.is_focused {
81 return Ordering::Greater;
82 }
83 if b.is_focused {
84 return Ordering::Less;
85 }
86
87 let a_visited = ids.contains(&a.id);
88 let b_visited = ids.contains(&b.id);
89
90 if a_visited && !b_visited {
91 return Ordering::Greater;
92 }
93 if !a_visited && b_visited {
94 return Ordering::Less;
95 }
96
97 a.id.cmp(&b.id)
98 });
99 log::debug!("ids: {:?}", ids);
100 if let Some(win) = wins.first() {
101 if !ids.contains(&win.id) {
102 ids.push(win.id);
103 }
104 focus_window(win.id)
105 } else {
106 let r = ipc::query_niri(Request::Action(Action::Spawn {
107 command: command.to_vec(),
108 }))?;
109 match r {
110 Response::Handled => Ok("Spawned successfully".to_string()),
111 x => Err(format!("Received unexpected reply {:?}", x)),
112 }
113 }
114 }
115 x => Err(format!("Received unexpected reply {:?}", x)),
116 }
117}
118
119fn focus_window(id: u64) -> Result<String, String> {
120 match ipc::query_niri(Request::Action(Action::FocusWindow { id }))? {
121 Response::Handled => Ok(format!("Focused window with id {}", id)),
122 x => Err(format!("Received unexpected reply {:?}", x)),
123 }
124}
125
126fn window_matches(w: &Window, match_opts: &MatchOptions) -> bool {
127 log::debug!("Matching window {:?}", w);
128 if w.app_id.is_none() && match_opts.app_id.is_some()
129 || match_opts.app_id.as_ref().is_some_and(|rx| {
130 !Regex::new(rx).unwrap().is_match(w.app_id.as_ref().unwrap())
131 })
132 {
133 log::debug!("app-id does not match.");
134 return false;
135 }
136
137 if w.title.is_none() && match_opts.title.is_some()
138 || match_opts.title.as_ref().is_some_and(|rx| {
139 !Regex::new(rx).unwrap().is_match(w.title.as_ref().unwrap())
140 })
141 {
142 log::debug!("title does not match.");
143 return false;
144 }
145
146 true
147}