From 86034308eef93d909706262f3ed14e57bdf69bd7 Mon Sep 17 00:00:00 2001 From: ThePayyavula Date: Sat, 6 Jun 2026 03:06:44 -0400 Subject: [PATCH 1/4] Add categories command and DB method Introduce a new CLI command `categories` that prints all categories (JSON when using porcelain formatter, otherwise newline-separated). Implement Database.categories() to query DISTINCT category values from the categories table (ordered) and return them as a list of strings, ensuring the DB cursor is closed. Add a test in tests/test_cli.py to verify categories are extracted from an ICS item and shown by the command. --- tests/test_cli.py | 9 +++++++++ todoman/cli.py | 9 +++++++++ todoman/model.py | 13 +++++++++++++ 3 files changed, 31 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 44a2d2ba..0557501d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,6 +40,15 @@ def test_list(tmpdir: py.path.local, runner: CliRunner, create: Callable) -> Non assert not result.exception assert "harhar" in result.output +def test_categories(runner, create): + create( + "one.ics", + "SUMMARY:test\nCATEGORIES:work,home\n", + ) + result = runner.invoke(cli, ["categories"]) + assert result.exit_code == 0 + assert "work" in result.output + assert "home" in result.output def test_no_default_list(runner: CliRunner) -> None: result = runner.invoke(cli, ["new", "Configure a default list"]) diff --git a/todoman/cli.py b/todoman/cli.py index b00bb112..516f19b6 100644 --- a/todoman/cli.py +++ b/todoman/cli.py @@ -686,6 +686,15 @@ def lists(ctx: AppContext) -> None: text = json.dumps(lists) if porcelain else "\n".join(lists) click.echo(text) +@cli.command() +@pass_ctx +@catch_errors +def categories(ctx: AppContext) -> None: + """Returns all the categories""" + categories = ctx.db.categories() + porcelain = ctx.formatter_class == formatters.PorcelainFormatter + text = json.dumps(categories) if porcelain else "\n".join(categories) + click.echo(text) @cli.command(name="list") @pass_ctx diff --git a/todoman/model.py b/todoman/model.py index 4dc786f0..17fe5e8f 100644 --- a/todoman/model.py +++ b/todoman/model.py @@ -1180,6 +1180,19 @@ def save(self, todo: Todo) -> None: todo.id = self.cache.add_vtodo(vtodo, todo.path, todo.id) self.cache.save_to_disk() + def categories(self) -> list[str]: + cursor = self._conn.cursor() + try: + rows = cursor.execute( + """ + SELECT DISTINCT category + FROM categories + ORDER BY category + """ + ).fetchall() + return [row[0] for row in rows] + finally: + cursor.close() def _getmtime(path: str) -> int: return os.stat(path).st_mtime_ns From 376a577bab4eb90a1c66521f9d203a5592b1dfc3 Mon Sep 17 00:00:00 2001 From: ThePayyavula Date: Sat, 6 Jun 2026 03:13:12 -0400 Subject: [PATCH 2/4] Update model.py --- todoman/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todoman/model.py b/todoman/model.py index 17fe5e8f..28934bd1 100644 --- a/todoman/model.py +++ b/todoman/model.py @@ -1181,7 +1181,7 @@ def save(self, todo: Todo) -> None: self.cache.save_to_disk() def categories(self) -> list[str]: - cursor = self._conn.cursor() + cursor = self.cache._conn.cursor() try: rows = cursor.execute( """ From d861ad238262293d30ff486752855b866abca73b Mon Sep 17 00:00:00 2001 From: ThePayyavula Date: Sat, 6 Jun 2026 03:24:50 -0400 Subject: [PATCH 3/4] Update test_cli.py --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0557501d..f2f82ed7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,7 +40,7 @@ def test_list(tmpdir: py.path.local, runner: CliRunner, create: Callable) -> Non assert not result.exception assert "harhar" in result.output -def test_categories(runner, create): +def test_categories(runner: CliRunner, create: Callable) -> None: create( "one.ics", "SUMMARY:test\nCATEGORIES:work,home\n", From 46f485726cce4d42466fad619a0400b33b5aff73 Mon Sep 17 00:00:00 2001 From: ThePayyavula Date: Sat, 6 Jun 2026 03:53:55 -0400 Subject: [PATCH 4/4] Ruff formatting Ruff formatting --- tests/test_cli.py | 2 ++ todoman/cli.py | 2 ++ todoman/model.py | 1 + 3 files changed, 5 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index f2f82ed7..c3a688df 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,6 +40,7 @@ def test_list(tmpdir: py.path.local, runner: CliRunner, create: Callable) -> Non assert not result.exception assert "harhar" in result.output + def test_categories(runner: CliRunner, create: Callable) -> None: create( "one.ics", @@ -50,6 +51,7 @@ def test_categories(runner: CliRunner, create: Callable) -> None: assert "work" in result.output assert "home" in result.output + def test_no_default_list(runner: CliRunner) -> None: result = runner.invoke(cli, ["new", "Configure a default list"]) diff --git a/todoman/cli.py b/todoman/cli.py index 516f19b6..7168657d 100644 --- a/todoman/cli.py +++ b/todoman/cli.py @@ -686,6 +686,7 @@ def lists(ctx: AppContext) -> None: text = json.dumps(lists) if porcelain else "\n".join(lists) click.echo(text) + @cli.command() @pass_ctx @catch_errors @@ -696,6 +697,7 @@ def categories(ctx: AppContext) -> None: text = json.dumps(categories) if porcelain else "\n".join(categories) click.echo(text) + @cli.command(name="list") @pass_ctx @click.argument("lists", nargs=-1, callback=_validate_lists_param) diff --git a/todoman/model.py b/todoman/model.py index 28934bd1..20f5b65b 100644 --- a/todoman/model.py +++ b/todoman/model.py @@ -1194,5 +1194,6 @@ def categories(self) -> list[str]: finally: cursor.close() + def _getmtime(path: str) -> int: return os.stat(path).st_mtime_ns