]> git.donarmstrong.com Git - kiibohd-kll.git/blob - kll_lib/containers.py
Adding support for "Soft Replace" kll 0.3c
[kiibohd-kll.git] / kll_lib / containers.py
1 #!/usr/bin/env python3
2 # KLL Compiler Containers
3 #
4 # Copyright (C) 2014-2015 by Jacob Alexander
5 #
6 # This file is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This file is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this file.  If not, see <http://www.gnu.org/licenses/>.
18
19 ### Imports ###
20
21 import copy
22
23
24
25 ### Decorators ###
26
27  ## Print Decorator Variables
28 ERROR = '\033[5;1;31mERROR\033[0m:'
29
30
31
32 ### Parsing ###
33
34
35  ## Containers
36
37 class ScanCode:
38         # Container for ScanCodes
39         #
40         # scancode        - Non-interconnect adjusted scan code
41         # interconnect_id - Unique id for the interconnect node
42         def __init__( self, scancode, interconnect_id ):
43                 self.scancode = scancode
44                 self.interconnect_id = interconnect_id
45
46         def __eq__( self, other ):
47                 return self.dict() == other.dict()
48
49         def __repr__( self ):
50                 return repr( self.dict() )
51
52         def dict( self ):
53                 return {
54                         'ScanCode' : self.scancode,
55                         'Id'       : self.interconnect_id,
56                 }
57
58         # Calculate the actual scancode using the offset list
59         def offset( self, offsetList ):
60                 if self.interconnect_id > 0:
61                         return self.scancode + offsetList[ self.interconnect_id - 1 ]
62                 else:
63                         return self.scancode
64
65
66 class ScanCodeStore:
67         # Unique lookup for ScanCodes
68         def __init__( self ):
69                 self.scancodes = []
70
71         def __getitem__( self, name ):
72                 # First check if this is a ScanCode object
73                 if isinstance( name, ScanCode ):
74                         # Do a reverse lookup
75                         for idx, scancode in enumerate( self.scancodes ):
76                                 if scancode == name:
77                                         return idx
78
79                         # Could not find scancode
80                         return None
81
82                 # Return scancode using unique id
83                 return self.scancodes[ name ]
84
85         # Attempt add ScanCode to list, return unique id
86         def append( self, new_scancode ):
87                 # Iterate through list to make sure this is a unique ScanCode
88                 for idx, scancode in enumerate( self.scancodes ):
89                         if new_scancode == scancode:
90                                 return idx
91
92                 # Unique entry, add to the list
93                 self.scancodes.append( new_scancode )
94
95                 return len( self.scancodes ) - 1
96
97
98 class Capabilities:
99         # Container for capabilities dictionary and convenience functions
100         def __init__( self ):
101                 self.capabilities = dict()
102
103         def __getitem__( self, name ):
104                 return self.capabilities[ name ]
105
106         def __setitem__( self, name, contents ):
107                 self.capabilities[ name ] = contents
108
109         def __repr__( self ):
110                 return "Capabilities => {0}\nIndexed Capabilities => {1}".format( self.capabilities, sorted( self.capabilities, key = self.capabilities.get ) )
111
112
113         # Total bytes needed to store arguments
114         def totalArgBytes( self, name ):
115                 totalBytes = 0
116
117                 # Iterate over the arguments, summing the total bytes
118                 for arg in self.capabilities[ name ][ 1 ]:
119                         totalBytes += int( arg[ 1 ] )
120
121                 return totalBytes
122
123         # Name of the capability function
124         def funcName( self, name ):
125                 return self.capabilities[ name ][ 0 ]
126
127
128         # Only valid while dictionary keys are not added/removed
129         def getIndex( self, name ):
130                 return sorted( self.capabilities, key = self.capabilities.get ).index( name )
131
132         def getName( self, index ):
133                 return sorted( self.capabilities, key = self.capabilities.get )[ index ]
134
135         def keys( self ):
136                 return sorted( self.capabilities, key = self.capabilities.get )
137
138
139 class Macros:
140         # Container for Trigger Macro : Result Macro correlation
141         # Layer selection for generating TriggerLists
142         #
143         # Only convert USB Code list once all the ResultMacros have been accumulated (does a macro reduction; not reversible)
144         # Two staged list for ResultMacros:
145         #  1) USB Code/Non-converted (may contain capabilities)
146         #  2) Capabilities
147         def __init__( self ):
148                 # Default layer (0)
149                 self.layer = 0
150
151                 # Unique ScanCode Hash Id Lookup
152                 self.scanCodeStore = ScanCodeStore()
153
154                 # Macro Storage
155                 self.macros = [ dict() ]
156
157                 # Base Layout Storage
158                 self.baseLayout = None
159                 self.layerLayoutMarkers = []
160
161                 # Correlated Macro Data
162                 self.resultsIndex = dict()
163                 self.triggersIndex = dict()
164                 self.resultsIndexSorted = []
165                 self.triggersIndexSorted = []
166                 self.triggerList = []
167                 self.maxScanCode = []
168                 self.firstScanCode = []
169                 self.interconnectOffset = []
170
171                 # USBCode Assignment Cache
172                 self.assignmentCache = []
173
174         def __repr__( self ):
175                 return "{0}".format( self.macros )
176
177         def completeBaseLayout( self ):
178                 # Copy base layout for later use when creating partial layers and add marker
179                 self.baseLayout = copy.deepcopy( self.macros[ 0 ] )
180                 self.layerLayoutMarkers.append( copy.deepcopy( self.baseLayout ) ) # Not used for default layer, just simplifies coding
181
182         def removeUnmarked( self ):
183                 # Remove all of the unmarked mappings from the partial layer
184                 for trigger in self.layerLayoutMarkers[ self.layer ].keys():
185                         del self.macros[ self.layer ][ trigger ]
186
187         def addLayer( self ):
188                 # Increment layer count, and append another macros dictionary
189                 self.layer += 1
190                 self.macros.append( copy.deepcopy( self.baseLayout ) )
191
192                 # Add a layout marker for each layer
193                 self.layerLayoutMarkers.append( copy.deepcopy( self.baseLayout ) )
194
195         # Use for ScanCode trigger macros
196         def appendScanCode( self, trigger, result ):
197                 if not trigger in self.macros[ self.layer ]:
198                         self.replaceScanCode( trigger, result )
199                 else:
200                         self.macros[ self.layer ][ trigger ].append( result )
201
202         # Remove the given trigger/result pair
203         def removeScanCode( self, trigger, result ):
204                 # Remove all instances of the given trigger/result pair
205                 while result in self.macros[ self.layer ][ trigger ]:
206                         self.macros[ self.layer ][ trigger ].remove( result )
207
208         # Replaces the given trigger with the given result
209         # If multiple results for a given trigger, clear, then add
210         def replaceScanCode( self, trigger, result ):
211                 self.macros[ self.layer ][ trigger ] = [ result ]
212
213                 # Mark layer scan code, so it won't be removed later
214                 # Also check to see if it hasn't already been removed before
215                 if not self.baseLayout is None and trigger in self.layerLayoutMarkers[ self.layer ]:
216                         del self.layerLayoutMarkers[ self.layer ][ trigger ]
217
218         # Return a list of ScanCode triggers with the given USB Code trigger
219         def lookupUSBCodes( self, usbCode ):
220                 scanCodeList = []
221
222                 # Scan current layer for USB Codes
223                 for macro in self.macros[ self.layer ].keys():
224                         if usbCode in self.macros[ self.layer ][ macro ]:
225                                 scanCodeList.append( macro )
226
227                 if len(scanCodeList) == 0:
228                         if len(usbCode) > 1 or len(usbCode[0]) > 1:
229                                 for combo in usbCode:
230                                         comboCodes = list()
231                                         for key in combo:
232                                                 scanCode = self.lookupUSBCodes(((key,),))
233                                                 comboCodes.append(scanCode[0][0][0])
234                                         scanCodeList.append(tuple(code for code in comboCodes))
235                                 scanCodeList = [tuple(scanCodeList)]
236
237                 return scanCodeList
238
239         # Check whether we should do soft replacement
240         def softReplaceCheck( self, scanCode ):
241                 # First check if not the default layer
242                 if self.layer == 0:
243                         return True
244
245                 # Check if current layer is set the same as the BaseMap
246                 if not self.baseLayout is None and scanCode in self.layerLayoutMarkers[ self.layer ]:
247                         return False
248
249                 # Otherwise, allow replacement
250                 return True
251
252         # Cache USBCode Assignment
253         def cacheAssignment( self, operator, scanCode, result ):
254                 self.assignmentCache.append( [ operator, scanCode, result ] )
255
256         # Assign cached USBCode Assignments
257         def replayCachedAssignments( self ):
258                 # Iterate over each item in the assignment cache
259                 for item in self.assignmentCache:
260                         # Check operator, and choose the specified assignment action
261                         # Append Case
262                         if item[0] == ":+":
263                                 self.appendScanCode( item[1], item[2] )
264
265                         # Remove Case
266                         elif item[0] == ":-":
267                                 self.removeScanCode( item[1], item[2] )
268
269                         # Replace Case
270                         elif item[0] == ":" or item[0] == "::":
271                                 self.replaceScanCode( item[1], item[2] )
272
273                 # Clear assignment cache
274                 self.assignmentCache = []
275
276         # Generate/Correlate Layers
277         def generate( self ):
278                 self.generateIndices()
279                 self.sortIndexLists()
280                 self.generateOffsetTable()
281                 self.generateTriggerLists()
282
283         # Generates Index of Results and Triggers
284         def generateIndices( self ):
285                 # Iterate over every trigger result, and add to the resultsIndex and triggersIndex
286                 for layer in range( 0, len( self.macros ) ):
287                         for trigger in self.macros[ layer ].keys():
288                                 # Each trigger has a list of results
289                                 for result in self.macros[ layer ][ trigger ]:
290                                         # Only add, with an index, if result hasn't been added yet
291                                         if not result in self.resultsIndex:
292                                                 self.resultsIndex[ result ] = len( self.resultsIndex )
293
294                                         # Then add a trigger for each result, if trigger hasn't been added yet
295                                         triggerItem = tuple( [ trigger, self.resultsIndex[ result ] ] )
296                                         if not triggerItem in self.triggersIndex:
297                                                 self.triggersIndex[ triggerItem ] = len( self.triggersIndex )
298
299         # Sort Index Lists using the indices rather than triggers/results
300         def sortIndexLists( self ):
301                 self.resultsIndexSorted = [ None ] * len( self.resultsIndex )
302                 # Iterate over the resultsIndex and sort by index
303                 for result in self.resultsIndex.keys():
304                         self.resultsIndexSorted[ self.resultsIndex[ result ] ] = result
305
306                 self.triggersIndexSorted = [ None ] * len( self.triggersIndex )
307                 # Iterate over the triggersIndex and sort by index
308                 for trigger in self.triggersIndex.keys():
309                         self.triggersIndexSorted[ self.triggersIndex[ trigger ] ] = trigger
310
311         # Generates list of offsets for each of the interconnect ids
312         def generateOffsetTable( self ):
313                 idMaxScanCode = [ 0 ]
314
315                 # Iterate over each layer to get list of max scancodes associated with each interconnect id
316                 for layer in range( 0, len( self.macros ) ):
317                         # Iterate through each trigger/sequence in the layer
318                         for sequence in self.macros[ layer ].keys():
319                                 # Iterate over the trigger to locate the ScanCodes
320                                 for combo in sequence:
321                                         # Iterate over each scancode id in the combo
322                                         for scancode_id in combo:
323                                                 # Lookup ScanCode
324                                                 scancode_obj = self.scanCodeStore[ scancode_id ]
325
326                                                 # Extend list if not large enough
327                                                 if scancode_obj.interconnect_id >= len( idMaxScanCode ):
328                                                         idMaxScanCode.extend( [ 0 ] * ( scancode_obj.interconnect_id - len( idMaxScanCode ) + 1 ) )
329
330                                                 # Determine if the max seen id for this interconnect id
331                                                 if scancode_obj.scancode > idMaxScanCode[ scancode_obj.interconnect_id ]:
332                                                         idMaxScanCode[ scancode_obj.interconnect_id ] = scancode_obj.scancode
333
334                 # Generate interconnect offsets
335                 self.interconnectOffset = [ idMaxScanCode[0] + 1 ]
336                 for index in range( 1, len( idMaxScanCode ) ):
337                         self.interconnectOffset.append( self.interconnectOffset[ index - 1 ] + idMaxScanCode[ index ] )
338
339         # Generates Trigger Lists per layer using index lists
340         def generateTriggerLists( self ):
341                 for layer in range( 0, len( self.macros ) ):
342                         # Set max scancode to 0xFF (255)
343                         # But keep track of the actual max scancode and reduce the list size
344                         self.triggerList.append( [ [] ] * 0xFF )
345                         self.maxScanCode.append( 0x00 )
346
347                         # Iterate through trigger macros to locate necessary ScanCodes and corresponding triggerIndex
348                         for trigger in self.macros[ layer ].keys():
349                                 for variant in range( 0, len( self.macros[ layer ][ trigger ] ) ):
350                                         # Identify result index
351                                         resultIndex = self.resultsIndex[ self.macros[ layer ][ trigger ][ variant ] ]
352
353                                         # Identify trigger index
354                                         triggerIndex = self.triggersIndex[ tuple( [ trigger, resultIndex ] ) ]
355
356                                         # Iterate over the trigger to locate the ScanCodes
357                                         for sequence in trigger:
358                                                 for combo_id in sequence:
359                                                         combo = self.scanCodeStore[ combo_id ].offset( self.interconnectOffset )
360                                                         # Append triggerIndex for each found scanCode of the Trigger List
361                                                         # Do not re-add if triggerIndex is already in the Trigger List
362                                                         if not triggerIndex in self.triggerList[ layer ][ combo ]:
363                                                                 # Append is working strangely with list pre-initialization
364                                                                 # Doing a 0 check replacement instead -HaaTa
365                                                                 if len( self.triggerList[ layer ][ combo ] ) == 0:
366                                                                         self.triggerList[ layer ][ combo ] = [ triggerIndex ]
367                                                                 else:
368                                                                         self.triggerList[ layer ][ combo ].append( triggerIndex )
369
370                                                         # Look for max Scan Code
371                                                         if combo > self.maxScanCode[ layer ]:
372                                                                 self.maxScanCode[ layer ] = combo
373
374                         # Shrink triggerList to actual max size
375                         self.triggerList[ layer ] = self.triggerList[ layer ][ : self.maxScanCode[ layer ] + 1 ]
376
377                         # Calculate first scan code for layer, useful for uC implementations trying to save RAM
378                         firstScanCode = 0
379                         for triggerList in range( 0, len( self.triggerList[ layer ] ) ):
380                                 firstScanCode = triggerList
381
382                                 # Break if triggerList has items
383                                 if len( self.triggerList[ layer ][ triggerList ] ) > 0:
384                                         break;
385                         self.firstScanCode.append( firstScanCode )
386
387                 # Determine overall maxScanCode
388                 self.overallMaxScanCode = 0x00
389                 for maxVal in self.maxScanCode:
390                         if maxVal > self.overallMaxScanCode:
391                                 self.overallMaxScanCode = maxVal
392
393
394 class Variables:
395         # Container for variables
396         # Stores three sets of variables, the overall combined set, per layer, and per file
397         def __init__( self ):
398                 # Dictionaries of variables
399                 self.baseLayout       = dict()
400                 self.fileVariables    = dict()
401                 self.layerVariables   = [ dict() ]
402                 self.overallVariables = dict()
403                 self.defines          = dict()
404
405                 self.currentFile = ""
406                 self.currentLayer = 0
407                 self.baseLayoutEnabled = True
408
409         def baseLayoutFinished( self ):
410                 self.baseLayoutEnabled = False
411
412         def setCurrentFile( self, name ):
413                 # Store using filename and current layer
414                 self.currentFile = name
415                 self.fileVariables[ name ] = dict()
416
417                 # If still processing BaseLayout
418                 if self.baseLayoutEnabled:
419                         if '*LayerFiles' in self.baseLayout.keys():
420                                 self.baseLayout['*LayerFiles'] += [ name ]
421                         else:
422                                 self.baseLayout['*LayerFiles'] = [ name ]
423                 # Set for the current layer
424                 else:
425                         if '*LayerFiles' in self.layerVariables[ self.currentLayer ].keys():
426                                 self.layerVariables[ self.currentLayer ]['*LayerFiles'] += [ name ]
427                         else:
428                                 self.layerVariables[ self.currentLayer ]['*LayerFiles'] = [ name ]
429
430         def incrementLayer( self ):
431                 # Store using layer index
432                 self.currentLayer += 1
433                 self.layerVariables.append( dict() )
434
435         def assignVariable( self, key, value ):
436                 # Overall set of variables
437                 self.overallVariables[ key ] = value
438
439                 # The Name variable is a special accumulation case
440                 if key == 'Name':
441                         # BaseLayout still being processed
442                         if self.baseLayoutEnabled:
443                                 if '*NameStack' in self.baseLayout.keys():
444                                         self.baseLayout['*NameStack'] += [ value ]
445                                 else:
446                                         self.baseLayout['*NameStack'] = [ value ]
447                         # Layers
448                         else:
449                                 if '*NameStack' in self.layerVariables[ self.currentLayer ].keys():
450                                         self.layerVariables[ self.currentLayer ]['*NameStack'] += [ value ]
451                                 else:
452                                         self.layerVariables[ self.currentLayer ]['*NameStack'] = [ value ]
453
454                 # If still processing BaseLayout
455                 if self.baseLayoutEnabled:
456                         self.baseLayout[ key ] = value
457                 # Set for the current layer
458                 else:
459                         self.layerVariables[ self.currentLayer ][ key ] = value
460
461                 # File context variables
462                 self.fileVariables[ self.currentFile ][ key ] = value
463