feat: add an optional fun animation on the bottom

This commit is contained in:
Joey Huang 2025-06-16 16:22:26 +08:00
commit df6b695dd6
8 changed files with 1545 additions and 777 deletions

View file

@ -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 ### Advanced Features
- ⌨️ **Vim-like Keybindings**: Navigate with `j`/`k`, `Space` to toggle - **Smart Sorting**: Multiple sort modes (creation order, priority, alphabetical) with completed tasks always at bottom
- 🎯 **Priority System**: High, normal, and low priority with color coding - **Priority-Based Colors**: Task text colored by priority (red=high, yellow=normal, cyan=low)
- 💾 **Persistent Storage**: Tasks saved in `~/.taskman/tasks.json` - **Humanized Timers**: Shows task age in human-readable format ([5m], [2h], [3d]) using local timezone
- 🎨 **Rich Colors**: Visual indicators for task status and priority - **Visual Separation**: Horizontal line separates active and completed tasks
- ⚡ **Zero Config**: Works immediately after installation - **Configurable Storage**: Set custom task file location via `TASKMAN_DATA_FILE` environment variable
- 🔧 **Shell Integration**: Aliases, completion, and sidebar workflow - **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 ```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 source ~/.zshrc
``` ```
3. Start using! ### Using Oh My Zsh Plugin Manager
If you're using a plugin manager like `oh-my-zsh-plugins`:
```bash ```bash
tasks add "My first task" # Add to your plugin list
plugins=(... taskman)
``` ```
## Requirements ## 🎮 Usage
- **Python 3.6+** (for interactive UI and CLI operations)
- **Terminal with color support** (most modern terminals)
## Usage
### Interactive UI ### Interactive UI
Launch the interactive task manager:
Launch the full-screen task manager:
```bash ```bash
tasks # Launch interactive UI taskman
tasks ui # Same as above # or use aliases
task-sidebar # Launch with sidebar usage tips tasks
tm
todo
``` ```
#### Keyboard Shortcuts ### Keyboard Shortcuts
| Key | Action | #### Navigation
|-----|--------| - `↑/k` - Move selection up
| `↑`/`k` | Move up | - `↓/j` - Move selection down
| `↓`/`j` | Move down |
| `n` | Create new task | #### Task Operations
| `Space` | Toggle task completion | - `n` - Create new task
| `d` | Delete selected task | - `Space` - Toggle task completion
| `Tab` | Cycle priority when creating tasks | - `d` - Delete selected task
| `h` | Toggle help panel |
| `q` | Quit | #### 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 ### Command Line Interface
#### Adding Tasks
```bash ```bash
tasks add "Fix login bug" # Normal priority # Add a new task
tasks add "Deploy to production" high # High priority tasks add "Complete project documentation"
tasks add "Update documentation" low # Low priority 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 ```bash
tasks list # All tasks export TASKMAN_DATA_FILE="$HOME/my-tasks.json"
tasks list pending # Only pending tasks
tasks list completed # Only completed tasks
tasks ls # Short alias
``` ```
#### 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 ## 🎨 Visual Features
tasks done 3 # Mark task ID 3 as completed
tasks delete 5 # Delete task ID 5 ### Color System
tasks help # Show help - **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 ### Task Data Format
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 <TAB> # Shows: add, list, done, delete, etc.
tasks add "task" <TAB> # Shows: high, normal, low
tasks done <TAB> # 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`:
```json ```json
{ {
"tasks": [ "tasks": [
{ {
"id": 1, "id": 1,
"text": "Fix login bug", "text": "Complete project setup",
"completed": false, "completed": true,
"priority": "high", "priority": "high",
"created_at": "2024-01-15T10:30:00" "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 ```bash
# Morning planning # Run with Python directly for debugging
tasks add "Review PR #123" high python3 ~/.oh-my-zsh/plugins/taskman/task_manager.py
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
``` ```
### Project Management ## 🤝 Contributing
```bash This is an Oh My Zsh version of the Taskman plugin. For contributions and bug reports, please refer to the original osh framework repository.
# Sprint planning
tasks add "Implement user auth" high
tasks add "Add unit tests" normal
tasks add "Update README" low
# Track progress ## 📄 License
tasks list
# Mark completed Part of the Oh My Zsh ecosystem. See individual license files for details.
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)
--- ---
**Happy task managing! 🚀** **Enjoy managing your tasks with style! 🐱✨**

View file

@ -49,7 +49,7 @@ _taskman() {
local -a task_ids local -a task_ids
# Try to get task IDs from the CLI # 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 if [[ ${#task_ids[@]} -gt 0 ]]; then
_describe 'task IDs' task_ids _describe 'task IDs' task_ids

View file

@ -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 <command> [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()

View file

@ -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 <id>",
" tasks delete <id>",
]
# 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()

View file

@ -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"

View file

@ -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 ...

View file

@ -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 <id>",
" tasks delete <id>",
"",
"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()

View file

@ -4,15 +4,17 @@
# A powerful sidebar-style task manager that runs entirely in your terminal # A powerful sidebar-style task manager that runs entirely in your terminal
# #
# Author: @oiahoon # Author: @oiahoon
# Version: 1.0.0 # Version: 2.0.0
# Repository: https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/taskman # Repository: https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/taskman
# #
# Features: # Features:
# - Interactive terminal UI with keyboard shortcuts # - Interactive terminal UI with keyboard shortcuts
# - Command line interface for quick operations # - Command line interface for quick operations
# - Priority system with color coding # - Priority system with color coding
# - Persistent JSON storage # - Persistent JSON storage with configurable location
# - Vim-like keybindings # - Vim-like keybindings
# - Multiple sorting modes with completed tasks at bottom
# - Color-coded bullets with neutral task text
# - Zero external dependencies (Python 3 only) # - Zero external dependencies (Python 3 only)
# #
@ -22,8 +24,21 @@ TASKMAN_PLUGIN_DIR="${0:h}"
# Data directory (store tasks in home directory) # Data directory (store tasks in home directory)
TASKMAN_DATA_DIR="$HOME/.taskman" TASKMAN_DATA_DIR="$HOME/.taskman"
# Ensure data directory exists # Allow users to configure custom task file path
[[ ! -d "$TASKMAN_DATA_DIR" ]] && mkdir -p "$TASKMAN_DATA_DIR" # 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 # Color definitions for output
TASKMAN_COLOR_RED="\033[31m" TASKMAN_COLOR_RED="\033[31m"
@ -36,7 +51,11 @@ TASKMAN_COLOR_RESET="\033[0m"
# Main task manager function # Main task manager function
tasks() { tasks() {
local action="$1" local action="$1"
# Only shift if there are arguments
if [[ $# -gt 0 ]]; then
shift shift
fi
case "$action" in case "$action" in
"" | "ui" | "show") "" | "ui" | "show")
@ -59,6 +78,10 @@ tasks() {
# Delete a task # Delete a task
_taskman_delete_task "$@" _taskman_delete_task "$@"
;; ;;
"sort")
# Set sorting mode
_taskman_set_sort "$@"
;;
"help" | "-h" | "--help") "help" | "-h" | "--help")
_taskman_show_help _taskman_show_help
;; ;;
@ -78,11 +101,11 @@ _taskman_launch_ui() {
return 1 return 1
fi fi
# Set the data file path # Set the data file path (use configured path)
export TASKMAN_DATA_FILE="$TASKMAN_DATA_DIR/tasks.json" export TASKMAN_DATA_FILE
# Run the Python task manager # 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 # Quick add task from command line
@ -107,7 +130,7 @@ _taskman_add_task() {
esac esac
if command -v python3 >/dev/null 2>&1; then 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 # Show color-coded confirmation
local priority_color local priority_color
@ -129,7 +152,7 @@ _taskman_list_tasks() {
local filter="$1" local filter="$1"
if command -v python3 >/dev/null 2>&1; then 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 else
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
return 1 return 1
@ -146,7 +169,7 @@ _taskman_complete_task() {
local task_id="$1" local task_id="$1"
if command -v python3 >/dev/null 2>&1; then 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 else
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
return 1 return 1
@ -163,7 +186,25 @@ _taskman_delete_task() {
local task_id="$1" local task_id="$1"
if command -v python3 >/dev/null 2>&1; then 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 <mode>"
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 else
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}" echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
return 1 return 1
@ -173,7 +214,7 @@ _taskman_delete_task() {
# Show help # Show help
_taskman_show_help() { _taskman_show_help() {
cat << 'EOF' cat << 'EOF'
Terminal Task Manager - Oh-My-Zsh Plugin Terminal Task Manager - Oh-My-Zsh Plugin v2.0
Usage: Usage:
tasks [action] [arguments] tasks [action] [arguments]
@ -185,6 +226,7 @@ Actions:
list [filter] List tasks (filter: all, pending, completed) list [filter] List tasks (filter: all, pending, completed)
done <id> Mark task as completed done <id> Mark task as completed
delete <id> Delete a task delete <id> Delete a task
sort <mode> Set sorting mode (default, priority, alphabetical)
help Show this help help Show this help
Examples: Examples:
@ -195,44 +237,94 @@ Examples:
tasks list pending # List only pending tasks tasks list pending # List only pending tasks
tasks done 3 # Mark task ID 3 as completed tasks done 3 # Mark task ID 3 as completed
tasks delete 5 # Delete task ID 5 tasks delete 5 # Delete task ID 5
tasks sort priority # Sort by priority
Interactive UI Keys: Interactive UI Keys:
↑/k Move up n New task ↑/k Move up n New task
↓/j Move down Space Toggle completion ↓/j Move down Space Toggle completion
h Help d Delete task s Cycle sort d Delete task
q Quit p Sort priority a Sort alphabetical
h Help q Quit
Aliases: Features:
tm, task, todo - All point to 'tasks' command • 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: Data Storage:
Tasks are stored in: ~/.taskman/tasks.json Default: ~/.taskman/tasks.json
Custom: Set TASKMAN_DATA_FILE environment variable
Requirements: Configuration:
Python 3.6+ (for interactive UI and CLI operations) # 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 EOF
} }
# Convenient aliases # Aliases for convenience
alias taskman='tasks'
alias tm='tasks' alias tm='tasks'
alias task='tasks' alias task='tasks'
alias todo='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 # Quick access function for sidebar workflow
task-sidebar() { task-sidebar() {
echo "${TASKMAN_COLOR_BLUE}Starting task manager in sidebar mode...${TASKMAN_COLOR_RESET}" 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 tasks ui
} }
# Optional: Show task summary on shell startup # Show a quick summary on shell startup (optional)
# Uncomment the next line to enable startup summary # Uncomment the next line if you want to see task summary when opening terminal
# _taskman_startup_summary # _taskman_startup_summary
_taskman_startup_summary() { _taskman_startup_summary() {
if [[ -f "$TASKMAN_DATA_DIR/tasks.json" ]] && command -v python3 >/dev/null 2>&1; then if [[ -f "$TASKMAN_DATA_FILE" ]] && 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 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=$(python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" count completed 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 if [[ "$pending_count" -gt 0 ]]; then
echo "${TASKMAN_COLOR_CYAN}📋 Task Summary: ${pending_count} pending, ${completed_count} completed${TASKMAN_COLOR_RESET}" echo "${TASKMAN_COLOR_CYAN}📋 Task Summary: ${pending_count} pending, ${completed_count} completed${TASKMAN_COLOR_RESET}"