70 lines
2.1 KiB
Python
Executable File
70 lines
2.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import plistlib
|
|
import struct
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
COLORS = {
|
|
"gray": 1,
|
|
"green": 2,
|
|
"purple": 3,
|
|
"blue": 4,
|
|
"yellow": 5,
|
|
"red": 6,
|
|
"orange": 7,
|
|
}
|
|
|
|
|
|
def tag_file(path: Path, color: str) -> None:
|
|
idx = COLORS[color]
|
|
new_tag = f"{color.capitalize()}\n{idx}"
|
|
|
|
# Read existing tags, then append if not already present
|
|
result = subprocess.run(["xattr", "-px", "com.apple.metadata:_kMDItemUserTags", path], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
raw = bytes.fromhex(result.stdout.replace("\n", "").replace(" ", ""))
|
|
existing = plistlib.loads(raw)
|
|
else:
|
|
existing = []
|
|
|
|
if new_tag not in existing:
|
|
existing.append(new_tag)
|
|
|
|
plist_data = plistlib.dumps(existing, fmt=plistlib.FMT_BINARY)
|
|
subprocess.run(["xattr", "-wx", "com.apple.metadata:_kMDItemUserTags", plist_data.hex(), path], check=True)
|
|
|
|
# FinderInfo: color in bits 1-3 of fdFlags (offset 8, big-endian uint16)
|
|
result = subprocess.run(["xattr", "-px", "com.apple.FinderInfo", path], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
raw = bytes.fromhex(result.stdout.replace("\n", "").replace(" ", ""))
|
|
fi = bytearray(raw) if len(raw) >= 32 else bytearray(32)
|
|
else:
|
|
fi = bytearray(32)
|
|
|
|
flags = struct.unpack_from(">H", fi, 8)[0]
|
|
flags = (flags & ~0x000E) | (idx << 1)
|
|
struct.pack_into(">H", fi, 8, flags)
|
|
subprocess.run(["xattr", "-wx", "com.apple.FinderInfo", bytes(fi).hex(), path], check=True)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Tag files with Finder color labels")
|
|
parser.add_argument(
|
|
"--tag", required=True, choices=list(COLORS),
|
|
metavar="COLOR", help=f"One of: {', '.join(COLORS)}",
|
|
)
|
|
parser.add_argument("files", nargs="+", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
for path in args.files:
|
|
if not path.exists():
|
|
print(f"skipping {path}: not found")
|
|
continue
|
|
tag_file(path, args.tag)
|
|
print(f"tagged {path} → {args.tag}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|