├── src ├── icon.png ├── icons │ ├── domain.png │ ├── openin.png │ ├── clipboard.png │ └── sourcebrowser.png ├── domain.py ├── actions.py ├── browser_dispatcher.py ├── Favicon.py ├── py3.sh ├── browser_config.py ├── chrom_bookmarks.py ├── chrom_history.py ├── Alfred3.py └── info.plist ├── Chromium Bookmarks and History Search.alfredworkflow ├── requirements.lock ├── requirements-dev.lock ├── pyproject.toml ├── README.md └── .gitignore /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/src/icon.png -------------------------------------------------------------------------------- /src/icons/domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/src/icons/domain.png -------------------------------------------------------------------------------- /src/icons/openin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/src/icons/openin.png -------------------------------------------------------------------------------- /src/icons/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/src/icons/clipboard.png -------------------------------------------------------------------------------- /src/icons/sourcebrowser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/src/icons/sourcebrowser.png -------------------------------------------------------------------------------- /Chromium Bookmarks and History Search.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/chromium-hist-bookmarks/HEAD/Chromium Bookmarks and History Search.alfredworkflow -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | click==8.3.0 14 | # via chromium-hist-bookmarks 15 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | click==8.3.0 14 | # via chromium-hist-bookmarks 15 | -------------------------------------------------------------------------------- /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 | # Create AlfJson object to store output 12 | aj = AlfJson() 13 | 14 | # Add the domain as a variable with key "url" 15 | aj.add_variables({"url": domain}) 16 | 17 | # Write the JSON output 18 | aj.write_json() 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "chromium-hist-bookmarks" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | authors = [ 6 | { name = "Acidham", email = "acidham@gmail.com" } 7 | ] 8 | dependencies = [ 9 | "click>=8.3.0", 10 | ] 11 | readme = "README.md" 12 | requires-python = ">= 3.8" 13 | 14 | [build-system] 15 | requires = ["hatchling"] 16 | build-backend = "hatchling.build" 17 | 18 | [tool.rye] 19 | managed = true 20 | dev-dependencies = [] 21 | 22 | [tool.hatch.metadata] 23 | allow-direct-references = true 24 | 25 | [tool.hatch.build.targets.wheel] 26 | packages = ["src/chromium_hist_bookmarks"] 27 | -------------------------------------------------------------------------------- /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 | - Comet (Perplexity AI) 16 | - Helium 17 | - Safari 18 | 19 | ## Requires 20 | 21 | * Python 3 22 | * Alfred 5 23 | 24 | ## Usage 25 | 26 | ### History 27 | 28 | 29 | Search History with keyword: `bh` 30 | 31 | Type `&` in between of the search terms to search for multiple entries e.g.: 32 | `Car&Bike` match entries with `Car or Bike rental` but NOT `Car driving school` 33 | 34 | ### Bookmarks 35 | 36 | Search Bookmarks with keyword: `bm` 37 | 38 | ### Other Actions 39 | 40 | Pressing `CMD` to enter `Other Actions...`: 41 | 42 | * `Copy to Clipboard`: Copies the URL into the Clipboard 43 | * `Open Domain`: Opens the domain (e.g. www.google.com) in default Browser 44 | * `Open In...`: Opens the URL with the Alfred's build in Open-In other Browser 45 | * `Open in Source Browser`: Opens the URL in the Browser of origin 46 | 47 | ### Bookmark Location 48 | 49 | When viewing bookmarks, press `SHIFT` to see the bookmark's location in your browser's folder structure: 50 | 51 | * The subtitle will show `Location: Bookmarks Bar > Folder Name > Subfolder` 52 | * Press `SHIFT + ENTER` to copy the location path to clipboard 53 | 54 | This helps you quickly find where a bookmark is organized in your browser. 55 | -------------------------------------------------------------------------------- /src/actions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | from urllib.parse import urlparse 5 | 6 | from Alfred3 import Items, Tools 7 | from browser_config import BROWSER_APPS, get_browser_display_name 8 | 9 | url_with_browser = Tools.getEnv('url') 10 | # Parse URL and browser from pipe-separated string 11 | if '|' in url_with_browser: 12 | url, browser = url_with_browser.split('|', 1) 13 | else: 14 | url = url_with_browser 15 | browser = None 16 | 17 | domain = Tools.getDomain(url) 18 | 19 | # Script Filter item [Title, Subtitle, arg] 20 | wf_items = [ 21 | ['Copy to Clipboard', 'Copy URL to Clipboard', 'clipboard'], 22 | ['Open Domain', f'Open {domain}', 'domain'], 23 | ['Open URL in...', 'Open URL in another Browser', 'openin'], 24 | ] 25 | 26 | # Add "Open in Source Browser" option if browser info is available 27 | if browser and browser in BROWSER_APPS: 28 | app_path = BROWSER_APPS[browser] 29 | # Check if the app exists 30 | if os.path.exists(app_path): 31 | browser_display_name = get_browser_display_name(browser) 32 | wf_items.append(['Open in Source Browser', f'Open URL in {browser_display_name}', 'sourcebrowser']) 33 | 34 | # Create WF script filter output object and emit 35 | wf = Items() 36 | for w in wf_items: 37 | wf.setItem( 38 | title=w[0], 39 | subtitle=w[1], 40 | arg=w[2] 41 | ) 42 | icon_path = f'icons/{w[2]}.png' 43 | wf.setIcon( 44 | icon_path, 45 | m_type='image' 46 | ) 47 | wf.addItem() 48 | wf.write() 49 | -------------------------------------------------------------------------------- /.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 | 28 | # Rye 29 | .rye/ 30 | rye.lock 31 | 32 | # macOS 33 | .DS_Store 34 | 35 | # PyInstaller 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv/ 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # VS Code 99 | .vscode/* 100 | !.vscode/settings.json 101 | !.vscode/tasks.json 102 | !.vscode/launch.json 103 | !.vscode/extensions.json 104 | *.code-workspace 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | -------------------------------------------------------------------------------- /src/browser_dispatcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import subprocess 5 | import sys 6 | 7 | from Alfred3 import Tools 8 | from browser_config import BROWSER_APPS 9 | 10 | 11 | def open_url_in_browser(app_path: str, url: str) -> bool: 12 | """ 13 | Opens a URL in the specified browser application 14 | 15 | Args: 16 | app_path (str): Path to the browser application (e.g., /Applications/Safari.app) 17 | url (str): URL to open 18 | 19 | Returns: 20 | bool: True if successful, False otherwise 21 | """ 22 | # Check if the app exists 23 | if not os.path.exists(app_path): 24 | Tools.log(f"Browser app not found: {app_path}") 25 | return False 26 | 27 | try: 28 | # Use 'open' command with -a flag to specify the application 29 | subprocess.run(['open', '-a', app_path, url], check=True) 30 | Tools.log(f"Successfully opened {url} in {app_path}") 31 | return True 32 | except subprocess.CalledProcessError as e: 33 | Tools.log(f"Error opening URL in browser: {e}") 34 | return False 35 | except Exception as e: 36 | Tools.log(f"Unexpected error: {e}") 37 | return False 38 | 39 | 40 | def main(): 41 | # Get the action argument passed from Alfred 42 | action = Tools.getArgv(1) 43 | 44 | if not action: 45 | Tools.log("No action provided") 46 | sys.exit(1) 47 | 48 | # Execute the action 49 | if action == "sourcebrowser": 50 | # Read URL and browser from environment variable 51 | url_with_browser = Tools.getEnv('url') 52 | 53 | if not url_with_browser: 54 | Tools.log("No URL provided in environment") 55 | sys.exit(1) 56 | 57 | # Parse URL and browser from pipe-separated string 58 | if '|' not in url_with_browser: 59 | Tools.log(f"Invalid URL format, missing browser info: {url_with_browser}") 60 | sys.exit(1) 61 | 62 | url, browser = url_with_browser.split('|', 1) 63 | 64 | Tools.log(f"URL: {url}") 65 | Tools.log(f"Browser: {browser}") 66 | 67 | # Get the app path for the browser 68 | if browser not in BROWSER_APPS: 69 | Tools.log(f"Unknown browser: {browser}") 70 | sys.exit(1) 71 | 72 | app_path = BROWSER_APPS[browser] 73 | Tools.log(f"App Path: {app_path}") 74 | 75 | # Open the URL in the specified browser 76 | success = open_url_in_browser(app_path, url) 77 | if not success: 78 | sys.exit(1) 79 | else: 80 | Tools.log(f"Unknown action: {action}") 81 | sys.exit(1) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | 87 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/browser_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Centralized browser configuration for the Alfred workflow. 5 | Contains all browser-related paths and mappings. 6 | """ 7 | 8 | # Browser application paths 9 | BROWSER_APPS = { 10 | "brave": "/Applications/Brave Browser.app", 11 | "brave_beta": "/Applications/Brave Browser Beta.app", 12 | "chromium": "/Applications/Chromium.app", 13 | "chrome": "/Applications/Google Chrome.app", 14 | "opera": "/Applications/Opera.app", 15 | "sidekick": "/Applications/Sidekick.app", 16 | "vivaldi": "/Applications/Vivaldi.app", 17 | "edge": "/Applications/Microsoft Edge.app", 18 | "arc": "/Applications/Arc.app", 19 | "dia": "/Applications/Dia.app", 20 | "thorium": "/Applications/Thorium.app", 21 | "comet": "/Applications/Comet.app", 22 | "helium": "/Applications/Helium.app", 23 | "safari": "/Applications/Safari.app" 24 | } 25 | 26 | # Browser history database paths (relative to user home directory) 27 | HISTORY_MAP = { 28 | "brave": "Library/Application Support/BraveSoftware/Brave-Browser/Default/History", 29 | "brave_beta": "Library/Application Support/BraveSoftware/Brave-Browser-Beta/Default/History", 30 | "chromium": "Library/Application Support/Chromium/Default/History", 31 | "chrome": "Library/Application Support/Google/Chrome/Default/History", 32 | "opera": "Library/Application Support/com.operasoftware.Opera/History", 33 | "sidekick": "Library/Application Support/Sidekick/Default/History", 34 | "vivaldi": "Library/Application Support/Vivaldi/Default/History", 35 | "edge": "Library/Application Support/Microsoft Edge/Default/History", 36 | "arc": "Library/Application Support/Arc/User Data/Default/History", 37 | "dia": "Library/Application Support/Dia/User Data/Default/History", 38 | "thorium": "Library/Application Support/Thorium/Default/History", 39 | "comet": "Library/Application Support/Comet/Default/History", 40 | "helium": "Library/Application Support/net.imput.helium/Default", 41 | "safari": "Library/Safari/History.db" 42 | } 43 | 44 | # Browser bookmark file paths (relative to user home directory) 45 | BOOKMARKS_MAP = { 46 | "brave": "Library/Application Support/BraveSoftware/Brave-Browser/Default/Bookmarks", 47 | "brave_beta": "Library/Application Support/BraveSoftware/Brave-Browser-Beta/Default/Bookmarks", 48 | "chrome": "Library/Application Support/Google/Chrome/Default/Bookmarks", 49 | "chromium": "Library/Application Support/Chromium/Default/Bookmarks", 50 | "opera": "Library/Application Support/com.operasoftware.Opera/Bookmarks", 51 | "sidekick": "Library/Application Support/Sidekick/Default/Bookmarks", 52 | "vivaldi": "Library/Application Support/Vivaldi/Default/Bookmarks", 53 | "edge": "Library/Application Support/Microsoft Edge/Default/Bookmarks", 54 | "arc": "Library/Application Support/Arc/User Data/Default/Bookmarks", 55 | "dia": "Library/Application Support/Dia/User Data/Default/Bookmarks", 56 | "thorium": "Library/Application Support/Thorium/Default/Bookmarks", 57 | "comet": "Library/Application Support/Comet/Default/Bookmarks", 58 | "helium": "Library/Application Support/net.imput.helium/Default/Bookmarks", 59 | "safari": "Library/Safari/Bookmarks.plist" 60 | } 61 | 62 | 63 | def get_browser_name_from_path(file_path: str, map_type: str = "history") -> str: 64 | """ 65 | Get browser name from file path by matching against the appropriate map. 66 | 67 | Args: 68 | file_path (str): Path to history/bookmark file 69 | map_type (str): Type of map to use - "history", "bookmarks", or "app" 70 | 71 | Returns: 72 | str: Browser name (e.g., 'chrome', 'brave', 'safari') or 'unknown' 73 | """ 74 | if map_type == "history": 75 | path_map = HISTORY_MAP 76 | elif map_type == "bookmarks": 77 | path_map = BOOKMARKS_MAP 78 | elif map_type == "app": 79 | path_map = BROWSER_APPS 80 | else: 81 | return "unknown" 82 | 83 | for browser_name, path in path_map.items(): 84 | if path in file_path: 85 | return browser_name 86 | 87 | return "unknown" 88 | 89 | 90 | def get_browser_display_name(browser: str) -> str: 91 | """ 92 | Convert browser key to display-friendly name. 93 | 94 | Args: 95 | browser (str): Browser key (e.g., 'brave_beta') 96 | 97 | Returns: 98 | str: Display name (e.g., 'Brave Beta') 99 | """ 100 | return browser.replace('_', ' ').title() 101 | 102 | -------------------------------------------------------------------------------- /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 | from browser_config import BOOKMARKS_MAP, get_browser_name_from_path 14 | 15 | 16 | # Show favicon in results or default wf icon 17 | show_favicon = Tools.getEnvBool("show_favicon") 18 | 19 | # Determine default search operator (AND/OR) 20 | search_operator_default = Tools.getEnv( 21 | "search_operator_default", "AND").upper() != "OR" 22 | 23 | BOOKMARKS = list() 24 | # Get Browser Histories to load based on user configuration 25 | for k in BOOKMARKS_MAP.keys(): 26 | if Tools.getEnvBool(k): 27 | BOOKMARKS.append(BOOKMARKS_MAP.get(k)) 28 | 29 | 30 | def removeDuplicates(li: list) -> list: 31 | """ 32 | Removes Duplicates from bookmark file based on URL. 33 | When same URL exists in multiple browsers, keeps first occurrence. 34 | 35 | Args: 36 | li(list): list of bookmark entries (name, url, path, browser) 37 | 38 | Returns: 39 | list: filtered bookmark entries with duplicate URLs removed 40 | """ 41 | seen_urls = {} 42 | result = [] 43 | for entry in li: 44 | url = entry[1] # URL is at index 1 45 | if url not in seen_urls: 46 | seen_urls[url] = True 47 | result.append(entry) 48 | return result 49 | 50 | 51 | def get_all_urls(the_json: str, browser: str) -> list: 52 | """ 53 | Extract all URLs, title, and folder path from Bookmark files 54 | 55 | Args: 56 | the_json (str): All Bookmarks read from file 57 | browser (str): Browser name 58 | 59 | Returns: 60 | list(tuple): List of tuple with Bookmarks (name, url, path, browser) 61 | """ 62 | def extract_data(data: dict, path: list): 63 | if isinstance(data, dict) and data.get('type') == 'url': 64 | folder_path = ' > '.join(path) if path else 'Root' 65 | urls.append({ 66 | 'name': data.get('name'), 67 | 'url': data.get('url'), 68 | 'path': folder_path 69 | }) 70 | if isinstance(data, dict) and data.get('type') == 'folder': 71 | folder_name = data.get('name', 'Unnamed Folder') 72 | the_children = data.get('children') 73 | new_path = path + [folder_name] 74 | get_container(the_children, new_path) 75 | 76 | def get_container(o: Union[list, dict], path: list = []): 77 | if isinstance(o, list): 78 | for i in o: 79 | extract_data(i, path) 80 | if isinstance(o, dict): 81 | for k, i in o.items(): 82 | # Use the key as folder name for root-level containers 83 | container_name = k.replace('_', ' ').title() if k not in [ 84 | 'children'] else '' 85 | if container_name and isinstance(i, dict) and i.get('type') == 'folder': 86 | extract_data(i, [container_name] 87 | if container_name else path) 88 | else: 89 | extract_data(i, path) 90 | 91 | urls = list() 92 | get_container(the_json) 93 | s_list_dict = sorted(urls, key=lambda k: k['name'], reverse=False) 94 | ret_list = [(l.get('name'), l.get('url'), l.get('path'), browser) 95 | for l in s_list_dict] 96 | return ret_list 97 | 98 | 99 | def paths_to_bookmarks() -> list: 100 | """ 101 | Get all valid bookmarks pahts from BOOKMARKS 102 | 103 | Returns: 104 | list: valid bookmark paths 105 | """ 106 | user_dir = os.path.expanduser('~') 107 | bms = [os.path.join(user_dir, b) for b in BOOKMARKS] 108 | valid_bms = list() 109 | for b in bms: 110 | if os.path.isfile(b): 111 | valid_bms.append(b) 112 | Tools.log(f"{b} → found") 113 | else: 114 | Tools.log(f"{b} → NOT found") 115 | 116 | return valid_bms 117 | 118 | 119 | def get_json_from_file(file: str) -> json: 120 | """ 121 | Get Bookmark JSON 122 | 123 | Args: 124 | file(str): File path to valid bookmark file 125 | 126 | Returns: 127 | str: JSON of Bookmarks 128 | """ 129 | return json.load(codecs.open(file, 'r', 'utf-8-sig'))['roots'] 130 | 131 | 132 | def extract_safari_bookmarks(bookmark_data, bookmarks_list, path=[], browser="safari") -> None: 133 | """ 134 | Recursively extract bookmarks (title, URL, path, and browser) from Safari bookmarks data. 135 | Args: 136 | bookmark_data (list or dict): The Safari bookmarks data, which can be a list or a dictionary. 137 | bookmarks_list (list): The list to which extracted bookmarks (title, URL, path, browser) will be appended. 138 | path (list): Current folder path as a list of folder names. 139 | browser (str): Browser name (default: "safari") 140 | Returns: 141 | None 142 | """ 143 | if isinstance(bookmark_data, list): 144 | for item in bookmark_data: 145 | extract_safari_bookmarks(item, bookmarks_list, path, browser) 146 | elif isinstance(bookmark_data, dict): 147 | if "Children" in bookmark_data: 148 | folder_name = bookmark_data.get("Title", "") 149 | new_path = path + [folder_name] if folder_name else path 150 | extract_safari_bookmarks( 151 | bookmark_data["Children"], bookmarks_list, new_path, browser) 152 | elif "URLString" in bookmark_data and "URIDictionary" in bookmark_data: 153 | title = bookmark_data["URIDictionary"].get("title", "Untitled") 154 | url = bookmark_data["URLString"] 155 | folder_path = ' > '.join(path) if path else 'Root' 156 | bookmarks_list.append((title, url, folder_path, browser)) 157 | 158 | 159 | def get_safari_bookmarks_json(file: str, browser: str = "safari") -> list: 160 | """ 161 | Get all bookmarks from Safari Bookmark file 162 | 163 | Args: 164 | file (str): Path to Safari Bookmark file 165 | browser (str): Browser name (default: "safari") 166 | 167 | Returns: 168 | list: List of bookmarks (title, URL, path, and browser) 169 | 170 | """ 171 | with open(file, "rb") as fp: 172 | plist = load(fp) 173 | bookmarks = [] 174 | extract_safari_bookmarks(plist, bookmarks, [], browser) 175 | return bookmarks 176 | 177 | 178 | def match(search_term: str, results: list) -> list: 179 | """ 180 | Filters a list of tuples based on a search term. 181 | Args: 182 | search_term (str): The term to search for. Can include '&' or '|' to specify AND or OR logic. 183 | results (list): A list of tuples (name, url, path, browser) to search within. 184 | Returns: 185 | list: A list of tuples that match the search term based on the specified logic. 186 | """ 187 | def is_in_tuple(tple: tuple, st: str) -> bool: 188 | # Search in name, url, path (but not browser) 189 | # Only search first 3 elements for better performance 190 | for e in tple[:3]: 191 | if st.lower() in str(e).lower(): 192 | return True # Early exit on first match 193 | return False 194 | 195 | result_lst = [] 196 | 197 | # Parse search terms once 198 | if '&' in search_term: 199 | search_terms = search_term.split('&') 200 | use_and_logic = True 201 | elif '|' in search_term: 202 | search_terms = search_term.split('|') 203 | use_and_logic = False 204 | else: 205 | search_terms = search_term.split() 206 | use_and_logic = search_operator_default 207 | 208 | # Determine check function once before loop 209 | check_func = all if use_and_logic else any 210 | 211 | for r in results: 212 | if check_func(is_in_tuple(r, ts) for ts in search_terms): 213 | result_lst.append(r) 214 | 215 | return result_lst 216 | 217 | 218 | def main(): 219 | # Log python version 220 | Tools.log("PYTHON VERSION:", sys.version) 221 | # check python > 3.7.0 222 | if sys.version_info < (3, 7): 223 | Tools.log("Python version 3.7.0 or higher required!") 224 | sys.exit(0) 225 | 226 | # Workflow item object 227 | wf = Items() 228 | query = Tools.getArgv(1) if Tools.getArgv(1) is not None else str() 229 | Tools.log(f"Search query: '{query}'") 230 | bms = paths_to_bookmarks() 231 | Tools.log(f"Found {len(bms)} bookmark file(s)") 232 | 233 | if len(bms) > 0: 234 | matches = list() 235 | # Generate list of bookmarks matches the search 236 | bookmarks = [] 237 | for bookmarks_file in bms: 238 | browser = get_browser_name_from_path(bookmarks_file, "bookmarks") 239 | if "Safari" in bookmarks_file: 240 | bookmarks = get_safari_bookmarks_json(bookmarks_file, browser) 241 | Tools.log(f"Loaded {len(bookmarks)} Safari bookmarks") 242 | # pass 243 | else: 244 | bm_json = get_json_from_file(bookmarks_file) 245 | bookmarks = get_all_urls(bm_json, browser) 246 | Tools.log( 247 | f"Loaded {len(bookmarks)} bookmarks from {bookmarks_file}") 248 | matches.extend(match(query, bookmarks)) 249 | # finally remove duplicates from all browser bookmarks 250 | matches = removeDuplicates(matches) 251 | Tools.log(f"Total matches after deduplication: {len(matches)}") 252 | # generate list of matches for Favicon download 253 | ico_matches = [] 254 | if show_favicon: 255 | ico_matches = [(i2, i1) for i1, i2, i3, i4 in matches] 256 | # Heat Favicon Cache 257 | ico = Icons(ico_matches) 258 | # generate script filter output 259 | for m in matches: 260 | url = m[1] 261 | name = m[0] if m[0] else url.split('/')[2] 262 | path = m[2] if len(m) > 2 else 'Unknown' 263 | browser = m[3] if len(m) > 3 else 'unknown' 264 | # Combine url and browser with pipe separator 265 | url_with_browser = f"{url}|{browser}" 266 | Tools.log(f"Bookmark: '{name}' | Path: '{path}' | Browser: '{browser}'") 267 | wf.setItem( 268 | title=name, 269 | subtitle=f"{url[:80]}", 270 | arg=url_with_browser, 271 | quicklookurl=url 272 | ) 273 | if show_favicon: 274 | # get favicoon for url 275 | favicon = ico.get_favion_path(url) 276 | if favicon: 277 | wf.setIcon( 278 | favicon, 279 | "image" 280 | ) 281 | wf.addMod( 282 | key='cmd', 283 | subtitle="Other Actions...", 284 | arg=url_with_browser 285 | ) 286 | wf.addMod( 287 | key="alt", 288 | subtitle=url, 289 | arg=url_with_browser 290 | ) 291 | wf.addMod( 292 | key="shift", 293 | subtitle=f"Location: {path}", 294 | arg=path 295 | ) 296 | wf.addItem() 297 | if wf.getItemsLengths() == 0: 298 | wf.setItem( 299 | title='No Bookmark found!', 300 | subtitle=f'Search "{query}" in Google...', 301 | arg=f'https://www.google.com/search?q={query}' 302 | ) 303 | wf.addItem() 304 | wf.write() 305 | 306 | 307 | if __name__ == "__main__": 308 | main() 309 | -------------------------------------------------------------------------------- /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 | from browser_config import HISTORY_MAP, get_browser_name_from_path 16 | 17 | # Get Browser Histories to load per env (true/false) 18 | HISTORIES = list() 19 | for k in HISTORY_MAP.keys(): 20 | if Tools.getEnvBool(k): 21 | HISTORIES.append(HISTORY_MAP.get(k)) 22 | 23 | # Get ignored Domains settings 24 | d = Tools.getEnv("ignored_domains", None) 25 | ignored_domains = d.split(',') if d else None 26 | 27 | # Show favicon in results or default wf icon 28 | show_favicon = Tools.getEnvBool("show_favicon") 29 | 30 | # Determine default search operator (AND/OR) 31 | search_operator_default = Tools.getEnv( 32 | "search_operator_default", "AND").upper() != "OR" 33 | 34 | # if set to true history entries will be sorted 35 | # based on recent visitied otherwise number of visits 36 | sort_recent = Tools.getEnvBool("sort_recent") 37 | 38 | # Date format settings 39 | DATE_FMT = Tools.getEnv("date_format", default='%d. %B %Y') 40 | 41 | 42 | def history_paths() -> list: 43 | """ 44 | Get valid pathes to history from HISTORIES variable 45 | 46 | Returns: 47 | list: available paths of history files 48 | """ 49 | user_dir = os.path.expanduser("~") 50 | hists = [os.path.join(user_dir, h) for h in HISTORIES] 51 | 52 | valid_hists = list() 53 | # write log if history db was found or not 54 | for h in hists: 55 | if os.path.isfile(h): 56 | valid_hists.append(h) 57 | Tools.log(f"{h} → found") 58 | else: 59 | Tools.log(f"{h} → NOT found") 60 | return valid_hists 61 | 62 | 63 | def get_histories(dbs: list, query: str = "") -> list: 64 | """ 65 | Load History files into list 66 | 67 | Args: 68 | dbs(list): list with valid history paths 69 | query(str): search query (optional, returns top 30 if empty) 70 | 71 | Returns: 72 | list: filters history entries 73 | """ 74 | 75 | results = list() 76 | with Pool(len(dbs)) as p: # Exec in ThreadPool 77 | # Pass both db path and browser name to sql function 78 | db_browser_pairs = [(db, get_browser_name_from_path(db, "history")) for db in dbs] 79 | results = p.starmap(sql, db_browser_pairs) 80 | # Flatten results using list comprehension for better performance 81 | matches = [item for r in results for item in r] 82 | 83 | # Only filter by search terms if query is provided 84 | if query: 85 | results = search_in_tuples(matches, query) 86 | else: 87 | results = matches 88 | 89 | # Remove duplicate Entries 90 | results = removeDuplicates(results) 91 | # Remove ignored domains 92 | if ignored_domains: 93 | results = remove_ignored_domains(results, ignored_domains) 94 | # Sort by element. Element 2=visits, 3=timestamp (recent) 95 | sort_by = 3 if sort_recent else 2 96 | results = Tools.sortListTuple(results, sort_by) # Sort based on visits or recency 97 | # Reduce search results to 30 AFTER sorting 98 | results = results[:30] 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, browser: 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 | browser (str): Browser name 136 | 137 | Returns: 138 | list: result list of tuples (Url, Title, VisiCount, Timestamp, Browser) 139 | """ 140 | res = [] 141 | history_db = f"/tmp/{uuid.uuid1()}" 142 | try: 143 | shutil.copy2(db, history_db) 144 | with sqlite3.connect(history_db) as c: 145 | cursor = c.cursor() 146 | # SQL satement for Safari 147 | if "Safari" in db: 148 | select_statement = f""" 149 | SELECT history_items.url, history_visits.title, history_items.visit_count,(history_visits.visit_time + 978307200) 150 | FROM history_items 151 | INNER JOIN history_visits 152 | ON history_visits.history_item = history_items.id 153 | WHERE history_items.url IS NOT NULL AND 154 | history_visits.TITLE IS NOT NULL AND 155 | history_items.url != '' order by visit_time DESC 156 | """ 157 | # SQL statement for Chromium Brothers 158 | else: 159 | select_statement = f""" 160 | SELECT DISTINCT urls.url, urls.title, urls.visit_count, (urls.last_visit_time/1000000 + (strftime('%s', '1601-01-01'))) 161 | FROM urls, visits 162 | WHERE urls.id = visits.url AND 163 | urls.title IS NOT NULL AND 164 | urls.title != '' order by last_visit_time DESC; """ 165 | Tools.log(select_statement) 166 | cursor.execute(select_statement) 167 | r = cursor.fetchall() 168 | # Add browser name to each tuple 169 | res.extend([(*row, browser) for row in r]) 170 | os.remove(history_db) # Delete History file in /tmp 171 | except sqlite3.Error as e: 172 | Tools.log(f"SQL Error: {e}") 173 | sys.exit(1) 174 | return res 175 | 176 | 177 | def get_search_terms(search: str) -> tuple: 178 | """ 179 | Explode search term string - now defaults to AND for multiple words 180 | 181 | Args: 182 | search(str): search term(s), can contain & or | for explicit operators 183 | 184 | Returns: 185 | tuple: Tuple with search terms 186 | """ 187 | # Check for explicit operators first 188 | if "&" in search: 189 | search_terms = tuple(search.split("&")) 190 | elif "|" in search: 191 | search_terms = tuple(search.split("|")) 192 | else: 193 | # Default behavior: split by spaces and treat as AND 194 | search_terms = tuple(search.split()) 195 | 196 | search_terms = [normalize("NFC", s) for s in search_terms] 197 | return search_terms 198 | 199 | 200 | def removeDuplicates(li: list) -> list: 201 | """ 202 | Removes Duplicates from history file, keeping entry with most visits. 203 | If visits are equal, keeps the most recent entry. 204 | 205 | Args: 206 | li(list): list of history entries (url, title, visits, timestamp, browser) 207 | 208 | Returns: 209 | list: filtered history entries with duplicates removed 210 | """ 211 | unique_entries = {} 212 | for url, title, visits, timestamp, browser in li: 213 | if url not in unique_entries: 214 | unique_entries[url] = (url, title, visits, timestamp, browser) 215 | else: 216 | # Keep entry with more visits, or more recent if visits are equal 217 | existing = unique_entries[url] 218 | if visits > existing[2] or (visits == existing[2] and timestamp > existing[3]): 219 | unique_entries[url] = (url, title, visits, timestamp, browser) 220 | return list(unique_entries.values()) 221 | 222 | 223 | def search_in_tuples(tuples: list, search: str) -> list: 224 | """ 225 | Search for search term in list of tuples 226 | 227 | Args: 228 | tuples(list): List contains tuple to search 229 | search(str): Search string (multiple words default to AND) 230 | 231 | Returns: 232 | list: tuple list with result of query string 233 | """ 234 | 235 | def is_in_tuple(tple: tuple, st: str) -> bool: 236 | # Search only first 4 elements (url, title, visits, timestamp) 237 | # Skip browser name at index 4 for better performance 238 | for e in tple[:4]: 239 | if st.lower() in str(e).lower(): 240 | return True # Early exit on first match 241 | return False 242 | 243 | search_terms = get_search_terms(search) 244 | result = list() 245 | 246 | # Determine search logic once before loop for better performance 247 | use_and_logic = ("&" in search) or ("|" not in search and search_operator_default) 248 | check_func = all if use_and_logic else any 249 | 250 | for t in tuples: 251 | if check_func(is_in_tuple(t, ts) for ts in search_terms): 252 | result.append(t) 253 | 254 | return result 255 | 256 | 257 | def formatTimeStamp(time_ms: int, fmt: str = '%d. %B %Y') -> str: 258 | """ 259 | Time Stamp (ms) into formatted date string 260 | 261 | Args: 262 | 263 | time_ms (int): time in ms from 01/01/1601 264 | fmt (str, optional): Format of the Date string. Defaults to '%d. %B %Y'. 265 | 266 | Returns: 267 | 268 | str: Formatted Date String 269 | """ 270 | t_string = time.strftime(fmt, time.gmtime(time_ms)) 271 | return t_string 272 | 273 | 274 | def main(): 275 | # Get wf cached directory for writing into debugger 276 | wf_cache_dir = Tools.getCacheDir() 277 | # Get wf data directory for writing into debugger 278 | wf_data_dir = Tools.getDataDir() 279 | # Check and write python version 280 | Tools.log(f"Cache Dir: {wf_cache_dir}") 281 | Tools.log(f'Data Dir: {wf_data_dir}') 282 | Tools.log("PYTHON VERSION:", sys.version) 283 | if sys.version_info < (3, 7): 284 | Tools.log("Python version 3.7.0 or higher required!") 285 | sys.exit(0) 286 | 287 | # Create Workflow items object 288 | wf = Items() 289 | search_term = Tools.getArgv(1) 290 | locked_history_dbs = history_paths() 291 | # if selected browser(s) in config was not found stop here 292 | if len(locked_history_dbs) == 0: 293 | wf.setItem( 294 | title="Browser History not found!", 295 | subtitle="Ensure Browser is installed or choose available browser(s) in CONFIGURE WORKFLOW", 296 | valid=False 297 | ) 298 | wf.addItem() 299 | wf.write() 300 | sys.exit(0) 301 | # get search results - if no search term, return top 30 items 302 | search_term = search_term if search_term else "" 303 | results = get_histories(locked_history_dbs, search_term) 304 | # if result the write alfred response 305 | if len(results) > 0: 306 | # Cache Favicons 307 | if show_favicon: 308 | ico = Icons(results) 309 | for i in results: 310 | url = i[0] 311 | # Safely extract title or domain from URL 312 | if i[1]: 313 | title = i[1] 314 | else: 315 | # Try to extract domain, fallback to full URL if it fails 316 | try: 317 | title = url.split('/')[2] 318 | except IndexError: 319 | title = url 320 | visits = i[2] 321 | last_visit = formatTimeStamp(i[3], fmt=DATE_FMT) 322 | browser = i[4] 323 | # Combine url and browser with pipe separator 324 | url_with_browser = f"{url}|{browser}" 325 | wf.setItem( 326 | title=title, 327 | subtitle=f"Last visit: {last_visit}(Visits: {visits})", 328 | arg=url_with_browser, 329 | quicklookurl=url 330 | ) 331 | if show_favicon: 332 | favicon = ico.get_favion_path(url) 333 | if favicon: 334 | wf.setIcon( 335 | favicon, 336 | "image" 337 | ) 338 | wf.addMod( 339 | key='cmd', 340 | subtitle="Other Actions...", 341 | arg=url_with_browser 342 | ) 343 | wf.addMod( 344 | key="alt", 345 | subtitle=url, 346 | arg=url_with_browser 347 | ) 348 | wf.addItem() 349 | if wf.getItemsLengths() == 0: 350 | wf.setItem( 351 | title="Nothing found in History!", 352 | subtitle=f'Search "{search_term}" in Google?', 353 | arg=f"https://www.google.com/search?q={search_term}", 354 | ) 355 | wf.addItem() 356 | wf.write() 357 | 358 | 359 | if __name__ == "__main__": 360 | main() 361 | -------------------------------------------------------------------------------- /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 | """ 578 | Unicode symbols for keyboard keys. 579 | 580 | Provides Unicode characters for common keyboard keys used in Alfred workflow 581 | displays and keyboard shortcut representations. 582 | """ 583 | CMD = u'\u2318' 584 | SHIFT = u'\u21E7' 585 | ENTER = u'\u23CE' 586 | ARROW_RIGHT = u'\u2192' 587 | 588 | 589 | class AlfJson(object): 590 | """ 591 | Alfred JSON configuration object builder. 592 | 593 | This class creates and manages Alfred workflow JSON configuration objects 594 | that can be passed between workflow elements. It handles args, config settings, 595 | and variables that need to be communicated through the Alfred workflow. 596 | 597 | The output format follows Alfred's JSON structure: 598 | { 599 | "alfredworkflow": { 600 | "arg": {...}, 601 | "config": {...}, 602 | "variables": {...} 603 | } 604 | } 605 | """ 606 | 607 | def __init__(self) -> None: 608 | self.arg: dict = dict() 609 | self.config: dict = dict() 610 | self.variables: dict = dict() 611 | 612 | def add_args(self, d) -> None: 613 | """ 614 | Add arg dictionary 615 | 616 | Args: 617 | 618 | d (dict): Key-Value pairs of args 619 | 620 | """ 621 | self.arg.update(d) 622 | 623 | def add_configs(self, d) -> None: 624 | """ 625 | Add config dictionary 626 | 627 | Args: 628 | 629 | d (dict): Key-Value pairs of configs 630 | 631 | """ 632 | self.config.update(d) 633 | 634 | def add_variables(self, d) -> None: 635 | """ 636 | Add variables dictionary 637 | 638 | Args: 639 | 640 | d (dict): Key-Value pairs of variables 641 | 642 | """ 643 | self.variables.update(d) 644 | 645 | def write_json(self) -> None: 646 | """ 647 | Write Alfred JSON config object to std out 648 | """ 649 | out = {"alfredworkflow": {"arg": self.arg, "config": self.config, "variables": self.variables}} 650 | sys.stdout.write(json.dumps(out)) 651 | -------------------------------------------------------------------------------- /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 | destinationuid 48 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5 49 | modifiers 50 | 131072 51 | modifiersubtext 52 | 53 | vitoclose 54 | 55 | 56 | 57 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39 58 | 59 | 60 | destinationuid 61 | D09358D1-C880-4E62-837E-6648A334152E 62 | modifiers 63 | 0 64 | modifiersubtext 65 | 66 | vitoclose 67 | 68 | 69 | 70 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165 71 | 72 | 73 | destinationuid 74 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2 75 | modifiers 76 | 0 77 | modifiersubtext 78 | 79 | vitoclose 80 | 81 | 82 | 83 | D09358D1-C880-4E62-837E-6648A334152E 84 | 85 | 86 | destinationuid 87 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB 88 | modifiers 89 | 0 90 | modifiersubtext 91 | 92 | vitoclose 93 | 94 | 95 | 96 | E420908F-C5F5-44DD-AB95-0B375998DC6F 97 | 98 | 99 | destinationuid 100 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9 101 | modifiers 102 | 0 103 | modifiersubtext 104 | 105 | vitoclose 106 | 107 | 108 | 109 | destinationuid 110 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39 111 | modifiers 112 | 1048576 113 | modifiersubtext 114 | 115 | vitoclose 116 | 117 | 118 | 119 | destinationuid 120 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5 121 | modifiers 122 | 131072 123 | modifiersubtext 124 | 125 | vitoclose 126 | 127 | 128 | 129 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9 130 | 131 | 132 | destinationuid 133 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2 134 | modifiers 135 | 0 136 | modifiersubtext 137 | 138 | vitoclose 139 | 140 | 141 | 142 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB 143 | 144 | 145 | destinationuid 146 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165 147 | modifiers 148 | 0 149 | modifiersubtext 150 | 151 | sourceoutputuid 152 | F19E20E4-AFB0-4DA1-84CE-D9C51949AFAA 153 | vitoclose 154 | 155 | 156 | 157 | destinationuid 158 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA 159 | modifiers 160 | 0 161 | modifiersubtext 162 | 163 | sourceoutputuid 164 | 9BFE8906-36E9-49A3-A4B7-41AC1E1A60F5 165 | vitoclose 166 | 167 | 168 | 169 | destinationuid 170 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5 171 | modifiers 172 | 0 173 | modifiersubtext 174 | 175 | sourceoutputuid 176 | 0B447EDC-254F-4D46-B2EC-24820A8265CB 177 | vitoclose 178 | 179 | 180 | 181 | destinationuid 182 | E48601B4-CA73-4C5B-9CF7-EC555651EE4D 183 | modifiers 184 | 0 185 | modifiersubtext 186 | 187 | sourceoutputuid 188 | 81E39A6C-EFCB-4BE2-A63A-CFBC6301325F 189 | vitoclose 190 | 191 | 192 | 193 | 194 | createdby 195 | Acidham 196 | description 197 | Search in Browser History and Bookmarks 198 | disabled 199 | 200 | name 201 | Chromium Bookmarks and History Search 202 | objects 203 | 204 | 205 | config 206 | 207 | browser 208 | 209 | skipqueryencode 210 | 211 | skipvarencode 212 | 213 | spaces 214 | 215 | url 216 | {var:url} 217 | 218 | type 219 | alfred.workflow.action.openurl 220 | uid 221 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2 222 | version 223 | 1 224 | 225 | 226 | config 227 | 228 | alfredfiltersresults 229 | 230 | alfredfiltersresultsmatchmode 231 | 0 232 | argumenttreatemptyqueryasnil 233 | 234 | argumenttrimmode 235 | 0 236 | argumenttype 237 | 1 238 | escaping 239 | 102 240 | keyword 241 | {var:history_keyword} 242 | queuedelaycustom 243 | 3 244 | queuedelayimmediatelyinitially 245 | 246 | queuedelaymode 247 | 1 248 | queuemode 249 | 2 250 | runningsubtext 251 | Searching, please wait... 252 | script 253 | ./py3.sh chrom_history.py "$1" 254 | 255 | scriptargtype 256 | 1 257 | scriptfile 258 | chrom_history.py 259 | subtext 260 | Search in Chromium History 261 | title 262 | Chromium History Search 263 | type 264 | 0 265 | withspace 266 | 267 | 268 | type 269 | alfred.workflow.input.scriptfilter 270 | uid 271 | A1F782AE-40F5-47BE-BA57-F230607162D5 272 | version 273 | 3 274 | 275 | 276 | config 277 | 278 | argument 279 | 280 | passthroughargument 281 | 282 | variables 283 | 284 | url 285 | {query} 286 | 287 | 288 | type 289 | alfred.workflow.utility.argument 290 | uid 291 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9 292 | version 293 | 1 294 | 295 | 296 | config 297 | 298 | concurrently 299 | 300 | escaping 301 | 102 302 | script 303 | ./py3.sh domain.py $1 304 | scriptargtype 305 | 1 306 | scriptfile 307 | 308 | type 309 | 5 310 | 311 | type 312 | alfred.workflow.action.script 313 | uid 314 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165 315 | version 316 | 2 317 | 318 | 319 | config 320 | 321 | jumpto 322 | alfred.action.url.openin 323 | path 324 | {var:url} 325 | type 326 | 2 327 | 328 | type 329 | alfred.workflow.action.actioninalfred 330 | uid 331 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA 332 | version 333 | 1 334 | 335 | 336 | config 337 | 338 | conditions 339 | 340 | 341 | inputstring 342 | 343 | matchcasesensitive 344 | 345 | matchmode 346 | 0 347 | matchstring 348 | domain 349 | outputlabel 350 | domain 351 | uid 352 | F19E20E4-AFB0-4DA1-84CE-D9C51949AFAA 353 | 354 | 355 | inputstring 356 | 357 | matchcasesensitive 358 | 359 | matchmode 360 | 0 361 | matchstring 362 | openin 363 | outputlabel 364 | openin 365 | uid 366 | 9BFE8906-36E9-49A3-A4B7-41AC1E1A60F5 367 | 368 | 369 | inputstring 370 | 371 | matchcasesensitive 372 | 373 | matchmode 374 | 0 375 | matchstring 376 | clipboard 377 | outputlabel 378 | clipboard 379 | uid 380 | 0B447EDC-254F-4D46-B2EC-24820A8265CB 381 | 382 | 383 | inputstring 384 | 385 | matchcasesensitive 386 | 387 | matchmode 388 | 0 389 | matchstring 390 | sourcebrowser 391 | outputlabel 392 | sourcebrowser 393 | uid 394 | 81E39A6C-EFCB-4BE2-A63A-CFBC6301325F 395 | 396 | 397 | elselabel 398 | else 399 | hideelse 400 | 401 | 402 | type 403 | alfred.workflow.utility.conditional 404 | uid 405 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB 406 | version 407 | 1 408 | 409 | 410 | config 411 | 412 | alfredfiltersresults 413 | 414 | alfredfiltersresultsmatchmode 415 | 0 416 | argumenttreatemptyqueryasnil 417 | 418 | argumenttrimmode 419 | 0 420 | argumenttype 421 | 2 422 | escaping 423 | 102 424 | queuedelaycustom 425 | 3 426 | queuedelayimmediatelyinitially 427 | 428 | queuedelaymode 429 | 0 430 | queuemode 431 | 1 432 | runningsubtext 433 | 434 | script 435 | ./py3.sh actions.py 436 | scriptargtype 437 | 1 438 | scriptfile 439 | 440 | subtext 441 | 442 | title 443 | 444 | type 445 | 5 446 | withspace 447 | 448 | 449 | type 450 | alfred.workflow.input.scriptfilter 451 | uid 452 | D09358D1-C880-4E62-837E-6648A334152E 453 | version 454 | 3 455 | 456 | 457 | config 458 | 459 | argument 460 | 461 | passthroughargument 462 | 463 | variables 464 | 465 | url 466 | {query} 467 | 468 | 469 | type 470 | alfred.workflow.utility.argument 471 | uid 472 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39 473 | version 474 | 1 475 | 476 | 477 | config 478 | 479 | autopaste 480 | 481 | clipboardtext 482 | {var:url} 483 | ignoredynamicplaceholders 484 | 485 | transient 486 | 487 | 488 | type 489 | alfred.workflow.output.clipboard 490 | uid 491 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5 492 | version 493 | 3 494 | 495 | 496 | config 497 | 498 | lastpathcomponent 499 | 500 | onlyshowifquerypopulated 501 | 502 | removeextension 503 | 504 | text 505 | {query} copied to the Clipboard 506 | title 507 | URL copied to Clipboard 508 | 509 | type 510 | alfred.workflow.output.notification 511 | uid 512 | 6B99D187-E195-432C-A079-D4A99D9A9B0D 513 | version 514 | 1 515 | 516 | 517 | config 518 | 519 | alfredfiltersresults 520 | 521 | alfredfiltersresultsmatchmode 522 | 0 523 | argumenttreatemptyqueryasnil 524 | 525 | argumenttrimmode 526 | 0 527 | argumenttype 528 | 1 529 | escaping 530 | 102 531 | keyword 532 | {var:bookmark_keyword} 533 | queuedelaycustom 534 | 3 535 | queuedelayimmediatelyinitially 536 | 537 | queuedelaymode 538 | 1 539 | queuemode 540 | 1 541 | runningsubtext 542 | Searching, please wait... 543 | script 544 | ./py3.sh chrom_bookmarks.py "$1" 545 | scriptargtype 546 | 1 547 | scriptfile 548 | chrom_bookmarks.py 549 | subtext 550 | Search in Chromium Bookmarks 551 | title 552 | Chromium Bookmarks Search 553 | type 554 | 0 555 | withspace 556 | 557 | 558 | type 559 | alfred.workflow.input.scriptfilter 560 | uid 561 | E420908F-C5F5-44DD-AB95-0B375998DC6F 562 | version 563 | 3 564 | 565 | 566 | config 567 | 568 | concurrently 569 | 570 | escaping 571 | 102 572 | script 573 | ./py3.sh browser_dispatcher.py $1 574 | scriptargtype 575 | 1 576 | scriptfile 577 | 578 | type 579 | 11 580 | 581 | type 582 | alfred.workflow.action.script 583 | uid 584 | E48601B4-CA73-4C5B-9CF7-EC555651EE4D 585 | version 586 | 2 587 | 588 | 589 | readme 590 | # Browser History and Bookmarks Search 591 | 592 | The Workflow searches History and Bookmarks of the configured Browsers simulatiously. 593 | 594 | ## Supported Browsers 595 | 596 | - Chromium 597 | - Google Chrome 598 | - Brave and Brave beta (Chromium) 599 | - MS Edge 600 | - Vivaldi 601 | - Opera 602 | - Sidekick 603 | - Arc 604 | - Dia 605 | - Thorium 606 | - Comet (Perplexity AI) 607 | - Safari 608 | 609 | ## Requires 610 | 611 | * Python 3 612 | * Alfred 5 613 | 614 | ## Usage 615 | 616 | ### Bookmarks Search 617 | 618 | Search History and Bookmarks: 619 | Type `&` in between of the search terms to search for multiple entries e.g.: 620 | `Car&Bike` match entries with `Car or Bike rental` but NOT `Car driving school` 621 | You can setup search operator default to change behaviour 622 | 623 | 624 | ### Other Actions 625 | 626 | Pressing `CMD` to enter `Other Actions...`: 627 | 628 | * `Copy to Clipboard`: Copies the URL into the Clipboard 629 | * `Open Domain`: Opens the domain (e.g. www.google.com) in default Browser 630 | * `Open In...`: Opens the URL with the Alfred's build in Open-In other Browser 631 | * `Open in Source Browser`: Opens the URL in the Browser of origin 632 | 633 | ### Bookmark Location 634 | 635 | When viewing bookmarks, press `SHIFT` to see the bookmark's location in your browser's folder structure. The subtitle will show the path like `Location: Bookmarks Bar > Folder > Subfolder`. Press `SHIFT + ENTER` to copy the location path to clipboard. 636 | uidata 637 | 638 | 0899A9C0-908D-4A32-9061-8DC000FAC6E2 639 | 640 | xpos 641 | 1010 642 | ypos 643 | 45 644 | 645 | 27EF6551-081A-4BF3-AE2C-3279905CA7FA 646 | 647 | xpos 648 | 1010 649 | ypos 650 | 235 651 | 652 | 6B99D187-E195-432C-A079-D4A99D9A9B0D 653 | 654 | xpos 655 | 1160 656 | ypos 657 | 370 658 | 659 | 6F7CBD9B-C876-48E2-B00E-92F71C1609A5 660 | 661 | xpos 662 | 1010 663 | ypos 664 | 370 665 | 666 | A1F782AE-40F5-47BE-BA57-F230607162D5 667 | 668 | xpos 669 | 30 670 | ypos 671 | 50 672 | 673 | AB2E1AB4-C5C4-4FBD-BE75-E0A8F8405D39 674 | 675 | xpos 676 | 305 677 | ypos 678 | 330 679 | 680 | C8B5C9B5-DF95-428D-A2CB-4DFFD4773165 681 | 682 | colorindex 683 | 3 684 | note 685 | Opens domain in standard Browser (domain.py) 686 | xpos 687 | 750 688 | ypos 689 | 115 690 | 691 | D09358D1-C880-4E62-837E-6648A334152E 692 | 693 | note 694 | Action dispatcher (actions.py) 695 | xpos 696 | 380 697 | ypos 698 | 300 699 | 700 | E420908F-C5F5-44DD-AB95-0B375998DC6F 701 | 702 | xpos 703 | 30 704 | ypos 705 | 470 706 | 707 | E48601B4-CA73-4C5B-9CF7-EC555651EE4D 708 | 709 | colorindex 710 | 3 711 | note 712 | Dispatch to source browser (browser_dispatcher.py) 713 | xpos 714 | 1010 715 | ypos 716 | 520 717 | 718 | ED77BD38-D63C-42CD-BEB6-6E3F454781C9 719 | 720 | xpos 721 | 405 722 | ypos 723 | 80 724 | 725 | F00B5055-2A7B-4DF4-AFAA-1B57E89B61DB 726 | 727 | xpos 728 | 555 729 | ypos 730 | 295 731 | 732 | 733 | userconfigurationconfig 734 | 735 | 736 | config 737 | 738 | default 739 | bh 740 | placeholder 741 | 742 | required 743 | 744 | trim 745 | 746 | 747 | description 748 | 749 | label 750 | History search keyword 751 | type 752 | textfield 753 | variable 754 | history_keyword 755 | 756 | 757 | config 758 | 759 | default 760 | bm 761 | placeholder 762 | 763 | required 764 | 765 | trim 766 | 767 | 768 | description 769 | 770 | label 771 | Bookmark search keyword 772 | type 773 | textfield 774 | variable 775 | bookmark_keyword 776 | 777 | 778 | config 779 | 780 | default 781 | 782 | required 783 | 784 | text 785 | Google Chrome 786 | 787 | description 788 | 789 | label 790 | Include 791 | type 792 | checkbox 793 | variable 794 | chrome 795 | 796 | 797 | config 798 | 799 | default 800 | 801 | required 802 | 803 | text 804 | Chromium 805 | 806 | description 807 | 808 | label 809 | 810 | type 811 | checkbox 812 | variable 813 | chromium 814 | 815 | 816 | config 817 | 818 | default 819 | 820 | required 821 | 822 | text 823 | Brave 824 | 825 | description 826 | 827 | label 828 | 829 | type 830 | checkbox 831 | variable 832 | brave 833 | 834 | 835 | config 836 | 837 | default 838 | 839 | required 840 | 841 | text 842 | Brave Beta 843 | 844 | description 845 | 846 | label 847 | 848 | type 849 | checkbox 850 | variable 851 | brave_beta 852 | 853 | 854 | config 855 | 856 | default 857 | 858 | required 859 | 860 | text 861 | Opera 862 | 863 | description 864 | 865 | label 866 | 867 | type 868 | checkbox 869 | variable 870 | opera 871 | 872 | 873 | config 874 | 875 | default 876 | 877 | required 878 | 879 | text 880 | Sidekick 881 | 882 | description 883 | 884 | label 885 | 886 | type 887 | checkbox 888 | variable 889 | sidekick 890 | 891 | 892 | config 893 | 894 | default 895 | 896 | required 897 | 898 | text 899 | Vivaldi 900 | 901 | description 902 | 903 | label 904 | 905 | type 906 | checkbox 907 | variable 908 | vivaldi 909 | 910 | 911 | config 912 | 913 | default 914 | 915 | required 916 | 917 | text 918 | Edge 919 | 920 | description 921 | 922 | label 923 | 924 | type 925 | checkbox 926 | variable 927 | edge 928 | 929 | 930 | config 931 | 932 | default 933 | 934 | required 935 | 936 | text 937 | Arc 938 | 939 | description 940 | 941 | label 942 | 943 | type 944 | checkbox 945 | variable 946 | arc 947 | 948 | 949 | config 950 | 951 | default 952 | 953 | required 954 | 955 | text 956 | Dia 957 | 958 | description 959 | 960 | label 961 | 962 | type 963 | checkbox 964 | variable 965 | dia 966 | 967 | 968 | config 969 | 970 | default 971 | 972 | required 973 | 974 | text 975 | Thorium 976 | 977 | description 978 | 979 | label 980 | 981 | type 982 | checkbox 983 | variable 984 | thorium 985 | 986 | 987 | config 988 | 989 | default 990 | 991 | required 992 | 993 | text 994 | Comet 995 | 996 | description 997 | 998 | label 999 | 1000 | type 1001 | checkbox 1002 | variable 1003 | comet 1004 | 1005 | 1006 | config 1007 | 1008 | default 1009 | 1010 | required 1011 | 1012 | text 1013 | Helium 1014 | 1015 | description 1016 | 1017 | label 1018 | 1019 | type 1020 | checkbox 1021 | variable 1022 | helium 1023 | 1024 | 1025 | config 1026 | 1027 | default 1028 | 1029 | required 1030 | 1031 | text 1032 | Safari 1033 | 1034 | description 1035 | Browsers to be included into history and bookmark search 1036 | label 1037 | 1038 | type 1039 | checkbox 1040 | variable 1041 | safari 1042 | 1043 | 1044 | config 1045 | 1046 | default 1047 | mail.google.com,mail.gmx.com 1048 | required 1049 | 1050 | trim 1051 | 1052 | verticalsize 1053 | 3 1054 | 1055 | description 1056 | Comma separated list of domains to be ignored in history search 1057 | label 1058 | Excluded Domains 1059 | type 1060 | textarea 1061 | variable 1062 | ignored_domains 1063 | 1064 | 1065 | config 1066 | 1067 | default 1068 | 1069 | required 1070 | 1071 | text 1072 | Show favicons 1073 | 1074 | description 1075 | Show favicons in results. NOTE: Displaying favicons slows down search 1076 | label 1077 | Favicons 1078 | type 1079 | checkbox 1080 | variable 1081 | show_favicon 1082 | 1083 | 1084 | config 1085 | 1086 | default 1087 | true 1088 | pairs 1089 | 1090 | 1091 | recent 1092 | true 1093 | 1094 | 1095 | visits 1096 | false 1097 | 1098 | 1099 | 1100 | description 1101 | History entries will be sorted based on recent visits OR number of visits 1102 | label 1103 | Sort history search results 1104 | type 1105 | popupbutton 1106 | variable 1107 | sort_recent 1108 | 1109 | 1110 | config 1111 | 1112 | default 1113 | %d.%m.%Y 1114 | placeholder 1115 | %d.%m.%Y 1116 | required 1117 | 1118 | trim 1119 | 1120 | 1121 | description 1122 | Define how dates should be displayed. https://strftime.org/ 1123 | label 1124 | Date Format 1125 | type 1126 | textfield 1127 | variable 1128 | date_format 1129 | 1130 | 1131 | config 1132 | 1133 | default 1134 | AND 1135 | pairs 1136 | 1137 | 1138 | AND 1139 | AND 1140 | 1141 | 1142 | OR 1143 | OR 1144 | 1145 | 1146 | 1147 | description 1148 | Sets how multiple words are combined: 1149 | AND = all terms must match (default) 1150 | OR = any term can match 1151 | (You can still use & or | in queries to override) 1152 | label 1153 | Default Search Operator 1154 | type 1155 | popupbutton 1156 | variable 1157 | search_operator_default 1158 | 1159 | 1160 | variablesdontexport 1161 | 1162 | version 1163 | 4.5.1 1164 | webaddress 1165 | https://github.com/Acidham/chromium-hist-bookmarks 1166 | 1167 | 1168 | --------------------------------------------------------------------------------