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