├── .gitignore ├── Makefile ├── README.md ├── chrome.py ├── icon.png ├── info.plist ├── requirements.txt ├── screenshot.png └── sitepackages.py /.gitignore: -------------------------------------------------------------------------------- 1 | History 2 | venv/ 3 | .venv/ 4 | *.alfredworkflow 5 | *.py 6 | *.pyc 7 | !chrome.py 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENV=venv 2 | 3 | all : 4 | @echo "Run \`make workflow\` to create the Alfred workflow file after removing the old one if it exists." 5 | 6 | distclean : 7 | rm -f ./alfred-chrome-history.alfredworkflow 8 | 9 | clean : 10 | rm -f alfred.py 11 | rm -f docopt.py 12 | rm -rf ${VENV} 13 | find . -iname "*.pyc" -delete 14 | 15 | venv : ${VENV}/bin/activate 16 | 17 | venv/bin/activate : requirements.txt 18 | test -d ${VENV} || virtualenv ${VENV} 19 | . ${VENV}/bin/activate 20 | touch ${VENV}/bin/activate 21 | 22 | install : venv 23 | . ${VENV}/bin/activate 24 | pip install -r requirements.txt --upgrade --force-reinstall 25 | 26 | lib : 27 | cp `python sitepackages.py`/alfred.py alfred.py 28 | cp `python sitepackages.py`/docopt.py docopt.py 29 | 30 | dev : install \ 31 | lib 32 | 33 | zip : 34 | zip -r ./alfred-chrome-history.alfredworkflow . -x "*.git*" "*venv*" .gitignore Makefile History requirements.txt README.md sitepackages.py screenshot.png 35 | 36 | workflow : distclean \ 37 | install \ 38 | lib \ 39 | zip 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alfred Chrome History Workflow 2 | 3 | Access your Google Chrome history from Alfred with `ch {query}`. 4 | 5 | ![alfred chrome history workflow](screenshot.png) 6 | 7 | ## How to install 8 | 9 | [Download the workflow from the releases page][releases] and install by double-clicking it. 10 | 11 | [releases]: https://github.com/tupton/alfred-chrome-history/releases 12 | 13 | ### From source 14 | 15 | Clone this repo and symlink it to `/Alfred.alfredpreferences/workflows/alfred-chrome-history`. Your Alfred sync directory can be found going to Preferences → Advanced → Syncing. 16 | 17 | Then run `make dev` to install requirements and set the repository up to be used as a workflow. 18 | 19 | ## Configuration 20 | 21 | The workflow should work out of the box with the `ch` prefix. If you'd like to change this, update the keyword in the Alfred workflow's script filter. 22 | 23 | ### Choosing the correct Google Chrome profile directory 24 | 25 | The Alfred script filter is set up to use the default Chrome profile located in `~/Library/Application Support/Google/Chrome/Default`. If you need to use a different profile, update the `PROFILE` environment variable in the Alfred workflow's script filter. This could be the necessary if you have signed in to Chrome with different or multiple accounts, and usually the profile directory is located in something like `Profile 1`. If that is the case, the entire Script contents in the workflow's script filter would be: 26 | 27 | ```sh 28 | PROFILE="~/Library/Application Support/Google/Chrome/Profile 1" 29 | PATH="env/bin:$PATH" 30 | python chrome.py "${PROFILE}" "{query}" 31 | ``` 32 | 33 | In a terminal, the following command can help you find the exact location of the profile directory that the workflow needs: 34 | 35 | ```sh 36 | ls ~/Library/Application\ Support/Google/Chrome/ | grep Profile 37 | ``` 38 | 39 | ### Disabling favicon support 40 | 41 | By default, the script tries to grab favicons from a separate database. This can sometimes slow down the results, which is not desirable. To turn off favicon support, pass `--no-favicons` in the Alfred workflow's script filter. The last line of the script should look like the following: 42 | 43 | ```sh 44 | python chrome.py "${PROFILE}" "{query}" --no-favicons 45 | ``` 46 | 47 | ## How to build 48 | 49 | `make workflow` will put any dependencies in place and build `alfred-chrome-history.alfredworkflow` in the current directory. 50 | 51 | Note that `sitepackages.py` attempts to find `alfred.py` and copy it into the workflow archive. Please let me know if this script fails to find `alfred.py`. It attempts to find it in both global installations and within a virtualenv, but I have only tested this on my local machine. 52 | 53 | `make dev` also puts dependencies in place so that the workflow can work when symlinked to the correct workflows directory as mentioned above. 54 | 55 | ## Thanks 56 | 57 | This workflow uses the wonderful [alfred-python][ap] library. It is provided in the generated workflow and does not need to be installed globally or otherwise before using this workflow. 58 | 59 | [ap]: https://github.com/nikipore/alfred-python 60 | -------------------------------------------------------------------------------- /chrome.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Get relevant history from the Google Chrome history database based on the given query and build 5 | Alfred items based on the results. 6 | 7 | Usage: 8 | chrome [--no-favicons | --favicons] PROFILE QUERY 9 | chrome (-h | --help) 10 | chrome --version 11 | 12 | The path to the Chrome user profile to get the history database from is given in PROFILE. The query 13 | to search for is given in QUERY. The output is formatted as the Alfred script filter XML output 14 | format. 15 | 16 | PROFILE The path to the Chrome profile whose history database should be searched 17 | QUERY The query to search the history database with 18 | 19 | Options: 20 | --no-favicons Do not return Alfred XML results with favicons [default: false] 21 | --favicons Include favicons in the Alfred XML results [default: true] 22 | """ 23 | 24 | from __future__ import print_function 25 | 26 | import alfred 27 | import sqlite3 28 | import shutil 29 | import os 30 | import sys 31 | import time 32 | import datetime 33 | from docopt import docopt 34 | 35 | __version__ = '0.8.0' 36 | 37 | CACHE_EXPIRY = 60 38 | HISTORY_DB = 'History' 39 | FAVICONS_DB = 'Favicons' 40 | FAVICONS_CACHE = 'Favicons-Cache' 41 | 42 | FAVICON_JOIN = u""" 43 | LEFT OUTER JOIN icon_mapping ON icon_mapping.page_url = urls.url, 44 | favicon_bitmaps ON favicon_bitmaps.id = 45 | (SELECT id FROM favicon_bitmaps 46 | WHERE favicon_bitmaps.icon_id = icon_mapping.icon_id 47 | ORDER BY width DESC LIMIT 1) 48 | """ 49 | FAVICON_SELECT = u""" 50 | , favicon_bitmaps.image_data, favicon_bitmaps.last_updated 51 | """ 52 | 53 | UNIX_EPOCH = datetime.datetime.utcfromtimestamp(0) 54 | WINDOWS_EPOCH = datetime.datetime(1601, 1, 1) 55 | SECONDS_BETWEEN_UNIX_AND_WINDOWS_EPOCH = (UNIX_EPOCH - WINDOWS_EPOCH).total_seconds() 56 | MICROSECS_PER_SEC = 10 ** -6 57 | 58 | class ErrorItem(alfred.Item): 59 | def __init__(self, error): 60 | alfred.Item.__init__(self, {u'valid': u'NO', u'autocomplete': error.message}, error.message, u'Check the workflow log for more information.') 61 | 62 | def alfred_error(error): 63 | alfred.write(alfred.xml([ErrorItem(error)])) 64 | 65 | def copy_db(name, profile): 66 | cache = os.path.join(alfred.work(True), name) 67 | if os.path.isfile(cache) and time.time() - os.path.getmtime(cache) < CACHE_EXPIRY: 68 | return cache 69 | 70 | db_file = os.path.join(os.path.expanduser(profile), name) 71 | try: 72 | shutil.copy(db_file, cache) 73 | except: 74 | raise IOError(u'Unable to copy Google Chrome history database from {}'.format(db_file)) 75 | 76 | return cache 77 | 78 | def history_db(profile, favicons=True): 79 | history = copy_db(HISTORY_DB, profile) 80 | db = sqlite3.connect(history) 81 | if favicons: 82 | favicons = copy_db(FAVICONS_DB, profile) 83 | db.cursor().execute('ATTACH DATABASE ? AS favicons', (favicons,)).close() 84 | return db 85 | 86 | def cache_favicon(image_data, uid, last_updated): 87 | cache_dir = os.path.join(alfred.work(True), FAVICONS_CACHE) 88 | if not os.path.isdir(cache_dir): 89 | os.makedirs(cache_dir) 90 | icon_file = os.path.join(cache_dir, str(uid)) 91 | if not os.path.isfile(icon_file) or last_updated > os.path.getmtime(icon_file): 92 | with open(icon_file, 'w') as f: 93 | f.write(image_data) 94 | os.utime(icon_file, (time.time(), last_updated)) 95 | 96 | return (icon_file, {'type': 'png'}) 97 | 98 | # Chrome measures time in microseconds since the Windows epoch (1601/1/1) 99 | # https://code.google.com/p/chromium/codesearch#chromium/src/base/time/time.h 100 | def convert_chrometime(chrometime): 101 | return (chrometime * MICROSECS_PER_SEC) - SECONDS_BETWEEN_UNIX_AND_WINDOWS_EPOCH 102 | 103 | def get_matching_rows(favicon_select, favicon_join, q): 104 | words = filter(lambda word: len(word.strip()) > 0, q.split()) 105 | criterion = " AND ".join(["(urls.title LIKE ? OR urls.url LIKE ?)" for word in words]) 106 | 107 | query = u""" 108 | SELECT urls.id, urls.title, urls.url {favicon_select} 109 | FROM urls 110 | {favicon_join} 111 | WHERE ({where_clause}) 112 | ORDER BY visit_count DESC, typed_count DESC, last_visit_time DESC 113 | """.format(favicon_select=favicon_select, favicon_join=favicon_join, where_clause=criterion) 114 | word_tuple = tuple([u'%{}%'.format(word) for word in words for i in range(2)]) 115 | return db.execute(query, word_tuple) 116 | 117 | def history_results(db, query, favicons=True): 118 | if favicons: 119 | favicon_select = FAVICON_SELECT 120 | favicon_join = FAVICON_JOIN 121 | else: 122 | favicon_select = '' 123 | favicon_join = '' 124 | for row in get_matching_rows(favicon_select, favicon_join, query): 125 | if favicons: 126 | (uid, title, url, image_data, image_last_updated) = row 127 | icon = cache_favicon(image_data, uid, convert_chrometime(image_last_updated)) if image_data and image_last_updated else None 128 | else: 129 | (uid, title, url) = row 130 | icon = None 131 | 132 | yield alfred.Item({u'uid': alfred.uid(uid), u'arg': url, u'autocomplete': url}, title or url, url, icon) 133 | 134 | if __name__ == '__main__': 135 | arguments = docopt(__doc__, version='Alfred Chrome History {}'.format(__version__)) 136 | favicons = arguments.get('--no-favicons') is False 137 | 138 | profile = unicode(arguments.get('PROFILE'), encoding='utf-8', errors='ignore') 139 | query = unicode(arguments.get('QUERY'), encoding='utf-8', errors='ignore') 140 | 141 | try: 142 | db = history_db(profile, favicons=favicons) 143 | except IOError as e: 144 | alfred_error(e) 145 | sys.exit(-1) 146 | 147 | alfred.write(alfred.xml(history_results(db, query, favicons=favicons))) 148 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tupton/alfred-chrome-history/98bcd99e94a1b9b02ebd701b81bd720f79789387/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.thomasupton.chrome-history 7 | category 8 | Internet 9 | connections 10 | 11 | F5EE6337-C93B-44AE-9DB3-C9C1C204D42F 12 | 13 | 14 | destinationuid 15 | B8584F68-2E3A-4883-AE38-1F31EE5C2657 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | destinationuid 25 | 28DD6FE4-9B8F-404F-858E-31FB8159BDAF 26 | modifiers 27 | 1048576 28 | modifiersubtext 29 | Copy URL to clipboard 30 | vitoclose 31 | 32 | 33 | 34 | 35 | createdby 36 | Thomas Upton 37 | description 38 | Access to URLs in your Google Chrome profile's history 39 | disabled 40 | 41 | name 42 | Google Chrome History 43 | objects 44 | 45 | 46 | config 47 | 48 | concurrently 49 | 50 | escaping 51 | 102 52 | script 53 | open -a "/Applications/Google Chrome.app" "{query}" 54 | scriptargtype 55 | 0 56 | scriptfile 57 | 58 | type 59 | 0 60 | 61 | type 62 | alfred.workflow.action.script 63 | uid 64 | B8584F68-2E3A-4883-AE38-1F31EE5C2657 65 | version 66 | 2 67 | 68 | 69 | config 70 | 71 | alfredfiltersresults 72 | 73 | alfredfiltersresultsmatchmode 74 | 0 75 | argumenttreatemptyqueryasnil 76 | 77 | argumenttrimmode 78 | 0 79 | argumenttype 80 | 0 81 | escaping 82 | 36 83 | keyword 84 | ch 85 | queuedelaycustom 86 | 3 87 | queuedelayimmediatelyinitially 88 | 89 | queuedelaymode 90 | 0 91 | queuemode 92 | 1 93 | runningsubtext 94 | Searching... 95 | script 96 | PROFILE="~/Library/Application Support/Google/Chrome/Default" 97 | PATH="env/bin:$PATH" 98 | python chrome.py "${PROFILE}" "{query}" 99 | scriptargtype 100 | 0 101 | scriptfile 102 | 103 | subtext 104 | Search URLs in your profile's history 105 | title 106 | Google Chrome History 107 | type 108 | 0 109 | withspace 110 | 111 | 112 | type 113 | alfred.workflow.input.scriptfilter 114 | uid 115 | F5EE6337-C93B-44AE-9DB3-C9C1C204D42F 116 | version 117 | 3 118 | 119 | 120 | config 121 | 122 | autopaste 123 | 124 | clipboardtext 125 | {query} 126 | ignoredynamicplaceholders 127 | 128 | transient 129 | 130 | 131 | type 132 | alfred.workflow.output.clipboard 133 | uid 134 | 28DD6FE4-9B8F-404F-858E-31FB8159BDAF 135 | version 136 | 3 137 | 138 | 139 | readme 140 | Access your Google Chrome history from Alfred with ch {query}. 141 | 142 | The workflow should work out of the box with the "ch" prefix. If you’d like to change this, update the keyword in the Alfred workflow’s script filter. 143 | 144 | Choosing the correct Google Chrome profile directory 145 | 146 | The Alfred script filter is set up to use the default Chrome profile located in "~/Library/Application Support/Google/Chrome/Default". If you need to use a different profile, update the "PROFILE" environment variable in the Alfred workflow’s script filter. This could be the necessary if you have signed in to Chrome with different or multiple accounts, and usually the profile directory is located in something like Profile 1. If that is the case, the entire Script contents in the workflow’s script filter would be: 147 | 148 | PROFILE="~/Library/Application Support/Google/Chrome/Profile 1" 149 | PATH="env/bin:$PATH" 150 | python chrome.py "${PROFILE}" "{query}" 151 | In a terminal, the following command can help you find the exact location of the profile directory that the workflow needs: 152 | 153 | ls ~/Library/Application\ Support/Google/Chrome/ | grep Profile 154 | uidata 155 | 156 | 28DD6FE4-9B8F-404F-858E-31FB8159BDAF 157 | 158 | xpos 159 | 700 160 | ypos 161 | 130 162 | 163 | B8584F68-2E3A-4883-AE38-1F31EE5C2657 164 | 165 | xpos 166 | 700 167 | ypos 168 | 10 169 | 170 | F5EE6337-C93B-44AE-9DB3-C9C1C204D42F 171 | 172 | xpos 173 | 300 174 | ypos 175 | 10 176 | 177 | 178 | version 179 | 0.8.0 180 | webaddress 181 | https://github.com/tupton/alfred-chrome-history 182 | 183 | 184 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/nikipore/alfred-python.git 2 | docopt==0.6.2 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tupton/alfred-chrome-history/98bcd99e94a1b9b02ebd701b81bd720f79789387/screenshot.png -------------------------------------------------------------------------------- /sitepackages.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import site 5 | 6 | def is_virtual_env(): 7 | return hasattr(sys, 'real_prefix') 8 | 9 | def virtual_env_site_packages(): 10 | site_packages = [p for p in sys.path if p.endswith('site-packages')] 11 | if len(site_packages) > 0: 12 | return site_packages[0] 13 | 14 | def site_packages(): 15 | site_packages = site.getsitepackages() 16 | if len(site_packages) > 0: 17 | return site_packages[0] 18 | 19 | def get_site_packages(): 20 | if is_virtual_env(): 21 | return virtual_env_site_packages() 22 | else: 23 | return site_packages() 24 | 25 | if __name__ == '__main__': 26 | print('{}'.format(get_site_packages())) 27 | --------------------------------------------------------------------------------