From c14113191ee2128824eaa3428361c6f2876ae32e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Oct 2025 00:09:05 +0530 Subject: [PATCH 1/4] Adding .env/ to ignore virtual environment --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3bbc29f..974dc47 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.env/ # Spyder project settings .spyderproject @@ -182,9 +183,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ From 1e6ca6cf621a9404c1b494bf9720a90c7ddf651a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Oct 2025 15:33:57 +0530 Subject: [PATCH 2/4] Adding schedule to requirements.txt --- requirements.txt | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 11e7c17..c84f355 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,26 @@ -click>=8.0.0 -rich>=13.0.0 -pytest>=7.0.0 -pytest-cov>=4.0.0 -textual>=0.50.0 \ No newline at end of file +Package Version Editable project location +----------------- ------- ------------------------- +click 8.3.0 +colorama 0.4.6 +coverage 7.10.7 +exceptiongroup 1.3.0 +iniconfig 2.1.0 +linkify-it-py 2.0.3 +markdown-it-py 4.0.0 +mdit-py-plugins 0.5.0 +mdurl 0.1.2 +packaging 25.0 +pip 25.2 +platformdirs 4.4.0 +pluggy 1.6.0 +Pygments 2.19.2 +pytest 8.4.2 +pytest-cov 7.0.0 +rich 14.1.0 +schedule 1.2.2 +setuptools 58.1.0 +textual 6.2.1 +tix-cli 0.8.0 C:\Users\Dell\tix-cli-vj3 +tomli 2.2.1 +typing_extensions 4.15.0 +uc-micro-py 1.0.3 From 8dc82796784c42b323150c12a1d80faf4857b7ee Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Oct 2025 00:12:24 +0530 Subject: [PATCH 3/4] feat: adding support for recurring events --- requirements.txt | 31 ++++------------------ tix/cli.py | 52 +++++++++++++++++++++++++++++++------ tix/models.py | 9 ++++--- tix/storage/json_storage.py | 6 ++--- 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/requirements.txt b/requirements.txt index c84f355..11e7c17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,5 @@ -Package Version Editable project location ------------------ ------- ------------------------- -click 8.3.0 -colorama 0.4.6 -coverage 7.10.7 -exceptiongroup 1.3.0 -iniconfig 2.1.0 -linkify-it-py 2.0.3 -markdown-it-py 4.0.0 -mdit-py-plugins 0.5.0 -mdurl 0.1.2 -packaging 25.0 -pip 25.2 -platformdirs 4.4.0 -pluggy 1.6.0 -Pygments 2.19.2 -pytest 8.4.2 -pytest-cov 7.0.0 -rich 14.1.0 -schedule 1.2.2 -setuptools 58.1.0 -textual 6.2.1 -tix-cli 0.8.0 C:\Users\Dell\tix-cli-vj3 -tomli 2.2.1 -typing_extensions 4.15.0 -uc-micro-py 1.0.3 +click>=8.0.0 +rich>=13.0.0 +pytest>=7.0.0 +pytest-cov>=4.0.0 +textual>=0.50.0 \ No newline at end of file diff --git a/tix/cli.py b/tix/cli.py index 5425398..312ee60 100644 --- a/tix/cli.py +++ b/tix/cli.py @@ -3,7 +3,8 @@ from rich.table import Table from pathlib import Path from tix.storage.json_storage import TaskStorage -from datetime import datetime +from datetime import datetime, timedelta, date +from dateutil.relativedelta import relativedelta import subprocess import platform import os @@ -40,7 +41,11 @@ def cli(ctx): @click.option('--tag', '-t', multiple=True, help='Add tags to task') @click.option('--attach', '-f', multiple=True, help='Attach file(s)') @click.option('--link', '-l', multiple=True, help='Attach URL(s)') -def add(task, priority, tag, attach, link): +@click.option('--repeat', '-r', + type=click.Choice(['daily', 'weekly', 'monthly']), + help='Set task repeatability') + +def add(task, priority, tag, attach, link, repeat): """Add a new task""" if not task or not task.strip(): console.print("[red]✗[/red] Task text cannot be empty") @@ -53,7 +58,7 @@ def add(task, priority, tag, attach, link): attachment_dir.mkdir(parents=True, exist_ok=True) for file_path in attach: try: - src = Path(file_path).expanduser().resolve() + src = Path(file_path).expanduser().resolve() if not src.exists(): console.print(f"[red]✗[/red] File not found: {file_path}") continue @@ -67,6 +72,10 @@ def add(task, priority, tag, attach, link): if link: new_task.links.extend(link) + # Handles repetition + if repeat: + new_task.repeats = repeat + storage.update_task(new_task) color = {'high': 'red', 'medium': 'yellow', 'low': 'green'}[priority] @@ -93,12 +102,14 @@ def ls(all): table.add_column("Priority", width=8) table.add_column("Task") table.add_column("Tags", style="dim") + table.add_column("Repeat", width=8) count = dict() for task in sorted(tasks, key=lambda t: (t.completed, t.id)): status = "✔" if task.completed else "○" priority_color = {"high": "red", "medium": "yellow", "low": "green"}[task.priority] tags_str = ", ".join(task.tags) if task.tags else "" + repeat_str = task.repeats # Show paperclip if task has attachments or links attach_icon = " 📎" if task.attachments or task.links else "" @@ -109,7 +120,9 @@ def ls(all): status, f"[{priority_color}]{task.priority}[/{priority_color}]", f"[{task_style}]{task.text}[/{task_style}]{attach_icon}" if task.completed else f"{task.text}{attach_icon}", - tags_str + tags_str, + repeat_str + ) count[task.completed] = count.get(task.completed, 0) + 1 @@ -145,6 +158,22 @@ def done(task_id): storage.update_task(task) console.print(f"[green]✔[/green] Completed: {task.text}") + if task.repeats and task.completed: + completed_date = date.fromisoformat(task.completed_at.split('T')[0]) + next_task_date = None + + if task.repeats == "daily": + next_task_date = completed_date + timedelta(days=1) + elif task.repeats == "weekly": + next_task_date = completed_date + timedelta(weeks=1) + elif task.repeats == "monthly": + next_task_date = completed_date + relativedelta(months=1) + + console.print(f"[bold green]➜[/bold green] New '{task.repeats}' task created for {next_task_date.isoformat()}") + + if date.today() == next_task_date: + new_task = storage.add_task(task.text, task.priority, task.tags) + @cli.command() @click.argument("task_id", type=int) @@ -261,7 +290,8 @@ def done_all(task_ids): @click.option('--remove-tag', multiple=True, help='Remove tags') @click.option('--attach', '-f', multiple=True, help='Attach file(s)') @click.option('--link', '-l', multiple=True, help='Attach URL(s)') -def edit(task_id, text, priority, add_tag, remove_tag, attach, link): +@click.option('--repeat', '-r', type=click.Choice(['daily', 'weekly', 'monthly']), help='New repeatability') +def edit(task_id, text, priority, add_tag, remove_tag, attach, link, repeat): """Edit a task""" task = storage.get_task(task_id) if not task: @@ -306,6 +336,12 @@ def edit(task_id, text, priority, add_tag, remove_tag, attach, link): task.links.extend(link) changes.append(f"links added: {list(link)}") + # Handle repeatability + if repeat: + old_repeat = task.repeats + task.repeats = repeat + changes.append(f"repeat: {old_repeat} → {repeat}") + if changes: storage.update_task(task) console.print(f"[green]✔[/green] Updated task #{task_id}:") @@ -625,7 +661,7 @@ def open(task_id): if not task.attachments and not task.links: console.print(f"[yellow]![/yellow] Task {task_id} has no attachments or links") return - + # Helper to open files cross-platform def safe_open(path_or_url, is_link=False): """Cross-platform safe opener for files and links (non-blocking).""" @@ -659,11 +695,11 @@ def safe_open(path_or_url, is_link=False): if not path.exists(): console.print(f"[red]✗[/red] File not found: {file_path}") continue - safe_open(path) + safe_open(path) # Open links for url in task.links: - safe_open(url, is_link=True) + safe_open(url, is_link=True) @cli.command() @click.option('--all', '-a', 'show_all', is_flag=True, help='Show completed tasks too') diff --git a/tix/models.py b/tix/models.py index c8e2467..6aea210 100644 --- a/tix/models.py +++ b/tix/models.py @@ -13,7 +13,8 @@ class Task: completed_at: Optional[str] = None tags: List[str] = field(default_factory=list) attachments: List[str] = field(default_factory=list) - links: List[str] = field(default_factory=list) + links: List[str] = field(default_factory=list) + repeats: Optional[str] = None def to_dict(self) -> dict: """Convert task to dictionary for JSON serialization""" @@ -27,6 +28,7 @@ def to_dict(self) -> dict: 'tags': self.tags, 'attachments': self.attachments, 'links': self.links, + 'repeats': self.repeats } @classmethod @@ -40,8 +42,9 @@ def from_dict(cls, data: dict): created_at=data.get('created_at', datetime.now().isoformat()), completed_at=data.get('completed_at'), tags=data.get('tags', []), - attachments=data.get('attachments', []), - links=data.get('links', []) + attachments=data.get('attachments', []), + links=data.get('links', []), + repeats=data.get('repeats') ) def mark_done(self): diff --git a/tix/storage/json_storage.py b/tix/storage/json_storage.py index 4d8445d..c232edf 100644 --- a/tix/storage/json_storage.py +++ b/tix/storage/json_storage.py @@ -65,11 +65,11 @@ def save_tasks(self, tasks: List[Task]): data["tasks"] = [task.to_dict() for task in tasks] self._write_data(data) - def add_task(self, text: str, priority: str = 'medium', tags: List[str] = None) -> Task: + def add_task(self, text: str, priority: str = 'medium', tags: List[str] = None, repeat: str = None) -> Task: """Add a new task and return it""" data = self._read_data() new_id = data["next_id"] - new_task = Task(id=new_id, text=text, priority=priority, tags=tags or []) + new_task = Task(id=new_id, text=text, priority=priority, tags=tags or [], repeats=repeat) data["tasks"].append(new_task.to_dict()) data["next_id"] = new_id + 1 self._write_data(data) @@ -109,7 +109,7 @@ def get_active_tasks(self) -> List[Task]: def get_completed_tasks(self) -> List[Task]: """Get all completed tasks""" return [t for t in self.load_tasks() if t.completed] - + def get_attachment_dir(self, task_id: int) -> Path: """Return the path where attachments for a task should be stored""" return Path.home() / ".tix" / "attachments" / str(task_id) From 997d581f986631e60908305f49e77841b6a5ffe4 Mon Sep 17 00:00:00 2001 From: vj031206 Date: Sat, 11 Oct 2025 17:56:43 +0530 Subject: [PATCH 4/4] fix/recurring task logic, show due dates and fixing conflict merges --- requirements.txt | 1 + tix/cli.py | 114 +++++++++++++++++------------------- tix/models.py | 21 ++++--- tix/storage/json_storage.py | 16 ++--- 4 files changed, 77 insertions(+), 75 deletions(-) diff --git a/requirements.txt b/requirements.txt index d8690c0..22ee1c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pytest>=7.0.0 pytest-cov>=4.0.0 textual>=0.50.0 pyyaml>=6.0 +python-dateutil==2.9.0 \ No newline at end of file diff --git a/tix/cli.py b/tix/cli.py index 1aa11b8..02f576e 100644 --- a/tix/cli.py +++ b/tix/cli.py @@ -33,7 +33,7 @@ def parse_time_estimate(time_str: str) -> int: """Parse time string like '2h', '30m', '1h30m' into minutes""" time_str = time_str.lower().strip() total_minutes = 0 - + if 'h' in time_str: parts = time_str.split('h') hours = int(parts[0]) @@ -46,7 +46,7 @@ def parse_time_estimate(time_str: str) -> int: total_minutes = int(time_str.replace('m', '')) else: total_minutes = int(time_str) - + return total_minutes @@ -187,6 +187,9 @@ def restore(backup_file, data_file, yes): @click.option('--attach', '-f', multiple=True, help='Attach file(s)') @click.option('--link', '-l', multiple=True, help='Attach URL(s)') @click.option('--estimate', '-e', help='Time estimate (e.g., 2h, 30m, 1h30m)') +@click.option('--repeat', '-r', + type=click.Choice(['daily', 'monthly', 'weekly']), + help='Set task priority') def add(task, priority, tag, attach, link, estimate, repeat): """Add a new task""" from tix.config import CONFIG @@ -206,11 +209,11 @@ def add(task, priority, tag, attach, link, estimate, repeat): # Use config defaults if not specified if priority is None: priority = CONFIG.get('defaults', {}).get('priority', 'medium') - + # Merge config default tags with provided tags default_tags = CONFIG.get('defaults', {}).get('tags', []) all_tags = list(set(list(tag) + default_tags)) - + new_task = storage.add_task(task, priority, all_tags, estimate=estimate_minutes) # Handle attachments if attach: @@ -235,14 +238,15 @@ def add(task, priority, tag, attach, link, estimate, repeat): new_task.links.extend(link) # Handles repetition - if repeat: + if repeat and repeat not in new_task.tags: new_task.repeats = repeat + new_task.tags.append(repeat) storage.update_task(new_task, record_history=False) color = {'high': 'red', 'medium': 'yellow', 'low': 'green'}[priority] console.print(f"[green]✔[/green] Added task #{new_task.id}: [{color}]{task}[/{color}]") - + # Show notification if enabled if CONFIG.get('notifications', {}).get('on_creation', True): if all_tags: @@ -286,32 +290,9 @@ def ls(show_all): table.add_column("Priority", width=8) table.add_column("Task") table.add_column("Tags", style="dim") - table.add_column("Repeat", width=8) + table.add_column("Due", width=10) count = dict() - for task in sorted(tasks, key=lambda t: (t.completed, t.id)): - status = "✔" if task.completed else "○" - priority_color = {"high": "red", "medium": "yellow", "low": "green"}[task.priority] - tags_str = ", ".join(task.tags) if task.tags else "" - repeat_str = task.repeats - if not compact_mode: - table.add_column("Tags", style=tag_color) - if show_dates: - table.add_column("Created", style="dim") - - count = dict() - - task_style = "dim strike" if task.completed else "" - table.add_row( - str(task.id), - status, - f"[{priority_color}]{task.priority}[/{priority_color}]", - f"[{task_style}]{task.text}[/{task_style}]{attach_icon}" if task.completed else f"{task.text}{attach_icon}", - tags_str, - repeat_str - - ) - count[task.completed] = count.get(task.completed, 0) + 1 for task in sorted(tasks, key=lambda t: (getattr(t, "completed", False), getattr(t, "id", 0))): status = "✔" if getattr(task, "completed", False) else "○" priority_color = priority_colors.get(getattr(task, "priority", "medium"), @@ -319,6 +300,13 @@ def ls(show_all): tags_str = ", ".join(getattr(task, "tags", [])) if getattr(task, "tags", None) else "" attach_icon = " 📎" if getattr(task, "attachments", None) or getattr(task, "links", None) else "" + due_str = "" + if getattr(task, "due", None): + try: + due_date = date.fromisoformat(task.due) + due_str = due_date.strftime("%Y-%m-%d") + except ValueError: + due_str = task.due # text truncation text_val = getattr(task, "text", getattr(task, "task", "")) @@ -337,6 +325,7 @@ def ls(show_all): row.append(f"{text_val}{attach_icon}") if not compact_mode: row.append(tags_str) + row.append(due_str) if show_dates: created = getattr(task, "created", getattr(task, "created_at", None)) if created: @@ -402,10 +391,17 @@ def done(task_id): elif task.repeats == "monthly": next_task_date = completed_date + relativedelta(months=1) - console.print(f"[bold green]➜[/bold green] New '{task.repeats}' task created for {next_task_date.isoformat()}") + due_str = next_task_date.isoformat() if next_task_date else None + + new_task = storage.add_task( + task.text, + task.priority, + task.tags, + repeat=task.repeats, + due=due_str + ) + console.print(f"[bold green]➜[/bold green] New '{task.repeats}' task due on {due_str}") - if date.today() == next_task_date: - new_task = storage.add_task(task.text, task.priority, task.tags) @cli.command() @@ -727,7 +723,7 @@ def done_all(task_ids): if not_found: console.print(f"[red]Not found: {', '.join(map(str, not_found))}[/red]") - + @click.argument("name") def context(name): """Switch or create context""" @@ -1246,22 +1242,22 @@ def start(task_id): if not task: console.print(f"[red]✗[/red] Task #{task_id} not found") return - + if task.completed: console.print(f"[yellow]![/yellow] Cannot start timer on completed task") return - + for t in storage.load_tasks(): if t.is_timer_running() and t.id != task_id: console.print(f"[yellow]![/yellow] Task #{t.id} timer is already running") console.print(f"[dim]Stop it first with: tix stop {t.id}[/dim]") return - + if task.is_timer_running(): duration = task.get_current_session_duration() console.print(f"[yellow]![/yellow] Timer already running for {task.format_time(duration)}") return - + try: task.start_timer() storage.update_task(task) @@ -1280,19 +1276,19 @@ def stop(task_id): if not task: console.print(f"[red]✗[/red] Task #{task_id} not found") return - + if not task.is_timer_running(): console.print(f"[yellow]![/yellow] No timer running for task #{task_id}") return - + try: duration = task.stop_timer() storage.update_task(task) - + console.print(f"[green]⏹[/green] Stopped timer for task #{task_id}") console.print(f"[cyan] Session duration: {task.format_time(duration)}[/cyan]") console.print(f"[dim] Total time spent: {task.format_time(task.time_spent)}[/dim]") - + if task.estimate: remaining = task.get_time_remaining() if remaining > 0: @@ -1314,7 +1310,7 @@ def status(task_id): if not task: console.print(f"[red]✗[/red] Task #{task_id} not found") return - + if task.is_timer_running(): duration = task.get_current_session_duration() console.print(f"[green]⏱[/green] Timer running for task #{task_id}: {task.text}") @@ -1327,7 +1323,7 @@ def status(task_id): else: tasks = storage.load_tasks() running_tasks = [t for t in tasks if t.is_timer_running()] - + if running_tasks: for task in running_tasks: duration = task.get_current_session_duration() @@ -1338,15 +1334,15 @@ def status(task_id): @cli.command() -@click.option('--period', '-p', type=click.Choice(['week', 'month', 'all']), +@click.option('--period', '-p', type=click.Choice(['week', 'month', 'all']), default='week', help='Time period for report') def timereport(period): """Generate time tracking report""" from datetime import timedelta - + tasks = storage.load_tasks() now = datetime.now() - + if period == 'week': start_date = now - timedelta(days=7) title = "Weekly Time Report" @@ -1356,24 +1352,24 @@ def timereport(period): else: start_date = None title = "All Time Report" - + relevant_tasks = [] for task in tasks: if task.time_spent > 0: if start_date: - task_logs = [log for log in task.time_logs + task_logs = [log for log in task.time_logs if datetime.fromisoformat(log['ended_at']) >= start_date] if task_logs: relevant_tasks.append((task, sum(log['duration'] for log in task_logs))) else: relevant_tasks.append((task, task.time_spent)) - + if not relevant_tasks: console.print(f"[dim]No time tracked in the {period}[/dim]") return - + console.print(f"\n[bold cyan]{title}[/bold cyan]\n") - + table = Table() table.add_column("ID", style="cyan", width=4) table.add_column("Task") @@ -1381,14 +1377,14 @@ def timereport(period): table.add_column("Spent", style="yellow", width=10) table.add_column("Remaining", width=10) table.add_column("Status", width=8) - + total_estimated = 0 total_spent = 0 - + for task, time_in_period in relevant_tasks: estimate_str = task.format_time(task.estimate) if task.estimate else "-" spent_str = task.format_time(time_in_period) - + if task.estimate: total_estimated += task.estimate remaining = task.get_time_remaining() @@ -1408,9 +1404,9 @@ def timereport(period): remaining_str = "-" status = "✓" if task.completed else "→" status_color = "green" if task.completed else "blue" - + total_spent += time_in_period - + table.add_row( str(task.id), task.text[:40] + "..." if len(task.text) > 40 else task.text, @@ -1419,9 +1415,9 @@ def timereport(period): remaining_str, f"[{status_color}]{status}[/{status_color}]" ) - + console.print(table) - + console.print(f"\n[bold]Summary:[/bold]") if total_estimated > 0: console.print(f" Total estimated: {format_time_helper(total_estimated)}") diff --git a/tix/models.py b/tix/models.py index 5c395a1..d1a57c9 100644 --- a/tix/models.py +++ b/tix/models.py @@ -15,12 +15,13 @@ class Task: attachments: List[str] = field(default_factory=list) links: List[str] = field(default_factory=list) repeats: Optional[str] = None - + # Time tracking fields estimate: Optional[int] = None time_spent: int = 0 started_at: Optional[str] = None time_logs: List[Dict] = field(default_factory=list) + due: Optional[str] = None def to_dict(self) -> dict: """Convert task to dictionary for JSON serialization""" @@ -34,11 +35,12 @@ def to_dict(self) -> dict: 'tags': self.tags, 'attachments': self.attachments, 'links': self.links, - 'repeats': self.repeats + 'repeats': self.repeats, 'estimate': self.estimate, 'time_spent': self.time_spent, 'started_at': self.started_at, - 'time_logs': self.time_logs + 'time_logs': self.time_logs, + "due": self.due } @classmethod @@ -54,11 +56,12 @@ def from_dict(cls, data: dict): tags=data.get('tags', []), attachments=data.get('attachments', []), links=data.get('links', []), - repeats=data.get('repeats') + repeats=data.get('repeats'), estimate=data.get('estimate'), time_spent=data.get('time_spent', 0), started_at=data.get('started_at'), - time_logs=data.get('time_logs', []) + time_logs=data.get('time_logs', []), + due=data.get("due") ) def mark_done(self): @@ -81,20 +84,20 @@ def stop_timer(self): """Stop tracking time and log the duration""" if not self.started_at: raise ValueError("Timer not running for this task") - + start_time = datetime.fromisoformat(self.started_at) end_time = datetime.now() duration = int((end_time - start_time).total_seconds() / 60) - + self.time_logs.append({ 'started_at': self.started_at, 'ended_at': end_time.isoformat(), 'duration': duration }) - + self.time_spent += duration self.started_at = None - + return duration def is_timer_running(self) -> bool: diff --git a/tix/storage/json_storage.py b/tix/storage/json_storage.py index 157029d..bf72982 100644 --- a/tix/storage/json_storage.py +++ b/tix/storage/json_storage.py @@ -1,8 +1,9 @@ import json +from datetime import datetime, date from pathlib import Path from typing import List, Optional from tix.models import Task -from tix.storage.history import HistoryManager +from tix.storage.history import HistoryManager class TaskStorage: @@ -11,7 +12,7 @@ class TaskStorage: def __init__(self, storage_path: Path = None, context: str = None, history: HistoryManager = None): """Initialize storage with default or custom path and context""" self.context = context or self._get_active_context() - + # Use context-specific storage path if storage_path: self.storage_path = storage_path @@ -21,7 +22,7 @@ def __init__(self, storage_path: Path = None, context: str = None, history: Hist self.storage_path = base_dir / "tasks.json" else: self.storage_path = base_dir / "contexts" / f"{self.context}.json" - + self.storage_path.parent.mkdir(parents=True, exist_ok=True) self._ensure_file() @@ -89,17 +90,18 @@ def save_tasks(self, tasks: List[Task]): data["tasks"] = [task.to_dict() for task in tasks] self._write_data(data) - def add_task(self, text: str, priority: str = 'medium', tags: List[str] = None, estimate: int = None, due: str = None, is_global: bool = False, record_history: bool = True) -> Task: + def add_task(self, text: str, priority: str = 'medium', tags: List[str] = None, estimate: int = None, due: str = None, is_global: bool = False, record_history: bool = True, repeat: str = None) -> Task: """Add a new task and return it""" data = self._read_data() new_id = data["next_id"] new_task = Task( - id=new_id, - text=text, - priority=priority, + id=new_id, + text=text, + priority=priority, tags=tags or [], estimate=estimate, repeats=repeat, + due=due.isoformat() if isinstance(due, (date, datetime)) else due ) data["tasks"].append(new_task.to_dict()) data["next_id"] = new_id + 1