1
1
import csv
2
- import json
3
2
from collections .abc import Container
4
3
from datetime import datetime
5
4
from pathlib import Path
26
25
desc = "The directory to use for cursorless settings csvs relative to talon user directory" ,
27
26
)
28
27
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"""
31
34
tag: user.cursorless_default_vocabulary
32
35
"""
33
36
34
37
35
- GetType = Callable [[ str , str ] , str ]
38
+ ListToSpokenForms = dict [ str , dict [ str , str ] ]
36
39
37
40
38
41
def init_csv_and_watch_changes (
39
42
filename : str ,
40
- default_values : dict [str , dict [str , str ]],
43
+ default_values : ListToSpokenForms ,
44
+ handle_new_values : Optional [Callable [[ListToSpokenForms ], None ]] = None ,
41
45
extra_ignored_values : Optional [list [str ]] = None ,
42
46
allow_unknown_values : bool = False ,
43
47
default_list_name : Optional [str ] = None ,
44
48
headers : list [str ] = [SPOKEN_FORM_HEADER , CURSORLESS_IDENTIFIER_HEADER ],
45
- ctx : Context = Context (),
46
49
no_update_file : bool = False ,
47
50
pluralize_lists : list [str ] = [],
48
- get_type : Optional [GetType ] = None ,
49
51
):
50
52
"""
51
53
Initialize a cursorless settings csv, creating it if necessary, and watch
52
54
for changes to the csv. Talon lists will be generated based on the keys of
53
55
`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
57
59
58
- actions.path.talon_user() / "cursorless-settings" / filename
60
+ ```
61
+ actions.path.talon_user() / "cursorless-settings" / filename
62
+ ```
59
63
60
64
Note that the settings directory location can be customized using the
61
65
`user.cursorless_settings_directory` setting.
62
66
63
67
Args:
64
68
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
76
87
"""
77
88
if extra_ignored_values is None :
78
89
extra_ignored_values = []
79
90
80
91
file_path = get_full_path (filename )
81
- output_file_path = get_output_path (filename )
82
92
super_default_values = get_super_values (default_values )
83
93
84
94
file_path .parent .mkdir (parents = True , exist_ok = True )
85
95
86
96
check_for_duplicates (filename , default_values )
87
97
create_default_vocabulary_dicts (default_values , pluralize_lists )
88
98
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
-
96
99
def on_watch (path , flags ):
97
100
if file_path .match (path ):
98
101
current_values , has_errors = read_file (
@@ -109,9 +112,7 @@ def on_watch(path, flags):
109
112
allow_unknown_values ,
110
113
default_list_name ,
111
114
pluralize_lists ,
112
- output_file_path ,
113
- get_type ,
114
- ctx ,
115
+ handle_new_values ,
115
116
)
116
117
117
118
fs .watch (str (file_path .parent ), on_watch )
@@ -132,9 +133,7 @@ def on_watch(path, flags):
132
133
allow_unknown_values ,
133
134
default_list_name ,
134
135
pluralize_lists ,
135
- output_file_path ,
136
- get_type ,
137
- ctx ,
136
+ handle_new_values ,
138
137
)
139
138
else :
140
139
if not no_update_file :
@@ -146,9 +145,7 @@ def on_watch(path, flags):
146
145
allow_unknown_values ,
147
146
default_list_name ,
148
147
pluralize_lists ,
149
- output_file_path ,
150
- get_type ,
151
- ctx ,
148
+ handle_new_values ,
152
149
)
153
150
154
151
def unsubscribe ():
@@ -184,7 +181,7 @@ def create_default_vocabulary_dicts(
184
181
if active_key :
185
182
updated_dict [active_key ] = value2
186
183
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 )
188
185
189
186
190
187
def update_dicts (
@@ -194,9 +191,7 @@ def update_dicts(
194
191
allow_unknown_values : bool ,
195
192
default_list_name : Optional [str ],
196
193
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 ]],
200
195
):
201
196
# Create map with all default values
202
197
results_map = {}
@@ -222,34 +217,32 @@ def update_dicts(
222
217
223
218
# Convert result map back to result list
224
219
results = {res ["list" ]: {} for res in results_map .values ()}
225
- output_file_entries = []
226
220
for obj in results_map .values ():
227
221
value = obj ["value" ]
228
222
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
236
234
237
235
# Assign result to talon context list
238
236
assign_lists_to_context (ctx , results , pluralize_lists )
239
237
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 )
247
240
248
241
249
242
def assign_lists_to_context (
250
243
ctx : Context ,
251
244
results : dict ,
252
- pluralize_lists : Optional [ list [str ] ],
245
+ pluralize_lists : list [str ],
253
246
):
254
247
for list_name , dict in results .items ():
255
248
list_singular_name = get_cursorless_list_name (list_name )
@@ -259,21 +252,6 @@ def assign_lists_to_context(
259
252
ctx .lists [list_plural_name ] = {pluralize (k ): v for k , v in dict .items ()}
260
253
261
254
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
-
277
255
def update_file (
278
256
path : Path ,
279
257
headers : list [str ],
@@ -414,27 +392,20 @@ def read_file(
414
392
return result , has_errors
415
393
416
394
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"
420
398
421
399
user_dir : Path = actions .path .talon_user ()
422
400
settings_directory = Path (cursorless_settings_directory .get ())
423
401
424
402
if not settings_directory .is_absolute ():
425
403
settings_directory = user_dir / settings_directory
426
404
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 ()
435
406
436
407
437
- def get_super_values (values : dict [ str , dict [ str , str ]] ):
408
+ def get_super_values (values : ListToSpokenForms ):
438
409
result : dict [str , str ] = {}
439
410
for value_dict in values .values ():
440
411
result .update (value_dict )
0 commit comments