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