1use std::path::PathBuf;
2use std::sync::OnceLock;
3
4use crate::search::output::style::PathDisplay;
5
6#[derive(Debug, Clone)]
15pub struct Candidate {
16 rel_path: PathBuf,
18 abs_path: PathBuf,
20 rel_str: OnceLock<String>,
21}
22
23impl Candidate {
24 #[must_use]
25 pub const fn new(rel_path: PathBuf, abs_path: PathBuf) -> Self {
26 Self {
27 rel_path,
28 abs_path,
29 rel_str: OnceLock::new(),
30 }
31 }
32
33 #[must_use]
34 pub fn rel_path(&self) -> &std::path::Path {
35 &self.rel_path
36 }
37
38 #[must_use]
39 pub fn abs_path(&self) -> &std::path::Path {
40 &self.abs_path
41 }
42
43 #[must_use]
45 pub fn rel_str(&self) -> &str {
46 self.rel_str
47 .get_or_init(|| self.rel_path.to_string_lossy().replace('\\', "/"))
48 }
49
50 #[must_use]
51 pub fn display_path(&self, display: PathDisplay, path_separator: Option<u8>) -> String {
52 let raw = match display {
53 PathDisplay::Absolute => self.abs_path().display().to_string(),
54 PathDisplay::Relative => self.rel_path().display().to_string(),
55 };
56 if let Some(sep) = path_separator {
57 let sep_char = sep as char;
58 raw.replace(std::path::MAIN_SEPARATOR, &sep_char.to_string())
59 } else {
60 raw
61 }
62 }
63
64 #[must_use]
66 pub fn within_depth(&self, max_depth: Option<usize>) -> bool {
67 max_depth.is_none_or(|d| self.rel_path.components().count().saturating_sub(1) <= d)
68 }
69
70 #[must_use]
72 pub fn within_filesize(&self, max_filesize: Option<u64>) -> bool {
73 max_filesize.is_none_or(|limit| {
74 std::fs::metadata(&self.abs_path).map_or(true, |m| m.len() <= limit)
75 })
76 }
77
78 #[must_use]
80 pub fn matches(&self, filter: &crate::search::filter::CandidateFilter) -> bool {
81 self.within_depth(filter.max_depth())
82 && self.within_filesize(filter.max_filesize())
83 && filter.matches_candidate(self)
84 }
85}
86
87impl PartialEq for Candidate {
88 fn eq(&self, other: &Self) -> bool {
89 self.rel_path == other.rel_path && self.abs_path == other.abs_path
90 }
91}
92
93impl Eq for Candidate {}