Skip to content

Shape enablement using state #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 40 additions & 21 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Note: If you'd like to customize any of the spoken forms, please see the [docume
- [Marks](#marks)
- [Decorated symbol](#decorated-symbol)
- [Colors](#colors)
- [Shapes](#shapes)
- [`"this"`](#this)
- [`"that"`](#that)
- [Modifiers](#modifiers)
Expand Down Expand Up @@ -82,32 +83,50 @@ Combining this with an action, we might say `"take blue air"` to select the toke

The following colors are supported:

| Command | Visible color |
| ---------- | ------------- |
| `"blue"` | Blue |
| `"green"` | Green |
| `"rose"` | Red |
| `"squash"` | Yellow |
| `"plum"` | Pink |
| Spoken form | Visible color | Internal ID |
| ----------- | ------------- | ----------- |
| `"blue"` | Blue | `blue` |
| `"green"` | Green | `green` |
| `"rose"` | Red | `rose` |
| `"squash"` | Yellow | `yellow` |
| `"plum"` | Pink | `pink` |

###### Shapes
You can enable or disable colors in your VSCode settings, by searching for `cursorless.hatEnablement.colors` and checking the box next to the internal ID for the given shape as listed above.

The following shapes are supported:
You can also tweak the visible colors for any of these colors in your VSCode settings, by searching for `cursorless.colors` and changing the hex color code next to the internal ID for the given shape as listed above. Note that you can configure different colors for dark and light themes.

| Command | Visible color | Enabled by default? |
| --------- | ---------------- | ------------------- |
| `"splat"` | Four-point star | ✅ |
| `"fox"` | Chevron | ✅ |
| `"wing"` | Three-point star | ❌ |
| `"hole"` | Hole | ❌ |
| `"frame"` | Frame | ❌ |
| `"curve"` | Curve | ❌ |
| `"stare"` | Eye | ❌ |
If you find these color names unintuitive / tough to remember, their
spoken forms can be [customized](customization.md) like any other spoken form
in cursorless. If you change a spoken form to be more than one syllable, you
can change the penalty in the `cursorless.hatPenalties.colors` setting to the
number of syllables you use, so that cursorless can optimize hat allocation to
minimize syllables.

To enable or disable shapes requires the following two steps:
###### Shapes

The following shapes are supported:

1. Check the box corresponding to the given shape in the `cursorless.hatEnablement.shapes` field of the cursorless vscode settings
2. Enable the corresponding spoken form in the [spoken form customization csvs](customization.md) for cursorless talon
| Spoken form | Internal ID | Shape | Enabled by default? |
| ----------- | ----------- | ---------------------------------------------------------------------------------------------- | ------------------- |
| `"ex"` | `ex` | ![Ex](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/ex.svg) | ❌ |
| `"fox"` | `fox` | ![Fox](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/fox.svg) | ❌ |
| `"wing"` | `wing` | ![Wing](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/wing.svg) | ❌ |
| `"hole"` | `hole` | ![Hole](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/hole.svg) | ❌ |
| `"frame"` | `frame` | ![Frame](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/frame.svg) | ❌ |
| `"curve"` | `curve` | ![Curve](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/curve.svg) | ❌ |
| `"eye"` | `eye` | ![Eye](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/eye.svg) | ❌ |
| `"play"` | `play` | ![Play](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/play.svg) | ❌ |
| `"star"` | `star` | ![Star](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/star.svg) | ❌ |
| `"bolt"` | `bolt` | ![Bolt](https://github.com/raw/pokey/cursorless-vscode/main/images/hats/bolt.svg) | ❌ |

You can enable or disable shapes in your VSCode settings, by searching for `cursorless.hatEnablement.shapes` and checking the box next to the internal ID for the given shape as listed above.

If you find these shape names unintuitive / tough to remember, their
spoken forms can be [customized](customization.md) like any other spoken form
in cursorless. If you change a spoken form to be more than one syllable, you
can change the penalty in the `cursorless.hatPenalties.shapes` setting to the
number of syllables you use, so that cursorless can optimize hat allocation to
minimize syllables.

##### `"this"`

Expand Down
18 changes: 11 additions & 7 deletions src/cheat_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,17 @@ def draw(self, canvas):
},
)

self.next_row()
self.draw_header(canvas, "Colors")
self.draw_items(canvas, get_list("hat_color"))

self.next_row()
self.draw_header(canvas, "Shapes")
self.draw_items(canvas, get_list("hat_shape"))
hat_colors = get_list("hat_color")
if hat_colors:
self.next_row()
self.draw_header(canvas, "Colors")
self.draw_items(canvas, hat_colors)

hat_shapes = get_list("hat_shape")
if hat_shapes:
self.next_row()
self.draw_header(canvas, "Shapes")
self.draw_items(canvas, hat_shapes)

self.next_row()
self.draw_header(canvas, "Examples")
Expand Down
54 changes: 42 additions & 12 deletions src/csv_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
)


def init_csv_and_watch_changes(filename: str, default_values: dict[str, dict]):
def init_csv_and_watch_changes(
filename: str,
default_values: dict[str, dict],
extra_acceptable_values: list[str] = None,
):
"""
Initialize a cursorless settings csv, creating it if necessary, and watch
for changes to the csv. Talon lists will be generated based on the keys of
Expand All @@ -33,7 +37,12 @@ def init_csv_and_watch_changes(filename: str, default_values: dict[str, dict]):
`cursorles-settings` dir
default_values (dict[str, dict]): The default values for the lists to
be customized in the given csv
extra_acceptable_values list[str]: Don't throw an exception if any of
these appear as values
"""
if extra_acceptable_values is None:
extra_acceptable_values = []

dir_path, file_path = get_file_paths(filename)
super_default_values = get_super_values(default_values)

Expand All @@ -42,25 +51,36 @@ def init_csv_and_watch_changes(filename: str, default_values: dict[str, dict]):
def on_watch(path, flags):
if file_path.match(path):
current_values, has_errors = read_file(
file_path, super_default_values.values()
file_path, super_default_values.values(), extra_acceptable_values
)
update_dicts(default_values, current_values)
update_dicts(default_values, current_values, extra_acceptable_values)

fs.watch(dir_path, on_watch)

if file_path.is_file():
current_values = update_file(file_path, super_default_values)
update_dicts(default_values, current_values)
current_values = update_file(
file_path, super_default_values, extra_acceptable_values
)
update_dicts(default_values, current_values, extra_acceptable_values)
else:
create_file(file_path, super_default_values)
update_dicts(default_values, super_default_values)
update_dicts(default_values, super_default_values, extra_acceptable_values)

def unsubscribe():
fs.unwatch(dir_path, on_watch)

return unsubscribe


def is_removed(value: str):
return value.startswith("-")


def update_dicts(default_values: dict[str, dict], current_values: dict):
def update_dicts(
default_values: dict[str, dict],
current_values: dict,
extra_acceptable_values: list[str],
):
# Create map with all default values
results_map = {}
for list_name, dict in default_values.items():
Expand All @@ -69,7 +89,13 @@ def update_dicts(default_values: dict[str, dict], current_values: dict):

# Update result with current values
for key, value in current_values.items():
results_map[value]["key"] = key
try:
results_map[value]["key"] = key
except KeyError:
if value in extra_acceptable_values:
pass
else:
raise

# Convert result map back to result list
results = {key: {} for key in default_values}
Expand All @@ -84,8 +110,10 @@ def update_dicts(default_values: dict[str, dict], current_values: dict):
ctx.lists[get_cursorless_list_name(list_name)] = dict


def update_file(path: Path, default_values: dict):
current_values, has_errors = read_file(path, default_values.values())
def update_file(path: Path, default_values: dict, extra_acceptable_values: list[str]):
current_values, has_errors = read_file(
path, default_values.values(), extra_acceptable_values
)
current_identifiers = current_values.values()

missing = {}
Expand Down Expand Up @@ -149,7 +177,9 @@ def csv_error(path: Path, index: int, message: str, value: str):
print(f"ERROR: {path}:{index+1}: {message} '{value}'")


def read_file(path: Path, default_identifiers: list[str]):
def read_file(
path: Path, default_identifiers: list[str], extra_acceptable_values: list[str]
):
with open(path) as f:
lines = list(f)

Expand Down Expand Up @@ -180,7 +210,7 @@ def read_file(path: Path, default_identifiers: list[str]):
seen_header = True
continue

if value not in default_identifiers:
if value not in default_identifiers and value not in extra_acceptable_values:
has_errors = True
csv_error(path, i, "Unknown identifier", value)
continue
Expand Down
75 changes: 60 additions & 15 deletions src/marks/mark.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from pathlib import Path
from ..conventions import get_cursorless_list_name
from talon import Module, app, Context
from talon import Module, actions, app, Context, fs, cron
from ..csv_overrides import init_csv_and_watch_changes

mod = Module()
Expand All @@ -21,13 +22,16 @@
}

hat_shapes = {
"splat": "fourPointStar",
"fox": "chevron",
"-wing": "threePointStar",
"-hole": "hole",
"-frame": "frame",
"-curve": "curve",
"-stare": "eye",
"ex": "ex",
"fox": "fox",
"wing": "wing",
"hole": "hole",
"frame": "frame",
"curve": "curve",
"eye": "eye",
"play": "play",
"star": "star",
"bolt": "bolt",
}


Expand Down Expand Up @@ -97,20 +101,61 @@ def cursorless_mark(m) -> str:
return m.cursorless_line_number_simple


def on_ready():
init_csv_and_watch_changes(
"special_marks",
unsubscribe_hat_styles = None


color_enablements = set()
shape_enablements = set()


def setup_hat_styles_csv(enablements: dict):
global unsubscribe_hat_styles, color_enablements, shape_enablements

new_color_enablements = set(enablements["colors"])
new_shape_enablements = set(enablements["shapes"])

if (
color_enablements == new_color_enablements
and shape_enablements == new_shape_enablements
):
return

shape_enablements = new_shape_enablements
color_enablements = new_color_enablements

active_hat_colors = {
spoken_form: value
for spoken_form, value in hat_colors.items()
if value in enablements["colors"]
}
active_hat_shapes = {
spoken_form: value
for spoken_form, value in hat_shapes.items()
if value in enablements["shapes"]
}

if unsubscribe_hat_styles is not None:
unsubscribe_hat_styles()

unsubscribe_hat_styles = init_csv_and_watch_changes(
"hat_styles",
{
"special_mark": special_marks_defaults,
"hat_color": active_hat_colors,
"hat_shape": active_hat_shapes,
},
[*hat_colors.values(), *hat_shapes.values()],
)


def on_ready():
init_csv_and_watch_changes(
"hat_styles",
"special_marks",
{
"hat_color": hat_colors,
"hat_shape": hat_shapes,
"special_mark": special_marks_defaults,
},
)

actions.user.watch_vscode_state("cursorless.hatEnablement", setup_hat_styles_csv)


app.register("ready", on_ready)
58 changes: 58 additions & 0 deletions src/marks/vscode_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import json
import os
from talon import Context, Module, actions, cron
from pathlib import Path
from ..vendor.jstyleson import loads

mod = Module()

windows_ctx = Context()
mac_ctx = Context()
linux_ctx = Context()

windows_ctx.matches = r"""
os: windows
"""
mac_ctx.matches = r"""
os: mac
"""
linux_ctx.matches = r"""
os: linux
"""


@mod.action_class
class Actions:
def vscode_settings_path() -> Path:
"""Get path of vscode settings json file"""
pass

def vscode_get_setting(key: str, default_value: any = None):
"""Get the value of vscode setting at the given key"""
path: Path = actions.user.vscode_settings_path()
settings: dict = loads(path.read_text())

if default_value is not None:
return settings.get(key, default_value)
else:
return settings[key]


@mac_ctx.action_class("user")
class MacUserActions:
def vscode_settings_path() -> Path:
return Path(
f"{os.environ['HOME']}/Library/Application Support/Code/User/settings.json"
)


@linux_ctx.action_class("user")
class LinuxUserActions:
def vscode_settings_path() -> Path:
return Path(f"{os.environ['HOME']}/.config/Code/User/settings.json")


@windows_ctx.action_class("user")
class WindowsUserActions:
def vscode_settings_path() -> Path:
return Path(f"{os.environ['APPDATA']}/Code/User/settings.json")
Empty file added src/vendor/__init__.py
Empty file.
Loading