diff --git a/submissions/TaskOrganizer/README.md b/submissions/TaskOrganizer/README.md new file mode 100644 index 00000000..c00ddf8f --- /dev/null +++ b/submissions/TaskOrganizer/README.md @@ -0,0 +1,49 @@ +# Task Organizer + +Task Organizer is a terminal-based tool that helps you plan, organize, and visualize your daily tasks as a time-blocked ASCII schedule. You can add, edit, and reorder tasks interactively, and save your schedule for future use. + +## Features +- Add tasks with custom durations (e.g., 30m, 1h, 1h30m) +- Edit or reorder tasks using keyboard controls +- Visualize your schedule as an ASCII timeline +- Save and load your schedule from disk + +## How to Run +1. **Run the executables** + - For Windows: `TaskOrganizer.exe` + - For Linux: `./TaskOrganizer.bin` +2. **Run the source code**: + 1.**Install dependencies:** + - Python 3.7 or higher + - Install required packages: + ```bash + pip install rich keyboard + ``` + + 2.**Run the program:** + ```bash + python main.py + ``` + +3. **Follow the prompts:** + - Add tasks and durations as prompted + - Use arrow keys and spacebar to reorder tasks + - Press 'a' to add, 'e' to edit, and 'Esc' to finish + +4. **Output:** + - Your schedule will be saved as `schedule.txt` (ASCII format) + - Optionally, you can save and load a schedule using the provided functions + +## Keyboard Controls +- **Arrow keys:** Move cursor or selected task +- **Spacebar:** Pick up/drop a task for reordering +- **a:** Add a new task +- **e:** Edit the selected task +- **Esc:** Finish and save your schedule + +## Why I created this? +I always wanted something to quickly plan the thing I need to do for the day. I am not realy good at using a mouse - I always click the wrong thing. So, a schedule app that uses only keyboard made my life much easier. + +# Dependencies +- On Windows: rich and keyboard +- On Linux: rich \ No newline at end of file diff --git a/submissions/TaskOrganizer/TaskOrganizer.bin b/submissions/TaskOrganizer/TaskOrganizer.bin new file mode 100644 index 00000000..b04cc217 Binary files /dev/null and b/submissions/TaskOrganizer/TaskOrganizer.bin differ diff --git a/submissions/TaskOrganizer/TaskOrganizer.exe b/submissions/TaskOrganizer/TaskOrganizer.exe new file mode 100644 index 00000000..9653dfbc Binary files /dev/null and b/submissions/TaskOrganizer/TaskOrganizer.exe differ diff --git a/submissions/TaskOrganizer/main.py b/submissions/TaskOrganizer/main.py new file mode 100644 index 00000000..af7bb43b --- /dev/null +++ b/submissions/TaskOrganizer/main.py @@ -0,0 +1,254 @@ +from rich.console import Console +from datetime import datetime, timedelta +import re +import keyboard +import os +import json +import sys + +IS_LINUX = os.name == 'posix' +if IS_LINUX: + import tty + import termios + +def parse_duration(duration_str): + duration_str = duration_str.strip().lower() + hours = 0 + minutes = 0 + matches = re.findall(r"(\d+)\s*h", duration_str) + if matches: + hours = int(matches[0]) + matches = re.findall(r"(\d+)\s*m", duration_str) + if matches: + minutes = int(matches[0]) + return timedelta(hours=hours, minutes=minutes) + +def get_task_heights(tasks, max_lines=24, min_height=1): + durations = [max(1, int(parse_duration(t['duration']).total_seconds() // 600)) for t in tasks] + total = sum(durations) + if total <= max_lines: + return durations + # Scale down proportionally + scaled = [max(min_height, int(d / total * max_lines)) for d in durations] + # Ensure at least min_height for each, and adjust to fit exactly max_lines + while sum(scaled) > max_lines: + for i in range(len(scaled)): + if scaled[i] > min_height: + scaled[i] -= 1 + if sum(scaled) == max_lines: + break + while sum(scaled) < max_lines: + for i in range(len(scaled)): + scaled[i] += 1 + if sum(scaled) == max_lines: + break + return scaled + +def render_ascii_schedule(tasks): + current_time = datetime.now() + lines = [] + heights = get_task_heights(tasks) + for idx, task in enumerate(tasks): + start_str = current_time.strftime('%H:%M') + duration_td = parse_duration(task['duration']) + finish_time = current_time + duration_td + finish_str = finish_time.strftime('%H:%M') + height = heights[idx] + block_lines = [] + for i in range(height): + if i == 0: + time_label = f"{start_str} - {finish_str}" + else: + time_label = " " * len(f"{start_str} - {finish_str}") + if i == height // 2: + content = f"{task['name']} ({task['duration']})" + else: + content = "" + block_lines.append(f"{time_label:<15} | {content:<30}") + lines.extend(block_lines) + current_time = finish_time + return lines + +def display_tasks_with_cursor(tasks, cursor_idx, moving_idx=None): + os.system('cls' if os.name == 'nt' else 'clear') + console = Console() + console.print("[bold yellow]Reorder Your Tasks[/bold yellow]\n") + lines = [] + current_time = datetime.now() + heights = get_task_heights(tasks) + for idx, task in enumerate(tasks): + start_str = current_time.strftime('%H:%M') + duration_td = parse_duration(task['duration']) + finish_time = current_time + duration_td + finish_str = finish_time.strftime('%H:%M') + height = heights[idx] + block_lines = [] + for i in range(height): + if i == 0: + time_label = f"{start_str} - {finish_str}" + else: + time_label = " " * len(f"{start_str} - {finish_str}") + if i == height // 2: + content = f"{task['name']} ({task['duration']})" + else: + content = "" + block_lines.append((time_label, content)) + style = "on blue" if idx == cursor_idx else ("on green" if moving_idx is not None and idx == moving_idx else "") + for time_label, content in block_lines: + if style: + lines.append(f"[{style}]{time_label:<15} | {content:<30}[/{style}]") + else: + lines.append(f"{time_label:<15} | {content:<30}") + current_time = finish_time + for line in lines: + console.print(line) + if moving_idx is not None: + console.print("\n[bold magenta]Use arrow keys to move the task. Press space to drop.[/bold magenta]") + else: + console.print("\n[bold magenta]Use arrow keys to move the cursor. Press space to pick up a task.[/bold magenta]") + console.print("[bold magenta]Press 'a' to add a new task, 'e' to edit the selected task.[/bold magenta]") + console.print("[bold magenta]Press Esc to end and save your schedule.[/bold magenta]") + +def is_valid_duration(duration_str): + duration_str = duration_str.strip().lower() + # Accepts formats like '1h', '30m', '1h30m', '2h 15m', etc. + return bool(re.fullmatch(r"(\d+\s*h)?\s*(\d+\s*m)?", duration_str)) and (re.search(r"\d", duration_str) is not None) + +def get_key_linux(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + if ch == '\x1b': # possible arrow key + ch2 = sys.stdin.read(1) + if ch2 == '[': + ch3 = sys.stdin.read(1) + if ch3 == 'A': + return 'up' + elif ch3 == 'B': + return 'down' + return 'esc' + elif ch == ' ': # space + return 'space' + elif ch == 'a': + return 'a' + elif ch == 'e': + return 'e' + elif ch == '\x1b': + return 'esc' + else: + return ch + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + +def reorder_tasks(tasks): + cursor_idx = 0 + moving = False + moving_idx = None + console = Console() + while True: + display_tasks_with_cursor(tasks, cursor_idx, moving_idx) + if IS_LINUX: + key = get_key_linux() + event_name = key + else: + event = keyboard.read_event(suppress=(os.name != 'posix')) + if event.event_type != 'down': + continue + event_name = event.name + if event_name == 'esc': + break + if event_name == 'a': + console.print("\n[bold yellow]Add a new task[/bold yellow]") + name = console.input("[green]Enter task name: [/green]") + while True: + duration = console.input("[yellow]Enter task duration (e.g., 30m, 1h): [/yellow]") + if is_valid_duration(duration): + break + console.print("[red]Invalid duration format. Please try again (e.g., 30m, 1h, 1h30m).[/red]") + tasks.append({'name': name, 'duration': duration}) + cursor_idx = len(tasks) - 1 + continue + if event_name == 'e' and tasks: + console.print(f"\n[bold yellow]Edit task: {tasks[cursor_idx]['name']} ({tasks[cursor_idx]['duration']})[/bold yellow]") + new_name = console.input(f"[green]Enter new name (leave blank to keep '{tasks[cursor_idx]['name']}'): [/green]") + while True: + new_duration = console.input(f"[yellow]Enter new duration (leave blank to keep '{tasks[cursor_idx]['duration']}'): [/yellow]") + if not new_duration.strip() or is_valid_duration(new_duration): + break + console.print("[red]Invalid duration format. Please try again (e.g., 30m, 1h, 1h30m).[/red]") + if new_name.strip(): + tasks[cursor_idx]['name'] = new_name + if new_duration.strip(): + tasks[cursor_idx]['duration'] = new_duration + continue + if not moving: + if event_name == 'down': + cursor_idx = min(len(tasks) - 1, cursor_idx + 1) + elif event_name == 'up': + cursor_idx = max(0, cursor_idx - 1) + elif event_name == 'space': + moving = True + moving_idx = cursor_idx + else: + if event_name == 'down' and moving_idx < len(tasks) - 1: + tasks[moving_idx], tasks[moving_idx + 1] = tasks[moving_idx + 1], tasks[moving_idx] + moving_idx += 1 + elif event_name == 'up' and moving_idx > 0: + tasks[moving_idx], tasks[moving_idx - 1] = tasks[moving_idx - 1], tasks[moving_idx] + moving_idx -= 1 + elif event_name == 'space': + cursor_idx = moving_idx + moving = False + moving_idx = None + return tasks + +def main(): + console = Console() + tasks = [] + # Check for existing schedule.json + if os.path.exists("schedule.json"): + console.print("[bold yellow]A saved schedule was found.[/bold yellow]") + choice = console.input("[cyan]Continue with existing schedule? (y/n): [/cyan]").strip().lower() + if choice == 'y': + with open("schedule.json", "r", encoding="utf-8") as f: + tasks = json.load(f) + else: + console.print("[italic]Starting with a new schedule.[/italic]") + os.remove("schedule.json") + if not tasks: + while True: + name = console.input("[green]Enter task name (or type 'done' to finish): [/green]") + if name.lower() == 'done': + break + while True: + duration = console.input("[yellow]Enter task duration (e.g., 30m, 1h): [/yellow]") + if is_valid_duration(duration): + break + console.print("[red]Invalid duration format. Please try again (e.g., 30m, 1h, 1h30m).[/red]") + tasks.append({'name': name, 'duration': duration}) + console.print(f"[bold green]Task '{name}' with duration '{duration}' added.[/bold green]\n") + if tasks: + reorder_tasks(tasks) + # After reordering, print the final ascii schedule and write to file + ascii_lines = render_ascii_schedule(tasks) + console.print("\n[bold green]Final Schedule:[/bold green]") + for line in ascii_lines: + console.print(line) + with open("schedule.txt", "w", encoding="utf-8") as f: + for line in ascii_lines: + f.write(line + "\n") + console.print("\n[bold cyan]Schedule written to schedule.txt[/bold cyan]") + # Ask to save as JSON + save_json = console.input("\n[cyan]Save this schedule? (y/n): [/cyan]").strip().lower() + if save_json == 'y': + with open("schedule.json", "w", encoding="utf-8") as f: + json.dump(tasks, f, indent=2, ensure_ascii=False) + console.print("[bold green]Schedule saved[/bold green]") + else: + console.print("[italic]No tasks added.[/italic]") + console.print("\n[bold cyan]Thank you for using Task Organizer![/bold cyan]") + +if __name__ == "__main__": + main()