]> git.donarmstrong.com Git - kiibohd-kll.git/blob - kll.py
Adding layer rotation (next/prev) support
[kiibohd-kll.git] / kll.py
1 #!/usr/bin/env python3
2 # KLL Compiler
3 # Keyboard Layout Langauge
4 #
5 # Copyright (C) 2014-2015 by Jacob Alexander
6 #
7 # This file is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This file is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this file.  If not, see <http://www.gnu.org/licenses/>.
19
20 ### Imports ###
21
22 import argparse
23 import importlib
24 import io
25 import os
26 import re
27 import sys
28 import token
29
30 from pprint   import pformat
31 from re       import VERBOSE
32 from tokenize import generate_tokens
33
34 from kll_lib.containers import *
35 from kll_lib.hid_dict   import *
36
37 from funcparserlib.lexer  import make_tokenizer, Token, LexerError
38 from funcparserlib.parser import (some, a, many, oneplus, skip, finished, maybe, skip, forward_decl, NoParseError)
39
40
41
42 ### Decorators ###
43
44  ## Print Decorator Variables
45 ERROR = '\033[5;1;31mERROR\033[0m:'
46
47
48  ## Python Text Formatting Fixer...
49  ##  Because the creators of Python are averse to proper capitalization.
50 textFormatter_lookup = {
51         "usage: "            : "Usage: ",
52         "optional arguments" : "Optional Arguments",
53 }
54
55 def textFormatter_gettext( s ):
56         return textFormatter_lookup.get( s, s )
57
58 argparse._ = textFormatter_gettext
59
60
61
62 ### Argument Parsing ###
63
64 def checkFileExists( filename ):
65         if not os.path.isfile( filename ):
66                 print ( "{0} {1} does not exist...".format( ERROR, filename ) )
67                 sys.exit( 1 )
68
69 def processCommandLineArgs():
70         # Setup argument processor
71         pArgs = argparse.ArgumentParser(
72                 usage="%(prog)s [options] <file1>...",
73                 description="Generates .h file state tables and pointer indices from KLL .kll files.",
74                 epilog="Example: {0} mykeyboard.kll -d colemak.kll -p hhkbpro2.kll -p symbols.kll".format( os.path.basename( sys.argv[0] ) ),
75                 formatter_class=argparse.RawTextHelpFormatter,
76                 add_help=False,
77 )
78
79         # Positional Arguments
80         pArgs.add_argument( 'files', type=str, nargs='+',
81                 help=argparse.SUPPRESS ) # Suppressed help output, because Python output is verbosely ugly
82
83         # Optional Arguments
84         pArgs.add_argument( '-b', '--backend', type=str, default="kiibohd",
85                 help="Specify target backend for the KLL compiler.\n"
86                 "Default: kiibohd\n"
87                 "Options: kiibohd, json" )
88         pArgs.add_argument( '-d', '--default', type=str, nargs='+',
89                 help="Specify .kll files to layer on top of the default map to create a combined map." )
90         pArgs.add_argument( '-p', '--partial', type=str, nargs='+', action='append',
91                 help="Specify .kll files to generate partial map, multiple files per flag.\n"
92                 "Each -p defines another partial map.\n"
93                 "Base .kll files (that define the scan code maps) must be defined for each partial map." )
94         pArgs.add_argument( '-t', '--templates', type=str, nargs='+',
95                 help="Specify template used to generate the keymap.\n"
96                 "Default: <backend specific>" )
97         pArgs.add_argument( '-o', '--outputs', type=str, nargs='+',
98                 help="Specify output file. Writes to current working directory by default.\n"
99                 "Default: <backend specific>" )
100         pArgs.add_argument( '-h', '--help', action="help",
101                 help="This message." )
102
103         # Process Arguments
104         args = pArgs.parse_args()
105
106         # Parameters
107         baseFiles = args.files
108         defaultFiles = args.default
109         partialFileSets = args.partial
110         if defaultFiles is None:
111                 defaultFiles = []
112         if partialFileSets is None:
113                 partialFileSets = [[]]
114
115         # Check file existance
116         for filename in baseFiles:
117                 checkFileExists( filename )
118
119         for filename in defaultFiles:
120                 checkFileExists( filename )
121
122         for partial in partialFileSets:
123                 for filename in partial:
124                         checkFileExists( filename )
125
126         return (baseFiles, defaultFiles, partialFileSets, args.backend, args.templates, args.outputs)
127
128
129
130 ### Tokenizer ###
131
132 def tokenize( string ):
133         """str -> Sequence(Token)"""
134
135         # Basic Tokens Spec
136         specs = [
137                 ( 'Comment',          ( r' *#.*', ) ),
138                 ( 'Space',            ( r'[ \t\r\n]+', ) ),
139                 ( 'USBCode',          ( r'U(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
140                 ( 'USBCodeStart',     ( r'U\[', ) ),
141                 ( 'ConsCode',         ( r'CONS(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
142                 ( 'ConsCodeStart',    ( r'CONS\[', ) ),
143                 ( 'SysCode',          ( r'SYS(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
144                 ( 'SysCodeStart',     ( r'SYS\[', ) ),
145                 ( 'LedCode',          ( r'LED(("[^"]+")|(0x[0-9a-fA-F]+)|([0-9]+))', ) ),
146                 ( 'LedCodeStart',     ( r'LED\[', ) ),
147                 ( 'ScanCode',         ( r'S((0x[0-9a-fA-F]+)|([0-9]+))', ) ),
148                 ( 'ScanCodeStart',    ( r'S\[', ) ),
149                 ( 'CodeEnd',          ( r'\]', ) ),
150                 ( 'String',           ( r'"[^"]*"', VERBOSE ) ),
151                 ( 'SequenceString',   ( r"'[^']*'", ) ),
152                 ( 'Operator',         ( r'=>|:\+|:-|:|=', ) ),
153                 ( 'Comma',            ( r',', ) ),
154                 ( 'Dash',             ( r'-', ) ),
155                 ( 'Plus',             ( r'\+', ) ),
156                 ( 'Parenthesis',      ( r'\(|\)', ) ),
157                 ( 'None',             ( r'None', ) ),
158                 ( 'Number',           ( r'-?(0x[0-9a-fA-F]+)|(0|([1-9][0-9]*))', VERBOSE ) ),
159                 ( 'Name',             ( r'[A-Za-z_][A-Za-z_0-9]*', ) ),
160                 ( 'VariableContents', ( r'''[^"' ;:=>()]+''', ) ),
161                 ( 'EndOfLine',        ( r';', ) ),
162         ]
163
164         # Tokens to filter out of the token stream
165         useless = ['Space', 'Comment']
166
167         tokens = make_tokenizer( specs )
168         return [x for x in tokens( string ) if x.type not in useless]
169
170
171
172 ### Parsing ###
173
174  ## Map Arrays
175 macros_map        = Macros()
176 variables_dict    = Variables()
177 capabilities_dict = Capabilities()
178
179
180  ## Parsing Functions
181
182 def make_scanCode( token ):
183         scanCode = int( token[1:], 0 )
184         # Check size, to make sure it's valid
185         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
186         #if scanCode > 0xFF:
187         #       print ( "{0} ScanCode value {1} is larger than 255".format( ERROR, scanCode ) )
188         #       raise
189         return scanCode
190
191 def make_hidCode( type, token ):
192         # If first character is a U, strip
193         if token[0] == "U":
194                 token = token[1:]
195         # CONS specifier
196         elif 'CONS' in token:
197                 token = token[4:]
198         # SYS specifier
199         elif 'SYS' in token:
200                 token = token[3:]
201
202         # If using string representation of USB Code, do lookup, case-insensitive
203         if '"' in token:
204                 try:
205                         hidCode = kll_hid_lookup_dictionary[ type ][ token[1:-1].upper() ][1]
206                 except LookupError as err:
207                         print ( "{0} {1} is an invalid USB HID Code Lookup...".format( ERROR, err ) )
208                         raise
209         else:
210                 # Already tokenized
211                 if type == 'USBCode' and token[0] == 'USB' or type == 'SysCode' and token[0] == 'SYS' or type == 'ConsCode' and token[0] == 'CONS':
212                         hidCode = token[1]
213                 # Convert
214                 else:
215                         hidCode = int( token, 0 )
216
217         # Check size if a USB Code, to make sure it's valid
218         # XXX Add better check that takes symbolic names into account (i.e. U"Latch5")
219         #if type == 'USBCode' and hidCode > 0xFF:
220         #       print ( "{0} USBCode value {1} is larger than 255".format( ERROR, hidCode ) )
221         #       raise
222
223         # Return a tuple, identifying which type it is
224         if type == 'USBCode':
225                 return make_usbCode_number( hidCode )
226         elif type == 'ConsCode':
227                 return make_consCode_number( hidCode )
228         elif type == 'SysCode':
229                 return make_sysCode_number( hidCode )
230
231         print ( "{0} Unknown HID Specifier '{1}'".format( ERROR, type ) )
232         raise
233
234 def make_usbCode( token ):
235         return make_hidCode( 'USBCode', token )
236
237 def make_consCode( token ):
238         return make_hidCode( 'ConsCode', token )
239
240 def make_sysCode( token ):
241         return make_hidCode( 'SysCode', token )
242
243 def make_hidCode_number( type, token ):
244         lookup = {
245                 'ConsCode' : 'CONS',
246                 'SysCode'  : 'SYS',
247                 'USBCode'  : 'USB',
248         }
249         return ( lookup[ type ], token )
250
251 def make_usbCode_number( token ):
252         return make_hidCode_number( 'USBCode', token )
253
254 def make_consCode_number( token ):
255         return make_hidCode_number( 'ConsCode', token )
256
257 def make_sysCode_number( token ):
258         return make_hidCode_number( 'SysCode', token )
259
260    # Replace key-word with None specifier (which indicates a noneOut capability)
261 def make_none( token ):
262         return [[[('NONE', 0)]]]
263
264 def make_seqString( token ):
265         # Shifted Characters, and amount to move by to get non-shifted version
266         # US ANSI
267         shiftCharacters = (
268                 ( "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x20 ),
269                 ( "+",       0x12 ),
270                 ( "&(",      0x11 ),
271                 ( "!#$%<>",  0x10 ),
272                 ( "*",       0x0E ),
273                 ( ")",       0x07 ),
274                 ( '"',       0x05 ),
275                 ( ":",       0x01 ),
276                 ( "^",      -0x10 ),
277                 ( "_",      -0x18 ),
278                 ( "{}|~",   -0x1E ),
279                 ( "@",      -0x32 ),
280                 ( "?",      -0x38 ),
281         )
282
283         listOfLists = []
284         shiftKey = kll_hid_lookup_dictionary['USBCode']["SHIFT"]
285
286         # Creates a list of USB codes from the string: sequence (list) of combos (lists)
287         for char in token[1:-1]:
288                 processedChar = char
289
290                 # Whether or not to create a combo for this sequence with a shift
291                 shiftCombo = False
292
293                 # Depending on the ASCII character, convert to single character or Shift + character
294                 for pair in shiftCharacters:
295                         if char in pair[0]:
296                                 shiftCombo = True
297                                 processedChar = chr( ord( char ) + pair[1] )
298                                 break
299
300                 # Do KLL HID Lookup on non-shifted character
301                 # NOTE: Case-insensitive, which is why the shift must be pre-computed
302                 usbCode = kll_hid_lookup_dictionary['USBCode'][ processedChar.upper() ]
303
304                 # Create Combo for this character, add shift key if shifted
305                 charCombo = []
306                 if shiftCombo:
307                         charCombo = [ [ shiftKey ] ]
308                 charCombo.append( [ usbCode ] )
309
310                 # Add to list of lists
311                 listOfLists.append( charCombo )
312
313         return listOfLists
314
315 def make_string( token ):
316         return token[1:-1]
317
318 def make_unseqString( token ):
319         return token[1:-1]
320
321 def make_number( token ):
322         return int( token, 0 )
323
324   # Range can go from high to low or low to high
325 def make_scanCode_range( rangeVals ):
326         start = rangeVals[0]
327         end   = rangeVals[1]
328
329         # Swap start, end if start is greater than end
330         if start > end:
331                 start, end = end, start
332
333         # Iterate from start to end, and generate the range
334         return list( range( start, end + 1 ) )
335
336   # Range can go from high to low or low to high
337   # Warn on 0-9 for USBCodes (as this does not do what one would expect) TODO
338   # Lookup USB HID tags and convert to a number
339 def make_hidCode_range( type, rangeVals ):
340         # Check if already integers
341         if isinstance( rangeVals[0], int ):
342                 start = rangeVals[0]
343         else:
344                 start = make_hidCode( type, rangeVals[0] )[1]
345
346         if isinstance( rangeVals[1], int ):
347                 end = rangeVals[1]
348         else:
349                 end = make_hidCode( type, rangeVals[1] )[1]
350
351         # Swap start, end if start is greater than end
352         if start > end:
353                 start, end = end, start
354
355         # Iterate from start to end, and generate the range
356         listRange = list( range( start, end + 1 ) )
357
358         # Convert each item in the list to a tuple
359         for item in range( len( listRange ) ):
360                 listRange[ item ] = make_hidCode_number( type, listRange[ item ] )
361         return listRange
362
363 def make_usbCode_range( rangeVals ):
364         return make_hidCode_range( 'USBCode', rangeVals )
365
366 def make_sysCode_range( rangeVals ):
367         return make_hidCode_range( 'SysCode', rangeVals )
368
369 def make_consCode_range( rangeVals ):
370         return make_hidCode_range( 'ConsCode', rangeVals )
371
372
373  ## Base Rules
374
375 const       = lambda x: lambda _: x
376 unarg       = lambda f: lambda x: f(*x)
377 flatten     = lambda list: sum( list, [] )
378
379 tokenValue  = lambda x: x.value
380 tokenType   = lambda t: some( lambda x: x.type == t ) >> tokenValue
381 operator    = lambda s: a( Token( 'Operator', s ) ) >> tokenValue
382 parenthesis = lambda s: a( Token( 'Parenthesis', s ) ) >> tokenValue
383 eol         = a( Token( 'EndOfLine', ';' ) )
384
385 def listElem( item ):
386         return [ item ]
387
388 def listToTuple( items ):
389         return tuple( items )
390
391   # Flatten only the top layer (list of lists of ...)
392 def oneLayerFlatten( items ):
393         mainList = []
394         for sublist in items:
395                 for item in sublist:
396                         mainList.append( item )
397
398         return mainList
399
400   # Capability arguments may need to be expanded (e.g. 1 16 bit argument needs to be 2 8 bit arguments for the state machine)
401 def capArgExpander( items ):
402         newArgs = []
403         # For each defined argument in the capability definition
404         for arg in range( 0, len( capabilities_dict[ items[0] ][1] ) ):
405                 argLen = capabilities_dict[ items[0] ][1][ arg ][1]
406                 num = items[1][ arg ]
407                 byteForm = num.to_bytes( argLen, byteorder='little' ) # XXX Yes, little endian from how the uC structs work
408
409                 # For each sub-argument, split into byte-sized chunks
410                 for byte in range( 0, argLen ):
411                         newArgs.append( byteForm[ byte ] )
412
413         return tuple( [ items[0], tuple( newArgs ) ] )
414
415   # Expand ranges of values in the 3rd dimension of the list, to a list of 2nd lists
416   # i.e. [ sequence, [ combo, [ range ] ] ] --> [ [ sequence, [ combo ] ], <option 2>, <option 3> ]
417 def optionExpansion( sequences ):
418         expandedSequences = []
419
420         # Total number of combinations of the sequence of combos that needs to be generated
421         totalCombinations = 1
422
423         # List of leaf lists, with number of leaves
424         maxLeafList = []
425
426         # Traverse to the leaf nodes, and count the items in each leaf list
427         for sequence in sequences:
428                 for combo in sequence:
429                         rangeLen = len( combo )
430                         totalCombinations *= rangeLen
431                         maxLeafList.append( rangeLen )
432
433         # Counter list to keep track of which combination is being generated
434         curLeafList = [0] * len( maxLeafList )
435
436         # Generate a list of permuations of the sequence of combos
437         for count in range( 0, totalCombinations ):
438                 expandedSequences.append( [] ) # Prepare list for adding the new combination
439                 position = 0
440
441                 # Traverse sequence of combos to generate permuation
442                 for sequence in sequences:
443                         expandedSequences[ -1 ].append( [] )
444                         for combo in sequence:
445                                 expandedSequences[ -1 ][ -1 ].append( combo[ curLeafList[ position ] ] )
446                                 position += 1
447
448                 # Increment combination tracker
449                 for leaf in range( 0, len( curLeafList ) ):
450                         curLeafList[ leaf ] += 1
451
452                         # Reset this position, increment next position (if it exists), then stop
453                         if curLeafList[ leaf ] >= maxLeafList[ leaf ]:
454                                 curLeafList[ leaf ] = 0
455                                 if leaf + 1 < len( curLeafList ):
456                                         curLeafList[ leaf + 1 ] += 1
457
458         return expandedSequences
459
460
461 # Converts USB Codes into Capabilities
462 # These are tuples (<type>, <integer>)
463 def hidCodeToCapability( items ):
464         # Items already converted to variants using optionExpansion
465         for variant in range( 0, len( items ) ):
466                 # Sequence of Combos
467                 for sequence in range( 0, len( items[ variant ] ) ):
468                         for combo in range( 0, len( items[ variant ][ sequence ] ) ):
469                                 if items[ variant ][ sequence ][ combo ][0] in backend.requiredCapabilities.keys():
470                                         try:
471                                                 # Use backend capability name and a single argument
472                                                 items[ variant ][ sequence ][ combo ] = tuple(
473                                                         [ backend.capabilityLookup( items[ variant ][ sequence ][ combo ][0] ),
474                                                         tuple( [ hid_lookup_dictionary[ items[ variant ][ sequence ][ combo ] ] ] ) ]
475                                                 )
476                                         except KeyError:
477                                                 print ( "{0} {1} is an invalid HID lookup value".format( ERROR, items[ variant ][ sequence ][ combo ] ) )
478                                                 sys.exit( 1 )
479         return items
480
481
482 # Convert tuple of tuples to list of lists
483 def listit( t ):
484         return list( map( listit, t ) ) if isinstance( t, ( list, tuple ) ) else t
485
486 # Convert list of lists to tuple of tuples
487 def tupleit( t ):
488         return tuple( map( tupleit, t ) ) if isinstance( t, ( tuple, list ) ) else t
489
490
491  ## Evaluation Rules
492
493 def eval_scanCode( triggers, operator, results ):
494         # Convert to lists of lists of lists to tuples of tuples of tuples
495         # Tuples are non-mutable, and can be used has index items
496         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
497         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
498
499         # Lookup interconnect id (Current file scope)
500         # Default to 0 if not specified
501         if 'ConnectId' not in variables_dict.overallVariables.keys():
502                 id_num = 0
503         else:
504                 id_num = int( variables_dict.overallVariables['ConnectId'] )
505
506         # Iterate over all combinations of triggers and results
507         for sequence in triggers:
508                 # Convert tuple of tuples to list of lists so each element can be modified
509                 trigger = listit( sequence )
510
511                 # Create ScanCode entries for trigger
512                 for seq_index, combo in enumerate( sequence ):
513                         for com_index, scancode in enumerate( combo ):
514                                 trigger[ seq_index ][ com_index ] = macros_map.scanCodeStore.append( ScanCode( scancode, id_num ) )
515
516                 # Convert back to a tuple of tuples
517                 trigger = tupleit( trigger )
518
519                 for result in results:
520                         # Append Case
521                         if operator == ":+":
522                                 macros_map.appendScanCode( trigger, result )
523
524                         # Remove Case
525                         elif operator == ":-":
526                                 macros_map.removeScanCode( trigger, result )
527
528                         # Replace Case
529                         elif operator == ":":
530                                 macros_map.replaceScanCode( trigger, result )
531
532 def eval_usbCode( triggers, operator, results ):
533         # Convert to lists of lists of lists to tuples of tuples of tuples
534         # Tuples are non-mutable, and can be used has index items
535         triggers = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in triggers )
536         results  = tuple( tuple( tuple( sequence ) for sequence in variant ) for variant in results )
537
538         # Iterate over all combinations of triggers and results
539         for trigger in triggers:
540                 scanCodes = macros_map.lookupUSBCodes( trigger )
541                 for scanCode in scanCodes:
542                         for result in results:
543                                 # Cache assignment until file finishes processing
544                                 macros_map.cacheAssignment( operator, scanCode, result )
545
546 def eval_variable( name, content ):
547         # Content might be a concatenation of multiple data types, convert everything into a single string
548         assigned_content = ""
549         for item in content:
550                 assigned_content += str( item )
551
552         variables_dict.assignVariable( name, assigned_content )
553
554 def eval_capability( name, function, args ):
555         capabilities_dict[ name ] = [ function, args ]
556
557 def eval_define( name, cdefine_name ):
558         variables_dict.defines[ name ] = cdefine_name
559
560 map_scanCode   = unarg( eval_scanCode )
561 map_usbCode    = unarg( eval_usbCode )
562
563 set_variable   = unarg( eval_variable )
564 set_capability = unarg( eval_capability )
565 set_define     = unarg( eval_define )
566
567
568  ## Sub Rules
569
570 usbCode     = tokenType('USBCode') >> make_usbCode
571 scanCode    = tokenType('ScanCode') >> make_scanCode
572 consCode    = tokenType('ConsCode') >> make_consCode
573 sysCode     = tokenType('SysCode') >> make_sysCode
574 none        = tokenType('None') >> make_none
575 name        = tokenType('Name')
576 number      = tokenType('Number') >> make_number
577 comma       = tokenType('Comma')
578 dash        = tokenType('Dash')
579 plus        = tokenType('Plus')
580 content     = tokenType('VariableContents')
581 string      = tokenType('String') >> make_string
582 unString    = tokenType('String') # When the double quotes are still needed for internal processing
583 seqString   = tokenType('SequenceString') >> make_seqString
584 unseqString = tokenType('SequenceString') >> make_unseqString # For use with variables
585
586   # Code variants
587 code_end = tokenType('CodeEnd')
588
589   # Scan Codes
590 scanCode_start     = tokenType('ScanCodeStart')
591 scanCode_range     = number + skip( dash ) + number >> make_scanCode_range
592 scanCode_listElem  = number >> listElem
593 scanCode_innerList = oneplus( ( scanCode_range | scanCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
594 scanCode_expanded  = skip( scanCode_start ) + scanCode_innerList + skip( code_end )
595 scanCode_elem      = scanCode >> listElem
596 scanCode_combo     = oneplus( ( scanCode_expanded | scanCode_elem ) + skip( maybe( plus ) ) )
597 scanCode_sequence  = oneplus( scanCode_combo + skip( maybe( comma ) ) )
598
599   # USB Codes
600 usbCode_start       = tokenType('USBCodeStart')
601 usbCode_number      = number >> make_usbCode_number
602 usbCode_range       = ( usbCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_usbCode_range
603 usbCode_listElemTag = unString >> make_usbCode
604 usbCode_listElem    = ( usbCode_number | usbCode_listElemTag ) >> listElem
605 usbCode_innerList   = oneplus( ( usbCode_range | usbCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
606 usbCode_expanded    = skip( usbCode_start ) + usbCode_innerList + skip( code_end )
607 usbCode_elem        = usbCode >> listElem
608 usbCode_combo       = oneplus( ( usbCode_expanded | usbCode_elem ) + skip( maybe( plus ) ) ) >> listElem
609 usbCode_sequence    = oneplus( ( usbCode_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
610
611   # Cons Codes
612 consCode_start       = tokenType('ConsCodeStart')
613 consCode_number      = number >> make_consCode_number
614 consCode_range       = ( consCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_consCode_range
615 consCode_listElemTag = unString >> make_consCode
616 consCode_listElem    = ( consCode_number | consCode_listElemTag ) >> listElem
617 consCode_innerList   = oneplus( ( consCode_range | consCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
618 consCode_expanded    = skip( consCode_start ) + consCode_innerList + skip( code_end )
619 consCode_elem        = consCode >> listElem
620
621   # Sys Codes
622 sysCode_start       = tokenType('SysCodeStart')
623 sysCode_number      = number >> make_sysCode_number
624 sysCode_range       = ( sysCode_number | unString ) + skip( dash ) + ( number | unString ) >> make_sysCode_range
625 sysCode_listElemTag = unString >> make_sysCode
626 sysCode_listElem    = ( sysCode_number | sysCode_listElemTag ) >> listElem
627 sysCode_innerList   = oneplus( ( sysCode_range | sysCode_listElem ) + skip( maybe( comma ) ) ) >> flatten
628 sysCode_expanded    = skip( sysCode_start ) + sysCode_innerList + skip( code_end )
629 sysCode_elem        = sysCode >> listElem
630
631   # HID Codes
632 hidCode_elem        = usbCode_expanded | usbCode_elem | sysCode_expanded | sysCode_elem | consCode_expanded | consCode_elem
633
634   # Capabilities
635 capFunc_arguments = many( number + skip( maybe( comma ) ) ) >> listToTuple
636 capFunc_elem      = name + skip( parenthesis('(') ) + capFunc_arguments + skip( parenthesis(')') ) >> capArgExpander >> listElem
637 capFunc_combo     = oneplus( ( hidCode_elem | capFunc_elem ) + skip( maybe( plus ) ) ) >> listElem
638 capFunc_sequence  = oneplus( ( capFunc_combo | seqString ) + skip( maybe( comma ) ) ) >> oneLayerFlatten
639
640   # Trigger / Result Codes
641 triggerCode_outerList    = scanCode_sequence >> optionExpansion
642 triggerUSBCode_outerList = usbCode_sequence >> optionExpansion >> hidCodeToCapability
643 resultCode_outerList     = ( ( capFunc_sequence >> optionExpansion ) | none ) >> hidCodeToCapability
644
645
646  ## Main Rules
647
648 #| <variable> = <variable contents>;
649 variable_contents   = name | content | string | number | comma | dash | unseqString
650 variable_expression = name + skip( operator('=') ) + oneplus( variable_contents ) + skip( eol ) >> set_variable
651
652 #| <capability name> => <c function>;
653 capability_arguments  = name + skip( operator(':') ) + number + skip( maybe( comma ) )
654 capability_expression = name + skip( operator('=>') ) + name + skip( parenthesis('(') ) + many( capability_arguments ) + skip( parenthesis(')') ) + skip( eol ) >> set_capability
655
656 #| <define name> => <c define>;
657 define_expression = name + skip( operator('=>') ) + name + skip( eol ) >> set_define
658
659 #| <trigger> : <result>;
660 operatorTriggerResult = operator(':') | operator(':+') | operator(':-')
661 scanCode_expression   = triggerCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_scanCode
662 usbCode_expression    = triggerUSBCode_outerList + operatorTriggerResult + resultCode_outerList + skip( eol ) >> map_usbCode
663
664 def parse( tokenSequence ):
665         """Sequence(Token) -> object"""
666
667         # Top-level Parser
668         expression = scanCode_expression | usbCode_expression | variable_expression | capability_expression | define_expression
669
670         kll_text = many( expression )
671         kll_file = maybe( kll_text ) + skip( finished )
672
673         return kll_file.parse( tokenSequence )
674
675
676
677 def processKLLFile( filename ):
678         with open( filename ) as file:
679                 data = file.read()
680                 tokenSequence = tokenize( data )
681                 #print ( pformat( tokenSequence ) ) # Display tokenization
682                 try:
683                         tree = parse( tokenSequence )
684                 except NoParseError as e:
685                         print("Error parsing %s. %s" % (filename, e.msg), file=sys.stderr)
686                         sys.exit(1)
687
688
689 ### Misc Utility Functions ###
690
691 def gitRevision( kllPath ):
692         import subprocess
693
694         # Change the path to where kll.py is
695         origPath = os.getcwd()
696         os.chdir( kllPath )
697
698         # Just in case git can't be found
699         try:
700                 # Get hash of the latest git commit
701                 revision = subprocess.check_output( ['git', 'rev-parse', 'HEAD'] ).decode()[:-1]
702
703                 # Get list of files that have changed since the commit
704                 changed = subprocess.check_output( ['git', 'diff-index', '--name-only', 'HEAD', '--'] ).decode().splitlines()
705         except:
706                 revision = "<no git>"
707                 changed = []
708
709         # Change back to the old working directory
710         os.chdir( origPath )
711
712         return revision, changed
713
714
715 ### Main Entry Point ###
716
717 if __name__ == '__main__':
718         (baseFiles, defaultFiles, partialFileSets, backend_name, templates, outputs) = processCommandLineArgs()
719
720         # Look up git information on the compiler
721         gitRev, gitChanges = gitRevision( os.path.dirname( os.path.realpath( __file__ ) ) )
722
723         # Load backend module
724         global backend
725         backend_import = importlib.import_module( "backends.{0}".format( backend_name ) )
726         backend = backend_import.Backend( templates )
727
728         # Process base layout files
729         for filename in baseFiles:
730                 variables_dict.setCurrentFile( filename )
731                 processKLLFile( filename )
732         macros_map.completeBaseLayout() # Indicates to macros_map that the base layout is complete
733         variables_dict.baseLayoutFinished()
734
735         # Default combined layer
736         for filename in defaultFiles:
737                 variables_dict.setCurrentFile( filename )
738                 processKLLFile( filename )
739                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
740                 macros_map.replayCachedAssignments()
741
742         # Iterate through additional layers
743         for partial in partialFileSets:
744                 # Increment layer for each -p option
745                 macros_map.addLayer()
746                 variables_dict.incrementLayer() # DefaultLayer is layer 0
747
748                 # Iterate and process each of the file in the layer
749                 for filename in partial:
750                         variables_dict.setCurrentFile( filename )
751                         processKLLFile( filename )
752
753                 # Apply assignment cache, see 5.1.2 USB Codes for why this is necessary
754                 macros_map.replayCachedAssignments()
755                 # Remove un-marked keys to complete the partial layer
756                 macros_map.removeUnmarked()
757
758         # Do macro correlation and transformation
759         macros_map.generate()
760
761         # Process needed templating variables using backend
762         backend.process(
763                 capabilities_dict,
764                 macros_map,
765                 variables_dict,
766                 gitRev,
767                 gitChanges
768         )
769
770         # Generate output file using template and backend
771         backend.generate( outputs )
772
773         # Successful Execution
774         sys.exit( 0 )
775