├── Changelog.md ├── LICENSE ├── README.md ├── __init__.py ├── documentation_images ├── Introd.png ├── coffee.png ├── intro.png ├── maptasker_logo_dark.png └── maptasker_logo_light.png ├── main.py ├── maptasker ├── __init__.py ├── assets │ ├── bmc-logo-no-background.png │ ├── color_wheel.png │ ├── icons │ │ └── arrow.png │ ├── json │ │ ├── arg_specs.json │ │ ├── category_descriptions.json │ │ └── task_all_actions.json │ ├── maptasker_logo_dark.png │ ├── maptasker_logo_light.png │ └── target.png └── src │ ├── __init__.py │ ├── acmerge.py │ ├── actargs.py │ ├── action.py │ ├── actionc.py │ ├── actiond.py │ ├── actione.py │ ├── actionr.py │ ├── actiont.py │ ├── addcss.py │ ├── caveats.py │ ├── clip.py │ ├── colors.py │ ├── colrmode.py │ ├── condition.py │ ├── config.py │ ├── cria.py │ ├── ctk_color_picker.py │ ├── debug.py │ ├── deprecate.py │ ├── diagcnst.py │ ├── diagram.py │ ├── diagutil.py │ ├── dirout.py │ ├── error.py │ ├── fonts.py │ ├── format.py │ ├── frontmtr.py │ ├── getbakup.py │ ├── getids.py │ ├── getputer.py │ ├── globalvr.py │ ├── guimap.py │ ├── guiutils.py │ ├── guiwins.py │ ├── initparg.py │ ├── kidapp.py │ ├── lineout.py │ ├── mailer.py │ ├── mapai.py │ ├── mapit.py │ ├── maputils.py │ ├── nameattr.py │ ├── outline.py │ ├── parsearg.py │ ├── prefers.py │ ├── primitem.py │ ├── priority.py │ ├── proclist.py │ ├── profiles.py │ ├── progargs.py │ ├── proginit.py │ ├── projects.py │ ├── property.py │ ├── runcli.py │ ├── rungui.py │ ├── scenes.py │ ├── servicec.py │ ├── share.py │ ├── shelsort.py │ ├── sysconst.py │ ├── taskactn.py │ ├── taskerd.py │ ├── taskflag.py │ ├── tasks.py │ ├── taskuniq.py │ ├── twisty.py │ ├── userhelp.py │ ├── userintr.py │ └── xmldata.py ├── maptasker_changelog.json ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── sample.prj.xml └── tests ├── __init__.py └── run_test.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Michael Rubin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/__init__.py -------------------------------------------------------------------------------- /documentation_images/Introd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/documentation_images/Introd.png -------------------------------------------------------------------------------- /documentation_images/coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/documentation_images/coffee.png -------------------------------------------------------------------------------- /documentation_images/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/documentation_images/intro.png -------------------------------------------------------------------------------- /documentation_images/maptasker_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/documentation_images/maptasker_logo_dark.png -------------------------------------------------------------------------------- /documentation_images/maptasker_logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/documentation_images/maptasker_logo_light.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # ########################################################################################## # 4 | # # 5 | # Main: MapTasker entry point # 6 | # # 7 | # GNU General Public License v3.0 # 8 | # Permissions of this strong copyleft license are conditioned on making available # 9 | # complete source code of licensed works and modifications, which include larger works # 10 | # using a licensed work, under the same license. Copyright and license notices must be # 11 | # preserved. Contributors provide an express grant of patent rights. # 12 | # # 13 | # ########################################################################################## # 14 | 15 | from maptasker.src import mapit 16 | 17 | 18 | def main(): 19 | """ 20 | Kick off the main program: mapit.pypwd 21 | 22 | """ 23 | 24 | # Call the core function passing an empty filename 25 | return_code = mapit.mapit_all("") 26 | exit(return_code) 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /maptasker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/__init__.py -------------------------------------------------------------------------------- /maptasker/assets/bmc-logo-no-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/bmc-logo-no-background.png -------------------------------------------------------------------------------- /maptasker/assets/color_wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/color_wheel.png -------------------------------------------------------------------------------- /maptasker/assets/icons/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/icons/arrow.png -------------------------------------------------------------------------------- /maptasker/assets/json/arg_specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": "Int", 3 | "1": "String", 4 | "2": "App", 5 | "3": "Boolean", 6 | "4": "Icon", 7 | "5": "Bundle", 8 | "6": "Scene" 9 | } -------------------------------------------------------------------------------- /maptasker/assets/json/category_descriptions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": 10, 4 | "name": "Alert" 5 | }, 6 | { 7 | "code": 15, 8 | "name": "App" 9 | }, 10 | { 11 | "code": 20, 12 | "name": "Audio" 13 | }, 14 | { 15 | "code": 35, 16 | "name": "Code" 17 | }, 18 | { 19 | "code": 40, 20 | "name": "Display" 21 | }, 22 | { 23 | "code": 50, 24 | "name": "File" 25 | }, 26 | { 27 | "code": 51, 28 | "name": "Google Drive" 29 | }, 30 | { 31 | "code": 52, 32 | "name": "Image" 33 | }, 34 | { 35 | "code": 55, 36 | "name": "Input" 37 | }, 38 | { 39 | "code": 60, 40 | "name": "Location" 41 | }, 42 | { 43 | "code": 141, 44 | "name": "Matter Home Automation (Experimental)" 45 | }, 46 | { 47 | "code": 65, 48 | "name": "Media" 49 | }, 50 | { 51 | "code": 80, 52 | "name": "Net" 53 | }, 54 | { 55 | "code": 90, 56 | "name": "Phone" 57 | }, 58 | { 59 | "code": 100, 60 | "name": "Plugin" 61 | }, 62 | { 63 | "code": 102, 64 | "name": "Scene" 65 | }, 66 | { 67 | "code": 30, 68 | "name": "Settings" 69 | }, 70 | { 71 | "code": 104, 72 | "name": "System" 73 | }, 74 | { 75 | "code": 105, 76 | "name": "Task" 77 | }, 78 | { 79 | "code": 110, 80 | "name": "Tasker" 81 | }, 82 | { 83 | "code": 120, 84 | "name": "Variables" 85 | }, 86 | { 87 | "code": 125, 88 | "name": "Zoom" 89 | }, 90 | { 91 | "code": 130, 92 | "name": "3rd Party" 93 | } 94 | ] -------------------------------------------------------------------------------- /maptasker/assets/maptasker_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/maptasker_logo_dark.png -------------------------------------------------------------------------------- /maptasker/assets/maptasker_logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/maptasker_logo_light.png -------------------------------------------------------------------------------- /maptasker/assets/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/maptasker/assets/target.png -------------------------------------------------------------------------------- /maptasker/src/__init__.py: -------------------------------------------------------------------------------- 1 | """Code space""" 2 | -------------------------------------------------------------------------------- /maptasker/src/actiond.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Action dictionary functions.""" 3 | 4 | # # 5 | # actiond: Task Action dictionary functions for MapTasker # 6 | # # 7 | from __future__ import annotations 8 | 9 | import contextlib 10 | from typing import TYPE_CHECKING, Any 11 | 12 | import maptasker.src.action as get_action 13 | from maptasker.src.actionc import action_codes 14 | from maptasker.src.sysconst import logger 15 | 16 | if TYPE_CHECKING: 17 | import defusedxml.ElementTree 18 | 19 | IGNORE_ITEMS = ["code", "label", "se", "on", "ListElementItem", "pri", "pin"] 20 | 21 | 22 | # Given a child xml element, determine if it is a boolean of condtion 23 | # add return if in a list 24 | def get_boolean_or_condition( 25 | child: defusedxml.ElementTree, 26 | condition_list: list, 27 | boolean_list: list, 28 | ) -> tuple[list, list]: 29 | """ 30 | Evaluates the condition/boolean and updates the condition_list and boolean_list. 31 | 32 | Args: 33 | child (Element): The XML element to evaluate. 34 | condition_list (list): The list of conditions. 35 | boolean_list (list): The list of booleans. 36 | 37 | Returns: 38 | tuple: A tuple containing the updated condition_list and boolean_list. 39 | """ 40 | 41 | if "bool" in child.tag: 42 | boolean_list.append(child.text.upper()) 43 | elif child.tag == "Condition": 44 | ( 45 | first_string, 46 | the_operation, 47 | second_string, 48 | ) = get_action.evaluate_condition(child) 49 | condition_list.append([first_string, the_operation, second_string]) 50 | return condition_list, boolean_list 51 | 52 | 53 | # Trundle through ConditionList "If" conditions 54 | # Return the list of conditions and list of associated booleans 55 | def process_condition_list( 56 | code_action: defusedxml.ElementTree, 57 | ) -> tuple[list[list[Any]], list[str]]: 58 | """ 59 | Extracts conditions and their associated booleans from the element. 60 | 61 | Args: 62 | code_action (ElementTree): XML element containing conditions. 63 | 64 | Returns: 65 | tuple[list[list[Any]], list[str]]: List of conditions and their associated booleans. 66 | """ 67 | condition_list_str = code_action.find("ConditionList") 68 | if condition_list_str is None: 69 | return [], [] 70 | 71 | condition_list, boolean_list = [], [] 72 | for child in condition_list_str: 73 | condition_list, boolean_list = get_boolean_or_condition(child, condition_list, boolean_list) 74 | 75 | return condition_list, boolean_list 76 | 77 | 78 | # Update the dictionary for the Action code 79 | # This is only called if the action code is already in our master 80 | # dictionary of codes. 81 | def update_action_codes( 82 | action: defusedxml.ElementTree, 83 | the_action_code_plus: defusedxml.ElementTree, 84 | ) -> defusedxml.ElementTree: 85 | """ 86 | Update the dictionary for the Action code 87 | :param action: xml element 88 | :param the_action_code_plus: the Action code with "action type" 89 | (e.g. 861t, t=Task, e=Event, s=State) 90 | :return: nothing 91 | """ 92 | # Update dictionary entry for this code in the format of an output line 93 | # dict = { 'the_code': 94 | # {num_args: num, 95 | # args: arg0, arg1, ... type: Int/Str } 96 | arg_list, type_list, arg_nums = get_action.get_args(action, IGNORE_ITEMS) 97 | arg_count = len(arg_list) 98 | 99 | # Compare this Actions num of args to dictionary's 100 | if arg_count > action_codes[the_action_code_plus].numargs: 101 | with contextlib.suppress(Exception): 102 | action_codes[the_action_code_plus].numargs = arg_count 103 | action_codes[the_action_code_plus].args = arg_nums 104 | action_codes[the_action_code_plus].types = type_list 105 | 106 | logger.debug( 107 | "update_action_codes:" 108 | f" {the_action_code_plus} {action_codes[the_action_code_plus]!s} numargs of {action_codes[the_action_code_plus].numargs} update to {arg_count}: needs to be updated in actionc.py!", 109 | ) 110 | return 111 | 112 | 113 | # Build the dictionary for the Action code. Only called if the action code is not 114 | # in our master dictionary of codes. 115 | def build_new_action_codes( 116 | action: defusedxml.ElementTree, 117 | the_action_code_plus: defusedxml.ElementTree, 118 | ) -> defusedxml.ElementTree: 119 | """ 120 | Build the dictionary for the Action code 121 | :param action: xml element 122 | :param the_action_code_plus: the Action code with "action type" (e.g. 861t, t=Task, e=Event, s=State) 123 | :return: nothing 124 | """ 125 | logger.info(f"...for {the_action_code_plus}") 126 | 127 | # Create a dictionary entry for this code in the format of an output line 128 | # dict = { 'the_code': 129 | # {num_args: num, args: ['arg0', 'arg1', ...], types: ['Str', 'Int', ...] 130 | arg_list, type_list, arg_nums = get_action.get_args(action, IGNORE_ITEMS) 131 | arg_count = len(arg_list) 132 | with contextlib.suppress(Exception): 133 | action_codes[the_action_code_plus].numargs = arg_count 134 | action_codes[the_action_code_plus].args = arg_nums 135 | action_codes[the_action_code_plus].types = type_list 136 | return 137 | 138 | 139 | # Build the dictionary for each Action code 140 | # This is only called if the action code is not already in our master dictionary of codes 141 | # child = pointer to xml 142 | # action = pointer to root xml ( or ) 143 | # adder = empty if . Else it is a Profile condition, and we need to make key unique 144 | def build_action_codes( 145 | action: defusedxml.ElementTree, 146 | child: defusedxml.ElementTree, 147 | ) -> defusedxml.ElementTree: 148 | """ 149 | Build the dictionary for each Action code 150 | We first check if the_action_code_plus is already in action_codes. 151 | If it is, we call the update_action_codes() function. Otherwise, we call the 152 | build_new_action_codes() function followed by some logging and debugging output 153 | (if debug mode is enabled) as before. 154 | 155 | :param action: xml element with Task action's "nnn" 156 | :param child: xml root element of Task action 157 | :return: 158 | """ 159 | # Get the actual dictionary/action code 160 | the_action_code_plus = child.text 161 | if the_action_code_plus in action_codes: 162 | update_action_codes(action, the_action_code_plus) 163 | else: 164 | build_new_action_codes(action, the_action_code_plus) 165 | 166 | return 167 | -------------------------------------------------------------------------------- /maptasker/src/actionr.py: -------------------------------------------------------------------------------- 1 | """Module containing action runner logic.""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # actionr: process Task "Action" and return the result # 7 | # # 8 | 9 | from __future__ import annotations 10 | 11 | import re 12 | from collections import defaultdict 13 | from typing import TYPE_CHECKING 14 | 15 | import maptasker.src.action as get_action 16 | from maptasker.src.actargs import action_args 17 | from maptasker.src.format import format_html 18 | from maptasker.src.primitem import PrimeItems 19 | from maptasker.src.sysconst import ( 20 | DISPLAY_DETAIL_LEVEL_all_tasks, 21 | DISPLAY_DETAIL_LEVEL_all_variables, 22 | pattern0, 23 | pattern1, 24 | pattern2, 25 | pattern3, 26 | pattern4, 27 | pattern11, 28 | pattern12, 29 | ) 30 | 31 | if TYPE_CHECKING: 32 | import defusedxml.ElementTree 33 | 34 | 35 | # Given a list of positional items, return a string in the correct order based 36 | # on position 37 | def get_results_in_arg_order(evaluated_results: dict) -> str: 38 | """ 39 | Get all of the evaluated results into a single list and return results as a string. 40 | :param evaluated_results: a dictionary containing evaluated results 41 | :return: a string containing the evaluated results in order 42 | """ 43 | # Get all of the evaluated results into a single list 44 | result_parts = [] 45 | for key in evaluated_results: 46 | if key not in ("returning_something", "error"): 47 | arg_value = evaluated_results[key]["value"] 48 | if arg_value is not None: 49 | if isinstance(arg_value, str | bool): 50 | result_parts.append(arg_value) 51 | else: 52 | result_parts.append(arg_value["value"]) 53 | # Eliminate empty values 54 | if result_parts and (result_parts[-1] == ", " or result_parts[-1] == ""): 55 | result_parts.pop() 56 | continue 57 | 58 | # Return results as a string 59 | if result_parts: 60 | return ", ".join(map(str, result_parts)) 61 | 62 | return "" 63 | 64 | 65 | # Search for and return all substrings in a string that begin with a percent sign and 66 | # have at least one capitalized letter in the substring. 67 | def find_capitalized_percent_substrings(string: str) -> list: 68 | """ 69 | Searches for and returns all of the occurrences of substrings that begin with a 70 | percent sign and have at least one capitalized letter in the substring. 71 | 72 | Args: 73 | string: A string to search. 74 | 75 | Returns: 76 | A list of all of the occurrences of substrings that begin with a percent sign and 77 | have at least one capitalized letter in the substring. 78 | """ 79 | 80 | # Create a regular expression to match substrings that begin with a percent sign 81 | # and have at least one capitalized letter. 82 | # regex = r".*[A-Z].*" <<< re.compile(regex) in sysconst.py as pattern11 83 | 84 | # Find all words that begin with a percent sign (%). 85 | # percent_list = re.findall("[%]\w+", string) << pattern12 in sysconst.py 86 | 87 | # Create a regular expression to match substrings that begin with a percent sign. 88 | return [word for word in pattern12.findall(string) if re.match(pattern11, word)] 89 | 90 | 91 | # Get the variables from this result and save them in the dictionary. 92 | def get_variables(result: str) -> None: 93 | # Fid all variables with at least one capitalized letter. 94 | """Get all variables with at least one capitalized letter. 95 | Parameters: 96 | - result (str): The string to search for variables. 97 | Returns: 98 | - None: This function does not return anything. 99 | Processing Logic: 100 | - Find all variables with at least one capitalized letter. 101 | - Check if the variable is in the variable dictionary. 102 | - If it is, add the current project name to the list of projects associated with the variable. 103 | - If it is not, add the variable to the variable dictionary with a default value and the current project name. 104 | - If the variable is not found in the dictionary, it is considered inactive.""" 105 | if not PrimeItems.current_project: 106 | return 107 | if variable_list := find_capitalized_percent_substrings(result): 108 | # Go thru list of capitalized percent substrings and see if they are 109 | # in our variable dictionary. If so, then add the project name to the list. 110 | for variable in variable_list: 111 | # Validate that this variable is for the Project we are currently doing. 112 | try: 113 | if primary_variable := PrimeItems.variables[variable]: 114 | if primary_variable["project"] and PrimeItems.current_project not in primary_variable["project"]: 115 | primary_variable["project"].append(PrimeItems.current_project) 116 | elif not primary_variable["project"]: 117 | primary_variable["project"] = [PrimeItems.current_project] 118 | except KeyError: 119 | # Drop here if variable is not in Tasker's variable list (i.e. the xml) 120 | PrimeItems.variables[variable] = { 121 | "value": "(Inactive)", 122 | "project": [PrimeItems.current_project], 123 | "verified": False, 124 | } 125 | 126 | 127 | def fix_config_parameters(s: str, target: str, replacement: str) -> str: 128 | """ 129 | Replaces all occurrences of 'target' in 's' with 'replacement', 130 | except the first occurrence is replaced with ' ' and the last occurrence remains unchanged. 131 | """ 132 | occurrences = [i for i in range(len(s)) if s.startswith(target, i)] 133 | 134 | if len(occurrences) < 2: 135 | return s # If less than 2 occurrences, nothing to replace except maybe the first 136 | 137 | first, *middle, last = occurrences 138 | 139 | # Put it all together 140 | s = s[:first] + " " + s[first + len(target) :].replace(target, replacement) 141 | if s[-2:] == ", ": 142 | # Remove the trailing comma if it exists 143 | s = s[:-2] 144 | 145 | return s 146 | 147 | 148 | # For the given code, save the display_name, required arg list and associated 149 | # type list in dictionary. 150 | # Then evaluate the data against the master dictionary of actions. 151 | def get_action_results( 152 | the_action_code_plus: str, 153 | action_codes: defusedxml.Element, 154 | code_action: defusedxml.Element, 155 | action_type: bool, 156 | ) -> str: 157 | """ 158 | For the given code, save the display_name, required arg list and associated type 159 | list in dictionary. 160 | Then evaluate the data against the master dictionary of actions 161 | :param the_action_code_plus: the code found in for the Action () 162 | plus the type (e.g. "861t", where "t" = Task, "s" = State, "e" = Event) 163 | :param action_codes: The Task acction code dictionary in actionc.py 164 | :param code_action: the xml element 165 | :param action_type: True if this is for a Task, false if for a Condition 166 | :return: the output line containing the Action details 167 | """ 168 | # Setup default dictionary as empty list 169 | result = "" 170 | our_action_code = action_codes[the_action_code_plus] 171 | 172 | # Setup default evaluation results as empty list 173 | evaluated_results = defaultdict(list) 174 | evaluated_results["returning_something"] = False 175 | 176 | program_arguments = PrimeItems.program_arguments 177 | # If just displaying action names or there are no action details, then just 178 | # display the name 179 | if ( 180 | action_codes[the_action_code_plus].args 181 | and program_arguments["display_detail_level"] != DISPLAY_DETAIL_LEVEL_all_tasks 182 | ): 183 | # Process the Task action arguments 184 | evaluated_results = action_args( 185 | the_action_code_plus, 186 | action_codes, 187 | code_action, 188 | evaluated_results, 189 | ) 190 | 191 | # If we have results from evaluation, then go put them in their appropriate order 192 | if evaluated_results["returning_something"]: 193 | # Get the results from the list of evaluations. 194 | result = get_results_in_arg_order(evaluated_results) 195 | 196 | elif evaluated_results["error"]: 197 | result = evaluated_results["error"] 198 | 199 | # Replace '\n' with ', ' if not pretty 200 | if not PrimeItems.program_arguments["pretty"] and "Configuration Parameter(s):" in result: 201 | result = fix_config_parameters(result, "\n", ", ") 202 | 203 | # Clean up the rest of it. Fix brackets, double commas, etc. 204 | if result: 205 | # Replace "<" with "<" 206 | result = pattern3.sub("<", result) 207 | # Replace ">" with ">" 208 | result = pattern4.sub(">", result) 209 | # Replace ", ," with "," 210 | result = pattern1.sub(",", result) 211 | # Replace " ," with "," (loop until all instances are handled) 212 | while pattern2.search(result): 213 | result = pattern2.sub(",", result) 214 | # Catch ",," and replace with "," 215 | result = pattern0.sub(",", result) 216 | # Add non-breaking spaces at the beginning 217 | result = f"  {result}" 218 | 219 | # Process variables if display_detail_level is 4 220 | if program_arguments["display_detail_level"] >= DISPLAY_DETAIL_LEVEL_all_variables: 221 | get_variables(result) 222 | 223 | # Return the properly formatted HTML (if Task) with the Action name and extra stuff 224 | if action_type: # If this is a Task... 225 | return format_html( 226 | "action_name_color", 227 | "", 228 | our_action_code.name, 229 | True, 230 | ) + format_html( 231 | "action_color", 232 | "", 233 | (f"{result}{get_action.get_extra_stuff(code_action, action_type)}"), 234 | False, 235 | ) 236 | 237 | return f"{our_action_code.name}{result}{get_action.get_extra_stuff(code_action, action_type)}" 238 | -------------------------------------------------------------------------------- /maptasker/src/addcss.py: -------------------------------------------------------------------------------- 1 | """Add required formatting CSS to HTML output""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # addcss: Add formatting CSS to output HTML for the colors and font to use # 7 | # # 8 | import contextlib 9 | 10 | from maptasker.src.primitem import PrimeItems 11 | from maptasker.src.sysconst import FONT_FAMILY, SPACE_COUNT1, SPACE_COUNT2, SPACE_COUNT3, FormatLine 12 | 13 | 14 | def add_css() -> None: 15 | """ 16 | Add formatting CSS to output HTML for the colors and font to use. 17 | We must re-add the font each time in case a Tasker element overrides the font. 18 | Args: 19 | None 20 | """ 21 | 22 | # Start the style css 23 | PrimeItems.output_lines.add_line_to_output( 24 | 5, 25 | '\n", FormatLine.dont_format_line) 64 | -------------------------------------------------------------------------------- /maptasker/src/caveats.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # caveats: display program caveats # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | 8 | from maptasker.src.format import format_html 9 | from maptasker.src.primitem import PrimeItems 10 | from maptasker.src.sysconst import FormatLine 11 | 12 | 13 | def display_caveats() -> None: 14 | """ 15 | Output the program caveats at the very end 16 | Inputs: 17 | - None 18 | Outputs: 19 | - None 20 | """ 21 | 22 | caveats = [ 23 | format_html( 24 | "trailing_comments_color", 25 | "", 26 | "CAVEATS:
", 27 | False, 28 | ), 29 | ( 30 | "- This has only been tested on my own backup.xml file." 31 | " For problems, report them on https://github.com/mctinker/Map-Tasker/issues .\n" 32 | ), 33 | ("- Tasks that are identified as '(Unnamed)' have no name and are considered Anonymous.\n"), 34 | ( 35 | '- All attempts are made to retain embedded HTML (e.g. color=...>") in Tasker' 36 | " fields, but is stripped out of Action labels and TaskerNet comments.\n" 37 | ), 38 | ( 39 | "- Profile names starting with '*' are anonymous/unnamed, and the name consists of" 40 | " the Profile conditions.\n" 41 | ), 42 | ( 43 | "- Task names that consist of the first action that has embed html will have all '<' and '>' characters" 44 | " converted to '{' and '}' respectively.\n" 45 | ), 46 | ] 47 | 48 | # Let 'em know about Google API key 49 | if PrimeItems.program_arguments["preferences"]: 50 | caveats.append( 51 | "- Your Google API key is displayed in the Tasker preferences!\n", 52 | ) 53 | 54 | if PrimeItems.program_arguments["display_detail_level"] > 0: # Caveat about Actions 55 | caveats.append( 56 | "- Most but not all Task actions have been mapped and will display as such." 57 | " Likewise for Profile conditions and Plug-ins.\n", 58 | ) 59 | 60 | if ( 61 | PrimeItems.program_arguments["display_detail_level"] == 0 62 | ): # Caveat about -d0 option and 1st Action for unnamed Tasks 63 | caveats.append( 64 | '- For option -d0, Tasks that are identified as "Unnamed/Anonymous" will' 65 | " have their first Action only listed....\n just like Tasker does.\n", 66 | ) 67 | 68 | if PrimeItems.program_arguments["display_detail_level"] > 2: # Caveat about labels being stripped of html 69 | caveats.extend( 70 | ("- Task labels have been stripped of all html to avoid output formatting issues.\n",), 71 | ) 72 | 73 | if ( 74 | PrimeItems.program_arguments["display_detail_level"] == 4 75 | ): # Caveat about -d0 option and 1st Action for unnamed Tasks 76 | caveats.extend( 77 | ( 78 | "- Inactive variables are global variables used in a Task which has not been run/used.\n", 79 | "- Unreference variables are global variables that may have been used in the past, but are not currently referenced (e.g. the Task's Profile is disabled).\n", 80 | ), 81 | ) 82 | 83 | # Start the output 84 | PrimeItems.output_lines.add_line_to_output(0, "
", FormatLine.dont_format_line) 85 | 86 | # Output all caveats 87 | for caveat in caveats: 88 | PrimeItems.output_lines.add_line_to_output( 89 | 0, 90 | caveat, 91 | ["", "trailing_comments_color", FormatLine.add_end_span], 92 | ) 93 | -------------------------------------------------------------------------------- /maptasker/src/colors.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # colors: get and set the program colors # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | import string 8 | 9 | from maptasker.src.error import error_handler 10 | from maptasker.src.primitem import PrimeItems 11 | from maptasker.src.sysconst import TYPES_OF_COLOR_NAMES, logger 12 | 13 | 14 | # Given a list [x,y,z] , print as x y z 15 | def print_list(title: str, parse_items: list) -> None: 16 | """ 17 | Given a list [x,y,z] , print as x y z 18 | :param title: the heading to output 19 | :param parse_items: a list of items separated by commas 20 | :return: nothing 21 | """ 22 | line_out = "" 23 | seperator = ", " 24 | list_length = len(parse_items) - 1 25 | if title: 26 | print(title) 27 | for item in parse_items: 28 | if parse_items.index(item) == list_length: # Last item in list? 29 | seperator = "" 30 | line_out = f"{line_out}{item}{seperator}" 31 | print(line_out) 32 | 33 | 34 | # Validate the color name provided. If color name is 'h', simply display all the colors 35 | def validate_color(the_color: str) -> object: 36 | """ 37 | Validate the color name provided. 38 | If color name is 'h', simply display all the colors and exit. 39 | IF the color passed in is hexidecimal, simply return True 40 | Else return the color if it is valid 41 | param: the_color: the color as a hexidecimal number, "h" for help, or 42 | a color name 43 | :rtype: object: True if a hexadecimal color was provided, or the color name 44 | """ 45 | red_color_names = [ 46 | "IndianRed", 47 | "LightCoral", 48 | "Salmon", 49 | "DarkSalmon", 50 | "LightSalmon", 51 | "Crimson", 52 | "Red", 53 | "FireBrick", 54 | "DarkRed", 55 | ] 56 | pink_color_names = [ 57 | "Pink", 58 | "LightPink", 59 | "HotPink", 60 | "DeepPink", 61 | "MediumVioletRed", 62 | "PaleVioletRed", 63 | ] 64 | orange_color_names = [ 65 | "LightSalmon", 66 | "Coral", 67 | "Tomato", 68 | "OrangeRed", 69 | "DarkOrange", 70 | "Orange", 71 | ] 72 | yellow_color_names = [ 73 | "Gold", 74 | "Yellow", 75 | "LightYellow", 76 | "LemonChiffon", 77 | "LightGoldenrodYellow", 78 | "PapayaWhip", 79 | "Moccasin", 80 | "PeachPuff", 81 | "PaleGoldenrod", 82 | "Khaki", 83 | "DarkKhaki", 84 | ] 85 | purple_color_names = [ 86 | "Lavender", 87 | "Thistle", 88 | "Plum", 89 | "Violet", 90 | "Orchid", 91 | "Fuchsia", 92 | "Magenta", 93 | "MediumOrchid", 94 | "MediumPurple", 95 | "RebeccaPurple", 96 | "BlueViolet", 97 | "DarkViolet", 98 | "DarkOrchid", 99 | "DarkMagenta", 100 | "Purple", 101 | "Indigo", 102 | "SlateBlue", 103 | "DarkSlateBlue", 104 | "MediumSlateBlue", 105 | ] 106 | green_color_names = [ 107 | "GreenYellow", 108 | "Chartreuse", 109 | "LawnGreen", 110 | "Lime", 111 | "LimeGreen", 112 | "PaleGreen", 113 | "LightGreen", 114 | "MediumSpringGreen", 115 | "SpringGreen", 116 | "MediumSeaGreen", 117 | "SeaGreen", 118 | "ForestGreen", 119 | "Green", 120 | "DarkGreen", 121 | "YellowGreen", 122 | "OliveDrab", 123 | "Olive", 124 | "DarkOliveGreen", 125 | "MediumAquamarine", 126 | "DarkSeaGreen", 127 | "LightSeaGreen", 128 | "DarkCyan", 129 | "Teal", 130 | ] 131 | blue_color_names = [ 132 | "Aqua", 133 | "Cyan", 134 | "LightCyan", 135 | "PaleTurquoise", 136 | "Aquamarine", 137 | "Turquoise", 138 | "MediumTurquoise", 139 | "DarkTurquoise", 140 | "CadetBlue", 141 | "SteelBlue", 142 | "LightSteelBlue", 143 | "PowderBlue", 144 | "LightBlue", 145 | "SkyBlue", 146 | "LightSkyBlue", 147 | "DeepSkyBlue", 148 | "DodgerBlue", 149 | "CornflowerBlue", 150 | "MediumSlateBlue", 151 | "RoyalBlue", 152 | "Blue", 153 | "MediumBlue", 154 | "DarkBlue", 155 | "Navy", 156 | "MidnightBlue", 157 | ] 158 | brown_color_names = [ 159 | "Cornsilk", 160 | "BlanchedAlmond", 161 | "Bisque", 162 | "NavajoWhite", 163 | "Wheat", 164 | "BurlyWood", 165 | "Tan", 166 | "RosyBrown", 167 | "SandyBrown", 168 | "Goldenrod", 169 | "DarkGoldenrod", 170 | "Peru", 171 | "Chocolate", 172 | "SaddleBrown", 173 | "Sienna", 174 | "Brown", 175 | "Maroon", 176 | ] 177 | white_color_names = [ 178 | "White", 179 | "Snow", 180 | "HoneyDew", 181 | "MintCream", 182 | "Azure", 183 | "AliceBlue", 184 | "GhostWhite", 185 | "WhiteSmoke", 186 | "SeaShell", 187 | "Beige", 188 | "OldLace", 189 | "FloralWhite", 190 | "Ivory", 191 | "AntiqueWhite", 192 | "Linen", 193 | "LavenderBlush", 194 | "MistyRose", 195 | ] 196 | gray_color_names = [ 197 | "Gainsboro", 198 | "LightGray", 199 | "Silver", 200 | "DarkGray", 201 | "Gray", 202 | "DimGray", 203 | "LightSlateGray", 204 | "SlateGray", 205 | "DarkSlateGray", 206 | "Black", 207 | ] 208 | any_color = ["hex value. Example 'db29ff'"] 209 | 210 | all_colors = ( 211 | red_color_names 212 | + pink_color_names 213 | + orange_color_names 214 | + yellow_color_names 215 | + purple_color_names 216 | + green_color_names 217 | + blue_color_names 218 | + brown_color_names 219 | + white_color_names 220 | + gray_color_names 221 | + any_color 222 | ) 223 | 224 | if the_color != "h": 225 | logger.debug(the_color in all_colors) 226 | # Test for hexdigits 227 | if all(c in string.hexdigits for c in the_color): 228 | return True 229 | # Return True/False based on whether it is in our named colors 230 | return the_color in all_colors 231 | print_list("\nRed color names:", red_color_names) 232 | print_list("\nPink color names:", pink_color_names) 233 | print_list("\nOrange color names:", orange_color_names) 234 | print_list("\nYellow color names:", yellow_color_names) 235 | print_list("\nPurple color names:", purple_color_names) 236 | print_list("\nGreen color names:", green_color_names) 237 | print_list("\nBlue color names:", blue_color_names) 238 | print_list("\nBrown color names:", brown_color_names) 239 | print_list("\nWhite color names:", white_color_names) 240 | print_list("\nGray color names:", gray_color_names) 241 | print_list("\nAny color:", any_color) 242 | exit(0) 243 | 244 | 245 | # Get the runtime option for a color change and set it 246 | def get_and_set_the_color(the_arg: str) -> None: 247 | """ 248 | Get the runtime option for a color change and set it 249 | :param the_arg: the color runtime argument (e.g. "cProfile=Blue" or 250 | "cProfile Blue") 251 | :return: nothing 252 | """ 253 | the_color_option = the_arg[2:].split("=") 254 | color_type = the_color_option[0] 255 | if len(the_color_option) < 2: # Do we have the second parameter? 256 | error_handler(f"{the_arg} has an invalid 'color'. See the help (-ch)!", 7) 257 | if color_type not in TYPES_OF_COLOR_NAMES: 258 | error_handler( 259 | (f"{color_type} is an invalid type for 'color'. See the help (-h)! Exit code 7"), 260 | 7, 261 | ) 262 | desired_color = the_color_option[1] 263 | logger.debug(f" desired_color:{desired_color}") 264 | if validate_color(desired_color): # If the color provided is valid... 265 | # match color_type: 266 | PrimeItems.colors_to_use[TYPES_OF_COLOR_NAMES[color_type]] = desired_color 267 | else: 268 | error_handler( 269 | (f"MapTasker...invalid color specified: {desired_color} for 'c{the_color_option[0]}'!"), 270 | 7, 271 | ) 272 | -------------------------------------------------------------------------------- /maptasker/src/colrmode.py: -------------------------------------------------------------------------------- 1 | """Assign Default Colors""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # colrmode: set the program colors based on color mode: dark or light # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | import darkdetect 10 | 11 | 12 | def set_color_mode(appearance_mode: str) -> dict: 13 | """ 14 | Given the color mode to use (dark or light), set the colors appropriately 15 | :param appearance_mode: color mode: dark or light 16 | :return new colormap of colors to use in the output 17 | """ 18 | # Deal with "System" color mode 19 | mode = ("dark" if darkdetect.isDark() else "light") if appearance_mode == "system" else appearance_mode 20 | 21 | # Now set the colors to use based on the appearance mode 22 | if mode == "dark": 23 | return { 24 | "project_color": "White", 25 | "profile_color": "Aqua", 26 | "disabled_profile_color": "Red", 27 | "launcher_task_color": "GreenYellow", 28 | "task_color": "Yellow", 29 | "unknown_task_color": "Red", 30 | "scene_color": "Lime", 31 | "action_name_color": "Gold", 32 | "action_color": "DarkOrange", 33 | "action_label_color": "Magenta", 34 | "action_condition_color": "PapayaWhip", 35 | "disabled_action_color": "Crimson", 36 | "profile_condition_color": "LightGrey", 37 | "background_color": "222623", 38 | "trailing_comments_color": "PeachPuff", 39 | "taskernet_color": "LightPink", 40 | "preferences_color": "PeachPuff", 41 | "highlight_color": "DarkTurquoise", 42 | "heading_color": "LimeGreen", 43 | } 44 | 45 | return { 46 | "project_color": "Black", 47 | "profile_color": "DarkBlue", 48 | "disabled_profile_color": "DarkRed", 49 | "launcher_task_color": "LawnGreen", 50 | "task_color": "DarkGreen", 51 | "unknown_task_color": "MediumVioletRed", 52 | "scene_color": "Purple", 53 | "action_color": "DarkSlateGray", 54 | "action_name_color": "Indigo", 55 | "action_label_color": "MediumOrchid", 56 | "action_condition_color": "Brown", 57 | "disabled_action_color": "IndianRed", 58 | "profile_condition_color": "DarkSlateGray", 59 | "background_color": "Lavender", 60 | "trailing_comments_color": "Tomato", 61 | "taskernet_color": "RoyalBlue", 62 | "preferences_color": "DodgerBlue", 63 | "highlight_color": "Yellow", 64 | "heading_color": "DarkSlateGray", 65 | } 66 | -------------------------------------------------------------------------------- /maptasker/src/config.py: -------------------------------------------------------------------------------- 1 | """User Modifiable Configutration File""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # config: Configuration file for MapTasker # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | 10 | # START User-modifiable global constants 11 | 12 | # Define the maximum number of Action lines to continue to avoid runaway for the display 13 | # of huge binary files 14 | CONTINUE_LIMIT = 75 15 | 16 | # Monospace fonts work best for if/then/else/end indentation alignment 17 | OUTPUT_FONT = "Courier" # OS X Default monospace font 18 | 19 | # Graphical User Interface (True) vs. CLI Command Line Interface (False) 20 | GUI = False 21 | # Light vs Dark Mode (refer to colrmode.py to hardcode the output colors) 22 | DARK_MODE = True 23 | 24 | # 25 | # Set up to fetch the backup file from Android device running the Tasker server. 26 | # 27 | # In addition, the Tasker HTTP sample Project must be installed on the Android device, 28 | # found at... 29 | # (https://shorturl.at/bwCD4), 30 | # and the server must be active on the Android device. 31 | 32 | # This is the HTTP IP address of the Android device from which to fetch the backup. 33 | # Example: ANDROID_IPADDR = "192.168.0.210" 34 | 35 | ANDROID_IPADDR = "" 36 | 37 | # This is the port number for the Android device from which to fetch the backup, 38 | # and is specified in the Tasker HTTP Server Example project notification. 39 | # From notification: HTTP Server Info {"device_name":"http://192.168.0.49:1821"} 40 | # Example: ANDROID_PORT = "1821" 41 | 42 | ANDROID_PORT = "" 43 | 44 | # This is the location on the Android device from which to pull the backup file 45 | # Example: ANDROID_FILE = "/Tasker/configs/user/backup.xml" 46 | 47 | ANDROID_FILE = "" 48 | 49 | # This is used as the default display detail level. It does not override the runtime option. 50 | # This value is used if the runtime option is not set. 51 | DEFAULT_DISPLAY_DETAIL_LEVEL = 5 52 | 53 | # Ai Analysis prompt...This will be proceeded by 'Given the following (Project/Profile/Task) in Tasker, ' 54 | AI_PROMPT = "suggest improvements for performance and readability:" 55 | 56 | # END User-modifiable global constants 57 | -------------------------------------------------------------------------------- /maptasker/src/cria.py: -------------------------------------------------------------------------------- 1 | """_Modified cria to fix bug: find process function: 2 | if process_name.lower() in proc.name().lower(): 3 | ...changed to... 4 | if process_name.lower() in proc.name().lower() and proc.info["cmdline"] is not None: 5 | """ 6 | 7 | import atexit 8 | import subprocess 9 | import time 10 | from contextlib import ContextDecorator 11 | from typing import Optional 12 | 13 | import httpx 14 | import ollama 15 | import psutil 16 | from ollama._client import Client as OllamaClient 17 | 18 | from maptasker.src.primitem import PrimeItems 19 | 20 | DEFAULT_MODEL = "llama3.1:8b" 21 | DEFAULT_MESSAGE_HISTORY = [{"role": "system", "content": "You are a helpful AI assistant."}] 22 | 23 | 24 | class Client(OllamaClient): 25 | def chat_stream(self, messages, **kwargs): 26 | model = self.model 27 | ai = ollama 28 | 29 | response = "" 30 | self.running = True 31 | 32 | try: 33 | for chunk in ai.chat(model=model, messages=messages, stream=True, **kwargs): 34 | if self.stop_stream: 35 | if self.allow_interruption: 36 | messages.append({"role": "assistant", "content": response}) 37 | self.running = False 38 | return 39 | content = chunk["message"]["content"] 40 | response += content 41 | yield content 42 | except ollama.ResponseError as e: 43 | error_msg = f"Error in chat_stream: {e.error}" 44 | self.messages = [{"role": "assistant", "content": error_msg}] 45 | return 46 | 47 | self.running = False 48 | 49 | messages.append({"role": "assistant", "content": response}) 50 | self.messages = messages 51 | 52 | stop_stream = False 53 | 54 | def stop(self): 55 | if self.running: 56 | self.stop_stream = True 57 | else: 58 | raise ValueError("No active chat stream to stop.") 59 | 60 | def chat( 61 | self, 62 | prompt: Optional[str] = None, 63 | messages: Optional[list] = DEFAULT_MESSAGE_HISTORY, 64 | stream: Optional[bool] = True, 65 | **kwargs, 66 | ) -> str: 67 | model = self.model 68 | ai = ollama 69 | 70 | if not prompt and not messages: 71 | raise ValueError("You must pass in a prompt.") 72 | 73 | if messages == DEFAULT_MESSAGE_HISTORY: 74 | messages = getattr( 75 | self, 76 | "messages", 77 | messages, 78 | ) 79 | 80 | if prompt: 81 | messages.append({"role": "user", "content": prompt}) 82 | 83 | if stream: 84 | return self.chat_stream(messages, **kwargs) 85 | 86 | chunk = ai.chat(model=model, messages=messages, stream=False, **kwargs) 87 | response = "".join(chunk["message"]["content"]) 88 | 89 | messages.append({"role": "assistant", "content": response}) 90 | self.messages = messages 91 | 92 | return response 93 | 94 | def generate_stream(self, prompt, **kwargs): 95 | model = self.model 96 | ai = ollama 97 | 98 | response = "" 99 | self.running = True 100 | 101 | for chunk in ai.generate(model=model, prompt=prompt, stream=True, **kwargs): 102 | if self.stop_stream: 103 | self.running = False 104 | return 105 | content = chunk["response"] 106 | response += content 107 | yield content 108 | 109 | self.running = False 110 | 111 | def generate(self, prompt: str, stream: Optional[bool] = True, **kwargs) -> str: 112 | model = self.model 113 | ai = ollama 114 | 115 | if stream: 116 | return self.generate_stream(prompt) 117 | 118 | chunk = ai.generate(model=model, prompt=prompt, stream=False, **kwargs) 119 | response = chunk["response"] 120 | 121 | return response 122 | 123 | def clear(self): 124 | self.messages = [{"role": "system", "content": "You are a helpful AI assistant."}] 125 | 126 | 127 | def check_models(model, silence_output): 128 | model_list = ollama.list().get("models", []) 129 | for m in model_list: 130 | m_name = m.get("name", "") 131 | if m_name == model: 132 | return model 133 | if model in m_name: 134 | m_without_version = next(iter(m_name.split(":")), "") 135 | if model == m_without_version: 136 | if not silence_output: 137 | print(f"LLM model found, running {m_name}...") 138 | return m_name 139 | if not silence_output: 140 | print(f"LLM partial match found, running {m_name}...") 141 | return m_name 142 | model_match = next((True if m.get("name") == model else False for m in model_list), False) 143 | if model_match: 144 | return model 145 | 146 | if not silence_output: 147 | print(f"LLM model not found, searching '{model}'...") 148 | 149 | try: 150 | progress = ollama.pull(model, stream=True) 151 | print(f"LLM model {model} found, downloading... (this will probably take a while)") 152 | if not silence_output: 153 | for chunk in progress: 154 | print(chunk) 155 | print(f"'{model}' downloaded, starting processes.") 156 | return model 157 | except Exception as e: 158 | print(e) 159 | # Model not found! 160 | PrimeItems.error_code = 1 161 | PrimeItems.error_msg = f"Invalid model {model} passed. See the model library here: https://ollama.com/library" 162 | return None 163 | # raise ValueError("Invalid model passed. See the model library here: https://ollama.com/library") 164 | 165 | 166 | def find_process(command, process_name="ollama"): 167 | process = None 168 | for proc in psutil.process_iter(attrs=["cmdline"]): 169 | try: 170 | if process_name.lower() in proc.name().lower() and proc.info["cmdline"] is not None: 171 | proc_command = proc.info["cmdline"] 172 | if proc_command[: len(command)] != command: 173 | continue 174 | process = psutil.Process(pid=proc.pid) 175 | break 176 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 177 | pass 178 | return process 179 | 180 | 181 | class Cria(Client): 182 | def __init__( 183 | self, 184 | model: Optional[str] = DEFAULT_MODEL, 185 | standalone: Optional[bool] = False, 186 | run_subprocess: Optional[bool] = False, 187 | capture_output: Optional[bool] = False, 188 | allow_interruption: Optional[bool] = True, 189 | silence_output: Optional[bool] = False, 190 | close_on_exit: Optional[bool] = True, 191 | ) -> None: 192 | self.run_subprocess = run_subprocess 193 | self.capture_output = capture_output 194 | self.silence_output = silence_output 195 | self.close_on_exit = close_on_exit 196 | self.allow_interruption = allow_interruption 197 | 198 | ollama_process = find_process(["ollama", "serve"]) 199 | self.ollama_process = ollama_process 200 | 201 | if ollama_process and run_subprocess: 202 | self.ollama_process.kill() 203 | 204 | try: 205 | ollama.list() 206 | except (httpx.ConnectError, httpx.ReadError): 207 | ollama_process = None 208 | 209 | if not ollama_process: 210 | ollama_stdout = subprocess.PIPE if capture_output else subprocess.DEVNULL 211 | ollama_stderr = subprocess.PIPE if capture_output else subprocess.DEVNULL 212 | try: 213 | self.ollama_subrprocess = subprocess.Popen( 214 | ["ollama", "serve"], 215 | stdout=ollama_stdout, 216 | stderr=ollama_stderr, 217 | ) 218 | except FileNotFoundError: 219 | raise FileNotFoundError( 220 | "Ollama is not installed, please install ollama from 'https://ollama.com/download'", 221 | ) 222 | retries = 10 223 | while retries: 224 | try: 225 | ollama.list() 226 | break 227 | except (httpx.ConnectError, httpx.ReadError): 228 | time.sleep(2) 229 | retries -= 1 230 | else: 231 | self.ollama_subrprocess = None 232 | 233 | self.model = check_models(model, silence_output) 234 | 235 | if not standalone: 236 | self.llm = find_process(["ollama", "run", self.model]) 237 | 238 | if run_subprocess and self.llm: 239 | self.llm.kill() 240 | self.llm = None 241 | 242 | if not self.llm: 243 | self.llm = subprocess.Popen( 244 | ["ollama", "run", self.model], 245 | stdout=subprocess.DEVNULL, 246 | stderr=subprocess.DEVNULL, 247 | ) 248 | 249 | if close_on_exit and self.ollama_subrprocess: 250 | atexit.register(lambda: self.ollama_subrprocess.kill()) 251 | 252 | if close_on_exit and not standalone: 253 | atexit.register(lambda: self.llm.kill()) 254 | 255 | messages = DEFAULT_MESSAGE_HISTORY 256 | 257 | def output(self): 258 | ollama_subprocess = self.ollama_subrprocess 259 | if not ollama_subprocess: 260 | raise ValueError( 261 | "Ollama is not running as a subprocess, you must pass run_subprocess as True to capture output.", 262 | ) 263 | if not self.capture_output: 264 | raise ValueError("You must pass in capture_ouput as True to capture output.") 265 | 266 | return iter(c for c in iter(lambda: ollama_subprocess.stdout.read(1), b"")) 267 | 268 | def close(self): 269 | llm = self.llm 270 | llm.kill() 271 | 272 | 273 | class Model(Cria, ContextDecorator): 274 | def __init__( 275 | self, 276 | model: Optional[str] = DEFAULT_MODEL, 277 | run_attached: Optional[bool] = False, 278 | run_subprocess: Optional[bool] = False, 279 | allow_interruption: Optional[bool] = True, 280 | capture_output: Optional[bool] = False, 281 | silence_output: Optional[bool] = False, 282 | close_on_exit: Optional[bool] = True, 283 | ) -> None: 284 | super().__init__( 285 | model=model, 286 | capture_output=capture_output, 287 | run_subprocess=False, 288 | standalone=True, 289 | close_on_exit=close_on_exit, 290 | ) 291 | 292 | self.capture_output = capture_output 293 | self.allow_interruption = allow_interruption 294 | self.silence_output = silence_output 295 | self.close_on_exit = close_on_exit 296 | 297 | self.model = check_models(model, silence_output) 298 | if self.model is None: 299 | return 300 | 301 | if run_attached and run_subprocess: 302 | raise ValueError("You cannot run attach to an LLM and run it as a subprocess at the same time.") 303 | 304 | if not run_attached: 305 | llm_stdout = subprocess.PIPE if capture_output else subprocess.DEVNULL 306 | llm_stderr = subprocess.PIPE if capture_output else subprocess.DEVNULL 307 | self.llm = subprocess.Popen(["ollama", "run", self.model], stdout=llm_stdout, stderr=llm_stderr) 308 | else: 309 | self.llm = find_process(["ollama", "run", self.model]) 310 | 311 | if self.llm and run_subprocess: 312 | self.llm.kill() 313 | 314 | llm_stdout = subprocess.PIPE if capture_output else subprocess.DEVNULL 315 | llm_stderr = subprocess.PIPE if capture_output else subprocess.DEVNULL 316 | self.llm = subprocess.Popen(["ollama", "run", self.model], stdout=llm_stdout, stderr=llm_stderr) 317 | 318 | if close_on_exit: 319 | atexit.register(lambda: self.llm.kill()) 320 | 321 | def capture_output(self): 322 | if not self.capture_output: 323 | raise ValueError("You must pass in capture_ouput as True to capture output.") 324 | 325 | return iter(lambda: self.llm.stdout.read(1), "") 326 | 327 | def __enter__(self): 328 | return self 329 | 330 | def __exit__(self, *exc): 331 | llm = self.llm 332 | llm.kill() 333 | -------------------------------------------------------------------------------- /maptasker/src/ctk_color_picker.py: -------------------------------------------------------------------------------- 1 | # CTk Color Picker for customtkinter 2 | # Original Author: Akash Bora (Akascape) 3 | # Contributers: Victor Vimbert-Guerlais (helloHackYnow) 4 | ## ruff: noqa 5 | 6 | import math 7 | import os 8 | import tkinter as tk 9 | 10 | import customtkinter 11 | from PIL import Image, ImageTk 12 | 13 | from maptasker.src.primitem import PrimeItems 14 | 15 | PATH = os.path.dirname(os.path.realpath(__file__)) 16 | 17 | 18 | class AskColor(customtkinter.CTkToplevel): 19 | def __init__( 20 | self, 21 | width: int = 300, 22 | title: str = "Choose Color", 23 | initial_color: str = None, 24 | bg_color: str = None, 25 | fg_color: str = None, 26 | button_color: str = None, 27 | button_hover_color: str = None, 28 | text: str = "OK", 29 | corner_radius: int = 24, 30 | slider_border: int = 1, 31 | **button_kwargs, 32 | ): 33 | super().__init__() 34 | 35 | self.title(title) 36 | WIDTH = width if width >= 200 else 200 37 | HEIGHT = WIDTH + 150 38 | self.image_dimension = self._apply_window_scaling(WIDTH - 100) 39 | self.target_dimension = self._apply_window_scaling(20) 40 | 41 | self.maxsize(WIDTH, HEIGHT) 42 | self.minsize(WIDTH, HEIGHT) 43 | # Position the widget 44 | try: 45 | self.geometry(self.master.color_window_position) 46 | except (AttributeError, TypeError): 47 | self.geometry(f"{WIDTH}x{HEIGHT}") 48 | self.resizable(width=False, height=False) 49 | self.transient(self.master) 50 | self.lift() 51 | # Temporary fix, since a ReRun will not show a Color Picker window. 52 | self.deiconify() 53 | self.grid_columnconfigure(0, weight=1) 54 | self.grid_rowconfigure(0, weight=1) 55 | self.after(10) 56 | self.protocol("WM_DELETE_WINDOW", self._on_closing) 57 | 58 | self.default_hex_color = "#ffffff" 59 | self.default_rgb = [255, 255, 255] 60 | self.rgb_color = self.default_rgb[:] 61 | 62 | self.bg_color = ( 63 | self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"]) 64 | if bg_color is None 65 | else bg_color 66 | ) 67 | self.fg_color = self.fg_color = ( 68 | self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) 69 | if fg_color is None 70 | else fg_color 71 | ) 72 | self.button_color = ( 73 | self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"]) 74 | if button_color is None 75 | else button_color 76 | ) 77 | self.button_hover_color = ( 78 | self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"]) 79 | if button_hover_color is None 80 | else button_hover_color 81 | ) 82 | self.button_text = text 83 | self.corner_radius = corner_radius 84 | self.slider_border = 10 if slider_border >= 10 else slider_border 85 | 86 | self.config(bg=self.bg_color) 87 | 88 | self.frame = customtkinter.CTkFrame(master=self, fg_color=self.fg_color, bg_color=self.bg_color) 89 | self.frame.grid(padx=20, pady=20, sticky="nswe") 90 | 91 | self.canvas = tk.Canvas( 92 | self.frame, 93 | height=self.image_dimension, 94 | width=self.image_dimension, 95 | highlightthickness=0, 96 | bg=self.fg_color, 97 | ) 98 | self.canvas.pack(pady=20) 99 | self.canvas.bind("", self.on_mouse_drag) 100 | 101 | # Set up for access to icons 102 | current_path = os.path.dirname(os.path.realpath(__file__)) 103 | icon_path = os.path.join(current_path, f"..{PrimeItems.slash}assets") 104 | color_wheel = os.path.join(icon_path, "color_wheel.png") 105 | self.img1 = Image.open(color_wheel).resize( 106 | (self.image_dimension, self.image_dimension), 107 | Image.Resampling.LANCZOS, 108 | ) 109 | target = color_wheel = os.path.join(icon_path, "target.png") 110 | self.img2 = Image.open(target).resize( 111 | (self.target_dimension, self.target_dimension), 112 | Image.Resampling.LANCZOS, 113 | ) 114 | 115 | self.wheel = ImageTk.PhotoImage(self.img1) 116 | self.target = ImageTk.PhotoImage(self.img2) 117 | 118 | self.canvas.create_image(self.image_dimension / 2, self.image_dimension / 2, image=self.wheel) 119 | self.set_initial_color(initial_color) 120 | 121 | self.brightness_slider_value = customtkinter.IntVar() 122 | self.brightness_slider_value.set(255) 123 | 124 | self.slider = customtkinter.CTkSlider( 125 | master=self.frame, 126 | height=20, 127 | border_width=self.slider_border, 128 | button_length=15, 129 | progress_color=self.default_hex_color, 130 | from_=0, 131 | to=255, 132 | variable=self.brightness_slider_value, 133 | number_of_steps=256, 134 | button_corner_radius=self.corner_radius, 135 | corner_radius=self.corner_radius, 136 | button_color=self.button_color, 137 | button_hover_color=self.button_hover_color, 138 | command=lambda x: self.update_colors(), 139 | ) 140 | self.slider.pack(fill="both", pady=(0, 15), padx=20 - self.slider_border) 141 | 142 | self.label = customtkinter.CTkLabel( 143 | master=self.frame, 144 | text_color="#000000", 145 | height=50, 146 | fg_color=self.default_hex_color, 147 | corner_radius=self.corner_radius, 148 | text=self.default_hex_color, 149 | ) 150 | self.label.pack(fill="both", padx=10) 151 | 152 | self.button = customtkinter.CTkButton( 153 | master=self.frame, 154 | text=self.button_text, 155 | height=50, 156 | corner_radius=self.corner_radius, 157 | fg_color=self.button_color, 158 | hover_color=self.button_hover_color, 159 | command=self._ok_event, 160 | **button_kwargs, 161 | ) 162 | self.button.pack(fill="both", padx=10, pady=20) 163 | 164 | self.after(150, lambda: self.label.focus()) 165 | 166 | self.grab_set() 167 | 168 | def get(self): 169 | self._color = self.label._fg_color 170 | self.master.wait_window(self) 171 | return self._color 172 | 173 | def _ok_event(self, event=None): 174 | self._color = self.label._fg_color 175 | self.master.color_window_position = self.wm_geometry() 176 | self.grab_release() 177 | self.destroy() 178 | del self.img1 179 | del self.img2 180 | del self.wheel 181 | del self.target 182 | 183 | def _on_closing(self): 184 | self._color = None 185 | self.master.color_window_position = self.wm_geometry() 186 | self.grab_release() 187 | self.destroy() 188 | del self.img1 189 | del self.img2 190 | del self.wheel 191 | del self.target 192 | 193 | def on_mouse_drag(self, event): 194 | x = event.x 195 | y = event.y 196 | self.canvas.delete("all") 197 | self.canvas.create_image(self.image_dimension / 2, self.image_dimension / 2, image=self.wheel) 198 | 199 | d_from_center = math.sqrt(((self.image_dimension / 2) - x) ** 2 + ((self.image_dimension / 2) - y) ** 2) 200 | 201 | if d_from_center < self.image_dimension / 2: 202 | self.target_x, self.target_y = x, y 203 | else: 204 | self.target_x, self.target_y = self.projection_on_circle( 205 | x, y, self.image_dimension / 2, self.image_dimension / 2, self.image_dimension / 2 - 1 206 | ) 207 | 208 | self.canvas.create_image(self.target_x, self.target_y, image=self.target) 209 | 210 | self.get_target_color() 211 | self.update_colors() 212 | 213 | def get_target_color(self): 214 | try: 215 | self.rgb_color = self.img1.getpixel((self.target_x, self.target_y)) 216 | 217 | r = self.rgb_color[0] 218 | g = self.rgb_color[1] 219 | b = self.rgb_color[2] 220 | self.rgb_color = [r, g, b] 221 | 222 | except AttributeError: 223 | self.rgb_color = self.default_rgb 224 | 225 | def update_colors(self): 226 | brightness = self.brightness_slider_value.get() 227 | 228 | self.get_target_color() 229 | 230 | r = int(self.rgb_color[0] * (brightness / 255)) 231 | g = int(self.rgb_color[1] * (brightness / 255)) 232 | b = int(self.rgb_color[2] * (brightness / 255)) 233 | 234 | self.rgb_color = [r, g, b] 235 | 236 | self.default_hex_color = "#{:02x}{:02x}{:02x}".format(*self.rgb_color) 237 | 238 | self.slider.configure(progress_color=self.default_hex_color) 239 | self.label.configure(fg_color=self.default_hex_color) 240 | 241 | self.label.configure(text=str(self.default_hex_color)) 242 | 243 | if self.brightness_slider_value.get() < 70: 244 | self.label.configure(text_color="white") 245 | else: 246 | self.label.configure(text_color="black") 247 | 248 | if str(self.label._fg_color) == "black": 249 | self.label.configure(text_color="white") 250 | 251 | def projection_on_circle(self, point_x, point_y, circle_x, circle_y, radius): 252 | angle = math.atan2(point_y - circle_y, point_x - circle_x) 253 | projection_x = circle_x + radius * math.cos(angle) 254 | projection_y = circle_y + radius * math.sin(angle) 255 | 256 | return projection_x, projection_y 257 | 258 | def set_initial_color(self, initial_color): 259 | # set_initial_color is in beta stage, cannot seek all colors accurately 260 | 261 | if initial_color and initial_color.startswith("#"): 262 | try: 263 | r, g, b = tuple(int(initial_color.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4)) 264 | except ValueError: 265 | return 266 | 267 | self.default_hex_color = initial_color 268 | for i in range(0, self.image_dimension): 269 | for j in range(0, self.image_dimension): 270 | self.rgb_color = self.img1.getpixel((i, j)) 271 | if (self.rgb_color[0], self.rgb_color[1], self.rgb_color[2]) == (r, g, b): 272 | self.canvas.create_image(i, j, image=self.target) 273 | self.target_x = i 274 | self.target_y = j 275 | return 276 | 277 | self.canvas.create_image(self.image_dimension / 2, self.image_dimension / 2, image=self.target) 278 | 279 | 280 | if __name__ == "__main__": 281 | app = AskColor() 282 | app.mainloop() 283 | AskColor.quit(app) 284 | del AskColor 285 | -------------------------------------------------------------------------------- /maptasker/src/debug.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """special debug code for MapTasker 3 | 4 | Returns: 5 | _type_: _description_ 6 | """ 7 | 8 | # # 9 | # debug: special debug code for MapTasker # 10 | # # 11 | import sys 12 | 13 | from maptasker.src.format import format_html 14 | from maptasker.src.primitem import PrimeItems 15 | from maptasker.src.sysconst import ( 16 | ARGUMENT_NAMES, 17 | TYPES_OF_COLOR_NAMES, 18 | FormatLine, 19 | logger, 20 | ) 21 | 22 | 23 | def output_debug_line(begin_or_end: str) -> None: 24 | """ 25 | Put out a line that identifies the following output as DEBUG 26 | 27 | :param begin_or_end: text identiying the beginning or end of debug 28 | """ 29 | arrow = ">" 30 | PrimeItems.output_lines.add_line_to_output( 31 | 0, 32 | f"Runtime Settings {begin_or_end} {arrow * 80}", 33 | ["", "disabled_profile_color", FormatLine.add_end_span], 34 | ) 35 | 36 | 37 | # Format a text line to a specific width by filling in blank spaces with periods. 38 | def format_line_debug(text: str, width: int) -> str: 39 | """Format text line to given width by filling with periods""" 40 | 41 | formatted = text 42 | 43 | if len(text) < width: 44 | formatted += "." * (width - len(text)) 45 | 46 | return formatted[:width] 47 | 48 | 49 | # ################################################################################ 50 | # Display the program arguments and colors to use in output for debug purposes 51 | # ################################################################################ 52 | def display_debug_info() -> None: 53 | """ 54 | Output our runtime arguments 55 | """ 56 | 57 | # Add blank line 58 | PrimeItems.output_lines.add_line_to_output(0, "", FormatLine.dont_format_line) 59 | 60 | # Identify the output as debug stuff 61 | output_debug_line("Start") 62 | if PrimeItems.program_arguments["debug"]: 63 | PrimeItems.output_lines.add_line_to_output( 64 | 0, 65 | f"sys.argv (runtime arguments):{sys.argv!s}", 66 | ["", "disabled_profile_color", FormatLine.add_end_span], 67 | ) 68 | 69 | # Copy our dictionary of runtime arguments and sort it alphabetically 70 | mydict = ARGUMENT_NAMES.copy() 71 | mykeys = sorted(mydict.keys()) 72 | mydict = {i: mydict[i] for i in mykeys} 73 | 74 | # Go through dictionary of arguments and output each one. 75 | for key, value in mydict.items(): 76 | try: 77 | line_formatted_to_length = format_line_debug(ARGUMENT_NAMES[key], 40) 78 | value = PrimeItems.program_arguments[key] # noqa: PLW2901 79 | if value is None or value == "": 80 | value = "None" # noqa: PLW2901 81 | # Set color for value 82 | color_to_use = "unknown_task_color" if not value or value == "None" else "heading_color" 83 | PrimeItems.output_lines.add_line_to_output( 84 | 0, 85 | f"{line_formatted_to_length}: {value}", 86 | ["", color_to_use, FormatLine.add_end_span], 87 | ) 88 | except KeyError: # noqa: PERF203 89 | msg = f"{ARGUMENT_NAMES[key]}: Error...not found!" 90 | PrimeItems.output_lines.add_line_to_output( 91 | 0, 92 | msg, 93 | ["", "heading_color", FormatLine.add_end_span], 94 | ) 95 | logger.debug(f"MapTasker Error ... {msg}") 96 | PrimeItems.output_lines.add_line_to_output(0, "", FormatLine.dont_format_line) 97 | 98 | # Do colors to use in output 99 | 100 | # Get our color names by reversing the lookup dictionary 101 | color_names = {v: k for k, v in TYPES_OF_COLOR_NAMES.items()} 102 | # Go through each color 103 | for key, value in PrimeItems.colors_to_use.items(): 104 | # Highlight background color. Otherwise it won't be visible 105 | if key == "background_color": 106 | value = f"{value} (highlighted for visibility)" # noqa: PLW2901 107 | # Convert the name of the color to the color 108 | the_color = format_html( 109 | key, 110 | "", 111 | value, 112 | True, 113 | ) 114 | 115 | # Add the line formatted with HTML 116 | color_set_to_width = format_line_debug(f"Color for {color_names[key]} set to", 40) 117 | PrimeItems.output_lines.add_line_to_output( 118 | 0, 119 | f"{ color_set_to_width}{the_color}", 120 | ["", "heading_color", FormatLine.add_end_span], 121 | ) 122 | 123 | # Get a total count of action_code entries in our dictionary. 124 | # from maptasker.src.actionc import action_codes 125 | # num = sum(1 for key, value in action_codes.items()) 126 | # PrimeItems.output_lines.add_line_to_output( 127 | # 128 | # 0, 129 | # format_html( 130 | # color_to_use, 131 | # "", 132 | # f"Number of Task Action codes = {num}", 133 | # True, 134 | # ), 135 | # ) 136 | 137 | # Finalize debug info 138 | output_debug_line("End") 139 | PrimeItems.output_lines.add_line_to_output( 140 | 0, 141 | "", 142 | FormatLine.dont_format_line, 143 | ) 144 | 145 | 146 | # Argument not found in dictionary 147 | def not_in_dictionary(condition_type: str, code: str) -> None: 148 | """ 149 | Handle condition if Action/Event/State code not found in our dictionary (actionc.py) 150 | Args: 151 | condition_type (str): name of condition: Action, State, Event 152 | code (str): the xml code""" 153 | logger.debug( 154 | f"Error action code {code} not in the dictionary!", 155 | ) 156 | if PrimeItems.program_arguments["debug"]: 157 | print(f"{condition_type} code {code} not found in actionc!") 158 | -------------------------------------------------------------------------------- /maptasker/src/deprecate.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | depricated = { 4 | "10": "Deprecated", 5 | "11": "Deprecated", 6 | "12": "Deprecated", 7 | "115": "Deprecated", 8 | "116": "Deprecated", 9 | "117": "Deprecated", 10 | "118": "Deprecated", 11 | "151": "Deprecated", 12 | "411": "Deprecated", 13 | "500": "Deprecated", 14 | "510": "Deprecated", 15 | "520": "Deprecated", 16 | "530": "Deprecated", 17 | "540": "Deprecated", 18 | "557": "Deprecated", 19 | "560": "Deprecated", 20 | "565": "Deprecated", 21 | "594": "Deprecated", 22 | "696": "Deprecated", 23 | "777": "Deprecated", 24 | "876": "Deprecated", 25 | } 26 | -------------------------------------------------------------------------------- /maptasker/src/diagcnst.py: -------------------------------------------------------------------------------- 1 | """Diagram Constants""" 2 | 3 | #! /usr/bin/env python3 4 | # # 5 | # diagram: Output a diagram/map of the Tasker configuration. # 6 | # # 7 | # Traverse our network map and print out everything in connected boxes. # 8 | # # 9 | bar = "│" 10 | box_line = "═" 11 | blank = " " 12 | straight_line = "─" 13 | line_right_arrow = f"{straight_line*2}▶" 14 | line_left_arrow = f"◄{straight_line*2}" 15 | down_arrow = "▼" 16 | up_arrow = "▲" 17 | left_arrow = "◄" 18 | right_arrow = "►" 19 | task_delimeter = "¥" 20 | angle = "└─ " 21 | angle_elbow = "└" 22 | 23 | right_arrow_corner_down = "╰" 24 | right_arrow_corner_up = "╯" 25 | left_arrow_corner_down = "╭" 26 | left_arrow_corner_up = "╮" 27 | -------------------------------------------------------------------------------- /maptasker/src/error.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Error handling module for MapTasker.""" 3 | 4 | import sys 5 | 6 | # # 7 | # Error: Process Errors # 8 | # # 9 | from maptasker.src.primitem import PrimeItems 10 | from maptasker.src.sysconst import ERROR_FILE, Colors, logger 11 | 12 | 13 | def error_handler(error_message: str, exit_code: int) -> None: 14 | """ 15 | Error handler: print and log the error. Exit with error code if provided 16 | :param error_message: text of error to print and log 17 | :param exit_code: error code to exit with 18 | """ 19 | # Add our heading to more easily identify the problem 20 | if exit_code in {0, 99}: 21 | final_error_message = f"{Colors.Green}{error_message}" 22 | # Warning? 23 | elif exit_code == 100: 24 | final_error_message = f"{Colors.Yellow}{error_message}" 25 | else: 26 | final_error_message = f"{Colors.Red}MapTasker error: {error_message}" 27 | 28 | # Process an error? 29 | if exit_code > 0 and exit_code < 100: 30 | logger.debug(final_error_message) 31 | if ( 32 | PrimeItems.program_arguments 33 | and PrimeItems.program_arguments["debug"] 34 | and not PrimeItems.program_arguments["gui"] 35 | ): 36 | print(final_error_message) 37 | 38 | # If coming from GUI, set error info. and return to GUI. 39 | if PrimeItems.program_arguments and PrimeItems.program_arguments["gui"]: 40 | # Write the rror to file for use by userinter (e.g. on rerun), so userintr can display error on entry. 41 | with open(ERROR_FILE, "w") as error_file: 42 | error_file.write(f"{error_message}\n") 43 | error_file.write(f"{exit_code}\n") 44 | # Set error info. for GUI to display. 45 | PrimeItems.error_code = exit_code 46 | PrimeItems.error_msg = error_message 47 | return 48 | # Not coming from GUI...just print error. 49 | print(final_error_message) 50 | sys.exit(exit_code) 51 | 52 | # If exit code is 100, then the user closed the window 53 | elif exit_code == 100: 54 | print(final_error_message) 55 | logger.info(final_error_message) 56 | sys.exit(0) 57 | 58 | # return code 0 59 | else: 60 | print(final_error_message) 61 | logger.info(final_error_message) 62 | return 63 | 64 | 65 | def rutroh_error(message: str) -> None: 66 | """ 67 | Prints or logs an error message. 68 | Args: 69 | message (str): The error message to print 70 | Returns: 71 | None: Does not return anything 72 | """ 73 | if PrimeItems.program_arguments["debug"]: 74 | print(f"Rutroh! {message}") 75 | else: 76 | logger.debug(f"Rutroh! Error...{message}") 77 | -------------------------------------------------------------------------------- /maptasker/src/fonts.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # fonts:Get/set up fonts to use in output # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | import sys 8 | from tkinter import Tk, font 9 | 10 | # from maptasker.src.nameattr import get_tk 11 | from maptasker.src.primitem import PrimeItems 12 | 13 | 14 | # Get all monospace ("f"=fixed) fonts 15 | def get_fonts(save_fonts: bool) -> dict: 16 | """ 17 | Get and set up the available monospace fonts and optionally save them 18 | Args: 19 | save_fonts (bool): True if we are to save the fonts 20 | 21 | Returns: 22 | dict: list of avilable monospace fonts 23 | """ 24 | # get_tk() # Get the Tkinter root window 25 | _ = Tk() 26 | fonts = [font.Font(family=f) for f in font.families()] 27 | our_font = PrimeItems.program_arguments["font"] 28 | 29 | # Set up our list of fonts, including Courier 30 | mono_fonts = ["Courier"] 31 | 32 | # If the font requested is 'help', then just display the fonts and exit 33 | if our_font == "help": 34 | print("Valid monospace fonts...") 35 | print(' "Courier" is the default') 36 | 37 | # Go thru list of fonts from tkinter 38 | PrimeItems.mono_fonts = {} 39 | for f in fonts: 40 | # Monospace only 41 | if f.metrics("fixed") and "Wingding" not in f.actual("family"): 42 | if our_font == "help": 43 | print(f' "{f.actual("family")}"') 44 | elif save_fonts: 45 | PrimeItems.mono_fonts[f.name] = f.actual("family") 46 | mono_fonts.append(f.actual("family")) 47 | if our_font == "help": 48 | sys.exit(0) 49 | 50 | del fonts 51 | return mono_fonts 52 | -------------------------------------------------------------------------------- /maptasker/src/format.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Module containing action runner logic.""" 3 | # # 4 | # format: Various formatting functions, # 5 | # # 6 | 7 | from maptasker.src.sysconst import pattern2, pattern8, pattern9, pattern10, pattern15 8 | 9 | 10 | # Given a line in the output queue, reformat it before writing to file 11 | def format_line(item: str) -> str: 12 | """ 13 | Given a line in our list of output lines, do some additional formatting 14 | to clean it up 15 | :param item: the specific text to reformat from the list of output lines 16 | :return: the reformatted text line for output 17 | """ 18 | space = " " 19 | # If item is a list, then get the actual output line 20 | if isinstance(item, list): 21 | item = item[1] 22 | 23 | # Get rid of trailing blanks 24 | item.rstrip() 25 | 26 | # Change "Action: nn ..." to "Action nn: ..." (i.e. move the colon) 27 | action_position = item.find("Action: ") 28 | if action_position != -1: 29 | action_number_list = item[action_position + 8 :].split(" ") 30 | action_number = action_number_list[0] 31 | action_number = action_number.split("<") 32 | output_line = item.replace( 33 | f"Action: {action_number[0]}", 34 | f"{action_number[0]}:", 35 | ) 36 | 37 | # No changes needed 38 | else: 39 | output_line = item 40 | 41 | # # Format the html...add a number of blanks if some sort of list. 42 | if "DOCTYPE" in item: # If imbedded html (e.g. Scene WebElement), add a break and some spacing. 43 | output_line = pattern15.sub(f"
{space * 30}", output_line) 44 | 45 | # Add a carriage return if this is a break: replace("
" with "
\r" 46 | output_line = pattern8.sub("
\r", output_line) 47 | # Get rid of trailing blank 48 | output_line = pattern2.sub("", output_line) # Get space-commas: " ," 49 | 50 | # Get rid of extraneous html code that somehow got in to the output 51 | output_line = pattern9.sub("", output_line) 52 | 53 | return pattern10.sub("

", output_line) 54 | 55 | 56 | # Plug in the html for color along with the text 57 | def format_html( 58 | color_code: str, 59 | text_before: str, 60 | text_after: str, 61 | end_span: bool, 62 | ) -> str: 63 | """ 64 | Plug in the html for color and font, along with the text 65 | :param color_code: the code to use to find the color in colormap 66 | :param text_before: text to insert before the color/font html 67 | :param text_after: text to insert after the color/font html 68 | :param end_span: True=add at end, False=don't add at end 69 | :return: string with text formatted with color and font 70 | """ 71 | 72 | # Determine and get the color to use. 73 | # Return completed HTML with color, font and text with text after 74 | if text_after: 75 | # The following line eliminates a {text_after}{trailing_span}' 83 | 84 | # No text after...just return it. 85 | return text_after 86 | -------------------------------------------------------------------------------- /maptasker/src/frontmtr.py: -------------------------------------------------------------------------------- 1 | """Display the front portion of the output.""" 2 | 3 | #! /usr/bin/env python3 4 | # # 5 | # frontmtr - Output the front matter: heading, runtime settings, directory, prefs # 6 | # # 7 | # MIT License Refer to https://opensource.org/license/mit # 8 | import datetime 9 | 10 | from maptasker.src.addcss import add_css 11 | from maptasker.src.debug import display_debug_info 12 | from maptasker.src.format import format_html 13 | from maptasker.src.prefers import get_preferences 14 | from maptasker.src.primitem import PrimeItems 15 | from maptasker.src.sysconst import MY_VERSION, NORMAL_TAB, FormatLine 16 | 17 | 18 | # Add the heading matter to the output: heading, source, screen size, etc. 19 | def output_the_heading() -> None: 20 | """ 21 | Display the heading and source file details 22 | """ 23 | # window_dimensions = """ 24 | #

25 | # """ 31 | 32 | # Check if Ai analysis running. 33 | ai_message = " Ai Analysis Run" if PrimeItems.program_arguments["ai_analyze"] else "" 34 | 35 | tasker_mapping = f"Tasker Mapping{ai_message}................ Tasker XML version:" 36 | 37 | # Get the screen dimensions from xml 38 | screen_element = PrimeItems.xml_root.find("dmetric") 39 | screen_size = ( 40 | f'  Device screen size: {screen_element.text.replace(",", " X ")}' 41 | if screen_element is not None 42 | else "" 43 | ) 44 | 45 | # Set up highlight background color if needed 46 | if PrimeItems.program_arguments["highlight"]: 47 | background_color_html = ( 48 | "\n" 49 | ) 50 | else: 51 | background_color_html = "" 52 | 53 | # Output date and time if in debug mode 54 | # now_for_output = NOW_TIME.strftime("%d-%B-%Y %H:%M:%S") 55 | current_time = datetime.datetime.now() # noqa: DTZ005 56 | # formatted_time = current_time.strftime('%H:%M:%S') 57 | now_for_output = current_time.strftime("%d-%B-%Y %H:%M:%S") 58 | 59 | # Format the output heading 60 | heading_color = "heading_color" 61 | PrimeItems.heading = ( 62 | f'\n\n\n{background_color_html}MapTasker\n\n" 64 | + format_html( 65 | heading_color, 66 | "", 67 | ( 68 | f"

MapTasker


{tasker_mapping}" 69 | f" {PrimeItems.xml_root.attrib['tv']}    " 70 | f"{MY_VERSION}{screen_size}    {now_for_output}" 71 | ), 72 | True, 73 | ) 74 | ) 75 | 76 | # Add script to get window dimensions 77 | # PrimeItems.output_lines.add_line_to_output( 78 | # 0, 79 | # window_dimensions, 80 | # FormatLine.dont_format_line, 81 | # ) 82 | 83 | # Add a blank line 84 | PrimeItems.output_lines.add_line_to_output( 85 | 0, 86 | PrimeItems.heading, 87 | FormatLine.dont_format_line, 88 | ) 89 | 90 | # Add css 91 | add_css() 92 | 93 | # Display where the source file came from 94 | # Did we restore the backup from Android? 95 | if PrimeItems.program_arguments["fetched_backup_from_android"]: 96 | source_file = ( 97 | "From Android device" 98 | f' TCP IP address:{PrimeItems.program_arguments["android_ipaddr"]}' 99 | f' on port:{PrimeItems.program_arguments["android_port"]}' 100 | f' with file location: {PrimeItems.program_arguments["android_file"]}' 101 | ) 102 | elif PrimeItems.program_arguments["debug"] or not PrimeItems.program_arguments["file"]: 103 | filename = isinstance(PrimeItems.file_to_get, str) 104 | filename = PrimeItems.file_to_get.name if not filename else PrimeItems.file_to_get 105 | source_file = filename 106 | else: 107 | source_file = PrimeItems.program_arguments["file"] 108 | # Add source to output 109 | PrimeItems.output_lines.add_line_to_output( 110 | 0, 111 | f"

{NORMAL_TAB}Source backup file: {source_file}", 112 | ["", "heading_color", FormatLine.add_end_span], 113 | ) 114 | 115 | 116 | # Output the heading etc. as the front matter. 117 | def output_the_front_matter() -> None: 118 | """ 119 | Generates the front matter for the output file: heading, runtime settings, 120 | directory, Tasker preferences. 121 | """ 122 | 123 | # Heading information 124 | output_the_heading() 125 | 126 | # If we are debugging, output the runtime arguments and colors 127 | if PrimeItems.program_arguments["debug"] or PrimeItems.program_arguments["runtime"]: 128 | display_debug_info() 129 | 130 | # Output a flag to indicate this is where the directory goes 131 | PrimeItems.output_lines.add_line_to_output(5, "maptasker_directory", FormatLine.dont_format_line) 132 | 133 | # If doing Tasker preferences, get them 134 | if PrimeItems.program_arguments["preferences"]: 135 | get_preferences() 136 | -------------------------------------------------------------------------------- /maptasker/src/getbakup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # getbakup: get the backup file directly from the Android device # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | from __future__ import annotations 8 | 9 | import os.path 10 | from os import getcwd 11 | 12 | from maptasker.src.error import error_handler 13 | from maptasker.src.primitem import PrimeItems 14 | from maptasker.src.sysconst import logger 15 | 16 | 17 | # We've read in the xml backup file. Now save it for processing. 18 | def write_out_backup_file(file_contents: bin) -> None: 19 | """ 20 | We've read in the xml backup file. Now save it for processing. 21 | 22 | :param file_contents: binary contents of backup xml file 23 | :return: Nothing 24 | """ 25 | # Store the output in the current directory 26 | my_output_dir = getcwd() 27 | if my_output_dir is None: 28 | error_handler( 29 | "MapTasker canceled. An error occurred in getbakup. Program canceled.", 30 | 2, 31 | ) 32 | 33 | # We must get just the file name and type since we will be using this to save it to our local path. 34 | # This is the file we will do all of our processing against...the local file fetched from the Android device. 35 | # Get position of the last "/" in path/file 36 | name_location = PrimeItems.program_arguments["android_file"].rfind(PrimeItems.slash) + 1 37 | # Get the name of the file 38 | my_file_name = PrimeItems.program_arguments["android_file"][name_location:] 39 | 40 | # Convert the binary code to string 41 | output_lines = file_contents.decode("utf-8") 42 | 43 | # Set up the backup file full path 44 | the_backup_file = PrimeItems.program_arguments["android_file"] 45 | put_message = f"Fetching backup file {my_file_name}: {the_backup_file}" 46 | logger.debug(put_message) 47 | 48 | # If backup.xml already exists, delete it first 49 | if os.path.isfile(my_file_name): 50 | os.remove(my_file_name) 51 | 52 | # Open output file 53 | with open(my_file_name, "w") as out_file: 54 | # Write out each line 55 | for item in output_lines: 56 | item.rstrip() # Get rid of trailing blanks 57 | out_file.write(item) 58 | 59 | # Set flag to identify that backup file was fetched from Android device 60 | PrimeItems.program_arguments["fetched_backup_from_android"] = True 61 | 62 | 63 | # Return the substring after the last occurance of a specific character in a string 64 | def substring_after_last(string: str, char: chr) -> str: 65 | """ 66 | Return the substring after the last occurance of a specific character in a string 67 | Args: 68 | string (str): The string to search for the substring 69 | char (chr): The character to find (the last occurance of) 70 | 71 | Returns: 72 | str: The substring in string after the last occurance of char 73 | """ 74 | index = string.rfind(char) 75 | return "" if index == -1 else string[index + 1 :] 76 | 77 | 78 | # Set up to fetch the Tasker backup xml file from the Android device running 79 | def get_backup_file() -> str: 80 | """ 81 | Set up to fetch the Tasker backup xml file from the Android device running 82 | the Tasker server 83 | 84 | :return: The name of the backup file (e.g. backup.xml) 85 | """ 86 | from maptasker.src.maputils import http_request 87 | 88 | # If ruinning from the GUI, then we have already gotten the file. Just return the name on the local drive.add 89 | if PrimeItems.program_arguments["gui"]: 90 | return substring_after_last(PrimeItems.program_arguments["android_file"], "/") 91 | 92 | # Get the contents of the file from the Android device. 93 | return_code, file_contents = http_request( 94 | PrimeItems.program_arguments["android_ipaddr"], 95 | PrimeItems.program_arguments["android_port"], 96 | PrimeItems.program_arguments["android_file"], 97 | "file", 98 | "?download=1", 99 | ) 100 | 101 | if return_code != 0: 102 | if PrimeItems.program_arguments["gui"]: 103 | PrimeItems.error_code = return_code 104 | return None 105 | error_handler(str(file_contents), 8) 106 | 107 | # Write the XML file to local storage. 108 | write_out_backup_file(file_contents) 109 | 110 | return substring_after_last(PrimeItems.program_arguments["android_file"], "/") 111 | -------------------------------------------------------------------------------- /maptasker/src/getids.py: -------------------------------------------------------------------------------- 1 | """Get Profile or Task IDs from XML""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # getids: Look for Profile / Task IDs in head_xml_element xml elements # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | 10 | import defusedxml.ElementTree # Need for type hints 11 | 12 | 13 | def get_ids( 14 | doing_head_xml_element: bool, 15 | head_xml_element: defusedxml.ElementTree, 16 | head_xml_element_name: str, 17 | head_xml_elements_without_profiles: list, 18 | ) -> list: 19 | """ 20 | Find either head_xml_element 'pids' (Profile IDs) or 'tids' (Task IDs) 21 | :param doing_head_xml_element: True if looking for Profile IDs, False if Task IDs. 22 | :param head_xml_element: head_xml_element xml element 23 | :param head_xml_element_name: name of head_xml_element 24 | :param head_xml_elements_without_profiles: list of elements without ids 25 | :return: list of found IDs, or empty list if none found 26 | """ 27 | 28 | found_ids = "" 29 | # Get Profiles by searching for element. If not Profile IDs, just get Task IDs via xml element. 30 | ids_to_find = "pids" if doing_head_xml_element else "tids" 31 | 32 | # Get the IDs. 33 | try: 34 | # Get a list of the Profiles for this head_xml_element 35 | found_ids = head_xml_element.find(ids_to_find).text 36 | except AttributeError: # head_xml_element has no Profile/Task IDs 37 | if head_xml_element_name not in head_xml_elements_without_profiles: 38 | head_xml_elements_without_profiles.append(head_xml_element_name) 39 | 40 | return found_ids.split(",") if found_ids != "" else [] 41 | -------------------------------------------------------------------------------- /maptasker/src/globalvr.py: -------------------------------------------------------------------------------- 1 | """Display global variables in output HTML""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # variables: process Tasker variables. # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | 10 | import defusedxml.ElementTree # Need for type hints 11 | 12 | from maptasker.src.primitem import PrimeItems 13 | from maptasker.src.sysconst import NORMAL_TAB, TABLE_BACKGROUND_COLOR, TABLE_BORDER, FormatLine 14 | 15 | # List of Tasker global variables 16 | tasker_global_variables = [ 17 | "%AIR", 18 | "%AIRR", 19 | "%BATT", 20 | "%BLUE", 21 | "%CALS", 22 | "%CALTITLE", 23 | "%CALDESCR", 24 | "%CALLOC", 25 | "%CNAME", 26 | "%CNUM", 27 | "%CDATE", 28 | "%CTIME", 29 | "%CONAME", 30 | "%CONUM", 31 | "%CODATE", 32 | "%COTIME", 33 | "%CODUR", 34 | "%CELLID", 35 | "%CELLSIG", 36 | "%CELLSRV", 37 | "%CPUFREQ", 38 | "%CPUGOV", 39 | "%DATE", 40 | "%DAYM", 41 | "%DAYW", 42 | "%DEVID", 43 | "%DEVMAN", 44 | "%DEVMOD", 45 | "%DEVPROD", 46 | "%DEVTID", 47 | "%BRIGHT", 48 | "%DTOUT", 49 | "%EFROM", 50 | "%ECC", 51 | "%ESUBJ", 52 | "%EDATE", 53 | "%ETIME", 54 | "%MEMF", 55 | "%GPS", 56 | "%HEART", 57 | "%HTTPR", 58 | "%HTTPD", 59 | "%HTTPL", 60 | "%HUMIDITY", 61 | "%IMETHOD", 62 | "%INTERRUPT", 63 | "%KEYG", 64 | "%LAPP", 65 | "%FOTO", 66 | "%LIGHT", 67 | "%LOC", 68 | "%LOCACC", 69 | "%LOCALT", 70 | "%LOCSPD", 71 | "%LOCTMS", 72 | "%LOCN", 73 | "%LOCNACC", 74 | "%LOCNTMS", 75 | "%MFIELD", 76 | "%MTRACK", 77 | "%MUTED", 78 | "%NIGHT", 79 | "%NTITLE", 80 | "%PNUM", 81 | "%PRESSURE", 82 | "%PACTIVE", 83 | "%PENABLED", 84 | "%ROAM", 85 | "%ROOT", 86 | "%SCREEN", 87 | "%SDK", 88 | "%SILENT", 89 | "%SIMNUM", 90 | "%SIMSTATE", 91 | "%SPHONE", 92 | "%SPEECH", 93 | "%TRUN", 94 | "%TNET", 95 | "%TEMP", 96 | "%SMSRF", 97 | "%SMSRN", 98 | "%SMSRB", 99 | "%MMSRS", 100 | "%SMSRD", 101 | "%SMSRT", 102 | "%TIME", 103 | "%TIMEMS", 104 | "%TIMES", 105 | "%UIMOD", 106 | "%UPS", 107 | "%VOLA", 108 | "%VOLC", 109 | "%VOLD", 110 | "%VOLM", 111 | "%VOLN", 112 | "%VOLR", 113 | "%VOLS", 114 | "%WIFII", 115 | "%WIFI", 116 | "%WIMAX", 117 | "%WIN", 118 | ] 119 | 120 | 121 | # Read in the variables and save them for now. 122 | def get_variables() -> None: 123 | """ 124 | Read in and save the Tasker variables. 125 | Args: 126 | 127 | """ 128 | # Get all of the Tasker variables 129 | if not (global_variables := PrimeItems.xml_root.findall("Variable")): 130 | return 131 | # Save each in a dictionary. 132 | # Loop through the variables. 133 | for variable in global_variables: 134 | for num, child in enumerate(variable): 135 | if num == 0: 136 | variable_name = child.text 137 | else: 138 | variable_value = child.text 139 | 140 | # Format the output 141 | if variable_value: 142 | variable_value = variable_value.replace(",", "
") 143 | variable_value = variable_value.replace(" ", " ") 144 | 145 | # Add it to our dictionary 146 | PrimeItems.variables[variable_name] = { 147 | "value": variable_value, 148 | "project": [], 149 | "verified": True, 150 | } 151 | 152 | 153 | # Print the variables (Project's or Unreferenced) 154 | def print_the_variables(color_to_use: str, project: defusedxml.ElementTree) -> None: 155 | """Parameters: 156 | - color_to_use (str): The color to use for the table definition. 157 | - project (defusedxml.ElementTree): The project to use, if applicable. 158 | Returns: 159 | - None: This function does not return anything. 160 | Processing Logic: 161 | - Create table definition. 162 | - Create empty list for variable output lines. 163 | - Sort the Tasker global variables. 164 | - If the key is a Tasker global variable, change the value to "global". 165 | - If project is not None or an empty string, find the Project. 166 | - If the variable has a list of Projects, extend the variable output lines with the key and value. 167 | - If the variable is a verified "tasker variable" and not a Project global variable, append the key and value to the variable output lines. 168 | - Return the variable output lines.""" 169 | table_definition = f'' 170 | variable_output_lines = [] 171 | 172 | # Go through all of the Tasker global variables. 173 | for key, value in sorted(PrimeItems.variables.items()): 174 | # If this is a Tasker global variable, change the value to "global" 175 | if key in tasker_global_variables: 176 | value["value"] = "Tasker Global" 177 | 178 | # If doing the Project variables, first find the Project 179 | if project is not None and project != "": 180 | # Does this variable have a list of Projects? 181 | if PrimeItems.variables[key]["project"]: 182 | variable_output_lines.extend( 183 | [ 184 | f"{table_definition}{key}{table_definition}{value['value']}" 185 | for variable_project in PrimeItems.variables[key]["project"] 186 | if variable_project["xml"] == project 187 | ], 188 | ) 189 | 190 | # If this is a verified "tasker variable", and not a Project global var? 191 | elif PrimeItems.variables[key]["verified"] and not PrimeItems.variables[key]["project"]: 192 | # It is an unrefereenced variable. 193 | variable_output_lines.append( 194 | f"{table_definition}{key}{table_definition}{value['value']}", 195 | ) 196 | 197 | return variable_output_lines 198 | 199 | 200 | # Print variables by adding them to the output. 201 | def output_variables(heading: str, project: defusedxml.ElementTree) -> None: 202 | """ 203 | Print variables by adding them to the output. 204 | Args: 205 | 206 | heading (str): Heading to print. 207 | project (xml.etree.ElementTree): Project to print. 208 | """ 209 | if not PrimeItems.variables: 210 | return 211 | # Add a directory entry for variables. 212 | if (project is None or project == "") and PrimeItems.program_arguments["directory"]: 213 | PrimeItems.output_lines.add_line_to_output( 214 | 5, 215 | '', 216 | FormatLine.dont_format_line, 217 | ) 218 | 219 | # Output unreferenced global variables. The Project will be "". 220 | # Force an indentation and set color to use in output. 221 | if project is None or project == "": 222 | color_to_use = PrimeItems.colors_to_use["trailing_comments_color"] 223 | color_name = "trailing_comments_color" 224 | PrimeItems.output_lines.add_line_to_output( 225 | 1, 226 | "", 227 | ["", "trailing_comments_color", FormatLine.add_end_span], 228 | ) 229 | # Print a ruler 230 | PrimeItems.output_lines.add_line_to_output( 231 | 5, 232 | "

", 233 | FormatLine.dont_format_line, 234 | ) 235 | else: 236 | color_to_use = PrimeItems.colors_to_use["project_color"] 237 | color_name = "project_color" 238 | 239 | # Print the heading if we have global variables. 240 | if variable_output_lines := print_the_variables(color_to_use, project): 241 | PrimeItems.output_lines.add_line_to_output( 242 | 5, 243 | f"
{NORMAL_TAB}{heading}", 244 | ["", color_name, FormatLine.add_end_span], 245 | ) 246 | 247 | # Define table 248 | table_definition = f'{TABLE_BORDER}\n\n\n\n' 249 | PrimeItems.output_lines.add_line_to_output( 250 | 5, 251 | table_definition, 252 | FormatLine.dont_format_line, 253 | ) 254 | 255 | # Now go through our dictionary outputing the (sorted) variables 256 | for line in variable_output_lines: 257 | PrimeItems.output_lines.add_line_to_output( 258 | 5, 259 | line, 260 | FormatLine.dont_format_line, 261 | ) 262 | 263 | # Wrap things up 264 | # End table 265 | PrimeItems.output_lines.add_line_to_output( 266 | 5, 267 | "
NameValue

", 268 | FormatLine.dont_format_line, 269 | ) 270 | # Un-indent the output only if doing unreferenced variables. 271 | if project is None or project == "": 272 | PrimeItems.output_lines.add_line_to_output( 273 | 3, 274 | "", 275 | FormatLine.dont_format_line, 276 | ) 277 | -------------------------------------------------------------------------------- /maptasker/src/initparg.py: -------------------------------------------------------------------------------- 1 | """Intialize command line interface/runtime arguments for MapTasker""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # initparg: intialize command line interface/runtime arguments for MapTasker # 7 | # # 8 | from maptasker.src.config import ANDROID_FILE, ANDROID_IPADDR, ANDROID_PORT, OUTPUT_FONT 9 | 10 | 11 | ####################################################################################### 12 | # Initialize Program runtime arguments to default values 13 | # Command line parameters 14 | def initialize_runtime_arguments() -> dict: 15 | """ 16 | Initialize the program's runtime arguments...as a dictionary of options. 17 | The key must be the same name as the key in PrimeItems.program_arguments. 18 | :return: runtime arguments in dictionary 19 | """ 20 | 21 | return { 22 | "ai_analysis_window_position": "", # Last-used ai analysis window position 23 | "ai_analyze": False, # Do local AI processing 24 | "ai_apikey": "", # AI API key 25 | "ai_apikey_window_position": "", # Last-used APIKey Options window position 26 | "ai_model": "", # AI model 27 | "ai_popup_window_position": "", # Last-used ai popup window position (currently not used) 28 | "ai_prompt": "", # AI prompt 29 | "android_file": ANDROID_FILE, 30 | "android_ipaddr": ANDROID_IPADDR, # IP address of Android device 31 | "android_port": ANDROID_PORT, # Port of Android device 32 | "appearance_mode": "system", # Appearance mode: "system", "dark", or "light" 33 | "bold": False, # Display Project/Profile?Task/Scene names in bold text 34 | "color_window_position": "", # Last-used color window position 35 | "conditions": False, # Display Profile and Task conditions 36 | "debug": False, # Run in debug mode (create log file) 37 | "doing_diagram": False, # Use the GUI to get the diagram view 38 | "diagram_window_position": "", # Last-used diagram window position 39 | "directory": False, # Display directory 40 | "display_detail_level": 4, # Display detail level 41 | "fetched_backup_from_android": False, # Backup file was fetched from Android device 42 | "file": "", # If we are re-running, then this is the file to re-use 43 | "font": OUTPUT_FONT, # Font to use in the output 44 | "gui": False, # Use the GUI to get the runtime and color options 45 | "guiview": False, # Use the GUI to get the view (Map, Diagram, Tree) 46 | "highlight": False, # Highlight Project/Profile?Task/Scene names 47 | "icon_alignement": True, # Align Diagram view with icons 48 | "indent": 4, # Backup file was fetched from Android device 49 | "italicize": False, # Italicise Project/Profile?Task/Scene names 50 | "list_unnamed_items": False, # List unnamed items 51 | "view_limit": 10000, # Map view limit 52 | "map_window_position": "", # Last-used map window position 53 | "outline": False, # Outline Project/Profile?Task/Scene names 54 | "preferences": False, # Display Tasker's preferences 55 | "pretty": False, # Pretty up the output (takes many more output lines) 56 | "progressbar_window_position": "", # Last-used progressbar window position 57 | "rerun": False, # Is this a GUI re-run? 58 | "reset": False, # Reset settings to default values 59 | "runtime": False, # Display the runtime arguments/settings 60 | "single_profile_name": "", # Display single Profile name only 61 | "single_project_name": "", # Display single Project name only 62 | "single_task_name": "", # Display single Task name only 63 | "task_action_warning_limit": 100, # Task action warning limit 64 | "taskernet": False, # Display TaskerNet information 65 | "tree_window_position": "", # Last-used treeview window position 66 | "twisty": False, # Add Task twisty "▶︎" clickable icons for Task details 67 | "underline": False, # Underline Project/Profile?Task/Scene names 68 | "window_position": "", # Last-used window position 69 | } 70 | -------------------------------------------------------------------------------- /maptasker/src/kidapp.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # kidapp: Process Kid Application details # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | import defusedxml.ElementTree # Need for type hints 8 | 9 | from maptasker.src.primitem import PrimeItems 10 | 11 | 12 | def get_kid_app(element: defusedxml.ElementTree) -> str: 13 | """ 14 | Get any associated Kid Application info and return it 15 | :param element: root element to search for 16 | :return: the Kid App info 17 | """ 18 | blank = " " 19 | kid_features = kid_plugins = "" 20 | four_spaces = "    " 21 | kid_element = element.find("Kid") 22 | if kid_element is None: 23 | return "" 24 | 25 | kid_package = kid_element.find("pkg").text 26 | kid_version = kid_element.find("vnme").text 27 | kid_target = kid_element.find("vTarg").text 28 | num_feature = num_plugin = 0 29 | 30 | for item in kid_element: # Get any special features 31 | if "feat" in item.tag: 32 | kid_features = f" {kid_features}{num_feature+1}={item.text}, " 33 | num_feature += 1 34 | elif "mplug" in item.tag: 35 | kid_plugins = f" {kid_plugins}{num_plugin+1}={item.text}, " 36 | num_plugin += 1 37 | if kid_features: 38 | kid_features = f"
{four_spaces}Features:{kid_features[:len(kid_features)-2]}" 39 | if kid_plugins: 40 | kid_plugins = f"
{four_spaces}Plugins:{kid_plugins[:len(kid_plugins)-2]}" 41 | 42 | kid_app_info = ( 43 | f"
   [Kid App Package:{kid_package}, Version" 44 | f" Name:{kid_version}, Target Android" 45 | f" Version:{kid_target} {kid_features} {kid_plugins}]" 46 | ) 47 | 48 | if PrimeItems.program_arguments["pretty"]: 49 | number_of_blanks = kid_app_info.find("Package:") - 4 50 | kid_app_info = kid_app_info.replace(",", f"
{blank*number_of_blanks}") 51 | 52 | return kid_app_info 53 | -------------------------------------------------------------------------------- /maptasker/src/mailer.py: -------------------------------------------------------------------------------- 1 | """Send mail""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # guiutil: Utilities used by GUI # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | 10 | # smtplib provides functionality to send emails using SMTP. 11 | import smtplib 12 | 13 | # MIMEApplication attaching application-specific data (like CSV files) to email messages. 14 | from email.mime.application import MIMEApplication 15 | 16 | # MIMEMultipart send emails with both text content and attachments. 17 | from email.mime.multipart import MIMEMultipart 18 | 19 | # MIMEText for creating body of the email message. 20 | from email.mime.text import MIMEText 21 | 22 | subject = "MapTasker Request/Issue" 23 | body = "This is the body of the text message" 24 | sender_email = "mikrubin@gmail.com" 25 | recipient_email = "mikrubin@gmail.com" 26 | sender_password = "tvybxugbbsxxocqu" # Application-specific Google app password 27 | smtp_server = "smtp.gmail.com" 28 | smtp_port = 465 29 | path_to_file = "MapTasker_mail.txt" # This file will be attached. 30 | 31 | # MIMEMultipart() creates a container for an email message that can hold 32 | # different parts, like text and attachments and in next line we are 33 | # attaching different parts to email container like subject and others. 34 | message = MIMEMultipart() 35 | message["Subject"] = subject 36 | message["From"] = sender_email 37 | message["To"] = recipient_email 38 | body_part = MIMEText(body) 39 | message.attach(body_part) 40 | 41 | # section 1 to attach file 42 | with open(path_to_file, "rb") as file: 43 | # Attach the file with filename to the email 44 | message.attach(MIMEApplication(file.read(), Name="example.csv")) 45 | 46 | # secction 2 for sending email 47 | with smtplib.SMTP_SSL(smtp_server, smtp_port) as server: 48 | server.login(sender_email, sender_password) 49 | server.sendmail(sender_email, recipient_email, message.as_string()) 50 | -------------------------------------------------------------------------------- /maptasker/src/nameattr.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Povide the highlighting attribute(s) based on the settings.""" 3 | 4 | # # 5 | # nameattr: Format the Project/Profile/Task/Scene name with bold, highlighting or # 6 | # italisized. Also used for some utility functions. # 7 | # # 8 | 9 | import tkinter as tk 10 | 11 | from maptasker.src.primitem import PrimeItems 12 | 13 | 14 | def add_name_attribute(name: str) -> str: 15 | """ 16 | Format the Project/Profile?Task/Scene name with bold and/or highlighting 17 | Args: 18 | 19 | name (str): the Project/Profile/Task/Scene name 20 | 21 | Returns: 22 | str: the name with bold and/or highlighting added 23 | """ 24 | 25 | # Set default values 26 | italicize = end_italicize = highlight = end_highlight = bold = end_bold = underline = end_underline = "" 27 | 28 | # Make the name bold if requested 29 | if PrimeItems.program_arguments["bold"]: 30 | bold = "" 31 | end_bold = "" 32 | 33 | # Make the name highlighted if requested 34 | if PrimeItems.program_arguments["highlight"]: 35 | highlight = "" 36 | end_highlight = "" 37 | 38 | # Make the name italicized if requested 39 | if PrimeItems.program_arguments["italicize"]: 40 | italicize = "" 41 | end_italicize = "" 42 | 43 | # Make the name underlined if requested 44 | if PrimeItems.program_arguments["underline"]: 45 | underline = "" 46 | end_underline = "" 47 | 48 | return f"{underline}{highlight}{bold}{italicize}{name}{end_italicize}{end_bold}{end_highlight}{end_underline}" 49 | 50 | 51 | # Get Tkinter (can only get it once) 52 | def get_tk() -> None: 53 | """ 54 | Initialize tkinter root window 55 | Args: 56 | None 57 | Returns: 58 | None 59 | - Check if PrimeItems.tkroot already exists 60 | - If not, initialize new Tkinter root window object and assign to PrimeItems.tkroot 61 | - Return PrimeItems.tkroot""" 62 | if not PrimeItems.program_arguments["gui"]: 63 | PrimeItems.tkroot = tk.Tk() 64 | -------------------------------------------------------------------------------- /maptasker/src/prefers.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Process the Tasker preferences via the services xml elements""" 3 | # # 4 | # prefers: Process Tasker's Preferences # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | 8 | import re 9 | from operator import itemgetter 10 | 11 | from maptasker.src.error import error_handler 12 | from maptasker.src.format import format_html 13 | from maptasker.src.primitem import PrimeItems 14 | from maptasker.src.servicec import service_codes 15 | from maptasker.src.sysconst import FormatLine 16 | 17 | 18 | def process_service( 19 | service_name: str, 20 | service_value: str, 21 | temp_output_lines: list, 22 | ) -> None: 23 | """ 24 | We have a service xml element that we have mapped as a preference. Process it. 25 | :param service_name: name of the preference in {blank * 50}\ 61 | {service_value[position + 14:packages_end[number]]}" 62 | service_value = package_names 63 | 64 | # Add the service name to the output list 65 | # Prefix it by the "num" number, since we will be sorting by this number 66 | temp_output_lines.append( 67 | [ 68 | service_codes[service_name]["num"], 69 | ( 70 | format_html( 71 | "preferences_color", 72 | "", 73 | ( 74 | f"{preferences_html}{blank * 2}{output_service_name}\ 75 | {blank * 4}{service_value}" 76 | ), 77 | True, 78 | ) 79 | ), 80 | ], 81 | ) 82 | 83 | 84 | # Go through all of the xml elements to process the Tasker preferences. 85 | def process_preferences(temp_output_lines: list) -> None: 86 | """ 87 | Go through all of the xml elements to process the Tasker preferences. 88 | :param temp_output_lines: list of service/preference output lines 89 | :return: nothing 90 | """ 91 | dummy_num = 200 92 | first_time = True 93 | blank = " " 94 | 95 | # No preferences if doing single object 96 | if not PrimeItems.tasker_root_elements["all_services"]: 97 | temp_output_lines.append( 98 | [ 99 | dummy_num, 100 | format_html( 101 | "preferences_color", 102 | "", 103 | "Preferences not found in this XML file. Most likely due to a single Project/Profile/Task/Scene only display.", 104 | True, 105 | ), 106 | ], 107 | ) 108 | return 109 | 110 | # Go through each xml element 111 | for service in PrimeItems.tasker_root_elements["all_services"]: 112 | # Make sure the xml element is valid 113 | if all(service.find(tag) is not None for tag in ("n", "t", "v")): 114 | # Get the service codes 115 | service_name = service.find("n").text or "" 116 | service_type = service.find("t").text or "" 117 | service_value = service.find("v").text or "" 118 | 119 | # See if the service name is in our dictionary of preferences 120 | if service_name in service_codes: 121 | # Got a hit. Go process it. 122 | process_service(service_name, service_value, temp_output_lines) 123 | 124 | # If debugging, list specific preferences which can't be identified. 125 | elif PrimeItems.program_arguments["debug"]: 126 | # Add a blank line and the output details to our list of output stuff 127 | # Add a blank line if this is the first unmapped item 128 | if first_time: 129 | first_time = False 130 | temp_output_lines.append([dummy_num, "
"]) 131 | # Add the output details to our list of output stuff 132 | temp_output_lines.append( 133 | [ 134 | dummy_num, 135 | format_html( 136 | "preferences_color", 137 | "", 138 | ( 139 | f"{blank * 2}Not yet" 140 | f" mapped or unused:{service_name}{blank * 4}type:{service_type}\ 141 | {blank * 4}value:{service_value}" 142 | ), 143 | True, 144 | ), 145 | ], 146 | ) 147 | dummy_num += 1 148 | # Invalid xml element 149 | else: 150 | error_handler("Error: the backup xml file is corrupt. Program terminated.", 3) 151 | 152 | 153 | def get_preferences() -> None: 154 | """ 155 | Go through the Tasker xml elements, each representing a Tasker preference 156 | :rtype: nothing 157 | """ 158 | section_names = [ 159 | "UI > General", 160 | "UI > Main Screen", 161 | "UI > UI Lock", 162 | "UI > Localization", 163 | "Monitor > General", 164 | "Monitor > Display On Monitoring", 165 | "Monitor > Display Off Monitoring", 166 | "Monitor > General Monitoring", 167 | "Monitor > Calibrate", 168 | "Action", 169 | "Action > Reset Error Notifications", 170 | "Misc", 171 | "Misc > Debugging", 172 | "Unlisted", 173 | ] 174 | temp_output_lines = [] 175 | 176 | # Output title line 177 | PrimeItems.output_lines.add_line_to_output( 178 | 0, 179 | "Tasker Preferences >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", 180 | ["", "preferences_color", FormatLine.add_end_span], 181 | ) 182 | 183 | # Go through each  Section: {section_names[section]}", 206 | ["", "preferences_color", FormatLine.add_end_span], 207 | ) 208 | previous_section = section 209 | PrimeItems.output_lines.add_line_to_output(0, f"{line}", FormatLine.dont_format_line) 210 | 211 | # Let user know that we have not mapped the remaining items 212 | # PrimeItems.output_lines.add_line_to_output( 213 | # 0, 214 | # "The remaining preferences are not yet mapped or are unused.", 215 | # ["", "preferences_color", FormatLine.add_end_span], 216 | # ) 217 | PrimeItems.output_lines.add_line_to_output(0, "", FormatLine.dont_format_line) 218 | -------------------------------------------------------------------------------- /maptasker/src/primitem.py: -------------------------------------------------------------------------------- 1 | """Prime items which are used throughout MapTasker (globals).""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # primitem = intialize PrimeItems which are used throughout MapTasker (globals). # 7 | # # 8 | # complete source code of licensed works and modifications which include larger works # 9 | # using a licensed work under the same license. Copyright and license notices must be # 10 | # preserved. Contributors provide an express grant of patent rights. # 11 | # # 12 | # Primary Items = global variables used throughout MapTasker 13 | # 14 | # Set up an initial empty dictionary of primary items used throughout this project 15 | # xml_tree = main xml element of our Tasker xml tree 16 | # xml_root = root xml element of our Tasker xml tree 17 | # program_arguments = runtime arguments entered by user and parsed. 18 | # See initparg.py for details. 19 | # colors_to_use = colors to use in the output 20 | # tasker_root_elements = root elements for all Projects/Profiles/Tasks/Scenes 21 | # output_lines = class for all lines added to output thus far 22 | # found_named_items = names/found-flags for single (if any) Project/Profile/Task 23 | # file_to_get = file object/name of Tasker backup file to read and parse 24 | # grand_totals = Total count of Projects/Profiles/Named Tasks Unnamed Task etc. 25 | # task_count_for_profile = number of Tasks in the specific Profile for Project 26 | # being processed 27 | # named_task_count_total = number of named Tasks for Project being processed 28 | # task_count_unnamed = number of unnamed Tasks for Project being processed 29 | # task_count_no_profile = number of Profiles in Project being processed. 30 | # directory_items = if displaying a directory then this is a dictionary of items 31 | # for the directory 32 | # name_list = list of names of Projects/Profiles/Tasks/Scenes found thus far 33 | # displaying_named_tasks_not_in_profile = True if we are displaying False if not 34 | # mono_fonts = dictionary of monospace fonts from TkInter 35 | # grand_totals = used for trcaking number of Projects/Profiles/Tasks/Scenes 36 | # tasker_root_elements points to our root xml for Projects/Profiles/Tasks/Scenes 37 | # directories = points to our directory items if we are displaying a directory 38 | # variables = Tasker variables. 39 | # current_project = current Project being processed 40 | # tkroot = root for Tkinter (can only get it once) 41 | # last_run = date of last run (set by restore_settings) 42 | # slash = backslash for Windows or forward slash for OS X and Linux. 43 | # 44 | # return 45 | from __future__ import annotations 46 | 47 | from typing import ClassVar 48 | 49 | from maptasker.src.sysconst import NOW_TIME 50 | 51 | 52 | class PrimeItems: 53 | """PrimeItems class contains global variables used throughout MapTasker""" 54 | 55 | ai_analyze = False 56 | ai: ClassVar = { 57 | "do_ai": False, 58 | "model": "", 59 | "output_lines": [], # Saved output results if doing an AI run. 60 | "api_key": "", 61 | "openai_key": "", 62 | "claude_key": "", 63 | "deepseek_key": "", 64 | "gemini_key": "", 65 | } 66 | xml_tree = None 67 | xml_root = None 68 | program_arguments: ClassVar[dict] = {} 69 | colors_to_use: ClassVar[dict] = {} 70 | output_lines: ClassVar = None 71 | file_to_get = "" 72 | file_to_use = "" 73 | task_count_for_profile = 0 74 | displaying_named_tasks_not_in_profile = False 75 | error_code = 0 76 | error_msg = "" 77 | mono_fonts: ClassVar = {} 78 | found_named_items: ClassVar[dict] = { 79 | "single_project_found": False, 80 | "single_profile_found": False, 81 | "single_task_found": False, 82 | } 83 | grand_totals: ClassVar[dict] = { 84 | "projects": 0, 85 | "profiles": 0, 86 | "unnamed_tasks": 0, 87 | "named_tasks": 0, 88 | "scenes": 0, 89 | } 90 | directory_items: ClassVar[dict] = { 91 | "current_item": "", 92 | "projects": [], 93 | "profiles": [], 94 | "tasks": [], 95 | "scenes": [], 96 | } 97 | tasker_root_elements: ClassVar[dict] = { 98 | "all_projects": [], 99 | "all_profiles": {}, 100 | "all_scenes": {}, 101 | "all_tasks": {}, 102 | "all_tasks_by_name": {}, 103 | "all_services": [], 104 | } 105 | directories: ClassVar[list] = [] 106 | variables: ClassVar[dict] = {} 107 | current_project = "" 108 | tkroot = None 109 | last_run = NOW_TIME 110 | slash = "/" 111 | task_action_warnings: ClassVar[dict] = {} 112 | tasker_action_codes: ClassVar[dict] = {} 113 | tasker_arg_specs: ClassVar[dict] = {} 114 | tasker_category_descriptions: ClassVar[dict] = {} 115 | tasker_event_codes: ClassVar[dict] = {} 116 | tasker_state_codes: ClassVar[dict] = {} 117 | 118 | 119 | # Reset all values 120 | class PrimeItemsReset: 121 | """Re-initialize all values in PrimeItems class""" 122 | 123 | def __init__(self) -> None: 124 | """ 125 | Initialize the PrimeItems class 126 | Args: 127 | self: The instance of the class 128 | Returns: 129 | None 130 | Initializes all attributes of the PrimeItems class with empty values or dictionaries: 131 | - Sets found_named_items flags to False 132 | - Initializes grand_totals and directory_items dictionaries 133 | - Initializes tasker_root_elements dictionary 134 | - Sets other attributes like xml_tree, program_arguments etc to empty values 135 | """ 136 | PrimeItems.found_named_items = { 137 | "single_project_found": False, 138 | "single_profile_found": False, 139 | "single_task_found": False, 140 | } 141 | PrimeItems.grand_totals = { 142 | "projects": 0, 143 | "profiles": 0, 144 | "unnamed_tasks": 0, 145 | "named_tasks": 0, 146 | "scenes": 0, 147 | } 148 | PrimeItems.directory_items = { 149 | "current_item": "", 150 | "projects": [], 151 | "profiles": [], 152 | "tasks": [], 153 | "scenes": [], 154 | } 155 | PrimeItems.tasker_root_elements = { 156 | "all_projects": [], 157 | "all_profiles": {}, 158 | "all_scenes": {}, 159 | "all_tasks": {}, 160 | "all_tasks_by_name": {}, 161 | "all_services": [], 162 | } 163 | PrimeItems.directories = [] 164 | PrimeItems.xml_tree = None 165 | PrimeItems.xml_root = None 166 | PrimeItems.program_arguments = {} 167 | PrimeItems.colors_to_use = {} 168 | PrimeItems.output_lines = None 169 | PrimeItems.file_to_get = "" 170 | PrimeItems.task_count_for_profile = 0 171 | PrimeItems.displaying_named_tasks_not_in_profile = False 172 | PrimeItems.mono_fonts = {} 173 | PrimeItems.directories = [] 174 | PrimeItems.variables = {} 175 | PrimeItems.current_project = "" 176 | PrimeItems.error_code = 0 177 | PrimeItems.error_msg = "" 178 | PrimeItems.tkroot = None 179 | PrimeItems.ai_analyze = False 180 | PrimeItems.ai = { 181 | "do_ai": False, 182 | "output_lines": [], 183 | "api_key": "", 184 | "openai_key": "", 185 | "claude_key": "", 186 | "deepseek_key": "", 187 | "gemini_key": "", 188 | } 189 | PrimeItems.task_action_warnings = {} 190 | -------------------------------------------------------------------------------- /maptasker/src/priority.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # ########################################################################################## # 4 | # # 5 | # priority: Get Profile/Task priority # 6 | # # 7 | # GNU General Public License v3.0 # 8 | # Permissions of this strong copyleft license are conditioned on making available # 9 | # complete source code of licensed works and modifications, which include larger works # 10 | # using a licensed work, under the same license. Copyright and license notices must be # 11 | # preserved. Contributors provide an express grant of patent rights. # 12 | # # 13 | # ########################################################################################## # 14 | import defusedxml.ElementTree # Need for type hints 15 | 16 | 17 | def get_priority(element: defusedxml.ElementTree.XML, event: bool) -> str: 18 | """ 19 | Get any associated priority for the Task/Profile 20 | :param element: root element to search for 21 | :param event: True if this is for an 'Event' condition, False if not 22 | :return: the priority or none 23 | """ 24 | 25 | priority_element = element.find("pri") 26 | if priority_element is None: 27 | return "" 28 | elif event: 29 | return f' Priority:{priority_element.text}' 30 | else: 31 | return f'   [Priority: {priority_element.text}]' 32 | -------------------------------------------------------------------------------- /maptasker/src/progargs.py: -------------------------------------------------------------------------------- 1 | """Process runtime program arguments""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # progargs: process program runtime arguments for MapTasker # 7 | # # 8 | # MIT License Refer to https://opensource.org/license/mit # 9 | 10 | from maptasker.src.config import GUI 11 | from maptasker.src.primitem import PrimeItems 12 | from maptasker.src.runcli import process_cli 13 | from maptasker.src.rungui import process_gui 14 | from maptasker.src.sysconst import DEBUG_PROGRAM 15 | 16 | 17 | # Get the program arguments (e.g. python mapit.py -x) 18 | def get_program_arguments() -> None: 19 | # Are we using the GUI? 20 | """ 21 | Process program arguments from GUI or CLI. 22 | Args: 23 | GUI: Whether GUI is being used 24 | DEBUG_PROGRAM: Whether program is in debug mode 25 | Returns: 26 | None: No return value 27 | - Parse GUI for program arguments and colors if GUI is being used 28 | - Parse command line if no GUI 29 | - Override debug argument to True if in debug mode""" 30 | if GUI: 31 | PrimeItems.program_arguments["gui"] = True 32 | 33 | # Process the command line runtime options 34 | process_cli() 35 | 36 | # Do GUI processing if GUI is being used 37 | if GUI: 38 | process_gui(True) 39 | 40 | # Make sure we don't have too much 41 | if ( 42 | (PrimeItems.program_arguments["single_project_name"] and PrimeItems.program_arguments["single_profile_name"]) 43 | or (PrimeItems.program_arguments["single_project_name"] and PrimeItems.program_arguments["single_task_name"]) 44 | or (PrimeItems.program_arguments["single_profile_name"] and PrimeItems.program_arguments["single_task_name"]) 45 | ): 46 | # More than one single item wasd specified in saved file. Set all to blank 47 | PrimeItems.program_arguments["single_task_name"] = "" 48 | PrimeItems.program_arguments["single_project_name"] = "" 49 | PrimeItems.program_arguments["single_profile_name"] = "" 50 | 51 | # Are we in development mode? If so, override debug argument 52 | if DEBUG_PROGRAM: 53 | PrimeItems.program_arguments["debug"] = True 54 | -------------------------------------------------------------------------------- /maptasker/src/property.py: -------------------------------------------------------------------------------- 1 | """Handle Object Properties""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # property: get Project/Profile/Task properties and output them # 7 | # # 8 | import defusedxml.ElementTree # Need for type hints 9 | 10 | from maptasker.src.actione import fix_json 11 | from maptasker.src.error import rutroh_error 12 | from maptasker.src.primitem import PrimeItems 13 | from maptasker.src.sysconst import FormatLine 14 | 15 | 16 | # Helper function to get text safely 17 | def get_text(element: defusedxml.ElementTree) -> str: 18 | """Return value or""" 19 | return element.text if element is not None else "" 20 | 21 | 22 | # Parse Property's variable and output it 23 | def parse_variable( 24 | property_tag: str, 25 | css_attribute: str, 26 | variable_header: defusedxml.ElementTree, 27 | cooldown: int, 28 | limit: int, 29 | ) -> None: 30 | """ 31 | Parses the variable header of a property tag and outputs the properties of the variable. 32 | Properties are identied in the XML with the tag: , where xxxx is Project/Profile/Task 33 | 34 | Args: 35 | property_tag (str): The property tag of the variable. 36 | css_attribute (str): The CSS attribute of the variable. 37 | variable_header (defusedxml.ElementTree): The XML element representing the variable header. 38 | cooldown (int): The cooldown time in seconds. 39 | limit (int): Limit repeats. 40 | 41 | Returns: 42 | None 43 | """ 44 | # Variable type definitions 45 | variable_type_lookup = { 46 | "yn": "Yes or No", 47 | "t": "Text", 48 | "b": "True or False", 49 | "f": "File", 50 | "n": "Number", 51 | "onoff": "On or Off", 52 | "fs": "File (System)", 53 | "fss": "Files (System)", 54 | "i": "Image", 55 | "is": "Images", 56 | "d": "Directory", 57 | "ds": "Directory (System)", 58 | "ws": "WiFi SSID", 59 | "wm": "WiFi MAC", 60 | "bn": "Bluetooth device's name", 61 | "bm": "Bluetooth device's MAC", 62 | "c": "Contact", 63 | "cn": "Contact Number", 64 | "cg": "Contact or Contact Group", 65 | "ti": "Time", 66 | "da": "Date", 67 | "a": "App", 68 | "as": "Apps", 69 | "la": "Launcher", 70 | "cl": "Color", 71 | "ln": "Language", 72 | "ttsv": "Text to Speech voice", 73 | "can": "Calendar", 74 | "cae": "Calendar Entry", 75 | "tz": "Time Zone", 76 | "ta": "Task", 77 | "prf": "Profile", 78 | "prj": "Project", 79 | "scn": "Scene", 80 | "cac": "User Certificate", 81 | } 82 | # Extract values from XML once 83 | fields = { 84 | "clearout": variable_header.find("clearout"), 85 | "immutable": variable_header.find("immutable"), 86 | "pvci": variable_header.find("pvci"), 87 | "pvd": variable_header.find("pvd"), 88 | "pvv": variable_header.find("pvv"), 89 | "pvdn": variable_header.find("pvdn"), 90 | "strout": variable_header.find("strout"), 91 | "pvn": variable_header.find("pvn"), 92 | "exportval": variable_header.find("exportval"), 93 | "pvt": variable_header.find("pvt"), 94 | } 95 | 96 | # Mapping field values to output strings. They are in the order as displayed in Tasker. 97 | components = [ 98 | f"Variable:{get_text(fields['pvn'])}, " if get_text(fields["pvn"]) else "", 99 | "Configure on Import, " if get_text(fields["pvci"]) != "false" else "", 100 | "Structured Variable (JSON, etc.), " if get_text(fields["strout"]) != "false" else "", 101 | "Immutable, " if get_text(fields["immutable"]) != "false" else "", 102 | f"Clear Out:{get_text(fields['clearout'])}, " if get_text(fields["clearout"]) != "false" else "", 103 | f"Prompt:{get_text(fields['pvd'])}, " if get_text(fields["pvd"]) else "", 104 | f"Value:{get_text(fields['pvv'])}, " if get_text(fields["pvv"]) else "", 105 | f"Display Name:{get_text(fields['pvdn'])}, " if get_text(fields["pvdn"]) else "", 106 | ] 107 | 108 | # Determine exported value 109 | exported_value = "Same as Value" if get_text(fields["pvn"]) == "1" else get_text(fields["exportval"]) 110 | components.append(f"Exported Value:{exported_value}, " if exported_value else "") 111 | 112 | # Get the variable type 113 | variable_type_code = get_text(fields["pvt"]) 114 | variable_type = variable_type_lookup.get(variable_type_code, variable_type_code) 115 | if variable_type_code not in variable_type_lookup: 116 | rutroh_error(f"Unknown variable type: {variable_type_code}") 117 | # Make sure the 'type' goes at the beginning. 118 | components.insert(0, f"Variable Type:{variable_type}, " if variable_type else "") 119 | 120 | # Additional attributes 121 | if limit: 122 | components.append(f"Limit Repeats:{limit}, ") 123 | if cooldown: 124 | components.append(f"Cooldown Time (seconds):{cooldown}, ") 125 | 126 | # Final output string 127 | out_string = f"
{property_tag} Properties..." + "".join(components) + "
\n" 128 | 129 | # Make it pretty 130 | blank = " " 131 | if PrimeItems.program_arguments["pretty"]: 132 | number_of_blanks = 20 if out_string.startswith("
Task") else 23 133 | out_string = out_string.replace(",", f"
{blank * number_of_blanks}") 134 | 135 | # Put the line '"Structure Output (JSON, etc)' back together. 136 | out_string = fix_json(out_string, " Structured Variable") 137 | 138 | # Ok, output the line. 139 | PrimeItems.output_lines.add_line_to_output( 140 | 2, 141 | out_string, 142 | ["", css_attribute, FormatLine.add_end_span], 143 | ) 144 | 145 | 146 | # Figure out which CSS attribute to insert into the output 147 | def get_css_attributes(property_tag: str) -> str: 148 | """ 149 | Get the CSS attribute based on the property tag. 150 | 151 | Args: 152 | property_tag (str): The property tag to determine the CSS attribute for. 153 | 154 | Returns: 155 | str: The CSS attribute corresponding to the property tag. 156 | """ 157 | if property_tag == "Project:": 158 | css_attribute = "project_color" 159 | elif property_tag == "Task:": 160 | css_attribute = "task_color" 161 | else: 162 | css_attribute = "profile_color" 163 | 164 | return css_attribute 165 | 166 | 167 | # Given the xml header to the Project/Profile/Task, get the properties belonging 168 | # to this header and write them out. 169 | def get_properties(property_tag: str, header: defusedxml.ElementTree) -> None: 170 | """ 171 | 172 | Args: 173 | property_tag (str): Either "Project:", "Profile:", or "Task:" 174 | header (defusedxml.ElementTree): xml header to Project/Profile/Task 175 | 176 | Returns: 177 | nothing 178 | """ 179 | collision = ["Abort New Task", "Abort Existing Task", "Run Both Together"] 180 | have_property = False 181 | 182 | # Get our HTML / CSS attributes 183 | css_attribute = get_css_attributes(property_tag) 184 | 185 | # Get the item comment, if any. Don't process it if we already have it 186 | comment_xml = header.find("pc") 187 | if comment_xml is not None: 188 | out_string = f"
{property_tag} Properties comment: {comment_xml.text}" 189 | PrimeItems.output_lines.add_line_to_output( 190 | 2, 191 | out_string, 192 | ["", css_attribute, FormatLine.add_end_span], 193 | ) 194 | have_property = True 195 | 196 | keep_alive = header.find("stayawake") 197 | if keep_alive is not None: 198 | out_string = f"{property_tag} Properties Keep Device Awake: {keep_alive.text}" 199 | PrimeItems.output_lines.add_line_to_output( 200 | 2, 201 | out_string, 202 | ["", css_attribute, FormatLine.add_end_span], 203 | ) 204 | have_property = True 205 | 206 | collision_handling = header.find("rty") 207 | if collision_handling is not None: 208 | out_string = f"{property_tag} Properties Collision Handling: {collision[int(collision_handling.text)]}" 209 | PrimeItems.output_lines.add_line_to_output( 210 | 2, 211 | out_string, 212 | ["", css_attribute, FormatLine.add_end_span], 213 | ) 214 | have_property = True 215 | 216 | # Look for variables in the head XML object (Project/Profile/Task). 217 | cooldown = "" 218 | limit = "" 219 | for item in header: 220 | if item.tag == "cldm": 221 | cooldown = item.text 222 | if item.tag == "limit": 223 | limit = item.text 224 | if item.tag == "ProfileVariable": 225 | parse_variable(property_tag, css_attribute, item, cooldown, limit) 226 | have_property = True 227 | 228 | # Force a new line if we output any properties. 229 | if have_property: 230 | PrimeItems.output_lines.add_line_to_output( 231 | 5, 232 | "
", 233 | FormatLine.dont_format_line, 234 | ) 235 | -------------------------------------------------------------------------------- /maptasker/src/rungui.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # rungui: process GUI for MapTasker # 5 | # # 6 | # Add the following statement (without quotes) to your Terminal Shell config file. # 7 | # (BASH, Fish, etc.) to eliminate the runtime msg: # 8 | # DEPRECATION WARNING: The system version of Tk is deprecated ... # 9 | # "export TK_SILENCE_DEPRECATION = 1" # 10 | # # 11 | # MIT License Refer to https://opensource.org/license/mit # 12 | from __future__ import annotations 13 | 14 | import contextlib 15 | import sys 16 | 17 | from maptasker.src.colrmode import set_color_mode 18 | from maptasker.src.error import error_handler 19 | from maptasker.src.getputer import save_restore_args 20 | from maptasker.src.initparg import initialize_runtime_arguments 21 | from maptasker.src.primitem import PrimeItems 22 | from maptasker.src.sysconst import ARGUMENT_NAMES, logger 23 | 24 | 25 | # ################################################################################ 26 | # Convert a value to integere, and if not an integer then use default value 27 | # ################################################################################ 28 | def convert_to_integer(value_to_convert: str, default_value: int) -> int: 29 | """ 30 | Convert a value to integere, and if not an integer then use default value 31 | Args: 32 | value_to_convert (str): The string value to convert to an integer 33 | where_to_put_it (int): Where to place the converted integer 34 | default_value (int): The default to plug in if the value to convert 35 | is not an integer 36 | :return: converted value as integer""" 37 | try: 38 | return int(value_to_convert) 39 | except (ValueError, TypeError): 40 | return default_value 41 | 42 | 43 | # Get the colors to use. 44 | def do_colors(user_input: dict) -> dict: 45 | """Sets color mode and processes colors. 46 | Parameters: 47 | - user_input (dict): User input dictionary containing appearance mode and color lookup. 48 | Returns: 49 | - colormap (dict): Dictionary of colors after processing. 50 | Processing Logic: 51 | - Set color mode based on user input. 52 | - Process color lookup if provided. 53 | - Set flag for GUI usage.""" 54 | 55 | # Appearance change: Dark or Light mode? 56 | colormap = set_color_mode(user_input.appearance_mode) 57 | 58 | # Process the colors 59 | if user_input.color_lookup: 60 | for key, value in user_input.color_lookup.items(): 61 | colormap[key] = value 62 | 63 | PrimeItems.program_arguments["gui"] = True # Set flag to indicate we are using GUI 64 | 65 | return colormap 66 | 67 | 68 | # Get the program arguments from GUI 69 | def process_gui(use_gui: bool) -> tuple[dict, dict]: 70 | # global MyGui 71 | """Parameters: 72 | - use_gui (bool): Flag to indicate whether to use GUI or not. 73 | Returns: 74 | - tuple[dict, dict]: Tuple containing program arguments and colors to use. 75 | Processing Logic: 76 | - Import MyGui if use_gui is True. 77 | - Set flag to indicate GUI usage. 78 | - Delete previous Tkinter window if it exists. 79 | - Display GUI and get user input. 80 | - Initialize runtime arguments if not already set. 81 | - If user clicks "Exit" button, save settings and exit program. 82 | - If user closes window, cancel program. 83 | - If user clicks "Run" button, get input from GUI variables. 84 | - Set program arguments in dictionary. 85 | - Convert display_detail_level and indent to integers. 86 | - Get font from GUI. 87 | - Return program arguments and colors to use.""" 88 | # Keep this here to avoid circular import 89 | if use_gui: 90 | from maptasker.src.userintr import MyGui 91 | 92 | PrimeItems.program_arguments["gui"] = True # Set flag to indicate we are using GUI 93 | 94 | # Get rid of any previous Tkinter window 95 | if PrimeItems.tkroot is not None: 96 | del PrimeItems.tkroot 97 | PrimeItems.tkroot = None 98 | # Display GUI and get the user input 99 | user_input = MyGui() 100 | user_input.mainloop() 101 | # Get rid of window 102 | MyGui.quit(user_input) 103 | del MyGui 104 | 105 | # Establish our runtime default values if we don't yet have 'em. 106 | if not PrimeItems.colors_to_use: 107 | PrimeItems.program_arguments = initialize_runtime_arguments() 108 | 109 | # Has the user closed the window? 110 | if not user_input.go_program and not user_input.rerun and not user_input.exit: 111 | error_handler("Program canceled by user (killed GUI)", 100) 112 | 113 | # 'Run' button hit. Get all the input from GUI variables 114 | PrimeItems.program_arguments["gui"] = True 115 | # Do we already have the file object? 116 | if value := user_input.file: 117 | PrimeItems.file_to_get = value if isinstance(value, str) else value.name 118 | 119 | # Hide the Ai key so when settings are saved, it isn't written to toml file. 120 | if user_input.ai_apikey is not None and user_input.ai_apikey: 121 | PrimeItems.ai["api_key"] = user_input.ai_apikey 122 | PrimeItems.program_arguments["ai_apikey"] = "HIDDEN" 123 | 124 | # Get the program arguments and save them in our dictionary 125 | for value in ARGUMENT_NAMES: 126 | with contextlib.suppress(AttributeError): 127 | PrimeItems.program_arguments[value] = getattr(user_input, value) 128 | logger.info(f"GUI arg: {value} set to: {getattr(user_input, value)}") 129 | 130 | # Convert display_detail_level to integer 131 | PrimeItems.program_arguments["display_detail_level"] = convert_to_integer( 132 | PrimeItems.program_arguments["display_detail_level"], 133 | 4, 134 | ) 135 | # Convert indent to integer 136 | PrimeItems.program_arguments["indent"] = convert_to_integer(PrimeItems.program_arguments["indent"], 4) 137 | # Get the font 138 | if the_font := user_input.font: 139 | PrimeItems.program_arguments["font"] = the_font 140 | 141 | # If user selected the "Exit" button, call it quits. 142 | if user_input.exit: 143 | # Save the runtijme settings first. 144 | _, _ = save_restore_args(PrimeItems.program_arguments, PrimeItems.colors_to_use, True) 145 | # Spit out the message and log it. 146 | error_handler("Program exited. Goodbye.", 0) 147 | sys.exit(0) 148 | 149 | # Return the program arguments and colors to use. 150 | return (PrimeItems.program_arguments, do_colors(user_input)) 151 | -------------------------------------------------------------------------------- /maptasker/src/share.py: -------------------------------------------------------------------------------- 1 | """Handle TaskerNet "Share" information""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # share: process TaskerNet "Share" information # 7 | # # 8 | import defusedxml.ElementTree # Need for type hints 9 | 10 | from maptasker.src.format import format_html 11 | from maptasker.src.primitem import PrimeItems 12 | from maptasker.src.sysconst import FormatLine 13 | 14 | 15 | # Go through xml elements to grab and output TaskerNet description and 16 | # search-on lines. 17 | def share( 18 | root_element: defusedxml.ElementTree, 19 | tab: str, 20 | ) -> None: 21 | """ 22 | Go through xml elements to grab and output TaskerNet description and search-on lines 23 | :param root_element: beginning xml element (e.g. Project or Task) 24 | :param tab: "projtab", "proftab" or "tasktab" 25 | """ 26 | # Get the element, if any 27 | share_element: defusedxml.ElementTree = root_element.find("Share") 28 | if share_element is not None: 29 | # We have a . Find the description 30 | description_element = share_element.find("d") 31 | # Process the description 32 | if description_element is not None: 33 | description_element_output( 34 | description_element, 35 | tab, 36 | ) 37 | 38 | # Look for TaskerNet search parameters 39 | search_element = share_element.find("g") 40 | if search_element is not None and search_element.text: 41 | # Found search...format and output 42 | out_string = format_html( 43 | "taskernet_color", 44 | "", 45 | f"\n
TaskerNet search on: {search_element.text}\n
", 46 | True, 47 | ) 48 | # Add the tab CSS call to the color. 49 | out_string = PrimeItems.output_lines.add_tab(tab, out_string) 50 | PrimeItems.output_lines.add_line_to_output( 51 | 2, 52 | f"
{out_string}
", 53 | FormatLine.dont_format_line, 54 | ) 55 | 56 | # Force a break when done with last Share element, only if there isn't one there already. 57 | break_html = "" if PrimeItems.output_lines.output_lines[-1] == "
" else "
" 58 | PrimeItems.output_lines.add_line_to_output( 59 | 0, 60 | f"{break_html}", 61 | FormatLine.dont_format_line, 62 | ) 63 | 64 | # Now get rid of the last duplicate
lines at the bottom of the output. 65 | for num, item in reversed( 66 | list(enumerate(PrimeItems.output_lines.output_lines)), 67 | ): 68 | if "TaskerNet description:" in item: 69 | break 70 | if item == "
" and PrimeItems.output_lines.output_lines[num - 1] == "
": 71 | PrimeItems.output_lines.output_lines.remove(num) 72 | break 73 | if tab != "proftab" and item.endswith("

"): 74 | PrimeItems.output_lines.output_lines[-1] = item.replace( 75 | "

", 76 | "
", 77 | ) 78 | break 79 | 80 | 81 | # ################################################################################ 82 | # Process the description element 83 | # ################################################################################ 84 | def description_element_output( 85 | description_element: defusedxml.ElementTree, 86 | tab: str, 87 | ) -> None: 88 | """ 89 | We have a Taskernet description (). Clean it up and add it to the output list. 90 | 91 | :param description_element: xml element TaskerNet description. 92 | :param tab: CSS tab class name to apply to the color HTML. 93 | """ 94 | # We need to properly format this since it has embedded stuff that screws it up 95 | out_string = format_html( 96 | "taskernet_color", 97 | "", 98 | f"TaskerNet description: {description_element.text}", 99 | True, 100 | ) 101 | 102 | # Replace all of the Taskernet imbedded HTML with our HTML. 103 | indent_html = f'
' 104 | 105 | # Indent the description and override various embedded HTML attributes 106 | out_string = out_string.replace("

", indent_html) 107 | out_string = out_string.replace("

", indent_html) 108 | out_string = out_string.replace("

", "") 109 | out_string = out_string.replace("

", "") 110 | out_string = out_string.replace("", "") 111 | out_string = out_string.replace("
", indent_html) 112 | out_string = out_string.replace("

", indent_html) 113 | out_string = out_string.replace("\r", indent_html) 114 | out_string = out_string.replace("
  • ", indent_html) 115 | out_string = out_string.replace("
  • ", "") 116 | out_string = out_string.replace("", "") 117 | out_string = out_string.replace("\n\n", "

    ") # N 118 | out_string = out_string.replace("\n", "
    ") # New line break. 119 | out_string = out_string.replace(" ", "
    ") # Break after two blanks. 120 | out_string = out_string.replace("- ", "
    ") # Break after dash blank. 121 | 122 | out_string = out_string.replace( 123 | "", 124 | ( 125 | "\n\n
    ' 128 | ), 129 | ) 130 | 131 | # Add the tab CSS call to the color. 132 | out_string = PrimeItems.output_lines.add_tab(tab, out_string) 133 | 134 | # Output the description line. 135 | PrimeItems.output_lines.add_line_to_output( 136 | 2, 137 | f"{out_string}", 138 | FormatLine.dont_format_line, 139 | ) 140 | -------------------------------------------------------------------------------- /maptasker/src/shelsort.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # tasks: shell_sort Sort Actions, args and misc. # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | 8 | from maptasker.src.sysconst import logger 9 | 10 | 11 | # Shell sort for Action list (Actions are not necessarily in numeric order in XML backup file). 12 | def shell_sort(arr: list, do_arguments: bool, by_numeric: bool) -> None: 13 | """ 14 | Shell sort the list in-place 15 | Args: 16 | arr: The list to sort 17 | do_arguments: Whether to treat elements as arguments 18 | by_numeric: Whether to sort numerically 19 | Returns: 20 | None 21 | Processing Logic: 22 | 1. Set the gap size initially as half of list size 23 | 2. Keep reducing gap size by half until it reaches 1 24 | 3. For current gap size, check all pairs of elements gap positions apart and swap if out of order 25 | 4. Repeat step 3 for all elements in list with current gap 26 | """ 27 | n = len(arr) 28 | gap = n // 2 29 | while gap > 0: 30 | j = gap 31 | # Check the array in from left to right 32 | # Till the last possible index of j 33 | while j < n: 34 | i = j - gap # This will keep help in maintain gap value 35 | while i >= 0: 36 | if do_arguments: 37 | # Get the n from as a number for comparison purposes 38 | attr1 = arr[i] 39 | attr2 = arr[i + gap] 40 | val1 = attr1.attrib.get("sr", "") 41 | val2 = attr2.attrib.get("sr", "") 42 | if val1[3:] == "" or val2[3:] == "": # 'if' argument...skip 43 | break 44 | comp1 = val1[3:] 45 | comp2 = val2[3:] 46 | else: 47 | # General list sort 48 | comp1 = arr[i] 49 | comp2 = arr[i + gap] 50 | # Sort by value or numeric(value)? 51 | if by_numeric: 52 | comp1 = int(comp1) 53 | comp2 = int(comp2) 54 | # If value on right side is already greater than left side value 55 | # We don't do swap else we swap 56 | if not comp1.isdigit() or not comp2.isdigit(): 57 | logger.debug(f"MapTasker.py:shell_sort: comp1:{comp1!s} comp2:{comp2!s}") 58 | if do_arguments and int(comp2) > int(comp1) or not do_arguments and comp2 > comp1: 59 | break 60 | arr[i + gap], arr[i] = arr[i], arr[i + gap] 61 | i = i - gap # To check left side also 62 | # If the element present is greater than current element 63 | j += 1 64 | gap = gap // 2 65 | # We are done 66 | -------------------------------------------------------------------------------- /maptasker/src/taskactn.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """ 3 | taskactn: deal with Task Actions 4 | 5 | MIT License Refer to https://opensource.org/license/mit 6 | """ 7 | 8 | # # 9 | # taskactn: deal with Task Actions # 10 | # # 11 | # MIT License Refer to https://opensource.org/license/mit # 12 | from __future__ import annotations 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | import maptasker.src.tasks as tasks # noqa: PLR0402 17 | from maptasker.src.error import error_handler 18 | from maptasker.src.guiutils import get_taskid_from_unnamed_task 19 | from maptasker.src.maputils import ( 20 | count_consecutive_substr, 21 | count_unique_substring, 22 | get_value_if_match, 23 | ) 24 | from maptasker.src.primitem import PrimeItems 25 | from maptasker.src.sysconst import UNNAMED_ITEM, FormatLine 26 | 27 | if TYPE_CHECKING: 28 | import defusedxml.ElementTree 29 | UNNAMED = " (Unnamed)" 30 | 31 | 32 | def ensure_argument_alignment(taction: str) -> str: 33 | """ 34 | Ensure that the arguments of the action are aligned correctly. 35 | Args: 36 | taction: {str}: Action text 37 | Returns: 38 | str: Correctly aligned action text 39 | """ 40 | action_breakdown = taction.replace("\n", "
    ").split("
    ") 41 | if len(action_breakdown) > 1: 42 | count_of_spaces = count_consecutive_substr(action_breakdown[1], " ") 43 | correct_spacing = " " * count_of_spaces 44 | for index, arg in enumerate(action_breakdown[2:]): 45 | # action_breakdown[index + 2] = remove_html_tags(arg.strip(), "") 46 | action_breakdown[index + 2] = arg.strip() 47 | if count_consecutive_substr(arg, " ") != count_of_spaces: 48 | action_breakdown[index + 2] = action_breakdown[index + 2].replace( 49 | " ", 50 | "", 51 | ) 52 | # Handle {DISABLED] task indicator at end of config parameters. 53 | if action_breakdown[index + 2] == "[⛔DISABLED]": 54 | correct_spacing = " " * (count_of_spaces - 18) 55 | 56 | # Now add the correct number of spaces to the start of the line 57 | action_breakdown[index + 2] = f"{correct_spacing}{action_breakdown[index + 2]}" 58 | # Put it all back together. 59 | taction = "
    ".join(action_breakdown) 60 | return taction 61 | 62 | 63 | # Go through list of actions and output them 64 | def output_list_of_actions( 65 | action_count: int, 66 | alist: list, 67 | the_item: defusedxml.ElementTree, 68 | ) -> None: 69 | """ 70 | Output the list of Task Actions 71 | 72 | Parameters: 73 | :param action_count: count of Task actions 74 | :param alist: list of task actions 75 | :param the_item: the specific Task's detailed line 76 | 77 | Returns: the count of the number of times the program has been called 78 | """ 79 | 80 | # Go through all Actions in Task Action list 81 | for taction in alist: 82 | # 'taction' has the Action text, including all of it's arguments. 83 | if taction is not None: 84 | # Optimize spacing if 'pretty' is enabled 85 | if PrimeItems.program_arguments.get("pretty"): 86 | updated_action = ensure_argument_alignment(taction) 87 | else: 88 | updated_action = taction 89 | 90 | # If Action continued ("...continued"), output it 91 | if updated_action[:3] == "...": 92 | PrimeItems.output_lines.add_line_to_output( 93 | 2, 94 | f"Action: {updated_action}", 95 | ["", "action_color", FormatLine.dont_add_end_span], 96 | ) 97 | else: 98 | # First remove one blank from action number if line number is > 99 and < 1000 99 | updated_action = ( 100 | updated_action.replace(" ", "", 1) 101 | if action_count > 99 and action_count < 1000 102 | else updated_action 103 | ) 104 | 105 | # Output the Action count = line number of action (fill to 2 leading zeros) 106 | PrimeItems.output_lines.add_line_to_output( 107 | 2, 108 | f"Action: {str(action_count).zfill(2)} {updated_action}", 109 | ["", "action_color", FormatLine.dont_add_end_span], 110 | ) 111 | action_count += 1 112 | if ( 113 | action_count == 2 and PrimeItems.program_arguments["display_detail_level"] == 0 and UNNAMED in the_item 114 | ): # Just show first Task if unknown Task 115 | break 116 | if PrimeItems.program_arguments["display_detail_level"] == 1 and UNNAMED not in the_item: 117 | break 118 | 119 | # Close Action list if doing straight print, no twisties 120 | if not PrimeItems.program_arguments["twisty"]: 121 | PrimeItems.output_lines.add_line_to_output(3, "", FormatLine.dont_format_line) 122 | 123 | 124 | # For this specific Task, get its Actions and output the Task and Actions 125 | def get_task_actions_and_output( 126 | the_task: defusedxml.ElementTree, 127 | list_type: str, 128 | the_item: str, 129 | tasks_found: list[str], 130 | ) -> None: 131 | # If Unknown task or displaying more detail, then 'the_task' is not valid, and we have to find it. 132 | """ 133 | Get task actions and output. 134 | Args: 135 | the_task: {Task xml element}: Task xml element 136 | list_type: {str}: Type of list 137 | the_item: {str}: Item being displayed 138 | tasks_found: {list[str]}: Tasks found so far 139 | Returns: 140 | None: No return value 141 | {Processing Logic}: 142 | 1. Check if task is unknown or detail level is high, find task ID 143 | 2. Get task xml element from ID 144 | 3. Get task actions from xml element 145 | 4. Output actions list with formatting 146 | 5. Handle errors if no task found 147 | """ 148 | # If the Task is unnamed or we are doing more detail, find the Task. 149 | if UNNAMED in the_item or PrimeItems.program_arguments["display_detail_level"] > 0: 150 | # Get the Task name so that we can get the Task xml element 151 | # "--Task:" denotes a Task in a Scene which we will handle below 152 | if UNNAMED in the_item: 153 | index = the_item.find(UNNAMED) 154 | task_name = the_item[: index + len(UNNAMED)] 155 | else: 156 | temp_id = "x" if "--Task:" in list_type else the_item.split("Task ID: ") 157 | task_name = temp_id[0].split(" ")[0] 158 | # Find the Task 159 | the_task, task_id = get_value_if_match( 160 | PrimeItems.tasker_root_elements["all_tasks"], 161 | "name", 162 | task_name, 163 | "xml", 164 | ) 165 | 166 | # Get the Task name from the ID if it wasn't found above. 167 | if the_task is None and task_name == "x": 168 | the_task = PrimeItems.tasker_root_elements["all_tasks"][the_item]["xml"] 169 | if the_task is None and UNNAMED_ITEM in the_item: 170 | task_id = get_taskid_from_unnamed_task(the_item) 171 | the_task = PrimeItems.tasker_root_elements["all_tasks"][task_id]["xml"] 172 | task_name = PrimeItems.tasker_root_elements["all_tasks"][the_item]["name"] 173 | task_id = the_item 174 | 175 | # It is a valid Task. If unknown and it is an Entry or Exit (valid) task, add it to the count of unnamed Tasks. 176 | elif UNNAMED in the_item and ("Entry Task" in the_item or "Exit" in the_item): 177 | PrimeItems.task_count_unnamed += 1 178 | 179 | # Keep tabs on the tasks processed so far. 180 | if task_id not in tasks_found: 181 | tasks_found.append(task_id) 182 | 183 | # Get Task actions 184 | if the_task is not None: 185 | # If we have Task Actions, then output them. The action list is a list of the Action output lines already 186 | # formatted. 187 | if alist := tasks.get_actions(the_task): 188 | # Track the task and action count if too many actions. 189 | action_count = len(alist) - count_unique_substring( 190 | alist, 191 | "...indent=", 192 | ) 193 | # Add the Task to our warning limit dictionary. 194 | if ( 195 | PrimeItems.program_arguments["task_action_warning_limit"] < 100 196 | and action_count > PrimeItems.program_arguments["task_action_warning_limit"] 197 | and task_name not in PrimeItems.task_action_warnings 198 | ): 199 | PrimeItems.task_action_warnings[task_name] = { 200 | "count": action_count, 201 | "id": task_id, 202 | } 203 | 204 | # Start a list of Actions 205 | PrimeItems.output_lines.add_line_to_output( 206 | 1, 207 | "", 208 | FormatLine.dont_format_line, 209 | ) 210 | action_count = 1 211 | output_list_of_actions(action_count, alist, the_item) 212 | # End list if Scene Task 213 | if "--Task:" in list_type: 214 | PrimeItems.output_lines.add_line_to_output( 215 | 3, 216 | "", 217 | FormatLine.dont_format_line, 218 | ) 219 | if PrimeItems.program_arguments["twisty"]: 220 | PrimeItems.output_lines.add_line_to_output( 221 | 3, 222 | "", 223 | FormatLine.dont_format_line, 224 | ) 225 | # End the list of Actions 226 | PrimeItems.output_lines.add_line_to_output( 227 | 3, 228 | "", 229 | FormatLine.dont_format_line, 230 | ) 231 | else: 232 | error_handler("No Task found!!!", 0) 233 | -------------------------------------------------------------------------------- /maptasker/src/taskerd.py: -------------------------------------------------------------------------------- 1 | """Read in XML""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # taskerd: get Tasker data from backup xml # 7 | # # 8 | 9 | import defusedxml.ElementTree as ET # noqa: N817 10 | 11 | from maptasker.src import condition 12 | from maptasker.src.actione import get_action_code 13 | from maptasker.src.error import error_handler 14 | from maptasker.src.primitem import PrimeItems 15 | from maptasker.src.profiles import conditions_to_name 16 | from maptasker.src.sysconst import UNNAMED_ITEM, FormatLine 17 | from maptasker.src.xmldata import rewrite_xml 18 | 19 | 20 | # Convert list of xml to dictionary 21 | # Optimized 22 | def move_xml_to_table(all_xml: list, get_id: bool, name_qualifier: str) -> dict: 23 | """ 24 | Given a list of Profile/Task/Scene elements, find each name and store the element and name in a dictionary. 25 | :param all_xml: the head xml element for Profile/Task/Scene 26 | :param get_id: True if we are to get the 27 | :param name_qualifier: the qualifier to find the element's name. 28 | :return: dictionary that we created 29 | """ 30 | new_table = {} 31 | for item in all_xml: 32 | # Get the element name 33 | name_element = item.find(name_qualifier) 34 | name = name_element.text.strip() if name_element is not None and name_element.text else "" 35 | 36 | # Get the Profile/Task identifier: id=number for Profiles and Tasks, 37 | id_element = item.find("id") 38 | item_id = id_element.text if get_id and id_element is not None else name 39 | 40 | new_table[item_id] = {"xml": item, "name": name} 41 | 42 | all_xml.clear() # Ok, we're done with the list 43 | return new_table 44 | 45 | 46 | # Load all of the Projects, Profiles and Tasks into a format we can easily 47 | # navigate through. 48 | # Optimized 49 | def get_the_xml_data() -> bool: 50 | # Put this code into a while loop in the event we have to re-call it again. 51 | """Gets the XML data from a Tasker backup file and returns it in a dictionary. 52 | Parameters: 53 | - None 54 | Returns: 55 | - int: 0 if successful, 1 if bad XML, 2 if not a Tasker backup file, 3 if not a valid Tasker backup file. 56 | Processing Logic: 57 | - Put code into a while loop in case it needs to be re-called. 58 | - Defines XML parser with ISO encoding. 59 | - If encoding error, rewrites XML with proper encoding and tries again. 60 | - If any other error, logs and exits. 61 | - Returns 1 if bad XML and not in GUI mode. 62 | - Returns 1 if bad XML and in GUI mode. 63 | - Gets XML root. 64 | - Checks for valid Tasker backup file. 65 | - Moves all data into dictionaries. 66 | - Returns all data in a dictionary.""" 67 | file_to_parse = PrimeItems.file_to_get.name 68 | counter = 0 69 | 70 | while True: 71 | try: 72 | xmlp = ET.XMLParser(encoding="utf-8") 73 | PrimeItems.xml_tree = ET.parse(file_to_parse, parser=xmlp) 74 | break 75 | except (ET.ParseError, UnicodeDecodeError) as e: 76 | counter += 1 77 | if counter > 2 or isinstance(e, ET.ParseError): 78 | error_handler(f"Error in {file_to_parse}: {e}", 1) 79 | return 1 80 | rewrite_xml(file_to_parse) 81 | 82 | if PrimeItems.xml_tree is None: 83 | return 1 if not PrimeItems.program_arguments["gui"] else _handle_gui_error("Bad XML file") 84 | 85 | PrimeItems.xml_root = PrimeItems.xml_tree.getroot() 86 | if PrimeItems.xml_root.tag != "TaskerData": 87 | return _handle_gui_error("Invalid Tasker backup XML file", code=3) 88 | 89 | # Extract and transform data 90 | PrimeItems.tasker_root_elements = { 91 | "all_projects": move_xml_to_table( 92 | PrimeItems.xml_root.findall("Project"), 93 | False, 94 | "name", 95 | ), 96 | "all_profiles": move_xml_to_table( 97 | PrimeItems.xml_root.findall("Profile"), 98 | True, 99 | "nme", 100 | ), 101 | "all_tasks": move_xml_to_table( 102 | PrimeItems.xml_root.findall("Task"), 103 | True, 104 | "nme", 105 | ), 106 | "all_scenes": move_xml_to_table( 107 | PrimeItems.xml_root.findall("Scene"), 108 | False, 109 | "nme", 110 | ), 111 | "all_services": PrimeItems.xml_root.findall("Setting"), 112 | } 113 | 114 | # Assign names to Profiles that have no name = their condition.nnn (Unnamed) 115 | for key, value in PrimeItems.tasker_root_elements["all_profiles"].items(): 116 | if not value["name"]: 117 | # The Profile doen't have a name. Name it using it's conditions. 118 | profile_name = UNNAMED_ITEM 119 | profile_xml = value["xml"] 120 | if profile_conditions := condition.parse_profile_condition(profile_xml): 121 | # fmt: off 122 | _, profile_name, profile_conditions = conditions_to_name( 123 | profile_xml, 124 | profile_conditions, 125 | profile_name, 126 | "", 127 | ) 128 | # fmt: on 129 | 130 | # CLean up the new name 131 | profile_name = profile_name.replace("", "").replace("", "") 132 | 133 | PrimeItems.tasker_root_elements["all_profiles"][key]["name"] = profile_name 134 | 135 | # Get Tasks by name and handle Tasks with no name. 136 | PrimeItems.tasker_root_elements["all_tasks_by_name"] = {} 137 | for key, value in PrimeItems.tasker_root_elements["all_tasks"].items(): 138 | if not value["name"]: 139 | # Get the first Task Action and user it as the Task name. 140 | first_action = get_first_action(value["xml"]) 141 | 142 | # Put the new name back into PrimeItems.tasker_root_elements["all_tasks"] 143 | value["name"] = f"{first_action.rstrip()}.{key!s} (Unnamed)" 144 | 145 | PrimeItems.tasker_root_elements["all_tasks_by_name"][value["name"]] = { 146 | "xml": value["xml"], 147 | "id": key, 148 | } 149 | 150 | # Sort them for easier debug. 151 | temp = sorted(PrimeItems.tasker_root_elements["all_tasks"].items()) 152 | PrimeItems.tasker_root_elements["all_tasks"] = dict(temp) 153 | temp = sorted(PrimeItems.tasker_root_elements["all_tasks_by_name"].items()) 154 | PrimeItems.tasker_root_elements["all_tasks_by_name"] = dict(temp) 155 | return 0 156 | 157 | 158 | def _handle_gui_error(message: str, code: int = 1) -> int: 159 | PrimeItems.output_lines.add_line_to_output(0, message, FormatLine.dont_format_line) 160 | if PrimeItems.program_arguments["gui"]: 161 | PrimeItems.error_msg = message 162 | return code 163 | 164 | 165 | def get_first_action(task: ET) -> str: 166 | """ 167 | Retrieve the name of the first action code from a Tasker task XML element. 168 | 169 | Args: 170 | task (ET.ElementTree): The XML element representing a Tasker task. 171 | 172 | Returns: 173 | str: The name of the first action's code if found, otherwise an empty string. 174 | 175 | Processing Logic: 176 | - Finds all "Action" elements within the task. 177 | - Searches for the first action with attribute sr="act0". 178 | - If found, retrieves the "code" child element of that action. 179 | - Looks up the action code in the action_codes dictionary and returns its name. 180 | - Returns an empty string if no suitable action is found. 181 | """ 182 | # Build the Taskere argument codes dictionary if we don't yet have it. 183 | if not PrimeItems.tasker_arg_specs: 184 | from maptasker.src.proginit import build_action_codes 185 | 186 | build_action_codes(False) 187 | 188 | task_actions = task.findall("Action") 189 | if task_actions is not None: 190 | have_first_action = False 191 | # Go through Actions looking for the first one ("act0") 192 | for action in task_actions: 193 | action_number = action.attrib.get("sr") 194 | if action_number == "act0": 195 | have_first_action = True 196 | break 197 | 198 | if not have_first_action: 199 | return "" 200 | 201 | # Now get the Action code 202 | child = action.find("code") 203 | the_result = get_action_code(child, action, True, "t") 204 | from maptasker.src.maputils import remove_html_tags 205 | 206 | clean_text = remove_html_tags(the_result) 207 | clean_text = ( 208 | clean_text.replace("  ", " ") 209 | .replace("( ", "(") 210 | .replace("(", "") 211 | .replace(")", "") 212 | .replace(" ", " ") 213 | .replace("...with label: ", "") 214 | .replace("<", "{") 215 | .replace(">", "}") 216 | ) 217 | # Truncate the string at 30 charatcers. 218 | from maptasker.src.maputils import truncate_string 219 | 220 | return truncate_string(clean_text, 30) 221 | return "" 222 | -------------------------------------------------------------------------------- /maptasker/src/taskflag.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # taskflag: Get Profile/Task fags: priority, collision, stay awake # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | import defusedxml.ElementTree # Need for type hints 8 | 9 | 10 | def get_priority(element: defusedxml.ElementTree, event: bool) -> str: 11 | """ 12 | Get any associated priority for the Task/Profile 13 | :param element: root element to search for 14 | :param event: True if this is for an 'Event' condition, False if not 15 | :return: the priority or none 16 | """ 17 | 18 | priority_element = element.find("pri") 19 | if priority_element is None: 20 | return "" 21 | if event: 22 | return f" Priority:{priority_element.text}" 23 | return f"  [Priority: {priority_element.text}]" 24 | 25 | 26 | def get_collision(element: defusedxml.ElementTree) -> str: 27 | """ 28 | Get any Task collision setting 29 | :param element: root element to search for 30 | :return: the collision setting as text or blank 31 | """ 32 | 33 | collision_element = element.find("rty") 34 | # No collision tag = default = Abort Task on collision (we'll leave it blank) 35 | if collision_element is None: 36 | return "" 37 | collision_flag = collision_element.text or "" 38 | if collision_flag == "1": 39 | collision_text = "Abort Existing Task" 40 | elif collision_flag == "2": 41 | collision_text = "Run both together" 42 | else: 43 | collision_text = "Abort New Task" 44 | 45 | return f"  [Collision: {collision_text}]" 46 | 47 | 48 | def get_awake(element: defusedxml.ElementTree) -> str: 49 | """ 50 | Get any Task Stay Awake (Keep Device Awake) setting 51 | :param element: root element to search for 52 | :return: the stay awake setting as text or blank 53 | """ 54 | 55 | awake_element = element.find("stayawake") 56 | return "" if awake_element is None else "  [Keep Device Awake]" 57 | -------------------------------------------------------------------------------- /maptasker/src/taskuniq.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | """Process Unique Task Situations""" 3 | # # 4 | # taskuniq: deal with unique Tasks # 5 | # # 6 | # MIT License Refer to https://opensource.org/license/mit # 7 | 8 | from maptasker.src.primitem import PrimeItems 9 | from maptasker.src.sysconst import NO_PROJECT, NORMAL_TAB, FormatLine 10 | from maptasker.src.tasks import ( 11 | get_project_for_solo_task, 12 | output_task_list, 13 | ) 14 | from maptasker.src.twisty import add_twisty, remove_twisty 15 | 16 | 17 | # Output Projects Without Tasks and Projects Without Profiles 18 | def process_missing_tasks_and_profiles( 19 | projects_with_no_tasks: list, 20 | projects_without_profiles: list, 21 | ) -> None: 22 | """ 23 | Output Projects Without Tasks and Projects Without Profiles 24 | all Tasker xml root elements, and a list of all output lines. 25 | :param projects_with_no_tasks: root xml entry for list of Projects with no Tasks 26 | :param projects_without_profiles: root xml entry for list of Projects with no Profiles 27 | :return: nothing 28 | """ 29 | 30 | # List Projects with no Tasks 31 | if len(projects_with_no_tasks) > 0 and not PrimeItems.found_named_items["single_task_found"]: 32 | PrimeItems.output_lines.add_line_to_output( 33 | 1, 34 | f"{NORMAL_TAB}
    {NORMAL_TAB}Projects Without Tasks...
    ", 35 | ["", "trailing_comments_color", FormatLine.add_end_span], 36 | ) 37 | 38 | for item in projects_with_no_tasks: 39 | PrimeItems.output_lines.add_line_to_output( 40 | 0, 41 | f"Project {item} has no Named Tasks", 42 | ["", "trailing_comments_color", FormatLine.add_end_span], 43 | ) 44 | # End list 45 | PrimeItems.output_lines.add_line_to_output( 46 | 3, 47 | "
    ", 48 | FormatLine.dont_format_line, 49 | ) 50 | 51 | # List all Projects without Profiles 52 | if projects_without_profiles: 53 | # Add heading 54 | PrimeItems.output_lines.add_line_to_output( 55 | 1, 56 | f"{NORMAL_TAB}Projects Without Profiles...
    ", 57 | ["
    ", "trailing_comments_color", FormatLine.add_end_span], 58 | ) 59 | for item in projects_without_profiles: 60 | PrimeItems.output_lines.add_line_to_output( 61 | 0, 62 | f"- Project '{item}' has no Profiles", 63 | ["", "trailing_comments_color", FormatLine.add_end_span], 64 | ) 65 | # End list 66 | PrimeItems.output_lines.add_line_to_output( 67 | 3, 68 | "
    ", 69 | FormatLine.dont_format_line, 70 | ) 71 | 72 | 73 | # Add heading to output for named Tasks not in any Profile 74 | def add_heading(save_twisty: bool) -> bool: 75 | """ 76 | Add a header to the output for the solo Tasks 77 | 78 | :param save_twisty: flag to indicate whether or not we are doing the twisty/hidden Tasks 79 | :return: True...flag that the heading has been created/output 80 | """ 81 | 82 | # Start a list and add a ruler-line across page 83 | PrimeItems.output_lines.add_line_to_output(1, "
    ", FormatLine.dont_format_line) 84 | text_line = f"{NORMAL_TAB}Named Tasks that are not called by any Profile...
    " 85 | 86 | # Add a twisty, if doing twisties, to hide the line 87 | if save_twisty: 88 | add_twisty("trailing_comments_color", text_line) 89 | 90 | # Add the header 91 | PrimeItems.output_lines.add_line_to_output( 92 | 1, 93 | text_line, 94 | ["", "trailing_comments_color", FormatLine.add_end_span], 95 | ) 96 | PrimeItems.displaying_named_tasks_not_in_profile = True 97 | PrimeItems.output_lines.add_line_to_output( 98 | 1, 99 | "", 100 | FormatLine.dont_format_line, 101 | ) # Start Task list 102 | return True 103 | 104 | 105 | # Process a single Task that does not belong to any Profile 106 | # This function is called recursively 107 | def process_solo_task_with_no_profile( 108 | task_id: str, 109 | found_tasks: list, 110 | task_count: int, 111 | have_heading: bool, 112 | projects_with_no_tasks: list, 113 | save_twisty: bool, 114 | ) -> tuple: 115 | """ 116 | Process a single Task that does not belong to any Profile 117 | 118 | :param task_id: the ID of the Task being displayed 119 | :param found_tasks: list of Tasks that we have found 120 | :param task_count: count of the unnamed Tasks 121 | :param have_heading: whether we have the heading 122 | :param projects_with_no_tasks: list of Projects without Tasks 123 | :param save_twisty: whether we are displaying twisty to Hide Task details 124 | :return: heading flag, xml element for this Task, and total count of unnamed Tasks 125 | """ 126 | unknown_task, specific_task = False, False 127 | 128 | # Get the Project this Task is under. 129 | project_name, _ = get_project_for_solo_task( 130 | task_id, 131 | projects_with_no_tasks, 132 | ) 133 | 134 | # Bump the count of Tasks and get the Task's details 135 | task_details = "" 136 | task_count += 1 137 | 138 | # At this point, we've found the Project this Task belongs to, 139 | # or it doesn't belong to any Profile 140 | if not have_heading and PrimeItems.program_arguments["display_detail_level"] > 2: 141 | # Add the heading to the output 142 | have_heading = add_heading(save_twisty) 143 | if not unknown_task and project_name != NO_PROJECT: 144 | if PrimeItems.program_arguments["debug"]: 145 | task_details += f" with Task ID: {task_id} ...in Project '{project_name}'  > No Profile" 146 | else: 147 | task_details += f" ...in Project '{project_name}'  > No Profile" 148 | 149 | # Output the Task's details 150 | if (not unknown_task) and ( 151 | PrimeItems.program_arguments["display_detail_level"] > 2 152 | ): # Only list named Tasks or if details are wanted. 153 | task_output_lines = [task_details] # Return as a list. 154 | 155 | # We have the Tasks. Now let's output them. 156 | our_task = PrimeItems.tasker_root_elements["all_tasks"][task_id] 157 | specific_task = output_task_list( 158 | [our_task], 159 | project_name, 160 | "", 161 | task_output_lines, 162 | found_tasks, 163 | False, 164 | ) 165 | 166 | return have_heading, specific_task, task_count 167 | 168 | 169 | # process_tasks: go through all tasks and output them 170 | def process_tasks_not_called_by_profile( 171 | projects_with_no_tasks: list, 172 | found_tasks_list: list, 173 | ) -> None: 174 | """ 175 | Go through all tasks and output those that are not called by any Profile. 176 | This is only called if we are not doing a single named item. 177 | :param projects_with_no_tasks: list of Project xml roots for which there are no Tasks 178 | :param found_tasks_list: list of all Tasks found so far 179 | :return: nothing 180 | """ 181 | task_count = 0 182 | task_name = "" 183 | have_heading = False 184 | # We only need twisty for top level, starting with the heading 185 | save_twisty = PrimeItems.program_arguments["twisty"] 186 | PrimeItems.program_arguments["twisty"] = False 187 | 188 | # Go through all Tasks, one at a time, and see if this one is not in it (not found) 189 | for task_id in PrimeItems.tasker_root_elements["all_tasks"]: 190 | # If we just processed a single task only, then bail out. 191 | if PrimeItems.found_named_items["single_task_found"]: 192 | break 193 | 194 | # We have a solo Task not associated to any Profile 195 | if task_id not in found_tasks_list: 196 | # Theoretcally, we should never get here. 197 | have_heading, specific_task, task_count = process_solo_task_with_no_profile( 198 | task_id, 199 | found_tasks_list, 200 | task_count, 201 | have_heading, 202 | projects_with_no_tasks, 203 | save_twisty, 204 | ) 205 | 206 | if ( 207 | specific_task 208 | or PrimeItems.program_arguments["single_task_name"] 209 | == PrimeItems.tasker_root_elements["all_tasks"][task_id]["name"] 210 | ): 211 | PrimeItems.found_named_items["single_task_found"] = True 212 | break 213 | 214 | # End the twisty hidden Task list. Remove it and restore the setting. 215 | if save_twisty: 216 | remove_twisty() 217 | PrimeItems.program_arguments["twisty"] = save_twisty 218 | 219 | # Provide spacing and end list if we have Tasks 220 | if task_count > 0: 221 | if PrimeItems.program_arguments["display_detail_level"] > 0: 222 | PrimeItems.output_lines.add_line_to_output( 223 | 0, 224 | "", 225 | FormatLine.dont_format_line, 226 | ) # blank line 227 | PrimeItems.output_lines.add_line_to_output( 228 | 3, 229 | "", 230 | FormatLine.dont_format_line, 231 | ) # Close Task list 232 | 233 | if task_name is True: 234 | PrimeItems.output_lines.add_line_to_output( 235 | 3, 236 | "", 237 | FormatLine.dont_format_line, 238 | ) # Close Task list 239 | 240 | PrimeItems.output_lines.add_line_to_output( 241 | 3, 242 | "", 243 | FormatLine.dont_format_line, 244 | ) # Close out the list 245 | -------------------------------------------------------------------------------- /maptasker/src/twisty.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | twisty: add special twisty to output 5 | 6 | MIT License Refer to https://opensource.org/license/mit 7 | """ 8 | 9 | # # 10 | # twisty: add special twisty to output # 11 | # # 12 | # MIT License Refer to https://opensource.org/license/mit # 13 | from maptasker.src.primitem import PrimeItems 14 | from maptasker.src.sysconst import FormatLine 15 | 16 | 17 | def add_twisty(output_color_name: str, line_to_output: str) -> None: 18 | """ 19 | Add the necessary html to hide the follow-on text under a twisty ► 20 | 21 | :param output_color_name: name of the color to insert into the html 22 | :param line_to_output: text line to output into the html 23 | :return: nothing. Add formatted html with the twisty magic 24 | """ 25 | # Add the "twisty" to hide the Task details 26 | PrimeItems.output_lines.add_line_to_output( 27 | 5, 28 | f"\n
    {line_to_output}\r", 29 | # f"\n
    {line_to_output}\r", 30 | ["", output_color_name, FormatLine.add_end_span], 31 | ) 32 | 33 | 34 | def remove_twisty() -> None: 35 | """ 36 | Add the html element to stop the hidden items..so the follow-up stuff is not hidden 37 | 38 | :return: nothing. The output line is modified to include "
    " 39 | """ 40 | PrimeItems.output_lines.output_lines[-1] = "

    \n" 41 | -------------------------------------------------------------------------------- /maptasker/src/xmldata.py: -------------------------------------------------------------------------------- 1 | """Module containing action runner logic.""" 2 | 3 | #! /usr/bin/env python3 4 | 5 | # # 6 | # xmldata: deal with the xml data # 7 | # # 8 | import os 9 | import shutil 10 | 11 | import defusedxml.ElementTree 12 | 13 | 14 | # See if the xml tag is one of the predefined types and return result 15 | def tag_in_type(tag: str, flag: bool) -> bool: 16 | """ 17 | Evaluate the xml tag to see if it is one of our predefined types 18 | 19 | Parameters: the tag to evaluate, and whether this is a Scene or not (which 20 | determines which list of types to look for) 21 | 22 | Returns: True if tag found, False otherwise 23 | """ 24 | scene_task_element_types = { 25 | "ListElementItem", 26 | "ListElement", 27 | "TextElement", 28 | "ImageElement", 29 | "ButtonElement", 30 | "OvalElement", 31 | "EditTextElement", 32 | "RectElement", 33 | "WebElement", 34 | "CheckBoxElement", 35 | "DoodleElement", 36 | "PickerElement", 37 | "SceneElement", 38 | "SliderElement", 39 | "SpinnerElement", 40 | "SwitchElement", 41 | "ToggleElement", 42 | "VideoElement", 43 | "PropertiesElement", # this element doesn't contain anything of value/import 44 | } 45 | scene_task_click_types = { 46 | "checkchangeTask", 47 | "clickTask", 48 | "focuschangeTask", 49 | "itemselectedTask", 50 | "keyTask", 51 | "linkclickTask", 52 | "longclickTask", 53 | "mapclickTask", 54 | "maplongclickTask", 55 | "pageloadedTask", 56 | "strokeTask", 57 | "valueselectedTask", 58 | "videoTask", 59 | "itemclickTask", 60 | "itemlongclickTask", 61 | } 62 | # Return a boolean: True if tag found in the appropriate list, False otherwise 63 | return (flag and tag in scene_task_element_types) or (not flag and tag in scene_task_click_types) # Boolean 64 | 65 | 66 | # We have an integer. Evaluaate it's value based oon the code's evaluation parameters. 67 | def extract_integer( 68 | code_action: defusedxml.ElementTree, 69 | the_arg: str, 70 | argeval: str, 71 | arg: list, 72 | ) -> str: 73 | """ 74 | Extract an integer value from an XML action element. 75 | 76 | Args: 77 | code_action (XML element): The XML action element to search. 78 | arg (str): The name of the argument to search for. 79 | argeval (str | list): The evaluation to perform on the integer. 80 | arg: (list): The list of arguments for this action from action_codes. 81 | 82 | Returns: 83 | str: The result of the integer evaluation. 84 | """ 85 | from maptasker.src.action import drop_trailing_comma, process_xml_list 86 | 87 | # Find the first matching element with the desired 'sr' attribute 88 | int_element = next( 89 | (child for child in code_action if child.tag == "Int" and child.attrib.get("sr") == the_arg), 90 | None, 91 | ) 92 | 93 | if int_element is None: 94 | return "" # No matching element found 95 | 96 | # Extract value or variable 97 | the_int_value = int_element.attrib.get("val") or ( 98 | int_element.find("var").text if int_element.find("var") is not None else "" 99 | ) 100 | 101 | if not the_int_value: 102 | return "" # No valid integer or variable name found 103 | 104 | # If this is a boolean and the integer is 0, then return the empty string 105 | if arg[3] == "3" and the_int_value == "0": 106 | return "" 107 | 108 | # Process the integer evaluation 109 | if isinstance(argeval, list): 110 | result = [] 111 | if len(argeval) > 1: 112 | # Handle the special case of "e" by adding a space before the value..expects a blank in element 0. 113 | new_argeval = ["", "e", argeval[1]] if argeval[0] == "e" else argeval 114 | # Handle special case of 'l' lookup. 115 | new_argeval = [f"{arg[2]}=", "l", argeval[2]] if arg[2] and argeval[1] == "l" else new_argeval 116 | else: 117 | new_argeval = argeval 118 | 119 | # Process the argument evaluation 120 | process_xml_list([new_argeval], 0, the_int_value, result, [the_arg]) 121 | final_result = " ".join(result) 122 | elif isinstance(argeval, str): 123 | if argeval[-1] != "=": 124 | argeval += "=" 125 | final_result = argeval + the_int_value 126 | else: 127 | final_result = argeval + the_int_value 128 | 129 | # Drop trailing comma if necessary 130 | return drop_trailing_comma([final_result])[0] if final_result else "" 131 | 132 | 133 | # Extracts and returns the text from the given argument as a string. 134 | def extract_string(action: defusedxml.ElementTree, arg: str, argeval: str) -> str: 135 | """ 136 | Extracts a string from an XML action element. 137 | 138 | Args: 139 | action (XML element): The XML action element to search. 140 | arg (str): The name of the string argument to search for. 141 | argeval (str): The prefix to add to the matched string. 142 | 143 | Returns: 144 | str: Extracted string with prefix or an empty string. 145 | """ 146 | from maptasker.src.action import drop_trailing_comma 147 | 148 | # Find the first matching element with the desired 'sr' attribute 149 | str_element = next( 150 | (child for child in action.findall("Str") if child.attrib.get("sr") == arg), 151 | None, 152 | ) 153 | 154 | if str_element is None or str_element.text is None: 155 | return "" # No matching element found 156 | 157 | # Extract text value with prefix 158 | new_argeval = f"{argeval}=" if argeval[-1] != "=" else argeval 159 | extracted_text = ( 160 | f"{argeval}(carriage return)" if str_element.text == "\n" else f"{new_argeval}{str_element.text or ''}" 161 | ) 162 | 163 | # Drop trailing comma if necessary 164 | return drop_trailing_comma([extracted_text])[0] if extracted_text else "" 165 | 166 | 167 | def tasker_object(text: str, blank_trailer: bool) -> bool: 168 | """ 169 | Checks if the input string contains any of the following keywords, 170 | where spaces are replaced with ' ': 171 | 'Task: ', 'Profile: ', 'Profile:$nbsp;', or 'Scene: '. 172 | 173 | Args: 174 | text: The string to be tested. 175 | blank_trailer: True if keyword followed by space, otherwise followed by ' ' 176 | 177 | Returns: 178 | True if any of the modified keywords are found in the text, False otherwise. 179 | """ 180 | keywords_nbsp = [ 181 | "Task: ", 182 | "Profile: ", 183 | "Profile:$nbsp;", 184 | "Scene: ", 185 | "Task ) tags from it 193 | def remove_html_tags(text: str, replacement: str) -> str: 194 | """ 195 | Remove html tags from a string 196 | :param text: text from which HTML is to be removed 197 | :param replacement: text to replace HTML with, if any 198 | :return: the text with HTML removed 199 | """ 200 | # If this is a Project/Profile/Task/Scene name, then we will leave the string asis. 201 | if tasker_object(text, False): 202 | return text 203 | 204 | # Go thru each character in the string and remove HTML tags 205 | result = [] 206 | in_tag = False 207 | n = len(text) 208 | i = 0 209 | # Iterate through the string character by character 210 | while i < n: 211 | char = text[i] 212 | if char == "<": 213 | if i + 1 < n and not text[i + 1].isspace(): 214 | in_tag = True 215 | i += 1 # Move past the '<' 216 | else: 217 | result.append(char) 218 | elif char == ">": 219 | if i > 0 and not text[i - 1].isspace(): 220 | in_tag = False 221 | else: 222 | result.append(char) 223 | elif not in_tag: 224 | result.append(char) 225 | i += 1 226 | 227 | # If we just have a '<' with no '>' then leave it alone. 228 | if in_tag: 229 | return text 230 | 231 | # Return the text with HTML tags removed 232 | # If the result is empty, return the replacement string 233 | return "".join(result) if result else replacement 234 | 235 | 236 | # Append file1 to file2 237 | def append_files(file1_path: str, file2_path: str) -> None: 238 | """Appends the contents of file1 to file2. 239 | Parameters: 240 | - file1_path (str): Path to file1. 241 | - file2_path (str): Path to file2. 242 | Returns: 243 | - None: No return value. 244 | Processing Logic: 245 | - Open file1 in read mode. 246 | - Open file2 in append mode. 247 | - Copy contents of file1 to file2.""" 248 | with open(file1_path) as file1, open(file2_path, "a") as file2: 249 | shutil.copyfileobj(file1, file2) 250 | 251 | 252 | # The XML file hs incorrect encoding. Let's read it in and rewrite it correctly. 253 | def rewrite_xml(file_to_parse: str) -> None: 254 | """Rewrite XML file with UTF-8 encoding. 255 | Parameters: 256 | - file_to_parse (str): Name of the file to be parsed. 257 | Returns: 258 | - None: No return value. 259 | Processing Logic: 260 | - Create new file with UTF-8 encoding. 261 | - Append, rename, and remove files. 262 | - Remove temporary file.""" 263 | utf_xml = '\n' 264 | 265 | # Create the XML file with the encoding we want 266 | with open(".maptasker_tmp.xml", "w") as new_file: 267 | new_file.write(utf_xml) 268 | new_file.close() 269 | 270 | # Append, rename and remove. 271 | append_files(file_to_parse, ".maptasker_tmp.xml") 272 | os.remove(file_to_parse) 273 | os.rename(".maptasker_tmp.xml", file_to_parse) 274 | os.remove(".maptasker_tmp.xml") 275 | -------------------------------------------------------------------------------- /maptasker_changelog.json: -------------------------------------------------------------------------------- 1 | {"version": "8.0.1", "change0": "### Added\n", "change1": "- Added: Full support for Tasker version 6.5.8/9.\n", "change2": "- Added: If selecting an unnamed Task to display from the single-name pulldown menu, display the owning Profile and Project names as well.\n", "change3": "### Changed\n", "change4": "- Changed:\n", "change5": "### Fixed\n", "change6": "- Fixed: Clicking a irectory hotlink can inadvertently go to a partial match of the Tasker object name.\n", "change7": "- Fixed: Unable to change the color for unnamed Tasks.\n", "change8": "- Fixed: Non-GUI mode abends in diagram.py.\n", "change9": "### Known Issues\n", "change10": "- Unable to change any colors in GUI if using UV to manage the application (UV bug).\n"} -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "maptasker" 3 | dynamic = [ "version" ] 4 | version = "8.0.1" 5 | description = "Utility to display your entire Android 'Tasker' configuration on your Desktop." 6 | requires-python = ">=3.11,<4.0" 7 | authors = [ 8 | { name = "Michael Rubin", email = "mikrubin@gmail.com" }, 9 | ] 10 | readme = "README_PyPl.md" 11 | license = "MIT License (MIT)" 12 | repository = "https://github.com/mctinker/Map-Tasker" 13 | # changelog = "https://github.com/mctinker/Map-Tasker/blob/Master/Changelog.md" 14 | keywords = [ 15 | "tasker", 16 | "Tasker", 17 | "map tasker", 18 | "configuration", 19 | "tasker configuration", 20 | "tasker", 21 | "tasker configuration" 22 | ] 23 | packages = [ 24 | { include = "maptasker"} 25 | ] 26 | include = [ 27 | "**/maptasker/**/*.py", 28 | "**/maptasker/assets/*.*", 29 | "**/sample.prj.xml" 30 | ] 31 | exclude = [ 32 | "run_test.py", 33 | "**/maptasker/**/backup.xml", 34 | "**/maptasker/maptasker.log", 35 | "**/maptasker/MapTasker.html", 36 | "**/maptasker/.MapTasker_RunCount.txt", 37 | "**/maptasker/.arguments.txt", 38 | "**/maptasker/**/__pycache__", 39 | "**/maptasker/**/.dep-tree.yml" 40 | ] 41 | dependencies = [ 42 | "anthropic>=0.52.0", # Ai Anthropics support 43 | "customtkinter>=5.2.2", # GUI 44 | "darkdetect>=0.8.0", # Appearance mode detection 45 | "defusedxml>=0.7.1", # More secure xml parser 46 | "google-generativeai>=0.8.5", # Ai Google Generative support 47 | "ollama>=0.4.8", # Ai Ollama support > cria rquires this 48 | "openai>=1.82.0", # Ai OpenAi support 49 | "pillow==11.2.1", # Image support in GUI. Revert back to 11.2.0 to avoid UV bug with tkinter. 50 | "psutil>=7.0.0", # System monitoring 51 | "requests>=2.32.3", # HTTP Server function request 52 | "tomli_w>=1.2.0", # Write toml file 53 | ] 54 | 55 | [project.scripts] 56 | maptasker = "maptasker.main:main" 57 | 58 | [project.urls] 59 | Homepage = "https://github.com/mctinker/Map-Tasker" 60 | "Bug Tracker" = "https://github.com/mctinker/Map-Tasker/issues" 61 | Changelog = "https://github.com/mctinker/Map-Tasker/CHANGELOG.md" 62 | 63 | 64 | [tool.poetry] 65 | requires-poetry = ">=2.0" 66 | 67 | [[tool.poetry.source]] 68 | name = "testpypi" 69 | url = "https://test.pypi.org/legacy/" 70 | priority = "primary" 71 | 72 | [[tool.poetry.source]] 73 | name = "PyPI" 74 | priority = "primary" 75 | 76 | [tool.ruff] 77 | include = ["pyproject.toml", "maptasker/src/**/*.py", "scripts/**/*.py"] 78 | respect-gitignore = true 79 | # editor.formatOnSaveMode = "modificationsIfAvailable" 80 | select = [ 81 | 'A', # Builtins 82 | 'ANN', # Annotations 83 | 'ARG', # Unused arguments 84 | 'B', # Bugbear 85 | 'BLE', # Blind except 86 | 'C4', # Comprehensions 87 | 'C90', # mccabe 88 | 'COM', # Commas 89 | 'D1', # Undocumented public elements 90 | # 'D2', # Docstring conventions 91 | 'D3', # Triple double quotes 92 | # 'D4', # Docstring text format 93 | 'DTZ', # Datetimes 94 | 'EM', # Error messages 95 | # 'ERA', # Commented-out code 96 | # 'EXE', # Executable 97 | 'F', # Pyflakes 98 | 'FA', # __future__ annotations 99 | 'FLY', # F-strings 100 | # 'FURB', # Refurb 101 | 'G', # Logging format 102 | 'I', # Isort 103 | 'ICN', # Import conventions 104 | 'INP', # Disallow PEP-420 (Implicit namespace packages) 105 | 'INT', # gettext 106 | 'ISC', # Implicit str concat 107 | # 'LOG', # Logging 108 | 'N', # PEP-8 Naming 109 | 'NPY', # Numpy 110 | 'PERF', # Unnecessary performance costs 111 | 'PGH', # Pygrep hooks 112 | 'PIE', # Unnecessary code 113 | 'PL', # Pylint 114 | 'PT', # Pytest 115 | # 'PTH', # Use Pathlib 116 | 'PYI', # Stub files 117 | 'Q', # Quotes 118 | 'RET', # Return 119 | 'RUF', # Ruff 120 | 'RSE', # Raise 121 | 'S', # Bandit 122 | 'SIM', # Code simplification 123 | 'SLF', # Private member access 124 | 'SLOT', # __slots__ 125 | 'T10', # Debugger 126 | 'T20', # Print 127 | 'TCH', # Type checking 128 | 'TID', # Tidy imports 129 | 'TRY', # Exception handling 130 | 'UP', # Pyupgrade 131 | 'W', # Warnings 132 | 'YTT', # sys.version 133 | ] 134 | # Exclude a variety of commonly ignored directories. 135 | exclude = [ 136 | ".direnv", 137 | "dist", 138 | ".eggs", 139 | ".git", 140 | ".git-rewrite", 141 | ".hg", 142 | ".mypy_cache", 143 | ".pyenv", 144 | ".pytest_cache", 145 | ".pytype", 146 | ".ruff_cache", 147 | ".venv", 148 | ".vscode", 149 | "__pypackages__", 150 | "_build", 151 | "build", 152 | "dist", 153 | "site-packages", 154 | "venv", 155 | ".md", 156 | "__init__.py", 157 | "**/maptasker/assets/*.*", 158 | "**/maptasker/MapTasker.html", 159 | "**/maptasker/.MapTasker_RunCount.txt", 160 | "**/maptasker/.arguments.txt", 161 | "**/maptasker/**/__pycache__", 162 | "**/maptasker/**/.dep-tree.yml" 163 | ] 164 | 165 | ignore = [ 166 | "PLR2004", # Constant value comparison 167 | "SIM115", # Missing "with" on oepn file 168 | "S606", # No shell 169 | "B009", # Do not perform function calls in argument defaults 170 | "T201", # Print found 171 | "ANN101", # Missing type annotation for self 172 | ] 173 | show-fixes = true 174 | src = ['src',] 175 | 176 | [tool.ruff.pycodestyle] 177 | ignore-overlong-task-comments = true 178 | 179 | [tool.ruff.flake8-quotes] 180 | docstring-quotes = 'double' 181 | multiline-quotes = 'double' 182 | 183 | [tool.ruff.mccabe] 184 | # Unlike Flake8, default to a complexity level of 15. 185 | max-complexity = 15 186 | 187 | [tool.ruff.per-file-ignores] 188 | # https://beta.ruff.rs/docs/rules/ 189 | '__init__.py' = ['F401','F403','F405',] 190 | 'tests/*' = ['ANN', 'ARG', 'INP001', 'S101',] 191 | 192 | [tool.ruff.pylint] 193 | max-args = 15 194 | max-branches = 20 195 | max-returns = 10 196 | max-statements = 80 197 | 198 | [tool.ruff.flake8-tidy-imports] 199 | ban-relative-imports = 'all' 200 | 201 | [tool.ruff.format] 202 | quote-style = "double" 203 | # indent-style = "tab" 204 | docstring-code-format = true 205 | # Like Black, automatically detect the appropriate line ending. 206 | line-ending = "auto" 207 | 208 | [tool.black] 209 | --line-length = 120 210 | line-length = 120 211 | 212 | [build-system] 213 | requires = ["poetry-core"] 214 | build-backend = "poetry.core.masonry.api" 215 | 216 | [dependency-groups] 217 | dev = [ 218 | "ruff>=0.11.6", 219 | ] 220 | 221 | [tool.pytest.ini_options] 222 | pythonpath = [ 223 | "." 224 | ] 225 | 226 | [tool.poetry.group.test.dependencies] 227 | pytest = "^7.3.2" 228 | pytest-mock = "*" 229 | 230 | # Bump version identifiers. Repeat this chunk for each file. 231 | [tool.poetry_bumpversion.replacements] 232 | files = ["maptasker/src/sysconst.py"] 233 | search = 'VERSION = "{current_version}"' 234 | replace = 'VERSION = "{new_version}"' 235 | 236 | 237 | 238 | [tool.poetry.group.dev.dependencies] 239 | black = "^24.10.0" 240 | # +black = { version = "*", allows-prereleases = true } 241 | line-profiler = "^4.1.3" 242 | vulture = "^2.13" 243 | poetry-bumpversion = "^0.3.3" 244 | pyinstrument = "^5.0.0" 245 | mypy = "^1.15.0" 246 | types-defusedxml = "^0.7.0" 247 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anthropic==0.52.0 2 | customtkinter==5.2.2 3 | darkdetect==0.8.0 4 | defusedxml==0.7.1 5 | distlib==0.3.9 6 | google-generativeai==0.8.5 7 | ollama==0.4.8 8 | openai==1.82.0 9 | pillow==11.2.1 10 | psutil==7.0.0 11 | requests==2.32.3 12 | tomli_w==1.2.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mctinker/Map-Tasker/7c05f09e791e5e4b7f01f8b603346a92b84b1772/tests/__init__.py -------------------------------------------------------------------------------- /tests/run_test.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # # 4 | # run_test: run MapTasker unit test routines # 5 | # # 6 | 7 | 8 | # Reference: https://github.com/Taskomater/Tasker-XML-Info # 9 | # # 10 | import time 11 | from unittest.mock import patch 12 | 13 | from maptasker.src import mapit 14 | 15 | 16 | def test_it(): 17 | """ 18 | Test the function 'test_it' by running it. 19 | 20 | This function is used to test the functionality of the 'test_it' function. It executes the following steps: 21 | 22 | 1. Prints the value of 'sys.argv'. 23 | 2. Calls the 'mapit.mapit_all' function with an empty string as the argument. 24 | 3. Pauses the execution for 1 second. 25 | 26 | This function does not take any parameters and does not return any values. 27 | """ 28 | # print('run_test sys.argv:', sys.argv) 29 | mapit.mapit_all("") 30 | # Take a breath between each run to avoid collision issues with browser 31 | time.sleep(1) 32 | 33 | 34 | # Run these in small chunks, depending on the size of the backup file being used. 35 | def test_main(): 36 | """ 37 | Test main function to test various scenarios using patch to simulate different sys.argv inputs. 38 | """ 39 | ip = "62" 40 | # # Test name attributes 41 | # with patch("sys.argv", ["-test=yes", "reset", "debug", "outline" ]): 42 | # test_it() 43 | # # Test name attributes 44 | # with patch("sys.argv", ["-test=yes", "reset", "detail=2", "debug", "names=bold highlight", "cHighlight LightBlue"]): 45 | # test_it() 46 | # with patch( 47 | # "sys.argv", ["-test=yes", "reset", "detail=1", "debug", "names=underline italicize", "font='Menlo'"], 48 | # ): 49 | # test_it() 50 | # # Test light mode 51 | # with patch("sys.argv", ["-test=yes", "reset", "detail=2", "debug", "appearance=light"]): 52 | # test_it() 53 | # # Test max detail 54 | # with patch("sys.argv", ["-test=yes", "reset", "detail=5", "debug"]): 55 | # test_it() 56 | # # Test max detail 57 | # with patch("sys.argv", ["-test=yes", "reset", "detail=4", "debug", "i=10"]): 58 | # test_it() 59 | # # Test full detail 60 | # with patch("sys.argv", ["-test=yes", "reset", "detail=3", "debug", "pretty"]): 61 | # test_it() 62 | # # Test limited detail 63 | # with patch("sys.argv", ["-test=yes", "reset", "detail=2", "debug"]): 64 | # test_it() 65 | # # Test limited detail 1 66 | # with patch("sys.argv", ["-test=yes", "reset", "detail=1", "debug"]): 67 | # test_it() 68 | # # Test no detail 69 | # with patch("sys.argv", ["-test=yes", "reset", "detail=0", "debug"]): 70 | # test_it() 71 | # # Test by Project name 72 | # with patch( 73 | # "sys.argv", ["-test=yes", "reset", "project=Base", "debug", "conditions", "taskernet"]): 74 | # test_it() 75 | # # Test by Profile name 76 | # with patch( 77 | # "sys.argv", 78 | # ["-test=yes", "reset", "profile=Call Volume", "detail=3", "debug", "pretty"]): 79 | # test_it() 80 | # # Test by Task name 81 | # with patch("sys.argv", ["-test=yes", "reset", "task=Check Batteries", "debug", "detail=4"]): 82 | # test_it() 83 | # # Test -pref 84 | # with patch("sys.argv", ["-test=yes", "reset", "preferences", "debug", "taskernet", "detail=2"]): 85 | # test_it() 86 | # # Test -dir 87 | # with patch("sys.argv", ["-test=yes", "reset", "directory", "debug", "taskernet", "detail=4"]): 88 | # test_it() 89 | # # Test new -everything with twisty and outline 90 | # with patch("sys.argv", ["-test=yes", "reset", "e"]): 91 | # test_it() 92 | # # Test fetch backup xml file 93 | # with patch( 94 | # "sys.argv", 95 | # [ 96 | # "-test=yes", "reset", 97 | # f"android_ipaddr=192.168.0.{ip}", "android_port=1821", "android_file=/Tasker/configs/user/backup.xml", 98 | # ], 99 | # ): 100 | # test_it() 101 | # # Test just a Profile 102 | # with patch( 103 | # "sys.argv", 104 | # [ 105 | # "-test=yes", "reset", 106 | # f"android_ipaddr=192.168.0.{ip}", "android_port=1821", "android_file=/Tasker/profiles/File_List.prf.xml", 107 | # ], 108 | # ): 109 | # test_it() 110 | # # Test just a Task 111 | # with patch( 112 | # "sys.argv", 113 | # [ 114 | # "-test=yes", "reset", 115 | # f"android_ipaddr=192.168.0.{ip}", "android_port=1821", "android_file=/Tasker/tasks/Setup_ADB_Permissions.tsk.xml", 116 | # ], 117 | # ): 118 | # test_it() 119 | # # Test just a Scene 120 | # with patch( 121 | # "sys.argv", 122 | # [ 123 | # "-test=yes", "reset", "pretty", 124 | # f"android_ipaddr=192.168.0.{ip}", "android_port=1821", "android_file=/Tasker/scenes/Lock.scn.xml", 125 | # ], 126 | # ): 127 | # test_it() 128 | # # Test colors 129 | # with patch( 130 | # "sys.argv", 131 | # [ 132 | # "-test=yes", "reset", 133 | # "cBackground=Black", 134 | # "cActionCondition=Yellow", 135 | # "cProfileCondition=Red", 136 | # "cActionLabel=White", 137 | # "cProfile=Yellow", 138 | # "cDisabledAction=Green", 139 | # "cLauncherTask=Purple", 140 | # "cActionName=White", 141 | # "cTask=Yellow", 142 | # "cUnknownTask=Green", 143 | # "cScene=Teal", 144 | # "cTaskerNetInfo=Violet", 145 | # "cProfile=Yellow", 146 | # "cDisabledProfile=Orange", 147 | # "cBullet=Red", 148 | # "cPreferences=Linen", 149 | # "cAction=Blue", 150 | # "cTrailingComments=LightGoldenrodYellow", 151 | # "e", 152 | # "debug", 153 | # ], 154 | # ): 155 | # test_it() 156 | 157 | # Test invalid runtime parameters 158 | 159 | # # Test bad IP address/port/file 160 | # with patch( 161 | # "sys.argv", 162 | # [ 163 | # "-test=yes", "reset", 164 | # "android_ipaddr192.168.0.6x", "android_port=1821", "android_file=/Tasker/configs/user/backup.xml", 165 | # ], 166 | # ): 167 | # test_it() 168 | 169 | 170 | if __name__ == "__main__": 171 | test_main() 172 | --------------------------------------------------------------------------------