Skip to content

Commit 83a9128

Browse files
committed
feat: update
1 parent fd2dd77 commit 83a9128

14 files changed

Lines changed: 1479 additions & 222 deletions

UI_IMPLEMENTATION_STATUS.md

Lines changed: 0 additions & 85 deletions
This file was deleted.

pyproject.toml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
[build-system]
2-
requires = ["hatchling"]
3-
build-backend = "hatchling.build"
4-
51
[project]
62
name = "todopro-cli"
73
version = "0.1.0"
@@ -16,7 +12,6 @@ classifiers = [
1612
"Programming Language :: Python :: 3.12",
1713
]
1814
dependencies = [
19-
"click>=8.1,<8.2",
2015
"rich>=13.7.0",
2116
"httpx>=0.27.0",
2217
"platformdirs>=4.2.0",
@@ -28,7 +23,7 @@ dependencies = [
2823
"packaging>=26.0",
2924
"cryptography>=42.0.0",
3025
"mnemonic>=0.20",
31-
"prompt-toolkit>=3.0.0",
26+
"textual[syntax]>=0.87.0",
3227
]
3328

3429
[project.optional-dependencies]
@@ -65,18 +60,18 @@ select = [
6560
"E", # pycodestyle errors
6661
"F", # Pyflakes
6762
"I", # isort
68-
"N", # pep8-naming
69-
"W", # pycodestyle warnings
7063
"UP", # pyupgrade
7164
"B", # flake8-bugbear
7265
"SIM", # flake8-simplify
66+
"DJ", # flake8-django
7367
"C4", # flake8-comprehensions
7468
"PIE", # flake8-pie
69+
"T20", # flake8-print
7570
"RET", # flake8-return
71+
"ARG", # flake8-unused-arguments
7672
]
7773
ignore = [
78-
"E501", # Line too long (handled by formatter)
79-
"UP007", # <--- Add this: Stop converting Optional[T] to T | None
74+
"E501", # Line too long (handled by formatter)
8075
]
8176

8277
[tool.ruff.lint.isort]
@@ -123,4 +118,5 @@ dev = [
123118
"pytest-asyncio>=1.3.0",
124119
"pytest-cov>=7.0.0",
125120
"ruff>=0.14.14",
121+
"textual-dev>=1.8.0",
126122
]

src/todopro_cli/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +0,0 @@
1-
"""TodoPro CLI - A professional command-line interface for TodoPro task management."""
2-
3-
__version__ = "0.1.0"
4-
__author__ = "TodoPro Team"

src/todopro_cli/commands/contexts.py

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
"""Context management commands for TodoPro CLI."""
22

3-
import click
3+
import typer
44
from rich.console import Console
55
from rich.table import Table
6+
67
from todopro_cli.api.client import APIClient
78
from todopro_cli.commands.utils import handle_api_error
89

910
console = Console()
11+
app = typer.Typer(help="Manage task contexts (@home, @office, @errands)")
1012

1113

12-
@click.group()
13-
def contexts():
14-
"""Manage task contexts (@home, @office, @errands)."""
15-
pass
16-
17-
18-
@contexts.command("list")
19-
@click.option("--location", "-l", is_flag=True, help="Request location for availability check")
20-
def list_contexts(location):
14+
@app.command("list")
15+
def list_contexts(
16+
location: bool = typer.Option(
17+
False, "--location", "-l", help="Request location for availability check"
18+
),
19+
):
2120
"""List all contexts."""
2221
client = APIClient()
2322

@@ -79,13 +78,14 @@ def list_contexts(location):
7978
handle_api_error(e, "listing contexts")
8079

8180

82-
@contexts.command("create")
83-
@click.argument("name")
84-
@click.option("--icon", default="📍", help="Context icon (emoji)")
85-
@click.option("--color", default="#3498DB", help="Context color (hex)")
86-
@click.option("--geo", is_flag=True, help="Enable geo-fencing at current location")
87-
@click.option("--radius", default=200, type=int, help="Geo-fence radius in meters")
88-
def create_context(name, icon, color, geo, radius):
81+
@app.command("create")
82+
def create_context(
83+
name: str = typer.Argument(..., help="Context name"),
84+
icon: str = typer.Option("📍", help="Context icon (emoji)"),
85+
color: str = typer.Option("#3498DB", help="Context color (hex)"),
86+
geo: bool = typer.Option(False, "--geo", help="Enable geo-fencing at current location"),
87+
radius: int = typer.Option(200, help="Geo-fence radius in meters"),
88+
):
8989
"""Create a new context."""
9090
client = APIClient()
9191

@@ -119,11 +119,20 @@ def create_context(name, icon, color, geo, radius):
119119
handle_api_error(e, "creating context")
120120

121121

122-
@contexts.command("delete")
123-
@click.argument("name")
124-
@click.confirmation_option(prompt="Are you sure you want to delete this context?")
125-
def delete_context(name):
122+
@app.command("delete")
123+
def delete_context(
124+
name: str = typer.Argument(..., help="Context name"),
125+
yes: bool = typer.Option(
126+
False, "--yes", "-y", help="Skip confirmation prompt"
127+
),
128+
):
126129
"""Delete a context."""
130+
if not yes:
131+
confirmed = typer.confirm("Are you sure you want to delete this context?")
132+
if not confirmed:
133+
console.print("[yellow]Cancelled.[/yellow]")
134+
raise typer.Exit(0)
135+
127136
client = APIClient()
128137

129138
try:
@@ -142,9 +151,10 @@ def delete_context(name):
142151
handle_api_error(e, "deleting context")
143152

144153

145-
@contexts.command("tasks")
146-
@click.argument("name")
147-
def context_tasks(name):
154+
@app.command("tasks")
155+
def context_tasks(
156+
name: str = typer.Argument(..., help="Context name"),
157+
):
148158
"""List tasks for a specific context."""
149159
client = APIClient()
150160

@@ -176,7 +186,7 @@ def context_tasks(name):
176186
handle_api_error(e, "listing context tasks")
177187

178188

179-
@contexts.command("check")
189+
@app.command("check")
180190
def check_available():
181191
"""Check which contexts are available at current location."""
182192
client = APIClient()

src/todopro_cli/commands/tasks.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
format_output,
1515
format_success,
1616
)
17+
from todopro_cli.ui.textual_prompt import get_interactive_input
1718
from todopro_cli.utils.task_helpers import resolve_task_id
1819
from todopro_cli.utils.typer_helpers import SuggestingGroup
1920

@@ -82,7 +83,10 @@ async def do_list() -> None:
8283
filtered_tasks = [
8384
task
8485
for task in tasks_list
85-
if not any(task.get("id", "").endswith(short_id) for short_id in completing_tasks)
86+
if not any(
87+
task.get("id", "").endswith(short_id)
88+
for short_id in completing_tasks
89+
)
8690
]
8791
if len(filtered_tasks) < original_count:
8892
if "tasks" in result:
@@ -448,7 +452,9 @@ async def do_reopen() -> None:
448452
@app.command("today")
449453
def today(
450454
output: str = typer.Option("pretty", "--output", "-o", help="Output format"),
451-
json: bool = typer.Option(False, "--json", help="Output as JSON (alias for --output json)"),
455+
json: bool = typer.Option(
456+
False, "--json", help="Output as JSON (alias for --output json)"
457+
),
452458
compact: bool = typer.Option(False, "--compact", help="Compact output"),
453459
profile: str = typer.Option("default", "--profile", help="Profile name"),
454460
) -> None:
@@ -513,7 +519,10 @@ async def do_today() -> None:
513519
all_tasks = [
514520
task
515521
for task in all_tasks
516-
if not any(task.get("id", "").endswith(short_id) for short_id in completing_tasks)
522+
if not any(
523+
task.get("id", "").endswith(short_id)
524+
for short_id in completing_tasks
525+
)
517526
]
518527
filtered_count = original_count - len(all_tasks)
519528

@@ -538,14 +547,21 @@ async def do_today() -> None:
538547
# Handle empty result based on output format
539548
if output == "json":
540549
import json
541-
print(json.dumps({
542-
"tasks": [],
543-
"overdue_count": 0,
544-
"today_count": 0,
545-
"message": "No tasks due today"
546-
}))
550+
551+
print(
552+
json.dumps(
553+
{
554+
"tasks": [],
555+
"overdue_count": 0,
556+
"today_count": 0,
557+
"message": "No tasks due today",
558+
}
559+
)
560+
)
547561
elif output == "yaml":
548-
print("tasks: []\noverdue_count: 0\ntoday_count: 0\nmessage: No tasks due today")
562+
print(
563+
"tasks: []\noverdue_count: 0\ntoday_count: 0\nmessage: No tasks due today"
564+
)
549565
else:
550566
console.print("[green]No tasks due today! 🎉[/green]")
551567

@@ -562,7 +578,9 @@ async def do_today() -> None:
562578
@app.command("next")
563579
def next_task(
564580
output: str = typer.Option("table", "--output", "-o", help="Output format"),
565-
json: bool = typer.Option(False, "--json", help="Output as JSON (alias for --output json)"),
581+
json: bool = typer.Option(
582+
False, "--json", help="Output as JSON (alias for --output json)"
583+
),
566584
profile: str = typer.Option("default", "--profile", help="Profile name"),
567585
) -> None:
568586
"""Show the next task to do right now."""
@@ -585,10 +603,8 @@ async def do_next() -> None:
585603
# No tasks found
586604
if output == "json":
587605
import json
588-
print(json.dumps({
589-
"task": None,
590-
"message": result["message"]
591-
}))
606+
607+
print(json.dumps({"task": None, "message": result["message"]}))
592608
elif output == "yaml":
593609
print(f"task: null\nmessage: {result['message']}")
594610
else:
@@ -600,6 +616,7 @@ async def do_next() -> None:
600616
else:
601617
# Custom simple format for next task
602618
from todopro_cli.ui.formatters import format_next_task
619+
603620
format_next_task(result)
604621

605622
finally:
@@ -745,8 +762,6 @@ def quick_add(
745762
# If no text provided, use interactive prompt
746763
if not text:
747764
try:
748-
from todopro_cli.ui.interactive_prompt import get_interactive_input
749-
750765
text = asyncio.run(get_interactive_input(profile=profile))
751766
except KeyboardInterrupt:
752767
console.print("\n[yellow]Cancelled.[/yellow]")
@@ -809,9 +824,7 @@ async def do_quick_add():
809824
due = datetime.fromisoformat(
810825
parsed["due_date"].replace("Z", "+00:00")
811826
)
812-
details.append(
813-
f"📅 {due.strftime('%b %d, %Y at %I:%M %p')}"
814-
)
827+
details.append(f"📅 {due.strftime('%b %d, %Y at %I:%M %p')}")
815828

816829
if parsed.get("project_name"):
817830
details.append(f"[magenta]📁 #{parsed['project_name']}[/magenta]")

0 commit comments

Comments
 (0)