Fixed non-funtional code, incorporated new file

pull/11/head
Nicholas Hope 2022-08-13 12:07:48 -04:00
parent cb3c248335
commit b39528af75
4 changed files with 257 additions and 152 deletions

View File

@ -123,16 +123,16 @@ def simpleHold(key, instruction):
return f'bind {key} "{instruction}"\n' return f'bind {key} "{instruction}"\n'
def listHold(key, options): def listHold(key, options):
press_str = options["press"] pressStr = options["press"]
if isinstance(press_str, list): if isinstance(pressStr, list):
press_str = ';'.join(press_str) pressStr = ';'.join(pressStr)
release_str = options["release"] releaseStr = options["release"]
if isinstance(release_str, list): if isinstance(releaseStr, list):
release_str = ';'.join(release_str) releaseStr = ';'.join(releaseStr)
ret = f'alias +{key}_bind "{press_str}"\n' +\ ret = f'alias +{key}_bind "{pressStr}"\n' +\
f'alias -{key}_bind "{release_str}"\n' +\ f'alias -{key}_bind "{releaseStr}"\n' +\
f'bind {key} "+{key}_bind"\n' f'bind {key} "+{key}_bind"\n'
return ret return ret
@ -148,29 +148,29 @@ def toggle(key, instruction):
return ret return ret
def double(key, options): def double(key, options):
prim_action = options["primary"] primaryAction = options["primary"]
pBindType = firstTypeIn(prim_action.keys()) pBindType = firstTypeIn(primaryAction.keys())
pBindContent = prim_action[pBindType] pBindContent = primaryAction[pBindType]
sec_action = options["secondary"] secAction = options["secondary"]
sBindType = firstTypeIn(sec_action.keys()) sBindType = firstTypeIn(secAction.keys())
sBindContent = sec_action[sBindType] sBindContent = secAction[sBindType]
main_str = f'{key}_main' mainStr = f'{key}_main'
alt_str = f'{key}_alt' altStr = f'{key}_alt'
tog_str = f'toggle_{key}' togStr = f'toggle_{key}'
recursive_code = branch(main_str, pBindContent, pBindType) +\ recursiveCode = branch(mainStr, pBindContent, pBindType) +\
branch(alt_str, sBindContent, sBindType) branch(altStr, sBindContent, sBindType)
newcode = [] 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", # 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(' ') llist = line.split(' ')
for i in range(len(llist)): for i in range(len(llist)):
alphanum_chars = ''.join(c for c in llist[i] if c.isalnum()) alphanumChars = ''.join(c for c in llist[i] if c.isalnum())
if alphanum_chars == 'bind': if alphanumChars == 'bind':
if llist[i][0].isalnum(): if llist[i][0].isalnum():
llist[i] = 'alias' llist[i] = 'alias'
else: else:
@ -180,23 +180,23 @@ def double(key, options):
newcode.append(' '.join(llist)) newcode.append(' '.join(llist))
ret = '\n'.join(newcode) +\ ret = '\n'.join(newcode) +\
f'alias +{tog_str} "bind {key} {alt_str}"\n' +\ f'alias +{togStr} "bind {key} {altStr}"\n' +\
f'alias -{tog_str} "bind {key} {main_str}"\n'+\ f'alias -{togStr} "bind {key} {mainStr}"\n'+\
f'bind {key} "{main_str}"\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, # If the condition key (like "mouse4") already has toggles,
# just append another toggle string (it gets encased in quotes later) # just append another toggle string (it gets encased in quotes later)
condDict[cond_name]["plus_toggles"] += f"; +{tog_str}" condDict[condName]["plus_toggles"] += f"; +{togStr}"
condDict[cond_name]["minus_toggles"] += f"; -{tog_str}" condDict[condName]["minus_toggles"] += f"; -{togStr}"
else: else:
# If the condition key doesn't already exist, make it with the correct values # If the condition key doesn't already exist, make it with the correct values
condDict.update( { condDict.update( {
cond_name: { condName: {
"plus_toggles": f"+{tog_str}", "plus_toggles": f"+{togStr}",
"minus_toggles": f"-{tog_str}" "minus_toggles": f"-{togStr}"
} }
} ) } )

View File

@ -15,7 +15,7 @@ import os
import argparse import argparse
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import yaml import yaml
from platform import system as OSName, OSRelease from platform import system as GetOSName, release as GetOSRelease
try: try:
import winreg import winreg
@ -26,79 +26,25 @@ except ModuleNotFoundError:
# Local libraries # Local libraries
import tfscript import tfscript
from tfscript import verify from tfscript import verify
from tfscript import writing
args = {} args = {}
targetDir = "" targetDir = ""
def parseFile(inputFile): def parseFile(inputFile) -> (dict, dict):
"""Parse, verify, and do the conversion.""" """Parse, verify, and do the conversion."""
config = yaml.safe_load(inputFile) config = yaml.safe_load(inputFile)
# See verify.py # See verify.py
config = tfscript.verify.verifyConfig(config) config, aliases = verify.verifyConfig(config)
if "errors" in config: if "errors" in config:
for e in config["errors"]: for e in config["errors"]:
print(e,file=sys.stderr) print(e, file=sys.stderr)
return None, None
else: else:
parseConfig(config) return config, aliases
def writeOutput(data, className) -> dict: def parseConfig(config, defaults):
"""
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):
"""With validated data structure, write out all the files.""" """With validated data structure, write out all the files."""
global args global args
global targetDir global targetDir
@ -110,20 +56,18 @@ def parseConfig(config):
tempsAndReals = {} tempsAndReals = {}
if defaults is not None:
stringToWrite = tfscript.makeCFG(defaults)
replaceDict = writing.writeOutput(stringToWrite, "default", args)
tempsAndReals.update(replaceDict)
for currentClass in config: for currentClass in config:
classDict = config[currentClass] classDict = config[currentClass]
stringToWrite = tfscript.makeCFG(classDict) stringToWrite = tfscript.makeCFG(classDict)
replaceDict = writeOutput(stringToWrite, currentClass) replaceDict = writing.writeOutput(stringToWrite, currentClass, args)
tempsAndReals.update(replaceDict) tempsAndReals.update(replaceDict)
for tmpName, realName in tempsAndReals.items(): return tempsAndReals
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)
def parseCLI(): def parseCLI():
# Handle command line # Handle command line
@ -143,7 +87,33 @@ def parseCLI():
help='File containing YAML to convert.') help='File containing YAML to convert.')
return parser 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. """ """ Command line interface. """
global args global args
global targetDir global targetDir
@ -153,31 +123,9 @@ def main():
if args.directory is not None: if args.directory is not None:
targetDir = args.directory targetDir = args.directory
else: else:
systemName = OSName() systemName = GetOSName()
if systemName == "Darwin": targetDir = getTargetDir(systemName)
if float( '.'.join(OSRelease().split('.')[0:2]) ) >= 10.15: if targetDir is not None:
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 != "":
# Supported OS: add steamapps path # Supported OS: add steamapps path
if targetDir[-1] != '/': if targetDir[-1] != '/':
targetDir += '/' targetDir += '/'
@ -191,7 +139,14 @@ def main():
# Unsupported OS and not forced to continue # Unsupported OS and not forced to continue
return 2 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 return 0
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,34 +1,84 @@
"""Verify all the things that could go wrong.""" """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 = [] aliasErrors = []
aliases = None defaults = None
if "aliases" in cfg: if "default" in cfg:
aliases = cfg.pop("aliases") defaults = cfg.pop("default")
for key, data in aliases.items(): for key, data in defaults.items():
errMessages = validBind(key, data, alias = True) isAlias = ("alias" in data and data["alias"] == True)
errMessages = validBind(key, data, alias = isAlias)
if len(errMessages) > 0: if len(errMessages) > 0:
for msg in errMessages: 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: errors = aliasErrors.copy()
for key, data in cfg[cclass].items():
errMessages = validBind(key, data)
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: if len(errMessages) > 0:
for msg in errMessages: for msg in errMessages:
errors.append(f"Error in {cclass}: {msg}") 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: if len(errors) > 0:
cfg.update({"errors": errors}) verifiedConfig.update({"errors": errors})
return cfg return verifiedConfig, defaults
def validBind(key, data, alias = False) -> list: def validBind(key, data, alias = False) -> list:
"""Check for valid key and valid binding""" """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 # These types are simple, just the bind type and argument
if bindType in ["impulse", "toggle"]: if bindType in ["impulse", "toggle"]:
data.pop(bindType) data.pop(bindType)
elif bindType == "hold": elif bindType == "hold":
content = data.pop("hold") content = data.pop("hold")
if isinstance(content, dict): if isinstance(content, dict):
@ -152,10 +202,28 @@ def removeRelaventFields(data, bindType):
if "interval" not in content: if "interval" not in content:
errMsgs.append("Repeat requires interval") errMsgs.append("Repeat requires interval")
else: else:
interval = content["interval"] intervalStr = content["interval"]
if interval < 0: if unit == "s":
errMsgs.append("Repeat interval cannot be negative") interval = float(intervalStr)
if (unit == "t" and not isinstance(interval, int)): else:
errMsgs.append("Repeat interval must be integer if unit is ticks") 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 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

82
src/tfscript/writing.py Normal file
View File

@ -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