11import csv
2+ from collections import defaultdict
23from collections .abc import Container
4+ from dataclasses import dataclass
35from datetime import datetime
46from pathlib import Path
5- from typing import Optional
7+ from typing import Callable , Iterable , Optional , TypedDict
68
79from talon import Context , Module , actions , app , fs
810
2527 desc = "The directory to use for cursorless settings csvs relative to talon user directory" ,
2628)
2729
28- default_ctx = Context ()
29- default_ctx .matches = r"""
30+ # The global context we use for our lists
31+ ctx = Context ()
32+
33+ # A context that contains default vocabulary, for use in testing
34+ normalized_ctx = Context ()
35+ normalized_ctx .matches = r"""
3036tag: user.cursorless_default_vocabulary
3137"""
3238
3339
40+ # Maps from Talon list name to a map from spoken form to value
41+ ListToSpokenForms = dict [str , dict [str , str ]]
42+
43+
44+ @dataclass
45+ class SpokenFormEntry :
46+ list_name : str
47+ id : str
48+ spoken_forms : list [str ]
49+
50+
3451def init_csv_and_watch_changes (
3552 filename : str ,
36- default_values : dict [str , dict [str , str ]],
53+ default_values : ListToSpokenForms ,
54+ handle_new_values : Optional [Callable [[list [SpokenFormEntry ]], None ]] = None ,
55+ * ,
3756 extra_ignored_values : Optional [list [str ]] = None ,
3857 extra_allowed_values : Optional [list [str ]] = None ,
3958 allow_unknown_values : bool = False ,
4059 default_list_name : Optional [str ] = None ,
4160 headers : list [str ] = [SPOKEN_FORM_HEADER , CURSORLESS_IDENTIFIER_HEADER ],
42- ctx : Context = Context (),
4361 no_update_file : bool = False ,
4462 pluralize_lists : Optional [list [str ]] = None ,
4563):
4664 """
4765 Initialize a cursorless settings csv, creating it if necessary, and watch
4866 for changes to the csv. Talon lists will be generated based on the keys of
4967 `default_values`. For example, if there is a key `foo`, there will be a
50- list created called `user.cursorless_foo` that will contain entries from
51- the original dict at the key `foo`, updated according to customization in
52- the csv at
68+ list created called `user.cursorless_foo` that will contain entries from the
69+ original dict at the key `foo`, updated according to customization in the
70+ csv at
5371
54- actions.path.talon_user() / "cursorless-settings" / filename
72+ ```
73+ actions.path.talon_user() / "cursorless-settings" / filename
74+ ```
5575
5676 Note that the settings directory location can be customized using the
5777 `user.cursorless_settings_directory` setting.
5878
5979 Args:
6080 filename (str): The name of the csv file to be placed in
61- `cursorles-settings` dir
62- default_values (dict[str, dict]): The default values for the lists to
63- be customized in the given csv
64- extra_ignored_values list[str]: Don't throw an exception if any of
65- these appear as values; just ignore them and don't add them to any list
66- allow_unknown_values bool: If unknown values appear, just put them in the list
67- default_list_name Optional[str]: If unknown values are allowed, put any
68- unknown values in this list
69- no_update_file Optional[bool]: Set this to `TRUE` to indicate that we should
70- not update the csv. This is used generally in case there was an issue coming up with the default set of values so we don't want to persist those to disk
71- pluralize_lists: Create plural version of given lists
81+ `cursorles-settings` dir
82+ default_values (ListToSpokenForms): The default values for the lists to
83+ be customized in the given csv
84+ handle_new_values (Optional[Callable[[list[SpokenFormEntry]], None]]): A
85+ callback to be called when the lists are updated
86+ extra_ignored_values (Optional[list[str]]): Don't throw an exception if
87+ any of these appear as values; just ignore them and don't add them
88+ to any list
89+ allow_unknown_values (bool): If unknown values appear, just put them in
90+ the list
91+ default_list_name (Optional[str]): If unknown values are
92+ allowed, put any unknown values in this list
93+ headers (list[str]): The headers to use for the csv
94+ no_update_file (bool): Set this to `True` to indicate that we should not
95+ update the csv. This is used generally in case there was an issue
96+ coming up with the default set of values so we don't want to persist
97+ those to disk
98+ pluralize_lists (list[str]): Create plural version of given lists
7299 """
73100 # Don't allow both `extra_allowed_values` and `allow_unknown_values`
74101 assert not (extra_allowed_values and allow_unknown_values )
@@ -112,7 +139,7 @@ def on_watch(path, flags):
112139 allow_unknown_values = allow_unknown_values ,
113140 default_list_name = default_list_name ,
114141 pluralize_lists = pluralize_lists ,
115- ctx = ctx ,
142+ handle_new_values = handle_new_values ,
116143 )
117144
118145 fs .watch (str (file_path .parent ), on_watch )
@@ -135,7 +162,7 @@ def on_watch(path, flags):
135162 allow_unknown_values = allow_unknown_values ,
136163 default_list_name = default_list_name ,
137164 pluralize_lists = pluralize_lists ,
138- ctx = ctx ,
165+ handle_new_values = handle_new_values ,
139166 )
140167 else :
141168 if not no_update_file :
@@ -148,7 +175,7 @@ def on_watch(path, flags):
148175 allow_unknown_values = allow_unknown_values ,
149176 default_list_name = default_list_name ,
150177 pluralize_lists = pluralize_lists ,
151- ctx = ctx ,
178+ handle_new_values = handle_new_values ,
152179 )
153180
154181 def unsubscribe ():
@@ -184,68 +211,92 @@ def create_default_vocabulary_dicts(
184211 if active_key :
185212 updated_dict [active_key ] = value2
186213 default_values_updated [key ] = updated_dict
187- assign_lists_to_context (default_ctx , default_values_updated , pluralize_lists )
214+ assign_lists_to_context (normalized_ctx , default_values_updated , pluralize_lists )
188215
189216
190217def update_dicts (
191- default_values : dict [ str , dict ] ,
192- current_values : dict ,
218+ default_values : ListToSpokenForms ,
219+ current_values : dict [ str , str ] ,
193220 extra_ignored_values : list [str ],
194221 extra_allowed_values : list [str ],
195222 allow_unknown_values : bool ,
196223 default_list_name : Optional [str ],
197224 pluralize_lists : list [str ],
198- ctx : Context ,
225+ handle_new_values : Optional [ Callable [[ list [ SpokenFormEntry ]], None ]] ,
199226):
200227 # Create map with all default values
201- results_map = {}
202- for list_name , dict in default_values .items ():
203- for key , value in dict .items ():
204- results_map [value ] = {"key " : key , "value " : value , "list" : list_name }
228+ results_map : dict [ str , ResultsListEntry ] = {}
229+ for list_name , obj in default_values .items ():
230+ for spoken , id in obj .items ():
231+ results_map [id ] = {"spoken " : spoken , "id " : id , "list" : list_name }
205232
206233 # Update result with current values
207- for key , value in current_values .items ():
234+ for spoken , id in current_values .items ():
208235 try :
209- results_map [value ]["key " ] = key
236+ results_map [id ]["spoken " ] = spoken
210237 except KeyError :
211- if value in extra_ignored_values :
238+ if id in extra_ignored_values :
212239 pass
213- elif allow_unknown_values or value in extra_allowed_values :
214- results_map [value ] = {
215- "key" : key ,
216- "value" : value ,
240+ elif allow_unknown_values or id in extra_allowed_values :
241+ assert default_list_name is not None
242+ results_map [id ] = {
243+ "spoken" : spoken ,
244+ "id" : id ,
217245 "list" : default_list_name ,
218246 }
219247 else :
220248 raise
221249
222- # Convert result map back to result list
223- results = {res ["list" ]: {} for res in results_map .values ()}
224- for obj in results_map .values ():
225- value = obj ["value" ]
226- key = obj ["key" ]
227- if not is_removed (key ):
228- for k in key .split ("|" ):
229- if value == "pasteFromClipboard" and k .endswith (" to" ):
250+ spoken_form_entries = list (generate_spoken_forms (results_map .values ()))
251+
252+ # Assign result to talon context list
253+ lists : ListToSpokenForms = defaultdict (dict )
254+ for entry in spoken_form_entries :
255+ for spoken_form in entry .spoken_forms :
256+ lists [entry .list_name ][spoken_form ] = entry .id
257+ assign_lists_to_context (ctx , lists , pluralize_lists )
258+
259+ if handle_new_values is not None :
260+ handle_new_values (spoken_form_entries )
261+
262+
263+ class ResultsListEntry (TypedDict ):
264+ spoken : str
265+ id : str
266+ list : str
267+
268+
269+ def generate_spoken_forms (results_list : Iterable [ResultsListEntry ]):
270+ for obj in results_list :
271+ id = obj ["id" ]
272+ spoken = obj ["spoken" ]
273+
274+ spoken_forms = []
275+ if not is_removed (spoken ):
276+ for k in spoken .split ("|" ):
277+ if id == "pasteFromClipboard" and k .endswith (" to" ):
230278 # FIXME: This is a hack to work around the fact that the
231279 # spoken form of the `pasteFromClipboard` action used to be
232280 # "paste to", but now the spoken form is just "paste" and
233281 # the "to" is part of the positional target. Users who had
234282 # cursorless before this change would have "paste to" as
235283 # their spoken form and so would need to say "paste to to".
236284 k = k [:- 3 ]
237- results [ obj [ "list" ]][ k .strip ()] = value
285+ spoken_forms . append ( k .strip ())
238286
239- # Assign result to talon context list
240- assign_lists_to_context (ctx , results , pluralize_lists )
287+ yield SpokenFormEntry (
288+ list_name = obj ["list" ],
289+ id = id ,
290+ spoken_forms = spoken_forms ,
291+ )
241292
242293
243294def assign_lists_to_context (
244295 ctx : Context ,
245- results : dict ,
296+ lists : ListToSpokenForms ,
246297 pluralize_lists : list [str ],
247298):
248- for list_name , dict in results .items ():
299+ for list_name , dict in lists .items ():
249300 list_singular_name = get_cursorless_list_name (list_name )
250301 ctx .lists [list_singular_name ] = dict
251302 if list_name in pluralize_lists :
@@ -410,7 +461,7 @@ def get_full_path(filename: str):
410461 return (settings_directory / filename ).resolve ()
411462
412463
413- def get_super_values (values : dict [ str , dict [ str , str ]] ):
464+ def get_super_values (values : ListToSpokenForms ):
414465 result : dict [str , str ] = {}
415466 for value_dict in values .values ():
416467 result .update (value_dict )
0 commit comments