#!/usr/bin/env cached-nix-shell
#! nix-shell -p ruby_2_7 rubyPackages_2_7.sqlite3 -i "ruby -S" --quiet
# Open your browser bookmarks or history in your default browser. Currently
# works with Firefox, Brave, and qutebrowser.
require 'shellwords'
require 'sqlite3'
require 'json'
require 'fileutils'
require 'cgi'
def rofi items, *args
  cmd = [*%w{rofi -dmenu -theme theme/bookmarkmenu.rasi -markup-rows -multi-select -p browser -i}, *args]
  IO.popen(cmd.compact.shelljoin, "r+") do |pipe|
    pipe.puts items.keys.join "\n"
    pipe.close_write
    (pipe.gets || "").split "\n"
  end
end
def db file, command
  copy = "/tmp/browsermenu-db-#{File.basename(file)}"
  begin
    # Copy the sqlite database because history/bookmark info is usually small
    # and may be locked by an open browser.
    unless File.exists? copy
      FileUtils.cp file, copy
      FileUtils.chmod 0400, copy, verbose: true
    end
    results = []
    db = SQLite3::Database.open copy
    db.execute(command).each { |r| results.push r }
    results
  ensure
    File.delete copy
  end
end
def walk_json children
  result = []
  for c in children
    if c['children']
      result += walk_json c['children']
    else
      result.push [ c['name'], c['url'] ]
    end
  end
  result
end
def get_bookmarks browser
  bookmarks = []
  case browser
  when :firefox
    # In my nixos dotfiles I hardcode the default profile to $USER.profile,
    # which makes this lookup easier, but means this script won't work on any
    # other system.
    user = ENV['USER']
    ["#{ENV['HOME']}/.mozilla/firefox/#{user}.default",
     "#{ENV['XDG_CONFIG_HOME']}/firefox/.mozilla/firefox/#{user}.default",
     "#{ENV['XDG_DATA_HOME']}/mozilla/firefox/#{user}.default"].each do |dir|
      if Dir.exists? dir
        db "#{dir}/places.sqlite",
           ["SELECT DISTINCT mb.title, mp.url FROM moz_bookmarks mb",
            "INNER JOIN moz_places mp ON mp.id = mb.fk",
            "WHERE mb.type = 1 AND mb.title IS NOT NULL",
            "ORDER BY mp.frecency DESC",
            "LIMIT 200"].join(' ')
      end
    end
  when :brave
    f = File.read "#{ENV['XDG_CONFIG_HOME']}/BraveSoftware/Brave-Browser/Default/Bookmarks"
    data = JSON.parse f
    walk_json data["roots"]["bookmark_bar"]["children"]
  when :qutebrowser
    bookmarks = File.readlines "#{ENV['XDG_CONFIG_HOME']}/qutebrowser/bookmarks/urls"
    bookmarks.map { |s| s.strip.split(' ', 2).reverse }
  end
end
def get_history browser
  case browser
  when :firefox
    # In my nixos dotfiles I hardcode the default profile to $USER.profile,
    # which makes this lookup easier, but means this script won't work on any
    # other system.
    user = ENV['USER']
    ["#{ENV['HOME']}/.mozilla/firefox/#{user}.default",
     "#{ENV['XDG_CONFIG_HOME']}/firefox/.mozilla/firefox/#{user}.default",
     "#{ENV['XDG_DATA_HOME']}/mozilla/firefox/#{user}.default"].each do |dir|
      if Dir.exists? dir
        return db "#{dir}/places.sqlite",
                  ["SELECT title, url",
                   "FROM moz_places",
                   "WHERE title<>''",
                   "ORDER BY last_visit_date DESC"].join(' ')
      end
    end
    return []
  when :brave
    db "#{ENV['XDG_CONFIG_HOME']}/BraveSoftware/Brave-Browser/Default/History",
       "SELECT title, url FROM urls GROUP BY url ORDER BY last_visit_time DESC LIMIT 200"
  when :qutebrowser
    db "#{ENV['XDG_DATA_HOME']}/qutebrowser/history.sqlite",
       ["SELECT title, url FROM History",
        "WHERE title != ''",
        "GROUP BY url",
        "ORDER BY atime DESC"].join(' ')
  end
end
def list urls
  items = urls.map do |x|
    x.map! { |x| CGI.escapeHTML x }
    [x[0].empty? ?
       "#{x[1]}" :
       "#{x[0]} #{x[1]}",
     x[1]]
  end.to_h
  selected = rofi items
  selected.map { |key| items[key] }
end
type, browser = *ARGV
browser = (browser || ENV['BROWSER']).to_sym
urls = if type == 'history'
  get_history browser
else
  get_bookmarks browser
end
urls = list urls
if urls.empty?
  puts "No URL selected. Aborting..."
  exit 1
end
Process.detach fork { exec ENV['BROWSER'], *urls }