Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/taskrepo/cli/commands/add_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Add-link command for adding URLs to task links."""

import sys
from typing import Optional

import click

from taskrepo.core.repository import RepositoryManager
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result


@click.command(name="add-link")
@click.argument("task_id", required=True)
@click.argument("url", required=True)
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.pass_context
def add_link(ctx, task_id: str, url: str, repo: Optional[str]):
"""Add a link/URL to a task.

Examples:
tsk add-link 5 "https://github.com/org/repo/issues/123"
tsk add-link 10 "https://mail.google.com/..." --repo work

TASK_ID: Task ID, UUID, or title
URL: URL to add to task links
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Validate URL format
if not url.startswith(("http://", "https://")):
click.secho("Error: URL must start with http:// or https://", fg="red", err=True)
ctx.exit(1)

# Find task
result = find_task_by_title_or_id(manager, task_id, repo)

if result[0] is None:
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
ctx.exit(1)

task, repository = select_task_from_result(ctx, result, task_id)

# Add link if not already present
if task.links is None:
task.links = []

if url in task.links:
click.secho(f"Link already exists in task: {task.title}", fg="yellow")
ctx.exit(0)

task.links.append(url)

# Save task
repository.save_task(task)

click.secho(f"βœ“ Added link to task: {task.title}", fg="green")
click.echo(f"\nLink added: {url}")
click.echo(f"Total links: {len(task.links)}")
49 changes: 49 additions & 0 deletions src/taskrepo/cli/commands/append.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Append command for adding content to task descriptions."""

import sys
from typing import Optional

import click

from taskrepo.core.repository import RepositoryManager
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result


@click.command()
@click.argument("task_id", required=True)
@click.option("--text", "-t", required=True, help="Text to append to task description")
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.pass_context
def append(ctx, task_id: str, text: str, repo: Optional[str]):
"""Append text to a task's description.

Examples:
tsk append 5 --text "Additional note from meeting"
tsk append 10 -t "Updated requirements" --repo work

TASK_ID: Task ID, UUID, or title to append to
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Find task
result = find_task_by_title_or_id(manager, task_id, repo)

if result[0] is None:
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
ctx.exit(1)

task, repository = select_task_from_result(ctx, result, task_id)

# Append text to description
if task.description:
task.description = task.description.rstrip() + "\n\n" + text
else:
task.description = text

# Save task
repository.save_task(task)

click.secho(f"βœ“ Appended text to task: {task.title}", fg="green")
click.echo(f"\nNew content added:")
click.echo(f" {text}")
40 changes: 39 additions & 1 deletion src/taskrepo/cli/commands/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,53 @@
@click.argument("task_ids", nargs=-1)
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.option("--yes", "-y", is_flag=True, help="Automatically archive subtasks (skip prompt)")
@click.option("--all-completed", is_flag=True, help="Archive all completed tasks")
@click.pass_context
def archive(ctx, task_ids: Tuple[str, ...], repo, yes):
def archive(ctx, task_ids: Tuple[str, ...], repo, yes, all_completed):
"""Archive one or more tasks, or list archived tasks if no task IDs are provided.

TASK_IDS: One or more task IDs to archive (optional - if omitted, lists archived tasks)

Use --all-completed to archive all tasks with status 'completed' in one command.
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Handle --all-completed flag
if all_completed and not task_ids:
# Get all completed tasks
if repo:
repository = manager.get_repository(repo)
if not repository:
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
ctx.exit(1)
all_tasks = repository.list_tasks(include_archived=False)
else:
all_tasks = manager.list_all_tasks(include_archived=False)

# Filter for completed status
completed_tasks = [task for task in all_tasks if task.status == "completed"]

if not completed_tasks:
repo_msg = f" in repository '{repo}'" if repo else ""
click.echo(f"No completed tasks found{repo_msg}.")
return

# Get display IDs from cache for completed tasks
from taskrepo.utils.id_mapping import get_display_id_from_uuid
completed_ids = []
for task in completed_tasks:
display_id = get_display_id_from_uuid(task.id)
if display_id:
completed_ids.append(str(display_id))

if not completed_ids:
click.echo("No completed tasks found with display IDs.")
return

click.echo(f"Found {len(completed_ids)} completed task(s) to archive.")
task_ids = tuple(completed_ids)

# If no task_ids provided, list archived tasks
if not task_ids:
# Get archived tasks from specified repo or all repos
Expand Down
6 changes: 6 additions & 0 deletions src/taskrepo/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def _display_config(config):
cluster_status = "enabled" if config.cluster_due_dates else "disabled"
click.echo(f" Due date clustering: {cluster_status}")
click.echo(f" TUI view mode: {config.tui_view_mode}")
remember_status = "enabled" if config.remember_tui_state else "disabled"
click.echo(f" Remember TUI state: {remember_status}")
tree_view_status = "enabled" if config.tui_tree_view else "disabled"
click.echo(f" TUI tree view default: {tree_view_status}")
last_item = config.tui_last_view_item or "(none)"
click.echo(f" TUI last view item: {last_item}")
click.secho("-" * 50, fg="green")


Expand Down
11 changes: 11 additions & 0 deletions src/taskrepo/cli/commands/delete.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Delete command for removing tasks."""

import sys
from typing import Tuple

import click
Expand Down Expand Up @@ -37,6 +38,11 @@ def delete(ctx, task_ids: Tuple[str, ...], repo, force):

# Batch confirmation for multiple tasks (unless --force flag is used)
if is_batch and not force:
# Check if we're in a terminal - if not, skip confirmation (auto-cancel for safety)
if not sys.stdin.isatty():
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
ctx.exit(1)

click.echo(f"\nAbout to delete {task_id_count} tasks. This cannot be undone.")

# Create a validator for y/n input
Expand All @@ -60,6 +66,11 @@ def delete_task_handler(task, repository):
"""Handler to delete a task with optional confirmation."""
# Single task confirmation (only if not batch and not force)
if not is_batch and not force:
# Check if we're in a terminal - if not, require --force flag
if not sys.stdin.isatty():
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
ctx.exit(1)

# Format task display with colored UUID and title
assignees_str = f" {', '.join(task.assignees)}" if task.assignees else ""
project_str = f" [{task.project}]" if task.project else ""
Expand Down
45 changes: 25 additions & 20 deletions src/taskrepo/cli/commands/done.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Done command for marking tasks as completed."""

import sys
from typing import Tuple

import click
Expand Down Expand Up @@ -87,26 +88,30 @@ def mark_as_completed(task, repository):
mark_subtasks = yes # Default to --yes flag value

if not yes:
# Show subtasks and prompt
click.echo(f"\nThis task has {count} {subtask_word}:")
for subtask, subtask_repo in subtasks_with_repos:
status_emoji = STATUS_EMOJIS.get(subtask.status, "")
click.echo(f" β€’ {status_emoji} {subtask.title} (repo: {subtask_repo.name})")

# Prompt for confirmation with Y as default
yn_validator = Validator.from_callable(
lambda text: text.lower() in ["y", "n", "yes", "no"],
error_message="Please enter 'y' or 'n'",
move_cursor_to_end=True,
)

response = prompt(
f"Mark all {count} {subtask_word} as completed too? (Y/n) ",
default="y",
validator=yn_validator,
).lower()

mark_subtasks = response in ["y", "yes"]
# Check if we're in a terminal - if not, default to yes
if not sys.stdin.isatty():
mark_subtasks = True
else:
# Show subtasks and prompt
click.echo(f"\nThis task has {count} {subtask_word}:")
for subtask, subtask_repo in subtasks_with_repos:
status_emoji = STATUS_EMOJIS.get(subtask.status, "")
click.echo(f" β€’ {status_emoji} {subtask.title} (repo: {subtask_repo.name})")

# Prompt for confirmation with Y as default
yn_validator = Validator.from_callable(
lambda text: text.lower() in ["y", "n", "yes", "no"],
error_message="Please enter 'y' or 'n'",
move_cursor_to_end=True,
)

response = prompt(
f"Mark all {count} {subtask_word} as completed too? (Y/n) ",
default="y",
validator=yn_validator,
).lower()

mark_subtasks = response in ["y", "yes"]

if mark_subtasks:
# Mark all subtasks as completed
Expand Down
Loading
Loading