wip
This commit is contained in:
284
src/utils/ticktick_utils.py
Normal file
284
src/utils/ticktick_utils.py
Normal 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
|
||||
Reference in New Issue
Block a user