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