diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..4e18b8d --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,12 @@ +name: pytest +on: + pull_request: + workflow_dispatch: +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/setup-python + - run: | + uv run pytest diff --git a/docs/schema/cheatsheet.schema.json b/docs/schema/cheatsheet.schema.json index 6b4f098..e255089 100644 --- a/docs/schema/cheatsheet.schema.json +++ b/docs/schema/cheatsheet.schema.json @@ -42,7 +42,7 @@ "shortcuts": { "type": "object", "minProperties": 1, - "description": "Categories mapped to shortcut definitions.", + "description": "Categories mapped to shortcut definitions. Shortcuts use '+' for simultaneous keys (e.g., 'CMD+C') and '>' for sequential keys/chords (e.g., 'Super+T>W>S').", "additionalProperties": { "type": "object", "minProperties": 1 @@ -68,7 +68,7 @@ } }, { - "description": "When AllowText is not true, restrict shortcut key syntax.", + "description": "When AllowText is not true, restrict shortcut key syntax. Supports '+' for simultaneous keys and '>' for sequential shortcuts (chords).", "if": { "not": { "properties": { "AllowText": { "const": true } }, @@ -120,6 +120,10 @@ "General": { "CMD+C": { "description": "Copy selected item" }, "CMD+Right": { "description": "Move cursor to end of line" } + }, + "Sequential Shortcuts": { + "Super+T>W>S": { "description": "Multi-sequence shortcut example" }, + "CTRL+K>CTRL+C": { "description": "Add line comment" } } } } diff --git a/pyproject.toml b/pyproject.toml index 7010708..216f18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ requires-python = ">=3.9" dependencies = ["jinja2>=3.1.6", "python-dotenv>=1.1.1", "ruamel-yaml>=0.18.14"] [dependency-groups] -dev = ["bandit>=1.8.6", "ruff>=0.14.0", "ty>=0.0.1a22", "vulture>=2.14"] +dev = ["bandit>=1.8.6", "ruff>=0.14.0", "ty>=0.0.1a22", "vulture>=2.14", "pytest>=8.4.2"] + [tool.ruff] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..edcbd06 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers + --disable-warnings + diff --git a/src/generate_cheatsheet.py b/src/generate_cheatsheet.py index 580e442..3db69e6 100644 --- a/src/generate_cheatsheet.py +++ b/src/generate_cheatsheet.py @@ -1,11 +1,14 @@ -from ruamel.yaml import YAML -import sys import os -from validate_yaml import validate_yaml, lint_yaml +import re +import sys +from pathlib import Path + from dotenv import load_dotenv -from template_renderer import render_template +from ruamel.yaml import YAML + from logger import get_logger -from pathlib import Path +from template_renderer import render_template +from validate_yaml import lint_yaml, validate_yaml yaml_safe = YAML(typ="safe") yaml_rw = YAML() @@ -56,17 +59,32 @@ def replace_shortcut_names(shortcut, system_mappings): try: processed_parts = [] i = 0 + shortcut = re.sub(r"(\+|\>)\s*(\+|\>)", r"\g<1>\g<2>", shortcut) + while i < len(shortcut): if shortcut[i] == "+": if i + 1 < len(shortcut) and shortcut[i + 1] == "+": - processed_parts.append("+") + processed_parts.append("+") + i += 2 + elif i + 1 < len(shortcut) and shortcut[i + 1] == ">": + processed_parts.append(">") i += 2 else: processed_parts.append("") i += 1 + elif shortcut[i] == ">": + if i + 1 < len(shortcut) and shortcut[i + 1] == ">": + processed_parts.append(">") + i += 2 + elif i + 1 < len(shortcut) and shortcut[i + 1] == "+": + processed_parts.append("+") + i += 2 + else: + processed_parts.append("") + i += 1 else: current_part = "" - while i < len(shortcut) and shortcut[i] != "+": + while i < len(shortcut) and shortcut[i] not in ("+", ">"): current_part += shortcut[i] i += 1 if current_part.strip(): diff --git a/src/layouts/keyboard_layouts.yaml b/src/layouts/keyboard_layouts.yaml index 3ee7cc1..fc57060 100644 --- a/src/layouts/keyboard_layouts.yaml +++ b/src/layouts/keyboard_layouts.yaml @@ -269,7 +269,7 @@ ES: "´", "Enter", ] - - ["Shift", "Ç", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"] + - ["Shift", ">", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"] - ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"] DVORAK: diff --git a/src/templates/cheatsheets/assets/cheatsheets.css b/src/templates/cheatsheets/assets/cheatsheets.css index 2149ff6..0e920c6 100644 --- a/src/templates/cheatsheets/assets/cheatsheets.css +++ b/src/templates/cheatsheets/assets/cheatsheets.css @@ -238,6 +238,10 @@ body.dark-mode .shortcut-key .key-part { margin-left: 0.3em; } +.shortcut-key .sequence-separator { + white-space-collapse: preserve; +} + body.dark-mode .shortcut-key .separator { color: var(--mocha-subtext0); } @@ -338,6 +342,67 @@ body.dark-mode .key.active { color: var(--mocha-base); box-shadow: 0 0 8px var(--mocha-green); } +.key.active-step-1 { + background: var(--latte-green); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-green); +} +body.dark-mode .key.active-step-1 { + background: var(--mocha-green); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-green); +} +.key.active-step-2 { + background: var(--latte-sapphire); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-sapphire); +} +body.dark-mode .key.active-step-2 { + background: var(--mocha-sapphire); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-sapphire); +} +.key.active-step-3 { + background: var(--latte-peach); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-peach); +} +body.dark-mode .key.active-step-3 { + background: var(--mocha-peach); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-peach); +} +.key.active-step-4 { + background: var(--latte-mauve); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-mauve); +} +body.dark-mode .key.active-step-4 { + background: var(--mocha-mauve); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-mauve); +} +.key.active-step-5 { + background: var(--latte-lavender); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-lavender); +} +body.dark-mode .key.active-step-5 { + background: var(--mocha-lavender); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-lavender); +} +/* Default style for steps greater than 5 */ +.key[class*="active-step-"]:not(.active-step-1):not(.active-step-2):not(.active-step-3):not(.active-step-4):not(.active-step-5) { + background: var(--latte-yellow); + color: var(--latte-base); + box-shadow: 0 0 8px var(--latte-yellow); +} +body.dark-mode .key[class*="active-step-"]:not(.active-step-1):not(.active-step-2):not(.active-step-3):not(.active-step-4):not(.active-step-5) { + background: var(--mocha-yellow); + color: var(--mocha-base); + box-shadow: 0 0 8px var(--mocha-yellow); +} .key__wide { width: 75px; } diff --git a/src/templates/cheatsheets/components/body.html b/src/templates/cheatsheets/components/body.html index 0c2cc5a..db6d7ff 100644 --- a/src/templates/cheatsheets/components/body.html +++ b/src/templates/cheatsheets/components/body.html @@ -145,9 +145,13 @@

{{ section }}

{% if allow_text %} {{ shortcut }} {% else %} - {% for key in shortcut.split('') %} - {{ key | safe }} - {% if not loop.last %}+{% endif %} + {% set key_groups = shortcut.split('') %} + {% for group in key_groups %} + {% if loop.index > 1 %} {% endif %} + {% for key in group.split('') %} + {{ key | safe }} + {% if not loop.last %}+{% endif %} + {% endfor %} {% endfor %} {% endif %} diff --git a/src/templates/cheatsheets/scripts/main.js b/src/templates/cheatsheets/scripts/main.js index 6cacbb5..8778022 100644 --- a/src/templates/cheatsheets/scripts/main.js +++ b/src/templates/cheatsheets/scripts/main.js @@ -171,15 +171,21 @@ updateDarkModeToggle(); adjustLayout(); - function highlightKeys(shortcut) { - // Clear any previously highlighted keys - document.querySelectorAll(".key").forEach((key) => key.classList.remove("active")); + // Helper function to clear all active classes from keys + function clearActiveKeyClasses() { + const allClasses = ["active"]; + // Generate classes dynamically for steps (support up to 20 steps) + for (let i = 1; i <= 20; i++) { + allClasses.push(`active-step-${i}`); + } + document.querySelectorAll(".key").forEach((key) => { + allClasses.forEach(className => key.classList.remove(className)); + }); + } - // Get the shortcut key parts - const shortcutParts = shortcut.split('+').map(part => part.trim()); - const system = document.getElementById("keyboard").getAttribute("data-system"); - shortcutParts.forEach((keyToFind) => { + function highlightChordsPart(chordsPart, stepClass) { + chordsPart.forEach((keyToFind) => { keyToFind = keyToFind.toLowerCase(); const keyElements = document.querySelectorAll(".key"); @@ -192,18 +198,60 @@ keyLabel === keyToFind || (keyToFind === "cmd" && (dataKey === "cmd" || dataKey === "win" || dataKey === "super")) ) { - element.classList.add("active"); + element.classList.add(stepClass); } }); }); } + // Track the current animation timeout so we can cancel/reset it + let sequentialShortcutTimeout = null; + + function animateChords(chords) { + // Cancel any ongoing animation + if (sequentialShortcutTimeout) { + clearTimeout(sequentialShortcutTimeout); + sequentialShortcutTimeout = null; + } + + clearActiveKeyClasses(); + + const animationDelay = 500; + let currentStep = 0; + + const animateStep = () => { + if (currentStep < chords.length) { + const stepClass = `active-step-${currentStep + 1}`; + highlightChordsPart(chords[currentStep], stepClass); + currentStep++; + sequentialShortcutTimeout = setTimeout(animateStep, animationDelay); + } else { + sequentialShortcutTimeout = null; + } + }; + + animateStep(); + } + document.querySelectorAll(".shortcut").forEach((shortcut) => { {% if not allow_text %} shortcut.addEventListener("click", function() { - // Get the text content and replace with + - const shortcutKey = this.querySelector(".shortcut-key").textContent.replace(/\s*\+\s*/g, '+'); - highlightKeys(shortcutKey); + + const isKeyPart = (x) => {return x.classList.contains("key-part")} + const isSequenceSeparator = (x) => {return x.classList.contains("sequence-separator")} + + const chords=[[]]; + let chords_idx=0; + this.querySelectorAll(".shortcut-key span").forEach(x => { + if (isKeyPart(x)) { + chords[chords_idx].push(x.textContent.trim()) + }; + if (isSequenceSeparator(x)) { + chords.push([]); + chords_idx++; + } + }) + animateChords(chords); }); {% endif %} }); diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..27fd2fd --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Tests for KoalaKeys + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6e3dc96 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +"""Pytest configuration and fixtures.""" + +import sys +from pathlib import Path + +# Add src directory to Python path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) diff --git a/tests/test_replace_shortcut_names.py b/tests/test_replace_shortcut_names.py new file mode 100644 index 0000000..cc9e34d --- /dev/null +++ b/tests/test_replace_shortcut_names.py @@ -0,0 +1,67 @@ +"""Tests for the replace_shortcut_names function.""" + +import pytest + +from generate_cheatsheet import replace_shortcut_names + + +def test_three_keys(): + """Test Ctrl+Shift+A -> CtrlShiftA""" + result = replace_shortcut_names("Ctrl+Shift+A", {}) + expected = "CtrlShiftA" + assert result == expected + + +def test_plus_key(): + """Test Ctrl++ -> Ctrl+""" + result = replace_shortcut_names("Ctrl++", {}) + expected = "Ctrl+" + assert result == expected + + +def test_angle_bracket(): + """Test CTRL+> -> CTRL>""" + result = replace_shortcut_names("CTRL+>", {}) + expected = "CTRL>" + assert result == expected + + +def test_simple_chord(): + """Test Super+T>W>S -> SuperTWS""" + result = replace_shortcut_names("Super+T>W>S", {}) + expected = "SuperTWS" + assert result == expected + + +def test_composed_chord(): + """Test CTRL+C>CTRL+K -> CTRLCCTRLK""" + result = replace_shortcut_names("CTRL+C>CTRL+K", {}) + expected = "CTRLCCTRLK" + assert result == expected + + +def test_angle_bracket_in_chord(): + """Test CTRL>> -> CTRL>""" + result = replace_shortcut_names("CTRL>>", {}) + expected = "CTRL>" + assert result == expected + + +def test_plus_key_in_chord(): + """Test CTRL>+ -> CTRL+""" + result = replace_shortcut_names("CTRL>+", {}) + expected = "CTRL+" + assert result == expected + + +def test_spaces(): + assert replace_shortcut_names("Ctrl + C", {}) == "CtrlC" + assert replace_shortcut_names("Ctrl + Shift + A", {}) == "CtrlShiftA" + assert replace_shortcut_names("Ctrl + +", {}) == "Ctrl+" + assert replace_shortcut_names("CTRL + >", {}) == "CTRL>" + assert replace_shortcut_names("Super + T > W > S", {}) == "SuperTWS" + assert replace_shortcut_names("CTRL + C > CTRL + K", {}) == "CTRLCCTRLK" + assert replace_shortcut_names("CTRL > >", {}) == "CTRL>" + assert replace_shortcut_names("CTRL > +", {}) == "CTRL+" + + diff --git a/uv.lock b/uv.lock index e632b52..274fc61 100644 --- a/uv.lock +++ b/uv.lock @@ -30,6 +30,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -55,6 +91,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, + { name = "pytest" }, { name = "ruff" }, { name = "ty" }, { name = "vulture" }, @@ -70,6 +107,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.6" }, + { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.14.0" }, { name = "ty", specifier = ">=0.0.1a22" }, { name = "vulture", specifier = ">=2.14" }, @@ -210,6 +248,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -219,6 +275,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -498,6 +573,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/df/38068fc44e3cfb455aeb41d0ff1850a4d3c9988010466d4a8d19860b8b9a/ty-0.0.1a22-py3-none-win_arm64.whl", hash = "sha256:1c7f040fe311e9696917417434c2a0e58402235be842c508002c6a2eff1398b0", size = 8367136, upload-time = "2025-10-10T13:07:14.518Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "vulture" version = "2.14" diff --git a/yaml_cheatsheet_spec.md b/yaml_cheatsheet_spec.md index 27b3998..aefa84f 100644 --- a/yaml_cheatsheet_spec.md +++ b/yaml_cheatsheet_spec.md @@ -9,6 +9,7 @@ - `shortcuts` organized by categories - Use all caps for key names (CMD, CTRL, SHIFT, ALT) - Use `+` to combine keys +- Use `>` to combine chords - For arrow keys, use Up, Down, Left, Right - Your YAML will be validated, linted, and automatically fixed if possible @@ -94,6 +95,7 @@ Each shortcut is represented by a key-value pair: - For special keys, use their full names: `Space`, `Tab`, `Enter`, `Backspace`, `Delete`, `Esc`. - For arrow keys, use `Up`, `Down`, `Left`, `Right`. - For function keys, use `F1`, `F2`, etc. +- Use `>` to combine chords (separate keypress actions): `Super+T>W>S` (press `Super+T`, then `W`, then `S`) #### System-Specific Key Mappings @@ -122,6 +124,8 @@ shortcuts: description: "Bold selected text" "CMD+Right": description: "Move cursor to end of current line" + "CTRL+K>CTRL+C": + description: "Add Line Comment" ``` ## Validation, Linting, and Fixing