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