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