├── 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\nName | \nValue | \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 | "
",
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 |
--------------------------------------------------------------------------------