├── flox ├── version ├── settings.py ├── clipboard.py ├── browser.py ├── utils.py ├── launcher.py ├── string_matcher.py └── __init__.py ├── setup.py ├── .github └── workflows │ ├── release.yml │ └── python-publish.yml ├── LICENSE.txt ├── README.md └── .gitignore /flox/version: -------------------------------------------------------------------------------- 1 | 0.20.1 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | URL = 'https://github.com/Garulf/Flox' 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | with open("flox/version", "r") as fh: 9 | version = fh.read().strip() 10 | 11 | setup(name='Flox-lib', 12 | version=version, 13 | description='Python library to help build Flow Launcher and Wox plugins.', 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url=URL, 17 | project_urls={ 18 | "Bug Tracker": f"{URL}/issues" 19 | }, 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: Microsoft :: Windows", 24 | ], 25 | author='William McAllister', 26 | author_email='dev.garulf@gmail.com', 27 | license='MIT', 28 | packages=['flox'], 29 | zip_safe=True, 30 | include_package_data=True, 31 | package_data = { 32 | 'flox': ['version'] 33 | }) 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | tags-ignore: 7 | - 'v*' 8 | paths: 9 | - flox/version 10 | env: 11 | VERSION_FILE: './flox/version' 12 | jobs: 13 | release: 14 | if: ${{ github.ref == 'refs/heads/main' }} 15 | name: "Build" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Get version 21 | id: version 22 | run: | 23 | read -r version<${{env.VERSION_FILE}} 24 | echo "::set-output name=VERSION::$version" 25 | - name: Publish 26 | uses: softprops/action-gh-release@v1 27 | with: 28 | draft: false 29 | tag_name: "v${{steps.version.outputs.VERSION}}" 30 | generate_release_notes: true 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | publish: 34 | needs: release 35 | uses: ./.github/workflows/python-publish.yml 36 | secrets: 37 | PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN}} 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | workflow_call: 13 | secrets: 14 | PYPI_API_TOKEN: 15 | required: true 16 | workflow_dispatch: 17 | release: 18 | types: [created] 19 | 20 | jobs: 21 | deploy: 22 | 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Set up Python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: '3.x' 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install build 35 | - name: Build package 36 | run: python -m build 37 | - name: Publish package 38 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.PYPI_API_TOKEN }} 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://github.com/Garulf/Flox/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/Garulf/Flox/actions/workflows/release.yml) 2 | 3 | Depreciated in favor of pyFlowLauncher! 4 | Please see: https://github.com/garulf/pyflowlauncher 5 | 6 | # FLOX 7 | 8 | Flox is a Python library to help build Flow Launcher and Wox plugins 9 | 10 | Flox adds many useful methods to speed up plugin development 11 | 12 | Heavily inspired from the great work done by deanishe at: [deanishe/alfred-workflow](https://github.com/deanishe/alfred-workflow) 13 | 14 | ## Installation 15 | 16 | 17 | ### PIP install from pypi 18 | 19 | ``` 20 | pip install flox-lib 21 | ``` 22 | 23 | ### PIP install from github 24 | 25 | ``` 26 | pip install git+https://github.com/garulf/flox.git 27 | ``` 28 | 29 | ## Basic Usage 30 | 31 | ``` 32 | from flox import Flox 33 | 34 | import requests 35 | 36 | # have your class inherit from Flox 37 | class YourClass(Flox): 38 | 39 | def query(self, query): 40 | for _ in range(250): 41 | self.add_item( 42 | title=self.args, 43 | subtitle=str(_) 44 | ) 45 | 46 | def context_menu(self, data): 47 | self.add_item( 48 | title=data, 49 | subtitle=data 50 | ) 51 | 52 | if __name__ == "__main__": 53 | your_class = YourClass() 54 | your_class.run() 55 | ``` 56 | -------------------------------------------------------------------------------- /flox/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import json 3 | 4 | class Settings(dict): 5 | 6 | def __init__(self, filepath): 7 | super(Settings, self).__init__() 8 | self._filepath = filepath 9 | self._save = True 10 | if Path(self._filepath).exists(): 11 | self._load() 12 | else: 13 | data = {} 14 | self.update(data) 15 | self.save() 16 | 17 | 18 | def _load(self): 19 | data = {} 20 | with open(self._filepath, 'r') as f: 21 | try: 22 | data.update(json.load(f)) 23 | except json.decoder.JSONDecodeError: 24 | pass 25 | 26 | self._save = False 27 | self.update(data) 28 | self._save = True 29 | 30 | def save(self): 31 | if self._save: 32 | data = {} 33 | data.update(self) 34 | with open(self._filepath, 'w') as f: 35 | json.dump(data, f, sort_keys=True, indent=4) 36 | return 37 | 38 | def __setitem__(self, key, value): 39 | super(Settings, self).__setitem__(key, value) 40 | self.save() 41 | 42 | def __delitem__(self, key): 43 | super(Settings, self).__delitem__(key) 44 | self.save() 45 | 46 | def update(self, *args, **kwargs): 47 | super(Settings, self).update(*args, **kwargs) 48 | self.save() 49 | 50 | def setdefault(self, key, value=None): 51 | ret = super(Settings, self).setdefault(key, value) 52 | self.save() 53 | return ret 54 | -------------------------------------------------------------------------------- /flox/clipboard.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | from ctypes.wintypes import BOOL, HWND, HANDLE, HGLOBAL, UINT, LPVOID 4 | from ctypes import c_size_t as SIZE_T 5 | 6 | # Credit for code goes to Mark Ransom at https://stackoverflow.com/a/25678113 7 | 8 | OpenClipboard = ctypes.windll.user32.OpenClipboard 9 | OpenClipboard.argtypes = HWND, 10 | OpenClipboard.restype = BOOL 11 | EmptyClipboard = ctypes.windll.user32.EmptyClipboard 12 | EmptyClipboard.restype = BOOL 13 | GetClipboardData = ctypes.windll.user32.GetClipboardData 14 | GetClipboardData.argtypes = UINT, 15 | GetClipboardData.restype = HANDLE 16 | SetClipboardData = ctypes.windll.user32.SetClipboardData 17 | SetClipboardData.argtypes = UINT, HANDLE 18 | SetClipboardData.restype = HANDLE 19 | CloseClipboard = ctypes.windll.user32.CloseClipboard 20 | CloseClipboard.restype = BOOL 21 | CF_UNICODETEXT = 13 22 | 23 | GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc 24 | GlobalAlloc.argtypes = UINT, SIZE_T 25 | GlobalAlloc.restype = HGLOBAL 26 | GlobalLock = ctypes.windll.kernel32.GlobalLock 27 | GlobalLock.argtypes = HGLOBAL, 28 | GlobalLock.restype = LPVOID 29 | GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock 30 | GlobalUnlock.argtypes = HGLOBAL, 31 | GlobalSize = ctypes.windll.kernel32.GlobalSize 32 | GlobalSize.argtypes = HGLOBAL, 33 | GlobalSize.restype = SIZE_T 34 | 35 | GMEM_MOVEABLE = 0x0002 36 | GMEM_ZEROINIT = 0x0040 37 | 38 | unicode_type = type(u'') 39 | 40 | class Clipboard(object): 41 | 42 | def get(self): 43 | return get() 44 | 45 | def put(self, text): 46 | return put(text) 47 | 48 | def get(): 49 | """ 50 | Get the contents of the clipboard. 51 | """ 52 | text = None 53 | OpenClipboard(None) 54 | handle = GetClipboardData(CF_UNICODETEXT) 55 | pcontents = GlobalLock(handle) 56 | size = GlobalSize(handle) 57 | if pcontents and size: 58 | raw_data = ctypes.create_string_buffer(size) 59 | ctypes.memmove(raw_data, pcontents, size) 60 | text = raw_data.raw.decode('utf-16le').rstrip(u'\0') 61 | GlobalUnlock(handle) 62 | CloseClipboard() 63 | return text 64 | 65 | def put(s): 66 | """ 67 | Put the given string onto the clipboard. 68 | """ 69 | if not isinstance(s, unicode_type): 70 | s = s.decode('mbcs') 71 | data = s.encode('utf-16le') 72 | OpenClipboard(None) 73 | EmptyClipboard() 74 | handle = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, len(data) + 2) 75 | pcontents = GlobalLock(handle) 76 | ctypes.memmove(pcontents, data, len(data)) 77 | GlobalUnlock(handle) 78 | SetClipboardData(CF_UNICODETEXT, handle) 79 | CloseClipboard() 80 | 81 | def copy(s): 82 | put(s) -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 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 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | #Launcher files 141 | plugin.json 142 | test_plugin.py -------------------------------------------------------------------------------- /flox/browser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from subprocess import Popen, PIPE, CREATE_NO_WINDOW 3 | import webbrowser 4 | from winreg import OpenKey, QueryValueEx, HKEY_CURRENT_USER as HKCU, HKEY_LOCAL_MACHINE as HKLM 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | DEFAULT_BROWSER_KEYWORD = "*" 9 | MICROSOFT_EDGE = 'msedge' 10 | CHROME = 'chrome' 11 | FIREFOX = 'firefox' 12 | NEW_WINDOW_ARG = "--new-window" 13 | 14 | 15 | CHROME_PATH = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" 16 | FIREFOX_PATH = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\firefox.exe" 17 | MSEDGE_PATH = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe" 18 | DEFAULT_BROWSER_PATH = r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice" 19 | 20 | DEFAULT_BROWSERS = { 21 | CHROME: CHROME_PATH, 22 | FIREFOX: FIREFOX_PATH, 23 | MICROSOFT_EDGE: MSEDGE_PATH, 24 | DEFAULT_BROWSER_KEYWORD: DEFAULT_BROWSER_PATH 25 | } 26 | 27 | def get_reg(path, base_path=HKLM, name=""): 28 | try: 29 | with OpenKey(base_path, path) as key: 30 | return QueryValueEx(key, name)[0] 31 | except FileNotFoundError: 32 | log.exception(f'Can\'t find browser "{path}"') 33 | 34 | class Browser(object): 35 | 36 | def __init__(self, settings): 37 | self.Name = None 38 | self.Path = None 39 | self.PrivateArg = None 40 | self.EnablePrivate = False 41 | self.OpenInTab = True 42 | self.Editable = False 43 | self.CustomBrowserIndex = settings.get('CustomBrowserIndex', 0) 44 | self.CustomBrowserList = settings.get('CustomBrowserList', []) 45 | try: 46 | self.current_browser = self.CustomBrowserList[self.CustomBrowserIndex] 47 | except IndexError: 48 | self.current_browser = {} 49 | for item in self.current_browser: 50 | setattr(self, item, self.current_browser[item]) 51 | 52 | def open(self, url): 53 | try: 54 | cmd = [self.get_exe(), url] 55 | if self.current_browser.get('EnablePrivate', False): 56 | cmd.append(self.current_browser['PrivateArg']) 57 | if not self.OpenInTab: 58 | cmd.append(NEW_WINDOW_ARG) 59 | log.debug(f'Opening {url} with {cmd}') 60 | Popen(cmd, creationflags=CREATE_NO_WINDOW) 61 | # All else fails, open in default browser and log error 62 | except Exception as e: 63 | log.exception(f'Can\'t open {url} with {self.Name}') 64 | webbrowser.open(url) 65 | 66 | def get_exe(self): 67 | key = self.Path or DEFAULT_BROWSER_KEYWORD 68 | if key == DEFAULT_BROWSER_KEYWORD: 69 | browser = get_reg(DEFAULT_BROWSER_PATH, HKCU, 'Progid') 70 | key = browser.split('-')[0].replace('url', '').replace('HTML', '').lower() 71 | if key in DEFAULT_BROWSERS: 72 | _path = DEFAULT_BROWSERS.get(key) 73 | return get_reg(_path) 74 | else: 75 | return key -------------------------------------------------------------------------------- /flox/utils.py: -------------------------------------------------------------------------------- 1 | from tempfile import gettempdir 2 | from urllib import request 3 | from urllib.error import URLError 4 | from pathlib import Path 5 | from functools import wraps 6 | import json 7 | import os 8 | from time import time 9 | import socket 10 | from concurrent.futures import ThreadPoolExecutor 11 | import logging 12 | 13 | logging = logging.getLogger(__name__) 14 | 15 | URL_SCHEMA = [ 16 | 'http://', 17 | 'https://', 18 | ] 19 | socket.setdefaulttimeout(15) 20 | 21 | def cache(file_name:str, max_age=30, dir=gettempdir()): 22 | """ 23 | Cache decorator 24 | """ 25 | def decorator(func): 26 | @wraps(func) 27 | def wrapper(*args, **kwargs): 28 | cache_file = Path(dir, file_name) 29 | if not Path(cache_file).is_absolute(): 30 | cache_file = Path(gettempdir(), cache_file) 31 | if cache_file.exists() and file_age(cache_file) < max_age and cache_file.stat().st_size != 0: 32 | with open(cache_file, 'r', encoding='utf-8') as f: 33 | try: 34 | cache = json.load(f) 35 | except json.JSONDecodeError: 36 | logging.warning('Unable to read cache file: %s', cache_file) 37 | f.close() 38 | os.remove(cache_file) 39 | else: 40 | return cache 41 | data = func(*args, **kwargs) 42 | if data is None: 43 | return None 44 | if len(data) != 0: 45 | try: 46 | write_json(data, cache_file) 47 | except FileNotFoundError: 48 | logging.warning('Unable to write cache file: %s', cache_file) 49 | return data 50 | return wrapper 51 | return decorator 52 | 53 | def read_json(path:str): 54 | """ 55 | Read json file 56 | """ 57 | with open(path, 'r', encoding='utf-8') as f: 58 | data = json.load(f) 59 | 60 | def write_json(data, path): 61 | if not Path(path).parent.exists(): 62 | Path(path).parent.mkdir(parents=True) 63 | with open(path, 'w') as f: 64 | json.dump(data, f) 65 | 66 | def file_age(path): 67 | age = time() - path.stat().st_mtime 68 | return age 69 | 70 | def get_cache(path, max_age=0): 71 | if Path(path).exists() and file_age(path) < max_age and path.stat().st_size != 0: 72 | return read_json(path) 73 | return None 74 | 75 | def refresh_cache(file_name:str, dir:str=gettempdir()): 76 | """ 77 | Touch cache file 78 | """ 79 | cache_file = Path(dir, file_name) 80 | if cache_file.exists(): 81 | cache_file.touch() 82 | 83 | def cache_path(file_name:str, dir:str=gettempdir()): 84 | """ 85 | Return path to cache file 86 | """ 87 | return Path(dir, file_name) 88 | 89 | def remove_cache(file_name:str, dir:str=gettempdir()): 90 | """ 91 | Remove cache file 92 | """ 93 | cache_file = Path(dir, file_name) 94 | if cache_file.exists(): 95 | cache_file.unlink() 96 | 97 | def download_file(url:str, path, **kwargs): 98 | """ 99 | Download file from url and save it to dir 100 | 101 | Args: 102 | url (str): image url. 103 | dir (str): directory to save image. 104 | file_name (str): file name to save image. 105 | 106 | Keyword Args: 107 | force_download (bool): Force download image even if it exists. 108 | """ 109 | force_download = kwargs.pop('force_download', False) 110 | if not force_download and path.exists(): 111 | return 112 | try: 113 | request.urlretrieve(url, path) 114 | except URLError as e: 115 | logging.exception(f'Unable to download: {url}') 116 | return Path(path) 117 | 118 | def get_icon(url:str, path, file_name:str=None, **kwargs): 119 | for schema in URL_SCHEMA: 120 | if url.startswith(schema): 121 | break 122 | else: 123 | return url 124 | executor = kwargs.pop('executor', False) 125 | if file_name is None: 126 | file_name = url.split('/')[-1] 127 | if not Path(path).is_absolute(): 128 | path = Path(gettempdir(), path) 129 | if not path.exists(): 130 | path.mkdir(parents=True) 131 | full_path = Path(path, file_name) 132 | if not full_path.exists(): 133 | if executor is False: 134 | download_file(url, full_path) 135 | else: 136 | executor.submit(download_file, url, full_path) 137 | return full_path -------------------------------------------------------------------------------- /flox/launcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import sys 4 | from time import time 5 | 6 | """ 7 | Slightly modified wox.py credit: https://github.com/Wox-launcher/Wox 8 | """ 9 | 10 | class Launcher(object): 11 | """ 12 | Launcher python plugin base 13 | """ 14 | 15 | def run(self, debug=None): 16 | if debug: 17 | self._debug = debug 18 | self.rpc_request = {'method': 'query', 'parameters': ['']} 19 | if len(sys.argv) > 1: 20 | self.rpc_request = json.loads(sys.argv[1]) 21 | if 'settings' in self.rpc_request.keys(): 22 | self._settings = self.rpc_request['settings'] 23 | self.logger.debug('Loaded settings from RPC request') 24 | if not self._debug: 25 | self._debug = self.settings.get('debug', False) 26 | if self._debug: 27 | self.logger_level("debug") 28 | self.logger.debug(f'Request:\n{json.dumps(self.rpc_request, indent=4)}') 29 | self.logger.debug(f"Params: {self.rpc_request.get('parameters')}") 30 | # proxy is not working now 31 | # self.proxy = rpc_request.get("proxy",{}) 32 | request_method_name = self.rpc_request.get("method") 33 | #transform query and context calls to internal flox methods 34 | if request_method_name == 'query' or request_method_name == 'context_menu': 35 | request_method_name = f"_{request_method_name}" 36 | 37 | request_parameters = self.rpc_request.get("parameters") 38 | 39 | request_method = getattr(self, request_method_name) 40 | try: 41 | results = request_method(*request_parameters) or self._results 42 | except Exception as e: 43 | self.logger.exception(e) 44 | results = self.exception(e) or self._results 45 | line_break = '#' * 10 46 | ms = int((time() - self._start) * 1000) 47 | self.logger.debug(f'{line_break} Total time: {ms}ms {line_break}') 48 | if request_method_name == "_query" or request_method_name == "_context_menu": 49 | results = {"result": results} 50 | if self._settings != self.rpc_request.get('Settings') and self._settings is not None: 51 | results['SettingsChange'] = self.settings 52 | 53 | print(json.dumps(results)) 54 | 55 | def query(self,query): 56 | """ 57 | sub class need to override this method 58 | """ 59 | return [] 60 | 61 | def context_menu(self, data): 62 | """ 63 | optional context menu entries for a result 64 | """ 65 | return [] 66 | 67 | def exception(self, exception): 68 | """ 69 | exception handler 70 | """ 71 | return [] 72 | 73 | def debug(self,msg): 74 | """ 75 | alert msg 76 | """ 77 | print("DEBUG:{}".format(msg)) 78 | sys.exit() 79 | 80 | def change_query(self, query, requery=False): 81 | """ 82 | change query 83 | """ 84 | print(json.dumps({"method": f"{self.api}.ChangeQuery","parameters":[query,requery]})) 85 | 86 | def shell_run(self, cmd): 87 | """ 88 | run shell commands 89 | """ 90 | print(json.dumps({"method": f"{self.api}.ShellRun","parameters":[cmd]})) 91 | 92 | def close_app(self): 93 | """ 94 | close launcher 95 | """ 96 | print(json.dumps({"method": f"{self.api}.CloseApp","parameters":[]})) 97 | 98 | def hide_app(self): 99 | """ 100 | hide launcher 101 | """ 102 | print(json.dumps({"method": f"{self.api}.HideApp","parameters":[]})) 103 | 104 | def show_app(self): 105 | """ 106 | show launcher 107 | """ 108 | print(json.dumps({"method": f"{self.api}.ShowApp","parameters":[]})) 109 | 110 | def show_msg(self, title, sub_title, ico_path=""): 111 | """ 112 | show messagebox 113 | """ 114 | print(json.dumps({"method": f"{self.api}.ShowMsg","parameters":[title,sub_title,ico_path]})) 115 | 116 | def open_setting_dialog(self): 117 | """ 118 | open setting dialog 119 | """ 120 | self.logger.debug(json.dumps({"method": f"{self.api}.OpenSettingDialog","parameters":[]})) 121 | print(json.dumps({"method": f"{self.api}.OpenSettingDialog","parameters":[]})) 122 | 123 | def start_loadingbar(self): 124 | """ 125 | start loading animation in wox 126 | """ 127 | print(json.dumps({"method": f"{self.api}.StartLoadingBar","parameters":[]})) 128 | 129 | def stop_loadingbar(self): 130 | """ 131 | stop loading animation in wox 132 | """ 133 | print(json.dumps({"method": f"{self.api}.StopLoadingBar","parameters":[]})) 134 | 135 | def reload_plugins(self): 136 | """ 137 | reload all launcher plugins 138 | """ 139 | print(json.dumps({"method": f"{self.api}.ReloadPlugins","parameters":[]})) 140 | -------------------------------------------------------------------------------- /flox/string_matcher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | 4 | SPACE_CHAR: str = ' ' 5 | QUERY_SEARCH_PRECISION = { 6 | 'Regular': 50, 7 | 'Low': 20, 8 | 'None': 0 9 | } 10 | DEFAULT_QUERY_SEARCH_PRECISION = QUERY_SEARCH_PRECISION['Regular'] 11 | 12 | """ 13 | This is a python copy of Flow Launcher's string matcher. 14 | I take no credit for the algorithm, I just translated it to python. 15 | """ 16 | 17 | 18 | @dataclass 19 | class MatchData: 20 | """Match data""" 21 | matched: bool 22 | score_cutoff: int 23 | index_list: List[int] = field(default_factory=list) 24 | score: int = 0 25 | 26 | 27 | def string_matcher(query: str, text: str, ignore_case: bool = True, query_search_precision: int = DEFAULT_QUERY_SEARCH_PRECISION) -> MatchData: 28 | """Compare query to text""" 29 | if not text or not query: 30 | return MatchData(False, query_search_precision) 31 | 32 | query = query.strip() 33 | 34 | current_acronym_query_index = 0 35 | acronym_match_data: List[int] = [] 36 | acronyms_total_count: int = 0 37 | acronyms_matched: int = 0 38 | 39 | full_text_lower: str = text.lower() if ignore_case else text 40 | query_lower: str = query.lower() if ignore_case else query 41 | 42 | query_substrings: List[str] = query_lower.split(' ') 43 | current_query_substring_index: int = 0 44 | current_query_substring = query_substrings[current_query_substring_index] 45 | current_query_substring_char_index = 0 46 | 47 | first_match_index = -1 48 | first_match_index_in_word = -1 49 | last_match_index = 0 50 | all_query_substrings_matched: bool = False 51 | match_found_in_previous_loop: bool = False 52 | all_substrings_contained_in_text: bool = True 53 | 54 | index_list: List[int] = [] 55 | space_indices: List[int] = [] 56 | for text_index in range(len(full_text_lower)): 57 | if current_acronym_query_index >= len(query_lower) and acronyms_matched == len(query_lower): 58 | 59 | if is_acronym_count(full_text_lower, text_index): 60 | acronyms_total_count += 1 61 | continue 62 | 63 | if current_acronym_query_index >= len(query_lower) or current_acronym_query_index >= len(query_lower) and all_query_substrings_matched: 64 | break 65 | 66 | if full_text_lower[text_index] == SPACE_CHAR and current_query_substring_char_index == 0: 67 | space_indices.append(text_index) 68 | 69 | if is_acronym(text, text_index): 70 | if full_text_lower[text_index] == query_lower[current_acronym_query_index]: 71 | acronym_match_data.append(text_index) 72 | acronyms_matched += 1 73 | current_acronym_query_index += 1 74 | 75 | if is_acronym_count(text, text_index): 76 | acronyms_total_count += 1 77 | 78 | if all_query_substrings_matched or full_text_lower[text_index] != current_query_substring[current_query_substring_char_index]: 79 | match_found_in_previous_loop = False 80 | continue 81 | 82 | if first_match_index < 0: 83 | first_match_index = text_index 84 | 85 | if current_query_substring_char_index == 0: 86 | match_found_in_previous_loop = True 87 | first_match_index_in_word = text_index 88 | elif not match_found_in_previous_loop: 89 | start_index_to_verify = text_index - current_query_substring_char_index 90 | 91 | if all_previous_chars_matched(start_index_to_verify, current_query_substring_char_index, full_text_lower, current_query_substring): 92 | match_found_in_previous_loop = True 93 | first_match_index_in_word = start_index_to_verify if current_query_substring_index == 0 else first_match_index 94 | 95 | index_list = get_updated_index_list( 96 | start_index_to_verify, current_query_substring_char_index, first_match_index_in_word, index_list) 97 | 98 | last_match_index = text_index + 1 99 | index_list.append(text_index) 100 | 101 | current_query_substring_char_index += 1 102 | 103 | if current_query_substring_char_index == len(current_query_substring): 104 | all_substrings_contained_in_text = match_found_in_previous_loop and all_substrings_contained_in_text 105 | 106 | current_query_substring_index += 1 107 | 108 | all_query_substrings_matched = all_query_substrings_matched_func( 109 | current_query_substring_index, len(query_substrings)) 110 | 111 | if all_query_substrings_matched: 112 | continue 113 | 114 | current_query_substring = query_substrings[current_query_substring_index] 115 | current_query_substring_char_index = 0 116 | 117 | if acronyms_matched > 0 and acronyms_matched == len(query): 118 | acronyms_score: int = acronyms_matched * 100 / acronyms_total_count 119 | 120 | if acronyms_score >= query_search_precision: 121 | return MatchData(True, query_search_precision, acronym_match_data, acronyms_score) 122 | 123 | if all_query_substrings_matched: 124 | 125 | nearest_space_index = calculate_closest_space_index( 126 | space_indices, first_match_index) 127 | 128 | score = calculate_search_score(query, text, first_match_index - nearest_space_index - 1, 129 | space_indices, last_match_index - first_match_index, all_substrings_contained_in_text) 130 | 131 | return MatchData(True, query_search_precision, index_list, score) 132 | 133 | return MatchData(False, query_search_precision) 134 | 135 | 136 | def calculate_search_score(query: str, text: str, first_index: int, space_indices: List[int], match_length: int, all_substrings_contained_in_text: bool): 137 | score = 100 * (len(query) + 1) / ((1 + first_index) + (match_length + 1)) 138 | 139 | if first_index == 0 and all_substrings_contained_in_text: 140 | score -= len(space_indices) 141 | 142 | if (len(text) - len(query)) < 5: 143 | score += 20 144 | elif (len(text) - len(query)) < 10: 145 | score += 10 146 | 147 | if all_substrings_contained_in_text: 148 | count: int = len(query.replace(' ', '')) 149 | threshold: int = 4 150 | if count <= threshold: 151 | score += count * 10 152 | else: 153 | score += threshold * 10 + (count - threshold) * 5 154 | 155 | return score 156 | 157 | 158 | def get_updated_index_list(start_index_to_verify: int, current_query_substring_char_index: int, first_matched_index_in_word: int, index_list: List[int]): 159 | updated_list: List[int] = [] 160 | 161 | for idx, item in enumerate(index_list): 162 | if item >= first_matched_index_in_word: 163 | index_list.pop(idx) 164 | 165 | updated_list.extend(index_list) 166 | 167 | for i in range(current_query_substring_char_index): 168 | updated_list.append(start_index_to_verify + i) 169 | 170 | return updated_list 171 | 172 | 173 | def all_query_substrings_matched_func(current_query_substring_index: int, query_substrings_length: int) -> bool: 174 | return current_query_substring_index >= query_substrings_length 175 | 176 | 177 | def all_previous_chars_matched(start_index_to_verify: int, current_query_substring_char_index: int, full_text_lower: str, current_query_substring: str) -> bool: 178 | all_match = True 179 | for i in range(current_query_substring_char_index): 180 | if full_text_lower[start_index_to_verify + i] != current_query_substring[i]: 181 | all_match = False 182 | 183 | return all_match 184 | 185 | 186 | def is_acronym(text: str, text_index: int) -> bool: 187 | if is_acronym_char(text, text_index) or is_acronym_number(text, text_index): 188 | return True 189 | return False 190 | 191 | 192 | def is_acronym_count(text: str, text_index: int) -> bool: 193 | if is_acronym_char(text, text_index): 194 | return True 195 | if is_acronym_number(text, text_index): 196 | return text_index == 0 or text[text_index - 1] == SPACE_CHAR 197 | 198 | return False 199 | 200 | 201 | def is_acronym_char(text: str, text_index: int) -> bool: 202 | return text[text_index].isupper() or text_index == 0 or text[text_index - 1] == SPACE_CHAR 203 | 204 | 205 | def is_acronym_number(text: str, text_index: int) -> bool: 206 | return text[text_index].isdigit() 207 | 208 | 209 | def calculate_closest_space_index(space_indices: List[int], first_match_index: int) -> int: 210 | 211 | closest_space_index = -1 212 | 213 | for i in space_indices: 214 | if i < first_match_index: 215 | closest_space_index = i 216 | else: 217 | break 218 | 219 | return closest_space_index 220 | -------------------------------------------------------------------------------- /flox/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | import os 4 | import json 5 | import time 6 | import webbrowser 7 | import urllib.parse 8 | from datetime import date 9 | import logging 10 | import logging.handlers 11 | from pathlib import Path 12 | from typing import Union 13 | from functools import wraps, cached_property 14 | from tempfile import gettempdir 15 | 16 | from .launcher import Launcher 17 | from .browser import Browser 18 | from .settings import Settings 19 | 20 | PLUGIN_MANIFEST = 'plugin.json' 21 | FLOW_LAUNCHER_DIR_NAME = "FlowLauncher" 22 | SCOOP_FLOW_LAUNCHER_DIR_NAME = "flow-launcher" 23 | WOX_DIR_NAME = "Wox" 24 | FLOW_API = 'Flow.Launcher' 25 | WOX_API = 'Wox' 26 | APP_DIR = None 27 | USER_DIR = None 28 | LOCALAPPDATA = Path(os.getenv('LOCALAPPDATA')) 29 | APPDATA = Path(os.getenv('APPDATA')) 30 | FILE_PATH = os.path.dirname(os.path.abspath(__file__)) 31 | CURRENT_WORKING_DIR = Path(sys.argv[0]).parent.resolve() 32 | LAUNCHER_NOT_FOUND_MSG = f"Unable to locate Launcher directory\nCurrent working directory: {CURRENT_WORKING_DIR}" 33 | 34 | 35 | launcher_dir = None 36 | path = CURRENT_WORKING_DIR 37 | if SCOOP_FLOW_LAUNCHER_DIR_NAME.lower() in str(path).lower(): 38 | launcher_name = SCOOP_FLOW_LAUNCHER_DIR_NAME 39 | API = FLOW_API 40 | elif FLOW_LAUNCHER_DIR_NAME.lower() in str(path).lower(): 41 | launcher_name = FLOW_LAUNCHER_DIR_NAME 42 | API = FLOW_API 43 | elif WOX_DIR_NAME.lower() in str(path).lower(): 44 | launcher_name = WOX_DIR_NAME 45 | API = WOX_API 46 | else: 47 | raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) 48 | 49 | while True: 50 | if len(path.parts) == 1: 51 | raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) 52 | if path.joinpath('Settings').exists(): 53 | USER_DIR = path 54 | if USER_DIR.name == 'UserData': 55 | APP_DIR = USER_DIR.parent 56 | elif str(CURRENT_WORKING_DIR).startswith(str(APPDATA)): 57 | APP_DIR = LOCALAPPDATA.joinpath(launcher_name) 58 | else: 59 | raise FileNotFoundError(LAUNCHER_NOT_FOUND_MSG) 60 | break 61 | 62 | path = path.parent 63 | 64 | APP_ICONS = APP_DIR.joinpath("Images") 65 | ICON_APP = APP_DIR.joinpath('app.png') 66 | ICON_APP_ERROR = APP_DIR.joinpath(APP_ICONS, 'app_error.png') 67 | ICON_BROWSER = APP_DIR.joinpath(APP_ICONS, 'browser.png') 68 | ICON_CALCULATOR = APP_DIR.joinpath(APP_ICONS, 'calculator.png') 69 | ICON_CANCEL = APP_DIR.joinpath(APP_ICONS, 'cancel.png') 70 | ICON_CLOSE = APP_DIR.joinpath(APP_ICONS, 'close.png') 71 | ICON_CMD = APP_DIR.joinpath(APP_ICONS, 'cmd.png') 72 | ICON_COLOR = APP_DIR.joinpath('color.png') 73 | ICON_CONTROL_PANEL = APP_DIR.joinpath('ControlPanel.png') 74 | ICON_COPY = APP_DIR.joinpath('copy.png') 75 | ICON_DELETE_FILE_FOLDER = APP_DIR.joinpath('deletefilefolder.png') 76 | ICON_DISABLE = APP_DIR.joinpath('disable.png') 77 | ICON_DOWN = APP_DIR.joinpath('down.png') 78 | ICON_EXE = APP_DIR.joinpath('exe.png') 79 | ICON_FILE = APP_DIR.joinpath('file.png') 80 | ICON_FIND = APP_DIR.joinpath('find.png') 81 | ICON_FOLDER = APP_DIR.joinpath('folder.png') 82 | ICON_HISTORY = APP_DIR.joinpath('history.png') 83 | ICON_IMAGE = APP_DIR.joinpath('image.png') 84 | ICON_LOCK = APP_DIR.joinpath('lock.png') 85 | ICON_LOGOFF = APP_DIR.joinpath('logoff.png') 86 | ICON_OK = APP_DIR.joinpath('ok.png') 87 | ICON_OPEN = APP_DIR.joinpath('open.png') 88 | ICON_PICTURES = APP_DIR.joinpath('pictures.png') 89 | ICON_PLUGIN = APP_DIR.joinpath('plugin.png') 90 | ICON_PROGRAM = APP_DIR.joinpath('program.png') 91 | ICON_RECYCLEBIN = APP_DIR.joinpath('recyclebin.png') 92 | ICON_RESTART = APP_DIR.joinpath('restart.png') 93 | ICON_SEARCH = APP_DIR.joinpath('search.png') 94 | ICON_SETTINGS = APP_DIR.joinpath('settings.png') 95 | ICON_SHELL = APP_DIR.joinpath('shell.png') 96 | ICON_SHUTDOWN = APP_DIR.joinpath('shutdown.png') 97 | ICON_SLEEP = APP_DIR.joinpath('sleep.png') 98 | ICON_UP = APP_DIR.joinpath('up.png') 99 | ICON_UPDATE = APP_DIR.joinpath('update.png') 100 | ICON_URL = APP_DIR.joinpath('url.png') 101 | ICON_USER = APP_DIR.joinpath('user.png') 102 | ICON_WARNING = APP_DIR.joinpath('warning.png') 103 | ICON_WEB_SEARCH = APP_DIR.joinpath('web_search.png') 104 | ICON_WORK = APP_DIR.joinpath('work.png') 105 | 106 | 107 | class Flox(Launcher): 108 | 109 | def __init_subclass__(cls, api=API, app_dir=APP_DIR, user_dir=USER_DIR): 110 | cls._debug = False 111 | cls.appdir = APP_DIR 112 | cls.user_dir = USER_DIR 113 | cls.api = api 114 | cls._start = time.time() 115 | cls._results = [] 116 | cls._settings = None 117 | cls.font_family = '/Resources/#Segoe Fluent Icons' 118 | cls.issue_item_title = 'Report Issue' 119 | cls.issue_item_subtitle = 'Report this issue to the developer' 120 | 121 | @cached_property 122 | def browser(self): 123 | return Browser(self.app_settings) 124 | 125 | def exception(self, exception): 126 | self.exception_item(exception) 127 | self.issue_item(exception) 128 | 129 | def _query(self, query): 130 | self.args = query.lower() 131 | 132 | self.query(query) 133 | 134 | def _context_menu(self, data): 135 | self.context_menu(data) 136 | 137 | def exception_item(self, exception): 138 | self.add_item( 139 | title=exception.__class__.__name__, 140 | subtitle=str(exception), 141 | icon=ICON_APP_ERROR, 142 | method=self.change_query, 143 | dont_hide=True 144 | ) 145 | 146 | def issue_item(self, e): 147 | trace = ''.join(traceback.format_exception(type(e), value=e, tb=e.__traceback__)).replace('\n', '%0A') 148 | self.add_item( 149 | title=self.issue_item_title, 150 | subtitle=self.issue_item_subtitle, 151 | icon=ICON_BROWSER, 152 | method=self.create_github_issue, 153 | parameters=[e.__class__.__name__, trace], 154 | ) 155 | 156 | def create_github_issue(self, title, trace, log=None): 157 | url = self.manifest['Website'] 158 | if 'github' in url.lower(): 159 | issue_body = f"Please+type+any+relevant+information+here%0A%0A%0A%0A%0A%0A%3Cdetails open%3E%3Csummary%3ETrace+Log%3C%2Fsummary%3E%0A%3Cp%3E%0A%0A%60%60%60%0A{trace}%0A%60%60%60%0A%3C%2Fp%3E%0A%3C%2Fdetails%3E" 160 | url = f"{url}/issues/new?title={title}&body={issue_body}" 161 | webbrowser.open(url) 162 | 163 | def add_item(self, title:str, subtitle:str='', icon:str=None, method:Union[str, callable]=None, parameters:list=None, context:list=None, glyph:str=None, score:int=0, **kwargs): 164 | icon = icon or self.icon 165 | if not Path(icon).is_absolute(): 166 | icon = Path(self.plugindir, icon) 167 | item = { 168 | "Title": str(title), 169 | "SubTitle": str(subtitle), 170 | "IcoPath": str(icon), 171 | "ContextData": context, 172 | "Score": score, 173 | "JsonRPCAction": {} 174 | } 175 | auto_complete_text = kwargs.pop("auto_complete_text", None) 176 | 177 | item["AutoCompleteText"] = auto_complete_text or f'{self.user_keyword} {title}'.replace('* ', '') 178 | if method: 179 | item['JsonRPCAction']['method'] = getattr(method, "__name__", method) 180 | item['JsonRPCAction']['parameters'] = parameters or [] 181 | item['JsonRPCAction']['dontHideAfterAction'] = kwargs.pop("dont_hide", False) 182 | if glyph: 183 | item['Glyph'] = {} 184 | item['Glyph']['Glyph'] = glyph 185 | font_family = kwargs.pop("font_family", self.font_family) 186 | if font_family.startswith("#"): 187 | font_family = str(Path(self.plugindir).joinpath(font_family)) 188 | item['Glyph']['FontFamily'] = font_family 189 | for kw in kwargs: 190 | item[kw] = kwargs[kw] 191 | self._results.append(item) 192 | return self._results[-1] 193 | 194 | @cached_property 195 | def plugindir(self): 196 | potential_paths = [ 197 | os.path.abspath(os.getcwd()), 198 | os.path.dirname(os.path.abspath(os.path.dirname(__file__))) 199 | ] 200 | 201 | for path in potential_paths: 202 | 203 | while True: 204 | if os.path.exists(os.path.join(path, PLUGIN_MANIFEST)): 205 | return path 206 | elif os.path.ismount(path): 207 | return os.getcwd() 208 | 209 | path = os.path.dirname(path) 210 | 211 | @cached_property 212 | def manifest(self): 213 | with open(os.path.join(self.plugindir, PLUGIN_MANIFEST), 'r', encoding='utf-8') as f: 214 | return json.load(f) 215 | 216 | @cached_property 217 | def id(self): 218 | return self.manifest['ID'] 219 | 220 | @cached_property 221 | def icon(self): 222 | return self.manifest['IcoPath'] 223 | 224 | @cached_property 225 | def action_keyword(self): 226 | return self.manifest['ActionKeyword'] 227 | 228 | @cached_property 229 | def version(self): 230 | return self.manifest['Version'] 231 | 232 | @cached_property 233 | def appdata(self): 234 | # Userdata should be up two directories from plugin root 235 | return os.path.dirname(os.path.dirname(self.plugindir)) 236 | 237 | @property 238 | def app_settings(self): 239 | with open(os.path.join(self.appdata, 'Settings', 'Settings.json'), 'r', encoding='utf-8') as f: 240 | return json.load(f) 241 | 242 | @property 243 | def query_search_precision(self): 244 | return self.app_settings.get('QuerySearchPrecision', 'Regular') 245 | 246 | @cached_property 247 | def user_keywords(self): 248 | return self.app_settings['PluginSettings']['Plugins'].get(self.id, {}).get('UserKeywords', [self.action_keyword]) 249 | 250 | @cached_property 251 | def user_keyword(self): 252 | return self.user_keywords[0] 253 | 254 | @cached_property 255 | def appicon(self, icon): 256 | return os.path.join(self.appdir, 'images', icon + '.png') 257 | 258 | @property 259 | def applog(self): 260 | today = date.today().strftime('%Y-%m-%d') 261 | file = f"{today}.txt" 262 | return os.path.join(self.appdata, 'Logs', self.appversion, file) 263 | 264 | 265 | @cached_property 266 | def appversion(self): 267 | return os.path.basename(self.appdir).replace('app-', '') 268 | 269 | @cached_property 270 | def logfile(self): 271 | file = "plugin.log" 272 | return os.path.join(self.plugindir, file) 273 | 274 | @cached_property 275 | def logger(self): 276 | logger = logging.getLogger('') 277 | formatter = logging.Formatter( 278 | '%(asctime)s %(levelname)s (%(filename)s): %(message)s', 279 | datefmt='%H:%M:%S') 280 | logfile = logging.handlers.RotatingFileHandler( 281 | self.logfile, 282 | maxBytes=1024 * 2024, 283 | backupCount=1) 284 | logfile.setFormatter(formatter) 285 | logger.addHandler(logfile) 286 | logger.setLevel(logging.WARNING) 287 | return logger 288 | 289 | def logger_level(self, level): 290 | if level == "info": 291 | self.logger.setLevel(logging.INFO) 292 | elif level == "debug": 293 | self.logger.setLevel(logging.DEBUG) 294 | elif level == "warning": 295 | self.logger.setLevel(logging.WARNING) 296 | elif level == "error": 297 | self.logger.setLevel(logging.ERROR) 298 | elif level == "critical": 299 | self.logger.setLevel(logging.CRITICAL) 300 | 301 | @cached_property 302 | def api(self): 303 | launcher = os.path.basename(os.path.dirname(self.appdir)) 304 | if launcher == 'FlowLauncher': 305 | return FLOW_API 306 | else: 307 | return WOX_API 308 | 309 | @cached_property 310 | def name(self): 311 | return self.manifest['Name'] 312 | 313 | @cached_property 314 | def author(self): 315 | return self.manifest['Author'] 316 | 317 | @cached_property 318 | def settings_path(self): 319 | dirname = self.name 320 | setting_file = "Settings.json" 321 | return os.path.join(self.appdata, 'Settings', 'Plugins', dirname, setting_file) 322 | 323 | @cached_property 324 | def settings(self): 325 | if self._settings: 326 | return self._settings 327 | if not os.path.exists(os.path.dirname(self.settings_path)): 328 | os.mkdir(os.path.dirname(self.settings_path)) 329 | return Settings(self.settings_path) 330 | 331 | def browser_open(self, url): 332 | self.browser.open(url) 333 | 334 | @cached_property 335 | def python_dir(self): 336 | return self.app_settings["PluginSettings"]["PythonDirectory"] 337 | 338 | def log(self): 339 | return self.logger --------------------------------------------------------------------------------