]> git.donarmstrong.com Git - kiibohd-kll.git/blob - kll.py
add don layers
[kiibohd-kll.git] / kll.py
1 #!/usr/bin/env python3
2 '''
3 KLL Compiler
4 Keyboard Layout Langauge
5 '''
6
7 # Copyright (C) 2014-2016 by Jacob Alexander
8 #
9 # This file is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This file is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this file.  If not, see <http://www.gnu.org/licenses/>.
21
22 ### Imports ###
23
24 import argparse
25 import importlib
26 import os
27 import sys
28
29 from re       import VERBOSE
30
31 from kll_lib.containers import *
32 from kll_lib.hid_dict   import *
33
34 from funcparserlib.lexer  import make_tokenizer, Token, LexerError
35 from funcparserlib.parser import (some, a, many, oneplus, finished, maybe, skip, NoParseError)
36
37
38
39 ### Decorators ###
40
41 ## Print Decorator Variables
42 ERROR = '\033[5;1;31mERROR\033[0m:'
43
44
45 ## Python Text Formatting Fixer...
46 ##  Because the creators of Python are averse to proper capitalization.
47 textFormatter_lookup = {
48         "usage: "            : "Usage: ",
49         "optional arguments" : "Optional Arguments",
50 }
51
52 def textFormatter_gettext( s ):
53         return textFormatter_lookup.get( s, s )
54
55 argparse._ = textFormatter_gettext
56
57
58
59 ### Argument Parsing ###
60
61 def checkFileExists( filename ):
62         if not os.path.isfile( filename ):
63                 print ( "{0} {1} does not exist...".format( ERROR, filename ) )
64                 sys.exit( 1 )
65
66 def processCommandLineArgs():
67         # Setup argument processor
68         pArgs = argparse.ArgumentParser(
69                 usage="%(prog)s [options] <file1>...",
70                 description="Generates .h file state tables and pointer indices from KLL .kll files.",
71                 epilog="Example: {0} mykeyboard.kll -d colemak.kll -p hhkbpro2.kll -p symbols.kll".format( os.path.basename( sys.argv[0] ) ),
72                 formatter_class=argparse.RawTextHelpFormatter,
73                 add_help=False,
74         )
75
76         # Positional Arguments
77         pArgs.add_argument( 'files', type=str, nargs='+',
78                 help=argparse.SUPPRESS ) # Suppressed help output, because Python output is verbosely ugly
79
80         # Optional Arguments
81         pArgs.add_argument( '-b', '--backend', type=str, default="kiibohd",
82                 help="Specify target backend for the KLL compiler.\n"
83                 "Default: kiibohd\n"
84                 "Options: kiibohd, json" )
85         pArgs.add_argument( '-d', '--default', type=str, nargs='+',
86                 help="Specify .kll files to layer on top of the default map to create a combined map." )
87         pArgs.add_argument( '-p', '--partial', type=str, nargs='+', action='append',
88                 help="Specify .kll files to generate partial map, multiple files per flag.\n"
89                 "Each -p defines another partial map.\n"
90                 "Base .kll files (that define the scan code maps) must be defined for each partial map." )
91         pArgs.add_argument( '-t', '--templates', type=str, nargs='+',
92                 help="Specify template used to generate the keymap.\n"
93                 "Default: <backend specific>" )
94         pArgs.add_argument( '-o', '--outputs', type=str, nargs='+',
95                 help="Specify output file. Writes to current working directory by default.\n"
96                 "Default: <backend specific>" )
97         pArgs.add_argument( '-h', '--help', action="help",
98                 help="This message." )
99
100         # Process Arguments
101         args = pArgs.parse_args()
102
103         # Parameters
104         baseFiles = args.files
105         defaultFiles = args.default
106         partialFileSets = args.partial
107         if defaultFiles is None:
108                 defaultFiles = []
109         if partialFileSets is None:
110                 partialFileSets = [[]]
111
112         # Check file existance
113         for filename in baseFiles:
114                 checkFileExists( filename )
115
116         for filename in defaultFiles:
117                 checkFileExists( filename )
118
119         for partial in partialFileSets:
120                 for filename in partial:
121                         checkFileExists( filename )
122
123         return (baseFiles, defaultFiles, partialFileSets, args.backend, args.templates, args.outputs)
124
125
126
127 ### Tokenizer ###
128
129 def tokenize( string ):
130         """str -> Sequence(Token)"""
131
132         # Basic Tokens Spec
133         specs = [
134                 ( 'Comment',          ( r' *#.*', ) ),
135                 ( 'Space',            ( r'[ \t\r\n]+', ) ),
136                 ( 'USBCode',          ( r'U(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
137                 ( 'USBCodeStart',     ( r'U\[', ) ),
138                 ( 'ConsCode',         ( r'CONS(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
139                 ( 'ConsCodeStart',    ( r'CONS\[', ) ),
140                 ( 'SysCode',          ( r'SYS(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
141                 ( 'SysCodeStart',     ( r'SYS\[', ) ),
142                 ( 'LedCode',          ( r'LED(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
143                 ( 'LedCodeStart',     ( r'LED\[', ) ),
144                 ( 'ScanCode',         ( r'S((0x[0-9a-fA-F]+)|([0-9]+))', ) ),
145                 ( 'ScanCodeStart',    ( r'S\[', ) ),
146                 ( 'CodeEnd',          ( r'\]', ) ),
147                 ( 'String',           ( r'"[^"]*"', ) ),
148                 ( 'SequenceString',   ( r"'[^']*'", ) ),
149                 ( 'Operator',         ( r'=>|:\+|:-|::|:|=', ) ),
150                 ( 'Number',           ( r'(-[ \t]*)?((0x[0-9a-fA-F]+)|(0|([1-9][0-9]*)))', VERBOSE ) ),
151                 ( 'Comma',            ( r',', ) ),
152                 ( 'Dash',             ( r'-', ) ),
153                 ( 'Plus',             ( r'\+', ) ),
154                 ( 'Parenthesis',      ( r'\(|\)', ) ),
155                 ( 'None',             ( r'None', ) ),
156                 ( 'Name',             ( r'[A-Za-z_][A-Za-z_0-9]*', ) ),
157                 ( 'VariableContents', ( r'''[^"' ;:=>()]+''', ) ),
158                 ( 'EndOfLine',        ( r';', ) ),
159         ]
160
161         # Tokens to filter out of the token stream
162         useless = ['Space', 'Comment']
163
164         tokens = make_tokenizer( specs )
165         return [x for x in tokens( string ) if x.type not in useless]
166
167
168
169 ### Parsing ###
170
171 ## Map Arrays
172 macros_map        = Macros()
173 variables_dict    = Variables()
174 capabilities_dict = Capabilities()
175
176
177 ## Parsing Functions
178
179 def make_scanCode( token ):
180         scanCode = int( token[1:], 0 )
181         # Check size, to make sure it's valid
182         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
183         #if scanCode > 0xFF:
184         #       print ( "{0} ScanCode value {1} is larger than 255".format( ERROR, scanCode ) )
185         #       raise
186         return scanCode
187
188 def make_hidCode( type, token ):
189         # If first character is a U, strip
190         if token[0] == "U":
191                 token = token[1:]
192         # CONS specifier
193         elif 'CONS' in token:
194                 token = token[4:]
195         # SYS specifier
196         elif 'SYS' in token:
197                 token = token[3:]
198
199         # If using string representation of USB Code, do lookup, case-insensitive
200         if '"' in token:
201                 try:
202                         hidCode = kll_hid_lookup_dictionary[ type ][ token[1:-1].upper() ][1]
203                 except LookupError as err:
204                         print ( "{0} {1} is an invalid USB HID Code Lookup...".format( ERROR, err ) )
205                         raise
206         else:
207                 # Already tokenized
208                 if type == 'USBCode' and token[0] == 'USB' or type == 'SysCode' and token[0] == 'SYS' or type == 'ConsCode' and token[0] == 'CONS':
209                         hidCode = token[1]
210                 # Convert
211                 else:
212                         hidCode = int( token, 0 )
213
214         # Check size if a USB Code, to make sure it's valid
215         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
216         #if type == 'USBCode' and hidCode > 0xFF:
217         #       print ( "{0} USBCode value {1} is larger than 255".format( ERROR, hidCode ) )
218         #       raise
219
220         # Return a tuple, identifying which type it is
221         if type == 'USBCode':
222                 return make_usbCode_number( hidCode )
223         elif type == 'ConsCode':
224                 return make_consCode_number( hidCode )
225         elif type == 'SysCode':
226                 return make_sysCode_number( hidCode )
227
228         print ( "{0} Unknown HID Specifier '{1}'".format( ERROR, type ) )
229         raise
230
231 def make_usbCode( token ):
232         return make_hidCode( 'USBCode', token )
233
234 def make_consCode( token ):
235         return make_hidCode( 'ConsCode', token )
236
237 def make_sysCode( token ):
238         return make_hidCode( 'SysCode', token )
239
240 def make_hidCode_number( type, token ):
241         lookup = {
242                 'ConsCode' : 'CONS',
243                 'SysCode'  : 'SYS',
244                 'USBCode'  : 'USB',
245         }
246         return ( lookup[ type ], token )
247
248 def make_usbCode_number( token ):
249         return make_hidCode_number( 'USBCode', token )
250
251 def make_consCode_number( token ):
252         return make_hidCode_number( 'ConsCode', token )
253
254 def make_sysCode_number( token ):
255         return make_hidCode_number( 'SysCode', token )
256
257  # Replace key-word with None specifier (which indicates a noneOut capability)
258 def make_none( token ):
259         return [[[('NONE', 0)]]]
260
261 def make_seqString( token ):
262         # Shifted Characters, and amount to move by to get non-shifted version
263         # US ANSI
264         shiftCharacters = (
265                 ( "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x20 ),
266                 ( "+",       0x12 ),
267                 ( "&(",      0x11 ),
268                 ( "!#$%",    0x10 ),
269                 ( "*",       0x0E ),
270                 ( ")",       0x07 ),
271                 ( '"',       0x05 ),
272                 ( ":",       0x01 ),
273                 ( "@",      -0x0E ),
274                 ( "<>?",    -0x10 ),
275                 ( "~",      -0x1E ),
276                 ( "{}|",    -0x20 ),
277                 ( "^",      -0x28 ),
278                 ( "_",      -0x32 ),
279         )
280
281         listOfLists = []
282         shiftKey = kll_hid_lookup_dictionary['USBCode']["SHIFT"]
283
284         # Creates a list of USB codes from the string: sequence (list) of combos (lists)
285         for char in token[1:-1]:
286                 processedChar = char
287
288                 # Whether or not to create a combo for this sequence with a shift
289                 shiftCombo = False
290
291                 # Depending on the ASCII character, convert to single character or Shift + character
292                 for pair in shiftCharacters:
293                         if char in pair[0]:
294                                 shiftCombo = True
295                                 processedChar = chr( ord( char ) + pair[1] )
296                                 break
297
298                 # Do KLL HID Lookup on non-shifted character
299                 # NOTE: Case-insensitive, which is why the shift must be pre-computed
300                 usbCode = kll_hid_lookup_dictionary['USBCode'][ processedChar.upper() ]
301
302                 # Create Combo for this character, add shift key if shifted
303                 charCombo = []
304                 if shiftCombo:
305                         charCombo = [ [ shiftKey ] ]
306                 charCombo.append( [ usbCode ] )
307
308                 # Add to list of lists
309                 listOfLists.append( charCombo )
310
311         return listOfLists
312
313 def make_string( token ):
314         return token[1:-1]
315
316 def make_unseqString( token ):
317         return token[1:-1]
318
319 def make_number( token ):
320         return int( token, 0 )
321
322 # Range can go from high to low or low to high
323 def make_scanCode_range( rangeVals ):
324         start = rangeVals[0]
325         end   = rangeVals[1]
326
327         # Swap start, end if start is greater than end
328         if start > end:
329                 start, end = end, start
330
331         # Iterate from start to end, and generate the range
332         return list( range( start, end + 1 ) )
333
334 # Range can go from high to low or low to high
335 # Warn on 0-9 for USBCodes (as this does not do what one would expect) TODO
336 # Lookup USB HID tags and convert to a number
337 def make_hidCode_range( type, rangeVals ):
338         # Check if already integers
339         if isinstance( rangeVals[0], int ):
340                 start = rangeVals[0]
341         else:
342                 start = make_hidCode( type, rangeVals[0] )[1]
343
344         if isinstance( rangeVals[1], int ):
345                 end = rangeVals[1]
346         else:
347                 end = make_hidCode( type, rangeVals[1] )[1]
348
349         # Swap start, end if start is greater than end
350         if start > end:
351                 start, end = end, start
352
353         # Iterate from start to end, and generate the range
354         listRange = list( range( start, end + 1 ) )
355
356         # Convert each item in the list to a tuple
357         for item in range( len( listRange ) ):
358                 listRange[ item ] = make_hidCode_number( type, listRange[ item ] )
359         return listRange
360
361 def make_usbCode_range( rangeVals ):
362         return make_hidCode_range( 'USBCode', rangeVals )
363
364 def make_sysCode_range( rangeVals ):
365         return make_hidCode_range( 'SysCode', rangeVals )
366
367 def make_consCode_range( rangeVals ):
368         return make_hidCode_range( 'ConsCode', rangeVals )
369
370
371 ## Base Rules
372
373 const       = lambda x: lambda _: x
374 unarg       = lambda f: lambda x: f(*x)
375 flatten     = lambda list: sum( list, [] )
376
377 tokenValue  = lambda x: x.value
378 tokenType   = lambda t: some( lambda x: x.type == t ) >> tokenValue
379 operator    = lambda s: a( Token( 'Operator', s ) ) >> tokenValue
380 parenthesis = lambda s: a( Token( 'Parenthesis', s ) ) >> tokenValue
381 eol         = a( Token( 'EndOfLine', ';' ) )
382
383 def listElem( item ):
384         return [ item ]
385
386 def listToTuple( items ):
387         return tuple( items )
388
389 # Flatten only the top layer (list of lists of ...)
390 def oneLayerFlatten( items ):
391         mainList = []
392         for sublist in items:
393                 for item in sublist:
394                         mainList.append( item )
395
396         return mainList
397
398 def capArgExpander( items ):
399         '''
400         Capability arguments may need to be expanded
401         (e.g. 1 16 bit argument needs to be 2 8 bit arguments for the state machine)
402
403         If the number is negative, determine width of the final value, mask to max, subtract,
404         then convert to multiple bytes
405         '''
406         newArgs = []
407         # For each defined argument in the capability definition
408         for arg in range( 0, len( capabilities_dict[ items[0] ][1] ) ):
409                 argLen = capabilities_dict[ items[0] ][1][ arg ][1]
410                 num = items[1][ arg ]
411
412                 # Set last bit if value is negative
413                 if num < 0:
414                         max_val = 2 ** (argLen * 8)
415                         num += max_val
416
417                 # XXX Yes, little endian from how the uC structs work
418                 byteForm = num.to_bytes( argLen, byteorder='little' )
419
420                 # For each sub-argument, split into byte-sized chunks
421                 for byte in range( 0, argLen ):
422                         newArgs.append( byteForm[ byte ] )
423
424         return tuple( [ items[0], tuple( newArgs ) ] )
425
426 # Expand ranges of values in the 3rd dimension of the list, to a list of 2nd lists
427 # i.e. [ sequence, [ combo, [ range ] ] ] --> [ [ sequence, [ combo ] ], <option 2>, <option 3> ]
428 def optionExpansion( sequences ):
429         expandedSequences = []
430
431         # Total number of combinations of the sequence of combos that needs to be generated
432         totalCombinations = 1
433
434         # List of leaf lists, with number of leaves
435         maxLeafList = []
436
437         # Traverse to the leaf nodes, and count the items in each leaf list
438         for sequence in sequences:
439                 for combo in sequence:
440                         rangeLen = len( combo )
441                         totalCombinations *= rangeLen
442                         maxLeafList.append( rangeLen )
443
444         # Counter list to keep track of which combination is being generated
445         curLeafList = [0] * len( maxLeafList )
446
447         # Generate a list of permuations of the sequence of combos
448         for count in range( 0, totalCombinations ):
449                 expandedSequences.append( [] ) # Prepare list for adding the new combination
450                 position = 0
451
452                 # Traverse sequence of combos to generate permuation
453                 for sequence in sequences:
454                         expandedSequences[ -1 ].append( [] )
455                         for combo in sequence:
456                                 expandedSequences[ -1 ][ -1 ].append( combo[ curLeafList[ position ] ] )
457                                 position += 1
458
459                 # Increment combination tracker
460                 for leaf in range( 0, len( curLeafList ) ):
461                         curLeafList[ leaf ] += 1
462
463                         # Reset this position, increment next position (if it exists), then stop
464                         if curLeafList[ leaf ] >= maxLeafList[ leaf ]:
465                                 curLeafList[ leaf ] = 0
466                                 if leaf + 1 < len( curLeafList ):
467                                         curLeafList[ leaf + 1 ] += 1
468
469         return expandedSequences
470
471
472 # Converts USB Codes into Capabilities
473 # These are tuples (<type>, <integer>)
474 def hidCodeToCapability( items ):
475         # Items already converted to variants using optionExpansion
476         for variant in range( 0, len( items ) ):
477                 # Sequence of Combos
478                 for sequence in range( 0, len( items[ variant ] ) ):
479                         for combo in range( 0, len( items[ variant ][ sequence ] ) ):
480                                 if items[ variant ][ sequence ][ combo ][0] in backend.requiredCapabilities.keys():
481                                         try:
482                                                 # Use backend capability name and a single argument
483                                                 items[ variant ][ sequence ][ combo ] = tuple(
484                                                         [ backend.capabilityLookup( items[ variant ][ sequence ][ combo ][0] ),
485                                                         tuple( [ hid_lookup_dictionary[ items[ variant ][ sequence ][ combo ] ] ] ) ]
486                                                 )
487                                         except KeyError:
488                                                 print ( "{0} {1} is an invalid HID lookup value".format( ERROR, items[ variant ][ sequence ][ combo ] ) )
489                                                 sys.exit( 1 )
490         return items
491
492
493 # Convert tuple of tuples to list of lists
494 def listit( t ):
495         return list( map( listit, t ) ) if isinstance( t, ( list, tuple ) ) else t
496
497 # Convert list of lists to tuple of tuples
498 def tupleit( t ):
499         return tuple( map( tupleit, t ) ) if isinstance( t, ( tuple, list ) ) else t
500
501
502 ## Evaluation Rules
503
504 def eval_scanCode( triggers, operator, results ):
505         # Convert to lists of lists of lists to tuples of tuples of tuples
506         # Tuples are non-mutable, and can be used has index items
507         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
508         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
509
510         # Lookup interconnect id (Current file scope)
511         # Default to 0 if not specified
512         if 'ConnectId' not in variables_dict.overallVariables.keys():
513                 id_num = 0
514         else:
515                 id_num = int( variables_dict.overallVariables['ConnectId'] )
516
517         # Iterate over all combinations of triggers and results
518         for sequence in triggers:
519                 # Convert tuple of tuples to list of lists so each element can be modified
520                 trigger = listit( sequence )
521
522                 # Create ScanCode entries for trigger
523                 for seq_index, combo in enumerate( sequence ):
524                         for com_index, scancode in enumerate( combo ):
525                                 trigger[ seq_index ][ com_index ] = macros_map.scanCodeStore.append( ScanCode( scancode, id_num ) )
526
527                 # Convert back to a tuple of tuples
528                 trigger = tupleit( trigger )
529
530                 for result in results:
531                         # Append Case
532                         if operator == ":+":
533                                 macros_map.appendScanCode( trigger, result )
534
535                         # Remove Case
536                         elif operator == ":-":
537                                 macros_map.removeScanCode( trigger, result )
538
539                         # Replace Case
540                         # Soft Replace Case is the same for Scan Codes
541                         elif operator == ":" or operator == "::":
542                                 macros_map.replaceScanCode( trigger, result )
543
544 def eval_usbCode( triggers, operator, results ):
545         # Convert to lists of lists of lists to tuples of tuples of tuples
546         # Tuples are non-mutable, and can be used has index items
547         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
548         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
549
550         # Iterate over all combinations of triggers and results
551         for trigger in triggers:
552                 scanCodes = macros_map.lookupUSBCodes( trigger )
553                 for scanCode in scanCodes:
554                         for result in results:
555                                 # Soft Replace needs additional checking to see if replacement is necessary
556                                 if operator == "::" and not macros_map.softReplaceCheck( scanCode ):
557                                         continue
558
559                                 # Cache assignment until file finishes processing
560                                 macros_map.cacheAssignment( operator, scanCode, result )
561
562 def eval_variable( name, content ):
563         # Content might be a concatenation of multiple data types, convert everything into a single string
564         assigned_content = ""
565         for item in content:
566                 assigned_content += str( item )
567
568         variables_dict.assignVariable( name, assigned_content )
569
570 def eval_capability( name, function, args ):
571         capabilities_dict[ name ] = [ function, args ]
572
573 def eval_define( name, cdefine_name ):
574         variables_dict.defines[ name ] = cdefine_name
575
576 map_scanCode   = unarg( eval_scanCode )
577 map_usbCode    = unarg( eval_usbCode )
578
579 set_variable   = unarg( eval_variable )
580 set_capability = unarg( eval_capability )
581 set_define     = unarg( eval_define )
582
583
584 ## Sub Rules
585
586 usbCode     = tokenType('USBCode') >> make_usbCode
587 scanCode    = tokenType('ScanCode') >> make_scanCode
588 consCode    = tokenType('ConsCode') >> make_consCode
589 sysCode     = tokenType('SysCode') >> make_sysCode
590 none        = tokenType('None') >> make_none
591 name        = tokenType('Name')
592 number      = tokenType('Number') >> make_number
593 comma       = tokenType('Comma')
594 dash        = tokenType('Dash')
595 plus        = tokenType('Plus')
596 content     = tokenType('VariableContents')
597 string      = tokenType('String') >> make_string
598 unString    = tokenType('String') # When the double quotes are still needed for internal processing
599 seqString   = tokenType('SequenceString') >> make_seqString
600 unseqString = tokenType('SequenceString') >> make_unseqString # For use with variables
601
602 # Code variants
603 code_end = tokenType('CodeEnd')
604
605 # Scan Codes
606 scanCode_start     = tokenType('ScanCodeStart')
607 scanCode_range     = number + skip( dash ) + number >> make_scanCode_range
608 scanCode_listElem  = number >> listElem
609 scanCode_innerList = oneplus( ( scanCode_range | scanCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
610 scanCode_expanded  = skip( scanCode_start ) + scanCode_innerList + skip( code_end )
611 scanCode_elem      = scanCode >> listElem
612 scanCode_combo     = oneplus( ( scanCode_expanded | scanCode_elem ) + skip( maybe( plus ) ) )
613 scanCode_sequence  = oneplus( scanCode_combo + skip( maybe( comma ) ) )
614
615 # USB Codes
616 usbCode_start       = tokenType('USBCodeStart')
617 usbCode_number      = number >> make_usbCode_number
618 usbCode_range       = ( usbCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_usbCode_range
619 usbCode_listElemTag = unString >> make_usbCode
620 usbCode_listElem    = ( usbCode_number | usbCode_listElemTag ) >> listElem
621 usbCode_innerList   = oneplus( ( usbCode_range | usbCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
622 usbCode_expanded    = skip( usbCode_start ) + usbCode_innerList + skip( code_end )
623 usbCode_elem        = usbCode >> listElem
624 usbCode_combo       = oneplus( ( usbCode_expanded | usbCode_elem ) + skip( maybe( plus ) ) ) >> listElem
625 usbCode_sequence    = oneplus( ( usbCode_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
626
627 # Cons Codes
628 consCode_start       = tokenType('ConsCodeStart')
629 consCode_number      = number >> make_consCode_number
630 consCode_range       = ( consCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_consCode_range
631 consCode_listElemTag = unString >> make_consCode
632 consCode_listElem    = ( consCode_number | consCode_listElemTag ) >> listElem
633 consCode_innerList   = oneplus( ( consCode_range | consCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
634 consCode_expanded    = skip( consCode_start ) + consCode_innerList + skip( code_end )
635 consCode_elem        = consCode >> listElem
636
637 # Sys Codes
638 sysCode_start       = tokenType('SysCodeStart')
639 sysCode_number      = number >> make_sysCode_number
640 sysCode_range       = ( sysCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_sysCode_range
641 sysCode_listElemTag = unString >> make_sysCode
642 sysCode_listElem    = ( sysCode_number | sysCode_listElemTag ) >> listElem
643 sysCode_innerList   = oneplus( ( sysCode_range | sysCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
644 sysCode_expanded    = skip( sysCode_start ) + sysCode_innerList + skip( code_end )
645 sysCode_elem        = sysCode >> listElem
646
647 # HID Codes
648 hidCode_elem        = usbCode_expanded | usbCode_elem | sysCode_expanded | sysCode_elem | consCode_expanded | consCode_elem
649
650 # Capabilities
651 capFunc_arguments = many( number + skip( maybe( comma ) ) ) >> listToTuple
652 capFunc_elem      = name + skip( parenthesis('(') ) + capFunc_arguments + skip( parenthesis(')') ) >> capArgExpander >> listElem
653 capFunc_combo     = oneplus( ( hidCode_elem | capFunc_elem ) + skip( maybe( plus ) ) ) >> listElem
654 capFunc_sequence  = oneplus( ( capFunc_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
655
656 # Trigger / Result Codes
657 triggerCode_outerList    = scanCode_sequence >> optionExpansion
658 triggerUSBCode_outerList = usbCode_sequence >> optionExpansion >> hidCodeToCapability
659 resultCode_outerList     = ( ( capFunc_sequence >> optionExpansion ) | none ) >> hidCodeToCapability
660
661
662 ## Main Rules
663
664 #| <variable> = <variable contents>;
665 variable_contents   = name | content | string | number | comma | dash | unseqString
666 variable_expression = name + skip( operator('=') ) + oneplus( variable_contents ) + skip( eol ) >> set_variable
667
668 #| <capability name> => <c function>;
669 capability_arguments  = name + skip( operator(':') ) + number + skip( maybe( comma ) )
670 capability_expression = name + skip( operator('=>') ) + name + skip( parenthesis('(') ) + many( capability_arguments ) + skip( parenthesis(')') ) + skip( eol ) >> set_capability
671
672 #| <define name> => <c define>;
673 define_expression = name + skip( operator('=>') ) + name + skip( eol ) >> set_define
674
675 #| <trigger> : <result>;
676 operatorTriggerResult = operator(':') | operator(':+') | operator(':-') | operator('::')
677 scanCode_expression   = triggerCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_scanCode
678 usbCode_expression    = triggerUSBCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_usbCode
679
680 def parse( tokenSequence ):
681         """Sequence(Token) -> object"""
682
683         # Top-level Parser
684         expression = scanCode_expression | usbCode_expression | variable_expression | capability_expression | define_expression
685
686         kll_text = many( expression )
687         kll_file = maybe( kll_text ) + skip( finished )
688
689         return kll_file.parse( tokenSequence )
690
691
692
693 def processKLLFile( filename ):
694         with open( filename, encoding='utf-8' ) as file:
695                 data = file.read()
696                 try:
697                         tokenSequence = tokenize( data )
698                 except LexerError as err:
699                         print ( "{0} Tokenization error in '{1}' - {2}".format( ERROR, filename, err ) )
700                         sys.exit( 1 )
701                 #print ( pformat( tokenSequence ) ) # Display tokenization
702                 try:
703                         tree = parse( tokenSequence )
704                 except (NoParseError, KeyError) as err:
705                         print ( "{0} Parsing error in '{1}' - {2}".format( ERROR, filename, err ) )
706                         sys.exit( 1 )
707
708
709 ### Misc Utility Functions ###
710
711 def gitRevision( kllPath ):
712         import subprocess
713
714         # Change the path to where kll.py is
715         origPath = os.getcwd()
716         os.chdir( kllPath )
717
718         # Just in case git can't be found
719         try:
720                 # Get hash of the latest git commit
721                 revision = subprocess.check_output( ['git', 'rev-parse', 'HEAD'] ).decode()[:-1]
722
723                 # Get list of files that have changed since the commit
724                 changed = subprocess.check_output( ['git', 'diff-index', '--name-only', 'HEAD', '--'] ).decode().splitlines()
725         except:
726                 revision = "<no git>"
727                 changed = []
728
729         # Change back to the old working directory
730         os.chdir( origPath )
731
732         return revision, changed
733
734
735 ### Main Entry Point ###
736
737 if __name__ == '__main__':
738         (baseFiles, defaultFiles, partialFileSets, backend_name, templates, outputs) = processCommandLineArgs()
739
740         # Look up git information on the compiler
741         gitRev, gitChanges = gitRevision( os.path.dirname( os.path.realpath( __file__ ) ) )
742
743         # Load backend module
744         global backend
745         backend_import = importlib.import_module( "backends.{0}".format( backend_name ) )
746         backend = backend_import.Backend( templates )
747
748         # Process base layout files
749         for filename in baseFiles:
750                 variables_dict.setCurrentFile( filename )
751                 processKLLFile( filename )
752         macros_map.completeBaseLayout() # Indicates to macros_map that the base layout is complete
753         variables_dict.baseLayoutFinished()
754
755         # Default combined layer
756         for filename in defaultFiles:
757                 variables_dict.setCurrentFile( filename )
758                 processKLLFile( filename )
759                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
760                 macros_map.replayCachedAssignments()
761
762         # Iterate through additional layers
763         for partial in partialFileSets:
764                 # Increment layer for each -p option
765                 macros_map.addLayer()
766                 variables_dict.incrementLayer() # DefaultLayer is layer 0
767
768                 # Iterate and process each of the file in the layer
769                 for filename in partial:
770                         variables_dict.setCurrentFile( filename )
771                         processKLLFile( filename )
772                         # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
773                         macros_map.replayCachedAssignments()
774                 # Remove un-marked keys to complete the partial layer
775                 macros_map.removeUnmarked()
776
777         # Do macro correlation and transformation
778         macros_map.generate()
779
780         # Process needed templating variables using backend
781         backend.process(
782                 capabilities_dict,
783                 macros_map,
784                 variables_dict,
785                 gitRev,
786                 gitChanges
787         )
788
789         # Generate output file using template and backend
790         backend.generate( outputs )
791
792         # Successful Execution
793         sys.exit( 0 )
794