├── .flake8 ├── .github ├── dependabot.yml └── workflows │ ├── create_release.yaml │ ├── docs.yaml │ ├── pypi.yaml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── examples │ └── guide │ │ ├── api_requests │ │ ├── example1.py │ │ └── example2.py │ │ ├── launcher_icons │ │ └── example1.py │ │ ├── plugin_methods │ │ ├── example1.py │ │ └── example2.py │ │ └── scoring_results │ │ └── example1.py ├── guide │ ├── API Requests.md │ ├── Launcher_Icons.md │ ├── Plugin methods.md │ └── scoring_results.md ├── index.md └── reference │ ├── api.md │ ├── event.md │ ├── plugin.md │ └── result.md ├── mkdocs.yml ├── pyflowlauncher ├── __init__.py ├── api.py ├── event.py ├── icons.py ├── jsonrpc.py ├── manifest.py ├── method.py ├── plugin.py ├── py.typed ├── result.py ├── settings.py ├── shared.py ├── string_matcher.py └── utils.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements-docs.txt ├── setup.py ├── tests ├── test_api.py ├── test_events.py ├── test_jsonrpc.py ├── test_method.py ├── test_plugin.py ├── test_result.py └── test_string_matcher.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 127 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '01:00' 8 | open-pull-requests-limit: 2 -------------------------------------------------------------------------------- /.github/workflows/create_release.yaml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v* 8 | 9 | env: 10 | python: 3.11 11 | 12 | jobs: 13 | test: 14 | uses: ./.github/workflows/tests.yaml 15 | release: 16 | needs: test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ env.python }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ env.python }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install --upgrade setuptools 28 | - name: Create release 29 | uses: ncipollo/release-action@v1 30 | with: 31 | prerelease: ${{ contains(github.ref_name, '-')}} 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | generateReleaseNotes: true 34 | publish: 35 | needs: release 36 | uses: ./.github/workflows/pypi.yaml 37 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | paths: 8 | - 'docs/**' 9 | - mkdocs.yml 10 | - README.md 11 | jobs: 12 | build: 13 | name: Deploy docs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout master 17 | uses: actions/checkout@v2 18 | - name: Lint examples 19 | run: | 20 | pip install tox 21 | tox -e docs 22 | - name: Deploy docs 23 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | REQUIREMENTS: ./requirements-docs.txt 27 | 28 | concurrency: 29 | group: docs 30 | cancel-in-progress: true -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Release on PYPI 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | 7 | env: 8 | DEFAULT_PYTHON: 3.11 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ env.DEFAULT_PYTHON }} 22 | 23 | - name: Install dependencies and build 24 | run: | 25 | pip install -U pip 26 | pip install setuptools twine wheel 27 | python setup.py sdist bdist_wheel 28 | 29 | - name: Verify README 30 | # https://packaging.python.org/guides/making-a-pypi-friendly-readme/#validating-restructuredtext-markup 31 | run: | 32 | python -m twine check dist/* 33 | 34 | - name: Upload builds 35 | uses: actions/upload-artifact@v3 36 | with: 37 | name: dist 38 | path: dist 39 | 40 | pypi: 41 | name: Publish to PyPI 42 | needs: build 43 | runs-on: ubuntu-latest 44 | environment: 45 | name: pypi 46 | url: https://pypi.org/p/pyflowlauncher 47 | permissions: 48 | id-token: write 49 | steps: 50 | - name: Download builds 51 | uses: actions/download-artifact@v3 52 | 53 | - name: Publish to PyPI 54 | uses: pypa/gh-action-pypi-publish@release/v1 55 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test package 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | push: 7 | paths: 8 | - pyflowlauncher/** 9 | - tests/** 10 | - tox.ini 11 | pull_request: 12 | paths: 13 | - pyflowlauncher/** 14 | - tests/** 15 | - tox.ini 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install tox tox-gh-actions 34 | - name: Test with tox 35 | run: tox 36 | 37 | concurrency: 38 | group: tests 39 | cancel-in-progress: true -------------------------------------------------------------------------------- /.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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .vscode 163 | 164 | test.py 165 | .history/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 William McAllister 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests Workflow Status](https://img.shields.io/github/actions/workflow/status/garulf/pyflowlauncher/tests.yaml?style=flat-square&label=tests)](https://github.com/Garulf/pyFlowLauncher/actions/workflows/tests.yaml) 2 | [![Docs Workflow Status](https://img.shields.io/github/actions/workflow/status/garulf/pyflowlauncher/tests.yaml?style=flat-square&label=docs)](https://github.com/Garulf/pyFlowLauncher/actions/workflows/docs.yaml) 3 | [![Release Workflow Status](https://img.shields.io/github/actions/workflow/status/garulf/pyflowlauncher/create_release.yaml?style=flat-square&label=release)](https://github.com/Garulf/pyFlowLauncher/actions/workflows/create_release.yaml) 4 | [![pypi](https://img.shields.io/pypi/v/pyflowlauncher?style=flat-square)](https://pypi.org/project/pyflowlauncher/) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyflowlauncher)](https://pypi.org/project/pyflowlauncher/) 6 | [![GitHub License](https://img.shields.io/github/license/garulf/pyflowlauncher?style=flat-square)](./LICENSE) 7 | [![buymeacoffee](https://img.shields.io/badge/buy%20me%20a%20coffee-yellow.svg?style=flat-square&logo=buymeacoffee&logoColor=000)](https://www.buymeacoffee.com/garulf) 8 | 9 | # pyFlowLauncher 10 | 11 | pyFlowLauncher is an API that allows you to quickly create plugins for Flow Launcher! 12 | 13 | ## Installation 14 | 15 | Install via pip: 16 | 17 | ```py 18 | python -m pip install pyflowlauncher[all] 19 | ``` 20 | 21 | > [!IMPORTANT] 22 | > Please use the `[all]` flag in order to support Python versions older then `3.11`. 23 | 24 | ## Usage 25 | 26 | ### Basic plugin 27 | 28 | A basic plugin using a function as the query method. 29 | 30 | ```py 31 | from pyflowlauncher import Plugin, Result, send_results 32 | from pyflowlauncher.result import ResultResponse 33 | 34 | plugin = Plugin() 35 | 36 | 37 | @plugin.on_method 38 | def query(query: str) -> ResultResponse: 39 | r = Result( 40 | Title="This is a title!", 41 | SubTitle="This is the subtitle!", 42 | IcoPath="icon.png" 43 | ) 44 | return send_results([r]) 45 | 46 | 47 | plugin.run() 48 | ``` 49 | 50 | ### Advanced plugin 51 | 52 | A more advanced usage using a `Method` class as the query method. 53 | 54 | ```py 55 | from pyflowlauncher import Plugin, Result, Method 56 | from pyflowlauncher.result import ResultResponse 57 | 58 | plugin = Plugin() 59 | 60 | 61 | class Query(Method): 62 | 63 | def __call__(self, query: str) -> ResultResponse: 64 | r = Result( 65 | Title="This is a title!", 66 | SubTitle="This is the subtitle!" 67 | ) 68 | self.add_result(r) 69 | return self.return_results() 70 | 71 | plugin.add_method(Query()) 72 | plugin.run() 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/examples/guide/api_requests/example1.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results, api 2 | from pyflowlauncher.result import ResultResponse 3 | 4 | plugin = Plugin() 5 | 6 | 7 | @plugin.on_method 8 | def query(query: str) -> ResultResponse: 9 | r = Result( 10 | Title="This is a title!", 11 | SubTitle="This is the subtitle!", 12 | IcoPath="icon.png", 13 | JsonRPCAction=api.change_query("This is a new query!"), 14 | ) 15 | return send_results([r]) 16 | 17 | 18 | plugin.run() 19 | -------------------------------------------------------------------------------- /docs/examples/guide/api_requests/example2.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results, api 2 | from pyflowlauncher.result import JsonRPCAction, ResultResponse 3 | 4 | plugin = Plugin() 5 | 6 | 7 | @plugin.on_method 8 | def example_method() -> JsonRPCAction: 9 | # Do stuff here 10 | return api.change_query("This is also a new query!") 11 | 12 | 13 | @plugin.on_method 14 | def query(query: str) -> ResultResponse: 15 | r = Result( 16 | Title="This is a title!", 17 | SubTitle="This is the subtitle!", 18 | IcoPath="icon.png", 19 | ) 20 | r.add_action(example_method) 21 | return send_results([r]) 22 | 23 | 24 | plugin.run() 25 | -------------------------------------------------------------------------------- /docs/examples/guide/launcher_icons/example1.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results 2 | from pyflowlauncher.result import ResultResponse 3 | from pyflowlauncher.icons import ADMIN 4 | 5 | plugin = Plugin() 6 | 7 | 8 | @plugin.on_method 9 | def query(query: str) -> ResultResponse: 10 | r = Result( 11 | Title="This is a title!", 12 | SubTitle="This is the subtitle!", 13 | IcoPath=ADMIN 14 | ) 15 | return send_results([r]) 16 | 17 | 18 | plugin.run() 19 | -------------------------------------------------------------------------------- /docs/examples/guide/plugin_methods/example1.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results 2 | from pyflowlauncher.result import ResultResponse 3 | 4 | plugin = Plugin() 5 | 6 | 7 | @plugin.on_method 8 | def query(query: str) -> ResultResponse: 9 | r = Result( 10 | Title="This is a title!", 11 | SubTitle="This is the subtitle!", 12 | JsonRPCAction={"method": "action", "parameters": []} 13 | ) 14 | return send_results([r]) 15 | 16 | 17 | @plugin.on_method 18 | def action(params: list[str]): 19 | pass 20 | # Do stuff here 21 | # ... 22 | 23 | 24 | plugin.run() 25 | -------------------------------------------------------------------------------- /docs/examples/guide/plugin_methods/example2.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results 2 | from pyflowlauncher.result import ResultResponse 3 | 4 | plugin = Plugin() 5 | 6 | 7 | @plugin.on_method 8 | def query(query: str) -> ResultResponse: 9 | r = Result( 10 | Title="This is a title!", 11 | SubTitle="This is the subtitle!", 12 | JsonRPCAction=plugin.action(action, ["stuff"]) 13 | ) 14 | return send_results([r]) 15 | 16 | 17 | def action(params: list[str]): 18 | pass 19 | # Do stuff here 20 | # ... 21 | 22 | 23 | plugin.run() 24 | -------------------------------------------------------------------------------- /docs/examples/guide/scoring_results/example1.py: -------------------------------------------------------------------------------- 1 | from pyflowlauncher import Plugin, Result, send_results 2 | from pyflowlauncher.result import ResultResponse 3 | from pyflowlauncher.utils import score_results 4 | 5 | plugin = Plugin() 6 | 7 | 8 | @plugin.on_method 9 | def query(query: str) -> ResultResponse: 10 | results = [] 11 | for _ in range(100): 12 | r = Result( 13 | Title="This is a title!", 14 | SubTitle="This is the subtitle!", 15 | ) 16 | results.append(r) 17 | return send_results(score_results(query, results)) 18 | 19 | 20 | plugin.run() 21 | -------------------------------------------------------------------------------- /docs/guide/API Requests.md: -------------------------------------------------------------------------------- 1 | # Sending API requests to Flow Launcher 2 | 3 | You can send special requests to Flow Launcher to control the launcher from your plugin. This communication is currently only one way. 4 | 5 | !!! warning 6 | 7 | You can not send API requests from a query or context_menu method! 8 | 9 | Using API requests in this way will cause your plugin to fail! 10 | 11 | ## Sending a command from a Result 12 | 13 | ```py 14 | --8<-- "docs/examples/guide/api_requests/example1.py" 15 | ``` 16 | 17 | The example above will change the query in Flow Launcher when the user selects your result. 18 | 19 | ## Sending a command from a Method 20 | 21 | You can also send an API request in a custom method like so: 22 | 23 | ```py 24 | --8<-- "docs/examples/guide/api_requests/example2.py" 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/guide/Launcher_Icons.md: -------------------------------------------------------------------------------- 1 | # Using icons included with Flow Launcher 2 | 3 | Flow Launcher comes with a decent amount of icons that it uses throughout it's UI and plugins. 4 | 5 | You can use some of these icons in your plugin by importing from the `icons` module. 6 | 7 | !!! warning 8 | 9 | If PyFlowLauncher is unable to locate the Flow Launcher directory these icons may not be loaded! 10 | 11 | This will not crash your plugin but will leave the icon blank. 12 | 13 | ## Example 14 | 15 | ```py 16 | --8<-- "docs/examples/guide/launcher_icons/example1.py" 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/guide/Plugin methods.md: -------------------------------------------------------------------------------- 1 | # Triggering Plugin methods 2 | 3 | Flow Launcher can call custom methods created by your plugin as well. To do so simply register the method with your plugin. 4 | 5 | You can register any Function by using the `on_method` decorator or by using the `add_method` method from `Plugin`. 6 | 7 | ## Example 1 8 | 9 | ```py 10 | --8<-- "docs/examples/guide/plugin_methods/example1.py" 11 | ``` 12 | 13 | Alternativley you can register and add the Method to a Result in one line by using `action` method. 14 | 15 | ## Example 2 16 | 17 | ```py 18 | --8<-- "docs/examples/guide/plugin_methods/example2.py" 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/guide/scoring_results.md: -------------------------------------------------------------------------------- 1 | # Score results 2 | 3 | PyFlowLauncher comes with a handy utility to help score your results in a similar manner to how Flow Launcher does internally. 4 | 5 | ## Example 6 | 7 | ```py 8 | --8<-- "docs/examples/guide/scoring_results/example1.py" 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | --8<-- "README.md" 4 | -------------------------------------------------------------------------------- /docs/reference/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ::: pyflowlauncher.api 4 | -------------------------------------------------------------------------------- /docs/reference/event.md: -------------------------------------------------------------------------------- 1 | # event 2 | 3 | ::: pyflowlauncher.event 4 | handler: python 5 | options: 6 | separate_signature: true 7 | show_signature_annotations: true 8 | -------------------------------------------------------------------------------- /docs/reference/plugin.md: -------------------------------------------------------------------------------- 1 | # Plugin Reference 2 | 3 | ::: pyflowlauncher.plugin.Plugin 4 | -------------------------------------------------------------------------------- /docs/reference/result.md: -------------------------------------------------------------------------------- 1 | # result 2 | 3 | ::: pyflowlauncher.result 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyFlowLauncher 2 | repo_url: https://github.com/Garulf/pyFlowLauncher 3 | repo_name: PyFlowLauncher 4 | 5 | 6 | 7 | theme: 8 | name: "material" 9 | features: 10 | - content.code.copy 11 | palette: 12 | 13 | # Palette toggle for dark mode 14 | - media: "(prefers-color-scheme: dark)" 15 | scheme: slate 16 | toggle: 17 | icon: material/brightness-4 18 | name: Switch to light mode 19 | 20 | # Palette toggle for light mode 21 | - media: "(prefers-color-scheme: light)" 22 | scheme: default 23 | toggle: 24 | icon: material/brightness-7 25 | name: Switch to dark mode 26 | 27 | 28 | 29 | plugins: 30 | - mkdocstrings: 31 | handlers: 32 | python: 33 | options: 34 | separate_signature: true 35 | show_signature_annotations: true 36 | show_if_no_docstring: true 37 | group_by_category: true 38 | show_category_heading: true 39 | docstring_section_style: list 40 | 41 | 42 | markdown_extensions: 43 | - admonition 44 | - pymdownx.snippets -------------------------------------------------------------------------------- /pyflowlauncher/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .plugin import Plugin 4 | from .result import JsonRPCAction, Result, send_results, ResultResponse 5 | from .method import Method 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | __all__ = [ 12 | "Plugin", 13 | "ResultResponse", 14 | "send_results", 15 | "Result", 16 | "JsonRPCAction", 17 | "Method", 18 | ] 19 | -------------------------------------------------------------------------------- /pyflowlauncher/api.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .result import JsonRPCAction 4 | 5 | NAME_SPACE = 'Flow.Launcher' 6 | 7 | 8 | def _send_action(method: str, *parameters) -> JsonRPCAction: 9 | return {"method": f"{NAME_SPACE}.{method}", "parameters": parameters} 10 | 11 | 12 | def change_query(query: str, requery: bool = False) -> JsonRPCAction: 13 | """Change the query in Flow Launcher.""" 14 | return _send_action("ChangeQuery", query, requery) 15 | 16 | 17 | def shell_run(command: str, filename: str = 'cmd.exe') -> JsonRPCAction: 18 | """Run a shell command.""" 19 | return _send_action("ShellRun", command, filename) 20 | 21 | 22 | def close_app() -> JsonRPCAction: 23 | """Close Flow Launcher.""" 24 | return _send_action("CloseApp") 25 | 26 | 27 | def hide_app() -> JsonRPCAction: 28 | """Hide Flow Launcher.""" 29 | return _send_action("HideApp") 30 | 31 | 32 | def show_app() -> JsonRPCAction: 33 | """Show Flow Launcher.""" 34 | return _send_action("ShowApp") 35 | 36 | 37 | def show_msg(title: str, sub_title: str, ico_path: str = "") -> JsonRPCAction: 38 | """Show a message in Flow Launcher.""" 39 | return _send_action("ShowMsg", title, sub_title, ico_path) 40 | 41 | 42 | def open_setting_dialog() -> JsonRPCAction: 43 | """Open the settings window in Flow Launcher.""" 44 | return _send_action("OpenSettingDialog") 45 | 46 | 47 | def start_loading_bar() -> JsonRPCAction: 48 | """Start the loading bar in Flow Launcher.""" 49 | return _send_action("StartLoadingBar") 50 | 51 | 52 | def stop_loading_bar() -> JsonRPCAction: 53 | """Stop the loading bar in Flow Launcher.""" 54 | return _send_action("StopLoadingBar") 55 | 56 | 57 | def reload_plugins() -> JsonRPCAction: 58 | """Reload the plugins in Flow Launcher.""" 59 | return _send_action("ReloadPlugins") 60 | 61 | 62 | def copy_to_clipboard(text: str, direct_copy: bool = False, show_default_notification=True) -> JsonRPCAction: 63 | """Copy text to the clipboard.""" 64 | return _send_action("CopyToClipboard", text, direct_copy, show_default_notification) 65 | 66 | 67 | def open_directory(directory_path: str, filename_or_filepath: Optional[str] = None) -> JsonRPCAction: 68 | """Open a directory.""" 69 | return _send_action("OpenDirectory", directory_path, filename_or_filepath) 70 | 71 | 72 | def open_url(url: str, in_private: bool = False) -> JsonRPCAction: 73 | """Open a URL.""" 74 | return _send_action("OpenUrl", url, in_private) 75 | 76 | 77 | def open_uri(uri: str) -> JsonRPCAction: 78 | """Open a URI.""" 79 | return _send_action("OpenAppUri", uri) 80 | -------------------------------------------------------------------------------- /pyflowlauncher/event.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Callable, Iterable, Type, Union 3 | 4 | 5 | class EventNotFound(Exception): 6 | 7 | def __init__(self, event: str): 8 | self.event = event 9 | super().__init__(f"Event '{event}' not found.") 10 | 11 | 12 | class EventHandler: 13 | 14 | def __init__(self): 15 | self._events = {} 16 | self._handlers = {} 17 | 18 | def _get_callable_name(self, method: Union[Callable[..., Any], Exception]): 19 | return getattr(method, '__name__', method.__class__.__name__).lower() 20 | 21 | def add_event(self, event: Callable[..., Any], *, name=None) -> str: 22 | key = name or self._get_callable_name(event) 23 | self._events[key] = event 24 | return key 25 | 26 | def add_events(self, events: Iterable[Callable[..., Any]]): 27 | for event in events: 28 | self.add_event(event) 29 | 30 | def add_exception_handler(self, exception: Type[Exception], handler: Callable[..., Any]): 31 | self._handlers[exception] = handler 32 | 33 | def get_event(self, event: str) -> Callable[..., Any]: 34 | try: 35 | return self._events[event] 36 | except KeyError: 37 | raise EventNotFound(event) 38 | 39 | async def _await_maybe(self, result: Any) -> Any: 40 | if asyncio.iscoroutine(result): 41 | return await result 42 | return result 43 | 44 | async def trigger_exception_handler(self, exception: Exception) -> Any: 45 | try: 46 | handler = self._handlers[exception.__class__] 47 | return await self._await_maybe(handler(exception)) 48 | except KeyError: 49 | raise exception 50 | 51 | async def trigger_event(self, event: str, *args, **kwargs) -> Any: 52 | try: 53 | result = self.get_event(event)(*args, **kwargs) 54 | return await self._await_maybe(result) 55 | except Exception as e: 56 | return await self.trigger_exception_handler(e) 57 | -------------------------------------------------------------------------------- /pyflowlauncher/icons.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | 7 | _logger = logging.getLogger(__name__) 8 | 9 | IMAGE_DIR = "Images" 10 | FLOW_PROGRAM_DIRECTORY = os.getenv("FLOW_PROGRAM_DIRECTORY", None) 11 | 12 | ENV_EXISTS: bool = FLOW_PROGRAM_DIRECTORY is not None 13 | 14 | if not ENV_EXISTS: 15 | _logger.warning("Unable to find FLOW_PROGRAM_DIRECTORY environment variable. Icons will not be loaded.") 16 | 17 | 18 | def _get_icon(icon_name: str, file_ext: str = "png") -> Optional[str]: 19 | if ENV_EXISTS: 20 | return str(Path(FLOW_PROGRAM_DIRECTORY) / IMAGE_DIR / f"{icon_name}.{file_ext}") # type: ignore 21 | return None 22 | 23 | 24 | ADMIN = _get_icon("admin") 25 | APP = _get_icon("app") 26 | APP_ERROR = _get_icon("app_error") 27 | APP_MISSING_IMG = _get_icon("app_missing_img") 28 | BAIDU = _get_icon("baidu") 29 | BING = _get_icon("bing") 30 | BOOKMARK = _get_icon("bookmark") 31 | BROWSER = _get_icon("Browser") 32 | CALCULATOR = _get_icon("calculator") 33 | CANCEL = _get_icon("cancel") 34 | CHECKUPDATE = _get_icon("checkupdate") 35 | CLOSE = _get_icon("close") 36 | CMD = _get_icon("cmd") 37 | COLOR = _get_icon("color") 38 | CONTEXT_MENU = _get_icon("context_menu") 39 | CONTROLPANEL_SMALL = _get_icon("ControlPanel_Small") 40 | COPY = _get_icon("copy") 41 | COPYLINK = _get_icon("copylink") 42 | DELETEDFOLDER = _get_icon("deletedfolder") 43 | DISABLE = _get_icon("disable") 44 | DOWN = _get_icon("down") 45 | DUCKDUCKGO = _get_icon("duckduckgo") 46 | ERROR = _get_icon("error") 47 | EVERYTHING_ERROR = _get_icon("everything_error") 48 | EXCLUDEINDEXPATH = _get_icon("excludeindexpath") 49 | EXE = _get_icon("EXE") 50 | EXPLORER = _get_icon("explorer") 51 | FACEBOOK = _get_icon("facebook") 52 | FILE = _get_icon("file") 53 | FIND = _get_icon("find") 54 | FOLDER = _get_icon("folder") 55 | GAMEMODE = _get_icon("gamemode") 56 | GIST = _get_icon("gist") 57 | GITHUB = _get_icon("github") 58 | GMAIL = _get_icon("gmail") 59 | GOOGLE = _get_icon("google") 60 | GOOGLE_DRIVE = _get_icon("google_drive") 61 | GOOGLE_MAPS = _get_icon("google_maps") 62 | GOOGLE_TRANSLATE = _get_icon("google_translate") 63 | HIBERNATE = _get_icon("hibernate") 64 | HISTORY = _get_icon("history") 65 | IMAGE = _get_icon("image") 66 | INDEX_ERROR = _get_icon("index_error") 67 | INDEX_ERROR2 = _get_icon("index_error2") 68 | INDEXOPTION = _get_icon("indexoption") 69 | LINK = _get_icon("link") 70 | LOADING = _get_icon("loading") 71 | LOCK = _get_icon("lock") 72 | LOGOFF = _get_icon("logoff") 73 | MANIFESTSITE = _get_icon("manifestsite") 74 | NETFLIX = _get_icon("netflix") 75 | NEW_MESSAGE = _get_icon("New Message") 76 | OK = _get_icon("ok") 77 | OPEN = _get_icon("open") 78 | OPENRECYCLEBIN = _get_icon("openrecyclebin") 79 | PICTURES = _get_icon("pictures") 80 | PLUGINSMANAGER = _get_icon("pluginsmanager") 81 | PROGRAM = _get_icon("program") 82 | QUICKACCESS = _get_icon("quickaccess") 83 | RECYCLEBIN = _get_icon("recyclebin") 84 | REMOVEQUICKACCESS = _get_icon("removequickaccess") 85 | REQUEST = _get_icon("request") 86 | RESTART = _get_icon("restart") 87 | RESTART_ADVANCED = _get_icon("restart_advanced") 88 | ROBOT_ERROR = _get_icon("robot_error") 89 | SEARCH = _get_icon("search") 90 | SETTINGS = _get_icon("settings") 91 | SHELL = _get_icon("shell") 92 | SHUTDOWN = _get_icon("shutdown") 93 | SLEEP = _get_icon("sleep") 94 | SOURCECODE = _get_icon("sourcecode") 95 | STACKOVERFLOW = _get_icon("stackoverflow") 96 | TWITTER = _get_icon("twitter") 97 | UP = _get_icon("up") 98 | UPDATE = _get_icon("update") 99 | URL = _get_icon("url") 100 | USER = _get_icon("user") 101 | WARNING = _get_icon("warning") 102 | WEB_SEARCH = _get_icon("web_search") 103 | WIKI = _get_icon("wiki") 104 | WINDOWSINDEXINGOPTIONS = _get_icon("windowsindexingoptions") 105 | WINDOWSSETTINGSLIGHT = _get_icon("windowssettings.light") 106 | WIZARD = _get_icon("wizard") 107 | WOLFRAMALPHA = _get_icon("wolframalpha") 108 | WORK = _get_icon("work") 109 | YAHOO = _get_icon("yahoo") 110 | YOUTUBE = _get_icon("youtube") 111 | YOUTUBEMUSIC = _get_icon("youtubemusic") 112 | -------------------------------------------------------------------------------- /pyflowlauncher/jsonrpc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import sys 5 | from typing import Any, Mapping 6 | 7 | if sys.version_info < (3, 11): 8 | from typing_extensions import NotRequired, TypedDict 9 | else: 10 | from typing import NotRequired, TypedDict 11 | 12 | 13 | class JsonRPCRequest(TypedDict): 14 | method: str 15 | parameters: list 16 | settings: NotRequired[dict[Any, Any]] 17 | 18 | 19 | class JsonRPCClient: 20 | 21 | def send(self, data: Mapping) -> None: 22 | json.dump(data, sys.stdout) 23 | 24 | def recieve(self) -> JsonRPCRequest: 25 | try: 26 | return json.loads(sys.argv[1]) 27 | except (IndexError, json.JSONDecodeError): 28 | return {'method': 'query', 'parameters': ['']} 29 | -------------------------------------------------------------------------------- /pyflowlauncher/manifest.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, Literal 2 | 3 | MANIFEST_FILE = 'plugin.json' 4 | 5 | Languages = Literal[ 6 | 'Python', 7 | 'CSharp', 8 | 'FSharp', 9 | 'Executable', 10 | 'TypeScript', 11 | 'JavaScript', 12 | ] 13 | 14 | 15 | class PluginManifestSchema(TypedDict): 16 | ID: str 17 | ActionKeyword: str 18 | Name: str 19 | Description: str 20 | Author: str 21 | Version: str 22 | Language: Languages 23 | Website: str 24 | IcoPath: str 25 | ExecuteFileName: str 26 | -------------------------------------------------------------------------------- /pyflowlauncher/method.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Any, Dict, Optional 5 | 6 | from .result import JsonRPCAction, Result, ResultResponse, send_results 7 | from .shared import logger 8 | 9 | 10 | class Method(ABC): 11 | 12 | def __init__(self) -> None: 13 | self._logger = logger(self) 14 | self._results: list[Result] = [] 15 | 16 | def add_result(self, result: Result) -> None: 17 | self._results.append(result) 18 | 19 | def return_results(self, settings: Optional[Dict[str, Any]] = None) -> ResultResponse: 20 | return send_results(self._results, settings) 21 | 22 | @abstractmethod 23 | def __call__(self, *args, **kwargs) -> ResultResponse | JsonRPCAction: 24 | pass 25 | -------------------------------------------------------------------------------- /pyflowlauncher/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from functools import wraps 5 | from typing import Any, Callable, Iterable, Optional, Type, Union 6 | from pathlib import Path 7 | import json 8 | import asyncio 9 | 10 | from pyflowlauncher.shared import logger 11 | 12 | from .event import EventHandler 13 | from .jsonrpc import JsonRPCClient 14 | from .result import JsonRPCAction, ResultResponse 15 | from .manifest import PluginManifestSchema, MANIFEST_FILE 16 | 17 | Method = Callable[..., Union[ResultResponse, JsonRPCAction, None]] 18 | 19 | 20 | class Plugin: 21 | 22 | def __init__(self, methods: list[Method] | None = None) -> None: 23 | self._logger = logger(self) 24 | self._client = JsonRPCClient() 25 | self._event_handler = EventHandler() 26 | self._settings: dict[str, Any] = {} 27 | if methods: 28 | self.add_methods(methods) 29 | 30 | def add_method(self, method: Method) -> str: 31 | """Add a method to the event handler.""" 32 | return self._event_handler.add_event(method) 33 | 34 | def add_methods(self, methods: Iterable[Method]) -> None: 35 | self._event_handler.add_events(methods) 36 | 37 | def on_method(self, method: Method) -> Method: 38 | @wraps(method) 39 | def wrapper(*args, **kwargs): 40 | return method(*args, **kwargs) 41 | self._event_handler.add_event(wrapper) 42 | return wrapper 43 | 44 | def method(self, method: Method) -> Method: 45 | """Register a method to be called when the plugin is run.""" 46 | return self.on_method(method) 47 | 48 | def add_exception_handler(self, exception: Type[Exception], handler: Callable[..., Any]) -> None: 49 | """Add exception handler to be called when an exception is raised in a method.""" 50 | self._event_handler.add_exception_handler(exception, handler) 51 | 52 | def on_except(self, exception: Type[Exception]) -> Callable[..., Any]: 53 | @wraps(exception) 54 | def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]: 55 | self.add_exception_handler(exception, handler) 56 | return handler 57 | return wrapper 58 | 59 | def action(self, method: Method, parameters: Optional[Iterable] = None) -> JsonRPCAction: 60 | """Register a method and return a JsonRPCAction that calls it.""" 61 | method_name = self.add_method(method) 62 | return {"method": method_name, "parameters": parameters or []} 63 | 64 | @property 65 | def settings(self) -> dict: 66 | if self._settings is None: 67 | self._settings = {} 68 | self._settings = self._client.recieve().get('settings', {}) 69 | return self._settings 70 | 71 | def run(self) -> None: 72 | request = self._client.recieve() 73 | method = request["method"] 74 | parameters = request.get('parameters', []) 75 | if sys.version_info >= (3, 10, 0): 76 | feedback = asyncio.run(self._event_handler.trigger_event(method, *parameters)) 77 | else: 78 | loop = asyncio.get_event_loop() 79 | feedback = loop.run_until_complete(self._event_handler.trigger_event(method, *parameters)) 80 | if not feedback: 81 | return 82 | self._client.send(feedback) 83 | 84 | @property 85 | def run_dir(self) -> Path: 86 | """Return the run directory of the plugin.""" 87 | return Path(sys.argv[0]).parent 88 | 89 | def root_dir(self) -> Path: 90 | """Return the root directory of the plugin.""" 91 | current_dir = self.run_dir 92 | for part in current_dir.parts: 93 | if current_dir.joinpath(MANIFEST_FILE).exists(): 94 | return current_dir 95 | current_dir = current_dir.parent 96 | raise FileNotFoundError(f"Could not find {MANIFEST_FILE} in {self.run_dir} or any parent directory.") 97 | 98 | def manifest(self) -> PluginManifestSchema: 99 | """Return the plugin manifest.""" 100 | with open(self.root_dir() / MANIFEST_FILE, 'r', encoding='utf-8') as f: 101 | manifest = json.load(f) 102 | return manifest 103 | -------------------------------------------------------------------------------- /pyflowlauncher/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Garulf/pyFlowLauncher/bb5e74949deac8044d77a68e63cd8d64d3a802df/pyflowlauncher/py.typed -------------------------------------------------------------------------------- /pyflowlauncher/result.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union 7 | 8 | if sys.version_info < (3, 11): 9 | from typing_extensions import NotRequired, TypedDict 10 | else: 11 | from typing import NotRequired, TypedDict 12 | 13 | 14 | if TYPE_CHECKING: 15 | from .plugin import Method 16 | 17 | 18 | class JsonRPCAction(TypedDict): 19 | """Flow Launcher JsonRPCAction""" 20 | method: str 21 | parameters: Iterable 22 | dontHideAfterAction: NotRequired[bool] 23 | 24 | 25 | class Glyph(TypedDict): 26 | """Flow Launcher Glyph""" 27 | Glyph: str 28 | FontFamily: str 29 | 30 | 31 | class PreviewInfo(TypedDict): 32 | """Flow Launcher Preview section""" 33 | PreviewImagePath: Optional[str] 34 | Description: Optional[str] 35 | IsMedia: bool 36 | PreviewDeligate: Optional[str] 37 | 38 | 39 | @dataclass 40 | class Result: 41 | Title: str 42 | SubTitle: Optional[str] = None 43 | IcoPath: Optional[Union[str, Path]] = None 44 | Score: int = 0 45 | JsonRPCAction: Optional[JsonRPCAction] = None 46 | ContextData: Optional[Iterable] = None 47 | Glyph: Optional[Glyph] = None 48 | CopyText: Optional[str] = None 49 | AutoCompleteText: Optional[str] = None 50 | RoundedIcon: bool = False 51 | Preview: Optional[PreviewInfo] = None 52 | TitleHighlightData: Optional[List[int]] = None 53 | 54 | def as_dict(self) -> Dict[str, Any]: 55 | return self.__dict__ 56 | 57 | def add_action(self, method: Method, 58 | parameters: Optional[Iterable[Any]] = None, 59 | *, 60 | dont_hide_after_action: bool = False) -> None: 61 | self.JsonRPCAction = { 62 | "method": method.__name__, 63 | "parameters": parameters or [], 64 | "dontHideAfterAction": dont_hide_after_action 65 | } 66 | 67 | 68 | class ResultResponse(TypedDict): 69 | result: List[Dict[str, Any]] 70 | SettingsChange: NotRequired[Optional[Dict[str, Any]]] 71 | 72 | 73 | def send_results(results: Iterable[Result], settings: Optional[Dict[str, Any]] = None) -> ResultResponse: 74 | """Formats and returns results as a JsonRPCResponse""" 75 | return {'result': [result.as_dict() for result in results], 'SettingsChange': settings} 76 | -------------------------------------------------------------------------------- /pyflowlauncher/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | from .jsonrpc import JsonRPCClient 3 | 4 | 5 | def settings() -> Dict[str, Any]: 6 | """Retrieve the settings from Flow Launcher.""" 7 | return JsonRPCClient().recieve().get('settings', {}) 8 | -------------------------------------------------------------------------------- /pyflowlauncher/shared.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | _logger = logging.getLogger(__name__) 7 | 8 | 9 | def logger(obj: Any) -> logging.Logger: 10 | module_file = sys.modules[obj.__module__].__file__ 11 | if module_file is not None: 12 | module_name = Path(module_file).stem 13 | return logging.getLogger(f"{module_name}.{obj.__class__.__name__}") 14 | return logging.getLogger(f"{obj.__module__}.{obj.__name__}") 15 | -------------------------------------------------------------------------------- /pyflowlauncher/string_matcher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from functools import lru_cache 3 | from typing import List 4 | from enum import IntEnum 5 | 6 | SPACE_CHAR: str = ' ' 7 | REGULAR_SEARCH_PRECISION: int = 50 8 | LOW_SEARCH_PRECISION: int = 20 9 | NONE_SEARCH_PRECISION: int = 0 10 | 11 | 12 | class SearchPrecision(IntEnum): 13 | REGULAR = REGULAR_SEARCH_PRECISION 14 | LOW = LOW_SEARCH_PRECISION 15 | NONE = NONE_SEARCH_PRECISION 16 | 17 | 18 | DEFAULT_QUERY_SEARCH_PRECISION = SearchPrecision.REGULAR 19 | 20 | """ 21 | This is a python copy of Flow Launcher's string matcher. 22 | I take no credit for the algorithm, I just translated it to python. 23 | """ 24 | 25 | 26 | @dataclass 27 | class MatchData: 28 | """Match data""" 29 | matched: bool 30 | score_cutoff: int 31 | index_list: List[int] = field(default_factory=list) 32 | score: int = 0 33 | 34 | 35 | @lru_cache(maxsize=128) 36 | def string_matcher(query: str, text: str, ignore_case: bool = True, 37 | query_search_precision: int = DEFAULT_QUERY_SEARCH_PRECISION) -> MatchData: 38 | """Compare query to text""" 39 | if not text or not query: 40 | return MatchData(False, query_search_precision) 41 | 42 | query = query.strip() 43 | 44 | current_acronym_query_index = 0 45 | acronym_match_data: List[int] = [] 46 | acronyms_total_count: int = 0 47 | acronyms_matched: int = 0 48 | 49 | full_text_lower: str = text.lower() if ignore_case else text 50 | query_lower: str = query.lower() if ignore_case else query 51 | 52 | query_substrings: List[str] = query_lower.split(' ') 53 | current_query_substring_index: int = 0 54 | current_query_substring = query_substrings[current_query_substring_index] 55 | current_query_substring_char_index = 0 56 | 57 | first_match_index = -1 58 | first_match_index_in_word = -1 59 | last_match_index = 0 60 | all_query_substrings_matched: bool = False 61 | match_found_in_previous_loop: bool = False 62 | all_substrings_contained_in_text: bool = True 63 | 64 | index_list: List[int] = [] 65 | space_indices: List[int] = [] 66 | for text_index in range(len(full_text_lower)): 67 | if (current_acronym_query_index >= len(query_lower) or 68 | (current_acronym_query_index >= len(query_lower) and all_query_substrings_matched)): 69 | break 70 | 71 | if full_text_lower[text_index] == SPACE_CHAR and current_query_substring_char_index == 0: 72 | space_indices.append(text_index) 73 | 74 | if is_acronym(text, text_index): 75 | if full_text_lower[text_index] == query_lower[current_acronym_query_index]: 76 | acronym_match_data.append(text_index) 77 | acronyms_matched += 1 78 | current_acronym_query_index += 1 79 | 80 | if is_acronym_count(text, text_index): 81 | acronyms_total_count += 1 82 | 83 | if all_query_substrings_matched or (full_text_lower[text_index] 84 | != current_query_substring[current_query_substring_char_index]): 85 | match_found_in_previous_loop = False 86 | continue 87 | 88 | if first_match_index < 0: 89 | first_match_index = text_index 90 | 91 | if current_query_substring_char_index == 0: 92 | match_found_in_previous_loop = True 93 | first_match_index_in_word = text_index 94 | elif not match_found_in_previous_loop: 95 | start_index_to_verify = text_index - current_query_substring_char_index 96 | 97 | if all_previous_chars_matched(start_index_to_verify, current_query_substring_char_index, full_text_lower, 98 | current_query_substring): 99 | match_found_in_previous_loop = True 100 | first_match_index_in_word = start_index_to_verify if current_query_substring_index == 0 else first_match_index 101 | 102 | index_list = get_updated_index_list( 103 | start_index_to_verify, current_query_substring_char_index, first_match_index_in_word, index_list) 104 | 105 | last_match_index = text_index + 1 106 | index_list.append(text_index) 107 | 108 | current_query_substring_char_index += 1 109 | 110 | if current_query_substring_char_index == len(current_query_substring): 111 | all_substrings_contained_in_text = match_found_in_previous_loop and all_substrings_contained_in_text 112 | 113 | current_query_substring_index += 1 114 | 115 | all_query_substrings_matched = all_query_substrings_matched_func( 116 | current_query_substring_index, len(query_substrings)) 117 | 118 | if all_query_substrings_matched: 119 | continue 120 | 121 | current_query_substring = query_substrings[current_query_substring_index] 122 | current_query_substring_char_index = 0 123 | 124 | if acronyms_matched > 0 and acronyms_matched == len(query): 125 | acronyms_score: int = int(acronyms_matched * 100 / acronyms_total_count) 126 | 127 | if acronyms_score >= query_search_precision: 128 | return MatchData(True, query_search_precision, acronym_match_data, acronyms_score) 129 | 130 | if all_query_substrings_matched: 131 | 132 | nearest_space_index = calculate_closest_space_index( 133 | space_indices, first_match_index) 134 | 135 | score: float = calculate_search_score(query, text, first_match_index - nearest_space_index - 1, 136 | space_indices, last_match_index - first_match_index, 137 | all_substrings_contained_in_text) 138 | 139 | return MatchData(True, query_search_precision, index_list, int(score)) 140 | 141 | return MatchData(False, query_search_precision) 142 | 143 | 144 | def calculate_search_score(query: str, text: str, first_index: int, space_indices: List[int], 145 | match_length: int, all_substrings_contained_in_text: bool): 146 | score = 100 * (len(query) + 1) / ((1 + first_index) + (match_length + 1)) 147 | 148 | if first_index == 0 and all_substrings_contained_in_text: 149 | score -= len(space_indices) 150 | 151 | if (len(text) - len(query)) < 5: 152 | score += 20 153 | elif (len(text) - len(query)) < 10: 154 | score += 10 155 | 156 | if all_substrings_contained_in_text: 157 | count: int = len(query.replace(' ', '')) 158 | threshold: int = 4 159 | if count <= threshold: 160 | score += count * 10 161 | else: 162 | score += threshold * 10 + (count - threshold) * 5 163 | 164 | return score 165 | 166 | 167 | def get_updated_index_list(start_index_to_verify: int, 168 | current_query_substring_char_index: int, 169 | first_matched_index_in_word: int, index_list: List[int]): 170 | updated_list: List[int] = [] 171 | 172 | for idx, item in enumerate(index_list): 173 | if item >= first_matched_index_in_word: 174 | index_list.pop(idx) 175 | 176 | updated_list.extend(index_list) 177 | 178 | for i in range(current_query_substring_char_index): 179 | updated_list.append(start_index_to_verify + i) 180 | 181 | return updated_list 182 | 183 | 184 | def all_query_substrings_matched_func(current_query_substring_index: int, query_substrings_length: int) -> bool: 185 | return current_query_substring_index >= query_substrings_length 186 | 187 | 188 | def all_previous_chars_matched(start_index_to_verify: int, 189 | current_query_substring_char_index: int, 190 | full_text_lower: str, current_query_substring: str) -> bool: 191 | all_match = True 192 | for i in range(current_query_substring_char_index): 193 | if full_text_lower[start_index_to_verify + i] != current_query_substring[i]: 194 | all_match = False 195 | 196 | return all_match 197 | 198 | 199 | def is_acronym(text: str, text_index: int) -> bool: 200 | if is_acronym_char(text, text_index) or is_acronym_number(text, text_index): 201 | return True 202 | return False 203 | 204 | 205 | def is_acronym_count(text: str, text_index: int) -> bool: 206 | if is_acronym_char(text, text_index): 207 | return True 208 | if is_acronym_number(text, text_index): 209 | return text_index == 0 or text[text_index - 1] == SPACE_CHAR 210 | 211 | return False 212 | 213 | 214 | def is_acronym_char(text: str, text_index: int) -> bool: 215 | return text[text_index].isupper() or text_index == 0 or text[text_index - 1] == SPACE_CHAR 216 | 217 | 218 | def is_acronym_number(text: str, text_index: int) -> bool: 219 | return text[text_index].isdigit() 220 | 221 | 222 | def calculate_closest_space_index(space_indices: List[int], first_match_index: int) -> int: 223 | 224 | closest_space_index = -1 225 | 226 | for i in space_indices: 227 | if i < first_match_index: 228 | closest_space_index = i 229 | else: 230 | break 231 | 232 | return closest_space_index 233 | -------------------------------------------------------------------------------- /pyflowlauncher/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Iterable 2 | from .result import Result 3 | from .string_matcher import string_matcher, DEFAULT_QUERY_SEARCH_PRECISION as DEFAULT_PRECISION 4 | 5 | 6 | def score_results( 7 | query: str, 8 | results: Iterable[Result], 9 | score_cutoff: int = DEFAULT_PRECISION, 10 | match_on_empty_query: bool = False, 11 | ) -> Generator[Result, None, None]: 12 | for result in results: 13 | match = string_matcher( 14 | query, 15 | result.Title, 16 | query_search_precision=score_cutoff 17 | ) 18 | if match.matched or (match_on_empty_query and not query): 19 | result.TitleHighlightData = match.index_list 20 | result.Score = match.score 21 | yield result 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | 4 | [project] 5 | name = "pyflowlauncher" 6 | authors = [ 7 | {name = "William McAllister", email = "dev.garulf@gmail.com"} 8 | ] 9 | version = '0.9.1' 10 | description = "Python library to help build Flow Launcher plugins." 11 | readme = "README.md" 12 | requires-python = ">=3.8" 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Typing :: Typed" 22 | ] 23 | dependencies = [ 24 | 'typing_extensions>=4.8.0; python_version < "3.11"' 25 | ] 26 | 27 | [project.optional-dependencies] 28 | all = ['typing_extensions>=4.8.0'] 29 | 30 | [tool.setuptools] 31 | packages = ["pyflowlauncher"] 32 | 33 | [tool.setuptools.package-data] 34 | "pyflowlauncher" = ["py.typed"] 35 | 36 | [tool.bumpversion] 37 | current_version = "0.9.1" 38 | parse = """(?x) 39 | (?P[0-9]+) 40 | \\.(?P[0-9]+) 41 | \\.(?P[0-9]+) 42 | (?: 43 | -(?P