Open-AutoTools 0.0.4rc1__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. autotools/autocaps/commands.py +21 -0
  2. autotools/autocolor/__init__.py +0 -0
  3. autotools/autocolor/commands.py +60 -0
  4. autotools/autocolor/core.py +99 -0
  5. autotools/autoconvert/__init__.py +0 -0
  6. autotools/autoconvert/commands.py +79 -0
  7. autotools/autoconvert/conversion/__init__.py +0 -0
  8. autotools/autoconvert/conversion/convert_audio.py +24 -0
  9. autotools/autoconvert/conversion/convert_image.py +29 -0
  10. autotools/autoconvert/conversion/convert_text.py +101 -0
  11. autotools/autoconvert/conversion/convert_video.py +25 -0
  12. autotools/autoconvert/core.py +54 -0
  13. autotools/autoip/commands.py +39 -1
  14. autotools/autoip/core.py +100 -43
  15. autotools/autolower/commands.py +21 -0
  16. autotools/autonote/__init__.py +0 -0
  17. autotools/autonote/commands.py +70 -0
  18. autotools/autonote/core.py +106 -0
  19. autotools/autopassword/commands.py +39 -1
  20. autotools/autotest/commands.py +43 -12
  21. autotools/autotodo/__init__.py +87 -0
  22. autotools/autotodo/commands.py +115 -0
  23. autotools/autotodo/core.py +567 -0
  24. autotools/autounit/__init__.py +0 -0
  25. autotools/autounit/commands.py +55 -0
  26. autotools/autounit/core.py +36 -0
  27. autotools/autozip/__init__.py +0 -0
  28. autotools/autozip/commands.py +88 -0
  29. autotools/autozip/core.py +107 -0
  30. autotools/cli.py +66 -62
  31. autotools/utils/commands.py +141 -10
  32. autotools/utils/performance.py +67 -35
  33. autotools/utils/requirements.py +21 -0
  34. autotools/utils/smoke.py +246 -0
  35. autotools/utils/text.py +73 -0
  36. open_autotools-0.0.5.dist-info/METADATA +100 -0
  37. open_autotools-0.0.5.dist-info/RECORD +54 -0
  38. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
  39. open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
  40. open_autotools-0.0.4rc1.dist-info/METADATA +0 -103
  41. open_autotools-0.0.4rc1.dist-info/RECORD +0 -28
  42. open_autotools-0.0.4rc1.dist-info/entry_points.txt +0 -6
  43. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
  44. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,115 @@
1
+ import click
2
+ from pathlib import Path
3
+ from .core import (autotodo_add_task, autotodo_start, autotodo_done, autotodo_remove, autotodo_list, DEFAULT_TODO_FILE)
4
+ from ..utils.loading import LoadingAnimation
5
+ from ..utils.updates import check_for_updates
6
+
7
+ # TOOL CATEGORY (USED BY 'autotools smoke')
8
+ TOOL_CATEGORY = 'Task'
9
+
10
+ # SMOKE TEST CASES (USED BY 'autotools smoke')
11
+ SMOKE_TESTS = [{'name': 'autotodo-list', 'args': ['--list']}]
12
+
13
+ # CLI COMMAND TO MANAGE TODO LIST
14
+ @click.command()
15
+ @click.option('--file', '-f', 'todo_path', default=DEFAULT_TODO_FILE, help='PATH TO TODO FILE (DEFAULT: TODO.md)')
16
+ @click.option('--add-task', 'add_task', metavar='DESCRIPTION', help='ADD A TASK')
17
+ @click.option('--prefix', type=str, default='fix', help='PREFIX FOR TASK (DEFAULT: fix, EXAMPLES: fix, add, change, update)')
18
+ @click.option('--priority', '-p', type=click.Choice(['high', 'mid', 'low'], case_sensitive=False), help='PRIORITY FOR TASK (OPTIONAL)')
19
+ @click.option('--start', 'start_task', type=int, metavar='INDEX', help='MOVE TASK TO IN PROGRESS (REQUIRES --section tasks)')
20
+ @click.option('--done', 'done_task', type=int, metavar='INDEX', help='MOVE TASK TO DONE (REQUIRES --section)')
21
+ @click.option('--remove', 'remove_task', type=int, metavar='INDEX', help='REMOVE TASK (REQUIRES --section)')
22
+ @click.option('--section', '-s', type=click.Choice(['tasks', 'in_progress', 'done'], case_sensitive=False), help='SECTION FOR --start, --done, OR --remove')
23
+ @click.option('--list', 'list_tasks', is_flag=True, help='LIST ALL TASKS')
24
+ @click.option('--list-section', type=click.Choice(['tasks', 'in_progress', 'done'], case_sensitive=False), help='LIST TASKS IN SPECIFIC SECTION')
25
+ def autotodo(todo_path, add_task, prefix, priority, start_task, done_task, remove_task, section, list_tasks, list_section):
26
+ """
27
+ MANAGES A SIMPLE TASK LIST IN A MARKDOWN FILE.
28
+
29
+ \b
30
+ OPERATIONS:
31
+ - ADD TASK: --add-task "description" [--prefix fix|add|change|...] [--priority high|mid|low]
32
+ - START TASK: --start INDEX --section tasks
33
+ - COMPLETE TASK: --done INDEX --section tasks|in_progress
34
+ - REMOVE TASK: --remove INDEX --section tasks|in_progress|done
35
+ - LIST TASKS: --list [--list-section SECTION]
36
+
37
+ \b
38
+ EXAMPLES:
39
+ autotodo --add-task "fix login issue" --prefix fix --priority high
40
+ autotodo --add-task "add dark mode" --prefix add
41
+ autotodo --add-task "update documentation" --prefix update
42
+ autotodo --start 0 --section tasks
43
+ autotodo --done 0 --section in_progress
44
+ autotodo --list
45
+ autotodo --list-section tasks
46
+ """
47
+
48
+ operations = sum([bool(add_task), bool(start_task is not None), bool(done_task is not None), bool(remove_task is not None), bool(list_tasks), bool(list_section)])
49
+
50
+ if operations == 0:
51
+ click.echo(click.style("ERROR: NO OPERATION SPECIFIED", fg='red'), err=True)
52
+ click.echo(click.get_current_context().get_help())
53
+ raise click.Abort()
54
+
55
+ if operations > 1:
56
+ click.echo(click.style("ERROR: ONLY ONE OPERATION CAN BE PERFORMED AT A TIME", fg='red'), err=True)
57
+ raise click.Abort()
58
+
59
+ if (start_task is not None or done_task is not None or remove_task is not None) and not section:
60
+ click.echo(click.style("ERROR: --section IS REQUIRED FOR --start, --done, OR --remove", fg='red'), err=True)
61
+ raise click.Abort()
62
+
63
+ try:
64
+ with LoadingAnimation():
65
+ _execute_operation(todo_path, add_task, prefix, priority, start_task, done_task, remove_task, section, list_tasks, list_section)
66
+ update_msg = check_for_updates()
67
+ if update_msg: click.echo(update_msg)
68
+
69
+ except ValueError as e:
70
+ click.echo(click.style(f"ERROR: {str(e)}", fg='red'), err=True)
71
+ raise click.Abort()
72
+ except Exception as e:
73
+ click.echo(click.style(f"UNEXPECTED ERROR: {str(e)}", fg='red'), err=True)
74
+ raise click.Abort()
75
+
76
+ # HANDLES LIST OPERATION
77
+ def _handle_list_operation(todo_path, list_section):
78
+ section_to_list = list_section if list_section else None
79
+ tasks = autotodo_list(todo_path, section_to_list)
80
+
81
+ if not tasks:
82
+ click.echo(click.style("NO TASKS FOUND", fg='yellow'))
83
+ else:
84
+ section_names = {'tasks': 'Task', 'in_progress': 'IN PROGRESS', 'done': 'DONE'}
85
+ for sec, task_lines in tasks:
86
+ section_name = section_names.get(sec, sec.upper())
87
+ click.echo(click.style(f"\n{section_name}:", fg='blue', bold=True))
88
+ for i, task_line in enumerate(task_lines): click.echo(f" [{i}] {task_line}")
89
+
90
+ # EXECUTES THE REQUESTED OPERATION
91
+ def _execute_operation(todo_path, add_task, prefix, priority, start_task, done_task, remove_task, section, list_tasks, list_section):
92
+ if add_task:
93
+ result = autotodo_add_task(todo_path, add_task, prefix, priority)
94
+ click.echo(click.style(f"SUCCESS: ADDED TASK TO {result}", fg='green'))
95
+
96
+ elif start_task is not None:
97
+ if section != 'tasks':
98
+ click.echo(click.style("ERROR: --start REQUIRES --section tasks", fg='red'), err=True)
99
+ raise click.Abort()
100
+ result = autotodo_start(todo_path, start_task, section)
101
+ click.echo(click.style(f"SUCCESS: MOVED TASK TO IN PROGRESS IN {result}", fg='green'))
102
+
103
+ elif done_task is not None:
104
+ if section not in ['tasks', 'in_progress']:
105
+ click.echo(click.style("ERROR: --done REQUIRES --section tasks OR in_progress", fg='red'), err=True)
106
+ raise click.Abort()
107
+ result = autotodo_done(todo_path, done_task, section)
108
+ click.echo(click.style(f"SUCCESS: MOVED TASK TO DONE IN {result}", fg='green'))
109
+
110
+ elif remove_task is not None:
111
+ result = autotodo_remove(todo_path, remove_task, section)
112
+ click.echo(click.style(f"SUCCESS: REMOVED TASK FROM {result}", fg='green'))
113
+
114
+ elif list_tasks or list_section:
115
+ _handle_list_operation(todo_path, list_section)
@@ -0,0 +1,567 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Optional, Literal
4
+
5
+ DEFAULT_TODO_FILE = "TODO.md"
6
+ PRIORITY_BADGES = {'high': '![HIGH][high]', 'mid': '![MID][mid]', 'low': '![LOW][low]'}
7
+
8
+ TODO_TEMPLATE = """
9
+ ### TO DO LIST
10
+
11
+ #### TASK
12
+
13
+ - [ ] **fix:** ![HIGH][high]
14
+
15
+ #### IN PROGRESS
16
+
17
+
18
+ - [ ] **refactoring:**
19
+
20
+ #### DONE
21
+
22
+ - [x] **added:**
23
+
24
+ [high]: https://img.shields.io/badge/-HIGH-red
25
+ [mid]: https://img.shields.io/badge/-MID-yellow
26
+ [low]: https://img.shields.io/badge/-LOW-green
27
+ """
28
+
29
+ # REGEX PATTERNS
30
+ PATTERN_TASK_HEADER = r'^#### TASK'
31
+ PATTERN_IN_PROGRESS = r'^#### IN PROGRESS'
32
+ PATTERN_DONE = r'^#### DONE'
33
+ PATTERN_DONE_SIMPLE = r'^#### DONE$'
34
+ PATTERN_TASK_LINE = r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*\w+:\*\*'
35
+ PATTERN_TASK_ING = r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*\w+ing:\*\*'
36
+ PATTERN_DONE_TASK = r'^[\s]*-[\s]*\[[\s]*x[\s]*\]'
37
+ PATTERN_EMPTY_ING = r'^-[\s]*\[[\s]*\][\s]*\*\*ing:\*\*[\s]*$'
38
+
39
+ # READS TODO FILE CONTENT
40
+ def _read_todo_file(todo_path: Path) -> str:
41
+ if not todo_path.exists(): return TODO_TEMPLATE
42
+ return todo_path.read_text(encoding='utf-8')
43
+
44
+ # WRITES TODO FILE CONTENT
45
+ def _write_todo_file(todo_path: Path, content: str):
46
+ todo_path.parent.mkdir(parents=True, exist_ok=True)
47
+ todo_path.write_text(content, encoding='utf-8')
48
+
49
+ # REMOVES EMPTY LINES AFTER INSERTION, KEEPS ONE IF BEFORE SECTION
50
+ def _clean_empty_lines_after_insert(lines: list, insert_idx: int):
51
+ if insert_idx + 1 >= len(lines): return
52
+ next_non_empty = insert_idx + 1
53
+
54
+ while next_non_empty < len(lines) and lines[next_non_empty].strip() == '':
55
+ next_non_empty += 1
56
+
57
+ if next_non_empty < len(lines) and lines[next_non_empty].startswith('####'):
58
+ _remove_excess_empty_lines_before_section(lines, insert_idx, next_non_empty)
59
+ else:
60
+ _remove_all_empty_lines_after_insert(lines, insert_idx)
61
+
62
+ # REMOVES EXCESS EMPTY LINES BEFORE SECTION HEADER
63
+ def _remove_excess_empty_lines_before_section(lines: list, insert_idx: int, next_non_empty: int):
64
+ empty_count = next_non_empty - insert_idx - 1
65
+ if empty_count > 1:
66
+ for _ in range(empty_count - 1):
67
+ if lines[insert_idx + 1].strip() == '': del lines[insert_idx + 1]
68
+
69
+ # REMOVES ALL EMPTY LINES AFTER INSERT
70
+ def _remove_all_empty_lines_after_insert(lines: list, insert_idx: int):
71
+ while insert_idx + 1 < len(lines) and lines[insert_idx + 1].strip() == '':
72
+ del lines[insert_idx + 1]
73
+
74
+ # EXTRACTS PREFIX AND TEXT FROM TASK LINE
75
+ def _extract_task_prefix_and_text(task_line: str) -> tuple[str, str]:
76
+ match = re.match(r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*(\w+):\*\* (.+)', task_line)
77
+ if match:
78
+ prefix = match.group(1)
79
+ task_text = match.group(2)
80
+ for badge in PRIORITY_BADGES.values(): task_text = task_text.replace(badge, '').strip()
81
+ return prefix, task_text
82
+
83
+ match = re.match(r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*(\w+):\*\*', task_line)
84
+ if match:
85
+ prefix = match.group(1)
86
+ task_text = re.sub(PATTERN_TASK_LINE, '', task_line).strip()
87
+ for badge in PRIORITY_BADGES.values(): task_text = task_text.replace(badge, '').strip()
88
+ return prefix, task_text
89
+
90
+ return 'ing', task_line.strip()
91
+
92
+ # CONVERTS PREFIX TO "ing" FORM
93
+ def _prefix_to_ing(prefix: str) -> str:
94
+ if prefix.endswith('ing'): return prefix
95
+ if prefix.endswith('e'): return prefix[:-1] + 'ing'
96
+ if prefix.endswith('y'): return prefix + 'ing'
97
+ return prefix + 'ing'
98
+
99
+ # EXTRACTS TASK TEXT FROM TASKS LINE
100
+ def _extract_text_from_tasks_line(task_line: str) -> str:
101
+ match = re.match(r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*\w+:\*\* (.+)', task_line)
102
+ if match:
103
+ task_text = match.group(1)
104
+ for badge in PRIORITY_BADGES.values(): task_text = task_text.replace(badge, '').strip()
105
+ return task_text
106
+ return re.sub(PATTERN_TASK_LINE, '', task_line).strip()
107
+
108
+ # EXTRACTS TASK TEXT FROM IN_PROGRESS LINE
109
+ def _extract_text_from_in_progress_line(task_line: str) -> str:
110
+ match = re.match(r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*\w+ing:\*\* (.+)', task_line)
111
+ if match: return match.group(1).strip()
112
+ match_empty = re.match(r'^[\s]*-[\s]*\[[\s]*\][\s]*\*\*\w+ing:\*\*[\s]*$', task_line)
113
+ if match_empty: return ''
114
+ return re.sub(r'^[\s]*-[\s]+', '', task_line).strip()
115
+
116
+ # EXTRACTS TASK TEXT FROM DONE/IN_PROGRESS LINE
117
+ def _extract_task_text_from_line(task_line: str, section: str) -> str:
118
+ if section == 'tasks': return _extract_text_from_tasks_line(task_line)
119
+ if section == 'in_progress': return _extract_text_from_in_progress_line(task_line)
120
+ return task_line.strip()
121
+
122
+ # INSERTS TASK LINE INTO SECTION
123
+ def _insert_task_into_section(lines: list, section_start: int, section_end: int, new_task_line: str, task_pattern: str):
124
+ last_task_idx = -1
125
+ for i in range(section_start + 1, min(section_end, len(lines))):
126
+ if re.match(task_pattern, lines[i]): last_task_idx = i
127
+
128
+ if last_task_idx != -1:
129
+ insert_idx = last_task_idx + 1
130
+ lines.insert(insert_idx, new_task_line)
131
+ _clean_empty_lines_after_insert(lines, insert_idx)
132
+ else:
133
+ insert_idx = section_start + 1
134
+ while insert_idx < len(lines) and lines[insert_idx].strip() == '': insert_idx += 1
135
+ lines.insert(insert_idx, new_task_line)
136
+ if insert_idx + 1 < len(lines) and lines[insert_idx + 1].strip() == '': del lines[insert_idx + 1]
137
+
138
+ # FINDS SECTION BOUNDARIES IN LINES
139
+ def _find_section_boundaries(lines: list, start_idx: int, end_markers: list) -> int:
140
+ for j in range(start_idx + 1, len(lines)):
141
+ line_stripped = lines[j].strip()
142
+ if any(line_stripped.startswith(marker) for marker in end_markers): return j
143
+ return len(lines)
144
+
145
+ # CALCULATES INSERT INDEX FOR NEW SECTION
146
+ def _calculate_insert_idx(in_progress_start_idx: int, first_done_idx: int, badges_start_idx: int, lines_len: int) -> int:
147
+ # TASK SECTION SHOULD BE BEFORE IN PROGRESS AND DONE
148
+ if first_done_idx != -1: return first_done_idx
149
+ if in_progress_start_idx != -1: return in_progress_start_idx
150
+ if badges_start_idx < lines_len: return badges_start_idx
151
+ return 1
152
+
153
+ # CREATES TASKS SECTION IF MISSING
154
+ def _ensure_tasks_section(lines: list, in_progress_start_idx: int, first_done_idx: int, badges_start_idx: int) -> tuple[int, int, int, int]:
155
+ tasks_start_idx, tasks_end_idx = _find_tasks_section(lines)
156
+ if tasks_end_idx == -1: return _create_new_tasks_section(lines, in_progress_start_idx, first_done_idx, badges_start_idx)
157
+ else: return _reposition_existing_tasks_section(lines, tasks_start_idx, tasks_end_idx, in_progress_start_idx, first_done_idx, badges_start_idx)
158
+
159
+ # FINDS TASKS SECTION INDICES
160
+ def _find_tasks_section(lines: list) -> tuple[int, int]:
161
+ for i, line in enumerate(lines):
162
+ if re.match(PATTERN_TASK_HEADER, line):
163
+ tasks_end_idx = _find_section_boundaries(lines, i, ['####', '**_'])
164
+ return i, tasks_end_idx
165
+ return -1, -1
166
+
167
+ # CREATES NEW TASKS SECTION
168
+ def _create_new_tasks_section(lines: list, in_progress_start_idx: int, first_done_idx: int, badges_start_idx: int) -> tuple[int, int, int, int]:
169
+ insert_idx = _calculate_insert_idx(in_progress_start_idx, first_done_idx, badges_start_idx, len(lines))
170
+ tasks_section = ['', '#### TASK', '', '']
171
+ for i, section_line in enumerate(tasks_section): lines.insert(insert_idx + i, section_line)
172
+
173
+ tasks_end_idx = insert_idx + len(tasks_section)
174
+ section_len = len(tasks_section)
175
+
176
+ if in_progress_start_idx != -1 and in_progress_start_idx >= insert_idx: in_progress_start_idx += section_len
177
+ if first_done_idx != -1 and first_done_idx >= insert_idx: first_done_idx += section_len
178
+
179
+ badges_start_idx += section_len
180
+ return tasks_end_idx, in_progress_start_idx, first_done_idx, badges_start_idx
181
+
182
+ # REPOSITIONS EXISTING TASKS SECTION
183
+ def _reposition_existing_tasks_section(lines: list, tasks_start_idx: int, tasks_end_idx: int, in_progress_start_idx: int, first_done_idx: int, badges_start_idx: int) -> tuple[int, int, int, int]:
184
+ target_pos = _calculate_target_position(tasks_start_idx, in_progress_start_idx, first_done_idx)
185
+
186
+ if target_pos != -1: tasks_end_idx, in_progress_start_idx, first_done_idx = _move_tasks_section(
187
+ lines, tasks_start_idx, tasks_end_idx, target_pos, in_progress_start_idx, first_done_idx
188
+ )
189
+
190
+ return tasks_end_idx, in_progress_start_idx, first_done_idx, badges_start_idx
191
+
192
+ # CALCULATES TARGET POSITION FOR TASKS SECTION
193
+ def _calculate_target_position(tasks_start_idx: int, in_progress_start_idx: int, first_done_idx: int) -> int:
194
+ if first_done_idx != -1 and tasks_start_idx > first_done_idx: return first_done_idx
195
+ elif in_progress_start_idx != -1 and tasks_start_idx > in_progress_start_idx: return in_progress_start_idx
196
+ return -1
197
+
198
+ # MOVES TASKS SECTION TO TARGET POSITION
199
+ def _move_tasks_section(lines: list, tasks_start_idx: int, tasks_end_idx: int, target_pos: int, in_progress_start_idx: int, first_done_idx: int) -> tuple[int, int, int]:
200
+ task_section_lines = lines[tasks_start_idx:tasks_end_idx]
201
+ del lines[tasks_start_idx:tasks_end_idx]
202
+ for i, line in enumerate(task_section_lines): lines.insert(target_pos + i, line)
203
+
204
+ section_len = len(task_section_lines)
205
+ tasks_end_idx = target_pos + section_len
206
+
207
+ in_progress_start_idx = _update_in_progress_idx_after_move(in_progress_start_idx, tasks_start_idx, target_pos, section_len)
208
+ first_done_idx = _update_done_idx_after_move(first_done_idx, section_len)
209
+
210
+ return tasks_end_idx, in_progress_start_idx, first_done_idx
211
+
212
+ # UPDATES IN PROGRESS INDEX AFTER TASK SECTION MOVE
213
+ def _update_in_progress_idx_after_move(in_progress_start_idx: int, tasks_start_idx: int, target_pos: int, section_len: int) -> int:
214
+ if in_progress_start_idx != -1:
215
+ if in_progress_start_idx >= tasks_start_idx or in_progress_start_idx >= target_pos:
216
+ in_progress_start_idx += section_len
217
+ return in_progress_start_idx
218
+
219
+ # UPDATES DONE INDEX AFTER TASK SECTION MOVE
220
+ def _update_done_idx_after_move(first_done_idx: int, section_len: int) -> int:
221
+ if first_done_idx != -1:
222
+ first_done_idx += section_len
223
+ return first_done_idx
224
+
225
+ # MOVES IN PROGRESS SECTION TO CORRECT POSITION
226
+ def _move_in_progress_section(lines: list, in_progress_start_idx: int, in_progress_end_idx: int, correct_idx: int) -> int:
227
+ in_progress_lines = lines[in_progress_start_idx:in_progress_end_idx]
228
+ del lines[in_progress_start_idx:in_progress_end_idx]
229
+ if in_progress_start_idx < correct_idx: correct_idx -= (in_progress_end_idx - in_progress_start_idx)
230
+ for i, section_line in enumerate(in_progress_lines): lines.insert(correct_idx + i, section_line)
231
+ return correct_idx + len(in_progress_lines)
232
+
233
+ # REMOVES EMPTY PLACEHOLDERS FROM IN PROGRESS
234
+ def _remove_empty_placeholders(lines: list, start_idx: int, end_idx: int):
235
+ lines_to_remove = []
236
+
237
+ for i in range(start_idx + 1, end_idx):
238
+ if re.match(PATTERN_EMPTY_ING, lines[i].strip()): lines_to_remove.append(i)
239
+
240
+ for i in reversed(lines_to_remove):
241
+ del lines[i]
242
+ if end_idx > i: end_idx -= 1
243
+
244
+ # FINDS IN PROGRESS SECTION POSITION
245
+ def _find_in_progress_position(lines: list) -> tuple[int, int]:
246
+ for i, line in enumerate(lines):
247
+ if re.match(PATTERN_IN_PROGRESS, line):
248
+ end_idx = _find_section_boundaries(lines, i, ['####', '**_', '['])
249
+ return i, end_idx
250
+ return -1, -1
251
+
252
+ # HANDLES IN PROGRESS SECTION
253
+ def _handle_in_progress_section(lines: list, in_progress_start_idx: int, in_progress_end_idx: int, correct_idx: int, first_done_idx: int, badges_start_idx: int) -> int:
254
+ if in_progress_start_idx != -1:
255
+ if in_progress_start_idx != correct_idx: badges_start_idx = _move_in_progress_section(lines, in_progress_start_idx, in_progress_end_idx, correct_idx)
256
+ in_progress_start_idx, in_progress_end_idx = _find_in_progress_position(lines)
257
+ if in_progress_start_idx != -1: _remove_empty_placeholders(lines, in_progress_start_idx, in_progress_end_idx)
258
+ else:
259
+ in_progress_section = ['', '#### IN PROGRESS', '', '']
260
+ for i, section_line in enumerate(in_progress_section): lines.insert(correct_idx + i, section_line)
261
+ if first_done_idx != -1: first_done_idx += len(in_progress_section)
262
+ badges_start_idx += len(in_progress_section)
263
+
264
+ return badges_start_idx
265
+
266
+ # CHECKS IF DONE SECTIONS EXIST
267
+ def _check_done_sections(lines: list) -> tuple[bool, bool]:
268
+ has_done_simple = any(re.match(PATTERN_DONE_SIMPLE, line.strip()) for line in lines)
269
+ has_done_versioned = any(re.match(PATTERN_DONE, line.strip()) and not re.match(PATTERN_DONE_SIMPLE, line.strip()) for line in lines)
270
+ return has_done_simple, has_done_versioned
271
+
272
+ # REMOVES EMPTY SIMPLE DONE SECTION
273
+ def _remove_empty_simple_done(lines: list) -> bool:
274
+ for i, line in enumerate(lines):
275
+ if re.match(PATTERN_DONE_SIMPLE, line.strip()):
276
+ simple_done_end = _find_section_boundaries(lines, i, ['####', '**_', '['])
277
+ section_content = [l.strip() for l in lines[i:simple_done_end] if l.strip() and not l.strip().startswith('####')]
278
+ if not section_content or (len(section_content) == 1 and section_content[0] == '- [x] **added:**'):
279
+ del lines[i:simple_done_end]
280
+ return False
281
+ break
282
+ return True
283
+
284
+ # HANDLES DONE SECTION
285
+ def _handle_done_section(lines: list, badges_start_idx: int):
286
+ has_done_simple, has_done_versioned = _check_done_sections(lines)
287
+ if has_done_versioned and has_done_simple: has_done_simple = _remove_empty_simple_done(lines)
288
+
289
+ if not has_done_simple and not has_done_versioned:
290
+ done_section = ['', '#### DONE', '', '- [x] **added:**', '']
291
+ for i, section_line in enumerate(done_section):
292
+ lines.insert(badges_start_idx + i, section_line)
293
+
294
+ # ENSURES REQUIRED SECTIONS EXIST IN TODO CONTENT
295
+ def _ensure_sections(content: str) -> str:
296
+ lines = content.split('\n')
297
+ initial_indices = _find_initial_section_indices(lines)
298
+
299
+ tasks_end_idx, _unused1, _unused2, badges_start_idx = _ensure_tasks_section(
300
+ lines, initial_indices['in_progress_start'], initial_indices['first_done'], initial_indices['badges_start']
301
+ )
302
+
303
+ recalculated_indices = _recalculate_section_indices(lines)
304
+ correct_in_progress_idx = _calculate_correct_in_progress_position(tasks_end_idx, recalculated_indices['first_done'])
305
+
306
+ badges_start_idx = _handle_in_progress_section(
307
+ lines, recalculated_indices['in_progress_start'], recalculated_indices['in_progress_end'],
308
+ correct_in_progress_idx, recalculated_indices['first_done'], badges_start_idx
309
+ )
310
+
311
+ _handle_done_section(lines, badges_start_idx)
312
+
313
+ return '\n'.join(lines)
314
+
315
+ # FINDS INITIAL SECTION INDICES IN LINES
316
+ def _find_initial_section_indices(lines: list) -> dict:
317
+ in_progress_start_idx = -1
318
+ first_done_idx = -1
319
+ badges_start_idx = len(lines)
320
+
321
+ for i, line in enumerate(lines):
322
+ if in_progress_start_idx == -1 and re.match(PATTERN_IN_PROGRESS, line): in_progress_start_idx = i
323
+ if first_done_idx == -1 and re.match(PATTERN_DONE, line.strip()) and not re.match(PATTERN_DONE_SIMPLE, line.strip()): first_done_idx = i
324
+ if line.strip().startswith('[') and ']:' in line and badges_start_idx == len(lines): badges_start_idx = i
325
+
326
+ return {'in_progress_start': in_progress_start_idx, 'first_done': first_done_idx, 'badges_start': badges_start_idx}
327
+
328
+ # RECALCULATES SECTION INDICES AFTER TASK REORGANIZATION
329
+ def _recalculate_section_indices(lines: list) -> dict:
330
+ in_progress_start_idx = -1
331
+ in_progress_end_idx = -1
332
+ first_done_idx = -1
333
+
334
+ for i, line in enumerate(lines):
335
+ if in_progress_start_idx == -1 and re.match(PATTERN_IN_PROGRESS, line):
336
+ in_progress_start_idx = i
337
+ in_progress_end_idx = _find_section_boundaries(lines, i, ['####', '**_', '['])
338
+ if first_done_idx == -1 and re.match(PATTERN_DONE, line.strip()) and not re.match(PATTERN_DONE_SIMPLE, line.strip()): first_done_idx = i
339
+
340
+ return {'in_progress_start': in_progress_start_idx, 'in_progress_end': in_progress_end_idx, 'first_done': first_done_idx}
341
+
342
+ # CALCULATES CORRECT POSITION FOR IN PROGRESS SECTION
343
+ def _calculate_correct_in_progress_position(tasks_end_idx: int, first_done_idx: int) -> int:
344
+ if first_done_idx != -1: return first_done_idx
345
+ return tasks_end_idx
346
+
347
+ # FINDS DONE SECTION (PREFERS SIMPLE OVER VERSIONED)
348
+ def _find_done_section(lines: list) -> int:
349
+ for i, line in enumerate(lines):
350
+ if re.match(PATTERN_DONE_SIMPLE, line.strip()): return i
351
+ for i, line in enumerate(lines):
352
+ if re.match(PATTERN_DONE, line.strip()) and not re.match(PATTERN_DONE_SIMPLE, line.strip()): return i
353
+ return -1
354
+
355
+ # FINDS SECTION START INDEX
356
+ def _find_section_start(lines: list, section_name: str, pattern: str) -> int:
357
+ if section_name == 'done': return _find_done_section(lines)
358
+ for i, line in enumerate(lines):
359
+ if re.match(pattern, line): return i
360
+ return -1
361
+
362
+ # FINDS SECTION IN TODO CONTENT
363
+ def _find_section(content: str, section_name: str) -> tuple[int, int]:
364
+ patterns = {'tasks': PATTERN_TASK_HEADER, 'in_progress': PATTERN_IN_PROGRESS, 'done': PATTERN_DONE}
365
+
366
+ pattern = patterns.get(section_name.lower())
367
+ if not pattern:
368
+ raise ValueError(f"UNKNOWN SECTION: {section_name}")
369
+
370
+ lines = content.split('\n')
371
+ start_idx = _find_section_start(lines, section_name, pattern)
372
+ if start_idx == -1: return -1, -1
373
+
374
+ end_idx = _find_section_boundaries(lines, start_idx, ['####', '**_'])
375
+ return start_idx, end_idx
376
+
377
+ # CREATES TASK LINE WITH OPTIONAL PRIORITY
378
+ def _create_task_line(prefix: str, task_text: str, priority: Optional[Literal['high', 'mid', 'low']] = None) -> str:
379
+ if priority:
380
+ priority_badge = PRIORITY_BADGES.get(priority or 'mid', PRIORITY_BADGES['mid'])
381
+ return f"- [ ] **{prefix}:** {priority_badge} {task_text}"
382
+ return f"- [ ] **{prefix}:** {task_text}"
383
+
384
+ # FINDS LAST TASK INDEX IN SECTION
385
+ def _find_last_task_in_section(lines: list, start_idx: int, end_idx: int) -> int:
386
+ last_task_idx = -1
387
+ for i in range(start_idx + 1, end_idx):
388
+ if re.match(PATTERN_TASK_LINE, lines[i]): last_task_idx = i
389
+ return last_task_idx
390
+
391
+ # INSERTS TASK LINE INTO EMPTY SECTION
392
+ def _insert_task_into_empty_section(lines: list, start_idx: int, task_line: str):
393
+ insert_idx = start_idx + 1
394
+ while insert_idx < len(lines) and lines[insert_idx].strip() == '': insert_idx += 1
395
+ empty_before = insert_idx - start_idx - 1
396
+
397
+ if empty_before > 1:
398
+ for _ in range(empty_before - 1):
399
+ if start_idx + 1 >= len(lines): break
400
+ if lines[start_idx + 1].strip() == '':
401
+ del lines[start_idx + 1]
402
+ insert_idx -= 1
403
+ lines.insert(insert_idx, task_line)
404
+
405
+ # ADDS TASK TO SECTION
406
+ def _add_task_to_section(content: str, section: str, task_text: str, prefix: str = 'fix', priority: Optional[Literal['high', 'mid', 'low']] = None) -> str:
407
+ if section != 'tasks':
408
+ raise ValueError(f"CANNOT ADD TASK TO SECTION: {section}")
409
+
410
+ content = _ensure_sections(content)
411
+ lines = content.split('\n')
412
+ start_idx, end_idx = _find_section(content, section)
413
+
414
+ if start_idx == -1:
415
+ raise ValueError(f"SECTION '{section}' NOT FOUND IN TODO FILE")
416
+
417
+ task_line = _create_task_line(prefix, task_text, priority)
418
+ last_task_idx = _find_last_task_in_section(lines, start_idx, end_idx)
419
+
420
+ if last_task_idx != -1:
421
+ insert_idx = last_task_idx + 1
422
+ lines.insert(insert_idx, task_line)
423
+ _clean_empty_lines_after_insert(lines, insert_idx)
424
+ else:
425
+ _insert_task_into_empty_section(lines, start_idx, task_line)
426
+
427
+ return '\n'.join(lines)
428
+
429
+ # GETS TASK LINE BY INDEX
430
+ def _get_task_line_by_index(lines: list, task_lines: list, task_index: int) -> tuple[int, str]:
431
+ if task_index < 0 or task_index >= len(task_lines):
432
+ raise ValueError(f"TASK INDEX {task_index} OUT OF RANGE")
433
+ task_line_idx = task_lines[task_index]
434
+ return task_line_idx, lines[task_line_idx]
435
+
436
+ # MOVES TASK TO IN PROGRESS
437
+ def _move_to_in_progress(content: str, task_index: int, section: str) -> str:
438
+ content = _ensure_sections(content)
439
+ lines = content.split('\n')
440
+ task_lines = _find_task_lines_in_section(lines, section)
441
+
442
+ task_line_idx, task_line = _get_task_line_by_index(lines, task_lines, task_index)
443
+ prefix, task_text = _extract_task_prefix_and_text(task_line)
444
+ lines.pop(task_line_idx)
445
+
446
+ updated_content = '\n'.join(lines)
447
+ updated_content = _ensure_sections(updated_content)
448
+ lines = updated_content.split('\n')
449
+
450
+ prefix_ing = _prefix_to_ing(prefix)
451
+ new_task_line = f"- [ ] **{prefix_ing}:** {task_text}"
452
+
453
+ in_progress_start, in_progress_end = _find_section(updated_content, 'in_progress')
454
+ if in_progress_start == -1:
455
+ raise ValueError("IN PROGRESS SECTION NOT FOUND")
456
+
457
+ _insert_task_into_section(lines, in_progress_start, in_progress_end, new_task_line, PATTERN_TASK_ING)
458
+
459
+ return '\n'.join(lines)
460
+
461
+ # MOVES TASK TO DONE
462
+ def _move_to_done(content: str, task_index: int, section: str) -> str:
463
+ content = _ensure_sections(content)
464
+ lines = content.split('\n')
465
+ task_lines = _find_task_lines_in_section(lines, section)
466
+
467
+ task_line_idx, task_line = _get_task_line_by_index(lines, task_lines, task_index)
468
+ task_text = _extract_task_text_from_line(task_line, section)
469
+ lines.pop(task_line_idx)
470
+
471
+ updated_content = '\n'.join(lines)
472
+ updated_content = _ensure_sections(updated_content)
473
+ lines = updated_content.split('\n')
474
+
475
+ new_task_line = f"- [x] **added:** {task_text}"
476
+
477
+ done_start, done_end = _find_section(updated_content, 'done')
478
+ if done_start == -1:
479
+ raise ValueError("DONE SECTION NOT FOUND")
480
+
481
+ _insert_task_into_section(lines, done_start, done_end, new_task_line, PATTERN_DONE_TASK)
482
+
483
+ return '\n'.join(lines)
484
+
485
+ # FINDS TASK LINES IN SECTION
486
+ def _find_task_lines_in_section(lines: list, section: str) -> list[int]:
487
+ task_lines = []
488
+ patterns = { 'tasks': PATTERN_TASK_LINE, 'in_progress': PATTERN_TASK_ING, 'done': PATTERN_DONE_TASK }
489
+ pattern = patterns.get(section)
490
+ if pattern:
491
+ for i, line in enumerate(lines):
492
+ if re.match(pattern, line): task_lines.append(i)
493
+ return task_lines
494
+
495
+ # REMOVES TASK FROM SECTION
496
+ def _remove_task(content: str, task_index: int, section: str) -> str:
497
+ lines = content.split('\n')
498
+ task_lines = _find_task_lines_in_section(lines, section)
499
+
500
+ if task_index < 0 or task_index >= len(task_lines):
501
+ raise ValueError(f"TASK INDEX {task_index} OUT OF RANGE")
502
+
503
+ lines.pop(task_lines[task_index])
504
+ return '\n'.join(lines)
505
+
506
+ # ADDS A TASK
507
+ def autotodo_add_task(todo_path: str, description: str, prefix: str = 'fix', priority: Optional[Literal['high', 'mid', 'low']] = None):
508
+ todo_file = Path(todo_path)
509
+ content = _read_todo_file(todo_file)
510
+ content = _add_task_to_section(content, 'tasks', description, prefix, priority)
511
+ _write_todo_file(todo_file, content)
512
+ return str(todo_file)
513
+
514
+ # MOVES TASK TO IN PROGRESS
515
+ def autotodo_start(todo_path: str, task_index: int, section: Literal['tasks']):
516
+ todo_file = Path(todo_path)
517
+ content = _read_todo_file(todo_file)
518
+ content = _move_to_in_progress(content, task_index, section)
519
+ _write_todo_file(todo_file, content)
520
+ return str(todo_file)
521
+
522
+ # MOVES TASK TO DONE
523
+ def autotodo_done(todo_path: str, task_index: int, section: Literal['tasks', 'in_progress']):
524
+ todo_file = Path(todo_path)
525
+ content = _read_todo_file(todo_file)
526
+ content = _move_to_done(content, task_index, section)
527
+ _write_todo_file(todo_file, content)
528
+ return str(todo_file)
529
+
530
+ # REMOVES TASK
531
+ def autotodo_remove(todo_path: str, task_index: int, section: Literal['tasks', 'in_progress', 'done']):
532
+ todo_file = Path(todo_path)
533
+ content = _read_todo_file(todo_file)
534
+ content = _remove_task(content, task_index, section)
535
+ _write_todo_file(todo_file, content)
536
+ return str(todo_file)
537
+
538
+ # EXTRACTS TASK LINES FROM SECTION
539
+ def _extract_task_lines_from_section(lines: list, start_idx: int, end_idx: int) -> list[str]:
540
+ section_lines = []
541
+ for i in range(start_idx + 1, min(end_idx, len(lines))):
542
+ line = lines[i]
543
+ stripped = line.strip()
544
+ if stripped == '' or stripped == '---': continue
545
+ if stripped.startswith('####') or (stripped.startswith('[') and ']:' in stripped): break
546
+ if stripped.startswith('-'): section_lines.append(stripped)
547
+ return section_lines
548
+
549
+ # LISTS TASKS IN SECTION
550
+ def autotodo_list(todo_path: str, section: Optional[Literal['tasks', 'in_progress', 'done']] = None):
551
+ todo_file = Path(todo_path)
552
+ content = _read_todo_file(todo_file)
553
+ content = _ensure_sections(content)
554
+
555
+ sections_to_show = [section] if section else ['tasks', 'in_progress', 'done']
556
+ result = []
557
+
558
+ for sec in sections_to_show:
559
+ try: start_idx, end_idx = _find_section(content, sec)
560
+ except ValueError: continue
561
+ if start_idx == -1: continue
562
+
563
+ lines = content.split('\n')
564
+ section_lines = _extract_task_lines_from_section(lines, start_idx, end_idx)
565
+ if section_lines: result.append((sec, section_lines))
566
+
567
+ return result
File without changes