├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── icon.png ├── images ├── alfred-history.sketch ├── screenshot-2.png ├── screenshot-3.png └── screenshot.png ├── info.plist ├── mkworkflow.py └── search_history.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: push 4 | 5 | jobs: 6 | build-and-release: 7 | name: Build Alfred workflow and release 8 | runs-on: macos-latest 9 | steps: 10 | 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Get Workflow Version 15 | run: echo "WF_VERSION=$(plutil -extract version xml1 -o - info.plist | sed -n 's/.*\(.*\)<\/string>.*/\1/p')" >> $GITHUB_ENV 16 | 17 | - name: Create Release 18 | id: create_release 19 | uses: actions/create-release@v1 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | tag_name: ${{ env.WF_VERSION }} 24 | release_name: ${{ env.WF_VERSION }} 25 | draft: false 26 | prerelease: false 27 | 28 | - name: Build Workflow 29 | run: python3 mkworkflow.py 30 | 31 | - name: Get Workflow Filename 32 | run: echo "WF_FILENAME=$(ls *.alfredworkflow)" >> $GITHUB_ENV 33 | 34 | - name: Upload Release Asset 35 | id: upload-release-asset 36 | uses: actions/upload-release-asset@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | upload_url: ${{ steps.create_release.outputs.upload_url }} 41 | asset_path: ./${{ env.WF_FILENAME }} 42 | asset_name: ${{ env.WF_FILENAME }} 43 | asset_content_type: application/zip 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | wfbuild 3 | *.alfredworkflow 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |
5 | Download
7 |
8 | Alfred History Search 9 |

10 | 11 | An [Alfred](https://alfredapp.com) workflow to search through query history, 12 | and execute queries from history. 13 | 14 | ## Usage 15 | 16 | Download the [latest release][1] 17 | 18 | - Requires `python3` on the system 19 | 20 | #### Searching Through Query History 21 | 22 | - Assign hotkey trigger (`⌃R` recommended) 23 | - While using Alfred, press the hotkey. Alfred's query history will show up: 24 | ![](images/screenshot.png) 25 | - Start typing to search through the query history. 26 | ![](images/screenshot-2.png) 27 | - Actioning any of the history entries makes Alfred search that query. 28 | ![](images/screenshot-3.png) 29 | 30 | #### Deleting Query History 31 | 32 | Type `.clear-alfred-query-history` in Alfred. 33 | 34 | 35 | ## Icon Credits 36 | 37 | Icon created by combining icons form [flaticon](https://www.flaticon.com) 38 | made by [Freepik](https://www.flaticon.com/authors/freepik) 39 | and [Pixel Perfect](https://www.flaticon.com/authors/pixel-perfect) 40 | 41 | [1]: https://github.com/mr-pennyworth/alfred-history-search/releases/latest/download/Alfred.History.Search.alfredworkflow 42 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-pennyworth/alfred-history-search/6c9b1969af113f172bad8285f80cbf4dad120643/icon.png -------------------------------------------------------------------------------- /images/alfred-history.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-pennyworth/alfred-history-search/6c9b1969af113f172bad8285f80cbf4dad120643/images/alfred-history.sketch -------------------------------------------------------------------------------- /images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-pennyworth/alfred-history-search/6c9b1969af113f172bad8285f80cbf4dad120643/images/screenshot-2.png -------------------------------------------------------------------------------- /images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-pennyworth/alfred-history-search/6c9b1969af113f172bad8285f80cbf4dad120643/images/screenshot-3.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-pennyworth/alfred-history-search/6c9b1969af113f172bad8285f80cbf4dad120643/images/screenshot.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | mr.pennyworth.AlfredHistorySearch 7 | category 8 | Tools 9 | connections 10 | 11 | 85A59381-6764-4FB2-85B6-A19FA66C195D 12 | 13 | 14 | destinationuid 15 | A6F1147F-E11B-4D6A-8929-E010779C231A 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | A6F1147F-E11B-4D6A-8929-E010779C231A 25 | 26 | 27 | destinationuid 28 | A8B5C998-33A0-46F9-8B79-7CCA3CE9070F 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 38 | createdby 39 | Mr. Pennyworth 40 | description 41 | Search through Alfred's query history 42 | disabled 43 | 44 | name 45 | Alfred History Search 46 | objects 47 | 48 | 49 | config 50 | 51 | alfredfiltersresults 52 | 53 | alfredfiltersresultsmatchmode 54 | 2 55 | argumenttreatemptyqueryasnil 56 | 57 | argumenttrimmode 58 | 0 59 | argumenttype 60 | 1 61 | escaping 62 | 102 63 | queuedelaycustom 64 | 3 65 | queuedelayimmediatelyinitially 66 | 67 | queuedelaymode 68 | 0 69 | queuemode 70 | 1 71 | runningsubtext 72 | Loading... 73 | script 74 | ./search_history.py 75 | scriptargtype 76 | 1 77 | scriptfile 78 | 79 | subtext 80 | 81 | title 82 | Search Alfred's Query History 83 | type 84 | 0 85 | withspace 86 | 87 | 88 | type 89 | alfred.workflow.input.scriptfilter 90 | uid 91 | A6F1147F-E11B-4D6A-8929-E010779C231A 92 | version 93 | 3 94 | 95 | 96 | config 97 | 98 | alfredfiltersresults 99 | 100 | alfredfiltersresultsmatchmode 101 | 0 102 | argumenttreatemptyqueryasnil 103 | 104 | argumenttrimmode 105 | 0 106 | argumenttype 107 | 2 108 | escaping 109 | 102 110 | keyword 111 | .clear-alfred-query-history 112 | queuedelaycustom 113 | 3 114 | queuedelayimmediatelyinitially 115 | 116 | queuedelaymode 117 | 0 118 | queuemode 119 | 1 120 | runningsubtext 121 | 122 | script 123 | rm "$alfred_workflow_data/query-history.txt" 124 | touch "$alfred_workflow_data/query-history.txt" 125 | 126 | scriptargtype 127 | 1 128 | scriptfile 129 | 130 | subtext 131 | .clear-alfred-query-history 132 | title 133 | Clear Alfred Query History 134 | type 135 | 0 136 | withspace 137 | 138 | 139 | type 140 | alfred.workflow.input.scriptfilter 141 | uid 142 | 0DEE19E2-B697-463E-84C7-C0215ACB9317 143 | version 144 | 3 145 | 146 | 147 | config 148 | 149 | action 150 | 0 151 | argument 152 | 0 153 | focusedappvariable 154 | 155 | focusedappvariablename 156 | 157 | hotkey 158 | 31 159 | hotmod 160 | 262144 161 | hotstring 162 | R 163 | leftcursor 164 | 165 | modsmode 166 | 0 167 | relatedApps 168 | 169 | com.runningwithcrayons.Alfred 170 | 171 | relatedAppsMode 172 | 1 173 | 174 | type 175 | alfred.workflow.trigger.hotkey 176 | uid 177 | 85A59381-6764-4FB2-85B6-A19FA66C195D 178 | version 179 | 2 180 | 181 | 182 | config 183 | 184 | applescript 185 | on alfred_script(q) 186 | tell application id "com.runningwithcrayons.Alfred" to search q 187 | end alfred_script 188 | cachescript 189 | 190 | 191 | type 192 | alfred.workflow.action.applescript 193 | uid 194 | A8B5C998-33A0-46F9-8B79-7CCA3CE9070F 195 | version 196 | 1 197 | 198 | 199 | readme 200 | # Alfred History Search 201 | An [Alfred](https://alfredapp.com) workflow to search through query history, 202 | and execute queries from history. 203 | 204 | ## Usage 205 | 206 | Download the [latest release][1] 207 | 208 | - Requires `python3` on the system 209 | 210 | #### Searching Through Query History 211 | 212 | - Assign hotkey trigger (`⌃R` recommended) 213 | - While using Alfred, press the hotkey. Alfred's query history will show up: 214 | ![](images/screenshot.png) 215 | - Start typing to search through the query history. 216 | ![](images/screenshot-2.png) 217 | - Actioning any of the history entries makes Alfred search that query. 218 | ![](images/screenshot-3.png) 219 | 220 | #### Deleting Query History 221 | 222 | Type `.clear-alfred-query-history` in Alfred. 223 | 224 | 225 | ## Icon Credits 226 | 227 | Icon created by combining icons form [flaticon](https://www.flaticon.com) 228 | made by [Freepik](https://www.flaticon.com/authors/freepik) 229 | and [Pixel Perfect](https://www.flaticon.com/authors/pixel-perfect) 230 | 231 | [1]: https://github.com/mr-pennyworth/alfred-history-search/releases/latest/download/Alfred.History.Search.alfredworkflow 232 | 233 | uidata 234 | 235 | 0DEE19E2-B697-463E-84C7-C0215ACB9317 236 | 237 | xpos 238 | 470 239 | ypos 240 | 10 241 | 242 | 85A59381-6764-4FB2-85B6-A19FA66C195D 243 | 244 | xpos 245 | 10 246 | ypos 247 | 10 248 | 249 | A6F1147F-E11B-4D6A-8929-E010779C231A 250 | 251 | xpos 252 | 155 253 | ypos 254 | 10 255 | 256 | A8B5C998-33A0-46F9-8B79-7CCA3CE9070F 257 | 258 | xpos 259 | 315 260 | ypos 261 | 10 262 | 263 | 264 | variablesdontexport 265 | 266 | version 267 | 0.0.5 268 | webaddress 269 | https://github.com/mr-pennyworth/alfred-history-search#readme 270 | 271 | 272 | -------------------------------------------------------------------------------- /mkworkflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import plistlib 5 | import shutil 6 | import subprocess 7 | import sys 8 | 9 | from contextlib import contextmanager 10 | from uuid import uuid4 11 | 12 | 13 | START_LETTERS = ( 14 | 'abcdefghijklmnopqrstuvwxyz' 15 | '0123456789' 16 | "._'`," 17 | ) 18 | 19 | SCRIPT_DIR = sys.path[0] 20 | BUILD_DIR = 'wfbuild' 21 | WF_FILES = [ 22 | 'icon.png', 23 | 'info.plist', 24 | 'README.md', 25 | 'search_history.py', 26 | ] 27 | 28 | 29 | def copy(filenames, dest_folder): 30 | if os.path.exists(dest_folder): 31 | shutil.rmtree(dest_folder) 32 | os.makedirs(dest_folder) 33 | 34 | for filename in filenames: 35 | if os.path.isdir(filename): 36 | shutil.copytree(filename, f'{dest_folder}/{filename}') 37 | else: 38 | shutil.copy(filename, f'{dest_folder}/{filename}') 39 | 40 | 41 | def plistRead(path): 42 | with open(path, 'rb') as f: 43 | return plistlib.load(f) 44 | 45 | 46 | def plistWrite(obj, path): 47 | with open(path, 'wb') as f: 48 | return plistlib.dump(obj, f) 49 | 50 | 51 | def fileContents(filepath): 52 | with open(filepath) as f: 53 | return f.read() 54 | 55 | 56 | @contextmanager 57 | def cwd(dir): 58 | old_wd = os.path.abspath(os.curdir) 59 | os.chdir(dir) 60 | yield 61 | os.chdir(old_wd) 62 | 63 | 64 | def make_query_logger_object(starting_letter): 65 | return { 66 | "uid": f'{uuid4()}'.upper(), 67 | "config": { 68 | "keyword": starting_letter, 69 | "script": f''' 70 | query="{starting_letter}$1" 71 | 72 | mkdir -p "$alfred_workflow_data" 73 | echo "$(date +%s) $query" >> "$alfred_workflow_data/query-history.txt" 74 | ''', 75 | "alfredfiltersresults": False, 76 | "alfredfiltersresultsmatchmode": 0, 77 | "argumenttreatemptyqueryasnil": True, 78 | "argumenttrimmode": 0, 79 | "argumenttype": 0, 80 | "escaping": 102, 81 | "queuedelaycustom": 3, 82 | "queuedelayimmediatelyinitially": True, 83 | "queuedelaymode": 0, 84 | "queuemode": 1, 85 | "runningsubtext": "", 86 | "scriptargtype": 1, 87 | "scriptfile": "", 88 | "subtext": "", 89 | "title": "", 90 | "type": 0, 91 | "withspace": False 92 | }, 93 | "type": "alfred.workflow.input.scriptfilter", 94 | "version": 3, 95 | } 96 | 97 | 98 | def make_export_ready(plist_path): 99 | wf = plistRead(plist_path) 100 | 101 | # remove noexport vars 102 | wf['variablesdontexport'] = [] 103 | 104 | # remove noexport objects 105 | noexport_uids = [ 106 | uid 107 | for uid, data 108 | in wf['uidata'].items() 109 | if 'noexport' in data 110 | ] 111 | 112 | num_columns = 6 113 | for i, start_letter in enumerate(START_LETTERS): 114 | x = (i % num_columns) * 150 115 | y = 150 + (i // num_columns) * 120 116 | new_filter_obj = make_query_logger_object(start_letter) 117 | wf['objects'].append(new_filter_obj) 118 | wf['uidata'][new_filter_obj['uid']] = {'xpos': x, 'ypos': y} 119 | 120 | # add readme 121 | with open('README.md') as f: 122 | wf['readme'] = f.read() 123 | 124 | plistWrite(wf, plist_path) 125 | return wf['name'] 126 | 127 | 128 | if __name__ == '__main__': 129 | copy(WF_FILES, BUILD_DIR) 130 | wf_name = make_export_ready(f'{BUILD_DIR}/info.plist') 131 | with cwd(BUILD_DIR): 132 | subprocess.call(['zip', '-r', f'../{wf_name}.alfredworkflow'] + WF_FILES) 133 | -------------------------------------------------------------------------------- /search_history.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import logging 5 | import os 6 | import re 7 | import sys 8 | 9 | from pathlib import Path 10 | from collections import defaultdict 11 | from datetime import datetime 12 | 13 | 14 | WF_DATA_DIR = Path(os.environ['alfred_workflow_data']) 15 | WF_DATA_DIR.mkdir(parents=True, exist_ok=True) 16 | HIST_PATH = Path(WF_DATA_DIR) / 'query-history.txt' 17 | HIST_PATH.touch() 18 | 19 | 20 | class Trie: 21 | def __init__(self, root_data=None): 22 | self.root_data = root_data 23 | self.children = defaultdict(Trie) 24 | 25 | def insert(self, key, data, key_idx=0): 26 | if key_idx == len(key) - 1: 27 | self.root_data = data 28 | else: 29 | self.children[key[key_idx]].insert(key, data, key_idx + 1) 30 | 31 | def _collect_leaves(self, accumulator=None): 32 | if accumulator is None: 33 | accumulator = [] 34 | if len(self.children) == 0 and self.root_data is not None: 35 | accumulator.append(self.root_data) 36 | else: 37 | for _, child in self.children.items(): 38 | child._collect_leaves(accumulator) 39 | 40 | return accumulator 41 | 42 | def leaves(self): 43 | return self._collect_leaves() 44 | 45 | 46 | def lines(filepath): 47 | with open(filepath, 'r') as f: 48 | return f.readlines() 49 | 50 | 51 | def get_timestamps_and_queries(): 52 | trie = Trie() 53 | regex = r'(\d+) (.*)' 54 | for linum, line in enumerate(lines(HIST_PATH)): 55 | try: 56 | timestamp, query = re.findall(regex, line)[0] 57 | trie.insert(query, (int(timestamp), query)) 58 | except Exception as e: 59 | logging.error(f"{linum}: {line} (len: {len(line)})\n") 60 | logging.exception(e) 61 | return sorted(trie.leaves(), reverse=True) 62 | 63 | 64 | def write_consolidated_query_log(timestamps_and_queries): 65 | hist = '\n'.join([ 66 | f"{ts} {q}" 67 | for ts, q in timestamps_and_queries 68 | ]) 69 | with open(HIST_PATH, 'w') as histfile: 70 | histfile.write(hist) 71 | 72 | 73 | def make_alfred_json(timestamps_and_queries): 74 | alfred_json = {'items': []} 75 | for timestamp, query in timestamps_and_queries: 76 | alfred_json['items'].append({ 77 | 'arg': query, 78 | 'title': query, 79 | 'autocomplete': query, 80 | 'subtitle': datetime.fromtimestamp(timestamp).strftime("%d %b %Y %H:%M:%S") 81 | }) 82 | return alfred_json 83 | 84 | 85 | if __name__ == '__main__': 86 | ts_n_qs = get_timestamps_and_queries() 87 | write_consolidated_query_log(ts_n_qs) 88 | json.dump(make_alfred_json(ts_n_qs), sys.stdout, indent=2) 89 | sys.stdout.flush() 90 | --------------------------------------------------------------------------------