diff --git a/docs/examples/guide/api_requests/example1.py b/docs/examples/guide/api_requests/example1.py index 49b6c63..d0162af 100644 --- a/docs/examples/guide/api_requests/example1.py +++ b/docs/examples/guide/api_requests/example1.py @@ -1,16 +1,16 @@ from pyflowlauncher import Plugin, Result, send_results, api -from pyflowlauncher.result import ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse plugin = Plugin() @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", - IcoPath="icon.png", - JsonRPCAction=api.change_query("This is a new query!"), + title="This is a title!", + subtitle="This is the subtitle!", + icon="icon.png", + json_rpc_action=api.change_query("This is a new query!"), ) return send_results([r]) diff --git a/docs/examples/guide/api_requests/example2.py b/docs/examples/guide/api_requests/example2.py index b7e5374..2f2529f 100644 --- a/docs/examples/guide/api_requests/example2.py +++ b/docs/examples/guide/api_requests/example2.py @@ -1,21 +1,21 @@ from pyflowlauncher import Plugin, Result, send_results, api -from pyflowlauncher.result import JsonRPCAction, ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse plugin = Plugin() @plugin.on_method -def example_method() -> JsonRPCAction: +def example_method() -> JsonRPCResponse: # Do stuff here return api.change_query("This is also a new query!") @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", - IcoPath="icon.png", + title="This is a title!", + subtitle="This is the subtitle!", + icon="icon.png", ) r.add_action(example_method) return send_results([r]) diff --git a/docs/examples/guide/launcher_icons/example1.py b/docs/examples/guide/launcher_icons/example1.py index 9a23808..39d9c88 100644 --- a/docs/examples/guide/launcher_icons/example1.py +++ b/docs/examples/guide/launcher_icons/example1.py @@ -1,16 +1,16 @@ from pyflowlauncher import Plugin, Result, send_results -from pyflowlauncher.result import ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse from pyflowlauncher.icons import ADMIN plugin = Plugin() @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", - IcoPath=ADMIN + title="This is a title!", + subtitle="This is the subtitle!", + icon=ADMIN ) return send_results([r]) diff --git a/docs/examples/guide/plugin_methods/example1.py b/docs/examples/guide/plugin_methods/example1.py index 190f8f1..5328afc 100644 --- a/docs/examples/guide/plugin_methods/example1.py +++ b/docs/examples/guide/plugin_methods/example1.py @@ -1,15 +1,15 @@ from pyflowlauncher import Plugin, Result, send_results -from pyflowlauncher.result import ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse plugin = Plugin() @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", - JsonRPCAction={"method": "action", "parameters": []} + title="This is a title!", + subtitle="This is the subtitle!", + json_rpc_action={"Method": "action", "Parameters": []} ) return send_results([r]) diff --git a/docs/examples/guide/plugin_methods/example2.py b/docs/examples/guide/plugin_methods/example2.py index 023cd65..6e7dcb1 100644 --- a/docs/examples/guide/plugin_methods/example2.py +++ b/docs/examples/guide/plugin_methods/example2.py @@ -1,16 +1,16 @@ from pyflowlauncher import Plugin, Result, send_results -from pyflowlauncher.result import ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse plugin = Plugin() @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", - JsonRPCAction=plugin.action(action, ["stuff"]) + title="This is a title!", + subtitle="This is the subtitle!", ) + r.add_action(action, ["stuff"]) return send_results([r]) diff --git a/docs/examples/guide/scoring_results/example1.py b/docs/examples/guide/scoring_results/example1.py index 38bf7be..2b1255c 100644 --- a/docs/examples/guide/scoring_results/example1.py +++ b/docs/examples/guide/scoring_results/example1.py @@ -1,17 +1,17 @@ from pyflowlauncher import Plugin, Result, send_results -from pyflowlauncher.result import ResultResponse +from pyflowlauncher.models.json_rpc import JsonRPCResponse from pyflowlauncher.utils import score_results plugin = Plugin() @plugin.on_method -def query(query: str) -> ResultResponse: +def query(query: str) -> JsonRPCResponse: results = [] for _ in range(100): r = Result( - Title="This is a title!", - SubTitle="This is the subtitle!", + title="This is a title!", + subtitle="This is the subtitle!", ) results.append(r) return send_results(score_results(query, results)) diff --git a/pyflowlauncher/__init__.py b/pyflowlauncher/__init__.py index 22bbdfa..346b7ca 100644 --- a/pyflowlauncher/__init__.py +++ b/pyflowlauncher/__init__.py @@ -1,7 +1,7 @@ import logging from .plugin import Plugin -from .result import JsonRPCAction, Result, send_results, ResultResponse +from .result import Result, send_results from .method import Method @@ -10,9 +10,7 @@ __all__ = [ "Plugin", - "ResultResponse", "send_results", "Result", - "JsonRPCAction", "Method", ] diff --git a/pyflowlauncher/api.py b/pyflowlauncher/api.py index f0b36cb..37359e3 100644 --- a/pyflowlauncher/api.py +++ b/pyflowlauncher/api.py @@ -1,79 +1,79 @@ from typing import Optional -from .result import JsonRPCAction +from .models.json_rpc import JsonRPCRequest NAME_SPACE = 'Flow.Launcher' -def _send_action(method: str, *parameters) -> JsonRPCAction: - return {"method": f"{NAME_SPACE}.{method}", "parameters": parameters} +def _send_action(method: str, *parameters) -> JsonRPCRequest: + return {"Method": f"{NAME_SPACE}.{method}", "Parameters": list(parameters)} -def change_query(query: str, requery: bool = False) -> JsonRPCAction: +def change_query(query: str, requery: bool = False) -> JsonRPCRequest: """Change the query in Flow Launcher.""" return _send_action("ChangeQuery", query, requery) -def shell_run(command: str, filename: str = 'cmd.exe') -> JsonRPCAction: +def shell_run(command: str, filename: str = 'cmd.exe') -> JsonRPCRequest: """Run a shell command.""" return _send_action("ShellRun", command, filename) -def close_app() -> JsonRPCAction: +def close_app() -> JsonRPCRequest: """Close Flow Launcher.""" return _send_action("CloseApp") -def hide_app() -> JsonRPCAction: +def hide_app() -> JsonRPCRequest: """Hide Flow Launcher.""" return _send_action("HideApp") -def show_app() -> JsonRPCAction: +def show_app() -> JsonRPCRequest: """Show Flow Launcher.""" return _send_action("ShowApp") -def show_msg(title: str, sub_title: str, ico_path: str = "") -> JsonRPCAction: +def show_msg(title: str, sub_title: str, ico_path: str = "") -> JsonRPCRequest: """Show a message in Flow Launcher.""" return _send_action("ShowMsg", title, sub_title, ico_path) -def open_setting_dialog() -> JsonRPCAction: +def open_setting_dialog() -> JsonRPCRequest: """Open the settings window in Flow Launcher.""" return _send_action("OpenSettingDialog") -def start_loading_bar() -> JsonRPCAction: +def start_loading_bar() -> JsonRPCRequest: """Start the loading bar in Flow Launcher.""" return _send_action("StartLoadingBar") -def stop_loading_bar() -> JsonRPCAction: +def stop_loading_bar() -> JsonRPCRequest: """Stop the loading bar in Flow Launcher.""" return _send_action("StopLoadingBar") -def reload_plugins() -> JsonRPCAction: +def reload_plugins() -> JsonRPCRequest: """Reload the plugins in Flow Launcher.""" return _send_action("ReloadPlugins") -def copy_to_clipboard(text: str, direct_copy: bool = False, show_default_notification=True) -> JsonRPCAction: +def copy_to_clipboard(text: str, direct_copy: bool = False, show_default_notification=True) -> JsonRPCRequest: """Copy text to the clipboard.""" return _send_action("CopyToClipboard", text, direct_copy, show_default_notification) -def open_directory(directory_path: str, filename_or_filepath: Optional[str] = None) -> JsonRPCAction: +def open_directory(directory_path: str, filename_or_filepath: Optional[str] = None) -> JsonRPCRequest: """Open a directory.""" return _send_action("OpenDirectory", directory_path, filename_or_filepath) -def open_url(url: str, in_private: bool = False) -> JsonRPCAction: +def open_url(url: str, in_private: bool = False) -> JsonRPCRequest: """Open a URL.""" return _send_action("OpenUrl", url, in_private) -def open_uri(uri: str) -> JsonRPCAction: +def open_uri(uri: str) -> JsonRPCRequest: """Open a URI.""" return _send_action("OpenAppUri", uri) diff --git a/pyflowlauncher/base.py b/pyflowlauncher/base.py new file mode 100644 index 0000000..1af97a8 --- /dev/null +++ b/pyflowlauncher/base.py @@ -0,0 +1,23 @@ +import logging + + +def _setup_file_logger() -> logging.Logger: + log = logging.getLogger('pyflowlauncher') + if not log.handlers: + handler = logging.FileHandler('plugin.log') + handler.setLevel(logging.WARNING) + handler.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')) + log.addHandler(handler) + log.setLevel(logging.WARNING) + log.propagate = False + return log + + +_setup_file_logger() + + +class pyFlowLauncherObject: + """Base class for all pyFlowLauncher objects.""" + + def __init__(self): + self.logger = logging.getLogger(f'pyflowlauncher.{self.__class__.__name__}') diff --git a/pyflowlauncher/manifest.py b/pyflowlauncher/manifest.py index a7bb457..3635a2c 100644 --- a/pyflowlauncher/manifest.py +++ b/pyflowlauncher/manifest.py @@ -1,25 +1,78 @@ -from typing import TypedDict, Literal - -MANIFEST_FILE = 'plugin.json' - -Languages = Literal[ - 'Python', - 'CSharp', - 'FSharp', - 'Executable', - 'TypeScript', - 'JavaScript', -] - - -class PluginManifestSchema(TypedDict): - ID: str - ActionKeyword: str - Name: str - Description: str - Author: str - Version: str - Language: Languages - Website: str - IcoPath: str - ExecuteFileName: str +from __future__ import annotations + +from dataclasses import dataclass, field +import json +from pathlib import Path +import sys + +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self + +from .models.plugin_manifest import FILE_NAME, PluginMetadata + + +@dataclass +class Manifest: + id: str + name: str + author: str + version: str + language: str + description: str + website: str + execute_file_name: str + ico_path: str + action_keyword: str + action_keywords: list[str] = field(default_factory=list) + + @classmethod + def from_json(cls, json_data: PluginMetadata) -> Self: + """Create a Manifest instance from a JSON dictionary.""" + return cls( + id=json_data['ID'], + name=json_data['Name'], + author=json_data['Author'], + version=json_data['Version'], + language=json_data['Language'], + description=json_data['Description'], + website=json_data['Website'], + execute_file_name=json_data['ExecuteFileName'], + ico_path=json_data['IcoPath'], + action_keyword=json_data['ActionKeyword'], + action_keywords=json_data.get('ActionKeywords', []), + ) + + @classmethod + def from_file(cls, path: Path) -> Self: + """Load the manifest from a JSON file.""" + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + return cls.from_json(data) + + @classmethod + def from_dir(cls, dir_path: Path) -> Self: + """Load the manifest from a directory containing a plugin.json file.""" + return cls.from_file(Path(dir_path) / FILE_NAME) + + def to_json(self) -> PluginMetadata: + """Convert the manifest to a JSON-serializable dictionary.""" + return { + "ID": self.id, + "Name": self.name, + "Author": self.author, + "Version": self.version, + "Language": self.language, + "Description": self.description, + "Website": self.website, + "ExecuteFileName": self.execute_file_name, + "IcoPath": self.ico_path, + "ActionKeyword": self.action_keyword, + "ActionKeywords": self.action_keywords, + } + + def save(self, path: Path) -> None: + """Save the manifest to a JSON file.""" + with open(path, 'w', encoding='utf-8') as f: + json.dump(self.to_json(), f, ensure_ascii=False, indent=4) diff --git a/pyflowlauncher/method.py b/pyflowlauncher/method.py index 50d3e0a..5ecff59 100644 --- a/pyflowlauncher/method.py +++ b/pyflowlauncher/method.py @@ -3,22 +3,23 @@ from abc import ABC, abstractmethod from typing import Any, Dict, Optional -from .result import JsonRPCAction, Result, ResultResponse, send_results -from .shared import logger +from .models.json_rpc import JsonRPCRequest, JsonRPCResponse +from .result import Result, send_results +from .base import pyFlowLauncherObject -class Method(ABC): +class Method(pyFlowLauncherObject, ABC): def __init__(self) -> None: - self._logger = logger(self) + super().__init__() self._results: list[Result] = [] def add_result(self, result: Result) -> None: self._results.append(result) - def return_results(self, settings: Optional[Dict[str, Any]] = None) -> ResultResponse: + def return_results(self, settings: Optional[Dict[str, Any]] = None) -> JsonRPCResponse: return send_results(self._results, settings) @abstractmethod - def __call__(self, *args, **kwargs) -> ResultResponse | JsonRPCAction: + def __call__(self, *args, **kwargs) -> JsonRPCResponse | JsonRPCRequest: pass diff --git a/pyflowlauncher/models/__init__.py b/pyflowlauncher/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyflowlauncher/models/json_rpc.py b/pyflowlauncher/models/json_rpc.py new file mode 100644 index 0000000..830cc34 --- /dev/null +++ b/pyflowlauncher/models/json_rpc.py @@ -0,0 +1,25 @@ +import sys +from typing import Any, Dict, List, Optional + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict +else: + from typing import NotRequired, TypedDict + +from .result import Result + + +class JsonRPCRequest(TypedDict): + Method: str + Parameters: List[str] + DontHideAfterAction: NotRequired[bool] + + +class JsonRPCResponse(TypedDict): + Result: List[Result] + SettingsChange: NotRequired[Optional[Dict[str, Any]]] + + +class JsonRPCResult(Result): + JsonRPCAction: NotRequired[JsonRPCRequest] + SettingsChange: NotRequired[Dict[str, Any]] diff --git a/pyflowlauncher/models/plugin_manifest.py b/pyflowlauncher/models/plugin_manifest.py new file mode 100644 index 0000000..c4a792e --- /dev/null +++ b/pyflowlauncher/models/plugin_manifest.py @@ -0,0 +1,24 @@ +import sys +from typing import List + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict +else: + from typing import NotRequired, TypedDict + + +FILE_NAME = 'plugin.json' + + +class PluginMetadata(TypedDict): + ID: str + Name: str + Author: str + Version: str + Language: str + Description: str + Website: str + ExecuteFileName: str + IcoPath: str + ActionKeyword: str + ActionKeywords: NotRequired[List[str]] diff --git a/pyflowlauncher/models/result.py b/pyflowlauncher/models/result.py new file mode 100644 index 0000000..12b443d --- /dev/null +++ b/pyflowlauncher/models/result.py @@ -0,0 +1,35 @@ +import sys +from typing import Optional, Iterable + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict, Required +else: + from typing import NotRequired, TypedDict, Required + + +class Glyph(TypedDict): + """Flow Launcher Glyph""" + Glyph: str + FontFamily: str + + +class PreviewInfo(TypedDict): + """Flow Launcher Preview section""" + PreviewImagePath: Optional[str] + Description: Optional[str] + IsMedia: bool + PreviewDeligate: Optional[str] + + +class Result(TypedDict, total=False): + Title: Required[str] + SubTitle: NotRequired[str] + IcoPath: NotRequired[str] + Score: NotRequired[int] + ContextData: NotRequired[Iterable] + Glyph: NotRequired[Glyph] + CopyText: NotRequired[str] + AutoCompleteText: NotRequired[str] + RoundedIcon: NotRequired[bool] + Preview: NotRequired[PreviewInfo] + TitleHighlightData: NotRequired[Iterable[int]] diff --git a/pyflowlauncher/plugin.py b/pyflowlauncher/plugin.py index 9f26ee6..09458e5 100644 --- a/pyflowlauncher/plugin.py +++ b/pyflowlauncher/plugin.py @@ -1,26 +1,26 @@ from __future__ import annotations import sys -from functools import wraps -from typing import Any, Callable, Iterable, Optional, Type, Union +from functools import cached_property, wraps +from typing import Any, Callable, Iterable, Optional, Type, List from pathlib import Path -import json import asyncio -from pyflowlauncher.shared import logger +from .base import pyFlowLauncherObject from .event import EventHandler -from .jsonrpc import JsonRPCClient -from .result import JsonRPCAction, ResultResponse -from .manifest import PluginManifestSchema, MANIFEST_FILE +from .jsonrpc import JsonRPCClient, JsonRPCRequest +from .models.plugin_manifest import FILE_NAME +from .manifest import Manifest -Method = Callable[..., Union[ResultResponse, JsonRPCAction, None]] +Method = Callable[..., Any] -class Plugin: + +class Plugin(pyFlowLauncherObject): def __init__(self, methods: list[Method] | None = None) -> None: - self._logger = logger(self) + super().__init__() self._client = JsonRPCClient() self._event_handler = EventHandler() self._settings: dict[str, Any] = {} @@ -29,6 +29,7 @@ def __init__(self, methods: list[Method] | None = None) -> None: def add_method(self, method: Method) -> str: """Add a method to the event handler.""" + setattr(method, '_is_registered_method', True) return self._event_handler.add_event(method) def add_methods(self, methods: Iterable[Method]) -> None: @@ -38,7 +39,7 @@ def on_method(self, method: Method) -> Method: @wraps(method) def wrapper(*args, **kwargs): return method(*args, **kwargs) - self._event_handler.add_event(wrapper) + self.add_method(wrapper) return wrapper def method(self, method: Method) -> Method: @@ -56,8 +57,8 @@ def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]: return handler return wrapper - def action(self, method: Method, parameters: Optional[Iterable] = None) -> JsonRPCAction: - """Register a method and return a JsonRPCAction that calls it.""" + def action(self, method: Method, parameters: Optional[List] = None) -> JsonRPCRequest: + """Register a method and return a JsonRPCRequest that calls it.""" method_name = self.add_method(method) return {"method": method_name, "parameters": parameters or []} @@ -84,19 +85,24 @@ def run(self) -> None: @property def run_dir(self) -> Path: """Return the run directory of the plugin.""" - return Path(sys.argv[0]).parent + return Path(sys.argv[0]).resolve().parent + @cached_property def root_dir(self) -> Path: """Return the root directory of the plugin.""" current_dir = self.run_dir for part in current_dir.parts: - if current_dir.joinpath(MANIFEST_FILE).exists(): + if current_dir.joinpath(FILE_NAME).exists(): return current_dir current_dir = current_dir.parent - raise FileNotFoundError(f"Could not find {MANIFEST_FILE} in {self.run_dir} or any parent directory.") + raise FileNotFoundError(f"Could not find {FILE_NAME} in {self.run_dir} or any parent directory.") + + @cached_property + def manifest_path(self) -> Path: + """Return the path to the plugin manifest.""" + return self.root_dir / FILE_NAME - def manifest(self) -> PluginManifestSchema: + @cached_property + def manifest(self) -> Manifest: """Return the plugin manifest.""" - with open(self.root_dir() / MANIFEST_FILE, 'r', encoding='utf-8') as f: - manifest = json.load(f) - return manifest + return Manifest.from_file(self.manifest_path) diff --git a/pyflowlauncher/result.py b/pyflowlauncher/result.py index 626933e..cd34800 100644 --- a/pyflowlauncher/result.py +++ b/pyflowlauncher/result.py @@ -1,75 +1,93 @@ from __future__ import annotations -import sys from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast -if sys.version_info < (3, 11): - from typing_extensions import NotRequired, TypedDict -else: - from typing import NotRequired, TypedDict +from .plugin import Method +from .models.result import Glyph, PreviewInfo +from .models.json_rpc import JsonRPCResult, JsonRPCRequest, JsonRPCResponse -if TYPE_CHECKING: - from .plugin import Method +class MethodNotRegisteredError(Exception): + """Exception raised when trying to add an action with a method that is not registered as a plugin method.""" - -class JsonRPCAction(TypedDict): - """Flow Launcher JsonRPCAction""" - method: str - parameters: Iterable - dontHideAfterAction: NotRequired[bool] - - -class Glyph(TypedDict): - """Flow Launcher Glyph""" - Glyph: str - FontFamily: str - - -class PreviewInfo(TypedDict): - """Flow Launcher Preview section""" - PreviewImagePath: Optional[str] - Description: Optional[str] - IsMedia: bool - PreviewDeligate: Optional[str] + def __init__(self, method: Callable[..., Any]): + self.method = method + super().__init__( + f"Method {method.__name__} is not registered as a plugin method. " + "Please use the @plugin.on_method decorator to register it." + ) @dataclass class Result: - Title: str - SubTitle: Optional[str] = None - IcoPath: Optional[Union[str, Path]] = None - Score: int = 0 - JsonRPCAction: Optional[JsonRPCAction] = None - ContextData: Optional[Iterable] = None - Glyph: Optional[Glyph] = None - CopyText: Optional[str] = None - AutoCompleteText: Optional[str] = None - RoundedIcon: bool = False - Preview: Optional[PreviewInfo] = None - TitleHighlightData: Optional[List[int]] = None + title: str + subtitle: Optional[str] = None + icon: Optional[Union[str, Path]] = None + score: int = 0 + json_rpc_action: Optional[JsonRPCRequest] = None + context_data: Optional[Iterable] = None + glyph: Optional[Glyph] = None + copy_text: Optional[str] = None + auto_complete_text: Optional[str] = None + rounded_icon: bool = False + preview: Optional[PreviewInfo] = None + title_highlight_data: Optional[List[int]] = None + + def add_action( + self, method: Method, parameters: Optional[Iterable[Any]] = None, dont_hide_after_action: bool = False + ) -> None: + """Adds a JsonRPC action to the result.""" + if not getattr(method, '_is_registered_method', False): + raise MethodNotRegisteredError(method) + self.json_rpc_action = { + 'Method': method.__name__, + 'Parameters': list(parameters) if parameters else [], + 'DontHideAfterAction': dont_hide_after_action, + } def as_dict(self) -> Dict[str, Any]: return self.__dict__ - def add_action(self, method: Method, - parameters: Optional[Iterable[Any]] = None, - *, - dont_hide_after_action: bool = False) -> None: - self.JsonRPCAction = { - "method": method.__name__, - "parameters": parameters or [], - "dontHideAfterAction": dont_hide_after_action - } - - -class ResultResponse(TypedDict): - result: List[Dict[str, Any]] - SettingsChange: NotRequired[Optional[Dict[str, Any]]] - - -def send_results(results: Iterable[Result], settings: Optional[Dict[str, Any]] = None) -> ResultResponse: + @staticmethod + def from_json(json_result: JsonRPCResult) -> Result: + """Creates a Result instance from a JsonRPCResult dictionary.""" + if 'Title' not in json_result: + raise ValueError("JsonRPCResult must have a 'Title' field") + return Result( + title=json_result['Title'], + subtitle=json_result.get('SubTitle'), + icon=json_result.get('IcoPath'), + score=json_result.get('Score', 0), + json_rpc_action=json_result.get('JsonRPCAction'), + context_data=json_result.get('ContextData'), + glyph=json_result.get('Glyph'), + copy_text=json_result.get('CopyText'), + auto_complete_text=json_result.get('AutoCompleteText'), + rounded_icon=json_result.get('RoundedIcon', False), + preview=json_result.get('Preview'), + title_highlight_data=list(json_result.get('TitleHighlightData', [])) + ) + + def to_json(self) -> JsonRPCResult: + """Converts the Result instance to a JsonRPCResult dictionary""" + return cast(JsonRPCResult, { + 'Title': self.title, + 'SubTitle': self.subtitle, + 'IcoPath': str(self.icon) if self.icon else None, + 'Score': self.score, + 'JsonRPCAction': self.json_rpc_action, + 'ContextData': self.context_data, + 'Glyph': self.glyph, + 'CopyText': self.copy_text, + 'AutoCompleteText': self.auto_complete_text, + 'RoundedIcon': self.rounded_icon, + 'Preview': self.preview, + 'TitleHighlightData': self.title_highlight_data, + }) + + +def send_results(results: Iterable[Result], settings: Optional[Dict[str, Any]] = None) -> JsonRPCResponse: """Formats and returns results as a JsonRPCResponse""" - return {'result': [result.as_dict() for result in results], 'SettingsChange': settings} + return {'Result': [result.to_json() for result in results], 'SettingsChange': settings} diff --git a/pyflowlauncher/shared.py b/pyflowlauncher/shared.py deleted file mode 100644 index 767d6b1..0000000 --- a/pyflowlauncher/shared.py +++ /dev/null @@ -1,14 +0,0 @@ -import logging -import sys -from pathlib import Path -from typing import Any - -_logger = logging.getLogger(__name__) - - -def logger(obj: Any) -> logging.Logger: - module_file = sys.modules[obj.__module__].__file__ - if module_file is not None: - module_name = Path(module_file).stem - return logging.getLogger(f"{module_name}.{obj.__class__.__name__}") - return logging.getLogger(f"{obj.__module__}.{obj.__name__}") diff --git a/pyflowlauncher/utils.py b/pyflowlauncher/utils.py index 754b9fe..0ba2441 100644 --- a/pyflowlauncher/utils.py +++ b/pyflowlauncher/utils.py @@ -12,10 +12,10 @@ def score_results( for result in results: match = string_matcher( query, - result.Title, + result.title, query_search_precision=score_cutoff ) if match.matched or (match_on_empty_query and not query): - result.TitleHighlightData = match.index_list - result.Score = match.score + result.title_highlight_data = match.index_list + result.score = match.score yield result diff --git a/pyproject.toml b/pyproject.toml index ac1764f..19c0eda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ all = ['typing_extensions>=4.8.0'] [tool.setuptools] -packages = ["pyflowlauncher"] +packages = ["pyflowlauncher", "pyflowlauncher.models"] [tool.setuptools.package-data] "pyflowlauncher" = ["py.typed"] diff --git a/tests/test_api.py b/tests/test_api.py index 3e2afe9..812a4ef 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,60 +2,60 @@ def test_send_action(): - assert api._send_action("Test", "Test") == {"method": "Flow.Launcher.Test", "parameters": ("Test",)} + assert api._send_action("Test", "Test") == {"Method": "Flow.Launcher.Test", "Parameters": ["Test"]} def test_change_query(): - assert api.change_query("Test", False) == {"method": "Flow.Launcher.ChangeQuery", "parameters": ("Test", False)} + assert api.change_query("Test", False) == {"Method": "Flow.Launcher.ChangeQuery", "Parameters": ["Test", False]} def test_shell_run(): - assert api.shell_run("Test", "Test") == {"method": "Flow.Launcher.ShellRun", "parameters": ("Test", "Test")} + assert api.shell_run("Test", "Test") == {"Method": "Flow.Launcher.ShellRun", "Parameters": ["Test", "Test"]} def test_close_app(): - assert api.close_app() == {"method": "Flow.Launcher.CloseApp", "parameters": ()} + assert api.close_app() == {"Method": "Flow.Launcher.CloseApp", "Parameters": []} def test_hide_app(): - assert api.hide_app() == {"method": "Flow.Launcher.HideApp", "parameters": ()} + assert api.hide_app() == {"Method": "Flow.Launcher.HideApp", "Parameters": []} def test_show_app(): - assert api.show_app() == {"method": "Flow.Launcher.ShowApp", "parameters": ()} + assert api.show_app() == {"Method": "Flow.Launcher.ShowApp", "Parameters": []} def test_show_msg(): - assert api.show_msg("Test", "Test", "Test") == {"method": "Flow.Launcher.ShowMsg", "parameters": ("Test", "Test", "Test")} + assert api.show_msg("Test", "Test", "Test") == {"Method": "Flow.Launcher.ShowMsg", "Parameters": ["Test", "Test", "Test"]} def test_open_setting_dialog(): - assert api.open_setting_dialog() == {"method": "Flow.Launcher.OpenSettingDialog", "parameters": ()} + assert api.open_setting_dialog() == {"Method": "Flow.Launcher.OpenSettingDialog", "Parameters": []} def test_start_loading_bar(): - assert api.start_loading_bar() == {"method": "Flow.Launcher.StartLoadingBar", "parameters": ()} + assert api.start_loading_bar() == {"Method": "Flow.Launcher.StartLoadingBar", "Parameters": []} def test_stop_loading_bar(): - assert api.stop_loading_bar() == {"method": "Flow.Launcher.StopLoadingBar", "parameters": ()} + assert api.stop_loading_bar() == {"Method": "Flow.Launcher.StopLoadingBar", "Parameters": []} def test_reload_plugins(): - assert api.reload_plugins() == {"method": "Flow.Launcher.ReloadPlugins", "parameters": ()} + assert api.reload_plugins() == {"Method": "Flow.Launcher.ReloadPlugins", "Parameters": []} def test_copy_to_clipboard(): - assert api.copy_to_clipboard("Test", False, True) == {"method": "Flow.Launcher.CopyToClipboard", "parameters": ("Test", False, True)} + assert api.copy_to_clipboard("Test", False, True) == {"Method": "Flow.Launcher.CopyToClipboard", "Parameters": ["Test", False, True]} def test_open_directory(): - assert api.open_directory("Test", "Test") == {"method": "Flow.Launcher.OpenDirectory", "parameters": ("Test", "Test")} + assert api.open_directory("Test", "Test") == {"Method": "Flow.Launcher.OpenDirectory", "Parameters": ["Test", "Test"]} def test_open_url(): - assert api.open_url("Test", False) == {"method": "Flow.Launcher.OpenUrl", "parameters": ("Test", False)} + assert api.open_url("Test", False) == {"Method": "Flow.Launcher.OpenUrl", "Parameters": ["Test", False]} def test_open_uri(): - assert api.open_uri("Test") == {"method": "Flow.Launcher.OpenAppUri", "parameters": ("Test",)} + assert api.open_uri("Test") == {"Method": "Flow.Launcher.OpenAppUri", "Parameters": ["Test"]} diff --git a/tests/test_method.py b/tests/test_method.py index 5aee754..9e088cf 100644 --- a/tests/test_method.py +++ b/tests/test_method.py @@ -22,4 +22,4 @@ def __call__(self, *args, **kwargs): return self.return_results() method = TestMethod() - assert method() == {"result": [result.as_dict()], 'SettingsChange': None} + assert method() == {"Result": [result.to_json()], 'SettingsChange': None} diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 6dba757..7f0ec1b 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -42,7 +42,7 @@ def test_root_dir(tmp_path, monkeypatch): monkeypatch.setattr('sys.argv', [tmp_path / 'plugin.py']) monkeypatch.setattr('pyflowlauncher.plugin.Path.exists', lambda _: True) plugin = Plugin() - assert plugin.root_dir() == tmp_path + assert plugin.root_dir == tmp_path def test_root_dir_not_found(tmp_path, monkeypatch): @@ -50,7 +50,7 @@ def test_root_dir_not_found(tmp_path, monkeypatch): monkeypatch.setattr('pyflowlauncher.plugin.Path.exists', lambda _: False) plugin = Plugin() with pytest.raises(FileNotFoundError): - assert plugin.root_dir() == tmp_path + assert plugin.root_dir == tmp_path def test_action(): diff --git a/tests/test_result.py b/tests/test_result.py index 557c897..3073f30 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -3,21 +3,21 @@ def test_asdict(): r = Result( - Title="Test", - SubTitle="Test", - IcoPath="Test.png", - TitleHighlightData=[0], - ContextData=["Test"], - Glyph={"Glyph": "Test", "FontFamily": "Test"}, - CopyText="Test", - AutoCompleteText="Test", - RoundedIcon=True, - JsonRPCAction={ + title="Test", + subtitle="Test", + icon="Test.png", + title_highlight_data=[0], + context_data=["Test"], + glyph={"Glyph": "Test", "FontFamily": "Test"}, + copy_text="Test", + auto_complete_text="Test", + rounded_icon=True, + json_rpc_action={ "Method": "Test", "Parameters": ["Test"], "DontHideAfterAction": True }, - Preview={ + preview={ "PreviewImagePath": "Test.png", "Description": "Test", "IsMedia": True, @@ -25,25 +25,25 @@ def test_asdict(): } ) assert r.as_dict() == { - "Title": "Test", - "SubTitle": "Test", - "IcoPath": "Test.png", - "TitleHighlightData": [0], - "Score": 0, - "JsonRPCAction": { + "title": "Test", + "subtitle": "Test", + "icon": "Test.png", + "title_highlight_data": [0], + "score": 0, + "json_rpc_action": { "Method": "Test", "Parameters": ["Test"], "DontHideAfterAction": True }, - "ContextData": ["Test"], - "Glyph": { + "context_data": ["Test"], + "glyph": { "Glyph": "Test", "FontFamily": "Test" }, - "CopyText": "Test", - "AutoCompleteText": "Test", - "RoundedIcon": True, - "Preview": { + "copy_text": "Test", + "auto_complete_text": "Test", + "rounded_icon": True, + "preview": { "PreviewImagePath": "Test.png", "Description": "Test", "IsMedia": True, @@ -53,12 +53,12 @@ def test_asdict(): def test_add_action(): - r = Result( - Title="Test" - ) - r.add_action(lambda: None, ["Test"], dont_hide_after_action=True) - assert r.JsonRPCAction == { - "method": "", - "parameters": ["Test"], - "dontHideAfterAction": True + r = Result(title="Test") + method = lambda: None + method._is_registered_method = True + r.add_action(method, ["Test"], dont_hide_after_action=True) + assert r.json_rpc_action == { + "Method": "", + "Parameters": ["Test"], + "DontHideAfterAction": True }