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.
- sortmeout/__init__.py +23 -0
- sortmeout/app.py +618 -0
- sortmeout/cli.py +550 -0
- sortmeout/config/__init__.py +11 -0
- sortmeout/config/manager.py +313 -0
- sortmeout/config/settings.py +201 -0
- sortmeout/core/__init__.py +21 -0
- sortmeout/core/action.py +889 -0
- sortmeout/core/condition.py +672 -0
- sortmeout/core/engine.py +421 -0
- sortmeout/core/rule.py +254 -0
- sortmeout/core/watcher.py +471 -0
- sortmeout/gui/__init__.py +10 -0
- sortmeout/gui/app.py +325 -0
- sortmeout/macos/__init__.py +19 -0
- sortmeout/macos/spotlight.py +337 -0
- sortmeout/macos/tags.py +308 -0
- sortmeout/macos/trash.py +449 -0
- sortmeout/utils/__init__.py +12 -0
- sortmeout/utils/file_info.py +363 -0
- sortmeout/utils/logger.py +214 -0
- sortmeout-1.0.0.dist-info/METADATA +302 -0
- sortmeout-1.0.0.dist-info/RECORD +27 -0
- sortmeout-1.0.0.dist-info/WHEEL +5 -0
- sortmeout-1.0.0.dist-info/entry_points.txt +3 -0
- sortmeout-1.0.0.dist-info/licenses/LICENSE +21 -0
- sortmeout-1.0.0.dist-info/top_level.txt +1 -0
sortmeout/macos/tags.py
ADDED
|
@@ -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
|