diff --git a/Firmware/actions.py b/Firmware/actions.py index dfbaeea..f08fb6b 100644 --- a/Firmware/actions.py +++ b/Firmware/actions.py @@ -1,7 +1,4 @@ -import usb_hid import time -import supervisor -import microcontroller class BoundAction: __slots__ = 'owner' @@ -212,7 +209,7 @@ def __init__(self, el_stringerino, delay_between_keystrokes, repeat_every=None): def one_shot(self): for index, char in enumerate(self.payload): - if index is not 0: # Prevents unnecessary delay after final char + if index != 0: # Prevents unnecessary delay after final char time.sleep(self.typing_rate) self.owner.hid_keyboard_layout.write(char) self.last_fire_timestamp = time.monotonic() @@ -324,4 +321,4 @@ def start_hold(self): self.owner.queue_reload() def __eq__(self, obj): - return isinstance(obj, ReloadkeymapAction) \ No newline at end of file + return isinstance(obj, ReloadKeymapAction) diff --git a/Firmware/parser.py b/Firmware/parser.py index eae1bea..614daa2 100644 --- a/Firmware/parser.py +++ b/Firmware/parser.py @@ -1,347 +1,753 @@ from actions import * +from enum import Enum from keynames import key_names - -key_binding_operation_tokens = ['on press', 'on click', 'on hold', 'on double-click', 'on release'] -key_binding_action_tokens = ['press', 'release', 'click', 'wait', 'switch to', - 'toggle', 'leave', 'type', 'reset keyboard', - 'bootloader', 'home', 'nothing', 'pass through', 'reload key maps'] +import string debug_line_callback = None def debug_out(*args): # print(' '.join(args)) - print(*args) + # print(*args) if debug_line_callback is not None: debug_line_callback(' '.join(args)) -def parse_layer_definition(filename, layer): - debug_out('Parsing', filename) - - layer.reset() - - lines = [] - with open(filename, 'r') as f: - lines += f.readlines() +class Token(Enum): + SYM_HASH = 1 + SYM_COMMA = 2 + SYM_COLON = 3 + SYM_QUOTE = 4 + SYM_NEWLINE = 5 + SYM_PLUS = 6 + STRING_LIT = 7 + ROW_LIT = 8 + KEY_LIT = 9 + NUM_LIT = 10 + HEX_LIT = 11 + IDENTIFIER = 12 + OPERATION_PRESS = 13 + OPERATION_CLICK = 14 + OPERATION_HOLD = 15 + OPERATION_DOUBLE_CLICK = 16 + OPERATION_RELEASE = 17 + ACTION_PRESS = 18 + ACTION_RELEASE = 19 + ACTION_CLICK = 20 + ACTION_WAIT = 21 + ACTION_SWITCH_TO = 22 + ACTION_TOGGLE = 23 + ACTION_LEAVE = 24 + ACTION_TYPE = 25 + ACTION_RESET_KEYBOARD = 26 + ACTION_BOOTLOADER = 27 + ACTION_HOME = 28 + ACTION_NOTHING = 29 + ACTION_PASS_THROUGH = 30 + ACTION_RELOAD_KEY_MAPS = 31 + PARAMETER_QUICKLY = 32 + PARAMETER_SLOWLY = 33 + PARAMETER_REPEATEDLY = 34 + PARAMETER_AT_HUMAN_SPEED = 35 + PARAMETER_UNTIL_RELEASED = 36 + PARAMETER_TIME_MS = 37 + PARAMETER_TIME_SEC = 38 + PARAMETER_TIME_MIN = 39 + TOP_OTHER_KEYS_FALL_THROUGH = 40 + TOP_BLOCK_OTHER_KEYS = 41 + +# Returns the tag of a token +def t_tag(token): + return token[0] + +# Returns the line number of a token +def t_lineno(token): + return token[3] + +# Returns the string that the token represents +def t_str(source, token): + return source[token[1]:token[2]] + +# Returns the full string between start and end token +# Only use the function for debugging/error purposes +# It is not safe for semantic use as two tokens might +# have a "removed" comment between them +# +# TODO: Update this function to be smart about comments +def t_run_str(source, start, end): + return source[start[1]: end[2]] + +# Takes a string and converts it into a list of tokens representing meaningful +# chunks of characters. This minimizes the amount of on the fly string comparisons, +# reduces the number of string copies, simplifies parsing logic and enables niceties +# like printing the line number on parsing failure. +# +# type: Array of token 4-tuples +# token type: (0,0,0,0) +# t[0]: Token tag from 'Token' class +# t[1]: Starting index in source of the token +# t[2]: Index after the last character of the token +# t[3]: Line number of the token +# +# Note: Use a string slicke to get the string representation of a token +# i.e. tok_str = source[t[1]:t[2]] +# +# Note: It is not safe to take a multi-token slice as two tokens may cross over a comment line +# i.e. tokens = [, , , , , ] +# str = source[tokens[0][1]:tokens[-1][2]] # this is using start from first token, end from last +# print(str) # 'Wait 100ms # do a wait [newline] R0, K0:' +def tokenize(source: str): + tokens = [] + line_number = 1 - layer.name = filename.lower()[:filename.rindex('.')] if '.' in filename else filename.lower() + index = 0 + while index < len(source): + char = source[index] - state = None # 'binding key', 'assigning screen', 'meta' - current_binding_row = None - current_binding_col = None - screen_being_configured = None - - for line in lines: - line = line.lstrip().strip() - - debug_out("About to parse:", line) - if '#' in line: # Strip out comment - debug_out('Line may have a comment') - in_a_string = False - comment_start_index = -1 - for index, char in enumerate(line): - if char == '"': - if in_a_string: - debug_out('String closed on char', index) - in_a_string = False - else: - debug_out('String opened on char', index) - in_a_string = True - elif char == '#': - if in_a_string: - debug_out('Hash in pos {} is in a literal'.format(index)) - else: - debug_out('Hash in pos {} is the start of a commment'.format(index)) - comment_start_index = index - break - - if comment_start_index >= 0: - line = line[:comment_start_index].strip() - debug_out("Yup, comment. Removed it, line is now", line) - - debug_out('Now processing line:', line) - - # if line[0] == '#': - # debug_out('We got a comment boys') - # continue - - if not len(line): - debug_out('Line handled') + if char == ' ' or char == '\t': + index = index + 1 continue - - if line[0] == 'R': # Could be a definition - if ',' in line and 'K' in line and ':' in line: - try: - row = int(line[1 : line.index(',')]) - col = int(line[line.index('K') + 1 : line.index(':')]) - state = 'binding key' - - current_binding_row = row - current_binding_col = col # Do this afterwards in case it fails partway - - debug_out('Now binding row {} col {}'.format( - current_binding_row, current_binding_col)) - - # There could be a boring regular key definition inline, let's see - try: - keycodes = parse_keycodes(line[line.index(':') + 1:].lstrip()) - debug_out('This has an inline generic key definition') - layer.bind(current_binding_row, current_binding_col, - GenericKeyAction(keycodes)) - debug_out('Line handled - nothing past this point matters') - continue - except AttributeError: - debug_out('No inline generic key definition') - except (AttributeError, ValueError): - debug_out('Nope, not a key definition') - - # Strip out coordinates - line = line[line.index(':') + 1:] - - elif 'Other keys fall through' in line and line.index('Other keys fall through') == 0: - layer.unassigned_keys_fall_through = True - debug_out('Unassigned keys pass through to lower layer') - debug_out('Line handled') # TODO: This is crude - add a fallback keybinding instead - continue - - elif 'Block other keys' in line and line.index('Block other keys') == 0: - layer.unassigned_keys_fall_through = False - debug_out('Unassigned keys do not pass through') - debug_out('Line handled') + if char == '\n': + line_number = line_number + 1 + index = index + 1 + continue + if char == '#': + while(index < len(source) and source[index] != '\n'): + index = index + 1 + index = index + 1 # eat the \n + line_number = line_number + 1 + continue + if char == ',': + tokens.append((Token.SYM_COMMA, index, index + 1, line_number)) + index = index + 1 + continue + if char == '+': + tokens.append((Token.SYM_PLUS, index, index + 1, line_number)) + index = index + 1 + continue + if char == ':': + tokens.append((Token.SYM_COLON, index, index + 1, line_number)) + index = index + 1 continue - # Doesn't make sense to configure screens in a layer - should get its own parser? - """ - elif 'Top Screen' in line and line.index('Top Screen') == 0: # TODO: Remove need to capitalize? Performance benefits? - state = 'assigning screen' - screen_being_configured = 0 - debug_out('Now configuring top screen') - line = line[len('Top Screen'):].lstrip() - - elif 'Middle Screen' in line and line.index('Middle Screen') == 0: - state = 'assigning screen' - screen_being_configured = 1 - debug_out('Now configuring middle screen') - line = line[len('Middle Screen'):].lstrip() - - elif 'Bottom Screen' in line and line.index('Bottom Screen') == 0: # TODO: Remove need to capitalize? Performance benefits? - state = 'assigning screen' - screen_being_configured = 'top' - debug_out('Now configuring top screen') - line = line[len('Top Screen'):].lstrip() - """ - - # TODO throw error if a layer has passthrough disabled and no way to dismiss itself - - # TODO detect screen and preamble bits here - - # At this point, we know this line is not a new definition. - # It should continue describing info for the ongoing state. - - line = line.lstrip().strip() - if not len(line): - debug_out('Line handled') + if char == '"': + start = index + index = index + 1 + try: + while(source[index] != '"'): + index += 1 + index = index + 1 # eat the " + except IndexError: + raise AttributeError('Line {}: Unterminated string'.format(line_number)) + + tokens.append((Token.STRING_LIT, start, index, line_number)) continue - if state == 'binding key': - operation, action = parse_binding(line) - debug_out('Binding a {} to {} on key {}, {}'.format( - type(action), operation, current_binding_row, current_binding_col)) - layer.bind(current_binding_row, current_binding_col, - action, operation) + # Hex/key literal + if char == '0' and source[index + 1] == 'x': + start = index + index += 2 - debug_out('Line handled') + if (source[index] not in string.hexdigits): + raise AttributeError('Line {}: Hex literal must have have all hex digits'.format(line_number)) + + while(index < len(source) and source[index] in string.hexdigits): + index += 1 - return layer - + tokens.append((Token.HEX_LIT, start, index, line_number)) + continue -def parse_binding(line): - # Assumes whitespace, coordinates already stripped out - tokens = [] - operation = None - action = None - key_combo_in_progress = False - - debug_out('Parsing binding line', line) - - if line.count('"') % 2: - debug_out('Line has at least one invalid typed string') - raise AttributeError('Unclosed typed string') - - for token in key_binding_operation_tokens: - if token + ':' in line.lower() and line.lower().index(token) == 0: - debug_out('This line is a', token, 'operation') - operation = token - line = line[line.index(':') + 1:].lstrip() # Remove processed operation token - break - - tokens = line.split(',') - debug_out('Action tokens:', tokens) - - if len(tokens) > 1: - debug_out('This binding is a sequence') - action = SequenceAction() - - for token in tokens: - sub_action = parse_action_token(token) - action.sequence.append(sub_action) - else: - debug_out('This binding is a single action') - action = parse_action_token(tokens[0]) - - if isinstance(action, TemporaryLayerAction) and not operation == 'on hold': - debug_out('TemporaryLayerAction can only be bound to On Hold, not', operation) - raise AttributeError('TemporaryLayerAction can only bind to On Hold') + # Number literal + if char.isdigit(): + start = index - # if isinstance(action, GenericKeyAction) and operation is not None: - # print ("Regular key behavior can't be bound to a special operation") + while(index < len(source) and source[index].isdigit()): + index += 1 + + tokens.append((Token.NUM_LIT, start, index, line_number)) + continue - return operation, action + if not char.isalnum(): + raise AttributeError('Line {}: Unexpected character \'{}\''.format(line_number, char)) -def parse_action_token(token): - debug_out('Parsing action token', token) + start = index + while(index < len(source) and source[index].isalnum()): + index += 1 - for action_token in key_binding_action_tokens: - if action_token in token.lower() and token.lower().index(action_token) == 0: - param = token[len(action_token) + 1:] + # Grab Row/Key Literals if they exist (K33, R999) + if (source[start] == 'R' or source[start] == 'K') and source[start+1:index].isdigit(): + if source[start] == 'R': + tokens.append((Token.ROW_LIT, start, index, line_number)) + else: + tokens.append((Token.KEY_LIT, start, index, line_number)) + continue - debug_out('This token is a(n)', action_token, 'action') + # Handle Keywords + identifier = source[start:index].lower() + token_type = Token.IDENTIFIER + + if identifier == 'on' and source[index] == ' ': + # make a restore point + old_index = index + + # move past the space + index = index + 1 + + # look forward to see if we find the trigger + while(index < len(source) and (source[index].isalnum() or source[index] == '-')): # '-' for double-click + index = index + 1 + + trigger = source[start:index].lower() + if trigger == 'on press': + token_type = Token.OPERATION_PRESS + if trigger == 'on click': + token_type = Token.OPERATION_CLICK + if trigger == 'on hold': + token_type = Token.OPERATION_HOLD + if trigger == 'on double-click': + token_type = Token.OPERATION_DOUBLE_CLICK + if trigger == 'on release': + token_type = Token.OPERATION_RELEASE + + # Trigger not found, restore index + if token_type == Token.IDENTIFIER: + index = old_index + if identifier == 'press': + token_type = Token.ACTION_PRESS + if identifier == 'release': + token_type = Token.ACTION_RELEASE + if identifier == 'click': + token_type = Token.ACTION_CLICK + if identifier == 'wait': + token_type = Token.ACTION_WAIT + if identifier == 'toggle': + token_type = Token.ACTION_TOGGLE + if identifier == 'leave': + token_type = Token.ACTION_LEAVE + if identifier == 'type': + token_type = Token.ACTION_TYPE + if identifier == 'bootloader': + token_type = Token.ACTION_BOOTLOADER + if identifier == 'home': + token_type = Token.ACTION_HOME + if identifier == 'nothing': + token_type = Token.ACTION_NOTHING + if identifier == 'quickly': + token_type = Token.PARAMETER_QUICKLY + if identifier == 'slowly': + token_type = Token.PARAMETER_SLOWLY + if identifier == 'repeatedly': + token_type = Token.PARAMETER_REPEATEDLY + if (identifier == "ms" or identifier == "milliseconds"): + token_type = Token.PARAMETER_TIME_MS + if (identifier == "sec" or identifier == "seconds"): + token_type = Token.PARAMETER_TIME_SEC + if (identifier == "min" or identifier == "minutes"): + token_type = Token.PARAMETER_TIME_MIN + if identifier == 'switch' and source[index:index + 3].lower() == ' to': + index = index + 3 + token_type = Token.ACTION_SWITCH_TO + if identifier == 'reset' and source[index:index + 9].lower() == ' keyboard': + index = index + 9 + token_type = Token.ACTION_RESET_KEYBOARD + if identifier == 'pass' and source[index:index + 8].lower() == ' through': + index = index + 8 + token_type = Token.ACTION_PASS_THROUGH + if identifier == 'reload' and source[index:index + 9].lower() == ' key maps': + index = index + 9 + token_type = Token.ACTION_RELOAD_KEY_MAPS + if identifier == 'at' and source[index:index + 12].lower() == ' human speed': + index = index + 12 + token_type = Token.PARAMETER_AT_HUMAN_SPEED + if identifier == 'until' and source[index:index + 9].lower() == ' released': + index = index + 9 + token_type = Token.PARAMETER_UNTIL_RELEASED + if identifier == 'other' and source[index:index + 18].lower() == ' keys fall through': + index = index + 18 + token_type = Token.TOP_OTHER_KEYS_FALL_THROUGH + if identifier == 'block' and source[index:index + 11].lower() == ' other keys': + index = index + 11 + token_type = Token.TOP_BLOCK_OTHER_KEYS + + tokens.append((token_type, start, index, line_number)) + + return tokens + +# Takes the full token list and the index of the current token to be parsed +# and returns an array of tokens representing a single action/action list and +# the index for the next token to be parsed +# +# ex [..., Wait, 100ms, A, B, +, C, R0, ...], index = 7 +# => [Wait, 100ms, A, B, +, C], index = 13 +def parse_action(source, tokens, index): + front_disallowed_tokens = [ + Token.SYM_PLUS, + Token.SYM_COMMA, + Token.STRING_LIT, + Token.PARAMETER_REPEATEDLY, + Token.PARAMETER_QUICKLY, + Token.PARAMETER_SLOWLY, + Token.PARAMETER_AT_HUMAN_SPEED, + Token.PARAMETER_UNTIL_RELEASED, + ] + + back_disallowed_tokens = [ + Token.SYM_PLUS, + Token.SYM_COMMA, + Token.NUM_LIT, + ] + + allowed_tokens = [ + Token.SYM_PLUS, + Token.SYM_COMMA, + Token.STRING_LIT, + Token.NUM_LIT, + Token.HEX_LIT, + Token.IDENTIFIER, + Token.ACTION_PRESS, + Token.ACTION_RELEASE, + Token.ACTION_CLICK, + Token.ACTION_WAIT, + Token.ACTION_SWITCH_TO, + Token.ACTION_TOGGLE, + Token.ACTION_LEAVE, + Token.ACTION_TYPE, + Token.ACTION_RESET_KEYBOARD, + Token.ACTION_BOOTLOADER, + Token.ACTION_HOME, + Token.ACTION_NOTHING, + Token.ACTION_PASS_THROUGH, + Token.ACTION_RELOAD_KEY_MAPS, + Token.PARAMETER_REPEATEDLY, + Token.PARAMETER_QUICKLY, + Token.PARAMETER_SLOWLY, + Token.PARAMETER_AT_HUMAN_SPEED, + Token.PARAMETER_UNTIL_RELEASED, + Token.PARAMETER_TIME_MS, + Token.PARAMETER_TIME_MIN, + Token.PARAMETER_TIME_SEC, + ] + + head = tokens[index] + if t_tag(head) in front_disallowed_tokens: + raise AttributeError('Line {}: Token not allowed at start of action: {}'.format(t_lineno(head), t_str(source, head))) + + index = index + 1 + start_index = index + actions = [head] + + while (index < len(tokens) + and (t_tag(tokens[index]) in allowed_tokens)): + actions.append(tokens[index]) + index = index + 1 + + # Check for trailing '+' and ',' + if t_tag(actions[-1]) in [Token.SYM_PLUS, Token.SYM_COMMA]: + raise AttributeError('Line {}: Token not allowed at end of action: {}'.format(t_lineno(actions[-1]), t_str(source, actions[-1]))) + + # Check for two '+' in a row + for i in range(len(actions) - 1): + if (t_tag(actions[i]) == Token.SYM_PLUS and t_tag(actions[i+1]) == Token.SYM_PLUS): + raise AttributeError('Line {}: Cannot have two consecutive \'+\''.format(t_lineno(actions[i]))) + + return (actions, index) + + +# Top level parsing function +# Returns the following parsed token sets +# top_level: List of all top level tokens in source +# ex: [BLOCK_OTHER_KEYS, OTHER_KEYS_FALL_THROUGH] +# bindings: A maps of all bindings parsed from the tokens. At this point, the +# tokens are only grouped into bindings and no action semantics have +# been extracted. +# +# type: (Row, Key) => Event => Values +# ex: { +# (0, 0): { +# 'standard': [LEFT, ALT, +, F, +, A], +# 'on hold': [TYPE, "abc", quickly, Wait, 100ms] +# }, +# (2, 3): { 'standard': [Q] } +# } +def parse(source, tokens): + # Build up the output map. + # (Row, Key) => Event => Values + bindings = [] + top_level = [] + + index = 0 + while index < len(tokens): + r = tokens[index] + + # If this is a top level statement, extract it into top_level then continue + # parsing the bindings. + if t_tag(r) in [Token.TOP_BLOCK_OTHER_KEYS, Token.TOP_OTHER_KEYS_FALL_THROUGH]: + top_level.append(r) + index = index + 1 + continue - if action_token == 'press': - keycodes = parse_keycodes(param) - action = PressKeyAction(keycodes) + if t_tag(r) != Token.ROW_LIT: + raise AttributeError('Line {}: Expected row literal, saw \'{}\' instead'.format(t_lineno(r), t_str(source, r))) - elif action_token == 'release': - keycodes = parse_keycodes(param) - action = ReleaseKeyAction(keycodes) + index = index + 1 + if index >= len(tokens) or t_tag(tokens[index]) != Token.SYM_COMMA: + raise AttributeError('Line {}: Expected comma after \'{}\'' + .format(t_lineno(r), t_str(source, r))) - elif action_token == 'click': - keycodes = parse_keycodes(param) - action = ClickKeyAction(keycodes) + index = index + 1 - elif action_token == 'wait': - action = DelayAction(parse_time(param)) + if index >= len(tokens) or t_tag(tokens[index]) != Token.KEY_LIT: + raise AttributeError('Line {}: Expected key literal'.format(t_lineno(tokens[index - 1]))) - elif action_token == 'switch to': - param = param.lower() - if ' until released' in param: - debug_out('This is a temporary layer switch') - action = TemporaryLayerAction(param[:param.index(' until released')]) - else: - debug_out('This is a regular layer switch') - action = SwitchToLayerAction(param) + k = tokens[index] + index = index + 1 + if index >= len(tokens) or t_tag(tokens[index]) != Token.SYM_COLON: + raise AttributeError('Line {}: Expected colon after \'{}\'' + .format(t_lineno(k), t_str(source, k))) - elif action_token == 'toggle': - action = ToggleLayerAction(param.lower()) + index = index + 1 - elif action_token == 'leave': - action = LeaveLayerAction(param.lower()) + key_tuple = (int(t_str(source, r)[1:]), int(t_str(source, k)[1:])) + binding = { key_tuple: {} } - elif action_token == 'type': - # TODO get these magic numbers outta here - subtokens = param.split('"') + inline = True + # if there is any on-x event, process them all + while index < len(tokens) and t_tag(tokens[index]) in [Token.OPERATION_PRESS, Token.OPERATION_CLICK, Token.OPERATION_HOLD, Token.OPERATION_DOUBLE_CLICK, Token.OPERATION_RELEASE]: + inline = False + operation = tokens[index] + index = index + 1 - delay = 0.01 - string_to_type = '' - repeating = False + if (index >= len(tokens) or t_tag(tokens[index]) != Token.SYM_COLON): + raise AttributeError('Line {}: Expected colon after \'{}\''.format(t_lineno(operation), t_str(source, operation))) + + index = index + 1 + if index >= len(tokens): + raise AttributeError('Line {}: Expected action definition after \'{}:\''.format(t_lineno(operation), t_str(source, operation))) - for raw_token in subtokens: - token = raw_token.lstrip().strip() + (operation_actions, index) = parse_action(source, tokens, index) - if not len(token): - continue + operation_str = t_str(source, operation).lower() + binding[key_tuple][operation_str] = operation_actions - if token == 'repeatedly': # TODO: 'once' and timing strings are mutually exclusive, improve parsing to catch multiple tokens - debug_out('Repeat during a hold') - repeating = True - elif token == 'slowly': - debug_out("Found speed token 'slowly'") - delay = 0.2 - elif token == 'at human speed': - debug_out("Found speed token 'at human speed'") - delay = 0.05 - elif token == 'quickly': - debug_out("Found speed token 'quickly'") - delay = 0 - else: - try: - delay = parse_time(token) - debug_out("Found explicit speed token '{}'".format(token)) - except AttributeError: - if len(string_to_type): - raise AttributeError('Parsing error! Either existing string "{}" or new token "{}" is malformed.'.format(string_to_type, token)) - string_to_type = token.replace('[COMMA]', ',').replace('[DOUBLE QUOTES]', '"') + if inline: # otherwise process the inline statement + if index >= len(tokens): + raise AttributeError('Line {}: Expected action definition after \'{}:\''.format(t_lineno(k), t_str(source, k))) + (actions, index) = parse_action(source, tokens, index) + binding[key_tuple]['standard'] = actions + + bindings.append(binding) - debug_out('String to type: "{}", delay: {}, {}'.format( - string_to_type, delay, 'repeating' if repeating else 'non-repeating')) + return (top_level, bindings) - if repeating: - action = StringTyperAction(string_to_type, delay) - else: - action = NonRepeatingStringTyperAction(string_to_type, delay) +# Parses a single token into a value of time in seconds +# '100ms' -> 0.1, '1sec'-> 1 +# Errors if more than one token, time missing units, chars before units not convertable +def parse_time(source, tokens): + if len(tokens) != 2: + raise AttributeError('Line {}: Expected 2 time parameters, saw {} ({}) paramters instead.' + .format(t_lineno(tokens[0]), len(tokens), t_run_str(source, tokens[0], tokens[-1]))) - elif action_token == 'reset keyboard': - action = ResetKeebAction() + duration = tokens[0] + units = tokens[1] - elif action_token == 'bootloader': - action = KeebBootloaderAction() + if t_tag(duration) != Token.NUM_LIT: + raise AttributeError('Line {}: Expected number in time literal'.format(t_lineno(duration))) - elif action_token == 'home': - action = ResetLayersAction() + try: + time = float(t_str(source, duration)) + except ValueError: + raise AttributeError('Line {}: Unable to convert \'{}\' to a time'.format(t_lineno(time_token), token)) - elif action_token == 'nothing': - action = NothingburgerAction() + if t_tag(units) == Token.PARAMETER_TIME_MS: + return time / 1000.0 - elif action_token == 'pass through': - action = PassThroughAction() + if t_tag(units) == Token.PARAMETER_TIME_SEC: + return time - elif action_token == 'reload key maps': - action = ReloadKeymapAction() + if t_tag(units) == Token.PARAMETER_TIME_MIN: + return time * 60.0 - else: - debug_out('/!\\ Zack forgot to implement the', action_token) + raise AttributeError('Line {}: Expected units in time literal'.format(t_lineno(units))) - return action - debug_out("Didn't parse to an action. Is it a keycode?") - keycodes = parse_keycodes(token) +# Parse a list of tokens representing a key conbination into the keycode +# [LEFT, ALT, +, F, +, A] -> [0xE2, 0x09, 0x04] +def parse_keycodes(source, tokens): # TODO: Make this auto-detect shift, alt, cmd, etc placement + keycodes = [] + subtokens = [[]] - debug_out("This is a boring old regular key action!") - return GenericKeyAction(keycodes) + # Split token list [LEFT, ALT, +, F, +, A] + # into [[LEFT, ALT], [F], [A]] + for token in tokens: + if t_tag(token) == Token.SYM_PLUS: + subtokens.append([]) + continue + subtokens[-1].append(token) + parsed_keys = [] + for token in subtokens: + if t_tag(token[0]) == Token.HEX_LIT: + if len(token) != 1: + raise AttributeError('Line {}: Hex literals must be separated by \'+\''.format(t_lineno(token[0]), t_str(source, token[0]))) + keycodes.append(int(t_str(source, token[0]), 16)) + continue -def parse_keycodes(token): # TODO: Make this auto-detect shift, alt, cmd, etc placement - subtokens = token.upper().replace(' ', '').split('+') - keycodes = [] + # Merge tokens into single name [LEFT, ALT] => 'LEFTALT' + key_name = '' + for t in token: + key_name = key_name + t_str(source, t) - for token in subtokens: - if not token in key_names: - debug_out(token, 'is not a key name') - raise AttributeError(token, 'is not a key name') + # Save list of parsed key names to print later + parsed_keys.append(key_name) - keycodes.append(key_names[token]) + if not key_name in key_names: + raise AttributeError('Line {}: Invalid Action or Key \'{}\''.format(t_lineno(token[0]), t_str(source, token[0]))) - debug_out('Parsed keycode(s) {}'.format(' and '.join(subtokens))) + keycodes.append(key_names[key_name]) + + debug_out('Parsed keycode(s) {}'.format(' and '.join(parsed_keys))) return keycodes +# Parses a list of tokens representing a single action (Type "hello" quickly 100ms) +# into the associated 'Action' class +def parse_action_token(source, tokens): + debug_out('Parsing action token', tokens) + + action_token = tokens[0] + action_token_type = t_tag(action_token) + + debug_out('This token is a(n)', action_token_type, 'action') + + if action_token_type == Token.ACTION_PRESS: + if len(tokens) == 1: + raise AttributeError('Line {}: Press action requires key parameter'.format(t_lineno(action_token))) + keycodes = parse_keycodes(source, tokens[1:]) + return PressKeyAction(keycodes) + + if action_token_type == Token.ACTION_RELEASE: + if len(tokens) == 1: + raise AttributeError('Line {}: Release action requires key parameter'.format(t_lineno(action_token))) + keycodes = parse_keycodes(source, tokens[1:]) + return ReleaseKeyAction(keycodes) + + if action_token_type == Token.ACTION_CLICK: + if len(tokens) == 1: + raise AttributeError('Line {}: Click action requires key parameter'.format(t_lineno(action_token))) + keycodes = parse_keycodes(source, tokens[1:]) + return ClickKeyAction(keycodes) + + if action_token_type == Token.ACTION_WAIT: + if len(tokens) != 3: + raise AttributeError('Line {}: Wait action requires 2 parameters'.format(t_lineno(action_token))) + return DelayAction(parse_time(source, tokens[1:])) + + if action_token_type == Token.ACTION_SWITCH_TO: + if len(tokens) == 1: + raise AttributeError('Line {}: Switch to action requires layer parameter'.format(t_lineno(action_token))) + if len(tokens) == 2 and tokens[1][0] == Token.PARAMETER_UNTIL_RELEASED: + raise AttributeError('Line {}: Missing layer name for temporary switch: \'{}\'' + .format(t_lineno(action_token), t_run_str(source, tokens[0], tokens[-1]))) + + if t_tag(tokens[-1]) == Token.PARAMETER_UNTIL_RELEASED: + layer_name = t_run_str(source, tokens[1], tokens[-2]).lower() + debug_out('This is a temporary layer switch to \'{}\''.format(layer_name)) + return TemporaryLayerAction(layer_name) + else: + layer_name = t_run_str(source, tokens[1], tokens[-1]).lower() + debug_out('This is a regular layer switch to \'{}\''.format(layer_name)) + return SwitchToLayerAction(layer_name) + + if action_token_type == Token.ACTION_TOGGLE: + if len(tokens) == 1: + raise AttributeError('Line {}: Toggle action requires layer parameter'.format(t_lineno(action_token))) + param = t_run_str(source, tokens[1], tokens[-1]) + return ToggleLayerAction(param.lower()) + + if action_token_type == Token.ACTION_LEAVE: + if len(tokens) == 1: + raise AttributeError('Line {}: Leave action requires layer parameter'.format(t_lineno(action_token))) + param = t_run_str(source, tokens[1], tokens[-1]) + return LeaveLayerAction(param.lower()) + + if action_token_type == Token.ACTION_RESET_KEYBOARD: + if len(tokens) != 1: + raise AttributeError('Line {}: Reset Keyboard action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return ResetKeebAction() + + if action_token_type == Token.ACTION_BOOTLOADER: + if len(tokens) != 1: + raise AttributeError('Line {}: Bootloader action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return KeebBootloaderAction() + + if action_token_type == Token.ACTION_HOME: + if len(tokens) != 1: + raise AttributeError('Line {}: Home action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return ResetLayersAction() + + if action_token_type == Token.ACTION_NOTHING: + if len(tokens) != 1: + raise AttributeError('Line {}: Nothing action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return NothingburgerAction() + + if action_token_type == Token.ACTION_PASS_THROUGH: + if len(tokens) != 1: + raise AttributeError('Line {}: Pass through action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return PassThroughAction() + + if action_token_type == Token.ACTION_RELOAD_KEY_MAPS: + if len(tokens) != 1: + raise AttributeError('Line {}: Reload Key Maps action shouldn\'t have any parameters'.format(t_lineno(action_token))) + return ReloadKeymapAction() + + if action_token_type == Token.ACTION_TYPE: + # TODO get these magic numbers outta here + if len(tokens) == 1: + raise AttributeError('Line {}: Type action missing text parameter'.format(t_lineno(action_token))) + + string_lit = tokens[1] + if t_tag(string_lit) != Token.STRING_LIT: + raise AttributeError('Line {}: Type action\'s first parameter must be quoted text'.format(t_lineno(action_token))) + + delay = 0.01 + + # Adjust both sides by 1 to remove quotes + string_to_type = t_str(source, string_lit)[1:-1] + repeating = False + time_keywork_count = 0 + + replacements = { + # '[COMMA]': ',', # Not needed, just type , + '[DOUBLE QUOTES]': '"', + '[SINGLE QUOTE]': '\'', + '[RETURN]': '\n', + # TODO: Should escape sequences be allowed? i.e. '\n' in the text + } + for match, replace in replacements.items(): + string_to_type = string_to_type.replace(match, replace) + + num_token = None + unit_token = None + + for token in tokens[2:]: + param = t_tag(token) + if param == Token.PARAMETER_REPEATEDLY: # TODO: 'once' and timing strings are mutually exclusive, improve parsing to catch multiple tokens + debug_out('Repeat during a hold') + repeating = True + if param == Token.PARAMETER_SLOWLY: + debug_out("Found speed token 'slowly'") + delay = 0.2 + time_keywork_count += 1 + if param == Token.PARAMETER_QUICKLY: + debug_out("Found speed token 'quickly'") + delay = 0 + time_keywork_count += 1 + if param == Token.PARAMETER_AT_HUMAN_SPEED: + debug_out("Found speed token 'at human speed'") + delay = 0.05 + time_keywork_count += 1 + if param == Token.NUM_LIT: # Explict speed + if num_token != None: # We've previously set this value, trigger a 'too many speeds' error + time_keywork_count += 1 + num_token = token + if param in [Token.PARAMETER_TIME_MS, Token.PARAMETER_TIME_SEC, Token.PARAMETER_TIME_MIN]: + if unit_token != None: # We've previously set this value, trigger a 'too many speeds' error + time_keywork_count += 1 + unit_token = token + + # TODO: Find a more elegant way to do this. This allows wacky things like + # Type "hello" seconds repeatedly 100 + if (num_token is not None and unit_token is not None): + delay = parse_time(source, [num_token, unit_token]) + debug_out("Found explicit speed token '{}'".format(t_str(source,token))) + time_keywork_count += 1 + + if (num_token is not None and unit_token is None): + raise AttributeError('Line {}: time provided without units: \'{}\''.format(t_lineno(num_token), t_str(source, num_token))) + + if (num_token is None and unit_token is not None): + raise AttributeError('Line {}: unit provided without time: \'{}\''.format(t_lineno(unit_token), t_str(source, unit_token))) + + if time_keywork_count > 1: + raise AttributeError('Line {}: Multiple speeds set for Type action. Please select one.\n\t{}'.format(t_lineno(action_token), t_run_str(source, tokens[0], tokens[-1]))) + + debug_out('String to type: "{}", delay: {}, {}'.format( + string_to_type, delay, 'repeating' if repeating else 'non-repeating')) + + if repeating: + return StringTyperAction(string_to_type, delay) + + return NonRepeatingStringTyperAction(string_to_type, delay) -def parse_time(token): - debug_out('Parsing time token:', token) + debug_out("Didn't parse to an action. Is it a keycode?") + keycodes = parse_keycodes(source, tokens) + debug_out("This is a boring old regular key action!") + return GenericKeyAction(keycodes) + +# Convert a list of tokens representing an action/action sequence +# i.e (Wait 100ms, Type "hello", Bootloader) into the associated Action() object +def parse_action_list(source, tokens, operation): + token_sets = [[]] + + # Split the token list on commas, each index into token_sets + # represents a single action with any params + for token in tokens: + if t_tag(token) == Token.SYM_COMMA: + token_sets.append([]) + else: + token_sets[-1].append(token) + + debug_out('Action tokens:', token_sets) + + actions = [] + for token_set in token_sets: + sub_action = parse_action_token(source, token_set) + if type(sub_action) is TemporaryLayerAction and operation != 'on hold': + debug_out('TemporaryLayerAction can only be bound to On Hold, not', operation) + raise AttributeError('Line {}: TemporaryLayerAction can only bind to On Hold'.format(t_lineno(token_set[0]))) + + actions.append(sub_action) + + if len(actions) == 1: + debug_out('This binding is a single action') + return actions[0] + + debug_out('This binding is a sequence') + action = SequenceAction() + action.sequence.extend(actions) + return action + +# Parse full source into Layer model +def parse_source(source, layer): + tokens = tokenize(source) + (top_level, bindings) = parse(source, tokens) + + for statement in top_level: + if t_tag(statement) == Token.TOP_OTHER_KEYS_FALL_THROUGH: + layer.unassigned_keys_fall_through = True + debug_out('Unassigned keys pass through to lower layer') + continue + if t_tag(statement) == Token.TOP_BLOCK_OTHER_KEYS: + layer.unassigned_keys_fall_through = False + debug_out('Unassigned keys do not pass through') + continue - time = None + for binding in bindings: + for key, events in binding.items(): + for event_name, actions in events.items(): + debug_out('Attemping to parse statement \'{}\'' + .format(t_run_str(source, actions[0], actions[-1]))) + action = parse_action_list(source, actions, event_name) + layer.bind(key[0], key[1], action, event_name) - if 'ms' in token or 'milliseconds' in token: - time = float(token[:token.index('m')]) / 1000.0 + return layer - elif 'sec' in token: - time = float(token[:token.index('s')]) +# Top level parse from file +def parse_layer_definition(filename, layer): + debug_out('Parsing', filename) - elif 'min' in token: - time = float(token[:token.index('m')]) * 60.0 + layer.reset() + layer.name = filename.lower()[:filename.rindex('.')] if '.' in filename else filename.lower() - if time is None: - debug_out('Parsing failed') - raise AttributeError('Failed to parse {} to a time'.format(token)) + with open(file, 'r') as f: + source = f.read() - debug_out('Parsed to {.2d} sec'.format(time)) - return time \ No newline at end of file + return parse_source(source, layer) diff --git a/Firmware/parser_test.py b/Firmware/parser_test.py new file mode 100644 index 0000000..0c0a32e --- /dev/null +++ b/Firmware/parser_test.py @@ -0,0 +1,955 @@ +from actions import * +from keynames import key_names +from parser import * + +class FakeLayer: + def __init__(self): + self.key_bindings = [] + self.unassigned_keys_fall_through = True + + def bind(self, row, col, action, operation=None): + self.key_bindings.append((row, col, action, operation)) + +def fail(message, expected, actual): + print(message) + print('\tExpected: ' + str(expected)) + print('\t Actual: ' + str(actual)) + +def assert_generic_key_action(expected, actual, tag = 'GenericKeyAction'): + if expected.keycodes != actual.keycodes: + fail(tag + ' keycodes don\'t match', expected.keycodes, actual.keycodes) + return False + return True + +def assert_generic_layer_action(expected, actual, tag = 'GenericLayerAction'): + if expected.target_layer != actual.target_layer: + fail(tag + ' target layers don\'t match', expected.target_layer, actual.target_layer) + return False + return True + +def assert_switch_to_layer_action(expected, actual): + return assert_generic_layer_action(expected, actual, 'SwitchToLayerAction') + +def assert_temporary_layer_action(expected, actual): + return assert_generic_layer_action(expected, actual, 'TemporaryLayerAction') + +def assert_leave_layer_action(expected, actual): + return assert_generic_layer_action(expected, actual, 'LeaveLayerAction') + +def assert_toggle_layer_action(expected, actual): + return assert_generic_layer_action(expected, actual, 'ToggleLayerAction') + +def assert_press_key_action(expected, actual): + return assert_generic_key_action(expected, actual, 'PressKeyAction') + +def assert_release_key_action(expected, actual): + return assert_generic_key_action(expected, actual, 'ReleaseKeyAction') + +def assert_click_key_action(expected, actual): + return assert_generic_key_action(expected, actual, 'ClickKeyAction') + +def assert_string_typer_action(expected, actual, tag = 'StringTyperAction'): + ex_payload = expected.payload + ac_payload = actual.payload + ex_typing_rate = expected.typing_rate + ac_typing_rate = actual.typing_rate + + if ex_payload != ac_payload: + fail(tag + ': Payload is different', ex_payload, ac_payload) + return False + + if ex_typing_rate != ac_typing_rate: + fail(tag + ': Typing rate is different', ex_typing_rate, ac_typing_rate) + return False + + return True + +def assert_delay_action(expected, actual): + if expected.duration != actual.duration: + fail('Durations don\'t match', expected.duration, actual.duration) + return False + + return True + +def assert_non_repeating_string_typer_action(expected, actual): + return assert_string_typer_action(expected, actual, 'NonRepeatingStringTyperAction') + +def assert_sequence_action(expected, actual): + expected_seq = expected.sequence + actual_seq = actual.sequence + if len(expected_seq) != len(actual_seq): + fail('Sequence length is different', len(expected_seq), len(actual_seq)) + return False + + for i in range(len(actual_seq)): + e_seq = expected_seq[i] + a_seq = actual_seq[i] + if type(a_seq) is GenericKeyAction: + if not assert_generic_key_action(e_seq, a_seq): + return False + continue + if type(a_seq) is NonRepeatingStringTyperAction: + if not assert_non_repeating_string_typer_action(e_seq, a_seq): + return False + continue + if type(a_seq) is StringTyperAction: + if not assert_string_typer_action(e_seq, a_seq): + return False + continue + if type(a_seq) is DelayAction: + if not assert_delay_action(e_seq, a_seq): + return False + continue + if type(a_seq) is ClickKeyAction: + if not assert_click_key_action(e_seq, a_seq): + return False + continue + + fail('Type is not supported in sequence', '---', type(a_seq)) + return False + + return True + +def assert_bindings_match(expected_bindings, actual_bindings): + if len(expected_bindings) != len(actual_bindings): + fail('Expected number of bindings does\'t match actual number', len(expected_bindings), len(actual_bindings)) + return False + + for i in range(len(expected_bindings)): + expected_binding = expected_bindings[i] + actual_binding = actual_bindings[i] + + if actual_binding[0] != expected_binding[0]: + fail('Expected row doesn\'t match actual', expected_binding, actual_binding) + return False + + if type(expected_binding[2]) != type(actual_binding[2]): + fail('Expected action type doesn\'t match actual', expected_binding, actual_binding) + return False + + if actual_binding[1] != expected_binding[1]: + fail('Expected key doesn\'t match actual', expected_binding, actual_binding) + return False + + if actual_binding[3] != expected_binding[3]: + fail('Expected operation doesn\'t match actual', expected_binding, actual_binding) + return False + + expected_action = expected_binding[2] + actual_action = actual_binding[2] + checked_action = False + + if type(actual_action) is GenericKeyAction: + checked_action = True + if not assert_generic_key_action(expected_action, actual_action): + return False + + if type(actual_action) is SequenceAction: + checked_action = True + if not assert_sequence_action(expected_action, actual_action): + return False + + if type(actual_action) is PressKeyAction: + checked_action = True + if not assert_press_key_action(expected_action, actual_action): + return False + + if type(actual_action) is ReleaseKeyAction: + checked_action = True + if not assert_press_key_action(expected_action, actual_action): + return False + + if type(actual_action) is ClickKeyAction: + checked_action = True + if not assert_press_key_action(expected_action, actual_action): + return False + + if type(actual_action) is GenericLayerAction: + checked_action = True + if not assert_generic_layer_action(expected_action, actual_action): + return False + + if type(actual_action) is SwitchToLayerAction: + checked_action = True + if not assert_switch_to_layer_action(expected_action, actual_action): + return False + + if type(actual_action) is TemporaryLayerAction: + checked_action = True + if not assert_temporary_layer_action(expected_action, actual_action): + return False + + if type(actual_action) is LeaveLayerAction: + checked_action = True + if not assert_leave_layer_action(expected_action, actual_action): + return False + + if type(actual_action) is ToggleLayerAction: + checked_action = True + if not assert_toggle_layer_action(expected_action, actual_action): + return False + + if type(actual_action) is StringTyperAction: + checked_action = True + if not assert_string_typer_action(expected_action, actual_action): + return False + + if type(actual_action) is NonRepeatingStringTyperAction: + checked_action = True + if not assert_non_repeating_string_typer_action(expected_action, actual_action): + return False + + if type(actual_action) is DelayAction: + checked_action = True + if not assert_delay_action(expected_action, actual_action): + return False + + # Actions with no parameters just need to be the same type (checked above) + if type(actual_action) is ResetKeebAction: + checked_action = True + if type(actual_action) is KeebBootloaderAction: + checked_action = True + if type(actual_action) is ResetLayersAction: + checked_action = True + if type(actual_action) is NothingburgerAction: + checked_action = True + if type(actual_action) is PassThroughAction: + checked_action = True + if type(actual_action) is ReloadKeymapAction: + checked_action = True + + if not checked_action: + fail('failed to check action', type(actual_action).__name__, '-----') + return False + + return True + +def assert_attribute_error(source, err_text): + layer = FakeLayer() + try: + parse_source(source, layer) + except AttributeError as err: + if err_text in str(err): + return True + fail('Wrong error message', err_text, str(err)) + return False + except Exception as err: + fail('Wrong error type', 'AttributeError', str(err)) + raise + return False + + fail('Error not raised', 'AttributeError', '--- Nothing ---') + return False + +def test_basic(): + source = """ +R4, K0: A +R3, K1: B +R2, K2: C +R1, K3: 1 +R0, K4: 2 +R50, K5: 3 +R1000, K6: 4 +R1 , K7: EQUALS +R2 , K8: UP ARROW +R2 , K9: UPARROW +R3 ,K10: LEFT ALT + 3 +R0, K0: 0x01 +R0, K0: + 0x01+0x02+0xFF + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 0, GenericKeyAction(key_names['A']), 'standard'), + (3, 1, GenericKeyAction(key_names['B']), 'standard'), + (2, 2, GenericKeyAction(key_names['C']), 'standard'), + (1, 3, GenericKeyAction(key_names['1']), 'standard'), + (0, 4, GenericKeyAction(key_names['2']), 'standard'), + (50, 5, GenericKeyAction(key_names['3']), 'standard'), + (1000, 6, GenericKeyAction(key_names['4']), 'standard'), + (1, 7, GenericKeyAction(key_names['EQUALS']), 'standard'), + (2, 8, GenericKeyAction(key_names['UPARROW']), 'standard'), + (2, 9, GenericKeyAction(key_names['UPARROW']), 'standard'), + (3, 10, GenericKeyAction([key_names['LEFTALT'], key_names['3']]), 'standard'), + (0, 0, GenericKeyAction(0x01), 'standard'), + (0, 0, GenericKeyAction([0x01, 0x02, 0xFF]), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_comments(): + source = """ +R0, K0: ESC +#R0, K0: +# On click: Click WINDOWS + A +# On double-click: Type "Hello # world" slowly # Try to confuse # "The # parser" lol +# On hold: Switch to Layer 2 until released +R1, K11: MINUS # TODO: Add alias for HYPHEN and DASH +R2, K0: CAPSLOCK # This should auto-detect to left windows +# TODO: Commented-out lines with another hash inside them confuse the parser +# This comment is here to confuse the parser +# # # # # asdfas asf # asdf""" + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (0, 0, GenericKeyAction(key_names['ESC']), 'standard'), + (1, 11, GenericKeyAction(key_names['MINUS']), 'standard'), + (2, 0, GenericKeyAction(key_names['CAPSLOCK']), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + + +def test_events(): + source = """ +R4, K5: + On click: A+B+C + # On click: A+A, type, Switch to ab a s, Type "ab[SINGLE QUOTE][RETURN][DOUBLE QUOTES]cd" repeatedly quickly, Toggle as, Leave asdfasdf adf a sasdf, Reset Keyboard, bootloader, Home, Nothing, pass Through, Reload Key Maps + On hold: A + A + C + On double-click: SHIFT + On Release: F1 + on Press: F2 + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, GenericKeyAction([key_names['A'], key_names['B'], key_names['C']]), 'on click'), + (4, 5, GenericKeyAction([key_names['A'], key_names['A'], key_names['C']]), 'on hold'), + (4, 5, GenericKeyAction(key_names['SHIFT']), 'on double-click'), + (4, 5, GenericKeyAction(key_names['F1']), 'on release'), + (4, 5, GenericKeyAction(key_names['F2']), 'on press'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_fall_through(): + source = """ +other keys fall through +R4, K5: G + """ + layer = FakeLayer() + layer.unassigned_keys_fall_through = False + parse_source(source, layer) + + expected_bindings = [ + (4, 5, GenericKeyAction(key_names['G']), 'standard'), + ] + + if layer.unassigned_keys_fall_through != True: + fail('Fall through has wrong value', True, layer.unassigned_keys_fall_through) + return False + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_block(): + source = """ +block other keys +R4, K5: G + """ + layer = FakeLayer() + layer.unassigned_keys_fall_through = True + parse_source(source, layer) + + expected_bindings = [ + (4, 5, GenericKeyAction(key_names['G']), 'standard'), + ] + + if layer.unassigned_keys_fall_through != False: + fail('Fall through has wrong value', False, layer.unassigned_keys_fall_through) + return False + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_sequence(): + source = """ +R4, K5: A, B, C, D, E + """ + layer = FakeLayer() + parse_source(source, layer) + + sequence = SequenceAction() + sequence.sequence.append(GenericKeyAction(key_names['A'])) + sequence.sequence.append(GenericKeyAction(key_names['B'])) + sequence.sequence.append(GenericKeyAction(key_names['C'])) + sequence.sequence.append(GenericKeyAction(key_names['D'])) + sequence.sequence.append(GenericKeyAction(key_names['E'])) + + expected_bindings = [ + (4, 5, sequence, 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_press(): + source = """ +R4, K5: press A+SHIFT + B + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, PressKeyAction([key_names['A'], key_names['SHIFT'], key_names['B']]), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_release(): + source = """ +R4, K5: release A+SHIFT + B + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, ReleaseKeyAction([key_names['A'], key_names['SHIFT'], key_names['B']]), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_click(): + source = """ +R4, K5: click A+SHIFT + B + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, ClickKeyAction([key_names['A'], key_names['SHIFT'], key_names['B']]), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_wait(): + source = """ +R4, K5: Wait 500ms +R4, K5: Wait 500 milliseconds +R4, K5: Wait 500 sec +R4, K5: Wait 5seconds +R4, K5: Wait 1min +R4, K5: Wait 5minutes + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, DelayAction(0.5), 'standard'), + (4, 5, DelayAction(0.5), 'standard'), + (4, 5, DelayAction(500), 'standard'), + (4, 5, DelayAction(5), 'standard'), + (4, 5, DelayAction(60), 'standard'), + (4, 5, DelayAction(300), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + + +def test_action_switch_to(): + source = """ +R4, K5: switch To My third layer + """ + layer = FakeLayer() + parse_source(source, layer) + + # TODO: Should we really be lower()'ing the layer names? + expected_bindings = [ + (4, 5, SwitchToLayerAction('my third layer'), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_temporary_switch_to(): + source = """ +R4, K5: on hold: switch To My third layer until released + """ + layer = FakeLayer() + parse_source(source, layer) + + # TODO: Should we really be lower()'ing the layer names? + expected_bindings = [ + (4, 5, TemporaryLayerAction('my third layer'), 'on hold'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_toggle(): + source = """ +R4, K5: toggle My third layer + """ + layer = FakeLayer() + parse_source(source, layer) + + # TODO: Should we really be lower()'ing the layer names? + expected_bindings = [ + (4, 5, ToggleLayerAction('my third layer'), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_leave(): + source = """ +R4, K5: leave My third layer + """ + layer = FakeLayer() + parse_source(source, layer) + + # TODO: Should we really be lower()'ing the layer names? + expected_bindings = [ + (4, 5, LeaveLayerAction('my third layer'), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_type_repeating(): + source = """ +R4, K5: type "Hello, World!" 50ms repeatedly + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, StringTyperAction('Hello, World!', 0.05), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_type_non_repeating(): + source = """ +R4, K5: type "Hello, World!" slowly + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, NonRepeatingStringTyperAction('Hello, World!', 0.2), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_reset_keyboard(): + source = """ +R4, K5: reset Keyboard + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, ResetKeebAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_bootloader(): + source = """ +R4, K5: bootloader + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, KeebBootloaderAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_home(): + source = """ +R4, K5: home + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, ResetLayersAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_nothing(): + source = """ +R4, K5: nothing + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, NothingburgerAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_pass_through(): + source = """ +R4, K5: pass through + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, PassThroughAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_action_reload_key_maps(): + source = """ +R4, K5: reload key maps + """ + layer = FakeLayer() + parse_source(source, layer) + + expected_bindings = [ + (4, 5, ReloadKeymapAction(), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +def test_error_unterminated_string(): + source = """ + + R4, K5: Test "missing quote quickly""" + return assert_attribute_error(source, 'Line 3: Unterminated string') + +def test_error_unrecognized_character(): + source = """ +R4, K5: $ + """ + return assert_attribute_error(source, 'Line 2: Unexpected character \'$\'') + +def test_error_bad_action_start_token(): + source_part = 'R4, K5: ' + return (assert_attribute_error(source_part + '+', 'Line 1: Token not allowed at start of action: +') + and assert_attribute_error(source_part + ',', 'Line 1: Token not allowed at start of action: ,') + and assert_attribute_error(source_part + '"text"', 'Line 1: Token not allowed at start of action: "text"') + and assert_attribute_error(source_part + 'repeatedly', 'Line 1: Token not allowed at start of action: repeatedly') + and assert_attribute_error(source_part + 'quickly', 'Line 1: Token not allowed at start of action: quickly') + and assert_attribute_error(source_part + 'slowly', 'Line 1: Token not allowed at start of action: slowly') + and assert_attribute_error(source_part + 'at human speed', 'Line 1: Token not allowed at start of action: at human speed') + and assert_attribute_error(source_part + 'until released', 'Line 1: Token not allowed at start of action: until released')) + +def test_error_bad_action_end_token(): + source_part = 'R4, K5: F' + return (assert_attribute_error(source_part + '+', 'Line 1: Token not allowed at end of action: +') + and assert_attribute_error(source_part + ',', 'Line 1: Token not allowed at end of action: ,')) + +def test_error_duplicate_plus_token(): + source = 'R4, K5: A++F' + return assert_attribute_error(source, 'Line 1: Cannot have two consecutive \'+\'') + +def test_error_bad_key_def(): + return (assert_attribute_error('X0', 'Line 1: Expected row literal, saw \'X0\' instead') + and assert_attribute_error('R0', 'Line 1: Expected comma after \'R0\'') + and assert_attribute_error('R0:', 'Line 1: Expected comma after \'R0\'') + and assert_attribute_error('R0,', 'Line 1: Expected key literal') + and assert_attribute_error('R0, :', 'Line 1: Expected key literal') + and assert_attribute_error('R0, X0', 'Line 1: Expected key literal') + and assert_attribute_error('R0, K0', 'Line 1: Expected colon after \'K0\'') + and assert_attribute_error('R0, K0,', 'Line 1: Expected colon after \'K0\'')) + +def test_error_bad_event(): + return (assert_attribute_error('R0, K0:', 'Line 1: Expected action definition after \'K0:\'') + and assert_attribute_error('R0, K0:\n', 'Line 1: Expected action definition after \'K0:\'') + and assert_attribute_error('R0, K0: on click', 'Line 1: Expected colon after \'on click\'') + and assert_attribute_error('R0, K0: on click:', 'Line 1: Expected action definition after \'on click:\'')) + +def test_error_partial_action_tokens(): + return (assert_attribute_error('R0, K0: switch', 'Line 1: Invalid Action or Key \'switch\'') + and assert_attribute_error('R0, K0: reset', 'Line 1: Invalid Action or Key \'reset\'') + and assert_attribute_error('R0, K0: pass', 'Line 1: Invalid Action or Key \'pass\'') + and assert_attribute_error('R0, K0: reload', 'Line 1: Invalid Action or Key \'reload\'') + and assert_attribute_error('R0, K0: reload key', 'Line 1: Invalid Action or Key \'reload\'') + and assert_attribute_error('R0, K0: at', 'Line 1: Invalid Action or Key \'at\'') + and assert_attribute_error('R0, K0: at human', 'Line 1: Invalid Action or Key \'at\'') + and assert_attribute_error('R0, K0: until', 'Line 1: Invalid Action or Key \'until\'') + and assert_attribute_error('R0, K0: other', 'Line 1: Invalid Action or Key \'other\'') + and assert_attribute_error('R0, K0: other keys', 'Line 1: Invalid Action or Key \'other\'') + and assert_attribute_error('R0, K0: other keys fall', 'Line 1: Invalid Action or Key \'other\'') + and assert_attribute_error('R0, K0: block', 'Line 1: Invalid Action or Key \'block\'') + and assert_attribute_error('R0, K0: block other', 'Line 1: Invalid Action or Key \'block\'')) + +def test_error_actions(): + return ( + # Invalid Action + assert_attribute_error('R0, K0: NotAnAction', 'Line 1: Invalid Action or Key \'NotAnAction\'') + + # Press Action + and assert_attribute_error('R0, K0: press', 'Line 1: Press action requires key parameter') + + # Release Action + and assert_attribute_error('R0, K0: release', 'Line 1: Release action requires key parameter') + + # Click Action + and assert_attribute_error('R0, K0: click', 'Line 1: Click action requires key parameter') + + # Wait Action + and assert_attribute_error('R0, K0: Wait', 'Line 1: Wait action requires 2 parameters') + and assert_attribute_error('R0, K0: Wait 100', 'Line 1: Wait action requires 2 parameters') # Wait with only 1 param + and assert_attribute_error('R0, K0: Wait type ms', 'Line 1: Expected number in time literal') + and assert_attribute_error('R0, K0: Wait 100 repeatedly', 'Line 1: Expected units in time literal') + and assert_attribute_error('R0, K0: Wait 100mm', 'Line 1: Expected units in time literal') + and assert_attribute_error('R0, K0: Wait tensec', 'Line 1: Wait action requires 2 parameters') + + # Switch To Action + and assert_attribute_error('R0, K0: Switch to', 'Line 1: Switch to action requires layer parameter') + and assert_attribute_error('R0, K0: Switch to until released', 'Line 1: Missing layer name for temporary switch: \'Switch to until released\'') + and assert_attribute_error("""R0, K0: switch to top until released""", 'Line 1: TemporaryLayerAction can only bind to On Hold') + and assert_attribute_error("""R0, K0: + on release: switch to top until released""", 'Line 2: TemporaryLayerAction can only bind to On Hold') + and assert_attribute_error("""R0, K0: A, switch to top until released""", 'Line 1: TemporaryLayerAction can only bind to On Hold') + + # Toggle Action + and assert_attribute_error('R0, K0: Toggle', 'Line 1: Toggle action requires layer parameter') + + # Leave Action + and assert_attribute_error('R0, K0: leave', 'Line 1: Leave action requires layer parameter') + + # Reset Keyboard Action + and assert_attribute_error('R0, K0: reset keyboard abc', 'Line 1: Reset Keyboard action shouldn\'t have any parameters') + + # Bootloader Action + and assert_attribute_error('R0, K0: bootloader abc', 'Line 1: Bootloader action shouldn\'t have any parameters') + + # Home Action + and assert_attribute_error('R0, K0: home abc', 'Line 1: Home action shouldn\'t have any parameters') + + # Nothing Action + and assert_attribute_error('R0, K0: nothing abc', 'Line 1: Nothing action shouldn\'t have any parameters') + + # Pass Through Action + and assert_attribute_error('R0, K0: pass through abc', 'Line 1: Pass through action shouldn\'t have any parameters') + + # Reload Key Maps Action + and assert_attribute_error('R0, K0: reload key maps abc', 'Line 1: Reload Key Maps action shouldn\'t have any parameters') + + # Type Action + and assert_attribute_error('R0, K0: type', 'Line 1: Type action missing text parameter') + and assert_attribute_error('R0, K0: type quickly "abc"', 'Line 1: Type action\'s first parameter must be quoted text') + and assert_attribute_error('R0, K0: type "abc" quickly slowly', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" quickly slowly') + and assert_attribute_error('R0, K0: type "abc" at human speed slowly', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" at human speed slowly') + and assert_attribute_error('R0, K0: type "abc" 100ms slowly', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" 100ms slowly') + and assert_attribute_error('R0, K0: type "abc" quickly at human speed', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" quickly at human speed') + and assert_attribute_error('R0, K0: type "abc" quickly 100ms', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" quickly 100ms') + and assert_attribute_error('R0, K0: type "abc" 100ms at human speed', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" 100ms at human speed') + and assert_attribute_error('R0, K0: type "abc" 100ms 100ms', 'Line 1: Multiple speeds set for Type action. Please select one.\n\ttype "abc" 100ms 100ms') + and assert_attribute_error('R0, K0: type "abc" 100', 'Line 1: time provided without units: \'100\'') + and assert_attribute_error('R0, K0: type "abc" seconds', 'Line 1: unit provided without time: \'seconds\'') + ) + +def test_large(): + source = """R4, K5: + On click: A+A + # On click: A+A, type, Switch to ab a s, Type "ab[SINGLE QUOTE][RETURN][DOUBLE QUOTES]cd" repeatedly quickly, Toggle as, Leave asdfasdf adf a sasdf, Reset Keyboard, bootloader, Home, Nothing, pass Through, Reload Key Maps + On hold: A+A, B, C, D+A, E, F,type "asdfa" slowly + + +R0, K0: ESC +#R0, K0: +# On click: Click WINDOWS + A +# On double-click: Type "Hello # world" slowly # Try to confuse # "The # parser" lol +# On hold: Switch to Layer 2 until released + +other keys fall through +block other keys + +R0, K1: 1 +R0, K2: 2 +R0, K3: 3 +R0, K4: 4 +R0, K5: 5 +R0, K6: 6 +R0, K7: 7 +R0, K8: 8 +R0, K9: 9 +R0, K10: 0 +R0, K11: EQUALS +R1, K0: TAB +R1, K1: Q +R1, K2: W +R1, K3: E +R1, K4: R +R1, K5: T +R1, K6: Y +R1, K7: U +R1, K8: I +R1, K9: O +R1, K10: P +R1, K11: MINUS # TODO: Add alias for HYPHEN and DASH +R2, K0: CAPSLOCK # This should auto-detect to left windows +R2, K1: A +R2, K2: S +R2, K3: D +R2, K4: F +R2, K5: G +R2, K6: H +R2, K7: J +R2, K8: K +R2, K9: L +R2, K10: SEMICOLON +R2, K11: QUOTE +R3, K0: SHIFT # This should auto-detect to left shift +R3, K1: Z +R3, K2: X +R3, K3: C +R3, K4: V +R3, K5: B +R3, K6: N +R3, K7: M +R3, K8: COMMA +R3, K9: PERIOD +R3, K10: UP ARROW +R3, K11: DOWN ARROW # This should auto-detect to right shift +R4, K0: WINDOWS +R4, K1: LEFT ALT + 3 +R4, K2: LEFT CONTROL +R4, K3: SPACE + +R4, K0: Type "QUAG,SIRE" repeatedly +R4, K0: F+R, wait 200ms, type "cmd", click ENTER + +R4, K4: BACKSPACE +R4, K5: + On click: Toggle Function Layer + On hold: Switch to Function Layer until released +R4, K6: Reset Keyboard +R4, K7: LEFT ARROW +R4, K8: RIGHT ARROW + +# TODO: Commented-out lines with another hash inside them confuse the parser +# This comment is here to confuse the parser +""" + layer = FakeLayer() + parse_source(source, layer) + + sequence_1 = SequenceAction() + sequence_1.sequence.append(GenericKeyAction([key_names['A'], key_names['A']])) + sequence_1.sequence.append(GenericKeyAction(key_names['B'])) + sequence_1.sequence.append(GenericKeyAction(key_names['C'])) + sequence_1.sequence.append(GenericKeyAction([key_names['D'], key_names['A']])) + sequence_1.sequence.append(GenericKeyAction(key_names['E'])) + sequence_1.sequence.append(GenericKeyAction(key_names['F'])) + sequence_1.sequence.append(StringTyperAction('asdfa', 0.2)) + + sequence_2 = SequenceAction() + sequence_2.sequence.append(GenericKeyAction([key_names['F'], key_names['R']])) + sequence_2.sequence.append(DelayAction(0.2)) + sequence_2.sequence.append(StringTyperAction('cmd', 0.01)) + sequence_2.sequence.append(ClickKeyAction(key_names['ENTER'])) + + expected_bindings = [ + (4, 5, GenericKeyAction([key_names['A'], key_names['A']]), 'on click'), + (4, 5, sequence_1, 'on hold'), + (0, 0, GenericKeyAction(key_names['ESC']), 'standard'), + (0, 1, GenericKeyAction(key_names['1']), 'standard'), + (0, 2, GenericKeyAction(key_names['2']), 'standard'), + (0, 3, GenericKeyAction(key_names['3']), 'standard'), + (0, 4, GenericKeyAction(key_names['4']), 'standard'), + (0, 5, GenericKeyAction(key_names['5']), 'standard'), + (0, 6, GenericKeyAction(key_names['6']), 'standard'), + (0, 7, GenericKeyAction(key_names['7']), 'standard'), + (0, 8, GenericKeyAction(key_names['8']), 'standard'), + (0, 9, GenericKeyAction(key_names['9']), 'standard'), + (0, 10, GenericKeyAction(key_names['0']), 'standard'), + (0, 11, GenericKeyAction(key_names['EQUALS']), 'standard'), + (1, 0, GenericKeyAction(key_names['TAB']), 'standard'), + (1, 1, GenericKeyAction(key_names['Q']), 'standard'), + (1, 2, GenericKeyAction(key_names['W']), 'standard'), + (1, 3, GenericKeyAction(key_names['E']), 'standard'), + (1, 4, GenericKeyAction(key_names['R']), 'standard'), + (1, 5, GenericKeyAction(key_names['T']), 'standard'), + (1, 6, GenericKeyAction(key_names['Y']), 'standard'), + (1, 7, GenericKeyAction(key_names['U']), 'standard'), + (1, 8, GenericKeyAction(key_names['I']), 'standard'), + (1, 9, GenericKeyAction(key_names['O']), 'standard'), + (1, 10, GenericKeyAction(key_names['P']), 'standard'), + (1, 11, GenericKeyAction(key_names['MINUS']), 'standard'), + (2, 0, GenericKeyAction(key_names['CAPSLOCK']), 'standard'), + (2, 1, GenericKeyAction(key_names['A']), 'standard'), + (2, 2, GenericKeyAction(key_names['S']), 'standard'), + (2, 3, GenericKeyAction(key_names['D']), 'standard'), + (2, 4, GenericKeyAction(key_names['F']), 'standard'), + (2, 5, GenericKeyAction(key_names['G']), 'standard'), + (2, 6, GenericKeyAction(key_names['H']), 'standard'), + (2, 7, GenericKeyAction(key_names['J']), 'standard'), + (2, 8, GenericKeyAction(key_names['K']), 'standard'), + (2, 9, GenericKeyAction(key_names['L']), 'standard'), + (2, 10, GenericKeyAction(key_names['SEMICOLON']), 'standard'), + (2, 11, GenericKeyAction(key_names['QUOTE']), 'standard'), + (3, 0, GenericKeyAction(key_names['SHIFT']), 'standard'), + (3, 1, GenericKeyAction(key_names['Z']), 'standard'), + (3, 2, GenericKeyAction(key_names['X']), 'standard'), + (3, 3, GenericKeyAction(key_names['C']), 'standard'), + (3, 4, GenericKeyAction(key_names['V']), 'standard'), + (3, 5, GenericKeyAction(key_names['B']), 'standard'), + (3, 6, GenericKeyAction(key_names['N']), 'standard'), + (3, 7, GenericKeyAction(key_names['M']), 'standard'), + (3, 8, GenericKeyAction(key_names['COMMA']), 'standard'), + (3, 9, GenericKeyAction(key_names['PERIOD']), 'standard'), + (3, 10, GenericKeyAction(key_names['UPARROW']), 'standard'), + (3, 11, GenericKeyAction(key_names['DOWNARROW']), 'standard'), + (4, 0, GenericKeyAction(key_names['WINDOWS']), 'standard'), + (4, 1, GenericKeyAction([key_names['LEFTALT'], key_names['3']]), 'standard'), + (4, 2, GenericKeyAction(key_names['LEFTCONTROL']), 'standard'), + (4, 3, GenericKeyAction(key_names['SPACE']), 'standard'), + (4, 0, StringTyperAction("QUAG,SIRE", 0.01), 'standard'), + (4, 0, sequence_2, 'standard'), + (4, 4, GenericKeyAction(key_names['BACKSPACE']), 'standard'), + (4, 5, ToggleLayerAction('function layer'), 'on click'), + (4, 5, TemporaryLayerAction('function layer'), 'on hold'), + (4, 6, ResetKeebAction(), 'standard'), + (4, 7, GenericKeyAction(key_names['LEFTARROW']), 'standard'), + (4, 8, GenericKeyAction(key_names['RIGHTARROW']), 'standard'), + ] + + return assert_bindings_match(expected_bindings, layer.key_bindings) + +test_cases = [ + test_basic, + test_comments, + test_events, + test_fall_through, + test_block, + test_action_sequence, + test_action_press, + test_action_release, + test_action_click, + test_action_wait, + test_action_switch_to, + test_action_temporary_switch_to, + test_action_toggle, + test_action_leave, + test_action_type_repeating, + test_action_type_non_repeating, + test_action_reset_keyboard, + test_action_bootloader, + test_action_home, + test_action_nothing, + test_action_pass_through, + test_action_reload_key_maps, + test_error_unterminated_string, + test_error_unrecognized_character, + test_error_bad_action_start_token, + test_error_bad_action_end_token, + test_error_duplicate_plus_token, + test_error_bad_key_def, + test_error_bad_event, + test_error_partial_action_tokens, + test_error_actions, + test_large, +] + +any_failure = False +for case in test_cases: + print('Running test: ' + case.__name__ + '.....') + if case(): + print('\tPassed!') + else: + print('\tFailed!') + any_failure = True + +if any_failure: + print('\n--- Some Tests Failed ---') +else: + print('\n--- All Tests Passed ---')