This commit is contained in:
Tim Bendt
2025-08-18 10:58:48 -04:00
parent c64fbbb072
commit ca6e4cdf5d
12 changed files with 2220 additions and 39 deletions

284
src/utils/ticktick_utils.py Normal file
View File

@@ -0,0 +1,284 @@
"""
TickTick utilities for formatting and helper functions.
"""
import platform
import subprocess
import webbrowser
from datetime import datetime, date, timedelta
from typing import Dict, Any, List, Optional
from rich.console import Console
from rich.table import Table
from rich.text import Text
from dateutil import parser as date_parser
console = Console()
def format_date(date_str: Optional[str]) -> str:
"""
Format a date string for display.
Args:
date_str: ISO date string
Returns:
Formatted date string
"""
if not date_str:
return ""
try:
dt = date_parser.parse(date_str)
today = datetime.now().date()
task_date = dt.date()
if task_date == today:
return "Today"
elif task_date == today + timedelta(days=1):
return "Tomorrow"
elif task_date == today - timedelta(days=1):
return "Yesterday"
else:
return dt.strftime("%Y-%m-%d")
except (ValueError, TypeError):
return str(date_str)
def get_priority_display(priority: int) -> Text:
"""
Get a rich Text object for priority display.
Args:
priority: Priority level (0-5)
Returns:
Rich Text object with colored priority
"""
if priority == 0:
return Text("", style="dim")
elif priority == 1:
return Text("!", style="blue")
elif priority == 2:
return Text("!!", style="yellow")
elif priority >= 3:
return Text("!!!", style="red bold")
else:
return Text("", style="dim")
def format_task_title(task: Dict[str, Any], max_length: int = 50) -> str:
"""
Format task title with truncation if needed.
Args:
task: Task dictionary
max_length: Maximum length for title
Returns:
Formatted title string
"""
title = task.get("title", "Untitled")
if len(title) > max_length:
return title[: max_length - 3] + "..."
return title
def create_task_table(tasks: List[Dict[str, Any]], show_project: bool = True) -> Table:
"""
Create a rich table for displaying tasks.
Args:
tasks: List of task dictionaries
show_project: Whether to show project column
Returns:
Rich Table object
"""
table = Table(show_header=True, header_style="bold magenta")
table.add_column("ID", style="dim", width=8)
table.add_column("Priority", width=8)
table.add_column("Title", style="white", min_width=30)
if show_project:
table.add_column("Project", style="cyan", width=15)
table.add_column("Due Date", style="yellow", width=12)
table.add_column("Tags", style="green", width=20)
for task in tasks:
task_id = str(task.get("id", ""))[:8]
priority_text = get_priority_display(task.get("priority", 0))
title = format_task_title(task)
due_date = format_date(task.get("dueDate"))
# Get project name (would need to be looked up from projects)
project_name = task.get("projectId", "Inbox")[:15] if show_project else None
# Format tags
tags_list = task.get("tags", [])
if isinstance(tags_list, list):
tags = ", ".join(tags_list[:3]) # Show max 3 tags
if len(tags_list) > 3:
tags += f" (+{len(tags_list) - 3})"
else:
tags = ""
if show_project:
table.add_row(task_id, priority_text, title, project_name, due_date, tags)
else:
table.add_row(task_id, priority_text, title, due_date, tags)
return table
def print_task_details(task: Dict[str, Any]):
"""
Print detailed view of a single task.
Args:
task: Task dictionary
"""
console.print(f"[bold cyan]Task Details[/bold cyan]")
console.print(f"ID: {task.get('id', 'N/A')}")
console.print(f"Title: [white]{task.get('title', 'Untitled')}[/white]")
if task.get("content"):
console.print(f"Description: {task.get('content')}")
console.print(f"Priority: {get_priority_display(task.get('priority', 0))}")
console.print(f"Project ID: {task.get('projectId', 'N/A')}")
if task.get("dueDate"):
console.print(f"Due Date: [yellow]{format_date(task.get('dueDate'))}[/yellow]")
if task.get("tags"):
console.print(f"Tags: [green]{', '.join(task.get('tags', []))}[/green]")
console.print(f"Status: {'Completed' if task.get('status') == 2 else 'Open'}")
if task.get("createdTime"):
console.print(f"Created: {format_date(task.get('createdTime'))}")
if task.get("modifiedTime"):
console.print(f"Modified: {format_date(task.get('modifiedTime'))}")
def open_task_in_browser(task_id: str):
"""
Open a task in the default web browser.
Args:
task_id: Task ID to open
"""
url = f"https://ticktick.com/webapp/#q/all/tasks/{task_id}"
webbrowser.open(url)
console.print(f"[green]Opened task in browser: {url}[/green]")
def open_task_in_macos_app(task_id: str) -> bool:
"""
Open a task in the TickTick macOS app.
Args:
task_id: Task ID to open
Returns:
True if successful, False otherwise
"""
if platform.system() != "Darwin":
console.print("[red]macOS app opening is only available on macOS[/red]")
return False
try:
# Try to open with TickTick URL scheme
ticktick_url = f"ticktick://task/{task_id}"
result = subprocess.run(
["open", ticktick_url], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
console.print(f"[green]Opened task in TickTick app[/green]")
return True
else:
console.print(
"[yellow]TickTick app not found, opening in browser instead[/yellow]"
)
open_task_in_browser(task_id)
return False
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
console.print(
"[yellow]Failed to open TickTick app, opening in browser instead[/yellow]"
)
open_task_in_browser(task_id)
return False
def open_task(task_id: str, prefer_app: bool = True):
"""
Open a task in browser or app based on preference.
Args:
task_id: Task ID to open
prefer_app: Whether to prefer native app over browser
"""
if prefer_app and platform.system() == "Darwin":
if not open_task_in_macos_app(task_id):
open_task_in_browser(task_id)
else:
open_task_in_browser(task_id)
def parse_priority(priority_str: str) -> int:
"""
Parse priority string to integer.
Args:
priority_str: Priority as string (low, medium, high, none, or 0-5)
Returns:
Priority integer (0-5)
"""
if not priority_str:
return 0
priority_str = priority_str.lower().strip()
if priority_str in ["none", "no", "0"]:
return 0
elif priority_str in ["low", "1"]:
return 1
elif priority_str in ["medium", "med", "2"]:
return 2
elif priority_str in ["high", "3"]:
return 3
elif priority_str in ["very high", "urgent", "4"]:
return 4
elif priority_str in ["critical", "5"]:
return 5
else:
try:
priority = int(priority_str)
return max(0, min(5, priority))
except ValueError:
return 0
def validate_date(date_str: str) -> bool:
"""
Validate if a date string is parseable.
Args:
date_str: Date string to validate
Returns:
True if valid, False otherwise
"""
if date_str.lower() in ["today", "tomorrow", "yesterday"]:
return True
try:
date_parser.parse(date_str)
return True
except ValueError:
return False