Skip to content

Commit

Permalink
[CLI] Add a subcommand for getting information about a keyboard (qmk#…
Browse files Browse the repository at this point in the history
…8666)

You can now use `qmk info` to get information about keyboards and keymaps.

Co-authored-by: Erovia <Erovia@users.noreply.github.com>
  • Loading branch information
skullydazed and Erovia authored May 26, 2020
1 parent 5d3bf8a commit 751316c
Show file tree
Hide file tree
Showing 17 changed files with 921 additions and 113 deletions.
37 changes: 35 additions & 2 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

This command allows you to compile firmware from any directory. You can compile JSON exports from <https://config.qmk.fm>, compile keymaps in the repo, or compile the keyboard in the current working directory.

This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.

**Usage for Configurator Exports**:

```
Expand Down Expand Up @@ -73,8 +75,9 @@ $ qmk compile -kb dz60

## `qmk flash`

This command is similar to `qmk compile`, but can also target a bootloader. The bootloader is optional, and is set to `:flash` by default.
To specify a different bootloader, use `-bl <bootloader>`. Visit the [Flashing Firmware](flashing.md) guide for more details of the available bootloaders.
This command is similar to `qmk compile`, but can also target a bootloader. The bootloader is optional, and is set to `:flash` by default. To specify a different bootloader, use `-bl <bootloader>`. Visit the [Flashing Firmware](flashing.md) guide for more details of the available bootloaders.

This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.

**Usage for Configurator Exports**:

Expand Down Expand Up @@ -128,6 +131,32 @@ Check your environment and report problems only:

qmk doctor -n

## `qmk info`

Displays information about keyboards and keymaps in QMK. You can use this to get information about a keyboard, show the layouts, display the underlying key matrix, or to pretty-print JSON keymaps.

**Usage**:

```
qmk info [-f FORMAT] [-m] [-l] [-km KEYMAP] [-kb KEYBOARD]
```

This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.

**Examples**:

Show basic information for a keyboard:

qmk info -kb planck/rev5

Show the matrix for a keyboard:

qmk info -kb ergodox_ez -m

Show a JSON keymap for a keyboard:

qmk info -kb clueboard/california -km default

## `qmk json2c`

Creates a keymap.c from a QMK Configurator export.
Expand All @@ -152,6 +181,8 @@ qmk list-keyboards

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

This command is directory aware. It will automatically fill in KEYBOARD if you are in a keyboard directory.

**Usage**:

```
Expand All @@ -162,6 +193,8 @@ qmk list-keymaps -kb planck/ez

This command creates a new keymap based on a keyboard's existing default keymap.

This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.

**Usage**:

```
Expand Down
161 changes: 161 additions & 0 deletions lib/python/qmk/c_parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""Functions for working with config.h files.
"""
from pathlib import Path

from milc import cli

from qmk.comment_remover import comment_remover

default_key_entry = {'x': -1, 'y': 0, 'w': 1}


def c_source_files(dir_names):
"""Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
Args:
dir_names
List of directories relative to `qmk_firmware`.
"""
files = []
for dir in dir_names:
files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
return files


def find_layouts(file):
"""Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
"""
file = Path(file)
aliases = {} # Populated with all `#define`s that aren't functions
parsed_layouts = {}

# Search the file for LAYOUT macros and aliases
file_contents = file.read_text()
file_contents = comment_remover(file_contents)
file_contents = file_contents.replace('\\\n', '')

for line in file_contents.split('\n'):
if line.startswith('#define') and '(' in line and 'LAYOUT' in line:
# We've found a LAYOUT macro
macro_name, layout, matrix = _parse_layout_macro(line.strip())

# Reject bad macro names
if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
continue

# Parse the matrix data
matrix_locations = _parse_matrix_locations(matrix, file, macro_name)

# Parse the layout entries into a basic structure
default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
layout = layout.strip()
parsed_layout = [_default_key(key) for key in layout.split(',')]

for key in parsed_layout:
key['matrix'] = matrix_locations.get(key['label'])

parsed_layouts[macro_name] = {
'key_count': len(parsed_layout),
'layout': parsed_layout,
'filename': str(file),
}

elif '#define' in line:
# Attempt to extract a new layout alias
try:
_, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
aliases[pp_macro_name] = pp_macro_text
except ValueError:
continue

# Populate our aliases
for alias, text in aliases.items():
if text in parsed_layouts and 'KEYMAP' not in alias:
parsed_layouts[alias] = parsed_layouts[text]

return parsed_layouts


def parse_config_h_file(config_h_file, config_h=None):
"""Extract defines from a config.h file.
"""
if not config_h:
config_h = {}

config_h_file = Path(config_h_file)

if config_h_file.exists():
config_h_text = config_h_file.read_text()
config_h_text = config_h_text.replace('\\\n', '')

for linenum, line in enumerate(config_h_text.split('\n')):
line = line.strip()

if '//' in line:
line = line[:line.index('//')].strip()

if not line:
continue

line = line.split()

if line[0] == '#define':
if len(line) == 1:
cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
elif len(line) == 2:
config_h[line[1]] = True
else:
config_h[line[1]] = ' '.join(line[2:])

elif line[0] == '#undef':
if len(line) == 2:
if line[1] in config_h:
if config_h[line[1]] is True:
del config_h[line[1]]
else:
config_h[line[1]] = False
else:
cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))

return config_h


def _default_key(label=None):
"""Increment x and return a copy of the default_key_entry.
"""
default_key_entry['x'] += 1
new_key = default_key_entry.copy()

if label:
new_key['label'] = label

return new_key


def _parse_layout_macro(layout_macro):
"""Split the LAYOUT macro into its constituent parts
"""
layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
macro_name, layout = layout_macro.split('(', 1)
layout, matrix = layout.split(')', 1)

return macro_name, layout, matrix


def _parse_matrix_locations(matrix, file, macro_name):
"""Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
"""
matrix_locations = {}

for row_num, row in enumerate(matrix.split('},{')):
if row.startswith('LAYOUT'):
cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
break

row = row.replace('{', '').replace('}', '')
for col_num, identifier in enumerate(row.split(',')):
if identifier != 'KC_NO':
matrix_locations[identifier] = (row_num, col_num)

return matrix_locations
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import doctor
from . import flash
from . import hello
from . import info
from . import json
from . import json2c
from . import list
Expand Down
10 changes: 6 additions & 4 deletions lib/python/qmk/cli/cformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from shutil import which

from milc import cli
import qmk.path

from qmk.path import normpath
from qmk.c_parse import c_source_files


def cformat_run(files, all_files):
Expand Down Expand Up @@ -45,18 +47,18 @@ def cformat(cli):
ignores = ['tmk_core/protocol/usb_hid', 'quantum/template']
# Find the list of files to format
if cli.args.files:
files.extend(qmk.path.normpath(file) for file in cli.args.files)
files.extend(normpath(file) for file in cli.args.files)
# If -a is specified
elif cli.args.all_files:
all_files = qmk.path.c_source_files(core_dirs)
all_files = c_source_files(core_dirs)
# The following statement checks each file to see if the file path is in the ignored directories.
files.extend(file for file in all_files if not any(i in str(file) for i in ignores))
# No files specified & no -a flag
else:
base_args = ['git', 'diff', '--name-only', cli.args.base_branch]
out = subprocess.run(base_args + core_dirs, check=True, stdout=subprocess.PIPE)
changed_files = filter(None, out.stdout.decode('UTF-8').split('\n'))
filtered_files = [qmk.path.normpath(file) for file in changed_files if not any(i in file for i in ignores)]
filtered_files = [normpath(file) for file in changed_files if not any(i in file for i in ignores)]
files.extend(file for file in filtered_files if file.exists() and file.suffix in ['.c', '.h', '.cpp'])

# Run clang-format on the files we've found
Expand Down
Loading

0 comments on commit 751316c

Please sign in to comment.