pax_global_header00006660000000000000000000000064147103305600014511gustar00rootroot0000000000000052 comment=ed0d5699ffafde8811109d8acdea1dc70b52ac83 qmk_cli-1.1.6/000077500000000000000000000000001471033056000131355ustar00rootroot00000000000000qmk_cli-1.1.6/.bumpversion.cfg000066400000000000000000000003761471033056000162530ustar00rootroot00000000000000[bumpversion] current_version = 1.1.6 commit = True tag = True tag_name = {new_version} message = New release: {current_version} → {new_version} [bumpversion:file:.bumpversion.cfg] [bumpversion:file:qmk_cli/__init__.py] [bumpversion:file:setup.cfg] qmk_cli-1.1.6/.editorconfig000066400000000000000000000015251471033056000156150ustar00rootroot00000000000000# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs # editorconfig.org root = true [*] indent_style = space indent_size = 4 # We recommend you to keep these unchanged charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false indent_size = 4 [{qmk,*.py}] charset = utf-8 max_line_length = 200 # Make these match what we have in .gitattributes [*.mk] end_of_line = lf [Makefile] end_of_line = lf [*.sh] end_of_line = lf # The gitattributes file will handle the line endings conversion properly according to the operating system settings for other files # We don't have gitattributes properly for these # So if the user have for example core.autocrlf set to true # the line endings would be wrong. [lib/**] end_of_line = unset qmk_cli-1.1.6/.github/000077500000000000000000000000001471033056000144755ustar00rootroot00000000000000qmk_cli-1.1.6/.github/dependabot.yml000066400000000000000000000010711471033056000173240ustar00rootroot00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" schedule: interval: "daily" reviewers: - "qmk/collaborators" - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" reviewers: - "qmk/collaborators" qmk_cli-1.1.6/.github/workflows/000077500000000000000000000000001471033056000165325ustar00rootroot00000000000000qmk_cli-1.1.6/.github/workflows/cli_setup.yml000066400000000000000000000035331471033056000212500ustar00rootroot00000000000000name: CLI Setup on: push: branches: - master pull_request: jobs: test_cli_base_container: runs-on: ubuntu-latest container: ghcr.io/qmk/qmk_base_container env: QMK_HOME: ~/qmk_firmware steps: - uses: actions/checkout@v4 - name: Install dependencies run: apt-get update && apt-get install -y python3-venv - name: Run ci_tests run: ./ci_tests -a test_cli_linux_macos: needs: test_cli_base_container runs-on: ${{ matrix.os }} env: QMK_HOME: ~/qmk_firmware strategy: matrix: os: [macos-latest, ubuntu-latest] python-version: ['3.9', '3.10', '3.11', '3.12'] include: - os: macos-13 python-version: '3.9' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Run ci_tests run: ./ci_tests -a test_cli_win: needs: test_cli_base_container runs-on: windows-latest env: QMK_HOME: $HOME/qmk_firmware steps: - uses: actions/checkout@v4 - name: (MSYS2) Setup and install dependencies uses: msys2/setup-msys2@v2 with: update: true install: git mingw-w64-x86_64-toolchain mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-build mingw-w64-x86_64-python-pillow mingw-w64-x86_64-rust # Upgrade pip due to msys packaging + pypa/build/pull/736 issues - name: (MSYS2) Install Python dependencies shell: msys2 {0} run: | python3 -m pip install --upgrade pip - name: (MSYS2) Install QMK CLI from source shell: msys2 {0} run: | python3 -m build python3 -m pip install dist/qmk-*.tar.gz - name: (MSYS2) Run qmk setup -y shell: msys2 {0} run: qmk setup -y qmk_cli-1.1.6/.github/workflows/codeql-analysis.yml000066400000000000000000000046201471033056000223470ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '22 18 * * 0' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 qmk_cli-1.1.6/.github/workflows/docker-republish.yml000066400000000000000000000025151471033056000225220ustar00rootroot00000000000000name: Rebuild Docker Image on: workflow_dispatch: jobs: redeploy: runs-on: ubuntu-latest steps: - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.9' - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 'Get Previous tag' id: previoustag uses: "WyriHaximus/github-action-get-previous-tag@v1" - name: Download previous artifact run: | pip download -d "${GITHUB_WORKSPACE}/dist" qmk=="${{ steps.previoustag.outputs.tag }}" - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Build and Push to Docker Hub uses: docker/build-push-action@v6.9.0 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: | ghcr.io/qmk/qmk_cli:latest qmkfm/qmk_cli:latest qmk_cli-1.1.6/.github/workflows/python-publish.yml000066400000000000000000000045221471033056000222450ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: workflow_dispatch: inputs: version_part: description: 'Which part of the version to increment (patch, minor, major)' required: true default: 'patch' jobs: deploy: runs-on: ubuntu-latest steps: - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.9' - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - uses: actions/checkout@v4 - name: Run ci_tests run: ./ci_tests - name: Install dependencies run: | python3 -m pip install --upgrade pip pip install setuptools wheel pip install -r requirements-dev.txt - name: Bump version run: | git config --local user.email "hello@qmk.fm" git config --local user.name "QMK Bot" bumpversion ${{ github.event.inputs.version_part }} - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: master tags: true - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_APITOKEN }} run: | python3 -m build twine upload dist/* - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Build and Push to Docker Hub uses: docker/build-push-action@v6.9.0 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: | ghcr.io/qmk/qmk_cli:latest qmkfm/qmk_cli:latest - name: Trigger OS package builds run: ./trigger_packages env: QMK_BOT_TOKEN: ${{ secrets.QMK_BOT_TOKEN }} qmk_cli-1.1.6/.gitignore000066400000000000000000000023151471033056000151260ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ *.swp # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # QMK qmk_firmware qmk_cli-1.1.6/.vscode/000077500000000000000000000000001471033056000144765ustar00rootroot00000000000000qmk_cli-1.1.6/.vscode/settings.json000066400000000000000000000000541471033056000172300ustar00rootroot00000000000000{ "python.formatting.provider": "yapf" }qmk_cli-1.1.6/Dockerfile000066400000000000000000000005561471033056000151350ustar00rootroot00000000000000FROM ghcr.io/qmk/qmk_base_container:latest # Copy package in ADD dist /tmp/dist # Install python packages RUN python3 -m pip uninstall -y qmk || true RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \ python3 -m pip install /tmp/dist/qmk-*.whl && \ rm -rf /tmp/dist # Set the default location for qmk_firmware ENV QMK_HOME /qmk_firmware qmk_cli-1.1.6/LICENSE000066400000000000000000000020441471033056000141420ustar00rootroot00000000000000MIT License Copyright (c) 2019 QMK Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qmk_cli-1.1.6/MANIFEST.in000066400000000000000000000001461471033056000146740ustar00rootroot00000000000000include *.md include *.txt include LICENSE include qmk include release recursive-include qmk_cli *.py qmk_cli-1.1.6/README.md000066400000000000000000000027551471033056000144250ustar00rootroot00000000000000# QMK CLI [![CLI Setup](https://github.com/qmk/qmk_cli/actions/workflows/cli_setup.yml/badge.svg)](https://github.com/qmk/qmk_cli/actions/workflows/cli_setup.yml) A program to help users work with [QMK Firmware](https://qmk.fm/). # Features * Interact with your qmk_firmware tree from any location * Use `qmk clone` to pull down anyone's `qmk_firmware` fork * Setup your build environment with `qmk setup` * Use `qmk console` to get debugging information from your keyboard(s) * Check that your environment is correctly setup with `qmk doctor` * Integrates with qmk_firmware for additional functionality: * `qmk compile` * `qmk info` * `qmk flash` * `qmk lint` * ...and many more! # Packages We provide "install and go" packages for many Operating Systems. ## Linux Packages for several distributions available here: https://github.com/qmk/qmk_fpm ## macOS Using [Homebrew](https://brew.sh): brew install qmk/qmk/qmk ## Windows Download our custom MSYS2 installer here: https://msys.qmk.fm/ # Quickstart * `python3 -m pip install qmk` * `qmk setup` # Building We follow PEP517, you can install using [build](https://pypi.org/project/build/): Setup: python3 -m pip install build Build: python3 -m build You can read more about working with PEP517 packages in the [Python Packaging User Guide](https://packaging.python.org/guides/distributing-packages-using-setuptools/#packaging-your-project). # Documentation Full documentation: qmk_cli-1.1.6/SECURITY.md000066400000000000000000000006421471033056000147300ustar00rootroot00000000000000# Security Policy ## Supported Versions Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 0.0.x | :white_check_mark: | ## Reporting a Vulnerability * [Open an Issue](https://github.com/qmk/qmk_cli/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBug%5D+) qmk_cli-1.1.6/ci_tests000077500000000000000000000022331471033056000147000ustar00rootroot00000000000000#!/usr/bin/env bash set -e #set -x ADVANCED=false TMPDIR=$(mktemp -d) QMK_HOME="$TMPDIR/qmk_firmware" export QMK_HOME for arg in $@; do if [ "$arg" = "-a" ]; then ADVANCED=true fi done if [ $ADVANCED = true ]; then echo "*** Running in advanced mode with tmp files in $TMPDIR" else echo "*** Running in basic mode with tmp files in $TMPDIR" fi # Setup our virtualenv if [ -e .ci_venv ]; then rm -rf .ci_venv fi python3 -m venv .ci_venv source .ci_venv/bin/activate # Install dependencies python3 -m pip install -U pip wheel python3 -m pip install . python3 -m pip install -r requirements-dev.txt # Ensure that qmk works echo "*** Testing 'qmk clone -h'" qmk clone -h #echo "*** Testing 'qmk config -a'" # Test disabled as `milc` at least 1.6.8+ returns False and thus non-zero exit code #qmk config -a echo "*** Testing 'qmk setup -n'" qmk setup -n echo echo "*** Basic tests completed successfully!" # Run advanced test if requested if [ $ADVANCED = true ]; then echo echo "*** Testing 'qmk setup -y'" qmk setup -y echo echo "*** Advanced tests completed successfully!" fi # Cleanup deactivate rm -rf .ci_venv $TMPDIR qmk_cli-1.1.6/pyproject.toml000066400000000000000000000001501471033056000160450ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=42", "wheel" ] build-backend = "setuptools.build_meta" qmk_cli-1.1.6/qmk000077500000000000000000000002011471033056000136440ustar00rootroot00000000000000#!/usr/bin/env python3 """Wrapper to call the qmk cli script entrypoint. """ import qmk_cli.script_qmk qmk_cli.script_qmk.main() qmk_cli-1.1.6/qmk_cli/000077500000000000000000000000001471033056000145545ustar00rootroot00000000000000qmk_cli-1.1.6/qmk_cli/__init__.py000066400000000000000000000001131471033056000166600ustar00rootroot00000000000000"""A program to help you work with qmk_firmware.""" __version__ = '1.1.6' qmk_cli-1.1.6/qmk_cli/git.py000066400000000000000000000020611471033056000157100ustar00rootroot00000000000000"""Helpers for working with git. """ import subprocess from milc import cli default_repo = 'qmk_firmware' default_fork = 'qmk/' + default_repo default_branch = 'master' def git_clone(url, destination, branch): git_clone = [ 'git', 'clone', '--recurse-submodules', '--branch=' + branch, url, str(destination), ] cli.log.debug('Git clone command: %s', git_clone) try: with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p: for line in p.stdout: print(line, end='') except Exception as e: git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone]) cli.log.error("Could not run '%s': %s: %s", git_cmd, e.__class__.__name__, e) return False if p.returncode == 0: cli.log.info('Successfully cloned %s to %s!', url, destination) return True else: cli.log.error('git clone exited %d', p.returncode) return False qmk_cli-1.1.6/qmk_cli/helpers.py000066400000000000000000000030271471033056000165720ustar00rootroot00000000000000"""Useful helper functions. """ import os from functools import lru_cache from importlib.util import find_spec from pathlib import Path from milc import cli def is_qmk_firmware(qmk_firmware): """Returns True if the given Path() is a qmk_firmware clone. """ paths = [ qmk_firmware, qmk_firmware / 'quantum', qmk_firmware / 'requirements.txt', qmk_firmware / 'requirements-dev.txt', qmk_firmware / 'lib/python/qmk/cli/__init__.py' ] for path in paths: if not path.exists(): return False return True @lru_cache(maxsize=2) def find_qmk_firmware(): """Look for qmk_firmware in the usual places. This function returns the path to qmk_firmware, or the default location if one does not exist. """ if in_qmk_firmware(): return in_qmk_firmware() if cli.config.user.qmk_home: return Path(cli.config.user.qmk_home).expanduser().resolve() if 'QMK_HOME' in os.environ: path = Path(os.environ['QMK_HOME']).expanduser() if path.exists(): return path.resolve() return path return Path.home() / 'qmk_firmware' def in_qmk_firmware(): """Returns the path to the qmk_firmware we are currently in, or None if we are not inside qmk_firmware. """ cur_dir = Path.cwd() while len(cur_dir.parents) > 0: if is_qmk_firmware(cur_dir): return cur_dir # Move up a directory before the next iteration cur_dir = cur_dir / '..' cur_dir = cur_dir.resolve() qmk_cli-1.1.6/qmk_cli/script_qmk.py000066400000000000000000000061611471033056000173060ustar00rootroot00000000000000#!/usr/bin/env python3 """CLI wrapper for running QMK commands. This program can be run from anywhere, with or without a qmk_firmware repository. If a qmk_firmware repository can be located we will use that to augment our available subcommands. """ import os import shlex import subprocess import sys from platform import platform from traceback import print_exc import milc from . import __version__ from .helpers import find_qmk_firmware, is_qmk_firmware milc.cli.milc_options(version=__version__) milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}' # These must happen after the milc.milc_options() call import milc.subcommand.config # noqa, must come after milc.milc_options() @milc.cli.entrypoint('CLI wrapper for running QMK commands.') def qmk_main(cli): """The function that gets run when there's no subcommand. """ cli.print_help() def run_cmd(*command): """Run a command in a subshell. """ if 'windows' in milc.cli.platform.lower(): safecmd = map(shlex.quote, command) safecmd = ' '.join(safecmd) command = [os.environ['SHELL'], '-c', safecmd] return subprocess.run(command) # Python setuptools entrypoint def main(): """Setup the environment before dispatching to the entrypoint. """ # Warn if they use an outdated python version if sys.version_info < (3, 9): print('Warning: Your Python version is out of date! Some subcommands may not work!') print('Please upgrade to Python 3.9 or later.') if 'windows' in platform().lower(): msystem = os.environ.get('MSYSTEM', '') if 'mingw64' not in sys.executable or 'MINGW64' not in msystem: print('ERROR: It seems you are not using the MINGW64 terminal.') print('Please close this terminal and open a new MSYS2 MinGW 64-bit terminal.') print('Python: %s, MSYSTEM: %s' % (sys.executable, msystem)) exit(1) # Environment setup qmk_firmware = find_qmk_firmware() os.environ['QMK_HOME'] = str(qmk_firmware) os.environ['ORIG_CWD'] = os.getcwd() import qmk_cli.subcommands # Check out and initialize the qmk_firmware environment if is_qmk_firmware(qmk_firmware): # All subcommands are run relative to the qmk_firmware root to make it easier to use the right copy of qmk_firmware. os.chdir(str(qmk_firmware)) sys.path.append(str(qmk_firmware / 'lib/python')) try: import qmk.cli # noqa except ImportError as e: if qmk_firmware.name != 'qmk_firmware': print('Warning: %s does not end in "qmk_firmware". Do you need to set QMK_HOME to "%s/qmk_firmware"?' % (qmk_firmware, qmk_firmware)) print('Error: %s: %s', (e.__class__.__name__, e)) print_exc() sys.exit(1) # Call the entrypoint return_code = milc.cli() if return_code is False: exit(1) elif return_code is not True and isinstance(return_code, int): if return_code < 0 or return_code > 255: milc.cli.log.error('Invalid return_code: %d', return_code) exit(255) exit(return_code) exit(0) qmk_cli-1.1.6/qmk_cli/subcommands/000077500000000000000000000000001471033056000170675ustar00rootroot00000000000000qmk_cli-1.1.6/qmk_cli/subcommands/__init__.py000066400000000000000000000004071471033056000212010ustar00rootroot00000000000000"""QMK CLI Subcommands We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup. """ from . import clone # noqa from . import console # noqa from . import env # noqa from . import setup # noqa qmk_cli-1.1.6/qmk_cli/subcommands/clone.py000066400000000000000000000024601471033056000205430ustar00rootroot00000000000000"""Clone a qmk_firmware fork. """ import os from pathlib import Path from milc import cli from qmk_cli.git import git_clone default_repo = 'qmk_firmware' default_fork = 'qmk/' + default_repo default_branch = 'master' @cli.argument('--baseurl', arg_only=True, default='https://github.com', help='The URL all git operations start from (Default: https://github.com)') @cli.argument('-b', '--branch', arg_only=True, default=default_branch, help='The branch to clone. Default: %s' % default_branch) @cli.argument('destination', arg_only=True, default=None, nargs='?', help='The directory to clone to. Default: (current directory)') @cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork) @cli.subcommand('Clone a qmk_firmware fork.') def clone(cli): if not cli.args.destination: cli.args.destination = os.path.join(os.environ['ORIG_CWD'], default_fork.split('/')[-1]) qmk_firmware = Path(cli.args.destination) git_url = '/'.join((cli.args.baseurl, cli.args.fork)) # Exists (but not an empty dir) if qmk_firmware.exists() and any(qmk_firmware.iterdir()): cli.log.error('Destination already exists: %s', cli.args.destination) exit(1) return git_clone(git_url, cli.args.destination, cli.args.branch) qmk_cli-1.1.6/qmk_cli/subcommands/console.py000066400000000000000000000325111471033056000211050ustar00rootroot00000000000000"""Acquire debugging information from usb hid devices cli implementation of https://www.pjrc.com/teensy/hid_listen.html """ from functools import lru_cache from pathlib import Path from platform import platform from threading import Thread from time import sleep, strftime from milc import cli from milc.questions import yesno LOG_COLOR = { 'next': 0, 'colors': [ '{fg_blue}', '{fg_cyan}', '{fg_green}', '{fg_magenta}', '{fg_red}', '{fg_yellow}', ], } KNOWN_BOOTLOADERS = { # VID , PID ('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader', ('03EB', '2067'): 'qmk-hid: HID Bootloader', ('03EB', '2FEF'): 'atmel-dfu: ATmega16U2', ('03EB', '2FF0'): 'atmel-dfu: ATmega32U2', ('03EB', '2FF3'): 'atmel-dfu: ATmega16U4', ('03EB', '2FF4'): 'atmel-dfu: ATmega32U4', ('03EB', '2FF9'): 'atmel-dfu: AT90USB64', ('03EB', '2FFA'): 'atmel-dfu: AT90USB162', ('03EB', '2FFB'): 'atmel-dfu: AT90USB128', ('03EB', '6124'): 'Microchip SAM-BA', ('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER', ('16C0', '05DC'): 'usbasploader: USBaspLoader', ('16C0', '05DF'): 'bootloadhid: HIDBoot', ('16C0', '0478'): 'halfkay: Teensy Halfkay', ('1B4F', '9203'): 'caterina: Pro Micro 3.3V', ('1B4F', '9205'): 'caterina: Pro Micro 5V', ('1B4F', '9207'): 'caterina: LilyPadUSB', ('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader', ('1EAF', '0003'): 'stm32duino: Maple 003', ('1FFB', '0101'): 'caterina: Pololu A-Star 32U4 Bootloader', ('2341', '0036'): 'caterina: Arduino Leonardo', ('2341', '0037'): 'caterina: Arduino Micro', ('239A', '000C'): 'caterina: Adafruit Feather 32U4', ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v', ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v', ('2A03', '0036'): 'caterina: Arduino Leonardo', ('2A03', '0037'): 'caterina: Arduino Micro', ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode', ('342D', 'DFA0'): 'wb32-dfu: WB32 Device in DFU Mode' } def install_deps(): """Install the necessary dependencies for qmk console. """ this_platform = platform().lower() if 'darwin' in this_platform or 'macos' in this_platform: command = ['brew', 'install', 'hidapi'] elif 'linux' in this_platform: command = ['sudo', 'apt', 'install', '-y', 'libhidapi-hidraw0', 'libusb-dev'] elif 'windows' in this_platform: command = ['pacboy', 'sync', '--needed', '--noconfirm', '--disable-download-timeout', 'hidapi:x'] else: cli.log.error('Unsupported platform: %s', this_platform) if yesno("Would you like to run `%s` to install the necessary package?", ' '.join(command)): cli.run(command, capture_output=False) return True @lru_cache(maxsize=0) def import_usb_core(): """Attempts to import the usb.core module. """ try: import usb.core return usb.core except ImportError as e: cli.log.error('Could not import usb.core: %s', e) if install_deps(): return import_usb_core() raise def import_hid(): """Attempts to import the hid module. """ try: import hid return hid except ImportError as e: cli.log.error('Could not import hid: %s', e) if install_deps(): return import_hid() raise class MonitorDevice(object): def __init__(self, hid_device, numeric): self.hid = import_hid() self.hid_device = hid_device self.numeric = numeric self.device = self.hid.Device(path=hid_device['path']) self.current_line = '' cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device) def read(self, size, encoding='ascii', timeout=1): """Read size bytes from the device. """ return self.device.read(size, timeout).decode(encoding) def read_line(self): """Read from the device's console until we get a \n. """ while '\n' not in self.current_line: self.current_line += self.read(32).replace('\x00', '') lines = self.current_line.split('\n', 1) self.current_line = lines[1] return lines[0] def run_forever(self): while True: try: message = {**self.hid_device, 'text': self.read_line()} identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string']) message['identifier'] = ':'.join(identifier) message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else '' cli.echo('%s', '%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message) except self.hid.HIDException: break class FindDevices(object): def __init__(self, vid, pid, index, numeric): self.hid = import_hid() self.vid = vid self.pid = pid self.index = index self.numeric = numeric def run_forever(self): """Process messages from our queue in a loop. """ live_devices = {} live_bootloaders = {} while True: try: for device in list(live_devices): if not live_devices[device]['thread'].is_alive(): cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device]) del live_devices[device] for device in self.find_devices(): if device['path'] not in live_devices: device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']] LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) live_devices[device['path']] = device try: monitor = MonitorDevice(device, self.numeric) device['thread'] = Thread(target=monitor.run_forever, daemon=True) device['thread'].start() except Exception as e: device['e'] = e device['e_name'] = e.__class__.__name__ cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all}): %(e_name)s: %(e)s", device) if cli.config.general.verbose: cli.log.exception(e) del live_devices[device['path']] if cli.args.bootloaders: for device in self.find_bootloaders(): if device.address in live_bootloaders: live_bootloaders[device.address]._qmk_found = True else: name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))] cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name) device._qmk_found = True live_bootloaders[device.address] = device for device in list(live_bootloaders): if live_bootloaders[device]._qmk_found: live_bootloaders[device]._qmk_found = False else: name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))] cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name) del live_bootloaders[device] sleep(.1) except KeyboardInterrupt: break def is_bootloader(self, hid_device): """Returns true if the device in question matches a known bootloader vid/pid. """ return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS def is_console_hid(self, hid_device): """Returns true when the usage page indicates it's a teensy-style console. """ return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074 def is_filtered_device(self, hid_device): """Returns True if the device should be included in the list of available consoles. """ return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid def find_devices_by_report(self, hid_devices): """Returns a list of available teensy-style consoles by doing a brute-force search. Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves. """ devices = [] for device in hid_devices: path = device['path'].decode('utf-8') if path.startswith('/dev/hidraw'): number = path[11:] report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor') if report.exists(): rp = report.read_bytes() if rp[1] == 0x31 and rp[3] == 0x09: devices.append(device) return devices def find_bootloaders(self): """Returns a list of available bootloader devices. """ import usb.core return list(filter(self.is_bootloader, usb.core.find(find_all=True))) def find_devices(self): """Returns a list of available teensy-style consoles. """ hid_devices = self.hid.enumerate() devices = list(filter(self.is_console_hid, hid_devices)) if not devices: devices = self.find_devices_by_report(hid_devices) if self.vid and self.pid: devices = list(filter(self.is_filtered_device, devices)) # Add index numbers device_index = {} for device in devices: id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id']))) if id not in device_index: device_index[id] = 0 device_index[id] += 1 device['index'] = device_index[id] return devices def int2hex(number): """Returns a string representation of the number as hex. """ return "%04X" % number def list_devices(device_finder): """Show the user a nicely formatted list of devices. """ devices = device_finder.find_devices() if devices: cli.log.info('Available devices:') for dev in devices: color = LOG_COLOR['colors'][LOG_COLOR['next']] LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string']) if cli.args.bootloaders: bootloaders = device_finder.find_bootloaders() if bootloaders: cli.log.info('Available Bootloaders:') for dev in bootloaders: cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))]) @cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.') @cli.argument('-d', '--device', help='Device to select - uses format :[:].') @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') @cli.argument('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.') @cli.argument('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.') @cli.argument('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)") @cli.subcommand('Acquire debugging information from usb hid devices.') def console(cli): """Acquire debugging information from usb hid devices """ vid = None pid = None index = 1 if cli.config.console.device: device = cli.config.console.device.split(':') if len(device) == 2: vid, pid = device elif len(device) == 3: vid, pid, index = device if not index.isdigit(): cli.log.error('Device index must be a number! Got "%s" instead.', index) exit(1) index = int(index) if index < 1: cli.log.error('Device index must be greater than 0! Got %s', index) exit(1) else: cli.log.error('Invalid format for device, expected ":[:]" but got "%s".', cli.config.console.device) cli.print_help() exit(1) vid = vid.upper() pid = pid.upper() device_finder = FindDevices(vid, pid, index, cli.args.numeric) if cli.args.list: return list_devices(device_finder) print('Looking for devices...', flush=True) device_finder.run_forever() qmk_cli-1.1.6/qmk_cli/subcommands/env.py000066400000000000000000000014741471033056000202370ustar00rootroot00000000000000"""Prints environment information. """ import os from pathlib import Path from milc import cli from qmk_cli.helpers import is_qmk_firmware @cli.argument('var', arg_only=True, default=None, nargs='?', help='Optional variable to query') @cli.subcommand('Prints environment information.') def env(cli): home = os.environ.get('QMK_HOME', "") data = { 'QMK_HOME': home, 'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else "" } # Now munge the current cli config for key, val in cli.config.general.items(): converted_key = 'QMK_' + key.upper() data[converted_key] = val if cli.args.var: # dump out requested arg print(data[cli.args.var]) else: # dump out everything for key, val in data.items(): print(f'{key}="{val}"') qmk_cli-1.1.6/qmk_cli/subcommands/setup.py000066400000000000000000000103601471033056000206010ustar00rootroot00000000000000"""Setup qmk_firmware on your computer. """ import os import shlex import subprocess import sys from pathlib import Path from milc import cli from milc.questions import yesno from qmk_cli.git import git_clone from qmk_cli.helpers import is_qmk_firmware default_base = 'https://github.com' default_repo = 'qmk_firmware' default_fork = 'qmk/' + default_repo default_branch = 'master' def git_upstream(destination): """Add the qmk/qmk_firmware upstream to a qmk_firmware clone. """ git_url = '/'.join((cli.args.baseurl, default_fork)) git_cmd = [ 'git', '-C', destination, 'remote', 'add', 'upstream', git_url, ] with subprocess.Popen(git_cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p: for line in p.stdout: print(line, end='') if p.returncode == 0: cli.log.info('Added %s as remote upstream.', git_url) return True else: cli.log.error('%s exited %d', ' '.join(git_cmd), p.returncode) return False @cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions') @cli.argument('-y', '--yes', arg_only=True, action='store_true', help='Answer yes to all questions') @cli.argument('--baseurl', arg_only=True, default=default_base, help='The URL all git operations start from. Default: %s' % default_base) @cli.argument('-b', '--branch', arg_only=True, default=default_branch, help='The branch to clone. Default: %s' % default_branch) @cli.argument('-H', '--home', arg_only=True, default=Path(os.environ['QMK_HOME']), type=Path, help='The location for QMK Firmware. Default: %s' % os.environ['QMK_HOME']) @cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork) @cli.subcommand('Setup your computer for qmk_firmware.') def setup(cli): """Guide the user through setting up their QMK environment. """ clone_prompt = 'Would you like to clone {fg_cyan}%s{fg_reset} to {fg_cyan}%s{fg_reset}?' % (cli.args.fork, shlex.quote(str(cli.args.home))) home_prompt = 'Would you like to set {fg_cyan}%s{fg_reset} as your QMK home?' % (shlex.quote(str(cli.args.home)),) # Sanity checks if cli.args.yes and cli.args.no: cli.log.error("Can't use both --yes and --no at the same time.") exit(1) # Check on qmk_firmware, and if it doesn't exist offer to check it out. if is_qmk_firmware(cli.args.home): cli.log.info('Found qmk_firmware at %s.', str(cli.args.home)) # Exists (but not an empty dir) elif cli.args.home.exists() and any(cli.args.home.iterdir()): path_str = str(cli.args.home) if cli.args.home.name != 'qmk_firmware': cli.log.warning('Warning: %s does not end in "qmk_firmware". Did you mean to use "--home %s/qmk_firmware"?' % (path_str, path_str)) cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str) exit(1) else: cli.log.error('Could not find qmk_firmware!') if yesno(clone_prompt): git_url = '/'.join((cli.args.baseurl, cli.args.fork)) if git_clone(git_url, cli.args.home, cli.args.branch): git_upstream(cli.args.home) else: exit(1) else: cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.') # Offer to set `user.qmk_home` for them. if str(cli.args.home) != os.environ['QMK_HOME'] and yesno(home_prompt): cli.config['user']['qmk_home'] = str(cli.args.home.absolute()) cli.config_source['user']['qmk_home'] = 'config_file' cli.write_config_option('user', 'qmk_home') # Run `qmk doctor` to check the rest of the environment out if cli.args.home.exists(): color = '--color' if cli.config.general.color else '--no-color' unicode = '--unicode' if cli.config.general.unicode else '--no-unicode' doctor_command = [Path(sys.argv[0]).as_posix(), color, unicode, 'doctor'] if cli.args.no: doctor_command.append('-n') if cli.args.yes: doctor_command.append('-y') cli.run(doctor_command, stdin=None, capture_output=False, cwd=cli.args.home) qmk_cli-1.1.6/release000077500000000000000000000002651471033056000145060ustar00rootroot00000000000000#!/bin/bash # Increment the verison number and release a new version of qmk_cli. # # Required packages: pip3 install bumpversion twine echo "Use the github action instead!" exit 1 qmk_cli-1.1.6/requirements-dev.txt000066400000000000000000000000411471033056000171700ustar00rootroot00000000000000build bumpversion requests twine qmk_cli-1.1.6/requirements.txt000066400000000000000000000000621471033056000164170ustar00rootroot00000000000000--index-url https://pypi.python.org/simple/ -e . qmk_cli-1.1.6/setup.cfg000066400000000000000000000060041471033056000147560ustar00rootroot00000000000000#[bumpversion] # Bumpversion config has been moved to .bumpversion.cfg [bdist_wheel] universal = 1 [flake8] ignore = E501,E226 [metadata] name = qmk version = 1.1.6 author = skullydazed author_email = skullydazed@gmail.com description = A program to help users work with QMK Firmware. long_description = file: README.md long_description_content_type = text/markdown license = MIT License project_urls = Bug Tracker = https://github.com/qmk/qmk_cli/issues Documentation = https://docs.qmk.fm/#/cli Homepage = https://qmk.fm/ Source = https://github.com/qmk/qmk_cli/ classifiers = Development Status :: 3 - Alpha Environment :: Console Intended Audience :: Developers Intended Audience :: System Administrators Intended Audience :: End Users/Desktop License :: OSI Approved :: MIT License Natural Language :: English Programming Language :: Python :: 3 :: Only Topic :: Scientific/Engineering Topic :: Software Development Topic :: Utilities [options] install_requires = hid milc>=1.9.0 pyusb setuptools>=45 # qmk_firmware packages dotty-dict hjson jsonschema>=4 pillow pygments pyserial packages = find: python_requires = >=3.9 [options.entry_points] console_scripts = qmk = qmk_cli.script_qmk:main [yapf] align_closing_bracket_with_visual_indent = True allow_multiline_dictionary_keys = False allow_multiline_lambdas = False allow_split_before_default_or_named_assigns = True allow_split_before_dict_value = True arithmetic_precedence_indication = True blank_lines_around_top_level_definition = 2 blank_line_before_class_docstring = False blank_line_before_module_docstring = False blank_line_before_nested_class_or_def = False coalesce_brackets = True column_limit = 256 continuation_align_style = SPACE continuation_indent_width = 4 dedent_closing_brackets = True disable_ending_comma_heuristic = False each_dict_entry_on_separate_line = True i18n_comment = i18n_function_call = indent_blank_lines = False indent_dictionary_value = True indent_width = 4 join_multiple_lines = False no_spaces_around_selected_binary_operators = spaces_around_default_or_named_assign = False spaces_around_power_operator = False spaces_before_comment = 2 space_between_ending_comma_and_closing_bracket = False split_all_comma_separated_values = False split_arguments_when_comma_terminated = True split_before_arithmetic_operator = False split_before_bitwise_operator = True split_before_closing_bracket = True split_before_dict_set_generator = True split_before_dot = False split_before_expression_after_opening_paren = False split_before_first_argument = False split_before_logical_operator = False split_before_named_assigns = True split_complex_comprehension = True split_penalty_after_opening_bracket = 300 split_penalty_after_unary_operator = 10000 split_penalty_arithmetic_operator = 300 split_penalty_before_if_expr = 0 split_penalty_bitwise_operator = 300 split_penalty_comprehension = 80 split_penalty_excess_character = 7000 split_penalty_for_added_line_split = 30 split_penalty_import_names = 0 split_penalty_logical_operator = 300 use_tabs = False qmk_cli-1.1.6/trigger_packages000077500000000000000000000023051471033056000163640ustar00rootroot00000000000000#!/usr/bin/env python3 """Trigger workflows that build QMK packages. """ from os import environ from pathlib import Path import requests # Pull in environment vars gh_user = environ.get('GH_USERNAME', 'qmk-bot') gh_pat = environ.get('QMK_BOT_TOKEN', '') gh_repo_owner = environ.get('REPO_OWNER', 'qmk') gh_repo_name = environ.get('REPO_NAME', 'qmk_fpm') gh_ref = environ.get('BRANCH_NAME', 'main') gh_workflow_ids = environ.get('WORKFLOW_IDS', 'all-trigger.yml').split(',') gh_api_url = environ.get('GITHUB_API_URL', 'https://api.github.com') gh_workflow_args = {'ref': gh_ref} gh_workflow_headers = {'Accept': 'application/vnd.github.v3+json'} for gh_workflow_id in gh_workflow_ids: gh_workflow_endpoint = f'{gh_api_url}/repos/{gh_repo_owner}/{gh_repo_name}/actions/workflows/{gh_workflow_id}/dispatches' print(f'Triggering {gh_workflow_id} workflow at: {gh_workflow_endpoint}') workflow_dispatch = requests.post(gh_workflow_endpoint, headers=gh_workflow_headers, json=gh_workflow_args, auth=(gh_user, gh_pat)) if workflow_dispatch.status_code != 204: print(f'Error from GitHub API, status_code {workflow_dispatch.status_code}!') print(workflow_dispatch.text) exit(1) exit(0)