mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-01-23 02:35:38 +01:00
feat: add an optional fun animation on the bottom
This commit is contained in:
parent
9fc3f3dcd6
commit
df6b695dd6
8 changed files with 1545 additions and 777 deletions
|
|
@ -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!
|
||||||
|
|
||||||

|
## ✨ Features
|
||||||
|
|
||||||
## Features
|
### Core Functionality
|
||||||
|
- **Sidebar Interface**: Clean, terminal-based UI that doesn't interfere with your workflow
|
||||||
|
- **Persistent Storage**: Tasks are automatically saved to JSON format
|
||||||
|
- **Priority System**: Three priority levels (high, normal, low) with visual indicators
|
||||||
|
- **Task Operations**: Add, complete, delete, and navigate tasks with keyboard shortcuts
|
||||||
|
|
||||||
- 📝 **Dual Interface**: Both interactive TUI and CLI operations
|
### Advanced Features
|
||||||
- ⌨️ **Vim-like Keybindings**: Navigate with `j`/`k`, `Space` to toggle
|
- **Smart Sorting**: Multiple sort modes (creation order, priority, alphabetical) with completed tasks always at bottom
|
||||||
- 🎯 **Priority System**: High, normal, and low priority with color coding
|
- **Priority-Based Colors**: Task text colored by priority (red=high, yellow=normal, cyan=low)
|
||||||
- 💾 **Persistent Storage**: Tasks saved in `~/.taskman/tasks.json`
|
- **Humanized Timers**: Shows task age in human-readable format ([5m], [2h], [3d]) using local timezone
|
||||||
- 🎨 **Rich Colors**: Visual indicators for task status and priority
|
- **Visual Separation**: Horizontal line separates active and completed tasks
|
||||||
- ⚡ **Zero Config**: Works immediately after installation
|
- **Configurable Storage**: Set custom task file location via `TASKMAN_DATA_FILE` environment variable
|
||||||
- 🔧 **Shell Integration**: Aliases, completion, and sidebar workflow
|
- **Completed Task Dimming**: Completed tasks retain priority colors but are visually dimmed
|
||||||
|
- **Fun Animation**: Optional Chrome dino-style mini-game for entertainment (toggle with 'x')
|
||||||
|
|
||||||
## Installation
|
### Display Format
|
||||||
|
```
|
||||||
|
Taskman v2.0 [Sort: default]
|
||||||
|
Pending: 3, Completed: 1 Press 'h' for help, 'q' to quit
|
||||||
|
|
||||||
1. Add `taskman` to your plugins list in `~/.zshrc`:
|
[ 5m] ○ [!] Fix critical bug in authentication system
|
||||||
|
[ 2h] ○ [-] Review pull request #123
|
||||||
|
[now] ○ [·] Update documentation
|
||||||
|
|
||||||
|
─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ 1d] ✓ [!] Complete project setup
|
||||||
|
|
||||||
|
◆
|
||||||
|
. . . ● . . . | . . . ▌ . . . █ . . . ┃ . . . ▐ . . .
|
||||||
|
|
||||||
|
n: New | Space: Toggle | d: Delete | s: Sort | ↑↓: Navigate | h: Help | q: Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
```bash
|
```bash
|
||||||
plugins=(git taskman)
|
# Clone or copy the plugin to your Oh My Zsh plugins directory
|
||||||
```
|
cp -r taskman ~/.oh-my-zsh/plugins/
|
||||||
|
|
||||||
2. Reload your shell:
|
# Add to your .zshrc plugins list
|
||||||
|
plugins=(... taskman)
|
||||||
|
|
||||||
```bash
|
# Reload your shell
|
||||||
source ~/.zshrc
|
source ~/.zshrc
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start using!
|
### Using Oh My Zsh Plugin Manager
|
||||||
|
If you're using a plugin manager like `oh-my-zsh-plugins`:
|
||||||
```bash
|
```bash
|
||||||
tasks add "My first task"
|
# Add to your plugin list
|
||||||
|
plugins=(... taskman)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
## 🎮 Usage
|
||||||
|
|
||||||
- **Python 3.6+** (for interactive UI and CLI operations)
|
|
||||||
- **Terminal with color support** (most modern terminals)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Interactive UI
|
### Interactive UI
|
||||||
|
Launch the interactive task manager:
|
||||||
Launch the full-screen task manager:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tasks # Launch interactive UI
|
taskman
|
||||||
tasks ui # Same as above
|
# or use aliases
|
||||||
task-sidebar # Launch with sidebar usage tips
|
tasks
|
||||||
|
tm
|
||||||
|
todo
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
| Key | Action |
|
#### Navigation
|
||||||
|-----|--------|
|
- `↑/k` - Move selection up
|
||||||
| `↑`/`k` | Move up |
|
- `↓/j` - Move selection down
|
||||||
| `↓`/`j` | Move down |
|
|
||||||
| `n` | Create new task |
|
#### Task Operations
|
||||||
| `Space` | Toggle task completion |
|
- `n` - Create new task
|
||||||
| `d` | Delete selected task |
|
- `Space` - Toggle task completion
|
||||||
| `Tab` | Cycle priority when creating tasks |
|
- `d` - Delete selected task
|
||||||
| `h` | Toggle help panel |
|
|
||||||
| `q` | Quit |
|
#### Sorting
|
||||||
|
- `s` - Cycle through sort modes (default → priority → alphabetical)
|
||||||
|
- `p` - Sort by priority (high → normal → low)
|
||||||
|
- `a` - Sort alphabetically
|
||||||
|
|
||||||
|
#### Other
|
||||||
|
- `h` - Toggle help panel
|
||||||
|
- `x` - Toggle animation on/off
|
||||||
|
- `q` - Quit application
|
||||||
|
|
||||||
### Command Line Interface
|
### Command Line Interface
|
||||||
|
|
||||||
#### Adding Tasks
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tasks add "Fix login bug" # Normal priority
|
# Add a new task
|
||||||
tasks add "Deploy to production" high # High priority
|
tasks add "Complete project documentation"
|
||||||
tasks add "Update documentation" low # Low priority
|
tasks add "Fix bug in login system" high
|
||||||
|
|
||||||
|
# List tasks
|
||||||
|
tasks list
|
||||||
|
tasks list pending
|
||||||
|
tasks list completed
|
||||||
|
|
||||||
|
# Mark task as completed
|
||||||
|
tasks done 1
|
||||||
|
|
||||||
|
# Delete a task
|
||||||
|
tasks delete 2
|
||||||
|
|
||||||
|
# Sort tasks
|
||||||
|
tasks sort priority
|
||||||
|
tasks sort alphabetical
|
||||||
|
tasks sort default
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
tasks help
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Listing Tasks
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Custom Storage Location
|
||||||
|
Set a custom location for your task file:
|
||||||
```bash
|
```bash
|
||||||
tasks list # All tasks
|
export TASKMAN_DATA_FILE="$HOME/my-tasks.json"
|
||||||
tasks list pending # Only pending tasks
|
|
||||||
tasks list completed # Only completed tasks
|
|
||||||
tasks ls # Short alias
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Managing Tasks
|
### Priority Levels
|
||||||
|
- **High Priority** (`!`): Red text - urgent tasks
|
||||||
|
- **Normal Priority** (`-`): Yellow text - regular tasks
|
||||||
|
- **Low Priority** (`·`): Cyan text - nice-to-have tasks
|
||||||
|
|
||||||
```bash
|
## 🎨 Visual Features
|
||||||
tasks done 3 # Mark task ID 3 as completed
|
|
||||||
tasks delete 5 # Delete task ID 5
|
### Color System
|
||||||
tasks help # Show help
|
- **Active Tasks**: Text colored by priority (red/yellow/cyan)
|
||||||
|
- **Completed Tasks**: Same priority colors but dimmed for subtle indication
|
||||||
|
- **Status Bullets**: Green checkmarks for completed, colored circles for active
|
||||||
|
- **Selection**: Reverse highlighting for currently selected task
|
||||||
|
|
||||||
|
### Timer Display
|
||||||
|
- Shows how long ago each task was created
|
||||||
|
- Updates in real-time for active tasks
|
||||||
|
- Uses local timezone for accurate time calculation
|
||||||
|
- Formats: `[now]`, `[5m]`, `[2h]`, `[3d]`
|
||||||
|
|
||||||
|
### Mini-Game Animation
|
||||||
|
- Chrome dino-style side-scrolling game with jumping player and obstacles
|
||||||
|
- Press 'x' to toggle animation on/off
|
||||||
|
- Automatic jumping and varied obstacles for entertainment
|
||||||
|
- Runs alongside task management without interference
|
||||||
|
|
||||||
|
## 📁 File Structure
|
||||||
|
```
|
||||||
|
~/.taskman/
|
||||||
|
└── tasks.json # Task storage (default location)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Aliases
|
### Task Data Format
|
||||||
|
|
||||||
The plugin provides convenient aliases:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tm add "Buy groceries" # Same as 'tasks add'
|
|
||||||
task list # Same as 'tasks list'
|
|
||||||
todo done 1 # Same as 'tasks done 1'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Priority Levels
|
|
||||||
|
|
||||||
Tasks support three priority levels with color coding:
|
|
||||||
|
|
||||||
- **High Priority** (`!`) - 🔴 Red, for urgent tasks
|
|
||||||
- **Normal Priority** (`-`) - 🟡 Yellow, default priority
|
|
||||||
- **Low Priority** (`·`) - 🔵 Cyan, for less urgent tasks
|
|
||||||
|
|
||||||
## Task Display
|
|
||||||
|
|
||||||
Tasks are displayed with visual indicators:
|
|
||||||
|
|
||||||
```
|
|
||||||
✓ [!] Completed high priority task (green)
|
|
||||||
○ [-] Pending normal priority task (yellow)
|
|
||||||
○ [·] Pending low priority task (cyan)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sidebar Workflow
|
|
||||||
|
|
||||||
Perfect for split-terminal development workflow:
|
|
||||||
|
|
||||||
1. **Split your terminal** horizontally or vertically
|
|
||||||
2. **Run `tasks ui`** in one pane for persistent task view
|
|
||||||
3. **Work in the other pane** while keeping tasks visible
|
|
||||||
4. **Quick updates** with keyboard shortcuts
|
|
||||||
|
|
||||||
## Auto-completion
|
|
||||||
|
|
||||||
The plugin provides intelligent tab completion:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tasks <TAB> # Shows: add, list, done, delete, etc.
|
|
||||||
tasks add "task" <TAB> # Shows: high, normal, low
|
|
||||||
tasks done <TAB> # Shows available task IDs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Optional Startup Summary
|
|
||||||
|
|
||||||
To show task summary when opening terminal, uncomment this line in the plugin:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In ~/.oh-my-zsh/plugins/taskman/taskman.plugin.zsh
|
|
||||||
_taskman_startup_summary # Uncomment this line
|
|
||||||
```
|
|
||||||
|
|
||||||
This shows:
|
|
||||||
```
|
|
||||||
📋 Task Summary: 3 pending, 2 completed
|
|
||||||
Type 'tasks' to manage your tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Storage
|
|
||||||
|
|
||||||
Tasks are stored in `~/.taskman/tasks.json`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"text": "Fix login bug",
|
"text": "Complete project setup",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"created_at": "2024-01-15T10:30:00"
|
"created_at": "2024-01-15T10:30:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"next_id": 2
|
"next_id": 2,
|
||||||
|
"sort_mode": "default"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## 🔧 Technical Details
|
||||||
|
|
||||||
### Daily Developer Workflow
|
- **Language**: Python 3.6+
|
||||||
|
- **Dependencies**: Built-in libraries only (curses, json, datetime)
|
||||||
|
- **Storage**: JSON format for human-readable task data
|
||||||
|
- **Cross-platform**: Works on macOS, Linux, and other Unix-like systems
|
||||||
|
- **Performance**: Efficient curses-based rendering with 100ms refresh rate
|
||||||
|
|
||||||
|
## 🎯 Tips & Tricks
|
||||||
|
|
||||||
|
1. **Quick Task Entry**: Use Tab in input mode to cycle through priority levels
|
||||||
|
2. **Efficient Navigation**: Use `k`/`j` (vim-style) or arrow keys for navigation
|
||||||
|
3. **Sort Persistence**: Your preferred sort mode is remembered between sessions
|
||||||
|
4. **Bulk Operations**: Use CLI commands for scripting and automation
|
||||||
|
5. **Custom Storage**: Set `TASKMAN_DATA_FILE` to sync tasks across different setups
|
||||||
|
6. **Visual Cues**: Completed tasks are automatically moved to bottom with visual separator
|
||||||
|
7. **Time Awareness**: Timer shows local time, perfect for tracking task age across time zones
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
- **Unicode Display**: Ensure your terminal supports Unicode for proper emoji display
|
||||||
|
- **Color Issues**: Some terminals may not support all color combinations
|
||||||
|
- **Permission Errors**: Check write permissions for the task storage directory
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
```bash
|
```bash
|
||||||
# Morning planning
|
# Run with Python directly for debugging
|
||||||
tasks add "Review PR #123" high
|
python3 ~/.oh-my-zsh/plugins/taskman/task_manager.py
|
||||||
tasks add "Fix login bug" high
|
|
||||||
tasks add "Update docs" low
|
|
||||||
|
|
||||||
# Check current tasks
|
|
||||||
tasks list pending
|
|
||||||
|
|
||||||
# Work in interactive mode (split terminal)
|
|
||||||
tasks ui
|
|
||||||
|
|
||||||
# Quick CLI updates
|
|
||||||
tm done 1
|
|
||||||
tm add "Deploy hotfix" high
|
|
||||||
|
|
||||||
# End of day review
|
|
||||||
tasks list completed
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Management
|
## 🤝 Contributing
|
||||||
|
|
||||||
```bash
|
This is an Oh My Zsh version of the Taskman plugin. For contributions and bug reports, please refer to the original osh framework repository.
|
||||||
# Sprint planning
|
|
||||||
tasks add "Implement user auth" high
|
|
||||||
tasks add "Add unit tests" normal
|
|
||||||
tasks add "Update README" low
|
|
||||||
|
|
||||||
# Track progress
|
## 📄 License
|
||||||
tasks list
|
|
||||||
|
|
||||||
# Mark completed
|
Part of the Oh My Zsh ecosystem. See individual license files for details.
|
||||||
tasks done 1
|
|
||||||
tasks done 2
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
tasks delete 3 # Remove completed/outdated tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comparison with Alternatives
|
|
||||||
|
|
||||||
| Feature | taskman | taskwarrior | todo.txt | Todoist |
|
|
||||||
|---------|---------|-------------|----------|----------|
|
|
||||||
| Interactive TUI | ✅ | ❌ | ❌ | ❌ |
|
|
||||||
| CLI Interface | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Zero Setup | ✅ | ❌ | ✅ | ❌ |
|
|
||||||
| No External Deps | ✅ | ❌ | ✅ | ❌ |
|
|
||||||
| Rich Visual UI | ✅ | ❌ | ❌ | ❌ |
|
|
||||||
| Vim Keybindings | ✅ | ❌ | ❌ | ❌ |
|
|
||||||
| Local Data | ✅ | ✅ | ✅ | ❌ |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Python Not Found
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install Python 3 (macOS)
|
|
||||||
brew install python3
|
|
||||||
|
|
||||||
# Install Python 3 (Ubuntu/Debian)
|
|
||||||
sudo apt-get install python3
|
|
||||||
|
|
||||||
# Verify installation
|
|
||||||
python3 --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Plugin Not Loading
|
|
||||||
|
|
||||||
1. Check that `taskman` is in your plugins list:
|
|
||||||
```bash
|
|
||||||
echo $plugins
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Reload your shell:
|
|
||||||
```bash
|
|
||||||
source ~/.zshrc
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Test plugin function:
|
|
||||||
```bash
|
|
||||||
tasks help
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Permissions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Make Python files executable
|
|
||||||
chmod +x ~/.oh-my-zsh/plugins/taskman/bin/*.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License - see the [Oh-My-Zsh license](https://github.com/ohmyzsh/ohmyzsh/blob/master/LICENSE.txt) for details.
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Created by [@oiahoon](https://github.com/oiahoon)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Happy task managing! 🚀**
|
**Enjoy managing your tasks with style! 🐱✨**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ _taskman() {
|
||||||
local -a task_ids
|
local -a task_ids
|
||||||
|
|
||||||
# Try to get task IDs from the CLI
|
# Try to get task IDs from the CLI
|
||||||
task_ids=($(python3 "$plugin_dir/bin/task_cli.py" list all 2>/dev/null | grep -o '(ID: [0-9]\+)' | grep -o '[0-9]\+' 2>/dev/null))
|
task_ids=($(python3 "$plugin_dir/task_cli.py" list all 2>/dev/null | grep -o '(ID: [0-9]\+)' | grep -o '[0-9]\+' 2>/dev/null))
|
||||||
|
|
||||||
if [[ ${#task_ids[@]} -gt 0 ]]; then
|
if [[ ${#task_ids[@]} -gt 0 ]]; then
|
||||||
_describe 'task IDs' task_ids
|
_describe 'task IDs' task_ids
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
471
plugins/taskman/dino_animation.py
Normal file
471
plugins/taskman/dino_animation.py
Normal 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"
|
||||||
44
plugins/taskman/task_cli.py
Normal file
44
plugins/taskman/task_cli.py
Normal 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 ...
|
||||||
750
plugins/taskman/task_manager.py
Normal file
750
plugins/taskman/task_manager.py
Normal 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()
|
||||||
|
|
||||||
|
|
@ -4,15 +4,17 @@
|
||||||
# A powerful sidebar-style task manager that runs entirely in your terminal
|
# A powerful sidebar-style task manager that runs entirely in your terminal
|
||||||
#
|
#
|
||||||
# Author: @oiahoon
|
# Author: @oiahoon
|
||||||
# Version: 1.0.0
|
# Version: 2.0.0
|
||||||
# Repository: https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/taskman
|
# Repository: https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/taskman
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Interactive terminal UI with keyboard shortcuts
|
# - Interactive terminal UI with keyboard shortcuts
|
||||||
# - Command line interface for quick operations
|
# - Command line interface for quick operations
|
||||||
# - Priority system with color coding
|
# - Priority system with color coding
|
||||||
# - Persistent JSON storage
|
# - Persistent JSON storage with configurable location
|
||||||
# - Vim-like keybindings
|
# - Vim-like keybindings
|
||||||
|
# - Multiple sorting modes with completed tasks at bottom
|
||||||
|
# - Color-coded bullets with neutral task text
|
||||||
# - Zero external dependencies (Python 3 only)
|
# - Zero external dependencies (Python 3 only)
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
@ -22,8 +24,21 @@ TASKMAN_PLUGIN_DIR="${0:h}"
|
||||||
# Data directory (store tasks in home directory)
|
# Data directory (store tasks in home directory)
|
||||||
TASKMAN_DATA_DIR="$HOME/.taskman"
|
TASKMAN_DATA_DIR="$HOME/.taskman"
|
||||||
|
|
||||||
# Ensure data directory exists
|
# Allow users to configure custom task file path
|
||||||
[[ ! -d "$TASKMAN_DATA_DIR" ]] && mkdir -p "$TASKMAN_DATA_DIR"
|
# Users can set TASKMAN_DATA_FILE in their .zshrc to customize storage location
|
||||||
|
# Example: export TASKMAN_DATA_FILE="$HOME/my-tasks/tasks.json"
|
||||||
|
if [[ -z "$TASKMAN_DATA_FILE" ]]; then
|
||||||
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_DIR/tasks.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure data directory exists (for default path)
|
||||||
|
if [[ "$TASKMAN_DATA_FILE" == "$TASKMAN_DATA_DIR/tasks.json" ]]; then
|
||||||
|
[[ ! -d "$TASKMAN_DATA_DIR" ]] && mkdir -p "$TASKMAN_DATA_DIR"
|
||||||
|
else
|
||||||
|
# For custom paths, ensure the directory exists
|
||||||
|
custom_dir=$(dirname "$TASKMAN_DATA_FILE")
|
||||||
|
[[ ! -d "$custom_dir" ]] && mkdir -p "$custom_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
# Color definitions for output
|
# Color definitions for output
|
||||||
TASKMAN_COLOR_RED="\033[31m"
|
TASKMAN_COLOR_RED="\033[31m"
|
||||||
|
|
@ -36,7 +51,11 @@ TASKMAN_COLOR_RESET="\033[0m"
|
||||||
# Main task manager function
|
# Main task manager function
|
||||||
tasks() {
|
tasks() {
|
||||||
local action="$1"
|
local action="$1"
|
||||||
shift
|
|
||||||
|
# Only shift if there are arguments
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
"" | "ui" | "show")
|
"" | "ui" | "show")
|
||||||
|
|
@ -59,6 +78,10 @@ tasks() {
|
||||||
# Delete a task
|
# Delete a task
|
||||||
_taskman_delete_task "$@"
|
_taskman_delete_task "$@"
|
||||||
;;
|
;;
|
||||||
|
"sort")
|
||||||
|
# Set sorting mode
|
||||||
|
_taskman_set_sort "$@"
|
||||||
|
;;
|
||||||
"help" | "-h" | "--help")
|
"help" | "-h" | "--help")
|
||||||
_taskman_show_help
|
_taskman_show_help
|
||||||
;;
|
;;
|
||||||
|
|
@ -78,11 +101,11 @@ _taskman_launch_ui() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set the data file path
|
# Set the data file path (use configured path)
|
||||||
export TASKMAN_DATA_FILE="$TASKMAN_DATA_DIR/tasks.json"
|
export TASKMAN_DATA_FILE
|
||||||
|
|
||||||
# Run the Python task manager
|
# Run the Python task manager
|
||||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_manager.py"
|
python3 "$TASKMAN_PLUGIN_DIR/task_manager.py"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Quick add task from command line
|
# Quick add task from command line
|
||||||
|
|
@ -107,7 +130,7 @@ _taskman_add_task() {
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" add "$task_text" "$priority"
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" add "$task_text" "$priority"
|
||||||
|
|
||||||
# Show color-coded confirmation
|
# Show color-coded confirmation
|
||||||
local priority_color
|
local priority_color
|
||||||
|
|
@ -129,7 +152,7 @@ _taskman_list_tasks() {
|
||||||
local filter="$1"
|
local filter="$1"
|
||||||
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" list "$filter"
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" list "$filter"
|
||||||
else
|
else
|
||||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -146,7 +169,7 @@ _taskman_complete_task() {
|
||||||
|
|
||||||
local task_id="$1"
|
local task_id="$1"
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" complete "$task_id"
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" complete "$task_id"
|
||||||
else
|
else
|
||||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -163,7 +186,25 @@ _taskman_delete_task() {
|
||||||
|
|
||||||
local task_id="$1"
|
local task_id="$1"
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" delete "$task_id"
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" delete "$task_id"
|
||||||
|
else
|
||||||
|
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set sorting mode
|
||||||
|
_taskman_set_sort() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
echo "${TASKMAN_COLOR_RED}Error: Please provide sort mode${TASKMAN_COLOR_RESET}"
|
||||||
|
echo "Usage: tasks sort <mode>"
|
||||||
|
echo "Available modes: default, priority, alphabetical"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local sort_mode="$1"
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" sort "$sort_mode"
|
||||||
else
|
else
|
||||||
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_RED}Error: Python 3 is required for task management.${TASKMAN_COLOR_RESET}"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -173,7 +214,7 @@ _taskman_delete_task() {
|
||||||
# Show help
|
# Show help
|
||||||
_taskman_show_help() {
|
_taskman_show_help() {
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
Terminal Task Manager - Oh-My-Zsh Plugin
|
Terminal Task Manager - Oh-My-Zsh Plugin v2.0
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
tasks [action] [arguments]
|
tasks [action] [arguments]
|
||||||
|
|
@ -185,6 +226,7 @@ Actions:
|
||||||
list [filter] List tasks (filter: all, pending, completed)
|
list [filter] List tasks (filter: all, pending, completed)
|
||||||
done <id> Mark task as completed
|
done <id> Mark task as completed
|
||||||
delete <id> Delete a task
|
delete <id> Delete a task
|
||||||
|
sort <mode> Set sorting mode (default, priority, alphabetical)
|
||||||
help Show this help
|
help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
@ -195,44 +237,94 @@ Examples:
|
||||||
tasks list pending # List only pending tasks
|
tasks list pending # List only pending tasks
|
||||||
tasks done 3 # Mark task ID 3 as completed
|
tasks done 3 # Mark task ID 3 as completed
|
||||||
tasks delete 5 # Delete task ID 5
|
tasks delete 5 # Delete task ID 5
|
||||||
|
tasks sort priority # Sort by priority
|
||||||
|
|
||||||
Interactive UI Keys:
|
Interactive UI Keys:
|
||||||
↑/k Move up n New task
|
↑/k Move up n New task
|
||||||
↓/j Move down Space Toggle completion
|
↓/j Move down Space Toggle completion
|
||||||
h Help d Delete task
|
s Cycle sort d Delete task
|
||||||
q Quit
|
p Sort priority a Sort alphabetical
|
||||||
|
h Help q Quit
|
||||||
|
|
||||||
Aliases:
|
Features:
|
||||||
tm, task, todo - All point to 'tasks' command
|
• Configurable storage location via TASKMAN_DATA_FILE environment variable
|
||||||
|
• Automatic sorting with completed tasks always at bottom
|
||||||
|
• Priority-based text colors with dimmed completed tasks
|
||||||
|
• Humanized creation timers showing task age
|
||||||
|
• Visual separator between active and completed tasks
|
||||||
|
• Multiple sort modes: creation order, priority, alphabetical
|
||||||
|
|
||||||
|
Priority Colors:
|
||||||
|
• High Priority (!) - Red text for active, dimmed red for completed
|
||||||
|
• Normal Priority (-) - Yellow text for active, dimmed yellow for completed
|
||||||
|
• Low Priority (·) - Cyan text for active, dimmed cyan for completed
|
||||||
|
|
||||||
Data Storage:
|
Data Storage:
|
||||||
Tasks are stored in: ~/.taskman/tasks.json
|
Default: ~/.taskman/tasks.json
|
||||||
|
Custom: Set TASKMAN_DATA_FILE environment variable
|
||||||
|
|
||||||
Requirements:
|
Configuration:
|
||||||
Python 3.6+ (for interactive UI and CLI operations)
|
# In your ~/.zshrc (before loading Oh-My-Zsh)
|
||||||
|
export TASKMAN_DATA_FILE="$HOME/Documents/my-tasks.json"
|
||||||
|
|
||||||
|
Priority Levels:
|
||||||
|
• High Priority (!) - Red text, for urgent tasks
|
||||||
|
• Normal Priority (-) - Yellow text, default priority
|
||||||
|
• Low Priority (·) - Cyan text, for less urgent tasks
|
||||||
|
|
||||||
|
Color Scheme:
|
||||||
|
• Task text is colored based on priority and completion status
|
||||||
|
• Completed tasks show dimmed priority colors to maintain context
|
||||||
|
• Visual separator divides active and completed tasks
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convenient aliases
|
# Aliases for convenience
|
||||||
|
alias taskman='tasks'
|
||||||
alias tm='tasks'
|
alias tm='tasks'
|
||||||
alias task='tasks'
|
alias task='tasks'
|
||||||
alias todo='tasks'
|
alias todo='tasks'
|
||||||
|
|
||||||
|
# Auto-completion for task actions
|
||||||
|
_taskman_completion() {
|
||||||
|
local -a actions
|
||||||
|
actions=(
|
||||||
|
'ui:Launch interactive UI'
|
||||||
|
'show:Launch interactive UI'
|
||||||
|
'add:Add new task'
|
||||||
|
'new:Add new task'
|
||||||
|
'create:Add new task'
|
||||||
|
'list:List tasks'
|
||||||
|
'ls:List tasks'
|
||||||
|
'done:Mark task complete'
|
||||||
|
'complete:Mark task complete'
|
||||||
|
'delete:Delete task'
|
||||||
|
'del:Delete task'
|
||||||
|
'rm:Delete task'
|
||||||
|
'sort:Set sorting mode'
|
||||||
|
'help:Show help'
|
||||||
|
)
|
||||||
|
_describe 'actions' actions
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef _taskman_completion tasks tm task todo
|
||||||
|
|
||||||
# Quick access function for sidebar workflow
|
# Quick access function for sidebar workflow
|
||||||
task-sidebar() {
|
task-sidebar() {
|
||||||
echo "${TASKMAN_COLOR_BLUE}Starting task manager in sidebar mode...${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_BLUE}Starting task manager in sidebar mode...${TASKMAN_COLOR_RESET}"
|
||||||
echo "${TASKMAN_COLOR_YELLOW}Tip: Use terminal split to run this alongside your work${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_YELLOW}Tip: Use 'Cmd+D' (or equivalent) to split terminal horizontally${TASKMAN_COLOR_RESET}"
|
||||||
|
echo "${TASKMAN_COLOR_CYAN}New features: Sort with 's/p/a' keys, completed tasks auto-move to bottom${TASKMAN_COLOR_RESET}"
|
||||||
tasks ui
|
tasks ui
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional: Show task summary on shell startup
|
# Show a quick summary on shell startup (optional)
|
||||||
# Uncomment the next line to enable startup summary
|
# Uncomment the next line if you want to see task summary when opening terminal
|
||||||
# _taskman_startup_summary
|
# _taskman_startup_summary
|
||||||
|
|
||||||
_taskman_startup_summary() {
|
_taskman_startup_summary() {
|
||||||
if [[ -f "$TASKMAN_DATA_DIR/tasks.json" ]] && command -v python3 >/dev/null 2>&1; then
|
if [[ -f "$TASKMAN_DATA_FILE" ]] && command -v python3 >/dev/null 2>&1; then
|
||||||
local pending_count=$(python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" count pending 2>/dev/null || echo "0")
|
local pending_count=$(TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" count pending 2>/dev/null || echo "0")
|
||||||
local completed_count=$(python3 "$TASKMAN_PLUGIN_DIR/bin/task_cli.py" count completed 2>/dev/null || echo "0")
|
local completed_count=$(TASKMAN_DATA_FILE="$TASKMAN_DATA_FILE" python3 "$TASKMAN_PLUGIN_DIR/task_cli.py" count completed 2>/dev/null || echo "0")
|
||||||
|
|
||||||
if [[ "$pending_count" -gt 0 ]]; then
|
if [[ "$pending_count" -gt 0 ]]; then
|
||||||
echo "${TASKMAN_COLOR_CYAN}📋 Task Summary: ${pending_count} pending, ${completed_count} completed${TASKMAN_COLOR_RESET}"
|
echo "${TASKMAN_COLOR_CYAN}📋 Task Summary: ${pending_count} pending, ${completed_count} completed${TASKMAN_COLOR_RESET}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue