Coverage for flogin/jsonrpc/results.py: 61%
127 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 22:51 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-03 22:51 +0000
1from __future__ import annotations
3import logging
4import secrets
5from typing import (
6 TYPE_CHECKING,
7 Any,
8 ClassVar,
9 Generic,
10 NotRequired,
11 Self,
12 TypedDict,
13 TypeVarTuple,
14 Unpack,
15 cast,
16)
18from .._types.search_handlers import (
19 ConvertableToResult,
20 PluginT,
21 SearchHandlerCallbackReturns,
22)
23from ..utils import MISSING, cached_property, copy_doc
24from .base_object import Base
25from .responses import ErrorResponse, ExecuteResponse
27if TYPE_CHECKING:
28 from collections.abc import Callable, Iterable
29 from types import CoroutineType
31 from .._types.jsonrpc.result import ( # noqa: F401
32 RawGlyph,
33 RawPreview,
34 RawProgressBar,
35 RawResult,
36 )
38TS = TypeVarTuple("TS")
39log = logging.getLogger(__name__)
41__all__ = ("Glyph", "ProgressBar", "Result", "ResultConstructorKwargs", "ResultPreview")
44class Glyph(Base["RawGlyph"]):
45 r"""This represents a glyph object with flow launcher, which is an alternative to :class:`~flogin.jsonrpc.results.Result` icons.
47 Attributes
48 ----------
49 text: :class:`str`
50 The text to be shown in the glyph
51 font_family: :class:`str`
52 The font that the text should be shown in
53 """
55 __slots__ = "font_family", "text"
56 __jsonrpc_option_names__: ClassVar[dict[str, str]] = {
57 "text": "Glyph",
58 "font_family": "FontFamily",
59 }
61 def __init__(self, text: str, font_family: str) -> None:
62 self.text = text
63 self.font_family = font_family
65 @classmethod
66 def from_dict(cls: type[Self], data: RawGlyph) -> Self:
67 r"""Converts a dictionary into a :class:`Glyph` object.
69 Parameters
70 ----------
71 data: dict[:class:`str`, Any]
72 The dictionary
73 """
75 return cls(text=data["glyph"], font_family=data["fontFamily"])
78class ProgressBar(Base["RawProgressBar"]):
79 r"""This represents the progress bar than can be shown on a result.
81 .. NOTE::
82 Visually, the progress bar takes the same spot as the title
84 Attributes
85 ----------
86 percentage: :class:`int`
87 The percentage of the progress bar that should be filled. must be 0-100.
88 color: :class:`str`
89 The color that the progress bar should be in hex code form. Defaults to #26a0da.
90 """
92 __slots__ = "color", "percentage"
93 __jsonrpc_option_names__: ClassVar[dict[str, str]] = {
94 "percentage": "ProgressBar",
95 "color": "ProgressBarColor",
96 }
98 def __init__(self, percentage: int, color: str = MISSING) -> None:
99 self.percentage = percentage
100 self.color = color or "#26a0da"
103class ResultPreview(Base["RawPreview"]):
104 r"""Represents a result's preview.
106 .. NOTE::
107 Previews are finicky, and may not work 100% of the time
109 Attributes
110 ----------
111 image_path: :class:`str`
112 The path to the image to be shown
113 description: Optional[:class:`str`]
114 The description to be shown
115 is_media: Optional[:class:`bool`]
116 Whther the preview should be treated as media or not
117 """
119 __slots__ = "description", "image_path", "is_media", "preview_deligate"
120 __jsonrpc_option_names__: ClassVar[dict[str, str]] = {
121 "image_path": "previewImagePath",
122 "is_media": "isMedia",
123 "description": "description",
124 }
126 def __init__(
127 self,
128 image_path: str,
129 *,
130 description: str | None = None,
131 is_media: bool = True,
132 ) -> None:
133 self.image_path = image_path
134 self.description = description
135 self.is_media = is_media
138class ResultConstructorKwargs(TypedDict, total=False):
139 r"""This represents the possible kwargs that can be passed to :class:`Result`.
141 This can be useful when overriding :class:`Result` to create a basic implementation of something for your project, but still want the ability to pass kwargs with proper typing.
143 .. NOTE::
144 See :class:`Result` for more information about each key and value.
146 Example
147 --------
148 This is an example of how you might use this to create a url result
150 .. code-block:: py3
152 from typing import Unpack
153 from flogin import Result, ResultConstructorKwargs
155 class MyRes(Result):
156 def __init__(self, url: str, **kwargs: Unpack[ResultConstructorKwargs]) -> None:
157 self.url = url
158 super().__init__(**kwargs)
160 async def callback(self):
161 await self.plugin.api.open_url(self.url)
162 """
164 title: NotRequired[str]
165 sub: NotRequired[str]
166 icon: NotRequired[str]
167 title_highlight_data: NotRequired[list[int]]
168 title_tooltip: NotRequired[str]
169 sub_tooltip: NotRequired[str]
170 copy_text: NotRequired[str]
171 score: NotRequired[int]
172 rounded_icon: NotRequired[bool]
173 glyph: NotRequired[Glyph]
174 auto_complete_text: NotRequired[str]
175 preview: NotRequired[ResultPreview]
176 progress_bar: NotRequired[ProgressBar]
179class Result(Base["RawResult"], Generic[PluginT]):
180 r"""This represents a result that would be returned as a result for a query or context menu.
182 For simple useage: create instances of this class as-is.
184 For advanced useage (handling clicks and custom context menus), it is recommended to subclass the result object to create your own subclass.
186 Subclassing
187 ------------
188 Subclassing lets you override the following methods: :func:`~flogin.jsonrpc.results.Result.callback` and :func:`~flogin.jsonrpc.results.Result.context_menu`. It also lets you create "universal" result properties (eg: same icon). Example:
190 .. code-block:: python3
192 class MyResult(Result):
193 def __init__(self, title: str) -> None:
194 super().__init__(self, title, icon="Images/app.png")
196 async def callback(self):
197 # handle what happens when the result gets clicked
199 async def context_menu(self):
200 # add context menu options to this result's context menu
202 Attributes
203 ----------
204 title: :class:`str`
205 The title/content of the result
206 sub: Optional[:class:`str`]
207 The subtitle to be shown.
208 icon: Optional[:class:`str`]
209 A path to the icon to be shown with the result. If this and :attr:`~flogin.jsonrpc.results.Result.glyph` are passed, the user's ``Use Segoe Fluent Icons`` setting will determine which is used.
210 title_highlight_data: Optional[Iterable[:class:`int`]]
211 The highlight data for the title. See the :ref:`FAQ section on highlights <highlights>` for more info.
212 title_tooltip: Optional[:class:`str`]
213 The text to be displayed when the user hovers over the result's title
214 sub_tooltip: Optional[:class:`str`]
215 The text to be displayed when the user hovers over the result's subtitle
216 copy_text: Optional[:class:`str`]
217 This is the text that will be copied when the user does ``CTRL+C`` on the result. If the text is a file/directory path, flow will copy the actual file/folder instead of just the path text.
218 plugin: :class:`~flogin.plugin.Plugin` | None
219 Your plugin instance. This is filled before :func:`~flogin.jsonrpc.results.Result.callback` or :func:`~flogin.jsonrpc.results.Result.context_menu` are triggered.
220 preview: Optional[:class:`~flogin.jsonrpc.results.ResultPreview`]
221 Customize the preview that is shown for the result. By default, the preview shows the result's title, subtitle, and icon
222 progress_bar: Optional[:class:`~flogin.jsonrpc.results.ProgressBar`]
223 The progress bar that could be shown in the place of the title
224 auto_complete_text: Optional[:class:`str`]
225 The text that will replace the :attr:`~flogin.query.Query.raw_text` in the flow menu when the autocomplete hotkey is used on the result. Defaults to the result's title.
226 rounded_icon: Optional[:class:`bool`]
227 Whether to have round the icon or not.
228 glyph: Optional[:class:`~flogin.jsonrpc.results.Glyph`]
229 The :class:`~flogin.jsonrpc.results.Glyph` object that will serve as the result's icon. If this and :attr:`~flogin.jsonrpc.results.Result.icon` are passed, the user's ``Use Segoe Fluent Icons`` setting will determine which is used.
230 """
232 def __init__(
233 self,
234 title: str | None = None,
235 sub: str | None = None,
236 icon: str | None = None,
237 title_highlight_data: Iterable[int] | None = None,
238 title_tooltip: str | None = None,
239 sub_tooltip: str | None = None,
240 copy_text: str | None = None,
241 score: int | None = None,
242 auto_complete_text: str | None = None,
243 preview: ResultPreview | None = None,
244 progress_bar: ProgressBar | None = None,
245 rounded_icon: bool | None = None,
246 glyph: Glyph | None = None,
247 ) -> None:
248 self.title = title
249 self.sub = sub
250 self.icon = icon
251 self.title_highlight_data = title_highlight_data
252 self.title_tooltip = title_tooltip
253 self.sub_tooltip = sub_tooltip
254 self.copy_text = copy_text
255 self.score = score
256 self.auto_complete_text = auto_complete_text
257 self.preview = preview
258 self.progress_bar = progress_bar
259 self.rounded_icon = rounded_icon
260 self.glyph = glyph
261 self.plugin: PluginT | None = None
263 async def on_error(self, error: Exception) -> ErrorResponse | ExecuteResponse:
264 r"""|coro|
266 Override this function to add an error response behavior to this result's callback.
268 Parameters
269 ----------
270 error: :class:`Exception`
271 The error that occured
272 """
273 log.exception("Ignoring exception in result callback (%r)", exc_info=error)
274 return ErrorResponse.internal_error(error)
276 async def callback(self) -> ExecuteResponse | bool | None:
277 r"""|coro|
279 Override this function to add a callback behavior to your result. This method will run when the user clicks on your result.
281 .. versionchanged:: 2.0.0
282 A result callback can not return :class:`bool` or ``None``
284 Returns
285 -------
286 :class:`~flogin.jsonrpc.responses.ExecuteResponse` | :class:`bool` | ``None``
287 A response to flow determining whether or not to hide flow's menu, or a bool that will be turned into a response. ``None`` will be converted into ``True`` to align with the default value for :attr:`~flogin.jsonrpc.responses.ExecuteResponse.hide`
288 """
290 return ExecuteResponse(False)
292 def context_menu(self) -> SearchHandlerCallbackReturns:
293 r"""|coro|
295 Override this function to add a context menu behavior to your result. This method will run when the user gets the context menu to your result.
297 This method can return/yield almost anything, and flogin will convert it into a list of :class:`~flogin.jsonrpc.results.Result` objects before sending it to flow.
299 Returns
300 -------
301 list[:class:`~flogin.jsonrpc.results.Result`] | :class:`~flogin.jsonrpc.results.Result` | str | Any
302 A list of results, an results, or something that can be converted into a list of results.
304 Yields
305 ------
306 :class:`~flogin.jsonrpc.results.Result` | str | Any
307 A result object or something that can be converted into a result object.
308 """
309 ...
311 def on_context_menu_error(self, error: Exception) -> SearchHandlerCallbackReturns:
312 r"""|coro|
314 Override this function to add an error response behavior to this result's context menu callback.
316 If the error was handled:
317 You can return/yield almost anything, and flogin will convert it into a list of :class:`~flogin.jsonrpc.results.Result` objects before sending it to flow.
319 If the error was not handled:
320 Return a :class:`~flogin.jsonrpc.responses.ErrorResponse` object
322 Parameters
323 ----------
324 error: :class:`Exception`
325 The error that occured
327 Returns
328 -------
329 :class:`~flogin.jsonrpc.responses.ErrorResponse` | list[:class:`~flogin.jsonrpc.results.Result`] | :class:`~flogin.jsonrpc.results.Result` | str | Any
330 A list of results, an results, or something that can be converted into a list of results.
332 Yields
333 ------
334 :class:`~flogin.jsonrpc.results.Result` | str | Any
335 A result object or something that can be converted into a result object.
336 """
337 ...
339 if not TYPE_CHECKING:
341 @copy_doc(context_menu)
342 async def context_menu(self) -> Any:
343 return []
345 @copy_doc(on_context_menu_error)
346 async def on_context_menu_error(self, error: Exception) -> Any:
347 log.exception(
348 "Ignoring exception in result's context menu callback (%r)",
349 self,
350 exc_info=error,
351 )
352 return ErrorResponse.internal_error(error)
354 def to_dict(self) -> RawResult:
355 r"""This converts the result into a json serializable dictionary
357 Returns
358 -------
359 dict[:class:`str`, Any]
360 """
362 x: RawResult = {
363 "jsonRPCAction": {
364 "method": f"flogin.action.{self.slug}",
365 },
366 "contextData": [
367 self.slug,
368 ],
369 }
371 if self.title is not None:
372 x["title"] = self.title
373 if self.sub is not None:
374 x["subTitle"] = self.sub
375 if self.icon is not None:
376 x["icoPath"] = self.icon
377 if self.title_highlight_data is not None:
378 x["titleHighlightData"] = self.title_highlight_data
379 if self.title_tooltip is not None:
380 x["titleTooltip"] = self.title_tooltip
381 if self.sub_tooltip is not None:
382 x["subtitleTooltip"] = self.sub_tooltip
383 if self.copy_text is not None:
384 x["copyText"] = self.copy_text
385 if self.score is not None:
386 x["score"] = self.score
387 if self.preview is not None:
388 x["preview"] = self.preview.to_dict()
389 if self.auto_complete_text is not None:
390 x["autoCompleteText"] = self.auto_complete_text
391 if self.progress_bar is not None:
392 x.update(self.progress_bar.to_dict()) # type: ignore
393 if self.rounded_icon is not None:
394 x["roundedIcon"] = self.rounded_icon
395 if self.glyph is not None:
396 x["glyph"] = self.glyph.to_dict()
397 return x
399 @classmethod
400 def from_dict(cls: type[Self], data: RawResult) -> Self:
401 r"""Creates a Result from a dictionary
403 .. NOTE::
404 This method does NOT fill the :func:`~flogin.jsonrpc.results.Result.callback` or :func:`~flogin.jsonrpc.results.Result.context_menu` attributes.
406 Parameters
407 ----------
408 data: dict[:class:`str`, Any]
409 The valid dictionary that includes the result data
411 Raises
412 ------
413 :class:`KeyError`
414 The dictionary did not include the only required field, ``title``.
416 Returns
417 --------
418 :class:`Result`
419 """
421 return cls(
422 title=data.get("title"),
423 sub=data.get("subTitle"),
424 icon=data.get("icoPath"),
425 title_highlight_data=data.get("titleHighlightData"),
426 title_tooltip=data.get("titleTooltip"),
427 sub_tooltip=data.get("subtitleTooltip"),
428 copy_text=data.get("copyText"),
429 )
431 @classmethod
432 def from_anything(cls: type[Result], item: ConvertableToResult[Any]) -> Result[Any]:
433 if isinstance(item, dict):
434 return cls.from_dict(cast("RawResult", item))
435 if isinstance(item, Result):
436 return item # type: ignore
437 return cls(str(item))
439 @classmethod
440 def create_with_partial(
441 cls: type[Result],
442 partial_callback: Callable[
443 [], CoroutineType[Any, Any, ExecuteResponse | bool | None]
444 ],
445 **kwargs: Unpack[ResultConstructorKwargs],
446 ) -> Result:
447 r"""A quick and easy way to create a result with a callback without subclassing.
449 .. NOTE::
450 This is meant to be used with :class:`~flogin.flow.api.FlowLauncherAPI` methods
452 Example
453 --------
454 .. code-block:: python3
456 result = Result.create_with_partial(
457 functools.partial(
458 plugin.api.show_notification,
459 "notification title",
460 "notification content"
461 ),
462 title="Result title",
463 sub="Result subtitle"
464 )
466 Parameters
467 ----------
468 partial_callback: partial :ref:`coroutine <coroutine>`
469 The callback wrapped in :obj:`functools.partial`
470 kwargs: See allowed kwargs here: :class:`~flogin.jsonrpc.results.Result`
471 The args that will be passed to the :class:`~flogin.jsonrpc.results.Result` constructor
472 """
474 self = cls(**kwargs)
475 self.callback = partial_callback
476 return self
478 @cached_property
479 def slug(self) -> str:
480 return secrets.token_hex(15)
482 def __repr__(self) -> str:
483 return f"<{self.__class__.__name__} {self.title=} {self.sub=} {self.icon=} {self.title_highlight_data=} {self.title_tooltip=} {self.sub_tooltip=} {self.copy_text=} {self.score=} {self.auto_complete_text=} {self.preview=} {self.progress_bar=} {self.rounded_icon=} {self.glyph=}>"