From 4cc20ef57b3076b9079b5df7b594743d67ef2cdd Mon Sep 17 00:00:00 2001 From: pavanteja443 Date: Fri, 3 Oct 2025 16:36:30 +0530 Subject: [PATCH 1/2] feat: Add saved searches/filters --- tix/cli.py | 176 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 152 insertions(+), 24 deletions(-) diff --git a/tix/cli.py b/tix/cli.py index 50f4e1a..955259f 100644 --- a/tix/cli.py +++ b/tix/cli.py @@ -15,6 +15,10 @@ console = Console() storage = TaskStorage() +from typing import Optional, Dict, Any +import json + + @click.group(invoke_without_command=True) @click.version_option(version="0.8.0", prog_name="tix") @@ -600,62 +604,186 @@ def search(query, tag, priority, completed): console.print(table) -@cli.command() +# ---- Saved filters: replace the old `filter` command with this group ---- +from typing import Optional, Dict, Any +import json + +FILTERS_PATH = Path.home() / ".tix" / "filters.json" +FILTERS_PATH.parent.mkdir(parents=True, exist_ok=True) + + +def _load_saved_filters() -> Dict[str, Dict[str, Any]]: + """Return mapping name -> filter-params""" + if not FILTERS_PATH.exists(): + return {} + try: + with FILTERS_PATH.open("r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict): + return data + return {} + except Exception: + return {} + + +def _save_saved_filters(filters: Dict[str, Dict[str, Any]]) -> bool: + try: + with FILTERS_PATH.open("w", encoding="utf-8") as f: + json.dump(filters, f, indent=2, sort_keys=True) + return True + except Exception: + return False + + +@cli.group() +def filter(): + """Manage and apply saved filters""" + pass + + +@filter.command("apply") @click.option("--priority", "-p", type=click.Choice(["low", "medium", "high"]), help="Filter by priority") @click.option("--tag", "-t", help="Filter by tag") @click.option("--completed/--active", "-c/-a", default=None, help="Filter by completion status") -def filter(priority, tag, completed): - """Filter tasks by criteria""" - tasks = storage.load_tasks() +@click.option("--saved", "-s", "saved_name", help="Apply a saved filter by name") +def filter_apply(priority: Optional[str], tag: Optional[str], completed: Optional[bool], saved_name: Optional[str]): + """ + Apply a filter (immediately). Use --saved to apply saved filters. + If --saved is provided, any inline options are ignored (saved filter takes precedence). + """ + # If saved filter requested, load and override CLI params + if saved_name: + saved = _load_saved_filters().get(saved_name) + if not saved: + console.print(f"[red]✗[/red] Saved filter '{saved_name}' not found") + return + priority = saved.get("priority") + tag = saved.get("tag") + completed = saved.get("completed") + + # Now perform filtering (same UX as previous 'filter' command) + tasks = storage.load_tasks() if hasattr(storage, "load_tasks") else [] + + # completion filter: None = all, True = completed, False = active + if completed is not None: + tasks = [t for t in tasks if getattr(t, "completed", False) == completed] - # Apply filters if priority: - tasks = [t for t in tasks if t.priority == priority] + tasks = [t for t in tasks if getattr(t, "priority", None) == priority] if tag: - tasks = [t for t in tasks if tag in t.tags] - - if completed is not None: - tasks = [t for t in tasks if t.completed == completed] + tasks = [t for t in tasks if tag in getattr(t, "tags", [])] if not tasks: console.print("[dim]No matching tasks[/dim]") return # Build filter description - filters = [] + filters_desc = [] if priority: - filters.append(f"priority={priority}") + filters_desc.append(f"priority={priority}") if tag: - filters.append(f"tag='{tag}'") + filters_desc.append(f"tag='{tag}'") if completed is not None: - filters.append("completed" if completed else "active") - - filter_desc = " AND ".join(filters) if filters else "all" + filters_desc.append("completed" if completed else "active") + filter_desc = " AND ".join(filters_desc) if filters_desc else "all" console.print(f"[bold]{len(tasks)} task(s) matching [{filter_desc}]:[/bold]\n") table = Table() table.add_column("ID", style="cyan", width=4) - table.add_column("✓", width=3) + table.add_column("✔", width=3) table.add_column("Priority", width=8) table.add_column("Task") table.add_column("Tags", style="dim") - for task in sorted(tasks, key=lambda t: (t.completed, t.id)): - status = "✓" if task.completed else "○" - priority_color = {"high": "red", "medium": "yellow", "low": "green"}[task.priority] - tags_str = ", ".join(task.tags) if task.tags else "" + for task in sorted(tasks, key=lambda t: (getattr(t, "completed", False), getattr(t, "id", 0))): + status = "✔" if getattr(task, "completed", False) else "○" + priority_color = {"high": "red", "medium": "yellow", "low": "green"}.get(getattr(task, "priority", "medium"), "yellow") + tags_str = ", ".join(getattr(task, "tags", [])) if getattr(task, "tags", None) else "" table.add_row( - str(task.id), + str(getattr(task, "id", "")), status, - f"[{priority_color}]{task.priority}[/{priority_color}]", - task.text, + f"[{priority_color}]{getattr(task, 'priority', '')}[/{priority_color}]", + getattr(task, "text", getattr(task, "task", "")), tags_str, ) console.print(table) +@filter.command("save") +@click.argument("name") +@click.option("--priority", "-p", type=click.Choice(["low", "medium", "high"]), help="Filter by priority") +@click.option("--tag", "-t", help="Filter by tag") +@click.option("--completed/--active", "-c/-a", default=None, help="Filter by completion status") +@click.option("--force", "-f", is_flag=True, help="Overwrite existing saved filter of same name") +def filter_save(name: str, priority: Optional[str], tag: Optional[str], completed: Optional[bool], force: bool): + """ + Save a filter under . Later you can apply it with `tix filter apply --saved `. + Example: tix filter save work -t work -p high + """ + filters = _load_saved_filters() + if name in filters and not force: + console.print(f"[red]✗[/red] A saved filter named '{name}' already exists. Use --force to overwrite.") + return + + storage_obj = { + "priority": priority, + "tag": tag, + # store completed as True/False/null + "completed": None if completed is None else (True if completed else False), + "saved_at": datetime.now().isoformat() + } + + # Remove empty keys + storage_obj = {k: v for k, v in storage_obj.items() if v is not None} + filters[name] = storage_obj + if _save_saved_filters(filters): + console.print(f"[green]✔[/green] Saved filter '{name}'") + # quick usage hint + parts = [] + if "priority" in storage_obj: + parts.append(f"-p {storage_obj['priority']}") + if "tag" in storage_obj: + parts.append(f"-t {storage_obj['tag']}") + if "completed" in storage_obj: + parts.append("--completed" if storage_obj["completed"] else "--active") + if parts: + console.print(f"[dim]Use: tix filter apply --saved {name} (equivalent: tix filter apply {' '.join(parts)})[/dim]") + else: + console.print(f"[red]✗[/red] Failed to save filter '{name}'") + + +@filter.command("list") +def filter_list(): + """List saved filters""" + filters = _load_saved_filters() + if not filters: + console.print("[dim]No saved filters[/dim]") + return + + table = Table(title="Saved Filters") + table.add_column("Name", style="cyan") + table.add_column("Filter", style="dim") + table.add_column("Saved At", style="green", width=22) + + for name, obj in sorted(filters.items(), key=lambda x: x[0]): + parts = [] + if "priority" in obj: + parts.append(f"priority={obj['priority']}") + if "tag" in obj: + parts.append(f"tag='{obj['tag']}'") + if "completed" in obj: + parts.append("completed" if obj["completed"] else "active") + filter_desc = " AND ".join(parts) if parts else "all" + saved_at = obj.get("saved_at", "-") + table.add_row(name, filter_desc, saved_at) + + console.print(table) +# ---- end saved filters ---- + + + @cli.command() @click.option("--no-tags", is_flag=True, help="Show tasks without tags") def tags(no_tags): From bcac13fa9fcf39d4b65961bb595dfd93c7e5e37e Mon Sep 17 00:00:00 2001 From: pavanteja443 Date: Fri, 3 Oct 2025 16:47:35 +0530 Subject: [PATCH 2/2] update readme usage guide with saved filters --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 9a4fb29..b8d9ba1 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,54 @@ tix backup restore ``` +# 📖 Filters + +#### Saved Filters (Saved Searches) + +You can save commonly used filters so you don’t have to re-type them every time. + +```bash +# Save a filter named "work" for high-priority tasks tagged "work" +tix filter save work -t work -p high + +# Save filter for completed tasks +tix filter save done-only --completed + +# Overwrite an existing filter (with --force) +tix filter save work -t work -p medium --force +```` + +#### Listing Saved Filters + +```bash +# Show all saved filters +tix filter list +``` + +Example output: + +``` +Saved Filters: + • work → priority=high AND tag='work' + • done-only → completed +``` + +#### Applying Saved Filters + +```bash +# Apply a saved filter +tix filter apply --saved work + +# Apply directly without saving +tix filter apply -p high -t urgent + +# Saved filter takes precedence over inline flags +tix filter apply --saved work -p low # will still use the saved 'work' filter +``` + +⚡ Saved filters are stored in `~/.tix/filters.json`. +You can edit/remove the file manually, but it’s recommended to use the CLI commands. + ## 🎨 Using Tab Completion Tab completion works automatically after installation: