albert
albert copied to clipboard
[python] Lazy action resolution
Problem
Currently, if you're writing Python extension, you have to provide "action value" for ClipAction, UrlAction, ProcAction or TermAction, there is no way to postpone that until certain action is triggered by user.
Solution
There are 2 possible solutions for this problem:
- Accept a callback value as an argument in actions in question, which will be called after the action is triggered by user. Specified callback must return value(s) required for the action to proceed.
- Provide actions in question as separate functions that can be invoked from
FuncActioncallback method. Invoking those functions should trigger corresponding actions immediately, without the need for user input.
2nd solution is preferred as it gives much wider flexibility. It allows, for example, to trigger 2 independent actions at once.
Rationale
Currently it is possible to invoke those actions using python built-ins or 3rd party libraries, but Albert has those actions implemented in some standardized manner and it is very hard (or maybe even impossible) to exactly match how Albert is triggering those actions (for example to trigger TermAction by hand, plugin author has to fetch from Albert settings which terminal application should be used, for ClipAction, it is currently impossible to invoke that command without external requirements). If those actions are already implemented inside either Albert Core or Python extension, there is no reasonable reason not to expose them directly.
Use cases and examples
This functionality may be needed for example for implementing OTP code generation using Yubikey. For some entries, Yubikey may require to "authenticate" the request by touching the key by user. This should be triggered only after user has selected certain credential for which he wants the OTP code to be generated and stored in his clipboard. It could've been resolved using 1st solution with code:
def handleQuery(query):
if not query.isTriggered:
return None
return [
Item(
id=entry.id,
text=entry.name,
actions=[
ClipAction(
text=f"Copy token to clipboard",
clipboardText=lambda: entry.fetch_code(timeout=3) # this function may wait for user input on the Yubikey device
),
],
)
for entry in get_yubikey_credentials(query.string)
]
or using 2nd solution:
def handleQuery(query):
if not query.isTriggered:
return None
return [
Item(
id=entry.id,
text=entry.name,
actions=[
FuncAction(
text=f"Copy token to clipboard",
callable=lambda: copy_to_clipboard(entry.fetch_code(timeout=3)) # this function may wait for user input on the Yubikey device
),
],
)
for entry in get_yubikey_credentials(query.string)
]
Additional things to consider
- Currently, after invoking
FuncAction, if this action is waiting for any event to happen, Albert stays visible on the screen, frozen. It may be beneficial to hide this window before starting to process the callable passed to any of the actions to improve user experience. - If #986 will be implemented, the 2nd implementation from ones proposed may be preferred.
Commenting to make stale bot happy :)
I thought about this recently, but from another perspective: it simply is not necessary to instanciate thousands of actions if one needs exactly one. (nah not the only reason: rather plugin interdependencies and interplugin action providers) therefore i thought rather implement it with action factories. Such that if the users shows or starts actions, a createActions function would be called but in the end this would not solve your problem. Lambda in clip action is overkill, i'd rather expose copy to clip to python. I could also expose openUrl, run terminal, etc but then I have to ask where do we stop?
There are 2 problems I'm encountering when I need to provide the value for the action upfront:
- User won't see any actions from my plugin without touching the Yubikey first
- A separate touch is required for every entry
And I don't have any way to inform user this is required. This leads to the situation when if user has 10 entries matching the word match that are protected by the touch on his Yubikey, he won't see anything after typing the otp match in albert window, unless he will touch the Yubikey 10 times in a row. And he has no way of knowing that. What's more, it is a security risk for the user, as he'll expose all his protected OTP codes at once.
To resolve both of the issues, any way of lazy evaluation the text being copied to the clipboard. I've come up with 2 solutions, but any other one that let's me evaluate it lazily after user already confirmed the action will do.
I can find a lot of other situations where such lazy evaluation would be handy, like a password manager that lets you list all the entries, but to fetch a secret you have to re-enter your password. Or some pretty slow API/Command that has to be executed first.
I can also find it handy for other functions, like openUrl. I actually have a use-case in mind: I want to create a browser extension that will be the companion for my Yubikey OTP plugin. After it being installed, you'll be able to call a special URL that will fill the currently highlighted field in browser with provided data.
makes sense. I'll remove the standard actions from the python interface and expose the underlying functions instead unless other things make problems. 0.18 breaks almost every bit of the native and python plugin api anyway. I guess ill ask for a 0.18-pre hackathon in the chats then 😬