Submitted upstream:
<https://github.com/benblazak/ergodox-firmware/pull/99>
<https://github.com/benblazak/ergodox-firmware/pull/98>

diff --git a/build-scripts/gen-layout.py b/build-scripts/gen-layout.py
index fd5e54c..251a463 100755
--- a/build-scripts/gen-layout.py
+++ b/build-scripts/gen-layout.py
@@ -22,8 +22,10 @@ import sys
 
 # -----------------------------------------------------------------------------
 
-class Namespace():
-	pass
+
+class Namespace:
+    pass
+
 
 template = Namespace()
 doc = Namespace()
@@ -31,45 +33,45 @@ info = Namespace()
 
 # -----------------------------------------------------------------------------
 
+
 def main():
-	arg_parser = argparse.ArgumentParser(
-			description = "Generate a picture of the firmware's "
-			            + "keyboard layout" )
+    arg_parser = argparse.ArgumentParser(
+        description="Generate a picture of the firmware's " + "keyboard layout"
+    )
 
-	arg_parser.add_argument(
-			'--ui-info-file',
-			required = True )
+    arg_parser.add_argument("--ui-info-file", required=True)
 
-	args = arg_parser.parse_args(sys.argv[1:])
+    args = arg_parser.parse_args(sys.argv[1:])
 
-	# constant file paths
-	args.template_svg_file = './build-scripts/gen_layout/template.svg'
-	args.template_js_file = './build-scripts/gen_layout/template.js'
+    # constant file paths
+    args.template_svg_file = "./build-scripts/gen_layout/template.svg"
+    args.template_js_file = "./build-scripts/gen_layout/template.js"
 
-	# normalize paths
-	args.ui_info_file = os.path.abspath(args.ui_info_file)
-	args.template_svg_file = os.path.abspath(args.template_svg_file)
-	args.template_js_file = os.path.abspath(args.template_js_file)
+    # normalize paths
+    args.ui_info_file = os.path.abspath(args.ui_info_file)
+    args.template_svg_file = os.path.abspath(args.template_svg_file)
+    args.template_js_file = os.path.abspath(args.template_js_file)
 
-	# set vars
-	doc.main = ''  # to store the html document we're generating
-	template.svg = open(args.template_svg_file).read()
-	template.js = open(args.template_js_file).read()
-	info.all = json.loads(open(args.ui_info_file).read())
+    # set vars
+    doc.main = ""  # to store the html document we're generating
+    template.svg = open(args.template_svg_file).read()
+    template.js = open(args.template_js_file).read()
+    info.all = json.loads(open(args.ui_info_file).read())
 
-	info.matrix_positions = info.all['mappings']['matrix-positions']
-	info.matrix_layout = info.all['mappings']['matrix-layout']
+    info.matrix_positions = info.all["mappings"]["matrix-positions"]
+    info.matrix_layout = info.all["mappings"]["matrix-layout"]
 
-	# prefix
-	doc.prefix = ("""
+    # prefix
+    doc.prefix = (
+        """
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <html>
 
 <head>
   <script>
 """
-+ template.js +
-""" </script>
+        + template.js
+        + """ </script>
 </head>
 
 <body>
@@ -78,9 +80,13 @@ def main():
 
 <ul>
   <li>git commit date:
-  <code>""" + info.all['miscellaneous']['git-commit-date'] + """</code></li>
+  <code>"""
+        + info.all["miscellaneous"]["git-commit-date"]
+        + """</code></li>
   <li>git commit id:
-  <code>""" + info.all['miscellaneous']['git-commit-id'] + """</code></li>
+  <code>"""
+        + info.all["miscellaneous"]["git-commit-id"]
+        + """</code></li>
 </ul>
 
 <h2>Notes</h2>
@@ -123,301 +129,293 @@ def main():
 
 <br>
 
-""")[1:-1]
+"""
+    )[1:-1]
 
-	# suffix
-	doc.suffix = ("""
+    # suffix
+    doc.suffix = (
+        """
 </body>
 </html>
 
-""")[1:-1]
-
-	# substitute into template
-	# -------
-	# note: this is not general enough to handle any possible layout well, at
-	# the moment.  but it should handle more standard ones well.  (hopefully
-	# minor) modifications may be necessary on a case by case basis
-	# -------
-	layer_number = -1
-	for (layout, layer) in zip( info.matrix_layout,
-								range(len(info.matrix_layout))):
-		layer_number += 1
-		svg = template.svg
-		for (name, (code, press, release)) \
-				in zip(info.matrix_positions, layout):
-			replace = ''
-			if press == 'kbfun_transparent':
-				replace = ''
-			elif press == 'kbfun_shift_press_release':
-				replace = 'sh ' + keycode_to_string.get(code, '[n/a]')
-			elif press == 'kbfun_jump_to_bootloader':
-				replace = '[btldr]'
-			elif press == 'NULL' and release == 'NULL':
-				replace = '(null)'
-			elif re.search(r'numpad', press+release):
-				replace = '[num]'
-			elif re.search(r'layer', press+release):
-				replace = 'la ' + re.findall(r'\d+', press+release)[0] + ' '
-				if re.search(r'push', press+release):
-					replace += '+'
-				if re.search(r'pop', press+release):
-					replace += '-'
-				replace += ' ' + str(code)
-			else:
-				replace = keycode_to_string.get(code, '[n/a]')
-
-			svg = re.sub(
-					'>'+name+'<', '>'+replace+'<', svg )
-			svg = re.sub(
-					r"\('(" + name + r".*)'\)",
-					r"('\1', " + str(layer) + r")",
-					svg )
-
-		doc.main += '<h2>Layer ' + str(layer_number) + '</h2>\n' + svg
-
-	# change the font size
-	doc.main = re.sub(r'22.5px', '15px', doc.main)
-
-	print(doc.prefix + doc.main + doc.suffix)
+"""
+    )[1:-1]
+
+    # substitute into template
+    # -------
+    # note: this is not general enough to handle any possible layout well, at
+    # the moment.  but it should handle more standard ones well.  (hopefully
+    # minor) modifications may be necessary on a case by case basis
+    # -------
+    layer_number = -1
+    for (layout, layer) in zip(
+        info.matrix_layout, range(len(info.matrix_layout))
+    ):
+        layer_number += 1
+        svg = template.svg
+        for (name, (code, press, release)) in zip(
+            info.matrix_positions, layout
+        ):
+            replace = ""
+            if press == "kbfun_transparent":
+                replace = ""
+            elif press == "kbfun_shift_press_release":
+                replace = "sh " + keycode_to_string.get(code, "[n/a]")
+            elif press == "kbfun_jump_to_bootloader":
+                replace = "[btldr]"
+            elif press == "NULL" and release == "NULL":
+                replace = "(null)"
+            elif re.search(r"numpad", press + release):
+                replace = "[num]"
+            elif re.search(r"layer", press + release):
+                replace = "la " + re.findall(r"\d+", press + release)[0] + " "
+                if re.search(r"push", press + release):
+                    replace += "+"
+                if re.search(r"pop", press + release):
+                    replace += "-"
+                replace += " " + str(code)
+            else:
+                replace = keycode_to_string.get(code, "[n/a]")
+
+            svg = re.sub(">" + name + "<", ">" + replace + "<", svg)
+            svg = re.sub(
+                r"\('(" + name + r".*)'\)", r"('\1', " + str(layer) + r")", svg
+            )
+
+        doc.main += "<h2>Layer " + str(layer_number) + "</h2>\n" + svg
+
+    # change the font size
+    doc.main = re.sub(r"22.5px", "15px", doc.main)
+
+    print(doc.prefix + doc.main + doc.suffix)
+
 
 # -----------------------------------------------------------------------------
 # -----------------------------------------------------------------------------
 
 keycode_to_string = {
-		0x01: "Error",  # ErrorRollOver
-		0x02: "POSTFail",
-		0x03: "Error",  # ErrorUndefined
-		0x04: "a A",
-		0x05: "b B",
-		0x06: "c C",
-		0x07: "d D",
-		0x08: "e E",
-		0x09: "f F",
-		0x0A: "g G",
-		0x0B: "h H",
-		0x0C: "i I",
-		0x0D: "j J",
-		0x0E: "k K",
-		0x0F: "l L",
-		0x10: "m M",
-		0x11: "n N",
-		0x12: "o O",
-		0x13: "p P",
-		0x14: "q Q",
-		0x15: "r R",
-		0x16: "s S",
-		0x17: "t T",
-		0x18: "u U",
-		0x19: "v V",
-		0x1A: "w W",
-		0x1B: "x X",
-		0x1C: "y Y",
-		0x1D: "z Z",
-		0x1E: "1 !",
-		0x1F: "2 @",
-		0x20: "3 #",
-		0x21: "4 $",
-		0x22: "5 %",
-		0x23: "6 ^",
-		0x24: "7 &",
-		0x25: "8 *",
-		0x26: "9 (",
-		0x27: "0 )",
-		0x28: "Return",
-		0x29: "Esc",
-		0x2A: "Backspace",
-		0x2B: "Tab",
-		0x2C: "Space",
-		0x2D: "- _",
-		0x2E: "= +",
-		0x2F: "[ {",
-		0x30: "] }",
-		0x31: "\ |",
-		0x32: "# ~",
-		0x33: "; :",
-		0x34: "\' \"",
-		0x35: "` ~",
-		0x36: ", <",
-		0x37: ". >",
-		0x38: "/ ?",
-		0x39: "Caps",
-		0x3A: "F1",
-		0x3B: "F2",
-		0x3C: "F3",
-		0x3D: "F4",
-		0x3E: "F5",
-		0x3F: "F6",
-		0x40: "F7",
-		0x41: "F8",
-		0x42: "F9",
-		0x43: "F10",
-		0x44: "F11",
-		0x45: "F12",
-		0x46: "PrintScreen",
-		0x47: "ScrollLock",
-		0x48: "Pause",
-		0x49: "Ins",  # Insert
-		0x4A: "Hm",  # Home
-		0x4B: "Pg\u2191",  # up arrow
-		0x4C: "Delete",
-		0x4D: "End",
-		0x4E: "Pg\u2193",  # down arrow
-		0x4F: "\u2192",  # right arrow
-		0x50: "\u2190",  # left arrow
-		0x51: "\u2193",  # down arrow
-		0x52: "\u2191",  # up arrow
-
-		0x53: "Num",
-		0x54: "/",
-		0x55: "*",
-		0x56: "-",
-		0x57: "+",
-		0x58: "Enter",
-		0x59: "1 End",
-		0x5A: "2 \u2193",  # down arrow
-		0x5B: "3 Pg\u2193",  # down arrow
-		0x5C: "4 \u2190",  # left arrow
-		0x5D: "5",
-		0x5E: "6 \u2192",  # right arrow
-		0x5F: "7 Hm",  # Home
-		0x60: "8 \u2191",  # up arrow
-		0x61: "9 Pg\u2191",  # up arrow
-		0x62: "0 Ins",  # Insert
-		0x63: ". Del",
-
-		0x64: "\ |",
-		0x65: "App",
-		0x66: "Power",
-
-		0x67: "=",
-
-		0x68: "F13",
-		0x69: "F14",
-		0x6A: "F15",
-		0x6B: "F16",
-		0x6C: "F17",
-		0x6D: "F18",
-		0x6E: "F19",
-		0x6F: "F20",
-		0x70: "F21",
-		0x71: "F22",
-		0x72: "F23",
-		0x73: "F24",
-		0x74: "Exec",
-		0x75: "Help",
-		0x76: "Menu",
-		0x77: "Select",
-		0x78: "Stop",
-		0x79: "Again",
-		0x7A: "Undo",
-		0x7B: "Cut",
-		0x7C: "Copy",
-		0x7D: "Paste",
-		0x7E: "Find",
-		0x7F: "Mute",
-		0x80: "VolUp",
-		0x81: "VolDown",
-		0x82: "LockingCapsLock",
-		0x83: "LockingNumLock",
-		0x84: "LockingScrollLock",
-
-		0x85: ",",
-		0x86: "=",
-
-		0x87: "Int1",
-		0x88: "Int2",
-		0x89: "Int3",
-		0x8A: "Int4",
-		0x8B: "Int5",
-		0x8C: "Int6",
-		0x8D: "Int7",
-		0x8E: "Int8",
-		0x8F: "Int9",
-		0x90: "LANG1",
-		0x91: "LANG2",
-		0x92: "LANG3",
-		0x93: "LANG4",
-		0x94: "LANG5",
-		0x95: "LANG6",
-		0x96: "LANG7",
-		0x97: "LANG8",
-		0x98: "LANG9",
-		0x99: "AlternateErase",
-		0x9A: "SysReq_Attention",
-		0x9B: "Cancel",
-		0x9C: "Clear",
-		0x9D: "Prior",
-		0x9E: "Return",
-		0x9F: "Separator",
-		0xA0: "Out",
-		0xA1: "Oper",
-		0xA2: "Clear_Again",
-		0xA3: "CrSel_Props",
-		0xA4: "ExSel",
-
-		0xB0: "00",
-		0xB1: "000",
-
-		0xB2: "Thousands_Sep",
-		0xB3: "Decimal_Sep",
-		0xB4: "$",
-		0xB5: "Currency_Subunit",
-
-		0xB6: "(",
-		0xB7: ")",
-		0xB8: "{",
-		0xB9: "}",
-
-		0xBA: "Tab",
-		0xBB: "Backspace",
-		0xBC: "A",
-		0xBD: "B",
-		0xBE: "C",
-		0xBF: "D",
-		0xC0: "E",
-		0xC1: "F",
-		0xC2: "XOR",
-		0xC3: "^",
-		0xC4: "%",
-		0xC5: "<",
-		0xC6: ">",
-		0xC7: "&",
-		0xC8: "&&",
-		0xC9: "|",
-		0xCA: "||",
-		0xCB: ":",
-		0xCC: "#",
-		0xCD: "Space",
-		0xCE: "@",
-		0xCF: "!",
-		0xD0: "Mem_Store",
-		0xD1: "Mem_Recall",
-		0xD2: "Mem_Clear",
-		0xD3: "Mem_+",
-		0xD4: "Mem_-",
-		0xD5: "Mem_*",
-		0xD6: "Mem_/",
-		0xD7: "+-",
-		0xD8: "Clear",
-		0xD9: "ClearEntry",
-		0xDA: "Binary",
-		0xDB: "Octal",
-		0xDC: ".",
-		0xDD: "Hexadecimal",
-
-		0xE0: "L-Ctrl",
-		0xE1: "L-Shift",
-		0xE2: "L-Alt",
-		0xE3: "L-GUI",
-		0xE4: "R-Ctrl",
-		0xE5: "R-Shift",
-		0xE6: "R-Alt",
-		0xE7: "R-GUI",
-		}
+    0x01: "Error",  # ErrorRollOver
+    0x02: "POSTFail",
+    0x03: "Error",  # ErrorUndefined
+    0x04: "a A",
+    0x05: "b B",
+    0x06: "c C",
+    0x07: "d D",
+    0x08: "e E",
+    0x09: "f F",
+    0x0A: "g G",
+    0x0B: "h H",
+    0x0C: "i I",
+    0x0D: "j J",
+    0x0E: "k K",
+    0x0F: "l L",
+    0x10: "m M",
+    0x11: "n N",
+    0x12: "o O",
+    0x13: "p P",
+    0x14: "q Q",
+    0x15: "r R",
+    0x16: "s S",
+    0x17: "t T",
+    0x18: "u U",
+    0x19: "v V",
+    0x1A: "w W",
+    0x1B: "x X",
+    0x1C: "y Y",
+    0x1D: "z Z",
+    0x1E: "1 !",
+    0x1F: "2 @",
+    0x20: "3 #",
+    0x21: "4 $",
+    0x22: "5 %",
+    0x23: "6 ^",
+    0x24: "7 &",
+    0x25: "8 *",
+    0x26: "9 (",
+    0x27: "0 )",
+    0x28: "Return",
+    0x29: "Esc",
+    0x2A: "Backspace",
+    0x2B: "Tab",
+    0x2C: "Space",
+    0x2D: "- _",
+    0x2E: "= +",
+    0x2F: "[ {",
+    0x30: "] }",
+    0x31: "\ |",
+    0x32: "# ~",
+    0x33: "; :",
+    0x34: "' \"",
+    0x35: "` ~",
+    0x36: ", <",
+    0x37: ". >",
+    0x38: "/ ?",
+    0x39: "Caps",
+    0x3A: "F1",
+    0x3B: "F2",
+    0x3C: "F3",
+    0x3D: "F4",
+    0x3E: "F5",
+    0x3F: "F6",
+    0x40: "F7",
+    0x41: "F8",
+    0x42: "F9",
+    0x43: "F10",
+    0x44: "F11",
+    0x45: "F12",
+    0x46: "PrintScreen",
+    0x47: "ScrollLock",
+    0x48: "Pause",
+    0x49: "Ins",  # Insert
+    0x4A: "Hm",  # Home
+    0x4B: "Pg\u2191",  # up arrow
+    0x4C: "Delete",
+    0x4D: "End",
+    0x4E: "Pg\u2193",  # down arrow
+    0x4F: "\u2192",  # right arrow
+    0x50: "\u2190",  # left arrow
+    0x51: "\u2193",  # down arrow
+    0x52: "\u2191",  # up arrow
+    0x53: "Num",
+    0x54: "/",
+    0x55: "*",
+    0x56: "-",
+    0x57: "+",
+    0x58: "Enter",
+    0x59: "1 End",
+    0x5A: "2 \u2193",  # down arrow
+    0x5B: "3 Pg\u2193",  # down arrow
+    0x5C: "4 \u2190",  # left arrow
+    0x5D: "5",
+    0x5E: "6 \u2192",  # right arrow
+    0x5F: "7 Hm",  # Home
+    0x60: "8 \u2191",  # up arrow
+    0x61: "9 Pg\u2191",  # up arrow
+    0x62: "0 Ins",  # Insert
+    0x63: ". Del",
+    0x64: "\ |",
+    0x65: "App",
+    0x66: "Power",
+    0x67: "=",
+    0x68: "F13",
+    0x69: "F14",
+    0x6A: "F15",
+    0x6B: "F16",
+    0x6C: "F17",
+    0x6D: "F18",
+    0x6E: "F19",
+    0x6F: "F20",
+    0x70: "F21",
+    0x71: "F22",
+    0x72: "F23",
+    0x73: "F24",
+    0x74: "Exec",
+    0x75: "Help",
+    0x76: "Menu",
+    0x77: "Select",
+    0x78: "Stop",
+    0x79: "Again",
+    0x7A: "Undo",
+    0x7B: "Cut",
+    0x7C: "Copy",
+    0x7D: "Paste",
+    0x7E: "Find",
+    0x7F: "Mute",
+    0x80: "VolUp",
+    0x81: "VolDown",
+    0x82: "LockingCapsLock",
+    0x83: "LockingNumLock",
+    0x84: "LockingScrollLock",
+    0x85: ",",
+    0x86: "=",
+    0x87: "Int1",
+    0x88: "Int2",
+    0x89: "Int3",
+    0x8A: "Int4",
+    0x8B: "Int5",
+    0x8C: "Int6",
+    0x8D: "Int7",
+    0x8E: "Int8",
+    0x8F: "Int9",
+    0x90: "LANG1",
+    0x91: "LANG2",
+    0x92: "LANG3",
+    0x93: "LANG4",
+    0x94: "LANG5",
+    0x95: "LANG6",
+    0x96: "LANG7",
+    0x97: "LANG8",
+    0x98: "LANG9",
+    0x99: "AlternateErase",
+    0x9A: "SysReq_Attention",
+    0x9B: "Cancel",
+    0x9C: "Clear",
+    0x9D: "Prior",
+    0x9E: "Return",
+    0x9F: "Separator",
+    0xA0: "Out",
+    0xA1: "Oper",
+    0xA2: "Clear_Again",
+    0xA3: "CrSel_Props",
+    0xA4: "ExSel",
+    0xB0: "00",
+    0xB1: "000",
+    0xB2: "Thousands_Sep",
+    0xB3: "Decimal_Sep",
+    0xB4: "$",
+    0xB5: "Currency_Subunit",
+    0xB6: "(",
+    0xB7: ")",
+    0xB8: "{",
+    0xB9: "}",
+    0xBA: "Tab",
+    0xBB: "Backspace",
+    0xBC: "A",
+    0xBD: "B",
+    0xBE: "C",
+    0xBF: "D",
+    0xC0: "E",
+    0xC1: "F",
+    0xC2: "XOR",
+    0xC3: "^",
+    0xC4: "%",
+    0xC5: "<",
+    0xC6: ">",
+    0xC7: "&",
+    0xC8: "&&",
+    0xC9: "|",
+    0xCA: "||",
+    0xCB: ":",
+    0xCC: "#",
+    0xCD: "Space",
+    0xCE: "@",
+    0xCF: "!",
+    0xD0: "Mem_Store",
+    0xD1: "Mem_Recall",
+    0xD2: "Mem_Clear",
+    0xD3: "Mem_+",
+    0xD4: "Mem_-",
+    0xD5: "Mem_*",
+    0xD6: "Mem_/",
+    0xD7: "+-",
+    0xD8: "Clear",
+    0xD9: "ClearEntry",
+    0xDA: "Binary",
+    0xDB: "Octal",
+    0xDC: ".",
+    0xDD: "Hexadecimal",
+    0xE0: "L-Ctrl",
+    0xE1: "L-Shift",
+    0xE2: "L-Alt",
+    0xE3: "L-GUI",
+    0xE4: "R-Ctrl",
+    0xE5: "R-Shift",
+    0xE6: "R-Alt",
+    0xE7: "R-GUI",
+}
 
 # -----------------------------------------------------------------------------
 # -----------------------------------------------------------------------------
 
-if __name__ == '__main__':
-	main()
-
+if __name__ == "__main__":
+    main()
diff --git a/build-scripts/gen-ui-info.py b/build-scripts/gen-ui-info.py
index 1c93d32..0fa52e3 100755
--- a/build-scripts/gen-ui-info.py
+++ b/build-scripts/gen-ui-info.py
@@ -13,7 +13,16 @@ Depends on:
 - the project '.map' file (generated by the compiler)
 """
 
-_FORMAT_DESCRIPTION = ("""
+import argparse
+import json
+import os
+import pathlib
+import re
+import subprocess
+import sys
+
+_FORMAT_DESCRIPTION = (
+    """
 /* ----------------------------------------------------------------------------
  * Version 0
  * ----------------------------------------------------------------------------
@@ -31,7 +40,7 @@ var ui_info = {
     ".meta-data": {                    // for the JSON file
         "version": "<number>",
         "date-generated": "<string>",  // format: RFC 3339
-		"description": "<string>",
+                "description": "<string>",
     },
     "keyboard-functions": {
         "<(function name)>": {
@@ -57,7 +66,7 @@ var ui_info = {
         "..."
     },
     "mappings": {
-        /* 
+        /*
          * The mappings prefixed with 'matrix' have their elements in the same
          * order as the .hex file (whatever order that is).  The mappings
          * prefixed with 'physical' will have their elements in an order
@@ -113,365 +122,304 @@ var ui_info = {
         "number-of-layers": "<number>"
     }
 }
-""")[1:-1]
+"""
+)[1:-1]
 
 # -----------------------------------------------------------------------------
 
-import argparse
-import json
-import os
-import re
-import subprocess
-import sys
-
-# -----------------------------------------------------------------------------
 
 def gen_static(current_date=None, git_commit_date=None, git_commit_id=None):
-	"""Generate static information"""
-
-	return {
-		'.meta-data': {
-			'version': 0,  # the format version number
-			'date-generated': current_date,
-			'description': _FORMAT_DESCRIPTION,
-		},
-		'miscellaneous': {
-			'git-commit-date': git_commit_date, # should be passed by makefile
-			'git-commit-id': git_commit_id, # should be passed by makefile
-		},
-	}
-
-def gen_derived(data):
-    return {}  # don't really need this info anymore
-# 	"""
-# 	Generate derived information
-# 	Should be called last
-# 	"""
-# 	return {
-# 		'miscellaneous': {
-# 			'number-of-layers':
-# 				int( data['layout-matrices']['_kb_layout']['length']/(6*14) ),
-# 				# because 6*14 is the number of bytes/layer for '_kb_layout'
-# 				# (which is a uint8_t matrix)
-# 		},
-# 	}
-
-# -----------------------------------------------------------------------------
+    """Generate static information"""
 
-def parse_mapfile(map_file_path):
-    return {}  # don't really need this info anymore
-# 	"""Parse the '.map' file"""
-# 
-# 	def parse_keyboard_function(f, line):
-# 		"""Parse keyboard-functions in the '.map' file"""
-# 
-# 		search = re.search(r'(0x\S+)\s+(0x\S+)', next(f))
-# 		position = int( search.group(1), 16 )
-# 		length = int( search.group(2), 16 )
-# 
-# 		search = re.search(r'0x\S+\s+(\S+)', next(f))
-# 		name = search.group(1)
-# 
-# 		return {
-# 			'keyboard-functions': {
-# 				name: {
-# 					'position': position,
-# 					'length': length,
-# 				},
-# 			},
-# 		}
-# 
-# 	def parse_layout_matrices(f, line):
-# 		"""Parse layout matrix information in the '.map' file"""
-# 
-# 		name = re.search(r'.progmem.data.(_kb_layout\S*)', line).group(1)
-# 
-# 		search = re.search(r'(0x\S+)\s+(0x\S+)', next(f))
-# 		position = int( search.group(1), 16 )
-# 		length = int( search.group(2), 16 )
-# 
-# 		return {
-# 			'layout-matrices': {
-# 				name: {
-# 					'position': position,
-# 					'length': length,
-# 				},
-# 			},
-# 		}
-# 
-# 	# --- parse_mapfile() ---
-# 
-# 	# normalize paths
-# 	map_file_path = os.path.abspath(map_file_path)
-# 	# check paths
-# 	if not os.path.exists(map_file_path):
-# 		raise ValueError("invalid 'map_file_path' given")
-# 
-# 	output = {}
-# 
-# 	f = open(map_file_path)
-# 
-# 	for line in f:
-# 		if re.search(r'^\s*\.text\.kbfun_', line):
-# 			dict_merge(output, parse_keyboard_function(f, line))
-# 		elif re.search(r'^\s*\.progmem\.data.*layout', line):
-# 			dict_merge(output, parse_layout_matrices(f, line))
-# 
-# 	return output
+    return {
+        ".meta-data": {
+            "version": 0,  # the format version number
+            "date-generated": current_date,
+            "description": _FORMAT_DESCRIPTION,
+        },
+        "miscellaneous": {
+            "git-commit-date": git_commit_date,  # should be passed by makefile
+            "git-commit-id": git_commit_id,  # should be passed by makefile
+        },
+    }
 
 
 def find_keyboard_functions(source_code_path):
-	"""Parse all files in the source directory"""
-
-	def read_comments(f, line):
-		"""
-		Read in properly formatted multi-line comments
-		- Comments must start with '/*' and end with '*/', each on their own
-		  line
-		"""
-		comments = ''
-		while(line.strip() != r'*/'):
-			comments += line[2:].strip()+'\n'
-			line = next(f)
-		return comments
-
-	def parse_comments(comments):
-		"""
-		Parse an INI style comment string
-		- Fields begin with '[field-name]', and continue until the next field,
-		  or the end of the comment
-		- Fields '[name]', '[description]', and '[note]' are treated specially
-		"""
-
-		def add_field(output, field, value):
-			"""Put a field+value pair in 'output', the way we want it, if the
-			pair is valid"""
-
-			value = value.strip()
-
-			if field is not None:
-				if field in ('name', 'description'):
-					if field not in output:
-						output[field] = value
-				else:
-					if field == 'note':
-						field = 'notes'
-
-					if field not in output:
-						output[field] = []
-
-					output[field] += [value]
-
-		# --- parse_comments() ---
-
-		output = {}
-
-		field = None
-		value = None
-		for line in comments.split('\n'):
-			line = line.strip()
-
-			if re.search(r'^\[.*\]$', line):
-				add_field(output, field, value)
-				field = line[1:-1]
-				value = None
-
-			else:
-				if value is None:
-					value = ''
-				if len(value) > 0 and value[-1] == '.':
-					line = ' '+line
-				value += ' '+line
-
-		add_field(output, field, value)
-
-		return output
-
-	def parse_keyboard_function(f, line, comments):
-		"""Parse keyboard-functions in the source code"""
-
-		search = re.search(r'void\s+(kbfun_\S+)\s*\(void\)', line)
-		name = search.group(1)
-
-		return {
-			'keyboard-functions': {
-				name: {
-					'comments': parse_comments(comments),
-				},
-			},
-		}
-
-	# --- find_keyboard_functions() ---
-
-	# normalize paths
-	source_code_path = os.path.abspath(source_code_path)
-	# check paths
-	if not os.path.exists(source_code_path):
-		raise ValueError("invalid 'source_code_path' given")
-
-	output = {}
-
-	for tup in os.walk(source_code_path):
-		for file_name in tup[2]:
-			# normalize paths
-			file_name = os.path.abspath( os.path.join( tup[0], file_name ) )
-
-			# ignore non '.c' files
-			if file_name[-2:] != '.c':
-				continue
-
-			f = open(file_name)
-
-			comments = ''
-			for line in f:
-				if line.strip() == r'/*':
-					comments = read_comments(f, line)
-				elif re.search(r'void\s+kbfun_\S+\s*\(void\)', line):
-					dict_merge(
-							output,
-							parse_keyboard_function(f, line, comments) )
-
-	return output
+    """Parse all files in the source directory"""
+
+    def read_comments(f, line):
+        """
+        Read in properly formatted multi-line comments
+        - Comments must start with '/*' and end with '*/', each on their own
+          line
+        """
+        comments = ""
+        while line.strip() != r"*/":
+            comments += line[2:].strip() + "\n"
+            line = next(f)
+        return comments
+
+    def parse_comments(comments):
+        """
+        Parse an INI style comment string
+        - Fields begin with '[field-name]', and continue until the next field,
+          or the end of the comment
+        - Fields '[name]', '[description]', and '[note]' are treated specially
+        """
+
+        def add_field(output, field, value):
+            """Put a field+value pair in 'output', the way we want it, if the
+            pair is valid"""
+
+            value = value.strip()
+
+            if field is not None:
+                if field in ("name", "description"):
+                    if field not in output:
+                        output[field] = value
+                else:
+                    if field == "note":
+                        field = "notes"
+
+                    if field not in output:
+                        output[field] = []
+
+                    output[field] += [value]
+
+        # --- parse_comments() ---
+
+        output = {}
+
+        field = None
+        value = None
+        for line in comments.split("\n"):
+            line = line.strip()
+
+            if re.search(r"^\[.*\]$", line):
+                add_field(output, field, value)
+                field = line[1:-1]
+                value = None
+            else:
+                if value is None:
+                    value = ""
+                if len(value) > 0 and value[-1] == ".":
+                    line = " " + line
+                value += " " + line
+
+        add_field(output, field, value)
+
+        return output
+
+    def parse_keyboard_function(f, line, comments):
+        """Parse keyboard-functions in the source code"""
+
+        search = re.search(r"void\s+(kbfun_\S+)\s*\(void\)", line)
+        name = search.group(1)
+
+        return {
+            "keyboard-functions": {
+                name: {
+                    "comments": parse_comments(comments),
+                },
+            },
+        }
+
+    # --- find_keyboard_functions() ---
+
+    # normalize paths
+    source_code_path = os.path.abspath(source_code_path)
+    # check paths
+    if not os.path.exists(source_code_path):
+        raise ValueError("invalid 'source_code_path' given")
+
+    output = {}
+
+    for tup in os.walk(source_code_path):
+        for file_name in tup[2]:
+            # normalize paths
+            file_name = os.path.abspath(os.path.join(tup[0], file_name))
+
+            # ignore non '.c' files
+            if file_name[-2:] != ".c":
+                continue
+
+            f = open(file_name)
+
+            comments = ""
+            for line in f:
+                if line.strip() == r"/*":
+                    comments = read_comments(f, line)
+                elif re.search(r"void\s+kbfun_\S+\s*\(void\)", line):
+                    dict_merge(
+                        output, parse_keyboard_function(f, line, comments)
+                    )
+
+    return output
 
 
 def gen_mappings(matrix_file_path, layout_file_path):
-	# normalize paths
-	matrix_file_path = os.path.abspath(matrix_file_path)
-	layout_file_path = os.path.abspath(layout_file_path)
-
-	def parse_matrix_file(matrix_file_path):
-		match = re.search(  # find the whole 'KB_MATRIX_LAYER' macro
-				r'#define\s+KB_MATRIX_LAYER\s*\(([^)]+)\)[^{]*\{\{([^#]+)\}\}',
-				open(matrix_file_path).read() )
-
-		return {
-			"mappings": {
-				"physical-positions": re.findall(r'k..', match.group(1)),
-				"matrix-positions": re.findall(r'k..|na', match.group(2)),
-			},
-		}
-
-	def parse_layout_file(layout_file_path):
-		match = re.findall(  # find each whole '_kb_layout*' matrix definition
-				r'(_kb_layout\w*)[^=]*=((?:[^{}]*\{){3}[^=]*(?:[^{}]*\}){3})',
-				subprocess.getoutput("gcc -E '"+layout_file_path+"'") )
-
-		layout = {}
-		# collect all the values
-		for (name, matrix) in match:
-			layout[name] = [
-					re.findall(  # find all numbers and function pointers
-						r'[x0-9A-F]+|&\w+|NULL',
-						re.sub(  # replace '((void *) 0)' with 'NULL'
-							r'\(\s*\(\s*void\s*\*\s*\)\s*0\s*\)',
-							'NULL',
-							el ) )
-					for el in
-						re.findall(  # find each whole layer
-							r'(?:[^{}]*\{){2}((?:[^}]|\}\s*,)+)(?:[^{}]*\}){2}',
-							matrix ) ]
-
-		# make the numbers into actual numbers
-		layout['_kb_layout'] = \
-				[[eval(el) for el in layer] for layer in layout['_kb_layout']]
-		# remove the preceeding '&' from function pointers
-		for matrix in ('_kb_layout_press', '_kb_layout_release'):
-			layout[matrix] = \
-					[ [re.sub(r'&', '', el) for el in layer]
-					  for layer in layout[matrix] ]
-
-		return {
-			"mappings": {
-				"matrix-layout":
-					# group them all properly
-					[ [[c, p, r] for (c, p, r) in zip(code, press, release)]
-					  for (code, press, release) in
-						  zip( layout['_kb_layout'],
-							   layout['_kb_layout_press'],
-							   layout['_kb_layout_release'] ) ]
-			},
-		}
-
-	return dict_merge(
-			parse_matrix_file(matrix_file_path),
-			parse_layout_file(layout_file_path) )
+    # normalize paths
+    matrix_file_path = os.path.abspath(matrix_file_path)
+    layout_file_path = os.path.abspath(layout_file_path)
+    layout_name = pathlib.Path(layout_file_path).with_suffix('').name
+
+    def parse_matrix_file(matrix_file_path):
+        match = re.search(  # find the whole 'KB_MATRIX_LAYER' macro
+            r"#define\s+KB_MATRIX_LAYER\s*\(([^)]+)\)[^{]*\{\{([^#]+)\}\}",
+            open(matrix_file_path).read(),
+        )
+
+        return {
+            "mappings": {
+                "physical-positions": re.findall(r"k..", match.group(1)),
+                "matrix-positions": re.findall(r"k..|na", match.group(2)),
+            },
+        }
+
+    def parse_layout_file(layout_file_path):
+        output = subprocess.check_output(
+            ['avr-gcc', f'-DMAKEFILE_KEYBOARD_LAYOUT={layout_name}',
+             '-E', layout_file_path], encoding='UTF-8')
+        match = re.findall(  # find each whole '_kb_layout*' matrix definition
+            r"(_kb_layout\w*)[^=]*=((?:[^{}]*\{){3}[^=]*(?:[^{}]*\}){3})",
+            output,
+        )
+
+        layout = {}
+        # collect all the values
+        for (name, matrix) in match:
+            layout[name] = [
+                re.findall(  # find all numbers and function pointers
+                    r"[x0-9A-F]+|&\w+|NULL",
+                    re.sub(  # replace '((void *) 0)' with 'NULL'
+                        r"\(\s*\(\s*void\s*\*\s*\)\s*0\s*\)", "NULL", el
+                    ),
+                )
+                for el in re.findall(  # find each whole layer
+                    r"(?:[^{}]*\{){2}((?:[^}]|\}\s*,)+)(?:[^{}]*\}){2}", matrix
+                )
+            ]
+
+        # make the numbers into actual numbers
+        layout["_kb_layout"] = [
+            [eval(el) for el in layer] for layer in layout["_kb_layout"]
+        ]
+        # remove the preceeding '&' from function pointers
+        for matrix in ("_kb_layout_press", "_kb_layout_release"):
+            layout[matrix] = [
+                [re.sub(r"&", "", el) for el in layer]
+                for layer in layout[matrix]
+            ]
+
+        return {
+            "mappings": {
+                "matrix-layout":
+                # group them all properly
+                [
+                    [[c, p, r] for (c, p, r) in zip(code, press, release)]
+                    for (code, press, release) in zip(
+                        layout["_kb_layout"],
+                        layout["_kb_layout_press"],
+                        layout["_kb_layout_release"],
+                    )
+                ]
+            },
+        }
+
+    return dict_merge(
+        parse_matrix_file(matrix_file_path),
+        parse_layout_file(layout_file_path),
+    )
 
 
 # -----------------------------------------------------------------------------
 
+
 def dict_merge(a, b):
-	"""
-	Recursively merge two dictionaries
-	- I was looking around for an easy way to do this, and found something
-	  [here]
-	  (http://www.xormedia.com/recursively-merge-dictionaries-in-python.html).
-	  This is pretty close, but i didn't copy it exactly.
-	"""
+    """
+    Recursively merge two dictionaries
+    - I was looking around for an easy way to do this, and found something
+      [here]
+      (http://www.xormedia.com/recursively-merge-dictionaries-in-python.html).
+      This is pretty close, but i didn't copy it exactly.
+    """
+
+    if not isinstance(a, dict) or not isinstance(b, dict):
+        return b
 
-	if not isinstance(a, dict) or not isinstance(b, dict):
-		return b
+    for (key, value) in b.items():
+        if key in a:
+            a[key] = dict_merge(a[key], value)
+        else:
+            a[key] = value
 
-	for (key, value) in b.items():
-		if key in a:
-			a[key] = dict_merge(a[key], value)
-		else:
-			a[key] = value
+    return a
 
-	return a
 
 # -----------------------------------------------------------------------------
 
+
 def main():
-	arg_parser = argparse.ArgumentParser(
-			description = 'Generate project data for use with the UI' )
-
-	arg_parser.add_argument(
-			'--current-date',
-			help = ( "should be in the format rfc-3339 "
-				   + "(e.g. 2006-08-07 12:34:56-06:00)" ),
-			required = True )
-	arg_parser.add_argument(
-			'--git-commit-date',
-			help = ( "should be in the format rfc-3339 "
-				   + "(e.g. 2006-08-07 12:34:56-06:00)" ),
-			required = True )
-	arg_parser.add_argument(
-			'--git-commit-id',
-			help = "the git commit ID",
-			required = True )
-	arg_parser.add_argument(
-			'--map-file-path',
-			help = "the path to the '.map' file",
-			required = True )
-	arg_parser.add_argument(
-			'--source-code-path',
-			help = "the path to the source code directory",
-			required = True )
-	arg_parser.add_argument(
-			'--matrix-file-path',
-			help = "the path to the matrix file we're using",
-			required = True )
-	arg_parser.add_argument(
-			'--layout-file-path',
-			help = "the path to the layout file we're using",
-			required = True )
-
-	args = arg_parser.parse_args(sys.argv[1:])
-
-	output = {}
-	dict_merge( output, gen_static( args.current_date,
-									args.git_commit_date,
-									args.git_commit_id ) )
-	dict_merge(output, parse_mapfile(args.map_file_path))
-	dict_merge(output, find_keyboard_functions(args.source_code_path))
-	dict_merge(output, gen_mappings( args.matrix_file_path,
-									  args.layout_file_path ))
-	dict_merge(output, gen_derived(output))
-
-	print(json.dumps(output, sort_keys=True, indent=4))
+    arg_parser = argparse.ArgumentParser(
+        description="Generate project data for use with the UI"
+    )
+
+    arg_parser.add_argument(
+        "--current-date",
+        help=(
+            "should be in the format rfc-3339 "
+            "(e.g. 2006-08-07 12:34:56-06:00)"
+        ),
+        required=True,
+    )
+    arg_parser.add_argument(
+        "--git-commit-date",
+        help=(
+            "should be in the format rfc-3339 "
+            "(e.g. 2006-08-07 12:34:56-06:00)"
+        ),
+        required=True,
+    )
+    arg_parser.add_argument(
+        "--git-commit-id", help="the git commit ID", required=True
+    )
+    arg_parser.add_argument(
+        "--map-file-path", help="the path to the '.map' file", required=True
+    )
+    arg_parser.add_argument(
+        "--source-code-path",
+        help="the path to the source code directory",
+        required=True,
+    )
+    arg_parser.add_argument(
+        "--matrix-file-path",
+        help="the path to the matrix file we're using",
+        required=True,
+    )
+    arg_parser.add_argument(
+        "--layout-file-path",
+        help="the path to the layout file we're using",
+        required=True,
+    )
+
+    args = arg_parser.parse_args(sys.argv[1:])
+
+    output = {}
+    dict_merge(
+        output,
+        gen_static(
+            args.current_date, args.git_commit_date, args.git_commit_id
+        )
+    )
+    dict_merge(output, find_keyboard_functions(args.source_code_path))
+    dict_merge(
+        output, gen_mappings(args.matrix_file_path, args.layout_file_path)
+    )
+
+    print(json.dumps(output, sort_keys=True, indent=4))
 
-# -----------------------------------------------------------------------------
 
-if __name__ == '__main__':
-	main()
+# -----------------------------------------------------------------------------
 
+if __name__ == "__main__":
+    main()
diff --git a/makefile b/makefile
index d9fe10c..971ee0e 100644
--- a/makefile
+++ b/makefile
@@ -58,24 +58,27 @@ SCRIPTS := build-scripts
 all: dist
 
 clean:
-	git clean -dX  # remove ignored files and directories
-	-rm -r '$(BUILD)'
+	git clean -fdX  # remove ignored files and directories
+	rm -rf '$(BUILD)'
 
 checkin:
 	-git commit -a
 
 build-dir:
-	-rm -r '$(BUILD)/$(TARGET)'*
-	-mkdir -p '$(BUILD)/$(TARGET)'
+	rm -rf '$(BUILD)/$(TARGET)'*
+	mkdir -p '$(BUILD)/$(TARGET)'
 
 firmware:
 	cd src; $(MAKE) LAYOUT=$(LAYOUT) all
 
-$(ROOT)/firmware.%: firmware
+$(ROOT):
+	mkdir -p '$@'
+
+$(ROOT)/firmware.%: firmware $(ROOT)
 	cp 'src/firmware.$*' '$@'
 
 
-$(ROOT)/firmware--ui-info.json: $(SCRIPTS)/gen-ui-info.py checkin
+$(ROOT)/firmware--ui-info.json: $(SCRIPTS)/gen-ui-info.py checkin firmware
 	( ./'$<' \
 		--current-date '$(shell $(DATE_PROG) --rfc-3339 s)' \
 		--git-commit-date '$(GIT_COMMIT_DATE)' \
