237 lines
7.4 KiB
Python
237 lines
7.4 KiB
Python
import json
|
|
from pathlib import Path
|
|
from typing import Callable, Concatenate, ParamSpec, TypeVar
|
|
|
|
from talon import app, cron, fs, registry
|
|
|
|
from .actions.actions import ACTION_LIST_NAMES
|
|
from .csv_overrides import (
|
|
SPOKEN_FORM_HEADER,
|
|
ListToSpokenForms,
|
|
SpokenFormEntry,
|
|
init_csv_and_watch_changes,
|
|
)
|
|
from .get_grapheme_spoken_form_entries import (
|
|
get_grapheme_spoken_form_entries,
|
|
get_graphemes_talon_list,
|
|
grapheme_capture_name,
|
|
)
|
|
from .marks.decorated_mark import init_hats
|
|
from .spoken_forms_output import SpokenFormsOutput
|
|
from .spoken_scope_forms import init_scope_spoken_forms
|
|
|
|
JSON_FILE = Path(__file__).parent / "spoken_forms.json"
|
|
disposables: list[Callable] = []
|
|
|
|
|
|
P = ParamSpec("P")
|
|
R = TypeVar("R")
|
|
|
|
|
|
def auto_construct_defaults(
|
|
spoken_forms: dict[str, ListToSpokenForms],
|
|
handle_new_values: Callable[[str, list[SpokenFormEntry]], None],
|
|
f: Callable[
|
|
Concatenate[str, ListToSpokenForms, Callable[[list[SpokenFormEntry]], None], P],
|
|
R,
|
|
],
|
|
):
|
|
"""
|
|
Decorator that automatically constructs the default values for the
|
|
`default_values` parameter of `f` based on the spoken forms in
|
|
`spoken_forms`, by extracting the value at the key given by the csv
|
|
filename.
|
|
|
|
Note that we only ever pass `init_csv_and_watch_changes` as `f`. The
|
|
reason we have this decorator is so that we can destructure the kwargs
|
|
of `init_csv_and_watch_changes` to remove the `default_values` parameter.
|
|
|
|
Args:
|
|
spoken_forms (dict[str, ListToSpokenForms]): The spoken forms
|
|
handle_new_values (Callable[[ListToSpokenForms], None]): A callback to be called when the lists are updated
|
|
f (Callable[Concatenate[str, ListToSpokenForms, P], R]): Will always be `init_csv_and_watch_changes`
|
|
"""
|
|
|
|
def ret(filename: str, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
default_values = spoken_forms[filename]
|
|
return f(
|
|
filename,
|
|
default_values,
|
|
lambda new_values: handle_new_values(filename, new_values),
|
|
*args,
|
|
**kwargs,
|
|
)
|
|
|
|
return ret
|
|
|
|
|
|
# Maps from Talon list name to the type of the value in that list, e.g.
|
|
# `pairedDelimiter` or `simpleScopeTypeType`
|
|
# FIXME: This is a hack until we generate spoken_forms.json from Typescript side
|
|
# At that point we can just include its type as part of that file
|
|
LIST_TO_TYPE_MAP = {
|
|
"wrapper_selectable_paired_delimiter": "pairedDelimiter",
|
|
"selectable_only_paired_delimiter": "pairedDelimiter",
|
|
"wrapper_only_paired_delimiter": "pairedDelimiter",
|
|
"surrounding_pair_scope_type": "pairedDelimiter",
|
|
"scope_type": "simpleScopeTypeType",
|
|
"glyph_scope_type": "complexScopeTypeType",
|
|
"custom_regex_scope_type": "customRegex",
|
|
**{
|
|
action_list_name: "action"
|
|
for action_list_name in ACTION_LIST_NAMES
|
|
if action_list_name != "custom_action"
|
|
},
|
|
"custom_action": "customAction",
|
|
}
|
|
|
|
|
|
def update():
|
|
global disposables
|
|
|
|
for disposable in disposables:
|
|
disposable()
|
|
|
|
with open(JSON_FILE, encoding="utf-8") as file:
|
|
spoken_forms = json.load(file)
|
|
|
|
initialized = False
|
|
|
|
# Maps from csv name to list of SpokenFormEntry
|
|
custom_spoken_forms: dict[str, list[SpokenFormEntry]] = {}
|
|
spoken_forms_output = SpokenFormsOutput()
|
|
spoken_forms_output.init()
|
|
graphemes_talon_list = get_graphemes_talon_list()
|
|
|
|
def update_spoken_forms_output():
|
|
spoken_forms_output.write(
|
|
[
|
|
*[
|
|
{
|
|
"type": LIST_TO_TYPE_MAP[entry.list_name],
|
|
"id": entry.id,
|
|
"spokenForms": entry.spoken_forms,
|
|
}
|
|
for spoken_form_list in custom_spoken_forms.values()
|
|
for entry in spoken_form_list
|
|
if entry.list_name in LIST_TO_TYPE_MAP
|
|
],
|
|
*get_grapheme_spoken_form_entries(graphemes_talon_list),
|
|
]
|
|
)
|
|
|
|
def handle_new_values(csv_name: str, values: list[SpokenFormEntry]):
|
|
custom_spoken_forms[csv_name] = values
|
|
if initialized:
|
|
# On first run, we just do one update at the end, so we suppress
|
|
# writing until we get there
|
|
init_scope_spoken_forms(graphemes_talon_list)
|
|
update_spoken_forms_output()
|
|
|
|
handle_csv = auto_construct_defaults(
|
|
spoken_forms, handle_new_values, init_csv_and_watch_changes
|
|
)
|
|
|
|
disposables = [
|
|
handle_csv("actions.csv"),
|
|
handle_csv("target_connectives.csv"),
|
|
handle_csv("modifiers.csv"),
|
|
handle_csv("positions.csv"),
|
|
handle_csv(
|
|
"paired_delimiters.csv",
|
|
pluralize_lists=[
|
|
"selectable_only_paired_delimiter",
|
|
"wrapper_selectable_paired_delimiter",
|
|
],
|
|
),
|
|
handle_csv("special_marks.csv"),
|
|
handle_csv("scope_visualizer.csv"),
|
|
handle_csv("experimental/experimental_actions.csv"),
|
|
handle_csv("experimental/miscellaneous.csv"),
|
|
handle_csv(
|
|
"modifier_scope_types.csv",
|
|
pluralize_lists=[
|
|
"scope_type",
|
|
"glyph_scope_type",
|
|
"surrounding_pair_scope_type",
|
|
],
|
|
extra_allowed_values=[
|
|
"private.fieldAccess",
|
|
"private.switchStatementSubject",
|
|
"textFragment",
|
|
"disqualifyDelimiter",
|
|
],
|
|
default_list_name="scope_type",
|
|
),
|
|
handle_csv(
|
|
"experimental/wrapper_snippets.csv",
|
|
allow_unknown_values=True,
|
|
default_list_name="wrapper_snippet",
|
|
),
|
|
handle_csv(
|
|
"experimental/insertion_snippets.csv",
|
|
allow_unknown_values=True,
|
|
default_list_name="insertion_snippet_no_phrase",
|
|
),
|
|
handle_csv(
|
|
"experimental/insertion_snippets_single_phrase.csv",
|
|
allow_unknown_values=True,
|
|
default_list_name="insertion_snippet_single_phrase",
|
|
),
|
|
handle_csv(
|
|
"experimental/actions_custom.csv",
|
|
headers=[SPOKEN_FORM_HEADER, "VSCode command"],
|
|
allow_unknown_values=True,
|
|
default_list_name="custom_action",
|
|
),
|
|
handle_csv(
|
|
"experimental/regex_scope_types.csv",
|
|
headers=[SPOKEN_FORM_HEADER, "Regex"],
|
|
allow_unknown_values=True,
|
|
default_list_name="custom_regex_scope_type",
|
|
pluralize_lists=["custom_regex_scope_type"],
|
|
),
|
|
init_hats(
|
|
spoken_forms["hat_styles.csv"]["hat_color"],
|
|
spoken_forms["hat_styles.csv"]["hat_shape"],
|
|
),
|
|
]
|
|
|
|
init_scope_spoken_forms(graphemes_talon_list)
|
|
update_spoken_forms_output()
|
|
initialized = True
|
|
|
|
|
|
def on_watch(path, flags):
|
|
if JSON_FILE.match(path):
|
|
update()
|
|
|
|
|
|
update_captures_cron = None
|
|
|
|
|
|
def update_captures_debounced(updated_captures: set[str]):
|
|
if grapheme_capture_name not in updated_captures:
|
|
return
|
|
|
|
global update_captures_cron
|
|
cron.cancel(update_captures_cron)
|
|
update_captures_cron = cron.after("100ms", update_captures)
|
|
|
|
|
|
def update_captures():
|
|
global update_captures_cron
|
|
update_captures_cron = None
|
|
|
|
update()
|
|
|
|
|
|
def on_ready():
|
|
update()
|
|
|
|
registry.register("update_captures", update_captures_debounced)
|
|
|
|
fs.watch(str(JSON_FILE.parent), on_watch)
|
|
|
|
|
|
app.register("ready", on_ready)
|