Skip to content

Commit a75e017

Browse files
authored
Write spokenForms.json from Talon (#1939)
- This PR is the Talon side of #1940 ## Checklist - [-] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) (will do in follow-up) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [x] I have not broken the cheatsheet - [x] have the json have its own version number
1 parent ddcf86d commit a75e017

File tree

4 files changed

+223
-67
lines changed

4 files changed

+223
-67
lines changed

cursorless-talon/src/csv_overrides.py

Lines changed: 103 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import csv
2+
from collections import defaultdict
23
from collections.abc import Container
4+
from dataclasses import dataclass
35
from datetime import datetime
46
from pathlib import Path
5-
from typing import Optional
7+
from typing import Callable, Iterable, Optional, TypedDict
68

79
from talon import Context, Module, actions, app, fs
810

@@ -25,50 +27,75 @@
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"""
3036
tag: 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+
3451
def 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

190217
def 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

243294
def 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)

cursorless-talon/src/marks/decorated_mark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def setup_hat_styles_csv(hat_colors: dict[str, str], hat_shapes: dict[str, str])
138138
"hat_color": active_hat_colors,
139139
"hat_shape": active_hat_shapes,
140140
},
141-
[*hat_colors.values(), *hat_shapes.values()],
141+
extra_ignored_values=[*hat_colors.values(), *hat_shapes.values()],
142142
no_update_file=is_shape_error or is_color_error,
143143
)
144144

0 commit comments

Comments
 (0)