Skip to content

Commit

Permalink
Another major refactoring, add documentation
Browse files Browse the repository at this point in the history
Move all useful functions to the qmk module and use the cli subcommand
as a wrapper around it.
Add both inline comments and documentation.
  • Loading branch information
Erovia authored and skullydazed committed Feb 15, 2020
1 parent f96085a commit 26f53d3
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 76 deletions.
10 changes: 10 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,16 @@ This command lists all the keyboards currently defined in `qmk_firmware`
qmk list-keyboards
```

## `qmk list_keymaps`

This command lists all the keymaps for a specified keyboard (and revision).

**Usage**:

```
qmk list_keymaps -kb planck/ez
```

## `qmk new-keymap`

This command creates a new keymap based on a keyboard's existing default keymap.
Expand Down
84 changes: 8 additions & 76 deletions lib/python/qmk/cli/list/keymaps.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,8 @@
"""List the keymaps for a specific keyboard
"""
import os
import re
import glob
from bs4 import UnicodeDammit

from milc import cli

def unicode_text(filename):
"""Returns the contents of filename as a UTF-8 string. Tries to DTRT when it comes to encoding.
"""
with open(filename, "rb") as fd:
text = UnicodeDammit(fd.read())

if text.contains_replacement_characters:
log_warning("%s: Could not determine file encoding, some characters were replaced." % (filename,))

return text.unicode_markup or ""


def unicode_lines(filename):
"""Returns the contents of filename as a UTF-8 string. Tries to DTRT when it comes to encoding.
"""
return unicode_text(filename).split("\n")

def parse_rules_mk(keyboard, revision = ""):
base_path = os.path.join(os.getcwd(), "keyboards", keyboard) + os.path.sep
rules_mk = dict()
if os.path.exists(base_path + os.path.sep + revision):
rules_mk_path_wildcard = os.path.join(base_path, "**", "rules.mk")
rules_mk_regex = re.compile(r"^" + base_path + "(?:" + revision + os.path.sep + ")?rules.mk")
paths = [path for path in glob.iglob(rules_mk_path_wildcard, recursive = True) if rules_mk_regex.search(path)]

config_regex = re.compile(r"^\s*(\S+)\s*=\s*((?:\s*\S+)+)")
for file_path in paths:
rules_mk_content = unicode_lines(file_path)
parsed_file = dict()
for line in rules_mk_content:
found = config_regex.search(line)
if found:
parsed_file[found.group(1)] = found.group(2)
version = file_path.replace(base_path, "").replace(os.path.sep, "").replace("rules.mk", "")
rules_mk[version if version else "base"] = parsed_file
return rules_mk

def find_keymaps(base_path, revision = "", community = False):
path_wildcard = os.path.join(base_path, "**", "keymap.c")
if community:
path_regex = re.compile(r"^" + re.escape(base_path) + "(\S+)" + os.path.sep + "keymap\.c")
else:
path_regex = re.compile(r"^" + re.escape(base_path) + "(?:" + re.escape(revision) + os.path.sep + ")?keymaps" + os.path.sep + "(\S+)" + os.path.sep + "keymap\.c")
names = [path_regex.sub(lambda name: name.group(1), path) for path in glob.iglob(path_wildcard, recursive = True) if path_regex.search(path)]
return names
import qmk.keymap
from qmk.errors import NoSuchKeyboardError

@cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
@cli.subcommand("List the keymaps for a specific keyboard")
Expand All @@ -60,29 +11,10 @@ def list_keymaps(cli):
"""
# ask for user input if keyboard was not provided in the command line
keyboard_name = cli.config.list_keymaps.keyboard if cli.config.list_keymaps.keyboard else input("Keyboard Name: ")
if os.path.sep in keyboard_name:
keyboard, revision = os.path.split(os.path.normpath(keyboard_name))
else:
keyboard = keyboard_name
revision = ""

# get all the rules.mk files for the keyboard
rules_mk = parse_rules_mk(keyboard, revision)
names = list()

if rules_mk:
if "base" in rules_mk or revision:
kb_base_path = os.path.join(os.getcwd(), "keyboards", keyboard) + os.path.sep
names = find_keymaps(kb_base_path, revision)

for rev, data in rules_mk.items():
if "LAYOUTS" in data:
for layout in data["LAYOUTS"].split():
cl_base_path = os.path.join(os.getcwd(), "layouts", "community", layout) + os.path.sep
names = names + find_keymaps(cl_base_path, rev, community = True)

names.sort()

for name in names:
# We echo instead of cli.log.info to allow easier piping of this output
cli.echo(keyboard_name + ":" + name)
try:
for name in qmk.keymap.list_keymaps(keyboard_name):
# We echo instead of cli.log.info to allow easier piping of this output
cli.echo(keyboard_name + ":" + name)
except NoSuchKeyboardError as e:
cli.echo("{fg_red}" + e.message)
59 changes: 59 additions & 0 deletions lib/python/qmk/keymap.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Functions that help you work with QMK keymaps.
"""
import os
from traceback import format_exc
import re
import glob

import qmk.path
import qmk.makefile
from qmk.errors import NoSuchKeyboardError

# The `keymap.c` template to use when a keyboard doesn't have its own
DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
Expand Down Expand Up @@ -94,3 +99,57 @@ def write(keyboard, keymap, layout, layers):
keymap_fd.write(keymap_c)

return keymap_file

def find_keymaps(base_path, revision = "", community = False):
""" Find the available keymaps for a keyboard and revision pair.
Args:
base_path: The base path of the keyboard.
revision: The keyboard's revision.
community: Set to True for the layouts under layouts/community.
Returns:
a list with whe keymaps's names
"""
path_wildcard = os.path.join(base_path, "**", "keymap.c")
if community:
path_regex = re.compile(r"^" + re.escape(base_path) + "(\S+)" + os.path.sep + "keymap\.c")
else:
path_regex = re.compile(r"^" + re.escape(base_path) + "(?:" + re.escape(revision) + os.path.sep + ")?keymaps" + os.path.sep + "(\S+)" + os.path.sep + "keymap\.c")
names = [path_regex.sub(lambda name: name.group(1), path) for path in glob.iglob(path_wildcard, recursive = True) if path_regex.search(path)]
return names

def list_keymaps(keyboard_name):
""" List the available keymaps for a keyboard.
Args:
keyboard_name: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
Returns:
a list with the names of the available keymaps
"""
if os.path.sep in keyboard_name:
keyboard, revision = os.path.split(os.path.normpath(keyboard_name))
else:
keyboard = keyboard_name
revision = ""

# parse all the rules.mk files for the keyboard
rules_mk = qmk.makefile.get_rules_mk(keyboard, revision)
names = list()

if rules_mk:
# get the keymaps from the keyboard's directory
kb_base_path = os.path.join(os.getcwd(), "keyboards", keyboard) + os.path.sep
names = find_keymaps(kb_base_path, revision)

# if community layouts are supported, get them
if "LAYOUTS" in rules_mk:
for layout in rules_mk["LAYOUTS"]["value"].split():
cl_base_path = os.path.join(os.getcwd(), "layouts", "community", layout) + os.path.sep
names = names + find_keymaps(cl_base_path, revision, community = True)

names.sort()
return names
77 changes: 77 additions & 0 deletions lib/python/qmk/makefile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
""" Functions for working with Makefiles
"""
import os
import glob
import re

import qmk.path
from qmk.errors import NoSuchKeyboardError

def parse_rules_mk(file_path):
""" Parse a rules.mk file
Args:
file_path: path to the rules.mk file
Returns:
a dictionary with the file's content
"""
# regex to match lines with comment at the end
# group(1) = option's name
# group(2) = operator (eg.: '=', '+=')
# group(3) = value(s)
commented_regex = re.compile(r"^\s*(\w+)\s*([\:\+\-]?=)\s*(.*?)(?=\s*\#)")
# regex to match lines without comment at the end
# group(1) = option's name
# group(2) = operator (eg.: '=', '+=')
# group(3) = value(s)
uncommented_regex = re.compile(r"^\s*(\w+)\s*([\:\+\-]?=)\s*(.*?)(?=\s*$)")
mk_content = qmk.path.unicode_lines(file_path)
parsed_file = dict()
for line in mk_content:
found = commented_regex.search(line) if "#" in line else uncommented_regex.search(line)
if found:
parsed_file[found.group(1)] = dict(operator = found.group(2), value = found.group(3))
return parsed_file

def merge_rules_mk_files(base, revision):
""" Merge a keyboard revision's rules.mk file with
the 'base' rules.mk file
Args:
base: the base rules.mk file's content as dictionary
revision: the revision's rules.mk file's content as dictionary
Returns:
a dictionary with the merged content
"""
return {**base, **revision}

def get_rules_mk(keyboard, revision = ""):
""" Get a rules.mk for a keyboard
Args:
keyboard: name of the keyboard
revision: revision of the keyboard
Returns:
a dictionary with the content of the rules.mk file
"""
base_path = os.path.join(os.getcwd(), "keyboards", keyboard) + os.path.sep
rules_mk = dict()
if os.path.exists(base_path + os.path.sep + revision):
rules_mk_path_wildcard = os.path.join(base_path, "**", "rules.mk")
rules_mk_regex = re.compile(r"^" + base_path + "(?:" + revision + os.path.sep + ")?rules.mk")
paths = [path for path in glob.iglob(rules_mk_path_wildcard, recursive = True) if rules_mk_regex.search(path)]
for file_path in paths:
rules_mk[revision if revision in file_path else "base"] = parse_rules_mk(file_path)
else:
raise NoSuchKeyboardError("The requested keyboard and/or revision does not exist.")

# if the base or the revision directory does not contain a rules.mk
if len(rules_mk) == 1:
rules_mk = rules_mk[revision]
# if both directories contain rules.mk files
elif len(rules_mk) == 2:
rules_mk = merge_rules_mk_files(rules_mk["base"], rules_mk[revision])
return rules_mk
18 changes: 18 additions & 0 deletions lib/python/qmk/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from qmk.errors import NoSuchKeyboardError

from bs4 import UnicodeDammit

def keymap(keyboard):
"""Locate the correct directory for storing a keymap.
Expand Down Expand Up @@ -33,3 +34,20 @@ def normpath(path):
return os.path.normpath(path)

return os.path.normpath(os.path.join(os.environ['ORIG_CWD'], path))

def unicode_text(filename):
"""Returns the contents of filename as a UTF-8 string. Tries to DTRT when it comes to encoding.
"""
with open(filename, "rb") as fd:
text = UnicodeDammit(fd.read())

if text.contains_replacement_characters:
log_warning("%s: Could not determine file encoding, some characters were replaced." % (filename,))

return text.unicode_markup or ""


def unicode_lines(filename):
"""Returns the contents of filename as a UTF-8 string. Tries to DTRT when it comes to encoding.
"""
return unicode_text(filename).split("\n")

0 comments on commit 26f53d3

Please sign in to comment.