diff --git a/plugins/taskman/README.md b/plugins/taskman/README.md index b7f6524b7..ffb7cea0f 100644 --- a/plugins/taskman/README.md +++ b/plugins/taskman/README.md @@ -1,286 +1,219 @@ -# taskman +# Taskman v2.0 - Oh My Zsh Plugin -A powerful terminal task manager plugin for Oh-My-Zsh. Manage your daily tasks without leaving the command line! +A modern, feature-rich terminal task manager plugin for Oh My Zsh. Taskman provides an intuitive sidebar-style interface for managing your tasks with advanced features like priority-based coloring, humanized timers, and even a fun running cat animation! -![Task Manager Demo](https://via.placeholder.com/600x400/1e1e1e/00ff00?text=Terminal+Task+Manager+Demo) +## โœจ Features -## Features +### Core Functionality +- **Sidebar Interface**: Clean, terminal-based UI that doesn't interfere with your workflow +- **Persistent Storage**: Tasks are automatically saved to JSON format +- **Priority System**: Three priority levels (high, normal, low) with visual indicators +- **Task Operations**: Add, complete, delete, and navigate tasks with keyboard shortcuts -- ๐Ÿ“ **Dual Interface**: Both interactive TUI and CLI operations -- โŒจ๏ธ **Vim-like Keybindings**: Navigate with `j`/`k`, `Space` to toggle -- ๐ŸŽฏ **Priority System**: High, normal, and low priority with color coding -- ๐Ÿ’พ **Persistent Storage**: Tasks saved in `~/.taskman/tasks.json` -- ๐ŸŽจ **Rich Colors**: Visual indicators for task status and priority -- โšก **Zero Config**: Works immediately after installation -- ๐Ÿ”ง **Shell Integration**: Aliases, completion, and sidebar workflow +### Advanced Features +- **Smart Sorting**: Multiple sort modes (creation order, priority, alphabetical) with completed tasks always at bottom +- **Priority-Based Colors**: Task text colored by priority (red=high, yellow=normal, cyan=low) +- **Humanized Timers**: Shows task age in human-readable format ([5m], [2h], [3d]) using local timezone +- **Visual Separation**: Horizontal line separates active and completed tasks +- **Configurable Storage**: Set custom task file location via `TASKMAN_DATA_FILE` environment variable +- **Completed Task Dimming**: Completed tasks retain priority colors but are visually dimmed +- **Fun Animation**: Optional Chrome dino-style mini-game for entertainment (toggle with 'x') -## Installation +### Display Format +``` +Taskman v2.0 [Sort: default] +Pending: 3, Completed: 1 Press 'h' for help, 'q' to quit -1. Add `taskman` to your plugins list in `~/.zshrc`: +[ 5m] โ—‹ [!] Fix critical bug in authentication system +[ 2h] โ—‹ [-] Review pull request #123 +[now] โ—‹ [ยท] Update documentation +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +[ 1d] โœ“ [!] Complete project setup + + โ—† +. . . โ— . . . | . . . โ–Œ . . . โ–ˆ . . . โ”ƒ . . . โ– . . . + +n: New | Space: Toggle | d: Delete | s: Sort | โ†‘โ†“: Navigate | h: Help | q: Quit +``` + +## ๐Ÿš€ Installation + +### Manual Installation ```bash - plugins=(git taskman) - ``` +# Clone or copy the plugin to your Oh My Zsh plugins directory +cp -r taskman ~/.oh-my-zsh/plugins/ -2. Reload your shell: +# Add to your .zshrc plugins list +plugins=(... taskman) - ```bash +# Reload your shell source ~/.zshrc ``` -3. Start using! - +### Using Oh My Zsh Plugin Manager +If you're using a plugin manager like `oh-my-zsh-plugins`: ```bash - tasks add "My first task" +# Add to your plugin list +plugins=(... taskman) ``` -## Requirements - -- **Python 3.6+** (for interactive UI and CLI operations) -- **Terminal with color support** (most modern terminals) - -## Usage +## ๐ŸŽฎ Usage ### Interactive UI - -Launch the full-screen task manager: - +Launch the interactive task manager: ```bash -tasks # Launch interactive UI -tasks ui # Same as above -task-sidebar # Launch with sidebar usage tips +taskman +# or use aliases +tasks +tm +todo ``` -#### Keyboard Shortcuts +### Keyboard Shortcuts -| Key | Action | -|-----|--------| -| `โ†‘`/`k` | Move up | -| `โ†“`/`j` | Move down | -| `n` | Create new task | -| `Space` | Toggle task completion | -| `d` | Delete selected task | -| `Tab` | Cycle priority when creating tasks | -| `h` | Toggle help panel | -| `q` | Quit | +#### Navigation +- `โ†‘/k` - Move selection up +- `โ†“/j` - Move selection down + +#### Task Operations +- `n` - Create new task +- `Space` - Toggle task completion +- `d` - Delete selected task + +#### Sorting +- `s` - Cycle through sort modes (default โ†’ priority โ†’ alphabetical) +- `p` - Sort by priority (high โ†’ normal โ†’ low) +- `a` - Sort alphabetically + +#### Other +- `h` - Toggle help panel +- `x` - Toggle animation on/off +- `q` - Quit application ### Command Line Interface - -#### Adding Tasks - ```bash -tasks add "Fix login bug" # Normal priority -tasks add "Deploy to production" high # High priority -tasks add "Update documentation" low # Low priority +# Add a new task +tasks add "Complete project documentation" +tasks add "Fix bug in login system" high + +# List tasks +tasks list +tasks list pending +tasks list completed + +# Mark task as completed +tasks done 1 + +# Delete a task +tasks delete 2 + +# Sort tasks +tasks sort priority +tasks sort alphabetical +tasks sort default + +# Show help +tasks help ``` -#### Listing Tasks +## โš™๏ธ Configuration +### Custom Storage Location +Set a custom location for your task file: ```bash -tasks list # All tasks -tasks list pending # Only pending tasks -tasks list completed # Only completed tasks -tasks ls # Short alias +export TASKMAN_DATA_FILE="$HOME/my-tasks.json" ``` -#### Managing Tasks +### Priority Levels +- **High Priority** (`!`): Red text - urgent tasks +- **Normal Priority** (`-`): Yellow text - regular tasks +- **Low Priority** (`ยท`): Cyan text - nice-to-have tasks -```bash -tasks done 3 # Mark task ID 3 as completed -tasks delete 5 # Delete task ID 5 -tasks help # Show help +## ๐ŸŽจ Visual Features + +### Color System +- **Active Tasks**: Text colored by priority (red/yellow/cyan) +- **Completed Tasks**: Same priority colors but dimmed for subtle indication +- **Status Bullets**: Green checkmarks for completed, colored circles for active +- **Selection**: Reverse highlighting for currently selected task + +### Timer Display +- Shows how long ago each task was created +- Updates in real-time for active tasks +- Uses local timezone for accurate time calculation +- Formats: `[now]`, `[5m]`, `[2h]`, `[3d]` + +### Mini-Game Animation +- Chrome dino-style side-scrolling game with jumping player and obstacles +- Press 'x' to toggle animation on/off +- Automatic jumping and varied obstacles for entertainment +- Runs alongside task management without interference + +## ๐Ÿ“ File Structure +``` +~/.taskman/ +โ””โ”€โ”€ tasks.json # Task storage (default location) ``` -### Aliases - -The plugin provides convenient aliases: - -```bash -tm add "Buy groceries" # Same as 'tasks add' -task list # Same as 'tasks list' -todo done 1 # Same as 'tasks done 1' -``` - -## Priority Levels - -Tasks support three priority levels with color coding: - -- **High Priority** (`!`) - ๐Ÿ”ด Red, for urgent tasks -- **Normal Priority** (`-`) - ๐ŸŸก Yellow, default priority -- **Low Priority** (`ยท`) - ๐Ÿ”ต Cyan, for less urgent tasks - -## Task Display - -Tasks are displayed with visual indicators: - -``` - โœ“ [!] Completed high priority task (green) - โ—‹ [-] Pending normal priority task (yellow) - โ—‹ [ยท] Pending low priority task (cyan) -``` - -## Sidebar Workflow - -Perfect for split-terminal development workflow: - -1. **Split your terminal** horizontally or vertically -2. **Run `tasks ui`** in one pane for persistent task view -3. **Work in the other pane** while keeping tasks visible -4. **Quick updates** with keyboard shortcuts - -## Auto-completion - -The plugin provides intelligent tab completion: - -```bash -tasks # Shows: add, list, done, delete, etc. -tasks add "task" # Shows: high, normal, low -tasks done # Shows available task IDs -``` - -## Configuration - -### Optional Startup Summary - -To show task summary when opening terminal, uncomment this line in the plugin: - -```bash -# In ~/.oh-my-zsh/plugins/taskman/taskman.plugin.zsh -_taskman_startup_summary # Uncomment this line -``` - -This shows: -``` -๐Ÿ“‹ Task Summary: 3 pending, 2 completed - Type 'tasks' to manage your tasks -``` - -## Data Storage - -Tasks are stored in `~/.taskman/tasks.json`: - +### Task Data Format ```json { "tasks": [ { "id": 1, - "text": "Fix login bug", - "completed": false, + "text": "Complete project setup", + "completed": true, "priority": "high", "created_at": "2024-01-15T10:30:00" } ], - "next_id": 2 + "next_id": 2, + "sort_mode": "default" } ``` -## Examples +## ๐Ÿ”ง Technical Details -### Daily Developer Workflow +- **Language**: Python 3.6+ +- **Dependencies**: Built-in libraries only (curses, json, datetime) +- **Storage**: JSON format for human-readable task data +- **Cross-platform**: Works on macOS, Linux, and other Unix-like systems +- **Performance**: Efficient curses-based rendering with 100ms refresh rate +## ๐ŸŽฏ Tips & Tricks + +1. **Quick Task Entry**: Use Tab in input mode to cycle through priority levels +2. **Efficient Navigation**: Use `k`/`j` (vim-style) or arrow keys for navigation +3. **Sort Persistence**: Your preferred sort mode is remembered between sessions +4. **Bulk Operations**: Use CLI commands for scripting and automation +5. **Custom Storage**: Set `TASKMAN_DATA_FILE` to sync tasks across different setups +6. **Visual Cues**: Completed tasks are automatically moved to bottom with visual separator +7. **Time Awareness**: Timer shows local time, perfect for tracking task age across time zones + +## ๐Ÿ› Troubleshooting + +### Common Issues +- **Unicode Display**: Ensure your terminal supports Unicode for proper emoji display +- **Color Issues**: Some terminals may not support all color combinations +- **Permission Errors**: Check write permissions for the task storage directory + +### Debug Mode ```bash -# Morning planning -tasks add "Review PR #123" high -tasks add "Fix login bug" high -tasks add "Update docs" low - -# Check current tasks -tasks list pending - -# Work in interactive mode (split terminal) -tasks ui - -# Quick CLI updates -tm done 1 -tm add "Deploy hotfix" high - -# End of day review -tasks list completed +# Run with Python directly for debugging +python3 ~/.oh-my-zsh/plugins/taskman/task_manager.py ``` -### Project Management +## ๐Ÿค Contributing -```bash -# Sprint planning -tasks add "Implement user auth" high -tasks add "Add unit tests" normal -tasks add "Update README" low +This is an Oh My Zsh version of the Taskman plugin. For contributions and bug reports, please refer to the original osh framework repository. -# Track progress -tasks list +## ๐Ÿ“„ License -# Mark completed -tasks done 1 -tasks done 2 - -# Cleanup -tasks delete 3 # Remove completed/outdated tasks -``` - -## Comparison with Alternatives - -| Feature | taskman | taskwarrior | todo.txt | Todoist | -|---------|---------|-------------|----------|----------| -| Interactive TUI | โœ… | โŒ | โŒ | โŒ | -| CLI Interface | โœ… | โœ… | โœ… | โœ… | -| Zero Setup | โœ… | โŒ | โœ… | โŒ | -| No External Deps | โœ… | โŒ | โœ… | โŒ | -| Rich Visual UI | โœ… | โŒ | โŒ | โŒ | -| Vim Keybindings | โœ… | โŒ | โŒ | โŒ | -| Local Data | โœ… | โœ… | โœ… | โŒ | - -## Troubleshooting - -### Python Not Found - -```bash -# Install Python 3 (macOS) -brew install python3 - -# Install Python 3 (Ubuntu/Debian) -sudo apt-get install python3 - -# Verify installation -python3 --version -``` - -### Plugin Not Loading - -1. Check that `taskman` is in your plugins list: - ```bash - echo $plugins - ``` - -2. Reload your shell: - ```bash - source ~/.zshrc - ``` - -3. Test plugin function: - ```bash - tasks help - ``` - -### File Permissions - -```bash -# Make Python files executable -chmod +x ~/.oh-my-zsh/plugins/taskman/bin/*.py -``` - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -MIT License - see the [Oh-My-Zsh license](https://github.com/ohmyzsh/ohmyzsh/blob/master/LICENSE.txt) for details. - -## Author - -Created by [@oiahoon](https://github.com/oiahoon) +Part of the Oh My Zsh ecosystem. See individual license files for details. --- -**Happy task managing! ๐Ÿš€** +**Enjoy managing your tasks with style! ๐Ÿฑโœจ** diff --git a/plugins/taskman/_taskman b/plugins/taskman/_taskman index 8f9dbf964..724e84ecf 100644 --- a/plugins/taskman/_taskman +++ b/plugins/taskman/_taskman @@ -47,10 +47,10 @@ _taskman() { if command -v python3 >/dev/null 2>&1; then local plugin_dir="${0:h}" local -a task_ids - + # Try to get task IDs from the CLI - task_ids=($(python3 "$plugin_dir/bin/task_cli.py" list all 2>/dev/null | grep -o '(ID: [0-9]\+)' | grep -o '[0-9]\+' 2>/dev/null)) - + task_ids=($(python3 "$plugin_dir/task_cli.py" list all 2>/dev/null | grep -o '(ID: [0-9]\+)' | grep -o '[0-9]\+' 2>/dev/null)) + if [[ ${#task_ids[@]} -gt 0 ]]; then _describe 'task IDs' task_ids else diff --git a/plugins/taskman/bin/task_cli.py b/plugins/taskman/bin/task_cli.py deleted file mode 100755 index 383fb3581..000000000 --- a/plugins/taskman/bin/task_cli.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 -""" -Terminal Task Manager CLI - Command line interface for task operations -""" - -import sys -import os -import json -from datetime import datetime -from typing import List, Dict, Optional - -# Import the Task and TaskManager classes from task_manager.py -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -from task_manager import Task, TaskManager - -class TaskCLI: - def __init__(self): - self.task_manager = TaskManager() - - def add_task(self, text: str, priority: str = "normal"): - """Add a new task via CLI""" - task = self.task_manager.add_task(text, priority) - return task - - def list_tasks(self, filter_type: str = "all"): - """List tasks with color coding""" - tasks = self.task_manager.tasks - - if filter_type == "pending": - tasks = [t for t in tasks if not t.completed] - elif filter_type == "completed": - tasks = [t for t in tasks if t.completed] - - if not tasks: - if filter_type == "all": - print("\033[33mNo tasks found. Add your first task with: tasks add 'task description'\033[0m") - else: - print(f"\033[33mNo {filter_type} tasks found.\033[0m") - return - - print(f"\033[1m{filter_type.title()} Tasks:\033[0m") - print() - - for task in tasks: - # Status icon - status_icon = "โœ“" if task.completed else "โ—‹" - - # Priority icon and color - priority_icons = {"high": "!", "normal": "-", "low": "ยท"} - priority_colors = {"high": "\033[31m", "normal": "\033[33m", "low": "\033[36m"} - - priority_icon = priority_icons.get(task.priority, "-") - priority_color = priority_colors.get(task.priority, "\033[33m") - - # Task color - if task.completed: - task_color = "\033[32m" # Green for completed - else: - task_color = priority_color - - # Format task line - task_line = f"{task_color} {status_icon} [{priority_icon}] (ID: {task.id}) {task.text}\033[0m" - print(task_line) - - print() - print(f"\033[2mTotal: {len(tasks)} tasks\033[0m") - - def complete_task(self, task_id: str): - """Mark a task as completed""" - try: - task_id_int = int(task_id) - except ValueError: - print(f"\033[31mError: Invalid task ID '{task_id}'. Must be a number.\033[0m") - return False - - task = next((t for t in self.task_manager.tasks if t.id == task_id_int), None) - if not task: - print(f"\033[31mError: Task with ID {task_id_int} not found.\033[0m") - return False - - if task.completed: - print(f"\033[33mTask '{task.text}' is already completed.\033[0m") - return True - - self.task_manager.toggle_task(task_id_int) - print(f"\033[32mโœ“ Completed task: {task.text}\033[0m") - return True - - def delete_task(self, task_id: str): - """Delete a task""" - try: - task_id_int = int(task_id) - except ValueError: - print(f"\033[31mError: Invalid task ID '{task_id}'. Must be a number.\033[0m") - return False - - task = next((t for t in self.task_manager.tasks if t.id == task_id_int), None) - if not task: - print(f"\033[31mError: Task with ID {task_id_int} not found.\033[0m") - return False - - task_text = task.text - self.task_manager.delete_task(task_id_int) - print(f"\033[31mร— Deleted task: {task_text}\033[0m") - return True - - def count_tasks(self, filter_type: str = "all"): - """Count tasks by type""" - tasks = self.task_manager.tasks - - if filter_type == "pending": - count = len([t for t in tasks if not t.completed]) - elif filter_type == "completed": - count = len([t for t in tasks if t.completed]) - else: - count = len(tasks) - - print(count) - return count - -def main(): - """Main CLI entry point""" - if len(sys.argv) < 2: - print("Usage: task_cli.py [args...]") - sys.exit(1) - - cli = TaskCLI() - command = sys.argv[1].lower() - - try: - if command == "add": - if len(sys.argv) < 3: - print("\033[31mError: Please provide task description\033[0m") - sys.exit(1) - - text = sys.argv[2] - priority = sys.argv[3] if len(sys.argv) > 3 else "normal" - - # Validate priority - if priority not in ["high", "normal", "low"]: - print(f"\033[33mWarning: Invalid priority '{priority}', using 'normal'\033[0m") - priority = "normal" - - cli.add_task(text, priority) - - elif command == "list": - filter_type = sys.argv[2] if len(sys.argv) > 2 else "all" - if filter_type not in ["all", "pending", "completed"]: - print(f"\033[33mWarning: Invalid filter '{filter_type}', using 'all'\033[0m") - filter_type = "all" - cli.list_tasks(filter_type) - - elif command == "complete": - if len(sys.argv) < 3: - print("\033[31mError: Please provide task ID\033[0m") - sys.exit(1) - cli.complete_task(sys.argv[2]) - - elif command == "delete": - if len(sys.argv) < 3: - print("\033[31mError: Please provide task ID\033[0m") - sys.exit(1) - cli.delete_task(sys.argv[2]) - - elif command == "count": - filter_type = sys.argv[2] if len(sys.argv) > 2 else "all" - cli.count_tasks(filter_type) - - else: - print(f"\033[31mError: Unknown command '{command}'\033[0m") - print("Available commands: add, list, complete, delete, count") - sys.exit(1) - - except Exception as e: - print(f"\033[31mError: {e}\033[0m") - sys.exit(1) - -if __name__ == "__main__": - main() - diff --git a/plugins/taskman/bin/task_manager.py b/plugins/taskman/bin/task_manager.py deleted file mode 100755 index af7e5df93..000000000 --- a/plugins/taskman/bin/task_manager.py +++ /dev/null @@ -1,342 +0,0 @@ -#!/usr/bin/env python3 -""" -Terminal Task Manager - A sidebar-style task manager for the terminal - -Features: -- Sidebar display in terminal -- Keyboard shortcuts for task operations -- Persistent task storage -- Task prioritization and highlighting -""" - -import curses -import json -import os -from datetime import datetime -from typing import List, Dict, Optional - -class Task: - def __init__(self, id: int, text: str, completed: bool = False, priority: str = "normal", created_at: str = None): - self.id = id - self.text = text - self.completed = completed - self.priority = priority # "high", "normal", "low" - self.created_at = created_at or datetime.now().isoformat() - - def to_dict(self) -> Dict: - return { - "id": self.id, - "text": self.text, - "completed": self.completed, - "priority": self.priority, - "created_at": self.created_at - } - - @classmethod - def from_dict(cls, data: Dict) -> 'Task': - return cls( - id=data["id"], - text=data["text"], - completed=data["completed"], - priority=data.get("priority", "normal"), - created_at=data.get("created_at") - ) - -class TaskManager: - def __init__(self, data_file: str = None): - # Use environment variable or default path - self.data_file = data_file or os.environ.get('TASKMAN_DATA_FILE', os.path.expanduser("~/.taskman/tasks.json")) - self.tasks: List[Task] = [] - self.selected_index = 0 - self.next_id = 1 - self.load_tasks() - - def load_tasks(self): - """Load tasks from JSON file""" - if os.path.exists(self.data_file): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.tasks = [Task.from_dict(task_data) for task_data in data.get("tasks", [])] - self.next_id = data.get("next_id", 1) - except (json.JSONDecodeError, FileNotFoundError): - self.tasks = [] - self.next_id = 1 - - def save_tasks(self): - """Save tasks to JSON file""" - # Ensure directory exists - os.makedirs(os.path.dirname(self.data_file), exist_ok=True) - - data = { - "tasks": [task.to_dict() for task in self.tasks], - "next_id": self.next_id - } - with open(self.data_file, 'w') as f: - json.dump(data, f, indent=2) - - def add_task(self, text: str, priority: str = "normal") -> Task: - """Add a new task""" - task = Task(self.next_id, text, priority=priority) - self.tasks.append(task) - self.next_id += 1 - self.save_tasks() - return task - - def toggle_task(self, task_id: int): - """Toggle task completion status""" - for task in self.tasks: - if task.id == task_id: - task.completed = not task.completed - self.save_tasks() - break - - def delete_task(self, task_id: int): - """Delete a task""" - self.tasks = [task for task in self.tasks if task.id != task_id] - self.save_tasks() - if self.selected_index >= len(self.tasks) and self.tasks: - self.selected_index = len(self.tasks) - 1 - elif not self.tasks: - self.selected_index = 0 - - def get_selected_task(self) -> Optional[Task]: - """Get currently selected task""" - if 0 <= self.selected_index < len(self.tasks): - return self.tasks[self.selected_index] - return None - - def move_selection(self, direction: int): - """Move selection up or down""" - if self.tasks: - self.selected_index = max(0, min(len(self.tasks) - 1, self.selected_index + direction)) - -class TaskManagerUI: - def __init__(self): - self.task_manager = TaskManager() - self.input_mode = False - self.input_text = "" - self.input_priority = "normal" - self.show_help = False - - def run(self, stdscr): - """Main application loop""" - curses.curs_set(0) # Hide cursor - stdscr.nodelay(1) # Non-blocking input - stdscr.timeout(100) # Refresh every 100ms - - # Color pairs - curses.start_color() - curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Selected - curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # Completed - curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) # High priority - curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Normal priority - curses.init_pair(5, curses.COLOR_CYAN, curses.COLOR_BLACK) # Low priority - curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_BLACK) # Default - curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_WHITE) # Input mode - - while True: - self.draw_ui(stdscr) - - try: - key = stdscr.getch() - except: - continue - - if key == -1: - continue - - if self.input_mode: - if self.handle_input_mode(key): - break - else: - if self.handle_normal_mode(key): - break - - def draw_ui(self, stdscr): - """Draw the user interface""" - stdscr.clear() - height, width = stdscr.getmaxyx() - - # Title - title = "Terminal Task Manager (osh plugin)" - stdscr.addstr(0, 2, title, curses.A_BOLD) - stdscr.addstr(0, width - 20, f"Tasks: {len(self.task_manager.tasks)}", curses.A_DIM) - - # Help line - if not self.show_help: - help_text = "Press 'h' for help, 'q' to quit" - stdscr.addstr(1, 2, help_text, curses.A_DIM) - - # Tasks list - start_row = 3 - for i, task in enumerate(self.task_manager.tasks): - if start_row + i >= height - 3: - break - - # Determine color based on task status and priority - color = curses.color_pair(6) # Default - if task.completed: - color = curses.color_pair(2) # Green for completed - elif task.priority == "high": - color = curses.color_pair(3) # Red for high priority - elif task.priority == "low": - color = curses.color_pair(5) # Cyan for low priority - else: - color = curses.color_pair(4) # Yellow for normal priority - - # Highlight selected task - if i == self.task_manager.selected_index: - color |= curses.A_REVERSE - - # Task status icon - status_icon = "โœ“" if task.completed else "โ—‹" - priority_icon = { - "high": "!", - "normal": "-", - "low": "ยท" - }.get(task.priority, "-") - - # Truncate task text if too long - max_text_width = width - 15 - task_text = task.text[:max_text_width] + "..." if len(task.text) > max_text_width else task.text - - task_line = f" {status_icon} [{priority_icon}] {task_text}" - stdscr.addstr(start_row + i, 2, task_line, color) - - # Input area - if self.input_mode: - input_y = height - 3 - stdscr.addstr(input_y, 2, "New task: ", curses.A_BOLD) - stdscr.addstr(input_y, 12, self.input_text, curses.color_pair(7)) - stdscr.addstr(input_y + 1, 2, f"Priority: {self.input_priority} (Tab to change)", curses.A_DIM) - curses.curs_set(1) # Show cursor in input mode - else: - curses.curs_set(0) # Hide cursor - - # Help panel - if self.show_help: - self.draw_help(stdscr, height, width) - - # Status line - status_y = height - 1 - if self.input_mode: - status_text = "Enter: Save task | Esc: Cancel | Tab: Change priority" - else: - status_text = "n: New | Space: Toggle | d: Delete | โ†‘โ†“: Navigate | h: Help | q: Quit" - - stdscr.addstr(status_y, 2, status_text[:width-4], curses.A_DIM) - - stdscr.refresh() - - def draw_help(self, stdscr, height, width): - """Draw help panel""" - help_lines = [ - "KEYBOARD SHORTCUTS:", - "", - "Navigation:", - " โ†‘/k - Move up", - " โ†“/j - Move down", - "", - "Task Operations:", - " n - New task", - " Space - Toggle completion", - " d - Delete task", - "", - "Priority Levels:", - " ! High priority (red)", - " - Normal priority (yellow)", - " ยท Low priority (cyan)", - "", - "Other:", - " h - Toggle this help", - " q - Quit application", - "", - "CLI Usage:", - " tasks add 'text' [priority]", - " tasks list [filter]", - " tasks done ", - " tasks delete ", - ] - - # Calculate help panel size - help_width = max(len(line) for line in help_lines) + 4 - help_height = len(help_lines) + 2 - - start_x = max(2, (width - help_width) // 2) - start_y = max(2, (height - help_height) // 2) - - # Draw help background - for i in range(help_height): - stdscr.addstr(start_y + i, start_x, " " * help_width, curses.color_pair(7)) - - # Draw help content - for i, line in enumerate(help_lines): - stdscr.addstr(start_y + 1 + i, start_x + 2, line, curses.color_pair(7)) - - def handle_normal_mode(self, key) -> bool: - """Handle key presses in normal mode""" - # Quit - if key == ord('q'): - return True - - # Navigation - elif key == curses.KEY_UP or key == ord('k'): - self.task_manager.move_selection(-1) - elif key == curses.KEY_DOWN or key == ord('j'): - self.task_manager.move_selection(1) - - # Task operations - elif key == ord('n'): - self.input_mode = True - self.input_text = "" - self.input_priority = "normal" - elif key == ord(' '): - selected_task = self.task_manager.get_selected_task() - if selected_task: - self.task_manager.toggle_task(selected_task.id) - elif key == ord('d'): - selected_task = self.task_manager.get_selected_task() - if selected_task: - self.task_manager.delete_task(selected_task.id) - - # Help - elif key == ord('h'): - self.show_help = not self.show_help - - return False - - def handle_input_mode(self, key) -> bool: - """Handle key presses in input mode""" - if key == 27: # Escape - self.input_mode = False - self.input_text = "" - elif key == ord('\n') or key == 10: # Enter - if self.input_text.strip(): - self.task_manager.add_task(self.input_text.strip(), self.input_priority) - self.input_mode = False - self.input_text = "" - elif key == ord('\t'): # Tab - cycle priority - priorities = ["normal", "high", "low"] - current_index = priorities.index(self.input_priority) - self.input_priority = priorities[(current_index + 1) % len(priorities)] - elif key == curses.KEY_BACKSPACE or key == 127: - self.input_text = self.input_text[:-1] - elif 32 <= key <= 126: # Printable characters - self.input_text += chr(key) - - return False - -def main(): - """Main entry point""" - ui = TaskManagerUI() - try: - curses.wrapper(ui.run) - except KeyboardInterrupt: - print("\nGoodbye!") - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - main() - diff --git a/plugins/taskman/dino_animation.py b/plugins/taskman/dino_animation.py new file mode 100644 index 000000000..0fa9d03da --- /dev/null +++ b/plugins/taskman/dino_animation.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +""" +Dino Animation Module for Taskman +Chrome dino-style game with enhanced physics and visual effects +""" + +import random +import time +import math + + +class DinoAnimation: + """Enhanced Chrome dino-style animation with physics-based jumping""" + + def __init__(self, width: int): + self.width = width - 10 # Leave margin for UI + self.enabled = False + self.last_update = 0 + + # Player state + self.player_pos = 8 # Fixed horizontal position + self.is_jumping = False + self.jump_velocity = 0 # Vertical velocity + self.jump_height = 0 # Current height + self.gravity = 0.6 # Reduced gravity for more natural feel + self.ground_level = 0 # Ground level + self.initial_jump_velocity = 2.8 # Reduced initial jump velocity + + # Animation state + self.frame_counter = 0 + self.player_sprites = ["โ—", "โ—‹", "โ—", "โ—"] # Running animation + self.jump_sprite = "โ—†" + + # Game elements + self.obstacles = [] + self.clouds = [] + self.ground_dots = [] + self.obstacle_spawn_timer = 0 + + # Game state + self.game_over = False + self.game_over_timer = 0 + self.score = 0 + + # State tracking for flicker reduction + self._last_display_state = None + self._display_cache = None + self._state_changed = True + + # Initialize ground pattern + self._init_ground() + self._init_obstacle_types() + self._init_cloud_patterns() + + def _init_ground(self): + """Initialize scrolling ground pattern""" + ground_chars = [".", "ยท", "ห™", "โˆ˜"] + for i in range(self.width + 20): + self.ground_dots.append({ + 'x': i * 2, + 'char': random.choice(ground_chars), + 'speed': 1.0 + }) + + def _init_obstacle_types(self): + """Initialize obstacle types with ASCII art for tall obstacles""" + self.obstacle_types = [ + # Low ground obstacles (height 1) - single element + {'type': 'ground_low', 'height': 1, 'chars': ['|', 'โ”ƒ', 'โ–Œ', 'โ–ˆ', 'โ–', 'โ•‘']}, + + # Medium ground obstacles (height 1) - single emoji elements + {'type': 'ground_medium', 'height': 1, 'chars': ['๐ŸŒต', '๐Ÿชจ', '๐Ÿ—ฟ']}, + + # Tall ground obstacles (height 2) - ASCII art stacked + {'type': 'ground_tall', 'height': 2, 'chars': [ + {'base': '/|\\', 'top': '/^\\'}, # Mountain peak + {'base': '|||', 'top': 'โ•โ•โ•'}, # Building with roof + {'base': 'โ–“โ–“โ–“', 'top': '^^^'}, # Rock formation + {'base': 'โ”‚โ–ˆโ”‚', 'top': 'โ”Œโ”€โ”'}, # Tower + {'base': '/โ–ˆ\\', 'top': ' โ—‹ '}, # Tree with crown + {'base': 'โ–€โ–€โ–€', 'top': 'โ”ฌโ”ฌโ”ฌ'}, # Fence/barrier + ]}, + + # Flying obstacles (height 2-3) - appear in air + {'type': 'flying', 'height': 0, 'chars': ['๐Ÿฆ…', 'โœˆ๏ธ', '๐Ÿš', '๐Ÿ›ธ', 'โ—Š', 'โ–ฒ', 'โ—']} + ] + + def _init_cloud_patterns(self): + """Initialize cloud patterns for background""" + self.cloud_patterns = ['โ˜', 'โ˜๏ธ', 'โ›…'] # Removed sun and moving elements + + def _start_jump(self): + """Start a jump with realistic physics""" + if not self.is_jumping: + self.is_jumping = True + self.jump_velocity = self.initial_jump_velocity + self.jump_height = 0 + + def _update_jump_physics(self): + """Update jump physics with realistic gravity""" + if self.is_jumping: + # Update position based on velocity + self.jump_height += self.jump_velocity + + # Apply gravity (decelerate upward velocity) + self.jump_velocity -= self.gravity + + # Land when hitting ground + if self.jump_height <= self.ground_level: + self.jump_height = self.ground_level + self.is_jumping = False + self.jump_velocity = 0 + + def _spawn_obstacle(self): + """Spawn a new obstacle at the right edge""" + if len(self.obstacles) < 3: # Reduced obstacle limit for less density + # Reduce flying obstacles frequency + ground_types = [t for t in self.obstacle_types if t['type'] != 'flying'] + flying_types = [t for t in self.obstacle_types if t['type'] == 'flying'] + + # 80% chance for ground obstacles, 20% for flying + if random.random() < 0.8: + obstacle_type = random.choice(ground_types) + else: + obstacle_type = random.choice(flying_types) + + # Handle different obstacle char structures + if obstacle_type['type'] == 'ground_tall': + # For tall obstacles, pick a stacked pair + obstacle_char = random.choice(obstacle_type['chars']) + else: + # For simple obstacles, pick a single char + obstacle_char = random.choice(obstacle_type['chars']) + + # Spawn at right edge with more spacing + spawn_x = self.width + random.randint(15, 40) # Increased spacing + + # Determine height based on obstacle type + if obstacle_type['type'] == 'flying': + height = random.choice([2, 3]) # Flying obstacles at height 2-3 + else: + height = obstacle_type['height'] + + self.obstacles.append({ + 'x': spawn_x, + 'char': obstacle_char, + 'height': height, + 'type': obstacle_type['type'], + 'speed': random.uniform(1.0, 2.0) # Slower speed range + }) + + def _spawn_cloud(self): + """Spawn background clouds for atmosphere""" + if len(self.clouds) < 2: # Reduced cloud limit + cloud_char = random.choice(self.cloud_patterns) + spawn_x = self.width + random.randint(20, 60) # More spacing between clouds + self.clouds.append({ + 'x': spawn_x, + 'char': cloud_char, + 'speed': random.uniform(0.1, 0.3) # Much slower cloud movement for background effect + }) + + def _check_collision(self): + """Check for collisions between player and obstacles""" + if self.is_jumping: + player_height = int(self.jump_height) + 1 + else: + player_height = 0 + + for obstacle in self.obstacles: + # Only check collision with ground-based obstacles + if obstacle['type'] in ['ground_low', 'ground_medium', 'ground_tall']: + # Calculate obstacle width for ASCII art + obstacle_width = 1 + if obstacle['type'] == 'ground_tall' and isinstance(obstacle['char'], dict): + if 'base' in obstacle['char']: + obstacle_width = len(obstacle['char']['base']) + + # Check horizontal collision with obstacle width + for i in range(obstacle_width): + if abs((obstacle['x'] + i) - self.player_pos) <= 1: + # Check vertical collision + obstacle_top = obstacle['height'] + if player_height < obstacle_top: + self.game_over = True + self.game_over_timer = 45 # 3.6 seconds at 80ms intervals + return True + # Flying obstacles don't cause collisions - player passes underneath + return False + + def _reset_game(self): + """Reset game state after game over""" + self.game_over = False + self.obstacles.clear() + self.clouds.clear() + self.score = 0 + self.is_jumping = False + self.jump_velocity = 0 + self.jump_height = 0 + self.obstacle_spawn_timer = 0 + + def toggle_animation(self): + """Toggle animation on/off""" + self.enabled = not self.enabled + if not self.enabled: + self._reset_game() + return self.enabled + + def is_enabled(self): + """Check if animation is enabled""" + return self.enabled + + def _get_display_state_hash(self): + """Generate a hash of the current display state to detect changes""" + state_data = [] + + # Player state + state_data.append(f"player:{self.player_pos}:{self.is_jumping}:{int(self.jump_height)}:{self.game_over}") + + # Obstacles + for obs in self.obstacles: + state_data.append(f"obs:{int(obs['x'])}:{obs['char']}:{obs['height']}") + + # Clouds (only position matters for display) + for cloud in self.clouds: + state_data.append(f"cloud:{int(cloud['x'])}:{cloud['char']}") + + # Frame counter for animation + sprite_index = (self.frame_counter // 3) % len(self.player_sprites) + state_data.append(f"frame:{sprite_index}") + + return hash(tuple(state_data)) + + def update(self): + """Update animation state with improved physics and state tracking""" + if not self.enabled: + return + + current_time = time.time() + # Slower animation: update every 80ms for more relaxed pace + if current_time - self.last_update < 0.08: + return + + self.last_update = current_time + old_state_hash = self._get_display_state_hash() + + # Handle game over state + if self.game_over: + if self.game_over_timer > 0: + self.game_over_timer -= 1 + else: + self._reset_game() + # Check if state changed + new_state_hash = self._get_display_state_hash() + self._state_changed = (old_state_hash != new_state_hash) + return + + # Update frame counter for animations + self.frame_counter += 1 + + # Update jump physics + self._update_jump_physics() + + # Smart auto-jump logic - only jump for ground obstacles + if not self.is_jumping: + for obstacle in self.obstacles: + # Only jump for obstacles that are on the ground or blocking the path + if obstacle['type'] in ['ground_low', 'ground_medium', 'ground_tall']: + distance = obstacle['x'] - self.player_pos + if 4 <= distance <= 7: # Reasonable jump timing window + self._start_jump() + break + # Flying obstacles don't require jumping - player passes underneath + + # Move obstacles + obstacles_changed = False + for obstacle in self.obstacles[:]: + old_x = obstacle['x'] + obstacle['x'] -= obstacle['speed'] + if obstacle['x'] < -5: + self.obstacles.remove(obstacle) + self.score += 1 + obstacles_changed = True + elif int(old_x) != int(obstacle['x']): # Only flag change if integer position changed + obstacles_changed = True + + # Move clouds + clouds_changed = False + for cloud in self.clouds[:]: + old_x = cloud['x'] + cloud['x'] -= cloud['speed'] + if cloud['x'] < -10: + self.clouds.remove(cloud) + clouds_changed = True + elif int(old_x) != int(cloud['x']): # Only flag change if integer position changed + clouds_changed = True + + # Move ground dots (these don't affect display state hash, so no tracking needed) + for dot in self.ground_dots: + dot['x'] -= dot['speed'] + if dot['x'] < -5: + dot['x'] = self.width + 5 + + # Spawn new obstacles and clouds with better timing + self.obstacle_spawn_timer += 1 + if self.obstacle_spawn_timer >= random.randint(15, 25): # Much less frequent spawning + self._spawn_obstacle() + self.obstacle_spawn_timer = 0 + obstacles_changed = True + + # Spawn clouds less frequently + if random.random() < 0.05: # 5% chance each update (reduced from 10%) + self._spawn_cloud() + clouds_changed = True + + # Check collisions + collision_occurred = self._check_collision() + + # Determine if display state changed + new_state_hash = self._get_display_state_hash() + self._state_changed = ( + old_state_hash != new_state_hash or + obstacles_changed or + clouds_changed or + collision_occurred + ) + + def has_display_changed(self): + """Check if the display state has changed since last check""" + return self._state_changed + + def get_display_lines(self) -> tuple: + """Generate all 6 display lines for the animation with caching""" + if not self.enabled: + return ("", "", "", "", "", "") + + # Return cached display if state hasn't changed + if not self._state_changed and self._display_cache is not None: + return self._display_cache + + # Generate new display + display_lines = self._generate_display_lines() + + # Cache the result + self._display_cache = display_lines + self._state_changed = False + + return display_lines + + def _generate_display_lines(self) -> tuple: + """Generate all 6 display lines for the animation""" + if not self.enabled: + return ("", "", "", "", "", "") + + # Initialize all lines + sky_line = [' '] * self.width + high_line = [' '] * self.width + mid_line = [' '] * self.width + jump_line = [' '] * self.width + ground_line = [' '] * self.width + base_line = [' '] * self.width + + # Draw clouds fixed in sky layer only + for cloud in self.clouds: + x = int(cloud['x']) + if 0 <= x < self.width and x < len(sky_line): + sky_line[x] = cloud['char'] + + # Draw obstacles at appropriate heights + for obstacle in self.obstacles: + x = int(obstacle['x']) + if 0 <= x < self.width: + height = obstacle['height'] + char = obstacle['char'] + + if obstacle['type'] == 'flying': + # Flying obstacles appear in high layers only + if height == 3 and x < len(sky_line): + sky_line[x] = char + elif height == 2 and x < len(high_line): + high_line[x] = char + elif obstacle['type'] == 'ground_tall': + # Tall obstacles use ASCII art stacked display + if isinstance(char, dict) and 'base' in char and 'top' in char: + base_art = char['base'] + top_art = char['top'] + + # Draw multi-character ASCII art + for i, c in enumerate(base_art): + if x + i < len(ground_line): + ground_line[x + i] = c + + for i, c in enumerate(top_art): + if x + i < len(jump_line): + jump_line[x + i] = c + else: + # Fallback for simple char + if x < len(ground_line): + ground_line[x] = char + else: + # Simple ground obstacles (height 1) + if x < len(ground_line): + ground_line[x] = char + + # Draw player + if self.player_pos < self.width: + if self.game_over: + # Game over state + if self.player_pos < len(ground_line): + ground_line[self.player_pos] = 'โœ—' + elif self.is_jumping: + # Calculate which line to draw player on based on jump height + # Use more reasonable height mapping + if self.jump_height >= 3: + # Very high jump - sky line + if self.player_pos < len(high_line): + high_line[self.player_pos] = self.jump_sprite + # Add trail at previous height + if self.player_pos > 0 and mid_line[self.player_pos - 1] == ' ': + mid_line[self.player_pos - 1] = 'ยท' + elif self.jump_height >= 2: + # Medium jump - mid line + if self.player_pos < len(mid_line): + mid_line[self.player_pos] = self.jump_sprite + # Add trail at previous height + if self.player_pos > 0 and jump_line[self.player_pos - 1] == ' ': + jump_line[self.player_pos - 1] = 'ยท' + elif self.jump_height >= 1: + # Low jump - jump line + if self.player_pos < len(jump_line): + jump_line[self.player_pos] = self.jump_sprite + # Add trail at ground level + if self.player_pos > 0 and ground_line[self.player_pos - 1] == ' ': + ground_line[self.player_pos - 1] = 'ยท' + else: + # Just off ground - still on ground line + if self.player_pos < len(ground_line): + ground_line[self.player_pos] = self.jump_sprite + else: + # Running animation + sprite_index = (self.frame_counter // 3) % len(self.player_sprites) + if self.player_pos < len(ground_line): + ground_line[self.player_pos] = self.player_sprites[sprite_index] + + # Draw ground pattern + for dot in self.ground_dots: + x = int(dot['x']) + if 0 <= x < self.width and x < len(base_line): + if base_line[x] == ' ': # Don't overwrite other elements + base_line[x] = dot['char'] + + # Convert to strings + return ( + ''.join(sky_line), + ''.join(high_line), + ''.join(mid_line), + ''.join(jump_line), + ''.join(ground_line), + ''.join(base_line) + ) + + def get_status(self) -> str: + """Get current game status""" + if not self.enabled: + return "Animation: OFF (press 'x' to enable)" + + if self.game_over: + return f"Game Over! Score: {self.score} (restarting...)" + + player_state = 'Jumping' if self.is_jumping else 'Running' + return f"Dino Game: {player_state} | Score: {self.score} | Press 'x' to toggle" \ No newline at end of file diff --git a/plugins/taskman/task_cli.py b/plugins/taskman/task_cli.py new file mode 100644 index 000000000..a2f9d73cd --- /dev/null +++ b/plugins/taskman/task_cli.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Terminal Task Manager CLI - Command-line interface for task management + +This module provides command-line access to the task management system. +""" + +import argparse +import json +import os +import sys +from datetime import datetime, timezone +from typing import List, Dict, Optional + +def humanize_time_delta(created_at: str) -> str: + """Convert ISO timestamp to human-readable time delta using local timezone""" + try: + created = datetime.fromisoformat(created_at.replace('Z', '+00:00')) + if created.tzinfo is None: + # If no timezone info, assume it's local time + created = created.replace(tzinfo=timezone.utc).astimezone() + else: + # Convert to local timezone + created = created.astimezone() + + now = datetime.now().astimezone() + delta = now - created + + days = delta.days + hours = delta.seconds // 3600 + minutes = (delta.seconds % 3600) // 60 + + if days > 0: + return f"{days}d" + elif hours > 0: + return f"{hours}h" + elif minutes > 0: + return f"{minutes}m" + else: + return "now" + except: + return "?" + +# ... existing code ... \ No newline at end of file diff --git a/plugins/taskman/task_manager.py b/plugins/taskman/task_manager.py new file mode 100644 index 000000000..35cf8ba45 --- /dev/null +++ b/plugins/taskman/task_manager.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python3 +""" +Terminal Task Manager - A sidebar-style task manager for the terminal + +Features: +- Sidebar display in terminal +- Keyboard shortcuts for task operations +- Persistent task storage +- Task prioritization and highlighting +- Configurable sorting with completed tasks at bottom +- Color-coded task text with priority-based colors +- Humanized creation timers with local timezone +- Visual separator for completed tasks +- Fun running cat animation +""" + +import curses +import json +import os +import random +import time +from datetime import datetime, timezone +from typing import List, Dict, Optional + +# Import the separate animation module +from dino_animation import DinoAnimation + +def humanize_time_delta(created_at: str) -> str: + """Convert ISO timestamp to human-readable time delta using local timezone""" + try: + created = datetime.fromisoformat(created_at.replace('Z', '+00:00')) + if created.tzinfo is None: + # If no timezone info, assume it's local time + created = created.replace(tzinfo=timezone.utc).astimezone() + else: + # Convert to local timezone + created = created.astimezone() + + now = datetime.now().astimezone() + delta = now - created + + days = delta.days + hours = delta.seconds // 3600 + minutes = (delta.seconds % 3600) // 60 + + if days > 0: + return f"{days}d" + elif hours > 0: + return f"{hours}h" + elif minutes > 0: + return f"{minutes}m" + else: + return "now" + except: + return "?" + +class Task: + def __init__(self, id: int, text: str, completed: bool = False, priority: str = "normal", created_at: str = None): + self.id = id + self.text = text + self.completed = completed + self.priority = priority # "high", "normal", "low" + self.created_at = created_at or datetime.now().isoformat() + + def to_dict(self) -> Dict: + return { + "id": self.id, + "text": self.text, + "completed": self.completed, + "priority": self.priority, + "created_at": self.created_at + } + + @classmethod + def from_dict(cls, data: Dict) -> 'Task': + return cls( + id=data["id"], + text=data["text"], + completed=data["completed"], + priority=data.get("priority", "normal"), + created_at=data.get("created_at") + ) + +class TaskManager: + def __init__(self, data_file: str = None): + # Use environment variable or default path + self.data_file = data_file or os.environ.get('TASKMAN_DATA_FILE', os.path.expanduser("~/.taskman/tasks.json")) + self.tasks: List[Task] = [] + self.selected_index = 0 + self.next_id = 1 + self.sort_mode = "default" # "default", "priority", "alphabetical" + self.load_tasks() + + def load_tasks(self): + """Load tasks from JSON file""" + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r') as f: + data = json.load(f) + self.tasks = [Task.from_dict(task_data) for task_data in data.get("tasks", [])] + self.next_id = data.get("next_id", 1) + self.sort_mode = data.get("sort_mode", "default") + except (json.JSONDecodeError, FileNotFoundError): + self.tasks = [] + self.next_id = 1 + self.sort_mode = "default" + + def save_tasks(self): + """Save tasks to JSON file""" + # Ensure directory exists + os.makedirs(os.path.dirname(self.data_file), exist_ok=True) + + data = { + "tasks": [task.to_dict() for task in self.tasks], + "next_id": self.next_id, + "sort_mode": self.sort_mode + } + with open(self.data_file, 'w') as f: + json.dump(data, f, indent=2) + + def add_task(self, text: str, priority: str = "normal") -> Task: + """Add a new task""" + task = Task(self.next_id, text, priority=priority) + self.tasks.append(task) + self.next_id += 1 + self.sort_tasks() + self.save_tasks() + return task + + def toggle_task(self, task_id: int): + """Toggle task completion status""" + for task in self.tasks: + if task.id == task_id: + task.completed = not task.completed + self.sort_tasks() + self.save_tasks() + break + + def delete_task(self, task_id: int): + """Delete a task""" + self.tasks = [task for task in self.tasks if task.id != task_id] + self.save_tasks() + if self.selected_index >= len(self.tasks) and self.tasks: + self.selected_index = len(self.tasks) - 1 + elif not self.tasks: + self.selected_index = 0 + + def get_selected_task(self) -> Optional[Task]: + """Get currently selected task""" + if 0 <= self.selected_index < len(self.tasks): + return self.tasks[self.selected_index] + return None + + def move_selection(self, direction: int): + """Move selection up or down""" + if self.tasks: + self.selected_index = max(0, min(len(self.tasks) - 1, self.selected_index + direction)) + + def set_sort_mode(self, mode: str): + """Set sorting mode and apply it""" + if mode in ["default", "priority", "alphabetical"]: + self.sort_mode = mode + self.sort_tasks() + self.save_tasks() + + def cycle_sort_mode(self): + """Cycle through sort modes""" + modes = ["default", "priority", "alphabetical"] + current_index = modes.index(self.sort_mode) + next_mode = modes[(current_index + 1) % len(modes)] + self.set_sort_mode(next_mode) + + def sort_tasks(self): + """Sort tasks with completed tasks always at bottom""" + if self.sort_mode == "priority": + # Sort by priority: high -> normal -> low, then by ID + priority_order = {"high": 0, "normal": 1, "low": 2} + self.tasks.sort(key=lambda t: ( + t.completed, # Completed tasks go to bottom + priority_order.get(t.priority, 1), + t.id + )) + elif self.sort_mode == "alphabetical": + # Sort alphabetically by task text + self.tasks.sort(key=lambda t: ( + t.completed, # Completed tasks go to bottom + t.text.lower() + )) + else: # default + # Sort by task ID (creation order) + self.tasks.sort(key=lambda t: ( + t.completed, # Completed tasks go to bottom + t.id + )) + +class TaskManagerUI: + def __init__(self): + self.task_manager = TaskManager() + self.input_mode = False + self.input_text = "" + self.input_priority = "normal" + self.show_help = False + self.dino_animation = None + + def run(self, stdscr): + """Main application loop with advanced flicker reduction""" + curses.curs_set(0) # Hide cursor + stdscr.nodelay(1) # Non-blocking input + stdscr.timeout(80) # Slightly slower refresh for stability (80ms) + + # Color pairs + curses.start_color() + curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Selected + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # Completed bullet + curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) # High priority + curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Normal priority + curses.init_pair(5, curses.COLOR_CYAN, curses.COLOR_BLACK) # Low priority + curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_BLACK) # Default text + curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_WHITE) # Input mode + curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLACK) # Dimmed text + # Completed task colors (grayed out versions) + curses.init_pair(9, curses.COLOR_RED, curses.COLOR_BLACK) # Completed high priority (dimmed red) + curses.init_pair(10, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Completed normal priority (dimmed yellow) + curses.init_pair(11, curses.COLOR_CYAN, curses.COLOR_BLACK) # Completed low priority (dimmed cyan) + + # Initialize dino animation + height, width = stdscr.getmaxyx() + self.dino_animation = DinoAnimation(width - 4) + + # Enhanced state tracking for flicker reduction + last_state = { + 'task_count': -1, + 'selected_index': -1, + 'input_mode': False, + 'show_help': False, + 'input_text': '', + 'input_priority': '', + 'sort_mode': '', + 'task_hash': '', # Hash of all task content + 'animation_enabled': True, + 'last_minute': -1, # Track minute changes for timer updates + } + + # Force initial draw + force_redraw = True + animation_counter = 0 + + while True: + # Update animation less frequently to reduce flicker + if self.dino_animation and animation_counter % 2 == 0: # Update every other cycle + self.dino_animation.update() + animation_counter += 1 + + # Get current state + current_state = { + 'task_count': len(self.task_manager.tasks), + 'selected_index': self.task_manager.selected_index, + 'input_mode': self.input_mode, + 'show_help': self.show_help, + 'input_text': self.input_text, + 'input_priority': self.input_priority, + 'sort_mode': self.task_manager.sort_mode, + 'task_hash': self._get_task_hash(), + 'animation_enabled': self.dino_animation.is_enabled() if self.dino_animation else False, + 'last_minute': self._get_current_minute(), + } + + # Determine what needs to be redrawn + needs_full_redraw = ( + force_redraw or + last_state['task_count'] != current_state['task_count'] or + last_state['task_hash'] != current_state['task_hash'] or + last_state['input_mode'] != current_state['input_mode'] or + last_state['show_help'] != current_state['show_help'] or + last_state['sort_mode'] != current_state['sort_mode'] or + last_state['last_minute'] != current_state['last_minute'] # Redraw when minute changes + ) + + needs_selection_update = ( + last_state['selected_index'] != current_state['selected_index'] + ) + + needs_input_update = ( + current_state['input_mode'] and ( + last_state['input_text'] != current_state['input_text'] or + last_state['input_priority'] != current_state['input_priority'] + ) + ) + + needs_animation_update = ( + current_state['animation_enabled'] and + not current_state['input_mode'] and + not current_state['show_help'] and + animation_counter % 3 == 0 and # Update animation every 3rd cycle + (self.dino_animation.has_display_changed() if self.dino_animation else False) + ) + + # Perform appropriate redraw + if needs_full_redraw: + self.draw_ui(stdscr) + force_redraw = False + elif needs_selection_update: + self.update_selection_display(stdscr) + elif needs_input_update: + self.update_input_display(stdscr) + elif needs_animation_update: + self.update_animation_display(stdscr) + + # Update state tracking + last_state = current_state.copy() + + try: + key = stdscr.getch() + except: + continue + + if key == -1: + continue + + # Handle input + if self.input_mode: + if self.handle_input_mode(key): + break + else: + if self.handle_normal_mode(key): + break + + def _get_task_hash(self): + """Generate a hash of all task content to detect changes""" + task_data = [] + for task in self.task_manager.tasks: + task_data.append(f"{task.id}:{task.text}:{task.completed}:{task.priority}") + return hash(tuple(task_data)) + + def _get_current_minute(self): + """Get current minute for timer update detection""" + from datetime import datetime + return datetime.now().minute + + def update_selection_display(self, stdscr): + """Update only the selection highlighting without full redraw""" + height, width = stdscr.getmaxyx() + start_row = 3 + completed_count = len([t for t in self.task_manager.tasks if t.completed]) + + # Clear previous selection and draw new one + for i, task in enumerate(self.task_manager.tasks): + if start_row + i >= height - 8: # Leave room for animation + break + + # Skip separator row + if task.completed and completed_count > 0 and i > 0 and not self.task_manager.tasks[i-1].completed: + start_row += 1 + if start_row + i >= height - 8: + break + + row = start_row + i + + # Only update this row if it's the current or previous selection + if i == self.task_manager.selected_index or i == getattr(self, '_last_selected', -1): + # Redraw this task line + self._draw_task_line(stdscr, task, i, row, width) + + self._last_selected = self.task_manager.selected_index + stdscr.refresh() + + def update_input_display(self, stdscr): + """Update only the input area without full redraw""" + height, width = stdscr.getmaxyx() + input_y = height - 5 + + # Clear input area + stdscr.addstr(input_y, 2, " " * (width - 4)) + stdscr.addstr(input_y + 1, 2, " " * (width - 4)) + + # Redraw input + stdscr.addstr(input_y, 2, "New task: ", curses.A_BOLD) + stdscr.addstr(input_y, 12, self.input_text, curses.color_pair(7)) + stdscr.addstr(input_y + 1, 2, f"Priority: {self.input_priority} (Tab to change)", curses.A_DIM) + + stdscr.refresh() + + def update_animation_display(self, stdscr): + """Update only the animation area without full redraw""" + if not self.dino_animation or not self.dino_animation.is_enabled(): + return + + height, width = stdscr.getmaxyx() + animation_y = height - 8 + + try: + # Clear animation area + for i in range(7): # 6 animation lines + 1 status line + stdscr.addstr(animation_y + i, 2, " " * (width - 4)) + + # Redraw animation + self.draw_dino_animation(stdscr, height, width) + stdscr.refresh() + except curses.error: + pass + + def _draw_task_line(self, stdscr, task, index, row, width): + """Draw a single task line""" + # Get humanized time + time_str = humanize_time_delta(task.created_at) + + # Determine colors based on task status and priority + if task.completed: + bullet_color = curses.color_pair(2) # Green for completed bullet + if task.priority == "high": + text_color = curses.color_pair(9) | curses.A_DIM # Dimmed red + elif task.priority == "low": + text_color = curses.color_pair(11) | curses.A_DIM # Dimmed cyan + else: + text_color = curses.color_pair(10) | curses.A_DIM # Dimmed yellow + else: + # Active tasks - bullet and text same color based on priority + if task.priority == "high": + bullet_color = curses.color_pair(3) # Red + text_color = curses.color_pair(3) # Red + elif task.priority == "low": + bullet_color = curses.color_pair(5) # Cyan + text_color = curses.color_pair(5) # Cyan + else: + bullet_color = curses.color_pair(4) # Yellow + text_color = curses.color_pair(4) # Yellow + + # Highlight selected task + if index == self.task_manager.selected_index: + bullet_color |= curses.A_REVERSE + text_color |= curses.A_REVERSE + + # Task status and priority icons + status_icon = "โœ“" if task.completed else "โ—‹" + priority_icon = { + "high": "!", + "normal": "-", + "low": "ยท" + }.get(task.priority, "-") + + # Truncate task text if too long + max_text_width = width - 25 + task_text = task.text[:max_text_width] + "..." if len(task.text) > max_text_width else task.text + + # Clear the line first + stdscr.addstr(row, 2, " " * (width - 4)) + + # Draw components + timer_part = f"[{time_str:>3}]" + stdscr.addstr(row, 2, timer_part, curses.A_DIM) + + bullet_part = f" {status_icon} [{priority_icon}]" + stdscr.addstr(row, 8, bullet_part, bullet_color) + + text_part = f" {task_text}" + stdscr.addstr(row, 8 + len(bullet_part), text_part, text_color) + + def draw_ui(self, stdscr): + """Draw the user interface""" + stdscr.clear() + height, width = stdscr.getmaxyx() + + # Title with version + title = "Taskman v2.2" + sort_indicator = f"[Sort: {self.task_manager.sort_mode}]" + stdscr.addstr(0, 2, title, curses.A_BOLD) + stdscr.addstr(0, width - len(sort_indicator) - 2, sort_indicator, curses.A_DIM) + + # Task count + pending_count = len([t for t in self.task_manager.tasks if not t.completed]) + completed_count = len([t for t in self.task_manager.tasks if t.completed]) + count_text = f"Pending: {pending_count}, Completed: {completed_count}" + stdscr.addstr(1, 2, count_text, curses.A_DIM) + + # Help line + if not self.show_help: + help_text = "Press 'h' for help, 'q' to quit" + stdscr.addstr(1, width - len(help_text) - 2, help_text, curses.A_DIM) + + # Tasks list + start_row = 3 + completed_separator_drawn = False + + # Calculate available space for tasks (leave room for animation) + max_task_row = height - 5 # Leave 5 lines for animation and status + + for i, task in enumerate(self.task_manager.tasks): + if start_row + i >= max_task_row: + break + + # Draw separator before first completed task + if not completed_separator_drawn and task.completed and completed_count > 0: + separator = "โ”€" * (width - 4) + stdscr.addstr(start_row + i, 2, separator, curses.A_DIM) + start_row += 1 + completed_separator_drawn = True + if start_row + i >= max_task_row: + break + + # Get humanized time + time_str = humanize_time_delta(task.created_at) + + # Determine colors based on task status and priority + if task.completed: + bullet_color = curses.color_pair(2) # Green for completed bullet + if task.priority == "high": + text_color = curses.color_pair(9) | curses.A_DIM # Dimmed red + elif task.priority == "low": + text_color = curses.color_pair(11) | curses.A_DIM # Dimmed cyan + else: + text_color = curses.color_pair(10) | curses.A_DIM # Dimmed yellow + else: + # Active tasks - bullet and text same color based on priority + if task.priority == "high": + bullet_color = curses.color_pair(3) # Red + text_color = curses.color_pair(3) # Red + elif task.priority == "low": + bullet_color = curses.color_pair(5) # Cyan + text_color = curses.color_pair(5) # Cyan + else: + bullet_color = curses.color_pair(4) # Yellow + text_color = curses.color_pair(4) # Yellow + + # Highlight selected task + if i == self.task_manager.selected_index: + bullet_color |= curses.A_REVERSE + text_color |= curses.A_REVERSE + + # Task status and priority icons + status_icon = "โœ“" if task.completed else "โ—‹" + priority_icon = { + "high": "!", + "normal": "-", + "low": "ยท" + }.get(task.priority, "-") + + # Truncate task text if too long (account for timer) + max_text_width = width - 25 # Leave space for timer and bullet + task_text = task.text[:max_text_width] + "..." if len(task.text) > max_text_width else task.text + + # Draw timer + timer_part = f"[{time_str:>3}]" + stdscr.addstr(start_row + i, 2, timer_part, curses.A_DIM) + + # Draw bullet with color + bullet_part = f" {status_icon} [{priority_icon}]" + stdscr.addstr(start_row + i, 8, bullet_part, bullet_color) + + # Draw task text with priority-based color + text_part = f" {task_text}" + stdscr.addstr(start_row + i, 8 + len(bullet_part), text_part, text_color) + + # Input area + if self.input_mode: + input_y = height - 5 + stdscr.addstr(input_y, 2, "New task: ", curses.A_BOLD) + stdscr.addstr(input_y, 12, self.input_text, curses.color_pair(7)) + stdscr.addstr(input_y + 1, 2, f"Priority: {self.input_priority} (Tab to change)", curses.A_DIM) + curses.curs_set(1) # Show cursor in input mode + else: + curses.curs_set(0) # Hide cursor + + # Help panel + if self.show_help: + self.draw_help(stdscr, height, width) + + # Dino animation (only if not in help mode) + if not self.show_help and self.dino_animation: + self.draw_dino_animation(stdscr, height, width) + + # Status line + status_y = height - 1 + if self.input_mode: + status_text = "Enter: Save task | Esc: Cancel | Tab: Change priority" + else: + status_text = "n: New | Space: Toggle | d: Delete | s: Sort | โ†‘โ†“: Navigate | h: Help | x: Animation | q: Quit" + + stdscr.addstr(status_y, 2, status_text[:width-4], curses.A_DIM) + + stdscr.refresh() + + def draw_dino_animation(self, stdscr, height: int, width: int): + """Draw the enhanced multi-line dino animation at the bottom""" + # Don't show animation in input mode or help mode to avoid covering input + if not self.dino_animation or not self.dino_animation.is_enabled() or self.input_mode or self.show_help: + return + + # Reserve more space for 6-line animation plus status + animation_y = height - 8 + + try: + # Get all animation lines + sky_line, high_line, mid_line, jump_line, ground_line, base_line = self.dino_animation.get_display_lines() + + # Draw all 6 lines of animation with proper spacing + stdscr.addstr(animation_y, 2, sky_line[:width-4], curses.color_pair(6)) + stdscr.addstr(animation_y + 1, 2, high_line[:width-4], curses.color_pair(6)) + stdscr.addstr(animation_y + 2, 2, mid_line[:width-4], curses.color_pair(6)) + stdscr.addstr(animation_y + 3, 2, jump_line[:width-4], curses.color_pair(6)) + stdscr.addstr(animation_y + 4, 2, ground_line[:width-4], curses.color_pair(6)) + stdscr.addstr(animation_y + 5, 2, base_line[:width-4], curses.color_pair(6)) + + # Draw animation status + if self.dino_animation.is_enabled(): + status = self.dino_animation.get_status() + stdscr.addstr(animation_y + 6, 2, status[:width-4], curses.A_DIM) + + except curses.error: + # Ignore curses errors (e.g., writing outside screen bounds) + pass + + def draw_help(self, stdscr, height, width): + """Draw help panel""" + help_lines = [ + "KEYBOARD SHORTCUTS:", + "", + "Navigation:", + " โ†‘/k - Move up", + " โ†“/j - Move down", + "", + "Task Operations:", + " n - New task", + " Space - Toggle completion", + " d - Delete task", + "", + "Sorting:", + " s - Cycle sort modes", + " p - Sort by priority", + " a - Sort alphabetically", + "", + "Priority Levels:", + " ! High priority (red text)", + " - Normal priority (yellow text)", + " ยท Low priority (cyan text)", + "", + "Features:", + " โ€ข Timer shows task age (local time)", + " โ€ข Completed tasks are dimmed", + " โ€ข Separator divides active/completed", + " โ€ข Running ASCII dino animation for fun!", + "", + "Other:", + " h - Toggle this help", + " x - Toggle animation on/off", + " q - Quit application", + "", + "CLI Usage:", + " tasks add 'text' [priority]", + " tasks list [filter]", + " tasks done ", + " tasks delete ", + "", + "Note: Completed tasks always appear at bottom", + ] + + # Calculate help panel size + help_width = max(len(line) for line in help_lines) + 4 + help_height = len(help_lines) + 2 + + start_x = max(2, (width - help_width) // 2) + start_y = max(2, (height - help_height) // 2) + + # Draw help background + for i in range(help_height): + stdscr.addstr(start_y + i, start_x, " " * help_width, curses.color_pair(7)) + + # Draw help content + for i, line in enumerate(help_lines): + stdscr.addstr(start_y + 1 + i, start_x + 2, line, curses.color_pair(7)) + + def handle_normal_mode(self, key) -> bool: + """Handle key presses in normal mode""" + # Quit + if key == ord('q'): + return True + + # Navigation + elif key == curses.KEY_UP or key == ord('k'): + self.task_manager.move_selection(-1) + elif key == curses.KEY_DOWN or key == ord('j'): + self.task_manager.move_selection(1) + + # Task operations + elif key == ord('n'): + self.input_mode = True + self.input_text = "" + self.input_priority = "normal" + elif key == ord(' '): + selected_task = self.task_manager.get_selected_task() + if selected_task: + self.task_manager.toggle_task(selected_task.id) + elif key == ord('d'): + selected_task = self.task_manager.get_selected_task() + if selected_task: + self.task_manager.delete_task(selected_task.id) + + # Sorting shortcuts + elif key == ord('s'): + self.task_manager.cycle_sort_mode() + elif key == ord('p'): + self.task_manager.set_sort_mode("priority") + elif key == ord('a'): + self.task_manager.set_sort_mode("alphabetical") + + # Help + elif key == ord('h'): + self.show_help = not self.show_help + + # Animation toggle + elif key == ord('x'): + if self.dino_animation: + enabled = self.dino_animation.toggle_animation() + # Brief status message could be added here if needed + + return False + + def handle_input_mode(self, key) -> bool: + """Handle key presses in input mode""" + if key == 27: # Escape + self.input_mode = False + self.input_text = "" + elif key == ord('\n') or key == 10: # Enter + if self.input_text.strip(): + self.task_manager.add_task(self.input_text.strip(), self.input_priority) + self.input_mode = False + self.input_text = "" + elif key == ord('\t'): # Tab - cycle priority + priorities = ["normal", "high", "low"] + current_index = priorities.index(self.input_priority) + self.input_priority = priorities[(current_index + 1) % len(priorities)] + elif key == curses.KEY_BACKSPACE or key == 127: + self.input_text = self.input_text[:-1] + elif 32 <= key <= 126: # Printable characters + self.input_text += chr(key) + + return False + +def main(): + """Main entry point""" + ui = TaskManagerUI() + try: + curses.wrapper(ui.run) + except KeyboardInterrupt: + print("\nGoodbye!") + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + main() + diff --git a/plugins/taskman/taskman.plugin.zsh b/plugins/taskman/taskman.plugin.zsh index 3cb772cc6..0d4dc737b 100644 --- a/plugins/taskman/taskman.plugin.zsh +++ b/plugins/taskman/taskman.plugin.zsh @@ -4,15 +4,17 @@ # A powerful sidebar-style task manager that runs entirely in your terminal # # Author: @oiahoon -# Version: 1.0.0 +# Version: 2.0.0 # Repository: https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/taskman # # Features: # - Interactive terminal UI with keyboard shortcuts # - Command line interface for quick operations # - Priority system with color coding -# - Persistent JSON storage +# - Persistent JSON storage with configurable location # - Vim-like keybindings +# - Multiple sorting modes with completed tasks at bottom +# - Color-coded bullets with neutral task text # - Zero external dependencies (Python 3 only) # @@ -22,8 +24,21 @@ TASKMAN_PLUGIN_DIR="${0:h}" # Data directory (store tasks in home directory) TASKMAN_DATA_DIR="$HOME/.taskman" -# Ensure data directory exists -[[ ! -d "$TASKMAN_DATA_DIR" ]] && mkdir -p "$TASKMAN_DATA_DIR" +# Allow users to configure custom task file path +# Users can set TASKMAN_DATA_FILE in their .zshrc to customize storage location +# Example: export TASKMAN_DATA_FILE="$HOME/my-tasks/tasks.json" +if [[ -z "$TASKMAN_DATA_FILE" ]]; then + TASKMAN_DATA_FILE="$TASKMAN_DATA_DIR/tasks.json" +fi + +# Ensure data directory exists (for default path) +if [[ "$TASKMAN_DATA_FILE" == "$TASKMAN_DATA_DIR/tasks.json" ]]; then + [[ ! -d "$TASKMAN_DATA_DIR" ]] && mkdir -p "$TASKMAN_DATA_DIR" +else + # For custom paths, ensure the directory exists + custom_dir=$(dirname "$TASKMAN_DATA_FILE") + [[ ! -d "$custom_dir" ]] && mkdir -p "$custom_dir" +fi # Color definitions for output TASKMAN_COLOR_RED="\033[31m" @@ -36,8 +51,12 @@ TASKMAN_COLOR_RESET="\033[0m" # Main task manager function tasks() { local action="$1" - shift - + + # Only shift if there are arguments + if [[ $# -gt 0 ]]; then + shift + fi + case "$action" in "" | "ui" | "show") # Launch the full UI @@ -59,6 +78,10 @@ tasks() { # Delete a task _taskman_delete_task "$@" ;; + "sort") + # Set sorting mode + _taskman_set_sort "$@" + ;; "help" | "-h" | "--help") _taskman_show_help ;; @@ -77,12 +100,12 @@ _taskman_launch_ui() { echo "${TASKMAN_COLOR_YELLOW}Please install Python 3 to use the interactive UI.${TASKMAN_COLOR_RESET}" return 1 fi - - # Set the data file path - export TASKMAN_DATA_FILE="$TASKMAN_DATA_DIR/tasks.json" - + + # Set the data file path (use configured path) + export TASKMAN_DATA_FILE + # Run the Python task manager - python3 "$TASKMAN_PLUGIN_DIR/bin/task_manager.py" + python3 "$TASKMAN_PLUGIN_DIR/task_manager.py" } # Quick add task from command line @@ -92,10 +115,10 @@ _taskman_add_task() { echo "Usage: tasks add [priority]" return 1 fi - + local task_text="$1" local priority="${2:-normal}" - + # Validate priority case "$priority" in "high"|"normal"|"low") @@ -105,10 +128,10 @@ _taskman_add_task() { priority="normal" ;; esac - + if command -v python3 >/dev/null 2>&1; then - python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" add "$task_text" "$priority" - + TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" add "$task_text" "$priority" + # Show color-coded confirmation local priority_color case "$priority" in @@ -116,7 +139,7 @@ _taskman_add_task() { "low") priority_color="$TASKMAN_COLOR_CYAN" ;; *) priority_color="$TASKMAN_COLOR_YELLOW" ;; esac - + echo "${TASKMAN_COLOR_GREEN}โœ“ Added task:${TASKMAN_COLOR_RESET} ${priority_color}[$priority]${TASKMAN_COLOR_RESET} $task_text" else echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" @@ -127,9 +150,9 @@ _taskman_add_task() { # List tasks in terminal _taskman_list_tasks() { local filter="$1" - + if command -v python3 >/dev/null 2>&1; then - python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" list "$filter" + TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" list "$filter" else echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" return 1 @@ -143,10 +166,10 @@ _taskman_complete_task() { echo "Usage: tasks done " return 1 fi - + local task_id="$1" if command -v python3 >/dev/null 2>&1; then - python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" complete "$task_id" + TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" complete "$task_id" else echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" return 1 @@ -160,10 +183,28 @@ _taskman_delete_task() { echo "Usage: tasks delete " return 1 fi - + local task_id="$1" if command -v python3 >/dev/null 2>&1; then - python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" delete "$task_id" + TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" delete "$task_id" + else + echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" + return 1 + fi +} + +# Set sorting mode +_taskman_set_sort() { + if [[ $# -eq 0 ]]; then + echo "${TASKMAN_COLOR_RED}Error: Please provide sort mode${TASKMAN_COLOR_RESET}" + echo "Usage: tasks sort " + echo "Available modes: default, priority, alphabetical" + return 1 + fi + + local sort_mode="$1" + if command -v python3 >/dev/null 2>&1; then + TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" sort "$sort_mode" else echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" return 1 @@ -173,7 +214,7 @@ _taskman_delete_task() { # Show help _taskman_show_help() { cat << 'EOF' -Terminal Task Manager - Oh-My-Zsh Plugin +Terminal Task Manager - Oh-My-Zsh Plugin v2.0 Usage: tasks [action] [arguments] @@ -185,6 +226,7 @@ Actions: list [filter] List tasks (filter: all, pending, completed) done Mark task as completed delete Delete a task + sort Set sorting mode (default, priority, alphabetical) help Show this help Examples: @@ -195,45 +237,95 @@ Examples: tasks list pending # List only pending tasks tasks done 3 # Mark task ID 3 as completed tasks delete 5 # Delete task ID 5 + tasks sort priority # Sort by priority Interactive UI Keys: โ†‘/k Move up n New task โ†“/j Move down Space Toggle completion - h Help d Delete task - q Quit + s Cycle sort d Delete task + p Sort priority a Sort alphabetical + h Help q Quit -Aliases: - tm, task, todo - All point to 'tasks' command +Features: + โ€ข Configurable storage location via TASKMAN_DATA_FILE environment variable + โ€ข Automatic sorting with completed tasks always at bottom + โ€ข Priority-based text colors with dimmed completed tasks + โ€ข Humanized creation timers showing task age + โ€ข Visual separator between active and completed tasks + โ€ข Multiple sort modes: creation order, priority, alphabetical + +Priority Colors: + โ€ข High Priority (!) - Red text for active, dimmed red for completed + โ€ข Normal Priority (-) - Yellow text for active, dimmed yellow for completed + โ€ข Low Priority (ยท) - Cyan text for active, dimmed cyan for completed Data Storage: - Tasks are stored in: ~/.taskman/tasks.json + Default: ~/.taskman/tasks.json + Custom: Set TASKMAN_DATA_FILE environment variable -Requirements: - Python 3.6+ (for interactive UI and CLI operations) +Configuration: + # In your ~/.zshrc (before loading Oh-My-Zsh) + export TASKMAN_DATA_FILE="$HOME/Documents/my-tasks.json" + +Priority Levels: + โ€ข High Priority (!) - Red text, for urgent tasks + โ€ข Normal Priority (-) - Yellow text, default priority + โ€ข Low Priority (ยท) - Cyan text, for less urgent tasks + +Color Scheme: + โ€ข Task text is colored based on priority and completion status + โ€ข Completed tasks show dimmed priority colors to maintain context + โ€ข Visual separator divides active and completed tasks EOF } -# Convenient aliases +# Aliases for convenience +alias taskman='tasks' alias tm='tasks' alias task='tasks' alias todo='tasks' +# Auto-completion for task actions +_taskman_completion() { + local -a actions + actions=( + 'ui:Launch interactive UI' + 'show:Launch interactive UI' + 'add:Add new task' + 'new:Add new task' + 'create:Add new task' + 'list:List tasks' + 'ls:List tasks' + 'done:Mark task complete' + 'complete:Mark task complete' + 'delete:Delete task' + 'del:Delete task' + 'rm:Delete task' + 'sort:Set sorting mode' + 'help:Show help' + ) + _describe 'actions' actions +} + +compdef _taskman_completion tasks tm task todo + # Quick access function for sidebar workflow task-sidebar() { echo "${TASKMAN_COLOR_BLUE}Starting task manager in sidebar mode...${TASKMAN_COLOR_RESET}" - echo "${TASKMAN_COLOR_YELLOW}Tip: Use terminal split to run this alongside your work${TASKMAN_COLOR_RESET}" + echo "${TASKMAN_COLOR_YELLOW}Tip: Use 'Cmd+D' (or equivalent) to split terminal horizontally${TASKMAN_COLOR_RESET}" + echo "${TASKMAN_COLOR_CYAN}New features: Sort with 's/p/a' keys, completed tasks auto-move to bottom${TASKMAN_COLOR_RESET}" tasks ui } -# Optional: Show task summary on shell startup -# Uncomment the next line to enable startup summary +# Show a quick summary on shell startup (optional) +# Uncomment the next line if you want to see task summary when opening terminal # _taskman_startup_summary _taskman_startup_summary() { - if [[ -f "$TASKMAN_DATA_DIR/tasks.json" ]] && command -v python3 >/dev/null 2>&1; then - local pending_count=$(python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" count pending 2>/dev/null || echo "0") - local completed_count=$(python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" count completed 2>/dev/null || echo "0") - + if [[ -f "$TASKMAN_DATA_FILE" ]] && command -v python3 >/dev/null 2>&1; then + local pending_count=$(TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" count pending 2>/dev/null || echo "0") + local completed_count=$(TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" count completed 2>/dev/null || echo "0") + if [[ "$pending_count" -gt 0 ]]; then echo "${TASKMAN_COLOR_CYAN}๐Ÿ“‹ Task Summary: ${pending_count} pending, ${completed_count} completed${TASKMAN_COLOR_RESET}" echo "${TASKMAN_COLOR_YELLOW} Type 'tasks' to manage your tasks${TASKMAN_COLOR_RESET}"