Skip to content
Merged
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
130 changes: 99 additions & 31 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,104 @@
name: Lint
name: checks
on:
push:
branches-ignore: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:

permissions: {}
pull_request:
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest

permissions:
contents: read
packages: read
statuses: write
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
version: 0.8.*
- name: Install dependencies
run: uv sync --locked
- name: Cache dependencies
uses: actions/cache/save@v4
with:
path: |
.venv
~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}

steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lint:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
version: 0.8.*
- name: Restore dependencies
uses: actions/cache/restore@v4
with:
path: |
.venv
~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
fail-on-cache-miss: true
- name: Run linting
run: |
uv run ruff check --fix
uv run ruff format

- name: Lint Code Base
uses: github/super-linter@v7
env:
VALIDATE_ALL_CODEBASE: true
FIX_PYTHON_RUFF: true
VALIDATE_PYTHON_RUFF: true
FIX_MARKDOWN_PRETTIER: true
VALIDATE_MARKDOWN_PRETTIER: true
VALIDATE_GITLEAKS: true
VALIDATE_GITHUB_ACTIONS: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
typecheck:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
version: 0.8.*
- name: Restore dependencies
uses: actions/cache/restore@v4
with:
path: |
.venv
~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
fail-on-cache-miss: true
- name: Run type checking
run: uv run ty check
vulture:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v5
- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: "pyproject.toml"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
version: 0.8.*
- name: Restore dependencies
uses: actions/cache/restore@v4
with:
path: |
.venv
~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
fail-on-cache-miss: true
- name: Run vulture
run: uv run vulture --min-confidence 100 --exclude ".venv" .
57 changes: 57 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,60 @@ description = "Build interactive, portable cheatsheets from YAML."
readme = "README.md"
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"]


[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
"PTH", # flake8-pathlib
"T10", # debugger
"TID", # flake8-tidy-imports
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D101",
"D107", # missing docstring in public module
"D102", # missing docstring in public class
"D104", # missing docstring in public package
"D213",
"D203",
"D400",
"D415",
"G004",
"PLR2004",
"E501", # line too long
"TRY",
"SIM105", # faster without contextlib
]

fixable = ["ALL"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"

docstring-code-format = false
55 changes: 26 additions & 29 deletions src/generate_cheatsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from logger import get_logger
from pathlib import Path

yaml_safe = YAML(typ='safe')
yaml_safe = YAML(typ="safe")
yaml_rw = YAML()
yaml_rw.indent(mapping=2, sequence=4, offset=2)
yaml_rw.preserve_quotes = True
Expand All @@ -18,7 +18,7 @@
BASE_DIR = Path(__file__).parent
PROJECT_ROOT = BASE_DIR.parent

OUTPUT_DIR = Path(os.getenv('CHEATSHEET_OUTPUT_DIR') or PROJECT_ROOT / "output")
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"
Expand All @@ -30,7 +30,7 @@

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

if keyboard_layouts is None or system_mappings is None:
logging.error("Failed to load configuration files.")
return None, None

return keyboard_layouts, system_mappings


def replace_shortcut_names(shortcut, system_mappings):
arrow_key_mappings = {
"Up": "↑",
"Down": "↓",
"Left": "←",
"Right": "→"
}
arrow_key_mappings = {"Up": "↑", "Down": "↓", "Left": "←", "Right": "→"}
try:
processed_parts = []
i = 0
while i < len(shortcut):
if shortcut[i] == '+':
if i + 1 < len(shortcut) and shortcut[i + 1] == '+':
processed_parts.append('+')
if shortcut[i] == "+":
if i + 1 < len(shortcut) and shortcut[i + 1] == "+":
processed_parts.append("+")
i += 2
else:
processed_parts.append('<sep>')
processed_parts.append("<sep>")
i += 1
else:
current_part = ''
while i < len(shortcut) and shortcut[i] != '+':
current_part = ""
while i < len(shortcut) and shortcut[i] != "+":
current_part += shortcut[i]
i += 1
if current_part.strip():
part = current_part.strip()
part = system_mappings.get(part.lower(), part)
if part in ['⌘', '⌥', '⌃', '⇧']:
if part in ["⌘", "⌥", "⌃", "⇧"]:
part = f'<span class="modifier-symbol">{part}</span>'

part = arrow_key_mappings.get(part, part)
processed_parts.append(part)


return ''.join(processed_parts)
return "".join(processed_parts)
except Exception as e:
logging.error(f"Error replacing shortcut names: {e}")
return shortcut
logging.error(f"Error replacing shortcut names: {e}")
return shortcut


def normalize_shortcuts(data, system_mappings):
normalized = {}
allow_text = data.get('AllowText', False)
allow_text = data.get("AllowText", False)
try:
for section, shortcuts in data.get("shortcuts", {}).items():
normalized[section] = {}
Expand All @@ -112,17 +108,16 @@ def get_layout_info(data):
"system": layout.get("system", "Darwin"),
}


def generate_html(data, keyboard_layouts, system_mappings):
template_path = "cheatsheets/cheatsheet-template.html"
layout_info = get_layout_info(data)
data["shortcuts"] = normalize_shortcuts(
data, system_mappings.get(layout_info["system"], {})
)
data["shortcuts"] = normalize_shortcuts(data, system_mappings.get(layout_info["system"], {}))
data["layout"] = layout_info
data["keyboard_layout"] = keyboard_layouts.get(layout_info["keyboard"], {}).get("layout")
data["render_keys"] = data.get("RenderKeys", True)
data["allow_text"] = data.get("AllowText", False)

return render_template(template_path, data)


Expand All @@ -141,15 +136,17 @@ def validate_and_lint(yaml_file):

return True


def write_html_content(html_output, html_content):
try:
with open(html_output, "w", encoding='utf-8') as file:
with open(html_output, "w", encoding="utf-8") as file:
file.write(html_content)
except IOError as e:
logging.error(f"Error writing to output file: {e}")
return False
return True


def main(yaml_file):
if not validate_and_lint(yaml_file):
return None, None
Expand Down Expand Up @@ -199,7 +196,7 @@ def generate_index(cheatsheets):

if cheatsheets:
OUTPUT_DIR.mkdir(exist_ok=True, parents=True)

html_content = generate_index(cheatsheets)
if html_content:
index_output = os.path.join(OUTPUT_DIR, "index.html")
Expand Down
10 changes: 4 additions & 6 deletions src/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

_logger: Optional[logging.Logger] = None


def setup_logging(
log_file: str = os.getenv("LOG_FILE", DEFAULT_LOG_FILE),
) -> logging.Logger:
Expand All @@ -20,7 +21,7 @@ def setup_logging(

logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(DEFAULT_FORMAT)

logger.handlers.clear()
Expand All @@ -32,10 +33,7 @@ def setup_logging(

try:
file_handler = RotatingFileHandler(
log_file,
maxBytes=DEFAULT_MAX_BYTES,
backupCount=DEFAULT_BACKUP_COUNT,
encoding='utf-8'
log_file, maxBytes=DEFAULT_MAX_BYTES, backupCount=DEFAULT_BACKUP_COUNT, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
Expand All @@ -48,9 +46,9 @@ def setup_logging(
_logger = logger
return logger


def get_logger() -> logging.Logger:
global _logger
if _logger is None:
_logger = setup_logging()
return _logger

Loading
Loading