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.
- autotools/autocaps/commands.py +21 -0
- autotools/autocolor/__init__.py +0 -0
- autotools/autocolor/commands.py +60 -0
- autotools/autocolor/core.py +99 -0
- autotools/autoconvert/__init__.py +0 -0
- autotools/autoconvert/commands.py +79 -0
- autotools/autoconvert/conversion/__init__.py +0 -0
- autotools/autoconvert/conversion/convert_audio.py +24 -0
- autotools/autoconvert/conversion/convert_image.py +29 -0
- autotools/autoconvert/conversion/convert_text.py +101 -0
- autotools/autoconvert/conversion/convert_video.py +25 -0
- autotools/autoconvert/core.py +54 -0
- autotools/autoip/commands.py +39 -1
- autotools/autoip/core.py +100 -43
- autotools/autolower/commands.py +21 -0
- autotools/autonote/__init__.py +0 -0
- autotools/autonote/commands.py +70 -0
- autotools/autonote/core.py +106 -0
- autotools/autopassword/commands.py +39 -1
- autotools/autotest/commands.py +43 -12
- autotools/autotodo/__init__.py +87 -0
- autotools/autotodo/commands.py +115 -0
- autotools/autotodo/core.py +567 -0
- autotools/autounit/__init__.py +0 -0
- autotools/autounit/commands.py +55 -0
- autotools/autounit/core.py +36 -0
- autotools/autozip/__init__.py +0 -0
- autotools/autozip/commands.py +88 -0
- autotools/autozip/core.py +107 -0
- autotools/cli.py +66 -62
- autotools/utils/commands.py +141 -10
- autotools/utils/performance.py +67 -35
- autotools/utils/requirements.py +21 -0
- autotools/utils/smoke.py +246 -0
- autotools/utils/text.py +73 -0
- open_autotools-0.0.5.dist-info/METADATA +100 -0
- open_autotools-0.0.5.dist-info/RECORD +54 -0
- {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
- open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
- open_autotools-0.0.4rc1.dist-info/METADATA +0 -103
- open_autotools-0.0.4rc1.dist-info/RECORD +0 -28
- open_autotools-0.0.4rc1.dist-info/entry_points.txt +0 -6
- {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {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
|