]> git.donarmstrong.com Git - kiibohd-kll.git/blob - kll.py
Adding None keyword (0.3b)
[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                                         # Use backend capability name and a single argument
470                                         items[ variant ][ sequence ][ combo ] = tuple(
471                                                 [ backend.capabilityLookup( items[ variant ][ sequence ][ combo ][0] ),
472                                                 tuple( [ hid_lookup_dictionary[ items[ variant ][ sequence ][ combo ] ] ] ) ]
473                                         )
474         return items
475
476
477  ## Evaluation Rules
478
479 def eval_scanCode( triggers, operator, results ):
480         # Convert to lists of lists of lists to tuples of tuples of tuples
481         # Tuples are non-mutable, and can be used has index items
482         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
483         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
484
485         # Iterate over all combinations of triggers and results
486         for trigger in triggers:
487                 for result in results:
488                         # Append Case
489                         if operator == ":+":
490                                 macros_map.appendScanCode( trigger, result )
491
492                         # Remove Case
493                         elif operator == ":-":
494                                 macros_map.removeScanCode( trigger, result )
495
496                         # Replace Case
497                         elif operator == ":":
498                                 macros_map.replaceScanCode( trigger, result )
499
500 def eval_usbCode( triggers, operator, results ):
501         # Convert to lists of lists of lists to tuples of tuples of tuples
502         # Tuples are non-mutable, and can be used has index items
503         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
504         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
505
506         # Iterate over all combinations of triggers and results
507         for trigger in triggers:
508                 scanCodes = macros_map.lookupUSBCodes( trigger )
509                 for scanCode in scanCodes:
510                         for result in results:
511                                 # Cache assignment until file finishes processing
512                                 macros_map.cacheAssignment( operator, scanCode, result )
513
514 def eval_variable( name, content ):
515         # Content might be a concatenation of multiple data types, convert everything into a single string
516         assigned_content = ""
517         for item in content:
518                 assigned_content += str( item )
519
520         variables_dict.assignVariable( name, assigned_content )
521
522 def eval_capability( name, function, args ):
523         capabilities_dict[ name ] = [ function, args ]
524
525 def eval_define( name, cdefine_name ):
526         variables_dict.defines[ name ] = cdefine_name
527
528 map_scanCode   = unarg( eval_scanCode )
529 map_usbCode    = unarg( eval_usbCode )
530
531 set_variable   = unarg( eval_variable )
532 set_capability = unarg( eval_capability )
533 set_define     = unarg( eval_define )
534
535
536  ## Sub Rules
537
538 usbCode     = tokenType('USBCode') >> make_usbCode
539 scanCode    = tokenType('ScanCode') >> make_scanCode
540 consCode    = tokenType('ConsCode') >> make_consCode
541 sysCode     = tokenType('SysCode') >> make_sysCode
542 none        = tokenType('None') >> make_none
543 name        = tokenType('Name')
544 number      = tokenType('Number') >> make_number
545 comma       = tokenType('Comma')
546 dash        = tokenType('Dash')
547 plus        = tokenType('Plus')
548 content     = tokenType('VariableContents')
549 string      = tokenType('String') >> make_string
550 unString    = tokenType('String') # When the double quotes are still needed for internal processing
551 seqString   = tokenType('SequenceString') >> make_seqString
552 unseqString = tokenType('SequenceString') >> make_unseqString # For use with variables
553
554   # Code variants
555 code_end = tokenType('CodeEnd')
556
557   # Scan Codes
558 scanCode_start     = tokenType('ScanCodeStart')
559 scanCode_range     = number + skip( dash ) + number >> make_scanCode_range
560 scanCode_listElem  = number >> listElem
561 scanCode_innerList = oneplus( ( scanCode_range | scanCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
562 scanCode_expanded  = skip( scanCode_start ) + scanCode_innerList + skip( code_end )
563 scanCode_elem      = scanCode >> listElem
564 scanCode_combo     = oneplus( ( scanCode_expanded | scanCode_elem ) + skip( maybe( plus ) ) )
565 scanCode_sequence  = oneplus( scanCode_combo + skip( maybe( comma ) ) )
566
567   # USB Codes
568 usbCode_start       = tokenType('USBCodeStart')
569 usbCode_number      = number >> make_usbCode_number
570 usbCode_range       = ( usbCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_usbCode_range
571 usbCode_listElemTag = unString >> make_usbCode
572 usbCode_listElem    = ( usbCode_number | usbCode_listElemTag ) >> listElem
573 usbCode_innerList   = oneplus( ( usbCode_range | usbCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
574 usbCode_expanded    = skip( usbCode_start ) + usbCode_innerList + skip( code_end )
575 usbCode_elem        = usbCode >> listElem
576 usbCode_combo       = oneplus( ( usbCode_expanded | usbCode_elem ) + skip( maybe( plus ) ) ) >> listElem
577 usbCode_sequence    = oneplus( ( usbCode_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
578
579   # Cons Codes
580 consCode_start       = tokenType('ConsCodeStart')
581 consCode_number      = number >> make_consCode_number
582 consCode_range       = ( consCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_consCode_range
583 consCode_listElemTag = unString >> make_consCode
584 consCode_listElem    = ( consCode_number | consCode_listElemTag ) >> listElem
585 consCode_innerList   = oneplus( ( consCode_range | consCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
586 consCode_expanded    = skip( consCode_start ) + consCode_innerList + skip( code_end )
587 consCode_elem        = consCode >> listElem
588
589   # Sys Codes
590 sysCode_start       = tokenType('SysCodeStart')
591 sysCode_number      = number >> make_sysCode_number
592 sysCode_range       = ( sysCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_sysCode_range
593 sysCode_listElemTag = unString >> make_sysCode
594 sysCode_listElem    = ( sysCode_number | sysCode_listElemTag ) >> listElem
595 sysCode_innerList   = oneplus( ( sysCode_range | sysCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
596 sysCode_expanded    = skip( sysCode_start ) + sysCode_innerList + skip( code_end )
597 sysCode_elem        = sysCode >> listElem
598
599   # HID Codes
600 hidCode_elem        = usbCode_expanded | usbCode_elem | sysCode_expanded | sysCode_elem | consCode_expanded | consCode_elem
601
602   # Capabilities
603 capFunc_arguments = many( number + skip( maybe( comma ) ) ) >> listToTuple
604 capFunc_elem      = name + skip( parenthesis('(') ) + capFunc_arguments + skip( parenthesis(')') ) >> capArgExpander >> listElem
605 capFunc_combo     = oneplus( ( hidCode_elem | capFunc_elem ) + skip( maybe( plus ) ) ) >> listElem
606 capFunc_sequence  = oneplus( ( capFunc_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
607
608   # Trigger / Result Codes
609 triggerCode_outerList    = scanCode_sequence >> optionExpansion
610 triggerUSBCode_outerList = usbCode_sequence >> optionExpansion >> hidCodeToCapability
611 resultCode_outerList     = ( ( capFunc_sequence >> optionExpansion ) | none ) >> hidCodeToCapability
612
613
614  ## Main Rules
615
616 #| <variable> = <variable contents>;
617 variable_contents   = name | content | string | number | comma | dash | unseqString
618 variable_expression = name + skip( operator('=') ) + oneplus( variable_contents ) + skip( eol ) >> set_variable
619
620 #| <capability name> => <c function>;
621 capability_arguments  = name + skip( operator(':') ) + number + skip( maybe( comma ) )
622 capability_expression = name + skip( operator('=>') ) + name + skip( parenthesis('(') ) + many( capability_arguments ) + skip( parenthesis(')') ) + skip( eol ) >> set_capability
623
624 #| <define name> => <c define>;
625 define_expression = name + skip( operator('=>') ) + name + skip( eol ) >> set_define
626
627 #| <trigger> : <result>;
628 operatorTriggerResult = operator(':') | operator(':+') | operator(':-')
629 scanCode_expression   = triggerCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_scanCode
630 usbCode_expression    = triggerUSBCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_usbCode
631
632 def parse( tokenSequence ):
633         """Sequence(Token) -> object"""
634
635         # Top-level Parser
636         expression = scanCode_expression | usbCode_expression | variable_expression | capability_expression | define_expression
637
638         kll_text = many( expression )
639         kll_file = maybe( kll_text ) + skip( finished )
640
641         return kll_file.parse( tokenSequence )
642
643
644
645 def processKLLFile( filename ):
646         with open( filename ) as file:
647                 data = file.read()
648                 tokenSequence = tokenize( data )
649                 #print ( pformat( tokenSequence ) ) # Display tokenization
650                 try:
651                         tree = parse( tokenSequence )
652                 except NoParseError as e:
653                         print("Error parsing %s. %s" % (filename, e.msg), file=sys.stderr)
654                         sys.exit(1)
655
656
657 ### Misc Utility Functions ###
658
659 def gitRevision( kllPath ):
660         import subprocess
661
662         # Change the path to where kll.py is
663         origPath = os.getcwd()
664         os.chdir( kllPath )
665
666         # Just in case git can't be found
667         try:
668                 # Get hash of the latest git commit
669                 revision = subprocess.check_output( ['git', 'rev-parse', 'HEAD'] ).decode()[:-1]
670
671                 # Get list of files that have changed since the commit
672                 changed = subprocess.check_output( ['git', 'diff-index', '--name-only', 'HEAD', '--'] ).decode().splitlines()
673         except:
674                 revision = "<no git>"
675                 changed = []
676
677         # Change back to the old working directory
678         os.chdir( origPath )
679
680         return revision, changed
681
682
683 ### Main Entry Point ###
684
685 if __name__ == '__main__':
686         (baseFiles, defaultFiles, partialFileSets, backend_name, templates, outputs) = processCommandLineArgs()
687
688         # Look up git information on the compiler
689         gitRev, gitChanges = gitRevision( os.path.dirname( os.path.realpath( __file__ ) ) )
690
691         # Load backend module
692         global backend
693         backend_import = importlib.import_module( "backends.{0}".format( backend_name ) )
694         backend = backend_import.Backend( templates )
695
696         # Process base layout files
697         for filename in baseFiles:
698                 variables_dict.setCurrentFile( filename )
699                 processKLLFile( filename )
700         macros_map.completeBaseLayout() # Indicates to macros_map that the base layout is complete
701         variables_dict.baseLayoutFinished()
702
703         # Default combined layer
704         for filename in defaultFiles:
705                 variables_dict.setCurrentFile( filename )
706                 processKLLFile( filename )
707                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
708                 macros_map.replayCachedAssignments()
709
710         # Iterate through additional layers
711         for partial in partialFileSets:
712                 # Increment layer for each -p option
713                 macros_map.addLayer()
714                 variables_dict.incrementLayer() # DefaultLayer is layer 0
715
716                 # Iterate and process each of the file in the layer
717                 for filename in partial:
718                         variables_dict.setCurrentFile( filename )
719                         processKLLFile( filename )
720
721                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
722                 macros_map.replayCachedAssignments()
723                 # Remove un-marked keys to complete the partial layer
724                 macros_map.removeUnmarked()
725
726         # Do macro correlation and transformation
727         macros_map.generate()
728
729         # Process needed templating variables using backend
730         backend.process(
731                 capabilities_dict,
732                 macros_map,
733                 variables_dict,
734                 gitRev,
735                 gitChanges
736         )
737
738         # Generate output file using template and backend
739         backend.generate( outputs )
740
741         # Successful Execution
742         sys.exit( 0 )
743