diff --git a/.vscode/settings.json b/.vscode/settings.json index 43371ea..4197b65 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ + "canbus", + "canivore", "coppercore", "ctre", "dists", diff --git a/Pipfile b/Pipfile index 84cea24..4340dc8 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,8 @@ jinja2 = "*" robotvibecoder = {file = ".", editable = true} mypy = "*" pylint = "*" +pick = "*" +prompt-toolkit = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 1ba75e1..894e8d9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "65a62f41824012d6f47a74ba34783901c313cd621c506d82fb8991e523dfc0fe" + "sha256": "dbfed6ea191866e725c8f01e3307aed3875f497ee82733a96089e9b89bbcd8fc" }, "pipfile-spec": 6, "requires": { @@ -171,6 +171,15 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "pick": { + "hashes": [ + "sha256:2b07be18d16d655c7f491e1ecca7a29de3be85e1e000c8d46193672f14faa203", + "sha256:71f1b1b5d83652f87652fea5f51a3ba0b3388a71718cdcf8c6bc1326f85ae0b9" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.4.0" + }, "platformdirs": { "hashes": [ "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", @@ -179,6 +188,15 @@ "markers": "python_version >= '3.9'", "version": "==4.3.7" }, + "prompt-toolkit": { + "hashes": [ + "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", + "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.0.51" + }, "pylint": { "hashes": [ "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6", @@ -207,6 +225,35 @@ ], "markers": "python_version >= '3.8'", "version": "==4.13.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", + "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" + ], + "version": "==0.2.13" + }, + "windows-curses": { + "hashes": [ + "sha256:05d1ca01e5199a435ccb6c8c2978df4a169cdff1ec99ab15f11ded9de8e5be26", + "sha256:325439cd4f37897a1de8a9c068a5b4c432f9244bf9c855ee2fbeb3fa721a770c", + "sha256:4588213f7ef3b0c24c5cb9e309653d7a84c1792c707561e8b471d466ca79f2b8", + "sha256:4fa1a176bfcf098d0c9bb7bc03dce6e83a4257fc0c66ad721f5745ebf0c00746", + "sha256:53d711e07194d0d3ff7ceff29e0955b35479bc01465d46c3041de67b8141db2f", + "sha256:5c9c2635faf171a229caca80e1dd760ab00db078e2a285ba2f667bbfcc31777c", + "sha256:618e31458fedba2cf8105485ff00533ece780026c544142fc1647a20dc6c7641", + "sha256:6a5a831cabaadde41a6856fea5a0c68c74b7d11d332a816e5a5e6c84577aef3a", + "sha256:775a2e0fefeddfdb0e00b3fa6c4f21caf9982db34df30e4e62c49caaef7b5e56", + "sha256:8cf653f8928af19c103ae11cfed38124f418dcdd92643c4cd17239c0cec2f9da", + "sha256:a36b8fd4e410ddfb1a8eb65af2116c588e9f99b2ff3404412317440106755485", + "sha256:bdbe7d58747408aef8a9128b2654acf6fbd11c821b91224b9a046faba8c6b6ca", + "sha256:db776df70c10bd523c4a1ab0a7624a1d58c7d47f83ec49c6988f05bc1189e7b8", + "sha256:e61be805edc390ccfdeaf0e0c39736d931d3c4a007d6bf0f98d1e792ce437796", + "sha256:e9ce84559f80de7ec770d28c3b2991e0da51748def04e25a3c08ada727cfac2d", + "sha256:fd7d7a9cf6c1758f46ed76b8c67f608bc5fcd5f0ca91f1580fd2d84cf41c7f4f" + ], + "markers": "sys_platform == 'win32'", + "version": "==2.4.1" } }, "develop": {} diff --git a/pyproject.toml b/pyproject.toml index c2ba8c8..056ef26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = ["jinja2==3.1.6"] +dependencies = ["jinja2==3.1.6", "pick==2.4.0", "prompt_toolkit==3.0.51"] [project.urls] Documentation = "https://github.com/team401/robotvibecoder#readme" diff --git a/src/robotvibecoder/cli.py b/src/robotvibecoder/cli.py index bdb946e..7017e82 100644 --- a/src/robotvibecoder/cli.py +++ b/src/robotvibecoder/cli.py @@ -4,7 +4,8 @@ from dataclasses import dataclass import sys -from typing import TextIO +from typing import TextIO, List +import pick @dataclass @@ -28,7 +29,7 @@ def print_err(message: str, file: TextIO = sys.stderr): :param message: The error message. :type message: str :param file: File to print the error message to, defaults to sys.stderr - :type file: _type_, optional + :type file: TextIO, optional """ print(f"{Colors.fg_red}{Colors.bold}Error{Colors.reset}: {message}", file=file) @@ -39,6 +40,31 @@ def print_warning(message: str, file: TextIO = sys.stderr): :param message: The warning message. :type message: str :param file: File to print the warning message to, defaults to sys.stderr - :type file: _type_, optional + :type file: TextIO, optional """ print(f"{Colors.fg_red}{Colors.bold}Warning{Colors.reset}: {message}", file=file) + + +def rvc_pick(options: List[str], title: str, spoof_input: bool = True) -> str: + """Make the user select an option from options using arrow keys/enter + + This is a custom wrapper around the pick library. + + :param options: The options to select from + :type options: list[str] + :param title: The title message to print above the picker + :type title: str + :param spoof_input: Print a prompt with the selected option? Defaults to true. + :type spoof_input: bool, optional + """ + + selection, _ = pick.pick(options, title) + assert isinstance( + selection, str + ), """Pick selection wasn't a string. This is a robotvibecoder issue! + Please report this on github: https://github.com/team401/robotvibecoder""" + + if spoof_input: + print(f"{title}\n> {selection}") + + return selection diff --git a/src/robotvibecoder/subcommands/new.py b/src/robotvibecoder/subcommands/new.py index f3b25d5..d614fc7 100644 --- a/src/robotvibecoder/subcommands/new.py +++ b/src/robotvibecoder/subcommands/new.py @@ -6,12 +6,32 @@ import json import os import sys +from prompt_toolkit import prompt +from prompt_toolkit.completion import Completer, Completion from robotvibecoder import constants import robotvibecoder.cli from robotvibecoder.config import MechanismConfig, MechanismKind +class AppendCompleter(Completer): + """ + An auto-completer that adds a string to the end of the input text. + """ + + def __init__(self, text: str): + """ + Create an AppendCompleter + + :param text: The text to suggest completing + :type text: str + """ + self.text = text + + def get_completions(self, document, complete_event): + yield Completion(self.text, start_position=0) + + def new_config_interactive() -> MechanismConfig: """ Prompt the user for each field of a config to generate one interactively @@ -21,25 +41,22 @@ def new_config_interactive() -> MechanismConfig: "Interactively generating new config. Please enter each field and press [Enter]." ) print("Package: will come after frc.robot (e.g. `subsystems.scoring`)") - package: str = input("> ") + package: str = prompt("> ", default="subsystems.") print( "Name: should be capitalized and should not end in Mechanism or Subsystem, as this is automatically added" # pylint: disable=line-too-long ) - name: str = input("> ") - - print("Kind: Should be either 'Elevator', 'Arm', or 'Flywheel'") - kind: str = "" - while MechanismKind.try_into(kind) is None: - kind = input("> ") - if MechanismKind.try_into(kind) is None: - print("Please input either Elevator, Arm, or Flywheel.") + name: str = prompt("> ") + + kind_choices = ["Elevator", "Arm", "Flywheel"] + kind: str = robotvibecoder.cli.rvc_pick(kind_choices, "Mechanism Kind:") + kind_try = MechanismKind.try_into(kind) kind_enum: MechanismKind = ( kind_try if kind_try is not None else MechanismKind.ELEVATOR ) print("CAN Bus: whatever the name of the mechanism's bus is (e.g. `canivore`)") - canbus: str = input("> ") + canbus: str = prompt("> ", default="canivore") num_motors: int = -1 while num_motors == -1: @@ -54,20 +71,22 @@ def new_config_interactive() -> MechanismConfig: motors: list[str] = [] for i in range(num_motors): motors.append( - input(f"Motor {i + 1} name: a camelcase motor name (e.g. leadMotor)\n> ") + prompt( + f"Motor {i + 1} name: a camelcase motor name (e.g. leadMotor)\n> ", + completer=AppendCompleter("Motor"), + complete_while_typing=True, + ) ) - lead_motor: str = "" - - while lead_motor not in motors: - lead_motor = input("Lead motor (must be one of previously defined motors)\n> ") - - if lead_motor not in motors: - print("Please select one of the previously defined motors:") - for motor in motors: - print(f" - {motor}") + lead_motor: str = robotvibecoder.cli.rvc_pick( + motors, "Lead Motor: (Up/Down/K/J to move, Enter to select)" + ) - encoder: str = input("Encoder name: a camelcase encoder name (e.g. armEncoder)\n> ") + encoder: str = prompt( + "Encoder name: a camelcase encoder name (e.g. armEncoder)\n> ", + completer=AppendCompleter("Encoder"), + complete_while_typing=True, + ) return MechanismConfig( package, name, kind_enum, canbus, motors, lead_motor, encoder