├── .gitignore ├── README.md ├── assets └── bbm-search.png ├── bin └── bbm.js ├── browser-plugins ├── browser-plugin.js ├── chrome-plugin.js ├── firefox-plugin.js └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | dist 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser Bookmark Manager 2 | 3 | [![dependencies](https://david-dm.org/dj-hedgehog/browser-bookmark-manager.svg)](https://david-dm.org/dj-hedgehog/browser-bookmark-manager) 4 | [![npm](https://img.shields.io/npm/v/browser-bookmark-manager.svg?maxAge=2592000)](https://www.npmjs.com/package/browser-bookmark-manager) 5 | [![npm2](https://img.shields.io/npm/dt/browser-bookmark-manager.svg?maxAge=2592000)](https://www.npmjs.com/package/browser-bookmark-manager) 6 | 7 | A CLI to help you (fuzzily) find and manage your browser bookmarks. 8 | 9 | ![](assets/bbm-search.png) 10 | 11 | ## Installation 12 | 13 | Via NPM: 14 | ``` 15 | npm install -g browser-bookmark-manager 16 | ``` 17 | 18 | ## Usage 19 | 20 | ``` 21 | bbm 22 | ``` 23 | 24 | If no `` is specified you may browse through all boomarks. 25 | 26 | ### Options 27 | 28 | 29 | ``` 30 | -b, --browser 31 | ``` 32 | Searches for bookmarks on this browser. Currently only Google Chrome and Firefox are supported 33 | 34 | Default: `chrome` 35 | 36 | 37 | ``` 38 | -p, --profile 39 | ``` 40 | Search for bookmarks using this profile. 41 | 42 | Default: `Default` 43 | 44 | ``` 45 | -h, --help 46 | ``` 47 | Shows the help screen 48 | 49 | ## Browser Support 50 | 51 | - [x] Google Chrome 52 | - [x] Firefox 53 | - [ ] Opera 54 | - [ ] Safari 55 | - [ ] Microsoft Edge 56 | 57 | ## OS Support 58 | 59 | - [x] OS X 60 | - [x] Windows 61 | - [x] Linux 62 | 63 | ## To-Do 64 | 65 | - [ ] Delete bookmarks 66 | - [ ] *Any ideas ?* 67 | -------------------------------------------------------------------------------- /assets/bbm-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchtime-ali/browser-bookmark-manager/06e5d72c5097482529a19295f5d37870ae6548e4/assets/bbm-search.png -------------------------------------------------------------------------------- /bin/bbm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander' 4 | import inquirer from 'inquirer' 5 | import * as plugins from '../browser-plugins/index' 6 | import {version} from '../package.json' 7 | import chalk from 'chalk' 8 | 9 | program 10 | .version(version) 11 | .usage('[options] ') 12 | .option('-b, --browser [browsername]', 'specified type of browser [chrome]', 'chrome') 13 | .option('-p, --profile [profilename]', 'name of browsers user profile', 'Default') 14 | .parse(process.argv) 15 | 16 | program.browser = program.browser.toLowerCase() 17 | const browserClass = plugins.browserNames[program.browser] 18 | 19 | // Exit if an invalid browser was chosen 20 | if (browserClass === undefined) { 21 | errorExit(`'${program.browser}' is not a valid browser name. Valid browsernames are "firefox", "chrome"`) 22 | } 23 | 24 | const currentPlugin = new plugins[browserClass]() 25 | const searchTerms = program.args 26 | 27 | // Perform the search for the actual bookmarks 28 | let results 29 | try { 30 | results = currentPlugin.search(searchTerms[0], program.profile) 31 | } catch (err) { 32 | errorExit(err.toString()) 33 | } 34 | 35 | inquirer.prompt([ 36 | { 37 | type: 'list', 38 | name: 'url', 39 | message: `Which bookmark do you want to open?`, 40 | choices: results 41 | } 42 | ]).then(function (answer) { 43 | currentPlugin.open(answer.url) 44 | console.log(`Opening ${answer.url}`) 45 | }) 46 | 47 | function errorExit (message) { 48 | const error = chalk.bold.red 49 | console.log(error(message)) 50 | process.exit(0) 51 | } 52 | -------------------------------------------------------------------------------- /browser-plugins/browser-plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Fuse from 'fuse.js' 4 | import chalk from 'chalk' 5 | 6 | class BrowserPlugin { 7 | constructor () { 8 | this.resultsCount = 0 9 | this.matchesCount = 0 10 | 11 | // Prevent creating BrowserPlugin 12 | if (new.target === BrowserPlugin) { 13 | throw new TypeError(`Cannot construct abstract BrowserPlugin directly`) 14 | } 15 | 16 | // Check whether the derived class contains all abstract methods 17 | ['search', 'open', 'getBookmarks'].forEach(requiredMethod => { 18 | if (typeof this[requiredMethod] !== 'function') { 19 | throw new TypeError(`Must override method ${requiredMethod}()`) 20 | } 21 | }) 22 | } 23 | 24 | search (searchTerm, profile = 'Default') { 25 | // Gather bookmarks 26 | const bookmarks = this.getBookmarks(profile) 27 | 28 | this.resultsCount = bookmarks.length 29 | 30 | // Just output all bookmarks 31 | if (searchTerm === undefined) { 32 | console.log(chalk.green(`Showing all ${this.resultsCount} bookmarks`)) 33 | return bookmarks 34 | } 35 | 36 | let options = { 37 | // include: ['score'], 38 | caseSensitive: false, 39 | shouldSort: true, 40 | tokenize: false, 41 | threshold: 0.3, 42 | location: 0, 43 | distance: 100, 44 | keys: ['name', 'value'] 45 | } 46 | 47 | let fuse = new Fuse(bookmarks, options) 48 | 49 | let filteredBookmarks = fuse.search(searchTerm) 50 | this.matchesCount = filteredBookmarks.length 51 | 52 | if (filteredBookmarks.length === 0) { 53 | console.log(chalk.green(`Searched through ${this.resultsCount} bookmarks`)) 54 | throw new Error(`None matched "${searchTerm}"`) 55 | } 56 | 57 | console.log(chalk.green(`Found ${this.matchesCount} matches in ${this.resultsCount} bookmarks`)) 58 | return filteredBookmarks 59 | } 60 | 61 | } 62 | 63 | export default BrowserPlugin 64 | -------------------------------------------------------------------------------- /browser-plugins/chrome-plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import childProc from 'child_process' 4 | import fs from 'fs' 5 | import os from 'os' 6 | import path from 'path' 7 | 8 | import BrowserPlugin from './browser-plugin' 9 | 10 | class ChromePlugin extends BrowserPlugin { 11 | 12 | /** 13 | * Determines the location of the file containing the bookmarks 14 | * @param {string} profile name of profiel 15 | * @return {string} path to Bookmarks file 16 | */ 17 | static getBookmarkLocation (profile) { 18 | // Determine Chrome config location 19 | if (os.type() === 'Darwin') { 20 | return `${os.homedir()}/Library/Application Support/Google/Chrome/${profile}/Bookmarks` 21 | } else if (os.type() === 'Windows_NT') { 22 | return path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', profile, 'Bookmarks') 23 | } else if (os.type() === 'Linux') { 24 | return path.join(os.homedir(), '.config', 'google-chrome', profile, 'Bookmarks') 25 | } 26 | } 27 | 28 | getBookmarks (profile) { 29 | // Yes we can use synchronous code here because the file needs to be loaded before something will happen anyways 30 | let data 31 | try { 32 | data = fs.readFileSync(ChromePlugin.getBookmarkLocation(profile), 'utf8') 33 | } catch (err) { 34 | throw new Error(`There is no profile '${profile}'`) 35 | } 36 | const obj = JSON.parse(data) 37 | 38 | let it 39 | let res 40 | let bookmarkItems = [] 41 | 42 | // Traverse all roots keys 43 | for (let key in obj.roots) { 44 | it = traverseTree(obj.roots[key]) 45 | 46 | res = it.next() 47 | while (!res.done) { 48 | if (res.value.type === 'url') { 49 | bookmarkItems.push({ 50 | name: res.value.name, 51 | value: res.value.url 52 | }) 53 | } 54 | res = it.next() 55 | } 56 | } 57 | return bookmarkItems 58 | } 59 | 60 | open (url) { 61 | if (os.type() === 'Darwin') { 62 | childProc.exec(`open -a "Google Chrome" "${url}"`) 63 | } else if (os.type() === 'Windows_NT') { 64 | childProc.exec(`start chrome "${url}"`) 65 | } else if (os.type() === 'Linux') { 66 | childProc.exec(`chrome "${url}"`) 67 | } 68 | } 69 | } 70 | 71 | function * traverseTree (data) { 72 | if (!data) { 73 | return 74 | } 75 | 76 | if (data.children) { 77 | yield * traverseTree(data.children) 78 | } 79 | 80 | for (var i = 0; i < data.length; i++) { 81 | var val = data[i] 82 | yield val 83 | 84 | if (val.children) { 85 | yield * traverseTree(val.children) 86 | } 87 | } 88 | } 89 | 90 | export default ChromePlugin 91 | -------------------------------------------------------------------------------- /browser-plugins/firefox-plugin.js: -------------------------------------------------------------------------------- 1 | import childProc from 'child_process' 2 | import fs from 'fs' 3 | import os from 'os' 4 | import path from 'path' 5 | 6 | import sql from 'sql.js' 7 | import ini from 'ini' 8 | 9 | import BrowserPlugin from './browser-plugin' 10 | 11 | // Determine Firefox config location 12 | let dir 13 | if (os.type() === 'Darwin') { 14 | dir = `${os.homedir()}/Library/Application Support/Firefox` 15 | } else if (os.type() === 'Windows_NT') { 16 | dir = path.join(os.homedir(), 'AppData', 'Roaming', 'Mozilla', 'Firefox') 17 | } else if (os.type() === 'Linux') { 18 | dir = path.join(os.homedir(), '.mozilla', 'firefox') 19 | } 20 | 21 | const iniName = 'profiles.ini' 22 | const filename = 'places.sqlite' 23 | 24 | class FirefoxPlugin extends BrowserPlugin { 25 | 26 | /** 27 | * Retrieves the location of `profileToSearch` 28 | * @param {string} profileToSearch Name of profile 29 | * @return {string} local path to profile 30 | */ 31 | getProfileLocation (profileToSearch) { 32 | const iniBuffer = fs.readFileSync(path.join(dir, iniName), 'utf8') 33 | const iniObj = ini.decode(iniBuffer) 34 | 35 | for (let profileId in iniObj) { 36 | let profile = iniObj[profileId] 37 | // Check whether this config item is a profile at all 38 | if (profile['Name'] !== undefined) { 39 | if (profileToSearch === 'Default' || profileToSearch.toLowerCase() === profile['Name'].toLowerCase()) { 40 | return path.join(dir, profile.Path) 41 | } 42 | } 43 | } 44 | 45 | throw new Error(`The profile ${profileToSearch} could not be found!`) 46 | } 47 | 48 | getBookmarks (profile) { 49 | let profilePath = this.getProfileLocation(profile) 50 | let bookmarks = [] 51 | const filebuffer = fs.readFileSync(path.join(profilePath, filename)) 52 | var db = new sql.Database(filebuffer) 53 | db.each(`SELECT moz_bookmarks.title as name, moz_places.url as value 54 | FROM moz_bookmarks 55 | INNER JOIN moz_places ON moz_bookmarks.fk = moz_places.id 56 | WHERE url NOT LIKE "place:%"`, (obj) => { 57 | bookmarks.push(obj) 58 | }) 59 | return bookmarks 60 | } 61 | 62 | open (url) { 63 | if (os.type() === 'Darwin') { 64 | childProc.exec(`open -a "Firefox" "${url}"`) 65 | } else if (os.type() === 'Windows_NT') { 66 | childProc.exec(`start firefox "${url}"`) 67 | } else if (os.type() === 'Linux') { 68 | childProc.exec(`firefox "${url}"`) 69 | } 70 | } 71 | } 72 | 73 | export default FirefoxPlugin 74 | -------------------------------------------------------------------------------- /browser-plugins/index.js: -------------------------------------------------------------------------------- 1 | import ChromePlugin from './chrome-plugin' 2 | import FirefoxPlugin from './firefox-plugin' 3 | export { FirefoxPlugin, ChromePlugin } 4 | 5 | const browserNames = { 6 | 'chrome': 'ChromePlugin', 7 | 'firefox': 'FirefoxPlugin' 8 | } 9 | 10 | export { browserNames } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-bookmark-manager", 3 | "version": "0.2.0", 4 | "description": "Find and manager your Chrome, Firefox, Safari, Opera and Edge bookmarks using the command-line", 5 | "keywords": [ 6 | "browser", 7 | "bookmark", 8 | "chrome", 9 | "firefox", 10 | "safari", 11 | "opera", 12 | "edge", 13 | "search", 14 | "cli" 15 | ], 16 | "bin": { 17 | "bbm": "bin/bbm.js" 18 | }, 19 | "preferGlobal": true, 20 | "scripts": { 21 | "start": "node_modules/.bin/babel-node bin/bbm.js", 22 | "bundle": "rm -rf dist/ && babel . --out-dir dist --ignore node_modules && cp README.md package.json dist/" 23 | }, 24 | "author": "Alexander Zigelski (http://ali.dj)", 25 | "repository": "dj-hedgehog/browser-bookmark-manager", 26 | "license": "MIT", 27 | "dependencies": { 28 | "chalk": "^1.1.3", 29 | "commander": "^2.9.0", 30 | "fuse.js": "^2.4.1", 31 | "ini": "^1.3.4", 32 | "inquirer": "^1.1.2", 33 | "sql.js": "^0.3.2" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.11.4", 37 | "babel-plugin-transform-async-to-generator": "^6.8.0", 38 | "babel-plugin-transform-es2015-modules-commonjs": "^6.11.5" 39 | }, 40 | "babel": { 41 | "plugins": [ 42 | "transform-es2015-modules-commonjs", 43 | "transform-async-to-generator" 44 | ] 45 | } 46 | } 47 | --------------------------------------------------------------------------------