1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-12 15:27:14 +03:00

213 lines
6.9 KiB
Python

"""
windows.py - Windows config implementation.
Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations
import functools
import pathlib
import sys
import uuid
import winreg
from typing import Literal
from config import AbstractConfig, applongname, appname, logger
from win32comext.shell import shell
assert sys.platform == 'win32'
REG_RESERVED_ALWAYS_ZERO = 0
def known_folder_path(guid: uuid.UUID) -> str | None:
"""Look up a Windows GUID to actual folder path name."""
return shell.SHGetKnownFolderPath(guid, 0, 0)
class WinConfig(AbstractConfig):
"""Implementation of AbstractConfig for Windows."""
def __init__(self) -> None:
super().__init__()
REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806
create_key_defaults = functools.partial(
winreg.CreateKeyEx,
key=winreg.HKEY_CURRENT_USER,
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
)
try:
self.__reg_handle: winreg.HKEYType = create_key_defaults(sub_key=REGISTRY_SUBKEY)
except OSError:
logger.exception('Could not create required registry keys')
raise
if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData):
self.app_dir_path = pathlib.Path(local_appdata) / appname
self.app_dir_path.mkdir(exist_ok=True)
self.default_plugin_dir_path = self.app_dir_path / 'plugins'
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
self.set("plugin_dir", str(self.default_plugin_dir_path))
plugdir_str = self.default_plugin_dir
self.plugin_dir_path = pathlib.Path(plugdir_str)
self.plugin_dir_path.mkdir(exist_ok=True)
if getattr(sys, 'frozen', False):
self.respath_path = pathlib.Path(sys.executable).parent
self.internal_plugin_dir_path = self.respath_path / 'plugins'
else:
self.respath_path = pathlib.Path(__file__).parent.parent
self.internal_plugin_dir_path = self.respath_path / 'plugins'
self.home_path = pathlib.Path.home()
journal_dir_path = pathlib.Path(
known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
self.identifier = applongname
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
docs = known_folder_path(shell.FOLDERID_Documents)
self.set("outdir", docs if docs is not None else self.home)
def __get_regentry(self, key: str) -> None | list | str | int:
"""Access the Registry for the raw entry."""
try:
value, _type = winreg.QueryValueEx(self.__reg_handle, key)
except FileNotFoundError:
# Key doesn't exist
return None
# For programmers who want to actually know what is going on
if _type == winreg.REG_SZ:
return str(value)
if _type == winreg.REG_DWORD:
return int(value)
if _type == winreg.REG_MULTI_SZ:
return list(value)
logger.warning(f'Registry key {key=} returned unknown type {_type=} {value=}')
return None
def get_str(self, key: str, *, default: str | None = None) -> str:
"""
Return the string referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_str`.
"""
res = self.__get_regentry(key)
if res is None:
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
if not isinstance(res, str):
raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}')
return res
def get_list(self, key: str, *, default: list | None = None) -> list:
"""
Return the list referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_list`.
"""
res = self.__get_regentry(key)
if res is None:
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
if not isinstance(res, list):
raise ValueError(f'Data from registry is not a list: {type(res)=} {res}')
return res
def get_int(self, key: str, *, default: int = 0) -> int:
"""
Return the int referred to by key if it exists in the config.
Implements :meth:`AbstractConfig.get_int`.
"""
res = self.__get_regentry(key)
if res is None:
return default
if not isinstance(res, int):
raise ValueError(f'Data from registry is not an int: {type(res)=} {res}')
return res
def get_bool(self, key: str, *, default: bool | None = None) -> bool:
"""
Return the bool referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_bool`.
"""
res = self.get_int(key, default=default) # type: ignore
if res is None:
return default # Yes it could be None, but we're _assuming_ that people gave us a default
return bool(res)
def set(self, key: str, val: int | str | list[str] | bool) -> None:
"""
Set the given key's data to the given value.
Implements :meth:`AbstractConfig.set`.
"""
# These are the types that winreg.REG_* below resolve to.
reg_type: Literal[1] | Literal[4] | Literal[7]
if isinstance(val, str):
reg_type = winreg.REG_SZ
elif isinstance(val, int):
reg_type = winreg.REG_DWORD
elif isinstance(val, list):
reg_type = winreg.REG_MULTI_SZ
elif isinstance(val, bool):
reg_type = winreg.REG_DWORD
val = int(val)
else:
raise ValueError(f'Unexpected type for value {type(val)=}')
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore
def delete(self, key: str, *, suppress=False) -> None:
"""
Delete the given key from the config.
'key' is relative to the base Registry path we use.
Implements :meth:`AbstractConfig.delete`.
"""
try:
winreg.DeleteValue(self.__reg_handle, key)
except OSError:
if suppress:
return
raise
def save(self) -> None:
"""
Save the configuration.
Not required for WinConfig as Registry keys are flushed on write.
"""
pass
def close(self):
"""
Close this config and release any associated resources.
Implements :meth:`AbstractConfig.close`.
"""
self.__reg_handle.Close()