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 cached_size: Option<u64>,
25 cached_depth: Option<usize>,
28}
29
30impl Candidate {
31 #[must_use]
32 pub const fn new(rel_path: PathBuf, abs_path: PathBuf) -> Self {
33 Self {
34 rel_path,
35 abs_path,
36 rel_str: OnceLock::new(),
37 cached_size: None,
38 cached_depth: None,
39 }
40 }
41
42 #[must_use]
43 pub const fn with_metadata(
44 rel_path: PathBuf,
45 abs_path: PathBuf,
46 size: Option<u64>,
47 depth: Option<usize>,
48 ) -> Self {
49 Self {
50 rel_path,
51 abs_path,
52 rel_str: OnceLock::new(),
53 cached_size: size,
54 cached_depth: depth,
55 }
56 }
57
58 #[must_use]
59 pub fn rel_path(&self) -> &std::path::Path {
60 &self.rel_path
61 }
62
63 #[must_use]
64 pub fn abs_path(&self) -> &std::path::Path {
65 &self.abs_path
66 }
67
68 #[must_use]
70 pub fn rel_str(&self) -> &str {
71 self.rel_str
72 .get_or_init(|| self.rel_path.to_string_lossy().replace('\\', "/"))
73 }
74
75 #[must_use]
76 pub fn display_path(&self, display: PathDisplay, path_separator: Option<u8>) -> String {
77 let raw = match display {
78 PathDisplay::Absolute => self.abs_path().display().to_string(),
79 PathDisplay::Relative => self.rel_path().display().to_string(),
80 };
81 if let Some(sep) = path_separator {
82 let mut buf = [0u8; 4];
83 let sep_str = (sep as char).encode_utf8(&mut buf);
84 raw.replace(std::path::MAIN_SEPARATOR, sep_str)
85 } else {
86 raw
87 }
88 }
89
90 #[must_use]
92 pub fn within_depth(&self, max_depth: Option<usize>) -> bool {
93 max_depth.is_none_or(|d| {
94 self.cached_depth
95 .unwrap_or_else(|| self.rel_path.components().count().saturating_sub(1))
96 <= d
97 })
98 }
99
100 #[must_use]
102 pub const fn cached_size(&self) -> Option<u64> {
103 self.cached_size
104 }
105
106 #[must_use]
108 pub fn within_filesize(&self, max_filesize: Option<u64>) -> bool {
109 max_filesize.is_none_or(|limit| {
110 self.cached_size.map_or_else(
111 || std::fs::metadata(&self.abs_path).map_or(true, |m| m.len() <= limit),
112 |size| size <= limit,
113 )
114 })
115 }
116
117 #[must_use]
119 pub fn total_file_bytes(candidates: &[Self]) -> u64 {
120 candidates.iter().fold(0u64, |acc, c| {
121 acc + c
122 .cached_size
123 .unwrap_or_else(|| std::fs::metadata(c.abs_path()).map_or(0, |m| m.len()))
124 })
125 }
126
127 #[must_use]
129 pub fn matches(&self, filter: &crate::search::filter::CandidateFilter) -> bool {
130 self.within_depth(filter.max_depth())
131 && self.within_filesize(filter.max_filesize())
132 && filter.matches_candidate(self)
133 }
134}
135
136impl PartialEq for Candidate {
137 fn eq(&self, other: &Self) -> bool {
138 self.rel_path == other.rel_path && self.abs_path == other.abs_path
139 }
140}
141
142impl Eq for Candidate {}