#!/usr/bin/env python3
'''
KLL Parsing Expressions
This file contains various parsing rules and processors used by funcparserlib for KLL
REMEMBER: When editing parser BNF-like expressions, order matters. Specifically lexer tokens and parser |
'''
# Parser doesn't play nice with linters, disable some checks
# pylint: disable=no-self-argument, too-many-public-methods, no-self-use, bad-builtin
# Copyright (C) 2016 by Jacob Alexander
#
# This file is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file. If not, see .
### Imports ###
from common.hid_dict import kll_hid_lookup_dictionary
from common.id import (
AnimationId, AnimationFrameId,
CapArgId, CapId,
HIDId,
NoneId,
PixelId, PixelLayerId,
ScanCodeId
)
from common.modifier import AnimationModifierList
from common.schedule import AnalogScheduleParam, ScheduleParam, Time
from funcparserlib.lexer import Token
from funcparserlib.parser import (some, a, many, oneplus, skip, maybe)
### Decorators ###
## Print Decorator Variables
ERROR = '\033[5;1;31mERROR\033[0m:'
WARNING = '\033[5;1;33mWARNING\033[0m:'
### Classes ###
## Parsing Functions
class Make:
'''
Collection of parse string interpreters
'''
def scanCode( token ):
'''
Converts a raw scan code string into an ScanCodeId /w integer
S0x10 -> 16
'''
if isinstance( token, int ):
return ScanCodeId( token )
else:
return ScanCodeId( int( token[1:], 0 ) )
def hidCode( type, token ):
'''
Convert a given raw hid token string to an integer /w a type
U"Enter" -> USB, Enter(0x28)
'''
# If already converted to a HIDId, just return
if isinstance( token, HIDId ):
return token
# If first character is a U or I, strip
if token[0] == "U" or token[0] == "I":
token = token[1:]
# CONS specifier
elif 'CONS' in token:
token = token[4:]
# SYS specifier
elif 'SYS' in token:
token = token[3:]
# If using string representation of USB Code, do lookup, case-insensitive
if '"' in token:
try:
hidCode = kll_hid_lookup_dictionary[ type ][ token[1:-1].upper() ][1]
except LookupError as err:
print ( "{0} {1} is an invalid USB HID Code Lookup...".format( ERROR, err ) )
raise
else:
# Already tokenized
if (
type == 'USBCode' and token[0] == 'USB'
or
type == 'SysCode' and token[0] == 'SYS'
or
type == 'ConsCode' and token[0] == 'CONS'
or
type == 'IndCode' and token[0] == 'IND'
):
hidCode = token[1]
# Convert
else:
hidCode = int( token, 0 )
return HIDId( type, hidCode )
def usbCode( token ):
'''
Convert a given raw USB Keyboard hid token string to an integer /w a type
U"Enter" -> USB, Enter(0x28)
'''
return Make.hidCode( 'USBCode', token )
def consCode( token ):
'''
Convert a given raw Consumer Control hid token string to an integer /w a type
'''
return Make.hidCode( 'ConsCode', token )
def sysCode( token ):
'''
Convert a given raw System Control hid token string to an integer /w a type
'''
return Make.hidCode( 'SysCode', token )
def indCode( token ):
'''
Convert a given raw Indicator hid token string to an integer /w a type
'''
return Make.hidCode( 'IndCode', token )
def animation( name ):
'''
Converts a raw animation value into an AnimationId /w name
A"myname" -> myname
'''
if name[0] == "A":
return AnimationId( name[2:-1] )
else:
return AnimationId( name )
def animationTrigger( animation, frame_indices ):
'''
Generate either an AnimationId or an AnimationFrameId
frame_indices indicate that this is an AnimationFrameId
'''
trigger_list = []
# AnimationFrameId
if len( frame_indices ) > 0:
for index in frame_indices:
trigger_list.append( [ [ AnimationFrameId( animation, index ) ] ] )
# AnimationId
else:
trigger_list.append( [ [ AnimationId( animation ) ] ] )
return trigger_list
def animationCapability( animation, modifiers ):
'''
Apply modifiers to AnimationId
'''
if modifiers is not None:
animation.setModifiers( modifiers )
return [ animation ]
def animationModlist( modifiers ):
'''
Build an AnimationModifierList
Only used for animation data association
'''
modlist = AnimationModifierList()
modlist.setModifiers( modifiers )
return modlist
def pixelCapability( pixels, modifiers ):
'''
Apply modifiers to list of pixels/pixellists
Results in a combination of pixel capabilities
'''
pixelcap_list = []
for pixel in pixels:
pixel.setModifiers( modifiers )
pixelcap_list.append( pixel )
return pixelcap_list
def pixel( token ):
'''
Converts a raw pixel value into a PixelId /w integer
P0x3 -> 3
'''
if isinstance( token, int ):
return PixelId( token )
else:
return PixelId( int( token[1:], 0 ) )
def pixel_list( pixel_list ):
'''
Converts a list a numbers into a list of PixelIds
'''
pixels = []
for pixel in pixel_list:
pixels.append( PixelId( pixel ) )
return pixels
def pixelLayer( token ):
'''
Converts a raw pixel layer value into a PixelLayerId /w integer
PL0x3 -> 3
'''
if isinstance( token, int ):
return PixelLayerId( token )
else:
return PixelLayerId( int( token[2:], 0 ) )
def pixelLayer_list( layer_list ):
'''
Converts a list a numbers into a list of PixelLayerIds
'''
layers = []
for layer in layer_list:
layers.append( PixelLayerId( layer ) )
return layers
def pixelchan( pixel_list, chans ):
'''
Apply channels to PixelId
Only one pixel at a time can be mapped, hence pixel_list[0]
'''
pixel = pixel_list[0]
pixel.setChannels( chans )
return pixel
def pixelmod( pixels, modifiers ):
'''
Apply modifiers to list of pixels/pixellists
Results in a combination of pixel capabilities
'''
pixelcap_list = []
for pixel in pixels:
pixel.setModifiers( modifiers )
pixelcap_list.append( pixel )
return pixelcap_list
def position( token ):
'''
Physical position split
x:20 -> (x, 20)
'''
return token.split(':')
def usbCode_number( token ):
'''
USB Keyboard HID Code lookup
'''
return HIDId( 'USBCode', token )
def consCode_number( token ):
'''
Consumer Control HID Code lookup
'''
return HIDId( 'ConsCode', token )
def sysCode_number( token ):
'''
System Control HID Code lookup
'''
return HIDId( 'SysCode', token )
def indCode_number( token ):
'''
Indicator HID Code lookup
'''
return HIDId( 'IndCode', token )
def none( token ):
'''
Replace key-word with NoneId specifier (which indicates a noneOut capability)
'''
return [[[NoneId()]]]
def seqString( token ):
'''
Converts sequence string to a sequence of combinations
'Ab' -> U"Shift" + U"A", U"B"
'''
# TODO - Add locale support
# Shifted Characters, and amount to move by to get non-shifted version
# US ANSI
shiftCharacters = (
( "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x20 ),
( "+", 0x12 ),
( "&(", 0x11 ),
( "!#$%", 0x10 ),
( "*", 0x0E ),
( ")", 0x07 ),
( '"', 0x05 ),
( ":", 0x01 ),
( "@", -0x0E ),
( "<>?", -0x10 ),
( "~", -0x1E ),
( "{}|", -0x20 ),
( "^", -0x28 ),
( "_", -0x32 ),
)
listOfLists = []
shiftKey = kll_hid_lookup_dictionary['USBCode']["SHIFT"]
# Creates a list of USB codes from the string: sequence (list) of combos (lists)
for char in token[1:-1]:
processedChar = char
# Whether or not to create a combo for this sequence with a shift
shiftCombo = False
# Depending on the ASCII character, convert to single character or Shift + character
for pair in shiftCharacters:
if char in pair[0]:
shiftCombo = True
processedChar = chr( ord( char ) + pair[1] )
break
# Do KLL HID Lookup on non-shifted character
# NOTE: Case-insensitive, which is why the shift must be pre-computed
usb_code = kll_hid_lookup_dictionary['USBCode'][ processedChar.upper() ]
# Create Combo for this character, add shift key if shifted
charCombo = []
if shiftCombo:
charCombo = [ [ HIDId( 'USBCode', shiftKey[1] ) ] ]
charCombo.append( [ HIDId( 'USBCode', usb_code[1] ) ] )
# Add to list of lists
listOfLists.append( charCombo )
return listOfLists
def string( token ):
'''
Converts a raw string to a Python string
"this string" -> this string
'''
return token[1:-1]
def unseqString( token ):
'''
Converts a raw sequence string to a Python string
'this string' -> this string
'''
return token[1:-1]
def number( token ):
'''
Convert string number to Python integer
'''
return int( token, 0 )
def timing( token ):
'''
Convert raw timing parameter to integer time and determine units
1ms -> 1, ms
'''
# Find ms, us, or s
if 'ms' in token:
unit = 'ms'
num = token.split('m')[0]
elif 'us' in token:
unit = 'us'
num = token.split('u')[0]
elif 'ns' in token:
unit = 'ns'
num = token.split('n')[0]
elif 's' in token:
unit = 's'
num = token.split('s')[0]
else:
print ( "{0} cannot find timing unit in token '{1}'".format( ERROR, token ) )
return Time( float( num ), unit )
def specifierTiming( timing ):
'''
When only timing is given, infer state at a later stage from the context of the mapping
'''
return ScheduleParam( None, timing )
def specifierState( state, timing=None ):
'''
Generate a Schedule Parameter
Automatically mutates itself into the correct object type
'''
return ScheduleParam( state, timing )
def specifierAnalog( value ):
'''
Generate an Analog Schedule Parameter
'''
return AnalogScheduleParam( value )
def specifierUnroll( identifier, schedule_params ):
'''
Unroll specifiers into the trigger/result identifier
First, combine all Schedule Parameters into a Schedul
Then attach Schedule to the identifier
If the identifier is a list, then iterate through them
and apply the schedule to each
'''
# Check if this is a list of identifiers
if isinstance( identifier, list ):
for ident in identifier:
ident.setSchedule( schedule_params )
return identifier
else:
identifier.setSchedule( schedule_params )
return [ identifier ]
# Range can go from high to low or low to high
def scanCode_range( rangeVals ):
'''
Scan Code range expansion
S[0x10-0x12] -> S0x10, S0x11, S0x12
'''
start = rangeVals[0]
end = rangeVals[1]
# Swap start, end if start is greater than end
if start > end:
start, end = end, start
# Iterate from start to end, and generate the range
values = list( range( start, end + 1 ) )
# Generate ScanCodeIds
return [ ScanCodeId( v ) for v in values ]
# Range can go from high to low or low to high
# Warn on 0-9 for USBCodes (as this does not do what one would expect) TODO
# Lookup USB HID tags and convert to a number
def hidCode_range( type, rangeVals ):
'''
HID Code range expansion
U["A"-"C"] -> U"A", U"B", U"C"
'''
# Check if already integers
if isinstance( rangeVals[0], int ):
start = rangeVals[0]
else:
start = Make.hidCode( type, rangeVals[0] ).uid
if isinstance( rangeVals[1], int ):
end = rangeVals[1]
else:
end = Make.hidCode( type, rangeVals[1] ).uid
# Swap start, end if start is greater than end
if start > end:
start, end = end, start
# Iterate from start to end, and generate the range
listRange = list( range( start, end + 1 ) )
# Convert each item in the list to a tuple
for item in range( len( listRange ) ):
listRange[ item ] = HIDId( type, listRange[ item ] )
return listRange
def usbCode_range( rangeVals ):
'''
USB Keyboard HID Code range expansion
'''
return Make.hidCode_range( 'USBCode', rangeVals )
def sysCode_range( rangeVals ):
'''
System Control HID Code range expansion
'''
return Make.hidCode_range( 'SysCode', rangeVals )
def consCode_range( rangeVals ):
'''
Consumer Control HID Code range expansion
'''
return Make.hidCode_range( 'ConsCode', rangeVals )
def indCode_range( rangeVals ):
'''
Indicator HID Code range expansion
'''
return Make.hidCode_range( 'IndCode', rangeVals )
def range( start, end ):
'''
Converts a start and end points of a range to a list of numbers
Can go low to high or high to low
'''
# High to low
if end < start:
return list( range( end, start + 1 ) )
# Low to high
return list( range( start, end + 1 ) )
def capArg( argument, width=None ):
'''
Converts a capability argument:width to a CapArgId
If no width is specified, it is ignored
'''
return CapArgId( argument, width )
def capUsage( name, arguments ):
'''
Converts a capability tuple, argument list to a CapId Usage
'''
return CapId( name, 'Usage', arguments )
### Rules ###
## Base Rules
const = lambda x: lambda _: x
unarg = lambda f: lambda x: f(*x)
flatten = lambda list: sum( list, [] )
tokenValue = lambda x: x.value
tokenType = lambda t: some( lambda x: x.type == t ) >> tokenValue
operator = lambda s: a( Token( 'Operator', s ) ) >> tokenValue
parenthesis = lambda s: a( Token( 'Parenthesis', s ) ) >> tokenValue
bracket = lambda s: a( Token( 'Bracket', s ) ) >> tokenValue
eol = a( Token( 'EndOfLine', ';' ) )
def maybeFlatten( items ):
'''
Iterate through top-level lists
Flatten, only if the element is also a list
[[1,2],3,[[4,5]]] -> [1,2,3,[4,5]]
'''
new_list = []
for elem in items:
# Flatten only if a list
if isinstance( elem, list ):
new_list.extend( elem )
else:
new_list.append( elem )
return new_list
def listElem( item ):
'''
Convert to a list element
'''
return [ item ]
def listToTuple( items ):
'''
Convert list to a tuple
'''
return tuple( items )
def oneLayerFlatten( items ):
'''
Flatten only the top layer (list of lists of ...)
'''
mainList = []
for sublist in items:
for item in sublist:
mainList.append( item )
return mainList
def optionExpansion( sequences ):
'''
Expand ranges of values in the 3rd dimension of the list, to a list of 2nd lists
i.e. [ sequence, [ combo, [ range ] ] ] --> [ [ sequence, [ combo ] ],