From 3df0c7532ff9714ab3cd6d75f08e8c042f12a974 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sat, 14 Feb 2026 16:54:51 +0100 Subject: [PATCH 01/57] feat: confluence - enable temp dir --- lib/utility.py | 16 ++++++++++++++++ when/when.py | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/lib/utility.py b/lib/utility.py index 2433999..451c618 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -257,6 +257,22 @@ def get_scriptsdir() -> str: return scriptdir +# determine temp directory and ensure that it exists +def get_tempdir() -> str: + configdir: str = AppConfig.get("APPDATA") # type: ignore + if is_windows(): + subdir = "Temp" + else: + subdir = "temp" + tempdir = os.path.join(configdir, subdir) + if not os.path.isdir(tempdir): + try: + os.makedirs(tempdir) + except Exception: + raise OSError(CLI_ERR_SPECIFICDIR_UNACCESSIBLE % tempdir) + return tempdir + + # determine lua library directory and ensure that it exists def get_luadir() -> str: configdir: str = AppConfig.get("APPDATA") # type: ignore diff --git a/when/when.py b/when/when.py index c8c3bd0..81d55e6 100644 --- a/when/when.py +++ b/when/when.py @@ -25,6 +25,7 @@ get_whenever_version, check_whenever_version, get_luadir, + get_tempdir, get_scriptsdir, get_appdata, get_logfile, @@ -291,6 +292,10 @@ def prepare_environment() -> None: _ = get_scriptsdir() except Exception as e: exit_error(e) + try: + _ = get_tempdir() + except Exception as e: + exit_error(e) try: _ = get_luadir() except Exception as e: From 2972c0395f51e7cc52e2678754f86864a7b78398 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 18 Feb 2026 15:04:08 +0100 Subject: [PATCH 02/57] temp: begin supporting MCRT aka confluence The `confluence` feature is now labeled MCRT, which stands for Multiple Conditions to Run Tasks, and will probably keep sporting this name --- lib/internal/more_conds_trigger_task.py | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 lib/internal/more_conds_trigger_task.py diff --git a/lib/internal/more_conds_trigger_task.py b/lib/internal/more_conds_trigger_task.py new file mode 100644 index 0000000..e78872b --- /dev/null +++ b/lib/internal/more_conds_trigger_task.py @@ -0,0 +1,96 @@ +# multiple conditions to run tasks (aka confluence) +# +# private items that enable the condition "confluence" feature (issue #187), +# that is the possibility for a task to depend on more than one condition. +# +# The idea is to have a persistence file where *all* the conditions that are +# defined as concurring to the triggering of tasks are recorded: this file is +# periodically read, and when all the conditions that concur to a specific +# task (or list/set of tasks) are found in the file, the following happens: +# +# 1. the conditions are _removed_ from the file +# 2. the corresponding task or list/set of tasks is executed +# +# For this purpose, the conditions that concur to tasks will all be bound to +# a single Lua-based task, which writes their name in the file, while multiple +# conditions will be built for each task group to be activated by multiple +# conditions. +# +# The persistence file does not need to be structured, it only has to be read, +# operated on, and written quickly. No concurrent reads or writes should be +# allowed, assuming that one instance of a verified condition cannot concur to +# multiple task groups (this is arbitrary, but cleaner than the opposite), and +# that once a certain set of conditions that concur to a task group is verified +# it has to be atomically removed from the list. +# +# The condition names are "mangled" before being written to the persistence +# file, but the mangling is simple and effective: it only consists of a pair +# of brackets around the name. This ensures that no name given to other items +# can conflict with the mangled name, since brackets are not allowed chars +# for item names. This also allow for a quick and precise search without the +# need for special considerations: if the name of a condition is "Cond1", for +# instance, searching for the string "(Cond1)" will match only the presence +# of "Cond1" and not, for instance, "ThatCond1" - which is mangled, instead, +# as "(ThatCond1)". This also eliminates the need for multiple lines, regex +# search, and other complications. + +import sys +import os + +from ..utility import get_tempdir + + + +# constants +_MCRT_PERSIST_FILE = ".mcrt_persist" +_MCRT_LOCK_FILE = ".mcrt_persist.lock" + + +_LUA_LIBRARY = ''' +-- mcrt: multiple conditions to trigger a task + +MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] +MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + +function mcrt_mangle_name(name) + return "(" .. name .. ")" +end + +function mcrt_has_name(name, s) + return string.find(s, mcrt_mangle_name(name)) ~= nil +end + +-- see if a file exists +function file_exists(file) + local f = io.open(file, "rb") + if f then f:close() end + return f ~= nil +end + +-- ...to be continued + + +''' + + + + + +# the persistence file is the file that contains the list of conditions that +# concur to task triggering which have been successfully checked +def mcrt_persist_file(): + return os.path.join(get_tempdir(), _MCRT_PERSIST_FILE) + +# the lock file is checked when trying to access the persistence file: no other +# access (including read-only access) will be performed when the lock file is +# present, which indicates a current access +def mcrt_lock_file(): + return os.path.join(get_tempdir(), _MCRT_LOCK_FILE) + + +# ...to be continued + + + + +# end. From 4cc180f187e39733ea4d6dd903ebd56e40e754f6 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 18 Feb 2026 19:58:23 +0100 Subject: [PATCH 03/57] temp: move Lua library to a separate file Move the Lua library for the `confluence` feature to a Lua source file, eventually it will be copy-pasted into a literal string in order to make it available to the Lua interpreter in whenever --- ...rigger_task.py => multi_conds_run_task.py} | 36 ++---- support/mcrt_lib.lua | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 28 deletions(-) rename lib/internal/{more_conds_trigger_task.py => multi_conds_run_task.py} (78%) create mode 100644 support/mcrt_lib.lua diff --git a/lib/internal/more_conds_trigger_task.py b/lib/internal/multi_conds_run_task.py similarity index 78% rename from lib/internal/more_conds_trigger_task.py rename to lib/internal/multi_conds_run_task.py index e78872b..52e1150 100644 --- a/lib/internal/more_conds_trigger_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -22,17 +22,19 @@ # multiple task groups (this is arbitrary, but cleaner than the opposite), and # that once a certain set of conditions that concur to a task group is verified # it has to be atomically removed from the list. -# +# # The condition names are "mangled" before being written to the persistence # file, but the mangling is simple and effective: it only consists of a pair -# of brackets around the name. This ensures that no name given to other items +# of colons around the name. This ensures that no name given to other items # can conflict with the mangled name, since brackets are not allowed chars # for item names. This also allow for a quick and precise search without the # need for special considerations: if the name of a condition is "Cond1", for -# instance, searching for the string "(Cond1)" will match only the presence +# instance, searching for the string ":Cond1:" will match only the presence # of "Cond1" and not, for instance, "ThatCond1" - which is mangled, instead, -# as "(ThatCond1)". This also eliminates the need for multiple lines, regex -# search, and other complications. +# as ":ThatCond1:". This also eliminates the need for multiple lines, regex +# search, and other complications. The colon character is used because it has +# no special meaning in Lua pattern syntax, so the `string.gsub` function can +# be used. import sys import os @@ -47,29 +49,7 @@ _LUA_LIBRARY = ''' --- mcrt: multiple conditions to trigger a task - -MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] -MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] - -function mcrt_mangle_name(name) - return "(" .. name .. ")" -end - -function mcrt_has_name(name, s) - return string.find(s, mcrt_mangle_name(name)) ~= nil -end - --- see if a file exists -function file_exists(file) - local f = io.open(file, "rb") - if f then f:close() end - return f ~= nil -end - --- ...to be continued - - +-- to be eventually replaced by the contents of ../../support/mcrt_lib.lua ''' diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua new file mode 100644 index 0000000..da60384 --- /dev/null +++ b/support/mcrt_lib.lua @@ -0,0 +1,119 @@ +-- mcrt: multiple conditions to run a task +-- NOTE: internals have a double underscore and will not be directly +-- used in the scripts that require the library + +MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] +MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + +-- mangle names +function __mangle_name(name) + return ":" .. name .. ":" +end + +-- find whether or not a (mangled) name is present in a string +function __has_name(name, s) + return string.find(s, mcrt_mangle_name(name)) ~= nil +end + +-- remove a name from a string +function __rm_name(name, s) + return string.gsub(s, __mangle_name(name), "") +end + +-- add a name to a string +function __add_name(name, s) + return s .. __mangle_name(name) +end + + +-- test if a file exists +function __file_exists(file) + local f = io.open(file, "rb") + if f then f:close() end + return f ~= nil +end + +-- wait for the lock file to disappear: unfortunately stock Lua has no +-- sleep() function, so we do busy wait here hoping that it will never be +-- useful and that the lock would last possibly a bunch of usecs at most +function __wait_lock() + while __file_exists(MCRT_LOCK_FILE) do end +end + +-- set and reset the lock +function __set_lock() + local f = io.open(MCRT_LOCK_FILE, "wb") + f:close() +end + +function __reset_lock() + if __file_exists(MCRT_LOCK_FILE) then + os.remove(MCRT_LOCK_FILE) + end +end + +-- read the contents of the persistent file, return nil if read failed +function __read_persistent() + local f = io.open(MCRT_PERSIST_FILE, "r") + local s = nil + if f then + s = f:read("*all") + f:close() + end + return s +end + +-- write the specified contents to the persistent file, truncate the file +-- on null content, which is useful for initialization +function __write_persistent(content) + local f = io.open(MCRT_PERSIST_FILE, "w") + if content ~= nil then + f:write(content) + end + f:close() +end + + +-- actual library functions + +-- set the condition bearing the provided name to verified +function set_condition_verified(cond_name) + __wait_lock() + __set_lock() + local persistent = __read_persistent() + if persistent ~= nil then + if !__has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") + end + __write_persistent(persistent) + __reset_lock() +end + +-- check whether the provided conditions are all verified, and if so remove +-- their names prior to returning true; otherwise return false +function check_conditions_verified(cond_names) + local res = true + __wait_lock() + __set_lock() + local persistent = __read_persistent() + for _, name in ipairs(cond_names) do + if !__has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) + end + __reset_lock() + return res +end + + +''' From d56877dc8c035fac775928ed2df21672691baabc Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 18 Feb 2026 22:04:55 +0100 Subject: [PATCH 04/57] temp: fix Lua library source --- support/mcrt_lib.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index da60384..e4107e6 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -115,5 +115,4 @@ function check_conditions_verified(cond_names) return res end - -''' +-- end. From 7552af70e4b74bccec5881713f3a2d947e7db8fb Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 20 Feb 2026 16:12:23 +0100 Subject: [PATCH 05/57] temp: add MCRT initializer in Lua Additionally, copy the Lua library to a string in the Python module --- lib/internal/multi_conds_run_task.py | 127 ++++++++++++++++++++++++++- support/mcrt_lib.lua | 8 ++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 52e1150..b021718 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -49,7 +49,132 @@ _LUA_LIBRARY = ''' --- to be eventually replaced by the contents of ../../support/mcrt_lib.lua +-- mcrt: multiple conditions to run a task +-- NOTE: internals have a double underscore and will not be directly +-- used in the scripts that require the library + +MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] +MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + +-- mangle names +function __mangle_name(name) + return ":" .. name .. ":" +end + +-- find whether or not a (mangled) name is present in a string +function __has_name(name, s) + return string.find(s, mcrt_mangle_name(name)) ~= nil +end + +-- remove a name from a string +function __rm_name(name, s) + return string.gsub(s, __mangle_name(name), "") +end + +-- add a name to a string +function __add_name(name, s) + return s .. __mangle_name(name) +end + + +-- test if a file exists +function __file_exists(file) + local f = io.open(file, "rb") + if f then f:close() end + return f ~= nil +end + +-- wait for the lock file to disappear: unfortunately stock Lua has no +-- sleep() function, so we do busy wait here hoping that it will never be +-- useful and that the lock would last possibly a bunch of usecs at most +function __wait_lock() + while __file_exists(MCRT_LOCK_FILE) do end +end + +-- set and reset the lock +function __set_lock() + local f = io.open(MCRT_LOCK_FILE, "wb") + f:close() +end + +function __reset_lock() + if __file_exists(MCRT_LOCK_FILE) then + os.remove(MCRT_LOCK_FILE) + end +end + +-- read the contents of the persistent file, return nil if read failed +function __read_persistent() + local f = io.open(MCRT_PERSIST_FILE, "r") + local s = nil + if f then + s = f:read("*all") + f:close() + end + return s +end + +-- write the specified contents to the persistent file, truncate the file +-- on null content, which is useful for initialization +function __write_persistent(content) + local f = io.open(MCRT_PERSIST_FILE, "w") + if content ~= nil then + f:write(content) + end + f:close() +end + + +-- actual library functions + +-- initialize the persistent file +function initialize() + __wait_lock() + __set_lock() + __write_persistent(nil) + __reset_lock() +end + +-- set the condition bearing the provided name to verified +function set_condition_verified(cond_name) + __wait_lock() + __set_lock() + local persistent = __read_persistent() + if persistent ~= nil then + if !__has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") + end + __write_persistent(persistent) + __reset_lock() +end + +-- check whether the provided conditions are all verified, and if so remove +-- their names prior to returning true; otherwise return false +function check_conditions_verified(cond_names) + local res = true + __wait_lock() + __set_lock() + local persistent = __read_persistent() + for _, name in ipairs(cond_names) do + if !__has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) + end + __reset_lock() + return res +end + +-- end. ''' diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index e4107e6..3d55639 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -76,6 +76,14 @@ end -- actual library functions +-- initialize the persistent file +function initialize() + __wait_lock() + __set_lock() + __write_persistent(nil) + __reset_lock() +end + -- set the condition bearing the provided name to verified function set_condition_verified(cond_name) __wait_lock() From f3f4dc99288b1b7f927b64303fc2d07ba45ef7e8 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sat, 21 Feb 2026 16:38:00 +0100 Subject: [PATCH 06/57] temp: define the items that enable confluence --- lib/internal/multi_conds_run_task.py | 88 +++++++++++++++++++++++++++- lib/utility.py | 11 +++- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index b021718..2524352 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -39,13 +39,23 @@ import sys import os -from ..utility import get_tempdir +from tomlkit.items import Table +from ..utility import ( + get_tempdir, + get_luadir, + get_private_item_name_prefix, + generate_item_name_suffix, + ) + +from tomlkit import items +from ..items import task_lua, cond_lua, cond_interval # constants _MCRT_PERSIST_FILE = ".mcrt_persist" _MCRT_LOCK_FILE = ".mcrt_persist.lock" +_MCRT_LIBRARY = "_mcrt_lib.lua" _LUA_LIBRARY = ''' @@ -178,7 +188,8 @@ ''' - +# this is the prefix for all of our item names +_ITEM_PREFIX = get_private_item_name_prefix() + "_MCRT_" # the persistence file is the file that contains the list of conditions that @@ -193,9 +204,82 @@ def mcrt_lock_file(): return os.path.join(get_tempdir(), _MCRT_LOCK_FILE) +# utility functions +def mcrt_install_lib(): + s = os.path.join(get_luadir(), _MCRT_LIBRARY) + if not os.path.exists(s): + lua_library = _LUA_LIBRARY.format( + MCRT_LOCK_FILE=mcrt_lock_file(), + MCRT_PERSIST_FILE=mcrt_persist_file(), + ) + with open(s, 'w') as f: + f.write(lua_library) + + +# the following items are the specific ones that implement the confluence + +# 1. initialization task: resets the persistence file; the reason why we want +# this to be performed by the Lua interpreter instead of the GUI application, +# is that in this way the MCRT system is initialized even when used with +# another frontend +_mcrt_InitializationTask = task_lua.LuaScriptTask() +_mcrt_InitializationTask.name = _ITEM_PREFIX + "_Initializer" +_mcrt_InitializationTask.script = f"""\ +local mcrt = require "{_MCRT_LIBRARY}" +mcrt.initialize() +""" +mcrt_Initializer = _mcrt_InitializationTask + +# 2. updater: just adds the verified condition to the persistence file +_mcrt_UpdateTask = task_lua.LuaScriptTask() +_mcrt_UpdateTask.name = _ITEM_PREFIX + "_Updater" +_mcrt_UpdateTask.script = f"""\ +local mcrt = require "{_MCRT_LIBRARY}" +mcrt.set_condition_verified(whenever_condition) +""" +mcrt_Updater = _mcrt_UpdateTask + +# 3. initialization condition: it is a once-only condition that only is +# verified at the first ttick, and runs the initialization task +_mcrt_InitializationCond = cond_interval.IntervalCondition() +_mcrt_InitializationCond.name = _ITEM_PREFIX + "_Initializer" +_mcrt_InitializationCond.interval_seconds = 1 # the bare minimum +_mcrt_InitializationCond.tasks = [_mcrt_InitializationTask.name] +mcrt_InitialCondition = _mcrt_InitializationCond + + +# the following part is more difficult, because a condition is needed for +# every set of conditions that have to be verified to run a task, so we use +# a function that creates it +_mcrt_CondConfluence_scriptTemplate = f"""\ +local mcrt = require "{_MCRT_LIBRARY}" +res = mcrt.check_conditions_verified([[COND_LIST]]) +""" +def mcrt_CondConfluence(conditions: list[str], suffix: str | None) -> cond_lua.LuaScriptCondition: + if suffix is None: + suffix = generate_item_name_suffix() + cond_list = '{"%s"}' % '", "'.join(conditions) + cond = cond_lua.LuaScriptCondition() + cond.name = _ITEM_PREFIX + "Check_" + suffix + cond.script = _mcrt_CondConfluence_scriptTemplate.replace( + "[[COND_LIST]]", + cond_list, + ) + cond.expected_results = { "res": True } + return cond + + # ...to be continued +# only return interesting elements (this may actually change) +__all__ = [ + "mcrt_InitialCondition", + "mcrt_Initializer", + "mcrt_Updater", + "mcrt_CondConfluence", +] + # end. diff --git a/lib/utility.py b/lib/utility.py index 451c618..e39c337 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -156,15 +156,20 @@ def guess_typed_value(s: str) -> bool | int | float | str: return s +# find an unique-ish suffix for an item name +def generate_item_name_suffix() -> str: + d = blake2s(digest_size=5) + d.update(str(time()).encode("utf-8")) + return d.hexdigest().upper() + + # find an unique-ish name for an item def generate_item_name(o=None) -> str: if o is None: base = "Item" else: base = o.__class__.__name__ - d = blake2s(digest_size=5) - d.update(str(time()).encode("utf-8")) - return "%s_%s" % (base, d.hexdigest().upper()) + return "%s_%s" % (base, generate_item_name_suffix()) # get an image from a stored icon From 847d3d8e788eeeb1a37004d3ef658c8f4e5cd801 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 22 Feb 2026 15:41:02 +0100 Subject: [PATCH 07/57] temp: complete utilities to implement MCRT --- lib/internal/multi_conds_run_task.py | 91 +++++++++++++++++----------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 2524352..d6c83cf 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -36,19 +36,14 @@ # no special meaning in Lua pattern syntax, so the `string.gsub` function can # be used. -import sys import os -from tomlkit.items import Table - from ..utility import ( get_tempdir, get_luadir, get_private_item_name_prefix, - generate_item_name_suffix, - ) +) -from tomlkit import items from ..items import task_lua, cond_lua, cond_interval @@ -57,8 +52,7 @@ _MCRT_LOCK_FILE = ".mcrt_persist.lock" _MCRT_LIBRARY = "_mcrt_lib.lua" - -_LUA_LIBRARY = ''' +_LUA_LIBRARY = """ -- mcrt: multiple conditions to run a task -- NOTE: internals have a double underscore and will not be directly -- used in the scripts that require the library @@ -185,7 +179,7 @@ end -- end. -''' +""" # this is the prefix for all of our item names @@ -194,13 +188,14 @@ # the persistence file is the file that contains the list of conditions that # concur to task triggering which have been successfully checked -def mcrt_persist_file(): +def _mcrt_persist_file(): return os.path.join(get_tempdir(), _MCRT_PERSIST_FILE) + # the lock file is checked when trying to access the persistence file: no other # access (including read-only access) will be performed when the lock file is # present, which indicates a current access -def mcrt_lock_file(): +def _mcrt_lock_file(): return os.path.join(get_tempdir(), _MCRT_LOCK_FILE) @@ -209,10 +204,10 @@ def mcrt_install_lib(): s = os.path.join(get_luadir(), _MCRT_LIBRARY) if not os.path.exists(s): lua_library = _LUA_LIBRARY.format( - MCRT_LOCK_FILE=mcrt_lock_file(), - MCRT_PERSIST_FILE=mcrt_persist_file(), + MCRT_LOCK_FILE=_mcrt_lock_file(), + MCRT_PERSIST_FILE=_mcrt_persist_file(), ) - with open(s, 'w') as f: + with open(s, "w") as f: f.write(lua_library) @@ -228,7 +223,12 @@ def mcrt_install_lib(): local mcrt = require "{_MCRT_LIBRARY}" mcrt.initialize() """ -mcrt_Initializer = _mcrt_InitializationTask + + +# return this item +def mcrt_initializer() -> task_lua.LuaScriptTask: + return _mcrt_InitializationTask + # 2. updater: just adds the verified condition to the persistence file _mcrt_UpdateTask = task_lua.LuaScriptTask() @@ -237,48 +237,65 @@ def mcrt_install_lib(): local mcrt = require "{_MCRT_LIBRARY}" mcrt.set_condition_verified(whenever_condition) """ -mcrt_Updater = _mcrt_UpdateTask + + +# return this item +def mcrt_updater() -> task_lua.LuaScriptTask: + return _mcrt_UpdateTask + # 3. initialization condition: it is a once-only condition that only is -# verified at the first ttick, and runs the initialization task +# verified at the first tick, and runs the initialization task; using a +# zero duration here makes us quite confident that, if the user has not +# edited the configuration file by hand, this will be the first interval +# based condition that will be verified, because the interval condition +# definition form only accepts values strictly above zero, while whenever +# also accepts a zero duration in the configuration (which is in fact the +# way to create a condition that is verified at startup) _mcrt_InitializationCond = cond_interval.IntervalCondition() _mcrt_InitializationCond.name = _ITEM_PREFIX + "_Initializer" -_mcrt_InitializationCond.interval_seconds = 1 # the bare minimum +_mcrt_InitializationCond.interval_seconds = 0 # that is, at the first tick _mcrt_InitializationCond.tasks = [_mcrt_InitializationTask.name] -mcrt_InitialCondition = _mcrt_InitializationCond -# the following part is more difficult, because a condition is needed for -# every set of conditions that have to be verified to run a task, so we use -# a function that creates it +# return this item +def mcrt_initial_condition() -> cond_interval.IntervalCondition: + return _mcrt_InitializationCond + + +# 4. the confluence condition creation function uses a template to define a +# new condition for each set of conditions that have to be verified in order +# to let a task group be run: verification depends on the result of the Lua +# function, that must be `true` _mcrt_CondConfluence_scriptTemplate = f"""\ local mcrt = require "{_MCRT_LIBRARY}" res = mcrt.check_conditions_verified([[COND_LIST]]) """ -def mcrt_CondConfluence(conditions: list[str], suffix: str | None) -> cond_lua.LuaScriptCondition: - if suffix is None: - suffix = generate_item_name_suffix() - cond_list = '{"%s"}' % '", "'.join(conditions) + + +def mcrt_confluence_condition( + name: str, conditions: list[str] +) -> cond_lua.LuaScriptCondition: cond = cond_lua.LuaScriptCondition() - cond.name = _ITEM_PREFIX + "Check_" + suffix + cond.name = name cond.script = _mcrt_CondConfluence_scriptTemplate.replace( "[[COND_LIST]]", - cond_list, + '{"%s"}' % '", "'.join(conditions), ) - cond.expected_results = { "res": True } + cond.expected_results = {"res": True} + cond.tags = { + "mcrt_confluent_conditions": conditions, + } return cond -# ...to be continued - - - # only return interesting elements (this may actually change) __all__ = [ - "mcrt_InitialCondition", - "mcrt_Initializer", - "mcrt_Updater", - "mcrt_CondConfluence", + "mcrt_initial_condition", + "mcrt_initializer", + "mcrt_updater", + "mcrt_confluence_condition", + "mcrt_install_lib", ] From c88551eaccda5316a78ebec94b6181f058dd192f Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 22 Feb 2026 16:36:00 +0100 Subject: [PATCH 08/57] temp: install Lua library at startup --- lib/internal/multi_conds_run_task.py | 6 +++++- lib/toolbox/install_lua.py | 9 +++++++++ when/when.py | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index d6c83cf..a53fe60 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -45,6 +45,7 @@ ) from ..items import task_lua, cond_lua, cond_interval +from ..toolbox import install_lua # constants @@ -199,7 +200,9 @@ def _mcrt_lock_file(): return os.path.join(get_tempdir(), _MCRT_LOCK_FILE) -# utility functions +# utility to install the Lua library: it also reserves the library file name +# so that it is not overwritten by the user in case he decides to install +# a Lua library of choice def mcrt_install_lib(): s = os.path.join(get_luadir(), _MCRT_LIBRARY) if not os.path.exists(s): @@ -209,6 +212,7 @@ def mcrt_install_lib(): ) with open(s, "w") as f: f.write(lua_library) + install_lua.reserve_lua(_MCRT_LIBRARY) # the following items are the specific ones that implement the confluence diff --git a/lib/toolbox/install_lua.py b/lib/toolbox/install_lua.py index 487b123..c12a419 100644 --- a/lib/toolbox/install_lua.py +++ b/lib/toolbox/install_lua.py @@ -170,9 +170,18 @@ def upgrade_lua(fname: str, verbose: bool = True) -> bool: return install_lua(fname, verbose) +# add a name to the list of reserved files and directories: this is used +# internally upon initialization of services by When itself, so we assume +# that the name is correct +def reserve_lua(fname: str): + if fname not in RESERVED: + RESERVED.append(fname) + + __all__ = [ "install_lua", "upgrade_lua", + "reserve_lua", ] diff --git a/when/when.py b/when/when.py index 81d55e6..c7c6f77 100644 --- a/when/when.py +++ b/when/when.py @@ -45,6 +45,8 @@ from lib.runner.process import Wrapper +from lib.internal.multi_conds_run_task import mcrt_install_lib + # main root window, to be withdrawn _root = None @@ -626,6 +628,9 @@ def main() -> None: AppConfig.set("APPDATA", default_appdata) AppConfig.set("WHENEVER", default_whenever) + # other initialization actions + mcrt_install_lib() + # main parser parser = argparse.ArgumentParser( description=CLI_APP_DESCRIPTION, From 6eb7d260b28b2bdc54b57bd7b94c10f8e2326f42 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 22 Feb 2026 19:29:38 +0100 Subject: [PATCH 09/57] temp: implement confluent cond like an extra item --- lib/i18n/strings_base.py | 1 + lib/internal/multi_conds_run_task.py | 91 +++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index 48da387..86016eb 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -17,6 +17,7 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" +ITEM_COND_MCRT_CONFLUENCE = "Condition Verified By Other Conditions" ITEM_EVENT = "Event" ITEM_EVENT_FSCHANGE = "Filesystem Monitoring Based Event" diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index a53fe60..9ddc3a8 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -36,7 +36,10 @@ # no special meaning in Lua pattern syntax, so the `string.gsub` function can # be used. +from ..i18n.strings import * + import os +from tomlkit import items, table from ..utility import ( get_tempdir, @@ -53,6 +56,8 @@ _MCRT_LOCK_FILE = ".mcrt_persist.lock" _MCRT_LIBRARY = "_mcrt_lib.lua" +_MCRT_EXTRA_DELAY = 15 + _LUA_LIBRARY = """ -- mcrt: multiple conditions to run a task -- NOTE: internals have a double underscore and will not be directly @@ -277,20 +282,76 @@ def mcrt_initial_condition() -> cond_interval.IntervalCondition: """ -def mcrt_confluence_condition( - name: str, conditions: list[str] -) -> cond_lua.LuaScriptCondition: - cond = cond_lua.LuaScriptCondition() - cond.name = name - cond.script = _mcrt_CondConfluence_scriptTemplate.replace( - "[[COND_LIST]]", - '{"%s"}' % '", "'.join(conditions), - ) - cond.expected_results = {"res": True} - cond.tags = { - "mcrt_confluent_conditions": conditions, - } - return cond +# we can implement this type of condition as an "extra" condition anyway, so +# we define an item and a form for it exactly in the same way: the only +# difference is that the item is forced into the available ones and not +# dynamically loaded from the `extra` module folder +class mcrt_ConfluenceCondition(cond_lua.LuaScriptCondition): + + # availability at class level: these variables *MUST* be set for all items + item_type = "lua" + item_subtype = "mcrt_confluence" + item_hrtype = ITEM_COND_MCRT_CONFLUENCE + available = True + + def __init__(self, t: items.Table | None = None): + self.type = self.item_type + self.subtype = self.item_subtype + self.hrtype = self.item_hrtype + if t: + assert t.get("type") == self.type + self.tags = t.get("tags", table()) + assert isinstance(self.tags, items.Table) + assert self.tags.get("subtype") == self.subtype + else: + self.tags = table() + self.tags.append("subtype", self.subtype) + self.tags.append("mcrt_confluent_conditions", list()) + self.updateitem() + + def updateitem(self): + confluent_conditions = self.tags.get("mcrt_confluent_conditions", list()) + self.script = _mcrt_CondConfluence_scriptTemplate.replace( + "[[COND_LIST]]", + '{"%s"}' % '", "'.join(confluent_conditions), + ) + self.expected_results = {"res": True} + self.check_after = _MCRT_EXTRA_DELAY # for now keep it fixed to 15 seconds + self.recur_after_failed_check = True + + @classmethod + def check_tags(cls, tags): + missing = [] + errors = [] + confluent_conditions = tags.get("mcrt_confluent_conditions") + if confluent_conditions is None: + missing.append("mcrt_confluent_conditions") + elif ( + not isinstance(confluent_conditions, list) + or not len(confluent_conditions) > 1 + or any(not isinstance(x, str) for x in confluent_conditions) + ): + errors.append("mcrt_confluent_conditions") + if errors or missing: + return (errors, missing) + return None + + +# def mcrt_confluence_condition( +# name: str, conditions: list[str] +# ) -> cond_lua.LuaScriptCondition: +# cond = cond_lua.LuaScriptCondition() +# cond.name = name +# cond.script = _mcrt_CondConfluence_scriptTemplate.replace( +# "[[COND_LIST]]", +# '{"%s"}' % '", "'.join(conditions), +# ) +# cond.expected_results = {"res": True} +# cond.tags = { +# "subtype": "mcrt_confluence", +# "mcrt_confluent_conditions": conditions, +# } +# return cond # only return interesting elements (this may actually change) @@ -298,8 +359,8 @@ def mcrt_confluence_condition( "mcrt_initial_condition", "mcrt_initializer", "mcrt_updater", - "mcrt_confluence_condition", "mcrt_install_lib", + "mcrt_ConfluenceCondition", ] From 48da7cab2b57ed10c7e8dfa8099e6b5add01ceb2 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sat, 28 Feb 2026 18:45:43 +0100 Subject: [PATCH 10/57] temp: start shaping the MCRT condition form --- lib/i18n/strings_base.py | 3 +- lib/internal/multi_conds_run_task.py | 152 ++++++++++++++++++++------- lib/items/item.py | 4 + 3 files changed, 119 insertions(+), 40 deletions(-) diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index f764ac6..91ba2c4 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -17,7 +17,7 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" -ITEM_COND_MCRT_CONFLUENCE = "Condition Verified By Other Conditions" +ITEM_COND_MCRT = "Condition Activated By Other Conditions" ITEM_EVENT = "Event" ITEM_EVENT_FSCHANGE = "Filesystem Monitoring Based Event" @@ -258,6 +258,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: Time Condition Editor" UI_TITLE_DBUSCOND = f"{UI_APP}: DBus Method Condition Editor" UI_TITLE_WMICOND = f"{UI_APP}: WMI Query Condition Editor" +UI_TITLE_MCRTCOND = f"{UI_APP}: Condition Activated by Other Conditions" UI_TITLE_DBUSEVENT = f"{UI_APP}: DBus Signal Event Editor" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: Filesystem Monitoring Event Editor" diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 9ddc3a8..e3032bf 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -41,13 +41,26 @@ import os from tomlkit import items, table +import tkinter as tk +import ttkbootstrap as ttk + +from typing import List, Tuple + +from ..utility import check_not_none, append_not_none + +from ..forms.ui import * + +# since a condition is defined, the base form is the one for conditions +from ..forms.cond import form_Condition + + from ..utility import ( get_tempdir, get_luadir, get_private_item_name_prefix, ) -from ..items import task_lua, cond_lua, cond_interval +from ..items import cond, task_lua, cond_lua, cond_interval from ..toolbox import install_lua @@ -191,6 +204,17 @@ # this is the prefix for all of our item names _ITEM_PREFIX = get_private_item_name_prefix() + "_MCRT_" +# specific names, local to this module +_TASK_INITIALIZER = _ITEM_PREFIX + "Initializer" +_TASK_UPDATER = _ITEM_PREFIX + "Updater" +_COND_INITIALIZER = _ITEM_PREFIX + "Initializer" + +# the template for the confluence condition Lua script +_MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE = f"""\ +local mcrt = require "{_MCRT_LIBRARY}" +res = mcrt.check_conditions_verified([[COND_LIST]]) +""" + # the persistence file is the file that contains the list of conditions that # concur to task triggering which have been successfully checked @@ -227,31 +251,37 @@ def mcrt_install_lib(): # is that in this way the MCRT system is initialized even when used with # another frontend _mcrt_InitializationTask = task_lua.LuaScriptTask() -_mcrt_InitializationTask.name = _ITEM_PREFIX + "_Initializer" +_mcrt_InitializationTask.name = _TASK_INITIALIZER _mcrt_InitializationTask.script = f"""\ local mcrt = require "{_MCRT_LIBRARY}" mcrt.initialize() """ -# return this item +# return this item and its name def mcrt_initializer() -> task_lua.LuaScriptTask: return _mcrt_InitializationTask +def mcrt_initializer_name() -> str: + return _TASK_INITIALIZER + # 2. updater: just adds the verified condition to the persistence file _mcrt_UpdateTask = task_lua.LuaScriptTask() -_mcrt_UpdateTask.name = _ITEM_PREFIX + "_Updater" +_mcrt_UpdateTask.name = _TASK_UPDATER _mcrt_UpdateTask.script = f"""\ local mcrt = require "{_MCRT_LIBRARY}" mcrt.set_condition_verified(whenever_condition) """ -# return this item +# return this item and its name def mcrt_updater() -> task_lua.LuaScriptTask: return _mcrt_UpdateTask +def mcrt_updater_name() -> str: + return _TASK_UPDATER + # 3. initialization condition: it is a once-only condition that only is # verified at the first tick, and runs the initialization task; using a @@ -262,36 +292,30 @@ def mcrt_updater() -> task_lua.LuaScriptTask: # also accepts a zero duration in the configuration (which is in fact the # way to create a condition that is verified at startup) _mcrt_InitializationCond = cond_interval.IntervalCondition() -_mcrt_InitializationCond.name = _ITEM_PREFIX + "_Initializer" +_mcrt_InitializationCond.name = _COND_INITIALIZER _mcrt_InitializationCond.interval_seconds = 0 # that is, at the first tick _mcrt_InitializationCond.tasks = [_mcrt_InitializationTask.name] -# return this item +# return this item and its name def mcrt_initial_condition() -> cond_interval.IntervalCondition: return _mcrt_InitializationCond - -# 4. the confluence condition creation function uses a template to define a -# new condition for each set of conditions that have to be verified in order -# to let a task group be run: verification depends on the result of the Lua -# function, that must be `true` -_mcrt_CondConfluence_scriptTemplate = f"""\ -local mcrt = require "{_MCRT_LIBRARY}" -res = mcrt.check_conditions_verified([[COND_LIST]]) -""" +def mcrt_initial_condition_name() -> str: + return _COND_INITIALIZER -# we can implement this type of condition as an "extra" condition anyway, so +# 4. confluence condition: uses the script template defined above; we +# can implement this type of condition as an "extra" condition anyway, so # we define an item and a form for it exactly in the same way: the only # difference is that the item is forced into the available ones and not # dynamically loaded from the `extra` module folder -class mcrt_ConfluenceCondition(cond_lua.LuaScriptCondition): +class ConfluenceCondition(cond_lua.LuaScriptCondition): # availability at class level: these variables *MUST* be set for all items item_type = "lua" item_subtype = "mcrt_confluence" - item_hrtype = ITEM_COND_MCRT_CONFLUENCE + item_hrtype = ITEM_COND_MCRT available = True def __init__(self, t: items.Table | None = None): @@ -311,7 +335,7 @@ def __init__(self, t: items.Table | None = None): def updateitem(self): confluent_conditions = self.tags.get("mcrt_confluent_conditions", list()) - self.script = _mcrt_CondConfluence_scriptTemplate.replace( + self.script = _MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE.replace( "[[COND_LIST]]", '{"%s"}' % '", "'.join(confluent_conditions), ) @@ -337,30 +361,80 @@ def check_tags(cls, tags): return None -# def mcrt_confluence_condition( -# name: str, conditions: list[str] -# ) -> cond_lua.LuaScriptCondition: -# cond = cond_lua.LuaScriptCondition() -# cond.name = name -# cond.script = _mcrt_CondConfluence_scriptTemplate.replace( -# "[[COND_LIST]]", -# '{"%s"}' % '", "'.join(conditions), -# ) -# cond.expected_results = {"res": True} -# cond.tags = { -# "subtype": "mcrt_confluence", -# "mcrt_confluent_conditions": conditions, -# } -# return cond - - -# only return interesting elements (this may actually change) +# TODO: this form should disable the possibility to be confluent +class form_ConfluenceCondition(form_Condition): + + # note that the available conditions should be filtered, the provided + # names must correspond to conditions that activate confluence: this + # module provides a helper to distinguish them from others + def __init__(self, tasks_available, conds_available, item=None): + # check that item is the expected one for safety, build one by default + if item: + assert isinstance(item, ConfluenceCondition) + else: + item = ConfluenceCondition() + super().__init__(UI_TITLE_MCRTCOND, tasks_available, item) + + # create a specific frame for the contents + area = ttk.Frame(super().contents) + area.grid(row=0, column=0, sticky=tk.NSEW) + PAD = WIDGET_PADDING_PIXELS + + # copy the list of conditions, since we have to manipulate it: remove + # the conditions that are already in the activating lists and sort + # the left ones for readability in the combo box; note that the items + # that are not present in the available list are probably leftovers + # from a manual edit of the configuration file, so they should be + # discarded if found + self._conds_available = conds_available.copy() + self._conds_activating = item.tags.get("mcrt_confluent_conditions") or list() + for cond in self._conds_activating.copy(): + if cond in self._conds_available: + self._conds_available.remove(cond) + else: + self._conds_activating.remove(cond) + self._conds_available.sort() + + # ... + + # always update the form at the end of initialization + self._updateform() + + # update the form with the specific parameters (usually in the `tags`) + def _updateform(self) -> None: + self.data_set("parameter1", self._item.tags.get("parameter1")) # type: ignore + return super()._updateform() + + # update the item from the form elements (usually update `tags`) + def _updatedata(self) -> None: + self._item.tags["parameter1"] = self.data_get("parameter1") # type: ignore + self._item.updateitem() # type: ignore + return super()._updatedata() + + +# check whether a condition is confluent +def is_mcrt_confluent_cond(c: cond.Condition) -> bool: + if c.tasks is not None and len(c.tasks) == 1: + return c.tasks[0] == _TASK_UPDATER + return False + +# check whether a condition implements confluence +def is_mcrt_confluence_cond(c: cond.Condition) -> bool: + return isinstance(c, ConfluenceCondition) + + +# only return interesting symbols __all__ = [ "mcrt_initial_condition", + "mcrt_initial_condition_name", "mcrt_initializer", + "mcrt_initializer_name", "mcrt_updater", + "mcrt_updater_name", + "is_mcrt_confluent_cond", + "is_mcrt_confluence_cond", "mcrt_install_lib", - "mcrt_ConfluenceCondition", + "ConfluenceCondition", ] diff --git a/lib/items/item.py b/lib/items/item.py index ca7b110..dd6202e 100644 --- a/lib/items/item.py +++ b/lib/items/item.py @@ -39,6 +39,9 @@ from lib.items.event_fschange import FilesystemChangeEvent from lib.items.event_wmi import WMIEvent +# this is a special case because it implies auxiliary item when used +from ..internal.multi_conds_run_task import ConfluenceCondition, form_ConfluenceCondition + # to dynamically determine nature of extra items from lib.items.task import Task from lib.items.cond import Condition @@ -85,6 +88,7 @@ ('cond:lua', ITEM_COND_LUA, form_LuaScriptCondition, LuaScriptCondition), ('cond:time', ITEM_COND_TIME, form_TimeCondition, TimeCondition), ('cond:wmi', ITEM_COND_WMI, form_WMICondition, WMICondition), + ('cond:mcrt', ITEM_COND_MCRT, form_ConfluenceCondition, ConfluenceCondition), ('event:cli', ITEM_EVENT_CLI, form_CommandEvent, CommandEvent), ('event:dbus', ITEM_EVENT_DBUS, form_DBusEvent, DBusEvent), From 0f0f927505dd878f87da8ef78b5dc4656c473ecd Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 1 Mar 2026 19:58:35 +0100 Subject: [PATCH 11/57] temp(ui): implement MCRT features in forms Create specific form for confluence based conditions, and add a flag to the common part of condition forms that sets a condition to be confluent. --- lib/forms/cond.py | 107 ++++++++++++++++++++-- lib/forms/event_fschange.py | 4 +- lib/i18n/strings_base.py | 6 +- lib/i18n/strings_de.py | 4 +- lib/i18n/strings_en.py | 4 +- lib/i18n/strings_fr.py | 4 +- lib/i18n/strings_it.py | 4 +- lib/internal/multi_conds_run_task.py | 132 +++++++++++++++++++++++---- 8 files changed, 225 insertions(+), 40 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index e0eeb1f..52d7190 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -12,6 +12,11 @@ from ..items.cond import Condition from ..utility import is_valid_item_name, clean_caption +from ..internal.multi_conds_run_task import mcrt_updater_name + + +# add a negated disabled to tk dictionary (please forgive me) +tk.NOT_DISABLED = f"!{tk.DISABLED}" # type: ignore # condition box base class: since this is the class that will be used in @@ -61,8 +66,11 @@ def __init__(self, title, tasks_available, item=None): ck_itemSuspended = ttk.Checkbutton( area_common, text=UI_FORM_SUSPENDCONDATSTARTUP ) - l_maxTasksRetries = ttk.Label(area_common, text=UI_FORM_MAXTASKRETRIES_SC) - e_maxTasksRetries = ttk.Entry(area_common) + ck_mcrtActivateFurther = ttk.Checkbutton( + area_common, text=UI_FORM_ACTIVATEFURTHERCONDS + ) + # l_maxTasksRetries = ttk.Label(area_common, text=UI_FORM_MAXTASKRETRIES_SC) + # e_maxTasksRetries = ttk.Entry(area_common) sep1 = ttk.Separator(area_common) l_tasks = ttk.Label(area_common, text=UI_FORM_ACTIVETASKS_SC) @@ -113,28 +121,39 @@ def __init__(self, title, tasks_available, item=None): # control flow section area_ctlflow = ttk.Frame(area_common) + area_ctlflowL = ttk.Frame(area_ctlflow) + area_ctlflowR = ttk.Frame(area_ctlflow) + area_ctlflowRsub = ttk.Frame(area_ctlflowR) + area_ctlflowL.grid(row=0, column=0, sticky=tk.W) + area_ctlflowR.grid(row=0, column=1, sticky=tk.SE) + area_ctlflowRsub.grid(row=0, column=0, sticky=tk.S) rb_noCheck = ttk.Radiobutton( - area_ctlflow, text=UI_FORM_BREAKNEVER, value="break_none" + area_ctlflowL, text=UI_FORM_BREAKNEVER, value="break_none" ) rb_breakFailure = ttk.Radiobutton( - area_ctlflow, text=UI_FORM_BREAKONFAILURE, value="break_failure" + area_ctlflowL, text=UI_FORM_BREAKONFAILURE, value="break_failure" ) rb_breakSuccess = ttk.Radiobutton( - area_ctlflow, text=UI_FORM_BREAKONSUCCESS, value="break_success" + area_ctlflowL, text=UI_FORM_BREAKONSUCCESS, value="break_success" ) + l_maxTasksRetries = ttk.Label(area_ctlflowRsub, text=UI_FORM_MAXTASKRETRIES_SC) + e_maxTasksRetries = ttk.Entry(area_ctlflowRsub, width=10) # control flow section: arrange widgets rb_noCheck.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) rb_breakFailure.grid(row=1, column=0, sticky=tk.W, padx=PAD, pady=PAD) rb_breakSuccess.grid(row=2, column=0, sticky=tk.W, padx=PAD, pady=PAD) + l_maxTasksRetries.grid(row=0, column=0, sticky=tk.E, padx=PAD, pady=PAD) + e_maxTasksRetries.grid(row=0, column=1, sticky=tk.E, padx=PAD, pady=PAD) # arrange top items items in the appropriate notebook l_itemName.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) e_itemName.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) ck_itemRecurring.grid(row=1, column=1, sticky=tk.W, padx=PAD, pady=PAD) ck_itemSuspended.grid(row=2, column=1, sticky=tk.W, padx=PAD, pady=PAD) - l_maxTasksRetries.grid(row=4, column=0, sticky=tk.W, padx=PAD, pady=PAD) - e_maxTasksRetries.grid(row=4, column=1, sticky=tk.EW, padx=PAD, pady=PAD) + ck_mcrtActivateFurther.grid(row=3, column=1, sticky=tk.W, padx=PAD, pady=PAD) + # l_maxTasksRetries.grid(row=4, column=0, sticky=tk.W, padx=PAD, pady=PAD) + # e_maxTasksRetries.grid(row=4, column=1, sticky=tk.EW, padx=PAD, pady=PAD) sep1.grid(row=5, column=0, columnspan=2, sticky=tk.EW, pady=PAD) l_tasks.grid(row=10, column=0, columnspan=2, sticky=tk.W, padx=PAD, pady=PAD) sftv_tasks.grid( @@ -148,17 +167,21 @@ def __init__(self, title, tasks_available, item=None): ck_itemRecurring.bind("", lambda _: self._check_recurring()) ck_itemRecurring.bind("", lambda _: self._check_recurring()) + ck_mcrtActivateFurther.bind("", lambda _: self._mcrt_confluent()) + ck_mcrtActivateFurther.bind("", lambda _: self._mcrt_confluent()) # expand appropriate sections area_common.rowconfigure(index=11, weight=1) area_common.columnconfigure(1, weight=1) area_taskchoose.columnconfigure(1, weight=1) + area_ctlflow.columnconfigure(0, weight=1) self._area_specific.rowconfigure(index=0, weight=1) self._area_specific.columnconfigure(index=0, weight=1) # bind data to widgets self.data_bind("@name", e_itemName, TYPE_STRING, is_valid_item_name) self.data_bind("@recurring", ck_itemRecurring) + self.data_bind("@confluent", ck_mcrtActivateFurther) self.data_bind( "@max_tasks_retries", e_maxTasksRetries, TYPE_INT, lambda x: x >= -1 ) @@ -176,6 +199,27 @@ def __init__(self, title, tasks_available, item=None): "@max_tasks_retries": clean_caption(UI_FORM_MAXTASKRETRIES_SC), } + # keep a list of task related widget to disable them when needed + self._task_elems = [ + # current list + l_tasks, + tv_tasks, + sb_tasks, + # chooser + l_chooseTask, + cb_chooseTask, + b_addTask, + b_delTask, + # control flow + ck_execSequence, + rb_noCheck, + rb_breakSuccess, + rb_breakFailure, + # retry control + l_maxTasksRetries, + e_maxTasksRetries, + ] + # propagate widgets that need to be accessed self._tv_tasks = tv_tasks self._max_retries = e_maxTasksRetries @@ -190,9 +234,9 @@ def __init__(self, title, tasks_available, item=None): self.changed = False def add_task(self) -> None: - task = self.data_get("@choose_task") - if task: - self._tasks.append(task) + elem = self.data_get("@choose_task") + if elem: + self._tasks.append(elem) self._updatedata() self._updateform() @@ -230,10 +274,52 @@ def _check_recurring(self) -> None: # for , while does better) not_rec = not self.data_get("@recurring") or False if not_rec: + self.data_set("@max_tasks_retries", 0) + self._updatedata() self._max_retries.config(state=tk.DISABLED) else: self._max_retries.config(state=tk.NORMAL) + def _mcrt_confluent(self) -> None: + # same consideration as above; this function also disables all task + # related form widgets when the condition is set to be confluent + not_rec = not self.data_get("@recurring") or False + mcrt = not self.data_get("@confluent") or False + # retrieve the name of the MCRT updater task + mcrt_updater = mcrt_updater_name() + if mcrt: + self.data_set("@name", "") + self.data_set("@control_flow", "break_none") + self.data_set("@execute_sequence", True) + self.data_set("@choose_task", "") + self.data_set("@max_tasks_retries", 0) + self._tv_tasks.delete(*self._tv_tasks.get_children()) + self._tasks = [mcrt_updater] + self._updatedata() + for elem in self._task_elems: + spec = list(elem.state()) + if tk.DISABLED not in spec: + spec.append(tk.DISABLED) + if tk.NOT_DISABLED in spec: # type:ignore + spec.remove(tk.NOT_DISABLED) # type:ignore + elem.state(spec) + self._max_retries.config(state=tk.DISABLED) + else: + for elem in self._task_elems: + spec = list(elem.state()) + if tk.DISABLED in spec: + spec.remove(tk.DISABLED) + if tk.NOT_DISABLED not in spec: # type:ignore + spec.append(tk.NOT_DISABLED) # type:ignore + elem.state(spec) + if not_rec: + self._max_retries.config(state=tk.DISABLED) + else: + self._max_retries.config(state=tk.NORMAL) + if mcrt_updater in self._tasks: + self._tasks.remove(mcrt_updater) + + # contents is the root for slave widgets @property def contents(self) -> ttk.Frame: @@ -278,6 +364,7 @@ def _updateform(self) -> None: self.data_set("@recurring", True) self.data_set("@suspended", False) self.data_set("@execute_sequence", True) + self.data_set("@max_tasks_retries", 0) self.data_set("@choose_task", "") # the data update utility loads data into the item diff --git a/lib/forms/event_fschange.py b/lib/forms/event_fschange.py index b7e15f1..5a2b125 100644 --- a/lib/forms/event_fschange.py +++ b/lib/forms/event_fschange.py @@ -37,7 +37,7 @@ def __init__(self, conditions_available, item=None): PAD = WIDGET_PADDING_PIXELS # parameters section - ck_recursive = ttk.Checkbutton(area, text=UI_FORM_RECURSIVE_DIRSCAN) + ck_recursive = ttk.Checkbutton(area, text=UI_FORM_RECURSIVEDIRSCAN) l_monitored = ttk.Label(area, text=UI_FORM_MONITOREDFSITEMS_SC) # build a scrolled frame for the treeview sftv_monitored = ttk.Frame(area) @@ -65,7 +65,7 @@ def __init__(self, conditions_available, item=None): b_delEntry = ttk.Button( area, text=UI_DEL, width=BUTTON_STANDARD_WIDTH, command=self.del_fsitem ) - ck_selectDir = ttk.Checkbutton(area, text=UI_FORM_SELECT_DIRECTORY) + ck_selectDir = ttk.Checkbutton(area, text=UI_FORM_SELECTDIRECTORY) # arrange top items in the grid ck_recursive.grid( diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index 91ba2c4..c9b61cb 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -204,6 +204,7 @@ UI_FORM_WMI_QUERY_SC = "WMI Query (WQL):" UI_FORM_WMI_RESULT_CHECKS_SC = "Result checks:" UI_FORM_WMI_NAMESPACE_SC = "WMI Namespace:" +UI_FORM_MCRT_ACTIVATINGCONDS_SC = "Activating conditions:" UI_FORM_ITEMTYPE_SC = "Choose item type:" UI_FORM_ITEMSUBTYPES_SC = "Available items:" @@ -229,9 +230,10 @@ UI_FORM_TIMESPECS = "Time specifications" UI_FORM_EXPECTRESULTS = "Expected results" UI_FORM_MATCHALLRESULTS = "Match ALL results" -UI_FORM_RECURSIVE_DIRSCAN = "Recursively scan directories" -UI_FORM_SELECT_DIRECTORY = "Use button to select directories" +UI_FORM_RECURSIVEDIRSCAN = "Recursively scan directories" +UI_FORM_SELECTDIRECTORY = "Use button to select directories" UI_FORM_IGNOREPERSISTSUCCESS = "Ignore persistently successful checks" +UI_FORM_ACTIVATEFURTHERCONDS = "Activate further conditions" UI_FORM_OR = "or" UI_FORM_FILELOCATION_SC = "File:" diff --git a/lib/i18n/strings_de.py b/lib/i18n/strings_de.py index 23d35d4..c56e27c 100644 --- a/lib/i18n/strings_de.py +++ b/lib/i18n/strings_de.py @@ -228,8 +228,8 @@ UI_FORM_TIMESPECS = "Zeitspezifikationen" UI_FORM_EXPECTRESULTS = "Erwartete Ergebnisse" UI_FORM_MATCHALLRESULTS = "Alle Ergebnisse übereinstimmen" -UI_FORM_RECURSIVE_DIRSCAN = "Verzeichnisse rekursiv scannen" -UI_FORM_SELECT_DIRECTORY = "Schaltfläche verwenden, um Verzeichnisse auszuwählen" +UI_FORM_RECURSIVEDIRSCAN = "Verzeichnisse rekursiv scannen" +UI_FORM_SELECTDIRECTORY = "Schaltfläche verwenden, um Verzeichnisse auszuwählen" UI_FORM_IGNOREPERSISTSUCCESS = "Anhaltend erfolgreiche Ergebnisse übergehen" UI_FORM_OR = "oder" diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index 0c03328..014636f 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -228,8 +228,8 @@ UI_FORM_TIMESPECS = "Time specifications" UI_FORM_EXPECTRESULTS = "Expected results" UI_FORM_MATCHALLRESULTS = "Match ALL results" -UI_FORM_RECURSIVE_DIRSCAN = "Recursively scan directories" -UI_FORM_SELECT_DIRECTORY = "Use button to select directories" +UI_FORM_RECURSIVEDIRSCAN = "Recursively scan directories" +UI_FORM_SELECTDIRECTORY = "Use button to select directories" UI_FORM_IGNOREPERSISTSUCCESS = "Ignore persistently successful checks" UI_FORM_OR = "or" diff --git a/lib/i18n/strings_fr.py b/lib/i18n/strings_fr.py index d4f80b5..f2a66e1 100644 --- a/lib/i18n/strings_fr.py +++ b/lib/i18n/strings_fr.py @@ -228,8 +228,8 @@ UI_FORM_TIMESPECS = "Spécifications de temps" UI_FORM_EXPECTRESULTS = "Résultats attendus" UI_FORM_MATCHALLRESULTS = "Faites correspondre tous les résultats" -UI_FORM_RECURSIVE_DIRSCAN = "Contrôler les répertoires récursivement" -UI_FORM_SELECT_DIRECTORY = "Utiliser le bouton pour sélectionner un répertoire" +UI_FORM_RECURSIVEDIRSCAN = "Contrôler les répertoires récursivement" +UI_FORM_SELECTDIRECTORY = "Utiliser le bouton pour sélectionner un répertoire" UI_FORM_IGNOREPERSISTSUCCESS = "Ignorer les cas de succès persistent" UI_FORM_OR = "ou" diff --git a/lib/i18n/strings_it.py b/lib/i18n/strings_it.py index 118d4de..26953e7 100644 --- a/lib/i18n/strings_it.py +++ b/lib/i18n/strings_it.py @@ -228,8 +228,8 @@ UI_FORM_TIMESPECS = "Specifiche temporali" UI_FORM_EXPECTRESULTS = "Risultati attesi" UI_FORM_MATCHALLRESULTS = "Verifica tutti i risultati" -UI_FORM_RECURSIVE_DIRSCAN = "Scansione ricorsiva directory" -UI_FORM_SELECT_DIRECTORY = "Usa il bottone per selezionare la directory" +UI_FORM_RECURSIVEDIRSCAN = "Scansione ricorsiva directory" +UI_FORM_SELECTDIRECTORY = "Usa il bottone per selezionare la directory" UI_FORM_IGNOREPERSISTSUCCESS = "Ignora successi persistenti" UI_FORM_OR = "o" diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index e3032bf..939b097 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -43,6 +43,7 @@ import tkinter as tk import ttkbootstrap as ttk +import ttkbootstrap.constants as ttkc from typing import List, Tuple @@ -58,6 +59,7 @@ get_tempdir, get_luadir, get_private_item_name_prefix, + clean_caption, ) from ..items import cond, task_lua, cond_lua, cond_interval @@ -262,6 +264,7 @@ def mcrt_install_lib(): def mcrt_initializer() -> task_lua.LuaScriptTask: return _mcrt_InitializationTask + def mcrt_initializer_name() -> str: return _TASK_INITIALIZER @@ -279,6 +282,7 @@ def mcrt_initializer_name() -> str: def mcrt_updater() -> task_lua.LuaScriptTask: return _mcrt_UpdateTask + def mcrt_updater_name() -> str: return _TASK_UPDATER @@ -301,6 +305,7 @@ def mcrt_updater_name() -> str: def mcrt_initial_condition() -> cond_interval.IntervalCondition: return _mcrt_InitializationCond + def mcrt_initial_condition_name() -> str: return _COND_INITIALIZER @@ -319,6 +324,10 @@ class ConfluenceCondition(cond_lua.LuaScriptCondition): available = True def __init__(self, t: items.Table | None = None): + # first initialize the base class + cond_lua.LuaScriptCondition.__init__(self, t) + + # then set type (same as base), subtype and human readable name self.type = self.item_type self.subtype = self.item_subtype self.hrtype = self.item_hrtype @@ -367,48 +376,134 @@ class form_ConfluenceCondition(form_Condition): # note that the available conditions should be filtered, the provided # names must correspond to conditions that activate confluence: this # module provides a helper to distinguish them from others - def __init__(self, tasks_available, conds_available, item=None): + def __init__(self, tasks_available, conds_available=None, item=None): # check that item is the expected one for safety, build one by default if item: assert isinstance(item, ConfluenceCondition) else: item = ConfluenceCondition() - super().__init__(UI_TITLE_MCRTCOND, tasks_available, item) - - # create a specific frame for the contents - area = ttk.Frame(super().contents) - area.grid(row=0, column=0, sticky=tk.NSEW) - PAD = WIDGET_PADDING_PIXELS - # copy the list of conditions, since we have to manipulate it: remove - # the conditions that are already in the activating lists and sort - # the left ones for readability in the combo box; note that the items + # copy the list of conditions, since we want to sort it; the items # that are not present in the available list are probably leftovers # from a manual edit of the configuration file, so they should be # discarded if found - self._conds_available = conds_available.copy() + if conds_available is None: + self._conds_available = list() + else: + self._conds_available = conds_available.copy() self._conds_activating = item.tags.get("mcrt_confluent_conditions") or list() for cond in self._conds_activating.copy(): - if cond in self._conds_available: - self._conds_available.remove(cond) - else: + if cond not in self._conds_available: self._conds_activating.remove(cond) self._conds_available.sort() + super().__init__(UI_TITLE_MCRTCOND, tasks_available, item) - # ... + # create a specific frame for the contents + area = ttk.Frame(super().contents) + area.grid(row=0, column=0, sticky=tk.NSEW) + PAD = WIDGET_PADDING_PIXELS + + l_activatingConds = ttk.Label(area, text=UI_FORM_MCRT_ACTIVATINGCONDS_SC) + sftv_activatingConds = ttk.Frame(area) + tv_activatingConds = ttk.Treeview( + sftv_activatingConds, + columns=("seq", "conditions"), + show="", + displaycolumns=(1,), + height=5, + bootstyle=ttkc.SECONDARY, + ) + sb_activatingConds = ttk.Scrollbar( + sftv_activatingConds, orient=tk.VERTICAL, command=tv_activatingConds.yview + ) + tv_activatingConds.configure(yscrollcommand=sb_activatingConds.set) + tv_activatingConds.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + sb_activatingConds.pack(side=tk.RIGHT, fill=tk.Y) + + sf_condChoose = ttk.Frame(area) + l_chooseCond = ttk.Label(sf_condChoose, text=UI_FORM_COND_SC) + cb_chooseCond = ttk.Combobox( + sf_condChoose, values=self._conds_available, state="readonly" + ) + b_addCond = ttk.Button( + sf_condChoose, + text=UI_ADD, + width=BUTTON_STANDARD_WIDTH, + command=self.add_cond, + ) + b_delCond = ttk.Button( + sf_condChoose, + text=UI_DEL, + width=BUTTON_STANDARD_WIDTH, + command=self.del_cond, + ) + + # choose condition section: arrange items + l_chooseCond.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) + cb_chooseCond.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) + b_addCond.grid(row=0, column=2, sticky=tk.E, padx=PAD, pady=PAD) + b_delCond.grid(row=0, column=3, sticky=tk.E, padx=PAD, pady=PAD) + + # notebook area: arrange items + l_activatingConds.grid(row=0, column=0, sticky=tk.EW, padx=PAD, pady=PAD) + sftv_activatingConds.grid(row=1, column=0, sticky=tk.NSEW, padx=PAD, pady=PAD) + sf_condChoose.grid(row=2, column=0, sticky=tk.EW, padx=PAD, pady=PAD) + + # expand appropriate sections + sf_condChoose.columnconfigure(1, weight=1) + area.rowconfigure(1, weight=1) + area.columnconfigure(0, weight=1) + + # bind data to widgets + self.data_bind( + "cond_selection", + tv_activatingConds, + check=lambda _: len(self._conds_activating) > 1, + ) + self.data_bind("choose_cond", cb_chooseCond, TYPE_STRING) + + # add a check that the chosen conditions should be more than one + self.add_check_caption( + "cond_selection", clean_caption(UI_FORM_MCRT_ACTIVATINGCONDS_SC) + ) + + # propagate widgets that need to be accessed + self._tv_activatingConds = tv_activatingConds # always update the form at the end of initialization self._updateform() + def add_cond(self): + # only add a condition if not present, ignore otherwise + elem = self.data_get("choose_cond") + if elem and elem not in self._conds_activating: + self._conds_activating.append(elem) + self._updatedata() + self._updateform() + + def del_cond(self): + elem = self.data_get("cond_selection") + if elem: + idx = int(elem[0]) + del self._conds_activating[idx] + self._updatedata() + self._updateform() + # update the form with the specific parameters (usually in the `tags`) def _updateform(self) -> None: - self.data_set("parameter1", self._item.tags.get("parameter1")) # type: ignore + self._tv_activatingConds.delete(*self._tv_tasks.get_children()) + idx = 0 + for cond in self._conds_activating: + self._tv_activatingConds.insert( + "", iid="%s-%s" % (idx, cond), values=(idx, cond), index=tk.END + ) + idx += 1 return super()._updateform() # update the item from the form elements (usually update `tags`) def _updatedata(self) -> None: - self._item.tags["parameter1"] = self.data_get("parameter1") # type: ignore - self._item.updateitem() # type: ignore + assert isinstance(self._item, ConfluenceCondition) + self._item.tags["mcrt_confluent_conditions"] = self._conds_activating # type: ignore return super()._updatedata() @@ -418,6 +513,7 @@ def is_mcrt_confluent_cond(c: cond.Condition) -> bool: return c.tasks[0] == _TASK_UPDATER return False + # check whether a condition implements confluence def is_mcrt_confluence_cond(c: cond.Condition) -> bool: return isinstance(c, ConfluenceCondition) From b85ff6774436840c982456ffec6da2207459d3c3 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 3 Mar 2026 19:46:40 +0100 Subject: [PATCH 12/57] temp: set confluent conditions for specific form --- lib/forms/cfgform.py | 17 +++++++++++++++++ lib/internal/multi_conds_run_task.py | 17 +++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index 03756ac..ddad2fe 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -16,6 +16,7 @@ from ..repocfg import AppConfig from ..utility import get_configfile, is_private_item_name from ..items.item import ALL_AVAILABLE_ITEMS_D +from ..internal.multi_conds_run_task import is_mcrt_confluent_cond, form_ConfluenceCondition from ..configurator.reader import read_whenever_config from ..configurator.writer import write_whenever_config @@ -473,6 +474,14 @@ def edit(self) -> None: x for x in self._tasks.keys() if not is_private_item_name(x) ) e = fform(available_tasks, self._conditions[item_name]) + # this is a special case, which has an extra parameter + if isinstance(e, form_ConfluenceCondition): + confluent_conds = list( + x for x in self._conditions.keys() + if not is_private_item_name(x) + and is_mcrt_confluent_cond(self._conditions[x]) + ) + e.set_available_conditions(confluent_conds) if e is not None: new_item = e.run() if new_item: @@ -531,6 +540,14 @@ def new(self) -> None: x for x in self._tasks.keys() if not is_private_item_name(x) ] form = form_class(list(available_tasks)) + # this is a special case, which has an extra parameter + if isinstance(form, form_ConfluenceCondition): + confluent_conds = list( + x for x in self._conditions.keys() + if not is_private_item_name(x) + and is_mcrt_confluent_cond(self._conditions[x]) + ) + form.set_available_conditions(confluent_conds) # note that, since providing a suitable event based # condition is mandatory for an event, the form will # refuse to create a new event diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 939b097..586c161 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -376,21 +376,14 @@ class form_ConfluenceCondition(form_Condition): # note that the available conditions should be filtered, the provided # names must correspond to conditions that activate confluence: this # module provides a helper to distinguish them from others - def __init__(self, tasks_available, conds_available=None, item=None): + def __init__(self, tasks_available, item=None): # check that item is the expected one for safety, build one by default if item: assert isinstance(item, ConfluenceCondition) else: item = ConfluenceCondition() - # copy the list of conditions, since we want to sort it; the items - # that are not present in the available list are probably leftovers - # from a manual edit of the configuration file, so they should be - # discarded if found - if conds_available is None: - self._conds_available = list() - else: - self._conds_available = conds_available.copy() + self._conds_available = list() self._conds_activating = item.tags.get("mcrt_confluent_conditions") or list() for cond in self._conds_activating.copy(): if cond not in self._conds_available: @@ -491,7 +484,7 @@ def del_cond(self): # update the form with the specific parameters (usually in the `tags`) def _updateform(self) -> None: - self._tv_activatingConds.delete(*self._tv_tasks.get_children()) + self._tv_activatingConds.delete(*self._tv_activatingConds.get_children()) idx = 0 for cond in self._conds_activating: self._tv_activatingConds.insert( @@ -506,6 +499,10 @@ def _updatedata(self) -> None: self._item.tags["mcrt_confluent_conditions"] = self._conds_activating # type: ignore return super()._updatedata() + # set the list of available conditions, that implement confluence + def set_available_conditions(self, conds: list[str]) -> None: + self._conds_available = conds.copy() + # check whether a condition is confluent def is_mcrt_confluent_cond(c: cond.Condition) -> bool: From d3366a703e0b373017027669f65a9816e39088d0 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 4 Mar 2026 19:05:22 +0100 Subject: [PATCH 13/57] temp: implement saving of MCRT configuration --- lib/forms/cfgform.py | 59 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index ddad2fe..c530fb4 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -16,7 +16,17 @@ from ..repocfg import AppConfig from ..utility import get_configfile, is_private_item_name from ..items.item import ALL_AVAILABLE_ITEMS_D -from ..internal.multi_conds_run_task import is_mcrt_confluent_cond, form_ConfluenceCondition +from ..internal.multi_conds_run_task import ( + is_mcrt_confluent_cond, + mcrt_initial_condition, + mcrt_initial_condition_name, + mcrt_initializer, + mcrt_initializer_name, + mcrt_updater, + mcrt_updater_name, + ConfluenceCondition, + form_ConfluenceCondition, +) from ..configurator.reader import read_whenever_config from ..configurator.writer import write_whenever_config @@ -368,6 +378,34 @@ def _save_config(self, fn) -> None: del self._conditions[name] if name in self._events.keys(): del self._events[name] + + # check whether there are MCRT conditions (both confluence and + # confluent) and, if so, create the support items, that is, the initial + # task and condition, and the updater task, otherwise remove them if + # present so that no time and resources are wasted for no reason + n_mcrt_initializer = mcrt_initializer_name() + n_mcrt_initial_cond = mcrt_initial_condition_name() + n_mcrt_updater = mcrt_updater_name() + mcrt_active = False + for _, cond in self._conditions: + if isinstance(cond, ConfluenceCondition) or is_mcrt_confluent_cond(cond): + mcrt_active = True + break + if mcrt_active: + if n_mcrt_updater not in self._tasks.keys(): + self._tasks[n_mcrt_updater] = mcrt_updater() + if n_mcrt_initializer not in self._tasks.keys(): + self._tasks[n_mcrt_initializer] = mcrt_initializer() + if n_mcrt_initial_cond not in self._conditions.keys(): + self._conditions[n_mcrt_initial_cond] = mcrt_initial_condition() + else: + if n_mcrt_updater in self._tasks.keys(): + del self._tasks[n_mcrt_updater] + if n_mcrt_initializer in self._tasks.keys(): + del self._tasks[n_mcrt_initializer] + if n_mcrt_initial_cond in self._conditions.keys(): + del self._conditions[n_mcrt_initial_cond] + # ... # finally write the configuration fle @@ -418,7 +456,9 @@ def save(self) -> None: fn = self.data_get("config_file") if fn and self._changed: if os.path.exists(fn): - if self.messagebox.askyesno(UI_POPUP_T_CONFIRM, UI_POPUP_OVERWRITEFILE_Q): + if self.messagebox.askyesno( + UI_POPUP_T_CONFIRM, UI_POPUP_OVERWRITEFILE_Q + ): self._save_config(fn) self._changed = False else: @@ -432,7 +472,7 @@ def edit(self) -> None: if selection is None: return - item_name, _, item_signature = selection # type: ignore + item_name, _, item_signature = selection # type: ignore item_type = item_signature.split(":", 1)[0] # task items @@ -477,7 +517,8 @@ def edit(self) -> None: # this is a special case, which has an extra parameter if isinstance(e, form_ConfluenceCondition): confluent_conds = list( - x for x in self._conditions.keys() + x + for x in self._conditions.keys() if not is_private_item_name(x) and is_mcrt_confluent_cond(self._conditions[x]) ) @@ -504,8 +545,7 @@ def edit(self) -> None: event_conds = list( x for x in self._conditions - if self._conditions[x].type == "event" - and not is_private_item_name(x) + if self._conditions[x].type == "event" and not is_private_item_name(x) ) available_item = ALL_AVAILABLE_ITEMS_D.get(item_signature) assert available_item is not None @@ -543,7 +583,8 @@ def new(self) -> None: # this is a special case, which has an extra parameter if isinstance(form, form_ConfluenceCondition): confluent_conds = list( - x for x in self._conditions.keys() + x + for x in self._conditions.keys() if not is_private_item_name(x) and is_mcrt_confluent_cond(self._conditions[x]) ) @@ -600,7 +641,9 @@ def reload(self) -> None: # has changed the user is asked whether or not he wants to discard it def exit_close(self) -> None: if self._changed: - if self.messagebox.askokcancel(UI_POPUP_T_CONFIRM, UI_POPUP_DISCARDCONFIG_Q): + if self.messagebox.askokcancel( + UI_POPUP_T_CONFIRM, UI_POPUP_DISCARDCONFIG_Q + ): return super().exit_close() else: return super().exit_close() From 0060f960ac338b833286963a95403a711b0eecc5 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 5 Mar 2026 00:45:50 +0100 Subject: [PATCH 14/57] temp: refactoring and fixes Part of the refactoring has been introduced in order to avoid circular imports and other bugs, with the side effects of less polluted namespaces --- lib/forms/cfgform.py | 36 ++++------ lib/forms/cond.py | 8 +-- lib/forms/cond_lua.py | 26 +------ lib/forms/task_lua.py | 23 +----- lib/internal/multi_conds_run_task.py | 101 +++++++++++++-------------- lib/utility.py | 23 +++++- when/when.py | 4 +- 7 files changed, 92 insertions(+), 129 deletions(-) diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index c530fb4..d9f3921 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -16,17 +16,7 @@ from ..repocfg import AppConfig from ..utility import get_configfile, is_private_item_name from ..items.item import ALL_AVAILABLE_ITEMS_D -from ..internal.multi_conds_run_task import ( - is_mcrt_confluent_cond, - mcrt_initial_condition, - mcrt_initial_condition_name, - mcrt_initializer, - mcrt_initializer_name, - mcrt_updater, - mcrt_updater_name, - ConfluenceCondition, - form_ConfluenceCondition, -) +from ..internal import multi_conds_run_task as mcrt from ..configurator.reader import read_whenever_config from ..configurator.writer import write_whenever_config @@ -383,21 +373,21 @@ def _save_config(self, fn) -> None: # confluent) and, if so, create the support items, that is, the initial # task and condition, and the updater task, otherwise remove them if # present so that no time and resources are wasted for no reason - n_mcrt_initializer = mcrt_initializer_name() - n_mcrt_initial_cond = mcrt_initial_condition_name() - n_mcrt_updater = mcrt_updater_name() + n_mcrt_initializer = mcrt.initializer_name() + n_mcrt_initial_cond = mcrt.initial_condition_name() + n_mcrt_updater = mcrt.updater_name() mcrt_active = False - for _, cond in self._conditions: - if isinstance(cond, ConfluenceCondition) or is_mcrt_confluent_cond(cond): + for cond in self._conditions.values(): + if isinstance(cond, mcrt.ConfluenceCondition) or mcrt.is_confluent_cond(cond): mcrt_active = True break if mcrt_active: if n_mcrt_updater not in self._tasks.keys(): - self._tasks[n_mcrt_updater] = mcrt_updater() + self._tasks[n_mcrt_updater] = mcrt.updater() if n_mcrt_initializer not in self._tasks.keys(): - self._tasks[n_mcrt_initializer] = mcrt_initializer() + self._tasks[n_mcrt_initializer] = mcrt.initializer() if n_mcrt_initial_cond not in self._conditions.keys(): - self._conditions[n_mcrt_initial_cond] = mcrt_initial_condition() + self._conditions[n_mcrt_initial_cond] = mcrt.initial_condition() else: if n_mcrt_updater in self._tasks.keys(): del self._tasks[n_mcrt_updater] @@ -515,12 +505,12 @@ def edit(self) -> None: ) e = fform(available_tasks, self._conditions[item_name]) # this is a special case, which has an extra parameter - if isinstance(e, form_ConfluenceCondition): + if isinstance(e, mcrt.form_ConfluenceCondition): confluent_conds = list( x for x in self._conditions.keys() if not is_private_item_name(x) - and is_mcrt_confluent_cond(self._conditions[x]) + and mcrt.is_confluent_cond(self._conditions[x]) ) e.set_available_conditions(confluent_conds) if e is not None: @@ -581,12 +571,12 @@ def new(self) -> None: ] form = form_class(list(available_tasks)) # this is a special case, which has an extra parameter - if isinstance(form, form_ConfluenceCondition): + if isinstance(form, mcrt.form_ConfluenceCondition): confluent_conds = list( x for x in self._conditions.keys() if not is_private_item_name(x) - and is_mcrt_confluent_cond(self._conditions[x]) + and mcrt.is_confluent_cond(self._conditions[x]) ) form.set_available_conditions(confluent_conds) # note that, since providing a suitable event based diff --git a/lib/forms/cond.py b/lib/forms/cond.py index 52d7190..a41dda0 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -12,7 +12,7 @@ from ..items.cond import Condition from ..utility import is_valid_item_name, clean_caption -from ..internal.multi_conds_run_task import mcrt_updater_name +from ..internal import multi_conds_run_task as mcrt # add a negated disabled to tk dictionary (please forgive me) @@ -284,10 +284,10 @@ def _mcrt_confluent(self) -> None: # same consideration as above; this function also disables all task # related form widgets when the condition is set to be confluent not_rec = not self.data_get("@recurring") or False - mcrt = not self.data_get("@confluent") or False + confluent = not self.data_get("@confluent") or False # retrieve the name of the MCRT updater task - mcrt_updater = mcrt_updater_name() - if mcrt: + mcrt_updater = mcrt.updater_name() + if confluent: self.data_set("@name", "") self.data_set("@control_flow", "break_none") self.data_set("@execute_sequence", True) diff --git a/lib/forms/cond_lua.py b/lib/forms/cond_lua.py index 6671b9d..931a126 100644 --- a/lib/forms/cond_lua.py +++ b/lib/forms/cond_lua.py @@ -16,6 +16,7 @@ guess_typed_value, get_editor_theme, get_luadir, + get_lua_path, get_lua_initscript, ) @@ -37,30 +38,7 @@ def __init__(self, tasks_available, item=None): super().__init__(UI_TITLE_LUACOND, tasks_available, item) assert isinstance(self._item, LuaScriptCondition) - # update the LUA_PATH for user scripts: note that it seems that - # LUA_PATH as a global variable to search modulesseems to have been - # replaced by the `package.path` table element,thus the need to use - # the initialization script; also, the default Lua search path is - # not added to the embedded Lua path, in order to avoid to search - # for unsupported or possibly binary modules - luabase = get_luadir() - ps = os.path.sep - lua_path = ( - ";".join( - [ - "?", - "?.lua", - f"{luabase}{ps}?", - f"{luabase}{ps}?.lua", - f"{luabase}{ps}?{ps}?", - f"{luabase}{ps}?{ps}?.lua", - ] - ) - + ";;" - ) - self._item.variables_to_set = { - "LUA_PATH": lua_path, - } + self._item.variables_to_set = { "LUA_PATH": get_lua_path() } # legacy self._item.init_script_path = get_lua_initscript() # form data diff --git a/lib/forms/task_lua.py b/lib/forms/task_lua.py index d439606..5c231bd 100644 --- a/lib/forms/task_lua.py +++ b/lib/forms/task_lua.py @@ -16,6 +16,7 @@ guess_typed_value, get_editor_theme, get_luadir, + get_lua_path, get_lua_initscript, ) @@ -41,27 +42,7 @@ def __init__(self, item=None): # form data self._results = [] - # update the LUA_PATH for user scripts: note that it seems that - # LUA_PATH as a global variable to search modulesseems to have been - # replaced by the `package.path` table element,thus the need to use - # the initialization script; also, the default Lua search path is - # not added to the embedded Lua path, in order to avoid to search - # for unsupported or possibly binary modules - luabase = get_luadir() - ps = os.path.sep - lua_path = ";".join( - [ - "?", - "?.lua", - f"{luabase}{ps}?", - f"{luabase}{ps}?.lua", - f"{luabase}{ps}?{ps}?", - f"{luabase}{ps}?{ps}?.lua", - ] - ) - self._item.variables_to_set = { - "LUA_PATH": lua_path, - } + self._item.variables_to_set = { "LUA_PATH": get_lua_path() } # legacy self._item.init_script_path = get_lua_initscript() # build the UI: build widgets, arrange them in the box, bind data diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 586c161..8707a43 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -47,17 +47,16 @@ from typing import List, Tuple -from ..utility import check_not_none, append_not_none - from ..forms.ui import * # since a condition is defined, the base form is the one for conditions from ..forms.cond import form_Condition - from ..utility import ( get_tempdir, get_luadir, + get_lua_initscript, + get_lua_path, get_private_item_name_prefix, clean_caption, ) @@ -234,7 +233,7 @@ def _mcrt_lock_file(): # utility to install the Lua library: it also reserves the library file name # so that it is not overwritten by the user in case he decides to install # a Lua library of choice -def mcrt_install_lib(): +def install_lib(): s = os.path.join(get_luadir(), _MCRT_LIBRARY) if not os.path.exists(s): lua_library = _LUA_LIBRARY.format( @@ -252,38 +251,35 @@ def mcrt_install_lib(): # this to be performed by the Lua interpreter instead of the GUI application, # is that in this way the MCRT system is initialized even when used with # another frontend -_mcrt_InitializationTask = task_lua.LuaScriptTask() -_mcrt_InitializationTask.name = _TASK_INITIALIZER -_mcrt_InitializationTask.script = f"""\ -local mcrt = require "{_MCRT_LIBRARY}" -mcrt.initialize() -""" - - -# return this item and its name -def mcrt_initializer() -> task_lua.LuaScriptTask: - return _mcrt_InitializationTask - - -def mcrt_initializer_name() -> str: +def initializer() -> task_lua.LuaScriptTask: + task = task_lua.LuaScriptTask() + task.name = _TASK_INITIALIZER + task.variables_to_set = { "LUA_PATH": get_lua_path() } + task.init_script_path = get_lua_initscript() + task.script = f"""\ + local mcrt = require "{_MCRT_LIBRARY}" + mcrt.initialize() + """ + return task + +def initializer_name() -> str: return _TASK_INITIALIZER # 2. updater: just adds the verified condition to the persistence file -_mcrt_UpdateTask = task_lua.LuaScriptTask() -_mcrt_UpdateTask.name = _TASK_UPDATER -_mcrt_UpdateTask.script = f"""\ -local mcrt = require "{_MCRT_LIBRARY}" -mcrt.set_condition_verified(whenever_condition) -""" - - -# return this item and its name -def mcrt_updater() -> task_lua.LuaScriptTask: - return _mcrt_UpdateTask - - -def mcrt_updater_name() -> str: +def updater() -> task_lua.LuaScriptTask: + task = task_lua.LuaScriptTask() + task.name = _TASK_UPDATER + task.variables_to_set = { "LUA_PATH": get_lua_path() } + task.init_script_path = get_lua_initscript() + task.script = f"""\ + local mcrt = require "{_MCRT_LIBRARY}" + mcrt.set_condition_verified(whenever_condition) + """ + return task + + +def updater_name() -> str: return _TASK_UPDATER @@ -295,18 +291,15 @@ def mcrt_updater_name() -> str: # definition form only accepts values strictly above zero, while whenever # also accepts a zero duration in the configuration (which is in fact the # way to create a condition that is verified at startup) -_mcrt_InitializationCond = cond_interval.IntervalCondition() -_mcrt_InitializationCond.name = _COND_INITIALIZER -_mcrt_InitializationCond.interval_seconds = 0 # that is, at the first tick -_mcrt_InitializationCond.tasks = [_mcrt_InitializationTask.name] - - -# return this item and its name -def mcrt_initial_condition() -> cond_interval.IntervalCondition: - return _mcrt_InitializationCond +def initial_condition() -> cond_interval.IntervalCondition: + cond = cond_interval.IntervalCondition() + cond.name = _COND_INITIALIZER + cond.interval_seconds = 0 # that is, at the first tick + cond.tasks = [_TASK_INITIALIZER] + return cond -def mcrt_initial_condition_name() -> str: +def initial_condition_name() -> str: return _COND_INITIALIZER @@ -344,6 +337,8 @@ def __init__(self, t: items.Table | None = None): def updateitem(self): confluent_conditions = self.tags.get("mcrt_confluent_conditions", list()) + self.variables_to_set = { "LUA_PATH": get_lua_path() } + self.init_script_path = get_lua_initscript() self.script = _MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE.replace( "[[COND_LIST]]", '{"%s"}' % '", "'.join(confluent_conditions), @@ -505,28 +500,28 @@ def set_available_conditions(self, conds: list[str]) -> None: # check whether a condition is confluent -def is_mcrt_confluent_cond(c: cond.Condition) -> bool: +def is_confluent_cond(c: cond.Condition) -> bool: if c.tasks is not None and len(c.tasks) == 1: return c.tasks[0] == _TASK_UPDATER return False # check whether a condition implements confluence -def is_mcrt_confluence_cond(c: cond.Condition) -> bool: +def is_confluence_cond(c: cond.Condition) -> bool: return isinstance(c, ConfluenceCondition) # only return interesting symbols __all__ = [ - "mcrt_initial_condition", - "mcrt_initial_condition_name", - "mcrt_initializer", - "mcrt_initializer_name", - "mcrt_updater", - "mcrt_updater_name", - "is_mcrt_confluent_cond", - "is_mcrt_confluence_cond", - "mcrt_install_lib", + "initial_condition", + "initial_condition_name", + "initializer", + "initializer_name", + "updater", + "updater_name", + "is_confluent_cond", + "is_confluence_cond", + "install_lib", "ConfluenceCondition", ] diff --git a/lib/utility.py b/lib/utility.py index e39c337..624e4bf 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -278,9 +278,9 @@ def get_tempdir() -> str: return tempdir -# determine lua library directory and ensure that it exists +# determine Lua library directory and ensure that it exists def get_luadir() -> str: - configdir: str = AppConfig.get("APPDATA") # type: ignore + configdir: str = AppConfig.get("APPDATA") # type: ignore if is_windows(): subdir = "Lua" else: @@ -294,6 +294,25 @@ def get_luadir() -> str: return luadir +# construct Lua path +def get_lua_path() -> str: + luabase = get_luadir() + ps = os.path.sep + return ( + ";".join( + [ + "?", + "?.lua", + f"{luabase}{ps}?", + f"{luabase}{ps}?.lua", + f"{luabase}{ps}?{ps}?", + f"{luabase}{ps}?{ps}?.lua", + ] + ) + + ";;" + ) + + # the following cannot be used, because the embedded Lua interpreter cannot # load Lua binary modules due to its safe setup, which disallows it # # determine binary lib extension depending on operating system diff --git a/when/when.py b/when/when.py index c7c6f77..b4842ec 100644 --- a/when/when.py +++ b/when/when.py @@ -45,7 +45,7 @@ from lib.runner.process import Wrapper -from lib.internal.multi_conds_run_task import mcrt_install_lib +from lib.internal import multi_conds_run_task as mcrt # main root window, to be withdrawn @@ -629,7 +629,7 @@ def main() -> None: AppConfig.set("WHENEVER", default_whenever) # other initialization actions - mcrt_install_lib() + mcrt.install_lib() # main parser parser = argparse.ArgumentParser( From dfffe2498e7924c99acde8c7b9fd7eb4011a28f2 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 5 Mar 2026 10:41:49 +0100 Subject: [PATCH 15/57] temp: update widgets in confluent condition form When editing an existing condition that is confluent, correctly update the form widgets by checking the specific flag, clearing tasks, and disabling task list related controls --- lib/forms/cond.py | 21 +++++++++++++-------- lib/i18n/strings_base.py | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index a41dda0..a5c26da 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -280,11 +280,13 @@ def _check_recurring(self) -> None: else: self._max_retries.config(state=tk.NORMAL) - def _mcrt_confluent(self) -> None: + def _mcrt_confluent(self, force=False) -> None: # same consideration as above; this function also disables all task # related form widgets when the condition is set to be confluent - not_rec = not self.data_get("@recurring") or False - confluent = not self.data_get("@confluent") or False + if force: + confluent = True + else: + confluent = not self.data_get("@confluent") or False # retrieve the name of the MCRT updater task mcrt_updater = mcrt.updater_name() if confluent: @@ -312,10 +314,6 @@ def _mcrt_confluent(self) -> None: if tk.NOT_DISABLED not in spec: # type:ignore spec.append(tk.NOT_DISABLED) # type:ignore elem.state(spec) - if not_rec: - self._max_retries.config(state=tk.DISABLED) - else: - self._max_retries.config(state=tk.NORMAL) if mcrt_updater in self._tasks: self._tasks.remove(mcrt_updater) @@ -404,7 +402,14 @@ def set_item(self, item: Condition) -> None: except ValueError: self._item = item # item was newly created: use it assert isinstance(self._item.tasks, list) - self._tasks = self._item.tasks.copy() + # check if this is a confluent condition and set widgets accordingly + if mcrt.is_confluent_cond(item): + self._tasks = list() + self.data_set("@confluent", True) + self._mcrt_confluent(True) + else: + self._tasks = self._item.tasks.copy() + self.data_set("@confluent", False) def reset_item(self) -> None: self._item = None diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index c9b61cb..6a629ab 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -260,7 +260,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: Time Condition Editor" UI_TITLE_DBUSCOND = f"{UI_APP}: DBus Method Condition Editor" UI_TITLE_WMICOND = f"{UI_APP}: WMI Query Condition Editor" -UI_TITLE_MCRTCOND = f"{UI_APP}: Condition Activated by Other Conditions" +UI_TITLE_MCRTCOND = f"{UI_APP}: Condition Activated by Other Conditions Editor" UI_TITLE_DBUSEVENT = f"{UI_APP}: DBus Signal Event Editor" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: Filesystem Monitoring Event Editor" From fb1e89039ffa6b7b109cd212acf59448facb1312 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 5 Mar 2026 12:10:52 +0100 Subject: [PATCH 16/57] temp: remove redundant underscore in MCRT names --- lib/internal/multi_conds_run_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 8707a43..9bf0877 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -203,7 +203,7 @@ # this is the prefix for all of our item names -_ITEM_PREFIX = get_private_item_name_prefix() + "_MCRT_" +_ITEM_PREFIX = get_private_item_name_prefix() + "MCRT_" # specific names, local to this module _TASK_INITIALIZER = _ITEM_PREFIX + "Initializer" From 88f82063968e9dd6505b1dcd2270dfe2fde6be74 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 6 Mar 2026 16:14:31 +0100 Subject: [PATCH 17/57] temp: partly fix bugs in Lua confluence library Yet others remain to be fixed --- lib/internal/multi_conds_run_task.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 9bf0877..93a88b1 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -68,7 +68,7 @@ # constants _MCRT_PERSIST_FILE = ".mcrt_persist" _MCRT_LOCK_FILE = ".mcrt_persist.lock" -_MCRT_LIBRARY = "_mcrt_lib.lua" +_MCRT_LIBRARY = "_mcrt_lib" _MCRT_EXTRA_DELAY = 15 @@ -165,7 +165,7 @@ __set_lock() local persistent = __read_persistent() if persistent ~= nil then - if !__has_name(cond_name, persistent) then + if not __has_name(cond_name, persistent) then persistent = __add_name(cond_name, persistent) end else @@ -183,7 +183,7 @@ __set_lock() local persistent = __read_persistent() for _, name in ipairs(cond_names) do - if !__has_name(name, persistent) then + if not __has_name(name, persistent) then res = false break end @@ -211,9 +211,9 @@ _COND_INITIALIZER = _ITEM_PREFIX + "Initializer" # the template for the confluence condition Lua script -_MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE = f"""\ -local mcrt = require "{_MCRT_LIBRARY}" -res = mcrt.check_conditions_verified([[COND_LIST]]) +_MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE = f""" + local mcrt = require("{_MCRT_LIBRARY}") + res = mcrt.check_conditions_verified([[COND_LIST]]) """ @@ -242,7 +242,7 @@ def install_lib(): ) with open(s, "w") as f: f.write(lua_library) - install_lua.reserve_lua(_MCRT_LIBRARY) + install_lua.reserve_lua(f"{_MCRT_LIBRARY}.lua") # the following items are the specific ones that implement the confluence @@ -256,8 +256,8 @@ def initializer() -> task_lua.LuaScriptTask: task.name = _TASK_INITIALIZER task.variables_to_set = { "LUA_PATH": get_lua_path() } task.init_script_path = get_lua_initscript() - task.script = f"""\ - local mcrt = require "{_MCRT_LIBRARY}" + task.script = f""" + local mcrt = require("{_MCRT_LIBRARY}") mcrt.initialize() """ return task @@ -273,7 +273,7 @@ def updater() -> task_lua.LuaScriptTask: task.variables_to_set = { "LUA_PATH": get_lua_path() } task.init_script_path = get_lua_initscript() task.script = f"""\ - local mcrt = require "{_MCRT_LIBRARY}" + local mcrt = require("{_MCRT_LIBRARY}") mcrt.set_condition_verified(whenever_condition) """ return task From dd53ae8440383acd67b22c133256dcfdf0f1523c Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 6 Mar 2026 17:45:50 +0100 Subject: [PATCH 18/57] temp: fix confluence Lua library issues --- lib/internal/multi_conds_run_task.py | 62 +++++++++++++++++----------- support/mcrt_lib.lua | 58 +++++++++++++++----------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 93a88b1..46b5c75 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -72,37 +72,43 @@ _MCRT_EXTRA_DELAY = 15 -_LUA_LIBRARY = """ +_LUA_LIBRARY = """\ -- mcrt: multiple conditions to run a task -- NOTE: internals have a double underscore and will not be directly -- used in the scripts that require the library -MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] -MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + +local __MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] +local __MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + + +-- the library itself +local mcrt = {} + -- mangle names -function __mangle_name(name) +local function __mangle_name(name) return ":" .. name .. ":" end -- find whether or not a (mangled) name is present in a string -function __has_name(name, s) - return string.find(s, mcrt_mangle_name(name)) ~= nil +local function __has_name(name, s) + return string.find(s, __mangle_name(name)) ~= nil end -- remove a name from a string -function __rm_name(name, s) +local function __rm_name(name, s) return string.gsub(s, __mangle_name(name), "") end -- add a name to a string -function __add_name(name, s) +local function __add_name(name, s) return s .. __mangle_name(name) end -- test if a file exists -function __file_exists(file) +local function __file_exists(file) local f = io.open(file, "rb") if f then f:close() end return f ~= nil @@ -111,25 +117,25 @@ -- wait for the lock file to disappear: unfortunately stock Lua has no -- sleep() function, so we do busy wait here hoping that it will never be -- useful and that the lock would last possibly a bunch of usecs at most -function __wait_lock() - while __file_exists(MCRT_LOCK_FILE) do end +local function __wait_lock() + while __file_exists(__MCRT_LOCK_FILE) do end end -- set and reset the lock -function __set_lock() - local f = io.open(MCRT_LOCK_FILE, "wb") +local function __set_lock() + local f = io.open(__MCRT_LOCK_FILE, "wb") f:close() end -function __reset_lock() - if __file_exists(MCRT_LOCK_FILE) then - os.remove(MCRT_LOCK_FILE) +local function __reset_lock() + if __file_exists(__MCRT_LOCK_FILE) then + os.remove(__MCRT_LOCK_FILE) end end -- read the contents of the persistent file, return nil if read failed -function __read_persistent() - local f = io.open(MCRT_PERSIST_FILE, "r") +local function __read_persistent() + local f = io.open(__MCRT_PERSIST_FILE, "r") local s = nil if f then s = f:read("*all") @@ -140,8 +146,8 @@ -- write the specified contents to the persistent file, truncate the file -- on null content, which is useful for initialization -function __write_persistent(content) - local f = io.open(MCRT_PERSIST_FILE, "w") +local function __write_persistent(content) + local f = io.open(__MCRT_PERSIST_FILE, "w") if content ~= nil then f:write(content) end @@ -152,7 +158,7 @@ -- actual library functions -- initialize the persistent file -function initialize() +function mcrt.initialize() __wait_lock() __set_lock() __write_persistent(nil) @@ -160,7 +166,7 @@ end -- set the condition bearing the provided name to verified -function set_condition_verified(cond_name) +function mcrt.set_condition_verified(cond_name) __wait_lock() __set_lock() local persistent = __read_persistent() @@ -177,7 +183,7 @@ -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false -function check_conditions_verified(cond_names) +function mcrt.check_conditions_verified(cond_names) local res = true __wait_lock() __set_lock() @@ -198,6 +204,10 @@ return res end +-- return the library table +return mcrt + + -- end. """ @@ -234,15 +244,17 @@ def _mcrt_lock_file(): # so that it is not overwritten by the user in case he decides to install # a Lua library of choice def install_lib(): - s = os.path.join(get_luadir(), _MCRT_LIBRARY) + libfilename = f"{_MCRT_LIBRARY}.lua" + s = os.path.join(get_luadir(), libfilename) if not os.path.exists(s): lua_library = _LUA_LIBRARY.format( + "{}", # this replaces the brackets! MCRT_LOCK_FILE=_mcrt_lock_file(), MCRT_PERSIST_FILE=_mcrt_persist_file(), ) with open(s, "w") as f: f.write(lua_library) - install_lua.reserve_lua(f"{_MCRT_LIBRARY}.lua") + install_lua.reserve_lua(libfilename) # the following items are the specific ones that implement the confluence diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index 3d55639..7a5839c 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -2,32 +2,38 @@ -- NOTE: internals have a double underscore and will not be directly -- used in the scripts that require the library -MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] -MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + +local __MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] +local __MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] + + +-- the library itself +local mcrt = {} + -- mangle names -function __mangle_name(name) +local function __mangle_name(name) return ":" .. name .. ":" end -- find whether or not a (mangled) name is present in a string -function __has_name(name, s) - return string.find(s, mcrt_mangle_name(name)) ~= nil +local function __has_name(name, s) + return string.find(s, __mangle_name(name)) ~= nil end -- remove a name from a string -function __rm_name(name, s) +local function __rm_name(name, s) return string.gsub(s, __mangle_name(name), "") end -- add a name to a string -function __add_name(name, s) +local function __add_name(name, s) return s .. __mangle_name(name) end -- test if a file exists -function __file_exists(file) +local function __file_exists(file) local f = io.open(file, "rb") if f then f:close() end return f ~= nil @@ -36,25 +42,25 @@ end -- wait for the lock file to disappear: unfortunately stock Lua has no -- sleep() function, so we do busy wait here hoping that it will never be -- useful and that the lock would last possibly a bunch of usecs at most -function __wait_lock() - while __file_exists(MCRT_LOCK_FILE) do end +local function __wait_lock() + while __file_exists(__MCRT_LOCK_FILE) do end end -- set and reset the lock -function __set_lock() - local f = io.open(MCRT_LOCK_FILE, "wb") +local function __set_lock() + local f = io.open(__MCRT_LOCK_FILE, "wb") f:close() end -function __reset_lock() - if __file_exists(MCRT_LOCK_FILE) then - os.remove(MCRT_LOCK_FILE) +local function __reset_lock() + if __file_exists(__MCRT_LOCK_FILE) then + os.remove(__MCRT_LOCK_FILE) end end -- read the contents of the persistent file, return nil if read failed -function __read_persistent() - local f = io.open(MCRT_PERSIST_FILE, "r") +local function __read_persistent() + local f = io.open(__MCRT_PERSIST_FILE, "r") local s = nil if f then s = f:read("*all") @@ -65,8 +71,8 @@ end -- write the specified contents to the persistent file, truncate the file -- on null content, which is useful for initialization -function __write_persistent(content) - local f = io.open(MCRT_PERSIST_FILE, "w") +local function __write_persistent(content) + local f = io.open(__MCRT_PERSIST_FILE, "w") if content ~= nil then f:write(content) end @@ -77,7 +83,7 @@ end -- actual library functions -- initialize the persistent file -function initialize() +function mcrt.initialize() __wait_lock() __set_lock() __write_persistent(nil) @@ -85,12 +91,12 @@ function initialize() end -- set the condition bearing the provided name to verified -function set_condition_verified(cond_name) +function mcrt.set_condition_verified(cond_name) __wait_lock() __set_lock() local persistent = __read_persistent() if persistent ~= nil then - if !__has_name(cond_name, persistent) then + if not __has_name(cond_name, persistent) then persistent = __add_name(cond_name, persistent) end else @@ -102,13 +108,13 @@ end -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false -function check_conditions_verified(cond_names) +function mcrt.check_conditions_verified(cond_names) local res = true __wait_lock() __set_lock() local persistent = __read_persistent() for _, name in ipairs(cond_names) do - if !__has_name(name, persistent) then + if not __has_name(name, persistent) then res = false break end @@ -123,4 +129,8 @@ function check_conditions_verified(cond_names) return res end +-- return the library table +return mcrt + + -- end. From 288e04ec4f37cd052e491f6ee62510e16284580b Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sat, 7 Mar 2026 00:52:31 +0100 Subject: [PATCH 19/57] tenmp: remove useless backslash --- lib/internal/multi_conds_run_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 46b5c75..4dde9ea 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -284,7 +284,7 @@ def updater() -> task_lua.LuaScriptTask: task.name = _TASK_UPDATER task.variables_to_set = { "LUA_PATH": get_lua_path() } task.init_script_path = get_lua_initscript() - task.script = f"""\ + task.script = f""" local mcrt = require("{_MCRT_LIBRARY}") mcrt.set_condition_verified(whenever_condition) """ From 6d4d1caf11cd3e2d4f5696a16e90f517d82eaf84 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 8 Mar 2026 02:06:54 +0100 Subject: [PATCH 20/57] temp: fix the confluence cond creation process --- lib/forms/cfgform.py | 4 ++-- lib/internal/multi_conds_run_task.py | 33 +++++++++++++++++----------- lib/items/item.py | 4 +++- support/mcrt_lib.lua | 22 +++++++++++-------- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index d9f3921..3377c27 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -505,7 +505,7 @@ def edit(self) -> None: ) e = fform(available_tasks, self._conditions[item_name]) # this is a special case, which has an extra parameter - if isinstance(e, mcrt.form_ConfluenceCondition): + if item_signature == "cond:lua:mcrt_confluence": confluent_conds = list( x for x in self._conditions.keys() @@ -571,7 +571,7 @@ def new(self) -> None: ] form = form_class(list(available_tasks)) # this is a special case, which has an extra parameter - if isinstance(form, mcrt.form_ConfluenceCondition): + if form_class is mcrt.form_ConfluenceCondition: confluent_conds = list( x for x in self._conditions.keys() diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 4dde9ea..da757a5 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -188,17 +188,21 @@ __wait_lock() __set_lock() local persistent = __read_persistent() - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) end - __write_persistent(persistent) + else + res = false end __reset_lock() return res @@ -392,10 +396,6 @@ def __init__(self, tasks_available, item=None): self._conds_available = list() self._conds_activating = item.tags.get("mcrt_confluent_conditions") or list() - for cond in self._conds_activating.copy(): - if cond not in self._conds_available: - self._conds_activating.remove(cond) - self._conds_available.sort() super().__init__(UI_TITLE_MCRTCOND, tasks_available, item) # create a specific frame for the contents @@ -469,6 +469,7 @@ def __init__(self, tasks_available, item=None): # propagate widgets that need to be accessed self._tv_activatingConds = tv_activatingConds + self._cb_chooseCond = cb_chooseCond # always update the form at the end of initialization self._updateform() @@ -509,6 +510,12 @@ def _updatedata(self) -> None: # set the list of available conditions, that implement confluence def set_available_conditions(self, conds: list[str]) -> None: self._conds_available = conds.copy() + self._conds_available.sort() + self._cb_chooseCond['values'] = self._conds_available + for cond in self._conds_activating.copy(): + if cond not in self._conds_available: + self._conds_activating.remove(cond) + self._updateform() # check whether a condition is confluent diff --git a/lib/items/item.py b/lib/items/item.py index dd6202e..98c0035 100644 --- a/lib/items/item.py +++ b/lib/items/item.py @@ -88,12 +88,14 @@ ('cond:lua', ITEM_COND_LUA, form_LuaScriptCondition, LuaScriptCondition), ('cond:time', ITEM_COND_TIME, form_TimeCondition, TimeCondition), ('cond:wmi', ITEM_COND_WMI, form_WMICondition, WMICondition), - ('cond:mcrt', ITEM_COND_MCRT, form_ConfluenceCondition, ConfluenceCondition), ('event:cli', ITEM_EVENT_CLI, form_CommandEvent, CommandEvent), ('event:dbus', ITEM_EVENT_DBUS, form_DBusEvent, DBusEvent), ('event:fschange', ITEM_EVENT_FSCHANGE, form_FilesystemChangeEvent, FilesystemChangeEvent), ('event:wmi', ITEM_EVENT_WMI, form_WMIEvent, WMIEvent), + + # the following item are native to When (and not to whenever), thus not extras + ('cond:lua:mcrt_confluence', ITEM_COND_MCRT, form_ConfluenceCondition, ConfluenceCondition), ] diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index 7a5839c..f276f49 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -113,17 +113,21 @@ function mcrt.check_conditions_verified(cond_names) __wait_lock() __set_lock() local persistent = __read_persistent() - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end end - __write_persistent(persistent) + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) + end + else + res = false end __reset_lock() return res From 40aef86db108be34c2e719a3bee0df2feff420b8 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Mon, 9 Mar 2026 00:38:07 +0100 Subject: [PATCH 21/57] temp: fix confluent conditions form issues Fix some issues while enabling/disabling task related widgets in conditions according to their being confluent or not; also, clean up temporary directory (`APPDATA/temp`) at startup: this should be mentioned in the documentation --- lib/forms/cond.py | 69 ++++++++++++++++++---------- lib/internal/multi_conds_run_task.py | 1 + lib/utility.py | 11 ++++- when/when.py | 5 +- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index a5c26da..f4c1cb4 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -11,7 +11,7 @@ from ..items.cond import Condition -from ..utility import is_valid_item_name, clean_caption +from ..utility import is_valid_item_name, clean_caption, is_private_item_name from ..internal import multi_conds_run_task as mcrt @@ -168,7 +168,9 @@ def __init__(self, title, tasks_available, item=None): ck_itemRecurring.bind("", lambda _: self._check_recurring()) ck_itemRecurring.bind("", lambda _: self._check_recurring()) ck_mcrtActivateFurther.bind("", lambda _: self._mcrt_confluent()) - ck_mcrtActivateFurther.bind("", lambda _: self._mcrt_confluent()) + ck_mcrtActivateFurther.bind( + "", lambda _: self._mcrt_confluent() + ) # expand appropriate sections area_common.rowconfigure(index=11, weight=1) @@ -278,7 +280,9 @@ def _check_recurring(self) -> None: self._updatedata() self._max_retries.config(state=tk.DISABLED) else: - self._max_retries.config(state=tk.NORMAL) + # before enabling, check that this is not a confluent condition + if not self.data_get("@confluent"): + self._max_retries.config(state=tk.NORMAL) def _mcrt_confluent(self, force=False) -> None: # same consideration as above; this function also disables all task @@ -305,7 +309,9 @@ def _mcrt_confluent(self, force=False) -> None: if tk.NOT_DISABLED in spec: # type:ignore spec.remove(tk.NOT_DISABLED) # type:ignore elem.state(spec) - self._max_retries.config(state=tk.DISABLED) + # TODO: there must be a better way to achieve this + if "config" in elem.__dict__: + elem.config(state=tk.DISABLED) else: for elem in self._task_elems: spec = list(elem.state()) @@ -314,10 +320,12 @@ def _mcrt_confluent(self, force=False) -> None: if tk.NOT_DISABLED not in spec: # type:ignore spec.append(tk.NOT_DISABLED) # type:ignore elem.state(spec) + # TODO: there must be a better way to do this (same as above) + if "config" in elem.__dict__: + elem.config(state=tk.NORMAL) if mcrt_updater in self._tasks: self._tasks.remove(mcrt_updater) - # contents is the root for slave widgets @property def contents(self) -> ttk.Frame: @@ -333,28 +341,38 @@ def _updateform(self) -> None: self._max_retries.config(state=tk.DISABLED) self.data_set("@name", self._item.name) self.data_set("@recurring", self._item.recurring or False) - self.data_set("@max_tasks_retries", self._item.max_tasks_retries or 0) self.data_set("@suspended", self._item.suspended or False) - self.data_set( - "@execute_sequence", - ( - self._item.execute_sequence - if self._item.execute_sequence is False - else True - ), - ) - idx = 0 - for task in self._tasks: - self._tv_tasks.insert( - "", iid="%s-%s" % (idx, task), values=(idx, task), index=tk.END - ) - idx += 1 - if self._item.break_on_failure: - self.data_set("@control_flow", "break_failure") - elif self._item.break_on_success: - self.data_set("@control_flow", "break_success") + if mcrt.is_confluent_cond(self._item): + self.data_set("@confluent", True) + self._mcrt_confluent(True) else: - self.data_set("@control_flow", "break_none") + self.data_set("@confluent", False) + self.data_set("@max_tasks_retries", self._item.max_tasks_retries or 0) + self.data_set( + "@execute_sequence", + ( + self._item.execute_sequence + if self._item.execute_sequence is False + else True + ), + ) + idx = 0 + for task in self._tasks: + # this is correct and avoid listing possible confluent tasks + if not is_private_item_name(task): + self._tv_tasks.insert( + "", + iid="%s-%s" % (idx, task), + values=(idx, task), + index=tk.END, + ) + idx += 1 + if self._item.break_on_failure: + self.data_set("@control_flow", "break_failure") + elif self._item.break_on_success: + self.data_set("@control_flow", "break_success") + else: + self.data_set("@control_flow", "break_none") else: self._max_retries.config(state=tk.DISABLED) self.data_set("@name", "") @@ -363,6 +381,7 @@ def _updateform(self) -> None: self.data_set("@suspended", False) self.data_set("@execute_sequence", True) self.data_set("@max_tasks_retries", 0) + self.data_set("@confluent", False) self.data_set("@choose_task", "") # the data update utility loads data into the item diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index da757a5..c211886 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -542,6 +542,7 @@ def is_confluence_cond(c: cond.Condition) -> bool: "is_confluence_cond", "install_lib", "ConfluenceCondition", + "form_ConfluenceCondition", ] diff --git a/lib/utility.py b/lib/utility.py index 624e4bf..012f93e 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -263,14 +263,21 @@ def get_scriptsdir() -> str: # determine temp directory and ensure that it exists -def get_tempdir() -> str: +def get_tempdir(cleanup: bool=False) -> str: configdir: str = AppConfig.get("APPDATA") # type: ignore if is_windows(): subdir = "Temp" else: subdir = "temp" tempdir = os.path.join(configdir, subdir) - if not os.path.isdir(tempdir): + if os.path.isdir(tempdir): + if cleanup: + shutil.rmtree(tempdir) + try: + os.makedirs(tempdir) + except Exception: + raise OSError(CLI_ERR_SPECIFICDIR_UNACCESSIBLE % tempdir) + else: try: os.makedirs(tempdir) except Exception: diff --git a/when/when.py b/when/when.py index b4842ec..09a332c 100644 --- a/when/when.py +++ b/when/when.py @@ -290,14 +290,17 @@ def prepare_environment() -> None: _ = get_appdata() except Exception as e: exit_error(e) + # create the scripts directory if it does not exist try: _ = get_scriptsdir() except Exception as e: exit_error(e) + # create temp directory if it does not exist, ensuring that it is clean try: - _ = get_tempdir() + _ = get_tempdir(True) except Exception as e: exit_error(e) + # create the Lua library directory if it does not exist try: _ = get_luadir() except Exception as e: From 1db76da2b57474aba6341a5a8643226ec0864093 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Mon, 9 Mar 2026 11:57:06 +0100 Subject: [PATCH 22/57] temp: enclose most error prone Lua code in `pcall` --- lib/internal/multi_conds_run_task.py | 59 +++++++++++++++++----------- support/mcrt_lib.lua | 59 +++++++++++++++++----------- 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index c211886..0654c91 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -161,7 +161,12 @@ function mcrt.initialize() __wait_lock() __set_lock() - __write_persistent(nil) + local ok, msg = pcall(function() + __write_persistent(nil) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + end __reset_lock() end @@ -169,40 +174,50 @@ function mcrt.set_condition_verified(cond_name) __wait_lock() __set_lock() - local persistent = __read_persistent() - if persistent ~= nil then - if not __has_name(cond_name, persistent) then - persistent = __add_name(cond_name, persistent) + local ok, msg = pcall(function() + local persistent = __read_persistent() + if persistent ~= nil then + if not __has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") end - else - persistent = __add_name(cond_name, "") + __write_persistent(persistent) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) end - __write_persistent(persistent) __reset_lock() end -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false function mcrt.check_conditions_verified(cond_names) - local res = true + res = true __wait_lock() __set_lock() - local persistent = __read_persistent() - if persistent ~= nil then - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + local ok, msg = pcall(function() + local persistent = __read_persistent() + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) end - __write_persistent(persistent) + else + res = false end - else - res = false + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) end __reset_lock() return res diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index f276f49..038cdf8 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -86,7 +86,12 @@ end function mcrt.initialize() __wait_lock() __set_lock() - __write_persistent(nil) + local ok, msg = pcall(function() + __write_persistent(nil) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + end __reset_lock() end @@ -94,40 +99,50 @@ end function mcrt.set_condition_verified(cond_name) __wait_lock() __set_lock() - local persistent = __read_persistent() - if persistent ~= nil then - if not __has_name(cond_name, persistent) then - persistent = __add_name(cond_name, persistent) + local ok, msg = pcall(function() + local persistent = __read_persistent() + if persistent ~= nil then + if not __has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") end - else - persistent = __add_name(cond_name, "") + __write_persistent(persistent) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) end - __write_persistent(persistent) __reset_lock() end -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false function mcrt.check_conditions_verified(cond_names) - local res = true + res = true __wait_lock() __set_lock() - local persistent = __read_persistent() - if persistent ~= nil then - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + local ok, msg = pcall(function() + local persistent = __read_persistent() + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + __write_persistent(persistent) end - __write_persistent(persistent) + else + res = false end - else - res = false + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) end __reset_lock() return res From 071023a762f3b53cbffba47dd6c39ffda4eb2f8e Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 1 Apr 2026 19:36:57 +0200 Subject: [PATCH 23/57] temp: exploit whenever sync primitives for Lua --- lib/internal/multi_conds_run_task.py | 168 +++++++++------------------ support/mcrt_lib.lua | 147 +++++++++-------------- 2 files changed, 108 insertions(+), 207 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 0654c91..7251ef6 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -66,8 +66,8 @@ # constants -_MCRT_PERSIST_FILE = ".mcrt_persist" -_MCRT_LOCK_FILE = ".mcrt_persist.lock" +_MCRT_LOCK = "Confluence_LOCK" +_MCRT_PERSIST = "Confluence_STATE" _MCRT_LIBRARY = "_mcrt_lib" _MCRT_EXTRA_DELAY = 15 @@ -78,8 +78,8 @@ -- used in the scripts that require the library -local __MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] -local __MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] +local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] +local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] -- the library itself @@ -107,120 +107,77 @@ end --- test if a file exists -local function __file_exists(file) - local f = io.open(file, "rb") - if f then f:close() end - return f ~= nil -end - --- wait for the lock file to disappear: unfortunately stock Lua has no --- sleep() function, so we do busy wait here hoping that it will never be --- useful and that the lock would last possibly a bunch of usecs at most -local function __wait_lock() - while __file_exists(__MCRT_LOCK_FILE) do end -end - --- set and reset the lock -local function __set_lock() - local f = io.open(__MCRT_LOCK_FILE, "wb") - f:close() -end - -local function __reset_lock() - if __file_exists(__MCRT_LOCK_FILE) then - os.remove(__MCRT_LOCK_FILE) - end -end - --- read the contents of the persistent file, return nil if read failed -local function __read_persistent() - local f = io.open(__MCRT_PERSIST_FILE, "r") - local s = nil - if f then - s = f:read("*all") - f:close() - end - return s -end - --- write the specified contents to the persistent file, truncate the file --- on null content, which is useful for initialization -local function __write_persistent(content) - local f = io.open(__MCRT_PERSIST_FILE, "w") - if content ~= nil then - f:write(content) - end - f:close() -end - - -- actual library functions --- initialize the persistent file +-- initialization is a do-nothing in this edition, but may come in handy function mcrt.initialize() - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - __write_persistent(nil) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + return true end - __reset_lock() -end -- set the condition bearing the provided name to verified function mcrt.set_condition_verified(cond_name) - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - local persistent = __read_persistent() - if persistent ~= nil then - if not __has_name(cond_name, persistent) then - persistent = __add_name(cond_name, persistent) + if sync.lock(__MCRT_LOCK, 1.0) then + local ok, msg = pcall(function() + local sst = sharedstate.load(__MCRT_PERSIST) + local persistent = sst.persistent + if persistent ~= nil then + if not __has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") end - else - persistent = __add_name(cond_name, "") + sst.persistent = persistent + sharedstate.save(__MCRT_PERSIST, sst) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + res = false end - __write_persistent(persistent) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end - __reset_lock() end -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false function mcrt.check_conditions_verified(cond_names) - res = true - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - local persistent = __read_persistent() - if persistent ~= nil then - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + if sync.lock(__MCRT_LOCK, 1.0) then + res = true + local ok, msg = pcall(function() + local sst = sharedstate.load(__MCRT_PERSIST) + local persistent = sst.persistent + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + sst.persistent = persistent + sharedstate.save(__MCRT_PERSIST, sst) end - __write_persistent(persistent) + else + res = false end - else + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) res = false end - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end - __reset_lock() - return res end -- return the library table @@ -246,19 +203,6 @@ """ -# the persistence file is the file that contains the list of conditions that -# concur to task triggering which have been successfully checked -def _mcrt_persist_file(): - return os.path.join(get_tempdir(), _MCRT_PERSIST_FILE) - - -# the lock file is checked when trying to access the persistence file: no other -# access (including read-only access) will be performed when the lock file is -# present, which indicates a current access -def _mcrt_lock_file(): - return os.path.join(get_tempdir(), _MCRT_LOCK_FILE) - - # utility to install the Lua library: it also reserves the library file name # so that it is not overwritten by the user in case he decides to install # a Lua library of choice @@ -268,8 +212,8 @@ def install_lib(): if not os.path.exists(s): lua_library = _LUA_LIBRARY.format( "{}", # this replaces the brackets! - MCRT_LOCK_FILE=_mcrt_lock_file(), - MCRT_PERSIST_FILE=_mcrt_persist_file(), + MCRT_SHAREDSTATE_LOCK=_ITEM_PREFIX + _MCRT_LOCK, + MCRT_PERSIST_FILE=_ITEM_PREFIX + _MCRT_PERSIST, ) with open(s, "w") as f: f.write(lua_library) diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index 038cdf8..571f1f4 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -3,8 +3,8 @@ -- used in the scripts that require the library -local __MCRT_LOCK_FILE = [[{MCRT_LOCK_FILE}]] -local __MCRT_PERSIST_FILE = [[{MCRT_PERSIST_FILE}]] +local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] +local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] -- the library itself @@ -32,120 +32,77 @@ local function __add_name(name, s) end --- test if a file exists -local function __file_exists(file) - local f = io.open(file, "rb") - if f then f:close() end - return f ~= nil -end - --- wait for the lock file to disappear: unfortunately stock Lua has no --- sleep() function, so we do busy wait here hoping that it will never be --- useful and that the lock would last possibly a bunch of usecs at most -local function __wait_lock() - while __file_exists(__MCRT_LOCK_FILE) do end -end - --- set and reset the lock -local function __set_lock() - local f = io.open(__MCRT_LOCK_FILE, "wb") - f:close() -end - -local function __reset_lock() - if __file_exists(__MCRT_LOCK_FILE) then - os.remove(__MCRT_LOCK_FILE) - end -end - --- read the contents of the persistent file, return nil if read failed -local function __read_persistent() - local f = io.open(__MCRT_PERSIST_FILE, "r") - local s = nil - if f then - s = f:read("*all") - f:close() - end - return s -end - --- write the specified contents to the persistent file, truncate the file --- on null content, which is useful for initialization -local function __write_persistent(content) - local f = io.open(__MCRT_PERSIST_FILE, "w") - if content ~= nil then - f:write(content) - end - f:close() -end - - -- actual library functions --- initialize the persistent file +-- initialization is a do-nothing in this edition, but may come in handy function mcrt.initialize() - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - __write_persistent(nil) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + return true end - __reset_lock() -end -- set the condition bearing the provided name to verified function mcrt.set_condition_verified(cond_name) - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - local persistent = __read_persistent() - if persistent ~= nil then - if not __has_name(cond_name, persistent) then - persistent = __add_name(cond_name, persistent) + if sync.lock(__MCRT_LOCK, 1.0) then + local ok, msg = pcall(function() + local sst = sharedstate.load(__MCRT_PERSIST) + local persistent = sst.persistent + if persistent ~= nil then + if not __has_name(cond_name, persistent) then + persistent = __add_name(cond_name, persistent) + end + else + persistent = __add_name(cond_name, "") end - else - persistent = __add_name(cond_name, "") + sst.persistent = persistent + sharedstate.save(__MCRT_PERSIST, sst) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + res = false end - __write_persistent(persistent) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end - __reset_lock() end -- check whether the provided conditions are all verified, and if so remove -- their names prior to returning true; otherwise return false function mcrt.check_conditions_verified(cond_names) - res = true - __wait_lock() - __set_lock() - local ok, msg = pcall(function() - local persistent = __read_persistent() - if persistent ~= nil then - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then + if sync.lock(__MCRT_LOCK, 1.0) then + res = true + local ok, msg = pcall(function() + local sst = sharedstate.load(__MCRT_PERSIST) + local persistent = sst.persistent + if persistent ~= nil then for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) + if not __has_name(name, persistent) then + res = false + break + end + end + if res then + for _, name in ipairs(cond_names) do + persistent = __rm_name(name, persistent) + end + sst.persistent = persistent + sharedstate.save(__MCRT_PERSIST, sst) end - __write_persistent(persistent) + else + res = false end - else + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) res = false end - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end - __reset_lock() - return res end -- return the library table From 7dbfef7aade18fb9a9d2a1e60e65b8899306a916 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 2 Apr 2026 23:45:21 +0200 Subject: [PATCH 24/57] amend: Lua confluence init resets sharedstate --- lib/internal/multi_conds_run_task.py | 18 ++++++++++++++++-- support/mcrt_lib.lua | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 7251ef6..91e6a87 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -109,9 +109,23 @@ -- actual library functions --- initialization is a do-nothing in this edition, but may come in handy +-- initialization just resets the shared state function mcrt.initialize() - return true + if sync.lock(__MCRT_LOCK, 1.0) then + local ok, msg = pcall(function() + local sst = {{ }} + sst.persistent = "" + sharedstate.save(__MCRT_PERSIST, sst) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + res = false + end + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end -- set the condition bearing the provided name to verified diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index 571f1f4..93200f5 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -34,9 +34,23 @@ end -- actual library functions --- initialization is a do-nothing in this edition, but may come in handy +-- initialization just resets the shared state function mcrt.initialize() - return true + if sync.lock(__MCRT_LOCK, 1.0) then + local ok, msg = pcall(function() + local sst = { } + sst.persistent = "" + sharedstate.save(__MCRT_PERSIST, sst) + end) + if not ok then + log.debug("the following error occurred: " .. (msg or "")) + res = false + end + sync.release(__MCRT_LOCK) + return res + else + log.debug("could not acquire shared state for condition confluence") + return false end -- set the condition bearing the provided name to verified From 419ae3ce3400a07fb6dedd24ca831ffcac2f9bab Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 3 Apr 2026 15:59:32 +0200 Subject: [PATCH 25/57] amend: add checks for new whenever options --- lib/utility.py | 72 +++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/utility.py b/lib/utility.py index 012f93e..bae7d8c 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -385,6 +385,14 @@ def check_whenever_version() -> bool: # return the output of `whenever --options` def retrieve_whenever_options() -> None: + # first assume no feature is available + AppConfig.set("WHENEVER_HAS_DBUS", False) + AppConfig.set("WHENEVER_HAS_WMI", False) + AppConfig.set("WHENEVER_HAS_LUAHTTP", False) + AppConfig.set("WHENEVER_HAS_LUASYNC", False) + # ...other options might appear above here + + # then ask the executable for options whenever_path: str = AppConfig.get("WHENEVER") # type: ignore try: result = subprocess.run( @@ -397,37 +405,34 @@ def retrieve_whenever_options() -> None: subprocess.CREATE_NO_WINDOW if sys.platform.startswith("win") else 0 ), ) - except Exception: - # maybe no executable has been found? - AppConfig.set("WHENEVER_HAS_DBUS", False) - AppConfig.set("WHENEVER_HAS_WMI", False) - # ...other options might appear - return None - if result: - # output has the form: `options: [wmi] [dbus]`, each one might or - # might not be present in the output, so we check whether the list - # below contains or not one of them - if result.returncode == 0: - opts = result.stdout.strip().split() - if "dbus" in opts: - AppConfig.set("WHENEVER_HAS_DBUS", True) - else: - AppConfig.set("WHENEVER_HAS_DBUS", False) - if sys.platform.startswith("win") and "wmi" in opts: - AppConfig.set("WHENEVER_HAS_WMI", True) + if result: + # output has the form: `options: ..`, each one might + # or might not be present in the output, so we check whether the + # list below contains or not one of them + if result.returncode == 0: + opts = result.stdout.strip().split() + if opts[0] != "options": + # maybe it is not the `whenever` we are looking for? + return + opts = opts[1:] + if "dbus" in opts: + AppConfig.set("WHENEVER_HAS_DBUS", True) + if sys.platform.startswith("win") and "wmi" in opts: + AppConfig.set("WHENEVER_HAS_WMI", True) + if "lua_sync" in opts: + AppConfig.set("WHENEVER_HAS_LUASYNC", True) + if "lua_httpreq" in opts: + AppConfig.set("WHENEVER_HAS_LUAHTTPREQ", True) + # ...other options might appear else: - AppConfig.set("WHENEVER_HAS_WMI", False) - # ...other options might appear + # this might be an older version, assume DBus is available + AppConfig.set("WHENEVER_HAS_DBUS", True) else: - # this might be an older version, assume DBus is available + # this too might be an older version, assume DBus is available AppConfig.set("WHENEVER_HAS_DBUS", True) - AppConfig.set("WHENEVER_HAS_WMI", False) - # ...other options might appear - else: - # this might be an older version, assume DBus is available - AppConfig.set("WHENEVER_HAS_DBUS", True) - AppConfig.set("WHENEVER_HAS_WMI", False) - # ...other options might appear + except Exception: + # maybe no executable has been found? Still, no available option. + return # check whether the scheduler is running @@ -453,16 +458,23 @@ def is_whenever_running() -> None | bool: return False -# a couple of shortcuts for whenever options +# some shortcuts for whenever options def whenever_has_dbus() -> bool: res = bool(AppConfig.get("WHENEVER_HAS_DBUS")) return res - def whenever_has_wmi() -> bool: res = bool(AppConfig.get("WHENEVER_HAS_WMI")) return res +def whenever_has_lua_sync() -> bool: + res = bool(AppConfig.get("WHENEVER_HAS_LUASYNC")) + return res + +def whenever_has_luahttpreq() -> bool: + res = bool(AppConfig.get("WHENEVER_HAS_LUAHTTPREQ")) + return res + # return the configuration file path def get_configfile() -> str: From b1a24c320502ac5d199750d7f901c5797ca2d1da Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 5 Apr 2026 23:06:08 +0200 Subject: [PATCH 26/57] feat: working MCRT confluence using shared states This version uses the shared states feature available for Lua in whenever 1.2.0 to implement MCRT (aka confluence), and tests are successful; implements #187. --- lib/forms/cond.py | 20 ++++++++++---------- lib/internal/multi_conds_run_task.py | 14 ++++++-------- support/mcrt_lib.lua | 2 +- when/when.py | 9 +++++++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index f4c1cb4..55aa640 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -11,7 +11,12 @@ from ..items.cond import Condition -from ..utility import is_valid_item_name, clean_caption, is_private_item_name +from ..utility import ( + is_valid_item_name, + clean_caption, + is_private_item_name, + whenever_has_lua_sync, + ) from ..internal import multi_conds_run_task as mcrt @@ -69,10 +74,12 @@ def __init__(self, title, tasks_available, item=None): ck_mcrtActivateFurther = ttk.Checkbutton( area_common, text=UI_FORM_ACTIVATEFURTHERCONDS ) - # l_maxTasksRetries = ttk.Label(area_common, text=UI_FORM_MAXTASKRETRIES_SC) - # e_maxTasksRetries = ttk.Entry(area_common) sep1 = ttk.Separator(area_common) + # disable confluence if `whenever` has no shared states + if not whenever_has_lua_sync(): + ck_mcrtActivateFurther.config(state=tk.DISABLED) + l_tasks = ttk.Label(area_common, text=UI_FORM_ACTIVETASKS_SC) # build a scrolled frame for the treeview sftv_tasks = ttk.Frame(area_common) @@ -152,8 +159,6 @@ def __init__(self, title, tasks_available, item=None): ck_itemRecurring.grid(row=1, column=1, sticky=tk.W, padx=PAD, pady=PAD) ck_itemSuspended.grid(row=2, column=1, sticky=tk.W, padx=PAD, pady=PAD) ck_mcrtActivateFurther.grid(row=3, column=1, sticky=tk.W, padx=PAD, pady=PAD) - # l_maxTasksRetries.grid(row=4, column=0, sticky=tk.W, padx=PAD, pady=PAD) - # e_maxTasksRetries.grid(row=4, column=1, sticky=tk.EW, padx=PAD, pady=PAD) sep1.grid(row=5, column=0, columnspan=2, sticky=tk.EW, pady=PAD) l_tasks.grid(row=10, column=0, columnspan=2, sticky=tk.W, padx=PAD, pady=PAD) sftv_tasks.grid( @@ -232,14 +237,12 @@ def __init__(self, title, tasks_available, item=None): else: self.reset_item() self._check_recurring() - # self._max_retries.config(state=tk.NORMAL) self.changed = False def add_task(self) -> None: elem = self.data_get("@choose_task") if elem: self._tasks.append(elem) - self._updatedata() self._updateform() def del_task(self) -> None: @@ -247,7 +250,6 @@ def del_task(self) -> None: if elem: idx = int(elem[0]) del self._tasks[idx] - self._updatedata() self._updateform() def add_check_caption(self, dataname, caption) -> None: @@ -277,7 +279,6 @@ def _check_recurring(self) -> None: not_rec = not self.data_get("@recurring") or False if not_rec: self.data_set("@max_tasks_retries", 0) - self._updatedata() self._max_retries.config(state=tk.DISABLED) else: # before enabling, check that this is not a confluent condition @@ -301,7 +302,6 @@ def _mcrt_confluent(self, force=False) -> None: self.data_set("@max_tasks_retries", 0) self._tv_tasks.delete(*self._tv_tasks.get_children()) self._tasks = [mcrt_updater] - self._updatedata() for elem in self._task_elems: spec = list(elem.state()) if tk.DISABLED not in spec: diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 91e6a87..9a42a73 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -3,20 +3,20 @@ # private items that enable the condition "confluence" feature (issue #187), # that is the possibility for a task to depend on more than one condition. # -# The idea is to have a persistence file where *all* the conditions that are -# defined as concurring to the triggering of tasks are recorded: this file is +# The idea is to have a persistence store where *all* the conditions that are +# defined as concurring to the triggering of tasks are recorded: this store is # periodically read, and when all the conditions that concur to a specific -# task (or list/set of tasks) are found in the file, the following happens: +# task (or list/set of tasks) are found in the store, the following happens: # -# 1. the conditions are _removed_ from the file +# 1. the conditions are _removed_ from the store # 2. the corresponding task or list/set of tasks is executed # # For this purpose, the conditions that concur to tasks will all be bound to -# a single Lua-based task, which writes their name in the file, while multiple +# a single Lua-based task, which writes their name in the store, while multiple # conditions will be built for each task group to be activated by multiple # conditions. # -# The persistence file does not need to be structured, it only has to be read, +# The persistence store does not need to be structured, it only has to be read, # operated on, and written quickly. No concurrent reads or writes should be # allowed, assuming that one instance of a verified condition cannot concur to # multiple task groups (this is arbitrary, but cleaner than the opposite), and @@ -53,7 +53,6 @@ from ..forms.cond import form_Condition from ..utility import ( - get_tempdir, get_luadir, get_lua_initscript, get_lua_path, @@ -267,7 +266,6 @@ def updater() -> task_lua.LuaScriptTask: """ return task - def updater_name() -> str: return _TASK_UPDATER diff --git a/support/mcrt_lib.lua b/support/mcrt_lib.lua index 93200f5..718a579 100644 --- a/support/mcrt_lib.lua +++ b/support/mcrt_lib.lua @@ -38,7 +38,7 @@ end function mcrt.initialize() if sync.lock(__MCRT_LOCK, 1.0) then local ok, msg = pcall(function() - local sst = { } + local sst = {{ }} sst.persistent = "" sharedstate.save(__MCRT_PERSIST, sst) end) diff --git a/when/when.py b/when/when.py index 09a332c..0a462a5 100644 --- a/when/when.py +++ b/when/when.py @@ -31,6 +31,7 @@ get_logfile, get_configfile, is_whenever_running, + whenever_has_lua_sync, get_image, get_UI_theme, get_tkroot, @@ -323,6 +324,10 @@ def main_config(args) -> None: AppConfig.set("APPDATA", args.dir_appdata) retrieve_whenever_options() prepare_environment() + # the following disables Confluence for `whenever` without shared states + if not whenever_has_lua_sync(): + mcrt.ConfluenceCondition.available = False + # different exception handling for DEBUG/RELEASE runs if DEBUG: configfile = get_configfile() if not os.path.exists(configfile): @@ -363,6 +368,9 @@ def main_start(args) -> None: AppConfig.set("WHENEVER", args.whenever) retrieve_whenever_options() prepare_environment() + # the following disables Confluence for `whenever` without shared states + if not whenever_has_lua_sync(): + mcrt.ConfluenceCondition.available = False # prepare application so that the logger can be initialized setup_windows() if is_whenever_running(): @@ -398,6 +406,7 @@ def main_start(args) -> None: status=log.STATUS_MSG, ).log(f"found `whenever` version {v}: please upgrade") exit_error(CLI_ERR_WHENEVER_WRONG_VERSION) + # different exception handling for DEBUG/RELEASE runs if DEBUG: # setup the scheduler and associate it to the application wrapper = Wrapper(config_file, whenever, _root) From c2d070d1a9553ce169c24799c7bfeb3593cd995c Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 5 Apr 2026 23:27:15 +0200 Subject: [PATCH 27/57] fix: Lua init script path saved as literal in config The path to the Lua initialization script is saved as literal for readability: fixes #192 --- lib/items/cond_lua.py | 9 +++++++-- lib/items/task_lua.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/items/cond_lua.py b/lib/items/cond_lua.py index 38f5ab6..3442503 100644 --- a/lib/items/cond_lua.py +++ b/lib/items/cond_lua.py @@ -6,7 +6,12 @@ import os from tomlkit import items -from ..utility import check_not_none, append_not_none, toml_script_string +from ..utility import ( + check_not_none, + append_not_none, + toml_script_string, + toml_literal, + ) from .cond import Condition from .itemhelp import CheckedTable @@ -96,7 +101,7 @@ def as_table(self): t = append_not_none(t, "expect_all", self.expect_all) t = append_not_none(t, "variables_to_set", self.variables_to_set) t = append_not_none(t, "expected_results", self.expected_results) - t = append_not_none(t, "init_script_path", self.init_script_path) + t = append_not_none(t, "init_script_path", toml_literal(self.init_script_path)) return t diff --git a/lib/items/task_lua.py b/lib/items/task_lua.py index 9440d1b..95e73dd 100644 --- a/lib/items/task_lua.py +++ b/lib/items/task_lua.py @@ -6,7 +6,12 @@ import os from tomlkit import items -from ..utility import check_not_none, append_not_none, toml_script_string +from ..utility import ( + check_not_none, + append_not_none, + toml_script_string, + toml_literal, + ) from .itemhelp import CheckedTable from .task import Task @@ -84,7 +89,7 @@ def as_table(self) -> items.Table: t = append_not_none(t, "expect_all", self.expect_all) t = append_not_none(t, "variables_to_set", self.variables_to_set) t = append_not_none(t, "expected_results", self.expected_results) - t = append_not_none(t, "init_script_path", self.init_script_path) + t = append_not_none(t, "init_script_path", toml_literal(self.init_script_path)) return t From 41e1c5002ab59744310785d8830771306a56a660 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sun, 5 Apr 2026 23:49:55 +0200 Subject: [PATCH 28/57] style: reformat modified source code --- lib/forms/cond.py | 6 +++--- lib/forms/cond_lua.py | 2 +- lib/forms/task_lua.py | 2 +- lib/internal/multi_conds_run_task.py | 11 +++++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index 55aa640..a2c44cf 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -12,11 +12,11 @@ from ..items.cond import Condition from ..utility import ( - is_valid_item_name, - clean_caption, + is_valid_item_name, + clean_caption, is_private_item_name, whenever_has_lua_sync, - ) +) from ..internal import multi_conds_run_task as mcrt diff --git a/lib/forms/cond_lua.py b/lib/forms/cond_lua.py index 931a126..ffd2e57 100644 --- a/lib/forms/cond_lua.py +++ b/lib/forms/cond_lua.py @@ -38,7 +38,7 @@ def __init__(self, tasks_available, item=None): super().__init__(UI_TITLE_LUACOND, tasks_available, item) assert isinstance(self._item, LuaScriptCondition) - self._item.variables_to_set = { "LUA_PATH": get_lua_path() } # legacy + self._item.variables_to_set = {"LUA_PATH": get_lua_path()} # legacy self._item.init_script_path = get_lua_initscript() # form data diff --git a/lib/forms/task_lua.py b/lib/forms/task_lua.py index 5c231bd..eb72717 100644 --- a/lib/forms/task_lua.py +++ b/lib/forms/task_lua.py @@ -42,7 +42,7 @@ def __init__(self, item=None): # form data self._results = [] - self._item.variables_to_set = { "LUA_PATH": get_lua_path() } # legacy + self._item.variables_to_set = {"LUA_PATH": get_lua_path()} # legacy self._item.init_script_path = get_lua_initscript() # build the UI: build widgets, arrange them in the box, bind data diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 9a42a73..c048147 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -235,6 +235,7 @@ def install_lib(): # the following items are the specific ones that implement the confluence + # 1. initialization task: resets the persistence file; the reason why we want # this to be performed by the Lua interpreter instead of the GUI application, # is that in this way the MCRT system is initialized even when used with @@ -242,7 +243,7 @@ def install_lib(): def initializer() -> task_lua.LuaScriptTask: task = task_lua.LuaScriptTask() task.name = _TASK_INITIALIZER - task.variables_to_set = { "LUA_PATH": get_lua_path() } + task.variables_to_set = {"LUA_PATH": get_lua_path()} task.init_script_path = get_lua_initscript() task.script = f""" local mcrt = require("{_MCRT_LIBRARY}") @@ -250,6 +251,7 @@ def initializer() -> task_lua.LuaScriptTask: """ return task + def initializer_name() -> str: return _TASK_INITIALIZER @@ -258,7 +260,7 @@ def initializer_name() -> str: def updater() -> task_lua.LuaScriptTask: task = task_lua.LuaScriptTask() task.name = _TASK_UPDATER - task.variables_to_set = { "LUA_PATH": get_lua_path() } + task.variables_to_set = {"LUA_PATH": get_lua_path()} task.init_script_path = get_lua_initscript() task.script = f""" local mcrt = require("{_MCRT_LIBRARY}") @@ -266,6 +268,7 @@ def updater() -> task_lua.LuaScriptTask: """ return task + def updater_name() -> str: return _TASK_UPDATER @@ -324,7 +327,7 @@ def __init__(self, t: items.Table | None = None): def updateitem(self): confluent_conditions = self.tags.get("mcrt_confluent_conditions", list()) - self.variables_to_set = { "LUA_PATH": get_lua_path() } + self.variables_to_set = {"LUA_PATH": get_lua_path()} self.init_script_path = get_lua_initscript() self.script = _MCRT_COND_CONFLUENCE_SCRIPT_TEMPLATE.replace( "[[COND_LIST]]", @@ -482,7 +485,7 @@ def _updatedata(self) -> None: def set_available_conditions(self, conds: list[str]) -> None: self._conds_available = conds.copy() self._conds_available.sort() - self._cb_chooseCond['values'] = self._conds_available + self._cb_chooseCond["values"] = self._conds_available for cond in self._conds_activating.copy(): if cond not in self._conds_available: self._conds_activating.remove(cond) From 3e3fd52735ba5b5b55575d2cde9e804923b4aef5 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Mon, 6 Apr 2026 22:32:55 +0200 Subject: [PATCH 29/57] fix(i18n): update translations for confluence --- lib/i18n/strings_de.py | 4 ++++ lib/i18n/strings_en.py | 4 ++++ lib/i18n/strings_fr.py | 4 ++++ lib/i18n/strings_it.py | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/lib/i18n/strings_de.py b/lib/i18n/strings_de.py index c56e27c..97d8da5 100644 --- a/lib/i18n/strings_de.py +++ b/lib/i18n/strings_de.py @@ -17,6 +17,7 @@ ITEM_COND_DBUS = "DBUS-Inspektion basierte Kondition" ITEM_COND_WMI = "WMI-Query basierte Kondition" ITEM_COND_EVENT = "Ereignis basierte Kondition" +ITEM_COND_MCRT = "Kondition durch andere Konditionen aktivierte" ITEM_EVENT = "Ereignis" ITEM_EVENT_FSCHANGE = "Dateisystem Überwachung basiertes Ereignis" @@ -203,6 +204,7 @@ UI_FORM_WMI_QUERY_SC = "WMI-Query (WQL):" UI_FORM_WMI_RESULT_CHECKS_SC = "Ergebnisprüfungen:" UI_FORM_WMI_NAMESPACE_SC = "WMI-Namespace:" +UI_FORM_MCRT_ACTIVATINGCONDS_SC = "Aktivierende Konditionen:" UI_FORM_ITEMTYPE_SC = "Elementtyp wählen:" UI_FORM_ITEMSUBTYPES_SC = "Verfügbare Elemente:" @@ -231,6 +233,7 @@ UI_FORM_RECURSIVEDIRSCAN = "Verzeichnisse rekursiv scannen" UI_FORM_SELECTDIRECTORY = "Schaltfläche verwenden, um Verzeichnisse auszuwählen" UI_FORM_IGNOREPERSISTSUCCESS = "Anhaltend erfolgreiche Ergebnisse übergehen" +UI_FORM_ACTIVATEFURTHERCONDS = "Weitere Konditionen aktivieren" UI_FORM_OR = "oder" UI_FORM_FILELOCATION_SC = "Datei:" @@ -257,6 +260,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: Zeitkondition Bearbeitung" UI_TITLE_DBUSCOND = f"{UI_APP}: DBUS-Methode Kondition Bearbeitung" UI_TITLE_WMICOND = f"{UI_APP}: WMI-Query Kondition Bearbeitung" +UI_TITLE_MCRTCOND = f"{UI_APP}: Kondition durch andere Konditionen aktivierte Bearbeitung" UI_TITLE_DBUSEVENT = f"{UI_APP}: DBUS-Signal Ereignis Bearbeitung" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: Dateisystemüberwachungsereignis Bearbeitung" diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index 014636f..6a629ab 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -17,6 +17,7 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" +ITEM_COND_MCRT = "Condition Activated By Other Conditions" ITEM_EVENT = "Event" ITEM_EVENT_FSCHANGE = "Filesystem Monitoring Based Event" @@ -203,6 +204,7 @@ UI_FORM_WMI_QUERY_SC = "WMI Query (WQL):" UI_FORM_WMI_RESULT_CHECKS_SC = "Result checks:" UI_FORM_WMI_NAMESPACE_SC = "WMI Namespace:" +UI_FORM_MCRT_ACTIVATINGCONDS_SC = "Activating conditions:" UI_FORM_ITEMTYPE_SC = "Choose item type:" UI_FORM_ITEMSUBTYPES_SC = "Available items:" @@ -231,6 +233,7 @@ UI_FORM_RECURSIVEDIRSCAN = "Recursively scan directories" UI_FORM_SELECTDIRECTORY = "Use button to select directories" UI_FORM_IGNOREPERSISTSUCCESS = "Ignore persistently successful checks" +UI_FORM_ACTIVATEFURTHERCONDS = "Activate further conditions" UI_FORM_OR = "or" UI_FORM_FILELOCATION_SC = "File:" @@ -257,6 +260,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: Time Condition Editor" UI_TITLE_DBUSCOND = f"{UI_APP}: DBus Method Condition Editor" UI_TITLE_WMICOND = f"{UI_APP}: WMI Query Condition Editor" +UI_TITLE_MCRTCOND = f"{UI_APP}: Condition Activated by Other Conditions Editor" UI_TITLE_DBUSEVENT = f"{UI_APP}: DBus Signal Event Editor" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: Filesystem Monitoring Event Editor" diff --git a/lib/i18n/strings_fr.py b/lib/i18n/strings_fr.py index f2a66e1..d67ec3e 100644 --- a/lib/i18n/strings_fr.py +++ b/lib/i18n/strings_fr.py @@ -17,6 +17,7 @@ ITEM_COND_DBUS = "Condition basée sur l'inspection du DBUS" ITEM_COND_WMI = "Condition basée sur une requête WMI" ITEM_COND_EVENT = "Condition basée sur un événement" +ITEM_COND_MCRT = "Condition activée par autres conditions" ITEM_EVENT = "Événement" ITEM_EVENT_FSCHANGE = "Événement basé sur la surveillance du système de fichiers" @@ -203,6 +204,7 @@ UI_FORM_WMI_QUERY_SC = "WMI Query (WQL):" UI_FORM_WMI_RESULT_CHECKS_SC = "Vérification des résultats:" UI_FORM_WMI_NAMESPACE_SC = "WMI Namespace:" +UI_FORM_MCRT_ACTIVATINGCONDS_SC = "Conditions qui activent:" UI_FORM_ITEMTYPE_SC = "Choisisser le type d'élément:" UI_FORM_ITEMSUBTYPES_SC = "Éléments disponibles:" @@ -231,6 +233,7 @@ UI_FORM_RECURSIVEDIRSCAN = "Contrôler les répertoires récursivement" UI_FORM_SELECTDIRECTORY = "Utiliser le bouton pour sélectionner un répertoire" UI_FORM_IGNOREPERSISTSUCCESS = "Ignorer les cas de succès persistent" +UI_FORM_ACTIVATEFURTHERCONDS = "Activer des autres conditions" UI_FORM_OR = "ou" UI_FORM_FILELOCATION_SC = "Fichier:" @@ -257,6 +260,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: éditeur de condition de temps" UI_TITLE_DBUSCOND = f"{UI_APP}: éditeur de condition de méthode DBUS" UI_TITLE_WMICOND = f"{UI_APP}: éditeur de condition de requête WMI" +UI_TITLE_MCRTCOND = f"{UI_APP}: éditeur de condition activée par autres conditions" UI_TITLE_DBUSEVENT = f"{UI_APP}: éditeur d'événements Signal DBUS" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: éditeur d'événements de surveillance du système de fichiers" diff --git a/lib/i18n/strings_it.py b/lib/i18n/strings_it.py index 26953e7..a79defa 100644 --- a/lib/i18n/strings_it.py +++ b/lib/i18n/strings_it.py @@ -17,6 +17,7 @@ ITEM_COND_DBUS = "Condizione basata sull'ispezione di DBUS" ITEM_COND_WMI = "Condizione basata su query WMI" ITEM_COND_EVENT = "Condizione basata su evento" +ITEM_COND_MCRT = "Condizione attivata da altre condizioni" ITEM_EVENT = "Evento" ITEM_EVENT_FSCHANGE = "Evento basato su monitoraggio del filesystem" @@ -203,6 +204,7 @@ UI_FORM_WMI_QUERY_SC = "Query WMI (WQL):" UI_FORM_WMI_RESULT_CHECKS_SC = "Verifiche risultati:" UI_FORM_WMI_NAMESPACE_SC = "Namespace WMI:" +UI_FORM_MCRT_ACTIVATINGCONDS_SC = "Attivato dalle condizioni:" UI_FORM_ITEMTYPE_SC = "Scegli il tipo di elemento:" UI_FORM_ITEMSUBTYPES_SC = "Elementi disponibili:" @@ -231,6 +233,7 @@ UI_FORM_RECURSIVEDIRSCAN = "Scansione ricorsiva directory" UI_FORM_SELECTDIRECTORY = "Usa il bottone per selezionare la directory" UI_FORM_IGNOREPERSISTSUCCESS = "Ignora successi persistenti" +UI_FORM_ACTIVATEFURTHERCONDS = "Attiva ulteriori condizioni" UI_FORM_OR = "o" UI_FORM_FILELOCATION_SC = "File:" @@ -257,6 +260,7 @@ UI_TITLE_TIMECOND = f"{UI_APP}: Editor di condizioni basate sul tempo" UI_TITLE_DBUSCOND = f"{UI_APP}: Editor di condizioni basate su chiamata DBUS" UI_TITLE_WMICOND = f"{UI_APP}: Editor di condizioni basate su query WMI" +UI_TITLE_MCRTCOND = f"{UI_APP}: Editor di condizioni attivate da altre condizioni" UI_TITLE_DBUSEVENT = f"{UI_APP}: Editor degli eventi di tipo DBUS" UI_TITLE_FSCHANGEEVENT = f"{UI_APP}: Editor degli eventi di monitoraggio del filesystem" From d3b874742a035c754c35e846ed9b770207e1cc8f Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Mon, 6 Apr 2026 23:34:39 +0200 Subject: [PATCH 30/57] refactor: reorganize initialization sequence Clean up application initialization sequence in order to make it appear more streamlined: this does not reduce complexity but prepares to evolution bound on environment-available discrimination (such as `whenever` options) to enable or disable application features; minimum required `whenever` version is bumped up to 1.2.0 in order to exploit new Lua features --- lib/toolbox/create_shortcuts.py | 1 - lib/utility.py | 26 +++++++++++--------- when/when.py | 43 ++++++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/lib/toolbox/create_shortcuts.py b/lib/toolbox/create_shortcuts.py index c5b0dfd..aefbb98 100644 --- a/lib/toolbox/create_shortcuts.py +++ b/lib/toolbox/create_shortcuts.py @@ -213,7 +213,6 @@ def create_shortcuts(main_script, desktop=True, autostart=True, verbose=False) - ) except Exception as e: if verbose: - print(e) write_warning(CLI_ERR_CANNOT_CREATE_SHORTCUT) return False return True diff --git a/lib/utility.py b/lib/utility.py index bae7d8c..d0cd6a9 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -35,7 +35,7 @@ # lowest whenever version supported -_MIN_WHENEVER_SUPPORTED = Version.parse("1.0.0") +_MIN_WHENEVER_SUPPORTED = Version.parse("1.2.0") # a regular expression to check whether an user-given name is valid @@ -388,8 +388,8 @@ def retrieve_whenever_options() -> None: # first assume no feature is available AppConfig.set("WHENEVER_HAS_DBUS", False) AppConfig.set("WHENEVER_HAS_WMI", False) - AppConfig.set("WHENEVER_HAS_LUAHTTP", False) AppConfig.set("WHENEVER_HAS_LUASYNC", False) + AppConfig.set("WHENEVER_HAS_LUAHTTPREQ", False) # ...other options might appear above here # then ask the executable for options @@ -411,24 +411,30 @@ def retrieve_whenever_options() -> None: # list below contains or not one of them if result.returncode == 0: opts = result.stdout.strip().split() - if opts[0] != "options": + if opts[0] != "options:": # maybe it is not the `whenever` we are looking for? return opts = opts[1:] if "dbus" in opts: + AppConfig.delete("WHENEVER_HAS_DBUS") AppConfig.set("WHENEVER_HAS_DBUS", True) if sys.platform.startswith("win") and "wmi" in opts: + AppConfig.delete("WHENEVER_HAS_WMI") AppConfig.set("WHENEVER_HAS_WMI", True) if "lua_sync" in opts: + AppConfig.delete("WHENEVER_HAS_LUASYNC") AppConfig.set("WHENEVER_HAS_LUASYNC", True) if "lua_httpreq" in opts: + AppConfig.delete("WHENEVER_HAS_LUAHTTPREQ") AppConfig.set("WHENEVER_HAS_LUAHTTPREQ", True) # ...other options might appear else: # this might be an older version, assume DBus is available + AppConfig.delete("WHENEVER_HAS_DBUS") AppConfig.set("WHENEVER_HAS_DBUS", True) else: # this too might be an older version, assume DBus is available + AppConfig.delete("WHENEVER_HAS_DBUS") AppConfig.set("WHENEVER_HAS_DBUS", True) except Exception: # maybe no executable has been found? Still, no available option. @@ -460,20 +466,16 @@ def is_whenever_running() -> None | bool: # some shortcuts for whenever options def whenever_has_dbus() -> bool: - res = bool(AppConfig.get("WHENEVER_HAS_DBUS")) - return res + return bool(AppConfig.get("WHENEVER_HAS_DBUS")) def whenever_has_wmi() -> bool: - res = bool(AppConfig.get("WHENEVER_HAS_WMI")) - return res + return bool(AppConfig.get("WHENEVER_HAS_WMI")) def whenever_has_lua_sync() -> bool: - res = bool(AppConfig.get("WHENEVER_HAS_LUASYNC")) - return res + return bool(AppConfig.get("WHENEVER_HAS_LUASYNC")) -def whenever_has_luahttpreq() -> bool: - res = bool(AppConfig.get("WHENEVER_HAS_LUAHTTPREQ")) - return res +def whenever_has_lua_httpreq() -> bool: + return bool(AppConfig.get("WHENEVER_HAS_LUAHTTPREQ")) # return the configuration file path diff --git a/when/when.py b/when/when.py index 0a462a5..58f5c6b 100644 --- a/when/when.py +++ b/when/when.py @@ -31,7 +31,10 @@ get_logfile, get_configfile, is_whenever_running, + whenever_has_wmi, + whenever_has_dbus, whenever_has_lua_sync, + whenever_has_lua_httpreq, get_image, get_UI_theme, get_tkroot, @@ -306,6 +309,28 @@ def prepare_environment() -> None: _ = get_luadir() except Exception as e: exit_error(e) + # ... + + +# activate or deactivate non-extra features according to availability +def check_prepare_features(): + # deactivate main capabilities depending on WMI + if not whenever_has_wmi(): + pass + # ... + # deactivate main capabilities depending on DBus + if not whenever_has_dbus(): + pass + # ... + # deactivate main capabilities depending on Lua sync facilities + if not whenever_has_lua_sync(): + mcrt.ConfluenceCondition.available = False + pass + # ... + # deactivate main capabilities depending on Lua HTTP access + if not whenever_has_lua_httpreq(): + pass + # ... # subcommand main functions @@ -322,12 +347,10 @@ def main_config(args) -> None: # set some global configuration values according to CLI options AppConfig.delete("APPDATA") AppConfig.set("APPDATA", args.dir_appdata) + # prepare application retrieve_whenever_options() prepare_environment() - # the following disables Confluence for `whenever` without shared states - if not whenever_has_lua_sync(): - mcrt.ConfluenceCondition.available = False - # different exception handling for DEBUG/RELEASE runs + check_prepare_features() if DEBUG: configfile = get_configfile() if not os.path.exists(configfile): @@ -366,16 +389,14 @@ def main_start(args) -> None: AppConfig.set("LOGLEVEL", args.log_level.upper()) AppConfig.delete("WHENEVER") AppConfig.set("WHENEVER", args.whenever) + # exit with an error if whenever is already running + if is_whenever_running(): + exit_error(CLI_ERR_ALREADY_RUNNING) + # prepare application and initialize the logger retrieve_whenever_options() prepare_environment() - # the following disables Confluence for `whenever` without shared states - if not whenever_has_lua_sync(): - mcrt.ConfluenceCondition.available = False - # prepare application so that the logger can be initialized + check_prepare_features() setup_windows() - if is_whenever_running(): - exit_error(CLI_ERR_ALREADY_RUNNING) - # get configuration options log_level = AppConfig.get("LOGLEVEL") log_file = get_logfile() config_file = get_configfile() From bdb31c9ab99a604b4aaf6a05eece077bd7f584e5 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 7 Apr 2026 13:34:07 +0200 Subject: [PATCH 31/57] fix: use condition availability to enable MCRT Instead of checking whether Lua has the shared states feature to enable or disable MCRT confluence, directly check availability of confluence based conditions --- lib/forms/cond.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index a2c44cf..3914f8d 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -76,8 +76,8 @@ def __init__(self, title, tasks_available, item=None): ) sep1 = ttk.Separator(area_common) - # disable confluence if `whenever` has no shared states - if not whenever_has_lua_sync(): + # disable confluence if confluence condition is not available + if not mcrt.ConfluenceCondition.available: ck_mcrtActivateFurther.config(state=tk.DISABLED) l_tasks = ttk.Label(area_common, text=UI_FORM_ACTIVETASKS_SC) @@ -124,8 +124,6 @@ def __init__(self, title, tasks_available, item=None): b_addTask.grid(row=0, column=2, sticky=tk.E, padx=PAD, pady=PAD) b_delTask.grid(row=0, column=3, sticky=tk.E, padx=PAD, pady=PAD) - # self._tv_tasks.bind('', lambda _: self.recall_task()) - # control flow section area_ctlflow = ttk.Frame(area_common) area_ctlflowL = ttk.Frame(area_ctlflow) From 92616d04546810fad4b85b4e5406864fca05752c Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 7 Apr 2026 14:18:16 +0200 Subject: [PATCH 32/57] build: update pyproject.toml format Also add Lua library directory in `\lua` --- lua/LIBRARY | 1 + pyproject.toml | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 lua/LIBRARY diff --git a/lua/LIBRARY b/lua/LIBRARY new file mode 100644 index 0000000..3247351 --- /dev/null +++ b/lua/LIBRARY @@ -0,0 +1 @@ +This directory contains Lua libraty files and their checksums \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e5826f7..9881cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,29 @@ -[tool.poetry] +[project] name = "when" version = "2.0.3" description = "Interface for the **whenever** automation tool" -authors = ["Francesco Garosi "] +authors = [ + { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, +] license = 'BSD 3-Clause "New" or "Revised" License' readme = "README.md" +requires-python = ">=3.10" + +[tool.poetry] packages = [ { include = "lib" }, { include = "when" }, ] +include = [ + "lua", +] + +[project.scripts] +when = "when.when:main" + +[project.gui-scripts] +when-bg = "when.when_bg:run_bg" + [tool.poetry.dependencies] python = "^3.10" @@ -34,10 +49,6 @@ ttkbootstrap-icons-mat = "^1.0.0" winshell = { version = "^0.6", markers = "sys_platform == 'win32'" } wmi = { version = "^1.5.1", markers = "sys_platform == 'win32'" } -[tool.poetry.scripts] -when = "when.when:main" -when-bg = "when.when_bg:run_bg" - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 9fbcc358c8914b256edd044f5cd1abb533f65c9c Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 7 Apr 2026 14:58:07 +0200 Subject: [PATCH 33/57] temp: use static Lua library Change the code to install Lua libraries along with Python code as static resources --- lib/internal/multi_conds_run_task.py | 282 +++++++++++----------- lib/utility.py | 10 + lua/LIBRARY | 1 - lua/LIBRARY.md | 1 + support/mcrt_lib.lua => lua/_mcrt_lib.lua | 0 pyproject.toml | 14 +- when/when.py | 12 +- 7 files changed, 168 insertions(+), 152 deletions(-) delete mode 100644 lua/LIBRARY create mode 100644 lua/LIBRARY.md rename support/mcrt_lib.lua => lua/_mcrt_lib.lua (100%) diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index c048147..4638372 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -71,134 +71,134 @@ _MCRT_EXTRA_DELAY = 15 -_LUA_LIBRARY = """\ --- mcrt: multiple conditions to run a task --- NOTE: internals have a double underscore and will not be directly --- used in the scripts that require the library - - -local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] -local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] - - --- the library itself -local mcrt = {} - - --- mangle names -local function __mangle_name(name) - return ":" .. name .. ":" -end - --- find whether or not a (mangled) name is present in a string -local function __has_name(name, s) - return string.find(s, __mangle_name(name)) ~= nil -end - --- remove a name from a string -local function __rm_name(name, s) - return string.gsub(s, __mangle_name(name), "") -end - --- add a name to a string -local function __add_name(name, s) - return s .. __mangle_name(name) -end - - --- actual library functions - --- initialization just resets the shared state -function mcrt.initialize() - if sync.lock(__MCRT_LOCK, 1.0) then - local ok, msg = pcall(function() - local sst = {{ }} - sst.persistent = "" - sharedstate.save(__MCRT_PERSIST, sst) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) - res = false - end - sync.release(__MCRT_LOCK) - return res - else - log.debug("could not acquire shared state for condition confluence") - return false - end - --- set the condition bearing the provided name to verified -function mcrt.set_condition_verified(cond_name) - if sync.lock(__MCRT_LOCK, 1.0) then - local ok, msg = pcall(function() - local sst = sharedstate.load(__MCRT_PERSIST) - local persistent = sst.persistent - if persistent ~= nil then - if not __has_name(cond_name, persistent) then - persistent = __add_name(cond_name, persistent) - end - else - persistent = __add_name(cond_name, "") - end - sst.persistent = persistent - sharedstate.save(__MCRT_PERSIST, sst) - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) - res = false - end - sync.release(__MCRT_LOCK) - return res - else - log.debug("could not acquire shared state for condition confluence") - return false - end -end - --- check whether the provided conditions are all verified, and if so remove --- their names prior to returning true; otherwise return false -function mcrt.check_conditions_verified(cond_names) - if sync.lock(__MCRT_LOCK, 1.0) then - res = true - local ok, msg = pcall(function() - local sst = sharedstate.load(__MCRT_PERSIST) - local persistent = sst.persistent - if persistent ~= nil then - for _, name in ipairs(cond_names) do - if not __has_name(name, persistent) then - res = false - break - end - end - if res then - for _, name in ipairs(cond_names) do - persistent = __rm_name(name, persistent) - end - sst.persistent = persistent - sharedstate.save(__MCRT_PERSIST, sst) - end - else - res = false - end - end) - if not ok then - log.debug("the following error occurred: " .. (msg or "")) - res = false - end - sync.release(__MCRT_LOCK) - return res - else - log.debug("could not acquire shared state for condition confluence") - return false - end -end - --- return the library table -return mcrt - - --- end. -""" +# _LUA_LIBRARY = """\ +# -- mcrt: multiple conditions to run a task +# -- NOTE: internals have a double underscore and will not be directly +# -- used in the scripts that require the library + + +# local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] +# local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] + + +# -- the library itself +# local mcrt = {} + + +# -- mangle names +# local function __mangle_name(name) +# return ":" .. name .. ":" +# end + +# -- find whether or not a (mangled) name is present in a string +# local function __has_name(name, s) +# return string.find(s, __mangle_name(name)) ~= nil +# end + +# -- remove a name from a string +# local function __rm_name(name, s) +# return string.gsub(s, __mangle_name(name), "") +# end + +# -- add a name to a string +# local function __add_name(name, s) +# return s .. __mangle_name(name) +# end + + +# -- actual library functions + +# -- initialization just resets the shared state +# function mcrt.initialize() +# if sync.lock(__MCRT_LOCK, 1.0) then +# local ok, msg = pcall(function() +# local sst = {{ }} +# sst.persistent = "" +# sharedstate.save(__MCRT_PERSIST, sst) +# end) +# if not ok then +# log.debug("the following error occurred: " .. (msg or "")) +# res = false +# end +# sync.release(__MCRT_LOCK) +# return res +# else +# log.debug("could not acquire shared state for condition confluence") +# return false +# end + +# -- set the condition bearing the provided name to verified +# function mcrt.set_condition_verified(cond_name) +# if sync.lock(__MCRT_LOCK, 1.0) then +# local ok, msg = pcall(function() +# local sst = sharedstate.load(__MCRT_PERSIST) +# local persistent = sst.persistent +# if persistent ~= nil then +# if not __has_name(cond_name, persistent) then +# persistent = __add_name(cond_name, persistent) +# end +# else +# persistent = __add_name(cond_name, "") +# end +# sst.persistent = persistent +# sharedstate.save(__MCRT_PERSIST, sst) +# end) +# if not ok then +# log.debug("the following error occurred: " .. (msg or "")) +# res = false +# end +# sync.release(__MCRT_LOCK) +# return res +# else +# log.debug("could not acquire shared state for condition confluence") +# return false +# end +# end + +# -- check whether the provided conditions are all verified, and if so remove +# -- their names prior to returning true; otherwise return false +# function mcrt.check_conditions_verified(cond_names) +# if sync.lock(__MCRT_LOCK, 1.0) then +# res = true +# local ok, msg = pcall(function() +# local sst = sharedstate.load(__MCRT_PERSIST) +# local persistent = sst.persistent +# if persistent ~= nil then +# for _, name in ipairs(cond_names) do +# if not __has_name(name, persistent) then +# res = false +# break +# end +# end +# if res then +# for _, name in ipairs(cond_names) do +# persistent = __rm_name(name, persistent) +# end +# sst.persistent = persistent +# sharedstate.save(__MCRT_PERSIST, sst) +# end +# else +# res = false +# end +# end) +# if not ok then +# log.debug("the following error occurred: " .. (msg or "")) +# res = false +# end +# sync.release(__MCRT_LOCK) +# return res +# else +# log.debug("could not acquire shared state for condition confluence") +# return false +# end +# end + +# -- return the library table +# return mcrt + + +# -- end. +# """ # this is the prefix for all of our item names @@ -219,18 +219,18 @@ # utility to install the Lua library: it also reserves the library file name # so that it is not overwritten by the user in case he decides to install # a Lua library of choice -def install_lib(): - libfilename = f"{_MCRT_LIBRARY}.lua" - s = os.path.join(get_luadir(), libfilename) - if not os.path.exists(s): - lua_library = _LUA_LIBRARY.format( - "{}", # this replaces the brackets! - MCRT_SHAREDSTATE_LOCK=_ITEM_PREFIX + _MCRT_LOCK, - MCRT_PERSIST_FILE=_ITEM_PREFIX + _MCRT_PERSIST, - ) - with open(s, "w") as f: - f.write(lua_library) - install_lua.reserve_lua(libfilename) +# def install_lib(): +# libfilename = f"{_MCRT_LIBRARY}.lua" +# s = os.path.join(get_luadir(), libfilename) +# if not os.path.exists(s): +# lua_library = _LUA_LIBRARY.format( +# "{}", # this replaces the brackets! +# MCRT_SHAREDSTATE_LOCK=_ITEM_PREFIX + _MCRT_LOCK, +# MCRT_PERSIST_FILE=_ITEM_PREFIX + _MCRT_PERSIST, +# ) +# with open(s, "w") as f: +# f.write(lua_library) +# install_lua.reserve_lua(libfilename) # the following items are the specific ones that implement the confluence @@ -514,7 +514,7 @@ def is_confluence_cond(c: cond.Condition) -> bool: "updater_name", "is_confluent_cond", "is_confluence_cond", - "install_lib", + # "install_lib", "ConfluenceCondition", "form_ConfluenceCondition", ] diff --git a/lib/utility.py b/lib/utility.py index d0cd6a9..c0e9b66 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -301,15 +301,25 @@ def get_luadir() -> str: return luadir +# get directory where the application is installed +def get_app_basedir(): + return AppConfig.get("WHEN_BASEDIR") + + # construct Lua path def get_lua_path() -> str: luabase = get_luadir() + appbase = get_app_basedir() + assert(isinstance(appbase, str)) + lualib = os.path.join(appbase, "lua") ps = os.path.sep return ( ";".join( [ "?", "?.lua", + f"{lualib}{ps}?", + f"{lualib}{ps}?.lua", f"{luabase}{ps}?", f"{luabase}{ps}?.lua", f"{luabase}{ps}?{ps}?", diff --git a/lua/LIBRARY b/lua/LIBRARY deleted file mode 100644 index 3247351..0000000 --- a/lua/LIBRARY +++ /dev/null @@ -1 +0,0 @@ -This directory contains Lua libraty files and their checksums \ No newline at end of file diff --git a/lua/LIBRARY.md b/lua/LIBRARY.md new file mode 100644 index 0000000..2dd0723 --- /dev/null +++ b/lua/LIBRARY.md @@ -0,0 +1 @@ +# This directory contains Lua library files diff --git a/support/mcrt_lib.lua b/lua/_mcrt_lib.lua similarity index 100% rename from support/mcrt_lib.lua rename to lua/_mcrt_lib.lua diff --git a/pyproject.toml b/pyproject.toml index 9881cdd..9297895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,13 @@ license = 'BSD 3-Clause "New" or "Revised" License' readme = "README.md" requires-python = ">=3.10" +[project.scripts] +when = "when.when:main" + +[project.gui-scripts] +when-bg = "when.when_bg:run_bg" + + [tool.poetry] packages = [ { include = "lib" }, @@ -18,13 +25,6 @@ include = [ "lua", ] -[project.scripts] -when = "when.when:main" - -[project.gui-scripts] -when-bg = "when.when_bg:run_bg" - - [tool.poetry.dependencies] python = "^3.10" chlorophyll = "^0.4.2" diff --git a/when/when.py b/when/when.py index 58f5c6b..4894f3b 100644 --- a/when/when.py +++ b/when/when.py @@ -25,6 +25,7 @@ get_whenever_version, check_whenever_version, get_luadir, + get_lua_initscript, get_tempdir, get_scriptsdir, get_appdata, @@ -59,6 +60,9 @@ # this is used to enable exception handling too DEBUG = AppConfig.get("DEBUG") +# add current base directory to the configuration store +AppConfig.set("WHEN_BASEDIR", BASEDIR) + # the following class is used to create an invisible window that actually # implements the tkinter main loop, and that reads virtual events to display @@ -309,6 +313,11 @@ def prepare_environment() -> None: _ = get_luadir() except Exception as e: exit_error(e) + # create the Lua initialization script if it does not exist + try: + _ = get_lua_initscript() + except Exception as e: + exit_error(e) # ... @@ -661,9 +670,6 @@ def main() -> None: AppConfig.set("APPDATA", default_appdata) AppConfig.set("WHENEVER", default_whenever) - # other initialization actions - mcrt.install_lib() - # main parser parser = argparse.ArgumentParser( description=CLI_APP_DESCRIPTION, From 91d8cb3d0cb353884171f787a73b958a4661bd00 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 7 Apr 2026 17:05:58 +0200 Subject: [PATCH 34/57] temp: try to better specify package data --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9297895..b55f308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = [ { include = "when" }, ] include = [ - "lua", + { path ="lua/*.lua", format = ["sdist", "wheel"] }, ] [tool.poetry.dependencies] From 4c1d3badee6ca5e4e06246e73865ae2ae9adbe61 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Tue, 7 Apr 2026 22:22:05 +0200 Subject: [PATCH 35/57] refactor: provide Lua MCRT lib as static resource This commit includes the `.lua` library files as a static resource within the When package: this helps to avoid that the library is inadvertently tampered with, and to reserve a name; the way this is achieved also serves as an example for other possible libraries that When could need in the future --- lib/forms/cond.py | 2 + lib/internal/multi_conds_run_task.py | 153 --------------------------- {lua => lib/lua}/LIBRARY.md | 0 lib/lua/__init__.py | 11 ++ {lua => lib/lua}/_mcrt_lib.lua | 7 +- lib/utility.py | 12 ++- pyproject.toml | 3 - 7 files changed, 26 insertions(+), 162 deletions(-) rename {lua => lib/lua}/LIBRARY.md (100%) create mode 100644 lib/lua/__init__.py rename {lua => lib/lua}/_mcrt_lib.lua (96%) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index 3914f8d..33b73b4 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -239,12 +239,14 @@ def __init__(self, title, tasks_available, item=None): def add_task(self) -> None: elem = self.data_get("@choose_task") + self._updatedata() if elem: self._tasks.append(elem) self._updateform() def del_task(self) -> None: elem = self.data_get("@tasks_selection") + self._updatedata() if elem: idx = int(elem[0]) del self._tasks[idx] diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 4638372..5418f93 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -38,22 +38,18 @@ from ..i18n.strings import * -import os from tomlkit import items, table import tkinter as tk import ttkbootstrap as ttk import ttkbootstrap.constants as ttkc -from typing import List, Tuple - from ..forms.ui import * # since a condition is defined, the base form is the one for conditions from ..forms.cond import form_Condition from ..utility import ( - get_luadir, get_lua_initscript, get_lua_path, get_private_item_name_prefix, @@ -65,141 +61,10 @@ # constants -_MCRT_LOCK = "Confluence_LOCK" -_MCRT_PERSIST = "Confluence_STATE" _MCRT_LIBRARY = "_mcrt_lib" _MCRT_EXTRA_DELAY = 15 -# _LUA_LIBRARY = """\ -# -- mcrt: multiple conditions to run a task -# -- NOTE: internals have a double underscore and will not be directly -# -- used in the scripts that require the library - - -# local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] -# local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] - - -# -- the library itself -# local mcrt = {} - - -# -- mangle names -# local function __mangle_name(name) -# return ":" .. name .. ":" -# end - -# -- find whether or not a (mangled) name is present in a string -# local function __has_name(name, s) -# return string.find(s, __mangle_name(name)) ~= nil -# end - -# -- remove a name from a string -# local function __rm_name(name, s) -# return string.gsub(s, __mangle_name(name), "") -# end - -# -- add a name to a string -# local function __add_name(name, s) -# return s .. __mangle_name(name) -# end - - -# -- actual library functions - -# -- initialization just resets the shared state -# function mcrt.initialize() -# if sync.lock(__MCRT_LOCK, 1.0) then -# local ok, msg = pcall(function() -# local sst = {{ }} -# sst.persistent = "" -# sharedstate.save(__MCRT_PERSIST, sst) -# end) -# if not ok then -# log.debug("the following error occurred: " .. (msg or "")) -# res = false -# end -# sync.release(__MCRT_LOCK) -# return res -# else -# log.debug("could not acquire shared state for condition confluence") -# return false -# end - -# -- set the condition bearing the provided name to verified -# function mcrt.set_condition_verified(cond_name) -# if sync.lock(__MCRT_LOCK, 1.0) then -# local ok, msg = pcall(function() -# local sst = sharedstate.load(__MCRT_PERSIST) -# local persistent = sst.persistent -# if persistent ~= nil then -# if not __has_name(cond_name, persistent) then -# persistent = __add_name(cond_name, persistent) -# end -# else -# persistent = __add_name(cond_name, "") -# end -# sst.persistent = persistent -# sharedstate.save(__MCRT_PERSIST, sst) -# end) -# if not ok then -# log.debug("the following error occurred: " .. (msg or "")) -# res = false -# end -# sync.release(__MCRT_LOCK) -# return res -# else -# log.debug("could not acquire shared state for condition confluence") -# return false -# end -# end - -# -- check whether the provided conditions are all verified, and if so remove -# -- their names prior to returning true; otherwise return false -# function mcrt.check_conditions_verified(cond_names) -# if sync.lock(__MCRT_LOCK, 1.0) then -# res = true -# local ok, msg = pcall(function() -# local sst = sharedstate.load(__MCRT_PERSIST) -# local persistent = sst.persistent -# if persistent ~= nil then -# for _, name in ipairs(cond_names) do -# if not __has_name(name, persistent) then -# res = false -# break -# end -# end -# if res then -# for _, name in ipairs(cond_names) do -# persistent = __rm_name(name, persistent) -# end -# sst.persistent = persistent -# sharedstate.save(__MCRT_PERSIST, sst) -# end -# else -# res = false -# end -# end) -# if not ok then -# log.debug("the following error occurred: " .. (msg or "")) -# res = false -# end -# sync.release(__MCRT_LOCK) -# return res -# else -# log.debug("could not acquire shared state for condition confluence") -# return false -# end -# end - -# -- return the library table -# return mcrt - - -# -- end. -# """ - # this is the prefix for all of our item names _ITEM_PREFIX = get_private_item_name_prefix() + "MCRT_" @@ -216,23 +81,6 @@ """ -# utility to install the Lua library: it also reserves the library file name -# so that it is not overwritten by the user in case he decides to install -# a Lua library of choice -# def install_lib(): -# libfilename = f"{_MCRT_LIBRARY}.lua" -# s = os.path.join(get_luadir(), libfilename) -# if not os.path.exists(s): -# lua_library = _LUA_LIBRARY.format( -# "{}", # this replaces the brackets! -# MCRT_SHAREDSTATE_LOCK=_ITEM_PREFIX + _MCRT_LOCK, -# MCRT_PERSIST_FILE=_ITEM_PREFIX + _MCRT_PERSIST, -# ) -# with open(s, "w") as f: -# f.write(lua_library) -# install_lua.reserve_lua(libfilename) - - # the following items are the specific ones that implement the confluence @@ -514,7 +362,6 @@ def is_confluence_cond(c: cond.Condition) -> bool: "updater_name", "is_confluent_cond", "is_confluence_cond", - # "install_lib", "ConfluenceCondition", "form_ConfluenceCondition", ] diff --git a/lua/LIBRARY.md b/lib/lua/LIBRARY.md similarity index 100% rename from lua/LIBRARY.md rename to lib/lua/LIBRARY.md diff --git a/lib/lua/__init__.py b/lib/lua/__init__.py new file mode 100644 index 0000000..f2bb99b --- /dev/null +++ b/lib/lua/__init__.py @@ -0,0 +1,11 @@ +# Lua library static source files + +import os +import sys + +def lua_library_path() -> str: + f = sys.modules[__name__].__file__ + assert(isinstance(f, str)) + return os.path.dirname(os.path.abspath(f)) + +# end. diff --git a/lua/_mcrt_lib.lua b/lib/lua/_mcrt_lib.lua similarity index 96% rename from lua/_mcrt_lib.lua rename to lib/lua/_mcrt_lib.lua index 718a579..6df86fb 100644 --- a/lua/_mcrt_lib.lua +++ b/lib/lua/_mcrt_lib.lua @@ -3,8 +3,8 @@ -- used in the scripts that require the library -local __MCRT_LOCK = [[{MCRT_SHAREDSTATE_LOCK}]] -local __MCRT_PERSIST = [[{MCRT_SHAREDSTATE_PERSIST}]] +local __MCRT_LOCK = "__When__private__MCRT_LockState" +local __MCRT_PERSIST = "__When__private__MCRT_SharedState" -- the library itself @@ -38,7 +38,7 @@ end function mcrt.initialize() if sync.lock(__MCRT_LOCK, 1.0) then local ok, msg = pcall(function() - local sst = {{ }} + local sst = {} sst.persistent = "" sharedstate.save(__MCRT_PERSIST, sst) end) @@ -52,6 +52,7 @@ function mcrt.initialize() log.debug("could not acquire shared state for condition confluence") return false end +end -- set the condition bearing the provided name to verified function mcrt.set_condition_verified(cond_name) diff --git a/lib/utility.py b/lib/utility.py index c0e9b66..5f16094 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -32,6 +32,7 @@ from .repocfg import AppConfig from .runner.logger import Logger from .platform import is_windows, is_linux, is_mac +from .lua import lua_library_path # lowest whenever version supported @@ -309,9 +310,7 @@ def get_app_basedir(): # construct Lua path def get_lua_path() -> str: luabase = get_luadir() - appbase = get_app_basedir() - assert(isinstance(appbase, str)) - lualib = os.path.join(appbase, "lua") + lualib = lua_library_path() ps = os.path.sep return ( ";".join( @@ -320,6 +319,8 @@ def get_lua_path() -> str: "?.lua", f"{lualib}{ps}?", f"{lualib}{ps}?.lua", + f"{lualib}{ps}?{ps}?", + f"{lualib}{ps}?{ps}?.lua", f"{luabase}{ps}?", f"{luabase}{ps}?.lua", f"{luabase}{ps}?{ps}?", @@ -349,6 +350,11 @@ def get_lua_initscript() -> str: return init +# return the path to Lua static library shipped with When +def get_lua_staticlib_path() -> str: + return lua_library_path() + + # save a script to the scripts directory and make it executable: possible # existing files are overwritten without confirmation as the scripts folder # should be completely managed by When diff --git a/pyproject.toml b/pyproject.toml index b55f308..74f06d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,6 @@ packages = [ { include = "lib" }, { include = "when" }, ] -include = [ - { path ="lua/*.lua", format = ["sdist", "wheel"] }, -] [tool.poetry.dependencies] python = "^3.10" From 07d89af0c49b71b2f006ae91442ae59b14d48af0 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 11:34:21 +0200 Subject: [PATCH 36/57] style: accept many linter suggestions Use `ruff` as an extra linting step, many suggestions accepted, some of them ignored because they would break linearity --- lib/extra/c_session_locked_win32.py | 2 +- lib/extra/i18n/extra_locale.py | 1 - lib/forms/cfgform.py | 1 - lib/forms/cond_command.py | 3 --- lib/forms/cond_lua.py | 2 -- lib/forms/menubox.py | 3 +-- lib/forms/task_command.py | 1 - lib/forms/task_lua.py | 2 -- lib/forms/ui.py | 1 - lib/i18n/localizer.py | 4 ++-- lib/i18n/strings.py | 2 +- lib/internal/multi_conds_run_task.py | 1 - lib/internal/reset_conds_on_resume.py | 3 --- lib/items/cond.py | 2 +- lib/items/event.py | 2 +- lib/items/itemhelp.py | 4 ++-- lib/items/task.py | 2 +- lib/toolbox/check_config.py | 18 +++++++++--------- lib/toolbox/create_shortcuts.py | 4 +--- lib/toolbox/fix_config.py | 2 +- lib/toolbox/install_whenever.py | 2 +- lib/trayapp.py | 1 - 22 files changed, 22 insertions(+), 41 deletions(-) diff --git a/lib/extra/c_session_locked_win32.py b/lib/extra/c_session_locked_win32.py index 38ed211..f15cc2a 100644 --- a/lib/extra/c_session_locked_win32.py +++ b/lib/extra/c_session_locked_win32.py @@ -130,7 +130,7 @@ def check_tags(cls, tags): missing.append("check_frequency") elif ( not isinstance(check_frequency, str) - and not check_frequency in CHECK_EXTRA_DELAY.keys() + and check_frequency not in CHECK_EXTRA_DELAY.keys() ): errors.append("check_frequency") if errors or missing: diff --git a/lib/extra/i18n/extra_locale.py b/lib/extra/i18n/extra_locale.py index a549d31..01634c6 100644 --- a/lib/extra/i18n/extra_locale.py +++ b/lib/extra/i18n/extra_locale.py @@ -1,7 +1,6 @@ # get localized versions of strings for extra items UIs -import os from importlib import import_module from ...i18n.localizer import get_locale diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index 3377c27..4863794 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -9,7 +9,6 @@ from ttkbootstrap_icons_mat import MatIcon as Icons from ..i18n.strings import * -from ..icons import APP_ICON32 as APP_ICON from .ui import * from .colors import * diff --git a/lib/forms/cond_command.py b/lib/forms/cond_command.py index 0585fab..339be7a 100644 --- a/lib/forms/cond_command.py +++ b/lib/forms/cond_command.py @@ -1,9 +1,6 @@ # command condition form -import sys -import os import re -import shutil from shlex import split as arg_split, quote from os.path import normpath diff --git a/lib/forms/cond_lua.py b/lib/forms/cond_lua.py index ffd2e57..0fb4675 100644 --- a/lib/forms/cond_lua.py +++ b/lib/forms/cond_lua.py @@ -1,7 +1,6 @@ # Lua condition form import re -import os import tkinter as tk import ttkbootstrap as ttk import ttkbootstrap.constants as ttkc @@ -15,7 +14,6 @@ from ..utility import ( guess_typed_value, get_editor_theme, - get_luadir, get_lua_path, get_lua_initscript, ) diff --git a/lib/forms/menubox.py b/lib/forms/menubox.py index cb80321..6aa21c0 100644 --- a/lib/forms/menubox.py +++ b/lib/forms/menubox.py @@ -5,7 +5,6 @@ import tkinter as tk import ttkbootstrap as ttk -from ttkbootstrap.icons import Icon from ttkbootstrap_icons_mat import MatIcon as Icons from PIL import ImageTk @@ -17,7 +16,7 @@ from ..repocfg import AppConfig -from ..utility import get_image, get_icon +from ..utility import get_image # default UI values diff --git a/lib/forms/task_command.py b/lib/forms/task_command.py index 2d2d150..5112205 100644 --- a/lib/forms/task_command.py +++ b/lib/forms/task_command.py @@ -1,6 +1,5 @@ # command task form -import sys from os.path import normpath from ..i18n.strings import * diff --git a/lib/forms/task_lua.py b/lib/forms/task_lua.py index eb72717..d76eced 100644 --- a/lib/forms/task_lua.py +++ b/lib/forms/task_lua.py @@ -1,7 +1,6 @@ # Lua task form import re -import os import tkinter as tk import ttkbootstrap as ttk import ttkbootstrap.constants as ttkc @@ -15,7 +14,6 @@ from ..utility import ( guess_typed_value, get_editor_theme, - get_luadir, get_lua_path, get_lua_initscript, ) diff --git a/lib/forms/ui.py b/lib/forms/ui.py index 59ce7cb..3eaefbf 100644 --- a/lib/forms/ui.py +++ b/lib/forms/ui.py @@ -5,7 +5,6 @@ # from tkinter import ttk import ttkbootstrap as ttk from ttkbootstrap import dialogs -import ttkbootstrap.constants as ttkc from ttkbootstrap.icons import Icon from ttkbootstrap_icons_mat import MatIcon as Icons diff --git a/lib/i18n/localizer.py b/lib/i18n/localizer.py index 10d985a..a632b5a 100644 --- a/lib/i18n/localizer.py +++ b/lib/i18n/localizer.py @@ -381,7 +381,7 @@ "dutch_netherlands": "nl", "nl_nl": "nl", "nl-nl": "nl", # Norwegian - "norwegian_bokmål": "no", "norwegian_nynorsk": "no", "no_no": "no", "no-no": "no", + "norwegian_bokmål": "no", "norwegian_nynorsk": "no", "norwegian_norway": "no", "no_no": "no", "no-no": "no", # Polish @@ -464,7 +464,7 @@ def get_locale(force_locale=None): if cur_locale in locale_map.keys(): _CURRENT_SHORT_LOCALE = locale_map[cur_locale] return locale_map[cur_locale] - except: + except Exception: pass return _CURRENT_SHORT_LOCALE diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index cb36a68..0e06c40 100644 --- a/lib/i18n/strings.py +++ b/lib/i18n/strings.py @@ -61,7 +61,7 @@ def which_locale() -> str | None: if _short_locale is not None: try: exec(f"from .strings_{_short_locale} import *") - except: + except Exception: pass diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 5418f93..4c8cb6e 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -57,7 +57,6 @@ ) from ..items import cond, task_lua, cond_lua, cond_interval -from ..toolbox import install_lua # constants diff --git a/lib/internal/reset_conds_on_resume.py b/lib/internal/reset_conds_on_resume.py index 85ff26a..3b8452e 100644 --- a/lib/internal/reset_conds_on_resume.py +++ b/lib/internal/reset_conds_on_resume.py @@ -7,9 +7,6 @@ # - a private condition that the above event triggers # - a private task that runs the whenever internal `reset_conditions` command -import sys -import os - from ..utility import get_private_item_name_prefix from ..platform import is_windows, is_linux, is_mac diff --git a/lib/items/cond.py b/lib/items/cond.py index 4575f76..47c4608 100644 --- a/lib/items/cond.py +++ b/lib/items/cond.py @@ -131,7 +131,7 @@ def check_in_document(cls, name: str, doc: TOMLDocument, tasks=None) -> bool: except Exception: raise ConfigurationError( name, - message=f"condition not found in the configuration", + message="condition not found in the configuration", ) # check that elem and elemd are not None assert elem is not None and elemd is not None diff --git a/lib/items/event.py b/lib/items/event.py index b2934ea..c4f7cd5 100644 --- a/lib/items/event.py +++ b/lib/items/event.py @@ -109,7 +109,7 @@ def check_in_document( except Exception: raise ConfigurationError( name, - message=f"event not found in the configuration", + message="event not found in the configuration", ) # check that elem and elemd are not None assert elem is not None and elemd is not None diff --git a/lib/items/itemhelp.py b/lib/items/itemhelp.py index b15c13c..328dffb 100644 --- a/lib/items/itemhelp.py +++ b/lib/items/itemhelp.py @@ -59,7 +59,7 @@ def get(self, entry: str, mandatory=False, default=None): name, entry_name=entry, item_line=self._table_line, - message=f"entry must be provided", + message="entry must be provided", ) return v @@ -73,7 +73,7 @@ def get_check( name, entry_name=entry, item_line=self._table_line, - message=f"item name invalid or not provided", + message="item name invalid or not provided", ) if not check(v): raise ConfigurationError( diff --git a/lib/items/task.py b/lib/items/task.py index edfd494..b9a5058 100644 --- a/lib/items/task.py +++ b/lib/items/task.py @@ -94,7 +94,7 @@ def check_in_document(cls, name: str, doc: TOMLDocument) -> bool: except Exception: raise ConfigurationError( name, - message=f"condition not found in the configuration", + message="task not found in the configuration", ) # check that elem and elemd are not None assert elem is not None and elemd is not None diff --git a/lib/toolbox/check_config.py b/lib/toolbox/check_config.py index f654cb2..dcdd8fd 100644 --- a/lib/toolbox/check_config.py +++ b/lib/toolbox/check_config.py @@ -37,7 +37,7 @@ def check_globals(doc: TOMLDocument) -> list[ConfigurationError]: if not isinstance(tags, items.Table): errors.append( ConfigurationError( - "(globals)", key, message=f"the tags entry must be a dictionary" + "(globals)", key, message="the tags entry must be a dictionary" ) ) else: @@ -73,7 +73,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: ConfigurationError( "task", name, - message=f"the tags entry must be a dictionary", + message="the tags entry must be a dictionary", ) ) else: @@ -96,7 +96,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: err = ConfigurationError(name, message=f"malformed task {name}") else: # TODO: report item line in TOML document - err = ConfigurationError("", message=f"unnamed task found") + err = ConfigurationError("", message="unnamed task found") if err is not None: errors.append(err) # retrieve conditions and store names of event based ones @@ -114,7 +114,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: ConfigurationError( "condition", name, - message=f"the tags entry must be a dictionary", + message="the tags entry must be a dictionary", ) ) else: @@ -141,7 +141,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: else: # TODO: report item line in TOML document err = ConfigurationError( - "", message=f"unnamed condition found" + "", message="unnamed condition found" ) if err is not None: errors.append(err) @@ -159,7 +159,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: ConfigurationError( "event", name, - message=f"the tags entry must be a dictionary", + message="the tags entry must be a dictionary", ) ) else: @@ -182,7 +182,7 @@ def check_items(doc: TOMLDocument) -> list[ConfigurationError]: err = ConfigurationError(name, message=f"malformed event {name}") else: # TODO: report item line in TOML document - err = ConfigurationError("", message=f"unnamed event found") + err = ConfigurationError("", message="unnamed event found") if err is not None: errors.append(err) return errors @@ -217,12 +217,12 @@ def check_config_file(filename, verbose=True) -> bool: except ParseError as err: if verbose: write_error(CLI_ERR_CONFIG_INVALID % (filename, str(err))) - except Exception as err: + except Exception as _err: if verbose: # uncomment the following to have the exception reported # if AppConfig.get("DEBUG"): # import traceback - # traceback.print_exception(err) + # traceback.print_exception(_err) write_error(CLI_ERR_ERROR_GENERIC) # if we are here the check was not positive return False diff --git a/lib/toolbox/create_shortcuts.py b/lib/toolbox/create_shortcuts.py index aefbb98..5b0001b 100644 --- a/lib/toolbox/create_shortcuts.py +++ b/lib/toolbox/create_shortcuts.py @@ -52,7 +52,7 @@ def create_icon(verbose=False) -> None | str: try: with open(target, "wb") as f: f.write(base64.b64decode(icon)) - except Exception as e: + except Exception: if verbose: write_error(CLI_ERR_CANNOT_CREATE_FILE % icon_filename) return None @@ -176,10 +176,8 @@ def create_shortcuts(main_script, desktop=True, autostart=True, verbose=False) - # first: check whether a `pipx`-style install is available if is_windows(): exename = "when-bg.exe" - interpreter = os.path.join(sys.exec_prefix, "pythonw.exe") else: exename = "when" - interpreter = sys.executable p = os.path.expanduser(os.path.join("~", ".local", "bin")) target = os.path.join(p, exename) if not (os.path.isfile(target) and os.access(target, os.X_OK)): diff --git a/lib/toolbox/fix_config.py b/lib/toolbox/fix_config.py index 7bcce39..2aa4288 100644 --- a/lib/toolbox/fix_config.py +++ b/lib/toolbox/fix_config.py @@ -299,7 +299,7 @@ def fix_config_file(filename, verbose=True, backup=True) -> bool: console.print(CLI_MSG_WRITE_NEW_CONFIG % filename) # type: ignore write_whenever_config(filename, tasks, conditions, events, globals) return True - except Exception as e: + except Exception: write_warning(CLI_ERR_CANNOT_FIX_CONFIG % filename) return False diff --git a/lib/toolbox/install_whenever.py b/lib/toolbox/install_whenever.py index 757d6e5..ec1ab5d 100644 --- a/lib/toolbox/install_whenever.py +++ b/lib/toolbox/install_whenever.py @@ -96,7 +96,7 @@ def get_whenever_download_metadata(verbose=False): if verbose: write_error(CLI_ERR_NO_SUITABLE_BINARY) return None - except ValueError as e: # too many values to unpack + except ValueError: # too many values to unpack if verbose: write_error(f"malformed `{CHECKSUM_FILE}` file") return None diff --git a/lib/trayapp.py b/lib/trayapp.py index 9a2c7c2..dd7652a 100644 --- a/lib/trayapp.py +++ b/lib/trayapp.py @@ -3,7 +3,6 @@ from lib.i18n.strings import * import pystray -import sys import threading from .utility import get_image From 2002235edb54fa00ae9211d148c346163d09c53e Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 13:22:57 +0200 Subject: [PATCH 37/57] fix: introduce a hidden startup condition This condition, functional to `When` in particular, ensures to be always executed at the first tick: normal interval conditions have a lower interval limit of 1 second, while this uses a zero-length interval, as supported by `whenever`; this actually solves the problem that raised issue #190 initially, as now the `--fix-config` tool will not complain on private `When` items --- lib/i18n/strings_base.py | 3 +- lib/i18n/strings_de.py | 1 + lib/i18n/strings_en.py | 3 +- lib/i18n/strings_fr.py | 1 + lib/i18n/strings_it.py | 1 + lib/internal/cond_startup.py | 106 +++++++++++++++++++++++++++ lib/internal/multi_conds_run_task.py | 15 ++-- lib/items/item.py | 2 + 8 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 lib/internal/cond_startup.py diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index 6a629ab..0d8e9fc 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -17,7 +17,8 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" -ITEM_COND_MCRT = "Condition Activated By Other Conditions" +ITEM_COND_MCRT = "Condition Activated Through Other Conditions" +ITEM_COND_STARTUP = "Startup Condition" ITEM_EVENT = "Event" ITEM_EVENT_FSCHANGE = "Filesystem Monitoring Based Event" diff --git a/lib/i18n/strings_de.py b/lib/i18n/strings_de.py index 97d8da5..b48a4a8 100644 --- a/lib/i18n/strings_de.py +++ b/lib/i18n/strings_de.py @@ -18,6 +18,7 @@ ITEM_COND_WMI = "WMI-Query basierte Kondition" ITEM_COND_EVENT = "Ereignis basierte Kondition" ITEM_COND_MCRT = "Kondition durch andere Konditionen aktivierte" +ITEM_COND_STARTUP = "Anfangkondition" ITEM_EVENT = "Ereignis" ITEM_EVENT_FSCHANGE = "Dateisystem Überwachung basiertes Ereignis" diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index 6a629ab..0d8e9fc 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -17,7 +17,8 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" -ITEM_COND_MCRT = "Condition Activated By Other Conditions" +ITEM_COND_MCRT = "Condition Activated Through Other Conditions" +ITEM_COND_STARTUP = "Startup Condition" ITEM_EVENT = "Event" ITEM_EVENT_FSCHANGE = "Filesystem Monitoring Based Event" diff --git a/lib/i18n/strings_fr.py b/lib/i18n/strings_fr.py index d67ec3e..7894873 100644 --- a/lib/i18n/strings_fr.py +++ b/lib/i18n/strings_fr.py @@ -18,6 +18,7 @@ ITEM_COND_WMI = "Condition basée sur une requête WMI" ITEM_COND_EVENT = "Condition basée sur un événement" ITEM_COND_MCRT = "Condition activée par autres conditions" +ITEM_COND_STARTUP = "Condition initiale" ITEM_EVENT = "Événement" ITEM_EVENT_FSCHANGE = "Événement basé sur la surveillance du système de fichiers" diff --git a/lib/i18n/strings_it.py b/lib/i18n/strings_it.py index a79defa..237410a 100644 --- a/lib/i18n/strings_it.py +++ b/lib/i18n/strings_it.py @@ -18,6 +18,7 @@ ITEM_COND_WMI = "Condizione basata su query WMI" ITEM_COND_EVENT = "Condizione basata su evento" ITEM_COND_MCRT = "Condizione attivata da altre condizioni" +ITEM_COND_STARTUP = "Condizione iniziale" ITEM_EVENT = "Evento" ITEM_EVENT_FSCHANGE = "Evento basato su monitoraggio del filesystem" diff --git a/lib/internal/cond_startup.py b/lib/internal/cond_startup.py new file mode 100644 index 0000000..3451012 --- /dev/null +++ b/lib/internal/cond_startup.py @@ -0,0 +1,106 @@ +# interval condition item + +from lib.i18n.strings import * + +from tomlkit import items, table + +import tkinter as tk +import ttkbootstrap as ttk + +from ..forms.ui import * + +# since a condition is defined, the base form is the one for conditions +from ..forms.cond import form_Condition +from ..utility import check_not_none, append_not_none + +from ..items.cond_interval import IntervalCondition +from ..items.itemhelp import CheckedTable, ConfigurationError + + +# an interval based condition +class StartupCondition(IntervalCondition): + + # availability at class level + item_type = "interval" + item_subtype = "startup" + item_hrtype = ITEM_COND_STARTUP + available = False + + def __init__(self, t: items.Table | None = None): + # first initialize the base class + IntervalCondition.__init__(self, t) + + # then set type (same as base), subtype and human readable name + self.type = self.item_type + self.subtype = self.item_subtype + self.hrtype = self.item_hrtype + if t: + assert t.get("type") == self.type + self.tags = t.get("tags", table()) + assert isinstance(self.tags, items.Table) + assert self.tags.get("subtype") == self.subtype + else: + self.tags = table() + self.tags.append("subtype", self.subtype) + self.interval_seconds = 0 + + def load_checking( + self, item: items.Table, item_line: int, tasks: list[str] | None = None + ) -> None: + try: + super().load_checking(item, item_line, tasks) + # ignore the erro on `interval_seconds` + except ConfigurationError as e: + if e.entry_name != "interval_seconds": + raise e + self.type = self.item_type + self.subtype = self.item_subtype + self.hrtype = self.item_hrtype + tab = CheckedTable(item, item_line) + assert tab.get_str("type") == self.type + # since this is used for checks only, check that this is still zero + self.interval_seconds = tab.get_int_between( + "interval_seconds", 0, 0, mandatory=True + ) + + @classmethod + def check_tags(cls, tags): + missing = [] + errors = [] + if errors or missing: + return (errors, missing) + return None + + def as_table(self): + return IntervalCondition.as_table(self) + + +# specialized subform: it will never be shown because the item is not available +class form_StartupCondition(form_Condition): + + def __init__(self, tasks_available, item=None): + if item: + assert isinstance(item, StartupCondition) + else: + item = StartupCondition() + super().__init__(UI_TITLE_EVENTCOND, tasks_available, item) + assert isinstance(self._item, StartupCondition) + + # build the UI: build widgets, arrange them in the box, bind data + + # client area + area = ttk.Frame(super().contents) + area.grid(row=0, column=0, sticky=tk.NSEW) + PAD = WIDGET_PADDING_PIXELS + + # widgets section + l_noParams = ttk.Label(area, text=UI_CAPTION_NOSPECIFICPARAMS) + + # arrange items in the grid + l_noParams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) + + # update the form + self._updateform() + + +# end. diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 4c8cb6e..d0a2985 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -58,6 +58,8 @@ from ..items import cond, task_lua, cond_lua, cond_interval +from .cond_startup import StartupCondition + # constants _MCRT_LIBRARY = "_mcrt_lib" @@ -121,17 +123,10 @@ def updater_name() -> str: # 3. initialization condition: it is a once-only condition that only is -# verified at the first tick, and runs the initialization task; using a -# zero duration here makes us quite confident that, if the user has not -# edited the configuration file by hand, this will be the first interval -# based condition that will be verified, because the interval condition -# definition form only accepts values strictly above zero, while whenever -# also accepts a zero duration in the configuration (which is in fact the -# way to create a condition that is verified at startup) -def initial_condition() -> cond_interval.IntervalCondition: - cond = cond_interval.IntervalCondition() +# verified at the first tick, and runs the initialization task +def initial_condition() -> StartupCondition: + cond = StartupCondition() cond.name = _COND_INITIALIZER - cond.interval_seconds = 0 # that is, at the first tick cond.tasks = [_TASK_INITIALIZER] return cond diff --git a/lib/items/item.py b/lib/items/item.py index 98c0035..89ab2dc 100644 --- a/lib/items/item.py +++ b/lib/items/item.py @@ -41,6 +41,7 @@ # this is a special case because it implies auxiliary item when used from ..internal.multi_conds_run_task import ConfluenceCondition, form_ConfluenceCondition +from ..internal.cond_startup import StartupCondition, form_StartupCondition # to dynamically determine nature of extra items from lib.items.task import Task @@ -96,6 +97,7 @@ # the following item are native to When (and not to whenever), thus not extras ('cond:lua:mcrt_confluence', ITEM_COND_MCRT, form_ConfluenceCondition, ConfluenceCondition), + ('cond:interval:startup', ITEM_COND_STARTUP, form_StartupCondition, StartupCondition), ] From 970360f45cf8f864a5cd12e531283a4a2cded604 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 13:23:52 +0200 Subject: [PATCH 38/57] style: remove unused imports --- lib/internal/cond_startup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/internal/cond_startup.py b/lib/internal/cond_startup.py index 3451012..9ae94ee 100644 --- a/lib/internal/cond_startup.py +++ b/lib/internal/cond_startup.py @@ -11,7 +11,6 @@ # since a condition is defined, the base form is the one for conditions from ..forms.cond import form_Condition -from ..utility import check_not_none, append_not_none from ..items.cond_interval import IntervalCondition from ..items.itemhelp import CheckedTable, ConfigurationError From 7918b410a11a7c20ed4a2a949633d354dd622b6f Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 13:49:07 +0200 Subject: [PATCH 39/57] fix(ui): disable confluence for confluence conditions Disable the `Activate further conditions` checkbox in the condition definition form for conditions that are already MCRT confluence conditions: since condition verifications sum up to concur in a confluence, it is useless and dangerous to build up confluences that depend on each other --- lib/forms/cond.py | 9 ++++++++- lib/internal/multi_conds_run_task.py | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/forms/cond.py b/lib/forms/cond.py index 33b73b4..757f5a4 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -204,7 +204,7 @@ def __init__(self, title, tasks_available, item=None): "@max_tasks_retries": clean_caption(UI_FORM_MAXTASKRETRIES_SC), } - # keep a list of task related widget to disable them when needed + # keep a list of task related widgets to disable them when needed self._task_elems = [ # current list l_tasks, @@ -231,6 +231,10 @@ def __init__(self, title, tasks_available, item=None): # finally set the item if item: + # disable the possibility to activate other condition if the + # associated condition is already a confluence + if isinstance(item, mcrt.ConfluenceCondition): + ck_mcrtActivateFurther.config(state=tk.DISABLED) self.set_item(item) else: self.reset_item() @@ -286,6 +290,9 @@ def _check_recurring(self) -> None: self._max_retries.config(state=tk.NORMAL) def _mcrt_confluent(self, force=False) -> None: + # since this reacts to click, bail out if item is a confluence + if isinstance(self._item, mcrt.ConfluenceCondition): + return # same consideration as above; this function also disables all task # related form widgets when the condition is set to be confluent if force: diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index d0a2985..5e31054 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -197,7 +197,6 @@ def check_tags(cls, tags): return None -# TODO: this form should disable the possibility to be confluent class form_ConfluenceCondition(form_Condition): # note that the available conditions should be filtered, the provided From dba823de5e87ab482f39bf60f2e296a5ff1d7fa3 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 15:04:39 +0200 Subject: [PATCH 40/57] fix(ui): adjust timestamp width in history box --- lib/forms/history.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/forms/history.py b/lib/forms/history.py index 7383ae5..39fe590 100644 --- a/lib/forms/history.py +++ b/lib/forms/history.py @@ -53,17 +53,17 @@ def __init__(self, wrapper, main=False): }, { "text": UI_FORM_HS_TIME, - "width": 130, + "width": 160, "anchor": ttkc.CENTER, }, { "text": UI_FORM_HS_TASK, - "width": 240, + "width": 225, "anchor": ttkc.W, }, { "text": UI_FORM_HS_TRIGGER, - "width": 240, + "width": 225, "anchor": ttkc.W, }, { From d3b924d64bc30c43ef809a2e87badc84466f65e8 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 16:35:28 +0200 Subject: [PATCH 41/57] feat: update tomlkit to version 0.14 Version 0.14 implements TOML 1.1.0, thus closes #191 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 74f06d8..f9f819e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ rich = "^13.9.4" semver = "^3.0.4" tkhtmlview = "^0.3.2" tklinenums = "^1.7.1" -tomlkit = "^0.13.3" +tomlkit = "^0.14.0" tomlkit-extras = "^0.2.0" ttkbootstrap = "^1.20.0" ttkbootstrap-icons = "^3.3.0" From e52466532dd3e5ed281848a6acf2445da7a1df70 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 16:38:28 +0200 Subject: [PATCH 42/57] build: bump version for distribution purposes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f9f819e..a78003e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.0.3" +version = "2.1.0" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, From 420153652438d2a69018b0d4a5ca69f52fae1001 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 16:43:56 +0200 Subject: [PATCH 43/57] fix: update reported version as well --- lib/i18n/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index 0e06c40..89c16c0 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.0.3" +UI_APP_VERSION = "2.1.0" # other strings that should not be translated UI_WHENEVER = "whenever" From 73dddf5dae67dc0fb4f7ce29ce0d370229406c26 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 21:01:07 +0200 Subject: [PATCH 44/57] fix(ui): human readable description of MCRT conditions --- lib/i18n/strings_base.py | 2 +- lib/i18n/strings_en.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index 0d8e9fc..91ce936 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -17,7 +17,7 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" -ITEM_COND_MCRT = "Condition Activated Through Other Conditions" +ITEM_COND_MCRT = "Condition Activated by Other Conditions" ITEM_COND_STARTUP = "Startup Condition" ITEM_EVENT = "Event" diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index 0d8e9fc..91ce936 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -17,7 +17,7 @@ ITEM_COND_DBUS = "DBus Inspection Based Condition" ITEM_COND_WMI = "WMI Query Based Condition" ITEM_COND_EVENT = "Event Based Condition" -ITEM_COND_MCRT = "Condition Activated Through Other Conditions" +ITEM_COND_MCRT = "Condition Activated by Other Conditions" ITEM_COND_STARTUP = "Startup Condition" ITEM_EVENT = "Event" From 3aaed1d94f83c1473dbeecb4bd4ca97be25dfa51 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Wed, 8 Apr 2026 22:45:31 +0200 Subject: [PATCH 45/57] docs: describe new MCRT features and update images --- support/docs/cond_confluence.md | 28 ++++++++++++++++++ support/docs/conditions.md | 14 ++++++--- support/docs/graphics/when-cond-common.png | Bin 26784 -> 27768 bytes .../docs/graphics/when-cond-confluence.png | Bin 0 -> 19677 bytes support/docs/index.rst | 1 + 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 support/docs/cond_confluence.md create mode 100644 support/docs/graphics/when-cond-confluence.png diff --git a/support/docs/cond_confluence.md b/support/docs/cond_confluence.md new file mode 100644 index 0000000..49a050e --- /dev/null +++ b/support/docs/cond_confluence.md @@ -0,0 +1,28 @@ +# Conditions Activated by Other Conditions + +These conditions are specifically built to be the consequence of the verification of other conditions. The specific parameter tab will display of list of _activating_ conditions that, after their verification, will cause the current condition to be verified. + +![ConditionConfluence](graphics/when-cond-confluence.png) + +To add a condition to the list, it is sufficient to select it from the box below the list, and click the _Add_ button. There are some constraints: + +* the conditions that appear in the box are only the ones that have been configured to _activate further conditions_, +* it would be useless, and there fore it is not possible, to add a condition twice, +* it is not possible to use a condition of this type to _activate further conditions_: the specific check box in the common parameters tab is disabled, +* at least two _activating_ conditions must be in the list for the configuration to be valid. + +These constraints are enforced by the form, but they should be considered just obvious: condition duplication and the use of this type of conditions to create complicated chains is discouraged. + +Although it might seem complex, the workflow is quite simple: _after_ **all** the conditions listed in the list of _activating conditions_ are verified, the tasks associated to the current condition are run, as with every other type of condition. In this sentence, the word _after_ is intentionally used: there is no concept of simultaneos validity of the conditions that activate the current one. This should give a hint on what conditions are more suitable to be used to concur in the activation of another, or at least which ones should _not_ anyway. In the category of conditions that are not ideal for this use, the following generically described kinds should be counted: + +* conditions that depend on the status of the system in a particular instant, for example on the current load, +* conditions that take into account interaction with the user, such as idle time or presence of removable drives, +* conditions that are verified at certain instants in time, + +as well as, possibily, other types. Other conditions, such as time intervals, availability of certain files or resources that do not strictly depend on user interaction, and under certain circumstances automatic session locks[^1] can be appropriate candidates for _condition confluence_. However, **When** does not limit what conditions can be used for this purpose, and even by exploiting the less encouraged types, there might be the opportunity to use this feature productively. + + +[`◀ Conditions`](conditions.md) + + +[^1]: However, **when** and **whenever** do not consider _automatic_ and _manual_ session locks as different conditions or events. diff --git a/support/docs/conditions.md b/support/docs/conditions.md index bf4fd04..9b36500 100644 --- a/support/docs/conditions.md +++ b/support/docs/conditions.md @@ -7,13 +7,18 @@ All condition editors share a common part, which encompasses all parameters that It allows to set the mandatory item _Name_ (an alphanumeric string beginning either with a letter or an underscore[^1]), the tasks associated with it, and to decide other behaviors specific to conditions: * _Check Condition Recurrently_: when set, the condition is continuously re-checked even after its verification, becoming _recurrent_. By default a verified condition stops being checked after the first occurrence, unless a [_reset conditions_](tray.md) command is sent via the system tray menu. -* _Max Task Retries_: when greater or equal to `0`, the number of times that the underlying scheduler will _retry_ to run the associated task or list of tasks in case one of them fails (of course `0` means _only one check and no retries_); a value of `-1` means that the scheduler will try to run the associated task(s) forever until they all succeed. Only available if the _Check Condition Recurrently_ flag is set. * _Suspend Condition at Startup_: to start the condition in suspended mode, which means that it will not be checked during the session. +* _Activate further conditions_: conditions for which this option is checked will concur in activation of other specifically built [conditions](cond_confluence.md). +* _Max Task Retries_: when greater or equal to `0`, the number of times that the underlying scheduler will _retry_ to run the associated task or list of tasks in case one of them fails (of course `0` means _only one check and no retries_); a value of `-1` means that the scheduler will try to run the associated task(s) forever until they all succeed. Only available if the _Check Condition Recurrently_ flag is set. The central list displays the list of tasks associated with the condition, in the order in which they would be run in case the _Execute Tasks Sequentially_ box is checked (otherwise, all tasks are spawned simultaneously). To add a task to the list, it must be selected from the drop down list below the list and the _Add_ button must be clicked. To remove a task, it must be double clicked on the list (or selected in the drop down list, with the same effect) and the _Remove_ button has to be clicked. Note that all occurrences of the task displayed in the text box are removed from the list. When the tasks are set to be run sequentially, the behavior upon success or failure of one of them (that is: stop the sequence on either success or failure) can be specified, by clicking the appropriate option, respectively the _Stop Running Sequence when a Task Succeeds/Fails_ options. Leave the _Do Not Check for Task Outcome_ selected to ignore the outcome of the associated tasks. +:::{caution} +When a condition is set to _activate further conditions_ by checking the appropriate box, it can only be used for the specific purpose: this means that no other _tasks_ can be triggered by this condition, and in fact possible _tasks_ that were listed as its consequence prior to checking the box are deleted, and the entire _tasks_ section in the form is disabled. +::: + These are the parameters that appear on the _Common Parameters_ tab: the _Specific Parameters_ tab, instead, varies according to the type of condition that is being edited. The conditions available in **When** that are natively supported by **whenever** are the following: @@ -27,9 +32,10 @@ The conditions available in **When** that are natively supported by **whenever** Other conditions are supported, that are implemented as reactions to particular commands, DBus messages or method invocations, WMI events or queries, _Lua_ scripts. These conditions appear along with the native ones, and the related documentation can be found at the following locations: -* [_System Load_ below Threshold](cond_extra01.md#system-load) conditions -* [_Battery Charge_ below Threshold](cond_extra01.md#low-battery) conditions -* [_Battery Charge_ above Threshold](cond_extra01.md#charging-battery) conditions +* Conditions [_activated by other conditions_](cond_confluence.md) +* [_System Load_ below threshold](cond_extra01.md#system-load) conditions +* [_Battery Charge_ below threshold](cond_extra01.md#low-battery) conditions +* [_Battery Charge_ above threshold](cond_extra01.md#charging-battery) conditions * [_Removable Drive_ available](cond_extra01.md#removable-drives) conditions * [_Session Locked_](cond_extra01.md#session-locked-windows) (Windows only) conditions diff --git a/support/docs/graphics/when-cond-common.png b/support/docs/graphics/when-cond-common.png index 0049da5725ed88e0e1785699a2a9f72c3f10ffe0..9274f96963cd87396b4f93aa1647155f8a19b708 100644 GIT binary patch literal 27768 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ê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 Ls{RBaXBzxJBW7{i literal 26784 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 diff --git a/support/docs/graphics/when-cond-confluence.png b/support/docs/graphics/when-cond-confluence.png new file mode 100644 index 0000000000000000000000000000000000000000..5668e3261dcc231c456978f68c841624d38b39a4 GIT binary patch literal 19677 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 Date: Thu, 9 Apr 2026 11:38:55 +0200 Subject: [PATCH 46/57] docs: update documentation about frontends --- support/docs/main.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/support/docs/main.md b/support/docs/main.md index 514fa71..68a2980 100644 --- a/support/docs/main.md +++ b/support/docs/main.md @@ -31,7 +31,11 @@ This document references: * [**whenever**](https://github.com/almostearthling/whenever): the main scheduler and automation tool used by **When**; * [**whenever_tray**](https://github.com/almostearthling/whenever_tray): a minimal, lightweight, cross platform wrapper and frontend for **whenever**. -The first is the main core that **When** uses to accomplish its mission: unlike the previous version, **When** totally relies on **whenever** as its internal engine instead of implementing a scheduler on its own. The second can be considered as a complement to **When**, in the sense that **When** is designed to share its configuration location with **whenever_tray** in an interoperable way that would allow the latter to be launched as an alternative frontend for running **whenever** after having configured it with the help of **When**. +The first is the main core that **When** uses to accomplish its mission: unlike the previous version, **When** totally relies on **whenever** as its internal engine instead of implementing a scheduler on its own. The second can be considered as a complement to **When**, in the sense that **When** is designed to share its configuration location with **whenever_tray** in an interoperable way that would allow the latter to be launched as an alternate frontend for running **whenever** after having configured it with the help of **When**. + +:::{tip} +A configuration file generated with **When** will still work in case **whenever** is started by an alternate frontend, or even on its own. This includes advanced features such as the ability to [_reset conditions on system resume_](cfgform.md#modify-scheduler-parameters), or the [_conditions activated by other conditions_](cond_confluence.md), as well as the specific items. Some of these features, however, require **When** to be _installed_ (although not _running_), because they depend on resources (for instance, _Lua_ scripts), that are located in the **When** installation tree. Apart from that, the configuration is read by **whenever** as a definitely regular configuration file. +::: ## Covered Topics @@ -57,16 +61,16 @@ For more information about the companion tools, **whenever** and **whenever_tray For the sake of readability, a glossary follows for some of the terms used throughout this documentation. -| **Term** | **Meaning** | -|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| -| _APPDATA_ | the _application data_ directory is where **When** keeps configuration files, data files, and logs: see [this page](appdata.md) for details | -| _condition_ | circumstance or set of circumstances that may or may not occur in a certain moment and whose occurrence determines the execution of tasks | -| _event_ | signal, message, or external coincidence anyway that **When** (or **whenever**) can be instructed to listen to | -| _item_ | used throughout the document to specify one of a _task_, a _condition_ or an _event_ | -| _system tray area_ | area of the desktop where background-running applications show an icon and notifications: goes by several other names | -| _task_ | an action that **When** will perform upon verification of a certain _condition_ | -| _tick_ | the instant in which the tests for condition verification are started and possibly consequential tasks are launched | -| ... | ... | +| **Term** | **Meaning** | +|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| _APPDATA_ | the _application data_ directory is where **When** keeps configuration files, data files, and logs: see [this page](appdata.md) for details | +| _condition_ | circumstance or set of circumstances that may or may not occur in a certain moment and whose occurrence determines the execution of tasks | +| _event_ | signal, message, or external coincidence anyway that **When** (or **whenever**) can be instructed to listen to | +| _item_ | used throughout the document to specify one of a _task_, a _condition_ or an _event_ | +| _system tray area_ | area of the desktop where background-running applications show an icon and notifications: goes by several other names | +| _task_ | an action that **When** will perform upon verification of a certain _condition_ | +| _tick_ | the instant in which the tests for condition verification are started and possibly consequential tasks are launched | +| ... | ... | [^1]: this actually fulfills what was requested in Issue #85. From c78ac357f7d217e55c2d18cbfb17ce2b01488409 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 15:05:21 +0200 Subject: [PATCH 47/57] style: more implemented linter suggestions --- lib/extra/c_battery_charging_linux.py | 2 +- lib/extra/c_battery_charging_win32.py | 2 +- lib/extra/c_battery_low_linux.py | 2 +- lib/extra/c_battery_low_win32.py | 2 +- lib/extra/c_removabledrive_win32.py | 2 +- lib/forms/cond_command.py | 12 ++++++------ lib/forms/cond_lua.py | 12 ++++++------ lib/forms/cond_wmi.py | 12 ++++++------ lib/forms/task_command.py | 12 ++++++------ lib/forms/task_internal.py | 12 ++++++------ lib/forms/task_lua.py | 12 ++++++------ lib/internal/multi_conds_run_task.py | 10 +++++----- lib/toolbox/create_shortcuts.py | 2 +- lib/utility.py | 4 ++-- pyproject.toml | 2 +- 15 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/extra/c_battery_charging_linux.py b/lib/extra/c_battery_charging_linux.py index 476d877..5b6c919 100644 --- a/lib/extra/c_battery_charging_linux.py +++ b/lib/extra/c_battery_charging_linux.py @@ -82,7 +82,7 @@ def _get_batteries(): batteries.append(str(x)) batteries.sort() return batteries - except: + except Exception: return [] diff --git a/lib/extra/c_battery_charging_win32.py b/lib/extra/c_battery_charging_win32.py index 2b0e351..68b32cc 100644 --- a/lib/extra/c_battery_charging_win32.py +++ b/lib/extra/c_battery_charging_win32.py @@ -66,7 +66,7 @@ def _has_battery(): conn = wmi.WMI() batteries = conn.query(query) return bool(len(batteries) > 0) - except: + except Exception: return False diff --git a/lib/extra/c_battery_low_linux.py b/lib/extra/c_battery_low_linux.py index dce942e..e44a7f5 100644 --- a/lib/extra/c_battery_low_linux.py +++ b/lib/extra/c_battery_low_linux.py @@ -84,7 +84,7 @@ def _get_batteries(): batteries.append(str(x)) batteries.sort() return batteries - except: + except Exception: return [] diff --git a/lib/extra/c_battery_low_win32.py b/lib/extra/c_battery_low_win32.py index dfc5355..5fc4a5e 100644 --- a/lib/extra/c_battery_low_win32.py +++ b/lib/extra/c_battery_low_win32.py @@ -65,7 +65,7 @@ def _has_battery(): conn = wmi.WMI() batteries = conn.query(query) return bool(len(batteries) > 0) - except: + except Exception: return False diff --git a/lib/extra/c_removabledrive_win32.py b/lib/extra/c_removabledrive_win32.py index 8aa9007..6f0df54 100644 --- a/lib/extra/c_removabledrive_win32.py +++ b/lib/extra/c_removabledrive_win32.py @@ -137,7 +137,7 @@ def check_tags(cls, tags): if drive_letter is not None: if ( not isinstance(drive_letter, str) - and not drive_letter[0] in AVAILABLE_DRIVE_LETTERS + and drive_letter[0] not in AVAILABLE_DRIVE_LETTERS ): errors.append("drive_letter") drive_label = tags.get("drive_label") diff --git a/lib/forms/cond_command.py b/lib/forms/cond_command.py index 339be7a..22c47f8 100644 --- a/lib/forms/cond_command.py +++ b/lib/forms/cond_command.py @@ -263,8 +263,8 @@ def _updatedata(self): v = bool(self.data_get("set_environment_variables")) self._item.set_environment_variables = False if not v else None e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None self._item.success_status = None self._item.success_stdout = None @@ -421,8 +421,8 @@ def add_var(self) -> None: ) self._envvars.append([name, value]) e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None self._envvars.sort(key=lambda x: x[0]) self._updatedata() @@ -436,8 +436,8 @@ def del_var(self) -> None: value = entry[1] self._envvars = list(entry for entry in self._envvars if entry[0] != name) e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None # first update the form data, then recall the variable in the input # fields, so that the user can re-add the variable again if needed diff --git a/lib/forms/cond_lua.py b/lib/forms/cond_lua.py index 0fb4675..7580838 100644 --- a/lib/forms/cond_lua.py +++ b/lib/forms/cond_lua.py @@ -177,8 +177,8 @@ def add_var(self): ) self._results.append([name, value]) e = {} - for l in self._results: - e[l[0]] = str(l[1]) + for c in self._results: + e[c[0]] = str(c[1]) self._item.expected_results = e or None self._results.sort(key=lambda x: x[0]) self._updatedata() @@ -194,8 +194,8 @@ def del_var(self) -> None: value = entry[1] self._results = list(entry for entry in self._results if entry[0] != name) e = {} - for l in self._results: - e[l[0]] = str(l[1]) + for c in self._results: + e[c[0]] = str(c[1]) self._item.expected_results = e or None # first update the form data, then recall the variable in the input # fields, so that the user can re-add the variable again if needed @@ -223,8 +223,8 @@ def _updatedata(self) -> None: self.data_get("ignore_persistent_success") or None ) e = {} - for l in self._results: - e[l[0]] = guess_typed_value(str(l[1])) + for c in self._results: + e[c[0]] = guess_typed_value(str(c[1])) self._item.expected_results = e or None return super()._updatedata() diff --git a/lib/forms/cond_wmi.py b/lib/forms/cond_wmi.py index 498f082..88d2c37 100644 --- a/lib/forms/cond_wmi.py +++ b/lib/forms/cond_wmi.py @@ -313,15 +313,15 @@ def _updatedata(self) -> None: self.data_get("ignore_persistent_success") or None ) e = [] - for l in self._results: + for c in self._results: d = { - "field": l[1], - "operator": l[2], - "value": l[3], + "field": c[1], + "operator": c[2], + "value": c[3], } - if l[0] != "" and l[0] is not None: + if c[0] != "" and c[0] is not None: try: - index = int(l[0]) + index = int(c[0]) d["index"] = index except ValueError: self.messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_INVALIDINDEX) diff --git a/lib/forms/task_command.py b/lib/forms/task_command.py index 5112205..f0193a9 100644 --- a/lib/forms/task_command.py +++ b/lib/forms/task_command.py @@ -239,8 +239,8 @@ def _updatedata(self) -> None: v = bool(self.data_get("set_environment_variables")) self._item.set_environment_variables = False if not v else None e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None self._item.success_status = None self._item.success_stdout = None @@ -389,8 +389,8 @@ def add_var(self) -> None: ) self._envvars.append([name, value]) e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None self._envvars.sort(key=lambda x: x[0]) self._updatedata() @@ -404,8 +404,8 @@ def del_var(self) -> None: value = entry[1] self._envvars = list(entry for entry in self._envvars if entry[0] != name) e = {} - for l in self._envvars: - e[l[0]] = str(l[1]) + for c in self._envvars: + e[c[0]] = str(c[1]) self._item.environment_variables = e or None # first update the form data, then recall the variable in the input # fields, so that the user can re-add the variable again if needed diff --git a/lib/forms/task_internal.py b/lib/forms/task_internal.py index 38a8767..51dff52 100644 --- a/lib/forms/task_internal.py +++ b/lib/forms/task_internal.py @@ -32,13 +32,13 @@ def _check_command(s: str): - l = s.split(None, 1) - if len(l) == 0: + c = s.split(None, 1) + if len(c) == 0: return False - elif len(l) == 1: - command, args = l[0], "" - elif len(l) == 2: - command, args = l[0], l[1] + elif len(c) == 1: + command, args = c[0], "" + elif len(c) == 2: + command, args = c[0], c[1] else: # unreachable return False diff --git a/lib/forms/task_lua.py b/lib/forms/task_lua.py index d76eced..087890e 100644 --- a/lib/forms/task_lua.py +++ b/lib/forms/task_lua.py @@ -155,8 +155,8 @@ def add_var(self) -> None: ) self._results.append([name, value]) e = {} - for l in self._results: - e[l[0]] = str(l[1]) + for c in self._results: + e[c[0]] = str(c[1]) self._item.expected_results = e or None self._results.sort(key=lambda x: x[0]) self._updatedata() @@ -172,8 +172,8 @@ def del_var(self) -> None: value = entry[1] self._results = list(entry for entry in self._results if entry[0] != name) e = {} - for l in self._results: - e[l[0]] = str(l[1]) + for c in self._results: + e[c[0]] = str(c[1]) self._item.expected_results = e or None # first update the form data, then recall the variable in the input # fields, so that the user can re-add the variable again if needed @@ -197,8 +197,8 @@ def _updatedata(self) -> None: self._item.script = script.strip() or "" self._item.expect_all = bool(self.data_get("expect_all")) or None e = {} - for l in self._results: - e[l[0]] = guess_typed_value(str(l[1])) + for c in self._results: + e[c[0]] = guess_typed_value(str(c[1])) self._item.expected_results = e or None return super()._updatedata() diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 5e31054..030bbc0 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -309,9 +309,9 @@ def del_cond(self): def _updateform(self) -> None: self._tv_activatingConds.delete(*self._tv_activatingConds.get_children()) idx = 0 - for cond in self._conds_activating: + for cnd in self._conds_activating: self._tv_activatingConds.insert( - "", iid="%s-%s" % (idx, cond), values=(idx, cond), index=tk.END + "", iid="%s-%s" % (idx, cnd), values=(idx, cnd), index=tk.END ) idx += 1 return super()._updateform() @@ -327,9 +327,9 @@ def set_available_conditions(self, conds: list[str]) -> None: self._conds_available = conds.copy() self._conds_available.sort() self._cb_chooseCond["values"] = self._conds_available - for cond in self._conds_activating.copy(): - if cond not in self._conds_available: - self._conds_activating.remove(cond) + for cnd in self._conds_activating.copy(): + if cnd not in self._conds_available: + self._conds_activating.remove(cnd) self._updateform() diff --git a/lib/toolbox/create_shortcuts.py b/lib/toolbox/create_shortcuts.py index 5b0001b..2ddf374 100644 --- a/lib/toolbox/create_shortcuts.py +++ b/lib/toolbox/create_shortcuts.py @@ -209,7 +209,7 @@ def create_shortcuts(main_script, desktop=True, autostart=True, verbose=False) - True, autostart, ) - except Exception as e: + except Exception: if verbose: write_warning(CLI_ERR_CANNOT_CREATE_SHORTCUT) return False diff --git a/lib/utility.py b/lib/utility.py index 5f16094..55896df 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -128,8 +128,8 @@ def is_valid_item_name(s: str) -> bool: # check that all passed arguments are not None -def check_not_none(*l) -> bool: - for x in l: +def check_not_none(*args) -> bool: + for x in args: if x is None: return False return True diff --git a/pyproject.toml b/pyproject.toml index a78003e..d5d7c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,4 +51,4 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff.lint] -ignore = ["E402", "F403", "F405"] +ignore = ["E402", "F401", "F403", "F405", "E731"] From 7e7d3f7dcd7023881aaf3cb78a221a0efc41242a Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 15:13:30 +0200 Subject: [PATCH 48/57] style: explain ignored lint warnings Describe ignored linter warnings in the project configuration file --- pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d5d7c17..e162572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,4 +51,10 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff.lint] -ignore = ["E402", "F401", "F403", "F405", "E731"] +ignore = [ + "E402", # module-import-not-at-top-of-file / Module level import not at top of cell + "F401", # unused-import / {name} imported but unused + "F403", # undefined-local-with-import-star / from {name} import * used; unable to detect undefined names + "F405", # undefined-local-with-import-star-usage / {name} may be undefined, or defined from star imports + "E731", # lambda-assignment / Do not assign a lambda expression, use a def +] From a8d0c34aa6b9063f55f36f32bfb52de35cc2bf52 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 16:05:51 +0200 Subject: [PATCH 49/57] feat: separate private section in configuration The generated configuration file is now divided in two sections: the first one, at the beginning of the file, contains user defined items and global parameters, while the second part, below a specific comment that warns about modification, contains all the private items (the ones whose names begin with `__When__private__` and should not be modified), so that the configuration file itself becomes more manageable --- lib/configurator/writer.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/configurator/writer.py b/lib/configurator/writer.py index 151557f..1832f95 100644 --- a/lib/configurator/writer.py +++ b/lib/configurator/writer.py @@ -4,31 +4,61 @@ from tomlkit import document, comment, item, aot from time import strftime +from ..utility import is_private_item_name from ..i18n.strings import UI_APP +# the comment text constants for the configuration file are defined here +COMMENT_PUBLIC_SECTION = "global parameters and user defined items" +COMMENT_PRIVATE_ITEMS = "private items: please do not modify below this line" + + # all items have an `as_table()` utility that converts them to TOML tables +# this writer separates private items from user created ones def write_whenever_config(filename, tasks, conditions, events, globals) -> None: + head = document() doc = document() + priv = document() mod_time = strftime("%Y-%m-%d @%H:%M:%S") - doc.add(comment(f"{UI_APP}: {mod_time}")) + head.add(comment(f"{UI_APP}: {mod_time}")) + head.add(comment(COMMENT_PUBLIC_SECTION)) for k in globals: if globals[k] is not None: doc.add(k, item(globals[k])) + priv.add(comment(COMMENT_PRIVATE_ITEMS)) + p = aot() t = aot() for elem in tasks: - t.append(elem.as_table()) + if is_private_item_name(elem.name): + p.append(elem.as_table()) + else: + t.append(elem.as_table()) doc.append("task", t) + priv.append("task", p) + p = aot() t = aot() for elem in conditions: - t.append(elem.as_table()) + if is_private_item_name(elem.name): + p.append(elem.as_table()) + else: + t.append(elem.as_table()) doc.append("condition", t) + priv.append("condition", p) + p = aot() t = aot() for elem in events: - t.append(elem.as_table()) + if is_private_item_name(elem.name): + p.append(elem.as_table()) + else: + t.append(elem.as_table()) doc.append("event", t) + priv.append("event", p) with open(filename, "w") as f: + f.write(head.as_string()) + f.write("\n") f.write(doc.as_string()) + f.write("\n") + f.write(priv.as_string()) # end. From 841e53a46b5c8d23556fd65a420920dbb8c20589 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 16:23:31 +0200 Subject: [PATCH 50/57] build: bump version for latest adjustments Introduce RC state so that possible fine tuning can progressively change release number allowing for `pipx` to effectively upgrade the package when required --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e162572..1365087 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.1.0" +version = "2.1.1-rc1" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, From 231d9d09234153c9ccf2064e5fc056db323b3918 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 18:24:43 +0200 Subject: [PATCH 51/57] build: update revision in resource strings --- lib/i18n/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index 89c16c0..5e454b6 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.0" +UI_APP_VERSION = "2.1.1-rc1" # other strings that should not be translated UI_WHENEVER = "whenever" From 89125c19e471c83a679a2bb0f8d3579cdf754382 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Thu, 9 Apr 2026 18:33:05 +0200 Subject: [PATCH 52/57] style: remove explicit `return None` hint --- lib/cfgapp.py | 2 +- lib/configurator/writer.py | 2 +- lib/extra/_template.py | 6 ++-- lib/forms/about.py | 2 +- lib/forms/cfgform.py | 22 ++++++------ lib/forms/cond.py | 24 ++++++------- lib/forms/cond_command.py | 12 +++---- lib/forms/cond_dbus.py | 4 +-- lib/forms/cond_lua.py | 8 ++--- lib/forms/cond_time.py | 14 ++++---- lib/forms/cond_wmi.py | 10 +++--- lib/forms/event.py | 16 ++++----- lib/forms/event_dbus.py | 4 +-- lib/forms/event_fschange.py | 10 +++--- lib/forms/history.py | 6 ++-- lib/forms/task.py | 16 ++++----- lib/forms/task_command.py | 12 +++---- lib/forms/task_internal.py | 4 +-- lib/forms/task_lua.py | 10 +++--- lib/forms/ui.py | 54 ++++++++++++++-------------- lib/i18n/strings.py | 2 +- lib/internal/cond_startup.py | 2 +- lib/internal/multi_conds_run_task.py | 6 ++-- lib/items/cond.py | 2 +- lib/items/cond_command.py | 2 +- lib/items/cond_dbus.py | 2 +- lib/items/cond_event.py | 2 +- lib/items/cond_idle.py | 2 +- lib/items/cond_interval.py | 2 +- lib/items/cond_lua.py | 10 +++--- lib/items/cond_time.py | 2 +- lib/items/cond_wmi.py | 2 +- lib/items/event.py | 2 +- lib/items/event_cli.py | 2 +- lib/items/event_dbus.py | 2 +- lib/items/event_fschange.py | 2 +- lib/items/event_wmi.py | 2 +- lib/items/task.py | 2 +- lib/items/task_command.py | 2 +- lib/items/task_internal.py | 2 +- lib/items/task_lua.py | 10 +++--- lib/repocfg.py | 8 ++--- lib/runner/history.py | 2 +- lib/runner/logger.py | 6 ++-- lib/runner/process.py | 4 +-- lib/toolbox/fix_config.py | 2 +- lib/trayapp.py | 24 ++++++------- lib/utility.py | 10 +++--- pyproject.toml | 2 +- when/when.py | 52 +++++++++++++-------------- when/when_bg.pyw | 2 +- 51 files changed, 206 insertions(+), 206 deletions(-) diff --git a/lib/cfgapp.py b/lib/cfgapp.py index 55859ba..f65ef5e 100644 --- a/lib/cfgapp.py +++ b/lib/cfgapp.py @@ -6,7 +6,7 @@ # entry point for the configuration application, which is also reachable # using the system tray menu when using the tray resident application -def main(root) -> None: +def main(root): # not setting the root of form_Config() informs that this is the # configuration app, thus no `Reload` button should be displayed form = form_Config() diff --git a/lib/configurator/writer.py b/lib/configurator/writer.py index 1832f95..42cd02c 100644 --- a/lib/configurator/writer.py +++ b/lib/configurator/writer.py @@ -15,7 +15,7 @@ # all items have an `as_table()` utility that converts them to TOML tables # this writer separates private items from user created ones -def write_whenever_config(filename, tasks, conditions, events, globals) -> None: +def write_whenever_config(filename, tasks, conditions, events, globals): head = document() doc = document() priv = document() diff --git a/lib/extra/_template.py b/lib/extra/_template.py index d0d7a15..9581392 100644 --- a/lib/extra/_template.py +++ b/lib/extra/_template.py @@ -97,7 +97,7 @@ def __init__(self, t: items.Table | None = None): self.updateitem() - def updateitem(self) -> None: + def updateitem(self): # set base item properties according to specific parameters in `tags` self.command = "ls" self.command_arguments = [ @@ -165,12 +165,12 @@ def __init__(self, tasks_available, item=None): self._updateform() # update the form with the specific parameters (usually in the `tags`) - def _updateform(self) -> None: + def _updateform(self): self.data_set("parameter1", self._item.tags.get("parameter1")) # type: ignore return super()._updateform() # update the item from the form elements (usually update `tags`) - def _updatedata(self) -> None: + def _updatedata(self): self._item.tags["parameter1"] = self.data_get("parameter1") # type: ignore self._item.updateitem() # type: ignore return super()._updatedata() diff --git a/lib/forms/about.py b/lib/forms/about.py index a852f7a..e84eaaa 100644 --- a/lib/forms/about.py +++ b/lib/forms/about.py @@ -73,7 +73,7 @@ def __init__(self, main=False): # display a simple about box -def show_about_box(main=False) -> None: +def show_about_box(main=False): box = AboutBox(main) box.run() del box diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index 4863794..9f16ff1 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -209,7 +209,7 @@ def tab_change(): self._updateform() # update the associated data according to what is in the form - def _updatedata(self) -> None: + def _updatedata(self): self._itemlistentries = [] # encode the invisible list field with the same signature that is used in # the _item_ module, so that the corresponding form and item factories can @@ -244,7 +244,7 @@ def _updatedata(self) -> None: ) # update the form fields according to the associated actual data - def _updateform(self) -> None: + def _updateform(self): self._tv_items.delete(*self._tv_items.get_children()) for entry in self._itemlistentries: t = entry[2].split(":", 1)[0] @@ -293,7 +293,7 @@ def _resetdata(self): self._changed = False # load the configuration from a TOML file, only display non-private items - def _load_config(self, fn) -> None: + def _load_config(self, fn): self._resetdata() default_globals = self._globals.copy() tasks, conditions, events, self._globals = read_whenever_config(fn) @@ -333,7 +333,7 @@ def _load_config(self, fn) -> None: self._changed = False # save the configuration to a TOML file: add private items when required - def _save_config(self, fn) -> None: + def _save_config(self, fn): # 0. set configuration globals as retrieved from the form AppConfig.delete("RESET_CONDS_ON_RESUME") AppConfig.set( @@ -407,7 +407,7 @@ def _save_config(self, fn) -> None: ) # to be called when one of the global parameters has been changed - def _set_changed(self, changed=True) -> None: + def _set_changed(self, changed=True): self._changed = changed # check currently loaded items for signature coherence @@ -425,7 +425,7 @@ def _check_items(self) -> bool: return True # command button reactions - def delete(self) -> None: + def delete(self): # TODO: do not ignore type error item_name, _, item_signature = self.data_get("item_selection") # type: ignore item_type = item_signature.split(":", 1)[0] @@ -440,7 +440,7 @@ def delete(self) -> None: self._updateform() self._changed = True - def save(self) -> None: + def save(self): self._updatedata() fn = self.data_get("config_file") if fn and self._changed: @@ -456,7 +456,7 @@ def save(self) -> None: # edit a specific item: to be complete, this is also bound # to the double click event for an element of the list - def edit(self) -> None: + def edit(self): selection = self.data_get("item_selection") if selection is None: return @@ -556,7 +556,7 @@ def edit(self) -> None: # create a new item: open the item type selection dialog and, if an # item type is chosen, open the appropriate form with default values - def new(self) -> None: + def new(self): e = form_NewItem() if e is not None: r = e.run() @@ -620,7 +620,7 @@ def new(self) -> None: # reload the configuration by sending the appropriate message to the # main (hidden) application form - def reload(self) -> None: + def reload(self): if self.messagebox.askyesno(UI_POPUP_T_CONFIRM, UI_POPUP_RELOADCONFIG_Q): if self._app: self._app.send_event("<>") @@ -628,7 +628,7 @@ def reload(self) -> None: # modify the reaction to the quit button so that if the configuration # has changed the user is asked whether or not he wants to discard it - def exit_close(self) -> None: + def exit_close(self): if self._changed: if self.messagebox.askokcancel( UI_POPUP_T_CONFIRM, UI_POPUP_DISCARDCONFIG_Q diff --git a/lib/forms/cond.py b/lib/forms/cond.py index 757f5a4..c802a64 100644 --- a/lib/forms/cond.py +++ b/lib/forms/cond.py @@ -241,14 +241,14 @@ def __init__(self, title, tasks_available, item=None): self._check_recurring() self.changed = False - def add_task(self) -> None: + def add_task(self): elem = self.data_get("@choose_task") self._updatedata() if elem: self._tasks.append(elem) self._updateform() - def del_task(self) -> None: + def del_task(self): elem = self.data_get("@tasks_selection") self._updatedata() if elem: @@ -256,7 +256,7 @@ def del_task(self) -> None: del self._tasks[idx] self._updateform() - def add_check_caption(self, dataname, caption) -> None: + def add_check_caption(self, dataname, caption): assert self.data_exists(dataname) self._captions[dataname] = clean_caption(caption) @@ -270,13 +270,13 @@ def _invalid_data_captions(self): else: return res - def _popup_invalid_data(self, captions) -> None: + def _popup_invalid_data(self, captions): captions.sort() capts = "- " + "\n- ".join(captions) msg = UI_POPUP_INVALIDPARAMETERS_T % capts self.messagebox.showerror(UI_POPUP_T_ERR, msg) - def _check_recurring(self) -> None: + def _check_recurring(self): # we use the opposite of the value because of : anyway # the counterpart does not work so well (same thing # for , while does better) @@ -289,7 +289,7 @@ def _check_recurring(self) -> None: if not self.data_get("@confluent"): self._max_retries.config(state=tk.NORMAL) - def _mcrt_confluent(self, force=False) -> None: + def _mcrt_confluent(self, force=False): # since this reacts to click, bail out if item is a confluence if isinstance(self._item, mcrt.ConfluenceCondition): return @@ -338,7 +338,7 @@ def _mcrt_confluent(self, force=False) -> None: def contents(self) -> ttk.Frame: return self._area_specific - def _updateform(self) -> None: + def _updateform(self): self._tv_tasks.delete(*self._tv_tasks.get_children()) if self._item: assert isinstance(self._item, Condition) @@ -392,7 +392,7 @@ def _updateform(self) -> None: self.data_set("@choose_task", "") # the data update utility loads data into the item - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, Condition) name = self.data_get("@name") if name is not None: @@ -419,7 +419,7 @@ def _updatedata(self) -> None: self._item.tasks = self._tasks.copy() # set and remove the associated item - def set_item(self, item: Condition) -> None: + def set_item(self, item: Condition): assert isinstance(item, Condition) try: self._item = item.__class__( @@ -437,18 +437,18 @@ def set_item(self, item: Condition) -> None: self._tasks = self._item.tasks.copy() self.data_set("@confluent", False) - def reset_item(self) -> None: + def reset_item(self): self._item = None self._tasks = [] # command button reactions: cancel deletes the current item so that None # is returned upon dialog close, while ok finalizes item initialization # and lets the run() function return a configured item - def exit_cancel(self) -> None: + def exit_cancel(self): self._item = None return super().exit_cancel() - def exit_ok(self) -> None: + def exit_ok(self): errs = self._invalid_data_captions() if errs is None: self._updatedata() diff --git a/lib/forms/cond_command.py b/lib/forms/cond_command.py index 22c47f8..97525cc 100644 --- a/lib/forms/cond_command.py +++ b/lib/forms/cond_command.py @@ -331,7 +331,7 @@ def _updatedata(self): self._item.case_sensitive = False if not case_sensitive else None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, CommandCondition) self.data_set("varname", "") self.data_set("newvalue", "") @@ -406,7 +406,7 @@ def _updateform(self) -> None: ) return super()._updateform() - def add_var(self) -> None: + def add_var(self): assert isinstance(self._item, CommandCondition) name = self.data_get("varname") value = self.data_get("newvalue") @@ -428,7 +428,7 @@ def add_var(self) -> None: self._updatedata() self._updateform() - def del_var(self) -> None: + def del_var(self): assert isinstance(self._item, CommandCondition) entry = self.data_get("envvar_selection") assert isinstance(entry, list) @@ -446,7 +446,7 @@ def del_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def recall_var(self) -> None: + def recall_var(self): entry = self.data_get("envvar_selection") assert isinstance(entry, list) name = entry[0] @@ -454,7 +454,7 @@ def recall_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def browse_command(self) -> None: + def browse_command(self): filetypes = [(UI_FILETYPE_ALL, ".*")] if is_windows(): exts = get_executable_extensions() @@ -465,7 +465,7 @@ def browse_command(self) -> None: if entry: self.data_set("command", entry) - def browse_startup_path(self) -> None: + def browse_startup_path(self): entry = filedialog.askdirectory(parent=self.dialog) if entry: self.data_set("startup_path", entry) diff --git a/lib/forms/cond_dbus.py b/lib/forms/cond_dbus.py index 4a8476e..1cd9754 100644 --- a/lib/forms/cond_dbus.py +++ b/lib/forms/cond_dbus.py @@ -168,7 +168,7 @@ def __init__(self, tasks_available, item=None): # update the form self._updateform() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, DBusCondition) self.data_set("bus", self._item.bus) self.data_set("service", self._item.service) @@ -185,7 +185,7 @@ def _updateform(self) -> None: ) return super()._updateform() - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, DBusCondition) parameter_call = self.data_get("parameter_call") parameter_check = self.data_get("parameter_check") diff --git a/lib/forms/cond_lua.py b/lib/forms/cond_lua.py index 7580838..b4b751f 100644 --- a/lib/forms/cond_lua.py +++ b/lib/forms/cond_lua.py @@ -186,7 +186,7 @@ def add_var(self): else: self.messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_INVALIDVARNAME) - def del_var(self) -> None: + def del_var(self): assert isinstance(self._item, LuaScriptCondition) entry = self.data_get("luavar_selection") assert isinstance(entry, list) @@ -204,7 +204,7 @@ def del_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def recall_var(self) -> None: + def recall_var(self): entry = self.data_get("luavar_selection") assert isinstance(entry, list) name = entry[0] @@ -212,7 +212,7 @@ def recall_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, LuaScriptCondition) script = self.data_get("script") assert isinstance(script, str) @@ -228,7 +228,7 @@ def _updatedata(self) -> None: self._item.expected_results = e or None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, LuaScriptCondition) self.data_set("script", self._item.script) self.data_set("expect_all", self._item.expect_all or False) diff --git a/lib/forms/cond_time.py b/lib/forms/cond_time.py index 17be7ce..8836c4c 100644 --- a/lib/forms/cond_time.py +++ b/lib/forms/cond_time.py @@ -224,7 +224,7 @@ def __init__(self, tasks_available, item=None): # update the form self._updateform() - def clear_timespec(self) -> None: + def clear_timespec(self): self.data_set("ts_year") self.data_set("ts_month") self.data_set("ts_day") @@ -233,7 +233,7 @@ def clear_timespec(self) -> None: self.data_set("ts_min") self.data_set("ts_sec") - def add_timespec(self) -> None: + def add_timespec(self): if m := self.data_get("ts_month"): try: month = _MONTHS_MAP[m] @@ -272,7 +272,7 @@ def add_timespec(self) -> None: self._updatedata() self._updateform() - def del_timespec(self) -> None: + def del_timespec(self): sel = self.data_get("timespec_selection") if sel: assert isinstance(sel, list) @@ -281,7 +281,7 @@ def del_timespec(self) -> None: self._updatedata() self._updateform() - def recall_timespec(self) -> None: + def recall_timespec(self): sel = self.data_get("timespec_selection") if sel: assert isinstance(sel, list) @@ -298,14 +298,14 @@ def recall_timespec(self) -> None: self.data_set("ts_min", spec.minute) self.data_set("ts_sec", spec.second) - def clear_alltimespecs(self) -> None: + def clear_alltimespecs(self): # this functionality is never implemented if self.messagebox.askyesno(UI_POPUP_T_CONFIRM, UI_POPUP_DELETEALLENTRIES_Q): self._timespecs = [] self._updatedata() self._updateform() - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, TimeCondition) e = [] for timespec in self._timespecs: @@ -313,7 +313,7 @@ def _updatedata(self) -> None: self._item.time_specifications = e or None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): self._tv_timeSpecs.delete(*self._tv_timeSpecs.get_children()) idx = 0 for ts in self._timespecs: diff --git a/lib/forms/cond_wmi.py b/lib/forms/cond_wmi.py index 88d2c37..31656b3 100644 --- a/lib/forms/cond_wmi.py +++ b/lib/forms/cond_wmi.py @@ -206,7 +206,7 @@ def __init__(self, tasks_available, item=None): # update the form self._updateform() - def add_check(self) -> None: + def add_check(self): assert isinstance(self._item, WMICondition) if self.data_get("index") == "": index = None @@ -257,7 +257,7 @@ def add_check(self) -> None: self._updatedata() self._updateform() - def del_check(self) -> None: + def del_check(self): assert isinstance(self._item, WMICondition) entry = self.data_get("result_selection") assert isinstance(entry, list) @@ -290,7 +290,7 @@ def del_check(self) -> None: self._updatedata() self._updateform() - def recall_check(self) -> None: + def recall_check(self): entry = self.data_get("result_selection") assert isinstance(entry, list) index = entry[0] @@ -302,7 +302,7 @@ def recall_check(self) -> None: self.data_set("operator", operator) self.data_set("value", value) - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, WMICondition) query = self.data_get("query") assert isinstance(query, str) @@ -329,7 +329,7 @@ def _updatedata(self) -> None: self._item.result_check = e or None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, WMICondition) self.data_set("query", self._item.query) self.data_set("check_all", self._item.result_check_all or False) diff --git a/lib/forms/event.py b/lib/forms/event.py index b6d0c70..c9b11a1 100644 --- a/lib/forms/event.py +++ b/lib/forms/event.py @@ -83,7 +83,7 @@ def __init__(self, title, conditions_available, item=None): self.reset_item() self.changed = False - def add_check_caption(self, dataname, caption) -> None: + def add_check_caption(self, dataname, caption): assert self.data_exists(dataname) self._captions[dataname] = clean_caption(caption) @@ -97,7 +97,7 @@ def _invalid_data_captions(self): else: return res - def _popup_invalid_data(self, captions) -> None: + def _popup_invalid_data(self, captions): captions.sort() capts = "- " + "\n- ".join(captions) msg = UI_POPUP_INVALIDPARAMETERS_T % capts @@ -108,7 +108,7 @@ def _popup_invalid_data(self, captions) -> None: def contents(self) -> ttk.Frame: return self._sub_contents - def _updateform(self) -> None: + def _updateform(self): if self._item: assert isinstance(self._item, Event) self.data_set("@name", self._item.name) @@ -118,7 +118,7 @@ def _updateform(self) -> None: self.data_set("@condition", "") # the data update utility loads data into the item - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, Event) name = self.data_get("@name") if name is not None: @@ -126,7 +126,7 @@ def _updatedata(self) -> None: self._item.condition = self.data_get("@condition") # set and remove the associated item - def set_item(self, item) -> None: + def set_item(self, item): assert isinstance(item, Event) try: self._item = item.__class__( @@ -135,17 +135,17 @@ def set_item(self, item) -> None: except ValueError: self._item = item # item was newly created: use it - def reset_item(self) -> None: + def reset_item(self): self._item = None # command button reactions: cancel deletes the current item so that None # is returned upon dialog close, while ok finalizes item initialization # and lets the run() function return a configured item - def exit_cancel(self) -> None: + def exit_cancel(self): self._item = None return super().exit_cancel() - def exit_ok(self) -> None: + def exit_ok(self): errs = self._invalid_data_captions() if errs is None: self._updatedata() diff --git a/lib/forms/event_dbus.py b/lib/forms/event_dbus.py index 78c9cda..a7a3f0b 100644 --- a/lib/forms/event_dbus.py +++ b/lib/forms/event_dbus.py @@ -107,7 +107,7 @@ def __init__(self, conditions_available, item=None): # update the form self._updateform() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, DBusEvent) self.data_set("bus", self._item.bus) self.data_set("rule", self._item.rule) @@ -115,7 +115,7 @@ def _updateform(self) -> None: self.data_set("parameter_check_all", self._item.parameter_check_all or False) return super()._updateform() - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, DBusEvent) parameter_check = self.data_get("parameter_check") assert isinstance(parameter_check, str) diff --git a/lib/forms/event_fschange.py b/lib/forms/event_fschange.py index 5a2b125..fd5fa21 100644 --- a/lib/forms/event_fschange.py +++ b/lib/forms/event_fschange.py @@ -114,7 +114,7 @@ def _updateform(self): idx += 1 return super()._updateform() - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, FilesystemChangeEvent) self._item.recursive = self.data_get("recursive") or None e = [] @@ -123,7 +123,7 @@ def _updatedata(self) -> None: self._item.watch = e or None return super()._updatedata() - def recall_fsitem(self) -> None: + def recall_fsitem(self): e = self.data_get("item_selection") if e: entry = e[1] @@ -133,7 +133,7 @@ def recall_fsitem(self) -> None: # in this case a non-existing item is selected self.data_set("item_monitor") - def browse_fsitem(self) -> None: + def browse_fsitem(self): if self.data_get("select_dir"): entry = filedialog.askdirectory(parent=self.dialog) else: @@ -141,7 +141,7 @@ def browse_fsitem(self) -> None: if entry: self.data_set("item_monitor", entry) - def add_fsitem(self) -> None: + def add_fsitem(self): self._updatedata() i = self.data_get("item_monitor") assert isinstance(i, str) @@ -150,7 +150,7 @@ def add_fsitem(self) -> None: self._watch.append(item) self._updateform() - def del_fsitem(self) -> None: + def del_fsitem(self): self._updatedata() e = self.data_get("item_monitor") if e: diff --git a/lib/forms/history.py b/lib/forms/history.py index 39fe590..3290064 100644 --- a/lib/forms/history.py +++ b/lib/forms/history.py @@ -107,7 +107,7 @@ def __init__(self, wrapper, main=False): self._updateform() - def set_history(self, history) -> None: + def set_history(self, history): h = list( ( [ @@ -124,7 +124,7 @@ def set_history(self, history) -> None: h.reverse() self._history = h - def _updateform(self) -> None: + def _updateform(self): self._tv_history.delete_rows() for entry, outcome in self._history: icon = ( @@ -136,7 +136,7 @@ def _updateform(self) -> None: self._tv_history.insert_row("end", values=entry) # reload history data when the `reload` button is clicked - def reload(self) -> None: + def reload(self): self.set_history(self._wrapper.get_history()) self._updateform() return super().reload() diff --git a/lib/forms/task.py b/lib/forms/task.py index 7999e84..7311584 100644 --- a/lib/forms/task.py +++ b/lib/forms/task.py @@ -71,7 +71,7 @@ def __init__(self, title, item=None): self.reset_item() self.changed = False - def add_check_caption(self, dataname, caption) -> None: + def add_check_caption(self, dataname, caption): assert self.data_exists(dataname) self._captions[dataname] = clean_caption(caption) @@ -85,7 +85,7 @@ def _invalid_data_captions(self): else: return res - def _popup_invalid_data(self, captions) -> None: + def _popup_invalid_data(self, captions): captions.sort() capts = "- " + "\n- ".join(captions) msg = UI_POPUP_INVALIDPARAMETERS_T % capts @@ -96,7 +96,7 @@ def _popup_invalid_data(self, captions) -> None: def contents(self) -> ttk.Frame: return self._sub_contents - def _updateform(self) -> None: + def _updateform(self): if self._item: assert isinstance(self._item, Task) self.data_set("@name", self._item.name) @@ -104,14 +104,14 @@ def _updateform(self) -> None: self.data_set("@name", "") # the data update utility loads data into the item - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, Task) name = self.data_get("@name") if name is not None: self._item.name = name # set and remove the associated item - def set_item(self, item) -> None: + def set_item(self, item): assert isinstance(item, Task) try: self._item = item.__class__( @@ -120,17 +120,17 @@ def set_item(self, item) -> None: except ValueError: self._item = item # item was newly created: use it - def reset_item(self) -> None: + def reset_item(self): self._item = None # command button reactions: cancel deletes the current item so that None # is returned upon dialog close, while ok finalizes item initialization # and lets the run() function return a configured item - def exit_cancel(self) -> None: + def exit_cancel(self): self._item = None return super().exit_cancel() - def exit_ok(self) -> None: + def exit_ok(self): errs = self._invalid_data_captions() if errs is None: self._updatedata() diff --git a/lib/forms/task_command.py b/lib/forms/task_command.py index f0193a9..e7e2655 100644 --- a/lib/forms/task_command.py +++ b/lib/forms/task_command.py @@ -223,7 +223,7 @@ def __init__(self, item=None): self._updateform() # the data update utility loads data into the item - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, CommandTask) command = self.data_get("command") args = self.data_get("command_arguments") @@ -304,7 +304,7 @@ def _updatedata(self) -> None: self._item.case_sensitive = False if not case_sensitive else None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, CommandTask) self.data_set("varname", "") self.data_set("newvalue", "") @@ -374,7 +374,7 @@ def _updateform(self) -> None: ) return super()._updateform() - def add_var(self) -> None: + def add_var(self): assert isinstance(self._item, CommandTask) name = self.data_get("varname") value = self.data_get("newvalue") @@ -396,7 +396,7 @@ def add_var(self) -> None: self._updatedata() self._updateform() - def del_var(self) -> None: + def del_var(self): assert isinstance(self._item, CommandTask) entry = self.data_get("envvar_selection") assert isinstance(entry, list) @@ -422,7 +422,7 @@ def recall_var(self): self.data_set("varname", name) self.data_set("newvalue", value) - def browse_command(self) -> None: + def browse_command(self): filetypes = [(UI_FILETYPE_ALL, ".*")] if is_windows(): exts = get_executable_extensions() @@ -433,7 +433,7 @@ def browse_command(self) -> None: if entry: self.data_set("command", entry) - def browse_startup_path(self) -> None: + def browse_startup_path(self): entry = filedialog.askdirectory(parent=self.dialog) if entry: self.data_set("startup_path", entry) diff --git a/lib/forms/task_internal.py b/lib/forms/task_internal.py index 51dff52..f8f84a5 100644 --- a/lib/forms/task_internal.py +++ b/lib/forms/task_internal.py @@ -86,14 +86,14 @@ def __init__(self, item=None): # update the form self._updateform() - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, InternalCommandTask) command = self.data_get("command") assert isinstance(command, str) self._item.command = command.strip() or "" return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, InternalCommandTask) self.data_set("command", self._item.command) return super()._updateform() diff --git a/lib/forms/task_lua.py b/lib/forms/task_lua.py index 087890e..297ddc0 100644 --- a/lib/forms/task_lua.py +++ b/lib/forms/task_lua.py @@ -140,7 +140,7 @@ def __init__(self, item=None): # update the form self._updateform() - def add_var(self) -> None: + def add_var(self): assert isinstance(self._item, LuaScriptTask) name = self.data_get("varname") value = self.data_get("newvalue") @@ -164,7 +164,7 @@ def add_var(self) -> None: else: self.messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_INVALIDVARNAME) - def del_var(self) -> None: + def del_var(self): assert isinstance(self._item, LuaScriptTask) entry = self.data_get("luavar_selection") assert isinstance(entry, list) @@ -182,7 +182,7 @@ def del_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def recall_var(self) -> None: + def recall_var(self): entry = self.data_get("luavar_selection") assert isinstance(entry, list) name = entry[0] @@ -190,7 +190,7 @@ def recall_var(self) -> None: self.data_set("varname", name) self.data_set("newvalue", value) - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, LuaScriptTask) script = self.data_get("script") assert isinstance(script, str) @@ -202,7 +202,7 @@ def _updatedata(self) -> None: self._item.expected_results = e or None return super()._updatedata() - def _updateform(self) -> None: + def _updateform(self): assert isinstance(self._item, LuaScriptTask) self.data_set("script", self._item.script) self.data_set("expect_all", self._item.expect_all or False) diff --git a/lib/forms/ui.py b/lib/forms/ui.py index 3eaefbf..2a87f13 100644 --- a/lib/forms/ui.py +++ b/lib/forms/ui.py @@ -472,7 +472,7 @@ def __init__( def __enter__(self): return self - def __exit__(self, _exc_type, _exc_value, _traceback) -> None: + def __exit__(self, _exc_type, _exc_value, _traceback): del self._dialog # contents is the root for slave widgets @@ -491,7 +491,7 @@ def messagebox(self) -> MessageBox: # internals # force a variable value (and mimic ttk *Var retrieval method used below) - def _force_set_data(self, name: str, value: Any) -> None: + def _force_set_data(self, name: str, value: Any): class _Elem(object): def __init__(self, v): self._v = v @@ -504,7 +504,7 @@ def get(self): # bind a ttk.Treeview to a variable: it involves defining a new # function and reacting to a (virtual) event - def _bind_ttk_treeview(self, name: str, treeview: ttk.Treeview) -> None: + def _bind_ttk_treeview(self, name: str, treeview: ttk.Treeview): def _store_data(event): iid = treeview.focus() data = treeview.item(iid)["values"] @@ -514,19 +514,19 @@ def _store_data(event): treeview.bind("<>", _store_data) # bind an event to this form - def event_bind(self, event, reaction) -> None: + def event_bind(self, event, reaction): self._dialog.bind(event, reaction) # bind and when gaining focus and unbind when n is lost - def _key_exit_close(self, event) -> None: + def _key_exit_close(self, event): if event.widget == self._dialog: self.exit_close() - def _key_exit_ok(self, event) -> None: + def _key_exit_ok(self, event): if event.widget == self._dialog: self.exit_ok() - def focus_in(self) -> None: + def focus_in(self): self.event_bind("", self._key_exit_close) self.event_bind("", self._key_exit_ok) @@ -547,7 +547,7 @@ def data_bind( widget: tk.Widget | tuple[ttk.Radiobutton, ...], dtype: str | None = None, check: Callable[..., bool] | None = None, - ) -> None: + ): # as per documentation, the configure() method is not the preferred # method to configure a widget in tkinter, and direct access to the # widget keys (as if it were a dict) is used in the examples @@ -662,7 +662,7 @@ def data_get(self, dataname: str, default: Any | None = None): return default # set the value of a widget: None is used to clear the widget - def data_set(self, dataname: str, value: Any | None = None) -> None: + def data_set(self, dataname: str, value: Any | None = None): if dataname in self._data: try: if value is None: @@ -700,7 +700,7 @@ def data_valid(self, dataname: str) -> bool: return False # set autocheck feature on or off - def set_autocheck(self, c: bool) -> None: + def set_autocheck(self, c: bool): self._autocheck = bool(c) # return a list of the widget-bound variables @@ -711,65 +711,65 @@ def data_vars(self): # exit functions are predefined and may be overridden or not: the # default implementation destroys the window and: if OK leave form # data accessible, otherwise clear form data - def exit_ok(self) -> None: + def exit_ok(self): self._dialog.destroy() - def exit_cancel(self) -> None: + def exit_cancel(self): self._data = {} self._dialog.destroy() - def exit_close(self) -> None: + def exit_close(self): self._dialog.destroy() - def exit_quit(self) -> None: + def exit_quit(self): self._dialog.destroy() # other button reactions have to be overridden because the default # implementation just does nothing - def load(self) -> None: + def load(self): pass - def save(self) -> None: + def save(self): pass - def new(self) -> None: + def new(self): pass - def add(self) -> None: + def add(self): pass - def delete(self) -> None: + def delete(self): pass - def edit(self) -> None: + def edit(self): pass - def modify(self) -> None: + def modify(self): pass - def reset(self) -> None: + def reset(self): pass - def reload(self) -> None: + def reload(self): pass - def remove(self) -> None: + def remove(self): pass # the following utilities allow to enable or disable the default buttons - def enable_buttons(self, *names: str) -> None: + def enable_buttons(self, *names: str): for name in map(str.lower, names): if name in self._std_buttons: self._std_buttons[name].enable(True) - def disable_buttons(self, *names: str) -> None: + def disable_buttons(self, *names: str): for name in map(str.lower, names): if name in self._std_buttons: self._std_buttons[name].enable(False) # main dialog loop: the initial dialog will actually have a main loop # while the following ones will just be spawned and then waited for - def run(self) -> None: + def run(self): if self._main: self._dialog.mainloop() else: diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index 5e454b6..6b5ae80 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-rc1" +UI_APP_VERSION = "2.1.1-rc2" # other strings that should not be translated UI_WHENEVER = "whenever" diff --git a/lib/internal/cond_startup.py b/lib/internal/cond_startup.py index 9ae94ee..9a4763b 100644 --- a/lib/internal/cond_startup.py +++ b/lib/internal/cond_startup.py @@ -45,7 +45,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): try: super().load_checking(item, item_line, tasks) # ignore the erro on `interval_seconds` diff --git a/lib/internal/multi_conds_run_task.py b/lib/internal/multi_conds_run_task.py index 030bbc0..6f17add 100644 --- a/lib/internal/multi_conds_run_task.py +++ b/lib/internal/multi_conds_run_task.py @@ -306,7 +306,7 @@ def del_cond(self): self._updateform() # update the form with the specific parameters (usually in the `tags`) - def _updateform(self) -> None: + def _updateform(self): self._tv_activatingConds.delete(*self._tv_activatingConds.get_children()) idx = 0 for cnd in self._conds_activating: @@ -317,13 +317,13 @@ def _updateform(self) -> None: return super()._updateform() # update the item from the form elements (usually update `tags`) - def _updatedata(self) -> None: + def _updatedata(self): assert isinstance(self._item, ConfluenceCondition) self._item.tags["mcrt_confluent_conditions"] = self._conds_activating # type: ignore return super()._updatedata() # set the list of available conditions, that implement confluence - def set_available_conditions(self, conds: list[str]) -> None: + def set_available_conditions(self, conds: list[str]): self._conds_available = conds.copy() self._conds_available.sort() self._cb_chooseCond["values"] = self._conds_available diff --git a/lib/items/cond.py b/lib/items/cond.py index 47c4608..9c8a354 100644 --- a/lib/items/cond.py +++ b/lib/items/cond.py @@ -74,7 +74,7 @@ def __str__(self): # to check the correctness of the configuration file as a side effect def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): self.type = None self.hrtype = None name_check = lambda x: is_valid_item_name(x) or is_private_item_name(x) diff --git a/lib/items/cond_command.py b/lib/items/cond_command.py index 9800a6b..6a4a4e4 100644 --- a/lib/items/cond_command.py +++ b/lib/items/cond_command.py @@ -86,7 +86,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "command" self.hrtype = ITEM_COND_COMMAND diff --git a/lib/items/cond_dbus.py b/lib/items/cond_dbus.py index b8f4237..3d71a0a 100644 --- a/lib/items/cond_dbus.py +++ b/lib/items/cond_dbus.py @@ -75,7 +75,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "dbus" self.hrtype = ITEM_COND_DBUS diff --git a/lib/items/cond_event.py b/lib/items/cond_event.py index 89a6f98..6698899 100644 --- a/lib/items/cond_event.py +++ b/lib/items/cond_event.py @@ -28,7 +28,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "event" self.hrtype = ITEM_COND_EVENT diff --git a/lib/items/cond_idle.py b/lib/items/cond_idle.py index 0afedf1..3eec61c 100644 --- a/lib/items/cond_idle.py +++ b/lib/items/cond_idle.py @@ -31,7 +31,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "idle" self.hrtype = ITEM_COND_IDLE diff --git a/lib/items/cond_interval.py b/lib/items/cond_interval.py index 5ea0d70..de4de7e 100644 --- a/lib/items/cond_interval.py +++ b/lib/items/cond_interval.py @@ -30,7 +30,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "interval" self.hrtype = ITEM_COND_INTERVAL diff --git a/lib/items/cond_lua.py b/lib/items/cond_lua.py index 3442503..b74fb0e 100644 --- a/lib/items/cond_lua.py +++ b/lib/items/cond_lua.py @@ -62,7 +62,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "lua" self.hrtype = ITEM_COND_LUA @@ -77,13 +77,13 @@ def load_checking( "init_script_path", check=os.path.isfile ) self.variables_to_set = tab.get_dict_check_and_keys_re( - "variables_to_set", - LUA_VAR_PATTERN, + "variables_to_set", + LUA_VAR_PATTERN, lambda x: isinstance(x, (bool, int, float, str)), ) self.expected_results = tab.get_dict_check_and_keys_re( - "expected_results", - LUA_VAR_PATTERN, + "expected_results", + LUA_VAR_PATTERN, lambda x: isinstance(x, (bool, int, float, str)), ) diff --git a/lib/items/cond_time.py b/lib/items/cond_time.py index b1bdbfb..403b827 100644 --- a/lib/items/cond_time.py +++ b/lib/items/cond_time.py @@ -258,7 +258,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "time" self.hrtype = ITEM_COND_TIME diff --git a/lib/items/cond_wmi.py b/lib/items/cond_wmi.py index ea16e67..2a99992 100644 --- a/lib/items/cond_wmi.py +++ b/lib/items/cond_wmi.py @@ -61,7 +61,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, tasks: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, tasks) self.type = "wmi" self.hrtype = ITEM_COND_WMI diff --git a/lib/items/event.py b/lib/items/event.py index c4f7cd5..fe03198 100644 --- a/lib/items/event.py +++ b/lib/items/event.py @@ -56,7 +56,7 @@ def __str__(self): # to check the correctness of the configuration file as a side effect def load_checking( self, item: items.Table, item_line: int, event_conds: list[str] | None = None - ) -> None: + ): self.type = None self.hrtype = None name_check = lambda x: is_valid_item_name(x) or is_private_item_name(x) diff --git a/lib/items/event_cli.py b/lib/items/event_cli.py index ea30cc8..49f8a80 100644 --- a/lib/items/event_cli.py +++ b/lib/items/event_cli.py @@ -22,7 +22,7 @@ def __init__(self, t: items.Table | None = None): if t: assert t.get("type") == self.type - def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None) -> None: + def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None): super().load_checking(item, item_line, event_conds) self.type = "cli" self.hrtype = ITEM_EVENT_CLI diff --git a/lib/items/event_dbus.py b/lib/items/event_dbus.py index 06434ae..c5172fa 100644 --- a/lib/items/event_dbus.py +++ b/lib/items/event_dbus.py @@ -46,7 +46,7 @@ def __init__(self, t: items.Table | None = None): self.parameter_check_all = None self.parameter_check = None - def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None) -> None: + def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None): super().load_checking(item, item_line, event_conds) self.type = "dbus" self.hrtype = ITEM_EVENT_DBUS diff --git a/lib/items/event_fschange.py b/lib/items/event_fschange.py index 5b6e483..a2265bd 100644 --- a/lib/items/event_fschange.py +++ b/lib/items/event_fschange.py @@ -35,7 +35,7 @@ def __init__(self, t: items.Table | None = None): self.recursive = None # self.poll_seconds = None - def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None) -> None: + def load_checking(self, item: items.Table, item_line: int, event_conds: list[str] | None = None): super().load_checking(item, item_line, event_conds) self.type = "fschange" self.hrtype = ITEM_EVENT_FSCHANGE diff --git a/lib/items/event_wmi.py b/lib/items/event_wmi.py index c163da4..d8edc8b 100644 --- a/lib/items/event_wmi.py +++ b/lib/items/event_wmi.py @@ -48,7 +48,7 @@ def __init__(self, t: items.Table | None = None): def load_checking( self, item: items.Table, item_line: int, event_conds: list[str] | None = None - ) -> None: + ): super().load_checking(item, item_line, event_conds) self.type = "wmi" self.hrtype = ITEM_EVENT_WMI diff --git a/lib/items/task.py b/lib/items/task.py index b9a5058..e5b34c2 100644 --- a/lib/items/task.py +++ b/lib/items/task.py @@ -48,7 +48,7 @@ def __str__(self): # the following too is a constructor, that may generate errors: it can be # used in a configuration checking function, or by the constructor itself # to check the correctness of the configuration file as a side effect - def load_checking(self, item: items.Table, item_line: int) -> None: + def load_checking(self, item: items.Table, item_line: int): self.type = None self.hrtype = None check = lambda x: is_valid_item_name(x) or is_private_item_name(x) diff --git a/lib/items/task_command.py b/lib/items/task_command.py index 4ea1964..2396df9 100644 --- a/lib/items/task_command.py +++ b/lib/items/task_command.py @@ -80,7 +80,7 @@ def __init__(self, t: items.Table | None = None): self.set_environment_variables = None self.environment_variables = None - def load_checking(self, item: items.Table, item_line: int) -> None: + def load_checking(self, item: items.Table, item_line: int): super().load_checking(item, item_line) self.type = "command" self.hrtype = ITEM_TASK_COMMAND diff --git a/lib/items/task_internal.py b/lib/items/task_internal.py index eb622a4..bff5038 100644 --- a/lib/items/task_internal.py +++ b/lib/items/task_internal.py @@ -27,7 +27,7 @@ def __init__(self, t: items.Table | None = None): else: self.command = DEFAULT_COMMAND - def load_checking(self, item: items.Table, item_line: int) -> None: + def load_checking(self, item: items.Table, item_line: int): super().load_checking(item, item_line) self.type = "internal" self.hrtype = ITEM_TASK_INTERNAL diff --git a/lib/items/task_lua.py b/lib/items/task_lua.py index 95e73dd..1a14a2a 100644 --- a/lib/items/task_lua.py +++ b/lib/items/task_lua.py @@ -56,7 +56,7 @@ def __init__(self, t: items.Table | None = None): self.variables_to_set = None self.init_script_path = None - def load_checking(self, item: items.Table, item_line: int) -> None: + def load_checking(self, item: items.Table, item_line: int): super().load_checking(item, item_line) self.type = "lua" self.hrtype = ITEM_TASK_LUA @@ -69,13 +69,13 @@ def load_checking(self, item: items.Table, item_line: int) -> None: "init_script_path", check=os.path.isfile ) self.variables_to_set = tab.get_dict_check_and_keys_re( - "variables_to_set", - LUA_VAR_PATTERN, + "variables_to_set", + LUA_VAR_PATTERN, lambda x: isinstance(x, (bool, int, float, str)), ) self.expected_results = tab.get_dict_check_and_keys_re( - "expected_results", - LUA_VAR_PATTERN, + "expected_results", + LUA_VAR_PATTERN, lambda x: isinstance(x, (bool, int, float, str)), ) diff --git a/lib/repocfg.py b/lib/repocfg.py index 8edc426..613417a 100644 --- a/lib/repocfg.py +++ b/lib/repocfg.py @@ -23,12 +23,12 @@ def __init__(self, initial_table=None): for k in initial_table: self._table[k] = initial_table[k] - def set(self, key: str, value) -> None: + def set(self, key: str, value): if key in self._table: raise ValueError("value already set for '%s': delete it first" % key) self._table[key] = value - def delete(self, key: str) -> None: + def delete(self, key: str): if key in self._table: del self._table[key] @@ -41,10 +41,10 @@ def get(self, key: str, default=None): def __getitem__(self, key: str): return self.get(key) - def __setitem__(self, key: str, value) -> None: + def __setitem__(self, key: str, value): self.set(key, value) - def __delitem__(self, key: str) -> None: + def __delitem__(self, key: str): self.delete(key) def __str__(self) -> str: diff --git a/lib/runner/history.py b/lib/runner/history.py index 6944ba8..4f4e678 100644 --- a/lib/runner/history.py +++ b/lib/runner/history.py @@ -15,7 +15,7 @@ def __init__(self, maxlen): self._open_records_timing = {} self._private_prefix = get_private_item_name_prefix() - def append(self, record) -> None: + def append(self, record): time = record["header"]["time"] # application = record['header']['application'] # level = record['header']['level'] diff --git a/lib/runner/logger.py b/lib/runner/logger.py index cda14f9..e6f5fde 100644 --- a/lib/runner/logger.py +++ b/lib/runner/logger.py @@ -180,7 +180,7 @@ def __init__( status=None, message=None, logger=None, - ) -> None: + ): self._level = level or self.LEVEL_INFO self._emitter = emitter self._action = action @@ -201,7 +201,7 @@ def update( when=None, status=None, message=None, - ) -> None: + ): if level is not None: self._level = level if emitter is not None: @@ -268,7 +268,7 @@ def as_record(self): }, } - def log(self, message=None) -> None: + def log(self, message=None): assert self._logger is not None if message is not None: self._message = str(message) diff --git a/lib/runner/process.py b/lib/runner/process.py index 9a2a3c2..c967282 100644 --- a/lib/runner/process.py +++ b/lib/runner/process.py @@ -27,7 +27,7 @@ # the following function will be used to start a thread that actually # reads subprocess output and possibly provides input to the subprocess # itself: it has to be aware of the wrapper instance that calls it -def _logreader(wrapper) -> None: +def _logreader(wrapper): pipe = wrapper.pipe() sleep_seconds = _MSECS_BETWEEN_READS / 1000.0 while wrapper.running(): @@ -63,7 +63,7 @@ def get_history(self): return self._history.get_copy() # use the logger to determine whether a line is pertinent to history - def process_output(self, line: str | None) -> None: + def process_output(self, line: str | None): if line: log_record = json.loads(line) if not self._logger.log(log_record): diff --git a/lib/toolbox/fix_config.py b/lib/toolbox/fix_config.py index 2aa4288..326fd3a 100644 --- a/lib/toolbox/fix_config.py +++ b/lib/toolbox/fix_config.py @@ -222,7 +222,7 @@ def update_legacy_items(): } -def update_conversions() -> None: +def update_conversions(): if whenever_has_dbus(): for k in CONVERSIONS_DBUS: CONVERSIONS[k] = CONVERSIONS_DBUS[k] diff --git a/lib/trayapp.py b/lib/trayapp.py index dd7652a..ef8f264 100644 --- a/lib/trayapp.py +++ b/lib/trayapp.py @@ -23,56 +23,56 @@ # menu reactions: all events are managed by the main application, and # therefore they are implemented as messages passed to the root window -def on_configure(root) -> None: +def on_configure(root): root.send_event("<>") -def on_about(root) -> None: +def on_about(root): root.send_event("<>") -def on_menu_box(root) -> None: +def on_menu_box(root): root.send_event("<>") -def on_pause_scheduler(root) -> None: +def on_pause_scheduler(root): root.send_event("<>") -def on_resume_scheduler(root) -> None: +def on_resume_scheduler(root): root.send_event("<>") -def on_reset_conditions(root) -> None: +def on_reset_conditions(root): root.send_event("<>") -def on_history(root) -> None: +def on_history(root): root.send_event("<>") # this sends an EXIT event to the main loop, so that the invisible # main window is destroyed and all the cleanup is performed -def on_exit(root) -> None: +def on_exit(root): root.send_exit() # set icon color to gray/color: these functions are called by the main # application to change the icon status when pausing/resuming the scheduler -def set_tray_icon_gray(icon) -> None: +def set_tray_icon_gray(icon): icon.icon = _tray_icon_gray -def set_tray_icon_normal(icon) -> None: +def set_tray_icon_normal(icon): icon.icon = _tray_icon -def set_tray_icon_busy(icon) -> None: +def set_tray_icon_busy(icon): icon.icon = _tray_icon_busy # entry point for the tray resident application -def main(root) -> None: +def main(root): # create the menu: this menu is OK for Windows and for Linux environments # that implement the `AppIndicator` protocol; unfortunately this protocol # seems to be deprecated by teh folks at Gnome, together with almost diff --git a/lib/utility.py b/lib/utility.py index 55896df..c676fb9 100644 --- a/lib/utility.py +++ b/lib/utility.py @@ -358,7 +358,7 @@ def get_lua_staticlib_path() -> str: # save a script to the scripts directory and make it executable: possible # existing files are overwritten without confirmation as the scripts folder # should be completely managed by When -def save_script(fname, text) -> None: +def save_script(fname, text): dest = os.path.join(get_scriptsdir(), fname) with open(dest, "w") as f: f.write(text) @@ -400,7 +400,7 @@ def check_whenever_version() -> bool: # return the output of `whenever --options` -def retrieve_whenever_options() -> None: +def retrieve_whenever_options(): # first assume no feature is available AppConfig.set("WHENEVER_HAS_DBUS", False) AppConfig.set("WHENEVER_HAS_WMI", False) @@ -533,12 +533,12 @@ def get_editor_theme(): # write a warning to stderr -def write_warning(s) -> None: +def write_warning(s): _err_console.print(f"[bold yellow]{UI_APP} - warning:[/] {s}", highlight=False) # write an error to stderr -def write_error(s) -> None: +def write_error(s): _err_console.print(f"[bold red]{UI_APP} - ERROR:[/] {s}", highlight=False) @@ -660,7 +660,7 @@ def clean_caption(s) -> str: # initialize the logger -def init_logger(filename, level, app=None) -> None: +def init_logger(filename, level, app=None): global _logger _logger = Logger(filename, level, app) diff --git a/pyproject.toml b/pyproject.toml index 1365087..fb98f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.1.1-rc1" +version = "2.1.1-rc2" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, diff --git a/when/when.py b/when/when.py index 4894f3b..4bab9ab 100644 --- a/when/when.py +++ b/when/when.py @@ -129,32 +129,32 @@ def __init__(self): self._busy = False # the main loop is mandatory to react to events - def run(self) -> None: + def run(self): if self._window is not None: self._window.mainloop() # the scheduler wrapper is needed as it provides access to task history - def set_wrapper(self, wrapper) -> None: + def set_wrapper(self, wrapper): self._wrapper = wrapper # the tray icon if any - def set_trayicon(self, icon) -> None: + def set_trayicon(self, icon): self._trayicon = icon # send an event to the main loop asynchronously - def send_event(self, event: str) -> None: + def send_event(self, event: str): if self._window: self._window.update() self._window.event_generate(event) # shortcut to send an EXIT event - def send_exit(self) -> None: + def send_exit(self): if self._window: self._window.update() self._window.event_generate("<>") # destroy the window, stop whenever, and cleanup internals - def destroy(self) -> None: + def destroy(self): if self._wrapper: self._wrapper.whenever_exit() self._wrapper = None @@ -172,14 +172,14 @@ def destroy(self) -> None: # event reactions: these are called by the systray app that runs in a # separate, detached thread so that it does not slow down the main loop - def open_history(self, _) -> None: + def open_history(self, _): if self._window and self._wrapper: form = self.form_History(self._wrapper) form.run() del form gc.collect() - def sched_pause(self, _) -> None: + def sched_pause(self, _): if not self._paused and self._window and self._wrapper: log = get_logger().context().use(emitter="FRONTEND") log.use( @@ -190,7 +190,7 @@ def sched_pause(self, _) -> None: ).log("pausing the scheduler") self._wrapper.whenever_pause() - def sched_resume(self, _) -> None: + def sched_resume(self, _): if self._paused and self._window and self._wrapper: log = get_logger().context().use(emitter="FRONTEND") log.use( @@ -201,7 +201,7 @@ def sched_resume(self, _) -> None: ).log("resuming scheduler activity") self._wrapper.whenever_resume() - def sched_reset_conditions(self, _) -> None: + def sched_reset_conditions(self, _): if self._window and self._wrapper: log = get_logger().context().use(emitter="FRONTEND") log.use( @@ -212,7 +212,7 @@ def sched_reset_conditions(self, _) -> None: ).log("resetting all conditions") self._wrapper.whenever_reset_conditions() - def sched_reload_configuration(self, _) -> None: + def sched_reload_configuration(self, _): if self._window and self._wrapper: log = get_logger().context().use(emitter="FRONTEND") log.use( @@ -223,7 +223,7 @@ def sched_reload_configuration(self, _) -> None: ).log("reloading configuration") self._wrapper.whenever_reload_configuration() - def sched_icon_busy(self, _) -> None: + def sched_icon_busy(self, _): if self._icon: # check current status to avoid useless icon swaps if not self._busy: @@ -231,7 +231,7 @@ def sched_icon_busy(self, _) -> None: if not self._paused: self.set_tray_icon_busy(self._trayicon) - def sched_icon_not_busy(self, _) -> None: + def sched_icon_not_busy(self, _): if self._icon: # check current status to avoid useless icon swaps if self._busy: @@ -241,14 +241,14 @@ def sched_icon_not_busy(self, _) -> None: else: self.set_tray_icon_normal(self._trayicon) - def sched_icon_paused(self, _) -> None: + def sched_icon_paused(self, _): if self._icon: # check current status to avoid useless icon swaps if not self._paused: self._paused = True self.set_tray_icon_gray(self._trayicon) - def sched_icon_not_paused(self, _) -> None: + def sched_icon_not_paused(self, _): if self._icon: # check current status to avoid useless icon swaps if self._paused: @@ -258,26 +258,26 @@ def sched_icon_not_paused(self, _) -> None: else: self.set_tray_icon_normal(self._trayicon) - def open_cfgapp(self, _) -> None: + def open_cfgapp(self, _): if self._window: form = self.form_Config(self) form.run() del form gc.collect() - def open_menubox(self, _) -> None: + def open_menubox(self, _): if self._window and self._wrapper: form = self.form_MenuBox(self) form.run() del form gc.collect() - def open_aboutbox(self, _) -> None: + def open_aboutbox(self, _): if self._window: self.show_about_box(False) gc.collect() - def exit_app(self, _) -> None: + def exit_app(self, _): self.destroy() @@ -286,13 +286,13 @@ def exit_app(self, _) -> None: # displays a window, because all other forms are just non-root toplevels; # NOTE: every windowed subprogram *must* receive a reference to the root # window and take care of sending a `send_exit()` event when finishing -def setup_windows() -> None: +def setup_windows(): global _root _root = App() # prepare expected environment, such as configuration directory -def prepare_environment() -> None: +def prepare_environment(): # create the application data directory if it does not exist try: _ = get_appdata() @@ -346,13 +346,13 @@ def check_prepare_features(): # version: display the application version and exit -def main_version(_) -> None: +def main_version(_): # print plain text: could be programatically used to determine version print("%s: v%s" % (UI_APP, UI_APP_VERSION)) # config: enter the configuration utility and exit (no scheduler launched) -def main_config(args) -> None: +def main_config(args): # set some global configuration values according to CLI options AppConfig.delete("APPDATA") AppConfig.set("APPDATA", args.dir_appdata) @@ -390,7 +390,7 @@ def main_config(args) -> None: # start: start the scheduler in the background and display the tray icon -def main_start(args) -> None: +def main_start(args): # set some global configuration values according to CLI options AppConfig.delete("APPDATA") AppConfig.set("APPDATA", args.dir_appdata) @@ -505,7 +505,7 @@ def main_start(args) -> None: # toolbox: various utilities that can help build a proper setup -def main_toolbox(args) -> None: +def main_toolbox(args): AppConfig.delete("APPDATA") AppConfig.set("APPDATA", args.dir_appdata) prepare_environment() @@ -660,7 +660,7 @@ def main_toolbox(args) -> None: # main program: perform CLI parsing and run the appropriate subcommand -def main() -> None: +def main(): global _root default_appdata = get_default_configdir() diff --git a/when/when_bg.pyw b/when/when_bg.pyw index 9d222da..a230433 100644 --- a/when/when_bg.pyw +++ b/when/when_bg.pyw @@ -5,7 +5,7 @@ from .when import main from lib.platform import is_windows -def run_bg() -> None: +def run_bg(): if is_windows(): if os.path.basename(sys.argv[0]) != os.path.basename(sys.executable): pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") From 89cf5d5f1e2ce408f2071ac6d07ba232d091b9d7 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 10 Apr 2026 15:24:09 +0200 Subject: [PATCH 53/57] fix: check that an item can be deleted Check dependencies of conditions from tasks and of events from conditions before deletion, and show an error message if other items depend on the one being deleted; this should close #194 --- lib/forms/cfgform.py | 16 +++++++++++++--- lib/i18n/strings_it.py | 4 ++-- lib/items/cond.py | 7 +++++++ lib/items/event.py | 3 +++ lib/items/task.py | 8 ++++++++ 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/forms/cfgform.py b/lib/forms/cfgform.py index 9f16ff1..a2a7140 100644 --- a/lib/forms/cfgform.py +++ b/lib/forms/cfgform.py @@ -377,7 +377,9 @@ def _save_config(self, fn): n_mcrt_updater = mcrt.updater_name() mcrt_active = False for cond in self._conditions.values(): - if isinstance(cond, mcrt.ConfluenceCondition) or mcrt.is_confluent_cond(cond): + if isinstance(cond, mcrt.ConfluenceCondition) or mcrt.is_confluent_cond( + cond + ): mcrt_active = True break if mcrt_active: @@ -431,9 +433,17 @@ def delete(self): item_type = item_signature.split(":", 1)[0] if self.messagebox.askyesno(UI_POPUP_T_CONFIRM, UI_POPUP_DELETEITEM_Q): if item_type == "task": - del self._tasks[item_name] + e = self._tasks[item_name] + if e.can_be_removed(self._conditions): + del self._tasks[item_name] + else: + self.messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_REFERENCEDTASK) elif item_type == "cond": - del self._conditions[item_name] + e = self._conditions[item_name] + if e.can_be_removed(self._events): + del self._conditions[item_name] + else: + self.messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_REFERENCEDCOND) elif item_type == "event": del self._events[item_name] self._updatedata() diff --git a/lib/i18n/strings_it.py b/lib/i18n/strings_it.py index 237410a..2c7033c 100644 --- a/lib/i18n/strings_it.py +++ b/lib/i18n/strings_it.py @@ -299,8 +299,8 @@ UI_POPUP_EMPTYCHECKVALUE = "Nessun valore fornito per la verifica" UI_POPUP_INVALIDFILEORDIR = "File o nome della directory non valido" UI_POPUP_INVALIDTIMESPEC = "Specifiche di tempo non valide o mancanti" -UI_POPUP_REFERENCEDTASK = "Il task è ancora citato in almeno\nUna condizione: rimuovere qualsiasi riferimento\nPrima di tentare di eliminarlo" -UI_POPUP_REFERENCEDCOND = "La condizione è ancora citata in almeno\nUn evento: rimuovere qualsiasi riferimento prima\ndi tentare di eliminarla" +UI_POPUP_REFERENCEDTASK = "Il task è ancora citato in almeno\nuna condizione: rimuovere qualsiasi riferimento\nprima di tentare di eliminarlo" +UI_POPUP_REFERENCEDCOND = "La condizione è ancora citata in almeno\nun evento: rimuovere qualsiasi riferimento prima\ndi tentare di eliminarla" UI_POPUP_MISSINGEVENTCOND = "Nessuna condizione specificata per l'evento" UI_POPUP_INVALIDOPERATOR = "L'operatore specificato non è valido" UI_POPUP_INVALIDINDEX = "L'indice specificato non è valido" diff --git a/lib/items/cond.py b/lib/items/cond.py index 9c8a354..3837f7f 100644 --- a/lib/items/cond.py +++ b/lib/items/cond.py @@ -27,6 +27,7 @@ ) from .itemhelp import CheckedTable, ConfigurationError +from .event import Event # base class for conditions: all condition items will have the same interface @@ -226,6 +227,12 @@ def signature(self): def private(self): assert isinstance(self.name, str) return is_private_item_name(self.name) + + def can_be_removed(self, all_events: dict[str, Event]) -> bool: + for k in all_events: + if self.name == all_events[k].condition: + return False + return True def as_table(self): if not check_not_none( diff --git a/lib/items/event.py b/lib/items/event.py index fe03198..47faaf4 100644 --- a/lib/items/event.py +++ b/lib/items/event.py @@ -205,6 +205,9 @@ def private(self) -> bool: assert isinstance(self.name, str) return is_private_item_name(self.name) + def can_be_removed(self) -> bool: + return True + def as_table(self) -> items.Table: if not check_not_none( self.name, diff --git a/lib/items/task.py b/lib/items/task.py index e5b34c2..33e93bc 100644 --- a/lib/items/task.py +++ b/lib/items/task.py @@ -16,6 +16,7 @@ is_private_item_name, ) +from .cond import Condition from .itemhelp import CheckedTable, ConfigurationError @@ -190,6 +191,13 @@ def private(self): assert isinstance(self.name, str) return is_private_item_name(self.name) + def can_be_removed(self, all_conds: dict[str, Condition]) -> bool: + for k in all_conds: + tasks = all_conds[k].tasks + if tasks is not None and self.name in tasks: + return False + return True + def as_table(self): if not check_not_none( self.name, From 95fb13efd8b49f4cb639b4290518c87b6a6f4ae0 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 10 Apr 2026 15:39:29 +0200 Subject: [PATCH 54/57] fix(ui): fixes in string resources and I18N --- lib/i18n/strings.py | 2 +- lib/i18n/strings_base.py | 4 ++-- lib/i18n/strings_en.py | 4 ++-- lib/i18n/strings_fr.py | 4 ++-- pyproject.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index 6b5ae80..ab89f4e 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-rc2" +UI_APP_VERSION = "2.1.1-rc3" # other strings that should not be translated UI_WHENEVER = "whenever" diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index 91ce936..d18b249 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -299,8 +299,8 @@ UI_POPUP_EMPTYCHECKVALUE = "No value provided for check" UI_POPUP_INVALIDFILEORDIR = "Invalid file or directory name" UI_POPUP_INVALIDTIMESPEC = "Invalid or missing time specification" -UI_POPUP_REFERENCEDTASK = "The Task is still referenced in at least\none Condition: remove any references\nbefore attempting to delete it" -UI_POPUP_REFERENCEDCOND = "The Condition is still referenced in at least\none Event: remove any references before\nattempting to delete it" +UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least\none condition: remove any references\nbefore attempting to delete it" +UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least\none event: remove any references before\nattempting to delete it" UI_POPUP_MISSINGEVENTCOND = "No condition specified for event" UI_POPUP_INVALIDOPERATOR = "The specified operator is not valid" UI_POPUP_INVALIDINDEX = "The specified index is not valid" diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index 91ce936..d18b249 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -299,8 +299,8 @@ UI_POPUP_EMPTYCHECKVALUE = "No value provided for check" UI_POPUP_INVALIDFILEORDIR = "Invalid file or directory name" UI_POPUP_INVALIDTIMESPEC = "Invalid or missing time specification" -UI_POPUP_REFERENCEDTASK = "The Task is still referenced in at least\none Condition: remove any references\nbefore attempting to delete it" -UI_POPUP_REFERENCEDCOND = "The Condition is still referenced in at least\none Event: remove any references before\nattempting to delete it" +UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least\none condition: remove any references\nbefore attempting to delete it" +UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least\none event: remove any references before\nattempting to delete it" UI_POPUP_MISSINGEVENTCOND = "No condition specified for event" UI_POPUP_INVALIDOPERATOR = "The specified operator is not valid" UI_POPUP_INVALIDINDEX = "The specified index is not valid" diff --git a/lib/i18n/strings_fr.py b/lib/i18n/strings_fr.py index 7894873..730ecb9 100644 --- a/lib/i18n/strings_fr.py +++ b/lib/i18n/strings_fr.py @@ -299,8 +299,8 @@ UI_POPUP_EMPTYCHECKVALUE = "Aucune valeur prévue pour le test" UI_POPUP_INVALIDFILEORDIR = "Nom de fichier ou de répertoire non valide" UI_POPUP_INVALIDTIMESPEC = "Spécification de temps non valide ou manquante" -UI_POPUP_REFERENCEDTASK = "La task est toujours référencée au moins\nUne condition: supprimez toutes les références\navant d'essayer de le supprimer" -UI_POPUP_REFERENCEDCOND = "La condition est toujours référencée au moins\nUn événement: supprimez toutes les références avant\nd'essayer de le supprimer" +UI_POPUP_REFERENCEDTASK = "Le task est toujours référencée au moins dans\nune condition: supprimez toutes les références\navant d'essayer de l'éliminer'" +UI_POPUP_REFERENCEDCOND = "La condition est toujours référencée au moins dans\nun événement: supprimez toutes les références avant\nd'essayer de l'éliminer" UI_POPUP_MISSINGEVENTCOND = "Aucune condition spécifiée pour l'événement" UI_POPUP_INVALIDOPERATOR = "L'opérateur spécifié n'est pas valide" UI_POPUP_INVALIDINDEX = "L'indice spécifié n'est pas valide" diff --git a/pyproject.toml b/pyproject.toml index fb98f8f..f26e317 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.1.1-rc2" +version = "2.1.1-rc3" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" }, From 3d839ddec7e6b7ffe35ab137aea687e4617f391a Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 10 Apr 2026 15:58:00 +0200 Subject: [PATCH 55/57] fix(ui): let the UI decide how to break long text --- lib/i18n/strings_base.py | 10 +++++----- lib/i18n/strings_de.py | 6 +++--- lib/i18n/strings_en.py | 10 +++++----- lib/i18n/strings_fr.py | 6 +++--- lib/i18n/strings_it.py | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/i18n/strings_base.py b/lib/i18n/strings_base.py index d18b249..7c7d946 100644 --- a/lib/i18n/strings_base.py +++ b/lib/i18n/strings_base.py @@ -299,16 +299,16 @@ UI_POPUP_EMPTYCHECKVALUE = "No value provided for check" UI_POPUP_INVALIDFILEORDIR = "Invalid file or directory name" UI_POPUP_INVALIDTIMESPEC = "Invalid or missing time specification" -UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least\none condition: remove any references\nbefore attempting to delete it" -UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least\none event: remove any references before\nattempting to delete it" +UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least one condition: remove any references before attempting to delete it" +UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least one event: remove any references before attempting to delete it" UI_POPUP_MISSINGEVENTCOND = "No condition specified for event" UI_POPUP_INVALIDOPERATOR = "The specified operator is not valid" UI_POPUP_INVALIDINDEX = "The specified index is not valid" UI_POPUP_INVALIDFIELD = "The specified field name is not valid" UI_POPUP_WARNFIXCONFIG = f"""\ -One or more items are not recognized: if the configuration -has been generated with a previous version of {UI_APP}, you may -need to run `{CLI_APP} tool --fix-config` from the command line. +One or more items are not recognized: if the configuration \ +has been generated with a previous version of {UI_APP}, you may \ +need to run `{CLI_APP} tool --fix-config` from the command line.\ """ diff --git a/lib/i18n/strings_de.py b/lib/i18n/strings_de.py index b48a4a8..822cae3 100644 --- a/lib/i18n/strings_de.py +++ b/lib/i18n/strings_de.py @@ -299,13 +299,13 @@ UI_POPUP_EMPTYCHECKVALUE = "Kein Wert für die Prüfung bereitgestellt" UI_POPUP_INVALIDFILEORDIR = "Ungültiger Datei- oder Verzeichnisname" UI_POPUP_INVALIDTIMESPEC = "Ungültige oder fehlende Zeitspezifikation" -UI_POPUP_REFERENCEDTASK = "Der Task wird zumindest immer noch verwiesen\neine Kondition: entfernen Sie alle Referenzen\nbevor Sie versuchen, ihn zu löschen" -UI_POPUP_REFERENCEDCOND = "Die Kondition wird zumindest immer noch verwiesen\nein Ereignis: entfernen Sie vor Beweisvorgängen vorher\nversuch, ihn zu löschen" +UI_POPUP_REFERENCEDTASK = "Der Task wird zumindest immer noch verwiesen eine Kondition: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" +UI_POPUP_REFERENCEDCOND = "Die Kondition wird zumindest immer noch verwiesen ein Ereignis: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" UI_POPUP_MISSINGEVENTCOND = "Keine Kondition für das Ereignis angegeben" UI_POPUP_INVALIDOPERATOR = "Der angegebene Operator ist nicht gültig" UI_POPUP_INVALIDINDEX = "Der angegebene Index ist nicht gültig" UI_POPUP_INVALIDFIELD = "Der angegebene Feldname ist nicht gültig" -UI_POPUP_WARNFIXCONFIG = f"Ein oder mehrere Elemente werden nicht erkannt: wenn die Konfiguration\nwurde mit einer früheren Version von {UI_APP} generiert, sollten Sie vielleicht\n`{CLI_APP} tool --fix-config` durch die Befehlszeile anwenden." +UI_POPUP_WARNFIXCONFIG = f"Ein oder mehrere Elemente werden nicht erkannt: wenn die Konfiguration wurde mit einer früheren Version von {UI_APP} generiert, sollten Sie vielleicht `{CLI_APP} tool --fix-config` durch die Befehlszeile anwenden." # about box diff --git a/lib/i18n/strings_en.py b/lib/i18n/strings_en.py index d18b249..7c7d946 100644 --- a/lib/i18n/strings_en.py +++ b/lib/i18n/strings_en.py @@ -299,16 +299,16 @@ UI_POPUP_EMPTYCHECKVALUE = "No value provided for check" UI_POPUP_INVALIDFILEORDIR = "Invalid file or directory name" UI_POPUP_INVALIDTIMESPEC = "Invalid or missing time specification" -UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least\none condition: remove any references\nbefore attempting to delete it" -UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least\none event: remove any references before\nattempting to delete it" +UI_POPUP_REFERENCEDTASK = "The task is still referenced in at least one condition: remove any references before attempting to delete it" +UI_POPUP_REFERENCEDCOND = "The condition is still referenced in at least one event: remove any references before attempting to delete it" UI_POPUP_MISSINGEVENTCOND = "No condition specified for event" UI_POPUP_INVALIDOPERATOR = "The specified operator is not valid" UI_POPUP_INVALIDINDEX = "The specified index is not valid" UI_POPUP_INVALIDFIELD = "The specified field name is not valid" UI_POPUP_WARNFIXCONFIG = f"""\ -One or more items are not recognized: if the configuration -has been generated with a previous version of {UI_APP}, you may -need to run `{CLI_APP} tool --fix-config` from the command line. +One or more items are not recognized: if the configuration \ +has been generated with a previous version of {UI_APP}, you may \ +need to run `{CLI_APP} tool --fix-config` from the command line.\ """ diff --git a/lib/i18n/strings_fr.py b/lib/i18n/strings_fr.py index 730ecb9..17076a2 100644 --- a/lib/i18n/strings_fr.py +++ b/lib/i18n/strings_fr.py @@ -299,13 +299,13 @@ UI_POPUP_EMPTYCHECKVALUE = "Aucune valeur prévue pour le test" UI_POPUP_INVALIDFILEORDIR = "Nom de fichier ou de répertoire non valide" UI_POPUP_INVALIDTIMESPEC = "Spécification de temps non valide ou manquante" -UI_POPUP_REFERENCEDTASK = "Le task est toujours référencée au moins dans\nune condition: supprimez toutes les références\navant d'essayer de l'éliminer'" -UI_POPUP_REFERENCEDCOND = "La condition est toujours référencée au moins dans\nun événement: supprimez toutes les références avant\nd'essayer de l'éliminer" +UI_POPUP_REFERENCEDTASK = "Le task est toujours référencée au moins dans une condition: supprimez toutes les références avant d'essayer de l'éliminer'" +UI_POPUP_REFERENCEDCOND = "La condition est toujours référencée au moins dans un événement: supprimez toutes les références avant d'essayer de l'éliminer" UI_POPUP_MISSINGEVENTCOND = "Aucune condition spécifiée pour l'événement" UI_POPUP_INVALIDOPERATOR = "L'opérateur spécifié n'est pas valide" UI_POPUP_INVALIDINDEX = "L'indice spécifié n'est pas valide" UI_POPUP_INVALIDFIELD = "Le nom de champ spécifié n'est pas valide" -UI_POPUP_WARNFIXCONFIG = f"Un ou plusieurs éléments ne sont pas reconnus: si la configuration\na été généré avec une version précédente de {UI_APP}, vous pouvez\navoir besoin d'exécuter l'outil `{CLI_APP} --fix-config` à partir de la ligne de commande." +UI_POPUP_WARNFIXCONFIG = f"Un ou plusieurs éléments ne sont pas reconnus: si la configuration a été généré avec une version précédente de {UI_APP}, vous pouvez avoir besoin d'exécuter l'outil `{CLI_APP} --fix-config` à partir de la ligne de commande." # about box diff --git a/lib/i18n/strings_it.py b/lib/i18n/strings_it.py index 2c7033c..d602736 100644 --- a/lib/i18n/strings_it.py +++ b/lib/i18n/strings_it.py @@ -299,13 +299,13 @@ UI_POPUP_EMPTYCHECKVALUE = "Nessun valore fornito per la verifica" UI_POPUP_INVALIDFILEORDIR = "File o nome della directory non valido" UI_POPUP_INVALIDTIMESPEC = "Specifiche di tempo non valide o mancanti" -UI_POPUP_REFERENCEDTASK = "Il task è ancora citato in almeno\nuna condizione: rimuovere qualsiasi riferimento\nprima di tentare di eliminarlo" -UI_POPUP_REFERENCEDCOND = "La condizione è ancora citata in almeno\nun evento: rimuovere qualsiasi riferimento prima\ndi tentare di eliminarla" +UI_POPUP_REFERENCEDTASK = "Il task è ancora citato in almeno una condizione: rimuovere qualsiasi riferimento prima di tentare di eliminarlo" +UI_POPUP_REFERENCEDCOND = "La condizione è ancora citata in almeno un evento: rimuovere qualsiasi riferimento prima di tentare di eliminarla" UI_POPUP_MISSINGEVENTCOND = "Nessuna condizione specificata per l'evento" UI_POPUP_INVALIDOPERATOR = "L'operatore specificato non è valido" UI_POPUP_INVALIDINDEX = "L'indice specificato non è valido" UI_POPUP_INVALIDFIELD = "Il nome del campo specificato non è valido" -UI_POPUP_WARNFIXCONFIG = f"Uno o più elementi non sono stati riconosciuti: se la configurazione\nè stata generata con una versione precedente di {UI_APP}, può\nessere necessario eseguire `{CLI_APP} tool --fix-config` dalla riga di comando." +UI_POPUP_WARNFIXCONFIG = f"Uno o più elementi non sono stati riconosciuti: se la configurazione è stata generata con una versione precedente di {UI_APP}, può essere necessario eseguire `{CLI_APP} tool --fix-config` dalla riga di comando." # about box From e00e58faa3e8eee7879e0be30b153d47d10eed54 Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Fri, 10 Apr 2026 16:09:07 +0200 Subject: [PATCH 56/57] fix(i18n): better German translation --- lib/i18n/strings_de.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/i18n/strings_de.py b/lib/i18n/strings_de.py index 822cae3..9207039 100644 --- a/lib/i18n/strings_de.py +++ b/lib/i18n/strings_de.py @@ -299,8 +299,8 @@ UI_POPUP_EMPTYCHECKVALUE = "Kein Wert für die Prüfung bereitgestellt" UI_POPUP_INVALIDFILEORDIR = "Ungültiger Datei- oder Verzeichnisname" UI_POPUP_INVALIDTIMESPEC = "Ungültige oder fehlende Zeitspezifikation" -UI_POPUP_REFERENCEDTASK = "Der Task wird zumindest immer noch verwiesen eine Kondition: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" -UI_POPUP_REFERENCEDCOND = "Die Kondition wird zumindest immer noch verwiesen ein Ereignis: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" +UI_POPUP_REFERENCEDTASK = "Der Task wird zumindest immer noch in einer Kondition verwendet: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" +UI_POPUP_REFERENCEDCOND = "Die Kondition wird zumindest immer noch in einem Ereignis verwendet: entfernen Sie alle Referenzen bevor Sie versuchen, ihn zu löschen" UI_POPUP_MISSINGEVENTCOND = "Keine Kondition für das Ereignis angegeben" UI_POPUP_INVALIDOPERATOR = "Der angegebene Operator ist nicht gültig" UI_POPUP_INVALIDINDEX = "Der angegebene Index ist nicht gültig" From 410acf368a54837d0b70d58dae7d57c03baab1ab Mon Sep 17 00:00:00 2001 From: Francesco Garosi Date: Sat, 11 Apr 2026 18:55:09 +0200 Subject: [PATCH 57/57] build: bump version for new release --- lib/i18n/strings.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/i18n/strings.py b/lib/i18n/strings.py index ab89f4e..d0f2881 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-rc3" +UI_APP_VERSION = "2.1.1" # other strings that should not be translated UI_WHENEVER = "whenever" diff --git a/pyproject.toml b/pyproject.toml index f26e317..aeeeb03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "when" -version = "2.1.1-rc3" +version = "2.1.1" description = "Interface for the **whenever** automation tool" authors = [ { name = "Francesco Garosi", email = "francesco.garosi@gmail.com" },