1#[cfg(feature = "persist_filters_xml")]
5mod xml_format {
6 use serde::{Deserialize, Serialize};
12 use std::borrow::Cow;
13
14 #[derive(Serialize, Deserialize)]
15 pub struct SaveFile {
16 #[serde(rename = "@xmlns:xsi", default)]
17 pub xmlns_xsi: Cow<'static, str>,
18 #[serde(rename = "@xmlns:xsd", default)]
19 pub xmlns_xsd: Cow<'static, str>,
20 pub filters: Filters,
21 }
22 impl Default for SaveFile {
23 fn default() -> Self {
24 Self {
25 xmlns_xsi: Cow::Borrowed("http://www.w3.org/2001/XMLSchema-instance"),
26 xmlns_xsd: Cow::Borrowed("http://www.w3.org/2001/XMLSchema"),
27 filters: Filters::default(),
28 }
29 }
30 }
31
32 #[derive(Default, Serialize, Deserialize)]
33 pub struct Filters {
34 #[serde(rename = "SaveData", default)]
35 pub save_data: Vec<SaveData>,
36 }
37
38 #[derive(Serialize, Deserialize)]
39 pub struct SaveData {
40 pub data: Data,
41 }
42
43 fn minus_one() -> i64 {
44 -1
45 }
46 fn deserialize_missing<'de, D>(de: D) -> Result<i64, D::Error>
52 where
53 D: serde::Deserializer<'de>,
54 {
55 let text = <&str>::deserialize(de)?;
56 if text.is_empty() {
57 Ok(-1)
58 } else {
59 text.parse::<i64>().map_err(serde::de::Error::custom)
60 }
61 }
62
63 #[derive(Serialize, Deserialize)]
64 pub struct Data {
65 #[serde(
67 rename = "indexLowerBound",
68 default = "minus_one",
69 deserialize_with = "deserialize_missing"
70 )]
71 pub index_lower_bound: i64,
72 #[serde(
74 rename = "indexUpperBound",
75 default = "minus_one",
76 deserialize_with = "deserialize_missing"
77 )]
78 pub index_upper_bound: i64,
79 #[serde(
81 rename = "desktopLowerBound",
82 default = "minus_one",
83 deserialize_with = "deserialize_missing"
84 )]
85 pub desktop_lower_bound: i64,
86 #[serde(
88 rename = "desktopUpperBound",
89 default = "minus_one",
90 deserialize_with = "deserialize_missing"
91 )]
92 pub desktop_upper_bound: i64,
93 #[serde(default)]
94 pub title: Title,
95 #[serde(default)]
96 pub process: Process,
97 #[serde(rename = "isMainProcessWindow")]
98 pub is_main_process_window: bool,
99 #[serde(rename = "checkIfMainWindow", default)]
100 pub check_if_main_window: bool,
101 #[serde(
103 rename = "desktopTarget",
104 default = "minus_one",
105 deserialize_with = "deserialize_missing"
106 )]
107 pub desktop_target: i64,
108 #[serde(rename = "desktopTargetAdv", default)]
109 pub desktop_target_adv: Option<DesktopTargetAdv>,
110 }
111
112 #[derive(Default, Serialize, Deserialize)]
113 pub struct Title {
114 #[serde(default)]
115 pub string: Vec<String>,
116 }
117
118 #[derive(Default, Serialize, Deserialize)]
119 pub struct Process {
120 #[serde(default)]
121 pub string: Vec<String>,
122 }
123
124 #[derive(Serialize, Deserialize)]
125 pub struct DesktopTargetAdv {
126 #[serde(rename = "allowUnpin", default)]
127 pub allow_unpin: bool,
128 #[serde(rename = "shouldPin", default)]
129 pub should_pin: bool,
130 #[serde(
132 rename = "targetDesktopIndex",
133 default = "minus_one",
134 deserialize_with = "deserialize_missing"
135 )]
136 pub target_desktop_index: i64,
137 }
138}
139
140#[cfg(feature = "persist_filters")]
141use serde::{Deserialize, Serialize};
142
143use std::{fmt, sync::Arc};
144
145use crate::window_info::{VirtualDesktopInfo, WindowInfo};
146
147#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
148#[cfg_attr(feature = "persist_filters", derive(Serialize, Deserialize))]
149pub struct IntegerRange {
150 pub lower_bound: Option<i64>,
151 pub upper_bound: Option<i64>,
152}
153impl IntegerRange {
154 pub fn contains(&self, value: i64) -> bool {
155 match (self.lower_bound, self.upper_bound) {
156 (None, None) => true,
157 (None, Some(upper)) => value <= upper,
158 (Some(lower), None) => lower <= value,
159 (Some(lower), Some(upper)) => lower <= value && value <= upper,
160 }
161 }
162 pub fn one_based_indexes(self) -> Self {
165 Self {
166 lower_bound: self.lower_bound.map(|v| v.saturating_add(1)),
167 upper_bound: self.upper_bound.map(|v| v.saturating_add(1)),
168 }
169 }
170}
171impl fmt::Display for IntegerRange {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 match (self.lower_bound, self.upper_bound) {
175 (None, None) => Ok(()),
176 (None, Some(upper)) => write!(f, "- {upper}"),
177 (Some(lower), None) => write!(f, "{lower} -"),
178 (Some(lower), Some(upper)) => write!(f, "{lower} - {upper}"),
179 }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
185#[cfg_attr(feature = "persist_filters", derive(Serialize, Deserialize))]
186#[cfg_attr(feature = "persist_filters", serde(transparent))]
187pub struct TextPattern {
188 pattern: Arc<str>,
189}
190impl TextPattern {
191 pub fn new(pattern: Arc<str>) -> Self {
192 Self { pattern }
193 }
194 pub fn check_newline_glob(&self, mut text: &str) -> bool {
197 if &*self.pattern == "\n" || &*self.pattern == "\r\n" {
198 return true;
200 }
201 let mut patterns = self.split_newline_glob();
202 {
204 let first = patterns
205 .next()
206 .expect("even an empty pattern string should have at least one part");
207 if first.is_empty() {
208 } else if let Some(after) = text.strip_prefix(first) {
210 text = after;
211 } else {
212 return false;
213 };
214 }
215 if let Some(last) = patterns.next_back() {
216 if last.is_empty() {
217 } else if let Some(before) = text.strip_suffix(last) {
219 text = before;
220 } else {
221 return false;
222 };
223 } else {
224 return text.is_empty();
226 }
227 for pattern in patterns {
230 if let Some((_, after)) = text.split_once(pattern) {
231 text = after;
232 } else {
233 return false;
236 }
237 }
238 true
240 }
241 pub fn split_newline_glob(&self) -> impl DoubleEndedIterator<Item = &'_ str> {
242 self.pattern
243 .split('\n')
244 .map(|line| line.trim_end_matches('\r'))
245 }
246 pub fn display_escaped_newline_glob(&self) -> impl fmt::Display + '_ {
248 struct Helper<'a>(&'a TextPattern);
249 impl fmt::Display for Helper<'_> {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 for (ix, part) in self.0.split_newline_glob().enumerate() {
252 if ix != 0 {
253 write!(f, "*")?;
254 }
255 write!(f, "\"{}\"", part.replace('\\', "\\\\").replace('"', "\\\""))?;
256 }
257 Ok(())
258 }
259 }
260 Helper(self)
261 }
262 pub fn pattern(&self) -> &Arc<str> {
263 &self.pattern
264 }
265}
266impl Default for TextPattern {
267 fn default() -> Self {
268 Self::new(Arc::from("\n"))
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
273#[cfg_attr(feature = "persist_filters", derive(Serialize, Deserialize))]
274pub struct ExportedWindowFilters {
275 pub version: u64,
276 pub filters: Vec<WindowFilter>,
277}
278impl ExportedWindowFilters {
279 pub fn migrate_and_get_filters(self) -> Vec<WindowFilter> {
280 self.filters
281 }
282}
283impl Default for ExportedWindowFilters {
284 fn default() -> Self {
285 Self {
286 version: 1,
287 filters: Vec::new(),
288 }
289 }
290}
291
292#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
295#[cfg_attr(feature = "persist_filters", derive(Serialize, Deserialize))]
296pub struct WindowFilter {
297 pub window_index: IntegerRange,
302 pub desktop_index: IntegerRange,
307 pub window_title: TextPattern,
309 pub process_name: TextPattern,
311 pub action: FilterAction,
314 pub target_desktop: i64,
319}
320impl WindowFilter {
321 #[cfg(feature = "persist_filters_xml")]
322 pub fn deserialize_from_xml(xml: &str) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
323 let mut deserializer = quick_xml::de::Deserializer::from_str(xml);
324 #[cfg(not(feature = "serde_path_to_error"))]
325 let data: xml_format::SaveFile = Deserialize::deserialize(&mut deserializer)?;
326 #[cfg(feature = "serde_path_to_error")]
327 let data: xml_format::SaveFile = serde_path_to_error::deserialize(&mut deserializer)?;
328
329 Ok(data
330 .filters
331 .save_data
332 .into_iter()
333 .map(|filter| Self {
334 window_index: IntegerRange {
335 lower_bound: Some(filter.data.index_lower_bound).filter(|&v| v >= 0),
336 upper_bound: Some(filter.data.index_upper_bound).filter(|&v| v >= 0),
337 },
338 desktop_index: IntegerRange {
339 lower_bound: Some(filter.data.desktop_lower_bound).filter(|&v| v >= 0),
340 upper_bound: Some(filter.data.desktop_upper_bound).filter(|&v| v >= 0),
341 },
342 window_title: if filter.data.title.string.is_empty()
343 || filter.data.title.string == [""]
344 {
345 TextPattern::default()
346 } else {
347 TextPattern::new(Arc::from(filter.data.title.string.join("\n")))
348 },
349 process_name: if filter.data.process.string.is_empty() {
350 TextPattern::default()
351 } else {
352 TextPattern::new(Arc::from(filter.data.process.string.join("\n")))
353 },
354 action: if let Some(adv) = &filter.data.desktop_target_adv {
355 if adv.should_pin {
356 FilterAction::Pin
357 } else if adv.target_desktop_index >= 0 {
358 if adv.allow_unpin {
359 FilterAction::UnpinAndMove
360 } else {
361 FilterAction::Move
362 }
363 } else if adv.allow_unpin {
364 FilterAction::Unpin
365 } else {
366 FilterAction::Disabled
367 }
368 } else if filter.data.desktop_target >= 0 {
369 FilterAction::Move
370 } else {
371 FilterAction::Disabled
372 },
373 target_desktop: filter
374 .data
375 .desktop_target_adv
376 .as_ref()
377 .map(|adv| adv.target_desktop_index)
378 .unwrap_or_else(|| filter.data.desktop_target)
379 .max(0),
380 })
381 .collect())
382 }
383 #[cfg(feature = "persist_filters_xml")]
384 pub fn serialize_to_xml(
385 filters: &[WindowFilter],
386 ) -> Result<String, Box<dyn std::error::Error>> {
387 let filters = filters
388 .iter()
389 .map(|filter| xml_format::SaveData {
390 data: xml_format::Data {
391 index_lower_bound: filter.window_index.lower_bound.unwrap_or(-1),
392 index_upper_bound: filter.window_index.upper_bound.unwrap_or(-1),
393 desktop_lower_bound: filter.desktop_index.lower_bound.unwrap_or(-1),
394 desktop_upper_bound: filter.desktop_index.upper_bound.unwrap_or(-1),
395 title: xml_format::Title {
396 string: filter
397 .window_title
398 .pattern()
399 .replace('\r', "")
400 .split('\n')
401 .map(String::from)
402 .collect(),
403 },
404 process: xml_format::Process {
405 string: filter
406 .process_name
407 .pattern()
408 .replace('\r', "")
409 .split('\n')
410 .map(String::from)
411 .collect(),
412 },
413 is_main_process_window: false,
414 check_if_main_window: false,
415 desktop_target: if matches!(
416 filter.action,
417 FilterAction::Move | FilterAction::UnpinAndMove
418 ) {
419 filter.target_desktop
420 } else {
421 -1
422 },
423 desktop_target_adv: Some(xml_format::DesktopTargetAdv {
424 allow_unpin: matches!(
425 filter.action,
426 FilterAction::Unpin | FilterAction::UnpinAndMove
427 ),
428 should_pin: matches!(filter.action, FilterAction::Pin),
429 target_desktop_index: if matches!(
430 filter.action,
431 FilterAction::Move | FilterAction::UnpinAndMove | FilterAction::Pin
432 ) {
433 filter.target_desktop
434 } else {
435 -1
436 },
437 }),
438 },
439 })
440 .collect::<Vec<_>>();
441 let xml_data = xml_format::SaveFile {
442 filters: xml_format::Filters { save_data: filters },
443 ..Default::default()
444 };
445 quick_xml::se::to_string(&xml_data).map_err(Into::into)
446 }
447 pub fn find_first_action<'a>(
448 filters: &'a [Self],
449 window_index: i32,
450 window: &WindowInfo,
451 ) -> Option<&'a Self> {
452 filters
453 .iter()
454 .filter(|filter| filter.action != FilterAction::Disabled)
456 .find(|filter| filter.check_window(window_index, window))
458 }
459 pub fn check_window(&self, window_index: i32, window: &WindowInfo) -> bool {
463 if !self.window_index.contains(i64::from(window_index)) {
464 return false;
465 }
466 if let VirtualDesktopInfo::AtDesktop { index, .. } = window.virtual_desktop {
467 if !self.desktop_index.contains(i64::from(index)) {
468 return false;
469 }
470 }
471 if !self.window_title.check_newline_glob(&window.title) {
472 return false;
473 }
474 if !self.process_name.check_newline_glob(&window.process_name) {
475 return false;
476 }
477 true
478 }
479 pub fn display_target_desktop(&self) -> impl fmt::Display {
482 struct TargetDesktopFmt {
483 action: FilterAction,
484 target_desktop: i64,
485 }
486 impl fmt::Display for TargetDesktopFmt {
487 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488 match self.action {
489 FilterAction::Move => write!(f, "{}", self.target_desktop),
490 FilterAction::UnpinAndMove => write!(f, "{} (Unpin)", self.target_desktop),
491 FilterAction::Unpin => write!(f, "Unpin"),
492 FilterAction::Pin => write!(f, "Pin"),
493 FilterAction::Nothing => write!(f, "None"),
494 FilterAction::Disabled => write!(f, "Disabled"),
495 }
496 }
497 }
498 TargetDesktopFmt {
499 action: self.action,
500 target_desktop: self.target_desktop.saturating_add(1),
501 }
502 }
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
508#[cfg_attr(feature = "persist_filters", derive(Serialize, Deserialize))]
509pub enum FilterAction {
510 Move,
513 UnpinAndMove,
516 Unpin,
518 Pin,
520 Nothing,
523 #[default]
525 Disabled,
526}
527impl FilterAction {
528 pub fn as_str(&self) -> &'static str {
529 match self {
530 FilterAction::Move => "Move",
531 FilterAction::UnpinAndMove => "Unpin and move",
532 FilterAction::Unpin => "Unpin",
533 FilterAction::Pin => "Pin",
534 FilterAction::Nothing => "Nothing",
535 FilterAction::Disabled => "Disabled",
536 }
537 }
538}
539impl fmt::Display for FilterAction {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 f.write_str(self.as_str())
543 }
544}