/
Vimgolf3a7491d
"""
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 ("<left>" -> "\<left>")
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()