summaryrefslogtreecommitdiff
path: root/lib/python/qmk
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2024-02-28 21:47:37 +1100
committerNick Brassel <nick@tzarc.org>2024-02-28 21:47:37 +1100
commit4e369d405af6bba1adce6337b2e1b1ea1788566c (patch)
treeb0f020feff1809e37c9e7795d344929ff0bb290a /lib/python/qmk
parent4e04da397ef643f8fcf4afbe1d19f63aee1fc561 (diff)
parentdd1706e468bb18dd7f7ae143de735a5d3be1bfb8 (diff)
Merge branch 'develop'
Diffstat (limited to 'lib/python/qmk')
-rw-r--r--lib/python/qmk/build_targets.py5
-rwxr-xr-xlib/python/qmk/cli/bux.py4
-rw-r--r--lib/python/qmk/cli/find.py4
-rwxr-xr-xlib/python/qmk/cli/format/json.py4
-rwxr-xr-xlib/python/qmk/cli/generate/compilation_database.py4
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py2
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_c.py25
-rw-r--r--lib/python/qmk/cli/git/submodule.py8
-rw-r--r--lib/python/qmk/cli/lint.py15
-rwxr-xr-xlib/python/qmk/cli/mass_compile.py6
-rw-r--r--lib/python/qmk/cli/migrate.py5
-rw-r--r--lib/python/qmk/info.py46
-rw-r--r--lib/python/qmk/painter_qgf.py304
-rw-r--r--lib/python/qmk/search.py111
14 files changed, 335 insertions, 208 deletions
diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py
index 80f587bcc0..d974d04020 100644
--- a/lib/python/qmk/build_targets.py
+++ b/lib/python/qmk/build_targets.py
@@ -119,9 +119,10 @@ class BuildTarget:
command = self.compile_command(build_target=build_target, dry_run=True, **env_vars)
from qmk.cli.generate.compilation_database import write_compilation_database # Lazy load due to circular references
output_path = QMK_FIRMWARE / 'compile_commands.json'
- write_compilation_database(command=command, output_path=output_path, skip_clean=skip_clean, **env_vars)
- if output_path.exists() and HAS_QMK_USERSPACE:
+ ret = write_compilation_database(command=command, output_path=output_path, skip_clean=skip_clean, **env_vars)
+ if ret and output_path.exists() and HAS_QMK_USERSPACE:
shutil.copy(str(output_path), str(QMK_USERSPACE / 'compile_commands.json'))
+ return ret
def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
if self._clean or self._compiledb:
diff --git a/lib/python/qmk/cli/bux.py b/lib/python/qmk/cli/bux.py
index 8c7f172779..669521d08e 100755
--- a/lib/python/qmk/cli/bux.py
+++ b/lib/python/qmk/cli/bux.py
@@ -19,7 +19,7 @@ def bux(cli):
config.set_config('user', 'bux', bux + 1)
cli.save_config()
- buck = """
+ buck = r"""
@@BBBBBBBBBBBBBBBBBBBBK `vP8#####BE2~ x###g_ `S###q n##} -j#Bl. vBBBBBBBBBBBBBBBBBBBB@@
@B `:!: ^#@#]- `!t@@&. 7@@B@#^ _Q@Q@@R y@@l:P@#1' `!!_ B@
@B r@@@B g@@| ` N@@u 7@@iv@@u *#@z"@@R y@@&@@Q- l@@@D B@
@@ -34,7 +34,7 @@ def bux(cli):
@B _y ]# ,c vUWNWWPsfsssN9WyccnckAfUfWb0DR0&R5RRRddq2_ `@D`jr@2U@#c3@1@Qc- B@
@B !7! .r]` }AE0RdRqNd9dNR9fUIzzosPqqAddNNdER9EE9dPy! BQ!zy@iU@.Q@@y@8x- B@
@B :****>. '7adddDdR&gRNdRbd&dNNbbRdNdd5NdRRD0RSf}- .k0&EW`xR .8Q=NRRx B@
-@B =**-rx*r}r~}" ;n2jkzsf3N3zsKsP5dddRddddRddNNqPzy\\" '~****" B@
+@B =**-rx*r}r~}" ;n2jkzsf3N3zsKsP5dddRddddRddNNqPzy\" '~****" B@
@B :!!~!;=~r>:*_ `:^vxikylulKfHkyjzzozoIoklix|^!-` B@
@B ```'-_""::::!:_-.`` B@
@B `- .` B@
diff --git a/lib/python/qmk/cli/find.py b/lib/python/qmk/cli/find.py
index 55a0530092..8f3a29c90c 100644
--- a/lib/python/qmk/cli/find.py
+++ b/lib/python/qmk/cli/find.py
@@ -1,7 +1,7 @@
"""Command to search through all keyboards and keymaps for a given search criteria.
"""
from milc import cli
-from qmk.search import search_keymap_targets
+from qmk.search import filter_help, search_keymap_targets
@cli.argument(
@@ -11,7 +11,7 @@ from qmk.search import search_keymap_targets
action='append',
default=[],
help= # noqa: `format-python` and `pytest` don't agree here.
- "Filter the list of keyboards based on their info.json data. Accepts the formats key=value, function(key), or function(key,value), eg. 'features.rgblight=true'. Valid functions are 'absent', 'contains', 'exists' and 'length'. May be passed multiple times; all filters need to match. Value may include wildcards such as '*' and '?'." # noqa: `format-python` and `pytest` don't agree here.
+ f"Filter the list of keyboards based on their info.json data. Accepts the formats key=value, function(key), or function(key,value), eg. 'features.rgblight=true'. Valid functions are {filter_help()}. May be passed multiple times; all filters need to match. Value may include wildcards such as '*' and '?'." # noqa: `format-python` and `pytest` don't agree here.
)
@cli.argument('-p', '--print', arg_only=True, action='append', default=[], help="For each matched target, print the value of the supplied info.json key. May be passed multiple times.")
@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 283513254c..87a3837d10 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -92,8 +92,8 @@ def format_json(cli):
output = json.dumps(json_data, cls=json_encoder, sort_keys=True)
if cli.args.inplace:
- with open(cli.args.json_file, 'w+', encoding='utf-8') as outfile:
- outfile.write(output)
+ with open(cli.args.json_file, 'w+', encoding='utf-8', newline='\n') as outfile:
+ outfile.write(output + '\n')
# Display the results if print was set
# We don't operate in-place by default, so also display to stdout
diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py
index 5100d2b6d2..a2190fee66 100755
--- a/lib/python/qmk/cli/generate/compilation_database.py
+++ b/lib/python/qmk/cli/generate/compilation_database.py
@@ -17,6 +17,7 @@ from qmk.constants import QMK_FIRMWARE
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.keymap import keymap_completer
+from qmk.build_targets import KeyboardKeymapBuildTarget
@lru_cache(maxsize=10)
@@ -138,4 +139,5 @@ def generate_compilation_database(cli: MILC) -> Union[bool, int]:
elif not current_keymap:
cli.log.error('Could not determine keymap!')
- return write_compilation_database(current_keyboard, current_keymap, QMK_FIRMWARE / 'compile_commands.json')
+ target = KeyboardKeymapBuildTarget(current_keyboard, current_keymap)
+ return target.generate_compilation_database()
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 00fb1d9585..fc681300a3 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -108,6 +108,8 @@ def generate_config_items(kb_info_json, config_h_lines):
elif key_type.startswith('array'):
config_h_lines.append(generate_define(config_key, f'{{ {", ".join(map(str, config_value))} }}'))
elif key_type == 'bool':
+ config_h_lines.append(generate_define(config_key, 'true' if config_value else 'false'))
+ elif key_type == 'flag':
if config_value:
config_h_lines.append(generate_define(config_key))
elif key_type == 'mapping':
diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py
index f010341613..5a6c967486 100755
--- a/lib/python/qmk/cli/generate/keyboard_c.py
+++ b/lib/python/qmk/cli/generate/keyboard_c.py
@@ -9,21 +9,25 @@ from qmk.path import normpath
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
-def _gen_led_config(info_data):
+def _gen_led_configs(info_data):
+ lines = []
+
+ if 'layout' in info_data.get('rgb_matrix', {}):
+ lines.extend(_gen_led_config(info_data, 'rgb_matrix'))
+
+ if 'layout' in info_data.get('led_matrix', {}):
+ lines.extend(_gen_led_config(info_data, 'led_matrix'))
+
+ return lines
+
+
+def _gen_led_config(info_data, config_type):
"""Convert info.json content to g_led_config
"""
cols = info_data['matrix_size']['cols']
rows = info_data['matrix_size']['rows']
- config_type = None
- if 'layout' in info_data.get('rgb_matrix', {}):
- config_type = 'rgb_matrix'
- elif 'layout' in info_data.get('led_matrix', {}):
- config_type = 'led_matrix'
-
lines = []
- if not config_type:
- return lines
matrix = [['NO_LED'] * cols for _ in range(rows)]
pos = []
@@ -53,6 +57,7 @@ def _gen_led_config(info_data):
lines.append(f' {{ {", ".join(flags)} }},')
lines.append('};')
lines.append('#endif')
+ lines.append('')
return lines
@@ -98,7 +103,7 @@ def generate_keyboard_c(cli):
# Build the layouts.h file.
keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '']
- keyboard_h_lines.extend(_gen_led_config(kb_info_json))
+ keyboard_h_lines.extend(_gen_led_configs(kb_info_json))
keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json))
# Show the results
diff --git a/lib/python/qmk/cli/git/submodule.py b/lib/python/qmk/cli/git/submodule.py
index ef116ea124..1cbfd74e88 100644
--- a/lib/python/qmk/cli/git/submodule.py
+++ b/lib/python/qmk/cli/git/submodule.py
@@ -1,8 +1,8 @@
import shutil
+from pathlib import Path
from milc import cli
-from qmk.path import normpath
from qmk import submodules
REMOVE_DIRS = [
@@ -40,12 +40,12 @@ def git_submodule(cli):
remove_dirs = REMOVE_DIRS
if cli.config.git_submodule.force:
# Also trash everything that isnt marked as "safe"
- for path in normpath('lib').iterdir():
+ for path in Path('lib').iterdir():
if not any(ignore in path.as_posix() for ignore in IGNORE_DIRS):
remove_dirs.append(path)
- for folder in map(normpath, remove_dirs):
- if normpath(folder).is_dir():
+ for folder in map(Path, remove_dirs):
+ if folder.is_dir():
print(f"Removing '{folder}'")
shutil.rmtree(folder)
diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py
index a7c85b5643..7ebb0cf9c4 100644
--- a/lib/python/qmk/cli/lint.py
+++ b/lib/python/qmk/cli/lint.py
@@ -13,6 +13,7 @@ from qmk.git import git_get_ignored_files
from qmk.c_parse import c_source_files
CHIBIOS_CONF_CHECKS = ['chconf.h', 'halconf.h', 'mcuconf.h', 'board.h']
+INVALID_KB_FEATURES = set(['encoder_map', 'dip_switch_map', 'combo', 'tap_dance', 'via'])
def _list_defaultish_keymaps(kb):
@@ -69,6 +70,17 @@ def _handle_json_errors(kb, info):
return ok
+def _handle_invalid_features(kb, info):
+ """Check for features that should never be enabled at the keyboard level
+ """
+ ok = True
+ features = set(info.get('features', []))
+ for found in features & INVALID_KB_FEATURES:
+ ok = False
+ cli.log.error(f'{kb}: Invalid keyboard level feature detected - {found}')
+ return ok
+
+
def _chibios_conf_includenext_check(target):
"""Check the ChibiOS conf.h for the correct inclusion of the next conf.h
"""
@@ -154,6 +166,9 @@ def keyboard_check(kb):
ok = False
# Additional checks
+ if not _handle_invalid_features(kb, kb_info):
+ ok = False
+
rules_mk_assignment_errors = _rules_mk_assignment_only(kb)
if rules_mk_assignment_errors:
ok = False
diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py
index 69b9103fdc..7db704d6c2 100755
--- a/lib/python/qmk/cli/mass_compile.py
+++ b/lib/python/qmk/cli/mass_compile.py
@@ -52,9 +52,9 @@ all: {keyboard_safe}_{keymap_name}_binary
{' '.join(command)} \\
>>"{build_log}" 2>&1 \\
|| cp "{build_log}" "{failed_log}"
- @{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
- || {{ grep '\[WARNINGS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;33m[WARNINGS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
- || printf "Build %-64s \e[1;32m[OK]\e[0m\\n" "{keyboard_name}:{keymap_name}"
+ @{{ grep '\\[ERRORS\\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \\e[1;31m[ERRORS]\\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
+ || {{ grep '\\[WARNINGS\\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \\e[1;33m[WARNINGS]\\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
+ || printf "Build %-64s \\e[1;32m[OK]\\e[0m\\n" "{keyboard_name}:{keymap_name}"
@rm -f "{build_log}" || true
"""# noqa
)
diff --git a/lib/python/qmk/cli/migrate.py b/lib/python/qmk/cli/migrate.py
index c1b1ad1ea9..0bab5c1949 100644
--- a/lib/python/qmk/cli/migrate.py
+++ b/lib/python/qmk/cli/migrate.py
@@ -47,9 +47,12 @@ def migrate(cli):
files = _candidate_files(cli.args.keyboard)
# Filter down keys if requested
- keys = info_map.keys()
+ keys = list(filter(lambda key: info_map[key].get("to_json", True), info_map.keys()))
if cli.args.filter:
keys = list(set(keys) & set(cli.args.filter))
+ rejected = set(cli.args.filter) - set(keys)
+ for key in rejected:
+ cli.log.info(f'{{fg_yellow}}Skipping {key} as migration not possible...')
cli.log.info(f'{{fg_green}}Migrating keyboard {{fg_cyan}}{cli.args.keyboard}{{fg_green}}.{{fg_reset}}')
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index d42ba5c660..13588abdb8 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -233,6 +233,9 @@ def _extract_features(info_data, rules):
key = '_'.join(key.split('_')[:-1]).lower()
value = True if value.lower() in true_values else False if value.lower() in false_values else value
+ if key in ['lto']:
+ continue
+
if 'config_h_features' not in info_data:
info_data['config_h_features'] = {}
@@ -524,6 +527,9 @@ def _config_to_json(key_type, config_value):
"""Convert config value using spec
"""
if key_type.startswith('array'):
+ if key_type.count('.') > 1:
+ raise Exception(f"Conversion of {key_type} not possible")
+
if '.' in key_type:
key_type, array_type = key_type.split('.', 1)
else:
@@ -536,7 +542,7 @@ def _config_to_json(key_type, config_value):
else:
return list(map(str.strip, config_value.split(',')))
- elif key_type == 'bool':
+ elif key_type in ['bool', 'flag']:
if isinstance(config_value, bool):
return config_value
return config_value in true_values
@@ -706,27 +712,23 @@ def _extract_led_config(info_data, keyboard):
cols = info_data['matrix_size']['cols']
rows = info_data['matrix_size']['rows']
- # Determine what feature owns g_led_config
- features = info_data.get("features", {})
- feature = None
- if features.get("rgb_matrix", False):
- feature = "rgb_matrix"
- elif features.get("led_matrix", False):
- feature = "led_matrix"
-
- if feature:
- # Process
- for file in find_keyboard_c(keyboard):
- try:
- ret = find_led_config(file, cols, rows)
- if ret:
- info_data[feature] = info_data.get(feature, {})
- info_data[feature]["layout"] = ret
- except Exception as e:
- _log_warning(info_data, f'led_config: {file.name}: {e}')
-
- if info_data[feature].get("layout", None) and not info_data[feature].get("led_count", None):
- info_data[feature]["led_count"] = len(info_data[feature]["layout"])
+ for feature in ['rgb_matrix', 'led_matrix']:
+ if info_data.get('features', {}).get(feature, False) or feature in info_data:
+
+ # Only attempt search if dd led config is missing
+ if 'layout' not in info_data.get(feature, {}):
+ # Process
+ for file in find_keyboard_c(keyboard):
+ try:
+ ret = find_led_config(file, cols, rows)
+ if ret:
+ info_data[feature] = info_data.get(feature, {})
+ info_data[feature]['layout'] = ret
+ except Exception as e:
+ _log_warning(info_data, f'led_config: {file.name}: {e}')
+
+ if info_data[feature].get('layout', None) and not info_data[feature].get('led_count', None):
+ info_data[feature]['led_count'] = len(info_data[feature]['layout'])
return info_data
diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py
index 2b8edfb04d..cc4697f1c6 100644
--- a/lib/python/qmk/painter_qgf.py
+++ b/lib/python/qmk/painter_qgf.py
@@ -1,9 +1,11 @@
# Copyright 2021 Nick Brassel (@tzarc)
+# Copyright 2023 Pablo Martinez (@elpekenin) <elpekenin@elpekenin.dev>
# SPDX-License-Identifier: GPL-2.0-or-later
# Quantum Graphics File "QGF" Image File Format.
# See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
+import functools
from colorsys import rgb_to_hsv
from types import FunctionType
from PIL import Image, ImageFile, ImageChops
@@ -15,6 +17,12 @@ def o24(i):
return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
+# Helper to convert from RGB888 to the QMK "dialect" of HSV888
+def rgb888_to_qmk_hsv888(e):
+ hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
+ return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
+
+
########################################################################################################################
@@ -60,6 +68,14 @@ class QGFGraphicsDescriptor:
+ o16(self.frame_count) # frame count
)
+ @property
+ def image_size(self):
+ return self.image_width, self.image_height
+
+ @image_size.setter
+ def image_size(self, size):
+ self.image_width, self.image_height = size
+
########################################################################################################################
@@ -180,6 +196,14 @@ class QGFFrameDeltaDescriptorV1:
+ o16(self.bottom) # bottom
)
+ @property
+ def bbox(self):
+ return self.left, self.top, self.right, self.bottom
+
+ @bbox.setter
+ def bbox(self, bbox):
+ self.left, self.top, self.right, self.bottom = bbox
+
########################################################################################################################
@@ -221,42 +245,159 @@ def _accept(prefix):
return False
-def _save(im, fp, filename):
+def _for_all_frames(x: FunctionType, /, images):
+ frame_num = 0
+ last_frame = None
+ for frame in images:
+ # Get number of of frames in this image
+ nfr = getattr(frame, "n_frames", 1)
+ for idx in range(nfr):
+ frame.seek(idx)
+ frame.load()
+ copy = frame.copy().convert("RGB")
+ x(frame_num, copy, last_frame)
+ last_frame = copy
+ frame_num += 1
+
+
+def _compress_image(frame, last_frame, *, use_rle, use_deltas, format_, **_kwargs):
+ # Convert the original frame so we can do comparisons
+ converted = qmk.painter.convert_requested_format(frame, format_)
+ graphic_data = qmk.painter.convert_image_bytes(converted, format_)
+
+ # Convert the raw data to RLE-encoded if requested
+ raw_data = graphic_data[1]
+ if use_rle:
+ rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
+ use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
+ image_data = raw_data if use_raw_this_frame else rle_data
+
+ # Work out if a delta frame is smaller than injecting it directly
+ use_delta_this_frame = False
+ bbox = None
+ if use_deltas and last_frame is not None:
+ # If we want to use deltas, then find the difference
+ diff = ImageChops.difference(frame, last_frame)
+
+ # Get the bounding box of those differences
+ bbox = diff.getbbox()
+
+ # If we have a valid bounding box...
+ if bbox:
+ # ...create the delta frame by cropping the original.
+ delta_frame = frame.crop(bbox)
+
+ # Convert the delta frame to the requested format
+ delta_converted = qmk.painter.convert_requested_format(delta_frame, format_)
+ delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format_)
+
+ # Work out how large the delta frame is going to be with compression etc.
+ delta_raw_data = delta_graphic_data[1]
+ if use_rle:
+ delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
+ delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
+ delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
+
+ # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
+ # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
+ # sizing constraints.
+ if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
+ # Copy across all the delta equivalents so that the rest of the processing acts on those
+ graphic_data = delta_graphic_data
+ raw_data = delta_raw_data
+ rle_data = delta_rle_data
+ use_raw_this_frame = delta_use_raw_this_frame
+ image_data = delta_image_data
+ use_delta_this_frame = True
+
+ # Default to whole image
+ bbox = bbox or [0, 0, *frame.size]
+ # Fix sze (as per #20296), we need to cast first as tuples are inmutable
+ bbox = list(bbox)
+ bbox[2] -= 1
+ bbox[3] -= 1
+
+ return {
+ "bbox": bbox,
+ "graphic_data": graphic_data,
+ "image_data": image_data,
+ "use_delta_this_frame": use_delta_this_frame,
+ "use_raw_this_frame": use_raw_this_frame,
+ }
+
+
+# Helper function to save each frame to the output file
+def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, **kwargs):
+ # Not an argument of the function as it would consume from **kwargs
+ format_ = kwargs["format_"]
+
+ # (potentially) Apply RLE and/or delta, and work out output image's information
+ outputs = _compress_image(frame, last_frame, **kwargs)
+ bbox = outputs["bbox"]
+ graphic_data = outputs["graphic_data"]
+ image_data = outputs["image_data"]
+ use_delta_this_frame = outputs["use_delta_this_frame"]
+ use_raw_this_frame = outputs["use_raw_this_frame"]
+
+ # Write out the frame descriptor
+ frame_offsets.frame_offsets[idx] = fp.tell()
+ vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ frame_descriptor = QGFFrameDescriptorV1()
+ frame_descriptor.is_delta = use_delta_this_frame
+ frame_descriptor.is_transparent = False
+ frame_descriptor.format = format_['image_format_byte']
+ frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t
+ frame_descriptor.delay = frame.info.get('duration', 1000) # If we're not an animation, just pretend we're delaying for 1000ms
+ frame_descriptor.write(fp)
+
+ # Write out the palette if required
+ if format_['has_palette']:
+ palette = graphic_data[0]
+ palette_descriptor = QGFFramePaletteDescriptorV1()
+
+ # Convert all palette entries to HSV888 and write to the output
+ palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
+ vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ palette_descriptor.write(fp)
+
+ # Write out the delta info if required
+ if use_delta_this_frame:
+ # Set up the rendering location of where the delta frame should be situated
+ delta_descriptor = QGFFrameDeltaDescriptorV1()
+ delta_descriptor.bbox = bbox
+
+ # Write the delta frame to the output
+ vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ delta_descriptor.write(fp)
+
+ # Write out the data for this frame to the output
+ data_descriptor = QGFFrameDataDescriptorV1()
+ data_descriptor.data = image_data
+ vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
+ data_descriptor.write(fp)
+
+
+def _save(im, fp, _filename):
"""Helper method used by PIL to write to an output file.
"""
# Work out from the parameters if we need to do anything special
encoderinfo = im.encoderinfo.copy()
- append_images = list(encoderinfo.get("append_images", []))
- verbose = encoderinfo.get("verbose", False)
- use_deltas = encoderinfo.get("use_deltas", True)
- use_rle = encoderinfo.get("use_rle", True)
- # Helper for inline verbose prints
- def vprint(s):
- if verbose:
- print(s)
+ # Helper for prints, noop taking any args if not verbose
+ global vprint
+ verbose = encoderinfo.get("verbose", False)
+ vprint = print if verbose else lambda *_args, **_kwargs: None
# Helper to iterate through all frames in the input image
- def _for_all_frames(x: FunctionType):
- frame_num = 0
- last_frame = None
- for frame in [im] + append_images:
- # Get number of of frames in this image
- nfr = getattr(frame, "n_frames", 1)
- for idx in range(nfr):
- frame.seek(idx)
- frame.load()
- copy = frame.copy().convert("RGB")
- x(frame_num, copy, last_frame)
- last_frame = copy
- frame_num += 1
+ append_images = list(encoderinfo.get("append_images", []))
+ for_all_frames = functools.partial(_for_all_frames, images=[im, *append_images])
# Collect all the frame sizes
frame_sizes = []
- _for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size))
+ for_all_frames(lambda _idx, frame, _last_frame: frame_sizes.append(frame.size))
# Make sure all frames are the same size
- if len(list(set(frame_sizes))) != 1:
+ if len(set(frame_sizes)) != 1:
raise ValueError("Mismatching sizes on frames")
# Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the
@@ -264,8 +405,7 @@ def _save(im, fp, filename):
graphics_descriptor_location = fp.tell()
graphics_descriptor = QGFGraphicsDescriptor()
graphics_descriptor.frame_count = len(frame_sizes)
- graphics_descriptor.image_width = frame_sizes[0][0]
- graphics_descriptor.image_height = frame_sizes[0][1]
+ graphics_descriptor.image_size = frame_sizes[0]
vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
graphics_descriptor.write(fp)
@@ -276,117 +416,9 @@ def _save(im, fp, filename):
vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
frame_offsets.write(fp)
- # Helper function to save each frame to the output file
- def _write_frame(idx, frame, last_frame):
- # If we replace the frame we're going to output with a delta, we can override it here
- this_frame = frame
- location = (0, 0)
- size = frame.size
-
- # Work out the format we're going to use
- format = encoderinfo["qmk_format"]
-
- # Convert the original frame so we can do comparisons
- converted = qmk.painter.convert_requested_format(this_frame, format)
- graphic_data = qmk.painter.convert_image_bytes(converted, format)
-
- # Convert the raw data to RLE-encoded if requested
- raw_data = graphic_data[1]
- if use_rle:
- rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
- use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
- image_data = raw_data if use_raw_this_frame else rle_data
-
- # Work out if a delta frame is smaller than injecting it directly
- use_delta_this_frame = False
- if use_deltas and last_frame is not None:
- # If we want to use deltas, then find the difference
- diff = ImageChops.difference(frame, last_frame)
-
- # Get the bounding box of those differences
- bbox = diff.getbbox()
-
- # If we have a valid bounding box...
- if bbox:
- # ...create the delta frame by cropping the original.
- delta_frame = frame.crop(bbox)
- delta_location = (bbox[0], bbox[1])
- delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
-
- # Convert the delta frame to the requested format
- delta_converted = qmk.painter.convert_requested_format(delta_frame, format)
- delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format)
-
- # Work out how large the delta frame is going to be with compression etc.
- delta_raw_data = delta_graphic_data[1]
- if use_rle:
- delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
- delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
- delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
-
- # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
- # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
- # sizing constraints.
- if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
- # Copy across all the delta equivalents so that the rest of the processing acts on those
- this_frame = delta_frame
- location = delta_location
- size = delta_size
- converted = delta_converted
- graphic_data = delta_graphic_data
- raw_data = delta_raw_data
- rle_data = delta_rle_data
- use_raw_this_frame = delta_use_raw_this_frame
- image_data = delta_image_data
- use_delta_this_frame = True
-
- # Write out the frame descriptor
- frame_offsets.frame_offsets[idx] = fp.tell()
- vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
- frame_descriptor = QGFFrameDescriptorV1()
- frame_descriptor.is_delta = use_delta_this_frame
- frame_descriptor.is_transparent = False
- frame_descriptor.format = format['image_format_byte']
- frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t
- frame_descriptor.delay = frame.info['duration'] if 'duration' in frame.info else 1000 # If we're not an animation, just pretend we're delaying for 1000ms
- frame_descriptor.write(fp)
-
- # Write out the palette if required
- if format['has_palette']:
- palette = graphic_data[0]
- palette_descriptor = QGFFramePaletteDescriptorV1()
-
- # Helper to convert from RGB888 to the QMK "dialect" of HSV888
- def rgb888_to_qmk_hsv888(e):
- hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
- return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
-
- # Convert all palette entries to HSV888 and write to the output
- palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
- vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
- palette_descriptor.write(fp)
-
- # Write out the delta info if required
- if use_delta_this_frame:
- # Set up the rendering location of where the delta frame should be situated
- delta_descriptor = QGFFrameDeltaDescriptorV1()
- delta_descriptor.left = location[0]
- delta_descriptor.top = location[1]
- delta_descriptor.right = location[0] + size[0] - 1
- delta_descriptor.bottom = location[1] + size[1] - 1
-
- # Write the delta frame to the output
- vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
- delta_descriptor.write(fp)
-
- # Write out the data for this frame to the output
- data_descriptor = QGFFrameDataDescriptorV1()
- data_descriptor.data = image_data
- vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
- data_descriptor.write(fp)
-
# Iterate over each if the input frames, writing it to the output in the process
- _for_all_frames(_write_frame)
+ write_frame = functools.partial(_write_frame, format_=encoderinfo["qmk_format"], fp=fp, use_deltas=encoderinfo.get("use_deltas", True), use_rle=encoderinfo.get("use_rle", True), frame_offsets=frame_offsets)
+ for_all_frames(write_frame)
# Go back and update the graphics descriptor now that we can determine the final file size
graphics_descriptor.total_file_size = fp.tell()
diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py
index 84cf6cbe32..33550a3db2 100644
--- a/lib/python/qmk/search.py
+++ b/lib/python/qmk/search.py
@@ -5,7 +5,7 @@ import functools
import fnmatch
import logging
import re
-from typing import List, Tuple
+from typing import Callable, List, Optional, Tuple
from dotty_dict import dotty, Dotty
from milc import cli
@@ -15,6 +15,82 @@ from qmk.keyboard import list_keyboards, keyboard_folder
from qmk.keymap import list_keymaps, locate_keymap
from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget
+TargetInfo = Tuple[str, str, dict]
+
+
+# by using a class for filters, we dont need to worry about capturing values
+# see details <https://github.com/qmk/qmk_firmware/pull/21090>
+class FilterFunction:
+ """Base class for filters.
+ It provides:
+ - __init__: capture key and value
+
+ Each subclass should provide:
+ - func_name: how it will be specified on CLI
+ >>> qmk find -f <func_name>...
+ - apply: function that actually applies the filter
+ ie: return whether the input kb/km satisfies the condition
+ """
+
+ key: str
+ value: Optional[str]
+
+ func_name: str
+ apply: Callable[[TargetInfo], bool]
+
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+
+
+class Exists(FilterFunction):
+ func_name = "exists"
+
+ def apply(self, target_info: TargetInfo) -> bool:
+ _kb, _km, info = target_info
+ return self.key in info
+
+
+class Absent(FilterFunction):
+ func_name = "absent"
+
+ def apply(self, target_info: TargetInfo) -> bool:
+ _kb, _km, info = target_info
+ return self.key not in info
+
+
+class Length(FilterFunction):
+ func_name = "length"
+
+ def apply(self, target_info: TargetInfo) -> bool:
+ _kb, _km, info = target_info
+ return (self.key in info and len(info[self.key]) == int(self.value))
+
+
+class Contains(FilterFunction):
+ func_name = "contains"
+
+ def apply(self, target_info: TargetInfo) -> bool:
+ _kb, _km, info = target_info
+ return (self.key in info and self.value in info[self.key])
+
+
+def _get_filter_class(func_name: str, key: str, value: str) -> Optional[FilterFunction]:
+ """Initialize a filter subclass based on regex findings and return it.
+ None if no there's no filter with the name queried.
+ """
+
+ for subclass in FilterFunction.__subclasses__():
+ if func_name == subclass.func_name:
+ return subclass(key, value)
+
+ return None
+
+
+def filter_help() -> str:
+ names = [f"'{f.func_name}'" for f in FilterFunction.__subclasses__()]
+ return ", ".join(names[:-1]) + f" and {names[-1]}"
+
def _set_log_level(level):
cli.acquire_lock()
@@ -48,11 +124,12 @@ def _keymap_exists(keyboard, keymap):
return keyboard if locate_keymap(keyboard, keymap) is not None else None
-def _load_keymap_info(kb_km):
+def _load_keymap_info(target: Tuple[str, str]) -> TargetInfo:
"""Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination.
"""
+ kb, km = target
with ignore_logging():
- return (kb_km[0], kb_km[1], keymap_json(kb_km[0], kb_km[1]))
+ return (kb, km, keymap_json(kb, km))
def expand_make_targets(targets: List[str]) -> List[Tuple[str, str]]:
@@ -139,26 +216,14 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str
key = function_match.group('key')
value = function_match.group('value')
- if value is not None:
- if func_name == 'length':
- valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and len(e[2].get(key)) == int(value), valid_keymaps)
- elif func_name == 'contains':
- valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and value in e[2].get(key), valid_keymaps)
- else:
- cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
- continue
-
- cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}, {{fg_cyan}}{value}{{fg_reset}})...')
- else:
- if func_name == 'exists':
- valid_keymaps = filter(lambda e, key=key: key in e[2], valid_keymaps)
- elif func_name == 'absent':
- valid_keymaps = filter(lambda e, key=key: key not in e[2], valid_keymaps)
- else:
- cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
- continue
-
- cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}})...')
+ filter_class = _get_filter_class(func_name, key, value)
+ if filter_class is None:
+ cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
+ continue
+ valid_keymaps = filter(filter_class.apply, valid_keymaps)
+
+ value_str = f", {{fg_cyan}}{value}{{fg_reset}})" if value is not None else ""
+ cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}{value_str}...')
elif equals_match is not None:
key = equals_match.group('key')