1#![warn(missing_docs)]
2#![warn(unused_extern_crates)]
3#![warn(unused_qualifications)]
4
5use serde::{Deserialize, Serialize};
8use std::{collections::HashSet, fmt, io, path::Path};
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Tag {
14 Gray,
16 Green,
18 Purple,
20 Blue,
22 Yellow,
24 Red,
26 Orange,
28 Custom(String),
30}
31
32#[derive(Debug, Error)]
33pub enum TagError {
35 #[error("xattr operation failed")]
37 XAttr(#[from] io::Error),
38 #[error("plist operation failed")]
40 Plist(#[from] plist::Error),
41 #[error("tag metadata for `{0}` is invalid")]
43 Invalid(String),
44 #[error("unknown error")]
46 Unknown,
47}
48
49impl fmt::Display for Tag {
50 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51 match self {
52 Self::Gray => write!(f, "Gray\n1"),
53 Self::Green => write!(f, "Green\n2"),
54 Self::Purple => write!(f, "Purple\n3"),
55 Self::Blue => write!(f, "Blue\n4"),
56 Self::Yellow => write!(f, "Yellow\n5"),
57 Self::Red => write!(f, "Red\n6"),
58 Self::Orange => write!(f, "Orange\n7"),
59 Self::Custom(val) => write!(f, "{}", val),
60 }
61 }
62}
63
64impl Tag {
65 pub fn from_string(s: &str) -> Result<Self, TagError> {
67 if s.contains('\n') {
68 let tag = s
69 .split_once('\n')
70 .ok_or_else(|| TagError::Invalid(s.to_owned()))?;
71
72 match tag {
73 ("Gray", "1") => return Ok(Tag::Gray),
74 ("Green", "2") => return Ok(Tag::Green),
75 ("Purple", "3") => return Ok(Tag::Purple),
76 ("Blue", "4") => return Ok(Tag::Blue),
77 ("Yellow", "5") => return Ok(Tag::Yellow),
78 ("Red", "6") => return Ok(Tag::Red),
79 ("Orange", "7") => return Ok(Tag::Orange),
80 _ => return Err(TagError::Invalid(s.to_owned())),
81 }
82 }
83
84 Ok(Tag::Custom(s.to_string()))
85 }
86}
87
88pub fn add_tag(path: &Path, tag: &Tag) -> Result<HashSet<Tag>, TagError> {
102 let tag_metadata =
103 xattr::get(path, "com.apple.metadata:_kMDItemUserTags").map_err(TagError::XAttr)?;
104
105 let parsed_tags = match tag_metadata {
106 Some(t) => plist::from_bytes::<plist::Value>(&t).map_err(TagError::Plist)?,
107 None => plist::Value::Array(vec![]),
108 };
109
110 match parsed_tags {
111 plist::Value::Array(t) => {
112 let mut existing_tag_set = t.iter().fold(HashSet::new(), |mut acc, x| {
114 if let plist::Value::String(s) = x {
115 acc.insert(s.to_owned());
116 }
117 acc
118 });
119
120 existing_tag_set.insert(tag.to_string());
121
122 let tags_to_set = &existing_tag_set
123 .iter()
124 .map(|t| plist::Value::String(t.to_string()))
125 .collect::<Vec<_>>();
126
127 let final_tag_set = existing_tag_set
128 .iter()
129 .map(|t| Tag::from_string(t))
130 .collect::<Result<HashSet<_>, TagError>>()?;
131
132 let mut binary_buffer: Vec<u8> = vec![];
133 plist::to_writer_binary(&mut binary_buffer, &tags_to_set).map_err(TagError::Plist)?;
134 xattr::set(path, "com.apple.metadata:_kMDItemUserTags", &binary_buffer)
135 .map_err(TagError::XAttr)?;
136 Ok(final_tag_set)
137 }
138 _ => Err(TagError::Unknown),
139 }
140}
141
142pub fn set_tags(path: &Path, tags: &HashSet<Tag>) -> Result<HashSet<Tag>, TagError> {
156 let tags_to_set = &tags
157 .iter()
158 .map(|t| plist::Value::String(t.to_string()))
159 .collect::<Vec<_>>();
160
161 let mut binary_buffer: Vec<u8> = vec![];
162 plist::to_writer_binary(&mut binary_buffer, &tags_to_set).map_err(TagError::Plist)?;
163 xattr::set(path, "com.apple.metadata:_kMDItemUserTags", &binary_buffer)
164 .map_err(TagError::XAttr)?;
165
166 Ok(tags.clone())
167}
168
169pub fn remove_tag(path: &Path, tag: &Tag) -> Result<HashSet<Tag>, TagError> {
183 let tag_metadata =
184 xattr::get(path, "com.apple.metadata:_kMDItemUserTags").map_err(TagError::XAttr)?;
185
186 let parsed_tags = match tag_metadata {
187 Some(t) => plist::from_bytes::<plist::Value>(&t).map_err(TagError::Plist)?,
188 None => plist::Value::Array(vec![]),
189 };
190
191 match parsed_tags {
192 plist::Value::Array(t) => {
193 let mut existing_tag_set = t.iter().fold(HashSet::new(), |mut acc, x| {
195 if let plist::Value::String(s) = x {
196 acc.insert(s.to_owned());
197 }
198 acc
199 });
200
201 existing_tag_set.remove(&tag.to_string());
202
203 let tags_to_set = &existing_tag_set
204 .iter()
205 .map(|t| plist::Value::String(t.to_string()))
206 .collect::<Vec<_>>();
207
208 let final_tag_set = existing_tag_set
209 .iter()
210 .map(|t| Tag::from_string(t))
211 .collect::<Result<HashSet<_>, TagError>>()?;
212
213 let mut binary_buffer: Vec<u8> = vec![];
214 plist::to_writer_binary(&mut binary_buffer, &tags_to_set).map_err(TagError::Plist)?;
215 xattr::set(path, "com.apple.metadata:_kMDItemUserTags", &binary_buffer)
216 .map_err(TagError::XAttr)?;
217 Ok(final_tag_set)
218 }
219 _ => Err(TagError::Unknown),
220 }
221}
222
223pub fn prune_tags(path: &Path) -> Result<HashSet<Tag>, TagError> {
237 xattr::remove(path, "com.apple.metadata:_kMDItemUserTags").map_err(TagError::XAttr)?;
238 Ok(HashSet::<Tag>::with_capacity(0))
239}
240
241pub fn read_tags(path: &Path) -> Result<HashSet<Tag>, TagError> {
255 let tag_metadata =
256 xattr::get(path, "com.apple.metadata:_kMDItemUserTags").map_err(TagError::XAttr)?;
257
258 let existing_tags = match tag_metadata {
259 Some(t) => plist::from_bytes::<plist::Value>(&t).map_err(TagError::Plist)?,
260 None => plist::Value::Array(vec![]),
261 };
262
263 match existing_tags {
264 plist::Value::Array(t) => {
265 let parsed_tags: HashSet<Tag> = t
266 .iter()
267 .filter_map(|t| {
268 if let plist::Value::String(s) = t {
269 let tag = Tag::from_string(s)
270 .map_err(|_| TagError::Invalid(s.to_owned()))
271 .ok()?;
272 Some(tag)
273 } else {
274 None
275 }
276 })
277 .collect::<HashSet<Tag>>();
278
279 Ok(parsed_tags)
280 }
281 _ => Err(TagError::Unknown),
282 }
283}