Skip to content

Instantly share code, notes, and snippets.

@cfm
Created May 25, 2026 20:37
Show Gist options
  • Select an option

  • Save cfm/ecf2a755989ff2d0d33436f54959eaa0 to your computer and use it in GitHub Desktop.

Select an option

Save cfm/ecf2a755989ff2d0d33436f54959eaa0 to your computer and use it in GitHub Desktop.
courtesy of Claude
#!/usr/bin/env python3
"""Export open Safari tabs from SafariTabs.db to a Netscape-format bookmarks.html."""
import argparse
import html
import sqlite3
import sys
from pathlib import Path
BOOKMARKS_HEADER = """\
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
"""
BOOKMARKS_FOOTER = "</DL><p>\n"
def get_tabs(db_path: str) -> list[dict]:
"""Return all open tabs grouped by window, in tab order."""
con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
con.row_factory = sqlite3.Row
cur = con.cursor()
# Each window has one or more tab groups; each tab group's children are tabs.
cur.execute("""
SELECT
w.id AS window_id,
wtg.tab_group_id,
tg.title AS group_title,
b.title AS tab_title,
b.url
FROM windows w
JOIN windows_tab_groups wtg ON wtg.window_id = w.id
JOIN bookmarks tg ON tg.id = wtg.tab_group_id
JOIN bookmarks b ON b.parent = tg.id
WHERE w.date_closed IS NULL
AND b.deleted = 0
AND b.url IS NOT NULL
AND b.url != ''
ORDER BY w.id, tg.order_index, b.order_index
""")
rows = cur.fetchall()
con.close()
return [dict(r) for r in rows]
def build_html(tabs: list[dict]) -> str:
if not tabs:
return BOOKMARKS_HEADER + BOOKMARKS_FOOTER
lines = [BOOKMARKS_HEADER]
# Group by (window_id, tab_group_id) to emit one folder per tab group.
seen: dict[tuple, list] = {}
for tab in tabs:
key = (tab["window_id"], tab["tab_group_id"])
seen.setdefault(key, []).append(tab)
for (window_id, _), group_tabs in seen.items():
group_title = group_tabs[0]["group_title"] or f"Window {window_id}"
lines.append(f' <DT><H3>{html.escape(group_title)}</H3>\n <DL><p>\n')
for tab in group_tabs:
title = html.escape(tab["tab_title"] or tab["url"])
url = html.escape(tab["url"])
lines.append(f' <DT><A HREF="{url}">{title}</A>\n')
lines.append(' </DL><p>\n')
lines.append(BOOKMARKS_FOOTER)
return "".join(lines)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("db", nargs="?", default="SafariTabs.db",
help="path to SafariTabs.db (default: SafariTabs.db)")
parser.add_argument("out", nargs="?", default="bookmarks.html",
help="output file (default: bookmarks.html)")
args = parser.parse_args()
db_path = Path(args.db)
if not db_path.exists():
sys.exit(f"error: {db_path} not found")
tabs = get_tabs(str(db_path))
if not tabs:
print("No open tabs found.", file=sys.stderr)
html_out = build_html(tabs)
out_path = Path(args.out)
out_path.write_text(html_out, encoding="utf-8")
print(f"Wrote {len(tabs)} tabs to {out_path}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment