mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-01-23 02:35:38 +01:00
feat(taskman): add terminal task manager plugin
This commit is contained in:
parent
042605ee6b
commit
9fc3f3dcd6
5 changed files with 1120 additions and 0 deletions
286
plugins/taskman/README.md
Normal file
286
plugins/taskman/README.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# taskman
|
||||
|
||||
A powerful terminal task manager plugin for Oh-My-Zsh. Manage your daily tasks without leaving the command line!
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- 📝 **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
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add `taskman` to your plugins list in `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
plugins=(git taskman)
|
||||
```
|
||||
|
||||
2. Reload your shell:
|
||||
|
||||
```bash
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
3. Start using!
|
||||
|
||||
```bash
|
||||
tasks add "My first task"
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Python 3.6+** (for interactive UI and CLI operations)
|
||||
- **Terminal with color support** (most modern terminals)
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive UI
|
||||
|
||||
Launch the full-screen task manager:
|
||||
|
||||
```bash
|
||||
tasks # Launch interactive UI
|
||||
tasks ui # Same as above
|
||||
task-sidebar # Launch with sidebar usage tips
|
||||
```
|
||||
|
||||
#### 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 |
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
#### Listing Tasks
|
||||
|
||||
```bash
|
||||
tasks list # All tasks
|
||||
tasks list pending # Only pending tasks
|
||||
tasks list completed # Only completed tasks
|
||||
tasks ls # Short alias
|
||||
```
|
||||
|
||||
#### Managing Tasks
|
||||
|
||||
```bash
|
||||
tasks done 3 # Mark task ID 3 as completed
|
||||
tasks delete 5 # Delete task ID 5
|
||||
tasks help # Show help
|
||||
```
|
||||
|
||||
### 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`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Fix login bug",
|
||||
"completed": false,
|
||||
"priority": "high",
|
||||
"created_at": "2024-01-15T10:30:00"
|
||||
}
|
||||
],
|
||||
"next_id": 2
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Daily Developer Workflow
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Project Management
|
||||
|
||||
```bash
|
||||
# Sprint planning
|
||||
tasks add "Implement user auth" high
|
||||
tasks add "Add unit tests" normal
|
||||
tasks add "Update README" low
|
||||
|
||||
# Track progress
|
||||
tasks list
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
**Happy task managing! 🚀**
|
||||
|
||||
69
plugins/taskman/_taskman
Normal file
69
plugins/taskman/_taskman
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#compdef tasks tm task todo
|
||||
|
||||
# Terminal Task Manager completion for Oh-My-Zsh
|
||||
# Provides tab completion for all task management commands
|
||||
|
||||
_taskman() {
|
||||
local context state state_descr line
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'1:command:->commands' \
|
||||
'*::arg:->args' && return 0
|
||||
|
||||
case $state in
|
||||
commands)
|
||||
local -a commands
|
||||
commands=(
|
||||
'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 as completed'
|
||||
'complete:Mark task as completed'
|
||||
'delete:Delete a task'
|
||||
'del:Delete a task'
|
||||
'rm:Delete a task'
|
||||
'help:Show help'
|
||||
)
|
||||
_describe 'taskman commands' commands
|
||||
;;
|
||||
args)
|
||||
case $line[1] in
|
||||
add|new|create)
|
||||
_arguments \
|
||||
'1:task description:' \
|
||||
'2:priority:(high normal low)'
|
||||
;;
|
||||
list|ls)
|
||||
_arguments \
|
||||
'1:filter:(all pending completed)'
|
||||
;;
|
||||
done|complete|delete|del|rm)
|
||||
# Complete with task IDs if possible
|
||||
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))
|
||||
|
||||
if [[ ${#task_ids[@]} -gt 0 ]]; then
|
||||
_describe 'task IDs' task_ids
|
||||
else
|
||||
_message 'task ID'
|
||||
fi
|
||||
else
|
||||
_message 'task ID'
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_taskman
|
||||
|
||||
180
plugins/taskman/bin/task_cli.py
Executable file
180
plugins/taskman/bin/task_cli.py
Executable file
|
|
@ -0,0 +1,180 @@
|
|||
#!/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()
|
||||
|
||||
342
plugins/taskman/bin/task_manager.py
Executable file
342
plugins/taskman/bin/task_manager.py
Executable file
|
|
@ -0,0 +1,342 @@
|
|||
#!/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()
|
||||
|
||||
243
plugins/taskman/taskman.plugin.zsh
Normal file
243
plugins/taskman/taskman.plugin.zsh
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env zsh
|
||||
#
|
||||
# Terminal Task Manager Plugin for Oh-My-Zsh
|
||||
# A powerful sidebar-style task manager that runs entirely in your terminal
|
||||
#
|
||||
# Author: @oiahoon
|
||||
# Version: 1.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
|
||||
# - Vim-like keybindings
|
||||
# - Zero external dependencies (Python 3 only)
|
||||
#
|
||||
|
||||
# Plugin directory detection (Oh-My-Zsh compatible)
|
||||
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"
|
||||
|
||||
# Color definitions for output
|
||||
TASKMAN_COLOR_RED="\033[31m"
|
||||
TASKMAN_COLOR_GREEN="\033[32m"
|
||||
TASKMAN_COLOR_YELLOW="\033[33m"
|
||||
TASKMAN_COLOR_BLUE="\033[34m"
|
||||
TASKMAN_COLOR_CYAN="\033[36m"
|
||||
TASKMAN_COLOR_RESET="\033[0m"
|
||||
|
||||
# Main task manager function
|
||||
tasks() {
|
||||
local action="$1"
|
||||
shift
|
||||
|
||||
case "$action" in
|
||||
"" | "ui" | "show")
|
||||
# Launch the full UI
|
||||
_taskman_launch_ui
|
||||
;;
|
||||
"add" | "new" | "create")
|
||||
# Quick add task from command line
|
||||
_taskman_add_task "$@"
|
||||
;;
|
||||
"list" | "ls")
|
||||
# List tasks in terminal
|
||||
_taskman_list_tasks "$@"
|
||||
;;
|
||||
"done" | "complete")
|
||||
# Mark task as complete
|
||||
_taskman_complete_task "$@"
|
||||
;;
|
||||
"delete" | "del" | "rm")
|
||||
# Delete a task
|
||||
_taskman_delete_task "$@"
|
||||
;;
|
||||
"help" | "-h" | "--help")
|
||||
_taskman_show_help
|
||||
;;
|
||||
*)
|
||||
echo "${TASKMAN_COLOR_RED}Unknown action: $action${TASKMAN_COLOR_RESET}"
|
||||
_taskman_show_help
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Launch the full UI
|
||||
_taskman_launch_ui() {
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required but not installed.${TASKMAN_COLOR_RESET}"
|
||||
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"
|
||||
|
||||
# Run the Python task manager
|
||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_manager.py"
|
||||
}
|
||||
|
||||
# Quick add task from command line
|
||||
_taskman_add_task() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "${TASKMAN_COLOR_RED}Error: Please provide task description${TASKMAN_COLOR_RESET}"
|
||||
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")
|
||||
;;
|
||||
*)
|
||||
echo "${TASKMAN_COLOR_YELLOW}Warning: Invalid priority '$priority', using 'normal'${TASKMAN_COLOR_RESET}"
|
||||
priority="normal"
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" add "$task_text" "$priority"
|
||||
|
||||
# Show color-coded confirmation
|
||||
local priority_color
|
||||
case "$priority" in
|
||||
"high") priority_color="$TASKMAN_COLOR_RED" ;;
|
||||
"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}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 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"
|
||||
else
|
||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Complete a task
|
||||
_taskman_complete_task() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "${TASKMAN_COLOR_RED}Error: Please provide task ID${TASKMAN_COLOR_RESET}"
|
||||
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"
|
||||
else
|
||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Delete a task
|
||||
_taskman_delete_task() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "${TASKMAN_COLOR_RED}Error: Please provide task ID${TASKMAN_COLOR_RESET}"
|
||||
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"
|
||||
else
|
||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Show help
|
||||
_taskman_show_help() {
|
||||
cat << 'EOF'
|
||||
Terminal Task Manager - Oh-My-Zsh Plugin
|
||||
|
||||
Usage:
|
||||
tasks [action] [arguments]
|
||||
|
||||
Actions:
|
||||
(no action) Launch full interactive UI
|
||||
ui, show Launch full interactive UI
|
||||
add <text> [priority] Add new task (priority: high, normal, low)
|
||||
list [filter] List tasks (filter: all, pending, completed)
|
||||
done <id> Mark task as completed
|
||||
delete <id> Delete a task
|
||||
help Show this help
|
||||
|
||||
Examples:
|
||||
tasks # Launch interactive UI
|
||||
tasks add "Fix bug in login" # Add normal priority task
|
||||
tasks add "Deploy to prod" high # Add high priority task
|
||||
tasks list # List all tasks
|
||||
tasks list pending # List only pending tasks
|
||||
tasks done 3 # Mark task ID 3 as completed
|
||||
tasks delete 5 # Delete task ID 5
|
||||
|
||||
Interactive UI Keys:
|
||||
↑/k Move up n New task
|
||||
↓/j Move down Space Toggle completion
|
||||
h Help d Delete task
|
||||
q Quit
|
||||
|
||||
Aliases:
|
||||
tm, task, todo - All point to 'tasks' command
|
||||
|
||||
Data Storage:
|
||||
Tasks are stored in: ~/.taskman/tasks.json
|
||||
|
||||
Requirements:
|
||||
Python 3.6+ (for interactive UI and CLI operations)
|
||||
EOF
|
||||
}
|
||||
|
||||
# Convenient aliases
|
||||
alias tm='tasks'
|
||||
alias task='tasks'
|
||||
alias todo='tasks'
|
||||
|
||||
# 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}"
|
||||
tasks ui
|
||||
}
|
||||
|
||||
# Optional: Show task summary on shell startup
|
||||
# Uncomment the next line to enable startup summary
|
||||
# _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 [[ "$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}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue