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
- ⌨️ **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 <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`:
### 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! 🐱✨**

View file

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

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
#
# 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 <task description> [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 <task_id>"
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 <task_id>"
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 <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
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 <id> Mark task as completed
delete <id> Delete a task
sort <mode> 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}"