Skip to content

Commit 14c71d5

Browse files
authored
Reimplement cheatsheet using html (#619)
* Cheatsheet nx html generation * HTML cheatsheet talon side * HTML cheatsheet VSCode extension-side * Tweak deploy * identifier => id * Tweaks from PR feedback * Rename dir * update defaults.json * Remove nx-welcome.tsx
1 parent d4e0d71 commit 14c71d5

File tree

7 files changed

+312
-5
lines changed

7 files changed

+312
-5
lines changed

src/cheatsheet/cheat_sheet.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import math
2-
import webbrowser
32
from typing import Optional
43

54
from talon import Module, actions, cron, skia, ui
@@ -302,10 +301,6 @@ def cursorless_cheat_sheet_toggle():
302301
cheat_sheet = CheatSheet()
303302
actions.mode.enable("user.cursorless_cheat_sheet")
304303

305-
def cursorless_open_instructions():
306-
"""Open web page with cursorless instructions"""
307-
webbrowser.open(instructions_url)
308-
309304

310305
def get_y(canvas):
311306
return canvas.y + outer_padding

src/cheatsheet_html/cheat_sheet.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import tempfile
2+
import webbrowser
3+
from pathlib import Path
4+
5+
from talon import Module, actions
6+
7+
from .get_list import get_list, get_lists
8+
from .sections.actions import get_actions
9+
from .sections.compound_targets import get_compound_targets
10+
from .sections.scopes import get_scopes
11+
12+
mod = Module()
13+
14+
cheatsheet_out_dir = Path(tempfile.mkdtemp())
15+
instructions_url = "https://www.cursorless.org/docs/"
16+
17+
18+
@mod.action_class
19+
class Actions:
20+
def cursorless_cheat_sheet_show_html():
21+
"""Show new cursorless html cheat sheet"""
22+
cheatsheet_out_path = cheatsheet_out_dir / "cheatsheet.html"
23+
actions.user.vscode_with_plugin_and_wait(
24+
"cursorless.showCheatsheet",
25+
{
26+
"version": 0,
27+
"spokenFormInfo": actions.user.cursorless_cheat_sheet_get_json(),
28+
"outputPath": str(cheatsheet_out_path),
29+
},
30+
)
31+
webbrowser.open(cheatsheet_out_path.as_uri())
32+
33+
def cursorless_cheat_sheet_get_json():
34+
"""Get cursorless cheat sheet json"""
35+
return {
36+
"sections": [
37+
{
38+
"name": "Actions",
39+
"id": "actions",
40+
"items": get_actions(),
41+
},
42+
{
43+
"name": "Scopes",
44+
"id": "scopes",
45+
"items": get_scopes(),
46+
},
47+
{
48+
"name": "Paired delimiters",
49+
"id": "pairedDelimiters",
50+
"items": get_lists(
51+
[
52+
"wrapper_only_paired_delimiter",
53+
"wrapper_selectable_paired_delimiter",
54+
"selectable_only_paired_delimiter",
55+
],
56+
"pairedDelimiter",
57+
),
58+
},
59+
{
60+
"name": "Special marks",
61+
"id": "specialMarks",
62+
"items": get_list("special_mark", "mark"),
63+
},
64+
{
65+
"name": "Positions",
66+
"id": "positions",
67+
"items": get_list("position", "position"),
68+
},
69+
{
70+
"name": "Compound targets",
71+
"id": "compoundTargets",
72+
"items": get_compound_targets(),
73+
},
74+
{
75+
"name": "Colors",
76+
"id": "colors",
77+
"items": get_list("hat_color", "hatColor"),
78+
},
79+
{
80+
"name": "Shapes",
81+
"id": "shapes",
82+
"items": get_list("hat_shape", "hatShape"),
83+
},
84+
]
85+
}
86+
87+
def cursorless_open_instructions():
88+
"""Open web page with cursorless instructions"""
89+
webbrowser.open(instructions_url)

src/cheatsheet_html/get_list.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import re
2+
3+
from talon import registry
4+
5+
from ..conventions import get_cursorless_list_name
6+
7+
8+
def get_list(name, type, descriptions=None):
9+
if descriptions is None:
10+
descriptions = {}
11+
12+
items = get_raw_list(name)
13+
item_dict = items if isinstance(items, dict) else {item: item for item in items}
14+
15+
return make_dict_readable(type, item_dict, descriptions)
16+
17+
18+
def get_lists(names: list[str], type: str, descriptions=None):
19+
20+
return [item for name in names for item in get_list(name, type, descriptions)]
21+
22+
23+
def get_raw_list(name):
24+
cursorless_list_name = get_cursorless_list_name(name)
25+
return registry.lists[cursorless_list_name][0].copy()
26+
27+
28+
def make_dict_readable(type: str, dict, descriptions=None):
29+
if descriptions is None:
30+
descriptions = {}
31+
32+
return [
33+
{
34+
"id": value,
35+
"type": type,
36+
"variations": [
37+
{
38+
"spokenForm": key,
39+
"description": descriptions.get(value, make_readable(value)),
40+
}
41+
],
42+
}
43+
for key, value in dict.items()
44+
]
45+
46+
47+
def make_readable(text):
48+
text = text.replace(".", " ")
49+
return de_camel(text).lower().capitalize()
50+
51+
52+
def de_camel(text: str) -> str:
53+
"""Replacing camelCase boundaries with blank space"""
54+
return re.sub(
55+
r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])|(?<=[0-9])(?=[a-zA-Z])",
56+
" ",
57+
text,
58+
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from ...actions.actions import ACTION_LIST_NAMES
2+
from ..get_list import get_raw_list, make_dict_readable
3+
4+
5+
def get_actions():
6+
all_actions = {}
7+
for name in ACTION_LIST_NAMES:
8+
all_actions.update(get_raw_list(name))
9+
10+
multiple_target_action_names = [
11+
"replaceWithTarget",
12+
"moveToTarget",
13+
"swapTargets",
14+
"applyFormatter",
15+
"wrapWithPairedDelimiter",
16+
"rewrap",
17+
]
18+
simple_actions = {
19+
f"{key} <T>": value
20+
for key, value in all_actions.items()
21+
if value not in multiple_target_action_names
22+
}
23+
complex_actions = {
24+
value: key
25+
for key, value in all_actions.items()
26+
if value in multiple_target_action_names
27+
}
28+
29+
swap_connective = list(get_raw_list("swap_connective").keys())[0]
30+
source_destination_connective = list(
31+
get_raw_list("source_destination_connective").keys()
32+
)[0]
33+
34+
return [
35+
*make_dict_readable(
36+
"action",
37+
simple_actions,
38+
{
39+
"callAsFunction": "Call T on S",
40+
},
41+
),
42+
{
43+
"id": "replaceWithTarget",
44+
"type": "action",
45+
"variations": [
46+
{
47+
"spokenForm": f"{complex_actions['replaceWithTarget']} <T1> {source_destination_connective} <T2>",
48+
"description": "Replace T2 with T1",
49+
},
50+
{
51+
"spokenForm": f"{complex_actions['replaceWithTarget']} <T>",
52+
"description": "Replace S with T",
53+
},
54+
],
55+
},
56+
{
57+
"id": "moveToTarget",
58+
"type": "action",
59+
"variations": [
60+
{
61+
"spokenForm": f"{complex_actions['moveToTarget']} <T1> {source_destination_connective} <T2>",
62+
"description": "Move T1 to T2",
63+
},
64+
{
65+
"spokenForm": f"{complex_actions['moveToTarget']} <T>",
66+
"description": "Move T to S",
67+
},
68+
],
69+
},
70+
{
71+
"id": "swapTargets",
72+
"type": "action",
73+
"variations": [
74+
{
75+
"spokenForm": f"{complex_actions['swapTargets']} <T1> {swap_connective} <T2>",
76+
"description": "Swap T1 with T2",
77+
},
78+
{
79+
"spokenForm": f"{complex_actions['swapTargets']} {swap_connective} <T>",
80+
"description": "Swap S with T",
81+
},
82+
],
83+
},
84+
{
85+
"id": "applyFormatter",
86+
"type": "action",
87+
"variations": [
88+
{
89+
"spokenForm": f"{complex_actions['applyFormatter']} <F> at <T>",
90+
"description": "Reformat T as F",
91+
}
92+
],
93+
},
94+
{
95+
"id": "wrapWithPairedDelimiter",
96+
"type": "action",
97+
"variations": [
98+
{
99+
"spokenForm": f"<P> {complex_actions['wrapWithPairedDelimiter']} <T>",
100+
"description": "Wrap T with P",
101+
}
102+
],
103+
},
104+
{
105+
"id": "rewrap",
106+
"type": "action",
107+
"variations": [
108+
{
109+
"spokenForm": f"<P> {complex_actions['rewrap']} <T>",
110+
"description": "Rewrap T with P",
111+
}
112+
],
113+
},
114+
]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from ..get_list import get_raw_list
2+
3+
4+
def get_compound_targets():
5+
include_both_term = next(
6+
spoken_form
7+
for spoken_form, value in get_raw_list("range_connective").items()
8+
if value == "rangeInclusive"
9+
)
10+
list_connective_term = next(
11+
spoken_form
12+
for spoken_form, value in get_raw_list("list_connective").items()
13+
if value == "listConnective"
14+
)
15+
16+
return [
17+
{
18+
"id": "listConnective",
19+
"type": "compoundTargetConnective",
20+
"variations": [
21+
{
22+
"spokenForm": f"<T1> {list_connective_term} <T2>",
23+
"description": "T1 and T2",
24+
},
25+
],
26+
},
27+
{
28+
"id": "rangeInclusive",
29+
"type": "action",
30+
"variations": [
31+
{
32+
"spokenForm": f"<T1> {include_both_term} <T2>",
33+
"description": "T1 through T2",
34+
},
35+
{
36+
"spokenForm": f"{include_both_term} <T>",
37+
"description": "S through T",
38+
},
39+
],
40+
},
41+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from ..get_list import get_lists
2+
3+
4+
def get_scopes():
5+
return get_lists(
6+
["scope_type", "subtoken_scope_type"],
7+
"scopeType",
8+
{"argumentOrParameter": "Argument"},
9+
)

src/cursorless.talon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ app: vscode
2121

2222
cursorless help: user.cursorless_cheat_sheet_toggle()
2323
cursorless instructions: user.cursorless_open_instructions()
24+
cursorless reference: user.cursorless_cheat_sheet_show_html()

0 commit comments

Comments
 (0)