From b39528af75338d0b35c8c0d16fe5cba44333d026 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 13 Aug 2022 12:07:48 -0400 Subject: [PATCH] Fixed non-funtional code, incorporated new file --- src/tfscript/__init__.py | 66 ++++++++--------- src/tfscript/cli.py | 151 ++++++++++++++------------------------- src/tfscript/verify.py | 110 ++++++++++++++++++++++------ src/tfscript/writing.py | 82 +++++++++++++++++++++ 4 files changed, 257 insertions(+), 152 deletions(-) create mode 100644 src/tfscript/writing.py diff --git a/src/tfscript/__init__.py b/src/tfscript/__init__.py index 225a0b1..33c740f 100644 --- a/src/tfscript/__init__.py +++ b/src/tfscript/__init__.py @@ -123,16 +123,16 @@ def simpleHold(key, instruction): return f'bind {key} "{instruction}"\n' def listHold(key, options): - press_str = options["press"] - if isinstance(press_str, list): - press_str = ';'.join(press_str) + pressStr = options["press"] + if isinstance(pressStr, list): + pressStr = ';'.join(pressStr) - release_str = options["release"] - if isinstance(release_str, list): - release_str = ';'.join(release_str) + releaseStr = options["release"] + if isinstance(releaseStr, list): + releaseStr = ';'.join(releaseStr) - ret = f'alias +{key}_bind "{press_str}"\n' +\ - f'alias -{key}_bind "{release_str}"\n' +\ + ret = f'alias +{key}_bind "{pressStr}"\n' +\ + f'alias -{key}_bind "{releaseStr}"\n' +\ f'bind {key} "+{key}_bind"\n' return ret @@ -148,29 +148,29 @@ def toggle(key, instruction): return ret def double(key, options): - prim_action = options["primary"] - pBindType = firstTypeIn(prim_action.keys()) - pBindContent = prim_action[pBindType] + primaryAction = options["primary"] + pBindType = firstTypeIn(primaryAction.keys()) + pBindContent = primaryAction[pBindType] - sec_action = options["secondary"] - sBindType = firstTypeIn(sec_action.keys()) - sBindContent = sec_action[sBindType] + secAction = options["secondary"] + sBindType = firstTypeIn(secAction.keys()) + sBindContent = secAction[sBindType] - main_str = f'{key}_main' - alt_str = f'{key}_alt' - tog_str = f'toggle_{key}' + mainStr = f'{key}_main' + altStr = f'{key}_alt' + togStr = f'toggle_{key}' - recursive_code = branch(main_str, pBindContent, pBindType) +\ - branch(alt_str, sBindContent, sBindType) + recursiveCode = branch(mainStr, pBindContent, pBindType) +\ + branch(altStr, sBindContent, sBindType) newcode = [] - for line in recursive_code.split('\n'): + for line in recursiveCode.split('\n'): # For every line gotten by the recursive call, change all "bind"s to "alias", - # since main_str and alt_str aren't valid bind targes + # since mainStr and altStr aren't valid bind targes llist = line.split(' ') for i in range(len(llist)): - alphanum_chars = ''.join(c for c in llist[i] if c.isalnum()) - if alphanum_chars == 'bind': + alphanumChars = ''.join(c for c in llist[i] if c.isalnum()) + if alphanumChars == 'bind': if llist[i][0].isalnum(): llist[i] = 'alias' else: @@ -180,23 +180,23 @@ def double(key, options): newcode.append(' '.join(llist)) ret = '\n'.join(newcode) +\ - f'alias +{tog_str} "bind {key} {alt_str}"\n' +\ - f'alias -{tog_str} "bind {key} {main_str}"\n'+\ - f'bind {key} "{main_str}"\n' + f'alias +{togStr} "bind {key} {altStr}"\n' +\ + f'alias -{togStr} "bind {key} {mainStr}"\n'+\ + f'bind {key} "{mainStr}"\n' - cond_name = options["condition"] + condName = options["condition"] - if cond_name in condDict: + if condName in condDict: # If the condition key (like "mouse4") already has toggles, # just append another toggle string (it gets encased in quotes later) - condDict[cond_name]["plus_toggles"] += f"; +{tog_str}" - condDict[cond_name]["minus_toggles"] += f"; -{tog_str}" + condDict[condName]["plus_toggles"] += f"; +{togStr}" + condDict[condName]["minus_toggles"] += f"; -{togStr}" else: # If the condition key doesn't already exist, make it with the correct values condDict.update( { - cond_name: { - "plus_toggles": f"+{tog_str}", - "minus_toggles": f"-{tog_str}" + condName: { + "plus_toggles": f"+{togStr}", + "minus_toggles": f"-{togStr}" } } ) diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index 7c8a23a..a0d533d 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -15,7 +15,7 @@ import os import argparse from tempfile import NamedTemporaryFile import yaml -from platform import system as OSName, OSRelease +from platform import system as GetOSName, release as GetOSRelease try: import winreg @@ -26,79 +26,25 @@ except ModuleNotFoundError: # Local libraries import tfscript from tfscript import verify +from tfscript import writing args = {} targetDir = "" -def parseFile(inputFile): +def parseFile(inputFile) -> (dict, dict): """Parse, verify, and do the conversion.""" config = yaml.safe_load(inputFile) # See verify.py - config = tfscript.verify.verifyConfig(config) + config, aliases = verify.verifyConfig(config) if "errors" in config: for e in config["errors"]: - print(e,file=sys.stderr) + print(e, file=sys.stderr) + return None, None else: - parseConfig(config) + return config, aliases -def writeOutput(data, className) -> dict: - """ - Write `data' to various files as needed, returning a dict of - the temporary file names and their target destination names, - not including the target directory - """ - global args - namesDict = {} # return dict - - # Variables - lineList = [ l.encode('utf8') for l in data.splitlines() ] - fileNum = 1 - bytesWritten = 0 - - # Constants - maxFileSize = 2 ** 20 # 1MiB maximum cfg file size - filesNeeded = 1 + int( len(data)/maxFileSize ) - if args.debug: - print( f'DEBUG: need {filesNeeded} files for {className}', file=sys.stderr) - - FilNedLen = len(str(filesNeeded)) - reservedSpace = len(f'{className}_script_{filesNeeded}.cfg') + 4 - - # Initialize variables - outfile = NamedTemporaryFile(prefix=className, delete=False) - # I know % formatting is old-school and pylint hates it, - # but "%*d" is the easiest way to left-pad with zeros - # without hardcoding a number. The extra 4 bytes is just some leeway - namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) }) - - while (fileNum <= filesNeeded and len(lineList) > 0): - line = lineList.pop(0) + '\n'.encode('utf8') - lineLen = len(line) # nice - - if bytesWritten + reservedSpace + lineLen > maxFileSize: - outfile.write( ('exec %s_script_%0*d' % (className, FilNedLen, fileNum+1)).encode('utf8') ) - bytesWritten += reservedSpace - if args.debug: - print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr) - - outfile.close() - outfile = NamedTemporaryFile(prefix=className, delete=False) - - fileNum += 1 - namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) }) - bytesWritten = 0 - - outfile.write(line) - bytesWritten += lineLen - - outfile.close() # the most-recent tempfile will not have been closed - if args.debug: - print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr) - - return namesDict - -def parseConfig(config): +def parseConfig(config, defaults): """With validated data structure, write out all the files.""" global args global targetDir @@ -110,20 +56,18 @@ def parseConfig(config): tempsAndReals = {} + if defaults is not None: + stringToWrite = tfscript.makeCFG(defaults) + replaceDict = writing.writeOutput(stringToWrite, "default", args) + tempsAndReals.update(replaceDict) + for currentClass in config: classDict = config[currentClass] stringToWrite = tfscript.makeCFG(classDict) - replaceDict = writeOutput(stringToWrite, currentClass) + replaceDict = writing.writeOutput(stringToWrite, currentClass, args) tempsAndReals.update(replaceDict) - for tmpName, realName in tempsAndReals.items(): - if args.dry_run: - if args.debug: - print( f'DEBUG: {tmpName} would be {targetDir}/{realName}.cfg', file=sys.stderr) - else: - os.replace( tmpName, f'{targetDir}/{realName}' ) - if args.debug: - print( f'DEBUG: Created {targetDir}/{realName}', file=sys.stderr) + return tempsAndReals def parseCLI(): # Handle command line @@ -143,7 +87,33 @@ def parseCLI(): help='File containing YAML to convert.') return parser -def main(): +def getTargetDir(systemName): + if systemName == "Darwin": + if float( '.'.join( GetOSRelease().split('.')[0:2] ) ) >= 10.15: + if not args.force: + print( + "As of macOS Catalina (v10.15), 32-bit applications " + "such as tf2 do not work, so tfscript does not function", + file=sys.stderr + ) + else: + return os.path.expanduser("~/Library/Application Support/Steam/") + + elif systemName == "Windows": + # oh god why do we have to use the registry + accessReg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + accessKey = winreg.OpenKey(accessReg, "SOFTWARE\\WOW6432Node\\Valve\\Steam\\") + return winreg.QueryValue(accessKey, "InstallPath") + + elif systemName == "Linux": + return os.path.expanduser("~/.local/Steam") + + elif systemName == "Java": + print("Java-based OSes are not supported yet by tfscript.", file=sys.stderr) + + return None + +def main() -> int: """ Command line interface. """ global args global targetDir @@ -153,31 +123,9 @@ def main(): if args.directory is not None: targetDir = args.directory else: - systemName = OSName() - if systemName == "Darwin": - if float( '.'.join(OSRelease().split('.')[0:2]) ) >= 10.15: - if not args.force: - print( - "As of macOS Catalina (v10.15), 32-bit applications " - "such as tf2 do not work, so tfscript does not function", - file=sys.stderr - ) - else: - targetDir = os.path.expanduser("~/Library/Application Support/Steam/") - - elif systemName == "Windows": - # oh god why do we have to use the registry - accessReg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - accessKey = winreg.OpenKey(accessReg, "SOFTWARE\\WOW6432Node\\Valve\\Steam\\") - targetDir = winreg.QueryValue(accessKey, "InstallPath") - - elif systemName == "Linux": - targetDir = os.path.expanduser("~/.local/Steam") - - elif systemName == "Java": - print("Java-based OSes are not supported yet by tfscript.", file=sys.stderr) - - if targetDir != "": + systemName = GetOSName() + targetDir = getTargetDir(systemName) + if targetDir is not None: # Supported OS: add steamapps path if targetDir[-1] != '/': targetDir += '/' @@ -191,7 +139,14 @@ def main(): # Unsupported OS and not forced to continue return 2 - parseFile(args.infile) + config, defaults = parseFile(args.infile) + if config is None: + return 2 + + fileNames = parseConfig(config, defaults) + fileList = writing.replaceFiles(targetDir, fileNames, args) + # writing.appendToActuals(targetDir, fileList) + return 0 if __name__ == "__main__": diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 9f209f2..ceb8e41 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -1,34 +1,84 @@ """Verify all the things that could go wrong.""" -def verifyConfig(cfg: dict): +def verifyConfig(cfg: dict) -> (dict, dict): + verifiedConfig = {} - # Do aliases first + # Do defaults first aliasErrors = [] - aliases = None + defaults = None - if "aliases" in cfg: - aliases = cfg.pop("aliases") - for key, data in aliases.items(): - errMessages = validBind(key, data, alias = True) + if "default" in cfg: + defaults = cfg.pop("default") + for key, data in defaults.items(): + isAlias = ("alias" in data and data["alias"] == True) + errMessages = validBind(key, data, alias = isAlias) if len(errMessages) > 0: for msg in errMessages: - aliasErrors.append(f"Error in aliases: {msg}") + aliasErrors.append(f"Error in defaults: {msg}") - errors = [] + classList = [ + "scout", + "soldier", + "pyro", + ("demo","demoman"), + ("engi","engineer"), + ("heavy","heavyweapons"), + "medic", + "sniper", + "spy" + ] - for cclass in cfg: - for key, data in cfg[cclass].items(): - errMessages = validBind(key, data) + errors = aliasErrors.copy() + + + for cclass in classList: + classCFG = None + className = cclass + if isinstance(cclass, str) and cclass in cfg: + classCFG = cfg.pop(cclass) + elif isinstance(cclass, tuple): + for tupClass in cclass: + if tupClass in cfg: + classCFG = cfg.pop(tupClass) + className = cclass[0] + break + if classCFG is None: + # Invalid class, this gets caught later. + # It may be less efficient this way, but + # it makes for more descriptive error messages + continue + for key, data in classCFG.items(): + errMessages = [] + isAlias = False + if "alias" in data: + isAlias = data["alias"] + if not isinstance(isAlias, bool): + errMessages.append(f'Key "{key}" has alias not set to true or false. Did you accidentally put it in quotes?') + errMessages.extend( validBind(key, data, alias = isAlias) ) if len(errMessages) > 0: for msg in errMessages: errors.append(f"Error in {cclass}: {msg}") + verifiedConfig.update({className: classCFG}) - errors += aliasErrors + # Turn list into only strings by expanding tuples + for i, clss in enumerate(classList): + if isinstance(clss, tuple): + classList.insert(i+1, clss[0]) + classList.insert(i+1, clss[1]) + classList.pop(i) + + for remainingClass in cfg: + if remainingClass not in classList: + errors.append(f'Error in {remainingClass}: "{remainingClass}" is not a valid class') + else: + otherName = findTwin(remainingClass) + if otherName is not None: + errors.append(f'Error in {remainingClass}: conflicting names for section: "{remainingClass}" and "{otherName}"') if len(errors) > 0: - cfg.update({"errors": errors}) + verifiedConfig.update({"errors": errors}) - return cfg + return verifiedConfig, defaults def validBind(key, data, alias = False) -> list: """Check for valid key and valid binding""" @@ -97,7 +147,7 @@ def removeRelaventFields(data, bindType): # These types are simple, just the bind type and argument if bindType in ["impulse", "toggle"]: data.pop(bindType) - + elif bindType == "hold": content = data.pop("hold") if isinstance(content, dict): @@ -152,10 +202,28 @@ def removeRelaventFields(data, bindType): if "interval" not in content: errMsgs.append("Repeat requires interval") else: - interval = content["interval"] - if interval < 0: - errMsgs.append("Repeat interval cannot be negative") - if (unit == "t" and not isinstance(interval, int)): - errMsgs.append("Repeat interval must be integer if unit is ticks") + intervalStr = content["interval"] + if unit == "s": + interval = float(intervalStr) + else: + interval = int(intervalStr) + if interval <= 0: + errMsgs.append("Repeat interval must be positive") + elif interval <= 200/6: + errMsgs.append(f"Repeat interval must be greater than 1 tick (approx. {200/6:.3f}s)") return data, errMsgs + +def findTwin(className): + classDict = { + "demo": "demoman", + "engi": "engineer", + "heavy": "heavyweapons" + } + for className1, className2 in classDict.items(): + if className == className1: + return className2 + elif className == className2: + return className1 + + return None \ No newline at end of file diff --git a/src/tfscript/writing.py b/src/tfscript/writing.py new file mode 100644 index 0000000..c704061 --- /dev/null +++ b/src/tfscript/writing.py @@ -0,0 +1,82 @@ +import os +from os.path import exists +from tempfile import NamedTemporaryFile + +def writeOutput(data, className, args) -> dict: + """ + Write `data' to various files as needed, returning a dict of + the temporary file names and their target destination names, + not including the target directory + """ + namesDict = {} # return dict + + # Variables + lineList = [ l.encode('utf8') for l in data.splitlines() ] + fileNum = 1 + bytesWritten = 0 + + # Constants + maxFileSize = 2 ** 20 # 1MiB maximum cfg file size + filesNeeded = 1 + int( len(data)/maxFileSize ) + if args.debug: + print( f'DEBUG: need {filesNeeded} files for {className}', file=sys.stderr) + + FilNedLen = len(str(filesNeeded)) + reservedSpace = len(f'{className}_script_{filesNeeded}.cfg') + 4 + + # Initialize variables + outfile = NamedTemporaryFile(prefix=className, delete=False) + # I know % formatting is old-school and pylint hates it, + # but "%*d" is the easiest way to left-pad with zeros + # without hardcoding a number. The extra 4 bytes is just some leeway + namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) }) + + while (fileNum <= filesNeeded and len(lineList) > 0): + line = lineList.pop(0) + '\n'.encode('utf8') + lineLen = len(line) # nice + + if bytesWritten + reservedSpace + lineLen > maxFileSize: + outfile.write( ('exec %s_script_%0*d' % (className, FilNedLen, fileNum+1)).encode('utf8') ) + bytesWritten += reservedSpace + if args.debug: + print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr) + + outfile.close() + outfile = NamedTemporaryFile(prefix=className, delete=False) + + fileNum += 1 + namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) }) + bytesWritten = 0 + + outfile.write(line) + bytesWritten += lineLen + + outfile.close() # the most-recent tempfile will not have been closed + if args.debug: + print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr) + + return namesDict + +def replaceFiles(targetDir, fileNames, args): + for tmpName, realName in fileNames.items(): + if args.dry_run: + if args.debug: + print( f'DEBUG: {tmpName} would be {targetDir}/{realName}.cfg', file=sys.stderr) + else: + os.replace( tmpName, f'{targetDir}/{realName}' ) + if args.debug: + print( f'DEBUG: Created {targetDir}/{realName}', file=sys.stderr) + + return fileNames.values() + +def execStrInFile(fileName, f): + execStr = f'exec {fileName}' + +def appendToActuals(targetDir, fileList): + for currFile in fileList: + cfgName = targetDir + '/' + open(currFile.split('_')[0] + '.cfg', 'r') + if exists(cfgName): + if execStrInFile(currFile, cfgFile): + pass + else: + pass \ No newline at end of file