mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-01-23 02:35:38 +01:00
750 lines
28 KiB
Python
750 lines
28 KiB
Python
#!/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()
|
|
|