|
|
|
@@ -0,0 +1,470 @@
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import sys
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional, Union
|
|
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
import typer
|
|
|
|
|
from dotenv import load_dotenv, set_key
|
|
|
|
|
from plankapy.v2 import Card, Planka
|
|
|
|
|
from rich.console import Console
|
|
|
|
|
from rich.table import Table
|
|
|
|
|
from typer.core import TyperGroup
|
|
|
|
|
|
|
|
|
|
def get_binary_dir() -> Path:
|
|
|
|
|
argv0 = sys.argv[0]
|
|
|
|
|
path = Path(argv0)
|
|
|
|
|
if not path.is_absolute():
|
|
|
|
|
resolved = shutil.which(argv0)
|
|
|
|
|
if resolved:
|
|
|
|
|
path = Path(resolved)
|
|
|
|
|
if path.exists():
|
|
|
|
|
return path.resolve().parent if path.is_file() else path.resolve()
|
|
|
|
|
return Path(__file__).resolve().parent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ENV_PATH = get_binary_dir() / ".env"
|
|
|
|
|
|
|
|
|
|
# Load environment variables from the .env next to the binary/script.
|
|
|
|
|
load_dotenv(dotenv_path=ENV_PATH)
|
|
|
|
|
|
|
|
|
|
class HelpOnUnknownCommandGroup(TyperGroup):
|
|
|
|
|
def get_command(self, ctx: click.Context, cmd_name: str):
|
|
|
|
|
command = super().get_command(ctx, cmd_name)
|
|
|
|
|
if command is None:
|
|
|
|
|
click.echo(ctx.get_help())
|
|
|
|
|
ctx.exit(2)
|
|
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = typer.Typer(cls=HelpOnUnknownCommandGroup)
|
|
|
|
|
console = Console()
|
|
|
|
|
projects_app = typer.Typer(cls=HelpOnUnknownCommandGroup, help="Manage projects")
|
|
|
|
|
boards_app = typer.Typer(cls=HelpOnUnknownCommandGroup, help="Manage boards")
|
|
|
|
|
lists_app = typer.Typer(cls=HelpOnUnknownCommandGroup, help="Manage lists")
|
|
|
|
|
cards_app = typer.Typer(cls=HelpOnUnknownCommandGroup, help="Manage cards")
|
|
|
|
|
notifications_app = typer.Typer(cls=HelpOnUnknownCommandGroup, help="Manage notifications")
|
|
|
|
|
app.add_typer(projects_app, name="projects")
|
|
|
|
|
app.add_typer(boards_app, name="boards")
|
|
|
|
|
app.add_typer(lists_app, name="lists")
|
|
|
|
|
app.add_typer(cards_app, name="cards")
|
|
|
|
|
app.add_typer(notifications_app, name="notifications")
|
|
|
|
|
|
|
|
|
|
def get_env_config() -> tuple[Optional[str], Optional[str], Optional[str]]:
|
|
|
|
|
return (
|
|
|
|
|
os.getenv("PLANKA_URL"),
|
|
|
|
|
os.getenv("PLANKA_USERNAME"),
|
|
|
|
|
os.getenv("PLANKA_PASSWORD"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def parse_iso_datetime(value: Optional[str]) -> Optional[datetime]:
|
|
|
|
|
if value is None:
|
|
|
|
|
return None
|
|
|
|
|
candidate = value.strip()
|
|
|
|
|
if candidate.endswith("Z"):
|
|
|
|
|
candidate = f"{candidate[:-1]}+00:00"
|
|
|
|
|
try:
|
|
|
|
|
return datetime.fromisoformat(candidate)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise typer.BadParameter(
|
|
|
|
|
"Invalid datetime. Use ISO-8601 like 2025-01-31 or 2025-01-31T10:30:00Z."
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
def parse_position(value: Optional[str]) -> Optional[Union[str, int]]:
|
|
|
|
|
if value is None:
|
|
|
|
|
return None
|
|
|
|
|
normalized = value.strip().lower()
|
|
|
|
|
if normalized in ("top", "bottom"):
|
|
|
|
|
return normalized
|
|
|
|
|
try:
|
|
|
|
|
return int(value)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise typer.BadParameter("Position must be 'top', 'bottom', or an integer.") from exc
|
|
|
|
|
|
|
|
|
|
def find_list(planka: Planka, list_id: str):
|
|
|
|
|
for project in planka.projects:
|
|
|
|
|
for board in project.boards:
|
|
|
|
|
for list_item in board.lists:
|
|
|
|
|
if list_item.id == list_id:
|
|
|
|
|
return list_item
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_card_by_id(planka: Planka, card_id: str) -> Optional[Card]:
|
|
|
|
|
try:
|
|
|
|
|
card_data = planka.endpoints.getCard(card_id)["item"]
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
return Card(card_data, planka)
|
|
|
|
|
|
|
|
|
|
def make_table(title: str) -> Table:
|
|
|
|
|
return Table(
|
|
|
|
|
title=title,
|
|
|
|
|
show_header=True,
|
|
|
|
|
header_style="bold",
|
|
|
|
|
show_edge=False,
|
|
|
|
|
show_lines=False,
|
|
|
|
|
box=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def render_notifications(title: str, notifications: list) -> None:
|
|
|
|
|
if not notifications:
|
|
|
|
|
console.print("No notifications found.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
table = make_table(title)
|
|
|
|
|
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
|
|
|
table.add_column("Type", style="magenta")
|
|
|
|
|
table.add_column("Read", justify="center")
|
|
|
|
|
table.add_column("Created At", justify="right")
|
|
|
|
|
table.add_column("Card ID", justify="right")
|
|
|
|
|
|
|
|
|
|
for notification in notifications:
|
|
|
|
|
card_id = notification.schema.get("cardId")
|
|
|
|
|
table.add_row(
|
|
|
|
|
str(notification.id),
|
|
|
|
|
str(notification.type),
|
|
|
|
|
"yes" if notification.is_read else "no",
|
|
|
|
|
str(notification.created_at),
|
|
|
|
|
str(card_id),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
|
|
|
|
|
def get_planka() -> Planka:
|
|
|
|
|
planka_url, planka_username, planka_password = get_env_config()
|
|
|
|
|
if not planka_url or not planka_username or not planka_password:
|
|
|
|
|
console.print(
|
|
|
|
|
"[bold red]Error:[/bold red] Missing credentials. "
|
|
|
|
|
f"Set PLANKA_URL, PLANKA_USERNAME, and PLANKA_PASSWORD or run "
|
|
|
|
|
f"`planka-cli login --url ... --username ... --password ...` "
|
|
|
|
|
f"to write {ENV_PATH}."
|
|
|
|
|
)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Initialize Planka client (v2) and authenticate
|
|
|
|
|
planka = Planka(planka_url)
|
|
|
|
|
planka.login(username=planka_username, password=planka_password)
|
|
|
|
|
return planka
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Connection Error:[/bold red] {e}")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
|
def login(
|
|
|
|
|
url: str = typer.Option(..., "--url", "-u", help="Planka base URL"),
|
|
|
|
|
username: str = typer.Option(..., "--username", "-n", help="Planka username"),
|
|
|
|
|
password: str = typer.Option(..., "--password", "-p", help="Planka password"),
|
|
|
|
|
):
|
|
|
|
|
"""Store credentials in a .env file next to the binary."""
|
|
|
|
|
try:
|
|
|
|
|
ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
set_key(str(ENV_PATH), "PLANKA_URL", url)
|
|
|
|
|
set_key(str(ENV_PATH), "PLANKA_USERNAME", username)
|
|
|
|
|
set_key(str(ENV_PATH), "PLANKA_PASSWORD", password)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] Could not write {ENV_PATH}: {e}")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
os.chmod(ENV_PATH, 0o600)
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
console.print(f"[green]Saved credentials to[/green] {ENV_PATH}")
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
|
def logout():
|
|
|
|
|
"""Delete the stored .env file next to the binary."""
|
|
|
|
|
if not ENV_PATH.exists():
|
|
|
|
|
console.print(f"No stored credentials found at {ENV_PATH}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
ENV_PATH.unlink()
|
|
|
|
|
except OSError as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] Could not delete {ENV_PATH}: {e}")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
console.print("[green]Logged out.[/green] Removed stored credentials.")
|
|
|
|
|
|
|
|
|
|
@app.command()
|
|
|
|
|
def status():
|
|
|
|
|
"""Check connection and print current user info."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
user = planka.me
|
|
|
|
|
console.print(f"[green]Connected![/green] Logged in as: [bold]{user.username}[/bold] (ID: {user.id})")
|
|
|
|
|
if user.name:
|
|
|
|
|
console.print(f"Name: {user.name}")
|
|
|
|
|
console.print(f"Email: {user.email}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error fetching status:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@projects_app.command("list")
|
|
|
|
|
def list_projects():
|
|
|
|
|
"""List all projects."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
projects = planka.projects
|
|
|
|
|
if not projects:
|
|
|
|
|
console.print("No projects found.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
table = make_table("Projects")
|
|
|
|
|
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
|
|
|
table.add_column("Name", style="magenta")
|
|
|
|
|
table.add_column("Created At", justify="right")
|
|
|
|
|
|
|
|
|
|
for p in projects:
|
|
|
|
|
table.add_row(str(p.id), p.name, str(p.created_at))
|
|
|
|
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@boards_app.command("list")
|
|
|
|
|
def list_boards(project_id: Optional[str] = typer.Argument(None, help="Project ID to filter by")):
|
|
|
|
|
"""List boards. Optionally filter by Project ID."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
if project_id is not None:
|
|
|
|
|
# Find specific project
|
|
|
|
|
project = next((p for p in planka.projects if p.id == project_id), None)
|
|
|
|
|
if not project:
|
|
|
|
|
console.print(f"[red]Project {project_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
boards_list = project.boards
|
|
|
|
|
title = f"Boards in Project {project.name}"
|
|
|
|
|
else:
|
|
|
|
|
# List all boards from all projects the user has access to
|
|
|
|
|
# plankapy doesn't have a direct 'all_boards', so we iterate projects
|
|
|
|
|
boards_list = []
|
|
|
|
|
for p in planka.projects:
|
|
|
|
|
boards_list.extend(p.boards)
|
|
|
|
|
title = "All Boards"
|
|
|
|
|
|
|
|
|
|
if not boards_list:
|
|
|
|
|
console.print("No boards found.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
table = make_table(title)
|
|
|
|
|
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
|
|
|
table.add_column("Name", style="magenta")
|
|
|
|
|
table.add_column("Project ID", justify="right")
|
|
|
|
|
|
|
|
|
|
for b in boards_list:
|
|
|
|
|
project_id_value = b.schema.get("projectId")
|
|
|
|
|
table.add_row(str(b.id), b.name, str(project_id_value))
|
|
|
|
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@lists_app.command("list")
|
|
|
|
|
def list_lists(board_id: str):
|
|
|
|
|
"""List all lists in a board."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
# We need to find the board first to get its lists
|
|
|
|
|
# This is a bit inefficient in plankapy if we don't know the project, but we can search
|
|
|
|
|
target_board = None
|
|
|
|
|
for p in planka.projects:
|
|
|
|
|
for b in p.boards:
|
|
|
|
|
if b.id == board_id:
|
|
|
|
|
target_board = b
|
|
|
|
|
break
|
|
|
|
|
if target_board:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not target_board:
|
|
|
|
|
console.print(f"[red]Board {board_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
table = make_table(f"Lists in Board: {target_board.name}")
|
|
|
|
|
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
|
|
|
table.add_column("Name", style="magenta")
|
|
|
|
|
table.add_column("Position", justify="right")
|
|
|
|
|
|
|
|
|
|
for l in target_board.lists:
|
|
|
|
|
table.add_row(str(l.id), l.name, str(l.position))
|
|
|
|
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@cards_app.command("list")
|
|
|
|
|
def list_cards(list_id: str):
|
|
|
|
|
"""List all cards in a list."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
target_list = find_list(planka, list_id)
|
|
|
|
|
|
|
|
|
|
if not target_list:
|
|
|
|
|
console.print(f"[red]List {list_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
table = make_table(f"Cards in List: {target_list.name}")
|
|
|
|
|
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
|
|
|
table.add_column("Name", style="magenta")
|
|
|
|
|
table.add_column("Position", justify="right")
|
|
|
|
|
|
|
|
|
|
for c in target_list.cards:
|
|
|
|
|
table.add_row(str(c.id), c.name, str(c.position))
|
|
|
|
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@cards_app.command("create")
|
|
|
|
|
def create_card(
|
|
|
|
|
list_id: str = typer.Argument(..., help="List ID to create the card in"),
|
|
|
|
|
name: str = typer.Argument(..., help="Card name/title"),
|
|
|
|
|
description: Optional[str] = typer.Option(None, "--description", "-d", help="Card description"),
|
|
|
|
|
position: str = typer.Option("bottom", "--position", "-p", help="Position: top, bottom, or integer"),
|
|
|
|
|
card_type: str = typer.Option("project", "--type", "-t", help="Card type (project, story)"),
|
|
|
|
|
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date (ISO-8601)"),
|
|
|
|
|
due_completed: bool = typer.Option(False, "--due-completed", help="Mark due date as completed"),
|
|
|
|
|
):
|
|
|
|
|
"""Create a new card in a list."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
target_list = find_list(planka, list_id)
|
|
|
|
|
if not target_list:
|
|
|
|
|
console.print(f"[red]List {list_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
parsed_due_date = parse_iso_datetime(due_date)
|
|
|
|
|
parsed_position = parse_position(position) or "bottom"
|
|
|
|
|
|
|
|
|
|
card = target_list.create_card(
|
|
|
|
|
name=name,
|
|
|
|
|
position=parsed_position,
|
|
|
|
|
type=card_type,
|
|
|
|
|
description=description,
|
|
|
|
|
due_date=parsed_due_date,
|
|
|
|
|
due_date_completed=due_completed,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.print(
|
|
|
|
|
f"[green]Created card[/green] [bold]{card.name}[/bold] "
|
|
|
|
|
f"(ID: {card.id}) in list [bold]{target_list.name}[/bold]"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@cards_app.command("update")
|
|
|
|
|
def update_card(
|
|
|
|
|
card_id: str = typer.Argument(..., help="Card ID to update"),
|
|
|
|
|
name: Optional[str] = typer.Option(None, "--name", help="New card name"),
|
|
|
|
|
description: Optional[str] = typer.Option(None, "--description", "-d", help="New card description"),
|
|
|
|
|
clear_description: bool = typer.Option(False, "--clear-description", help="Clear the description"),
|
|
|
|
|
position: Optional[str] = typer.Option(None, "--position", "-p", help="Position: top, bottom, or integer"),
|
|
|
|
|
list_id: Optional[str] = typer.Option(None, "--list-id", help="Move to a new list"),
|
|
|
|
|
card_type: Optional[str] = typer.Option(None, "--type", "-t", help="Card type (project, story)"),
|
|
|
|
|
due_date: Optional[str] = typer.Option(None, "--due-date", help="Due date (ISO-8601)"),
|
|
|
|
|
clear_due_date: bool = typer.Option(False, "--clear-due-date", help="Clear the due date"),
|
|
|
|
|
due_completed: Optional[bool] = typer.Option(
|
|
|
|
|
None,
|
|
|
|
|
"--due-completed/--no-due-completed",
|
|
|
|
|
help="Mark due date as completed/uncompleted",
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
"""Update an existing card."""
|
|
|
|
|
if description is not None and clear_description:
|
|
|
|
|
console.print("[bold red]Error:[/bold red] Use either --description or --clear-description.")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
if due_date is not None and clear_due_date:
|
|
|
|
|
console.print("[bold red]Error:[/bold red] Use either --due-date or --clear-due-date.")
|
|
|
|
|
raise typer.Exit(1)
|
|
|
|
|
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
card = get_card_by_id(planka, card_id)
|
|
|
|
|
if not card:
|
|
|
|
|
console.print(f"[red]Card {card_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
update_fields: dict[str, object] = {}
|
|
|
|
|
if name is not None:
|
|
|
|
|
update_fields["name"] = name
|
|
|
|
|
if description is not None:
|
|
|
|
|
update_fields["description"] = description
|
|
|
|
|
elif clear_description:
|
|
|
|
|
update_fields["description"] = None
|
|
|
|
|
if card_type is not None:
|
|
|
|
|
update_fields["type"] = card_type
|
|
|
|
|
if due_date is not None:
|
|
|
|
|
update_fields["dueDate"] = parse_iso_datetime(due_date)
|
|
|
|
|
elif clear_due_date:
|
|
|
|
|
update_fields["dueDate"] = None
|
|
|
|
|
if due_completed is not None:
|
|
|
|
|
update_fields["isDueCompleted"] = due_completed
|
|
|
|
|
|
|
|
|
|
move_position = parse_position(position)
|
|
|
|
|
if list_id is not None or move_position is not None:
|
|
|
|
|
target_list = find_list(planka, list_id) if list_id is not None else card.list
|
|
|
|
|
if not target_list:
|
|
|
|
|
console.print(f"[red]List {list_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
card.move(target_list, position=move_position or "top")
|
|
|
|
|
|
|
|
|
|
if update_fields:
|
|
|
|
|
card.update(**update_fields)
|
|
|
|
|
|
|
|
|
|
if not update_fields and list_id is None and move_position is None:
|
|
|
|
|
console.print("[yellow]No updates provided.[/yellow]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
console.print(f"[green]Updated card[/green] [bold]{card.name}[/bold] (ID: {card.id})")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@cards_app.command("delete")
|
|
|
|
|
def delete_card(
|
|
|
|
|
card_id: str = typer.Argument(..., help="Card ID to delete"),
|
|
|
|
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
|
|
|
):
|
|
|
|
|
"""Delete a card."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
card = get_card_by_id(planka, card_id)
|
|
|
|
|
if not card:
|
|
|
|
|
console.print(f"[red]Card {card_id} not found.[/red]")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not yes:
|
|
|
|
|
confirm = typer.confirm(f"Delete card '{card.name}' ({card.id})?")
|
|
|
|
|
if not confirm:
|
|
|
|
|
console.print("Cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
card.delete()
|
|
|
|
|
console.print(f"[green]Deleted card[/green] {card_id}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@notifications_app.command("all")
|
|
|
|
|
def all_notifications():
|
|
|
|
|
"""List all notifications."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
render_notifications("Notifications", planka.notifications)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
@notifications_app.command("unread")
|
|
|
|
|
def unread_notifications():
|
|
|
|
|
"""List unread notifications."""
|
|
|
|
|
planka = get_planka()
|
|
|
|
|
try:
|
|
|
|
|
render_notifications("Unread Notifications", planka.unread_notifications)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
app()
|