]> git.donarmstrong.com Git - kiibohd-kll.git/blob - kll.py
Fixing ascii string macro processing
[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'"[^"]*"', ) ),
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         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
186         #if scanCode > 0xFF:
187         #       print ( "{0} ScanCode value {1} is larger than 255".format( ERROR, scanCode ) )
188         #       raise
189         return scanCode
190
191 def make_hidCode( type, token ):
192         # If first character is a U, strip
193         if token[0] == "U":
194                 token = token[1:]
195         # CONS specifier
196         elif 'CONS' in token:
197                 token = token[4:]
198         # SYS specifier
199         elif 'SYS' in token:
200                 token = token[3:]
201
202         # If using string representation of USB Code, do lookup, case-insensitive
203         if '"' in token:
204                 try:
205                         hidCode = kll_hid_lookup_dictionary[ type ][ token[1:-1].upper() ][1]
206                 except LookupError as err:
207                         print ( "{0} {1} is an invalid USB HID Code Lookup...".format( ERROR, err ) )
208                         raise
209         else:
210                 # Already tokenized
211                 if type == 'USBCode' and token[0] == 'USB' or type == 'SysCode' and token[0] == 'SYS' or type == 'ConsCode' and token[0] == 'CONS':
212                         hidCode = token[1]
213                 # Convert
214                 else:
215                         hidCode = int( token, 0 )
216
217         # Check size if a USB Code, to make sure it's valid
218         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
219         #if type == 'USBCode' and hidCode > 0xFF:
220         #       print ( "{0} USBCode value {1} is larger than 255".format( ERROR, hidCode ) )
221         #       raise
222
223         # Return a tuple, identifying which type it is
224         if type == 'USBCode':
225                 return make_usbCode_number( hidCode )
226         elif type == 'ConsCode':
227                 return make_consCode_number( hidCode )
228         elif type == 'SysCode':
229                 return make_sysCode_number( hidCode )
230
231         print ( "{0} Unknown HID Specifier '{1}'".format( ERROR, type ) )
232         raise
233
234 def make_usbCode( token ):
235         return make_hidCode( 'USBCode', token )
236
237 def make_consCode( token ):
238         return make_hidCode( 'ConsCode', token )
239
240 def make_sysCode( token ):
241         return make_hidCode( 'SysCode', token )
242
243 def make_hidCode_number( type, token ):
244         lookup = {
245                 'ConsCode' : 'CONS',
246                 'SysCode'  : 'SYS',
247                 'USBCode'  : 'USB',
248         }
249         return ( lookup[ type ], token )
250
251 def make_usbCode_number( token ):
252         return make_hidCode_number( 'USBCode', token )
253
254 def make_consCode_number( token ):
255         return make_hidCode_number( 'ConsCode', token )
256
257 def make_sysCode_number( token ):
258         return make_hidCode_number( 'SysCode', token )
259
260    # Replace key-word with None specifier (which indicates a noneOut capability)
261 def make_none( token ):
262         return [[[('NONE', 0)]]]
263
264 def make_seqString( token ):
265         # Shifted Characters, and amount to move by to get non-shifted version
266         # US ANSI
267         shiftCharacters = (
268                 ( "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x20 ),
269                 ( "+",       0x12 ),
270                 ( "&(",      0x11 ),
271                 ( "!#$%",    0x10 ),
272                 ( "*",       0x0E ),
273                 ( ")",       0x07 ),
274                 ( '"',       0x05 ),
275                 ( ":",       0x01 ),
276                 ( "@",      -0x0E ),
277                 ( "<>?",    -0x10 ),
278                 ( "~",      -0x1E ),
279                 ( "{}|",    -0x20 ),
280                 ( "^",      -0x28 ),
281                 ( "_",      -0x32 ),
282         )
283
284         listOfLists = []
285         shiftKey = kll_hid_lookup_dictionary['USBCode']["SHIFT"]
286
287         # Creates a list of USB codes from the string: sequence (list) of combos (lists)
288         for char in token[1:-1]:
289                 processedChar = char
290
291                 # Whether or not to create a combo for this sequence with a shift
292                 shiftCombo = False
293
294                 # Depending on the ASCII character, convert to single character or Shift + character
295                 for pair in shiftCharacters:
296                         if char in pair[0]:
297                                 shiftCombo = True
298                                 processedChar = chr( ord( char ) + pair[1] )
299                                 break
300
301                 # Do KLL HID Lookup on non-shifted character
302                 # NOTE: Case-insensitive, which is why the shift must be pre-computed
303                 usbCode = kll_hid_lookup_dictionary['USBCode'][ processedChar.upper() ]
304
305                 # Create Combo for this character, add shift key if shifted
306                 charCombo = []
307                 if shiftCombo:
308                         charCombo = [ [ shiftKey ] ]
309                 charCombo.append( [ usbCode ] )
310
311                 # Add to list of lists
312                 listOfLists.append( charCombo )
313
314         return listOfLists
315
316 def make_string( token ):
317         return token[1:-1]
318
319 def make_unseqString( token ):
320         return token[1:-1]
321
322 def make_number( token ):
323         return int( token, 0 )
324
325   # Range can go from high to low or low to high
326 def make_scanCode_range( rangeVals ):
327         start = rangeVals[0]
328         end   = rangeVals[1]
329
330         # Swap start, end if start is greater than end
331         if start > end:
332                 start, end = end, start
333
334         # Iterate from start to end, and generate the range
335         return list( range( start, end + 1 ) )
336
337   # Range can go from high to low or low to high
338   # Warn on 0-9 for USBCodes (as this does not do what one would expect) TODO
339   # Lookup USB HID tags and convert to a number
340 def make_hidCode_range( type, rangeVals ):
341         # Check if already integers
342         if isinstance( rangeVals[0], int ):
343                 start = rangeVals[0]
344         else:
345                 start = make_hidCode( type, rangeVals[0] )[1]
346
347         if isinstance( rangeVals[1], int ):
348                 end = rangeVals[1]
349         else:
350                 end = make_hidCode( type, rangeVals[1] )[1]
351
352         # Swap start, end if start is greater than end
353         if start > end:
354                 start, end = end, start
355
356         # Iterate from start to end, and generate the range
357         listRange = list( range( start, end + 1 ) )
358
359         # Convert each item in the list to a tuple
360         for item in range( len( listRange ) ):
361                 listRange[ item ] = make_hidCode_number( type, listRange[ item ] )
362         return listRange
363
364 def make_usbCode_range( rangeVals ):
365         return make_hidCode_range( 'USBCode', rangeVals )
366
367 def make_sysCode_range( rangeVals ):
368         return make_hidCode_range( 'SysCode', rangeVals )
369
370 def make_consCode_range( rangeVals ):
371         return make_hidCode_range( 'ConsCode', rangeVals )
372
373
374  ## Base Rules
375
376 const       = lambda x: lambda _: x
377 unarg       = lambda f: lambda x: f(*x)
378 flatten     = lambda list: sum( list, [] )
379
380 tokenValue  = lambda x: x.value
381 tokenType   = lambda t: some( lambda x: x.type == t ) >> tokenValue
382 operator    = lambda s: a( Token( 'Operator', s ) ) >> tokenValue
383 parenthesis = lambda s: a( Token( 'Parenthesis', s ) ) >> tokenValue
384 eol         = a( Token( 'EndOfLine', ';' ) )
385
386 def listElem( item ):
387         return [ item ]
388
389 def listToTuple( items ):
390         return tuple( items )
391
392   # Flatten only the top layer (list of lists of ...)
393 def oneLayerFlatten( items ):
394         mainList = []
395         for sublist in items:
396                 for item in sublist:
397                         mainList.append( item )
398
399         return mainList
400
401   # Capability arguments may need to be expanded (e.g. 1 16 bit argument needs to be 2 8 bit arguments for the state machine)
402 def capArgExpander( items ):
403         newArgs = []
404         # For each defined argument in the capability definition
405         for arg in range( 0, len( capabilities_dict[ items[0] ][1] ) ):
406                 argLen = capabilities_dict[ items[0] ][1][ arg ][1]
407                 num = items[1][ arg ]
408                 byteForm = num.to_bytes( argLen, byteorder='little' ) # XXX Yes, little endian from how the uC structs work
409
410                 # For each sub-argument, split into byte-sized chunks
411                 for byte in range( 0, argLen ):
412                         newArgs.append( byteForm[ byte ] )
413
414         return tuple( [ items[0], tuple( newArgs ) ] )
415
416   # Expand ranges of values in the 3rd dimension of the list, to a list of 2nd lists
417   # i.e. [ sequence, [ combo, [ range ] ] ] --> [ [ sequence, [ combo ] ], <option 2>, <option 3> ]
418 def optionExpansion( sequences ):
419         expandedSequences = []
420
421         # Total number of combinations of the sequence of combos that needs to be generated
422         totalCombinations = 1
423
424         # List of leaf lists, with number of leaves
425         maxLeafList = []
426
427         # Traverse to the leaf nodes, and count the items in each leaf list
428         for sequence in sequences:
429                 for combo in sequence:
430                         rangeLen = len( combo )
431                         totalCombinations *= rangeLen
432                         maxLeafList.append( rangeLen )
433
434         # Counter list to keep track of which combination is being generated
435         curLeafList = [0] * len( maxLeafList )
436
437         # Generate a list of permuations of the sequence of combos
438         for count in range( 0, totalCombinations ):
439                 expandedSequences.append( [] ) # Prepare list for adding the new combination
440                 position = 0
441
442                 # Traverse sequence of combos to generate permuation
443                 for sequence in sequences:
444                         expandedSequences[ -1 ].append( [] )
445                         for combo in sequence:
446                                 expandedSequences[ -1 ][ -1 ].append( combo[ curLeafList[ position ] ] )
447                                 position += 1
448
449                 # Increment combination tracker
450                 for leaf in range( 0, len( curLeafList ) ):
451                         curLeafList[ leaf ] += 1
452
453                         # Reset this position, increment next position (if it exists), then stop
454                         if curLeafList[ leaf ] >= maxLeafList[ leaf ]:
455                                 curLeafList[ leaf ] = 0
456                                 if leaf + 1 < len( curLeafList ):
457                                         curLeafList[ leaf + 1 ] += 1
458
459         return expandedSequences
460
461
462 # Converts USB Codes into Capabilities
463 # These are tuples (<type>, <integer>)
464 def hidCodeToCapability( items ):
465         # Items already converted to variants using optionExpansion
466         for variant in range( 0, len( items ) ):
467                 # Sequence of Combos
468                 for sequence in range( 0, len( items[ variant ] ) ):
469                         for combo in range( 0, len( items[ variant ][ sequence ] ) ):
470                                 if items[ variant ][ sequence ][ combo ][0] in backend.requiredCapabilities.keys():
471                                         try:
472                                                 # Use backend capability name and a single argument
473                                                 items[ variant ][ sequence ][ combo ] = tuple(
474                                                         [ backend.capabilityLookup( items[ variant ][ sequence ][ combo ][0] ),
475                                                         tuple( [ hid_lookup_dictionary[ items[ variant ][ sequence ][ combo ] ] ] ) ]
476                                                 )
477                                         except KeyError:
478                                                 print ( "{0} {1} is an invalid HID lookup value".format( ERROR, items[ variant ][ sequence ][ combo ] ) )
479                                                 sys.exit( 1 )
480         return items
481
482
483 # Convert tuple of tuples to list of lists
484 def listit( t ):
485         return list( map( listit, t ) ) if isinstance( t, ( list, tuple ) ) else t
486
487 # Convert list of lists to tuple of tuples
488 def tupleit( t ):
489         return tuple( map( tupleit, t ) ) if isinstance( t, ( tuple, list ) ) else t
490
491
492  ## Evaluation Rules
493
494 def eval_scanCode( triggers, operator, results ):
495         # Convert to lists of lists of lists to tuples of tuples of tuples
496         # Tuples are non-mutable, and can be used has index items
497         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
498         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
499
500         # Lookup interconnect id (Current file scope)
501         # Default to 0 if not specified
502         if 'ConnectId' not in variables_dict.overallVariables.keys():
503                 id_num = 0
504         else:
505                 id_num = int( variables_dict.overallVariables['ConnectId'] )
506
507         # Iterate over all combinations of triggers and results
508         for sequence in triggers:
509                 # Convert tuple of tuples to list of lists so each element can be modified
510                 trigger = listit( sequence )
511
512                 # Create ScanCode entries for trigger
513                 for seq_index, combo in enumerate( sequence ):
514                         for com_index, scancode in enumerate( combo ):
515                                 trigger[ seq_index ][ com_index ] = macros_map.scanCodeStore.append( ScanCode( scancode, id_num ) )
516
517                 # Convert back to a tuple of tuples
518                 trigger = tupleit( trigger )
519
520                 for result in results:
521                         # Append Case
522                         if operator == ":+":
523                                 macros_map.appendScanCode( trigger, result )
524
525                         # Remove Case
526                         elif operator == ":-":
527                                 macros_map.removeScanCode( trigger, result )
528
529                         # Replace Case
530                         # Soft Replace Case is the same for Scan Codes
531                         elif operator == ":" or operator == "::":
532                                 macros_map.replaceScanCode( trigger, result )
533
534 def eval_usbCode( triggers, operator, results ):
535         # Convert to lists of lists of lists to tuples of tuples of tuples
536         # Tuples are non-mutable, and can be used has index items
537         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
538         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
539
540         # Iterate over all combinations of triggers and results
541         for trigger in triggers:
542                 scanCodes = macros_map.lookupUSBCodes( trigger )
543                 for scanCode in scanCodes:
544                         for result in results:
545                                 # Soft Replace needs additional checking to see if replacement is necessary
546                                 if operator == "::" and not macros_map.softReplaceCheck( scanCode ):
547                                         continue
548
549                                 # Cache assignment until file finishes processing
550                                 macros_map.cacheAssignment( operator, scanCode, result )
551
552 def eval_variable( name, content ):
553         # Content might be a concatenation of multiple data types, convert everything into a single string
554         assigned_content = ""
555         for item in content:
556                 assigned_content += str( item )
557
558         variables_dict.assignVariable( name, assigned_content )
559
560 def eval_capability( name, function, args ):
561         capabilities_dict[ name ] = [ function, args ]
562
563 def eval_define( name, cdefine_name ):
564         variables_dict.defines[ name ] = cdefine_name
565
566 map_scanCode   = unarg( eval_scanCode )
567 map_usbCode    = unarg( eval_usbCode )
568
569 set_variable   = unarg( eval_variable )
570 set_capability = unarg( eval_capability )
571 set_define     = unarg( eval_define )
572
573
574  ## Sub Rules
575
576 usbCode     = tokenType('USBCode') >> make_usbCode
577 scanCode    = tokenType('ScanCode') >> make_scanCode
578 consCode    = tokenType('ConsCode') >> make_consCode
579 sysCode     = tokenType('SysCode') >> make_sysCode
580 none        = tokenType('None') >> make_none
581 name        = tokenType('Name')
582 number      = tokenType('Number') >> make_number
583 comma       = tokenType('Comma')
584 dash        = tokenType('Dash')
585 plus        = tokenType('Plus')
586 content     = tokenType('VariableContents')
587 string      = tokenType('String') >> make_string
588 unString    = tokenType('String') # When the double quotes are still needed for internal processing
589 seqString   = tokenType('SequenceString') >> make_seqString
590 unseqString = tokenType('SequenceString') >> make_unseqString # For use with variables
591
592   # Code variants
593 code_end = tokenType('CodeEnd')
594
595   # Scan Codes
596 scanCode_start     = tokenType('ScanCodeStart')
597 scanCode_range     = number + skip( dash ) + number >> make_scanCode_range
598 scanCode_listElem  = number >> listElem
599 scanCode_innerList = oneplus( ( scanCode_range | scanCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
600 scanCode_expanded  = skip( scanCode_start ) + scanCode_innerList + skip( code_end )
601 scanCode_elem      = scanCode >> listElem
602 scanCode_combo     = oneplus( ( scanCode_expanded | scanCode_elem ) + skip( maybe( plus ) ) )
603 scanCode_sequence  = oneplus( scanCode_combo + skip( maybe( comma ) ) )
604
605   # USB Codes
606 usbCode_start       = tokenType('USBCodeStart')
607 usbCode_number      = number >> make_usbCode_number
608 usbCode_range       = ( usbCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_usbCode_range
609 usbCode_listElemTag = unString >> make_usbCode
610 usbCode_listElem    = ( usbCode_number | usbCode_listElemTag ) >> listElem
611 usbCode_innerList   = oneplus( ( usbCode_range | usbCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
612 usbCode_expanded    = skip( usbCode_start ) + usbCode_innerList + skip( code_end )
613 usbCode_elem        = usbCode >> listElem
614 usbCode_combo       = oneplus( ( usbCode_expanded | usbCode_elem ) + skip( maybe( plus ) ) ) >> listElem
615 usbCode_sequence    = oneplus( ( usbCode_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
616
617   # Cons Codes
618 consCode_start       = tokenType('ConsCodeStart')
619 consCode_number      = number >> make_consCode_number
620 consCode_range       = ( consCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_consCode_range
621 consCode_listElemTag = unString >> make_consCode
622 consCode_listElem    = ( consCode_number | consCode_listElemTag ) >> listElem
623 consCode_innerList   = oneplus( ( consCode_range | consCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
624 consCode_expanded    = skip( consCode_start ) + consCode_innerList + skip( code_end )
625 consCode_elem        = consCode >> listElem
626
627   # Sys Codes
628 sysCode_start       = tokenType('SysCodeStart')
629 sysCode_number      = number >> make_sysCode_number
630 sysCode_range       = ( sysCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_sysCode_range
631 sysCode_listElemTag = unString >> make_sysCode
632 sysCode_listElem    = ( sysCode_number | sysCode_listElemTag ) >> listElem
633 sysCode_innerList   = oneplus( ( sysCode_range | sysCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
634 sysCode_expanded    = skip( sysCode_start ) + sysCode_innerList + skip( code_end )
635 sysCode_elem        = sysCode >> listElem
636
637   # HID Codes
638 hidCode_elem        = usbCode_expanded | usbCode_elem | sysCode_expanded | sysCode_elem | consCode_expanded | consCode_elem
639
640   # Capabilities
641 capFunc_arguments = many( number + skip( maybe( comma ) ) ) >> listToTuple
642 capFunc_elem      = name + skip( parenthesis('(') ) + capFunc_arguments + skip( parenthesis(')') ) >> capArgExpander >> listElem
643 capFunc_combo     = oneplus( ( hidCode_elem | capFunc_elem ) + skip( maybe( plus ) ) ) >> listElem
644 capFunc_sequence  = oneplus( ( capFunc_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
645
646   # Trigger / Result Codes
647 triggerCode_outerList    = scanCode_sequence >> optionExpansion
648 triggerUSBCode_outerList = usbCode_sequence >> optionExpansion >> hidCodeToCapability
649 resultCode_outerList     = ( ( capFunc_sequence >> optionExpansion ) | none ) >> hidCodeToCapability
650
651
652  ## Main Rules
653
654 #| <variable> = <variable contents>;
655 variable_contents   = name | content | string | number | comma | dash | unseqString
656 variable_expression = name + skip( operator('=') ) + oneplus( variable_contents ) + skip( eol ) >> set_variable
657
658 #| <capability name> => <c function>;
659 capability_arguments  = name + skip( operator(':') ) + number + skip( maybe( comma ) )
660 capability_expression = name + skip( operator('=>') ) + name + skip( parenthesis('(') ) + many( capability_arguments ) + skip( parenthesis(')') ) + skip( eol ) >> set_capability
661
662 #| <define name> => <c define>;
663 define_expression = name + skip( operator('=>') ) + name + skip( eol ) >> set_define
664
665 #| <trigger> : <result>;
666 operatorTriggerResult = operator(':') | operator(':+') | operator(':-') | operator('::')
667 scanCode_expression   = triggerCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_scanCode
668 usbCode_expression    = triggerUSBCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_usbCode
669
670 def parse( tokenSequence ):
671         """Sequence(Token) -> object"""
672
673         # Top-level Parser
674         expression = scanCode_expression | usbCode_expression | variable_expression | capability_expression | define_expression
675
676         kll_text = many( expression )
677         kll_file = maybe( kll_text ) + skip( finished )
678
679         return kll_file.parse( tokenSequence )
680
681
682
683 def processKLLFile( filename ):
684         with open( filename ) as file:
685                 data = file.read()
686                 tokenSequence = tokenize( data )
687                 #print ( pformat( tokenSequence ) ) # Display tokenization
688                 try:
689                         tree = parse( tokenSequence )
690                 except NoParseError as e:
691                         print("Error parsing %s. %s" % (filename, e.msg), file=sys.stderr)
692                         sys.exit(1)
693
694
695 ### Misc Utility Functions ###
696
697 def gitRevision( kllPath ):
698         import subprocess
699
700         # Change the path to where kll.py is
701         origPath = os.getcwd()
702         os.chdir( kllPath )
703
704         # Just in case git can't be found
705         try:
706                 # Get hash of the latest git commit
707                 revision = subprocess.check_output( ['git', 'rev-parse', 'HEAD'] ).decode()[:-1]
708
709                 # Get list of files that have changed since the commit
710                 changed = subprocess.check_output( ['git', 'diff-index', '--name-only', 'HEAD', '--'] ).decode().splitlines()
711         except:
712                 revision = "<no git>"
713                 changed = []
714
715         # Change back to the old working directory
716         os.chdir( origPath )
717
718         return revision, changed
719
720
721 ### Main Entry Point ###
722
723 if __name__ == '__main__':
724         (baseFiles, defaultFiles, partialFileSets, backend_name, templates, outputs) = processCommandLineArgs()
725
726         # Look up git information on the compiler
727         gitRev, gitChanges = gitRevision( os.path.dirname( os.path.realpath( __file__ ) ) )
728
729         # Load backend module
730         global backend
731         backend_import = importlib.import_module( "backends.{0}".format( backend_name ) )
732         backend = backend_import.Backend( templates )
733
734         # Process base layout files
735         for filename in baseFiles:
736                 variables_dict.setCurrentFile( filename )
737                 processKLLFile( filename )
738         macros_map.completeBaseLayout() # Indicates to macros_map that the base layout is complete
739         variables_dict.baseLayoutFinished()
740
741         # Default combined layer
742         for filename in defaultFiles:
743                 variables_dict.setCurrentFile( filename )
744                 processKLLFile( filename )
745                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
746                 macros_map.replayCachedAssignments()
747
748         # Iterate through additional layers
749         for partial in partialFileSets:
750                 # Increment layer for each -p option
751                 macros_map.addLayer()
752                 variables_dict.incrementLayer() # DefaultLayer is layer 0
753
754                 # Iterate and process each of the file in the layer
755                 for filename in partial:
756                         variables_dict.setCurrentFile( filename )
757                         processKLLFile( filename )
758
759                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
760                 macros_map.replayCachedAssignments()
761                 # Remove un-marked keys to complete the partial layer
762                 macros_map.removeUnmarked()
763
764         # Do macro correlation and transformation
765         macros_map.generate()
766
767         # Process needed templating variables using backend
768         backend.process(
769                 capabilities_dict,
770                 macros_map,
771                 variables_dict,
772                 gitRev,
773                 gitChanges
774         )
775
776         # Generate output file using template and backend
777         backend.generate( outputs )
778
779         # Successful Execution
780         sys.exit( 0 )
781