├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── sebbie.jpg └── tabby.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby "3.4.2" 3 | 4 | gem "htmlbeautifier" 5 | gem "markaby" 6 | gem "sqlite3" 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | builder (3.3.0) 5 | htmlbeautifier (1.4.3) 6 | markaby (0.9.4) 7 | builder 8 | sqlite3 (2.6.0-aarch64-linux-gnu) 9 | sqlite3 (2.6.0-aarch64-linux-musl) 10 | sqlite3 (2.6.0-arm-linux-gnu) 11 | sqlite3 (2.6.0-arm-linux-musl) 12 | sqlite3 (2.6.0-arm64-darwin) 13 | sqlite3 (2.6.0-x86-linux-gnu) 14 | sqlite3 (2.6.0-x86-linux-musl) 15 | sqlite3 (2.6.0-x86_64-darwin) 16 | sqlite3 (2.6.0-x86_64-linux-gnu) 17 | sqlite3 (2.6.0-x86_64-linux-musl) 18 | 19 | PLATFORMS 20 | aarch64-linux-gnu 21 | aarch64-linux-musl 22 | arm-linux-gnu 23 | arm-linux-musl 24 | arm64-darwin 25 | x86-linux-gnu 26 | x86-linux-musl 27 | x86_64-darwin 28 | x86_64-linux-gnu 29 | x86_64-linux-musl 30 | 31 | DEPENDENCIES 32 | htmlbeautifier 33 | markaby 34 | sqlite3 35 | 36 | RUBY VERSION 37 | ruby 3.4.2 38 | 39 | BUNDLED WITH 40 | 2.6.2 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Patrick Crowley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tabby 2 | ====== 3 | Backup your Safari tab groups with Tabby. 4 | 5 | Safari tab groups are an awesome way to manage your bookmarks. But, unfortunately at the moment, Safari does not provide a way to backup bookmarks stored within tab groups. 6 | 7 | So let's fix that. 8 | 9 | Tabby can: 10 | - Export tab groups as HTML 11 | - Export tab groups as CSV 12 | - Export tab groups from default profile 13 | - Export tab groups from all other profiles 14 | - Export tab groups in current order (including tab order within groups) 15 | 16 | ## Installation 17 | 18 | ### 1. Install ruby 19 | Unfortunately, macOS includes an outdated version of Ruby, so you will need to install a modern version of Ruby (3.4.2 or higher) to use Tabby. 20 | 21 | If you need to install ruby, follow this guide: 22 | https://gorails.com/setup/macos/15-sequoia 23 | 24 | ### 2. Enable full disk permissions for Terminal app 25 | 1. Open System Settings. 26 | 2. Select Privacy & Security settings. 27 | 3. Select Full Disk Access. 28 | 4. Add Terminal app if not listed. 29 | 5. Enable Full Disk Access for Terminal app. 30 | 31 | ### 3. Install script 32 | 1. `git clone git@github.com:mokolabs/tabby.git ~/Desktop/tabby` 33 | 2. Open your terminal app. 34 | 3. Run `cd ~/Desktop/tabby` to open the tabby directory. 35 | 4. Run `bundle install` to install dependencies. 36 | 5. Run `ruby tabby.rb` to export your tab groups to the desktop. 37 | 38 | ### 4. Optional steps 39 | - Need to customize the export location? Just pass a file path to the tabby command. 40 | `ruby tabby.rb ~/Library/Backup` 41 | - Need to move tabby to a different location? The script should work in any location within your home directory. 42 | - Need to backup your tab groups on a daily basis? Just write a cron task that runs the tabby command! 43 | - Using Safari Technology Preview? Just add this flag: `ruby tabby.rb -stp` 44 | 45 | ## Feedback 46 | Have a suggestion? Your feedback is welcome! Feel free to open an issue or PR. 47 | 48 | (Tabby is brought to you by Sebbie.) 49 | 50 | ![Sebbie](sebbie.jpg) 51 | -------------------------------------------------------------------------------- /sebbie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokolabs/tabby/96766fc6e0888e5874a0f9fbd1330e02a804ea07/sebbie.jpg -------------------------------------------------------------------------------- /tabby.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | require "fileutils" 3 | require "htmlbeautifier" 4 | require "markaby" 5 | require "sqlite3" 6 | require "tempfile" 7 | 8 | # Set export path 9 | base = ARGV[0] && !ARGV[0].start_with?("-") ? File.expand_path(ARGV[0]) : File.expand_path("~/Desktop") 10 | base = File.join(base, "tabgroups"); FileUtils.mkdir_p(base) 11 | 12 | # Set library path 13 | app = ARGV.include?("-stp") ? "SafariTechnologyPreview" : "Safari" 14 | library = File.expand_path("~/Library/Containers/com.apple.#{app}/Data/Library/#{app}") 15 | 16 | # Copy safari tabs into temporary file 17 | original = File.expand_path("#{library}/SafariTabs.db") 18 | temporary = Tempfile.new("SafariTabs.db"); FileUtils.cp(original, temporary.path) 19 | 20 | # Export tab groups 21 | begin 22 | db = SQLite3::Database.open(temporary.path) 23 | 24 | # Select tab groups from personal profile 25 | personal = [:personal, "SELECT id, title FROM bookmarks WHERE type = 1 AND parent = 0 AND subtype == 0 AND num_children > 0 AND hidden == 0 ORDER BY id DESC"] 26 | 27 | # Select tab groups from all other profiles 28 | profiles = "SELECT id, title FROM bookmarks WHERE subtype = '2' and title != ''" 29 | profiles = db.execute(profiles).map do |profile| 30 | [profile[1].downcase.to_sym, "SELECT id, title FROM bookmarks WHERE parent = #{profile[0]} AND subtype == 0 AND num_children > 0 ORDER BY id DESC"] 31 | end 32 | 33 | # Export tab groups to CSV and HTML 34 | groups = [personal] 35 | groups << profiles.flatten unless profiles.empty? 36 | groups.each do |profile| 37 | profile, query = profile 38 | tab_groups = db.execute(query) 39 | 40 | profile_directory = File.join(base, profile.to_s) 41 | FileUtils.mkdir_p(profile_directory) 42 | 43 | # Export to CSV 44 | CSV.open("#{profile_directory}/bookmarks.csv", "w") do |csv| 45 | csv << ["Tab Group", "Bookmark", "URL"] 46 | 47 | tab_groups.each do |group| 48 | id, group = group 49 | 50 | query = "SELECT title, url FROM bookmarks WHERE parent = #{id} AND title NOT IN ('TopScopedBookmarkList', 'Untitled', 'Start Page') ORDER BY order_index ASC" 51 | bookmarks = db.execute(query) 52 | 53 | bookmarks.each do |bookmark| 54 | name, url = bookmark 55 | csv << [group, name, url] 56 | end 57 | end 58 | end 59 | 60 | # Export to HTML 61 | File.open("#{profile_directory}/bookmarks.html", "w") do |html| 62 | # Add header 63 | html.puts <<~HTML 64 | 65 | 66 | #{profile.capitalize} Bookmarks 67 |

#{profile.capitalize} Bookmarks

68 | HTML 69 | 70 | # Add bookmarks 71 | mab = Markaby::Builder.new 72 | tab_groups.each do |group| 73 | id, name = group 74 | 75 | query = "SELECT title, url FROM bookmarks WHERE parent = #{id} AND title NOT IN ('TopScopedBookmarkList', 'Untitled', 'Start Page') ORDER BY order_index ASC" 76 | bookmarks = db.execute(query) 77 | 78 | mab.dt { h3 name } 79 | mab.dd do 80 | mab.dl do 81 | bookmarks.each do |bookmark| 82 | name, url = bookmark 83 | mab.dt do 84 | mab.a name, href: url 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | # Format markup and write to file 92 | html.puts HtmlBeautifier.beautify(mab.to_s, tab_width: 2) 93 | end 94 | end 95 | 96 | db.close 97 | ensure 98 | # Delete temporary file 99 | temporary.close 100 | temporary.unlink 101 | end 102 | --------------------------------------------------------------------------------