11import csv
2- import json
32from collections .abc import Container
43from datetime import datetime
54from pathlib import Path
2625 desc = "The directory to use for cursorless settings csvs relative to talon user directory" ,
2726)
2827
29- default_ctx = Context ()
30- default_ctx .matches = r"""
28+ # The global context we use for our lists
29+ ctx = Context ()
30+
31+ # A context that contains default vocabulary, for use in testing
32+ normalized_ctx = Context ()
33+ normalized_ctx .matches = r"""
3134tag: user.cursorless_default_vocabulary
3235"""
3336
3437
35- GetType = Callable [[ str , str ] , str ]
38+ ListToSpokenForms = dict [ str , dict [ str , str ] ]
3639
3740
3841def init_csv_and_watch_changes (
3942 filename : str ,
40- default_values : dict [str , dict [str , str ]],
43+ default_values : ListToSpokenForms ,
44+ handle_new_values : Optional [Callable [[ListToSpokenForms ], None ]] = None ,
4145 extra_ignored_values : Optional [list [str ]] = None ,
4246 allow_unknown_values : bool = False ,
4347 default_list_name : Optional [str ] = None ,
4448 headers : list [str ] = [SPOKEN_FORM_HEADER , CURSORLESS_IDENTIFIER_HEADER ],
45- ctx : Context = Context (),
4649 no_update_file : bool = False ,
4750 pluralize_lists : list [str ] = [],
48- get_type : Optional [GetType ] = None ,
4951):
5052 """
5153 Initialize a cursorless settings csv, creating it if necessary, and watch
5254 for changes to the csv. Talon lists will be generated based on the keys of
5355 `default_values`. For example, if there is a key `foo`, there will be a
54- list created called `user.cursorless_foo` that will contain entries from
55- the original dict at the key `foo`, updated according to customization in
56- the csv at
56+ list created called `user.cursorless_foo` that will contain entries from the
57+ original dict at the key `foo`, updated according to customization in the
58+ csv at
5759
58- actions.path.talon_user() / "cursorless-settings" / filename
60+ ```
61+ actions.path.talon_user() / "cursorless-settings" / filename
62+ ```
5963
6064 Note that the settings directory location can be customized using the
6165 `user.cursorless_settings_directory` setting.
6266
6367 Args:
6468 filename (str): The name of the csv file to be placed in
65- `cursorles-settings` dir
66- default_values (dict[str, dict]): The default values for the lists to
67- be customized in the given csv
68- extra_ignored_values list[str]: Don't throw an exception if any of
69- these appear as values; just ignore them and don't add them to any list
70- allow_unknown_values bool: If unknown values appear, just put them in the list
71- default_list_name Optional[str]: If unknown values are allowed, put any
72- unknown values in this list
73- no_update_file Optional[bool]: Set this to `TRUE` to indicate that we should
74- 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
75- pluralize_lists: Create plural version of given lists
69+ `cursorles-settings` dir
70+ default_values (ListToSpokenForms): The default values for the lists to
71+ be customized in the given csv
72+ handle_new_values (Optional[Callable[[ListToSpokenForms], None]]): A
73+ callback to be called when the lists are updated
74+ extra_ignored_values (Optional[list[str]]): Don't throw an exception if
75+ any of these appear as values; just ignore them and don't add them
76+ to any list
77+ allow_unknown_values (bool): If unknown values appear, just put them in
78+ the list
79+ default_list_name (Optional[str]): If unknown values are
80+ allowed, put any unknown values in this list
81+ headers (list[str]): The headers to use for the csv
82+ no_update_file (bool): Set this to `True` to indicate that we should not
83+ update the csv. This is used generally in case there was an issue
84+ coming up with the default set of values so we don't want to persist
85+ those to disk
86+ pluralize_lists (list[str]): Create plural version of given lists
7687 """
7788 if extra_ignored_values is None :
7889 extra_ignored_values = []
7990
8091 file_path = get_full_path (filename )
81- output_file_path = get_output_path (filename )
8292 super_default_values = get_super_values (default_values )
8393
8494 file_path .parent .mkdir (parents = True , exist_ok = True )
8595
8696 check_for_duplicates (filename , default_values )
8797 create_default_vocabulary_dicts (default_values , pluralize_lists )
8898
89- try :
90- output_file_path .parent .mkdir (parents = True , exist_ok = True )
91- except Exception :
92- error_message = f"Error creating spoken form dir { output_file_path .parent } "
93- print (error_message )
94- app .notify (error_message )
95-
9699 def on_watch (path , flags ):
97100 if file_path .match (path ):
98101 current_values , has_errors = read_file (
@@ -109,9 +112,7 @@ def on_watch(path, flags):
109112 allow_unknown_values ,
110113 default_list_name ,
111114 pluralize_lists ,
112- output_file_path ,
113- get_type ,
114- ctx ,
115+ handle_new_values ,
115116 )
116117
117118 fs .watch (str (file_path .parent ), on_watch )
@@ -132,9 +133,7 @@ def on_watch(path, flags):
132133 allow_unknown_values ,
133134 default_list_name ,
134135 pluralize_lists ,
135- output_file_path ,
136- get_type ,
137- ctx ,
136+ handle_new_values ,
138137 )
139138 else :
140139 if not no_update_file :
@@ -146,9 +145,7 @@ def on_watch(path, flags):
146145 allow_unknown_values ,
147146 default_list_name ,
148147 pluralize_lists ,
149- output_file_path ,
150- get_type ,
151- ctx ,
148+ handle_new_values ,
152149 )
153150
154151 def unsubscribe ():
@@ -184,7 +181,7 @@ def create_default_vocabulary_dicts(
184181 if active_key :
185182 updated_dict [active_key ] = value2
186183 default_values_updated [key ] = updated_dict
187- assign_lists_to_context (default_ctx , default_values_updated , pluralize_lists )
184+ assign_lists_to_context (normalized_ctx , default_values_updated , pluralize_lists )
188185
189186
190187def update_dicts (
@@ -194,9 +191,7 @@ def update_dicts(
194191 allow_unknown_values : bool ,
195192 default_list_name : Optional [str ],
196193 pluralize_lists : list [str ],
197- output_file_path : Path ,
198- get_type : Optional [GetType ],
199- ctx : Context ,
194+ handle_new_values : Optional [Callable [[ListToSpokenForms ], None ]],
200195):
201196 # Create map with all default values
202197 results_map = {}
@@ -222,34 +217,32 @@ def update_dicts(
222217
223218 # Convert result map back to result list
224219 results = {res ["list" ]: {} for res in results_map .values ()}
225- output_file_entries = []
226220 for obj in results_map .values ():
227221 value = obj ["value" ]
228222 key = obj ["key" ]
229- spoken_forms = list (get_spoken_forms (value , key ))
230- for spoken_form in spoken_forms :
231- results [obj ["list" ]][spoken_form ] = value
232- if get_type is not None and (entry_type := get_type (obj ["list" ], value )):
233- output_file_entries .append (
234- {"type" : entry_type , "id" : value , "spokenForms" : spoken_forms }
235- )
223+ if not is_removed (key ):
224+ for k in key .split ("|" ):
225+ if value == "pasteFromClipboard" and k .endswith (" to" ):
226+ # FIXME: This is a hack to work around the fact that the
227+ # spoken form of the `pasteFromClipboard` action used to be
228+ # "paste to", but now the spoken form is just "paste" and
229+ # the "to" is part of the positional target. Users who had
230+ # cursorless before this change would have "paste to" as
231+ # their spoken form and so would need to say "paste to to".
232+ k = k [:- 3 ]
233+ results [obj ["list" ]][k .strip ()] = value
236234
237235 # Assign result to talon context list
238236 assign_lists_to_context (ctx , results , pluralize_lists )
239237
240- with open (output_file_path , "w" ) as out :
241- try :
242- out .write (json .dumps ({"version" : 0 , "entries" : output_file_entries }))
243- except Exception :
244- error_message = f"Error writing spoken form json { output_file_path } "
245- print (error_message )
246- app .notify (error_message )
238+ if handle_new_values is not None :
239+ handle_new_values (results )
247240
248241
249242def assign_lists_to_context (
250243 ctx : Context ,
251244 results : dict ,
252- pluralize_lists : Optional [ list [str ] ],
245+ pluralize_lists : list [str ],
253246):
254247 for list_name , dict in results .items ():
255248 list_singular_name = get_cursorless_list_name (list_name )
@@ -259,21 +252,6 @@ def assign_lists_to_context(
259252 ctx .lists [list_plural_name ] = {pluralize (k ): v for k , v in dict .items ()}
260253
261254
262- def get_spoken_forms (id : str , spoken_form : str ):
263- if not is_removed (spoken_form ):
264- for k in spoken_form .split ("|" ):
265- if id == "pasteFromClipboard" and k .endswith (" to" ):
266- # FIXME: This is a hack to work around the fact that the
267- # spoken form of the `pasteFromClipboard` action used to be
268- # "paste to", but now the spoken form is just "paste" and
269- # the "to" is part of the positional target. Users who had
270- # cursorless before this change would have "paste to" as
271- # their spoken form and so would need to say "paste to to".
272- k = k [:- 3 ]
273-
274- yield k .strip ()
275-
276-
277255def update_file (
278256 path : Path ,
279257 headers : list [str ],
@@ -414,27 +392,20 @@ def read_file(
414392 return result , has_errors
415393
416394
417- def get_full_path (output_file_path : str ):
418- if not output_file_path .endswith (".csv" ):
419- output_file_path = f"{ output_file_path } .csv"
395+ def get_full_path (filename : str ):
396+ if not filename .endswith (".csv" ):
397+ filename = f"{ filename } .csv"
420398
421399 user_dir : Path = actions .path .talon_user ()
422400 settings_directory = Path (cursorless_settings_directory .get ())
423401
424402 if not settings_directory .is_absolute ():
425403 settings_directory = user_dir / settings_directory
426404
427- return (settings_directory / output_file_path ).resolve ()
428-
429-
430- def get_output_path (output_file_path : str ):
431- if output_file_path .endswith (".csv" ):
432- output_file_path = output_file_path [:- 4 ]
433-
434- return Path .home () / ".cursorless" / "spokenForms" / f"{ output_file_path } .json"
405+ return (settings_directory / filename ).resolve ()
435406
436407
437- def get_super_values (values : dict [ str , dict [ str , str ]] ):
408+ def get_super_values (values : ListToSpokenForms ):
438409 result : dict [str , str ] = {}
439410 for value_dict in values .values ():
440411 result .update (value_dict )
0 commit comments