├── .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 | --------------------------------------------------------------------------------