-
Notifications
You must be signed in to change notification settings - Fork 0
/
kle-gen.py
240 lines (198 loc) · 10.1 KB
/
kle-gen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# Generate KLE json from KLFC keyboard layout json
import sys, json, re, copy, os
from os import path
MIN_ARGS = 3
VALID_OPTIONS = [
["-kt", "-ks", "-kc", "-lc", "-sl", "-bc"],
["--keyboard-type", "--keyboard-size", "--key-color", "--label-color", "--shift-levels", "--back-color"]]
OPTION_DEFAULTS = {
"-kt" : "iso",
"-ks" : "100",
"-kc" : "545454",
"-lc" : "e0dcc8",
"-bc" : "454545",
"-sl" : "2"
}
#Each entry is an array of positions (positions.md). First entry is the one in the base .json. Other ones are aliases.
#This array is processed to match a keyboard type and size if supplied.
ALIASES = [
["Esc", "Escape", "ESC"], ["F1", "FK01"], ["F2", "FK02"], ["F3", "FK03"], ["F4", "FK04"], ["F5", "FK05"], ["F6", "FK06"], ["F7", "FK07"], ["F8", "FK08"], ["F9", "FK09"], ["F10", "FK10"], ["F11", "FK11"], ["F12", "FK12"], ["PrintScreen", "PRSC"], ["ScrollLock", "SCLK"], ["Pause", "PAUS"],
["~", "``` ``", "Tilde", "TLDE"], ["1", "AE01"], ["2", "AE02"], ["3", "AE03"], ["4", "AE04"], ["5", "AE05"], ["6", "AE06"], ["7", "AE07"], ["8", "AE08"], ["9", "AE09"], ["0", "AE10"], ["-", "Minus", "AE11"], ["+", "=", "Plus", "AE12"], ["Yen", "AE13"], ["Backspace", "BKSP"],
["Tab", "TAB"], ["Q", "AD01"], ["W", "AD02"], ["E", "AD03"], ["R", "AD04"], ["T", "AD05"], ["Y", "AD06"], ["U", "AD07"], ["I", "AD08"], ["O", "AD09"], ["P", "AD10"], ["[", "AD11"], ["]", "AD12"], ["\\", "BKSL"],
["CapsLock", "CAPS"], ["A", "AC01"], ["S", "AC02"], ["D", "AC03"], ["F", "AC04"], ["G", "AC05"], ["H", "AC06"], ["J", "AC07"], ["K", "AC08"], ["L", "AC09"], [";", "AC10"], ["'", "AC11"], ["\\", "BKSL"], ["Enter", "RTRN"],
["Shift_L", "LFSH"], ["Iso", "LSGT"], ["Z", "AB01"], ["X", "AB02"], ["C", "AB03"], ["V", "AB04"], ["B", "AB05"], ["N", "AB06"], ["M", "AB07"], [",", "AB08"], [".", "AB09"], ["/", "AB10"], ["Ro", "AB11"], ["Shift_R", "RTSH"],
["Control_L", "LCTL"], ["Win_L", "LWIN"], ["Alt_L", "LALT"], ["Mhen", "Muhenkan"], ["Space", "SPCE"], ["Henk", "Henkan"], ["Kana", "Katakana"], ["Alt_R", "RALT"], ["Win_R", "RWIN"], ["Menu", "MENU"], ["Control_R", "RCTL"],
["Insert", "INS"], ["Delete", "DELE"], ["Home", "HOME"], ["End", "END"], ["PageUp", "PGUP"], ["PageDown", "PGDN"], ["Up", "UP"], ["Left", "LEFT"], ["Down", "DOWN"], ["Right", "RGHT"],
["NumLock", "NMLK"], ["KP_Div", "KPDV"], ["KP_Mult", "KPMU"], ["KP_Min", "KPSU"], ["KP_7", "KP7"], ["KP_8", "KP8"], ["KP_9", "KP9"], ["KP_Plus", "KPAD"], ["KP_4", "KP4"], ["KP_5", "KP5"], ["KP_6", "KP6"], ["KP_Comma"], ["KP_1", "KP1"], ["KP_2", "KP2"], ["KP_3", "KP3"], ["KP_Enter", "KPEN"], ["KP_0", "KP0"], ["KP_Dec", "KPDL"]
]
def main():
#Check for arg numbers
if len(sys.argv) == 1:
usage()
return
elif len(sys.argv) == 2:
if (sys.argv[1] == 'man' or sys.argv[1] == 'help'):
help()
return
else:
usage()
return
elif len(sys.argv) > 2 and len(sys.argv) < MIN_ARGS:
usage()
return
elif len(sys.argv) >= MIN_ARGS:
# Check that args are good
if not check_args():
usage()
return
#Fall-through only with good args and good arg number.
#Get all options, already tested
comps = parse_components()
#Open input file, parse .json, strip of all comments, load to memory
file1 = open(comps["file"])
file2 = open("./temp", 'w')
for line in file1.readlines():
x = re.findall('^//', line)
if not x:
file2.write(line)
file1.close()
file2.close()
with open("./temp") as f:
data = json.load(f)
os.remove("./temp")
# Identify source file formatting and parse source file to a dictionary
key_list = parse_source_json(data, comps)
#Create output file from base, change based on memory
keyb_type = comps["-kt"].lower()
keyb_size = comps["-ks"]
base_file = "base_" + keyb_type + "_" + keyb_size + ".json"
def parse_source_json(data, comps):
if "keys" in data: #single keyblock
key_list = data["keys"]
#remove all shiftlevels beyond the stated in the -sl option
max_shiftlevels = int(comps["-sl"])
print("Keeping the following shift levels: " + str(data["shiftlevels"][:max_shiftlevels]))
for i in key_list:
i["letters"] = i["letters"][:max_shiftlevels]
#normalize keylist to keep all shiftlevels populated
while len(i["letters"]) < max_shiftlevels:
i["letters"].append("")
return key_list
else: #multiple keyblocks
#get which shiftlevels to keep with a cursed double list comprehension which i can't think too much about right now
full_shiftlevels = [n for m in [i["shiftlevels"] for i in data["keysWithShiftlevels"]] for n in m]
#remove duplicates
full_shiftlevels = list(dict.fromkeys(full_shiftlevels))
shiftlevels_to_keep = full_shiftlevels[:int(comps["-sl"])]
print("Keeping the following shift levels: " + str(shiftlevels_to_keep))
#Add shiftlevels to all blocks
#Canon order is the one in full_shiftlevels,other blocks are adapted to fit the ordering of the first block
#1. Add shiftlevels to first block so it matches the -sl number
firstblock = data["keysWithShiftlevels"][0]
#2. Process subsequent blocks
for block in data["keysWithShiftlevels"][1:]
def parse_components():
#Get all components
comps = extract_components(True)
#Get list of present options
keys = []
for k in comps.keys():
keys.append(k)
#Normalize options to short form
for i in range(0, len(keys)):
if keys[i] in VALID_OPTIONS[1]:
comps[VALID_OPTIONS[0][VALID_OPTIONS[1].index(keys[i])]] = comps[keys[i]]
del comps[keys[i]]
#Fill with missing options and their default values
for i in OPTION_DEFAULTS.keys():
if i not in comps.keys():
comps[i] = OPTION_DEFAULTS[i]
return comps
# Check args for number and validity.
def check_args():
max_args = len(VALID_OPTIONS[0]) + len(VALID_OPTIONS[1]) + MIN_ARGS
# Check that last arg is valid output path
if not (path.isdir(sys.argv[len(sys.argv)-1])):
print("Invalid output path. Use a relative path (e.g. './') or absolute path (e.g. '/home/user/klfc/kle/myfiles', 'C://User/KLFC/MyFiles'). No quotes.")
# Check that second to last arg is valid file
if not (path.exists(sys.argv[len(sys.argv)-2]) and re.match('^.*\.json$', sys.argv[len(sys.argv)-2])):
print("Invalid input file. Use a relative or absolute path. Filename must end with .json. File must be a KLFC layout file. No quotes.")
# Check that arg number makes sense (odd, equal or more than 3+len(valid_options), equal or less than MIN_ARGS)
if len(sys.argv) > max_args:
print("Too many arguments. Expected no less than " + str(MIN_ARGS) + " and no more than " + max_args + ".")
return False
if len(sys.argv) % 2 == 0:
print("Bad input.")
return False
# Obtain option arguments for checking, without file and path entries
options = extract_components(False)
# Check if options exist
for i in options.keys():
if not i in VALID_OPTIONS[0] and not i in VALID_OPTIONS[1]:
print("\nInvalid option: " + i + "\n")
return False
# Check if option values exist
for i in options.keys():
value = options[i]
if i == "-kt" or i == "--keyboard-type":
if not (value in ["ansi", "ANSI", "iso", "ISO", "ans", "ANS", "jis", "JIS", "abnt", "ABNT"]):
print("\nUnrecognized keyboard type: " + value + "\nSupported types: ANSI, ISO, JIS, ABNT.")
return False
if i == "-ks" or i == "--keyboard-size":
if not (value in ["60", "80", "100"]):
print("\nUnrecognized keyboard size: " + value + "\nSupported sizes: 60, 80, 100.")
return False
if i == "-kc" or i == "--key-color":
if not (re.match("^[0-9a-fA-F]{6}$", value)):
print("\nUnrecognized hex color: " + value + "\nSupported format: ffffff. No hash.")
return False
if i == "-lc" or i == "--label-color":
if not (re.match("^[0-9a-fA-F]{6}$", value)):
print("\nUnrecognized hex color: " + value + "\nSupported format: ffffff. No hash.")
return False
if i == "-sl" or i == "--shift-levels":
if int(value) < 1 or int(value) > 4:
print("\nUnrecognized shift values: " + value + "\nSupported values: 1, 2, 3, 4")
return False
if i == "-bc" or i == "--back-color":
if not (re.match("^[0-9a-fA-F]{6}$", value)):
print("\nUnrecognized hex color: " + value + "\nSupported format: ffffff. No hash.")
return False
return True
# Returns a dictionary with all relevant components. Components not checked for validity.
def extract_components(include_file_and_path):
extracted = {}
#Process file and path
if include_file_and_path:
extracted["file"] = sys.argv[-2]
extracted["output_path"] = sys.argv[-1]
#Process options
used_options = copy.deepcopy(sys.argv)
del used_options[-1]
del used_options[-1]
del used_options[0]
for i in range(0, len(used_options)-1):
if i % 2 == 0:
extracted[used_options[i]] = used_options[i+1]
#Return
return extracted
# Short help
def usage():
print("\nUsage: python kle-gen.py [OPTIONS] [KLFC_FILE.json] [OUTPUT_PATH]")
print("Input 'python kle-gen.py help' or 'python kle-gen.py man' for help.\n")
# Long help
def help():
print("\nSYNTAX:")
print("python kle-gen.py [OPTIONS] file_to_convert.json [OUTPUT_PATH]\n")
print("OPTIONS:")
print("-kt / --keyboard-type (optional): Keyboard type. Defaults to ISO. Can be ANSI, ISO, JIS or ABNT")
print("-ks / --keyboard-size (optional): Keyboard size (integer; 100, 80 or 60). Defaults to 100%.")
print("-kc / --key-color (optional): Key color (hex value). Defaults to grey.")
print("-lc / --label-color (optional): Label color (hex value). Defaults to off-white.")
print("-sl / --shift-levels (optional): Shift levels (integer, 1-4). Shift levels will be placed in the following order: top left, bottom left, top right, bottom right. Defaults to 2.\n")
print("The last argument MUST be the output path. Simply add '.' to default to the script dir. Must be a directory.")
print("The second to last argument MUST be the KLFC .json file to convert. Absolute or relative path.\n")
print("Full input example: python kle-gen.py -kt iso -ks 80 -kc #a5a5a5 -lc #aaaaaa -sl 2 ./colemak.json .")
print("Minimum input example: python kle-gen.py ./colemak.json .\n")
if __name__ == "__main__":
main()