Skip to content
Open
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
9 changes: 9 additions & 0 deletions .pre-commit-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.4
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
31 changes: 27 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,29 @@ dependencies = ["jinja2>=3.1.6", "python-dotenv>=1.1.1", "ruamel-yaml>=0.18.14"]

[dependency-groups]
dev = [
"bandit>=1.8.6",
"prek>=0.3.6",
"pytest>=8.4.2",
"pytest-cov>=7.0.0",
"pytest-cov>=7.1.0",
"ruff>=0.14.0",
"ty>=0.0.1a22",
"vulture>=2.14",
]

[build-system]
requires = ["uv_build>=0.10.9,<0.11.0"]
build-backend = "uv_build"

[tool.coverage.run]
omit = ["tests/*"]

[tool.ruff]
indent-width = 4
line-length = 120

[tool.ruff.lint]
# select = ["ALL"]
ignore = [
"ANN", # flake8-annotations
"COM", # flake8-commas
"C90", # mccabe complexity
"DJ", # django
"EXE", # flake8-executable
"BLE", # blind except
Expand All @@ -52,6 +57,24 @@ ignore = [
"TRY",
"SIM105", # faster without contextlib
]
extend-select = [
"F", # Pyflakes rules
"W", # PyCodeStyle warnings
"E", # PyCodeStyle errors
"I", # Sort imports properly
"UP", # Warn if certain things can changed due to newer Python versions
"C4", # Catch incorrect use of comprehensions, dict, list, etc
"FA", # Enforce from __future__ import annotations
"ISC", # Good use of string concatenation
"ICN", # Use common import conventions
"RET", # Good return practices
"SIM", # Common simplification rules
"TID", # Some good import practices
"TC", # Enforce importing certain types in a TYPE_CHECKING block
"PTH", # Use pathlib instead of os.path
"TD", # Be diligent with TODO comments
"C90",
]
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
Expand Down
File renamed without changes.
86 changes: 47 additions & 39 deletions src/generate_cheatsheet.py → src/koalakeys/generate_cheatsheet.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import re
import sys
Expand All @@ -6,9 +8,9 @@
from dotenv import load_dotenv
from ruamel.yaml import YAML

from logger import get_logger
from template_renderer import render_template
from validate_yaml import lint_yaml, validate_yaml
from koalakeys.logger import get_logger
from koalakeys.template_renderer import render_template
from koalakeys.validate_yaml import lint_yaml, validate_yaml

yaml_safe = YAML(typ="safe")
yaml_rw = YAML()
Expand All @@ -18,22 +20,24 @@

load_dotenv()

BASE_DIR = Path(__file__).parent
PROJECT_ROOT = BASE_DIR.parent
PACKAGE_DIR = Path(__file__).parent
PROJECT_ROOT = PACKAGE_DIR.parent.parent

OUTPUT_DIR = Path(os.getenv("CHEATSHEET_OUTPUT_DIR") or PROJECT_ROOT / "output")
TEMPLATES_DIR = BASE_DIR / "templates"
LAYOUTS_DIR = BASE_DIR / "layouts"
CHEATSHEETS_DIR = PROJECT_ROOT / "cheatsheets"
LAYOUTS_DIR = PACKAGE_DIR / "layouts"

OUTPUT_DIR.mkdir(exist_ok=True, parents=True)

layout_file = LAYOUTS_DIR / "keyboard_layouts.yaml"
system_mapping_file = LAYOUTS_DIR / "system_mappings.yaml"

logging = get_logger()


def load_yaml(file_path: Path) -> dict | None:
try:
with open(file_path, "r", encoding="utf-8") as file:
with file_path.open(encoding="utf-8") as file:
return yaml_safe.load(file)
except FileNotFoundError:
logging.error(f"Error: YAML file '{file_path}' not found.")
Expand All @@ -44,8 +48,8 @@ def load_yaml(file_path: Path) -> dict | None:


def load_layout():
keyboard_layouts = load_yaml(LAYOUTS_DIR / "keyboard_layouts.yaml")
system_mappings = load_yaml(LAYOUTS_DIR / "system_mappings.yaml")
keyboard_layouts = load_yaml(layout_file)
system_mappings = load_yaml(system_mapping_file)

if keyboard_layouts is None or system_mappings is None:
logging.error("Failed to load configuration files.")
Expand All @@ -54,47 +58,51 @@ def load_layout():
return keyboard_layouts, system_mappings


def replace_shortcut_names(shortcut, system_mappings):
def consume_separator(shortcut: str, index: int) -> tuple[str, int]:
current = shortcut[index]
next_index = index + 1

if next_index < len(shortcut):
next_char = shortcut[next_index]
if current == "+" and next_char == "+":
return "<sep>+", index + 2
if current == "+" and next_char == ">":
return "<sep>>", index + 2
if current == ">" and next_char == ">":
return "<seq>>", index + 2
if current == ">" and next_char == "+":
return "<seq>+", index + 2

return ("<sep>", index + 1) if current == "+" else ("<seq>", index + 1)


def format_shortcut_part(part: str, system_mappings: dict) -> str:
arrow_key_mappings = {"Up": "↑", "Down": "↓", "Left": "←", "Right": "→"}

mapped_part = system_mappings.get(part.lower(), part)
if mapped_part in ["⌘", "⌥", "⌃", "⇧"]:
mapped_part = f'<span class="modifier-symbol">{mapped_part}</span>'

return arrow_key_mappings.get(mapped_part, mapped_part)


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("<sep>+")
i += 2
elif i + 1 < len(shortcut) and shortcut[i + 1] == ">":
processed_parts.append("<sep>>")
i += 2
else:
processed_parts.append("<sep>")
i += 1
elif shortcut[i] == ">":
if i + 1 < len(shortcut) and shortcut[i + 1] == ">":
processed_parts.append("<seq>>")
i += 2
elif i + 1 < len(shortcut) and shortcut[i + 1] == "+":
processed_parts.append("<seq>+")
i += 2
else:
processed_parts.append("<seq>")
i += 1
if shortcut[i] in ("+", ">"):
separator, i = consume_separator(shortcut, i)
processed_parts.append(separator)
else:
current_part = ""
while i < len(shortcut) and shortcut[i] not in ("+", ">"):
current_part += shortcut[i]
i += 1
if current_part.strip():
part = current_part.strip()
part = system_mappings.get(part.lower(), part)
if part in ["⌘", "⌥", "⌃", "⇧"]:
part = f'<span class="modifier-symbol">{part}</span>'

part = arrow_key_mappings.get(part, part)
processed_parts.append(part)
processed_parts.append(format_shortcut_part(current_part.strip(), system_mappings))

return "".join(processed_parts)
except Exception as e:
Expand Down Expand Up @@ -159,7 +167,7 @@ def write_html_content(html_output, html_content):
try:
with open(html_output, "w", encoding="utf-8") as file:
file.write(html_content)
except IOError as e:
except OSError as e:
logging.error(f"Error writing to output file: {e}")
return False
return True
Expand Down
54 changes: 54 additions & 0 deletions src/koalakeys/layouts/keyboard_layouts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
US:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"]
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"]
- ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "Enter"]
- ["Shift", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl", "Left", "Up", "Down", "Right"]

UK:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Backspace"]
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\"]
- ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "Enter"]
- ["Shift", "\\", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"]

DE:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "Backspace"]
- ["Tab", "Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", "Ü", "+", "Enter"]
- ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ö", "Ä", "#"]
- ["Shift", "Y", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"]

FR:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["²", "&", "é", '"', "'", "(", "-", "è", "_", "ç", "à", ")", "=", "Backspace"]
- ["Tab", "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", "^", "$", "\\"]
- ["CapsLock", "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", "ù", "*", "Enter"]
- ["Shift", "W", "X", "C", "V", "B", "N", ",", ";", ":", "!", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"]

ES:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["º", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "¡", "Backspace"]
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "`", "+", "\\"]
- ["CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ñ", "´", "Enter"]
- ["Shift", ">", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "-", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl"]

DVORAK:
layout:
- ["Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
- ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "[", "]", "Backspace"]
- ["Tab", "'", ",", ".", "P", "Y", "F", "G", "C", "R", "L", "/", "=", "\\"]
- ["CapsLock", "A", "O", "E", "U", "I", "D", "H", "T", "N", "S", "-", "Enter"]
- ["Shift", ";", "Q", "J", "K", "X", "B", "M", "W", "V", "Z", "Shift"]
- ["Ctrl", "Alt", "Cmd", "Space", "Cmd", "Alt", "Ctrl", "Left", "Up", "Down", "Right"]
21 changes: 21 additions & 0 deletions src/koalakeys/layouts/system_mappings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
Darwin:
cmd: "⌘"
alt: "⌥"
ctrl: "⌃"
shift: "Shift"
space: "Space"

Linux:
cmd: "Super"
alt: "Alt"
ctrl: "Ctrl"
shift: "Shift"
space: "Space"

Windows:
cmd: "Win"
alt: "Alt"
ctrl: "Ctrl"
shift: "Shift"
space: "Space"
5 changes: 3 additions & 2 deletions src/logger.py → src/koalakeys/logger.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from __future__ import annotations

import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from typing import Optional

DEFAULT_LOG_FILE = "app.log"
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
DEFAULT_BACKUP_COUNT = 5
DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

_logger: Optional[logging.Logger] = None
_logger: logging.Logger | None = None


def setup_logging(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from jinja2 import Environment, FileSystemLoader
from logger import get_logger
from pathlib import Path

from jinja2 import Environment, FileSystemLoader

from koalakeys.logger import get_logger

logging = get_logger()


Expand Down
File renamed without changes.
Loading
Loading