-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcloud-tabs-to-notes.sh
More file actions
executable file
·156 lines (135 loc) · 5.26 KB
/
Copy pathcloud-tabs-to-notes.sh
File metadata and controls
executable file
·156 lines (135 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env bash
#
# cloud-tabs-to-notes.sh — snapshot Safari iCloud tabs from a device.
# Default sink is a new Notes.app note; --open lands them all in a fresh
# Safari window instead.
#
# Usage:
# cloud-tabs-to-notes.sh # list devices and tab counts
# cloud-tabs-to-notes.sh "Device Name" # → new Notes.app note
# cloud-tabs-to-notes.sh --open "Device Name" # → new Safari window
#
# Reads ~/Library/Safari/CloudTabs.db (joins cloud_tabs ↔ cloud_tab_devices).
# Read-only — running this does not push anything back to the source device.
set -euo pipefail
# Modern Safari (sandboxed) writes iCloud Tabs to its container. The
# pre-sandbox path ~/Library/Safari/CloudTabs.db still exists on most
# machines but stopped getting updated when Safari was sandboxed —
# reading from it gives a years-stale snapshot. Prefer the container.
SANDBOX_DB="$HOME/Library/Containers/com.apple.Safari/Data/Library/Safari/CloudTabs.db"
LEGACY_DB="$HOME/Library/Safari/CloudTabs.db"
if [[ -f "$SANDBOX_DB" ]]; then
DB="$SANDBOX_DB"
elif [[ -f "$LEGACY_DB" ]]; then
DB="$LEGACY_DB"
echo "warning: using legacy CloudTabs.db (pre-sandbox); data may be stale" >&2
else
echo "Safari CloudTabs.db not found in container or legacy paths" >&2
exit 1
fi
OPEN_IN_SAFARI=0
DEVICE=""
usage() {
cat <<EOF
Usage:
$(basename "$0") # list devices with tab counts
$(basename "$0") "Device Name" # snapshot tabs into a Notes.app note
$(basename "$0") --open "Device Name" # open all tabs in a new Safari window
Devices currently in iCloud Tabs:
EOF
sqlite3 -separator $'\t' "$DB" "
SELECT d.device_name, count(*)
FROM cloud_tabs t
JOIN cloud_tab_devices d ON t.device_uuid = d.device_uuid
GROUP BY d.device_name
ORDER BY 2 DESC;" \
| awk -F'\t' '{ printf " %5d %s\n", $2, $1 }'
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-o|--open) OPEN_IN_SAFARI=1; shift ;;
--) shift; DEVICE="${1:-}"; shift || true; break ;;
-*) echo "unknown option: $1" >&2; exit 1 ;;
*) DEVICE="$1"; shift ;;
esac
done
if [[ -z "$DEVICE" ]]; then
usage
exit 0
fi
# SQL safety: escape single quotes in the device name (' → '')
DEVICE_SQL=${DEVICE//\'/\'\'}
COUNT=$(sqlite3 "$DB" "
SELECT count(*) FROM cloud_tabs t
JOIN cloud_tab_devices d ON t.device_uuid = d.device_uuid
WHERE d.device_name = '$DEVICE_SQL';")
if [[ "$COUNT" -eq 0 ]]; then
echo "No tabs found for device: $DEVICE" >&2
echo "Run without arguments to see available devices." >&2
exit 2
fi
# ─── Branch: open all tabs in a single new Safari window ──────────────────────
if [[ $OPEN_IN_SAFARI -eq 1 ]]; then
urls=$(mktemp -t cloudtabs)
trap 'rm -f "$urls"' EXIT
sqlite3 "$DB" "
SELECT url FROM cloud_tabs t
JOIN cloud_tab_devices d ON t.device_uuid = d.device_uuid
WHERE d.device_name = '$DEVICE_SQL'
ORDER BY title COLLATE NOCASE;" > "$urls"
# One AppleScript reads all URLs and creates the window + tabs in a single
# invocation — much faster than spawning osascript per URL.
osascript >/dev/null <<APPLESCRIPT
set urlsFile to POSIX file "$urls"
set urlList to paragraphs of (read urlsFile as «class utf8»)
tell application "Safari"
activate
set newDoc to make new document with properties {URL:item 1 of urlList}
set theWindow to front window
repeat with i from 2 to count of urlList
set u to item i of urlList
if u is not "" then
tell theWindow to make new tab with properties {URL:u}
end if
end repeat
end tell
APPLESCRIPT
echo "Opened $COUNT tabs in a new Safari window from device: $DEVICE"
exit 0
fi
# ─── Branch (default): create a Notes.app note ────────────────────────────────
DATE=$(date '+%Y-%m-%d')
TITLE="iPhone tabs — $DEVICE — $DATE"
# File-passing avoids escaping the body into AppleScript (URLs and titles
# contain quotes, ampersands, etc.).
tmp=$(mktemp -t cloudtabs).html
trap 'rm -f "$tmp"' EXIT
{
printf '<h1>%s</h1>\n' "$TITLE"
printf '<p>Snapshot of iCloud tabs from <b>%s</b> on %s. %s tabs.</p>\n' \
"$DEVICE" "$DATE" "$COUNT"
printf '<ol>\n'
# \x01 (SOH) as separator — won't appear in titles or URLs.
sqlite3 -separator $'\x01' "$DB" "
SELECT COALESCE(NULLIF(title, ''), url), url
FROM cloud_tabs t
JOIN cloud_tab_devices d ON t.device_uuid = d.device_uuid
WHERE d.device_name = '$DEVICE_SQL'
ORDER BY title COLLATE NOCASE;" \
| while IFS=$'\x01' read -r title url; do
esc_title=$(printf '%s' "$title" \
| python3 -c 'import sys,html; print(html.escape(sys.stdin.read()), end="")')
printf ' <li><a href="%s">%s</a></li>\n' "$url" "$esc_title"
done
printf '</ol>\n'
} > "$tmp"
TITLE_ESC=$(printf '%s' "$TITLE" | sed 's/\\/\\\\/g; s/"/\\"/g')
osascript >/dev/null <<APPLESCRIPT
set bodyText to read POSIX file "$tmp" as «class utf8»
tell application "Notes"
activate
make new note with properties {name:"$TITLE_ESC", body:bodyText}
end tell
APPLESCRIPT
echo "Created Notes.app note: $TITLE ($COUNT tabs)"