From 88219e75871af3ec0fcf63a5189829e98c0dd643 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:04:07 +0100 Subject: [PATCH 1/6] feat: scan registry for installed driver, progress bar --- kalamine/layout.py | 12 ++++-- kalamine/msklc_manager.py | 83 +++++++++++++++++++++++++++++++-------- kalamine/template.py | 2 +- pyproject.toml | 5 +++ 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/kalamine/layout.py b/kalamine/layout.py index ddc2924..9d7e7b7 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -43,6 +43,13 @@ # +def get_langid(locale: str) -> str: + locale_codes = load_data("win_locales") + if locale not in locale_codes: + raise ValueError(f"`{locale}` is not a valid locale") + return locale_codes[locale] + + def upper_key(letter: str, blank_if_obvious: bool = True) -> str: """This is used for presentation purposes: in a key, the upper character becomes blank if it's an obvious uppercase version of the base character.""" @@ -517,11 +524,8 @@ def klc(self) -> str: version = re.compile(r"^\d+\.\d+\.\d+(\.\d+)?$") if version.match(self.meta["version"]) is None: raise ValueError("`version` must be in `a.b.c[.d]` form") - locale_codes = load_data("win_locales") locale = self.meta["locale"] - if locale not in locale_codes: - raise ValueError(f"`{locale}` is not a valid locale") - langid = locale_codes[locale] + langid = get_langid(locale) out = load_tpl(self, ".klc") out = substitute_lines(out, "LAYOUT", klc_keymap(self)) out = substitute_lines(out, "DEAD_KEYS", klc_deadkeys(self)) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index 9b884c9..753dfc4 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -2,12 +2,15 @@ import os import subprocess import sys +import winreg from pathlib import Path from shutil import move, rmtree from stat import S_IREAD, S_IWUSR +from progress.bar import ChargingBar + from .help import dummy_layout -from .layout import KeyboardLayout +from .layout import KeyboardLayout, get_langid class MsklcManager: @@ -22,6 +25,9 @@ def __init__( self._msklc_dir = msklc_dir self._verbose = verbose self._working_dir = working_dir + self._progress = ChargingBar( + f"Creating MSKLC driver for `{layout.meta['name']}`", max=14 + ) def create_c_files(self): """Call kbdutool on the KLC descriptor to generate C files.""" @@ -40,9 +46,34 @@ def _is_already_installed(self) -> bool: """Check if the keyboard driver is already installed, which would cause MSKLC to launch the GUI instead of creating the installer.""" + # check if the DLL is present sys32 = Path(os.environ["WINDIR"]) / Path("System32") - dll = sys32 / Path(f'{self._layout.meta["name8"]}.dll') - return dll.exists() + sysWow = Path(os.environ["WINDIR"]) / Path("SysWOW64") + dll_name = f'{self._layout.meta["name8"]}.dll' + dll_exists = (sys32 / dll_name).exists() or (sysWow / Path(dll_name)).exists() + + if dll_exists: + print(f"Error: {dll_name} is already installed") + return True + + # check if the registry still has it + # that can happen after a botch uninstall of the driver + langid = get_langid(self._layout.meta["locale"]).lower() + kbd_layouts_handle = winreg.OpenKeyEx( + winreg.HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", + ) + # [0] is the number of sub keys + for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): + sub_key = winreg.EnumKey(kbd_layouts_handle, i) + # a sub_key is on 8 chars, the last 4 ones being a langid + if sub_key.endswith(langid): + sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) + layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] + if layout_file == dll_name: + print(f"Error: The registry still have reference to `{dll_name}`") + return True + return False def _create_dummy_layout(self) -> str: return dummy_layout( @@ -53,16 +84,25 @@ def _create_dummy_layout(self) -> str: ).klc def build_msklc_installer(self) -> bool: - name8 = self._layout.meta["name8"] - - if (self._working_dir / Path(name8)).exists(): - print( - f"WARN: `{self._working_dir / Path(name8)}` already exists, " - "assuming installer sits there." + def installer_exists(installer: Path) -> bool: + return ( + installer.exists() + and installer.is_dir() + and (installer / Path("setup.exe")).exists() + and (installer / Path("amd64")).exists() + and (installer / Path("i386")).exists() + and (installer / Path("ia64")).exists() + and (installer / Path("wow64")).exists() ) + + name8 = self._layout.meta["name8"] + installer_dir = self._working_dir / Path(name8) + if installer_exists(installer_dir): + self._progress.next(4) return True if self._is_already_installed(): + self._progress.finish() print( "Error: layout already installed and " "installer package not found in the current directory.\n" @@ -71,6 +111,7 @@ def build_msklc_installer(self) -> bool: ) return False + self._progress.next() # Create a dummy klc file to generate the installer. # The file must have a correct name to be reflected in the installer. dummy_klc = self._create_dummy_layout() @@ -78,11 +119,13 @@ def build_msklc_installer(self) -> bool: with klc_file.open("w", encoding="utf-16le", newline="\r\n") as file: file.write(dummy_klc) + self._progress.next() msklc = self._msklc_dir / Path("MSKLC.exe") result = subprocess.run( - [msklc, klc_file, "-build"], capture_output=not self._verbose + [msklc, klc_file, "-build"], capture_output=not self._verbose, text=True ) + self._progress.next() # move the installer from "My Documents" to current dir if sys.platform == "win32": # let mypy know this is win32-specific CSIDL_PERSONAL = 5 # My Documents @@ -94,14 +137,12 @@ def build_msklc_installer(self) -> bool: my_docs = Path(buf.value) installer = my_docs / Path(name8) - if ( - installer.exists() - and installer.is_dir() - and (installer / Path("setup.exe")).exists() - ): + self._progress.next() + if installer_exists(installer): move(str(installer), str(self._working_dir / Path(name8))) else: - print(f"Exit code: {result.returncode}") + self._progress.finish() + print(f"MSKLC Exit code: {result.returncode}") print(result.stdout) print(result.stderr) print("Error: installer was not created.") @@ -110,6 +151,7 @@ def build_msklc_installer(self) -> bool: return True def build_msklc_dll(self) -> bool: + self._progress.next() name8 = self._layout.meta["name8"] prev = os.getcwd() os.chdir(self._working_dir) @@ -123,6 +165,7 @@ def build_msklc_dll(self) -> bool: rmtree(full_dir) os.mkdir(full_dir) + self._progress.next() # create correct klc klc_file = self._working_dir / Path(f"{name8}.klc") with klc_file.open("w", encoding="utf-16le", newline="\r\n") as file: @@ -132,18 +175,22 @@ def build_msklc_dll(self) -> bool: print(f"ERROR: {err}") return False + self._progress.next() self.create_c_files() + self._progress.next() rc_file = klc_file.with_suffix(".RC") with rc_file.open("w", encoding="utf-16le", newline="\r\n") as file: file.write(self._layout.klc_rc) + self._progress.next() c_file = klc_file.with_suffix(".C") with c_file.open("w", encoding="utf-16le", newline="\r\n") as file: file.write(self._layout.klc_c) c_files = [".C", ".RC", ".H", ".DEF"] + self._progress.next() # Make files read-only to prevent MSKLC from overwriting them. for suffix in c_files: os.chmod(klc_file.with_suffix(suffix), S_IREAD) @@ -159,6 +206,7 @@ def build_msklc_dll(self) -> bool: ("-i", "ia64"), ("-o", "wow64"), ]: + self._progress.next() result = subprocess.run( [kbdutool, "-u", arch_flag, klc_file], text=True, @@ -170,7 +218,7 @@ def build_msklc_dll(self) -> bool: # Restore write permission for suffix in c_files: os.chmod(klc_file.with_suffix(suffix), S_IWUSR) - + self._progress.finish() print(f"Error while creating DLL for arch {arch}:") print(result.stdout) print(result.stderr) @@ -180,4 +228,5 @@ def build_msklc_dll(self) -> bool: for suffix in c_files: os.chmod(klc_file.with_suffix(suffix), S_IWUSR) os.chdir(prev) + self._progress.finish() return True diff --git a/kalamine/template.py b/kalamine/template.py index 38a3038..1757028 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -237,7 +237,7 @@ def get_chr(symbol: str) -> str: def klc_virtual_key(layout: "KeyboardLayout", symbols: list, scan_code: str) -> str: - if (layout.meta["geometry"] == "ISO" and scan_code == "56") or symbols[0] == "-1": + if scan_code == "56": # manage the ISO key (between shift and Z on ISO keyboards). # We're assuming that its scancode is always 56 # https://www.win.tue.nl/~aeb/linux/kbd/scancodes.html diff --git a/pyproject.toml b/pyproject.toml index 319fc70..ec7c4e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "livereload", "pyyaml", "tomli", + "progress", ] [project.optional-dependencies] @@ -53,3 +54,7 @@ wkalamine = "kalamine.cli_msklc:cli" [tool.isort] profile = "black" + +[[tool.mypy.overrides]] +module = ["progress.bar"] +ignore_missing_imports = true \ No newline at end of file From df41747820e19c0acc937b6fe1e27d94ce6b95f4 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:07:36 +0100 Subject: [PATCH 2/6] chore: mypy on Linux --- kalamine/msklc_manager.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index 753dfc4..0d35ec6 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -56,23 +56,24 @@ def _is_already_installed(self) -> bool: print(f"Error: {dll_name} is already installed") return True - # check if the registry still has it - # that can happen after a botch uninstall of the driver - langid = get_langid(self._layout.meta["locale"]).lower() - kbd_layouts_handle = winreg.OpenKeyEx( - winreg.HKEY_LOCAL_MACHINE, - "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", - ) - # [0] is the number of sub keys - for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): - sub_key = winreg.EnumKey(kbd_layouts_handle, i) - # a sub_key is on 8 chars, the last 4 ones being a langid - if sub_key.endswith(langid): - sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) - layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] - if layout_file == dll_name: - print(f"Error: The registry still have reference to `{dll_name}`") - return True + if sys.platform == "win32": # let mypy know this is win32-specific + # check if the registry still has it + # that can happen after a botch uninstall of the driver + langid = get_langid(self._layout.meta["locale"]).lower() + kbd_layouts_handle = winreg.OpenKeyEx( + winreg.HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", + ) + # [0] is the number of sub keys + for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): + sub_key = winreg.EnumKey(kbd_layouts_handle, i) + # a sub_key is on 8 chars, the last 4 ones being a langid + if sub_key.endswith(langid): + sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) + layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] + if layout_file == dll_name: + print(f"Error: The registry still have reference to `{dll_name}`") + return True return False def _create_dummy_layout(self) -> str: From 2d47a6cf64813b6e8c1078b2ecd5d5c144604d68 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:10:07 +0100 Subject: [PATCH 3/6] chore: black --- kalamine/msklc_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index 0d35ec6..be329a4 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -72,7 +72,9 @@ def _is_already_installed(self) -> bool: sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] if layout_file == dll_name: - print(f"Error: The registry still have reference to `{dll_name}`") + print( + f"Error: The registry still have reference to `{dll_name}`" + ) return True return False From 15be9b8a6b9d6f908ae2879b620e8b1767ecaee7 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:19:59 +0100 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20don=E2=80=99t=20limit=20subkey=20to?= =?UTF-8?q?=20the=20one=20on=20the=20curent=20locale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kalamine/msklc_manager.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index be329a4..68726b3 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -10,7 +10,7 @@ from progress.bar import ChargingBar from .help import dummy_layout -from .layout import KeyboardLayout, get_langid +from .layout import KeyboardLayout class MsklcManager: @@ -59,7 +59,6 @@ def _is_already_installed(self) -> bool: if sys.platform == "win32": # let mypy know this is win32-specific # check if the registry still has it # that can happen after a botch uninstall of the driver - langid = get_langid(self._layout.meta["locale"]).lower() kbd_layouts_handle = winreg.OpenKeyEx( winreg.HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", @@ -67,15 +66,14 @@ def _is_already_installed(self) -> bool: # [0] is the number of sub keys for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): sub_key = winreg.EnumKey(kbd_layouts_handle, i) - # a sub_key is on 8 chars, the last 4 ones being a langid - if sub_key.endswith(langid): - sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) - layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] - if layout_file == dll_name: - print( - f"Error: The registry still have reference to `{dll_name}`" - ) - return True + sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) + layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] + if layout_file == dll_name: + print( + f"Error: The registry still have reference to `{dll_name}` in" + f"`HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{sub_key}`" + ) + return True return False def _create_dummy_layout(self) -> str: From ce26336018bf7abb2dad4a19d12d2f816a02bc58 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:35:09 +0100 Subject: [PATCH 5/6] apply review --- kalamine/msklc_manager.py | 82 ++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index 68726b3..671e594 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -56,24 +56,25 @@ def _is_already_installed(self) -> bool: print(f"Error: {dll_name} is already installed") return True - if sys.platform == "win32": # let mypy know this is win32-specific - # check if the registry still has it - # that can happen after a botch uninstall of the driver - kbd_layouts_handle = winreg.OpenKeyEx( - winreg.HKEY_LOCAL_MACHINE, - "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", - ) - # [0] is the number of sub keys - for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): - sub_key = winreg.EnumKey(kbd_layouts_handle, i) - sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) - layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] - if layout_file == dll_name: - print( - f"Error: The registry still have reference to `{dll_name}` in" - f"`HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{sub_key}`" - ) - return True + if sys.platform != "win32": # let mypy know this is win32-specific + return False + # check if the registry still has it + # that can happen after a botch uninstall of the driver + kbd_layouts_handle = winreg.OpenKeyEx( + winreg.HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", + ) + # [0] is the number of sub keys + for i in range(0, winreg.QueryInfoKey(kbd_layouts_handle)[0]): + sub_key = winreg.EnumKey(kbd_layouts_handle, i) + sub_handle = winreg.OpenKey(kbd_layouts_handle, sub_key) + layout_file = winreg.QueryValueEx(sub_handle, "Layout File")[0] + if layout_file == dll_name: + print( + f"Error: The registry still have reference to `{dll_name}` in" + f"`HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\{sub_key}`" + ) + return True return False def _create_dummy_layout(self) -> str: @@ -111,7 +112,7 @@ def installer_exists(installer: Path) -> bool: f"folder in the current directory: ({self._working_dir})" ) return False - + self._progress.message = "Creating installer package" self._progress.next() # Create a dummy klc file to generate the installer. # The file must have a correct name to be reflected in the installer. @@ -128,27 +129,28 @@ def installer_exists(installer: Path) -> bool: self._progress.next() # move the installer from "My Documents" to current dir - if sys.platform == "win32": # let mypy know this is win32-specific - CSIDL_PERSONAL = 5 # My Documents - SHGFP_TYPE_CURRENT = 0 # Get current, not default value - buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) - ctypes.windll.shell32.SHGetFolderPathW( - None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf - ) - my_docs = Path(buf.value) - installer = my_docs / Path(name8) - - self._progress.next() - if installer_exists(installer): - move(str(installer), str(self._working_dir / Path(name8))) - else: - self._progress.finish() - print(f"MSKLC Exit code: {result.returncode}") - print(result.stdout) - print(result.stderr) - print("Error: installer was not created.") - return False + if sys.platform != "win32": # let mypy know this is win32-specific + return False + + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW( + None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf + ) + my_docs = Path(buf.value) + installer = my_docs / Path(name8) + self._progress.next() + if not installer_exists(installer): + self._progress.finish() + print(f"MSKLC Exit code: {result.returncode}") + print(result.stdout) + print(result.stderr) + print("Error: installer was not created.") + return False + + move(str(installer), str(self._working_dir / Path(name8))) return True def build_msklc_dll(self) -> bool: @@ -200,7 +202,7 @@ def build_msklc_dll(self) -> bool: kbdutool = self._msklc_dir / Path("bin/i386/kbdutool.exe") dll = klc_file.with_suffix(".dll") - + self._progress.message = "Creating driver DLLs" for arch_flag, arch in [ ("-x", "i386"), ("-m", "amd64"), From 82945b3738c5d77786ae8f90283413956e295b19 Mon Sep 17 00:00:00 2001 From: Geobert Quach Date: Fri, 1 Mar 2024 20:37:18 +0100 Subject: [PATCH 6/6] =?UTF-8?q?chore:=E2=80=AFblack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kalamine/msklc_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kalamine/msklc_manager.py b/kalamine/msklc_manager.py index 671e594..d2b3ad3 100644 --- a/kalamine/msklc_manager.py +++ b/kalamine/msklc_manager.py @@ -131,7 +131,7 @@ def installer_exists(installer: Path) -> bool: # move the installer from "My Documents" to current dir if sys.platform != "win32": # let mypy know this is win32-specific return False - + CSIDL_PERSONAL = 5 # My Documents SHGFP_TYPE_CURRENT = 0 # Get current, not default value buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) @@ -149,7 +149,7 @@ def installer_exists(installer: Path) -> bool: print(result.stderr) print("Error: installer was not created.") return False - + move(str(installer), str(self._working_dir / Path(name8))) return True