diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index d0f2881..6c1bc19 100644 --- a/lib/i18n/strings.py +++ b/lib/i18n/strings.py @@ -15,7 +15,7 @@ CLI_APP = "when" UI_APP_LABEL = "When Automation Tool" UI_APP_COPYRIGHT = "© 2023-2026 Francesco Garosi" -UI_APP_VERSION = "2.1.1" +UI_APP_VERSION = "2.1.2" # other strings that should not be translated UI_WHENEVER = "whenever" diff --git a/lib/utility.py b/lib/utility.py index c676fb9..7662e37 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -14,7 +14,7 @@ string, items, exceptions, - ) +) from hashlib import blake2s from base64 import decodebytes as b64_decodeb from io import BytesIO @@ -85,13 +85,24 @@ # operators allowed in WMI and DBus result/parameter checks _WMI_RESULT_CHECK_OPERATORS = ("eq", "neq", "gt", "ge", "lt", "le", "match") -_DBUS_PARAM_CHECK_OPERATORS = ("eq", "neq", "gt", "ge", "lt", "le", "match", "contains", "ncontains") +_DBUS_PARAM_CHECK_OPERATORS = ( + "eq", + "neq", + "gt", + "ge", + "lt", + "le", + "match", + "contains", + "ncontains", +) # check that an operator is correct for either DBus or WMI result checks def is_wmi_operator(s: str) -> bool: return s in _WMI_RESULT_CHECK_OPERATORS + def is_dbus_operator(s: str) -> bool: return s in _DBUS_PARAM_CHECK_OPERATORS @@ -198,18 +209,21 @@ def get_appicon(image: bytes) -> ImageTk.PhotoImage: # determine where configuration is stored by default def get_default_configdir() -> str: if is_windows(): - appdata = os.environ["APPDATA"] - cfgname: str = AppConfig.get("CFGNAME") # type: ignore + appdata = os.getenv("APPDATA") or os.path.join( + os.getenv("USERPROFILE"), # type: ignore + "AppData", + ) + cfgname: str = AppConfig.get("CFGNAME") # type: ignore if AppConfig.get("DEBUG"): cfgname += "_DEBUG" return os.path.join(appdata, cfgname) else: - s: str = AppConfig.get("CFGNAME") # type: ignore + s: str = AppConfig.get("CFGNAME") # type: ignore cfgname = "." + s.lower() if AppConfig.get("DEBUG"): cfgname += "_DEBUG" home = os.path.expanduser("~") - if sys.platform == "darwin": + if is_mac(): return os.path.join(home, "Library", "Application Support", cfgname) elif is_linux(): return os.path.join(home, cfgname) @@ -238,18 +252,27 @@ def get_default_whenever() -> str | None: # determine appdata directory and ensure it exists def get_appdata() -> str: - appdata: str = AppConfig.get("APPDATA") # type: ignore + appdata: str = AppConfig.get("APPDATA") # type: ignore if not os.path.isdir(appdata): try: os.makedirs(appdata) except Exception: raise OSError(CLI_ERR_DATADIR_UNACCESSIBLE) + # the following is to work around UWP redirection on Windows: not sure + # whether or not it can be of any use on other platforms, so this remains + # Windows specific for the moment + if is_windows(): + realappdata = os.path.realpath(appdata) + if realappdata != appdata: + appdata = realappdata + AppConfig.delete("APPDATA") + AppConfig.set("APPDATA", appdata) return appdata # determine scripts directory and ensure that it exists def get_scriptsdir() -> str: - configdir: str = AppConfig.get("APPDATA") # type: ignore + configdir: str = AppConfig.get("APPDATA") # type: ignore if is_windows(): subdir = "Scripts" else: @@ -264,8 +287,8 @@ def get_scriptsdir() -> str: # determine temp directory and ensure that it exists -def get_tempdir(cleanup: bool=False) -> str: - configdir: str = AppConfig.get("APPDATA") # type: ignore +def get_tempdir(cleanup: bool = False) -> str: + configdir: str = AppConfig.get("APPDATA") # type: ignore if is_windows(): subdir = "Temp" else: @@ -345,7 +368,7 @@ def get_lua_path() -> str: def get_lua_initscript() -> str: init = os.path.join(get_scriptsdir(), _LUA_INIT_NAME) if not os.path.exists(init): - with open(init, 'w') as f: + with open(init, "w") as f: f.write(_LUA_INIT_SCRIPT) return init @@ -484,28 +507,31 @@ def is_whenever_running() -> None | bool: def whenever_has_dbus() -> bool: return bool(AppConfig.get("WHENEVER_HAS_DBUS")) + def whenever_has_wmi() -> bool: return bool(AppConfig.get("WHENEVER_HAS_WMI")) + def whenever_has_lua_sync() -> bool: return bool(AppConfig.get("WHENEVER_HAS_LUASYNC")) + def whenever_has_lua_httpreq() -> bool: return bool(AppConfig.get("WHENEVER_HAS_LUAHTTPREQ")) # return the configuration file path def get_configfile() -> str: - s: str = AppConfig.get("CFGNAME") # type: ignore - d: str = AppConfig.get("APPDATA") # type: ignore + s: str = AppConfig.get("CFGNAME") # type: ignore + d: str = AppConfig.get("APPDATA") # type: ignore basename = "%s.toml" % s.lower() return os.path.join(d, basename) # return the log file path def get_logfile() -> str: - s: str = AppConfig.get("CFGNAME") # type: ignore - d: str = AppConfig.get("APPDATA") # type: ignore + s: str = AppConfig.get("CFGNAME") # type: ignore + d: str = AppConfig.get("APPDATA") # type: ignore basename = "%s.log" % s.lower() return os.path.join(d, basename) @@ -619,9 +645,9 @@ def toml_list_of_literals(los) -> items.Array | None: # in the array, possibly followed by the first non-dashed argument def toml_list_of_command_args(los) -> items.Array | None: if los is not None: - switch_start = ['-', '--'] + switch_start = ["-", "--"] if is_windows(): - switch_start.append('/') + switch_start.append("/") cur_line = [] r = array() for s in los: @@ -649,7 +675,6 @@ def toml_literal(s) -> items.String | None: return toml_try_literal(s) - # clean a caption from non-alphanumeric characters at the end def clean_caption(s) -> str: s = " ".join(s.split()) diff --git a/pyproject.toml b/pyproject.toml index aeeeb03..8a7a350 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.1.1" +version = "2.1.2" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, @@ -32,7 +32,7 @@ Pygments = "^2.19.2" pygobject = { version = "3.50.0", markers = "sys_platform == 'linux'" } # pygobject = { version = "^3.52.2", markers = "sys_platform == 'linux'" } pystray = "^0.19.5" -pywin32 = { version = "^308", markers = "sys_platform == 'win32'" } +pywin32 = { version = ">=308", markers = "sys_platform == 'win32'" } requests = "^2.32.5" rich = "^13.9.4" semver = "^3.0.4" diff --git a/support/docs/install.md b/support/docs/install.md index 60aa114..79e5b92 100644 --- a/support/docs/install.md +++ b/support/docs/install.md @@ -15,7 +15,10 @@ Note that even though a [recent release](https://github.com/almostearthling/when These steps can be followed on both Windows 10 and Windows 11: -1. install **Python** using the standalone installer, and choosing to install it for all users[^1] +1. install **Python**, preferably using one of the following methods:[^1] + * the [_winget_](https://learn.microsoft.com/en-us/windows/package-manager/winget) utility, by running `winget install Python` in a terminal window[^2] + * the [_Python Install Manager_](https://apps.microsoft.com/detail/9nq7512cxl7t), by running `py install 3` in a terminal window after installing it from the store[^2] + * the standalone installer, which can be found at the downloads page on the Python main site 2. install pipx by issuing the command `py -m pip install --user pipx` in a console window: after installation launch `pipx ensurepath` from the command prompt 3. install the latest release of **When**, using **pipx**: @@ -164,4 +167,5 @@ Both the installation of **When** using the **pipx** method, and the installatio [`◀ Main`](main.md) -[^1]: the reason is that, when Python is installed for the current user or via the _App Store_, the APPDATA directory is relocated within the interpreter to a non-canonical location; the problem is under investigation in order to support all installation methods for Python. +[^1]: the reason is that, when Python is installed via the _Microsoft Store_, the _APPDATA_ directory is relocated within the interpreter in order to confine it in a sandbox: the way of handling file system redirections for packaged applications is [documented here](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-behind-the-scenes#file-system). **When** uses the redirected location to store configuration and data, and **whenever** works anyway, however this type of setup is discouraged because it leads to configuration files that are both hard to read and hard to find. Other installations performed using either _winget_, or the _Python Install Manager_, which in turn is becoming the preferred installation method for Python on Windows, or the standalone Python installer, are not affected by this problem. If a _Microsoft Store_ based setup is really needed, be careful to disable the setting that limits the path length, as it might cause issues because of the intrinsic length of the base path. +[^2]: the _PATH_ variable might need to be adjusted if this method is used, as suggested by the `pip` command, or the command `python -m pipx` has to be used instead of simply typing `pipx`; the same yields for _Microsoft Store_ based Python installations. diff --git a/support/docs/tutorial.md b/support/docs/tutorial.md index be233b0..8a7e36a 100644 --- a/support/docs/tutorial.md +++ b/support/docs/tutorial.md @@ -24,7 +24,7 @@ The examples assume that **When** has been installed using the [suggested method It's sometimes useful to have the ability to trace the configuration of one or more conditions for debug purposes: configuring a condition can sometimes be awful, especially when it is one of the most complex ones (such as a [command](cond_actionrelated.md#command) or a [Lua](cond_actionrelated.md#lua-script) based condition), and you might just want to test your condition with no side effects other than dropping a line to the log. For this I use a simple _trace_ task, consisting in a minimal Lua script: ```lua -log.warn("Trace: *** VERIFIED CONDITION *** `" .. whenever_condition .. "`"); +log.warn("Trace: *** VERIFIED CONDITION *** `" .. whenever_condition .. "`") ``` that exploits the [abilities](lua_considerations.md) of the internal Lua interpreter to