""" Vimgolf API wrapper """ # TODO: convert vimgolf keys to terminal input string, and convert terminal input string to vimgolf keys # TODO: figure out if the vim -w recorded things are the same as terminal input string # TODO: figure out the action space of all three input representations import argparse import json import logging import typing from vimgolf_gym._vimrc import ( _CYBERGOD_VIMGOLF_VIMRC_FILEPATH, _prepare_cybergod_vimrc_with_buffer_file, _VIMGOLF_VIMRC_FILEPATH, ) from vimgolf.vimgolf import ( IGNORED_KEYSTROKES, Challenge, Result, Status, VimRunner, filecmp, format_, get_challenge_url, get_keycode_repr, input_loop, logger, os, parse_keycodes, sys, tempfile, tokenize_keycode_reprs, upload_result, working_directory, write, ) def parse_commandline_arguments(): """ Parse the command line arguments using argparse. Args: None Returns: A Namespace object with the parsed arguments. The parsed arguments are: - --input_file: str, the path to the input file - --output_file: str, the path to the output file - --log_file: str, the path to the log file - --buffer_file: str, the path to the buffer file - --init_keys: str, the initial keys to type into Vim """ parser = argparse.ArgumentParser() parser.add_argument("--input_file", type=str, required=True) parser.add_argument("--output_file", type=str, required=True) parser.add_argument("--log_file", type=str, required=True) parser.add_argument("--buffer_file", type=str, default=None) parser.add_argument("--init_keys", type=str, default="") args = parser.parse_args() return args def main(): """ Parse command line arguments and execute the vimgolf local challenge. It sets up the logging to a file and then calls the local function to execute the challenge. The local function is called with the input file, output file, buffer file and init keys. """ args = parse_commandline_arguments() log_file = args.log_file file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.DEBUG) logger.addHandler(file_handler) if args.buffer_file: local( infile=args.input_file, outfile=args.output_file, buffer_file=args.buffer_file, init_keys=args.init_keys, ) else: local( infile=args.input_file, outfile=args.output_file, init_keys=args.init_keys, ) def local( infile: str, outfile: str, buffer_file: typing.Optional[str] = None, init_keys="" ): """ Execute a local VimGolf challenge. It reads the input file, output file, and runs the challenge. The challenge is defined by the input and output text and the file extensions. Args: infile: str, the path to the input file outfile: str, the path to the output file init_keys: str, the initial keys to type into Vim buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. Returns: Status: The status of the challenge """ logger.info("local(%s, %s)", infile, outfile) with open(infile, "r") as f: in_text = format_(f.read()) with open(outfile, "r") as f: out_text = format_(f.read()) _, in_extension = os.path.splitext(infile) _, out_extension = os.path.splitext(outfile) challenge = Challenge( in_text=in_text, out_text=out_text, in_extension=in_extension, out_extension=out_extension, id=None, compliant=None, api_key=None, init_keys=init_keys, ) status = play(challenge, buffer_file=buffer_file) return status def play(challenge, results=None, buffer_file: typing.Optional[str] = None): """ Execute a VimGolf challenge. Args: challenge: Challenge, the challenge to play results: list of Result, the results of previous plays buffer_file: typing.Optional[str]: Where to write the vim editor buffer. Defaults to None. Returns: Status: The status of the challenge """ if results is None: results = [] logger.info("play(...)") with tempfile.TemporaryDirectory() as workspace, working_directory(workspace): infile = "in" if challenge.in_extension: infile += challenge.in_extension outfile = "out" if challenge.out_extension: outfile += challenge.out_extension logfile = "log" with open(outfile, "w") as f: f.write(challenge.out_text) try: # If there were init keys specified, we need to convert them to a # form suitable for feedkeys(). We can't use Vim's -s option since # it takes escape codes, not key codes. See Vim #4041 and TODO.txt # ("Bug: script written with "-W scriptout" contains Key codes, # while the script read with "-s scriptin" expects escape codes"). # The conversion is conducted here so that we can fail fast on # error (prior to playing) and to avoid repeated computation. keycode_reprs = tokenize_keycode_reprs(challenge.init_keys) init_feedkeys = [] for item in keycode_reprs: if item == "\\": item = "\\\\" # Replace '\' with '\\' elif item == '"': item = '\\"' # Replace '"' with '\"' elif item.startswith("<") and item.endswith(">"): item = "\\" + item # Escape special keys ("" -> "\") init_feedkeys.append(item) init_feedkeys = "".join(init_feedkeys) except Exception: logger.exception("invalid init keys") write("Invalid keys: {}".format(challenge.init_keys), color="red") return Status.FAILURE write("Launching vimgolf session", color="yellow") while True: with open(infile, "w") as f: f.write(challenge.in_text) with open(outfile, "w") as f: f.write(challenge.out_text) if buffer_file: _prepare_cybergod_vimrc_with_buffer_file(buffer_file) vimrc = _CYBERGOD_VIMGOLF_VIMRC_FILEPATH else: vimrc = _VIMGOLF_VIMRC_FILEPATH play_args = [ "-Z", # restricted mode, utilities not allowed "-n", # no swap file, memory only editing "--noplugin", # no plugins "-i", "NONE", # don't load .viminfo (e.g., has saved macros, etc.) "+0", # start on line 0 "-u", vimrc, # vimgolf .vimrc "-U", "NONE", # don't load .gvimrc "-W", logfile, # keylog file (overwrites existing) '+call feedkeys("{}", "t")'.format(init_feedkeys), # initial keys infile, ] if VimRunner.run(play_args) != Status.SUCCESS: return Status.FAILURE correct = filecmp.cmp(infile, outfile, shallow=False) logger.info("correct: %s", str(correct).lower()) with open(logfile, "rb") as _f: # raw keypress representation saved by vim's -w raw_keys = _f.read() # list of parsed keycode byte strings keycodes = parse_keycodes(raw_keys) keycodes = [ keycode for keycode in keycodes if keycode not in IGNORED_KEYSTROKES ] # list of human-readable key strings keycode_reprs = [get_keycode_repr(keycode) for keycode in keycodes] logger.info("keys: %s", "".join(keycode_reprs)) score = len(keycodes) logger.info("score: %d", score) result = Result(correct=correct, keys="".join(keycode_reprs), score=score) logger.info( json.dumps( dict( event_type="vimgolf_result", event_data=dict( correct=correct, keys="".join(keycode_reprs), score=score ), ) ) ) results.append(result) write("Here are your keystrokes:", color="green") for keycode_repr in keycode_reprs: color = "magenta" if len(keycode_repr) > 1 else None write(keycode_repr, color=color, end=None) write("") if correct: write("Success! Your output matches.", color="green") write("Your score:", color="green") else: write( "Uh oh, looks like your entry does not match the desired output.", color="red", ) write("Your score for this failed attempt:", color="red") write(score) upload_eligible = challenge.id and challenge.compliant and challenge.api_key while True: # Generate the menu items inside the loop since it can change across iterations # (e.g., upload option can be removed) with open(infile, "w") as f: f.write(challenge.in_text) with open(outfile, "w") as f: f.write(challenge.out_text) menu = [] if not correct: menu.append(("d", "Show diff")) if upload_eligible and correct: menu.append(("w", "Upload result")) menu.append(("r", "Retry the current challenge")) menu.append(("q", "Quit vimgolf")) valid_codes = [x[0] for x in menu] for option in menu: write("[{}] {}".format(*option), color="yellow") selection = input_loop("Choice> ") if selection not in valid_codes: write( "Invalid selection: {}".format(selection), stream=sys.stderr, color="red", ) elif selection == "d": # diffsplit is used instead of 'vim -d' to avoid the "2 files to edit" message. diff_args = [ "-n", outfile, "-c", "vertical diffsplit {}".format(infile), ] if VimRunner.run(diff_args) != Status.SUCCESS: return Status.FAILURE elif selection == "w": upload_status = upload_result( challenge.id, challenge.api_key, raw_keys ) if upload_status == Status.SUCCESS: write("Uploaded entry!", color="green") leaderboard_url = get_challenge_url(challenge.id) write( "View the leaderboard: {}".format(leaderboard_url), color="green", ) upload_eligible = False else: write( "The entry upload has failed", stream=sys.stderr, color="red", ) message = "Please check your API key on vimgolf.com" write(message, stream=sys.stderr, color="red") else: break if selection == "q": break write("Retrying vimgolf challenge", color="yellow") write("Thanks for playing!", color="green") return Status.SUCCESS if __name__ == "__main__": main()