From 259be4a4c965834c4613fd422250dcd09aed02d6 Mon Sep 17 00:00:00 2001 From: gofrendi Date: Wed, 27 Nov 2024 19:02:21 +0700 Subject: [PATCH] Add todo tasks --- src/zrb/builtin/__init__.py | 4 +- src/zrb/builtin/todo.py | 92 +++++++++++++++++++++++++++++++++---- src/zrb/util/todo.py | 55 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 10 deletions(-) diff --git a/src/zrb/builtin/__init__.py b/src/zrb/builtin/__init__.py index aed23c67..241ac6b8 100644 --- a/src/zrb/builtin/__init__.py +++ b/src/zrb/builtin/__init__.py @@ -15,7 +15,7 @@ from zrb.builtin.shell.autocomplete.bash import make_bash_autocomplete from zrb.builtin.shell.autocomplete.subcmd import get_shell_subcommands from zrb.builtin.shell.autocomplete.zsh import make_zsh_autocomplete -from zrb.builtin.todo import todo_add, todo_edit, todo_list +from zrb.builtin.todo import todo_add, todo_complete, todo_edit, todo_list, todo_log assert create_project assert add_fastapp_to_project @@ -39,3 +39,5 @@ assert todo_list assert todo_add assert todo_edit +assert todo_complete +assert todo_log diff --git a/src/zrb/builtin/todo.py b/src/zrb/builtin/todo.py index 3fd569dd..dd5c83f2 100644 --- a/src/zrb/builtin/todo.py +++ b/src/zrb/builtin/todo.py @@ -9,11 +9,13 @@ from zrb.task.make_task import make_task from zrb.util.todo import ( TodoTaskModel, + add_durations, cascade_todo_task, get_visual_todo_list, line_to_todo_task, load_todo_list, save_todo_list, + select_todo_task, todo_task_to_line, ) @@ -49,12 +51,12 @@ ) def todo_add(ctx: AnyContext): todo_file_path = os.path.join(TODO_DIR, "todo.txt") - todo_tasks: list[TodoTaskModel] = [] + todo_list: list[TodoTaskModel] = [] if os.path.isfile(todo_file_path): - todo_tasks = load_todo_list(todo_file_path) + todo_list = load_todo_list(todo_file_path) else: os.makedirs(TODO_DIR, exist_ok=True) - todo_tasks.append( + todo_list.append( cascade_todo_task( TodoTaskModel( priority=ctx.input.priority.upper(), @@ -72,8 +74,8 @@ def todo_add(ctx: AnyContext): ) ) ) - save_todo_list(todo_file_path, todo_tasks) - return get_visual_todo_list(todo_tasks) + save_todo_list(todo_file_path, todo_list) + return get_visual_todo_list(todo_list) @make_task(name="todo-list", description="📋 List todo", group=todo_group, alias="list") @@ -85,6 +87,78 @@ def todo_list(ctx: AnyContext): return get_visual_todo_list(todo_tasks) +@make_task( + name="todo-complete", + input=StrInput(name="keyword", prompt="Task keyword", description="Task Keyword"), + description="✅ Complete todo", + group=todo_group, + alias="complete", +) +def todo_complete(ctx: AnyContext): + todo_file_path = os.path.join(TODO_DIR, "todo.txt") + todo_list: list[TodoTaskModel] = [] + if os.path.isfile(todo_file_path): + todo_list = load_todo_list(todo_file_path) + # Get todo task + todo_task = select_todo_task(todo_list, ctx.input.keyword) + if todo_task is None: + raise Exception("Task not found") + # Update todo task + if todo_task.creation_date is not None: + todo_task.completion_date = datetime.date.today() + todo_task.completed = True + # Save todo list + save_todo_list(todo_list) + return get_visual_todo_list(todo_list) + + +@make_task( + name="todo-log", + input=[ + StrInput(name="keyword", prompt="Task keyword", description="Task Keyword"), + StrInput( + name="start", + prompt="Working start time (%Y-%m-%d %H:%M:%S)", + description="Working start time", + default_str=lambda _: _get_default_start(), + ), + StrInput( + name="duration", + prompt="Working duration", + description="Working duration", + default_str="30m", + ), + StrInput( + name="log", + prompt="Working log", + description="Working log", + ), + ], + description="🕒 Log work todo", + group=todo_group, + alias="log", +) +def todo_log(ctx: AnyContext): + todo_file_path = os.path.join(TODO_DIR, "todo.txt") + todo_list: list[TodoTaskModel] = [] + if os.path.isfile(todo_file_path): + todo_list = load_todo_list(todo_file_path) + # Get todo task + todo_task = select_todo_task(todo_list, ctx.input.keyword) + if todo_task is None: + raise Exception("Task not found") + # Update todo task + current_duration = todo_task.keyval.get("duration", "0") + todo_task.keyval["duration"] = add_durations(current_duration, ctx.input.duration) + # Save todo list + save_todo_list(todo_list) + return get_visual_todo_list(todo_list) + + +def _get_default_start() -> str: + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + @make_task( name="todo-edit", input=[ @@ -100,17 +174,17 @@ def todo_list(ctx: AnyContext): alias="edit", ) def todo_edit(ctx: AnyContext): - todo_tasks = [ + todo_list = [ cascade_todo_task(line_to_todo_task(line)) for line in ctx.input.text.split("\n") if line.strip() != "" ] - new_content = "\n".join(todo_task_to_line(todo_task) for todo_task in todo_tasks) + new_content = "\n".join(todo_task_to_line(todo_task) for todo_task in todo_list) todo_file_path = os.path.join(TODO_DIR, "todo.txt") with open(todo_file_path, "w") as f: f.write(new_content) - todo_tasks = load_todo_list(todo_file_path) - return get_visual_todo_list(todo_tasks) + todo_list = load_todo_list(todo_file_path) + return get_visual_todo_list(todo_list) def _get_todo_txt_content() -> str: diff --git a/src/zrb/util/todo.py b/src/zrb/util/todo.py index 889aa437..4ad5b70e 100644 --- a/src/zrb/util/todo.py +++ b/src/zrb/util/todo.py @@ -50,6 +50,28 @@ def cascade_todo_task(todo_task: TodoTaskModel): return todo_task +def select_todo_task( + todo_list: list[TodoTaskModel], keyword: str +) -> TodoTaskModel | None: + for todo_task in todo_list: + id = todo_task.keyval.get("id", "") + if keyword.lower().strip() == id.lower.strip(): + return todo_task + for todo_task in todo_list: + description = todo_task.description + if keyword.lower().strip() == description.lower.strip(): + return todo_task + for todo_task in todo_list: + id = todo_task.keyval.get("id", "") + if keyword.lower().strip() in id.lower.strip(): + return todo_task + for todo_task in todo_list: + description = todo_task.description + if keyword.lower().strip() in description.lower.strip(): + return todo_task + return None + + def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]: with open(todo_file_path, "r") as f: todo_lines = f.read().strip().split("\n") @@ -202,3 +224,36 @@ def _date_to_str(date: datetime.date | None) -> str: if date is None: return "".ljust(14) return date.strftime("%a %Y-%m-%d") + + +def add_durations(duration1: str, duration2: str) -> str: + total_seconds = _parse_duration(duration1) + _parse_duration(duration2) + # Format and return the result + return _format_duration(total_seconds) + + +def _parse_duration(duration: str) -> int: + """Parse a duration string into total seconds.""" + units = {"M": 2592000, "w": 604800, "d": 86400, "h": 3600, "m": 60, "s": 1} + total_seconds = 0 + match = re.findall(r"(\d+)([Mwdhms])", duration) + for value, unit in match: + total_seconds += int(value) * units[unit] + return total_seconds + + +def _format_duration(total_seconds: int) -> str: + """Format total seconds into a duration string.""" + units = [ + ("w", 604800), # 7 days in a week + ("d", 86400), # 24 hours in a day + ("h", 3600), # 60 minutes in an hour + ("m", 60), # 60 seconds in a minute + ("s", 1), # seconds + ] + result = [] + for unit, value_in_seconds in units: + if total_seconds >= value_in_seconds: + amount, total_seconds = divmod(total_seconds, value_in_seconds) + result.append(f"{amount}{unit}") + return " ".join(result) if result else "0s"