sortmeout 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,308 @@
1
+ """
2
+ macOS Finder tags management.
3
+
4
+ Provides functions for reading and modifying Finder tags on files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import plistlib
10
+ import subprocess
11
+ from typing import List, Optional
12
+
13
+ from sortmeout.utils.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ # Standard macOS tag colors
18
+ TAG_COLORS = {
19
+ "none": 0,
20
+ "gray": 1,
21
+ "green": 2,
22
+ "purple": 3,
23
+ "blue": 4,
24
+ "yellow": 5,
25
+ "red": 6,
26
+ "orange": 7,
27
+ }
28
+
29
+ # Reverse mapping
30
+ COLOR_NAMES = {v: k for k, v in TAG_COLORS.items()}
31
+
32
+
33
+ def get_tags(file_path: str) -> List[str]:
34
+ """
35
+ Get Finder tags for a file.
36
+
37
+ Args:
38
+ file_path: Path to the file.
39
+
40
+ Returns:
41
+ List of tag names.
42
+ """
43
+ try:
44
+ result = subprocess.run(
45
+ ["xattr", "-px", "com.apple.metadata:_kMDItemUserTags", file_path],
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=5,
49
+ )
50
+
51
+ if result.returncode != 0:
52
+ return []
53
+
54
+ # Parse hex output to binary
55
+ hex_data = result.stdout.replace(" ", "").replace("\n", "")
56
+ binary_data = bytes.fromhex(hex_data)
57
+
58
+ # Parse plist
59
+ tags_data = plistlib.loads(binary_data)
60
+
61
+ # Extract tag names (format: "name\ncolor_index")
62
+ tags = []
63
+ for tag in tags_data:
64
+ if isinstance(tag, str):
65
+ name = tag.split("\n")[0]
66
+ tags.append(name)
67
+
68
+ return tags
69
+
70
+ except Exception as e:
71
+ logger.debug("Failed to get tags for %s: %s", file_path, e)
72
+ return []
73
+
74
+
75
+ def set_tags(file_path: str, tags: List[str]) -> bool:
76
+ """
77
+ Set Finder tags for a file (replaces all existing tags).
78
+
79
+ Args:
80
+ file_path: Path to the file.
81
+ tags: List of tag names to set.
82
+
83
+ Returns:
84
+ True if successful.
85
+ """
86
+ try:
87
+ if not tags:
88
+ # Remove all tags
89
+ subprocess.run(
90
+ ["xattr", "-d", "com.apple.metadata:_kMDItemUserTags", file_path],
91
+ capture_output=True,
92
+ timeout=5,
93
+ )
94
+ return True
95
+
96
+ # Create plist data
97
+ tag_data = [f"{tag}\n0" for tag in tags] # 0 = no color
98
+ plist_data = plistlib.dumps(tag_data)
99
+
100
+ # Convert to hex string for xattr
101
+ hex_string = plist_data.hex()
102
+
103
+ # Write using xattr
104
+ result = subprocess.run(
105
+ ["xattr", "-wx", "com.apple.metadata:_kMDItemUserTags", hex_string, file_path],
106
+ capture_output=True,
107
+ timeout=5,
108
+ )
109
+
110
+ return result.returncode == 0
111
+
112
+ except Exception as e:
113
+ logger.error("Failed to set tags for %s: %s", file_path, e)
114
+ return False
115
+
116
+
117
+ def add_tags(file_path: str, tags: List[str]) -> bool:
118
+ """
119
+ Add tags to a file (preserves existing tags).
120
+
121
+ Args:
122
+ file_path: Path to the file.
123
+ tags: List of tag names to add.
124
+
125
+ Returns:
126
+ True if successful.
127
+ """
128
+ existing = get_tags(file_path)
129
+ new_tags = list(set(existing + tags))
130
+ return set_tags(file_path, new_tags)
131
+
132
+
133
+ def remove_tags(file_path: str, tags: List[str]) -> bool:
134
+ """
135
+ Remove specific tags from a file.
136
+
137
+ Args:
138
+ file_path: Path to the file.
139
+ tags: List of tag names to remove.
140
+
141
+ Returns:
142
+ True if successful.
143
+ """
144
+ existing = get_tags(file_path)
145
+ new_tags = [t for t in existing if t not in tags]
146
+ return set_tags(file_path, new_tags)
147
+
148
+
149
+ def has_tag(file_path: str, tag: str) -> bool:
150
+ """
151
+ Check if a file has a specific tag.
152
+
153
+ Args:
154
+ file_path: Path to the file.
155
+ tag: Tag name to check.
156
+
157
+ Returns:
158
+ True if file has the tag.
159
+ """
160
+ tags = get_tags(file_path)
161
+ return tag in tags
162
+
163
+
164
+ def clear_tags(file_path: str) -> bool:
165
+ """
166
+ Remove all tags from a file.
167
+
168
+ Args:
169
+ file_path: Path to the file.
170
+
171
+ Returns:
172
+ True if successful.
173
+ """
174
+ return set_tags(file_path, [])
175
+
176
+
177
+ def get_all_tags() -> List[str]:
178
+ """
179
+ Get all tags defined in the system.
180
+
181
+ Returns:
182
+ List of all tag names.
183
+ """
184
+ try:
185
+ # Read from Finder preferences
186
+ result = subprocess.run(
187
+ ["defaults", "read", "com.apple.finder", "FavoriteTagNames"],
188
+ capture_output=True,
189
+ text=True,
190
+ timeout=5,
191
+ )
192
+
193
+ if result.returncode == 0:
194
+ # Parse the output (it's in plist format)
195
+ import re
196
+ tags = re.findall(r'"([^"]+)"', result.stdout)
197
+ return tags
198
+ except Exception as e:
199
+ logger.debug("Failed to get all tags: %s", e)
200
+
201
+ # Return default tags
202
+ return ["Red", "Orange", "Yellow", "Green", "Blue", "Purple", "Gray"]
203
+
204
+
205
+ def set_finder_comment(file_path: str, comment: str) -> bool:
206
+ """
207
+ Set Finder comment for a file.
208
+
209
+ Args:
210
+ file_path: Path to the file.
211
+ comment: Comment to set.
212
+
213
+ Returns:
214
+ True if successful.
215
+ """
216
+ try:
217
+ # Escape special characters
218
+ comment = comment.replace('"', '\\"')
219
+
220
+ script = f'''
221
+ tell application "Finder"
222
+ set theFile to POSIX file "{file_path}" as alias
223
+ set comment of theFile to "{comment}"
224
+ end tell
225
+ '''
226
+
227
+ result = subprocess.run(
228
+ ["osascript", "-e", script],
229
+ capture_output=True,
230
+ timeout=10,
231
+ )
232
+
233
+ return result.returncode == 0
234
+
235
+ except Exception as e:
236
+ logger.error("Failed to set Finder comment: %s", e)
237
+ return False
238
+
239
+
240
+ def get_finder_comment(file_path: str) -> Optional[str]:
241
+ """
242
+ Get Finder comment for a file.
243
+
244
+ Args:
245
+ file_path: Path to the file.
246
+
247
+ Returns:
248
+ Comment string or None.
249
+ """
250
+ try:
251
+ script = f'''
252
+ tell application "Finder"
253
+ set theFile to POSIX file "{file_path}" as alias
254
+ get comment of theFile
255
+ end tell
256
+ '''
257
+
258
+ result = subprocess.run(
259
+ ["osascript", "-e", script],
260
+ capture_output=True,
261
+ text=True,
262
+ timeout=10,
263
+ )
264
+
265
+ if result.returncode == 0:
266
+ return result.stdout.strip()
267
+ return None
268
+
269
+ except Exception as e:
270
+ logger.debug("Failed to get Finder comment: %s", e)
271
+ return None
272
+
273
+
274
+ def set_label_color(file_path: str, color: str | int) -> bool:
275
+ """
276
+ Set Finder label color for a file.
277
+
278
+ Args:
279
+ file_path: Path to the file.
280
+ color: Color name or index (0-7).
281
+
282
+ Returns:
283
+ True if successful.
284
+ """
285
+ if isinstance(color, str):
286
+ color_index = TAG_COLORS.get(color.lower(), 0)
287
+ else:
288
+ color_index = color
289
+
290
+ try:
291
+ script = f'''
292
+ tell application "Finder"
293
+ set theFile to POSIX file "{file_path}" as alias
294
+ set label index of theFile to {color_index}
295
+ end tell
296
+ '''
297
+
298
+ result = subprocess.run(
299
+ ["osascript", "-e", script],
300
+ capture_output=True,
301
+ timeout=10,
302
+ )
303
+
304
+ return result.returncode == 0
305
+
306
+ except Exception as e:
307
+ logger.error("Failed to set label color: %s", e)
308
+ return False