11# -*- coding: utf-8 -*-
22
3+ import time
4+ from dataclasses import dataclass
5+ from enum import Enum
36from pathlib import Path
4- from subprocess import run , CalledProcessError
7+ from subprocess import CalledProcessError , run
58
69from albert import *
710
811md_iid = "3.0"
9- md_version = "3.0 "
12+ md_version = "3.1 "
1013md_name = "Bitwarden"
1114md_description = "'rbw' wrapper extension"
1215md_license = "MIT"
1316md_url = "https://github.com/albertlauncher/python/tree/main/bitwarden"
1417md_authors = ["@ovitor" , "@daviddeadly" , "@manuelschneid3r" ]
1518md_bin_dependencies = ["rbw" ]
1619
20+ MAX_MINUTES_CACHE_TIMEOUT = 60
21+ DEFAULT_MINUTE_CACHE_TIMEOUT = 5
22+
23+
24+ @dataclass (frozen = True )
25+ class ConfigKeys :
26+ CACHE_TIMEOUT = "cache_timeout"
27+
1728
1829class Plugin (PluginInstance , TriggerQueryHandler ):
30+ _cached_items = None
31+ _last_fetch_time = 0
1932
2033 iconUrls = [f"file:{ Path (__file__ ).parent } /bw.svg" ]
2134
2235 def __init__ (self ):
2336 PluginInstance .__init__ (self )
2437 TriggerQueryHandler .__init__ (self )
2538
39+ self .cache_timeout = (
40+ self .readConfig (ConfigKeys .CACHE_TIMEOUT , int )
41+ or DEFAULT_MINUTE_CACHE_TIMEOUT
42+ )
43+
2644 def defaultTrigger (self ):
27- return 'bw '
45+ return "bw "
46+
47+ @property
48+ def cache_timeout (self ):
49+ return int (self ._cache_timeout / 60 )
50+
51+ @cache_timeout .setter
52+ def cache_timeout (self , value ):
53+ self ._cache_timeout = int (value * 60 )
54+ self .writeConfig (ConfigKeys .CACHE_TIMEOUT , value )
55+
56+ def configWidget (self ):
57+ return [
58+ {
59+ "type" : "label" ,
60+ "text" : "Cache (result of `rbw list`) duration" ,
61+ },
62+ {
63+ "type" : "spinbox" ,
64+ "property" : ConfigKeys .CACHE_TIMEOUT ,
65+ "label" : f"Minutes: (max: { MAX_MINUTES_CACHE_TIMEOUT } , disable: 0)" ,
66+ "widget_properties" : {"maximum" : MAX_MINUTES_CACHE_TIMEOUT },
67+ },
68+ ]
2869
2970 def handleTriggerQuery (self , query ):
3071 if query .string .strip ().lower () == "sync" :
@@ -37,11 +78,9 @@ def handleTriggerQuery(self, query):
3778 Action (
3879 id = "sync" ,
3980 text = "Syncing Bitwarden Vault" ,
40- callable = lambda : run (
41- ["rbw" , "sync" ],
42- )
81+ callable = lambda : self ._sync_vault (),
4382 )
44- ]
83+ ],
4584 )
4685 )
4786
@@ -56,30 +95,38 @@ def handleTriggerQuery(self, query):
5695 Action (
5796 id = "copy" ,
5897 text = "Copy password to clipboard" ,
59- callable = lambda item = p : self ._password_to_clipboard (item )
98+ callable = lambda item = p : self ._password_to_clipboard (item ),
6099 ),
61100 Action (
62101 id = "copy-auth" ,
63102 text = "Copy auth code to clipboard" ,
64- callable = lambda item = p : self ._code_to_clipboard (item )
103+ callable = lambda item = p : self ._code_to_clipboard (item ),
65104 ),
66105 Action (
67106 id = "copy-username" ,
68107 text = "Copy username to clipboard" ,
69- callable = lambda username = p ["user" ]:
70- setClipboardText (text = username )
108+ callable = lambda username = p ["user" ]: setClipboardText (
109+ text = username
110+ ),
71111 ),
72112 Action (
73113 id = "edit" ,
74114 text = "Edit entry in terminal" ,
75- callable = lambda item = p : self ._edit_entry (item )
76- )
77- ]
115+ callable = lambda item = p : self ._edit_entry (item ),
116+ ),
117+ ],
78118 )
79119 )
80120
81- @staticmethod
82- def _get_items ():
121+ def _get_items (self ):
122+ not_first_time = self ._cached_items is not None
123+
124+ time_passed = time .time () - self ._last_fetch_time
125+ is_chache_fresh = time_passed < self ._cache_timeout
126+
127+ if not_first_time and is_chache_fresh :
128+ return self ._cached_items
129+
83130 field_names = ["id" , "name" , "user" , "folder" ]
84131 raw_items = run (
85132 ["rbw" , "list" , "--fields" , "," .join (field_names )],
@@ -98,12 +145,16 @@ def _get_items():
98145 item ["path" ] = item ["folder" ] + "/" + item ["name" ]
99146 else :
100147 item ["path" ] = item ["name" ]
148+
101149 items .append (item )
102150
151+ self ._cached_items = items
152+ self ._last_fetch_time = time .time ()
153+
103154 return items
104155
105156 def _filter_items (self , query ):
106- passwords = self ._get_items ()
157+ passwords = self ._get_items () or []
107158 search_fields = ["path" , "user" ]
108159 # Use a set for faster membership tests
109160 words = set (query .string .strip ().lower ().split ())
@@ -121,15 +172,18 @@ def _filter_items(self, query):
121172
122173 return filtered_passwords
123174
175+ def _sync_vault (self ):
176+ run (["rbw" , "sync" ], check = True )
177+
178+ self ._cached_items = None
179+ self ._last_fetch_time = 0
180+
124181 @staticmethod
125182 def _password_to_clipboard (item ):
126183 rbw_id = item ["id" ]
127184
128185 password = run (
129- ["rbw" , "get" , rbw_id ],
130- capture_output = True ,
131- encoding = "utf-8" ,
132- check = True
186+ ["rbw" , "get" , rbw_id ], capture_output = True , encoding = "utf-8" , check = True
133187 ).stdout .strip ()
134188
135189 setClipboardText (text = password )
@@ -143,14 +197,14 @@ def _code_to_clipboard(item):
143197 ["rbw" , "code" , rbw_id ],
144198 capture_output = True ,
145199 encoding = "utf-8" ,
146- check = True
200+ check = True ,
147201 ).stdout .strip ()
148202 except CalledProcessError as err :
149203 code = run (
150204 ["echo" , err .__str__ ()],
151205 capture_output = True ,
152206 encoding = "utf-8" ,
153- check = True
207+ check = True ,
154208 ).stdout .strip ()
155209
156210 setClipboardText (text = code )
0 commit comments