← back to Opened Tagger
add light-blue-text web viewer (opened=blue, unopened=bold white)
bc8dd097a3e2d074c0564eedff696c6cd7b43203 · 2026-06-24 14:10:31 -0700 · Steve
Files touched
M .gitignoreM README.mdA viewer.py
Diff
commit bc8dd097a3e2d074c0564eedff696c6cd7b43203
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 24 14:10:31 2026 -0700
add light-blue-text web viewer (opened=blue, unopened=bold white)
---
.gitignore | 1 +
README.md | 13 ++++
viewer.py | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 214 insertions(+)
diff --git a/.gitignore b/.gitignore
index 05de3c6..1ce4f85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
tagger.log
.DS_Store
+v2.out
diff --git a/README.md b/README.md
index 8749f40..b1ea4ea 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,19 @@ Disable later:
launchctl bootout gui/$(id -u)/com.steve.opened-tagger
```
+## Literal light-blue TEXT (the viewer)
+
+Finder only does the dot. For actual blue *text*, run the viewer — a local web
+page where opened items render in **light blue** and never-opened in **bold white**:
+
+```sh
+python3 ~/Projects/opened-tagger/viewer.py # prints a http://127.0.0.1:PORT/ URL
+```
+
+Open the printed URL. Live from Spotlight, has a filter box, groups by surface
+(Desktop / Documents / Downloads / Projects / Apps) with opened-count per group.
+It's a separate window, not the real Finder — that's the tradeoff for true text color.
+
## Known limitations (honest)
- **Blue dot, not blue text.** Finder renders tags as a colored dot/pill next to
diff --git a/viewer.py b/viewer.py
new file mode 100644
index 0000000..50e803e
--- /dev/null
+++ b/viewer.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+"""
+viewer.py — the literal light-blue-text version of opened-tagger.
+
+Finder can only show a colored dot for tags, never recolored filename text.
+This is the workaround: a tiny local web page where WE control every pixel, so
+opened items render in light blue and never-opened items render bold white.
+
+Same data source as the tagger: Spotlight's kMDItemUseCount (times opened) and
+kMDItemLastUsedDate (when). Pure stdlib http.server — nothing to install.
+
+Run:
+ python3 viewer.py # serves on a free port, prints the URL
+ python3 viewer.py --port 8765
+Then open the printed http://127.0.0.1:PORT/ in a browser.
+"""
+
+import argparse
+import html
+import json
+import os
+import socket
+import subprocess
+from datetime import datetime, timezone
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+
+SURFACES = {
+ "Desktop": os.path.expanduser("~/Desktop"),
+ "Documents": os.path.expanduser("~/Documents"),
+ "Downloads": os.path.expanduser("~/Downloads"),
+ "Projects": os.path.expanduser("~/Projects"),
+}
+EXCLUDE = ("/node_modules/", "/.git/", "/Library/Caches/", "/.Trash/")
+
+
+def mdfind(query, only_in=None):
+ cmd = ["mdfind"]
+ if only_in:
+ cmd += ["-onlyin", only_in]
+ cmd.append(query)
+ try:
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=60).stdout
+ except subprocess.TimeoutExpired:
+ return []
+ return [l for l in out.splitlines() if l]
+
+
+def attrs(path):
+ """Return (use_count, last_used_iso_or_None) for a path via mdls."""
+ r = subprocess.run(
+ ["mdls", "-name", "kMDItemUseCount", "-name", "kMDItemLastUsedDate",
+ "-raw", path],
+ capture_output=True, text=True)
+ parts = r.stdout.split("\0") if "\0" in r.stdout else r.stdout.splitlines()
+ # -raw with two attrs returns them null-separated; fall back gracefully.
+ use, last = 0, None
+ raw = r.stdout
+ # mdls -raw multi-attr is finicky; query individually for reliability.
+ u = subprocess.run(["mdls", "-name", "kMDItemUseCount", "-raw", path],
+ capture_output=True, text=True).stdout.strip()
+ d = subprocess.run(["mdls", "-name", "kMDItemLastUsedDate", "-raw", path],
+ capture_output=True, text=True).stdout.strip()
+ if u.isdigit():
+ use = int(u)
+ if d and d != "(null)":
+ last = d
+ return use, last
+
+
+def collect():
+ rows = []
+ for label, d in SURFACES.items():
+ if not os.path.isdir(d):
+ continue
+ # Every direct child of the surface (files + folders), opened or not.
+ try:
+ children = sorted(os.listdir(d))
+ except OSError:
+ continue
+ for name in children:
+ if name.startswith("."):
+ continue
+ p = os.path.join(d, name)
+ if any(s in p for s in EXCLUDE):
+ continue
+ use, last = attrs(p)
+ rows.append({
+ "surface": label, "name": name, "path": p,
+ "opened": use > 0, "use": use, "last": last,
+ })
+ # Launched apps, Mac-wide.
+ for p in mdfind('kMDItemContentType == "com.apple.application-bundle" '
+ '&& kMDItemUseCount > 0'):
+ use, last = attrs(p)
+ rows.append({
+ "surface": "Apps", "name": os.path.basename(p), "path": p,
+ "opened": True, "use": use, "last": last,
+ })
+ return rows
+
+
+PAGE = """<!doctype html><html><head><meta charset=utf-8>
+<title>Opened / Unopened</title><style>
+ body{{background:#0b0d12;color:#fff;font:14px -apple-system,Helvetica,sans-serif;margin:0;padding:24px}}
+ h1{{font-weight:600;font-size:18px;margin:0 0 4px}}
+ .sub{{color:#8a93a6;margin:0 0 20px}}
+ .legend{{margin:0 0 18px;color:#8a93a6}}
+ .legend b{{color:#fff;font-weight:700}} .legend i{{color:#7fd4ff;font-style:normal}}
+ .surface{{font-weight:600;color:#aab3c5;margin:22px 0 6px;border-bottom:1px solid #1d2230;padding-bottom:4px}}
+ .row{{padding:3px 0;display:flex;gap:12px;align-items:baseline}}
+ .opened{{color:#7fd4ff}} /* light blue = you've opened it */
+ .unopened{{color:#fff;font-weight:700}} /* bold white = never touched */
+ .meta{{color:#5b6377;font-size:12px;margin-left:auto;white-space:nowrap}}
+ input{{background:#161a24;border:1px solid #29303f;color:#fff;border-radius:6px;
+ padding:7px 10px;width:280px;margin-bottom:8px}}
+</style></head><body>
+<h1>Opened / Unopened</h1>
+<p class=sub>Light blue = you've opened it before · bold white = never touched. Live from Spotlight.</p>
+<input id=q placeholder="filter…" oninput="filt()">
+<p class=legend><i>opened</i> · <b>never opened</b></p>
+<div id=list>{body}</div>
+<script>
+function filt(){{var v=document.getElementById('q').value.toLowerCase();
+ document.querySelectorAll('.row').forEach(function(r){{
+ r.style.display=r.dataset.n.indexOf(v)>-1?'flex':'none';}});}}
+</script></body></html>"""
+
+
+def fmt_last(iso):
+ if not iso:
+ return ""
+ try:
+ # mdls gives e.g. 2026-06-24 20:41:27 +0000
+ dt = datetime.strptime(iso[:19], "%Y-%m-%d %H:%M:%S")
+ return dt.strftime("%b %-d, %Y %-I:%M %p")
+ except Exception:
+ return iso
+
+
+def render(rows):
+ by_surface = {}
+ for r in rows:
+ by_surface.setdefault(r["surface"], []).append(r)
+ out = []
+ order = ["Desktop", "Documents", "Downloads", "Projects", "Apps"]
+ for s in order:
+ items = by_surface.get(s)
+ if not items:
+ continue
+ # opened first (blue), then unopened (white); each alphabetical.
+ items.sort(key=lambda r: (not r["opened"], r["name"].lower()))
+ op = sum(1 for r in items if r["opened"])
+ out.append(f'<div class=surface>{html.escape(s)} '
+ f'<span style="color:#5b6377;font-weight:400">'
+ f'({op} opened / {len(items)} total)</span></div>')
+ for r in items:
+ cls = "opened" if r["opened"] else "unopened"
+ meta = ""
+ if r["opened"]:
+ last = fmt_last(r["last"])
+ meta = f'opened {r["use"]}× · {last}' if last else f'opened {r["use"]}×'
+ else:
+ meta = "never opened"
+ nm = html.escape(r["name"])
+ out.append(
+ f'<div class=row data-n="{html.escape(r["name"].lower())}">'
+ f'<span class={cls}>{nm}</span>'
+ f'<span class=meta>{html.escape(meta)}</span></div>')
+ return "\n".join(out)
+
+
+class H(BaseHTTPRequestHandler):
+ def do_GET(self):
+ body = render(collect())
+ page = PAGE.format(body=body)
+ self.send_response(200)
+ self.send_header("Content-Type", "text/html; charset=utf-8")
+ self.end_headers()
+ self.wfile.write(page.encode())
+
+ def log_message(self, *a):
+ pass
+
+
+def free_port():
+ s = socket.socket()
+ s.bind(("127.0.0.1", 0))
+ p = s.getsockname()[1]
+ s.close()
+ return p
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--port", type=int, default=0)
+ args = ap.parse_args()
+ port = args.port or free_port()
+ srv = ThreadingHTTPServer(("127.0.0.1", port), H)
+ print(f"Opened/Unopened viewer → http://127.0.0.1:{port}/")
+ srv.serve_forever()
← a25520e opened-tagger: blue Finder tag for everything opened/launche
·
back to Opened Tagger
·
scope to entire computer (global mdfind + noise filter); add 7b12d6c →