├── .github └── workflows │ └── documentation.yml ├── LICENSE ├── README.md ├── lib ├── __init__.py ├── cfgapp.py ├── configurator │ ├── __init__.py │ ├── defaults.py │ ├── reader.py │ └── writer.py ├── extra │ ├── __init__.py │ ├── _template.py │ ├── c_battery_charging_linux.py │ ├── c_battery_charging_win32.py │ ├── c_battery_low_linux.py │ ├── c_battery_low_win32.py │ ├── c_removabledrive_linux.py │ ├── c_removabledrive_win32.py │ ├── c_session_locked_win32.py │ ├── c_sysload_linux.py │ ├── c_sysload_win32.py │ ├── e_session_lock_linux.py │ ├── e_session_unlock_linux.py │ ├── i18n │ │ └── i18n.txt │ ├── t_hybernate.py │ ├── t_locksession.py │ ├── t_logoff.py │ ├── t_reboot.py │ └── t_shutdown.py ├── forms │ ├── __init__.py │ ├── about.py │ ├── cfgform.py │ ├── cond.py │ ├── cond_command.py │ ├── cond_dbus.py │ ├── cond_event.py │ ├── cond_idle.py │ ├── cond_interval.py │ ├── cond_lua.py │ ├── cond_time.py │ ├── cond_wmi.py │ ├── event.py │ ├── event_cli.py │ ├── event_dbus.py │ ├── event_fschange.py │ ├── event_wmi.py │ ├── history.py │ ├── menubox.py │ ├── newitem.py │ ├── task.py │ ├── task_command.py │ ├── task_internal.py │ ├── task_lua.py │ └── ui.py ├── i18n │ ├── __init__.py │ ├── strings.py │ └── strings_base.py ├── icons.py ├── items │ ├── __init__.py │ ├── cond.py │ ├── cond_command.py │ ├── cond_dbus.py │ ├── cond_event.py │ ├── cond_idle.py │ ├── cond_interval.py │ ├── cond_lua.py │ ├── cond_time.py │ ├── cond_wmi.py │ ├── event.py │ ├── event_cli.py │ ├── event_dbus.py │ ├── event_fschange.py │ ├── event_wmi.py │ ├── item.py │ ├── task.py │ ├── task_command.py │ ├── task_internal.py │ └── task_lua.py ├── repocfg.py ├── runner │ ├── __init__.py │ ├── history.py │ ├── logger.py │ └── process.py ├── toolbox │ ├── __init__.py │ ├── create_shortcuts.py │ ├── create_shortcuts_icons.py │ ├── dlutils.py │ ├── fix_config.py │ └── install_whenever.py ├── trayapp.py └── utility.py ├── pyproject.toml ├── support ├── append_icon.py ├── build_html.py ├── docs │ ├── _static │ │ └── favicon.ico │ ├── appdata.md │ ├── cfgform.md │ ├── cli.md │ ├── cond_actionrelated.md │ ├── cond_eventrelated.md │ ├── cond_extra01.md │ ├── cond_timerelated.md │ ├── conditions.md │ ├── conf.py │ ├── configfile.md │ ├── events.md │ ├── events_extra01.md │ ├── graphics │ │ ├── install-gnome-login.png │ │ ├── install-linux-extmgr.png │ │ ├── rafi-clock-256.png │ │ ├── tutorial_cond_chores01.png │ │ ├── tutorial_cond_chores02.png │ │ ├── tutorial_cond_idle01.png │ │ ├── tutorial_cond_idle02.png │ │ ├── tutorial_cond_idle03.png │ │ ├── tutorial_cond_interval01.png │ │ ├── tutorial_cond_interval02.png │ │ ├── tutorial_cond_new_chores01.png │ │ ├── tutorial_cond_new_idle01.png │ │ ├── tutorial_cond_new_interval01.png │ │ ├── tutorial_config_main01.png │ │ ├── tutorial_task_backup01.png │ │ ├── tutorial_task_backup02.png │ │ ├── tutorial_task_chores01.png │ │ ├── tutorial_task_lua01.png │ │ ├── tutorial_task_new_cmd01.png │ │ ├── tutorial_task_new_lua01.png │ │ ├── when-application.png │ │ ├── when-cond-command.png │ │ ├── when-cond-common.png │ │ ├── when-cond-event.png │ │ ├── when-cond-extra-batterycharging.png │ │ ├── when-cond-extra-batterylow.png │ │ ├── when-cond-extra-locked-win.png │ │ ├── when-cond-extra-rmdrive-linux.png │ │ ├── when-cond-extra-rmdrive-win.png │ │ ├── when-cond-extra-sysload.png │ │ ├── when-cond-idle.png │ │ ├── when-cond-interval.png │ │ ├── when-cond-lua.png │ │ ├── when-cond-time.png │ │ ├── when-config-main.png │ │ ├── when-event-extra-lock-linux.png │ │ ├── when-event-fschange.png │ │ ├── when-history.png │ │ ├── when-menu-form.png │ │ ├── when-task-command.png │ │ ├── when-task-extra-session.png │ │ ├── when-task-lua.png │ │ └── when-tray-menu.png │ ├── history.md │ ├── index.rst │ ├── install.md │ ├── main.md │ ├── tasks.md │ ├── tasks_extra_session.md │ ├── tray.md │ └── tutorial.md └── icons │ ├── favicon.ico │ ├── icons8-add-112x24.png │ ├── icons8-add-48.png │ ├── icons8-add-96.png │ ├── icons8-cancel-112x24.png │ ├── icons8-cancel-48.png │ ├── icons8-cancel-96.png │ ├── icons8-check-mark-112x24.png │ ├── icons8-check-mark-48.png │ ├── icons8-check-mark-96.png │ ├── icons8-circle-16.png │ ├── icons8-circled-play-112x24.png │ ├── icons8-circled-play-32.png │ ├── icons8-circled-play-32x32.png │ ├── icons8-circled-play-48.png │ ├── icons8-clock-48-busy.png │ ├── icons8-clock-48.png │ ├── icons8-clock-96.png │ ├── icons8-clock-gray-48.png │ ├── icons8-clock-gray-96.png │ ├── icons8-close-window-112x24.png │ ├── icons8-close-window-48.png │ ├── icons8-close-window-96.png │ ├── icons8-delete-112x24.png │ ├── icons8-delete-48.png │ ├── icons8-delete-96.png │ ├── icons8-enter-112x24.png │ ├── icons8-enter-48.png │ ├── icons8-enter-96.png │ ├── icons8-error-48.png │ ├── icons8-error-96.png │ ├── icons8-exclamation-mark-48.png │ ├── icons8-exclamation-mark-96.png │ ├── icons8-exit-32.png │ ├── icons8-exit-32x32.png │ ├── icons8-exit-48.png │ ├── icons8-file-112x24.png │ ├── icons8-file-48.png │ ├── icons8-file-96.png │ ├── icons8-folder-112x24.png │ ├── icons8-folder-48.png │ ├── icons8-folder-96.png │ ├── icons8-help-20.png │ ├── icons8-index-112x24.png │ ├── icons8-index-32.png │ ├── icons8-index-32x32.png │ ├── icons8-index-48.png │ ├── icons8-kite-shape-16.png │ ├── icons8-medium-priority-20.png │ ├── icons8-new-document-112x24.png │ ├── icons8-new-document-48.png │ ├── icons8-new-document-96.png │ ├── icons8-pause-squared-112x24.png │ ├── icons8-pause-squared-32.png │ ├── icons8-pause-squared-32x32.png │ ├── icons8-pause-squared-48.png │ ├── icons8-pencil-drawing-112x24.png │ ├── icons8-pencil-drawing-48.png │ ├── icons8-pencil-drawing-96.png │ ├── icons8-question-mark-48.png │ ├── icons8-question-mark-96.png │ ├── icons8-remove-112x24.png │ ├── icons8-remove-48.png │ ├── icons8-remove-96.png │ ├── icons8-reset-112x24.png │ ├── icons8-reset-32.png │ ├── icons8-reset-32x32.png │ ├── icons8-reset-48.png │ ├── icons8-save-112x24.png │ ├── icons8-save-48.png │ ├── icons8-save-96.png │ ├── icons8-settings-48.png │ ├── icons8-settings-96.png │ ├── icons8-square-16.png │ ├── icons8-switch-20.png │ ├── icons8-task-20.png │ ├── rafi-clock-128.png │ ├── rafi-clock-256.png │ ├── rafi-clock-32.png │ ├── rafi-clock-64.ico │ └── rafi-clock-64.png └── when ├── __init__.py ├── when.py └── when_bg.pyw /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.11' 16 | - name: Install dependencies 17 | run: | 18 | pip install sphinx sphinx_rtd_theme sphinx-favicon myst_parser 19 | - name: Sphinx build 20 | run: | 21 | sphinx-build support/docs _build 22 | - name: Deploy to GitHub Pages 23 | uses: peaceiris/actions-gh-pages@v3 24 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/docs' }} 25 | with: 26 | publish_branch: gh-pages 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: _build/ 29 | force_orphan: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018, Francesco Garosi 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of when-command nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | # library 2 | -------------------------------------------------------------------------------- /lib/cfgapp.py: -------------------------------------------------------------------------------- 1 | # configuration application: implements the `config` command 2 | 3 | 4 | from lib.forms.cfgform import form_Config 5 | 6 | 7 | # entry point for the configuration application, which is also reachable 8 | # using the system tray menu when using the tray resident application 9 | def main(root): 10 | # not setting the root of form_Config() informs that this is the 11 | # configuration app, thus no `Reload` button should be displayed 12 | form = form_Config() 13 | form.run() 14 | root.send_exit() 15 | 16 | 17 | # end. 18 | -------------------------------------------------------------------------------- /lib/configurator/__init__.py: -------------------------------------------------------------------------------- 1 | # configuration reader/writer module 2 | -------------------------------------------------------------------------------- /lib/configurator/defaults.py: -------------------------------------------------------------------------------- 1 | # some default values 2 | 3 | 4 | DEFAULT_SCHEDULER_TICK_SECONDS = 5 5 | DEFAULT_RANDOMIZE_CHECKS_WITHIN_TICKS = False 6 | 7 | 8 | # end. 9 | -------------------------------------------------------------------------------- /lib/configurator/reader.py: -------------------------------------------------------------------------------- 1 | # reader 2 | # read and parse a configuration file 3 | 4 | from tomlkit import parse 5 | 6 | # import item definitions 7 | from ..items.item import ALL_AVAILABLE_ITEMS_D 8 | from ..utility import write_warning 9 | 10 | 11 | # read the configuration: this reader supports the `tags` entry as table 12 | # so that items different from the standard ones and built on top of them 13 | # can be correctly read and interpreted, and handled by the appropriate 14 | # editor forms; unreadable items are skipped and a warning is printed to 15 | # the standard output 16 | def read_whenever_config(filename): 17 | with open(filename) as f: 18 | toml = f.read() 19 | doc = parse(toml) 20 | res_tasks = [] 21 | res_conditions = [] 22 | res_events = [] 23 | res_globals = { 24 | 'scheduler_tick_seconds': doc.get('scheduler_tick_seconds', 5), 25 | 'randomize_checks_within_ticks': doc.get('randomize_checks_within_ticks', False), 26 | } 27 | if 'task' in doc: 28 | for item_table in doc['task']: 29 | try: 30 | signature = 'task:%s' % item_table['type'] 31 | tags = item_table.get('tags') 32 | if tags: 33 | signature = '%s:%s' % (signature, tags['subtype']) 34 | t = ALL_AVAILABLE_ITEMS_D.get(signature) 35 | if t: 36 | factory = t[2] 37 | item = factory(item_table) 38 | res_tasks.append(item) 39 | else: 40 | write_warning("unknown signature (%s) for task `%s`" % (signature, item_table.get('name', ''))) 41 | except KeyError: 42 | write_warning("skipping malformed task `%s`" % item_table.get('name', '')) 43 | if 'condition' in doc: 44 | for item_table in doc['condition']: 45 | try: 46 | signature = 'cond:%s' % item_table['type'] 47 | tags = item_table.get('tags') 48 | if tags: 49 | signature = '%s:%s' % (signature, tags['subtype']) 50 | t = ALL_AVAILABLE_ITEMS_D.get(signature) 51 | if t: 52 | factory = t[2] 53 | item = factory(item_table) 54 | res_conditions.append(item) 55 | else: 56 | write_warning("unknown signature (%s) for condition `%s`" % (signature, item_table.get('name', ''))) 57 | except KeyError: 58 | write_warning("skipping malformed condition `%s`" % item_table.get('name', '')) 59 | if 'event' in doc: 60 | for item_table in doc['event']: 61 | try: 62 | signature = 'event:%s' % item_table['type'] 63 | tags = item_table.get('tags') 64 | if tags: 65 | signature = '%s:%s' % (signature, tags['subtype']) 66 | t = ALL_AVAILABLE_ITEMS_D.get(signature) 67 | if t: 68 | factory = t[2] 69 | item = factory(item_table) 70 | res_events.append(item) 71 | else: 72 | write_warning("unknown signature (%s) for event `%s`" % (signature, item_table.get('name', ''))) 73 | except KeyError: 74 | write_warning("skipping malformed event `%s`" % item_table.get('name', '')) 75 | return (res_tasks, res_conditions, res_events, res_globals) 76 | 77 | 78 | # end. 79 | -------------------------------------------------------------------------------- /lib/configurator/writer.py: -------------------------------------------------------------------------------- 1 | # writer 2 | # write a configuration file according to passed values and items 3 | 4 | from tomlkit import * 5 | 6 | 7 | # all items have an `as_table()` utility that converts them to TOML tables 8 | def write_whenever_config(filename, tasks, conditions, events, globals): 9 | doc = document() 10 | for k in globals: 11 | if globals[k] is not None: 12 | doc.add(k, item(globals[k])) 13 | t = aot() 14 | for elem in tasks: 15 | t.append(elem.as_table()) 16 | doc.append("task", t) 17 | t = aot() 18 | for elem in conditions: 19 | t.append(elem.as_table()) 20 | doc.append("condition", t) 21 | t = aot() 22 | for elem in events: 23 | t.append(elem.as_table()) 24 | doc.append("event", t) 25 | with open(filename, "w") as f: 26 | f.write(doc.as_string()) 27 | 28 | 29 | # end. 30 | -------------------------------------------------------------------------------- /lib/extra/__init__.py: -------------------------------------------------------------------------------- 1 | # extra items module 2 | # 3 | # dynamically load all items in this directory 4 | 5 | import os 6 | 7 | 8 | # export two dictionaries, one for successfully loaded items/forms and the 9 | # other for items that could not be loaded and thus will not be enabled 10 | factories = {} 11 | not_loaded = {} 12 | 13 | 14 | # load all Python modules in the directory, except the ones whose filename 15 | # begins with an underscore: in this way a `_template.py` module can be put 16 | # in the directory to actually serve as a template; note that, since all 17 | # modules should export the `factories()` function, the names of the item 18 | # and form classes is ininfluent -- if not for default item names in the 19 | # editor forms; factory class names can also be repeated (for example for 20 | # items that do the same things on different platforms) since the file name 21 | # of the module acts as an index, and is necessarily unique 22 | _basepath = os.path.dirname(__file__) 23 | for elem in [ 24 | x[:-3] 25 | for x in os.listdir(_basepath) 26 | if x.lower().endswith(".py") and not x.startswith("_") 27 | ]: 28 | try: 29 | exec("from lib.extra import %s" % elem) 30 | factories[elem] = eval("%s.factories()" % elem) 31 | except Exception as e: 32 | not_loaded[elem] = e 33 | 34 | 35 | # only export the useful objects, that is, the two dictionaries 36 | __all__ = ["factories", "not_loaded"] 37 | 38 | 39 | # end 40 | -------------------------------------------------------------------------------- /lib/extra/_template.py: -------------------------------------------------------------------------------- 1 | # template for extra modules 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | from tkinter import messagebox 9 | 10 | from ..i18n.strings import * 11 | from ..utility import check_not_none, append_not_none 12 | 13 | from ..forms.ui import * 14 | 15 | 16 | # since a condition is defined, the base form is the one for conditions 17 | from ..forms.cond import form_Condition 18 | 19 | # import item to derive from 20 | from ..items.cond_command import CommandCondition 21 | 22 | 23 | # imports specific to this module 24 | import shutil 25 | 26 | 27 | # resource strings (not internationalized for the moment) 28 | ITEM_HR_NAME = "Template Condition" 29 | 30 | _UI_FORM_TITLE = "%s: Template Condition Editor" % UI_APP 31 | _UI_FORM_PARAM1_SC = "Parameter is:" 32 | 33 | 34 | # default values 35 | _DEFAULT_PARAM1_VALUE = "somestring" 36 | 37 | 38 | # check for availability: include all needed checks in this function, may 39 | # or may not include actually checking the hosting platform 40 | def _available(): 41 | if shutil.which("ls"): 42 | return True 43 | return False 44 | 45 | 46 | # the specific item is derived from the actual parent item 47 | class TemplateCondition(CommandCondition): 48 | 49 | # availability at class level: these variables *MUST* be set for all items 50 | item_type = "command" 51 | item_subtype = "template" 52 | item_hrtype = ITEM_HR_NAME 53 | available = _available() 54 | 55 | def __init__(self, t: items.Table = None) -> None: 56 | # first initialize the base class (mandatory) 57 | super().__init__(t) 58 | 59 | # then set type (same as base), subtype and human readable name: this 60 | # is mandatory in order to correctly display the item in all forms 61 | self.type = self.item_type 62 | self.subtype = self.item_subtype 63 | self.hrtype = self.item_hrtype 64 | 65 | # initializing from a table should always have this form: 66 | if t: 67 | assert t.get("type") == self.type 68 | self.tags = t.get("tags") 69 | assert isinstance(self.tags, items.Table) 70 | assert self.tags.get("subtype") == self.subtype 71 | 72 | # while creating a new item must always initialize specific parameters 73 | else: 74 | self.tags = table() 75 | self.tags.append("subtype", self.subtype) 76 | self.tags.append("parameter1", _DEFAULT_PARAM1_VALUE) 77 | 78 | self.updateitem() 79 | 80 | def updateitem(self): 81 | # set base item properties according to specific parameters in `tags` 82 | self.command = "ls" 83 | self.command_arguments = [ 84 | "-l", 85 | self.tags.get("parameter1", _DEFAULT_PARAM1_VALUE), 86 | ] 87 | self.startup_path = "." 88 | self.success_status = 0 89 | 90 | 91 | # dedicated form definition derived directly from one of the base forms 92 | class form_TemplateCondition(form_Condition): 93 | 94 | def __init__(self, tasks_available, item=None): 95 | 96 | # check that item is the expected one for safety, build one by default 97 | if item: 98 | assert isinstance(item, TemplateCondition) 99 | else: 100 | item = TemplateCondition() 101 | super().__init__(_UI_FORM_TITLE, tasks_available, item) 102 | 103 | # create a specific frame for the contents 104 | area = ttk.Frame(super().contents) 105 | area.grid(row=0, column=0, sticky=tk.NSEW) 106 | PAD = WIDGET_PADDING_PIXELS 107 | 108 | # build the UI elements as needed and configure the layout 109 | l_parameter1 = ttk.Label(area, text=_UI_FORM_PARAM1_SC) 110 | e_parameter1 = ttk.Entry(area) 111 | self.data_bind("parameter1", e_parameter1, TYPE_STRING) 112 | 113 | l_parameter1.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 114 | e_parameter1.grid(row=0, column=1, sticky=tk.NSEW, padx=PAD, pady=PAD) 115 | 116 | area.columnconfigure(1, weight=1) 117 | 118 | # always update the form at the end of initialization 119 | self._updateform() 120 | 121 | # update the form with the specific parameters (usually in the `tags`) 122 | def _updateform(self): 123 | self.data_set("parameter1", self._item.tags.get("parameter1")) 124 | return super()._updateform() 125 | 126 | # update the item from the form elements (usually update `tags`) 127 | def _updatedata(self): 128 | self._item.tags["parameter1"] = self.data_get("parameter1") 129 | self._item.updateitem() 130 | return super()._updatedata() 131 | 132 | 133 | # function common to all extra modules to declare class items as factories 134 | def factories(): 135 | return (TemplateCondition, form_TemplateCondition) 136 | 137 | 138 | # end. 139 | -------------------------------------------------------------------------------- /lib/extra/e_session_lock_linux.py: -------------------------------------------------------------------------------- 1 | # Module to create a session locked event for linux systems 2 | # 3 | # On Linux this is implemented via DBus, so it actually does not check 4 | # continuously for the current state, it waits for a specific event with 5 | # specific message parameters instead, being lighter and more reactive 6 | # than its Windows counterpart (which is directly a condition). 7 | # 8 | # WARNING: this module has not been tested, it is in the development 9 | # branch in order to be tested on linux machines. 10 | 11 | # this header is common to all extra modules 12 | from tomlkit import items, table 13 | 14 | import tkinter as tk 15 | import ttkbootstrap as ttk 16 | 17 | from ..i18n.strings import * 18 | from ..utility import whenever_has_dbus 19 | 20 | from ..forms.ui import * 21 | 22 | 23 | # import form to derive from 24 | from ..forms.event import form_Event 25 | 26 | # import item to derive from 27 | from ..items.event_dbus import DBusEvent 28 | 29 | 30 | # imports specific to this module 31 | import sys 32 | 33 | 34 | # resource strings (not internationalized for the moment) 35 | ITEM_HR_NAME = "Session Locked Event" 36 | 37 | _UI_FORM_TITLE = "%s: Session Locked Event Editor" % UI_APP 38 | 39 | 40 | # check for availability: include all needed checks in this function, may 41 | # or may not include actually checking the hosting platform 42 | # check for availability 43 | def _available(): 44 | if sys.platform == "linux": 45 | return whenever_has_dbus() 46 | else: 47 | return False 48 | 49 | 50 | # the DBus filter 51 | _DBUS_FILTER_EXPRESSION = "".join( 52 | ( 53 | """ 54 | type='signal', 55 | sender='org.freedesktop.login1', 56 | interface='org.freedesktop.DBus.Properties', 57 | member='PropertiesChanged' 58 | """ 59 | ) 60 | .strip() 61 | .split() 62 | ) 63 | 64 | # the DBus message parameters check 65 | _DBUS_PARAMETER_CHECK = ( 66 | '[{ "index": [1, "LockedHint"], "operator": "eq", "value": true }]' 67 | ) 68 | 69 | 70 | # the specific item is derived from the actual parent item 71 | class SessionLockEvent(DBusEvent): 72 | 73 | # availability at class level: these variables *MUST* be set for all items 74 | item_type = "dbus" 75 | item_subtype = "session_lock" 76 | item_hrtype = ITEM_HR_NAME 77 | available = _available() 78 | 79 | def __init__(self, t: items.Table = None) -> None: 80 | # first initialize the base class (mandatory) 81 | super().__init__(t) 82 | 83 | # then set type (same as base), subtype and human readable name: this 84 | # is mandatory in order to correctly display the item in all forms 85 | self.type = self.item_type 86 | self.subtype = self.item_subtype 87 | self.hrtype = self.item_hrtype 88 | 89 | # initializing from a table should always have this form: 90 | if t: 91 | assert t.get("type") == self.type 92 | self.tags = t.get("tags") 93 | assert isinstance(self.tags, items.Table) 94 | assert self.tags.get("subtype") == self.subtype 95 | 96 | # while creating a new item must always initialize specific parameters 97 | else: 98 | self.tags = table() 99 | self.tags.append("subtype", self.subtype) 100 | 101 | self.updateitem() 102 | 103 | def updateitem(self): 104 | # set base item properties according to specific parameters in `tags` 105 | self.bus = ":system" 106 | self.rule = _DBUS_FILTER_EXPRESSION 107 | self.parameter_check = _DBUS_PARAMETER_CHECK 108 | 109 | 110 | # dedicated form definition derived directly from one of the base forms 111 | class form_SessionLockEvent(form_Event): 112 | 113 | def __init__(self, conditions_available, item=None): 114 | 115 | # check that item is the expected one for safety, build one by default 116 | if item: 117 | assert isinstance(item, SessionLockEvent) 118 | else: 119 | item = SessionLockEvent() 120 | super().__init__(_UI_FORM_TITLE, conditions_available, item) 121 | 122 | # create a specific frame for the contents 123 | area = ttk.Frame(super().contents) 124 | area.grid(row=0, column=0, sticky=tk.NSEW) 125 | PAD = WIDGET_PADDING_PIXELS 126 | 127 | # build the UI elements as needed and configure the layout 128 | l_parameter1 = ttk.Label(area, text=UI_CAPTION_NOSPECIFICPARAMS) 129 | l_parameter1.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 130 | 131 | # always update the form at the end of initialization 132 | self._updateform() 133 | 134 | # update the form with the specific parameters (usually in the `tags`) 135 | def _updateform(self): 136 | return super()._updateform() 137 | 138 | # update the item from the form elements (usually update `tags`) 139 | def _updatedata(self): 140 | self._item.updateitem() 141 | return super()._updatedata() 142 | 143 | 144 | # function common to all extra modules to declare class items as factories 145 | def factories(): 146 | return (SessionLockEvent, form_SessionLockEvent) 147 | 148 | 149 | # end. 150 | -------------------------------------------------------------------------------- /lib/extra/e_session_unlock_linux.py: -------------------------------------------------------------------------------- 1 | # Module to create a session unlocked event for linux systems 2 | # 3 | # On Linux this is implemented via DBus, so it actually does not check 4 | # continuously for the current state, it waits for a specific event with 5 | # specific message parameters instead, being lighter and more reactive 6 | # than its Windows counterpart (which is directly a condition). 7 | # 8 | # WARNING: this module has not been tested, it is in the development 9 | # branch in order to be tested on linux machines. 10 | 11 | # this header is common to all extra modules 12 | from tomlkit import items, table 13 | 14 | import tkinter as tk 15 | import ttkbootstrap as ttk 16 | 17 | from ..i18n.strings import * 18 | from ..utility import whenever_has_dbus 19 | 20 | from ..forms.ui import * 21 | 22 | 23 | # import form to derive from 24 | from ..forms.event import form_Event 25 | 26 | # import item to derive from 27 | from ..items.event_dbus import DBusEvent 28 | 29 | 30 | # imports specific to this module 31 | import sys 32 | 33 | 34 | # resource strings (not internationalized for the moment) 35 | ITEM_HR_NAME = "Session Unlocked Event" 36 | 37 | _UI_FORM_TITLE = "%s: Session Unlocked Event Editor" % UI_APP 38 | 39 | 40 | # check for availability: include all needed checks in this function, may 41 | # or may not include actually checking the hosting platform 42 | # check for availability 43 | def _available(): 44 | if sys.platform == "linux": 45 | return whenever_has_dbus() 46 | else: 47 | return False 48 | 49 | 50 | # the DBus filter 51 | _DBUS_FILTER_EXPRESSION = "".join( 52 | ( 53 | """ 54 | type='signal', 55 | sender='org.freedesktop.login1', 56 | interface='org.freedesktop.DBus.Properties', 57 | member='PropertiesChanged' 58 | """ 59 | ) 60 | .strip() 61 | .split() 62 | ) 63 | 64 | # the DBus message parameters check 65 | _DBUS_PARAMETER_CHECK = ( 66 | '[{ "index": [1, "LockedHint"], "operator": "eq", "value": false }]' 67 | ) 68 | 69 | 70 | # the specific item is derived from the actual parent item 71 | class SessionUnlockEvent(DBusEvent): 72 | 73 | # availability at class level: these variables *MUST* be set for all items 74 | item_type = "dbus" 75 | item_subtype = "session_unlock" 76 | item_hrtype = ITEM_HR_NAME 77 | available = _available() 78 | 79 | def __init__(self, t: items.Table = None) -> None: 80 | # first initialize the base class (mandatory) 81 | super().__init__(t) 82 | 83 | # then set type (same as base), subtype and human readable name: this 84 | # is mandatory in order to correctly display the item in all forms 85 | self.type = self.item_type 86 | self.subtype = self.item_subtype 87 | self.hrtype = self.item_hrtype 88 | 89 | # initializing from a table should always have this form: 90 | if t: 91 | assert t.get("type") == self.type 92 | self.tags = t.get("tags") 93 | assert isinstance(self.tags, items.Table) 94 | assert self.tags.get("subtype") == self.subtype 95 | 96 | # while creating a new item must always initialize specific parameters 97 | else: 98 | self.tags = table() 99 | self.tags.append("subtype", self.subtype) 100 | 101 | self.updateitem() 102 | 103 | def updateitem(self): 104 | # set base item properties according to specific parameters in `tags` 105 | self.bus = ":system" 106 | self.rule = _DBUS_FILTER_EXPRESSION 107 | self.parameter_check = _DBUS_PARAMETER_CHECK 108 | 109 | 110 | # dedicated form definition derived directly from one of the base forms 111 | class form_SessionUnlockEvent(form_Event): 112 | 113 | def __init__(self, conditions_available, item=None): 114 | 115 | # check that item is the expected one for safety, build one by default 116 | if item: 117 | assert isinstance(item, SessionUnlockEvent) 118 | else: 119 | item = SessionUnlockEvent() 120 | super().__init__(_UI_FORM_TITLE, conditions_available, item) 121 | 122 | # create a specific frame for the contents 123 | area = ttk.Frame(super().contents) 124 | area.grid(row=0, column=0, sticky=tk.NSEW) 125 | PAD = WIDGET_PADDING_PIXELS 126 | 127 | # build the UI elements as needed and configure the layout 128 | l_parameter1 = ttk.Label(area, text=UI_CAPTION_NOSPECIFICPARAMS) 129 | l_parameter1.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 130 | 131 | # always update the form at the end of initialization 132 | self._updateform() 133 | 134 | # update the form with the specific parameters (usually in the `tags`) 135 | def _updateform(self): 136 | return super()._updateform() 137 | 138 | # update the item from the form elements (usually update `tags`) 139 | def _updatedata(self): 140 | self._item.updateitem() 141 | return super()._updatedata() 142 | 143 | 144 | # function common to all extra modules to declare class items as factories 145 | def factories(): 146 | return (SessionUnlockEvent, form_SessionUnlockEvent) 147 | 148 | 149 | # end. 150 | -------------------------------------------------------------------------------- /lib/extra/i18n/i18n.txt: -------------------------------------------------------------------------------- 1 | Place internationalization files for extra items here. -------------------------------------------------------------------------------- /lib/extra/t_hybernate.py: -------------------------------------------------------------------------------- 1 | # hybernate 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | 9 | from ..i18n.strings import * 10 | 11 | from ..forms.ui import * 12 | 13 | 14 | # since a task is defined, the base form is the one for tasks 15 | from ..forms.task import form_Task 16 | 17 | # import item to derive from 18 | from ..items.task_command import CommandTask 19 | 20 | 21 | # imports specific to this module 22 | import shutil 23 | import sys 24 | 25 | 26 | # resource strings (not internationalized for the moment) 27 | ITEM_HR_NAME = "Hybernate Task" 28 | 29 | _UI_FORM_TITLE = "%s: Hybernate Task Editor" % UI_APP 30 | 31 | _UI_FORM_NOPARAMS = "(This type of item does not need any specific parameters)" 32 | 33 | 34 | # default values 35 | 36 | 37 | # check for availability (at the moment Windows only) 38 | def _available(): 39 | if sys.platform.startswith("win"): 40 | if shutil.which("shutdown.exe"): 41 | return True 42 | return False 43 | # elif sys.platform == 'linux': 44 | # global ITEM_HR_NAME, _UI_FORM_TITLE 45 | # ITEM_HR_NAME += " (Gnome)" 46 | # _UI_FORM_TITLE += " (Gnome)" 47 | # if shutil.which("gnome-session-quit"): 48 | # return True 49 | # return False 50 | return False 51 | 52 | 53 | # the specific item is derived from the actual parent item 54 | class HybernateTask(CommandTask): 55 | 56 | # availability at class level: these variables *MUST* be set for all items 57 | item_type = "command" 58 | item_subtype = "hybernate" 59 | item_hrtype = ITEM_HR_NAME 60 | available = _available() 61 | 62 | def __init__(self, t: items.Table = None) -> None: 63 | # first initialize the base class (mandatory) 64 | CommandTask.__init__(self, t) 65 | 66 | # then set type (same as base), subtype and human readable name: this 67 | # is mandatory in order to correctly display the item in all forms 68 | self.type = self.item_type 69 | self.subtype = self.item_subtype 70 | self.hrtype = self.item_hrtype 71 | 72 | # initializing from a table should always have this form: 73 | if t: 74 | assert t.get("type") == self.type 75 | self.tags = t.get("tags") 76 | assert isinstance(self.tags, items.Table) 77 | assert self.tags.get("subtype") == self.subtype 78 | 79 | # while creating a new item must always initialize specific parameters 80 | else: 81 | self.tags = table() 82 | self.tags.append("subtype", self.subtype) 83 | 84 | self.updateitem() 85 | 86 | def updateitem(self): 87 | # set base item properties according to specific parameters in `tags` 88 | if sys.platform.startswith("win"): 89 | self.command = "shutdown.exe" 90 | self.command_arguments = ["/s"] 91 | # elif sys.platform == 'linux': 92 | # self.command = "gnome-session-quit" 93 | # self.command_arguments = [ 94 | # # "--no-prompt", 95 | # # "--hybernate", 96 | # ] 97 | self.startup_path = "." 98 | 99 | 100 | # dedicated form definition derived directly from one of the base forms 101 | class form_HybernateTask(form_Task): 102 | 103 | def __init__(self, item=None): 104 | 105 | # check that item is the expected one for safety, build one by default 106 | if item: 107 | assert isinstance(item, HybernateTask) 108 | else: 109 | item = HybernateTask() 110 | super().__init__(_UI_FORM_TITLE, item) 111 | 112 | # create a specific frame for the contents 113 | area = ttk.Frame(super().contents) 114 | area.grid(row=0, column=0, sticky=tk.NSEW) 115 | PAD = WIDGET_PADDING_PIXELS 116 | 117 | # build the UI elements as needed and configure the layout 118 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 119 | l_noparams.configure(anchor=tk.CENTER) 120 | pad = ttk.Frame(area) 121 | 122 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 123 | pad.grid(row=10, column=0, sticky=tk.NSEW) 124 | 125 | area.columnconfigure(0, weight=1) 126 | area.rowconfigure(10, weight=1) 127 | 128 | # always update the form at the end of initialization 129 | self._updateform() 130 | 131 | # no need actually for this definition 132 | # def _updateform(self): 133 | # return super()._updateform() 134 | 135 | # no need actually for this definition 136 | # def _updatedata(self): 137 | # return super()._updatedata() 138 | 139 | 140 | # function common to all extra modules to declare class items as factories 141 | def factories(): 142 | return (HybernateTask, form_HybernateTask) 143 | 144 | 145 | # end. 146 | -------------------------------------------------------------------------------- /lib/extra/t_locksession.py: -------------------------------------------------------------------------------- 1 | # lock the session 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | 9 | from ..i18n.strings import * 10 | 11 | from ..forms.ui import * 12 | 13 | 14 | # since a task is defined, the base form is the one for tasks 15 | from ..forms.task import form_Task 16 | 17 | # import item to derive from 18 | from ..items.task_command import CommandTask 19 | 20 | 21 | # imports specific to this module 22 | import shutil 23 | import sys 24 | 25 | 26 | # resource strings (not internationalized for the moment) 27 | ITEM_HR_NAME = "Session Lock Task" 28 | 29 | _UI_FORM_TITLE = "%s: Session Lock Task Editor" % UI_APP 30 | 31 | _UI_FORM_NOPARAMS = "(This type of item does not need any specific parameters)" 32 | 33 | 34 | # default values 35 | 36 | 37 | # check for availability 38 | def _available(): 39 | if sys.platform.startswith("win"): 40 | if shutil.which("rundll32.exe") and shutil.which("user32.dll"): 41 | return True 42 | return False 43 | elif sys.platform == "linux": 44 | if shutil.which("dbus-send"): 45 | return True 46 | return False 47 | return False 48 | 49 | 50 | # the specific item is derived from the actual parent item 51 | class LockSessionTask(CommandTask): 52 | 53 | # availability at class level: these variables *MUST* be set for all items 54 | item_type = "command" 55 | item_subtype = "lock_session" 56 | item_hrtype = ITEM_HR_NAME 57 | available = _available() 58 | 59 | def __init__(self, t: items.Table = None) -> None: 60 | # first initialize the base class (mandatory) 61 | CommandTask.__init__(self, t) 62 | 63 | # then set type (same as base), subtype and human readable name: this 64 | # is mandatory in order to correctly display the item in all forms 65 | self.type = self.item_type 66 | self.subtype = self.item_subtype 67 | self.hrtype = self.item_hrtype 68 | 69 | # initializing from a table should always have this form: 70 | if t: 71 | assert t.get("type") == self.type 72 | self.tags = t.get("tags") 73 | assert isinstance(self.tags, items.Table) 74 | assert self.tags.get("subtype") == self.subtype 75 | 76 | # while creating a new item must always initialize specific parameters 77 | else: 78 | self.tags = table() 79 | self.tags.append("subtype", self.subtype) 80 | 81 | self.updateitem() 82 | 83 | def updateitem(self): 84 | # set base item properties according to specific parameters in `tags` 85 | if sys.platform.startswith("win"): 86 | self.command = "rundll32.exe" 87 | self.command_arguments = ["user32.dll,LockWorkStation"] 88 | elif sys.platform == "linux": 89 | self.command = "dbus-send" 90 | self.command_arguments = [ 91 | "--type=method_call", 92 | "--dest=org.gnome.ScreenSaver", 93 | "/org/gnome/ScreenSaver", 94 | "org.gnome.ScreenSaver.Lock", 95 | ] 96 | self.startup_path = "." 97 | 98 | 99 | # dedicated form definition derived directly from one of the base forms 100 | class form_LockSessionTask(form_Task): 101 | 102 | def __init__(self, item=None): 103 | 104 | # check that item is the expected one for safety, build one by default 105 | if item: 106 | assert isinstance(item, LockSessionTask) 107 | else: 108 | item = LockSessionTask() 109 | super().__init__(_UI_FORM_TITLE, item) 110 | 111 | # create a specific frame for the contents 112 | area = ttk.Frame(super().contents) 113 | area.grid(row=0, column=0, sticky=tk.NSEW) 114 | PAD = WIDGET_PADDING_PIXELS 115 | 116 | # build the UI elements as needed and configure the layout 117 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 118 | l_noparams.configure(anchor=tk.CENTER) 119 | pad = ttk.Frame(area) 120 | 121 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 122 | pad.grid(row=10, column=0, sticky=tk.NSEW) 123 | 124 | area.columnconfigure(0, weight=1) 125 | area.rowconfigure(10, weight=1) 126 | 127 | # always update the form at the end of initialization 128 | self._updateform() 129 | 130 | # no need actually for this definition 131 | # def _updateform(self): 132 | # return super()._updateform() 133 | 134 | # no need actually for this definition 135 | # def _updatedata(self): 136 | # return super()._updatedata() 137 | 138 | 139 | # function common to all extra modules to declare class items as factories 140 | def factories(): 141 | return (LockSessionTask, form_LockSessionTask) 142 | 143 | 144 | # end. 145 | -------------------------------------------------------------------------------- /lib/extra/t_logoff.py: -------------------------------------------------------------------------------- 1 | # log off 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | 9 | from ..i18n.strings import * 10 | 11 | from ..forms.ui import * 12 | 13 | 14 | # since a task is defined, the base form is the one for tasks 15 | from ..forms.task import form_Task 16 | 17 | # import item to derive from 18 | from ..items.task_command import CommandTask 19 | 20 | 21 | # imports specific to this module 22 | import shutil 23 | import sys 24 | 25 | 26 | # resource strings (not internationalized for the moment) 27 | ITEM_HR_NAME = "Logoff Task" 28 | 29 | _UI_FORM_TITLE = "%s: Logoff Task Editor" % UI_APP 30 | 31 | _UI_FORM_NOPARAMS = "(This type of item does not need any specific parameters)" 32 | 33 | 34 | # default values 35 | 36 | 37 | # check for availability 38 | def _available(): 39 | if sys.platform.startswith("win"): 40 | if shutil.which("shutdown.exe"): 41 | return True 42 | return False 43 | elif sys.platform == "linux": 44 | global ITEM_HR_NAME, _UI_FORM_TITLE 45 | ITEM_HR_NAME += " (Gnome)" 46 | _UI_FORM_TITLE += " (Gnome)" 47 | if shutil.which("gnome-session-quit"): 48 | return True 49 | return False 50 | return False 51 | 52 | 53 | # the specific item is derived from the actual parent item 54 | class LogoffTask(CommandTask): 55 | 56 | # availability at class level: these variables *MUST* be set for all items 57 | item_type = "command" 58 | item_subtype = "logoff" 59 | item_hrtype = ITEM_HR_NAME 60 | available = _available() 61 | 62 | def __init__(self, t: items.Table = None) -> None: 63 | # first initialize the base class (mandatory) 64 | CommandTask.__init__(self, t) 65 | 66 | # then set type (same as base), subtype and human readable name: this 67 | # is mandatory in order to correctly display the item in all forms 68 | self.type = self.item_type 69 | self.subtype = self.item_subtype 70 | self.hrtype = self.item_hrtype 71 | 72 | # initializing from a table should always have this form: 73 | if t: 74 | assert t.get("type") == self.type 75 | self.tags = t.get("tags") 76 | assert isinstance(self.tags, items.Table) 77 | assert self.tags.get("subtype") == self.subtype 78 | 79 | # while creating a new item must always initialize specific parameters 80 | else: 81 | self.tags = table() 82 | self.tags.append("subtype", self.subtype) 83 | 84 | self.updateitem() 85 | 86 | def updateitem(self): 87 | # set base item properties according to specific parameters in `tags` 88 | if sys.platform.startswith("win"): 89 | self.command = "shutdown.exe" 90 | self.command_arguments = ["/l"] 91 | elif sys.platform == "linux": 92 | self.command = "gnome-session-quit" 93 | self.command_arguments = [ 94 | "--no-prompt", 95 | "--logout", 96 | ] 97 | self.startup_path = "." 98 | 99 | 100 | # dedicated form definition derived directly from one of the base forms 101 | class form_LogoffTask(form_Task): 102 | 103 | def __init__(self, item=None): 104 | 105 | # check that item is the expected one for safety, build one by default 106 | if item: 107 | assert isinstance(item, LogoffTask) 108 | else: 109 | item = LogoffTask() 110 | super().__init__(_UI_FORM_TITLE, item) 111 | 112 | # create a specific frame for the contents 113 | area = ttk.Frame(super().contents) 114 | area.grid(row=0, column=0, sticky=tk.NSEW) 115 | PAD = WIDGET_PADDING_PIXELS 116 | 117 | # create a specific frame for the contents 118 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 119 | l_noparams.configure(anchor=tk.CENTER) 120 | pad = ttk.Frame(area) 121 | 122 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 123 | pad.grid(row=10, column=0, sticky=tk.NSEW) 124 | 125 | area.columnconfigure(0, weight=1) 126 | area.rowconfigure(10, weight=1) 127 | 128 | # build the UI elements as needed and configure the layout 129 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 130 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 131 | area.columnconfigure(1, weight=1) 132 | 133 | # always update the form at the end of initialization 134 | self._updateform() 135 | 136 | # no need actually for this definition 137 | # def _updateform(self): 138 | # return super()._updateform() 139 | 140 | # no need actually for this definition 141 | # def _updatedata(self): 142 | # return super()._updatedata() 143 | 144 | 145 | # function common to all extra modules to declare class items as factories 146 | def factories(): 147 | return (LogoffTask, form_LogoffTask) 148 | 149 | 150 | # end. 151 | -------------------------------------------------------------------------------- /lib/extra/t_reboot.py: -------------------------------------------------------------------------------- 1 | # reboot 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | 9 | from ..i18n.strings import * 10 | 11 | from ..forms.ui import * 12 | 13 | 14 | # since a task is defined, the base form is the one for tasks 15 | from ..forms.task import form_Task 16 | 17 | # import item to derive from 18 | from ..items.task_command import CommandTask 19 | 20 | 21 | # imports specific to this module 22 | import shutil 23 | import sys 24 | 25 | 26 | # resource strings (not internationalized for the moment) 27 | ITEM_HR_NAME = "Reboot Task" 28 | 29 | _UI_FORM_TITLE = "%s: Reboot Task Editor" % UI_APP 30 | 31 | _UI_FORM_NOPARAMS = "(This type of item does not need any specific parameters)" 32 | 33 | 34 | # default values 35 | 36 | 37 | # check for availability 38 | def _available(): 39 | if sys.platform.startswith("win"): 40 | if shutil.which("shutdown.exe"): 41 | return True 42 | return False 43 | elif sys.platform == "linux": 44 | global ITEM_HR_NAME, _UI_FORM_TITLE 45 | ITEM_HR_NAME += " (Gnome)" 46 | _UI_FORM_TITLE += " (Gnome)" 47 | if shutil.which("gnome-session-quit"): 48 | return True 49 | return False 50 | return False 51 | 52 | 53 | # the specific item is derived from the actual parent item 54 | class RebootTask(CommandTask): 55 | 56 | # availability at class level: these variables *MUST* be set for all items 57 | item_type = "command" 58 | item_subtype = "reboot" 59 | item_hrtype = ITEM_HR_NAME 60 | available = _available() 61 | 62 | def __init__(self, t: items.Table = None) -> None: 63 | # first initialize the base class (mandatory) 64 | CommandTask.__init__(self, t) 65 | 66 | # then set type (same as base), subtype and human readable name: this 67 | # is mandatory in order to correctly display the item in all forms 68 | self.type = self.item_type 69 | self.subtype = self.item_subtype 70 | self.hrtype = self.item_hrtype 71 | 72 | # initializing from a table should always have this form: 73 | if t: 74 | assert t.get("type") == self.type 75 | self.tags = t.get("tags") 76 | assert isinstance(self.tags, items.Table) 77 | assert self.tags.get("subtype") == self.subtype 78 | 79 | # while creating a new item must always initialize specific parameters 80 | else: 81 | self.tags = table() 82 | self.tags.append("subtype", self.subtype) 83 | 84 | self.updateitem() 85 | 86 | def updateitem(self): 87 | # set base item properties according to specific parameters in `tags` 88 | if sys.platform.startswith("win"): 89 | self.command = "shutdown.exe" 90 | self.command_arguments = ["/r"] 91 | elif sys.platform == "linux": 92 | self.command = "gnome-session-quit" 93 | self.command_arguments = [ 94 | # "--no-prompt", 95 | "--reboot", 96 | ] 97 | self.startup_path = "." 98 | 99 | 100 | # dedicated form definition derived directly from one of the base forms 101 | class form_RebootTask(form_Task): 102 | 103 | def __init__(self, item=None): 104 | 105 | # check that item is the expected one for safety, build one by default 106 | if item: 107 | assert isinstance(item, RebootTask) 108 | else: 109 | item = RebootTask() 110 | super().__init__(_UI_FORM_TITLE, item) 111 | 112 | # create a specific frame for the contents 113 | area = ttk.Frame(super().contents) 114 | area.grid(row=0, column=0, sticky=tk.NSEW) 115 | PAD = WIDGET_PADDING_PIXELS 116 | 117 | # build the UI elements as needed and configure the layout 118 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 119 | l_noparams.configure(anchor=tk.CENTER) 120 | pad = ttk.Frame(area) 121 | 122 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 123 | pad.grid(row=10, column=0, sticky=tk.NSEW) 124 | 125 | area.columnconfigure(0, weight=1) 126 | area.rowconfigure(10, weight=1) 127 | 128 | # always update the form at the end of initialization 129 | self._updateform() 130 | 131 | # no need actually for this definition 132 | # def _updateform(self): 133 | # return super()._updateform() 134 | 135 | # no need actually for this definition 136 | # def _updatedata(self): 137 | # return super()._updatedata() 138 | 139 | 140 | # function common to all extra modules to declare class items as factories 141 | def factories(): 142 | return (RebootTask, form_RebootTask) 143 | 144 | 145 | # end. 146 | -------------------------------------------------------------------------------- /lib/extra/t_shutdown.py: -------------------------------------------------------------------------------- 1 | # shutdown 2 | 3 | # this header is common to all extra modules 4 | from tomlkit import items, table 5 | 6 | import tkinter as tk 7 | import ttkbootstrap as ttk 8 | 9 | from ..i18n.strings import * 10 | 11 | from ..forms.ui import * 12 | 13 | 14 | # since a task is defined, the base form is the one for tasks 15 | from ..forms.task import form_Task 16 | 17 | # import item to derive from 18 | from ..items.task_command import CommandTask 19 | 20 | 21 | # imports specific to this module 22 | import shutil 23 | import sys 24 | 25 | 26 | # resource strings (not internationalized for the moment) 27 | ITEM_HR_NAME = "Shutdown Task" 28 | 29 | _UI_FORM_TITLE = "%s: Shutdown Task Editor" % UI_APP 30 | 31 | _UI_FORM_NOPARAMS = "(This type of item does not need any specific parameters)" 32 | 33 | 34 | # default values 35 | 36 | 37 | # check for availability 38 | def _available(): 39 | if sys.platform.startswith("win"): 40 | if shutil.which("shutdown.exe"): 41 | return True 42 | return False 43 | elif sys.platform == "linux": 44 | global ITEM_HR_NAME, _UI_FORM_TITLE 45 | ITEM_HR_NAME += " (Gnome)" 46 | _UI_FORM_TITLE += " (Gnome)" 47 | if shutil.which("gnome-session-quit"): 48 | return True 49 | return False 50 | return False 51 | 52 | 53 | # the specific item is derived from the actual parent item 54 | class ShutdownTask(CommandTask): 55 | 56 | # availability at class level: these variables *MUST* be set for all items 57 | item_type = "command" 58 | item_subtype = "shutdown" 59 | item_hrtype = ITEM_HR_NAME 60 | available = _available() 61 | 62 | def __init__(self, t: items.Table = None) -> None: 63 | # first initialize the base class (mandatory) 64 | CommandTask.__init__(self, t) 65 | 66 | # then set type (same as base), subtype and human readable name: this 67 | # is mandatory in order to correctly display the item in all forms 68 | self.type = self.item_type 69 | self.subtype = self.item_subtype 70 | self.hrtype = self.item_hrtype 71 | 72 | # initializing from a table should always have this form: 73 | if t: 74 | assert t.get("type") == self.type 75 | self.tags = t.get("tags") 76 | assert isinstance(self.tags, items.Table) 77 | assert self.tags.get("subtype") == self.subtype 78 | 79 | # while creating a new item must always initialize specific parameters 80 | else: 81 | self.tags = table() 82 | self.tags.append("subtype", self.subtype) 83 | 84 | self.updateitem() 85 | 86 | def updateitem(self): 87 | # set base item properties according to specific parameters in `tags` 88 | if sys.platform.startswith("win"): 89 | self.command = "shutdown.exe" 90 | self.command_arguments = ["/s"] 91 | elif sys.platform == "linux": 92 | self.command = "gnome-session-quit" 93 | self.command_arguments = [ 94 | # "--no-prompt", 95 | "--power-off", 96 | ] 97 | self.startup_path = "." 98 | 99 | 100 | # dedicated form definition derived directly from one of the base forms 101 | class form_ShutdownTask(form_Task): 102 | 103 | def __init__(self, item=None): 104 | 105 | # check that item is the expected one for safety, build one by default 106 | if item: 107 | assert isinstance(item, ShutdownTask) 108 | else: 109 | item = ShutdownTask() 110 | super().__init__(_UI_FORM_TITLE, item) 111 | 112 | # create a specific frame for the contents 113 | area = ttk.Frame(super().contents) 114 | area.grid(row=0, column=0, sticky=tk.NSEW) 115 | PAD = WIDGET_PADDING_PIXELS 116 | 117 | # build the UI elements as needed and configure the layout 118 | l_noparams = ttk.Label(area, text=_UI_FORM_NOPARAMS) 119 | l_noparams.configure(anchor=tk.CENTER) 120 | pad = ttk.Frame(area) 121 | 122 | l_noparams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 123 | pad.grid(row=10, column=0, sticky=tk.NSEW) 124 | 125 | area.columnconfigure(0, weight=1) 126 | area.rowconfigure(10, weight=1) 127 | 128 | # always update the form at the end of initialization 129 | self._updateform() 130 | 131 | # no need actually for this definition 132 | # def _updateform(self): 133 | # return super()._updateform() 134 | 135 | # no need actually for this definition 136 | # def _updatedata(self): 137 | # return super()._updatedata() 138 | 139 | 140 | # function common to all extra modules to declare class items as factories 141 | def factories(): 142 | return (ShutdownTask, form_ShutdownTask) 143 | 144 | 145 | # end. 146 | -------------------------------------------------------------------------------- /lib/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # forms module 2 | -------------------------------------------------------------------------------- /lib/forms/about.py: -------------------------------------------------------------------------------- 1 | # about box 2 | 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | from tkhtmlview import HTMLLabel 6 | from PIL import ImageTk 7 | 8 | from ..i18n.strings import * 9 | from ..icons import APP_ICON64 as APP_BITMAP 10 | from .ui import * 11 | 12 | from ..repocfg import AppConfig 13 | from ..utility import get_whenever_version, get_UI_theme, get_image 14 | 15 | 16 | # the last paragraphs are to fiull the background in gray 17 | _htmlabout = """ 18 |
{title}
19 |
{text}
20 |
{copyright}
21 |
22 | {about_app_version}: {appversion}
23 | {about_sched_version}: {schedversion} 24 |
25 | """ 26 | 27 | 28 | class AboutBox(ApplicationForm): 29 | 30 | def __init__(self, main=False): 31 | version = get_whenever_version() 32 | appversion = UI_APP_VERSION 33 | if version: 34 | text = _htmlabout.format( 35 | title=UI_APP, 36 | text=UI_ABOUT_TEXT, 37 | copyright=UI_APP_COPYRIGHT, 38 | about_app_version=UI_ABOUT_APP_VERSION, 39 | appversion=appversion, 40 | about_sched_version=UI_ABOUT_WHENEVER_VERSION, 41 | schedversion=version, 42 | ) 43 | else: 44 | text = UI_ABOUT_TEXT 45 | super().__init__( 46 | UI_ABOUT_TITLE, 47 | AppConfig.get("SIZE_ABOUT_BOX"), 48 | None, 49 | (BBOX_CLOSE,), 50 | main 51 | ) 52 | self._image = ImageTk.PhotoImage(get_image(APP_BITMAP)) 53 | 54 | # build the UI: build widgets, arrange them in the box, bind data 55 | 56 | # client area 57 | area = ttk.Frame(self.contents) 58 | area.grid(row=0, column=0, sticky=tk.NSEW) 59 | PAD = WIDGET_PADDING_PIXELS 60 | 61 | # widgets section 62 | l_aboutTxt = HTMLLabel(area, html=text) 63 | l_aboutImg = ttk.Label(area, image=self._image) 64 | 65 | # arrange items in the grid 66 | l_aboutImg.grid(row=0, column=0, sticky=tk.N, padx=PAD, pady=PAD) 67 | l_aboutTxt.grid(row=0, column=1, sticky=tk.NSEW, padx=PAD, pady=PAD) 68 | 69 | # expand appropriate sections 70 | area.columnconfigure(1, weight=1) 71 | 72 | 73 | # display a simple about box 74 | def show_about_box(main=False): 75 | box = AboutBox(main) 76 | box.run() 77 | del box 78 | 79 | 80 | # end. 81 | -------------------------------------------------------------------------------- /lib/forms/cond_event.py: -------------------------------------------------------------------------------- 1 | # event condition form 2 | 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | 6 | from ..i18n.strings import * 7 | from .ui import * 8 | 9 | from .cond import form_Condition 10 | from ..items.cond_event import EventCondition 11 | 12 | 13 | # specialized subform 14 | class form_EventCondition(form_Condition): 15 | 16 | def __init__(self, tasks_available, item=None): 17 | if item: 18 | assert isinstance(item, EventCondition) 19 | else: 20 | item = EventCondition() 21 | super().__init__(UI_TITLE_EVENTCOND, tasks_available, item) 22 | 23 | # build the UI: build widgets, arrange them in the box, bind data 24 | 25 | # client area 26 | area = ttk.Frame(super().contents) 27 | area.grid(row=0, column=0, sticky=tk.NSEW) 28 | PAD = WIDGET_PADDING_PIXELS 29 | 30 | # widgets section 31 | l_noParams = ttk.Label(area, text=UI_CAPTION_NOSPECIFICPARAMS) 32 | 33 | # arrange items in the grid 34 | l_noParams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 35 | 36 | # update the form 37 | self._updateform() 38 | 39 | 40 | # end. 41 | -------------------------------------------------------------------------------- /lib/forms/cond_idle.py: -------------------------------------------------------------------------------- 1 | # idle session condition form 2 | 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | 6 | from ..i18n.strings import * 7 | from .ui import * 8 | 9 | from .cond import form_Condition 10 | from ..items.cond_idle import IdleCondition 11 | 12 | 13 | DEFAULT_INTERVAL_TIME = 5 14 | DEFAULT_INTERVAL_UNIT = UI_TIME_MINUTES 15 | 16 | 17 | # specialized subform 18 | class form_IdleCondition(form_Condition): 19 | 20 | def __init__(self, tasks_available, item=None): 21 | if item: 22 | assert isinstance(item, IdleCondition) 23 | else: 24 | item = IdleCondition() 25 | super().__init__(UI_TITLE_IDLECOND, tasks_available, item) 26 | 27 | # build the UI: build widgets, arrange them in the box, bind data 28 | 29 | # client area 30 | area = ttk.Frame(super().contents) 31 | area.grid(row=0, column=0, sticky=tk.NSEW) 32 | PAD = WIDGET_PADDING_PIXELS 33 | 34 | # parameters section 35 | l_intervalTime = ttk.Label(area, text=UI_FORM_IDLEDURATION) 36 | e_intervalTime = ttk.Entry(area) 37 | cb_timeUnit = ttk.Combobox( 38 | area, 39 | values=[UI_TIME_SECONDS, UI_TIME_MINUTES, UI_TIME_HOURS], 40 | state="readonly", 41 | ) 42 | pad = ttk.Frame(area) 43 | 44 | # arrange top items in the grid 45 | l_intervalTime.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 46 | e_intervalTime.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 47 | cb_timeUnit.grid(row=0, column=2, sticky=tk.E, padx=PAD, pady=PAD) 48 | pad.grid(row=1, column=0, columnspan=3, sticky=tk.NSEW) 49 | 50 | # expand appropriate sections 51 | area.columnconfigure(1, weight=1) 52 | area.rowconfigure(1, weight=1) 53 | 54 | # bind data to widgets 55 | self.data_bind("idle_time", e_intervalTime, TYPE_INT, lambda x: x > 0) 56 | self.data_bind("time_unit", cb_timeUnit, TYPE_STRING) 57 | 58 | # propagate widgets that need to be accessed 59 | # NOTE: no data to propagate 60 | 61 | # update the form 62 | self._updateform() 63 | 64 | def _updateform(self): 65 | super()._updateform() 66 | if self._item: 67 | if ( 68 | self._item.idle_seconds is not None 69 | and self._item.idle_seconds % 3600 == 0 70 | ): 71 | intv = int(self._item.idle_seconds / 3600) 72 | intvu = UI_TIME_HOURS 73 | elif ( 74 | self._item.idle_seconds is not None 75 | and self._item.idle_seconds % 60 == 0 76 | ): 77 | intv = int(self._item.idle_seconds / 60) 78 | intvu = UI_TIME_MINUTES 79 | else: 80 | intv = self._item.idle_seconds 81 | intvu = UI_TIME_SECONDS 82 | self.data_set("idle_time", intv) 83 | self.data_set("time_unit", intvu) 84 | else: 85 | self.data_set("idle_time", DEFAULT_INTERVAL_TIME) 86 | self.data_set("time_unit", DEFAULT_INTERVAL_UNIT) 87 | 88 | def _updatedata(self): 89 | super()._updatedata() 90 | intv = self.data_get("idle_time") 91 | intvu = self.data_get("time_unit") 92 | if intv is not None: 93 | if intvu == UI_TIME_HOURS: 94 | self._item.idle_seconds = intv * 3600 95 | elif intvu == UI_TIME_MINUTES: 96 | self._item.idle_seconds = intv * 60 97 | else: 98 | self._item.idle_seconds = intv 99 | 100 | 101 | # end. 102 | -------------------------------------------------------------------------------- /lib/forms/cond_interval.py: -------------------------------------------------------------------------------- 1 | # interval condition form 2 | 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | 6 | from ..i18n.strings import * 7 | from .ui import * 8 | 9 | from .cond import form_Condition 10 | from ..items.cond_interval import IntervalCondition 11 | 12 | 13 | DEFAULT_INTERVAL_TIME = 30 14 | DEFAULT_INTERVAL_UNIT = UI_TIME_SECONDS 15 | 16 | 17 | # specialized subform 18 | class form_IntervalCondition(form_Condition): 19 | 20 | def __init__(self, tasks_available, item=None): 21 | if item: 22 | assert isinstance(item, IntervalCondition) 23 | else: 24 | item = IntervalCondition() 25 | super().__init__(UI_TITLE_INTERVALCOND, tasks_available, item) 26 | 27 | # build the UI: build widgets, arrange them in the box, bind data 28 | 29 | # client area 30 | area = ttk.Frame(super().contents) 31 | area.grid(row=0, column=0, sticky=tk.NSEW) 32 | PAD = WIDGET_PADDING_PIXELS 33 | 34 | # parameters section 35 | l_intervalTime = ttk.Label(area, text=UI_FORM_DELAYSPEC) 36 | e_intervalTime = ttk.Entry(area) 37 | cb_timeUnit = ttk.Combobox( 38 | area, 39 | values=[UI_TIME_SECONDS, UI_TIME_MINUTES, UI_TIME_HOURS], 40 | state="readonly", 41 | ) 42 | pad = ttk.Frame(area) 43 | 44 | # arrange top items in the grid 45 | l_intervalTime.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 46 | e_intervalTime.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 47 | cb_timeUnit.grid(row=0, column=2, sticky=tk.E, padx=PAD, pady=PAD) 48 | pad.grid(row=1, column=0, columnspan=3, sticky=tk.NSEW) 49 | 50 | # expand appropriate sections 51 | area.columnconfigure(1, weight=1) 52 | area.rowconfigure(1, weight=1) 53 | 54 | # bind data to widgets 55 | self.data_bind("idle_time", e_intervalTime, TYPE_INT, lambda x: x > 0) 56 | self.data_bind("time_unit", cb_timeUnit, TYPE_STRING) 57 | 58 | # propagate widgets that need to be accessed 59 | # NOTE: no data to propagate 60 | 61 | # update the form 62 | self._updateform() 63 | 64 | def _updateform(self): 65 | super()._updateform() 66 | if self._item: 67 | if ( 68 | self._item.interval_seconds is not None 69 | and self._item.interval_seconds % 3600 == 0 70 | ): 71 | intv = int(self._item.interval_seconds / 3600) 72 | intvu = UI_TIME_HOURS 73 | elif ( 74 | self._item.interval_seconds is not None 75 | and self._item.interval_seconds % 60 == 0 76 | ): 77 | intv = int(self._item.interval_seconds / 60) 78 | intvu = UI_TIME_MINUTES 79 | else: 80 | intv = self._item.interval_seconds 81 | intvu = UI_TIME_SECONDS 82 | self.data_set("idle_time", intv) 83 | self.data_set("time_unit", intvu) 84 | else: 85 | self.data_set("idle_time", DEFAULT_INTERVAL_TIME) 86 | self.data_set("time_unit", DEFAULT_INTERVAL_UNIT) 87 | 88 | def _updatedata(self): 89 | super()._updatedata() 90 | intv = self.data_get("idle_time") 91 | intvu = self.data_get("time_unit") 92 | if intv is not None: 93 | if intvu == UI_TIME_HOURS: 94 | self._item.interval_seconds = intv * 3600 95 | elif intvu == UI_TIME_MINUTES: 96 | self._item.interval_seconds = intv * 60 97 | else: 98 | self._item.interval_seconds = intv 99 | 100 | 101 | # end. 102 | -------------------------------------------------------------------------------- /lib/forms/event.py: -------------------------------------------------------------------------------- 1 | # base event form 2 | 3 | import re 4 | import tkinter as tk 5 | import ttkbootstrap as ttk 6 | from tkinter import messagebox 7 | 8 | from ..i18n.strings import * 9 | from .ui import * 10 | 11 | from ..repocfg import AppConfig 12 | 13 | from ..items.event import Event 14 | 15 | 16 | # regular expression for item name checking 17 | _RE_VALIDNAME = re.compile("^[a-zA-Z_][a-zA-Z0-9_]*$") 18 | 19 | 20 | # event box base class: since this is the class that will be used in derived 21 | # forms too, in order to avoid variable name conflicts, all variable names 22 | # used here are prefixed with '@': agreeing that base values are prefixed 23 | # and specific values are not, consistent names can be used in derived forms 24 | class form_Event(ApplicationForm): 25 | 26 | def __init__(self, title, conditions_available, item=None): 27 | size = AppConfig.get("SIZE_EDITOR_FORM") 28 | bbox = (BBOX_OK, BBOX_CANCEL) 29 | super().__init__(title, size, None, bbox) 30 | 31 | conditions_available = conditions_available.copy() 32 | conditions_available.sort() 33 | 34 | # build the UI: build widgets, arrange them in the box, bind data 35 | 36 | # client area 37 | area = ttk.Frame(super().contents) 38 | area.grid(row=0, column=0, sticky=tk.NSEW) 39 | PAD = WIDGET_PADDING_PIXELS 40 | 41 | # common widgets 42 | l_itemName = ttk.Label(area, text=UI_FORM_NAME_SC) 43 | e_itemName = ttk.Entry(area) 44 | l_associatedCondition = ttk.Label(area, text=UI_FORM_COND_SC) 45 | cb_associatedCondition = ttk.Combobox( 46 | area, values=conditions_available, state="readonly" 47 | ) 48 | sep = ttk.Separator(area) 49 | 50 | # the following is the client area that is exposed to derived forms 51 | self._sub_contents = ttk.Frame(area) 52 | self._sub_contents.rowconfigure(0, weight=1) 53 | self._sub_contents.columnconfigure(0, weight=1) 54 | self._sub_contents.grid(row=10, column=0, columnspan=2, sticky=tk.NSEW) 55 | 56 | # arrange top items in the grid 57 | l_itemName.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 58 | e_itemName.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 59 | l_associatedCondition.grid(row=1, column=0, sticky=tk.W, padx=PAD, pady=PAD) 60 | cb_associatedCondition.grid(row=1, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 61 | sep.grid(row=2, column=0, sticky=tk.EW, columnspan=2, padx=PAD, pady=PAD) 62 | 63 | # expand appropriate sections 64 | area.columnconfigure(1, weight=1) 65 | area.rowconfigure(10, weight=1) 66 | 67 | # bind data to widgets 68 | self.data_bind( 69 | "@name", e_itemName, TYPE_STRING, lambda x: _RE_VALIDNAME.match(x) 70 | ) 71 | self.data_bind("@condition", cb_associatedCondition, TYPE_STRING) 72 | 73 | # finally set the item 74 | if item: 75 | self.set_item(item) 76 | else: 77 | self.reset_item() 78 | self.changed = False 79 | 80 | # contents is the root for slave widgets 81 | @property 82 | def contents(self): 83 | return self._sub_contents 84 | 85 | def _updateform(self): 86 | if self._item: 87 | self.data_set("@name", self._item.name) 88 | self.data_set("@condition", self._item.condition or "") 89 | else: 90 | self.data_set("@name", "") 91 | self.data_set("@condition", "") 92 | 93 | # the data update utility loads data into the item 94 | def _updatedata(self): 95 | name = self.data_get("@name") 96 | if name is not None: 97 | self._item.name = name 98 | self._item.condition = self.data_get("@condition") 99 | 100 | # set and remove the associated item 101 | def set_item(self, item): 102 | assert isinstance(item, Event) 103 | try: 104 | self._item = item.__class__( 105 | item.as_table() 106 | ) # get an exact copy to mess with 107 | except ValueError: 108 | self._item = item # item was newly created: use it 109 | 110 | def reset_item(self): 111 | self._item = None 112 | 113 | # command button reactions: cancel deletes the current item so that None 114 | # is returned upon dialog close, while ok finalizes item initialization 115 | # and lets the run() function return a configured item 116 | def exit_cancel(self): 117 | self._item = None 118 | return super().exit_cancel() 119 | 120 | def exit_ok(self): 121 | name = self.data_get("@name") 122 | condition = self.data_get("@condition") 123 | if name and condition: 124 | self._updatedata() 125 | return super().exit_ok() 126 | elif not name: 127 | messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_INVALIDITEMNAME) 128 | else: 129 | messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_MISSINGEVENTCOND) 130 | 131 | # main loop: returns the current item if any 132 | def run(self): 133 | super().run() 134 | return self._item 135 | 136 | 137 | # end. 138 | -------------------------------------------------------------------------------- /lib/forms/event_cli.py: -------------------------------------------------------------------------------- 1 | # command event form 2 | # this form is here only for completeness and **must never** be displayed 3 | 4 | import tkinter as tk 5 | import ttkbootstrap as ttk 6 | 7 | from ..i18n.strings import * 8 | from .ui import * 9 | 10 | from .event import form_Event 11 | from ..items.event_cli import CommandEvent 12 | 13 | 14 | # specialized subform 15 | class form_CommandEvent(form_Event): 16 | 17 | def __init__(self, conditions_available, item=None): 18 | if item: 19 | assert isinstance(item, CommandEvent) 20 | else: 21 | item = CommandEvent() 22 | form_Event.__init__(self, UI_TITLE_CLIEVENT, conditions_available, item) 23 | 24 | # build the UI: build widgets, arrange them in the box, bind data 25 | 26 | # client area 27 | area = ttk.Frame(super().contents) 28 | area.grid(row=0, column=0, sticky=tk.NSEW) 29 | PAD = WIDGET_PADDING_PIXELS 30 | 31 | # widgets section 32 | l_noParams = ttk.Label(area, text=UI_CAPTION_NOSPECIFICPARAMS) 33 | 34 | # arrange items in the grid 35 | l_noParams.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 36 | 37 | # update the form 38 | self._updateform() 39 | 40 | 41 | # end. 42 | -------------------------------------------------------------------------------- /lib/forms/event_dbus.py: -------------------------------------------------------------------------------- 1 | # DBus event form 2 | # this form is here only for completeness and **must never** be displayed 3 | 4 | import tkinter as tk 5 | import ttkbootstrap as ttk 6 | 7 | import pygments.lexers 8 | from chlorophyll import CodeView 9 | 10 | from ..i18n.strings import * 11 | from .ui import * 12 | 13 | from ..utility import get_editor_theme 14 | 15 | from .event import form_Event 16 | from ..items.event_dbus import DBusEvent 17 | 18 | 19 | _DBUS_BUS_VALUES = [":session", ":system"] 20 | 21 | 22 | # specialized subform 23 | class form_DBusEvent(form_Event): 24 | 25 | def __init__(self, conditions_available, item=None): 26 | if item: 27 | assert isinstance(item, DBusEvent) 28 | else: 29 | item = DBusEvent() 30 | super().__init__(UI_TITLE_DBUSEVENT, conditions_available, item) 31 | 32 | # build the UI: build widgets, arrange them in the box, bind data 33 | 34 | # client area 35 | area = ttk.Frame(super().contents) 36 | area.grid(row=0, column=0, sticky=tk.NSEW) 37 | PAD = WIDGET_PADDING_PIXELS 38 | 39 | # widgets section 40 | l_dbusBus = ttk.Label(area, text=UI_FORM_DBUS_BUS_SC) 41 | cb_dbusBus = ttk.Combobox(area, values=_DBUS_BUS_VALUES, state="readonly") 42 | 43 | # TODO: choose an appropriate lexer (although `bash` seems to be OK) 44 | sep1 = ttk.Separator(area) 45 | l_dbusRule = ttk.Label(area, text=UI_FORM_DBUS_RULE_SC) 46 | cv_dbusRule = CodeView( 47 | area, 48 | pygments.lexers.BashLexer, 49 | font="TkFixedFont", 50 | height=2, 51 | color_scheme=get_editor_theme(), 52 | ) 53 | 54 | sep2 = ttk.Separator(area) 55 | l_dbusParamsCheck = ttk.Label(area, text=UI_FORM_DBUS_PARAMS_CHECK_SC) 56 | cv_dbusParamsCheck = CodeView( 57 | area, 58 | pygments.lexers.JsonLexer, 59 | font="TkFixedFont", 60 | height=2, 61 | color_scheme=get_editor_theme(), 62 | ) 63 | ck_dbusCheckAll = ttk.Checkbutton(area, text=UI_FORM_MATCHALLRESULTS) 64 | 65 | # arrange items in the grid 66 | l_dbusBus.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 67 | cb_dbusBus.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 68 | 69 | sep1.grid(row=20, column=0, columnspan=4, sticky=tk.EW, pady=PAD) 70 | l_dbusRule.grid(row=21, column=0, sticky=tk.W, columnspan=2, padx=PAD, pady=PAD) 71 | cv_dbusRule.grid( 72 | row=22, column=0, sticky=tk.NSEW, columnspan=2, padx=PAD, pady=PAD 73 | ) 74 | 75 | sep2.grid(row=30, column=0, columnspan=4, sticky=tk.EW, pady=PAD) 76 | l_dbusParamsCheck.grid( 77 | row=31, column=0, sticky=tk.W, columnspan=2, padx=PAD, pady=PAD 78 | ) 79 | cv_dbusParamsCheck.grid( 80 | row=32, column=0, sticky=tk.NSEW, columnspan=2, padx=PAD, pady=PAD 81 | ) 82 | ck_dbusCheckAll.grid( 83 | row=33, column=0, sticky=tk.W, columnspan=4, padx=PAD, pady=PAD 84 | ) 85 | 86 | # expand appropriate sections 87 | area.rowconfigure(22, weight=1) 88 | area.rowconfigure(32, weight=1) 89 | area.columnconfigure(1, weight=1) 90 | 91 | # bind data to widgets 92 | self.data_bind("bus", cb_dbusBus, TYPE_STRING) 93 | self.data_bind("rule", cv_dbusRule, TYPE_STRING) 94 | self.data_bind("parameter_check", cv_dbusParamsCheck, TYPE_STRING) 95 | self.data_bind("parameter_check_all", ck_dbusCheckAll) 96 | 97 | # propagate widgets that need to be accessed 98 | # NOTE: no data to propagate 99 | 100 | # update the form 101 | self._updateform() 102 | 103 | def _updateform(self): 104 | self.data_set("bus", self._item.bus) 105 | self.data_set("rule", self._item.rule) 106 | self.data_set("parameter_check", self._item.parameter_check or None) 107 | self.data_set("parameter_check_all", self._item.parameter_check_all or False) 108 | return super()._updateform() 109 | 110 | def _updatedata(self): 111 | self._item.bus = self.data_get("bus") 112 | self._item.rule = self.data_get("rule") 113 | self._item.parameter_check = self.data_get("parameter_check") or "" 114 | self._item.parameter_check_all = self.data_get("parameter_check_all") or False 115 | return super()._updatedata() 116 | 117 | 118 | # end. 119 | -------------------------------------------------------------------------------- /lib/forms/event_wmi.py: -------------------------------------------------------------------------------- 1 | # DBus event form 2 | # this form is here only for completeness and **must never** be displayed 3 | 4 | import tkinter as tk 5 | import ttkbootstrap as ttk 6 | 7 | import pygments.lexers 8 | from chlorophyll import CodeView 9 | 10 | from ..i18n.strings import * 11 | from .ui import * 12 | 13 | from ..utility import get_editor_theme 14 | 15 | from .event import form_Event 16 | from ..items.event_wmi import WMIEvent 17 | 18 | 19 | # specialized subform 20 | class form_WMIEvent(form_Event): 21 | 22 | def __init__(self, conditions_available, item=None): 23 | if item: 24 | assert isinstance(item, WMIEvent) 25 | else: 26 | item = WMIEvent() 27 | super().__init__(UI_TITLE_WMIEVENT, conditions_available, item) 28 | 29 | # build the UI: build widgets, arrange them in the box, bind data 30 | 31 | # client area 32 | area = ttk.Frame(super().contents) 33 | area.grid(row=0, column=0, sticky=tk.NSEW) 34 | PAD = WIDGET_PADDING_PIXELS 35 | 36 | # TODO: choose an appropriate lexer (although `bash` seems to be OK) 37 | l_wmiQuery = ttk.Label(area, text=UI_FORM_WMI_QUERY_SC) 38 | cv_wmiQuery = CodeView( 39 | area, 40 | pygments.lexers.SqlLexer, 41 | font="TkFixedFont", 42 | height=2, 43 | color_scheme=get_editor_theme(), 44 | ) 45 | 46 | l_wmiQuery.grid(row=1, column=0, sticky=tk.W, padx=PAD, pady=PAD) 47 | cv_wmiQuery.grid(row=2, column=0, sticky=tk.NSEW, padx=PAD, pady=PAD) 48 | 49 | # expand appropriate sections 50 | area.rowconfigure(2, weight=1) 51 | area.columnconfigure(0, weight=1) 52 | 53 | # bind data to widgets 54 | self.data_bind("query", cv_wmiQuery, TYPE_STRING) 55 | 56 | # propagate widgets that need to be accessed 57 | # NOTE: no data to propagate 58 | 59 | # update the form 60 | self._updateform() 61 | 62 | def _updateform(self): 63 | self.data_set("query", self._item.query) 64 | return super()._updateform() 65 | 66 | def _updatedata(self): 67 | self._item.query = self.data_get("query") 68 | return super()._updatedata() 69 | -------------------------------------------------------------------------------- /lib/forms/newitem.py: -------------------------------------------------------------------------------- 1 | # form to select what type of new item should be created 2 | 3 | import tkinter as tk 4 | import ttkbootstrap as ttk 5 | 6 | from ..i18n.strings import * 7 | from .ui import * 8 | 9 | from ..repocfg import AppConfig 10 | from ..items.item import ALL_AVAILABLE_ITEMS, ALL_AVAILABLE_ITEMS_D 11 | 12 | 13 | # form class: this form is fixed and will not be derived 14 | class form_NewItem(ApplicationForm): 15 | 16 | def __init__(self): 17 | size = AppConfig.get("SIZE_NEWITEM_FORM") 18 | bbox = (BBOX_OK, BBOX_CANCEL) 19 | super().__init__(UI_TITLE_NEWITEM, size, None, bbox) 20 | 21 | # form data 22 | self._subtypes_display = [] 23 | self._type = "task" 24 | self._ret = None 25 | 26 | subtypes = list( 27 | x 28 | for x in ALL_AVAILABLE_ITEMS 29 | if x[0].startswith("%s:" % self._type) and x[3].available 30 | ) 31 | self._subtypes_display = list([x[1], x[0]] for x in subtypes) 32 | self._subtypes_display.sort(key=lambda x: x[0]) 33 | 34 | # build the UI: build widgets, arrange them in the box, bind data 35 | 36 | # client area 37 | area = ttk.Frame(self.contents) 38 | area.grid(row=0, column=0, sticky=tk.NSEW) 39 | PAD = WIDGET_PADDING_PIXELS 40 | 41 | # type section 42 | l_itemType = ttk.Label(area, text=UI_FORM_ITEMTYPE_SC) 43 | rb_itemTask = ttk.Radiobutton( 44 | area, text=ITEM_TASK, value="task", command=lambda: self.set_itemtype() 45 | ) 46 | rb_itemCond = ttk.Radiobutton( 47 | area, text=ITEM_COND, value="cond", command=lambda: self.set_itemtype() 48 | ) 49 | rb_itemEvent = ttk.Radiobutton( 50 | area, text=ITEM_EVENT, value="event", command=lambda: self.set_itemtype() 51 | ) 52 | f_spacer1 = ttk.Frame(area) 53 | 54 | # subtype section 55 | l_itemSubTypes = ttk.Label(area, text=UI_FORM_ITEMSUBTYPES_SC) 56 | # build a scrolled frame for the treeview 57 | sftv_itemSubTypes = ttk.Frame(area) 58 | tv_itemSubTypes = ttk.Treeview( 59 | sftv_itemSubTypes, 60 | columns=("type", "code"), 61 | displaycolumns=("type",), 62 | show="", 63 | height=5, 64 | ) 65 | tv_itemSubTypes.heading("type", anchor=tk.W, text=UI_FORM_ITEM) 66 | sb_itemSubTypes = ttk.Scrollbar( 67 | sftv_itemSubTypes, orient=tk.VERTICAL, command=tv_itemSubTypes.yview 68 | ) 69 | tv_itemSubTypes.configure(yscrollcommand=sb_itemSubTypes.set) 70 | tv_itemSubTypes.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 71 | sb_itemSubTypes.pack(side=tk.RIGHT, fill=tk.Y) 72 | 73 | # arrange items in the grid 74 | l_itemType.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 75 | rb_itemTask.grid(row=1, column=0, sticky=tk.W, padx=PAD, pady=PAD) 76 | rb_itemCond.grid(row=2, column=0, sticky=tk.W, padx=PAD, pady=PAD) 77 | rb_itemEvent.grid(row=3, column=0, sticky=tk.W, padx=PAD, pady=PAD) 78 | f_spacer1.grid(row=4, column=0, sticky=tk.EW, padx=PAD, pady=PAD) 79 | l_itemSubTypes.grid(row=10, column=0, sticky=tk.W, padx=PAD, pady=PAD) 80 | sftv_itemSubTypes.grid(row=11, column=0, sticky=tk.NSEW, padx=PAD, pady=PAD) 81 | 82 | # bind double click to variable recall 83 | tv_itemSubTypes.bind("", lambda _: self.exit_ok()) 84 | 85 | # expand appropriate sections 86 | area.columnconfigure(0, weight=1) 87 | area.rowconfigure(11, weight=1) 88 | 89 | # bind data to widgets 90 | self.data_bind( 91 | "item_type", (rb_itemTask, rb_itemCond, rb_itemEvent), TYPE_STRING 92 | ) 93 | self.data_bind("item_selection", tv_itemSubTypes) 94 | 95 | # propagate widgets that need to be accessed 96 | self._tv_itemtypes = tv_itemSubTypes 97 | 98 | # update the form 99 | self._updateform() 100 | 101 | def set_itemtype(self): 102 | self._type = self.data_get("item_type") 103 | subtypes = list( 104 | x 105 | for x in ALL_AVAILABLE_ITEMS 106 | if x[0].startswith("%s:" % self._type) and x[3].available 107 | ) 108 | self._subtypes_display = list([x[1], x[0]] for x in subtypes) 109 | self._subtypes_display.sort(key=lambda x: x[0]) 110 | self._updateform() 111 | 112 | def _updateform(self): 113 | self.data_set("item_type", self._type) 114 | self._tv_itemtypes.delete(*self._tv_itemtypes.get_children()) 115 | for entry in self._subtypes_display: 116 | self._tv_itemtypes.insert("", iid=entry[1], values=entry, index=ttk.END) 117 | 118 | def exit_ok(self): 119 | choice = self.data_get("item_selection") 120 | if choice: 121 | self._ret = self._type, ALL_AVAILABLE_ITEMS_D[choice[1]][1] 122 | return super().exit_ok() 123 | 124 | def run(self): 125 | super().run() 126 | return self._ret 127 | 128 | 129 | # end. 130 | -------------------------------------------------------------------------------- /lib/forms/task.py: -------------------------------------------------------------------------------- 1 | # base task form 2 | 3 | import re 4 | import tkinter as tk 5 | import ttkbootstrap as ttk 6 | from tkinter import messagebox 7 | 8 | from ..i18n.strings import * 9 | from .ui import * 10 | 11 | from ..repocfg import AppConfig 12 | 13 | from ..items.task import Task 14 | 15 | 16 | # regular expression for item name checking 17 | _RE_VALIDNAME = re.compile("^[a-zA-Z_][a-zA-Z0-9_]*$") 18 | 19 | 20 | # task box base class: since this is the class that will be used in derived 21 | # forms too, in order to avoid variable name conflicts, all variable names 22 | # used here are prefixed with '@': agreeing that base values are prefixed 23 | # and specific values are not, consistent names can be used in derived forms 24 | # without conflicts 25 | class form_Task(ApplicationForm): 26 | 27 | def __init__(self, title, item=None): 28 | size = AppConfig.get("SIZE_EDITOR_FORM") 29 | bbox = (BBOX_OK, BBOX_CANCEL) 30 | super().__init__(title, size, None, bbox) 31 | 32 | # build the UI: build widgets, arrange them in the box, bind data 33 | 34 | # client area 35 | area = ttk.Frame(super().contents) 36 | area.grid(row=0, column=0, sticky=tk.NSEW) 37 | PAD = WIDGET_PADDING_PIXELS 38 | 39 | # common widgets 40 | l_itemName = ttk.Label(area, text=UI_FORM_NAME_SC) 41 | e_itemName = ttk.Entry(area) 42 | sep = ttk.Separator(area) 43 | 44 | # the following is the client area that is exposed to derived forms 45 | self._sub_contents = ttk.Frame(area) 46 | self._sub_contents.rowconfigure(0, weight=1) 47 | self._sub_contents.columnconfigure(0, weight=1) 48 | 49 | # arrange top items in the grid 50 | l_itemName.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 51 | e_itemName.grid(row=0, column=1, sticky=tk.EW, padx=PAD, pady=PAD) 52 | sep.grid(row=1, column=0, columnspan=2, sticky=tk.EW, pady=PAD) 53 | self._sub_contents.grid(row=99, column=0, columnspan=2, sticky=tk.NSEW) 54 | 55 | # expand appropriate sections 56 | area.rowconfigure(index=99, weight=1) 57 | area.columnconfigure(1, weight=1) 58 | 59 | # bind data to widgets 60 | self.data_bind( 61 | "@name", e_itemName, TYPE_STRING, lambda x: _RE_VALIDNAME.match(x) 62 | ) 63 | 64 | # finally set the item 65 | if item: 66 | self.set_item(item) 67 | else: 68 | self.reset_item() 69 | self.changed = False 70 | 71 | # contents is the root for slave widgets 72 | @property 73 | def contents(self): 74 | return self._sub_contents 75 | 76 | def _updateform(self): 77 | if self._item: 78 | self.data_set("@name", self._item.name) 79 | else: 80 | self.data_set("@name", "") 81 | 82 | # the data update utility loads data into the item 83 | def _updatedata(self): 84 | name = self.data_get("@name") 85 | if name is not None: 86 | self._item.name = name 87 | 88 | # set and remove the associated item 89 | def set_item(self, item): 90 | assert isinstance(item, Task) 91 | try: 92 | self._item = item.__class__( 93 | item.as_table() 94 | ) # get an exact copy to mess with 95 | except ValueError: 96 | self._item = item # item was newly created: use it 97 | 98 | def reset_item(self): 99 | self._item = None 100 | 101 | # command button reactions: cancel deletes the current item so that None 102 | # is returned upon dialog close, while ok finalizes item initialization 103 | # and lets the run() function return a configured item 104 | def exit_cancel(self): 105 | self._item = None 106 | return super().exit_cancel() 107 | 108 | def exit_ok(self): 109 | name = self.data_get("@name") 110 | if name is not None: 111 | self._updatedata() 112 | return super().exit_ok() 113 | else: 114 | messagebox.showerror(UI_POPUP_T_ERR, UI_POPUP_INVALIDITEMNAME) 115 | 116 | # main loop: returns the current item if any 117 | def run(self): 118 | super().run() 119 | return self._item 120 | 121 | 122 | # end. 123 | -------------------------------------------------------------------------------- /lib/forms/task_internal.py: -------------------------------------------------------------------------------- 1 | # internal command task form 2 | 3 | import re 4 | import os.path 5 | import tkinter as tk 6 | import ttkbootstrap as ttk 7 | from tkinter import messagebox 8 | 9 | from ..i18n.strings import * 10 | from .ui import * 11 | 12 | from .task import form_Task 13 | from ..items.task_internal import InternalCommandTask 14 | 15 | 16 | # commands are checked for formal validity: no check on existing/not existing 17 | # items is made; the dictionary associates all commands to a function that 18 | # checks for formal validity 19 | _RE_VALIDNAME = re.compile("^[a-zA-Z_][a-zA-Z0-9_]*$") 20 | 21 | _COMMANDS = { 22 | "pause": lambda s: s == "", 23 | "resume": lambda s: s == "", 24 | "exit": lambda s: s == "", 25 | "quit": lambda s: s == "", 26 | "kill": lambda s: s == "", 27 | "reset_conditions": lambda s: s == "" 28 | or len(list(x for x in s.split() if not _RE_VALIDNAME.match(x))) == 0, 29 | "suspend_condition": lambda s: _RE_VALIDNAME.match(s), 30 | "resume_condition": lambda s: _RE_VALIDNAME.match(s), 31 | "trigger": lambda s: _RE_VALIDNAME.match(s), 32 | "configure": lambda s: os.path.exists(s), 33 | } 34 | 35 | 36 | def _check_command(s: str): 37 | l = s.split(None, 1) 38 | if len(l) == 0: 39 | return False 40 | elif len(l) == 1: 41 | command, args = l[0], "" 42 | elif len(l) == 2: 43 | command, args = l[0], l[1] 44 | else: 45 | # unreachable 46 | return False 47 | if command in _COMMANDS.keys(): 48 | f = _COMMANDS[command] 49 | return f(args) 50 | else: 51 | return False 52 | 53 | 54 | class form_InternalCommandTask(form_Task): 55 | 56 | def __init__(self, item=None): 57 | if item: 58 | assert isinstance(item, InternalCommandTask) 59 | else: 60 | item = InternalCommandTask() 61 | super().__init__(UI_TITLE_INTERNALTASK, item) 62 | 63 | # build the UI: build widgets, arrange them in the box, bind data 64 | 65 | # client area 66 | area = ttk.Frame(super().contents) 67 | area.grid(row=0, column=0, sticky=tk.NSEW) 68 | PAD = WIDGET_PADDING_PIXELS 69 | 70 | # script section 71 | l_command = ttk.Label(area, text=UI_FORM_COMMAND_SC) 72 | e_command = ttk.Entry(area) 73 | pad = ttk.Frame(area) 74 | 75 | l_command.grid(row=0, column=0, sticky=tk.W, padx=PAD, pady=PAD) 76 | e_command.grid(row=1, column=0, sticky=tk.EW, padx=PAD, pady=PAD) 77 | pad.grid(row=10, column=0, sticky=tk.NSEW) 78 | 79 | area.columnconfigure(0, weight=1) 80 | area.rowconfigure(10, weight=1) 81 | 82 | self.data_bind("command", e_command, TYPE_STRING, _check_command) 83 | 84 | # update the form 85 | self._updateform() 86 | 87 | def _updatedata(self): 88 | self._item.command = self.data_get("command").strip() or "" 89 | return super()._updatedata() 90 | 91 | def _updateform(self): 92 | self.data_set("command", self._item.command) 93 | return super()._updateform() 94 | 95 | 96 | # end. 97 | -------------------------------------------------------------------------------- /lib/i18n/__init__.py: -------------------------------------------------------------------------------- 1 | # internationalization (i18n) module -------------------------------------------------------------------------------- /lib/i18n/strings.py: -------------------------------------------------------------------------------- 1 | # load the strings according to current I18N settings 2 | 3 | # for now: just fall back to base 4 | from lib.i18n.strings_base import * 5 | 6 | 7 | # end. 8 | -------------------------------------------------------------------------------- /lib/items/__init__.py: -------------------------------------------------------------------------------- 1 | # items module 2 | -------------------------------------------------------------------------------- /lib/items/cond.py: -------------------------------------------------------------------------------- 1 | # condition items 2 | # 3 | # base condition items can be of the following types: 4 | # - interval conditions 5 | # - time conditions 6 | # - idle session conditions 7 | # - event conditions 8 | # - DBus inspection conditions 9 | # - command conditions 10 | # - Lua script conditions 11 | # as per whenever documentation. However, more types can be derived especially 12 | # by crafting command, event, DBus inspection, and Lua script based conditions 13 | # into more specialized versions. whenever allows a list of strings to be 14 | # stored in the `tags` entry for each item, so that specialized items can be 15 | # created in the configuration file and recognized by the wrapper when the 16 | # configuration is loaded back. 17 | 18 | from tomlkit import table, items 19 | from ..utility import check_not_none, append_not_none, generate_item_name 20 | 21 | 22 | # base class for conditions: all condition items will have the same interface 23 | # thus are derived from this object: the string conversion is provided also as 24 | # a debugging helper; all base methods will have to be invoked **first** by 25 | # derived methods, as they perform base initialization and checks 26 | class Condition(object): 27 | 28 | # availability at class level 29 | available = False 30 | 31 | def __init__(self, t: items.Table = None) -> None: 32 | self.type = None 33 | self.hrtype = None 34 | if t: 35 | self.name = t.get("name") 36 | self.execute_sequence = t.get("execute_sequence", True) 37 | self.break_on_failure = t.get("break_on_failure", False) 38 | self.break_on_success = t.get("break_on_success", False) 39 | self.suspended = t.get("suspended", False) 40 | self.recurring = t.get("recurring", False) 41 | self.max_tasks_retries = t.get("max_tasks_retries", 0) 42 | self.tasks = t.get("tasks") 43 | tags = t.get("tags") 44 | if tags: 45 | self.tags = dict(tags) 46 | else: 47 | self.tags = None 48 | else: 49 | self.name = generate_item_name(self) 50 | self.recurring = None 51 | self.max_tasks_retries = None 52 | self.execute_sequence = None 53 | self.break_on_failure = None 54 | self.break_on_success = None 55 | self.suspended = None 56 | self.tasks = [] 57 | self.tags = None 58 | 59 | def __str__(self): 60 | return "[[condition]]\n%s" % self.as_table().as_string() 61 | 62 | @property 63 | def signature(self): 64 | s = "cond:%s" % self.type 65 | if 'subtype' in self.__dict__: 66 | s += ":%s" % self.subtype 67 | return s 68 | 69 | def as_table(self): 70 | if not check_not_none( 71 | self.name, 72 | self.type, 73 | ): 74 | raise ValueError("Invalid Condition: mandatory field(s) missing") 75 | t = table() 76 | t.append("name", self.name) 77 | t.append("type", self.type) 78 | t.append("tasks", self.tasks) 79 | t = append_not_none(t, "recurring", self.recurring) 80 | t = append_not_none(t, "max_tasks_retries", self.max_tasks_retries) 81 | t = append_not_none(t, "execute_sequence", self.execute_sequence) 82 | t = append_not_none(t, "break_on_failure", self.break_on_failure) 83 | t = append_not_none(t, "break_on_success", self.break_on_success) 84 | t = append_not_none(t, "suspended", self.suspended) 85 | t = append_not_none(t, "tags", self.tags) 86 | return t 87 | 88 | 89 | # end. 90 | -------------------------------------------------------------------------------- /lib/items/cond_command.py: -------------------------------------------------------------------------------- 1 | # command condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | from os.path import expanduser 11 | 12 | 13 | # default values for non-optional parameters 14 | DEFAULT_COMMAND = "replace_me" 15 | DEFAULT_STARTUP_PATH = expanduser("~") 16 | 17 | 18 | # a command based condition 19 | class CommandCondition(Condition): 20 | 21 | # availability at class level 22 | available = True 23 | 24 | def __init__(self, t: items.Table = None) -> None: 25 | Condition.__init__(self, t) 26 | self.type = "command" 27 | self.hrtype = ITEM_COND_COMMAND 28 | if t: 29 | assert t.get("type") == self.type 30 | self.check_after = t.get("check_after") 31 | self.recur_after_failed_check = t.get("recur_after_failed_check") 32 | self.startup_path = t.get("startup_path") 33 | self.command = t.get("command") 34 | self.match_exact = t.get("match_exact", False) 35 | self.match_regular_expression = t.get("match_regular_expression", False) 36 | self.success_stdout = t.get("success_stdout") 37 | self.success_stderr = t.get("success_stderr") 38 | self.success_status = t.get("success_status") 39 | self.failure_stdout = t.get("failure_stdout") 40 | self.failure_stderr = t.get("failure_stderr") 41 | self.failure_status = t.get("failure_status") 42 | self.timeout_seconds = t.get("timeout_seconds") 43 | self.case_sensitive = t.get("case_sensitive", False) 44 | self.include_environment = t.get("include_environment", True) 45 | self.set_environment_variables = t.get("set_environment_variables", True) 46 | command_arguments = t.get("command_arguments") 47 | if command_arguments: 48 | self.command_arguments = list(command_arguments) 49 | else: 50 | self.command_arguments = [] 51 | environment_variables = t.get("environment_variables") 52 | if environment_variables: 53 | self.environment_variables = dict(environment_variables) 54 | else: 55 | self.environment_variables = None 56 | else: 57 | self.check_after = None 58 | self.recur_after_failed_check = None 59 | self.startup_path = DEFAULT_STARTUP_PATH 60 | self.command = DEFAULT_COMMAND 61 | self.command_arguments = [] 62 | self.match_exact = False 63 | self.match_regular_expression = False 64 | self.success_stdout = None 65 | self.success_stderr = None 66 | self.success_status = None 67 | self.failure_stdout = None 68 | self.failure_stderr = None 69 | self.failure_status = None 70 | self.timeout_seconds = None 71 | self.case_sensitive = False 72 | self.include_environment = True 73 | self.set_environment_variables = True 74 | self.environment_variables = None 75 | 76 | def as_table(self): 77 | if not check_not_none( 78 | self.command, 79 | self.command_arguments, 80 | self.startup_path, 81 | ): 82 | raise ValueError("Invalid Command Condition: mandatory field(s) missing") 83 | t = Condition.as_table(self) 84 | t = append_not_none(t, "check_after", self.check_after) 85 | t = append_not_none( 86 | t, "recur_after_failed_check", self.recur_after_failed_check 87 | ) 88 | t.append("startup_path", self.startup_path) 89 | t.append("command", self.command) 90 | t.append("command_arguments", self.command_arguments) 91 | t = append_not_none(t, "match_exact", self.match_exact) 92 | t = append_not_none( 93 | t, "match_regular_expression", self.match_regular_expression 94 | ) 95 | t = append_not_none(t, "success_stdout", self.success_stdout) 96 | t = append_not_none(t, "success_stderr", self.success_stderr) 97 | t = append_not_none(t, "success_status", self.success_status) 98 | t = append_not_none(t, "failure_stdout", self.failure_stdout) 99 | t = append_not_none(t, "failure_stderr", self.failure_stderr) 100 | t = append_not_none(t, "failure_status", self.failure_status) 101 | t = append_not_none(t, "timeout_seconds", self.timeout_seconds) 102 | t = append_not_none(t, "case_sensitive", self.case_sensitive) 103 | t = append_not_none(t, "include_environment", self.include_environment) 104 | t = append_not_none( 105 | t, "set_environment_variables", self.set_environment_variables 106 | ) 107 | t = append_not_none(t, "environment_variables", self.environment_variables) 108 | return t 109 | 110 | 111 | # end. 112 | -------------------------------------------------------------------------------- /lib/items/cond_dbus.py: -------------------------------------------------------------------------------- 1 | # DBus condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_BUS = ":session" 13 | DEFAULT_SERVICE = "org.gnome.Settings" 14 | DEFAULT_OBJECT_PATH = "/org/gnome/Settings" 15 | DEFAULT_INTERFACE = "org.gtk.Actions" 16 | DEFAULT_METHOD = "List" 17 | 18 | 19 | # a DBus inspection based condition 20 | class DBusCondition(Condition): 21 | 22 | # availability at class level 23 | available = False 24 | 25 | def __init__(self, t: items.Table = None) -> None: 26 | Condition.__init__(self, t) 27 | self.type = "dbus" 28 | self.hrtype = ITEM_COND_DBUS 29 | if t: 30 | assert t.get("type") == self.type 31 | self.check_after = t.get("check_after") 32 | self.recur_after_failed_check = t.get("recur_after_failed_check") 33 | self.bus = t.get("bus") 34 | self.service = t.get("service") 35 | self.object_path = t.get("object_path") 36 | self.interface = t.get("interface") 37 | self.method = t.get("method") 38 | self.parameter_call = t.get("parameter_call") 39 | self.parameter_check_all = t.get("parameter_check_all", False) 40 | self.parameter_check = t.get("parameter_check") 41 | else: 42 | self.check_after = None 43 | self.recur_after_failed_check = None 44 | self.bus = DEFAULT_BUS 45 | self.service = DEFAULT_SERVICE 46 | self.object_path = DEFAULT_OBJECT_PATH 47 | self.interface = DEFAULT_INTERFACE 48 | self.method = DEFAULT_METHOD 49 | self.parameter_call = None 50 | self.parameter_check_all = False 51 | self.parameter_check = None 52 | 53 | def as_table(self): 54 | if not check_not_none( 55 | self.bus, 56 | self.service, 57 | self.object_path, 58 | self.interface, 59 | self.method, 60 | ): 61 | raise ValueError("Invalid DBus Condition: mandatory field(s) missing") 62 | t = Condition.as_table(self) 63 | t = append_not_none(t, "check_after", self.check_after) 64 | t = append_not_none( 65 | t, "recur_after_failed_check", self.recur_after_failed_check 66 | ) 67 | t.append("bus", self.bus) 68 | t.append("service", self.service) 69 | t.append("object_path", self.object_path) 70 | t.append("interface", self.interface) 71 | t.append("method", self.method) 72 | t = append_not_none(t, "parameter_call", self.parameter_call) 73 | t = append_not_none(t, "parameter_check_all", self.parameter_check_all) 74 | t = append_not_none(t, "parameter_check", self.parameter_check) 75 | return t 76 | 77 | 78 | # end. 79 | -------------------------------------------------------------------------------- /lib/items/cond_event.py: -------------------------------------------------------------------------------- 1 | # evenb condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | # (none here) 13 | 14 | 15 | # an event based condition: this does not support the 'bucket' keyword! 16 | class EventCondition(Condition): 17 | 18 | # availability at class level 19 | available = True 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Condition.__init__(self, t) 23 | self.type = "event" 24 | self.hrtype = ITEM_COND_EVENT 25 | if t: 26 | assert t.get("type") == self.type 27 | 28 | def as_table(self): 29 | if not check_not_none( 30 | # nothing to be checked! 31 | ): 32 | raise ValueError("Invalid Event Condition: mandatory field(s) missing") 33 | t = Condition.as_table(self) 34 | return t 35 | 36 | 37 | # end. 38 | -------------------------------------------------------------------------------- /lib/items/cond_idle.py: -------------------------------------------------------------------------------- 1 | # idle session condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_IDLE_SECONDS = 600 13 | 14 | 15 | # an idle session based condition 16 | class IdleCondition(Condition): 17 | 18 | # availability at class level 19 | available = True 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Condition.__init__(self, t) 23 | self.type = "idle" 24 | self.hrtype = ITEM_COND_IDLE 25 | if t: 26 | assert t.get("type") == self.type 27 | self.idle_seconds = t.get("idle_seconds") 28 | else: 29 | self.idle_seconds = DEFAULT_IDLE_SECONDS 30 | 31 | def as_table(self): 32 | if not check_not_none( 33 | self.idle_seconds, 34 | ): 35 | raise ValueError("Invalid Idle Condition: mandatory field(s) missing") 36 | t = Condition.as_table(self) 37 | t = append_not_none(t, "idle_seconds", self.idle_seconds) 38 | return t 39 | 40 | 41 | # end. 42 | -------------------------------------------------------------------------------- /lib/items/cond_interval.py: -------------------------------------------------------------------------------- 1 | # interval condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_INTERVAL_SECONDS = 120 13 | 14 | 15 | # an interval based condition 16 | class IntervalCondition(Condition): 17 | 18 | # availability at class level 19 | available = True 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Condition.__init__(self, t) 23 | self.type = "interval" 24 | self.hrtype = ITEM_COND_INTERVAL 25 | if t: 26 | self.interval_seconds = t.get("interval_seconds") 27 | else: 28 | self.interval_seconds = DEFAULT_INTERVAL_SECONDS 29 | 30 | def as_table(self): 31 | if not check_not_none( 32 | self.interval_seconds, 33 | ): 34 | raise ValueError("Invalid Interval Condition: mandatory field(s) missing") 35 | t = Condition.as_table(self) 36 | t = append_not_none(t, "interval_seconds", self.interval_seconds) 37 | return t 38 | 39 | 40 | # end. 41 | -------------------------------------------------------------------------------- /lib/items/cond_lua.py: -------------------------------------------------------------------------------- 1 | # Lua condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_LUASCRIPT = "-- write your Lua script here" 13 | 14 | 15 | # a Lua script based condition 16 | class LuaScriptCondition(Condition): 17 | 18 | # availability at class level 19 | available = True 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Condition.__init__(self, t) 23 | self.type = "lua" 24 | self.hrtype = ITEM_COND_LUA 25 | if t: 26 | assert t.get("type") == self.type 27 | self.check_after = t.get("check_after") 28 | self.recur_after_failed_check = t.get("recur_after_failed_check") 29 | self.script = t.get("script") 30 | self.expect_all = t.get("expect_all", False) 31 | expected_results = t.get("expected_results") 32 | if expected_results: 33 | self.expected_results = dict(expected_results) 34 | else: 35 | self.expected_results = None 36 | else: 37 | self.script = DEFAULT_LUASCRIPT 38 | self.check_after = None 39 | self.recur_after_failed_check = None 40 | self.expect_all = None 41 | self.expected_results = None 42 | 43 | def as_table(self): 44 | if not check_not_none( 45 | self.script, 46 | ): 47 | raise ValueError("Invalid Lua Condition: mandatory field(s) missing") 48 | t = Condition.as_table(self) 49 | t = append_not_none(t, "check_after", self.check_after) 50 | t = append_not_none( 51 | t, "recur_after_failed_check", self.recur_after_failed_check 52 | ) 53 | t.append("script", self.script) 54 | t = append_not_none(t, "expect_all", self.expect_all) 55 | t = append_not_none(t, "expected_results", self.expected_results) 56 | return t 57 | 58 | 59 | # end. 60 | -------------------------------------------------------------------------------- /lib/items/cond_wmi.py: -------------------------------------------------------------------------------- 1 | # WMI condition item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .cond import Condition 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_QUERY = "SELECT * from Win32_Processor" 13 | 14 | 15 | # a WMI query based condition 16 | class WMICondition(Condition): 17 | 18 | # availability at class level 19 | available = False 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Condition.__init__(self, t) 23 | self.type = "wmi" 24 | self.hrtype = ITEM_COND_WMI 25 | if t: 26 | assert t.get("type") == self.type 27 | self.check_after = t.get("check_after") 28 | self.recur_after_failed_check = t.get("recur_after_failed_check") 29 | self.query = t.get("query") 30 | self.result_check_all = t.get("result_check_all", False) 31 | self.result_check = t.get("result_check") 32 | else: 33 | self.check_after = None 34 | self.recur_after_failed_check = None 35 | self.query = DEFAULT_QUERY 36 | self.result_check_all = False 37 | self.result_check = None 38 | 39 | def as_table(self): 40 | if not check_not_none( 41 | self.query, 42 | ): 43 | raise ValueError("Invalid WMI Condition: mandatory field(s) missing") 44 | t = Condition.as_table(self) 45 | t = append_not_none(t, "check_after", self.check_after) 46 | t = append_not_none( 47 | t, "recur_after_failed_check", self.recur_after_failed_check 48 | ) 49 | t.append("query", self.query) 50 | t = append_not_none(t, "result_check_all", self.result_check_all) 51 | t = append_not_none(t, "result_check", self.result_check) 52 | return t 53 | 54 | 55 | # end. 56 | -------------------------------------------------------------------------------- /lib/items/event.py: -------------------------------------------------------------------------------- 1 | # event items 2 | # 3 | # base event items can be of the following types: 4 | # - DBus based 5 | # - file system changes 6 | # - command line reactions (direct `trigger` command) 7 | # as per whenever documentation. However, more types can be derived mainly by 8 | # using DBus or, if the wrapper can specialize enough, by listening to system 9 | # events and trigger dedicated command line reactions when certain events 10 | # not known to the scheduler occur. 11 | 12 | from tomlkit import table, items 13 | from ..utility import check_not_none, append_not_none, generate_item_name 14 | 15 | 16 | # base class for event: all event items will have the same interface thus they 17 | # are derived from this object: the string conversion is provided also as a 18 | # debugging helper; all base methods will have to be invoked **first** by 19 | # derived methods, as they perform base initialization and checks 20 | class Event(object): 21 | 22 | # availability at class level 23 | available = False 24 | 25 | def __init__(self, t: items.Table = None) -> None: 26 | self.type = None 27 | self.hrtype = None 28 | if t: 29 | self.name = t.get("name") 30 | self.condition = t.get("condition") 31 | tags = t.get("tags") 32 | if tags: 33 | self.tags = dict(tags) 34 | else: 35 | self.tags = None 36 | else: 37 | self.name = generate_item_name(self) 38 | self.condition = None 39 | self.tags = None 40 | 41 | def __str__(self): 42 | return "[[event]]\n%s" % self.as_table().as_string() 43 | 44 | @property 45 | def signature(self): 46 | s = "event:%s" % self.type 47 | if 'subtype' in self.__dict__: 48 | s += ":%s" % self.subtype 49 | return s 50 | 51 | def as_table(self): 52 | if not check_not_none( 53 | self.name, 54 | self.type, 55 | ): 56 | raise ValueError("Invalid Event: mandatory field(s) missing") 57 | t = table() 58 | t.append("name", self.name) 59 | t.append("type", self.type) 60 | t = append_not_none(t, "condition", self.condition) 61 | t = append_not_none(t, "tags", self.tags) 62 | return t 63 | 64 | 65 | # end. 66 | -------------------------------------------------------------------------------- /lib/items/event_cli.py: -------------------------------------------------------------------------------- 1 | # direct command event item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none 7 | 8 | from .event import Event 9 | 10 | 11 | # a direct command based event 12 | class CommandEvent(Event): 13 | 14 | # availability at class level 15 | available = False 16 | 17 | def __init__(self, t: items.Table = None) -> None: 18 | Event.__init__(self, t) 19 | self.type = "cli" 20 | self.hrtype = ITEM_EVENT_CLI 21 | if t: 22 | assert t.get("type") == self.type 23 | 24 | def as_table(self): 25 | if not check_not_none( 26 | # nothing to be checked! 27 | ): 28 | raise ValueError("Invalid Command Event: mandatory field(s) missing") 29 | t = Event.as_table(self) 30 | return t 31 | 32 | 33 | # end. 34 | -------------------------------------------------------------------------------- /lib/items/event_dbus.py: -------------------------------------------------------------------------------- 1 | # DBus event item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .event import Event 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_BUS = ":session" 13 | DEFAULT_RULE = ( 14 | "type='signal',sender='org.gnome.TypingMonitor',interface='org.gnome.TypingMonitor'" 15 | ) 16 | 17 | 18 | # a DBus signal based event 19 | class DBusEvent(Event): 20 | 21 | # availability at class level 22 | available = False 23 | 24 | def __init__(self, t: items.Table = None) -> None: 25 | Event.__init__(self, t) 26 | self.type = "dbus" 27 | self.hrtype = ITEM_EVENT_DBUS 28 | if t: 29 | assert t.get("type") == self.type 30 | self.bus = t.get("bus") 31 | assert self.bus in (":session", ":system") 32 | self.rule = t.get("rule") 33 | self.parameter_check_all = t.get("parameter_check_all") 34 | self.parameter_check = t.get("parameter_check") 35 | else: 36 | self.bus = DEFAULT_BUS 37 | self.rule = DEFAULT_RULE 38 | self.parameter_check_all = False 39 | self.parameter_check = None 40 | 41 | def as_table(self): 42 | if not check_not_none( 43 | self.bus, 44 | self.rule, 45 | ): 46 | raise ValueError("Invalid DBus Event: mandatory field(s) missing") 47 | t = Event.as_table(self) 48 | t.append("bus", self.bus) 49 | t.append("rule", self.rule) 50 | t = append_not_none(t, "parameter_check_all", self.parameter_check_all) 51 | t = append_not_none(t, "parameter_check", self.parameter_check) 52 | return t 53 | 54 | 55 | # end. 56 | -------------------------------------------------------------------------------- /lib/items/event_fschange.py: -------------------------------------------------------------------------------- 1 | # filesystem monitor event item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .event import Event 9 | 10 | from os.path import expanduser 11 | 12 | 13 | # default values for non-optional parameters 14 | DEFAULT_WATCH = [expanduser("~")] 15 | 16 | 17 | # a filesystem change based event (NOTE: `poll_seconds` unsupported for now) 18 | class FilesystemChangeEvent(Event): 19 | 20 | # availability at class level 21 | available = True 22 | 23 | def __init__(self, t: items.Table = None) -> None: 24 | Event.__init__(self, t) 25 | self.type = "fschange" 26 | self.hrtype = ITEM_EVENT_FSCHANGE 27 | if t: 28 | assert t.get("type") == self.type 29 | self.watch = t.get("watch") 30 | self.recursive = t.get("recursive") 31 | # self.poll_seconds = t.get('poll_seconds') 32 | else: 33 | self.watch = DEFAULT_WATCH 34 | self.recursive = None 35 | # self.poll_seconds = None 36 | 37 | def as_table(self): 38 | if not check_not_none( 39 | self.watch, 40 | ): 41 | raise ValueError( 42 | "Invalid File System Change Event: mandatory field(s) missing" 43 | ) 44 | t = Event.as_table(self) 45 | t.append("watch", self.watch) 46 | t = append_not_none(t, "recursive", self.recursive) 47 | # t = append_not_none(t, 'poll_seconds', self.poll_seconds) 48 | return t 49 | 50 | 51 | # end. 52 | -------------------------------------------------------------------------------- /lib/items/event_wmi.py: -------------------------------------------------------------------------------- 1 | # WMI event item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .event import Event 9 | 10 | 11 | # TODO: use a real query 12 | DEFAULT_QUERY = """\ 13 | SELECT * FROM __InstanceCreationEvent 14 | WITHIN 10 15 | WHERE TargetInstance ISA 'Win32_Process' 16 | AND TargetInstance.Name = 'Notepad.exe' 17 | """ 18 | 19 | 20 | # a direct command based event 21 | class WMIEvent(Event): 22 | 23 | # availability at class level 24 | available = False 25 | 26 | def __init__(self, t: items.Table = None) -> None: 27 | Event.__init__(self, t) 28 | self.type = "wmi" 29 | self.hrtype = ITEM_EVENT_WMI 30 | if t: 31 | assert t.get("type") == self.type 32 | self.query = t.get("query") 33 | else: 34 | self.query = DEFAULT_QUERY 35 | 36 | def as_table(self): 37 | if not check_not_none( 38 | self.query, 39 | ): 40 | raise ValueError("Invalid WMI Event: mandatory field(s) missing") 41 | t = Event.as_table(self) 42 | t.append("query", self.query) 43 | return t 44 | -------------------------------------------------------------------------------- /lib/items/task.py: -------------------------------------------------------------------------------- 1 | # task items 2 | # 3 | # task items can be of two types: 4 | # - command tasks 5 | # - Lua script tasks 6 | # as per whenever documentation. 7 | 8 | from tomlkit import table, items 9 | from ..utility import check_not_none, append_not_none, generate_item_name 10 | 11 | 12 | # base class for tasks: all task items will have the same interface thus they 13 | # are derived from this object: the string conversion is provided also as a 14 | # debugging helper; all base methods will have to be invoked **first** by 15 | # derived methods, as they perform base initialization and checks 16 | class Task(object): 17 | 18 | # availability at class level 19 | available = False 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | self.type = None 23 | self.hrtype = None 24 | if t: 25 | self.name = t.get("name") 26 | tags = t.get("tags") 27 | if tags: 28 | self.tags = dict(tags) 29 | else: 30 | self.tags = None 31 | else: 32 | self.name = generate_item_name(self) 33 | self.tags = None 34 | 35 | def __str__(self): 36 | return "[[task]]\n%s" % self.as_table().as_string() 37 | 38 | @property 39 | def signature(self): 40 | s = "task:%s" % self.type 41 | if 'subtype' in self.__dict__: 42 | s += ":%s" % self.subtype 43 | return s 44 | 45 | def as_table(self): 46 | if not check_not_none( 47 | self.name, 48 | self.type, 49 | ): 50 | raise ValueError("Invalid Task: mandatory field(s) missing") 51 | t = table() 52 | t.append("name", self.name) 53 | t.append("type", self.type) 54 | t = append_not_none(t, "tags", self.tags) 55 | return t 56 | 57 | 58 | # end. 59 | -------------------------------------------------------------------------------- /lib/items/task_command.py: -------------------------------------------------------------------------------- 1 | # command task item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .task import Task 9 | 10 | from os.path import expanduser 11 | 12 | 13 | # default values for non-optional parameters 14 | DEFAULT_COMMAND = "replace_me" 15 | DEFAULT_STARTUP_PATH = expanduser("~") 16 | 17 | 18 | # a command based task 19 | class CommandTask(Task): 20 | 21 | # availability at class level 22 | available = True 23 | 24 | def __init__(self, t: items.Table = None) -> None: 25 | Task.__init__(self, t) 26 | self.type = "command" 27 | self.hrtype = ITEM_TASK_COMMAND 28 | if t: 29 | assert t.get("type") == self.type 30 | self.startup_path = t.get("startup_path") 31 | self.command = t.get("command") 32 | self.match_exact = t.get("match_exact", False) 33 | self.match_regular_expression = t.get("match_regular_expression", False) 34 | self.success_stdout = t.get("success_stdout") 35 | self.success_stderr = t.get("success_stderr") 36 | self.success_status = t.get("success_status") 37 | self.failure_stdout = t.get("failure_stdout") 38 | self.failure_stderr = t.get("failure_stderr") 39 | self.failure_status = t.get("failure_status") 40 | self.timeout_seconds = t.get("timeout_seconds") 41 | self.case_sensitive = t.get("case_sensitive", False) 42 | self.include_environment = t.get("include_environment", True) 43 | self.set_environment_variables = t.get("set_environment_variables", True) 44 | command_arguments = t.get("command_arguments") 45 | if command_arguments: 46 | self.command_arguments = list(command_arguments) 47 | else: 48 | self.command_arguments = [] 49 | environment_variables = t.get("environment_variables") 50 | if environment_variables: 51 | self.environment_variables = dict(environment_variables) 52 | else: 53 | self.environment_variables = None 54 | else: 55 | self.startup_path = DEFAULT_STARTUP_PATH 56 | self.command = DEFAULT_COMMAND 57 | self.command_arguments = [] 58 | self.match_exact = False 59 | self.match_regular_expression = False 60 | self.success_stdout = None 61 | self.success_stderr = None 62 | self.success_status = None 63 | self.failure_stdout = None 64 | self.failure_stderr = None 65 | self.failure_status = None 66 | self.timeout_seconds = None 67 | self.case_sensitive = False 68 | self.include_environment = True 69 | self.set_environment_variables = True 70 | self.environment_variables = None 71 | 72 | def as_table(self): 73 | if not check_not_none( 74 | self.command, 75 | self.command_arguments, 76 | self.startup_path, 77 | ): 78 | raise ValueError("Invalid Command Task: mandatory field(s) missing") 79 | t = Task.as_table(self) 80 | t.append("startup_path", self.startup_path) 81 | t.append("command", self.command) 82 | t.append("command_arguments", self.command_arguments) 83 | t = append_not_none(t, "match_exact", self.match_exact) 84 | t = append_not_none( 85 | t, "match_regular_expression", self.match_regular_expression 86 | ) 87 | t = append_not_none(t, "success_stdout", self.success_stdout) 88 | t = append_not_none(t, "success_stderr", self.success_stderr) 89 | t = append_not_none(t, "success_status", self.success_status) 90 | t = append_not_none(t, "failure_stdout", self.failure_stdout) 91 | t = append_not_none(t, "failure_stderr", self.failure_stderr) 92 | t = append_not_none(t, "failure_status", self.failure_status) 93 | t = append_not_none(t, "timeout_seconds", self.timeout_seconds) 94 | t = append_not_none(t, "case_sensitive", self.case_sensitive) 95 | t = append_not_none(t, "include_environment", self.include_environment) 96 | t = append_not_none( 97 | t, "set_environment_variables", self.set_environment_variables 98 | ) 99 | t = append_not_none(t, "environment_variables", self.environment_variables) 100 | return t 101 | 102 | 103 | # end. 104 | -------------------------------------------------------------------------------- /lib/items/task_internal.py: -------------------------------------------------------------------------------- 1 | # internal task item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .task import Task 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_COMMAND = "reset_conditions" 13 | 14 | 15 | class InternalCommandTask(Task): 16 | 17 | available = False 18 | 19 | def __init__(self, t: items.Table = None): 20 | Task.__init__(self, t) 21 | self.type = "internal" 22 | self.hrtype = ITEM_TASK_INTERNAL 23 | if t: 24 | assert t.get("type") == self.type 25 | self.command = t.get("command") 26 | else: 27 | self.command = DEFAULT_COMMAND 28 | 29 | def as_table(self): 30 | if not check_not_none( 31 | self.command, 32 | ): 33 | raise ValueError( 34 | "Invalid Internal Command Task: mandatory field(s) missing" 35 | ) 36 | t = Task.as_table(self) 37 | t.append("command", self.command) 38 | return t 39 | 40 | 41 | # end. 42 | -------------------------------------------------------------------------------- /lib/items/task_lua.py: -------------------------------------------------------------------------------- 1 | # Lua task item 2 | 3 | from lib.i18n.strings import * 4 | 5 | from tomlkit import table, items 6 | from ..utility import check_not_none, append_not_none 7 | 8 | from .task import Task 9 | 10 | 11 | # default values for non-optional parameters 12 | DEFAULT_LUASCRIPT = "-- write your Lua script here" 13 | 14 | 15 | # a Lua script based task 16 | class LuaScriptTask(Task): 17 | 18 | # availability at class level 19 | available = True 20 | 21 | def __init__(self, t: items.Table = None) -> None: 22 | Task.__init__(self, t) 23 | self.type = "lua" 24 | self.hrtype = ITEM_TASK_LUA 25 | if t: 26 | assert t.get("type") == self.type 27 | self.script = t.get("script") 28 | self.expect_all = t.get("expect_all", False) 29 | expected_results = t.get("expected_results") 30 | if expected_results: 31 | self.expected_results = dict(expected_results) 32 | else: 33 | self.expected_results = None 34 | else: 35 | self.script = DEFAULT_LUASCRIPT 36 | self.expect_all = None 37 | self.expected_results = None 38 | 39 | def as_table(self): 40 | if not check_not_none( 41 | self.script, 42 | ): 43 | raise ValueError("Invalid Lua Task: mandatory field(s) missing") 44 | t = Task.as_table(self) 45 | t.append("script", self.script) 46 | t = append_not_none(t, "expect_all", self.expect_all) 47 | t = append_not_none(t, "expected_results", self.expected_results) 48 | return t 49 | 50 | 51 | # end. 52 | -------------------------------------------------------------------------------- /lib/repocfg.py: -------------------------------------------------------------------------------- 1 | # global application configuration repository: this repository is implemented 2 | # so that only one object of its type can be created, and the items that are 3 | # inserted are _read-only_: the only way to update a configuration item is to 4 | # delete it and add it back 5 | 6 | _singleton_lock = False 7 | 8 | from pygments.styles import get_style_by_name 9 | 10 | 11 | class _AppConfiguration(object): 12 | 13 | def __init__(self, initial_table=None): 14 | global _singleton_lock 15 | if _singleton_lock: 16 | raise TypeError("only an instance of the configuration can exist") 17 | _singleton_lock = True 18 | if initial_table is None: 19 | self._table = {} 20 | else: 21 | assert isinstance(initial_table, dict) 22 | self._table = {} 23 | for k in initial_table: 24 | self._table[k] = initial_table[k] 25 | 26 | def set(self, key: str, value): 27 | if key in self._table: 28 | raise ValueError("value already set for '%s': delete it first" % key) 29 | self._table[key] = value 30 | 31 | def delete(self, key: str): 32 | if key in self._table: 33 | del self._table[key] 34 | 35 | def get(self, key: str, default=None): 36 | if key in self._table: 37 | return self._table[key] 38 | else: 39 | return default 40 | 41 | def __getitem__(self, key: str): 42 | return self.get(key) 43 | 44 | def __setitem__(self, key: str, value): 45 | self.set(key, value) 46 | 47 | def __delitem__(self, key: str): 48 | self.delete(key) 49 | 50 | def __str__(self) -> str: 51 | keys = list(self._table.keys()) 52 | keys.sort() 53 | s = "" 54 | for k in keys: 55 | s += "%s: %s\n" % (k, repr(self._table[k])) 56 | return s 57 | 58 | 59 | # the unique instance of the configuration object: note that it also contains 60 | # some initial configuration values that might have to be modified according 61 | # to the type of release (eg. the DEBUG flag, see below) 62 | AppConfig = _AppConfiguration( 63 | { 64 | # this flag should be set to False on normal operation 65 | "DEBUG": False, 66 | 67 | # the application base name for configuration directory determination 68 | "CFGNAME": "Whenever", 69 | 70 | # milliseconds between log reads by the secondary thread 71 | "MSECS_BETWEEN_READS": 100.0, 72 | 73 | # history queue length 74 | "HISTORY_LENGTH": 100, 75 | 76 | # configuration window size 77 | "SIZE_MAIN_FORM": (960, 640), 78 | 79 | # editor form size 80 | "SIZE_EDITOR_FORM": (960, 640), 81 | 82 | # new item chooser size 83 | "SIZE_NEWITEM_FORM": (640, 400), 84 | 85 | # history box size 86 | "SIZE_HISTORY_FORM": (960, 640), 87 | 88 | # about box size 89 | "SIZE_ABOUT_BOX": (480, 300), 90 | 91 | # menu box size 92 | "SIZE_MENU_BOX": (272, 440), 93 | 94 | # themes 95 | "DEFAULT_THEME_DARK": "darkly", 96 | "DEFAULT_THEME_LIGHT": "flatly", 97 | "DEFAULT_THEME_DEBUG": "morph", 98 | 99 | # editor themes 100 | "EDITOR_THEME_DARK": "ayu-dark", 101 | "EDITOR_THEME_LIGHT": "ayu-light", 102 | "EDITOR_THEME_DEBUG": "ayu-light", 103 | 104 | # ... 105 | } 106 | ) 107 | 108 | 109 | __all__ = [AppConfig] 110 | 111 | 112 | # end. 113 | -------------------------------------------------------------------------------- /lib/runner/__init__.py: -------------------------------------------------------------------------------- 1 | # scheduler starter 2 | 3 | -------------------------------------------------------------------------------- /lib/runner/history.py: -------------------------------------------------------------------------------- 1 | # history manager 2 | # 3 | # builds a limited log that only holds history records and task duration 4 | 5 | 6 | from datetime import datetime 7 | 8 | 9 | class History(object): 10 | 11 | def __init__(self, maxlen): 12 | self._history = [] 13 | self._maxlen = maxlen 14 | self._open_records_timing = {} 15 | 16 | def append(self, record): 17 | time = record["header"]["time"] 18 | # application = record['header']['application'] 19 | # level = record['header']['level'] 20 | # emitter = record['contents']['context']['emitter'] 21 | # action = record['contents']['context']['action'] 22 | item = record["contents"]["context"]["item"] 23 | item_id = record["contents"]["context"]["item_id"] 24 | when = record["contents"]["message_type"]["when"] 25 | status = record["contents"]["message_type"]["status"] 26 | message = record["contents"]["message"] 27 | itemstr = "%s/%s" % (item, item_id) 28 | if when == "HIST": 29 | if status == "START": 30 | self._open_records_timing[itemstr] = time 31 | else: 32 | end = datetime.fromisoformat(time) 33 | start = datetime.fromisoformat(self._open_records_timing[itemstr]) 34 | del self._open_records_timing[itemstr] 35 | duration = end - start 36 | if len(self._history) == self._maxlen: 37 | self._history = self._history[1:] 38 | ident, msg = message.split(" ", 1) 39 | outcome, t = ident.split("/") 40 | _, trigger = t.split(":") 41 | self._history.append( 42 | { 43 | "time": time, 44 | "task": item, 45 | "task_id": item_id, 46 | "trigger": trigger, 47 | "duration": duration, 48 | "success": outcome, 49 | "message": msg, 50 | } 51 | ) 52 | else: 53 | # ignore the message 54 | pass 55 | 56 | def get(self): 57 | return self._history 58 | 59 | def get_copy(self): 60 | return self._history.copy() 61 | 62 | 63 | # end. 64 | -------------------------------------------------------------------------------- /lib/runner/logger.py: -------------------------------------------------------------------------------- 1 | # logging module: log all records issued by the subprocess according to the 2 | # chosen log level. 3 | # 4 | # As per **whenever** documentation, a log record has the following format: 5 | # 6 | # { 7 | # "header": { 8 | # "application": "whenever", 9 | # "level": "TRACE", 10 | # "time": "2023-11-04T11:17:25.257970" 11 | # }, 12 | # "contents": { 13 | # "context": { 14 | # "action": "scheduler_tick", 15 | # "emitter": "MAIN", 16 | # "item": null, 17 | # "item_id": null 18 | # }, 19 | # "message": "condition Cond_TIME tested with no outcome (tasks not executed)", 20 | # "message_type": { 21 | # "status": "MSG", 22 | # "when": "PROC" 23 | # } 24 | # } 25 | # } 26 | 27 | 28 | from ..utility import get_tkroot 29 | from ..repocfg import AppConfig 30 | 31 | # levels are fixed, format mimics original **whenever** format 32 | _LOGLEVELS = ["trace", "debug", "info", "warn", "error"] 33 | _LOGFMT = "{time} ({application}) {level} {emitter} {action}{itemstr}: [{when}/{status}]: {message}" 34 | 35 | 36 | # for now a very basic logger: improvements will be rotation and persistence 37 | class Logger(object): 38 | 39 | def __init__(self, filename, level, root=None): 40 | self._logfile = open(filename, "w") 41 | self._level = level 42 | self._level_num = _LOGLEVELS.index(self._level) 43 | self._root = root 44 | 45 | def log(self, record): 46 | time = record["header"]["time"] 47 | application = record["header"]["application"] 48 | level = record["header"]["level"] 49 | emitter = record["contents"]["context"]["emitter"] 50 | action = record["contents"]["context"]["action"] 51 | item = record["contents"]["context"]["item"] 52 | item_id = record["contents"]["context"]["item_id"] 53 | when = record["contents"]["message_type"]["when"] 54 | status = record["contents"]["message_type"]["status"] 55 | message = record["contents"]["message"] 56 | if item is not None and item_id is not None: 57 | itemstr = " %s/%s" % (item, item_id) 58 | else: 59 | itemstr = "" 60 | ln = _LOGLEVELS.index(level.lower()) 61 | if when == "HIST": 62 | if AppConfig.get("DEBUG"): 63 | self._logfile.write( 64 | "%s\n" 65 | % _LOGFMT.format( 66 | time=time, 67 | application=application, 68 | level=level, 69 | emitter=emitter, 70 | action=action, 71 | itemstr=itemstr, 72 | when=when, 73 | status=status, 74 | message=message, 75 | ) 76 | ) 77 | self._logfile.flush() 78 | return False 79 | elif when == "BUSY": 80 | if self._root: 81 | if status == "YES": 82 | self._root.send_event("<>") 83 | else: 84 | self._root.send_event("<>") 85 | elif when == "PAUSE": 86 | if self._root: 87 | if status == "YES": 88 | self._root.send_event("<>") 89 | else: 90 | self._root.send_event("<>") 91 | else: 92 | if ln >= self._level_num: 93 | self._logfile.write( 94 | "%s\n" 95 | % _LOGFMT.format( 96 | time=time, 97 | application=application, 98 | level=level, 99 | emitter=emitter, 100 | action=action, 101 | itemstr=itemstr, 102 | when=when, 103 | status=status, 104 | message=message, 105 | ) 106 | ) 107 | self._logfile.flush() 108 | return True 109 | 110 | 111 | # end. 112 | -------------------------------------------------------------------------------- /lib/toolbox/__init__.py: -------------------------------------------------------------------------------- 1 | # toolbox 2 | -------------------------------------------------------------------------------- /lib/toolbox/dlutils.py: -------------------------------------------------------------------------------- 1 | # utilities to download assets from GitHub 2 | 3 | 4 | import os 5 | import sys 6 | import requests 7 | import urllib 8 | 9 | from ..i18n.strings import * 10 | from ..utility import write_error, get_default_configdir 11 | 12 | 13 | # URL definition strings 14 | GITHUB_BASE = "github.com/almostearthling" 15 | GITHUB_PROTOCOL = "https" 16 | 17 | 18 | # retrieve an asset from an URL 19 | def retrieve_asset(URL, verbose=False): 20 | try: 21 | r = requests.get(URL) 22 | return r.content 23 | except Exception as e: 24 | if verbose: 25 | write_error(CLI_ERR_DOWNLOADING_ASSET_MSG % (URL, e)) 26 | return None 27 | 28 | 29 | # retrieve text from an URL 30 | def retrieve_text_asset(URL, verbose=False): 31 | try: 32 | r = requests.get(URL) 33 | return r.text 34 | except Exception as e: 35 | if verbose: 36 | write_error(CLI_ERR_DOWNLOADING_ASSET_MSG % (URL, e)) 37 | return None 38 | 39 | 40 | # download an asset identified by an URL to a provided directory or to 41 | # the TEMP directory if none is specified 42 | def download_asset(URL, local=None, verbose=False): 43 | sr = urllib.parse.urlsplit(URL) 44 | fname = os.path.basename(sr.path) 45 | if local is None: 46 | try: 47 | local = os.environ["TEMP"] 48 | except KeyError: 49 | tmpdir = "Temp" if sys.platform == "win32" else "tmp" 50 | local = os.path.join(get_default_configdir(), tmpdir) 51 | if not os.path.isdir(local): 52 | os.makedirs(local) 53 | else: 54 | if not os.path.isdir(local): 55 | if verbose: 56 | write_error(f"{local} is not a directory") 57 | return None 58 | dlname = os.path.join(local, fname) 59 | b = retrieve_asset(URL) 60 | if b is not None: 61 | with open(dlname, "wb") as f: 62 | f.write(b) 63 | return dlname 64 | else: 65 | return None 66 | 67 | 68 | # some URL entries 69 | def get_repo_base_url(repo): 70 | return f"{GITHUB_PROTOCOL}://{GITHUB_BASE}/{repo}" 71 | 72 | 73 | def get_repo_latest_releases(repo): 74 | base = get_repo_base_url(repo) 75 | return f"{base}/releases/latest/download" 76 | 77 | 78 | __all__ = [ 79 | "retrieve_asset", 80 | "retrieve_text_asset", 81 | "download_asset", 82 | "get_repo_base_url", 83 | "get_repo_latest_releases", 84 | ] 85 | 86 | 87 | # end. 88 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "when" 3 | version = "1.10.1b1" 4 | description = "Interface for the **whenever** automation tool" 5 | authors = ["Francesco Garosi "] 6 | license = 'BSD 3-Clause "New" or "Revised" License' 7 | readme = "README.md" 8 | packages = [ 9 | { include = "lib" }, 10 | { include = "when" }, 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.10" 15 | tomlkit = "^0.12.4" 16 | requests = "^2.32.3" 17 | pystray = "^0.19.5" 18 | pillow = "^10.3.0" 19 | darkdetect = "^0.8.0" 20 | Pygments = "^2.17.2" 21 | tklinenums = "^1.7.1" 22 | chlorophyll = "^0.4.1" 23 | ttkbootstrap = "^1.10.1" 24 | rich = "^13.9.4" 25 | dbus-python = { version = "^1.3.2", markers = "sys_platform == 'linux'" } 26 | # pygobject = { version = "^3.52.2", markers = "sys_platform == 'linux'" } 27 | pygobject = { version = "3.50.0", markers = "sys_platform == 'linux'" } 28 | winshell = { version = "^0.6", markers = "sys_platform == 'win32'" } 29 | pywin32 = { version = "^308", markers = "sys_platform == 'win32'" } 30 | tkhtmlview = "^0.3.1" 31 | 32 | [tool.poetry.scripts] 33 | when = "when.when:main" 34 | when-bg = "when.when_bg:run_bg" 35 | 36 | [build-system] 37 | requires = ["poetry-core"] 38 | build-backend = "poetry.core.masonry.api" 39 | -------------------------------------------------------------------------------- /support/append_icon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This small utility is used to append graphics/icons to the `lib/icons.py` 4 | # resource file, as base64 encoded binary strings. To use it, just launch 5 | # 6 | # $ python support/append_icon.py VARNAME path/to/iconfile.png 7 | # 8 | # from the project base directory to update the resource file automatically. 9 | 10 | import os 11 | import sys 12 | import shutil 13 | import base64 14 | 15 | import re 16 | 17 | 18 | RE_VARNAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") 19 | 20 | ICON_PLACEHOLDER = "# APPEND_ICONS_HERE" 21 | ICON_SOURCE = "lib/icons.py" 22 | 23 | 24 | # verbose output shortcut 25 | def oerr(s, verbose=True): 26 | if verbose: 27 | sys.stderr.write("append_icon: %s\n" % s) 28 | 29 | 30 | 31 | if __name__ == '__main__': 32 | if len(sys.argv) != 3: 33 | oerr("invalid number of arguments") 34 | sys.exit(1) 35 | if not RE_VARNAME.match(sys.argv[1]): 36 | oerr("argument `%s` not suitable as variable name" % sys.argv[1]) 37 | sys.exit(1) 38 | try: 39 | with open(sys.argv[2], 'rb') as f: 40 | bytes = f.read() 41 | except Exception as e: 42 | oerr("could not open `%s`" % sys.argv[2]) 43 | sys.exit(2) 44 | txt = "%s = %s" % (sys.argv[1], base64.b64encode(bytes)) 45 | mfile = os.path.normpath(os.path.join(os.path.dirname(sys.argv[0]), "..", ICON_SOURCE)) 46 | try: 47 | with open(mfile) as f: 48 | src = f.read() 49 | shutil.copy(mfile, "%s~" % mfile) 50 | except Exception as e: 51 | oerr("could not open `%s`" % mfile) 52 | sys.exit(2) 53 | src = src.replace(ICON_PLACEHOLDER, "%s\n%s" % (txt, ICON_PLACEHOLDER)) 54 | try: 55 | with open(mfile, 'w') as f: 56 | f.write(src) 57 | except Exception as e: 58 | oerr("could not write to `%s`" % mfile) 59 | sys.exit(2) 60 | 61 | 62 | #end. 63 | -------------------------------------------------------------------------------- /support/build_html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # build html documentation using Sphinx (https://www.sphinx-doc.org/) 3 | 4 | import os 5 | import sys 6 | 7 | import shutil 8 | import subprocess 9 | 10 | 11 | # set to True if the README.md file has to be added to the documentation 12 | COPY_README = False 13 | 14 | 15 | 16 | # utilities 17 | def _write_stderr(msg, ty): 18 | cmd = os.path.basename(sys.argv[0]) 19 | if cmd.endswith(".py"): 20 | cmd = cmd[:-3] 21 | elif cmd.endswith(".pyw") or cmd.endswith(".exe"): 22 | cmd = cmd[:-4] 23 | s = f"{cmd} {ty}: {msg}\n" 24 | sys.stderr.write(s) 25 | 26 | def write_error(msg): 27 | _write_stderr(msg, "error") 28 | 29 | def write_warning(msg): 30 | _write_stderr(msg, "warning") 31 | 32 | def write_info(msg): 33 | _write_stderr(msg, "info") 34 | 35 | def exit_error(msg, code=2): 36 | _write_stderr(msg, "fatal error") 37 | sys.exit(code) 38 | 39 | def buildpath(*args): 40 | return os.path.normpath(os.path.join(*args)) 41 | 42 | 43 | 44 | # command-line utilities that are needed to build documentation 45 | REQUIRED_COMMANDS = [ 46 | "sphinx-build", 47 | ] 48 | 49 | def check_requirements(): 50 | for cmd in REQUIRED_COMMANDS: 51 | if not shutil.which(cmd): 52 | return False 53 | return True 54 | 55 | 56 | 57 | # paths 58 | THIS_PATH = os.path.dirname(sys.argv[0]) 59 | BASE_PATH = buildpath(THIS_PATH, "..") 60 | SOURCE_PATH = buildpath(BASE_PATH, "support/docs") 61 | DEST_PATH = buildpath(BASE_PATH, "_scratch/docs") 62 | 63 | 64 | 65 | # convert the README.md file to index.md in the source directory 66 | def copy_readme(): 67 | try: 68 | src = buildpath(BASE_PATH, "README.md") 69 | dest = buildpath(SOURCE_PATH, "README.md") 70 | with open(src, encoding='utf-8') as f: 71 | txt = f.read() 72 | txt = txt.replace("support/docs/", "") 73 | with open(dest, 'w', encoding='utf-8') as f: 74 | f.write(txt) 75 | except Exception as e: 76 | exit_error(f"README copy failed: {e}") 77 | 78 | 79 | 80 | # main program 81 | def main(): 82 | if not check_requirements(): 83 | exit_error("requirements not met") 84 | if not os.path.isdir(DEST_PATH): 85 | os.makedirs(DEST_PATH) 86 | if COPY_README: 87 | copy_readme() 88 | command = "sphinx-build" 89 | args = [ 90 | SOURCE_PATH, 91 | DEST_PATH, 92 | ] 93 | try: 94 | run = [command] + args 95 | subprocess.run(run) 96 | except Exception as e: 97 | exit_error(f"error running `{command}`: {e}") 98 | return 0 99 | 100 | 101 | 102 | # main entry point 103 | if __name__ == "__main__": 104 | main() 105 | 106 | 107 | # end. 108 | -------------------------------------------------------------------------------- /support/docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/_static/favicon.ico -------------------------------------------------------------------------------- /support/docs/appdata.md: -------------------------------------------------------------------------------- 1 | # Application Data Directory 2 | 3 | The _application data_ directory, mostly referred to in this documentation as _APPDATA_ (from the name of the environment variable that, on _Windows_, determines its position within the user home directory), is where all persistent data common to **When**, **whenever**, and **whenever_tray** is kept. Its actual location depends on the host platform: 4 | 5 | * `~/.whenever` on Linux 6 | * `%APPDATA%\Whenever` on Windows 7 | * `~/Library/Application Support/.whenever` on Mac. 8 | 9 | Logs and configuration files can be found in this directory. 10 | 11 | When launching the resident wrapper, the following parameter can be specified on the command line: 12 | 13 | - `-D`/`--dir-appdata` _PATH_: specify the application data and configuration directory 14 | 15 | However, it is recommended _not_ to specify a custom _APPDATA_ directory unless really needed, because by default both **When** and **whenever_tray** use this directory to locate the scheduler configuration file -- that is, the one generated by **When** in configuration mode. 16 | 17 | 18 | ## See Also 19 | 20 | * [Configuration Utility](cfgform.md) 21 | * [Configuration File](configfile.md) 22 | 23 | 24 | [`◀ Main`](main.md) 25 | -------------------------------------------------------------------------------- /support/docs/cfgform.md: -------------------------------------------------------------------------------- 1 | # Main Configuration Form 2 | 3 | The _Main Configuration Form_ provides access to all _item_ editors that are available for the hosting platform. 4 | 5 | ![MainWindow](graphics/when-config-main.png) 6 | 7 | This form allows to [review, edit](#edit-items), [remove](#remove-items), and [create](#create-items) _items_ in the current configuration file, as well as to modify the global, scheduler related [parameters](#modify-scheduler-parameters). The configuration file path is shown in the top part of the form, for reference, and cannot be altered: this path, based on the [application data directory](appdata.md), ensures that the same configuration file can be used with either **When** or **whenever_tray** as the resident frontend (or _wrapper_) for the actual scheduler, assuming that the base directory is not altered via the specific [CLI option](cli.md). 8 | 9 | 10 | ## Edit Items 11 | 12 | An item can be edited by selecting its name row in the _Current Items_ list, and then clicking on the _Edit..._ button. This opens the appropriate item editor, displaying all the actual values for the selected item, in order to let the user modify them according to his needs or requirements. 13 | 14 | 15 | ## Create Items 16 | 17 | By clicking the _New..._ button, a dialog will pop up asking which type of item has to be created: after selecting it and clicking _OK_ the appropriate editor opens for the desired item type. Note that mandatory parameters are set with default values: of course such values can be modified to suit the user's preferences. Item creation forms are, other than this, in no way different from the forms used to edit existing item parameters. These forms are specifically described throughout the documentation. 18 | 19 | 20 | ## Remove Items 21 | 22 | Just clicking the _Remove_ button after selecting the item that has to be deleted will actually remove it from the configuration file, obviously after prompting for confirmation. 23 | 24 | 25 | ## Modify Scheduler Parameters 26 | 27 | The two global scheduler related parameters that are avaiable through this form are the following: 28 | 29 | * _Tick Interval_: the amount of seconds that the scheduler will wait between subsequent checks that the defined, active conditions are verified: the default value is 5 seconds, which is actually frequent enough to ensure a quick reaction to external events; 30 | * _Randomize Checks Within Ticks_: whether or not the actual execution of tests for each condition (excluding those that strictly depend on time) have to be performed at random instants within the tick interval or at the exact time when the interval has actually passed. 31 | 32 | The second option, if checked, allows the scheduler to try to run tests for different conditions at a random instant within the interval between ticks. This allows to avoid running all the condition tests at the same time, which could in certain cases cause a significant load on the local machine. Unless there are specific reasons not to, it is advisable to check this option. 33 | 34 | 35 | ## Reload Configuration 36 | 37 | When the application has been launched as a resident frontend for a live instance of **whenever**, this form also shows a _Reload_ button which can be used to reload the configuration to the scheduler: only modified items are affected and will be updated and reset. 38 | 39 | > **Note**: only _items_ are reloaded, the global scheduler parameters do not change while the scheduler is running and will be applied at the next start. 40 | 41 | 42 | ## Exiting 43 | 44 | The _Exit_ button has different behaviors depending on how the main configuration form was entered: if a configuration-only session was launched using the `config` command, then the button allows for completely leaving the application. If the form was launched through the _Configurator..._ entry in the system tray menu, the resident part of **When** remains active, and only the configuration utility is exited. Please note that writing a new configuration file will not cause the main scheduler to automatically reload it. 45 | 46 | Also note that, if anything had changed since the configuration file was last saved, the configuration form will ask anyway to save it again -- no matter whether it had been launched via the `config` command or via the system tray menu. 47 | 48 | 49 | ## See Also 50 | 51 | * [Installation](install.md) 52 | * [Toolbox](cli.md#toolbox) 53 | * [Resident Wrapper](tray.md) 54 | 55 | 56 | [`◀ Main`](main.md) 57 | -------------------------------------------------------------------------------- /support/docs/cli.md: -------------------------------------------------------------------------------- 1 | # Command Line Interface (CLI) 2 | 3 | The documentation assumes that **When** has been installed using the instructions provided in the installation [guide](install.md). In this case the `when` command should be available from the command line, and could be invoked as follows: 4 | 5 | ```shell 6 | when COMMAND [OPTIONS] 7 | ``` 8 | 9 | where `COMMAND` is one of the following: 10 | 11 | - `config` to launch the [configuration utility](cfgform.md), without staying resident (i.e. no system tray icon) 12 | - `start` to launch the resident **whenever** [wrapper](tray.md) displaying a control icon on the system tray area 13 | - `tool` to launch one of the utilities that can help in the setup of a working environment 14 | - `version` to display version information. 15 | 16 | More commands might be supported in the future. `OPTIONS` are the possible options, which have effect on specific commands: 17 | 18 | - `-D`/`--dir-appdata` _PATH_: specify the application data and configuration directory (default: _%APPDATA%\Whenever_ on Windows, _~/.whenever_ on Linux) 19 | - `-W`/`--whenever` _PATH_: specify the path to the whenever executable (defaults to the one found in the PATH if any, otherwise exit with error, specific to `start`) 20 | - `-L`/`--log-level` _LEVEL_: specify the log level, all **whenever** levels are supported (default: _info_, specific to `start`) 21 | - `-h`/`--help`: print a brief help message about commands and options. 22 | 23 | In order to know which options can be used for each command, `when COMMAND --help` can be invoked from the command line, where `COMMAND` is one of the commands described above. 24 | 25 | > **Note**: In order to simplify the usage of **When**, many values that could have been implemented as parameters are instead left as defaults that cannot be changed, at least for now, if not via direct intervention on the code. 26 | 27 | This version of **When** uses [poetry](https://python-poetry.org/) to manage dependencies and to provide a suitable environment for a source distribution, therefore after running `poetry install` in the project directory to install all necessary Python dependencies, **When** can also be launched as `poetry run when COMMAND [OPTIONS]`. 28 | 29 | 30 | ## Toolbox 31 | 32 | The `tool` command provides various utilities that can help in the setup of **When** for a desktop environment. Each utility is invoked by means of a subcommand, which might possibly have variants. The following list explains the available subcommands and their options: 33 | 34 | * `--install-whenever`: downloads and installs the latest version of **whenever** for the current user 35 | * `--create-icons`: create the **When** desktop shortcuts (for both the configuration utility and for the resident application) for the current user in the _Start_ or _Applications_ menu -- depending on the host platform; accepts the following modifiers 36 | * `--autostart`: (option) creates a shortcut that launches the resident version of **When** when the user logs in 37 | * `--desktop`: (option) also creates icons on the desktop[^1] 38 | * `--fix-config`: fix legacy configuration files converting [old item definitions](configfile.md#legacy-configuration-files) to new ones 39 | * ... 40 | * `--quiet`: (option) applies to all the operations described above, and inhibits printing messages to the console. 41 | 42 | The subcommands cannot be combined. The **whenever** installation step should be performed first if there is no working copy of the core scheduler on the system. 43 | 44 | 45 | ## See Also 46 | 47 | * [Installation](install.md) 48 | * [Configuration Utility](cfgform.md) 49 | * [Resident Wrapper](tray.md) 50 | 51 | 52 | [`◀ Main`](main.md) 53 | 54 | 55 | [^1]: on some Linux desktop (for example, the most recent versions of Gnome) the deployment of launcher files in the `~/Desktop` subfolder is not honored as a way to create desktop icons, thus the `--desktop` option is actually executed, but has no effect. 56 | -------------------------------------------------------------------------------- /support/docs/cond_eventrelated.md: -------------------------------------------------------------------------------- 1 | # Event Conditions 2 | 3 | Event based conditions are the simplest ones in terms of definition: there are no specific parameters to be set, because it is left to the [events](events.md) to decide what condition has to be fired. 4 | 5 | ![WhenCondEvent](graphics/when-cond-event.png) 6 | 7 | Therefore, the _Specific Parameters_ panel only displays a notice stating that no particular configuration is required. 8 | 9 | Since this type of conditions is not time related, the related check might be performed at a random time between two scheduler ticks if the corresponding global scheduler [option](cfgform.md#scheduler-parameters) is set. 10 | 11 | 12 | ## See Also 13 | 14 | * [Events](events.md) 15 | 16 | 17 | [`◀ Conditions`](conditions.md) 18 | -------------------------------------------------------------------------------- /support/docs/cond_extra01.md: -------------------------------------------------------------------------------- 1 | # Extra Conditions 2 | 3 | The condition items described here cover various different aspects of a session, and their availability may vary depending on the hosting platform and the presence of certain features. On Linux **whenever** is generally expected to be compiled with DBus support, on Windows it is expected to be compiled with WMI support. 4 | 5 | 6 | ## System Load 7 | 8 | This test verifies whether or not the system load is below a certain percentage and, if so, runs the related tasks. The only available specific parameter is the percentage threshold below which the test is considered successful. 9 | 10 | ![WhenCondExtraSysload](graphics/when-cond-extra-sysload.png) 11 | 12 | The item can be used on Windows and Linux systems, on Linux it depends on the presence of the `vmstat` and `bc` OS commands, which may need to be installed on some distributions. The checks for this condition are performed about every minute. 13 | 14 | 15 | ## Low Battery 16 | 17 | This test checks whether the battery is draining and its charge is below a certain percentage and, if so, runs the related tasks. The only available specific parameter is the percentage threshold below which the test is considered successful. 18 | 19 | ![WhenCondExtraSysload](graphics/when-cond-extra-batterylow.png) 20 | 21 | The checks for this condition are performed about every minute. 22 | 23 | 24 | ## Charging Battery 25 | 26 | This test checks whether the battery is charging and its charge is above a certain percentage and, if so, runs the related tasks. The only available specific parameter is the percentage threshold above which the test is considered successful. 27 | 28 | ![WhenCondExtraSysload](graphics/when-cond-extra-batterycharging.png) 29 | 30 | The checks for this condition are performed about every minute. 31 | 32 | 33 | ## Removable Drives 34 | 35 | Definition forms to check for presence of a removable drive are available, both for Windows and for Linux. The behavior and configuration is slightly different depending on the host platform. 36 | 37 | 38 | ### Windows 39 | 40 | On Windows, the _label_ of the removable drive has to be specified in order to be somewhat more selective in choosing which removable drive has been made available to the workstation: the label can be edited by any windows user, so it's possible to appropriately set labels for thumb drives, cards, and so on, in a way that helps select tasks to be executed when a removable drive is inserted. 41 | 42 | ![WhenCondRemovableDriveWin](graphics/when-cond-extra-rmdrive-win.png) 43 | 44 | The label has to be specified by the user with no assistance: the exact string to be checked must be entered. Optionally, by checking the appropriate box, it is possible to specify on which drive letter the support is supposed to be auto-mounted: this is another parameter for fine-tuning, since often Windows reuses the same drive letter when the same removable drive is used. 45 | 46 | 47 | ### Linux 48 | 49 | On Linux, it's only possible to choose among a system-determined list of available removable drive _names_, or it is possible for the user to specify the exact name: since these names are not always easy to remember, it is suggested that the removable storage to monitor is inserted when creating the condition in **whenever**, so that its name appears in the list. 50 | 51 | ![WhenCondRemovableDriveLinux](graphics/when-cond-extra-rmdrive-linux.png) 52 | 53 | It is not possible to specify the expected mount point: its determination, which depends on the specific Linux distribution and configuration, is up to the user. 54 | 55 | 56 | ## Session Locked (Windows) 57 | 58 | This check detects whether the session is locked. It does not depend on the desktop being idle (there is the [idle session](cond_timerelated.md#idle-session) based condition for this purpose), so it is verified also when the user voluntarily locks the session via a key combination or a session menu entry. 59 | 60 | ![WhenCondSessionLockedWin](graphics/when-cond-extra-locked-win.png) 61 | 62 | Since on Windows the state is detected by querying the system actively, the user can decide if the check is performed at a normal pace (the default, every second minute), very often (the _pedantic_ choice, every minute), or in a more relaxed way -- that is, every five minutes. 63 | 64 | 65 | ## See also 66 | 67 | * [Command Based Conditions](cond_actionrelated.md#command) 68 | * [Lua Script Based Conditions](cond_actionrelated.md#lua-script) 69 | * [Conditions](conditions.md) 70 | * [Tasks](tasks.md) 71 | * [Events](events.md) 72 | 73 | 74 | [`◀ Conditions`](conditions.md) 75 | -------------------------------------------------------------------------------- /support/docs/cond_timerelated.md: -------------------------------------------------------------------------------- 1 | # Time Related Conditions 2 | 3 | The condition items described in this section depend on time checking, therefore the related tests will always be performed exactly at the scheduler tick: this means that such tests ar never performed [at a random instant](cfgform.md#modify-scheduler-parameters) between two ticks. 4 | 5 | 6 | ## Interval 7 | 8 | Interval based conditions are possibly the simplest ones: verification depends on the passing of a certain amount of time since the start of the scheduler. In combination with the _recurring_ flag (in the common section) leads to _periodic_ conditions, that is, occurring every time the specified interval passes. 9 | 10 | ![WhenCondInterval](graphics/when-cond-interval.png) 11 | 12 | The only available parameter is the _interval duration_, that may be specified in seconds, minutes, or hours. 13 | 14 | 15 | ## Time Specification 16 | 17 | This type of condition overlaps with the common, system provided time based scheduler. One or more time specifications can be provided, at which the condition is verified. The time specification can be given indicating a (partial) instant, by filling part of the entries in the row above the time specification list. Omitting part of the entries has different effects depending on which ones are omitted. Generally, specifying the date entries and omitting the time related ones indicates that the condition occurs at midnight at the specified date. Omitting the year indicates that the condition occurs every specified month and day at midnight. Omitting year and month and specifying day _N_ indicates that the condition occurs every day _N_ of the month in all months (unless, of course, _N_ is above 28 or 29, which excludes some months). Specifying a day of the week of course indicates the occurrence every that day of the week. Providing hours, minutes and seconds restricts occurrence to the specified time. If no date or weekday are specified, the condition occurs every day at the specified time, but in this case, when the hour is omitted, the occurrence happens every hour at the specified minute (and second if provided). 18 | 19 | ![WhenCondTime](graphics/when-cond-time.png) 20 | 21 | The list tries to display the provided specifications in human-readable form. In order to add a specification, once the necessary fields are filled, it is sufficient to click the _Add_ button. To remove a specification it must be double clicked in the list, and then the _Remove_ button must be clicked. The _Clear_ button in the first row is useful to clear all the entries at its left (it does not modify the list contents). To remove _all_ provided time specifications at once, the _Clear All_ button is available in the lower part of the form. 22 | 23 | Note that, if the condition is not recurrent, it will occur when just _one_ of the provided time specifications is reached for the first time. 24 | 25 | 26 | ## Idle Session 27 | 28 | This condition occurs after a certain time has passed since there was any type of user interaction (mouse, keyboard, touch screen, etc.) with the current session. 29 | 30 | ![WhenCondIdle](graphics/when-cond-idle.png) 31 | 32 | The only available parameter is the _duration of the idle session_, that may be specified in seconds, minutes, or hours. In this case specifying the condition as _recurring_ will cause it to occur again if the session exits its idle state first, and then returns idle for the same amount of time. 33 | 34 | > **Note**: formally _Wayland_ based Linux desktops are supported by **whenever** and therefore by **When**. However, in case the _X.org_ libraries related to idle time (namely, _libx11-dev_ and _libxss-dev_), the **whenever** binary must be specifically built for these platforms: these binaries calculate the _idle time_ as the time spent when the session is _locked_ instead of considering the last time that an user interacted with the desktop. The provided binary distributions actually support desktops where _X.org_ is available. 35 | 36 | 37 | [`◀ Conditions`](conditions.md) 38 | -------------------------------------------------------------------------------- /support/docs/conditions.md: -------------------------------------------------------------------------------- 1 | # Condition Editors 2 | 3 | All condition editors share a common part, which encompasses all parameters that are common to all condition items: 4 | 5 | ![ConditionCommon](graphics/when-cond-common.png) 6 | 7 | It allows to set the mandatory item _Name_ (an alphanumeric string beginning either with a letter or an underscore), the tasks associated with it, and to decide other behaviors specific to conditions: 8 | 9 | * _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. 10 | * _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. 11 | * _Suspend Condition at Startup_: to start the condition in suspended mode, which means that it will not be checked during the session. 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | 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. 18 | 19 | The conditions available in **When** that are natively supported by **whenever** are the following: 20 | 21 | * [_Command_ conditions](cond_actionrelated.md#command) 22 | * [_Event_ conditions](cond_eventrelated.md) 23 | * [_Idle Session_ conditions](cond_timerelated.md#idle-session) 24 | * [Time _Interval_ conditions](cond_timerelated.md#interval) 25 | * [_Lua Script_ conditions](cond_actionrelated.md#lua-script) 26 | * [_Time_ related conditions](cond_timerelated.md#time-specification) 27 | 28 | 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: 29 | 30 | * [_System Load_ below Threshold](cond_extra01.md#system-load) conditions 31 | * [_Battery Charge_ below Threshold](cond_extra01.md#low-battery) conditions 32 | * [_Battery Charge_ above Threshold](cond_extra01.md#charging-battery) conditions 33 | * [_Removable Drive_ available](cond_extra01.md#removable-drives) conditions 34 | * [_Session Locked_](cond_extra01.md#session-locked-windows) (Windows only) conditions 35 | 36 | The above list will grow with time, along with the discovery of new configuration possibilities on the supported platforms and the development of the related forms. 37 | 38 | 39 | ## See Also 40 | 41 | * [Tasks](tasks.md) 42 | * [Events](events.md) 43 | 44 | 45 | [`◀ Main`](main.md) 46 | -------------------------------------------------------------------------------- /support/docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "The When Automation Tool" 10 | author = "Francesco Garosi" 11 | copyright = "2015-%Y, Francesco Garosi" 12 | release = '1.9' 13 | 14 | html_logo = "graphics/rafi-clock-256.png" 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = ['myst_parser', 'sphinx_favicon'] 20 | source_suffix = ['.rst', '.md'] 21 | 22 | # templates_path = ['_templates'] 23 | exclude_patterns = ['_*.rst', '_*.md', '_build', 'Thumbs.db', '.DS_Store'] 24 | 25 | # -- Options for HTML output ------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 27 | 28 | html_theme = 'sphinx_rtd_theme' 29 | html_static_path = ['_static'] 30 | 31 | favicons = ['favicon.ico'] 32 | 33 | # end. 34 | -------------------------------------------------------------------------------- /support/docs/configfile.md: -------------------------------------------------------------------------------- 1 | # Generated Configuration File 2 | 3 | Some notes follow about the **whenever** configuration files generated by **When**. 4 | 5 | * The generated configuration files are well-formed TOML files: this means, for instance, that they can be re-edited by hand and, if the result is still well-formed and compliant to the format used by **whenever**, used to configure the scheduler. However there are some peculiarities that have to be taken into account: 6 | * the strings and arrays generated by **When** are all one-liners: it is possible to convert them to multiline arrays and strings, and **When** wil be able to read and edit the resulting files without problems, but this human-readable formatting is lost for all item definitions as soon as **When** rewrites the configuration file: as a consequence, one-line strings will contain escaped characters when needed; 7 | * TOML tables, on the other hand, are written using the TOML extended format (thus not as one-line pair sequences enclosed in curly braces), even for simple and short mappings: any conversion of tables into the simpler, one-line format can be read by **When** but is lost also in this case if the file is rewritten. 8 | * **When** uses the `tags` configuration entry for specialized items built on top of the ones that are native to **whenever**: of course, it also sets the standard parameters known to **whenever** (it ignores the contents of the `tags` entry), and the user can modify any of these standard parameters by hand if needed: **whenever** will obey these changes when using the resulting configuration files even when it is launched using **When** as frontend; however, as soon as **When** re-reads the file for editing, it will ignore all the changes that have been made to the standard parameters and reuse the ones that are calculated according to the values stored in the `tags` section. 9 | * Since the files generated by **When** are just standard **whenever** oriented configuration files, they can be used with any frontend and even with no frontend at all, by just respectively instructing the chosen frontend (for example, **whenever_tray**) or **whenever** itself to use the _%APPDATA%\Whenever\whenever.toml_ or _~/.whenever/whenever.toml_ configuration file depending on the host platform. 10 | 11 | This means, for instance, that **When** can also be used as a way to start configuring the scheduler only for the first time, and that the resulting configuration file can then be edited and enhanced manually to suit the user's needs. Also note that if the `tags` table is completely removed from the definition of an item, _that item will still work_ in **whenever**. The only drawback is that **When** will not be able to edit that item using the specific editor and, _if supported_, the editor for the corresponding standard item is used. 12 | 13 | 14 | ## Legacy Configuration Files 15 | 16 | During the development of **When**, especially in case any new features are added to the actual **whenever** scheduler, some of the item definitions for items _specific to **When**_[^1] in the configuration files may change, in order to use more efficient or lightweight ways to achieve the same result. In this case, provided that the prerequisites for the legacy version of a changed item are still verified, using the old configuration file _does not affect_ the expected behavior of the scheduler. Instead, it may become impossible to use the GUI to edit these items because **When** does not recognize them anymore. 17 | 18 | In order to be able to use the configuration utility it may be necessary to fix the configuration file using the provided `--fix-config` tool from the [command line](cli.md#toolbox). 19 | 20 | 21 | ## See Also 22 | 23 | * [Main Configuration Form](cfgform.md) 24 | * [Application Data Directory](appdata.md) 25 | * [Tasks](tasks.md) 26 | * [Conditions](conditions.md) 27 | * [Events](events.md) 28 | 29 | 30 | [`◀ Main`](main.md) 31 | 32 | 33 | [^1]: Generally this does not affect _native_ **whenever** items: the evolution of **whenever** usually consists in _adding_ new features instead of changing existing ones. 34 | -------------------------------------------------------------------------------- /support/docs/events.md: -------------------------------------------------------------------------------- 1 | # Native Events Editors 2 | 3 | The only type of event editable in **When** that is natively supported by the scheduler is the one that depends on changes in files and directories that are monitored by the operating system. 4 | 5 | 6 | ## File System Monitoring 7 | 8 | This type of event fires whenever one of the monitored files or directories undergoes a modification: for files it means that its contents or metadata are altered, for directories it also occurs when there is a change in any of the files that they contain. In the latter case, subdirectories are also monitored if the _Recursive_ flag is set. 9 | 10 | ![WhenEventFSChange](graphics/when-event-fschange.png) 11 | 12 | The event _Name_ is mandatory and must be an alphanumeric string beginning with a letter or an underscore. The associated _Condition_ is also mandatory, and must be selected among the appropriate ones using the drop down list. 13 | 14 | The items (files and directories) to be monitored can be provided by entering their path in the _Item_ fiels below the list, and then checking the _Add_ button to populate the list: the button with three dots on the right provides a convenient way to browse the filesystem and select objects. By checking the _Use button to select directories_ option, the dialog box associated with the three-dotted button will allow to choose directories, otherwise to choose files. 15 | 16 | To remove an item, double click it on the list and then click the _Remove_ button. 17 | 18 | 19 | ## Other Event Types 20 | 21 | Items that handle particular events, which may in many cases be available for a specific host platform, are described in this [section](events_extra01.md). In particular, the following types of event are available: 22 | 23 | * [Session Lock Events](events_extra01.md#session-lockedunlocked-events-linux) (Linux only) 24 | * [Session Unlock Events](events_extra01.md#session-lockedunlocked-events-linux) (Linux only) 25 | 26 | and more may be added in the future. 27 | 28 | 29 | ## See Also 30 | 31 | * [Event Based Conditions](cond_eventrelated.md) 32 | * [Conditions](conditions.md) 33 | * [Tasks](tasks.md) 34 | 35 | 36 | [`◀ Main`](main.md) 37 | -------------------------------------------------------------------------------- /support/docs/events_extra01.md: -------------------------------------------------------------------------------- 1 | # Specific Events 2 | 3 | The condition items described here cover various different aspects of a session, and their availability may vary depending on the hosting platform and the presence of certain features. 4 | 5 | 6 | ## Session Locked/Unlocked Events (Linux) 7 | 8 | The _Session Locked Event_ and _Session Unlocked Event_ occur, as it might result quite obvious, respectively when the session is either locked or unlocked. No matter whether the workstation has been locked voluntarily or by the session manager after a certain period of inactivity: **When** catches the event in both cases -- in this it does not overlap with [idle session](cond_timerelated.md#idle-session) based conditions. 9 | 10 | ![WhenEventSessionLockLinux](graphics/when-event-extra-lock-linux.png) 11 | 12 | Both types of event do not require specific parameters: only the name should be set to something meaningful and an [event based condition](cond_eventrelated.md#event-conditions) _must_ be associated to the event. 13 | 14 | 15 | ## See also 16 | 17 | * [Event Based Conditions](cond_eventrelated.md) 18 | * [File System Monitoring Events](events.md#file-system-monitoring) 19 | * [Events](events.md) 20 | * [Conditions](conditions.md) 21 | * [Tasks](tasks.md) 22 | 23 | 24 | [`◀ Events`](events.md) 25 | -------------------------------------------------------------------------------- /support/docs/graphics/install-gnome-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/install-gnome-login.png -------------------------------------------------------------------------------- /support/docs/graphics/install-linux-extmgr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/install-linux-extmgr.png -------------------------------------------------------------------------------- /support/docs/graphics/rafi-clock-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/rafi-clock-256.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_chores01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_chores01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_chores02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_chores02.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_idle01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_idle01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_idle02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_idle02.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_idle03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_idle03.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_interval01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_interval01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_interval02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_interval02.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_new_chores01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_new_chores01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_new_idle01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_new_idle01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_cond_new_interval01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_cond_new_interval01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_config_main01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_config_main01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_backup01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_backup01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_backup02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_backup02.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_chores01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_chores01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_lua01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_lua01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_new_cmd01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_new_cmd01.png -------------------------------------------------------------------------------- /support/docs/graphics/tutorial_task_new_lua01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/tutorial_task_new_lua01.png -------------------------------------------------------------------------------- /support/docs/graphics/when-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-application.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-command.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-common.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-event.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-batterycharging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-batterycharging.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-batterylow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-batterylow.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-locked-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-locked-win.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-rmdrive-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-rmdrive-linux.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-rmdrive-win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-rmdrive-win.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-extra-sysload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-extra-sysload.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-idle.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-interval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-interval.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-lua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-lua.png -------------------------------------------------------------------------------- /support/docs/graphics/when-cond-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-cond-time.png -------------------------------------------------------------------------------- /support/docs/graphics/when-config-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-config-main.png -------------------------------------------------------------------------------- /support/docs/graphics/when-event-extra-lock-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-event-extra-lock-linux.png -------------------------------------------------------------------------------- /support/docs/graphics/when-event-fschange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-event-fschange.png -------------------------------------------------------------------------------- /support/docs/graphics/when-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-history.png -------------------------------------------------------------------------------- /support/docs/graphics/when-menu-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-menu-form.png -------------------------------------------------------------------------------- /support/docs/graphics/when-task-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-task-command.png -------------------------------------------------------------------------------- /support/docs/graphics/when-task-extra-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-task-extra-session.png -------------------------------------------------------------------------------- /support/docs/graphics/when-task-lua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-task-lua.png -------------------------------------------------------------------------------- /support/docs/graphics/when-tray-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/docs/graphics/when-tray-menu.png -------------------------------------------------------------------------------- /support/docs/history.md: -------------------------------------------------------------------------------- 1 | # History Box 2 | 3 | This simple dialog shows the tasks that have been executed by the scheduler in reaction to conditions. 4 | 5 | ![HistoryBox](graphics/when-history.png) 6 | 7 | The list, representing the last completed tasks in descending time order (that is, the first row shows the most recent task), shows the following information: 8 | 9 | * _Time_: the time at which the task started running 10 | * _Task_: the name of the executed task 11 | * _Triggered By_: the name of the condition that triggered the task 12 | * _Duration_: the duration of the task, in seconds 13 | * _OK_: a sign indicating the outcome: a checkmark for a positive outcome, a cross for a negative one, the empty set sign for undetermined results 14 | * _Message_: additional information provided by **whenever** 15 | 16 | The history box does not capture results in real time, but the _Reload_ button can be used to update its contents instantly. Only the latest 100 results are displayed -- this might become a configurable parameter in further versions. 17 | 18 | 19 | ## See Also 20 | 21 | * [Tasks](tasks.md) 22 | 23 | 24 | [`◀ Main`](main.md) 25 | -------------------------------------------------------------------------------- /support/docs/index.rst: -------------------------------------------------------------------------------- 1 | The When Automation Tool 2 | ======================== 3 | 4 | This document describes the new version of **When**, a Python-based automation tool for the desktop. This version, instead of incorporating the scheduler, relies on the `whenever `_ core, which focuses on reliability and lightweightness, while trying to achieve a good performance even when running at low priority. In this sense, **When** acts as a *wrapper* for **whenever**, both providing a simple interface for configuration and an easy way to control the scheduler via an icon sitting in the tray area of your desktop. This version of **When** aims at being cross-platform, dynamically providing access to the features of **whenever** that are supported on the host environment. 5 | 6 | `When `_ development is hosted on GitHub. 7 | 8 | 9 | .. image:: graphics/when-application.png 10 | 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | main 17 | install 18 | tutorial 19 | cli 20 | cfgform 21 | tray 22 | tasks 23 | tasks_extra_session 24 | conditions 25 | cond_timerelated 26 | cond_actionrelated 27 | cond_eventrelated 28 | cond_extra01 29 | events 30 | events_extra01 31 | history 32 | appdata 33 | configfile 34 | -------------------------------------------------------------------------------- /support/docs/tasks_extra_session.md: -------------------------------------------------------------------------------- 1 | # Extra Tasks: Session Related 2 | 3 | ## Tasks 4 | 5 | The following are simple session related tasks, useful for closing or locking a session: 6 | 7 | * **Lock Session**: locks the session and waits for credentials, but all applications remain open. 8 | * **Log Off**: closes all open applications and _exits_ the current session, opening the login screen. 9 | * **Hybernate**: (_Windows only_) takes the workstation into hybernated state, without closing any application. 10 | * **Shutdown**: closes all open applications and _powers off_ the workstation. 11 | * **Reboot**: closes all open applications and _restarts_ the workstation. 12 | 13 | In all cases the task definition form is similar to the one below. 14 | 15 | ![WhenSessionTasks](graphics/when-task-extra-session.png) 16 | 17 | These tasks do not require any extra parameter. 18 | 19 | 20 | ## See also 21 | 22 | * [Command Based Conditions](cond_actionrelated.md#command) 23 | * [Lua Script Based Conditions](cond_actionrelated.md#lua-script) 24 | * [Conditions](conditions.md) 25 | * [Tasks](tasks.md) 26 | * [Events](events.md) 27 | 28 | 29 | [`◀ Tasks`](tasks.md) 30 | -------------------------------------------------------------------------------- /support/docs/tray.md: -------------------------------------------------------------------------------- 1 | # System Tray Resident Application 2 | 3 | The system tray resident wrapper to **whenever** is launched via the `start` command or via the _When Start_ desktop or menu icon: it launches the **whenever** scheduler executable in the background, also avoiding to show the console window on _Windows_ desktops, and taking care to capture the scheduler output, interpret it to keep track of the tasks that have been executed along with their outcomes, and write the scheduler log to the [_application data_](appdata.md) directory. The resident wrapper also displays a clock icon in the tray area, meaning that the scheduler is active, which can be right-clicked (left-clicked on some Linux machines, see below) to allow a certain degree of interaction with the underlying scheduler itself. The icon may display a yellow dot in its lower-left corner, meaning that the scheduler is busy checking conditions or executing one or more tasks. 4 | 5 | ![TrayMenu](graphics/when-tray-menu.png) 6 | 7 | The available entries are: 8 | 9 | * _Pause/Resume Scheduler_: respectively pause scheduler checks or resume them; when the scheduler is paused no condition checks are performed and thus no tasks are launched. This does not affect however currently running tasks which keep running until their conclusion or until a timeout has been reached. When the scheduler is paused, the clock icon appears grayed out. 10 | * _Reset Conditions_: reset the status of all conditions, and in particular consider all _non recurrent_ conditions that were verified during the session (and that, therefore, would not be checked anymore) as _not_ verified, thus restarting checks. 11 | * _Show History_: display the [_History Box_](history.md), a streamlined viewer that displays the tasks that have been executed along with their outcomes. 12 | * _Configurator_: display the [configuration application](cfgform.md). 13 | * _About_: display a simple information box. 14 | * _Exit_: stop the underlying scheduler and exit the resident application: this may require some time because **When** waits for all the tasks to finish before releasing the scheduler.[^1] 15 | 16 | When launching the resident wrapper, the following parameters can be specified on the command line: 17 | 18 | - `-D`/`--dir-appdata` _PATH_: specify the application data and configuration directory 19 | - `-W`/`--whenever` _PATH_: specify the path to the whenever executable (defaults to the one found in the PATH if any, otherwise exit with error) 20 | - `-L`/`--log-level` _LEVEL_: specify the log level, all **whenever** levels are supported (default: _info_, possible values are _error_, _warn_, _info_, _debug_, and _trace_) 21 | 22 | However, it is recommended _not_ to specify a custom _APPDATA_ directory unless really needed, because by default both **When** and **whenever_tray** use this directory to locate the scheduler configuration file -- that is, the one generated by **When** in configuration mode. 23 | 24 | The suggested [installation procedure](install.md) and, in particular, adding icons for **When** with the `--autostart` option, can be used to set it up to automatically start at the beginning of the desktop session. 25 | 26 | 27 | ## Menu Form 28 | 29 | Modern linux distributions based on the Gnome desktop environment do not always support system tray menus directly: in some cases **When** falls back to a menu window that is launched by _left-clicking_ the tray icon: 30 | 31 | ![MenuForm](graphics/when-menu-form.png) 32 | 33 | This menu window shows exactly the same entries as the missing tray menu (plus a _Cancel_ button to hide it): clicking the buttons invokes the same tools that the corresponding menu entries would bring up. Following the suggested [installation instructions](install.md#linux) generally helps to set up a fully-functional instance of **When**, that includes the system tray menu. 34 | 35 | 36 | ## See Also 37 | 38 | * [Installation](install.md) 39 | * [Toolbox](cli.md#toolbox) 40 | * [Configuration Utility](cfgform.md) 41 | 42 | 43 | [`◀ Main`](main.md) 44 | 45 | 46 | [^1]: Unless a timeout is set, some tasks may actually never exit: in this case **When** itself will not be able to shut down. 47 | -------------------------------------------------------------------------------- /support/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/favicon.ico -------------------------------------------------------------------------------- /support/icons/icons8-add-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-add-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-add-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-add-48.png -------------------------------------------------------------------------------- /support/icons/icons8-add-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-add-96.png -------------------------------------------------------------------------------- /support/icons/icons8-cancel-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-cancel-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-cancel-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-cancel-48.png -------------------------------------------------------------------------------- /support/icons/icons8-cancel-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-cancel-96.png -------------------------------------------------------------------------------- /support/icons/icons8-check-mark-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-check-mark-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-check-mark-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-check-mark-48.png -------------------------------------------------------------------------------- /support/icons/icons8-check-mark-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-check-mark-96.png -------------------------------------------------------------------------------- /support/icons/icons8-circle-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-circle-16.png -------------------------------------------------------------------------------- /support/icons/icons8-circled-play-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-circled-play-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-circled-play-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-circled-play-32.png -------------------------------------------------------------------------------- /support/icons/icons8-circled-play-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-circled-play-32x32.png -------------------------------------------------------------------------------- /support/icons/icons8-circled-play-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-circled-play-48.png -------------------------------------------------------------------------------- /support/icons/icons8-clock-48-busy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-clock-48-busy.png -------------------------------------------------------------------------------- /support/icons/icons8-clock-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-clock-48.png -------------------------------------------------------------------------------- /support/icons/icons8-clock-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-clock-96.png -------------------------------------------------------------------------------- /support/icons/icons8-clock-gray-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-clock-gray-48.png -------------------------------------------------------------------------------- /support/icons/icons8-clock-gray-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-clock-gray-96.png -------------------------------------------------------------------------------- /support/icons/icons8-close-window-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-close-window-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-close-window-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-close-window-48.png -------------------------------------------------------------------------------- /support/icons/icons8-close-window-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-close-window-96.png -------------------------------------------------------------------------------- /support/icons/icons8-delete-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-delete-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-delete-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-delete-48.png -------------------------------------------------------------------------------- /support/icons/icons8-delete-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-delete-96.png -------------------------------------------------------------------------------- /support/icons/icons8-enter-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-enter-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-enter-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-enter-48.png -------------------------------------------------------------------------------- /support/icons/icons8-enter-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-enter-96.png -------------------------------------------------------------------------------- /support/icons/icons8-error-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-error-48.png -------------------------------------------------------------------------------- /support/icons/icons8-error-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-error-96.png -------------------------------------------------------------------------------- /support/icons/icons8-exclamation-mark-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-exclamation-mark-48.png -------------------------------------------------------------------------------- /support/icons/icons8-exclamation-mark-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-exclamation-mark-96.png -------------------------------------------------------------------------------- /support/icons/icons8-exit-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-exit-32.png -------------------------------------------------------------------------------- /support/icons/icons8-exit-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-exit-32x32.png -------------------------------------------------------------------------------- /support/icons/icons8-exit-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-exit-48.png -------------------------------------------------------------------------------- /support/icons/icons8-file-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-file-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-file-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-file-48.png -------------------------------------------------------------------------------- /support/icons/icons8-file-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-file-96.png -------------------------------------------------------------------------------- /support/icons/icons8-folder-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-folder-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-folder-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-folder-48.png -------------------------------------------------------------------------------- /support/icons/icons8-folder-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-folder-96.png -------------------------------------------------------------------------------- /support/icons/icons8-help-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-help-20.png -------------------------------------------------------------------------------- /support/icons/icons8-index-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-index-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-index-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-index-32.png -------------------------------------------------------------------------------- /support/icons/icons8-index-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-index-32x32.png -------------------------------------------------------------------------------- /support/icons/icons8-index-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-index-48.png -------------------------------------------------------------------------------- /support/icons/icons8-kite-shape-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-kite-shape-16.png -------------------------------------------------------------------------------- /support/icons/icons8-medium-priority-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-medium-priority-20.png -------------------------------------------------------------------------------- /support/icons/icons8-new-document-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-new-document-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-new-document-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-new-document-48.png -------------------------------------------------------------------------------- /support/icons/icons8-new-document-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-new-document-96.png -------------------------------------------------------------------------------- /support/icons/icons8-pause-squared-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pause-squared-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-pause-squared-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pause-squared-32.png -------------------------------------------------------------------------------- /support/icons/icons8-pause-squared-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pause-squared-32x32.png -------------------------------------------------------------------------------- /support/icons/icons8-pause-squared-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pause-squared-48.png -------------------------------------------------------------------------------- /support/icons/icons8-pencil-drawing-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pencil-drawing-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-pencil-drawing-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pencil-drawing-48.png -------------------------------------------------------------------------------- /support/icons/icons8-pencil-drawing-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-pencil-drawing-96.png -------------------------------------------------------------------------------- /support/icons/icons8-question-mark-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-question-mark-48.png -------------------------------------------------------------------------------- /support/icons/icons8-question-mark-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-question-mark-96.png -------------------------------------------------------------------------------- /support/icons/icons8-remove-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-remove-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-remove-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-remove-48.png -------------------------------------------------------------------------------- /support/icons/icons8-remove-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-remove-96.png -------------------------------------------------------------------------------- /support/icons/icons8-reset-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-reset-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-reset-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-reset-32.png -------------------------------------------------------------------------------- /support/icons/icons8-reset-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-reset-32x32.png -------------------------------------------------------------------------------- /support/icons/icons8-reset-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-reset-48.png -------------------------------------------------------------------------------- /support/icons/icons8-save-112x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-save-112x24.png -------------------------------------------------------------------------------- /support/icons/icons8-save-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-save-48.png -------------------------------------------------------------------------------- /support/icons/icons8-save-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-save-96.png -------------------------------------------------------------------------------- /support/icons/icons8-settings-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-settings-48.png -------------------------------------------------------------------------------- /support/icons/icons8-settings-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-settings-96.png -------------------------------------------------------------------------------- /support/icons/icons8-square-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-square-16.png -------------------------------------------------------------------------------- /support/icons/icons8-switch-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-switch-20.png -------------------------------------------------------------------------------- /support/icons/icons8-task-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/icons8-task-20.png -------------------------------------------------------------------------------- /support/icons/rafi-clock-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/rafi-clock-128.png -------------------------------------------------------------------------------- /support/icons/rafi-clock-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/rafi-clock-256.png -------------------------------------------------------------------------------- /support/icons/rafi-clock-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/rafi-clock-32.png -------------------------------------------------------------------------------- /support/icons/rafi-clock-64.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/rafi-clock-64.ico -------------------------------------------------------------------------------- /support/icons/rafi-clock-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almostearthling/when-command/bdbc8c37faccf02c734c4728dd87b357dd6342d6/support/icons/rafi-clock-64.png -------------------------------------------------------------------------------- /when/__init__.py: -------------------------------------------------------------------------------- 1 | # main module 2 | -------------------------------------------------------------------------------- /when/when_bg.pyw: -------------------------------------------------------------------------------- 1 | # when consoleless launcher for Windows 2 | 3 | import sys, os, subprocess 4 | from .when import main 5 | 6 | 7 | def run_bg(): 8 | if sys.platform.startswith("win"): 9 | if os.path.basename(sys.argv[0]) != os.path.basename(sys.executable): 10 | pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") 11 | args = [ 12 | "cmd.exe", 13 | "/c", 14 | "start", 15 | "", 16 | "/B", 17 | pythonw, 18 | "-m", 19 | "when.when", 20 | ] + list(sys.argv[1:]) 21 | subprocess.run(args) 22 | else: 23 | main() 24 | 25 | 26 | # end. 27 | --------------------------------------------------------------------------------