Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,13 @@ skill covers every app that shares a storage format:

| Format | Apps covered |
|---|---|
| Chromium `Bookmarks` JSON | Chrome · Brave · Edge · Arc · Chromium · Vivaldi · Opera |
| Chromium `Bookmarks` JSON | Chrome · Brave · Edge · Chromium · Vivaldi · Arc · Opera · Whale |
| Safari plist | Safari |
| `places.sqlite` | Firefox · Tor Browser |

**Ten browsers, one markdown file.** Adding Vivaldi took three lines and no new skill.
**Eleven browsers, one markdown file.** Adding a browser is three lines in two files
and one `sync` command. No new skill, no new code, and every existing browser skill
covers it immediately.

## Platforms

Expand Down
9 changes: 7 additions & 2 deletions docs/ADDING-A-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ that category covers it immediately. No new skill, no new code.
| `scripts/platforms.py`, the family table | where the data lives, on each platform |
| `scripts/platforms.py`, `PROCESS_NAMES` | what the running process is called |

Then run `python3 scripts/sync-skills.py sync`. That regenerates the vendored copies
inside each skill and updates the browser count and list that the README and the site
publish, so nothing has to be remembered by hand. CI fails if you skip it.

**All three, or none.** Registering a path without a process name is the dangerous
half-addition: `running()` returns `None`, so a skill cannot tell the app is open, and
a browser that rewrites its store on exit will clobber the change. Opera shipped in
exactly that state until a test caught it. A test now enforces the pairing, but the
test only helps if you know why it is there.

Vivaldi was added this way. It is a Chromium browser, so it keeps bookmarks in the
same JSON format as Chrome, and it needed no new skill at all.
Every browser this repo supports beyond the first one was added this way. Most of them
are built on Chromium, which means they store bookmarks in exactly the same JSON format
as Chrome does, so supporting one more is a matter of saying where its copy lives.

For the table row, include:

Expand Down
1 change: 1 addition & 0 deletions references/app-data-locations.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ row supports every app in it.
| Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` |
| Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` |
| Arc | `Arc/User Data` | not available | `Arc\User Data` |
| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` |

The bookmarks file is `<root>/<Profile>/Bookmarks`, where `<Profile>` is `Default` or
`Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume
Expand Down
2 changes: 2 additions & 0 deletions scripts/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"},
"arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"},
"opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"},
"whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"},
}

FIREFOX = {
Expand All @@ -70,6 +71,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"},
"arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"},
"opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"},
"whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"},
"firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"},
"safari": {MAC: "Safari", LINUX: None, WINDOWS: None},
"obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"},
Expand Down
62 changes: 57 additions & 5 deletions scripts/sync-skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
So `references/` and `scripts/` stay the single source of truth that humans edit
and tests run against, and this script copies what each skill needs into it.

sync copy shared files into every skill directory
check exit non-zero if any skill is out of date (used by CI)
sync copy shared files into every skill directory, and refresh the
browser counts and lists that the README and the site publish
check exit non-zero if anything is out of date (used by CI)

Adding a browser should cost three lines in two files and this command.
Anything a human has to remember to update by hand will eventually be wrong,
which is why the published numbers are derived here rather than typed.
"""
import argparse, filecmp, glob, hashlib, os, re, shutil, sys
import argparse, glob, hashlib, importlib.util, os, re, shutil, sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Expand Down Expand Up @@ -45,6 +50,51 @@ def digest(path):
return hashlib.sha256(fh.read()).hexdigest()


NUMBER = {1:"One",2:"Two",3:"Three",4:"Four",5:"Five",6:"Six",7:"Seven",8:"Eight",
9:"Nine",10:"Ten",11:"Eleven",12:"Twelve",13:"Thirteen",14:"Fourteen"}


def load_platforms():
spec = importlib.util.spec_from_file_location(
"platforms", os.path.join(ROOT, "scripts", "platforms.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


def published(check_only):
"""Keep the numbers the README and the site advertise equal to the code."""
pf = load_platforms()
chromium = [a.capitalize() for a in pf.CHROMIUM]
total = len(pf.CHROMIUM) + 1 + len(pf.FIREFOX) # + Safari + Firefox family
stale = []

site_path = os.path.join(ROOT, "site", "index.html")
site = open(site_path, encoding="utf-8").read()
new_site = re.sub(r'(class="stat">)\d+(<)', r"\g<1>%d\g<2>" % total, site)
chips = "".join('<span class="chip">%s</span>' % a for a in chromium)
new_site = re.sub(r'(<dt>Chromium JSON</dt>\s*<dd>).*?(</dd>)',
lambda m: m.group(1) + chips + m.group(2), new_site, flags=re.S)

readme_path = os.path.join(ROOT, "README.md")
readme = open(readme_path, encoding="utf-8").read()
new_readme = re.sub(r'\*\*\w+ browsers, one markdown file\.\*\*',
"**%s browsers, one markdown file.**" % NUMBER[total], readme)
new_readme = re.sub(r'(\| Chromium `Bookmarks` JSON \| ).*?( \|)',
lambda m: m.group(1) + " \u00b7 ".join(chromium) + m.group(2),
new_readme)

for path, old, new, label in ((site_path, site, new_site, "site/index.html"),
(readme_path, readme, new_readme, "README.md")):
if old == new:
continue
if check_only:
stale.append(label + " (published browser count or list)")
else:
open(path, "w", encoding="utf-8").write(new)
return stale


def run(check_only):
stale = []
for skill in skill_dirs():
Expand Down Expand Up @@ -74,15 +124,17 @@ def run(check_only):
else:
os.remove(os.path.join(skill, rel))

stale += published(check_only)

if check_only:
if stale:
print("Vendored copies are out of date:\n " + "\n ".join(stale))
print("\nRun: python3 scripts/sync-skills.py sync")
return 1
print(f"All {len(skill_dirs())} skills are in sync.")
print(f"All {len(skill_dirs())} skills and published counts are in sync.")
return 0

print(f"Synced {len(skill_dirs())} skills.")
print(f"Synced {len(skill_dirs())} skills and the published browser counts.")
return 0


Expand Down
10 changes: 6 additions & 4 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
}
@media(max-width:720px){ .row{grid-template-columns:1fr; gap:12px} }
.punch{margin-top:36px; font-size:20px; font-weight:600; letter-spacing:-.02em}
.punch + p code{font:500 13px/1 var(--mono); background:var(--bg-3); padding:2px 6px; border-radius:5px}
.punch em{font-style:normal; color:var(--accent-text)}

/* ---------- contribute: code + prose ---------- */
Expand Down Expand Up @@ -598,7 +599,7 @@ <h2>Three skills, one habit.</h2>
<div class="cat">browser</div>
<h3>organizing&#8209;bookmarks</h3>
<p>Dedupes across tracking parameters, <code>www</code>, and trailing slashes. Prunes dead links. Flattens folder sprawl.</p>
<div class="stat">10<span>browsers covered by this one file</span></div>
<div class="stat">11<span>browsers covered by this one file</span></div>
</div>
<div class="cell span2">
<div class="cat">files</div>
Expand All @@ -623,7 +624,7 @@ <h2>One skill, every app in the category.</h2>
<dl class="cover rise">
<div class="row">
<dt>Chromium JSON</dt>
<dd><span class="chip">Chrome</span><span class="chip">Brave</span><span class="chip">Edge</span><span class="chip">Arc</span><span class="chip">Chromium</span><span class="chip">Vivaldi</span><span class="chip">Opera</span></dd>
<dd><span class="chip">Chrome</span><span class="chip">Brave</span><span class="chip">Edge</span><span class="chip">Chromium</span><span class="chip">Vivaldi</span><span class="chip">Arc</span><span class="chip">Opera</span><span class="chip">Whale</span></dd>
</div>
<div class="row">
<dt>Safari plist</dt>
Expand All @@ -634,8 +635,9 @@ <h2>One skill, every app in the category.</h2>
<dd><span class="chip">Firefox</span><span class="chip">Tor Browser</span></dd>
</div>
</dl>
<p class="punch rise">Adding Vivaldi took <em>three lines and no new skill.</em></p>
<p class="rise" style="margin-top:14px; font-size:15px; color:var(--fg-2); max-width:62ch">The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.</p>
<p class="punch rise">Adding a browser takes <em>three lines and one command.</em></p>
<p class="rise" style="margin-top:14px; font-size:15px; color:var(--fg-2); max-width:62ch">A row in the reference table, the data path, the process name, then one <code>sync</code> command. No new skill and no new code, and every browser skill covers it immediately. Every browser listed above was added that way.</p>
<p class="rise" style="margin-top:12px; font-size:15px; color:var(--fg-2); max-width:62ch">The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.</p>
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ row supports every app in it.
| Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` |
| Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` |
| Arc | `Arc/User Data` | not available | `Arc\User Data` |
| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` |

The bookmarks file is `<root>/<Profile>/Bookmarks`, where `<Profile>` is `Default` or
`Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume
Expand Down
2 changes: 2 additions & 0 deletions skills/browser/organizing-bookmarks/scripts/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"},
"arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"},
"opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"},
"whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"},
}

FIREFOX = {
Expand All @@ -70,6 +71,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"},
"arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"},
"opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"},
"whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"},
"firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"},
"safari": {MAC: "Safari", LINUX: None, WINDOWS: None},
"obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ row supports every app in it.
| Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` |
| Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` |
| Arc | `Arc/User Data` | not available | `Arc\User Data` |
| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` |

The bookmarks file is `<root>/<Profile>/Bookmarks`, where `<Profile>` is `Default` or
`Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume
Expand Down
2 changes: 2 additions & 0 deletions skills/files/organizing-folders/scripts/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"},
"arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"},
"opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"},
"whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"},
}

FIREFOX = {
Expand All @@ -70,6 +71,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"},
"arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"},
"opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"},
"whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"},
"firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"},
"safari": {MAC: "Safari", LINUX: None, WINDOWS: None},
"obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ row supports every app in it.
| Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` |
| Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` |
| Arc | `Arc/User Data` | not available | `Arc\User Data` |
| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` |

The bookmarks file is `<root>/<Profile>/Bookmarks`, where `<Profile>` is `Default` or
`Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume
Expand Down
2 changes: 2 additions & 0 deletions skills/notes/organizing-notes/scripts/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"},
"arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"},
"opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"},
"whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"},
}

FIREFOX = {
Expand All @@ -70,6 +71,7 @@ def _appdata(kind="local"):
"vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"},
"arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"},
"opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"},
"whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"},
"firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"},
"safari": {MAC: "Safari", LINUX: None, WINDOWS: None},
"obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"},
Expand Down
Loading