Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scan registry for installed driver, progress bar #147

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions kalamine/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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))
Expand Down
86 changes: 69 additions & 17 deletions kalamine/msklc_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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."""
Expand All @@ -40,9 +46,37 @@ 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

if sys.platform == "win32": # let mypy know this is win32-specific
Geobert marked this conversation as resolved.
Show resolved Hide resolved
# 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):
Geobert marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand All @@ -53,16 +87,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()
)
Geobert marked this conversation as resolved.
Show resolved Hide resolved

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"
Expand All @@ -71,18 +114,21 @@ 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()
klc_file = Path(self._working_dir) / Path(f"{name8}.klc")
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
Geobert marked this conversation as resolved.
Show resolved Hide resolved
CSIDL_PERSONAL = 5 # My Documents
Expand All @@ -94,14 +140,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)))
Geobert marked this conversation as resolved.
Show resolved Hide resolved
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.")
Expand All @@ -110,6 +154,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)
Expand All @@ -123,6 +168,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:
Expand All @@ -132,18 +178,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)
Expand All @@ -159,6 +209,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,
Expand All @@ -170,7 +221,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)
Expand All @@ -180,4 +231,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
2 changes: 1 addition & 1 deletion kalamine/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"livereload",
"pyyaml",
"tomli",
"progress",
]

[project.optional-dependencies]
Expand All @@ -53,3 +54,7 @@ wkalamine = "kalamine.cli_msklc:cli"

[tool.isort]
profile = "black"

[[tool.mypy.overrides]]
module = ["progress.bar"]
ignore_missing_imports = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this here ?
and what does the double [[ ]] mean ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[[]] means array, needed for this setting

and this is needed to avoid mypy to check progress code it seems. Otherwise I get an error