├── .gitignore
├── Chromium Bookmarks and History Search.alfredworkflow
├── README.md
└── src
├── Alfred3.py
├── Favicon.py
├── actions.py
├── chrom_bookmarks.py
├── chrom_history.py
├── domain.py
├── icon.png
├── icons
├── clipboard.png
├── domain.png
└── openin.png
├── info.plist
└── py3.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 | .DS_Store
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # Jupyter Notebook
74 | .ipynb_checkpoints
75 |
76 | # pyenv
77 | .python-version
78 |
79 | # celery beat schedule file
80 | celerybeat-schedule
81 |
82 | # SageMath parsed files
83 | *.sage.py
84 |
85 | # Environments
86 | .env
87 | .venv
88 | env/
89 | venv/
90 | ENV/
91 | env.bak/
92 | venv.bak/
93 |
94 | # .vscode
95 | .vscode/*
96 | !.vscode/settings.json
97 | !.vscode/tasks.json
98 | !.vscode/launch.json
99 | !.vscode/extensions.json
100 | *.code-workspace
101 |
102 | # Spyder project settings
103 | .spyderproject
104 | .spyproject
105 |
106 | # Rope project settings
107 | .ropeproject
108 |
109 | # mkdocs documentation
110 | /site
111 |
112 | # mypy
113 | .mypy_cache/
114 |
--------------------------------------------------------------------------------
/Chromium Bookmarks and History Search.alfredworkflow:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/8ca285e67c6769e9ed8313ab45d99c63ee8c00cb/Chromium Bookmarks and History Search.alfredworkflow
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Browser History and Bookmarks Search
2 |
3 | The Workflow searches History and Bookmarks of the configured Browsers simulatiously.
4 |
5 | ## Supported Browsers
6 |
7 | - Chromium
8 | - Google Chrome
9 | - Brave and Brave beta (Chromium)
10 | - MS Edge
11 | - Vivaldi
12 | - Opera
13 | - Sidekick
14 | - Arc
15 | - Safari
16 |
17 | ## Requires
18 |
19 | * Python 3
20 | * Alfred 5
21 |
22 | ## Usage
23 |
24 | ### History
25 |
26 |
27 | Search History with keyword: `bh`
28 |
29 | Type `&` in between of the search terms to search for multiple entries e.g.:
30 | `Car&Bike` match entries with `Car or Bike rental` but NOT `Car driving school`
31 |
32 | ### Bookmarks
33 |
34 | Search Bookmarks with keyword: `bm`
35 |
36 | ### Other Actions
37 |
38 | Pressing `CMD` to enter `Other Actions...`:
39 |
40 | * `Copy to Clipboard`: Copies the URL into the Clipboard
41 | * `Open Domain`: Opens the domain (e.g. www.google.com) in default Browser
42 | * `Open In...`: Opens the URL with the Alfred's build in Open-In other Browser
43 |
--------------------------------------------------------------------------------
/src/Alfred3.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import json
4 | import os
5 | import sys
6 | import time
7 | from plistlib import dump, load
8 | from urllib.parse import urlparse
9 |
10 | """
11 | Alfred Script Filter generator class
12 | Version 4.2
13 | Python 3 required!
14 | """
15 |
16 |
17 | class Items(object):
18 | """
19 | Alfred WF Items object to generate Script Filter object
20 |
21 | Returns:
22 |
23 | object: WF object
24 | """
25 |
26 | def __init__(self):
27 | self.item = {}
28 | self.items = []
29 | self.mods = {}
30 |
31 | def getItemsLengths(self) -> int:
32 | """
33 | Get amount of items in object
34 |
35 | Returns:
36 |
37 | int: Number of items
38 |
39 | """
40 | return len(self.items)
41 |
42 | def setKv(self, key: str, value: str) -> None:
43 | """
44 | Set a key value pair to item
45 |
46 | Args:
47 |
48 | key (str): Name of the Key
49 | value (str): Value of the Key
50 | """
51 | self.item.update({key: value})
52 |
53 | def addItem(self) -> None:
54 | """
55 | Add/commits an item to the Script Filter Object
56 |
57 | Note: addItem needs to be called after setItem, addMod, setIcon
58 | """
59 | self.addModsToItem()
60 | self.items.append(self.item)
61 | self.item = {}
62 | self.mods = {}
63 |
64 | def setItem(self, **kwargs: str) -> None:
65 | """
66 | Add multiple key values to define an item
67 |
68 | Note: addItem needs to be called to submit a Script Filter item
69 | to the Script Filter object
70 |
71 | Args:
72 |
73 | kwargs (kwargs): title,subtitle,arg,valid,quicklookurl,uid,automcomplete,type
74 | """
75 | for key, value in kwargs.items():
76 | self.setKv(key, value)
77 |
78 | def getItem(self, d_type: str = "") -> str:
79 | """
80 | Get current item definition for validation
81 |
82 | Args:
83 |
84 | d_type (str, optional): defines returned object format "JSON" if it needs to be readable . Defaults to "".
85 |
86 | Returns:
87 |
88 | str: JSON represenation of an item
89 | """
90 | if d_type == "":
91 | return self.item
92 | else:
93 | return json.dumps(self.item, default=str, indent=4)
94 |
95 | def getItems(self, response_type: str = "json") -> json:
96 | """
97 | get the final items data for which represents the script filter output
98 |
99 | Args:
100 |
101 | response_type (str, optional): "dict"|"json". Defaults to "json".
102 |
103 | Raises:
104 |
105 | ValueError: If key is not "dict"|"json"
106 |
107 | Returns:
108 |
109 | str: returns the item representing script filter output
110 | """
111 | valid_keys = {"json", "dict"}
112 | if response_type not in valid_keys:
113 | raise ValueError(f"Type must be in: {valid_keys}")
114 | the_items = dict()
115 | the_items.update({"items": self.items})
116 | if response_type == "dict":
117 | return the_items
118 | elif response_type == "json":
119 | return json.dumps(the_items, default=str, indent=4)
120 |
121 | def setIcon(self, m_path: str, m_type: str = "") -> None:
122 | """
123 | Set the icon of an item.
124 | Needs to be called before addItem!
125 |
126 | Args:
127 |
128 | m_path (str): Path to the icon
129 | m_type (str, optional): "icon"|"fileicon". Defaults to "".
130 | """
131 | self.setKv("icon", self.__define_icon(m_path, m_type))
132 |
133 | def __define_icon(self, path: str, m_type: str = "") -> dict:
134 | """
135 | Private method to create icon set
136 |
137 | Args:
138 |
139 | path (str): Path to the icon file
140 |
141 | m_type (str, optional): "image"|"fileicon". Defaults to "".
142 |
143 | Returns:
144 |
145 | dict: icon and type
146 | """
147 | icon = {}
148 | if m_type != "":
149 | icon.update({"type": m_type})
150 | icon.update({"path": path})
151 | return icon
152 |
153 | def addMod(
154 | self,
155 | key: str,
156 | arg: str,
157 | subtitle: str,
158 | valid: bool = True,
159 | icon_path: str = "",
160 | icon_type: str = "",
161 | ) -> None:
162 | """
163 | Add a mod to an item
164 |
165 | Args:
166 |
167 | key (str): "alt"|"cmd"|"shift"|"fn"|"ctrl
168 | arg (str): Value of Mod arg
169 | subtitle (str): Subtitle
170 | valid (bool, optional): Arg valid or not. Defaults to True.
171 | icon_path (str, optional): Path to the icon relative to WF dir. Defaults to "".
172 | icon_type (str, optional): "image"|"fileicon". Defaults to "".
173 |
174 | Raises:
175 |
176 | ValueError: if key is not in list
177 | """
178 | valid_keys = {"alt", "cmd", "shift", "ctrl", "fn"}
179 | if key not in valid_keys:
180 | raise ValueError(f"Key must be in: {valid_keys}")
181 | mod = {}
182 | mod.update({"arg": arg})
183 | mod.update({"subtitle": subtitle})
184 | mod.update({"valid": valid})
185 | if icon_path != "":
186 | the_icon = self.__define_icon(icon_path, icon_type)
187 | mod.update({"icon": the_icon})
188 | self.mods.update({key: mod})
189 |
190 | def addModsToItem(self) -> None:
191 | """
192 | Adds mod to an item
193 | """
194 | if bool(self.mods):
195 | self.setKv("mods", self.mods)
196 | self.mods = dict()
197 |
198 | def updateItem(self, id: int, key: str, value: str) -> None:
199 | """
200 | Update an Alfred script filter item key with a new value
201 |
202 | Args:
203 |
204 | id (int): list indes
205 | key (str): key which needs to be updated
206 | value (str): new value
207 | """
208 | dict_item = self.items[id]
209 | kv = dict_item[key]
210 | dict_item[key] = kv + value
211 | self.items[id] = dict_item
212 |
213 | def write(self, response_type: str = "json") -> None:
214 | """
215 | Generate Script Filter Output and write back to stdout
216 |
217 | Args:
218 |
219 | response_type (str, optional): json or dict as output format. Defaults to 'json'.
220 | """
221 | output = self.getItems(response_type=response_type)
222 | sys.stdout.write(output)
223 |
224 |
225 | class Tools(object):
226 | """
227 | Alfred Tools, helpful methos when dealing with Scripts in Alfred
228 |
229 | Args:
230 |
231 | object (obj): Object class
232 | """
233 | @staticmethod
234 | def logPyVersion() -> None:
235 | """
236 | Log Python Version to shell
237 | """
238 | Tools.log("PYTHON VERSION:", sys.version)
239 |
240 | @staticmethod
241 | def log(*message) -> None:
242 | """
243 | Log message to stderr
244 | """
245 | sys.stderr.write(f'{" ".join(message)}\n')
246 |
247 | @staticmethod
248 | def getEnv(var: str, default: str = str()) -> str:
249 | """
250 | Reads environment variable
251 |
252 | Args:
253 |
254 | var (string}: Variable name
255 | default (string, optional): fallback if None
256 |
257 | Returns:
258 |
259 | (str): Env value or string if not available
260 | """
261 | return os.getenv(var) if os.getenv(var) is not None else default
262 |
263 | @staticmethod
264 | def getEnvBool(var: str, default: bool = False) -> bool:
265 | """
266 | Reads boolean env variable provided as text.
267 | 0 will be treated as False
268 | >1 will be treated as True
269 |
270 | Args:
271 |
272 | var (str): Name of the env variable
273 | default (bool, optional): Default if not found. Defaults to False.
274 |
275 | Returns:
276 |
277 | bool: True or False as bool
278 | """
279 | try:
280 | if os.getenv(var).isdigit():
281 | if os.getenv(var) == '0':
282 | return False
283 | else:
284 | return True
285 | if os.getenv(var).lower() == "true":
286 | return True
287 | else:
288 | return default
289 | except AttributeError as e:
290 | sys.exit(f'ERROR: Alfred Environment "{var}" Variable not found!')
291 |
292 | @staticmethod
293 | def getArgv(i: int, default=str()) -> str:
294 | """
295 | Get argument values from input in Alfred or empty if not available
296 |
297 | Args:
298 |
299 | i (int): index of argument
300 | default (string, optional): Fallback if None, default string
301 |
302 | Returns:
303 |
304 | response_type (str) -- argv string or None
305 | """
306 | try:
307 | return sys.argv[i]
308 | except IndexError:
309 | return default
310 | pass
311 |
312 | @staticmethod
313 | def getDateStr(float_time: float, format: str = "%d.%m.%Y") -> str:
314 | """
315 | Format float time to string
316 |
317 | Args:
318 |
319 | float_time (float): Time in float
320 |
321 | format (str, optional): format string. Defaults to '%d.%m.%Y'.
322 |
323 | Returns:
324 |
325 | str: Formatted Date String
326 | """
327 | time_struct = time.gmtime(float_time)
328 | return time.strftime(format, time_struct)
329 |
330 | @staticmethod
331 | def getDateEpoch(float_time: float) -> str:
332 | return time.strftime("%d.%m.%Y", time.gmtime(float_time / 1000))
333 |
334 | @staticmethod
335 | def sortListDict(list_dict: list, key: str, reverse: bool = True) -> list:
336 | """
337 | Sort List with Dictionary based on given key in Dict
338 |
339 | Args:
340 |
341 | list_dict (list(dict)): List which contains unsorted dictionaries
342 |
343 | key (str): name of the key of the dict
344 |
345 | reverse (bool, optional): Reverse order. Defaults to True.
346 |
347 | Returns:
348 |
349 | list(dict): sorted list of dictionaries
350 | """
351 | return sorted(list_dict, key=lambda k: k[key], reverse=reverse)
352 |
353 | @staticmethod
354 | def sortListTuple(list_tuple: list, el: int, reverse: bool = True) -> list:
355 | """
356 | Sort List with Tubles based on a given element in Tuple
357 |
358 | Args:
359 |
360 | list_tuple (list(tuble)): Sort List with Tubles based on a given element in Tuple
361 | el (int): which element
362 | reverse (bool, optional): Reverse order. Defaults to True.
363 |
364 | Returns:
365 |
366 | list(tuble) -- sorted list with tubles
367 | """
368 | return sorted(list_tuple, key=lambda tup: tup[el], reverse=reverse)
369 |
370 | @staticmethod
371 | def notify(title: str, text: str) -> None:
372 | """
373 | Send Notification to mac Notification Center
374 |
375 | Arguments:
376 |
377 | title (str): Title String
378 | text (str): The message
379 | """
380 | os.system(
381 | f"""
382 | osascript -e 'display notification "{text}" with title "{title}"'
383 | """
384 | )
385 |
386 | @staticmethod
387 | def strJoin(*args: str) -> str:
388 | """Joins a list of strings
389 |
390 | Arguments:
391 |
392 | *args (list): List which contains strings
393 |
394 | Returns:
395 |
396 | str: joined str
397 | """
398 | return str().join(args)
399 |
400 | @staticmethod
401 | def chop(theString: str, ext: str) -> str:
402 | """
403 | Cuts a string from the end and return the remaining
404 |
405 | Args:
406 |
407 | theString (str): The String to cut
408 | ext (str): String which needs to be removed
409 |
410 | Returns:
411 |
412 | str: chopped string
413 | """
414 | if theString.endswith(ext):
415 | return theString[: -len(ext)]
416 | return theString
417 |
418 | @staticmethod
419 | def getEnvironment() -> dict:
420 | """
421 | Get all environment variablse as a dict
422 |
423 | Returns:
424 |
425 | dict: Dict with env variables e.g. {"env1": "value"}
426 | """
427 | environment = os.environ
428 | env_dict = dict()
429 | for k, v in environment.iteritems():
430 | env_dict.update({k: v})
431 | return env_dict
432 |
433 | @staticmethod
434 | def getDataDir() -> str:
435 | """
436 | Get Alfred Data Directory
437 |
438 | Returns:
439 |
440 | str: Path to Alfred's data directory
441 |
442 | """
443 | data_dir = os.getenv("alfred_workflow_data")
444 | if not (os.path.isdir(data_dir)):
445 | os.mkdir(data_dir)
446 | return data_dir
447 |
448 | @staticmethod
449 | def getCacheDir() -> str:
450 | """
451 | Get Alfreds Cache Directory
452 |
453 | Returns:
454 |
455 | str: path to Alfred's cache directory
456 |
457 | """
458 | cache_dir = os.getenv("alfred_workflow_cache")
459 | if not (os.path.isdir(cache_dir)):
460 | os.mkdir(cache_dir)
461 | return cache_dir
462 |
463 | @staticmethod
464 | def formatUrl(url: str) -> str:
465 | """
466 | Format a given string into URL format
467 |
468 | Args:
469 |
470 | url (str): string
471 |
472 |
473 | Returns:
474 |
475 | str: URL string
476 |
477 | """
478 | if not (url.startswith("http://")) and not (url.startswith("https://")):
479 | url = f"https://{url}"
480 | return url
481 |
482 | @staticmethod
483 | def getDomain(url: str) -> str:
484 | """
485 | Get Domain of an URL
486 |
487 | Args:
488 |
489 | url (str): string
490 |
491 |
492 | Returns:
493 |
494 | str: URL string
495 |
496 | """
497 | url = Tools.formatUrl(url)
498 | p = urlparse(url=url)
499 | return f"{p.scheme}://{p.netloc}"
500 |
501 |
502 | class Plist:
503 | """
504 | Plist handling class
505 |
506 | Returns:
507 |
508 | object: A plist object
509 |
510 |
511 | """
512 |
513 | def __init__(self):
514 | # Read info.plist into a standard Python dictionary
515 | with open("info.plist", "rb") as fp:
516 | self.info = load(fp)
517 |
518 | def getConfig(self) -> str:
519 | return self.info["variables"]
520 |
521 | def getVariable(self, variable: str) -> str:
522 | """
523 | Get Plist variable with name
524 |
525 | Args:
526 |
527 | variable (str): Name of the variable
528 |
529 | Returns:
530 |
531 | str: Value of variable with name
532 |
533 | """
534 | try:
535 | return self.info["variables"][variable]
536 | except KeyError:
537 | pass
538 |
539 | def setVariable(self, variable: str, value: str) -> None:
540 | """
541 | Set a Plist variable
542 |
543 | Args:
544 |
545 | variable (str): Name of Plist Variable
546 | value (str): Value of Plist Variable
547 |
548 | """
549 | # Set a variable
550 | self.info["variables"][variable] = value
551 | self._saveChanges()
552 |
553 | def deleteVariable(self, variable: str) -> None:
554 | """
555 | Delete a Plist variable with name
556 |
557 | Args:
558 |
559 | variable (str): Name of the Plist variable
560 |
561 | """
562 | try:
563 | del self.info["variables"][variable]
564 | self._saveChanges()
565 | except KeyError:
566 | pass
567 |
568 | def _saveChanges(self) -> None:
569 | """
570 | Save changes to Plist
571 | """
572 | with open("info.plist", "wb") as fp:
573 | dump(self.info, fp)
574 |
575 |
576 | class Keys(object):
577 | CMD = u'\u2318'
578 | SHIFT = u'\u21E7'
579 | ENTER = u'\u23CE'
580 | ARROW_RIGHT = u'\u2192'
581 |
582 |
583 | class AlfJson(object):
584 |
585 | def __init__(self) -> None:
586 | self.arg: dict = dict()
587 | self.config: dict = dict()
588 | self.variables: dict = dict()
589 |
590 | def add_args(self, d) -> None:
591 | """
592 | Add arg dictionary
593 |
594 | Args:
595 |
596 | d (dict): Key-Value pairs of args
597 |
598 | """
599 | self.arg.update(d)
600 |
601 | def add_configs(self, d) -> None:
602 | """
603 | Add config dictionary
604 |
605 | Args:
606 |
607 | d (dict): Key-Value pairs of configs
608 |
609 | """
610 | self.config.update(d)
611 |
612 | def add_variables(self, d) -> None:
613 | """
614 | Add variables dictionary
615 |
616 | Args:
617 |
618 | d (dict): Key-Value pairs of variables
619 |
620 | """
621 | self.variables.update(d)
622 |
623 | def write_json(self) -> None:
624 | """
625 | Write Alfred JSON config object to std out
626 | """
627 | out = {"alfredworkflow": {"arg": self.arg, "config": self.config, "variables": self.variables}}
628 | sys.stdout.write(json.dumps(out))
629 |
--------------------------------------------------------------------------------
/src/Favicon.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import os
3 | import time
4 | import urllib.request
5 | from urllib.parse import urlparse
6 |
7 | from Alfred3 import Tools
8 |
9 |
10 | class Icons(object):
11 | """
12 | Heat favicon cache and provide fiepath to cached png file
13 |
14 | Args:
15 |
16 | object (obj): -
17 |
18 | """
19 |
20 | def __init__(self, histories: list) -> None:
21 | """
22 | Heat cache of favicon files
23 |
24 | Args:
25 |
26 | histories (list): Hiosty object with URL, NAME, addtional.
27 |
28 | """
29 | self.wf_cache_dir = Tools.getCacheDir()
30 | self.histories = histories
31 | self._cache_controller()
32 |
33 | def get_favion_path(self, url: str) -> str:
34 | """
35 | Returns fav ico image (PNG) file path
36 |
37 | Args:
38 | url (str): The URL
39 |
40 | Returns:
41 | str: Full path to img (PNG) file
42 | """
43 | netloc = urlparse(url).netloc
44 | img = os.path.join(self.wf_cache_dir, f"{netloc}.png")
45 | if not (os.path.exists(img)):
46 | img = None
47 | if img and os.path.getsize(img) == 0:
48 | os.remove(img)
49 | img = None
50 | return img
51 |
52 | def _cache_favicon(self, netloc: str) -> None:
53 | """
54 | Download favicon from domain and save in wf cache directory
55 |
56 | Args:
57 | netloc (str): Network location e.g. http://www.google.com = www.google.com
58 | """
59 | if len(netloc) > 0:
60 | url = f"https://www.google.com/s2/favicons?domain={netloc}&sz=128"
61 | img = os.path.join(self.wf_cache_dir, f"{netloc}.png")
62 | os.path.exists(img) and self._cleanup_img_cache(60, img)
63 | if not (os.path.exists(img)):
64 | req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
65 | with open(img, "wb") as f:
66 | try:
67 | with urllib.request.urlopen(req) as r:
68 | f.write(r.read())
69 | except urllib.error.HTTPError as e:
70 | os.path.exists(img) and os.remove(img)
71 |
72 | def _cache_controller(self) -> None:
73 | """
74 | Cache Controller to heat up cache and invalidation
75 |
76 | Args:
77 | histories (list): List with history entries
78 | """
79 | domains = [urlparse(i[0]).netloc for i in self.histories]
80 | pool = multiprocessing.Pool()
81 | pool.map(self._cache_favicon, domains)
82 |
83 | def _cleanup_img_cache(self, number_of_days: int, f_path: str) -> None:
84 | """
85 | Delete cached image after specific amount of days
86 |
87 | Args:
88 | number_of_days (int): Numer of days back in history
89 | f_path (str): path to file
90 | """
91 | now = time.time()
92 | old = now - number_of_days * 24 * 60 * 60
93 | stats = os.stat(f_path)
94 | c_time = stats.st_ctime
95 | if c_time < old and os.path.isfile(f_path):
96 | os.remove(f_path)
97 |
--------------------------------------------------------------------------------
/src/actions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | from urllib.parse import urlparse
4 |
5 | from Alfred3 import Items, Tools
6 |
7 | url = Tools.getEnv('url')
8 | domain = Tools.getDomain(url)
9 |
10 | # Script Filter item [Title,Subtitle,arg/uid/icon]
11 | wf_items = [
12 | ['Copy to Clipboard', 'Copy URL to Clipboard', 'clipboard'],
13 | ['Open Domain', f'Open {domain}', 'domain'],
14 | ['Open URL in...', 'Open URL in another Browser', 'openin'],
15 | ]
16 |
17 | # Create WF script filter output object and emit
18 | wf = Items()
19 | for w in wf_items:
20 | wf.setItem(
21 | title=w[0],
22 | subtitle=w[1],
23 | arg=w[2]
24 | )
25 | icon_path = f'icons/{w[2]}.png'
26 | wf.setIcon(
27 | icon_path,
28 | m_type='image'
29 | )
30 | wf.addItem()
31 | wf.write()
32 |
--------------------------------------------------------------------------------
/src/chrom_bookmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import codecs
4 | import json
5 | import os
6 | import sys
7 | from plistlib import load
8 | from typing import Union
9 |
10 | from Alfred3 import Items as Items
11 | from Alfred3 import Tools as Tools
12 | from Favicon import Icons
13 |
14 | # Bookmark file path relative to HOME
15 |
16 | BOOKMARKS_MAP = {
17 | "brave": 'Library/Application Support/BraveSoftware/Brave-Browser/Default/Bookmarks',
18 | "brave_beta": 'Library/Application Support/BraveSoftware/Brave-Browser-Beta/Default/Bookmarks',
19 | "chrome": 'Library/Application Support/Google/Chrome/Default/Bookmarks',
20 | "chromium": 'Library/Application Support/Chromium/Default/Bookmarks',
21 | "opera": 'Library/Application Support/com.operasoftware.Opera/Bookmarks',
22 | "sidekick": 'Library/Application Support/Sidekick/Default/Bookmarks',
23 | "vivaldi": 'Library/Application Support/Vivaldi/Default/Bookmarks',
24 | "edge": 'Library/Application Support/Microsoft Edge/Default/Bookmarks',
25 | "arc": "Library/Application Support/Arc/User Data/Default/Bookmarks",
26 | "safari": 'Library/Safari/Bookmarks.plist'
27 | }
28 |
29 | # Show favicon in results or default wf icon
30 | show_favicon = Tools.getEnvBool("show_favicon")
31 |
32 | BOOKMARKS = list()
33 | # Get Browser Histories to load based on user configuration
34 | for k in BOOKMARKS_MAP.keys():
35 | if Tools.getEnvBool(k):
36 | BOOKMARKS.append(BOOKMARKS_MAP.get(k))
37 |
38 |
39 | def removeDuplicates(li: list) -> list:
40 | """
41 | Removes Duplicates from bookmark file
42 |
43 | Args:
44 | li(list): list of bookmark entries
45 |
46 | Returns:
47 | list: filtered bookmark entries
48 | """
49 | visited = set()
50 | output = []
51 | for a, b in li:
52 | if a not in visited:
53 | visited.add(a)
54 | output.append((a, b))
55 | return output
56 |
57 |
58 | def get_all_urls(the_json: str) -> list:
59 | """
60 | Extract all URLs and title from Bookmark files
61 |
62 | Args:
63 | the_json (str): All Bookmarks read from file
64 |
65 | Returns:
66 | list(tuble): List of tublle with Bookmarks url and title
67 | """
68 | def extract_data(data: dict):
69 | if isinstance(data, dict) and data.get('type') == 'url':
70 | urls.append({'name': data.get('name'), 'url': data.get('url')})
71 | if isinstance(data, dict) and data.get('type') == 'folder':
72 | the_children = data.get('children')
73 | get_container(the_children)
74 |
75 | def get_container(o: Union[list, dict]):
76 | if isinstance(o, list):
77 | for i in o:
78 | extract_data(i)
79 | if isinstance(o, dict):
80 | for k, i in o.items():
81 | extract_data(i)
82 |
83 | urls = list()
84 | get_container(the_json)
85 | s_list_dict = sorted(urls, key=lambda k: k['name'], reverse=False)
86 | ret_list = [(l.get('name'), l.get('url')) for l in s_list_dict]
87 | return ret_list
88 |
89 |
90 | def paths_to_bookmarks() -> list:
91 | """
92 | Get all valid bookmarks pahts from BOOKMARKS
93 |
94 | Returns:
95 | list: valid bookmark paths
96 | """
97 | user_dir = os.path.expanduser('~')
98 | bms = [os.path.join(user_dir, b) for b in BOOKMARKS]
99 | valid_bms = list()
100 | for b in bms:
101 | if os.path.isfile(b):
102 | valid_bms.append(b)
103 | Tools.log(f"{b} → found")
104 | else:
105 | Tools.log(f"{b} → NOT found")
106 |
107 | return valid_bms
108 |
109 |
110 | def get_json_from_file(file: str) -> json:
111 | """
112 | Get Bookmark JSON
113 |
114 | Args:
115 | file(str): File path to valid bookmark file
116 |
117 | Returns:
118 | str: JSON of Bookmarks
119 | """
120 | return json.load(codecs.open(file, 'r', 'utf-8-sig'))['roots']
121 |
122 |
123 | def extract_bookmarks(bookmark_data, bookmarks_list) -> None:
124 | """
125 | Recursively extract bookmarks (title and URL) from Safari bookmarks data.
126 | Args:
127 | bookmark_data (list or dict): The Safari bookmarks data, which can be a list or a dictionary.
128 | bookmarks_list (list): The list to which extracted bookmarks (title and URL) will be appended.
129 | Returns:
130 | None
131 | """
132 | if isinstance(bookmark_data, list):
133 | for item in bookmark_data:
134 | extract_bookmarks(item, bookmarks_list)
135 | elif isinstance(bookmark_data, dict):
136 | if "Children" in bookmark_data:
137 | extract_bookmarks(bookmark_data["Children"], bookmarks_list)
138 | elif "URLString" in bookmark_data and "URIDictionary" in bookmark_data:
139 | title = bookmark_data["URIDictionary"].get("title", "Untitled")
140 | url = bookmark_data["URLString"]
141 | bookmarks_list.append((title, url))
142 |
143 |
144 | def get_safari_bookmarks_json(file: str) -> list:
145 | """
146 | Get all bookmarks from Safari Bookmark file
147 |
148 | Args:
149 | file (str): Path to Safari Bookmark file
150 |
151 | Returns:
152 | list: List of bookmarks (title and URL)
153 |
154 | """
155 | with open(file, "rb") as fp:
156 | plist = load(fp)
157 | bookmarks = []
158 | extract_bookmarks(plist, bookmarks)
159 | return bookmarks
160 |
161 |
162 | def match(search_term: str, results: list) -> list:
163 | """
164 | Filters a list of tuples based on a search term.
165 | Args:
166 | search_term (str): The term to search for. Can include '&' or '|' to specify AND or OR logic.
167 | results (list): A list of tuples to search within.
168 | Returns:
169 | list: A list of tuples that match the search term based on the specified logic.
170 | The function supports the following search operators:
171 | - '&': All search terms must be present in a tuple for it to be included in the result.
172 | - '|': At least one of the search terms must be present in a tuple for it to be included in the result.
173 | - No operator: The search term must be present in a tuple for it to be included in the result.
174 | """
175 |
176 | def is_in_tuple(tple: tuple, st: str) -> bool:
177 | match = False
178 | for e in tple:
179 | if st.lower() in str(e).lower():
180 | match = True
181 | return match
182 |
183 | result_lst = []
184 | if '&' in search_term:
185 | search_terms = search_term.split('&')
186 | search_operator = "&"
187 | elif '|' in search_term:
188 | search_terms = search_term.split('|')
189 | search_operator = "|"
190 | else:
191 | search_terms = [search_term, ]
192 | search_operator = ""
193 |
194 | for r in results:
195 | if search_operator == "&" and all([is_in_tuple(r, ts) for ts in search_terms]):
196 | result_lst.append(r)
197 | if search_operator == "|" and any([is_in_tuple(r, ts) for ts in search_terms]):
198 | result_lst.append(r)
199 | if search_operator != "|" and search_operator != "&" and any([is_in_tuple(r, ts) for ts in search_terms]):
200 | result_lst.append(r)
201 | return result_lst
202 |
203 |
204 | def main():
205 | # Log python version
206 | Tools.log("PYTHON VERSION:", sys.version)
207 | # check python > 3.7.0
208 | if sys.version_info < (3, 7):
209 | Tools.log("Python version 3.7.0 or higher required!")
210 | sys.exit(0)
211 |
212 | # Workflow item object
213 | wf = Items()
214 | query = Tools.getArgv(1) if Tools.getArgv(1) is not None else str()
215 | bms = paths_to_bookmarks()
216 |
217 | if len(bms) > 0:
218 | matches = list()
219 | # Generate list of bookmarks matches the search
220 | bookmarks = []
221 | for bookmarks_file in bms:
222 | if "Safari" in bookmarks_file:
223 | bookmarks = get_safari_bookmarks_json(bookmarks_file)
224 | # pass
225 | else:
226 | bm_json = get_json_from_file(bookmarks_file)
227 | bookmarks = get_all_urls(bm_json)
228 | matches.extend(match(query, bookmarks))
229 | # finally remove duplicates from all browser bookmarks
230 | matches = removeDuplicates(matches)
231 | # generate list of matches for Favicon download
232 | ico_matches = []
233 | if show_favicon:
234 | ico_matches = [(i2, i1) for i1, i2 in matches]
235 | # Heat Favicon Cache
236 | ico = Icons(ico_matches)
237 | # generate script filter output
238 | for m in matches:
239 | name = m[0]
240 | url = m[1]
241 | wf.setItem(
242 | title=name,
243 | subtitle=f"{url[:80]}",
244 | arg=url,
245 | quicklookurl=url
246 | )
247 | if show_favicon:
248 | # get favicoon for url
249 | favicon = ico.get_favion_path(url)
250 | if favicon:
251 | wf.setIcon(
252 | favicon,
253 | "image"
254 | )
255 | wf.addMod(
256 | key='cmd',
257 | subtitle="Other Actions...",
258 | arg=url
259 | )
260 | wf.addMod(
261 | key="alt",
262 | subtitle=url,
263 | arg=url
264 | )
265 | wf.addItem()
266 | if wf.getItemsLengths() == 0:
267 | wf.setItem(
268 | title='No Bookmark found!',
269 | subtitle=f'Search "{query}" in Google...',
270 | arg=f'https://www.google.com/search?q={query}'
271 | )
272 | wf.addItem()
273 | wf.write()
274 |
275 |
276 | if __name__ == "__main__":
277 | main()
278 |
--------------------------------------------------------------------------------
/src/chrom_history.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import os
4 | import shutil
5 | import sqlite3
6 | import sys
7 | import time
8 | import uuid
9 | from multiprocessing.pool import ThreadPool as Pool
10 | from unicodedata import normalize
11 |
12 | from Alfred3 import Items as Items
13 | from Alfred3 import Tools as Tools
14 | from Favicon import Icons
15 |
16 | HISTORY_MAP = {
17 | "brave": "Library/Application Support/BraveSoftware/Brave-Browser/Default/History",
18 | "brave_beta": "Library/Application Support/BraveSoftware/Brave-Browser-Beta/Default/History",
19 | "chromium": "Library/Application Support/Chromium/Default/History",
20 | "chrome": "Library/Application Support/Google/Chrome/Default/History",
21 | "opera": "Library/Application Support/com.operasoftware.Opera/History",
22 | "sidekick": 'Library/Application Support/Sidekick/Default/History',
23 | "vivaldi": "Library/Application Support/Vivaldi/Default/History",
24 | "edge": "Library/Application Support/Microsoft Edge/Default/History",
25 | "arc": "Library/Application Support/Arc/User Data/Default/History",
26 | "safari": "Library/Safari/History.db"
27 | }
28 |
29 | # Get Browser Histories to load per env (true/false)
30 | HISTORIES = list()
31 | for k in HISTORY_MAP.keys():
32 | if Tools.getEnvBool(k):
33 | HISTORIES.append(HISTORY_MAP.get(k))
34 |
35 | # Get ignored Domains settings
36 | d = Tools.getEnv("ignored_domains", None)
37 | ignored_domains = d.split(',') if d else None
38 |
39 | # Show favicon in results or default wf icon
40 | show_favicon = Tools.getEnvBool("show_favicon")
41 |
42 | # if set to true history entries will be sorted
43 | # based on recent visitied otherwise number of visits
44 | sort_recent = Tools.getEnvBool("sort_recent")
45 |
46 | # Date format settings
47 | DATE_FMT = Tools.getEnv("date_format", default='%d. %B %Y')
48 |
49 |
50 | def history_paths() -> list:
51 | """
52 | Get valid pathes to history from HISTORIES variable
53 |
54 | Returns:
55 | list: available paths of history files
56 | """
57 | user_dir = os.path.expanduser("~")
58 | hists = [os.path.join(user_dir, h) for h in HISTORIES]
59 |
60 | valid_hists = list()
61 | # write log if history db was found or not
62 | for h in hists:
63 | if os.path.isfile(h):
64 | valid_hists.append(h)
65 | Tools.log(f"{h} → found")
66 | else:
67 | Tools.log(f"{h} → NOT found")
68 | return valid_hists
69 |
70 |
71 | def get_histories(dbs: list, query: str) -> list:
72 | """
73 | Load History files into list
74 |
75 | Args:
76 | dbs(list): list with valid history paths
77 |
78 | Returns:
79 | list: filters history entries
80 | """
81 |
82 | results = list()
83 | with Pool(len(dbs)) as p: # Exec in ThreadPool
84 | results = p.map(sql, [db for db in dbs])
85 | matches = []
86 | for r in results:
87 | matches = matches + r
88 | results = search_in_tuples(matches, query)
89 | # Remove duplicate Entries
90 | results = removeDuplicates(results)
91 | # evmove ignored domains
92 | if ignored_domains:
93 | results = remove_ignored_domains(results, ignored_domains)
94 | # Reduce search results to 30
95 | results = results[:30]
96 | # Sort by element. Element 2=visited, 3=recent
97 | sort_by = 3 if sort_recent else 2
98 | results = Tools.sortListTuple(results, sort_by) # Sort based on visits
99 | return results
100 |
101 |
102 | def remove_ignored_domains(results: list, ignored_domains: list) -> list:
103 | """
104 | removes results based on domain ignore list
105 |
106 | Args:
107 | results (list): History results list with tubles
108 | ignored_domains (list): list of domains to ignore
109 |
110 | Returns:
111 | list: _description_
112 | """
113 | new_results = list()
114 | if len(ignored_domains) > 0:
115 | for r in results:
116 | for i in ignored_domains:
117 | inner_result = r
118 | if i in r[0]:
119 | inner_result = None
120 | break
121 | if inner_result:
122 | new_results.append(inner_result)
123 | else:
124 | new_results = results
125 | return new_results
126 |
127 |
128 | def sql(db: str) -> list:
129 | """
130 | Executes SQL depending on History path
131 | provided in db: str
132 |
133 | Args:
134 | db (str): Path to History file
135 |
136 | Returns:
137 | list: result list of dictionaries (Url, Title, VisiCount)
138 | """
139 | res = []
140 | history_db = f"/tmp/{uuid.uuid1()}"
141 | try:
142 | shutil.copy2(db, history_db)
143 | with sqlite3.connect(history_db) as c:
144 | cursor = c.cursor()
145 | # SQL satement for Safari
146 | if "Safari" in db:
147 | select_statement = f"""
148 | SELECT history_items.url, history_visits.title, history_items.visit_count,(history_visits.visit_time + 978307200)
149 | FROM history_items
150 | INNER JOIN history_visits
151 | ON history_visits.history_item = history_items.id
152 | WHERE history_items.url IS NOT NULL AND
153 | history_visits.TITLE IS NOT NULL AND
154 | history_items.url != '' order by visit_time DESC
155 | """
156 | # SQL statement for Chromium Brothers
157 | else:
158 | select_statement = f"""
159 | SELECT DISTINCT urls.url, urls.title, urls.visit_count, (urls.last_visit_time/1000000 + (strftime('%s', '1601-01-01')))
160 | FROM urls, visits
161 | WHERE urls.id = visits.url AND
162 | urls.title IS NOT NULL AND
163 | urls.title != '' order by last_visit_time DESC; """
164 | Tools.log(select_statement)
165 | cursor.execute(select_statement)
166 | r = cursor.fetchall()
167 | res.extend(r)
168 | os.remove(history_db) # Delete History file in /tmp
169 | except sqlite3.Error as e:
170 | Tools.log(f"SQL Error: {e}")
171 | sys.exit(1)
172 | return res
173 |
174 |
175 | def get_search_terms(search: str) -> tuple:
176 | """
177 | Explode search term string
178 |
179 | Args:
180 | search(str): search term(s), can contain & or |
181 |
182 | Returns:
183 | tuple: Tuple with search terms
184 | """
185 | if "&" in search:
186 | search_terms = tuple(search.split("&"))
187 | elif "|" in search:
188 | search_terms = tuple(search.split("|"))
189 | else:
190 | search_terms = (search,)
191 | search_terms = [normalize("NFC", s) for s in search_terms]
192 | return search_terms
193 |
194 |
195 | def removeDuplicates(li: list) -> list:
196 | """
197 | Removes Duplicates from history file
198 |
199 | Args:
200 | li(list): list of history entries
201 |
202 | Returns:
203 | list: filtered history entries
204 | """
205 | visited = set()
206 | output = []
207 | for a, b, c, d in li:
208 | if b not in visited:
209 | visited.add(b)
210 | output.append((a, b, c, d))
211 | return output
212 |
213 |
214 | def search_in_tuples(tuples: list, search: str) -> list:
215 | """
216 | Search for serach term in list of tuples
217 |
218 | Args:
219 | tuples(list): List contains tuple to search
220 | search(str): Search contains & or & or none
221 |
222 | Returns:
223 | list: tuple list with result of query srting
224 | """
225 |
226 | def is_in_tuple(tple: tuple, st: str) -> bool:
227 | match = False
228 | for e in tple:
229 | if st.lower() in str(e).lower():
230 | match = True
231 | return match
232 |
233 | search_terms = get_search_terms(search)
234 | result = list()
235 | for t in tuples:
236 | # Search AND
237 | if "&" in search and all([is_in_tuple(t, ts) for ts in search_terms]):
238 | result.append(t)
239 | # Search OR
240 | if "|" in search and any([is_in_tuple(t, ts) for ts in search_terms]):
241 | result.append(t)
242 | # Search Single term
243 | if "|" not in search and "&" not in search and any([is_in_tuple(t, ts) for ts in search_terms]):
244 | result.append(t)
245 | return result
246 |
247 |
248 | def formatTimeStamp(time_ms: int, fmt: str = '%d. %B %Y') -> str:
249 | """
250 | Time Stamp (ms) into formatted date string
251 |
252 | Args:
253 |
254 | time_ms (int): time in ms from 01/01/1601
255 | fmt (str, optional): Format of the Date string. Defaults to '%d. %B %Y'.
256 |
257 | Returns:
258 |
259 | str: Formatted Date String
260 | """
261 | t_string = time.strftime(fmt, time.gmtime(time_ms))
262 | return t_string
263 |
264 |
265 | def main():
266 | # Get wf cached directory for writing into debugger
267 | wf_cache_dir = Tools.getCacheDir()
268 | # Get wf data directory for writing into debugger
269 | wf_data_dir = Tools.getDataDir()
270 | # Check and write python version
271 | Tools.log(f"Cache Dir: {wf_cache_dir}")
272 | Tools.log(f'Data Dir: {wf_data_dir}')
273 | Tools.log("PYTHON VERSION:", sys.version)
274 | if sys.version_info < (3, 7):
275 | Tools.log("Python version 3.7.0 or higher required!")
276 | sys.exit(0)
277 |
278 | # Create Workflow items object
279 | wf = Items()
280 | search_term = Tools.getArgv(1)
281 | locked_history_dbs = history_paths()
282 | # if selected browser(s) in config was not found stop here
283 | if len(locked_history_dbs) == 0:
284 | wf.setItem(
285 | title="Browser History not found!",
286 | subtitle="Ensure Browser is installed or choose available browser(s) in CONFIGURE WORKFLOW",
287 | valid=False
288 | )
289 | wf.addItem()
290 | wf.write()
291 | sys.exit(0)
292 | # get search results exit if Nothing was entered in search
293 | results = list()
294 | if search_term is not None:
295 | results = get_histories(locked_history_dbs, search_term)
296 | else:
297 | sys.exit(0)
298 | # if result the write alfred response
299 | if len(results) > 0:
300 | # Cache Favicons
301 | if show_favicon:
302 | ico = Icons(results)
303 | for i in results:
304 | url = i[0]
305 | title = i[1]
306 | visits = i[2]
307 | last_visit = formatTimeStamp(i[3], fmt=DATE_FMT)
308 | wf.setItem(
309 | title=title,
310 | subtitle=f"Last visit: {last_visit}(Visits: {visits})",
311 | arg=url,
312 | quicklookurl=url
313 | )
314 | if show_favicon:
315 | favicon = ico.get_favion_path(url)
316 | wf.setIcon(
317 | favicon,
318 | "image"
319 | )
320 | wf.addMod(
321 | key='cmd',
322 | subtitle="Other Actions...",
323 | arg=url
324 | )
325 | wf.addMod(
326 | key="alt",
327 | subtitle=url,
328 | arg=url
329 | )
330 | wf.addItem()
331 | if wf.getItemsLengths() == 0:
332 | wf.setItem(
333 | title="Nothing found in History!",
334 | subtitle=f'Search "{search_term}" in Google?',
335 | arg=f"https://www.google.com/search?q={search_term}",
336 | )
337 | wf.addItem()
338 | wf.write()
339 |
340 |
341 | if __name__ == "__main__":
342 | main()
343 |
--------------------------------------------------------------------------------
/src/domain.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | from sys import stdout
4 | from urllib.parse import urlparse
5 |
6 | from Alfred3 import AlfJson, Tools
7 |
8 | url = Tools.getEnv('url')
9 | domain = Tools.getDomain(url)
10 |
11 | aj = AlfJson()
12 | aj.add_variables({"url": domain})
13 | aj.write_json()
14 |
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/8ca285e67c6769e9ed8313ab45d99c63ee8c00cb/src/icon.png
--------------------------------------------------------------------------------
/src/icons/clipboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/8ca285e67c6769e9ed8313ab45d99c63ee8c00cb/src/icons/clipboard.png
--------------------------------------------------------------------------------
/src/icons/domain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/8ca285e67c6769e9ed8313ab45d99c63ee8c00cb/src/icons/domain.png
--------------------------------------------------------------------------------
/src/icons/openin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/8ca285e67c6769e9ed8313ab45d99c63ee8c00cb/src/icons/openin.png
--------------------------------------------------------------------------------
/src/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 | com.apple.alfred.workflow.chromium-hist
7 | category
8 | Internet
9 | connections
10 |
11 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5
12 |
13 |
14 | destinationuid
15 | 6B99D187-E195-432C-A079-D4A99D9A9B0D
16 | modifiers
17 | 0
18 | modifiersubtext
19 |
20 | vitoclose
21 |
22 |
23 |
24 | A1F782AE-40F5-47BE-BA57-F230607162D5
25 |
26 |
27 | destinationuid
28 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9
29 | modifiers
30 | 0
31 | modifiersubtext
32 |
33 | vitoclose
34 |
35 |
36 |
37 | destinationuid
38 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39
39 | modifiers
40 | 1048576
41 | modifiersubtext
42 |
43 | vitoclose
44 |
45 |
46 |
47 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39
48 |
49 |
50 | destinationuid
51 | D09358D1-C880-4E62-837E-6648A334152E
52 | modifiers
53 | 0
54 | modifiersubtext
55 |
56 | vitoclose
57 |
58 |
59 |
60 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165
61 |
62 |
63 | destinationuid
64 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2
65 | modifiers
66 | 0
67 | modifiersubtext
68 |
69 | vitoclose
70 |
71 |
72 |
73 | D09358D1-C880-4E62-837E-6648A334152E
74 |
75 |
76 | destinationuid
77 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB
78 | modifiers
79 | 0
80 | modifiersubtext
81 |
82 | vitoclose
83 |
84 |
85 |
86 | E420908F-C5F5-44DD-AB95-0B375998DC6F
87 |
88 |
89 | destinationuid
90 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9
91 | modifiers
92 | 0
93 | modifiersubtext
94 |
95 | vitoclose
96 |
97 |
98 |
99 | destinationuid
100 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39
101 | modifiers
102 | 1048576
103 | modifiersubtext
104 |
105 | vitoclose
106 |
107 |
108 |
109 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9
110 |
111 |
112 | destinationuid
113 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2
114 | modifiers
115 | 0
116 | modifiersubtext
117 |
118 | vitoclose
119 |
120 |
121 |
122 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB
123 |
124 |
125 | destinationuid
126 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165
127 | modifiers
128 | 0
129 | modifiersubtext
130 |
131 | sourceoutputuid
132 | F19E20E4-AFB0-4DA1-84CE-D9C51949AFAA
133 | vitoclose
134 |
135 |
136 |
137 | destinationuid
138 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA
139 | modifiers
140 | 0
141 | modifiersubtext
142 |
143 | sourceoutputuid
144 | 9BFE8906-36E9-49A3-A4B7-41AC1E1A60F5
145 | vitoclose
146 |
147 |
148 |
149 | destinationuid
150 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5
151 | modifiers
152 | 0
153 | modifiersubtext
154 |
155 | sourceoutputuid
156 | 0B447EDC-254F-4D46-B2EC-24820A8265CB
157 | vitoclose
158 |
159 |
160 |
161 |
162 | createdby
163 | Acidham
164 | description
165 | Search in Browser History and Bookmarks
166 | disabled
167 |
168 | name
169 | Chromium Bookmarks and History Search
170 | objects
171 |
172 |
173 | config
174 |
175 | browser
176 |
177 | skipqueryencode
178 |
179 | skipvarencode
180 |
181 | spaces
182 |
183 | url
184 | {var:url}
185 |
186 | type
187 | alfred.workflow.action.openurl
188 | uid
189 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2
190 | version
191 | 1
192 |
193 |
194 | config
195 |
196 | alfredfiltersresults
197 |
198 | alfredfiltersresultsmatchmode
199 | 0
200 | argumenttreatemptyqueryasnil
201 |
202 | argumenttrimmode
203 | 0
204 | argumenttype
205 | 1
206 | escaping
207 | 102
208 | keyword
209 | {var:history_keyword}
210 | queuedelaycustom
211 | 3
212 | queuedelayimmediatelyinitially
213 |
214 | queuedelaymode
215 | 1
216 | queuemode
217 | 2
218 | runningsubtext
219 | Searching, please wait...
220 | script
221 | ./py3.sh chrom_history.py "$1"
222 |
223 | scriptargtype
224 | 1
225 | scriptfile
226 | chrom_history.py
227 | subtext
228 | Search in Chromium History
229 | title
230 | Chromium History Search
231 | type
232 | 0
233 | withspace
234 |
235 |
236 | type
237 | alfred.workflow.input.scriptfilter
238 | uid
239 | A1F782AE-40F5-47BE-BA57-F230607162D5
240 | version
241 | 3
242 |
243 |
244 | config
245 |
246 | argument
247 |
248 | passthroughargument
249 |
250 | variables
251 |
252 | url
253 | {query}
254 |
255 |
256 | type
257 | alfred.workflow.utility.argument
258 | uid
259 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9
260 | version
261 | 1
262 |
263 |
264 | config
265 |
266 | concurrently
267 |
268 | escaping
269 | 102
270 | script
271 | ./py3.sh domain.py $1
272 | scriptargtype
273 | 1
274 | scriptfile
275 |
276 | type
277 | 5
278 |
279 | type
280 | alfred.workflow.action.script
281 | uid
282 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165
283 | version
284 | 2
285 |
286 |
287 | config
288 |
289 | jumpto
290 | alfred.action.url.openin
291 | path
292 | {var:url}
293 | type
294 | 2
295 |
296 | type
297 | alfred.workflow.action.actioninalfred
298 | uid
299 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA
300 | version
301 | 1
302 |
303 |
304 | config
305 |
306 | alfredfiltersresults
307 |
308 | alfredfiltersresultsmatchmode
309 | 0
310 | argumenttreatemptyqueryasnil
311 |
312 | argumenttrimmode
313 | 0
314 | argumenttype
315 | 2
316 | escaping
317 | 102
318 | queuedelaycustom
319 | 3
320 | queuedelayimmediatelyinitially
321 |
322 | queuedelaymode
323 | 0
324 | queuemode
325 | 1
326 | runningsubtext
327 |
328 | script
329 | ./py3.sh actions.py
330 | scriptargtype
331 | 1
332 | scriptfile
333 |
334 | subtext
335 |
336 | title
337 |
338 | type
339 | 5
340 | withspace
341 |
342 |
343 | type
344 | alfred.workflow.input.scriptfilter
345 | uid
346 | D09358D1-C880-4E62-837E-6648A334152E
347 | version
348 | 3
349 |
350 |
351 | config
352 |
353 | conditions
354 |
355 |
356 | inputstring
357 |
358 | matchcasesensitive
359 |
360 | matchmode
361 | 0
362 | matchstring
363 | domain
364 | outputlabel
365 | domain
366 | uid
367 | F19E20E4-AFB0-4DA1-84CE-D9C51949AFAA
368 |
369 |
370 | inputstring
371 |
372 | matchcasesensitive
373 |
374 | matchmode
375 | 0
376 | matchstring
377 | openin
378 | outputlabel
379 | openin
380 | uid
381 | 9BFE8906-36E9-49A3-A4B7-41AC1E1A60F5
382 |
383 |
384 | inputstring
385 |
386 | matchcasesensitive
387 |
388 | matchmode
389 | 0
390 | matchstring
391 | clipboard
392 | outputlabel
393 | clipboard
394 | uid
395 | 0B447EDC-254F-4D46-B2EC-24820A8265CB
396 |
397 |
398 | elselabel
399 | else
400 | hideelse
401 |
402 |
403 | type
404 | alfred.workflow.utility.conditional
405 | uid
406 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB
407 | version
408 | 1
409 |
410 |
411 | config
412 |
413 | argument
414 |
415 | passthroughargument
416 |
417 | variables
418 |
419 | url
420 | {query}
421 |
422 |
423 | type
424 | alfred.workflow.utility.argument
425 | uid
426 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39
427 | version
428 | 1
429 |
430 |
431 | config
432 |
433 | lastpathcomponent
434 |
435 | onlyshowifquerypopulated
436 |
437 | removeextension
438 |
439 | text
440 | {query} copied to the Clipboard
441 | title
442 | URL copied to Clipboard
443 |
444 | type
445 | alfred.workflow.output.notification
446 | uid
447 | 6B99D187-E195-432C-A079-D4A99D9A9B0D
448 | version
449 | 1
450 |
451 |
452 | config
453 |
454 | alfredfiltersresults
455 |
456 | alfredfiltersresultsmatchmode
457 | 0
458 | argumenttreatemptyqueryasnil
459 |
460 | argumenttrimmode
461 | 0
462 | argumenttype
463 | 1
464 | escaping
465 | 102
466 | keyword
467 | {var:bookmark_keyword}
468 | queuedelaycustom
469 | 3
470 | queuedelayimmediatelyinitially
471 |
472 | queuedelaymode
473 | 1
474 | queuemode
475 | 1
476 | runningsubtext
477 | Searching, please wait...
478 | script
479 | ./py3.sh chrom_bookmarks.py "$1"
480 | scriptargtype
481 | 1
482 | scriptfile
483 | chrom_bookmarks.py
484 | subtext
485 | Search in Chromium Bookmarks
486 | title
487 | Chromium Bookmarks Search
488 | type
489 | 0
490 | withspace
491 |
492 |
493 | type
494 | alfred.workflow.input.scriptfilter
495 | uid
496 | E420908F-C5F5-44DD-AB95-0B375998DC6F
497 | version
498 | 3
499 |
500 |
501 | config
502 |
503 | autopaste
504 |
505 | clipboardtext
506 | {var:url}
507 | ignoredynamicplaceholders
508 |
509 | transient
510 |
511 |
512 | type
513 | alfred.workflow.output.clipboard
514 | uid
515 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5
516 | version
517 | 3
518 |
519 |
520 | readme
521 | # Browser History and Bookmarks Search
522 |
523 | The Workflow searches History and Bookmarks of the configured Browsers simulatiously.
524 |
525 | ## Supported Browsers
526 |
527 | - Chromium
528 | - Google Chrome
529 | - Brave and Brave beta (Chromium)
530 | - MS Edge
531 | - Vivaldi
532 | - Opera
533 | - Sidekick
534 | - Arc
535 | - Safari
536 |
537 | ## Requires
538 |
539 | * Python 3
540 | * Alfred 5
541 |
542 | ## Usage
543 |
544 | ### Bookmarks Search
545 |
546 | Search History and Bookmarks:
547 | Type `&` in between of the search terms to search for multiple entries e.g.:
548 | `Car&Bike` match entries with `Car or Bike rental` but NOT `Car driving school`
549 |
550 |
551 | ### Other Actions
552 |
553 | Pressing `CMD` to enter `Other Actions...`:
554 |
555 | * `Copy to Clipboard`: Copies the URL into the Clipboard
556 | * `Open Domain`: Opens the domain (e.g. www.google.com) in default Browser
557 | * `Open In...`: Opens the URL with the Alfred's build in Open-In other Browser
558 | uidata
559 |
560 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2
561 |
562 | xpos
563 | 940
564 | ypos
565 | 45
566 |
567 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA
568 |
569 | xpos
570 | 940
571 | ypos
572 | 190
573 |
574 | 6B99D187-E195-432C-A079-D4A99D9A9B0D
575 |
576 | xpos
577 | 1090
578 | ypos
579 | 355
580 |
581 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5
582 |
583 | xpos
584 | 940
585 | ypos
586 | 355
587 |
588 | A1F782AE-40F5-47BE-BA57-F230607162D5
589 |
590 | xpos
591 | 30
592 | ypos
593 | 50
594 |
595 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39
596 |
597 | xpos
598 | 310
599 | ypos
600 | 220
601 |
602 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165
603 |
604 | xpos
605 | 680
606 | ypos
607 | 125
608 |
609 | D09358D1-C880-4E62-837E-6648A334152E
610 |
611 | xpos
612 | 380
613 | ypos
614 | 190
615 |
616 | E420908F-C5F5-44DD-AB95-0B375998DC6F
617 |
618 | xpos
619 | 30
620 | ypos
621 | 355
622 |
623 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9
624 |
625 | xpos
626 | 685
627 | ypos
628 | 75
629 |
630 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB
631 |
632 | xpos
633 | 565
634 | ypos
635 | 195
636 |
637 |
638 | userconfigurationconfig
639 |
640 |
641 | config
642 |
643 | default
644 | bh
645 | placeholder
646 |
647 | required
648 |
649 | trim
650 |
651 |
652 | description
653 |
654 | label
655 | History search keyword
656 | type
657 | textfield
658 | variable
659 | history_keyword
660 |
661 |
662 | config
663 |
664 | default
665 | bm
666 | placeholder
667 |
668 | required
669 |
670 | trim
671 |
672 |
673 | description
674 |
675 | label
676 | Bookmark search keyword
677 | type
678 | textfield
679 | variable
680 | bookmark_keyword
681 |
682 |
683 | config
684 |
685 | default
686 |
687 | required
688 |
689 | text
690 | Google Chrome
691 |
692 | description
693 |
694 | label
695 | Include
696 | type
697 | checkbox
698 | variable
699 | chrome
700 |
701 |
702 | config
703 |
704 | default
705 |
706 | required
707 |
708 | text
709 | Chromium
710 |
711 | description
712 |
713 | label
714 |
715 | type
716 | checkbox
717 | variable
718 | chromium
719 |
720 |
721 | config
722 |
723 | default
724 |
725 | required
726 |
727 | text
728 | Brave
729 |
730 | description
731 |
732 | label
733 |
734 | type
735 | checkbox
736 | variable
737 | brave
738 |
739 |
740 | config
741 |
742 | default
743 |
744 | required
745 |
746 | text
747 | Brave Beta
748 |
749 | description
750 |
751 | label
752 |
753 | type
754 | checkbox
755 | variable
756 | brave_beta
757 |
758 |
759 | config
760 |
761 | default
762 |
763 | required
764 |
765 | text
766 | Opera
767 |
768 | description
769 |
770 | label
771 |
772 | type
773 | checkbox
774 | variable
775 | opera
776 |
777 |
778 | config
779 |
780 | default
781 |
782 | required
783 |
784 | text
785 | Sidekick
786 |
787 | description
788 |
789 | label
790 |
791 | type
792 | checkbox
793 | variable
794 | sidekick
795 |
796 |
797 | config
798 |
799 | default
800 |
801 | required
802 |
803 | text
804 | Vivaldi
805 |
806 | description
807 |
808 | label
809 |
810 | type
811 | checkbox
812 | variable
813 | vivaldi
814 |
815 |
816 | config
817 |
818 | default
819 |
820 | required
821 |
822 | text
823 | Edge
824 |
825 | description
826 |
827 | label
828 |
829 | type
830 | checkbox
831 | variable
832 | edge
833 |
834 |
835 | config
836 |
837 | default
838 |
839 | required
840 |
841 | text
842 | Arc
843 |
844 | description
845 |
846 | label
847 |
848 | type
849 | checkbox
850 | variable
851 | arc
852 |
853 |
854 | config
855 |
856 | default
857 |
858 | required
859 |
860 | text
861 | Safari
862 |
863 | description
864 | Browsers to be included into history and bookmark search
865 | label
866 |
867 | type
868 | checkbox
869 | variable
870 | safari
871 |
872 |
873 | config
874 |
875 | default
876 | mail.google.com,mail.gmx.com
877 | required
878 |
879 | trim
880 |
881 | verticalsize
882 | 3
883 |
884 | description
885 | Comma separated list of domains to be ignored in history search
886 | label
887 | Excluded Domains
888 | type
889 | textarea
890 | variable
891 | ignored_domains
892 |
893 |
894 | config
895 |
896 | default
897 |
898 | required
899 |
900 | text
901 | Show favicons
902 |
903 | description
904 | Show favicons in results. NOTE: Displaying favicons slows down search
905 | label
906 | Favicons
907 | type
908 | checkbox
909 | variable
910 | show_favicon
911 |
912 |
913 | config
914 |
915 | default
916 | true
917 | pairs
918 |
919 |
920 | recent
921 | true
922 |
923 |
924 | visits
925 | false
926 |
927 |
928 |
929 | description
930 | History entries will be sorted based on recent visits OR number of visits
931 | label
932 | Sort history search results
933 | type
934 | popupbutton
935 | variable
936 | sort_recent
937 |
938 |
939 | config
940 |
941 | default
942 | %d.%m.%Y
943 | placeholder
944 | %d.%m.%Y
945 | required
946 |
947 | trim
948 |
949 |
950 | description
951 | Define how dates should be displayed. https://strftime.org/
952 | label
953 | Date Format
954 | type
955 | textfield
956 | variable
957 | date_format
958 |
959 |
960 | variablesdontexport
961 |
962 | version
963 | 4.2.1
964 | webaddress
965 | https://github.com/Acidham/chromium-hist-bookmarks
966 |
967 |
968 |
--------------------------------------------------------------------------------
/src/py3.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | #Set to 0 to use the first Python3 version found
3 | PREFER_LATEST=1
4 |
5 | #Array of python3 paths
6 | PYPATHS=(/usr/bin /usr/local/bin)
7 |
8 | #Input arguments
9 | SCR="${1}"
10 | QUERY="${2}"
11 | WF_DATA_DIR=$alfred_workflow_data
12 |
13 | #Create wf data dir if not available
14 | [ ! -d "$WF_DATA_DIR" ] && mkdir "$WF_DATA_DIR"
15 |
16 | SCRPATH="$0"
17 | SCRIPT_DIR="$(dirname $SCRPATH)"
18 |
19 | #Cache file for python binary - Allowes for faster execution
20 | PYALIAS="$WF_DATA_DIR/py3"
21 |
22 | CONFIG_PREFIX="Config"
23 | DEBUG=0
24 |
25 | pyrun() {
26 | $py3 "${SCR}" "${QUERY}"
27 | RES=$?
28 | [[ $RES -eq 127 ]] && handle_py_notfound
29 | return $RES
30 | }
31 |
32 | handle_py_notfound() {
33 | #we need this in case of some OS reconfiguration , python3 uninstalled ,etc..
34 | log_debug "python3 configuration changed, attemping to reconfigure"
35 | setup_python_alias
36 | }
37 |
38 | verify_not_stub() {
39 | PYBIN="${1}"
40 | $PYBIN -V > /dev/null 2>&1
41 | return $?
42 | }
43 |
44 | getver() {
45 | PYBIN="${1}"
46 | #Extract py3 version info and convert to comparable decimal
47 | VER=$($PYBIN -V | cut -f2 -d" " | sed -E 's/\.([0-9]+)$/\1/')
48 | echo $VER
49 | log_debug "Version: $VER"
50 | }
51 |
52 | make_alias() {
53 | PYBIN="${1}"
54 | PYVER="$2"
55 | #last sanitization
56 | [ -z "${PYBIN}" ] && log_msg "Error: invalid python3 path" && exit 255
57 | [ -z "${PYVER}" ] && PYVER="$(getver "$PYBIN")"
58 | echo "export py3='$PYBIN'" > "$PYALIAS"
59 | log_msg "Python3 was found at $PYBIN." "Version: $PYVER, Proceed typing query or re-run worfklow"
60 | }
61 |
62 | log_msg() {
63 | log_json "$CONFIG_PREFIX: $1" "$2"
64 | log_debug "$1"
65 | }
66 |
67 | log_json() {
68 | #need to use json for notifications since we're in script filter
69 | title="$1"
70 | sub="$2"
71 | [ -z "$sub" ] && sub="$title"
72 | cat <&2
86 | }
87 |
88 | setup_python_alias() {
89 | current_py=""
90 | current_ver=0.00
91 | for p in "${PYPATHS[@]}"
92 | do
93 | if [ -f $p/python3 ]
94 | then
95 | #check path does not contain a stub
96 | # set -x
97 | ! verify_not_stub "$p/python3" && continue
98 | #check for latest py3 version
99 | if [ $PREFER_LATEST -eq 1 ]
100 | then
101 | thisver=$(getver $p/python3)
102 | if [[ $(echo "$thisver > $current_ver" | bc -l) -eq 1 ]]
103 | then
104 | current_ver=$thisver
105 | current_py=$p/python3
106 | fi
107 | else
108 | #Just take the first valid python3 found
109 | make_alias "$p/python3"
110 | return 0
111 | fi
112 | fi
113 | done
114 | if [ $current_ver = 0.00 ]
115 | then
116 | log_msg "Error: no valid python3 version found" "Please locate python version and add to PYPATHS variable"
117 | exit 255
118 | fi
119 | make_alias "$current_py" "$current_ver"
120 | . "$PYALIAS"
121 | }
122 |
123 | #Main
124 | if [ -f "$PYALIAS" ]
125 | then
126 | . "$PYALIAS"
127 | pyrun
128 | exit
129 | else
130 | setup_python_alias
131 | fi
132 |
--------------------------------------------------------------------------------