-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bakery.py
2835 lines (2518 loc) · 88.7 KB
/
bakery.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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Copyright 2023 BredOS
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import platform
import tempfile
from typing import Callable
import parted
import psutil
import toml
import subprocess
import os
import gettext
import io
import sys
from time import sleep, monotonic
from pathlib import Path
import socket
from datetime import datetime
import requests
import json
from traceback import print_exception
from threading import Lock
from functools import partial, wraps
import time
from pyrunning import logging, LogMessage, LoggingHandler, Command, LoggingLevel
import gi
gi.require_version("NM", "1.0")
from gi.repository import GLib, NM
import config
# Config functions
# Config spec
#
# author = str | Author of the config
# title = str | A title for the config.
# config_version = int | Used to test if this config is compatible.
# uninstall_upon_completion = bool | Remove bakery on completion.
# headless = bool | Do not ask or wait for any user confirmation.
#
# [gui]
# logo = str | Used to override the BredOS logo
#
# [settings]
# lang = str | Main display language ex. "en_us"
# kb_lang = list | Languages to be added to the keyboard.
#
# region = str | Region identifier.
# timezone = str | Timezone identifier.
#
# use_ntp = bool | Use network provided time.
#
# user = false / str | The configured username. Pass false to ask for input.
# passwd = false / str | Pass an empty string to disable or false to ask for input.
# ssh_pub = false / str | Install a pubkey in the user's home and enable sshd key login.
# | If a key is installed, password entry via ssh is disabled.
# root_passwd = false / str | Pass an empty string to disable root login or false to ask for input.
#
# [apps]
# app_selection = list | Apps selected to be installed.
# greeter_enable = false / str | Which greeter to enable.
# | Use None to boot to tty.
# [postint]
# type = int | Type of post-installation script to run.
# | 0 for none, 1 for local / remote script, 2 for a set of commands.
# data = false / str / list | For 0, 1, 2 respectively.
dryrun = False if "DO_DRYRUN" not in os.listdir() else True
# Timer functions
_stimer = monotonic()
def reset_timer() -> None:
global _stimer
_stimer = monotonic()
def get_timer() -> float:
return monotonic() - _stimer
# Translations
def setup_translations(lang: object = None) -> gettext.GNUTranslations:
"""
Setup translations
Does the following:
- Loads the translations from the locale folder
- Sets the translations for the gettext module
Returns: A gettext translation object
:rtype: object
"""
lang_path = os.path.join(os.path.dirname(__file__), "locale")
# Load translations
if lang is not None:
gettext.bindtextdomain("bakery", lang_path)
gettext.textdomain("bakery")
translation = gettext.translation("bakery", lang_path, languages=[lang])
translation.install()
return translation.gettext # type: ignore
else:
gettext.bindtextdomain("bakery", lang_path)
gettext.textdomain("bakery")
return gettext.gettext # type: ignore
_ = None
# Logging
logging_handler = None
st_msgs = []
def populate_messages(
lang=None,
type: str = "on_device_offline",
) -> None:
global _
_ = setup_translations(lang=lang)
global st_msgs
st_msgs.clear()
if type == "on_device_offline":
st_msgs += [
[_("Preparing for installation"), 0], # 0
[_("Applying Locale Settings"), 10], # 1
[_("Applying Keyboard Settings"), 20], # 2
[_("Applying Timezone Settings"), 30], # 3
[_("Creating User account"), 40], # 4
[_("Setting Hostname"), 50], # 5
[_("Finalizing installation"), 90], # 6
[_("Cleaning up installation"), 100], # 7
]
elif type == "from_iso_offline":
st_msgs += [
[_("Preparing for installation"), 0], # 0
[_("Partitioning Disk"), 0], # 1
[_("Mounting Disk"), 15], # 2
[_("Copying Files from iso"), 20], # 3
[_("Regenerating initramfs"), 24], # 4
[_("Generating fstab"), 28], # 5
[_("Setting up bootloader"), 32], # 6
[_("Removing packages"), 40], # 7
[_("Applying Locale Settings"), 50], # 8
[_("Applying Keyboard Settings"), 60], # 9
[_("Applying Timezone Settings"), 70], # 10
[_("Creating User account"), 80], # 11
[_("Setting Hostname"), 85], # 12
[_("Finalizing installation"), 90], # 13
[_("Cleaning up installation"), 100], # 14
]
populate_messages()
def lp(message, mode="info") -> None:
if mode == "info":
LogMessage.Info(message).write(logging_handler=logging_handler)
elif mode == "debug":
LogMessage.Debug(message).write(logging_handler=logging_handler)
elif mode == "warn":
LogMessage.Warning(message).write(logging_handler=logging_handler)
elif mode == "crit":
LogMessage.Critical(message).write(logging_handler=logging_handler)
elif mode == "error":
LogMessage.Error(message).write(logging_handler=logging_handler)
elif mode == "exception":
LogMessage.Exception(message).write(logging_handler=logging_handler)
else:
raise ValueError("Invalid mode.")
def st(msg_id: int) -> None:
sleep(0.2)
lp("%ST" + str(msg_id) + "%")
sleep(0.2)
def console_logging(
logging_level: int,
message: str,
*args,
loginfo_filename="",
loginfo_line_number=-1,
loginfo_function_name="",
loginfo_stack_info=None,
**kwargs,
) -> None:
logging_level_name = LoggingLevel(logging_level).name
pos = message.find("%ST")
if pos != -1:
prs = message.rfind("%")
stm = st_msgs[int(message[pos + 3 : prs])]
lp("STATUS : " + stm[0])
lp("PROGRESS: " + str(stm[1]) + "%")
log_path = None
def setup_logging() -> logging.Logger:
"""
Setup logging
Does the following:
- Creates a logger with a name
- Sets the format for the logs
- Sets up logging to a file and future console
"""
logger = logging.getLogger("bredos-bakery")
logger.setLevel(logging.DEBUG)
if dryrun:
log_dir = os.path.abspath(os.path.join("."))
log_file = os.path.abspath(os.path.join(log_dir, "DRYRUN.log"))
else:
log_dir = os.path.join(os.path.expanduser("~"), ".bredos", "bakery", "logs")
log_file = os.path.join(
log_dir, datetime.now().strftime("BAKERY-%Y-%m-%d-%H-%M-%S.log")
)
try:
Path(log_dir).mkdir(parents=True, exist_ok=True)
if not os.path.isdir(log_dir):
raise FileNotFoundError("The directory {} does not exist".format(log_dir))
# get write perms
elif not os.access(log_dir, os.W_OK):
raise PermissionError(
"You do not have permission to write to {}".format(log_dir)
)
except Exception as e:
print_exception(type(e), e, e.__traceback__)
exit(1)
print("Logging to:", log_file)
global log_path
log_path = log_file
rm_old_logs(log_dir, keep=5)
log_file_handler = logging.FileHandler(log_file)
log_file_handler.setLevel(logging.DEBUG)
log_file_formatter = logging.Formatter(
"%(asctime)s [%(levelname)8s] %(message)s",
)
log_file_handler.setFormatter(log_file_formatter)
logger.addHandler(log_file_handler)
log_error_handler = logging.StreamHandler()
log_error_handler.setLevel(logging.INFO)
log_error_formatter = logging.Formatter("%(levelname)8s: %(message)s")
log_error_handler.setFormatter(log_error_formatter)
logger.addHandler(log_error_handler)
return logger
def rm_old_logs(log_dir_path: str, keep: int) -> None:
for i in os.listdir(log_dir_path):
if i.startswith("BAKERY" if not dryrun else "DRYRUN"):
os.remove(f"{log_dir_path}/{i}")
def copy_logs(new_usern: str, chroot: bool = False, mnt_dir: str = None) -> None:
if dryrun:
lp("Would have synced and copied logs.")
return
subprocess.run("sync")
if chroot:
subprocess.run(
[
"cp",
"-v",
log_path,
mnt_dir + "/home/" + new_usern + "/.bredos/bakery/logs",
]
)
else:
subprocess.run(
[
"cp",
"-vr",
"/home/bred/.bredos",
"/home/" + new_usern + "/.bredos",
]
)
subprocess.run(
[
"chown",
"-R",
new_usern + ":" + new_usern,
"/home/" + new_usern + "/.bredos",
]
)
# Logger config
# class StdErrRedirector(io.TextIOBase):
# def write(self, message):
# if message.strip():
# lp(message, mode="info")
# sys.stderr = StdErrRedirector()
print("Starting logger..")
logger = setup_logging()
logging_handler = LoggingHandler(
logger=logger,
logging_functions=[console_logging],
)
def post_run_cmd(info, exitcode) -> None:
if exitcode:
lp(f"Command failed with exit code {exitcode}", mode="error")
raise Exception(f"Command failed with exit code {exitcode}")
def expected_to_fail(info, exitcode) -> None:
if exitcode:
lp(f"Command failed with exit code {exitcode}", mode="error")
def lrun(
cmd: list,
force: bool = False,
silent: bool = False,
shell: bool = False,
cwd: str = ".",
postrunfn: Callable = post_run_cmd,
wait=True,
) -> None:
"""
Run a command and log the output
Parameters:
- cmd: The command to run
- force: Whether to run the command even if dryrun is enabled. Default is False
- silent: Whether to run the command silently. Default is False
- shell: Whether to run the command in a shell. Default is False
- cwd: The working directory to run the command in. Default is "."
Returns: None
"""
if dryrun and not force:
lp("Would have run: " + " ".join(cmd))
else:
if shell and wait:
new_cmd = " ".join(cmd)
Command.Shell(
new_cmd,
is_silent=silent,
working_directory=cwd,
post_run_function=partial(postrunfn),
do_send_output_to_post_run_function=True,
do_send_exit_code_to_post_run_function=True,
).run_log_and_wait(logging_handler=logging_handler)
elif shell and not wait:
new_cmd = " ".join(cmd)
Command.Shell(
new_cmd,
is_silent=silent,
working_directory=cwd,
post_run_function=partial(postrunfn),
do_send_output_to_post_run_function=True,
do_send_exit_code_to_post_run_function=True,
).run_and_log(logging_handler=logging_handler)
elif not shell and wait:
Command(
cmd,
is_silent=silent,
working_directory=cwd,
post_run_function=partial(postrunfn),
do_send_output_to_post_run_function=True,
do_send_exit_code_to_post_run_function=True,
).run_log_and_wait(logging_handler=logging_handler)
elif not shell and not wait:
Command(
cmd,
is_silent=silent,
working_directory=cwd,
post_run_function=partial(postrunfn),
do_send_output_to_post_run_function=True,
do_send_exit_code_to_post_run_function=True,
).run_and_log(logging_handler=logging_handler)
lp("Logger initialized.")
lp("Dry run = " + str(dryrun))
# Exception handling
def catch_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
lp(f"Exception in {func.__name__}: {e}", mode="error")
raise e
return wrapper
# TOML
@catch_exceptions
def check_override_config() -> bool:
# check if a /boot/override.toml exists.
try:
with open("/boot/override.toml"):
pass
return True
except:
return False
@catch_exceptions
def load_config(file_path: str = "/bakery/config.toml") -> dict:
# Load a config file as a dict.
lp("Loaded config: " + file_path)
return toml.load(file_path)
@catch_exceptions
def export_config(config: dict, file_path: str = "/bakery/output.toml") -> bool:
# Export a config file from a stored config dict.
try:
with open(file_path, "w") as f:
lp("Exporting config to: " + file_path)
f.write(toml.dumps(config))
except:
return False
return True
# Networking functions
@catch_exceptions
def test_up(hostport: tuple) -> bool:
if not networking_up():
return False
try:
socket.setdefaulttimeout(10)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(hostport)
return True
except:
return False
@catch_exceptions
def networking_up() -> bool:
# Tests if an interface is connected.
client = NM.Client.new(None)
devices = client.get_devices()
for device in devices:
if (
device.get_type_description() in ["wifi", "ethernet"]
and device.get_state().value_nick == "activated"
):
return True
return False
@catch_exceptions
def internet_up() -> bool:
res = False
for i in [
("8.8.8.8", 53),
("9.9.9.9", 53),
("1.1.1.1", 53),
("130.61.177.30", 443),
]:
res = test_up(i)
if res:
break
lp("Internet status: " + str(res))
return res
@catch_exceptions
def geoip() -> dict:
try:
if not internet_up():
raise OSError
tz_data = requests.get("https://geoip.kde.org/v1/timezone").json()
region, zone = tz_data["time_zone"].split("/")
return {"region": region, "zone": zone}
except:
return config.timezone
@catch_exceptions
def ethernet_available() -> bool:
client = NM.Client.new(None)
devices = client.get_devices()
for device in devices:
if device.get_type_description() == "ethernet":
return True
return False
@catch_exceptions
def ethernet_connected() -> bool:
client = NM.Client.new(None)
devices = client.get_devices()
for device in devices:
if (
device.get_type_description() == "ethernet"
and device.get_state().value_nick == "activated"
):
return True
return False
@catch_exceptions
def wifi_available() -> bool:
client = NM.Client.new(None)
devices = client.get_devices()
for device in devices:
if device.get_type_description() == "wifi":
return True
return False
@catch_exceptions
def wifi_connected() -> bool:
client = NM.Client.new(None)
devices = client.get_devices()
for device in devices:
if (
device.get_type_description() == "wifi"
and device.get_state().value_nick == "activated"
):
return True
return False
@catch_exceptions
def open_nm_settings() -> None:
# Opens whichever gui for network settings is found.
pass
@catch_exceptions
def check_updated() -> bool:
# Check if bakery version has chanced in pacman.
return False
@catch_exceptions
def nmtui() -> None:
# Opens nmtui for network settings configurations.
st = True
while st:
subprocess.run(["nmtui"])
while st:
try:
if not internet_up():
print("\nWARNING: The internet is still unreachable.")
res = input("Do you want to run nmtui again? (Y/N): ")
if res in ["n", "N"]:
st = False
elif res in ["y", "Y"]:
break
except:
pass
# Locale functions
_langmap = {
"aa": "Afar",
"af": "Afrikaans",
"am": "Amharic",
"an": "Aragonese",
"ak": "Akan",
"ar": "Arabic",
"as": "Assamese",
"ast": "Asturian",
"az": "Azerbaijani",
"be": "Belarusian",
"bg": "Bulgarian",
"bi": "Bislama",
"bn": "Bengali",
"bo": "Tibetan",
"br": "Breton",
"bs": "Bosnian",
"ca": "Catalan",
"ce": "Chechen",
"cs": "Czech",
"cv": "Chuvash",
"cy": "Welsh",
"da": "Danish",
"de": "German",
"dv": "Divehi",
"dz": "Dzongkha",
"el": "Greek",
"en": "English",
"eo": "Esperanto",
"es": "Spanish",
"et": "Estonian",
"eu": "Basque",
"fa": "Persian",
"fi": "Finnish",
"ff": "Fulah",
"fil": "Filipino",
"fo": "Faroese",
"fr": "French",
"fy": "Western Frisian",
"ga": "Irish",
"gd": "Scottish Gaelic",
"gl": "Galician",
"gu": "Gujarati",
"gv": "Manx",
"ha": "Hausa",
"he": "Hebrew",
"hi": "Hindi",
"hr": "Croatian",
"ht": "Haitian Creole",
"hu": "Hungarian",
"hy": "Armenian",
"ia": "Interlingua",
"id": "Indonesian",
"ig": "Igbo",
"ik": "Inupiaq",
"is": "Icelandic",
"it": "Italian",
"iu": "Inuktitut",
"ja": "Japanese",
"ka": "Georgian",
"kab": "Kabyle",
"kl": "Kalaallisut",
"kk": "Kazakh",
"km": "Khmer",
"kn": "Kannada",
"ko": "Korean",
"kok": "Konkani",
"ks": "Kashmiri",
"ku": "Kurdish",
"kw": "Cornish",
"kv": "Komi",
"ky": "Kyrgyz",
"lb": "Luxembourgish",
"ln": "Lingala",
"lo": "Lao",
"lg": "Ganda",
"li": "Limburgish",
"lt": "Lithuanian",
"lv": "Latvian",
"mai": "Maithili",
"mg": "Malagasy",
"mhr": "Eastern Mari",
"mi": "Maori",
"mk": "Macedonian",
"ml": "Malayalam",
"mn": "Mongolian",
"mr": "Marathi",
"ms": "Malay",
"mt": "Maltese",
"my": "Burmese",
"nb": "Norwegian Bokmål",
"nds": "Low German",
"ne": "Nepali",
"nl": "Dutch",
"nn": "Norwegian Nynorsk",
"nr": "Southern Ndebele",
"nso": "Northern Sotho",
"oc": "Occitan",
"om": "Oromo",
"or": "Oriya",
"os": "Ossetic",
"pa": "Punjabi",
"pl": "Polish",
"ps": "Pashto",
"pt": "Portuguese",
"ro": "Romanian",
"ru": "Russian",
"rw": "Kinyarwanda",
"sa": "Sanskrit",
"sc": "Sardinian",
"sd": "Sindhi",
"se": "Northern Sami",
"sm": "Samoan",
"so": "Somali",
"shs": "Shuswap",
"si": "Sinhala",
"sk": "Slovak",
"sl": "Slovenian",
"sq": "Albanian",
"sr": "Serbian",
"ss": "Swati",
"su": "Basa Sunda",
"st": "Southern Sotho",
"sv": "Swedish",
"sw": "Swahili",
"ta": "Tamil",
"ti": "Tigrinya",
"to": "Tongan",
"te": "Telugu",
"tg": "Tajik",
"th": "Thai",
"tk": "Turkmen",
"tl": "Tagalog",
"tn": "Tswana",
"tr": "Turkish",
"ts": "Tsonga",
"tt": "Tatar",
"ug": "Uighur",
"uk": "Ukrainian",
"ur": "Urdu",
"uz": "Uzbek",
"ve": "Venda",
"vi": "Vietnamese",
"wa": "Walloon",
"wae": "Walser",
"wo": "Wolof",
"xh": "Xhosa",
"yi": "Yiddish",
"yo": "Yoruba",
"yue": "Yue Chinese",
"zh": "Chinese",
"zu": "Zulu",
}
_kbmodelmap = {
"armada": "Laptop/notebook Compaq (eg. Armada) Laptop Keyboard",
"apex300": "Apex 300 Gaming Keyboard",
"chromebook": "Chromebook Laptop Keyboard",
"pc101": "Generic 101-key PC",
"pc102": "Generic 102-key (Intl) PC",
"pc104": "Generic 104-key PC",
"pc105": "Generic 105-key (Intl) PC",
"pc104alt": "Generic 104-key PC (Alternative)",
"pc86": "Generic 86-key PC",
"ppkb": "PinePhone Keyboard",
"dell101": "Dell 101-key PC",
"latitude": "Dell Latitude series laptop",
"dellm65": "Dell Precision M65",
"everex": "Everex STEPnote",
"flexpro": "Keytronic FlexPro",
"microsoft": "Microsoft Natural",
"omnikey101": "Northgate OmniKey 101",
"winbook": "Winbook Model XP5",
"pc98": "PC-98xx Series",
"a4techKB21": "A4Tech KB-21",
"a4techKBS8": "A4Tech KBS-8",
"a4_rfkb23": "A4Tech Wireless Desktop RFKB-23",
"airkey": "Acer AirKey V",
"azonaRF2300": "Azona RF2300 wireless Internet Keyboard",
"scorpius": "Advance Scorpius KI",
"brother": "Brother Internet Keyboard",
"btc5113rf": "BTC 5113RF Multimedia",
"btc5126t": "BTC 5126T",
"btc6301urf": "BTC 6301URF",
"btc9000": "BTC 9000",
"btc9000a": "BTC 9000A",
"btc9001ah": "BTC 9001AH",
"btc5090": "BTC 5090",
"btc9019u": "BTC 9019U",
"btc9116u": "BTC 9116U Mini Wireless Internet and Gaming",
"cherryblue": "Cherry Blue Line CyBo@rd",
"cherryblueb": "Cherry CyMotion Master XPress",
"cherrybluea": "Cherry Blue Line CyBo@rd (alternate option)",
"cherrycyboard": "Cherry CyBo@rd USB-Hub",
"cherrycmexpert": "Cherry CyMotion Expert",
"cherrybunlim": "Cherry B.UNLIMITED",
"chicony": "Chicony Internet Keyboard",
"chicony0108": "Chicony KU-0108",
"chicony0420": "Chicony KU-0420",
"chicony9885": "Chicony KB-9885",
"compaqeak8": "Compaq Easy Access Keyboard",
"compaqik7": "Compaq Internet Keyboard (7 keys)",
"compaqik13": "Compaq Internet Keyboard (13 keys)",
"compaqik18": "Compaq Internet Keyboard (18 keys)",
"cymotionlinux": "Cherry CyMotion Master Linux",
"armada": "Laptop/notebook Compaq (eg. Armada) Laptop Keyboard",
"presario": "Laptop/notebook Compaq (eg. Presario) Internet Keyboard",
"ipaq": "Compaq iPaq Keyboard",
"dell": "Dell",
"dellsk8125": "Dell SK-8125",
"dellsk8135": "Dell SK-8135",
"dellusbmm": "Dell USB Multimedia Keyboard",
"inspiron": "Dell Laptop/notebook Inspiron 6xxx/8xxx",
"precision_m": "Dell Laptop/notebook Precision M series",
"dexxa": "Dexxa Wireless Desktop Keyboard",
"diamond": "Diamond 9801 / 9802 series",
"dtk2000": "DTK2000",
"ennyah_dkb1008": "Ennyah DKB-1008",
"fscaa1667g": "Fujitsu-Siemens Computers AMILO laptop",
"genius": "Genius Comfy KB-16M / Genius MM Keyboard KWD-910",
"geniuscomfy": "Genius Comfy KB-12e",
"geniuscomfy2": "Genius Comfy KB-21e-Scroll",
"geniuskb19e": "Genius KB-19e NB",
"geniuskkb2050hs": "Genius KKB-2050HS",
"gyration": "Gyration",
"htcdream": "HTC Dream",
"kinesis": "Kinesis",
"logitech_base": "Logitech Generic Keyboard",
"logitech_g15": "Logitech G15 extra keys via G15daemon",
"hpi6": "Hewlett-Packard Internet Keyboard",
"hp250x": "Hewlett-Packard SK-250x Multimedia Keyboard",
"hpxe3gc": "Hewlett-Packard Omnibook XE3 GC",
"hpxe3gf": "Hewlett-Packard Omnibook XE3 GF",
"hpxt1000": "Hewlett-Packard Omnibook XT1000",
"hpdv5": "Hewlett-Packard Pavilion dv5",
"hpzt11xx": "Hewlett-Packard Pavilion ZT11xx",
"hp500fa": "Hewlett-Packard Omnibook 500 FA",
"hp5xx": "Hewlett-Packard Omnibook 5xx",
"hpnx9020": "Hewlett-Packard nx9020",
"hp6000": "Hewlett-Packard Omnibook 6000/6100",
"honeywell_euroboard": "Honeywell Euroboard",
"hpmini110": "Hewlett-Packard Mini 110 Notebook",
"rapidaccess": "IBM Rapid Access",
"rapidaccess2": "IBM Rapid Access II",
"thinkpad": "IBM ThinkPad 560Z/600/600E/A22E",
"thinkpad60": "IBM ThinkPad R60/T60/R61/T61",
"thinkpadz60": "IBM ThinkPad Z60m/Z60t/Z61m/Z61t",
"ibm_spacesaver": "IBM Space Saver",
"logiaccess": "Logitech Access Keyboard",
"logiclx300": "Logitech Cordless Desktop LX-300",
"logii350": "Logitech Internet 350 Keyboard",
"logimel": "Logitech Media Elite Keyboard",
"logicd": "Logitech Cordless Desktop",
"logicd_it": "Logitech Cordless Desktop iTouch",
"logicd_nav": "Logitech Cordless Desktop Navigator",
"logicd_opt": "Logitech Cordless Desktop Optical",
"logicda": "Logitech Cordless Desktop (alternate option)",
"logicdpa2": "Logitech Cordless Desktop Pro (alternate option 2)",
"logicfn": "Logitech Cordless Freedom/Desktop Navigator",
"logicdn": "Logitech Cordless Desktop Navigator",
"logiitc": "Logitech iTouch Cordless Keyboard (model Y-RB6)",
"logiik": "Logitech Internet Keyboard",
"itouch": "Logitech iTouch",
"logicink": "Logitech Internet Navigator Keyboard",
"logiex110": "Logitech Cordless Desktop EX110",
"logiinkse": "Logitech iTouch Internet Navigator Keyboard SE",
"logiinkseusb": "Logitech iTouch Internet Navigator Keyboard SE (USB)",
"logiultrax": "Logitech Ultra-X Keyboard",
"logiultraxc": "Logitech Ultra-X Cordless Media Desktop Keyboard",
"logidinovo": "Logitech diNovo Keyboard",
"logidinovoedge": "Logitech diNovo Edge Keyboard",
"mx1998": "Memorex MX1998",
"mx2500": "Memorex MX2500 EZ-Access Keyboard",
"mx2750": "Memorex MX2750",
"microsoft4000": "Microsoft Natural Ergonomic Keyboard 4000",
"microsoft7000": "Microsoft Natural Wireless Ergonomic Keyboard 7000",
"microsoftinet": "Microsoft Internet Keyboard",
"microsoftpro": "Microsoft Natural Keyboard Pro / Microsoft Internet Keyboard Pro",
"microsoftprousb": "Microsoft Natural Keyboard Pro USB / Microsoft Internet Keyboard Pro",
"microsoftprooem": "Microsoft Natural Keyboard Pro OEM",
"vsonku306": "ViewSonic KU-306 Internet Keyboard",
"microsoftprose": "Microsoft Internet Keyboard Pro, Swedish",
"microsoftoffice": "Microsoft Office Keyboard",
"microsoftmult": "Microsoft Wireless Multimedia Keyboard 1.0A",
"microsoftelite": "Microsoft Natural Keyboard Elite",
"microsoftccurve2k": "Microsoft Comfort Curve Keyboard 2000",
"microsoftsurface": "Microsoft Surface Keyboard",
"oretec": "Ortek MCK-800 MM/Internet keyboard",
"propeller": "Propeller Voyager (KTEZ-1000)",
"qtronix": "QTronix Scorpius 98N+",
"samsung4500": "Samsung SDM 4500P",
"samsung4510": "Samsung SDM 4510P",
"sanwaskbkg3": "Sanwa Supply SKB-KG3",
"sk1300": "SK-1300",
"sk2500": "SK-2500",
"sk6200": "SK-6200",
"sk7100": "SK-7100",
"sp_inet": "Super Power Multimedia Keyboard",
"sven": "SVEN Ergonomic 2500",
"sven303": "SVEN Slim 303",
"symplon": "Symplon PaceBook (tablet PC)",
"toshiba_s3000": "Toshiba Satellite S3000",
"trust": "Trust Wireless Keyboard Classic",
"trustda": "Trust Direct Access Keyboard",
"trust_slimline": "Trust Slimline",
"tm2020": "TypeMatrix EZ-Reach 2020",
"tm2030PS2": "TypeMatrix EZ-Reach 2030 PS2",
"tm2030USB": "TypeMatrix EZ-Reach 2030 USB",
"tm2030USB-102": "TypeMatrix EZ-Reach 2030 USB (102/105:EU mode)",
"tm2030USB-106": "TypeMatrix EZ-Reach 2030 USB (106:JP mode)",
"yahoo": "Yahoo! Internet Keyboard",
"macbook78": "MacBook/MacBook Pro",
"macbook79": "MacBook/MacBook Pro (Intl)",
"macintosh": "Macintosh",
"macintosh_old": "Macintosh Old",
"macintosh_hhk": "Happy Hacking Keyboard for Mac",
"acer_c300": "Acer C300",
"acer_ferrari4k": "Acer Ferrari 4000",
"acer_laptop": "Acer Laptop",
"asus_laptop": "Asus Laptop",
"apple": "Apple",
"apple_laptop": "Apple Laptop",
"applealu_ansi": "Apple Aluminium Keyboard (ANSI)",
"applealu_iso": "Apple Aluminium Keyboard (ISO)",
"applealu_jis": "Apple Aluminium Keyboard (JIS)",
"silvercrest": "SILVERCREST Multimedia Wireless Keyboard",
"emachines": "Laptop/notebook eMachines m68xx",
"benqx": "BenQ X-Touch",
"benqx730": "BenQ X-Touch 730",
"benqx800": "BenQ X-Touch 800",
"hhk": "Happy Hacking Keyboard",
"classmate": "Classmate PC",
"olpc": "OLPC",
"sun_type7_usb": "Sun Type 7 USB",
"sun_type7_euro_usb": "Sun Type 7 USB (European layout)",
"sun_type7_unix_usb": "Sun Type 7 USB (Unix layout)",
"sun_type7_jp_usb": "Sun Type 7 USB (Japanese layout) / Japanese 106-key",
"sun_type6_usb": "Sun Type 6/7 USB",
"sun_type6_euro_usb": "Sun Type 6/7 USB (European layout)",
"sun_type6_unix_usb": "Sun Type 6 USB (Unix layout)",
"sun_type6_jp_usb": "Sun Type 6 USB (Japanese layout)",
"sun_type6_jp": "Sun Type 6 (Japanese layout)",
"targa_v811": "Targa Visionary 811",
"unitekkb1925": "Unitek KB-1925",
"compalfl90": "FL90",
"creativedw7000": "Creative Desktop Wireless 7000",
"htcdream": "Htc Dream phone",
"teck227": "Truly Ergonomic Computer Keyboard Model 227 (Wide Alt keys)",
"teck229": (
"Truly Ergonomic Computer Keyboard Model 229 (Standard sized Alt keys, additional "
+ "Super and Menu key)"
),
}
_kblangmap = {
"af": "Afrikaans",
"al": "Albanian",
"am": "Amharic",
"ara": "Arabic",
"at": "Austrian German",
"au": "Australian English",
"az": "Azerbaijani",
"ba": "Bosnian",
"bd": "Bangla",
"be": "Belgian Dutch",
"bg": "Bulgarian",
"br": "Portuguese (Brazil)",
"brai": "Braille",
"bt": "Bhutanese",
"bw": "Tswana",
"by": "Belarusian",
"ca": "Canadian English",
"cd": "Congolese",
"ch": "Swiss German",
"cm": "Cameroonian",
"cn": "Chinese",
"cz": "Czech",
"de": "German",
"dk": "Danish",
"dz": "Algerian Arabic",
"ee": "Estonian",
"epo": "Esperanto",
"es": "Spanish",
"eg": "Egyptian",
"et": "Estonian",
"fi": "Finnish",
"fo": "Faroese",
"fr": "French",
"gb": "British English",
"ge": "Georgian",
"gh": "Ghanaian",
"gn": "Guinean",
"gr": "Greek",
"hr": "Croatian",
"hu": "Hungarian",
"id": "Indonesian",
"ie": "Irish",
"il": "Hebrew",
"in": "Indian English",
"iq": "Iraqi Arabic",
"ir": "Persian",
"is": "Icelandic",