From 0b2ae03cc6a440e2d2d4762b67335f258f4b7acd Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Sat, 7 Feb 2026 11:49:00 +0000 Subject: [PATCH 01/21] feat: add CLI interface --- quick-add/cli/ArgumentParser.vala | 133 +++++++++++++++++++++++++++++ quick-add/cli/Main.vala | 94 ++++++++++++++++++++ quick-add/cli/OutputFormatter.vala | 30 +++++++ quick-add/cli/TaskCreator.vala | 119 ++++++++++++++++++++++++++ quick-add/cli/TaskValidator.vala | 72 ++++++++++++++++ quick-add/meson.build | 20 ++++- 6 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 quick-add/cli/ArgumentParser.vala create mode 100644 quick-add/cli/Main.vala create mode 100644 quick-add/cli/OutputFormatter.vala create mode 100644 quick-add/cli/TaskCreator.vala create mode 100644 quick-add/cli/TaskValidator.vala diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala new file mode 100644 index 000000000..8a19753b0 --- /dev/null +++ b/quick-add/cli/ArgumentParser.vala @@ -0,0 +1,133 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +namespace PlanifyCLI { + public class TaskArguments : Object { + public string? content { get; set; default = null; } + public string? description { get; set; default = null; } + public string? project_name { get; set; default = null; } + public string? project_id { get; set; default = null; } + public int priority { get; set; default = 4; } + public string? due_date { get; set; default = null; } + } + + public class ArgumentParser : Object { + public static TaskArguments? parse (string[] args, out int exit_code) { + exit_code = 0; + + // Check for command + if (args.length < 2) { + stderr.printf ("Error: No command specified\n"); + stderr.printf ("Usage: %s add [OPTIONS]\n", args[0]); + stderr.printf ("Run '%s --help' for more information\n", args[0]); + exit_code = 1; + return null; + } + + string command = args[1]; + + if (command != "add") { + stderr.printf ("Error: Unknown command '%s'\n", command); + stderr.printf ("Available commands: add\n"); + exit_code = 1; + return null; + } + + var task_args = new TaskArguments (); + + // Parse options starting from index 2 + for (int i = 2; i < args.length; i++) { + string arg = args[i]; + + if (arg == "-c" || arg == "--content") { + if (i + 1 < args.length) { + task_args.content = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-d" || arg == "--description") { + if (i + 1 < args.length) { + task_args.description = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-p" || arg == "--project") { + if (i + 1 < args.length) { + task_args.project_name = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-i" || arg == "--project-id") { + if (i + 1 < args.length) { + task_args.project_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-P" || arg == "--priority") { + if (i + 1 < args.length) { + task_args.priority = int.parse (args[++i]); + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-D" || arg == "--due") { + if (i + 1 < args.length) { + task_args.due_date = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-h" || arg == "--help") { + print_help (args[0]); + exit_code = 0; + return null; + } else { + stderr.printf ("Error: Unknown option '%s'\n", arg); + stderr.printf ("Run '%s add --help' for usage information\n", args[0]); + exit_code = 1; + return null; + } + } + + return task_args; + } + + private static void print_help (string program_name) { + stdout.printf ("Usage: %s add [OPTIONS]\n\n", program_name); + stdout.printf ("Options:\n"); + stdout.printf (" -c, --content=CONTENT Task content (required)\n"); + stdout.printf (" -d, --description=DESC Task description\n"); + stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); + stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); + stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); + stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -h, --help Show this help message\n"); + } + } +} \ No newline at end of file diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala new file mode 100644 index 000000000..4dbd25e74 --- /dev/null +++ b/quick-add/cli/Main.vala @@ -0,0 +1,94 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +namespace PlanifyCLI { + private static int add_task (TaskArguments args) { + // Validate content + string? error_message; + if (!TaskValidator.validate_content (args.content, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + + // Validate priority + if (!TaskValidator.validate_priority (args.priority, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + + // Validate and parse due date + GLib.DateTime? due_datetime; + if (!TaskValidator.validate_and_parse_date (args.due_date, out due_datetime, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + + // Initialize database + Services.Database.get_default ().init_database (); + + // Find target project + Objects.Project? target_project = TaskCreator.find_project ( + args.project_id, + args.project_name, + out error_message + ); + + if (target_project == null) { + stderr.printf ("%s\n", error_message); + return 1; + } + + // Create item + var item = TaskCreator.create_item ( + args.content, + args.description, + args.priority, + due_datetime, + target_project + ); + + // Save and notify + TaskCreator.save_and_notify (item); + + // Output result + OutputFormatter.print_task_result (item, target_project); + + return 0; + } + + public static int main (string[] args) { + // Initialize localization + Intl.setlocale (LocaleCategory.ALL, ""); + string langpack_dir = Path.build_filename (Build.INSTALL_PREFIX, "share", "locale"); + Intl.bindtextdomain (Build.GETTEXT_PACKAGE, langpack_dir); + Intl.bind_textdomain_codeset (Build.GETTEXT_PACKAGE, "UTF-8"); + Intl.textdomain (Build.GETTEXT_PACKAGE); + + // Parse arguments + int exit_code; + TaskArguments? task_args = ArgumentParser.parse (args, out exit_code); + + if (task_args == null) { + return exit_code; + } + + // Execute add command + return add_task (task_args); + } +} \ No newline at end of file diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala new file mode 100644 index 000000000..85e93e51b --- /dev/null +++ b/quick-add/cli/OutputFormatter.vala @@ -0,0 +1,30 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +namespace PlanifyCLI { + public class OutputFormatter : Object { + public static void print_task_result (Objects.Item item, Objects.Project project) { + stdout.printf ("{\n"); + stdout.printf (" \"taskId\": \"%s\",\n", item.id); + stdout.printf (" \"projectId\": \"%s\",\n", project.id); + stdout.printf (" \"projectName\": \"%s\"\n", project.name); + stdout.printf ("}\n"); + } + } +} \ No newline at end of file diff --git a/quick-add/cli/TaskCreator.vala b/quick-add/cli/TaskCreator.vala new file mode 100644 index 000000000..172cc2015 --- /dev/null +++ b/quick-add/cli/TaskCreator.vala @@ -0,0 +1,119 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +namespace PlanifyCLI { + public class TaskCreator : Object { + public static Objects.Project? find_project (string? project_id, string? project_name, out string? error_message) { + error_message = null; + Objects.Project? target_project = null; + + // Prefer project ID over name + if (project_id != null && project_id.strip () != "") { + // Search by project ID + foreach (var project in Services.Store.instance ().projects) { + if (project.id == project_id.strip () && !project.is_archived && !project.is_deleted) { + target_project = project; + break; + } + } + + if (target_project == null) { + error_message = "Error: Project ID '%s' not found".printf (project_id); + return null; + } + } else if (project_name != null && project_name.strip () != "") { + // Search for project by name (case-insensitive) + string search_name = project_name.strip ().down (); + foreach (var project in Services.Store.instance ().projects) { + if (project.name.down () == search_name && !project.is_archived && !project.is_deleted) { + target_project = project; + break; + } + } + + if (target_project == null) { + error_message = "Error: Project '%s' not found".printf (project_name); + return null; + } + } else { + // Default to inbox + target_project = Services.Store.instance ().get_inbox_project (); + if (target_project == null) { + error_message = "Error: No inbox project found"; + return null; + } + } + + return target_project; + } + + public static Objects.Item create_item ( + string content, + string? description, + int priority, + GLib.DateTime? due_datetime, + Objects.Project project + ) { + var item = new Objects.Item (); + item.id = Util.get_default ().generate_id (item); + item.content = content.strip (); + item.project_id = project.id; + // Convert user-friendly priority (1=high, 4=none) to internal format (4=high, 1=none) + item.priority = 5 - priority; + item.added_at = new GLib.DateTime.now_local ().to_string (); + + // Set description if provided + if (description != null && description.strip () != "") { + item.description = description.strip (); + } + + // Set due date if provided + if (due_datetime != null) { + item.due.date = Utils.Datetime.get_todoist_datetime_format (due_datetime); + } + + return item; + } + + public static bool save_and_notify (Objects.Item item) { + // Insert item into database + Services.Store.instance ().insert_item (item); + + // Notify main app via DBus + bool dbus_notified = false; + try { + DBusClient.get_default ().interface.add_item (item.id); + dbus_notified = true; + } catch (Error e) { + // Not a critical error - main app might not be running + debug ("DBus notification failed: %s", e.message); + } + + // Ensure DBus message is flushed before exit + if (dbus_notified) { + var main_context = MainContext.default (); + while (main_context.pending ()) { + main_context.iteration (false); + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/quick-add/cli/TaskValidator.vala b/quick-add/cli/TaskValidator.vala new file mode 100644 index 000000000..6658b8f15 --- /dev/null +++ b/quick-add/cli/TaskValidator.vala @@ -0,0 +1,72 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +namespace PlanifyCLI { + public class TaskValidator : Object { + public static bool validate_content (string? content, out string? error_message) { + error_message = null; + + if (content == null || content.strip () == "") { + error_message = "Error: --content is required"; + return false; + } + + return true; + } + + public static bool validate_priority (int priority, out string? error_message) { + error_message = null; + + if (priority < 1 || priority > 4) { + error_message = "Error: --priority must be between 1 and 4"; + return false; + } + + return true; + } + + public static bool validate_and_parse_date (string? due_date, out GLib.DateTime? datetime, out string? error_message) { + datetime = null; + error_message = null; + + if (due_date == null || due_date.strip () == "") { + return true; // No date provided is valid + } + + // Parse YYYY-MM-DD format + string[] parts = due_date.strip ().split ("-"); + if (parts.length != 3) { + error_message = "Error: Invalid date format. Use YYYY-MM-DD"; + return false; + } + + int year = int.parse (parts[0]); + int month = int.parse (parts[1]); + int day = int.parse (parts[2]); + + if (year < 1900 || year > 3000 || month < 1 || month > 12 || day < 1 || day > 31) { + error_message = "Error: Invalid date values"; + return false; + } + + datetime = new GLib.DateTime.local (year, month, day, 0, 0, 0); + return true; + } + } +} \ No newline at end of file diff --git a/quick-add/meson.build b/quick-add/meson.build index f1f76bb23..3c37f7b05 100644 --- a/quick-add/meson.build +++ b/quick-add/meson.build @@ -11,4 +11,22 @@ executable( config_file, dependencies: deps, install: true, -) \ No newline at end of file +) + +# CLI executable +cli_sources = files( + 'Services/DBusClient.vala', + 'cli/ArgumentParser.vala', + 'cli/TaskValidator.vala', + 'cli/TaskCreator.vala', + 'cli/OutputFormatter.vala', + 'cli/Main.vala', +) + +executable( + 'io.github.alainm23.planify.cli', + cli_sources, + config_file, + dependencies: deps, + install: true, +) From d602d96f1456e1e8a8e35dcc5b1681e54523ecd2 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Sat, 7 Feb 2026 11:49:16 +0000 Subject: [PATCH 02/21] fix: update sidebar counter on task add from CLI --- core/Services/Store.vala | 4 ++++ src/MainWindow.vala | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/Services/Store.vala b/core/Services/Store.vala index 506643d06..8f796cbc9 100644 --- a/core/Services/Store.vala +++ b/core/Services/Store.vala @@ -654,6 +654,10 @@ public class Services.Store : GLib.Object { } #endif + public void clear_items_by_project_cache (string project_id) { + _items_by_project_cache.unset (project_id); + } + public void add_item (Objects.Item item, bool insert = true) { items.add (item); item_added (item, insert); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 66dacec06..58e26c982 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -195,9 +195,8 @@ public class MainWindow : Adw.ApplicationWindow { Objects.Item item = Services.Database.get_default ().get_item_by_id (id); Gee.ArrayList reminders = Services.Database.get_default ().get_reminders_by_item_id (id); - Services.Store.instance ().clear_project_cache (item.project_id); + Services.Store.instance ().clear_items_by_project_cache (item.project_id); Services.Store.instance ().add_item (item); - foreach (Objects.Reminder reminder in reminders) { item.add_reminder_events (reminder); } From 311d1d476043d8be80a800607e77eb7ebd51e5c8 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:05:54 +0000 Subject: [PATCH 03/21] feat(cli): add list-projects command --- quick-add/cli/ArgumentParser.vala | 46 ++++++++++++++++++++++-------- quick-add/cli/Main.vala | 31 ++++++++++++++++---- quick-add/cli/OutputFormatter.vala | 18 +++++++++++- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index 8a19753b0..84a09905a 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -18,6 +18,12 @@ */ namespace PlanifyCLI { + public enum CommandType { + NONE, + ADD, + LIST_PROJECTS + } + public class TaskArguments : Object { public string? content { get; set; default = null; } public string? description { get; set; default = null; } @@ -27,29 +33,34 @@ namespace PlanifyCLI { public string? due_date { get; set; default = null; } } + public class ParsedCommand : Object { + public CommandType command_type { get; set; default = CommandType.NONE; } + public TaskArguments? task_args { get; set; default = null; } + } + public class ArgumentParser : Object { - public static TaskArguments? parse (string[] args, out int exit_code) { + public static ParsedCommand? parse (string[] args, out int exit_code) { exit_code = 0; // Check for command if (args.length < 2) { stderr.printf ("Error: No command specified\n"); - stderr.printf ("Usage: %s add [OPTIONS]\n", args[0]); + stderr.printf ("Usage: %s [OPTIONS]\n", args[0]); + stderr.printf ("Commands: add, list-projects\n"); stderr.printf ("Run '%s --help' for more information\n", args[0]); exit_code = 1; return null; } string command = args[1]; - - if (command != "add") { - stderr.printf ("Error: Unknown command '%s'\n", command); - stderr.printf ("Available commands: add\n"); - exit_code = 1; - return null; - } + var parsed = new ParsedCommand (); - var task_args = new TaskArguments (); + if (command == "list-projects") { + parsed.command_type = CommandType.LIST_PROJECTS; + return parsed; + } else if (command == "add") { + parsed.command_type = CommandType.ADD; + var task_args = new TaskArguments (); // Parse options starting from index 2 for (int i = 2; i < args.length; i++) { @@ -115,11 +126,22 @@ namespace PlanifyCLI { } } - return task_args; + parsed.task_args = task_args; + return parsed; + } else { + stderr.printf ("Error: Unknown command '%s'\n", command); + stderr.printf ("Available commands: add, list-projects\n"); + exit_code = 1; + return null; + } } private static void print_help (string program_name) { - stdout.printf ("Usage: %s add [OPTIONS]\n\n", program_name); + stdout.printf ("Usage: %s [OPTIONS]\n\n", program_name); + stdout.printf ("Commands:\n"); + stdout.printf (" add Add a new task\n"); + stdout.printf (" list-projects List all projects (JSON output)\n\n"); + stdout.printf ("Add command options:\n"); stdout.printf ("Options:\n"); stdout.printf (" -c, --content=CONTENT Task content (required)\n"); stdout.printf (" -d, --description=DESC Task description\n"); diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 4dbd25e74..8f7d2827b 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -18,6 +18,19 @@ */ namespace PlanifyCLI { + private static int list_projects () { + // Initialize database + Services.Database.get_default ().init_database (); + + // Get all projects from store + var projects = Services.Store.instance ().projects; + + // Output as JSON + OutputFormatter.print_projects_list (projects); + + return 0; + } + private static int add_task (TaskArguments args) { // Validate content string? error_message; @@ -82,13 +95,21 @@ namespace PlanifyCLI { // Parse arguments int exit_code; - TaskArguments? task_args = ArgumentParser.parse (args, out exit_code); + ParsedCommand? parsed = ArgumentParser.parse (args, out exit_code); - if (task_args == null) { + if (parsed == null) { return exit_code; } - // Execute add command - return add_task (task_args); + // Route to appropriate command handler + switch (parsed.command_type) { + case CommandType.LIST_PROJECTS: + return list_projects (); + case CommandType.ADD: + return add_task (parsed.task_args); + default: + stderr.printf ("Error: Unknown command\n"); + return 1; + } } -} \ No newline at end of file +} diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index 85e93e51b..f47d2b42b 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -26,5 +26,21 @@ namespace PlanifyCLI { stdout.printf (" \"projectName\": \"%s\"\n", project.name); stdout.printf ("}\n"); } + + public static void print_projects_list (Gee.ArrayList projects) { + var builder = new Json.Builder (); + builder.begin_array (); + + foreach (var project in projects) { + builder.add_value (Json.gobject_serialize (project)); + } + + builder.end_array (); + + var generator = new Json.Generator (); + generator.set_root (builder.get_root ()); + generator.pretty = true; + stdout.printf ("%s\n", generator.to_data (null)); + } } -} \ No newline at end of file +} From 9e06a8861448821ae0ed6c3939ca606163478b4e Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:09:06 +0000 Subject: [PATCH 04/21] refactor(cli): use JSON serialisation library for task output --- quick-add/cli/OutputFormatter.vala | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index f47d2b42b..bb5b9b9a3 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -20,11 +20,18 @@ namespace PlanifyCLI { public class OutputFormatter : Object { public static void print_task_result (Objects.Item item, Objects.Project project) { - stdout.printf ("{\n"); - stdout.printf (" \"taskId\": \"%s\",\n", item.id); - stdout.printf (" \"projectId\": \"%s\",\n", project.id); - stdout.printf (" \"projectName\": \"%s\"\n", project.name); - stdout.printf ("}\n"); + var builder = new Json.Builder (); + builder.begin_object (); + builder.set_member_name ("task"); + builder.add_value (Json.gobject_serialize (item)); + builder.set_member_name ("project"); + builder.add_value (Json.gobject_serialize (project)); + builder.end_object (); + + var generator = new Json.Generator (); + generator.set_root (builder.get_root ()); + generator.pretty = true; + stdout.printf ("%s\n", generator.to_data (null)); } public static void print_projects_list (Gee.ArrayList projects) { From d87af9d2c778a4cba75bb0cce637175cf3b82954 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:17:13 +0000 Subject: [PATCH 05/21] feat(cli): add support for creating subtasks with parent-id option --- quick-add/cli/ArgumentParser.vala | 10 ++++++++++ quick-add/cli/Main.vala | 9 ++++++++- quick-add/cli/TaskCreator.vala | 8 +++++++- quick-add/cli/TaskValidator.vala | 17 +++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index 84a09905a..de574fff6 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -29,6 +29,7 @@ namespace PlanifyCLI { public string? description { get; set; default = null; } public string? project_name { get; set; default = null; } public string? project_id { get; set; default = null; } + public string? parent_id { get; set; default = null; } public int priority { get; set; default = 4; } public string? due_date { get; set; default = null; } } @@ -98,6 +99,14 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "-a" || arg == "--parent-id") { + if (i + 1 < args.length) { + task_args.parent_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } } else if (arg == "-P" || arg == "--priority") { if (i + 1 < args.length) { task_args.priority = int.parse (args[++i]); @@ -147,6 +156,7 @@ namespace PlanifyCLI { stdout.printf (" -d, --description=DESC Task description\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); + stdout.printf (" -a, --parent-id=ID Parent task ID (creates a subtask)\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -h, --help Show this help message\n"); diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 8f7d2827b..9efc2ce01 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -55,6 +55,12 @@ namespace PlanifyCLI { // Initialize database Services.Database.get_default ().init_database (); + // Validate parent_id if provided + if (!TaskValidator.validate_parent_id (args.parent_id, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + // Find target project Objects.Project? target_project = TaskCreator.find_project ( args.project_id, @@ -73,7 +79,8 @@ namespace PlanifyCLI { args.description, args.priority, due_datetime, - target_project + target_project, + args.parent_id ); // Save and notify diff --git a/quick-add/cli/TaskCreator.vala b/quick-add/cli/TaskCreator.vala index 172cc2015..68ce238ce 100644 --- a/quick-add/cli/TaskCreator.vala +++ b/quick-add/cli/TaskCreator.vala @@ -68,7 +68,8 @@ namespace PlanifyCLI { string? description, int priority, GLib.DateTime? due_datetime, - Objects.Project project + Objects.Project project, + string? parent_id ) { var item = new Objects.Item (); item.id = Util.get_default ().generate_id (item); @@ -78,6 +79,11 @@ namespace PlanifyCLI { item.priority = 5 - priority; item.added_at = new GLib.DateTime.now_local ().to_string (); + // Set parent_id if provided + if (parent_id != null && parent_id.strip () != "") { + item.parent_id = parent_id.strip (); + } + // Set description if provided if (description != null && description.strip () != "") { item.description = description.strip (); diff --git a/quick-add/cli/TaskValidator.vala b/quick-add/cli/TaskValidator.vala index 6658b8f15..de55ebf6c 100644 --- a/quick-add/cli/TaskValidator.vala +++ b/quick-add/cli/TaskValidator.vala @@ -68,5 +68,22 @@ namespace PlanifyCLI { datetime = new GLib.DateTime.local (year, month, day, 0, 0, 0); return true; } + + public static bool validate_parent_id (string? parent_id, out string? error_message) { + error_message = null; + + if (parent_id == null || parent_id.strip () == "") { + return true; // No parent_id provided is valid + } + + // Check if parent task exists + Objects.Item? parent_item = Services.Store.instance ().get_item (parent_id.strip ()); + if (parent_item == null) { + error_message = "Error: Parent task ID '%s' not found".printf (parent_id); + return false; + } + + return true; + } } } \ No newline at end of file From 56c08eb556e53d808c1e6cb7ab230779fd8484ec Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:29:25 +0000 Subject: [PATCH 06/21] refactor(cli): simplify JSON output by controlling serialized fields --- quick-add/cli/OutputFormatter.vala | 50 ++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index bb5b9b9a3..c44156de2 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -19,13 +19,46 @@ namespace PlanifyCLI { public class OutputFormatter : Object { + private delegate void FieldValueAdder (Json.Builder builder); + + private struct FieldDef { + string name; + FieldValueAdder add_value; + } + + // Define fields to include in output + private static FieldDef[] get_project_fields (Objects.Project project) { + return { + { "id", (builder) => builder.add_string_value (project.id) }, + { "name", (builder) => builder.add_string_value (project.name) }, + { "item-count", (builder) => builder.add_int_value (project.item_count) }, + { "description", (builder) => builder.add_string_value (project.description) } + }; + } + + private static FieldDef[] get_item_fields (Objects.Item item) { + return { + { "id", (builder) => builder.add_string_value (item.id) }, + { "content", (builder) => builder.add_string_value (item.content) }, + { "description", (builder) => builder.add_string_value (item.description) }, + { "project-id", (builder) => builder.add_string_value (item.project_id) }, + { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, + { "added-at", (builder) => builder.add_string_value (item.added_at) }, + { "completed-at", (builder) => builder.add_string_value (item.completed_at) }, + { "updated-at", (builder) => builder.add_string_value (item.updated_at) } + }; + } + public static void print_task_result (Objects.Item item, Objects.Project project) { var builder = new Json.Builder (); builder.begin_object (); + builder.set_member_name ("task"); - builder.add_value (Json.gobject_serialize (item)); + add_object (builder, get_item_fields (item)); + builder.set_member_name ("project"); - builder.add_value (Json.gobject_serialize (project)); + add_object (builder, get_project_fields (project)); + builder.end_object (); var generator = new Json.Generator (); @@ -39,7 +72,7 @@ namespace PlanifyCLI { builder.begin_array (); foreach (var project in projects) { - builder.add_value (Json.gobject_serialize (project)); + add_object (builder, get_project_fields (project)); } builder.end_array (); @@ -49,5 +82,16 @@ namespace PlanifyCLI { generator.pretty = true; stdout.printf ("%s\n", generator.to_data (null)); } + + private static void add_object (Json.Builder builder, FieldDef[] fields) { + builder.begin_object (); + + foreach (var field in fields) { + builder.set_member_name (field.name); + field.add_value (builder); + } + + builder.end_object (); + } } } From 96a114c99064aa895d793e159a44f022d6be53ae Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:35:56 +0000 Subject: [PATCH 07/21] feat(cli): add list command to display tasks from a project --- quick-add/cli/ArgumentParser.vala | 66 ++++++++++++++++++++++++++++-- quick-add/cli/Main.vala | 28 +++++++++++++ quick-add/cli/OutputFormatter.vala | 16 ++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index de574fff6..ebb4fb6fa 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -21,7 +21,8 @@ namespace PlanifyCLI { public enum CommandType { NONE, ADD, - LIST_PROJECTS + LIST_PROJECTS, + LIST } public class TaskArguments : Object { @@ -34,9 +35,15 @@ namespace PlanifyCLI { public string? due_date { get; set; default = null; } } + public class ListArguments : Object { + public string? project_name { get; set; default = null; } + public string? project_id { get; set; default = null; } + } + public class ParsedCommand : Object { public CommandType command_type { get; set; default = CommandType.NONE; } public TaskArguments? task_args { get; set; default = null; } + public ListArguments? list_args { get; set; default = null; } } public class ArgumentParser : Object { @@ -47,7 +54,7 @@ namespace PlanifyCLI { if (args.length < 2) { stderr.printf ("Error: No command specified\n"); stderr.printf ("Usage: %s [OPTIONS]\n", args[0]); - stderr.printf ("Commands: add, list-projects\n"); + stderr.printf ("Commands: add, list, list-projects\n"); stderr.printf ("Run '%s --help' for more information\n", args[0]); exit_code = 1; return null; @@ -59,6 +66,44 @@ namespace PlanifyCLI { if (command == "list-projects") { parsed.command_type = CommandType.LIST_PROJECTS; return parsed; + } else if (command == "list") { + parsed.command_type = CommandType.LIST; + var list_args = new ListArguments (); + + // Parse options starting from index 2 + for (int i = 2; i < args.length; i++) { + string arg = args[i]; + + if (arg == "-p" || arg == "--project") { + if (i + 1 < args.length) { + list_args.project_name = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-i" || arg == "--project-id") { + if (i + 1 < args.length) { + list_args.project_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-h" || arg == "--help") { + print_list_help (args[0]); + exit_code = 0; + return null; + } else { + stderr.printf ("Error: Unknown option '%s'\n", arg); + stderr.printf ("Run '%s list --help' for usage information\n", args[0]); + exit_code = 1; + return null; + } + } + + parsed.list_args = list_args; + return parsed; } else if (command == "add") { parsed.command_type = CommandType.ADD; var task_args = new TaskArguments (); @@ -139,7 +184,7 @@ namespace PlanifyCLI { return parsed; } else { stderr.printf ("Error: Unknown command '%s'\n", command); - stderr.printf ("Available commands: add, list-projects\n"); + stderr.printf ("Available commands: add, list, list-projects\n"); exit_code = 1; return null; } @@ -149,9 +194,9 @@ namespace PlanifyCLI { stdout.printf ("Usage: %s [OPTIONS]\n\n", program_name); stdout.printf ("Commands:\n"); stdout.printf (" add Add a new task\n"); + stdout.printf (" list List tasks from a project (JSON output)\n"); stdout.printf (" list-projects List all projects (JSON output)\n\n"); stdout.printf ("Add command options:\n"); - stdout.printf ("Options:\n"); stdout.printf (" -c, --content=CONTENT Task content (required)\n"); stdout.printf (" -d, --description=DESC Task description\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); @@ -159,6 +204,19 @@ namespace PlanifyCLI { stdout.printf (" -a, --parent-id=ID Parent task ID (creates a subtask)\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -h, --help Show this help message\n\n"); + stdout.printf ("List command options:\n"); + stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); + stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); + stdout.printf (" -h, --help Show this help message\n"); + } + + private static void print_list_help (string program_name) { + stdout.printf ("Usage: %s list [OPTIONS]\n\n", program_name); + stdout.printf ("List tasks from a project (JSON output)\n\n"); + stdout.printf ("Options:\n"); + stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); + stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); stdout.printf (" -h, --help Show this help message\n"); } } diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 9efc2ce01..c546c403f 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -31,6 +31,32 @@ namespace PlanifyCLI { return 0; } + private static int list_tasks (ListArguments args) { + // Initialize database + Services.Database.get_default ().init_database (); + + // Find target project + string? error_message; + Objects.Project? target_project = TaskCreator.find_project ( + args.project_id, + args.project_name, + out error_message + ); + + if (target_project == null) { + stderr.printf ("%s\n", error_message); + return 1; + } + + // Get all items for the project + var items = Services.Store.instance ().get_items_by_project (target_project); + + // Output result + OutputFormatter.print_tasks_list (items); + + return 0; + } + private static int add_task (TaskArguments args) { // Validate content string? error_message; @@ -112,6 +138,8 @@ namespace PlanifyCLI { switch (parsed.command_type) { case CommandType.LIST_PROJECTS: return list_projects (); + case CommandType.LIST: + return list_tasks (parsed.list_args); case CommandType.ADD: return add_task (parsed.task_args); default: diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index c44156de2..fba701a5d 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -83,6 +83,22 @@ namespace PlanifyCLI { stdout.printf ("%s\n", generator.to_data (null)); } + public static void print_tasks_list (Gee.ArrayList items) { + var builder = new Json.Builder (); + builder.begin_array (); + + foreach (var item in items) { + add_object (builder, get_item_fields (item)); + } + + builder.end_array (); + + var generator = new Json.Generator (); + generator.set_root (builder.get_root ()); + generator.pretty = true; + stdout.printf ("%s\n", generator.to_data (null)); + } + private static void add_object (Json.Builder builder, FieldDef[] fields) { builder.begin_object (); From 09286f076446d3123abb212165001137994fac19 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:43:28 +0000 Subject: [PATCH 08/21] feat(cli): add update command for modifying existing tasks --- quick-add/cli/ArgumentParser.vala | 131 +++++++++++++++++++++++++++++- quick-add/cli/Main.vala | 106 ++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 3 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index ebb4fb6fa..8bc653ce5 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -22,7 +22,8 @@ namespace PlanifyCLI { NONE, ADD, LIST_PROJECTS, - LIST + LIST, + UPDATE } public class TaskArguments : Object { @@ -40,10 +41,22 @@ namespace PlanifyCLI { public string? project_id { get; set; default = null; } } + public class UpdateArguments : Object { + public string? task_id { get; set; default = null; } + public string? content { get; set; default = null; } + public string? description { get; set; default = null; } + public string? project_name { get; set; default = null; } + public string? project_id { get; set; default = null; } + public string? parent_id { get; set; default = null; } + public int priority { get; set; default = -1; } + public string? due_date { get; set; default = null; } + } + public class ParsedCommand : Object { public CommandType command_type { get; set; default = CommandType.NONE; } public TaskArguments? task_args { get; set; default = null; } public ListArguments? list_args { get; set; default = null; } + public UpdateArguments? update_args { get; set; default = null; } } public class ArgumentParser : Object { @@ -54,7 +67,7 @@ namespace PlanifyCLI { if (args.length < 2) { stderr.printf ("Error: No command specified\n"); stderr.printf ("Usage: %s [OPTIONS]\n", args[0]); - stderr.printf ("Commands: add, list, list-projects\n"); + stderr.printf ("Commands: add, list, update, list-projects\n"); stderr.printf ("Run '%s --help' for more information\n", args[0]); exit_code = 1; return null; @@ -104,6 +117,92 @@ namespace PlanifyCLI { parsed.list_args = list_args; return parsed; + } else if (command == "update") { + parsed.command_type = CommandType.UPDATE; + var update_args = new UpdateArguments (); + + // Parse options starting from index 2 + for (int i = 2; i < args.length; i++) { + string arg = args[i]; + + if (arg == "-t" || arg == "--task-id") { + if (i + 1 < args.length) { + update_args.task_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-c" || arg == "--content") { + if (i + 1 < args.length) { + update_args.content = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-d" || arg == "--description") { + if (i + 1 < args.length) { + update_args.description = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-p" || arg == "--project") { + if (i + 1 < args.length) { + update_args.project_name = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-i" || arg == "--project-id") { + if (i + 1 < args.length) { + update_args.project_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-a" || arg == "--parent-id") { + if (i + 1 < args.length) { + update_args.parent_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-P" || arg == "--priority") { + if (i + 1 < args.length) { + update_args.priority = int.parse (args[++i]); + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-D" || arg == "--due") { + if (i + 1 < args.length) { + update_args.due_date = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-h" || arg == "--help") { + print_update_help (args[0]); + exit_code = 0; + return null; + } else { + stderr.printf ("Error: Unknown option '%s'\n", arg); + stderr.printf ("Run '%s update --help' for usage information\n", args[0]); + exit_code = 1; + return null; + } + } + + parsed.update_args = update_args; + return parsed; } else if (command == "add") { parsed.command_type = CommandType.ADD; var task_args = new TaskArguments (); @@ -184,7 +283,7 @@ namespace PlanifyCLI { return parsed; } else { stderr.printf ("Error: Unknown command '%s'\n", command); - stderr.printf ("Available commands: add, list, list-projects\n"); + stderr.printf ("Available commands: add, list, update, list-projects\n"); exit_code = 1; return null; } @@ -195,6 +294,7 @@ namespace PlanifyCLI { stdout.printf ("Commands:\n"); stdout.printf (" add Add a new task\n"); stdout.printf (" list List tasks from a project (JSON output)\n"); + stdout.printf (" update Update an existing task\n"); stdout.printf (" list-projects List all projects (JSON output)\n\n"); stdout.printf ("Add command options:\n"); stdout.printf (" -c, --content=CONTENT Task content (required)\n"); @@ -208,6 +308,16 @@ namespace PlanifyCLI { stdout.printf ("List command options:\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); + stdout.printf (" -h, --help Show this help message\n\n"); + stdout.printf ("Update command options:\n"); + stdout.printf (" -t, --task-id=ID Task ID to update (required)\n"); + stdout.printf (" -c, --content=CONTENT New task content\n"); + stdout.printf (" -d, --description=DESC New task description\n"); + stdout.printf (" -p, --project=PROJECT Move to project by name\n"); + stdout.printf (" -i, --project-id=ID Move to project by ID (preferred over name)\n"); + stdout.printf (" -a, --parent-id=ID New parent task ID\n"); + stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); + stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -h, --help Show this help message\n"); } @@ -219,5 +329,20 @@ namespace PlanifyCLI { stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); stdout.printf (" -h, --help Show this help message\n"); } + + private static void print_update_help (string program_name) { + stdout.printf ("Usage: %s update [OPTIONS]\n\n", program_name); + stdout.printf ("Update an existing task. Only provided fields will be changed.\n\n"); + stdout.printf ("Options:\n"); + stdout.printf (" -t, --task-id=ID Task ID to update (required)\n"); + stdout.printf (" -c, --content=CONTENT New task content\n"); + stdout.printf (" -d, --description=DESC New task description\n"); + stdout.printf (" -p, --project=PROJECT Move to project by name\n"); + stdout.printf (" -i, --project-id=ID Move to project by ID (preferred over name)\n"); + stdout.printf (" -a, --parent-id=ID New parent task ID\n"); + stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); + stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -h, --help Show this help message\n"); + } } } \ No newline at end of file diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index c546c403f..3e8272b52 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -57,6 +57,110 @@ namespace PlanifyCLI { return 0; } + private static int update_task (UpdateArguments args) { + // Validate task_id + if (args.task_id == null || args.task_id.strip () == "") { + stderr.printf ("Error: --task-id is required\n"); + return 1; + } + + // Initialize database + Services.Database.get_default ().init_database (); + + // Get existing item + Objects.Item? item = Services.Store.instance ().get_item (args.task_id.strip ()); + if (item == null) { + stderr.printf ("Error: Task ID '%s' not found\n", args.task_id); + return 1; + } + + string? error_message; + + // Update content if provided + if (args.content != null && args.content.strip () != "") { + item.content = args.content.strip (); + } + + // Update description if provided + if (args.description != null) { + item.description = args.description.strip (); + } + + // Update priority if provided + if (args.priority != -1) { + if (!TaskValidator.validate_priority (args.priority, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + // Convert user-friendly priority (1=high, 4=none) to internal format (4=high, 1=none) + item.priority = 5 - args.priority; + } + + // Update due date if provided + if (args.due_date != null) { + GLib.DateTime? due_datetime; + if (!TaskValidator.validate_and_parse_date (args.due_date, out due_datetime, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + if (due_datetime != null) { + item.due.date = Utils.Datetime.get_todoist_datetime_format (due_datetime); + } + } + + // Handle project change if provided + bool project_changed = false; + string old_project_id = item.project_id; + string old_section_id = item.section_id; + string old_parent_id = item.parent_id; + + if (args.project_id != null || args.project_name != null) { + Objects.Project? target_project = TaskCreator.find_project ( + args.project_id, + args.project_name, + out error_message + ); + + if (target_project == null) { + stderr.printf ("%s\n", error_message); + return 1; + } + + if (item.project_id != target_project.id) { + item.project_id = target_project.id; + item.section_id = ""; // Reset section when moving to new project + project_changed = true; + } + } + + // Update parent_id if provided + if (args.parent_id != null) { + if (!TaskValidator.validate_parent_id (args.parent_id, out error_message)) { + stderr.printf ("%s\n", error_message); + return 1; + } + item.parent_id = args.parent_id.strip (); + } + + // Save changes + if (project_changed) { + Services.Store.instance ().move_item (item, old_project_id, old_section_id, old_parent_id); + } else { + Services.Store.instance ().update_item (item); + } + + // Get the current project for output + Objects.Project? current_project = Services.Store.instance ().get_project (item.project_id); + if (current_project == null) { + current_project = Services.Store.instance ().get_inbox_project (); + } + + // Output result + OutputFormatter.print_task_result (item, current_project); + + return 0; + } + private static int add_task (TaskArguments args) { // Validate content string? error_message; @@ -140,6 +244,8 @@ namespace PlanifyCLI { return list_projects (); case CommandType.LIST: return list_tasks (parsed.list_args); + case CommandType.UPDATE: + return update_task (parsed.update_args); case CommandType.ADD: return add_task (parsed.task_args); default: From b9b19a1db810b9d30a257980c24524b65c1afdb9 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Wed, 18 Feb 2026 23:56:49 +0000 Subject: [PATCH 09/21] Revert "fix: update sidebar counter on task add from CLI" This reverts commit d602d96f1456e1e8a8e35dcc5b1681e54523ecd2. Fix was also implemented upstream in main. --- core/Services/Store.vala | 4 ---- src/MainWindow.vala | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/Services/Store.vala b/core/Services/Store.vala index 8f796cbc9..506643d06 100644 --- a/core/Services/Store.vala +++ b/core/Services/Store.vala @@ -654,10 +654,6 @@ public class Services.Store : GLib.Object { } #endif - public void clear_items_by_project_cache (string project_id) { - _items_by_project_cache.unset (project_id); - } - public void add_item (Objects.Item item, bool insert = true) { items.add (item); item_added (item, insert); diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 58e26c982..66dacec06 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -195,8 +195,9 @@ public class MainWindow : Adw.ApplicationWindow { Objects.Item item = Services.Database.get_default ().get_item_by_id (id); Gee.ArrayList reminders = Services.Database.get_default ().get_reminders_by_item_id (id); - Services.Store.instance ().clear_items_by_project_cache (item.project_id); + Services.Store.instance ().clear_project_cache (item.project_id); Services.Store.instance ().add_item (item); + foreach (Objects.Reminder reminder in reminders) { item.add_reminder_events (reminder); } From 5fe2d04fa38692d2033e11ae46ee4e2af6f75b1e Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:04:52 +0000 Subject: [PATCH 10/21] feat(cli): add label support for task creation and updates --- quick-add/cli/ArgumentParser.vala | 23 +++++++++++++++- quick-add/cli/Main.vala | 44 ++++++++++++++++++++++++++++++ quick-add/cli/OutputFormatter.vala | 9 +++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index 8bc653ce5..78fc5918b 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -34,6 +34,7 @@ namespace PlanifyCLI { public string? parent_id { get; set; default = null; } public int priority { get; set; default = 4; } public string? due_date { get; set; default = null; } + public string? labels { get; set; default = null; } } public class ListArguments : Object { @@ -50,6 +51,7 @@ namespace PlanifyCLI { public string? parent_id { get; set; default = null; } public int priority { get; set; default = -1; } public string? due_date { get; set; default = null; } + public string? labels { get; set; default = null; } } public class ParsedCommand : Object { @@ -189,6 +191,14 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "-l" || arg == "--labels") { + if (i + 1 < args.length) { + update_args.labels = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } } else if (arg == "-h" || arg == "--help") { print_update_help (args[0]); exit_code = 0; @@ -267,6 +277,14 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "-l" || arg == "--labels") { + if (i + 1 < args.length) { + task_args.labels = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } } else if (arg == "-h" || arg == "--help") { print_help (args[0]); exit_code = 0; @@ -304,6 +322,7 @@ namespace PlanifyCLI { stdout.printf (" -a, --parent-id=ID Parent task ID (creates a subtask)\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" -h, --help Show this help message\n\n"); stdout.printf ("List command options:\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); @@ -318,6 +337,7 @@ namespace PlanifyCLI { stdout.printf (" -a, --parent-id=ID New parent task ID\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" -h, --help Show this help message\n"); } @@ -342,7 +362,8 @@ namespace PlanifyCLI { stdout.printf (" -a, --parent-id=ID New parent task ID\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); + stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" -h, --help Show this help message\n"); } } -} \ No newline at end of file +} diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 3e8272b52..5a44d6a84 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -142,6 +142,29 @@ namespace PlanifyCLI { item.parent_id = args.parent_id.strip (); } + // Update labels if provided + if (args.labels != null) { + var new_labels = new Gee.HashMap (); + string[] label_names = args.labels.split (","); + foreach (string label_name in label_names) { + string trimmed = label_name.strip (); + if (trimmed != "") { + Objects.Label? label = Services.Store.instance ().get_label_by_name (trimmed, true, item.project.source_id); + if (label == null) { + // Create new label if it doesn't exist + label = new Objects.Label (); + label.id = Util.get_default ().generate_id (label); + label.name = trimmed; + label.color = Util.get_default ().get_random_color (); + label.source_id = item.project.source_id; + Services.Store.instance ().insert_label (label); + } + new_labels[label.id] = label; + } + } + item.check_labels (new_labels); + } + // Save changes if (project_changed) { Services.Store.instance ().move_item (item, old_project_id, old_section_id, old_parent_id); @@ -213,6 +236,27 @@ namespace PlanifyCLI { args.parent_id ); + // Add labels if provided + if (args.labels != null && args.labels.strip () != "") { + string[] label_names = args.labels.split (","); + foreach (string label_name in label_names) { + string trimmed = label_name.strip (); + if (trimmed != "") { + Objects.Label? label = Services.Store.instance ().get_label_by_name (trimmed, true, target_project.source_id); + if (label == null) { + // Create new label if it doesn't exist + label = new Objects.Label (); + label.id = Util.get_default ().generate_id (label); + label.name = trimmed; + label.color = Util.get_default ().get_random_color (); + label.source_id = target_project.source_id; + Services.Store.instance ().insert_label (label); + } + item.labels.add (label); + } + } + } + // Save and notify TaskCreator.save_and_notify (item); diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index fba701a5d..443892af0 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -45,7 +45,14 @@ namespace PlanifyCLI { { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, { "added-at", (builder) => builder.add_string_value (item.added_at) }, { "completed-at", (builder) => builder.add_string_value (item.completed_at) }, - { "updated-at", (builder) => builder.add_string_value (item.updated_at) } + { "updated-at", (builder) => builder.add_string_value (item.updated_at) }, + { "labels", (builder) => { + builder.begin_array (); + foreach (var label in item.labels) { + builder.add_string_value (label.name); + } + builder.end_array (); + }} }; } From 2c16322efddf55357ad21aab2a6d676e6bc9f82c Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:08:34 +0000 Subject: [PATCH 11/21] feat(cli): add task completion status flags to update command --- quick-add/cli/ArgumentParser.vala | 7 +++++++ quick-add/cli/Main.vala | 26 ++++++++++++++++++++++++++ quick-add/cli/OutputFormatter.vala | 1 + 3 files changed, 34 insertions(+) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index 78fc5918b..f31009258 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -52,6 +52,7 @@ namespace PlanifyCLI { public int priority { get; set; default = -1; } public string? due_date { get; set; default = null; } public string? labels { get; set; default = null; } + public int checked { get; set; default = -1; } // -1 = not set, 0 = uncomplete, 1 = complete } public class ParsedCommand : Object { @@ -199,6 +200,10 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "--complete") { + update_args.checked = 1; + } else if (arg == "--uncomplete") { + update_args.checked = 0; } else if (arg == "-h" || arg == "--help") { print_update_help (args[0]); exit_code = 0; @@ -363,6 +368,8 @@ namespace PlanifyCLI { stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); + stdout.printf (" --complete Mark task as complete\n"); + stdout.printf (" --uncomplete Mark task as incomplete\n"); stdout.printf (" -h, --help Show this help message\n"); } } diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 5a44d6a84..0075407dd 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -165,6 +165,32 @@ namespace PlanifyCLI { item.check_labels (new_labels); } + // Handle completion status change if provided + if (args.checked != -1) { + bool old_checked = item.checked; + bool new_checked = args.checked == 1; + + if (old_checked != new_checked) { + item.checked = new_checked; + if (new_checked) { + item.completed_at = new GLib.DateTime.now_local ().to_string (); + } else { + item.completed_at = ""; + } + + // Use async completion handler + var loop = new MainLoop (); + item.complete_item.begin (old_checked, (obj, res) => { + var response = item.complete_item.end (res); + if (!response.status) { + stderr.printf ("Error: Failed to update task completion status\n"); + } + loop.quit (); + }); + loop.run (); + } + } + // Save changes if (project_changed) { Services.Store.instance ().move_item (item, old_project_id, old_section_id, old_parent_id); diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index 443892af0..fc5047a6d 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -41,6 +41,7 @@ namespace PlanifyCLI { { "id", (builder) => builder.add_string_value (item.id) }, { "content", (builder) => builder.add_string_value (item.content) }, { "description", (builder) => builder.add_string_value (item.description) }, + { "checked", (builder) => builder.add_boolean_value (item.checked) }, { "project-id", (builder) => builder.add_string_value (item.project_id) }, { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, { "added-at", (builder) => builder.add_string_value (item.added_at) }, From de2af0163b27a00031f941f15fe85cc883aed195 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:13:02 +0000 Subject: [PATCH 12/21] fix: update UI on task update from CLI --- quick-add/cli/Main.vala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 0075407dd..912d9edca 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -198,6 +198,24 @@ namespace PlanifyCLI { Services.Store.instance ().update_item (item); } + // Notify main app via DBus + bool dbus_notified = false; + try { + DBusClient.get_default ().interface.update_item (item.id); + dbus_notified = true; + } catch (Error e) { + // Not a critical error - main app might not be running + debug ("DBus notification failed: %s", e.message); + } + + // Ensure DBus message is flushed before exit + if (dbus_notified) { + var main_context = MainContext.default (); + while (main_context.pending ()) { + main_context.iteration (false); + } + } + // Get the current project for output Objects.Project? current_project = Services.Store.instance ().get_project (item.project_id); if (current_project == null) { From 896191f13202e6866eb973f658165a97e2580e26 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:30:33 +0000 Subject: [PATCH 13/21] feat(cli): add task pinned arg to CLI --- quick-add/cli/ArgumentParser.vala | 38 ++++++++++++++++++++++++++++++ quick-add/cli/Main.vala | 10 ++++++++ quick-add/cli/OutputFormatter.vala | 1 + 3 files changed, 49 insertions(+) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index f31009258..e93987ea3 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -35,6 +35,7 @@ namespace PlanifyCLI { public int priority { get; set; default = 4; } public string? due_date { get; set; default = null; } public string? labels { get; set; default = null; } + public int pinned { get; set; default = -1; } // -1 = not set, 0 = unpinned, 1 = pinned } public class ListArguments : Object { @@ -53,6 +54,7 @@ namespace PlanifyCLI { public string? due_date { get; set; default = null; } public string? labels { get; set; default = null; } public int checked { get; set; default = -1; } // -1 = not set, 0 = uncomplete, 1 = complete + public int pinned { get; set; default = -1; } // -1 = not set, 0 = unpinned, 1 = pinned } public class ParsedCommand : Object { @@ -204,6 +206,23 @@ namespace PlanifyCLI { update_args.checked = 1; } else if (arg == "--uncomplete") { update_args.checked = 0; + } else if (arg == "--pin") { + if (i + 1 < args.length) { + string value = args[++i].down (); + if (value == "true") { + update_args.pinned = 1; + } else if (value == "false") { + update_args.pinned = 0; + } else { + stderr.printf ("Error: --pin requires 'true' or 'false'\n"); + exit_code = 1; + return null; + } + } else { + stderr.printf ("Error: --pin requires an argument (true or false)\n"); + exit_code = 1; + return null; + } } else if (arg == "-h" || arg == "--help") { print_update_help (args[0]); exit_code = 0; @@ -290,6 +309,23 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "--pin") { + if (i + 1 < args.length) { + string value = args[++i].down (); + if (value == "true") { + task_args.pinned = 1; + } else if (value == "false") { + task_args.pinned = 0; + } else { + stderr.printf ("Error: --pin requires 'true' or 'false'\n"); + exit_code = 1; + return null; + } + } else { + stderr.printf ("Error: --pin requires an argument (true or false)\n"); + exit_code = 1; + return null; + } } else if (arg == "-h" || arg == "--help") { print_help (args[0]); exit_code = 0; @@ -328,6 +364,7 @@ namespace PlanifyCLI { stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); + stdout.printf (" --pin=true|false Pin or unpin the task\n"); stdout.printf (" -h, --help Show this help message\n\n"); stdout.printf ("List command options:\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); @@ -370,6 +407,7 @@ namespace PlanifyCLI { stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" --complete Mark task as complete\n"); stdout.printf (" --uncomplete Mark task as incomplete\n"); + stdout.printf (" --pin=true|false Pin or unpin the task\n"); stdout.printf (" -h, --help Show this help message\n"); } } diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 912d9edca..49f60bd93 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -191,6 +191,11 @@ namespace PlanifyCLI { } } + // Update pinned if provided + if (args.pinned != -1) { + item.pinned = args.pinned == 1; + } + // Save changes if (project_changed) { Services.Store.instance ().move_item (item, old_project_id, old_section_id, old_parent_id); @@ -301,6 +306,11 @@ namespace PlanifyCLI { } } + // Set pinned if provided + if (args.pinned != -1) { + item.pinned = args.pinned == 1; + } + // Save and notify TaskCreator.save_and_notify (item); diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index fc5047a6d..1814b2e91 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -42,6 +42,7 @@ namespace PlanifyCLI { { "content", (builder) => builder.add_string_value (item.content) }, { "description", (builder) => builder.add_string_value (item.description) }, { "checked", (builder) => builder.add_boolean_value (item.checked) }, + { "pinned", (builder) => builder.add_boolean_value (item.pinned) }, { "project-id", (builder) => builder.add_string_value (item.project_id) }, { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, { "added-at", (builder) => builder.add_string_value (item.added_at) }, From 89218f2d05b64c77943cedc5e02944cb742c083c Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:30:57 +0000 Subject: [PATCH 14/21] feat(cli): add task section arg to CLI --- quick-add/cli/ArgumentParser.vala | 40 ++++++++++++++++++++++++++++++ quick-add/cli/Main.vala | 39 +++++++++++++++++++++++++++++ quick-add/cli/OutputFormatter.vala | 1 + quick-add/cli/TaskCreator.vala | 37 +++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index e93987ea3..f2f302ab4 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -31,6 +31,8 @@ namespace PlanifyCLI { public string? description { get; set; default = null; } public string? project_name { get; set; default = null; } public string? project_id { get; set; default = null; } + public string? section_name { get; set; default = null; } + public string? section_id { get; set; default = null; } public string? parent_id { get; set; default = null; } public int priority { get; set; default = 4; } public string? due_date { get; set; default = null; } @@ -49,6 +51,8 @@ namespace PlanifyCLI { public string? description { get; set; default = null; } public string? project_name { get; set; default = null; } public string? project_id { get; set; default = null; } + public string? section_name { get; set; default = null; } + public string? section_id { get; set; default = null; } public string? parent_id { get; set; default = null; } public int priority { get; set; default = -1; } public string? due_date { get; set; default = null; } @@ -170,6 +174,22 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "-s" || arg == "--section") { + if (i + 1 < args.length) { + update_args.section_name = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-S" || arg == "--section-id") { + if (i + 1 < args.length) { + update_args.section_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } } else if (arg == "-a" || arg == "--parent-id") { if (i + 1 < args.length) { update_args.parent_id = args[++i]; @@ -277,6 +297,22 @@ namespace PlanifyCLI { exit_code = 1; return null; } + } else if (arg == "-s" || arg == "--section") { + if (i + 1 < args.length) { + task_args.section_name = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } + } else if (arg == "-S" || arg == "--section-id") { + if (i + 1 < args.length) { + task_args.section_id = args[++i]; + } else { + stderr.printf ("Error: %s requires an argument\n", arg); + exit_code = 1; + return null; + } } else if (arg == "-a" || arg == "--parent-id") { if (i + 1 < args.length) { task_args.parent_id = args[++i]; @@ -360,6 +396,8 @@ namespace PlanifyCLI { stdout.printf (" -d, --description=DESC Task description\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); + stdout.printf (" -s, --section=SECTION Section name\n"); + stdout.printf (" -S, --section-id=ID Section ID (preferred over name)\n"); stdout.printf (" -a, --parent-id=ID Parent task ID (creates a subtask)\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); @@ -376,6 +414,8 @@ namespace PlanifyCLI { stdout.printf (" -d, --description=DESC New task description\n"); stdout.printf (" -p, --project=PROJECT Move to project by name\n"); stdout.printf (" -i, --project-id=ID Move to project by ID (preferred over name)\n"); + stdout.printf (" -s, --section=SECTION Move to section by name\n"); + stdout.printf (" -S, --section-id=ID Move to section by ID (preferred over name)\n"); stdout.printf (" -a, --parent-id=ID New parent task ID\n"); stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); diff --git a/quick-add/cli/Main.vala b/quick-add/cli/Main.vala index 49f60bd93..391854a7b 100644 --- a/quick-add/cli/Main.vala +++ b/quick-add/cli/Main.vala @@ -133,6 +133,28 @@ namespace PlanifyCLI { } } + // Handle section change if provided + if (args.section_id != null || args.section_name != null) { + Objects.Project? current_project = Services.Store.instance ().get_project (item.project_id); + if (current_project != null) { + Objects.Section? target_section = TaskCreator.find_section ( + args.section_id, + args.section_name, + current_project, + out error_message + ); + + if (target_section == null) { + stderr.printf ("%s\n", error_message); + return 1; + } + + if (item.section_id != target_section.id) { + item.section_id = target_section.id; + } + } + } + // Update parent_id if provided if (args.parent_id != null) { if (!TaskValidator.validate_parent_id (args.parent_id, out error_message)) { @@ -285,6 +307,23 @@ namespace PlanifyCLI { args.parent_id ); + // Find and set section if provided + if (args.section_id != null || args.section_name != null) { + Objects.Section? target_section = TaskCreator.find_section ( + args.section_id, + args.section_name, + target_project, + out error_message + ); + + if (target_section == null) { + stderr.printf ("%s\n", error_message); + return 1; + } + + item.section_id = target_section.id; + } + // Add labels if provided if (args.labels != null && args.labels.strip () != "") { string[] label_names = args.labels.split (","); diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index 1814b2e91..55d829284 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -44,6 +44,7 @@ namespace PlanifyCLI { { "checked", (builder) => builder.add_boolean_value (item.checked) }, { "pinned", (builder) => builder.add_boolean_value (item.pinned) }, { "project-id", (builder) => builder.add_string_value (item.project_id) }, + { "section-id", (builder) => builder.add_string_value (item.section_id) }, { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, { "added-at", (builder) => builder.add_string_value (item.added_at) }, { "completed-at", (builder) => builder.add_string_value (item.completed_at) }, diff --git a/quick-add/cli/TaskCreator.vala b/quick-add/cli/TaskCreator.vala index 68ce238ce..f2a268785 100644 --- a/quick-add/cli/TaskCreator.vala +++ b/quick-add/cli/TaskCreator.vala @@ -19,6 +19,43 @@ namespace PlanifyCLI { public class TaskCreator : Object { + public static Objects.Section? find_section (string? section_id, string? section_name, Objects.Project project, out string? error_message) { + error_message = null; + Objects.Section? target_section = null; + + // Prefer section ID over name + if (section_id != null && section_id.strip () != "") { + // Search by section ID + foreach (var section in Services.Store.instance ().get_sections_by_project (project)) { + if (section.id == section_id.strip ()) { + target_section = section; + break; + } + } + + if (target_section == null) { + error_message = "Error: Section ID '%s' not found in project '%s'".printf (section_id, project.name); + return null; + } + } else if (section_name != null && section_name.strip () != "") { + // Search for section by name (case-insensitive) + string search_name = section_name.strip ().down (); + foreach (var section in Services.Store.instance ().get_sections_by_project (project)) { + if (section.name.down () == search_name) { + target_section = section; + break; + } + } + + if (target_section == null) { + error_message = "Error: Section '%s' not found in project '%s'".printf (section_name, project.name); + return null; + } + } + + return target_section; + } + public static Objects.Project? find_project (string? project_id, string? project_name, out string? error_message) { error_message = null; Objects.Project? target_project = null; From 60b18c6baffd8c54bbf855a403ed12cae64beba4 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 18:39:50 +0000 Subject: [PATCH 15/21] feat(cli): Output prirority for tasks --- quick-add/cli/OutputFormatter.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/quick-add/cli/OutputFormatter.vala b/quick-add/cli/OutputFormatter.vala index 55d829284..53065f28a 100644 --- a/quick-add/cli/OutputFormatter.vala +++ b/quick-add/cli/OutputFormatter.vala @@ -43,6 +43,7 @@ namespace PlanifyCLI { { "description", (builder) => builder.add_string_value (item.description) }, { "checked", (builder) => builder.add_boolean_value (item.checked) }, { "pinned", (builder) => builder.add_boolean_value (item.pinned) }, + { "priority", (builder) => builder.add_int_value (5 - item.priority) }, { "project-id", (builder) => builder.add_string_value (item.project_id) }, { "section-id", (builder) => builder.add_string_value (item.section_id) }, { "parent-id", (builder) => builder.add_string_value (item.parent_id) }, From 7a0ef7c7316f21dffa707531cad159ce74ecc490 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Thu, 19 Feb 2026 19:02:40 +0000 Subject: [PATCH 16/21] fix: add update_item to dbus --- quick-add/Services/DBusClient.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/quick-add/Services/DBusClient.vala b/quick-add/Services/DBusClient.vala index 216006c9e..6eaf70d4b 100644 --- a/quick-add/Services/DBusClient.vala +++ b/quick-add/Services/DBusClient.vala @@ -22,6 +22,7 @@ [DBus (name = "io.github.alainm23.planify")] public interface DBusClientInterface : Object { public abstract void add_item (string id) throws Error; + public abstract void update_item (string id) throws Error; } public class DBusClient : Object { From e3eb8ed9fa499a3b18d65aa38266f600d2dd21b4 Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Fri, 20 Feb 2026 09:19:04 +0000 Subject: [PATCH 17/21] test: add unit test suite for CLI --- test/cli/test-argument-parser.vala | 233 +++++++++++++++++++++++++ test/cli/test-priority-conversion.vala | 50 ++++++ test/cli/test-task-validator.vala | 149 ++++++++++++++++ test/meson.build | 19 ++ test/test-cli-runner.vala | 36 ++++ 5 files changed, 487 insertions(+) create mode 100644 test/cli/test-argument-parser.vala create mode 100644 test/cli/test-priority-conversion.vala create mode 100644 test/cli/test-task-validator.vala create mode 100644 test/test-cli-runner.vala diff --git a/test/cli/test-argument-parser.vala b/test/cli/test-argument-parser.vala new file mode 100644 index 000000000..5347fd58d --- /dev/null +++ b/test/cli/test-argument-parser.vala @@ -0,0 +1,233 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +/** + * ArgumentParser Tests + * + * Tests for command-line argument parsing including: + * - Command recognition (add, list, update, list-projects) + * - Option parsing (short and long forms) + * - Error handling (missing args, invalid values) + */ + +namespace PlanifyCLI.Tests.ArgumentParser { + void test_no_command () { + print ("Testing: No command provided\n"); + int exit_code; + string[] args = {"planify-cli"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns null with exit code 1\n---\n"); + } + + void test_unknown_command () { + print ("Testing: Unknown command\n"); + int exit_code; + string[] args = {"planify-cli", "invalid-command"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns null with exit code 1\n---\n"); + } + + void test_list_projects () { + print ("Testing: 'list-projects' command\n"); + int exit_code; + string[] args = {"planify-cli", "list-projects"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.LIST_PROJECTS); + print (" ✓ Parses list-projects command\n---\n"); + } + + void test_add_minimal () { + print ("Testing: 'add' command with minimal args\n"); + int exit_code; + string[] args = {"planify-cli", "add", "-c", "Test task"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.ADD); + assert (parsed.task_args != null); + assert (parsed.task_args.content == "Test task"); + print (" ✓ Parses add command with content\n---\n"); + } + + void test_add_full () { + print ("Testing: 'add' command with all options\n"); + int exit_code; + string[] args = { + "planify-cli", "add", + "-c", "Complete task", + "-d", "Task description", + "-p", "Work", + "-s", "In Progress", + "-P", "1", + "-D", "2024-12-31", + "-l", "urgent,important", + "--pin", "true" + }; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.ADD); + assert (parsed.task_args != null); + assert (parsed.task_args.content == "Complete task"); + assert (parsed.task_args.description == "Task description"); + assert (parsed.task_args.project_name == "Work"); + assert (parsed.task_args.section_name == "In Progress"); + assert (parsed.task_args.priority == 1); + assert (parsed.task_args.due_date == "2024-12-31"); + assert (parsed.task_args.labels == "urgent,important"); + assert (parsed.task_args.pinned == 1); + print (" ✓ Parses all add options correctly\n---\n"); + } + + void test_list_with_project () { + print ("Testing: 'list' command with project\n"); + int exit_code; + string[] args = {"planify-cli", "list", "-p", "Work"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.LIST); + assert (parsed.list_args != null); + assert (parsed.list_args.project_name == "Work"); + print (" ✓ Parses list command with project name\n---\n"); + } + + void test_list_with_project_id () { + print ("Testing: 'list' command with project ID\n"); + int exit_code; + string[] args = {"planify-cli", "list", "-i", "project-123"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.LIST); + assert (parsed.list_args != null); + assert (parsed.list_args.project_id == "project-123"); + print (" ✓ Parses list command with project ID\n---\n"); + } + + void test_update_minimal () { + print ("Testing: 'update' command with minimal args\n"); + int exit_code; + string[] args = {"planify-cli", "update", "-t", "task-456", "-c", "Updated content"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.command_type == PlanifyCLI.CommandType.UPDATE); + assert (parsed.update_args != null); + assert (parsed.update_args.task_id == "task-456"); + assert (parsed.update_args.content == "Updated content"); + print (" ✓ Parses update command with task ID and content\n---\n"); + } + + void test_update_completion () { + print ("Testing: 'update' command with completion flags\n"); + int exit_code; + + // Test --complete + string[] args_complete = {"planify-cli", "update", "-t", "task-123", "--complete"}; + var parsed = PlanifyCLI.ArgumentParser.parse (args_complete, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.update_args.checked == 1); + print (" ✓ Parses --complete flag\n"); + + // Test --uncomplete + string[] args_uncomplete = {"planify-cli", "update", "-t", "task-123", "--uncomplete"}; + parsed = PlanifyCLI.ArgumentParser.parse (args_uncomplete, out exit_code); + + assert (parsed != null); + assert (exit_code == 0); + assert (parsed.update_args.checked == 0); + print (" ✓ Parses --uncomplete flag\n---\n"); + } + + void test_missing_required_arg () { + print ("Testing: Missing required argument value\n"); + int exit_code; + string[] args = {"planify-cli", "add", "-c"}; // -c without value + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns error for missing argument value\n---\n"); + } + + void test_invalid_pin_value () { + print ("Testing: Invalid --pin value\n"); + int exit_code; + string[] args = {"planify-cli", "add", "-c", "Task", "--pin", "invalid"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns error for invalid pin value\n---\n"); + } + + void test_unknown_option () { + print ("Testing: Unknown option\n"); + int exit_code; + string[] args = {"planify-cli", "add", "-c", "Task", "--unknown-option"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns error for unknown option\n---\n"); + } + + public void register_tests () { + Test.add_func ("/cli/argument_parser/no_command", test_no_command); + Test.add_func ("/cli/argument_parser/unknown_command", test_unknown_command); + Test.add_func ("/cli/argument_parser/list_projects", test_list_projects); + Test.add_func ("/cli/argument_parser/add_minimal", test_add_minimal); + Test.add_func ("/cli/argument_parser/add_full", test_add_full); + Test.add_func ("/cli/argument_parser/list_with_project", test_list_with_project); + Test.add_func ("/cli/argument_parser/list_with_project_id", test_list_with_project_id); + Test.add_func ("/cli/argument_parser/update_minimal", test_update_minimal); + Test.add_func ("/cli/argument_parser/update_completion", test_update_completion); + Test.add_func ("/cli/argument_parser/missing_required_arg", test_missing_required_arg); + Test.add_func ("/cli/argument_parser/invalid_pin_value", test_invalid_pin_value); + Test.add_func ("/cli/argument_parser/unknown_option", test_unknown_option); + } +} \ No newline at end of file diff --git a/test/cli/test-priority-conversion.vala b/test/cli/test-priority-conversion.vala new file mode 100644 index 000000000..7396402cc --- /dev/null +++ b/test/cli/test-priority-conversion.vala @@ -0,0 +1,50 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +/** + * Priority Conversion Tests + * + * Tests the conversion between user-friendly priority values + * and internal priority representation. + */ + +namespace PlanifyCLI.Tests.PriorityConversion { + void test_priority_conversion () { + print ("Testing: Priority conversion (user-friendly to internal)\n"); + + // User-friendly: 1=high, 2=medium, 3=low, 4=none + // Internal: 4=high, 3=medium, 2=low, 1=none + // Conversion: internal = 5 - user_friendly + + assert (5 - 1 == 4); // high + assert (5 - 2 == 3); // medium + assert (5 - 3 == 2); // low + assert (5 - 4 == 1); // none + + print (" ✓ Priority conversion logic verified\n"); + print (" 1 (high) -> 4 (internal)\n"); + print (" 2 (medium) -> 3 (internal)\n"); + print (" 3 (low) -> 2 (internal)\n"); + print (" 4 (none) -> 1 (internal)\n---\n"); + } + + public void register_tests () { + Test.add_func ("/cli/priority_conversion", test_priority_conversion); + } +} \ No newline at end of file diff --git a/test/cli/test-task-validator.vala b/test/cli/test-task-validator.vala new file mode 100644 index 000000000..cd3521a63 --- /dev/null +++ b/test/cli/test-task-validator.vala @@ -0,0 +1,149 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +/** + * TaskValidator Tests + * + * Tests for input validation logic including: + * - Content validation (required field) + * - Priority validation (1-4 range) + * - Date format validation (YYYY-MM-DD) + */ + +namespace PlanifyCLI.Tests.TaskValidator { + void test_content_required () { + print ("Testing: Content validation - null\n"); + string? error_message; + + bool result = PlanifyCLI.TaskValidator.validate_content (null, out error_message); + + assert (result == false); + assert (error_message != null); + print (" ✓ Rejects null content\n---\n"); + } + + void test_content_empty () { + print ("Testing: Content validation - empty string\n"); + string? error_message; + + bool result = PlanifyCLI.TaskValidator.validate_content (" ", out error_message); + + assert (result == false); + assert (error_message != null); + print (" ✓ Rejects empty/whitespace content\n---\n"); + } + + void test_content_valid () { + print ("Testing: Content validation - valid\n"); + string? error_message; + + bool result = PlanifyCLI.TaskValidator.validate_content ("Valid task content", out error_message); + + assert (result == true); + assert (error_message == null); + print (" ✓ Accepts valid content\n---\n"); + } + + void test_priority_range () { + print ("Testing: Priority validation - range checks\n"); + string? error_message; + + // Test invalid priorities + assert (PlanifyCLI.TaskValidator.validate_priority (0, out error_message) == false); + print (" ✓ Rejects priority 0\n"); + + assert (PlanifyCLI.TaskValidator.validate_priority (5, out error_message) == false); + print (" ✓ Rejects priority 5\n"); + + assert (PlanifyCLI.TaskValidator.validate_priority (-1, out error_message) == false); + print (" ✓ Rejects priority -1\n"); + + // Test valid priorities + assert (PlanifyCLI.TaskValidator.validate_priority (1, out error_message) == true); + assert (PlanifyCLI.TaskValidator.validate_priority (2, out error_message) == true); + assert (PlanifyCLI.TaskValidator.validate_priority (3, out error_message) == true); + assert (PlanifyCLI.TaskValidator.validate_priority (4, out error_message) == true); + print (" ✓ Accepts priorities 1-4\n---\n"); + } + + void test_date_format_valid () { + print ("Testing: Date validation - valid formats\n"); + string? error_message; + GLib.DateTime? datetime; + + bool result = PlanifyCLI.TaskValidator.validate_and_parse_date ("2024-12-31", out datetime, out error_message); + + assert (result == true); + assert (datetime != null); + assert (datetime.get_year () == 2024); + assert (datetime.get_month () == 12); + assert (datetime.get_day_of_month () == 31); + assert (error_message == null); + print (" ✓ Parses valid YYYY-MM-DD format\n---\n"); + } + + void test_date_format_invalid () { + print ("Testing: Date validation - invalid formats\n"); + string? error_message; + GLib.DateTime? datetime; + + // Invalid format + bool result = PlanifyCLI.TaskValidator.validate_and_parse_date ("12/31/2024", out datetime, out error_message); + assert (result == false); + assert (error_message != null); + print (" ✓ Rejects invalid date format\n"); + + // Invalid values + result = PlanifyCLI.TaskValidator.validate_and_parse_date ("2024-13-01", out datetime, out error_message); + assert (result == false); + print (" ✓ Rejects invalid month\n"); + + result = PlanifyCLI.TaskValidator.validate_and_parse_date ("2024-01-32", out datetime, out error_message); + assert (result == false); + print (" ✓ Rejects invalid day\n---\n"); + } + + void test_date_null_or_empty () { + print ("Testing: Date validation - null/empty (optional)\n"); + string? error_message; + GLib.DateTime? datetime; + + // Null is valid (optional field) + bool result = PlanifyCLI.TaskValidator.validate_and_parse_date (null, out datetime, out error_message); + assert (result == true); + assert (datetime == null); + print (" ✓ Accepts null date (optional)\n"); + + // Empty string is valid (optional field) + result = PlanifyCLI.TaskValidator.validate_and_parse_date ("", out datetime, out error_message); + assert (result == true); + assert (datetime == null); + print (" ✓ Accepts empty date (optional)\n---\n"); + } + + public void register_tests () { + Test.add_func ("/cli/task_validator/content_required", test_content_required); + Test.add_func ("/cli/task_validator/content_empty", test_content_empty); + Test.add_func ("/cli/task_validator/content_valid", test_content_valid); + Test.add_func ("/cli/task_validator/priority_range", test_priority_range); + Test.add_func ("/cli/task_validator/date_format_valid", test_date_format_valid); + Test.add_func ("/cli/task_validator/date_format_invalid", test_date_format_invalid); + Test.add_func ("/cli/task_validator/date_null_or_empty", test_date_null_or_empty); + } +} \ No newline at end of file diff --git a/test/meson.build b/test/meson.build index d71dc9f6d..c654b9bb5 100644 --- a/test/meson.build +++ b/test/meson.build @@ -16,3 +16,22 @@ test_chrono = executable( test('chrono', test_chrono, timeout: 30, suite: ['chrono']) +test_cli_sources = [ + 'test-cli-runner.vala', + 'cli/test-argument-parser.vala', + 'cli/test-task-validator.vala', + 'cli/test-output-formatter.vala', + 'cli/test-priority-conversion.vala', + join_paths(meson.project_source_root(), 'quick-add', 'cli', 'ArgumentParser.vala'), + join_paths(meson.project_source_root(), 'quick-add', 'cli', 'TaskValidator.vala') +] + +test_cli = executable( + 'test-cli', + test_cli_sources, + dependencies: [ core_dep, glib_dep ], + install: false +) + +test('cli', test_cli, timeout: 30, suite: ['cli']) + diff --git a/test/test-cli-runner.vala b/test/test-cli-runner.vala new file mode 100644 index 000000000..819757e67 --- /dev/null +++ b/test/test-cli-runner.vala @@ -0,0 +1,36 @@ +/* + * Copyright © 2026 Alain M. (https://github.com/alainm23/planify) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +/** + * CLI Test Runner + * + * Main test runner that orchestrates all CLI component tests. + * Each component has its own test file in test/cli/ directory. + */ + +void main (string[] args) { + Test.init (ref args); + + PlanifyCLI.Tests.ArgumentParser.register_tests (); + PlanifyCLI.Tests.TaskValidator.register_tests (); + PlanifyCLI.Tests.OutputFormatter.register_tests (); + PlanifyCLI.Tests.PriorityConversion.register_tests (); + + Test.run (); +} \ No newline at end of file From 3be2dd0437f00a00a63560f7e3f5fdc7931c536e Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Fri, 20 Feb 2026 09:32:04 +0000 Subject: [PATCH 18/21] ffix: remove referenced path that's not used --- test/meson.build | 1 - test/test-cli-runner.vala | 1 - 2 files changed, 2 deletions(-) diff --git a/test/meson.build b/test/meson.build index c654b9bb5..e1dee0920 100644 --- a/test/meson.build +++ b/test/meson.build @@ -20,7 +20,6 @@ test_cli_sources = [ 'test-cli-runner.vala', 'cli/test-argument-parser.vala', 'cli/test-task-validator.vala', - 'cli/test-output-formatter.vala', 'cli/test-priority-conversion.vala', join_paths(meson.project_source_root(), 'quick-add', 'cli', 'ArgumentParser.vala'), join_paths(meson.project_source_root(), 'quick-add', 'cli', 'TaskValidator.vala') diff --git a/test/test-cli-runner.vala b/test/test-cli-runner.vala index 819757e67..f04b5ced9 100644 --- a/test/test-cli-runner.vala +++ b/test/test-cli-runner.vala @@ -29,7 +29,6 @@ void main (string[] args) { PlanifyCLI.Tests.ArgumentParser.register_tests (); PlanifyCLI.Tests.TaskValidator.register_tests (); - PlanifyCLI.Tests.OutputFormatter.register_tests (); PlanifyCLI.Tests.PriorityConversion.register_tests (); Test.run (); From b2c9f936a042d0c1911167a6a9f08012708b65aa Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Fri, 20 Feb 2026 10:08:10 +0000 Subject: [PATCH 19/21] refactor(cli): change completion flag to use true|false like pin does --- quick-add/cli/ArgumentParser.vala | 18 +++++++++++++++--- test/cli/test-argument-parser.vala | 25 +++++++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index f2f302ab4..4f2535af9 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -223,9 +223,22 @@ namespace PlanifyCLI { return null; } } else if (arg == "--complete") { + if (i + 1 < args.length) { + string value = args[++i].down (); + if (value == "true") { update_args.checked = 1; - } else if (arg == "--uncomplete") { + } else if (value == "false") { update_args.checked = 0; + } else { + stderr.printf ("Error: --complete requires 'true' or 'false'\n"); + exit_code = 1; + return null; + } + } else { + stderr.printf ("Error: --complete requires an argument (true or false)\n"); + exit_code = 1; + return null; + } } else if (arg == "--pin") { if (i + 1 < args.length) { string value = args[++i].down (); @@ -445,8 +458,7 @@ namespace PlanifyCLI { stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); - stdout.printf (" --complete Mark task as complete\n"); - stdout.printf (" --uncomplete Mark task as incomplete\n"); + stdout.printf (" --complete=true|false Mark task as complete or incomplete\n"); stdout.printf (" --pin=true|false Pin or unpin the task\n"); stdout.printf (" -h, --help Show this help message\n"); } diff --git a/test/cli/test-argument-parser.vala b/test/cli/test-argument-parser.vala index 5347fd58d..7a895099e 100644 --- a/test/cli/test-argument-parser.vala +++ b/test/cli/test-argument-parser.vala @@ -161,23 +161,23 @@ namespace PlanifyCLI.Tests.ArgumentParser { print ("Testing: 'update' command with completion flags\n"); int exit_code; - // Test --complete - string[] args_complete = {"planify-cli", "update", "-t", "task-123", "--complete"}; + // Test --complete=true + string[] args_complete = {"planify-cli", "update", "-t", "task-123", "--complete", "true"}; var parsed = PlanifyCLI.ArgumentParser.parse (args_complete, out exit_code); assert (parsed != null); assert (exit_code == 0); assert (parsed.update_args.checked == 1); - print (" ✓ Parses --complete flag\n"); + print (" ✓ Parses --complete=true\n"); - // Test --uncomplete - string[] args_uncomplete = {"planify-cli", "update", "-t", "task-123", "--uncomplete"}; + // Test --complete=false + string[] args_uncomplete = {"planify-cli", "update", "-t", "task-123", "--complete", "false"}; parsed = PlanifyCLI.ArgumentParser.parse (args_uncomplete, out exit_code); assert (parsed != null); assert (exit_code == 0); assert (parsed.update_args.checked == 0); - print (" ✓ Parses --uncomplete flag\n---\n"); + print (" ✓ Parses --complete=false\n---\n"); } void test_missing_required_arg () { @@ -204,6 +204,18 @@ namespace PlanifyCLI.Tests.ArgumentParser { print (" ✓ Returns error for invalid pin value\n---\n"); } + void test_invalid_complete_value () { + print ("Testing: Invalid --complete value\n"); + int exit_code; + string[] args = {"planify-cli", "update", "-t", "task-123", "--complete", "invalid"}; + + var parsed = PlanifyCLI.ArgumentParser.parse (args, out exit_code); + + assert (parsed == null); + assert (exit_code == 1); + print (" ✓ Returns error for invalid complete value\n---\n"); + } + void test_unknown_option () { print ("Testing: Unknown option\n"); int exit_code; @@ -228,6 +240,7 @@ namespace PlanifyCLI.Tests.ArgumentParser { Test.add_func ("/cli/argument_parser/update_completion", test_update_completion); Test.add_func ("/cli/argument_parser/missing_required_arg", test_missing_required_arg); Test.add_func ("/cli/argument_parser/invalid_pin_value", test_invalid_pin_value); + Test.add_func ("/cli/argument_parser/invalid_complete_value", test_invalid_complete_value); Test.add_func ("/cli/argument_parser/unknown_option", test_unknown_option); } } \ No newline at end of file From 5b0888d1e9bb7c2b6e774a8f3535decf32f6801e Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Fri, 20 Feb 2026 10:10:47 +0000 Subject: [PATCH 20/21] refactor(cli): cleanup help info with per-command output --- quick-add/cli/ArgumentParser.vala | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index 4f2535af9..bf2953795 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -72,6 +72,13 @@ namespace PlanifyCLI { public static ParsedCommand? parse (string[] args, out int exit_code) { exit_code = 0; + // Check for top-level help + if (args.length >= 2 && (args[1] == "-h" || args[1] == "--help")) { + print_general_help (args[0]); + exit_code = 0; + return null; + } + // Check for command if (args.length < 2) { stderr.printf ("Error: No command specified\n"); @@ -226,9 +233,9 @@ namespace PlanifyCLI { if (i + 1 < args.length) { string value = args[++i].down (); if (value == "true") { - update_args.checked = 1; + update_args.checked = 1; } else if (value == "false") { - update_args.checked = 0; + update_args.checked = 0; } else { stderr.printf ("Error: --complete requires 'true' or 'false'\n"); exit_code = 1; @@ -376,7 +383,7 @@ namespace PlanifyCLI { return null; } } else if (arg == "-h" || arg == "--help") { - print_help (args[0]); + print_add_help (args[0]); exit_code = 0; return null; } else { @@ -397,14 +404,24 @@ namespace PlanifyCLI { } } - private static void print_help (string program_name) { + private static void print_general_help (string program_name) { stdout.printf ("Usage: %s [OPTIONS]\n\n", program_name); stdout.printf ("Commands:\n"); stdout.printf (" add Add a new task\n"); - stdout.printf (" list List tasks from a project (JSON output)\n"); + stdout.printf (" list List tasks from a project\n"); stdout.printf (" update Update an existing task\n"); - stdout.printf (" list-projects List all projects (JSON output)\n\n"); - stdout.printf ("Add command options:\n"); + stdout.printf (" list-projects List all projects\n\n"); + stdout.printf ("Run '%s --help' for command-specific options\n\n", program_name); + stdout.printf ("Examples:\n"); + stdout.printf (" %s add --help\n", program_name); + stdout.printf (" %s list --help\n", program_name); + stdout.printf (" %s update --help\n", program_name); + } + + private static void print_add_help (string program_name) { + stdout.printf ("Usage: %s add [OPTIONS]\n\n", program_name); + stdout.printf ("Add a new task to Planify\n\n"); + stdout.printf ("Options:\n"); stdout.printf (" -c, --content=CONTENT Task content (required)\n"); stdout.printf (" -d, --description=DESC Task description\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); @@ -416,29 +433,12 @@ namespace PlanifyCLI { stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" --pin=true|false Pin or unpin the task\n"); - stdout.printf (" -h, --help Show this help message\n\n"); - stdout.printf ("List command options:\n"); - stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); - stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); - stdout.printf (" -h, --help Show this help message\n\n"); - stdout.printf ("Update command options:\n"); - stdout.printf (" -t, --task-id=ID Task ID to update (required)\n"); - stdout.printf (" -c, --content=CONTENT New task content\n"); - stdout.printf (" -d, --description=DESC New task description\n"); - stdout.printf (" -p, --project=PROJECT Move to project by name\n"); - stdout.printf (" -i, --project-id=ID Move to project by ID (preferred over name)\n"); - stdout.printf (" -s, --section=SECTION Move to section by name\n"); - stdout.printf (" -S, --section-id=ID Move to section by ID (preferred over name)\n"); - stdout.printf (" -a, --parent-id=ID New parent task ID\n"); - stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); - stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); - stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); stdout.printf (" -h, --help Show this help message\n"); } private static void print_list_help (string program_name) { stdout.printf ("Usage: %s list [OPTIONS]\n\n", program_name); - stdout.printf ("List tasks from a project (JSON output)\n\n"); + stdout.printf ("List tasks from a project\n\n"); stdout.printf ("Options:\n"); stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); From 851c2bb0b11660591e3b6982dfa69f4da4ffad6a Mon Sep 17 00:00:00 2001 From: Philip Whiteside Date: Fri, 20 Feb 2026 10:17:52 +0000 Subject: [PATCH 21/21] refactor(cli): switch args & help to use simpler OptionContext --- quick-add/cli/ArgumentParser.vala | 545 +++++++++++------------------- 1 file changed, 199 insertions(+), 346 deletions(-) diff --git a/quick-add/cli/ArgumentParser.vala b/quick-add/cli/ArgumentParser.vala index bf2953795..04780abb6 100644 --- a/quick-add/cli/ArgumentParser.vala +++ b/quick-add/cli/ArgumentParser.vala @@ -92,315 +92,213 @@ namespace PlanifyCLI { string command = args[1]; var parsed = new ParsedCommand (); - if (command == "list-projects") { - parsed.command_type = CommandType.LIST_PROJECTS; - return parsed; - } else if (command == "list") { - parsed.command_type = CommandType.LIST; - var list_args = new ListArguments (); - - // Parse options starting from index 2 - for (int i = 2; i < args.length; i++) { - string arg = args[i]; - - if (arg == "-p" || arg == "--project") { - if (i + 1 < args.length) { - list_args.project_name = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-i" || arg == "--project-id") { - if (i + 1 < args.length) { - list_args.project_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-h" || arg == "--help") { - print_list_help (args[0]); - exit_code = 0; - return null; - } else { - stderr.printf ("Error: Unknown option '%s'\n", arg); - stderr.printf ("Run '%s list --help' for usage information\n", args[0]); - exit_code = 1; - return null; - } - } + string[] command_args = new string[args.length - 1]; + command_args[0] = args[0] + " " + command; + for (int i = 2; i < args.length; i++) { + command_args[i - 1] = args[i]; + } - parsed.list_args = list_args; - return parsed; - } else if (command == "update") { - parsed.command_type = CommandType.UPDATE; - var update_args = new UpdateArguments (); - - // Parse options starting from index 2 - for (int i = 2; i < args.length; i++) { - string arg = args[i]; - - if (arg == "-t" || arg == "--task-id") { - if (i + 1 < args.length) { - update_args.task_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-c" || arg == "--content") { - if (i + 1 < args.length) { - update_args.content = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-d" || arg == "--description") { - if (i + 1 < args.length) { - update_args.description = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-p" || arg == "--project") { - if (i + 1 < args.length) { - update_args.project_name = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-i" || arg == "--project-id") { - if (i + 1 < args.length) { - update_args.project_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-s" || arg == "--section") { - if (i + 1 < args.length) { - update_args.section_name = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-S" || arg == "--section-id") { - if (i + 1 < args.length) { - update_args.section_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-a" || arg == "--parent-id") { - if (i + 1 < args.length) { - update_args.parent_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-P" || arg == "--priority") { - if (i + 1 < args.length) { - update_args.priority = int.parse (args[++i]); - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-D" || arg == "--due") { - if (i + 1 < args.length) { - update_args.due_date = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-l" || arg == "--labels") { - if (i + 1 < args.length) { - update_args.labels = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "--complete") { - if (i + 1 < args.length) { - string value = args[++i].down (); - if (value == "true") { - update_args.checked = 1; - } else if (value == "false") { - update_args.checked = 0; - } else { - stderr.printf ("Error: --complete requires 'true' or 'false'\n"); - exit_code = 1; - return null; - } - } else { - stderr.printf ("Error: --complete requires an argument (true or false)\n"); - exit_code = 1; - return null; - } - } else if (arg == "--pin") { - if (i + 1 < args.length) { - string value = args[++i].down (); - if (value == "true") { - update_args.pinned = 1; - } else if (value == "false") { - update_args.pinned = 0; - } else { - stderr.printf ("Error: --pin requires 'true' or 'false'\n"); - exit_code = 1; - return null; - } - } else { - stderr.printf ("Error: --pin requires an argument (true or false)\n"); - exit_code = 1; - return null; - } - } else if (arg == "-h" || arg == "--help") { - print_update_help (args[0]); - exit_code = 0; - return null; - } else { - stderr.printf ("Error: Unknown option '%s'\n", arg); - stderr.printf ("Run '%s update --help' for usage information\n", args[0]); - exit_code = 1; - return null; - } - } + try { + switch (command) { + case "list-projects": + parsed.command_type = CommandType.LIST_PROJECTS; + return parsed; - parsed.update_args = update_args; - return parsed; - } else if (command == "add") { - parsed.command_type = CommandType.ADD; - var task_args = new TaskArguments (); + case "list": + parsed.command_type = CommandType.LIST; + parsed.list_args = parse_list_command (command_args); + return parsed; - // Parse options starting from index 2 - for (int i = 2; i < args.length; i++) { - string arg = args[i]; - - if (arg == "-c" || arg == "--content") { - if (i + 1 < args.length) { - task_args.content = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-d" || arg == "--description") { - if (i + 1 < args.length) { - task_args.description = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-p" || arg == "--project") { - if (i + 1 < args.length) { - task_args.project_name = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-i" || arg == "--project-id") { - if (i + 1 < args.length) { - task_args.project_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-s" || arg == "--section") { - if (i + 1 < args.length) { - task_args.section_name = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-S" || arg == "--section-id") { - if (i + 1 < args.length) { - task_args.section_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-a" || arg == "--parent-id") { - if (i + 1 < args.length) { - task_args.parent_id = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-P" || arg == "--priority") { - if (i + 1 < args.length) { - task_args.priority = int.parse (args[++i]); - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-D" || arg == "--due") { - if (i + 1 < args.length) { - task_args.due_date = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "-l" || arg == "--labels") { - if (i + 1 < args.length) { - task_args.labels = args[++i]; - } else { - stderr.printf ("Error: %s requires an argument\n", arg); - exit_code = 1; - return null; - } - } else if (arg == "--pin") { - if (i + 1 < args.length) { - string value = args[++i].down (); - if (value == "true") { - task_args.pinned = 1; - } else if (value == "false") { - task_args.pinned = 0; - } else { - stderr.printf ("Error: --pin requires 'true' or 'false'\n"); - exit_code = 1; - return null; - } - } else { - stderr.printf ("Error: --pin requires an argument (true or false)\n"); + case "add": + parsed.command_type = CommandType.ADD; + parsed.task_args = parse_add_command (command_args); + return parsed; + + case "update": + parsed.command_type = CommandType.UPDATE; + parsed.update_args = parse_update_command (command_args); + return parsed; + + default: + stderr.printf ("Error: Unknown command '%s'\n", command); + stderr.printf ("Available commands: add, list, update, list-projects\n"); exit_code = 1; return null; - } - } else if (arg == "-h" || arg == "--help") { - print_add_help (args[0]); - exit_code = 0; - return null; - } else { - stderr.printf ("Error: Unknown option '%s'\n", arg); - stderr.printf ("Run '%s add --help' for usage information\n", args[0]); - exit_code = 1; - return null; } + } catch (OptionError e) { + stderr.printf ("Error: %s\n", e.message); + exit_code = 1; + return null; } + } + + private static ListArguments parse_list_command (string[] args) throws OptionError { + string? project_name = null; + string? project_id = null; + + var options = new OptionEntry[3]; + options[0] = { "project", 'p', 0, OptionArg.STRING, ref project_name, + "Project name (defaults to inbox)", "PROJECT" }; + options[1] = { "project-id", 'i', 0, OptionArg.STRING, ref project_id, + "Project ID (preferred over name)", "ID" }; + options[2] = { null }; + + var context = new OptionContext ("- List tasks from a project"); + context.add_main_entries (options, null); + context.set_help_enabled (true); + + unowned string[] tmp = args; + context.parse (ref tmp); + + var list_args = new ListArguments (); + list_args.project_name = project_name; + list_args.project_id = project_id; + + return list_args; + } + + private static TaskArguments parse_add_command (string[] args) throws OptionError { + string? content = null; + string? description = null; + string? project_name = null; + string? project_id = null; + string? section_name = null; + string? section_id = null; + string? parent_id = null; + int priority = 4; + string? due_date = null; + string? labels = null; + string? pin_str = null; + + var options = new OptionEntry[12]; + options[0] = { "content", 'c', 0, OptionArg.STRING, ref content, + "Task content (required)", "CONTENT" }; + options[1] = { "description", 'd', 0, OptionArg.STRING, ref description, + "Task description", "DESC" }; + options[2] = { "project", 'p', 0, OptionArg.STRING, ref project_name, + "Project name (defaults to inbox)", "PROJECT" }; + options[3] = { "project-id", 'i', 0, OptionArg.STRING, ref project_id, + "Project ID (preferred over name)", "ID" }; + options[4] = { "section", 's', 0, OptionArg.STRING, ref section_name, + "Section name", "SECTION" }; + options[5] = { "section-id", 'S', 0, OptionArg.STRING, ref section_id, + "Section ID (preferred over name)", "ID" }; + options[6] = { "parent-id", 'a', 0, OptionArg.STRING, ref parent_id, + "Parent task ID (creates a subtask)", "ID" }; + options[7] = { "priority", 'P', 0, OptionArg.INT, ref priority, + "Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)", "1-4" }; + options[8] = { "due", 'D', 0, OptionArg.STRING, ref due_date, + "Due date in YYYY-MM-DD format", "DATE" }; + options[9] = { "labels", 'l', 0, OptionArg.STRING, ref labels, + "Comma-separated list of label names", "LABELS" }; + options[10] = { "pin", 0, 0, OptionArg.STRING, ref pin_str, + "Pin or unpin the task", "true|false" }; + options[11] = { null }; + + var context = new OptionContext ("- Add a new task to Planify"); + context.add_main_entries (options, null); + context.set_help_enabled (true); + + unowned string[] tmp = args; + context.parse (ref tmp); + + var task_args = new TaskArguments (); + task_args.content = content; + task_args.description = description; + task_args.project_name = project_name; + task_args.project_id = project_id; + task_args.section_name = section_name; + task_args.section_id = section_id; + task_args.parent_id = parent_id; + task_args.priority = priority; + task_args.due_date = due_date; + task_args.labels = labels; + task_args.pinned = parse_boolean_option (pin_str); + + return task_args; + } + + private static UpdateArguments parse_update_command (string[] args) throws OptionError { + string? task_id = null; + string? content = null; + string? description = null; + string? project_name = null; + string? project_id = null; + string? section_name = null; + string? section_id = null; + string? parent_id = null; + int priority = -1; + string? due_date = null; + string? labels = null; + string? complete_str = null; + string? pin_str = null; - parsed.task_args = task_args; - return parsed; + var options = new OptionEntry[14]; + options[0] = { "task-id", 't', 0, OptionArg.STRING, ref task_id, + "Task ID to update (required)", "ID" }; + options[1] = { "content", 'c', 0, OptionArg.STRING, ref content, + "New task content", "CONTENT" }; + options[2] = { "description", 'd', 0, OptionArg.STRING, ref description, + "New task description", "DESC" }; + options[3] = { "project", 'p', 0, OptionArg.STRING, ref project_name, + "Move to project by name", "PROJECT" }; + options[4] = { "project-id", 'i', 0, OptionArg.STRING, ref project_id, + "Move to project by ID (preferred over name)", "ID" }; + options[5] = { "section", 's', 0, OptionArg.STRING, ref section_name, + "Section name", "SECTION" }; + options[6] = { "section-id", 'S', 0, OptionArg.STRING, ref section_id, + "Section ID (preferred over name)", "ID" }; + options[7] = { "parent-id", 'a', 0, OptionArg.STRING, ref parent_id, + "New parent task ID", "ID" }; + options[8] = { "priority", 'P', 0, OptionArg.INT, ref priority, + "Priority: 1=high, 2=medium, 3=low, 4=none", "1-4" }; + options[9] = { "due", 'D', 0, OptionArg.STRING, ref due_date, + "Due date in YYYY-MM-DD format", "DATE" }; + options[10] = { "labels", 'l', 0, OptionArg.STRING, ref labels, + "Comma-separated list of label names", "LABELS" }; + options[11] = { "complete", 0, 0, OptionArg.STRING, ref complete_str, + "Mark task as complete or incomplete", "true|false" }; + options[12] = { "pin", 0, 0, OptionArg.STRING, ref pin_str, + "Pin or unpin the task", "true|false" }; + options[13] = { null }; + + var context = new OptionContext ("- Update an existing task. Only provided fields will be changed."); + context.add_main_entries (options, null); + context.set_help_enabled (true); + + unowned string[] tmp = args; + context.parse (ref tmp); + + var update_args = new UpdateArguments (); + update_args.task_id = task_id; + update_args.content = content; + update_args.description = description; + update_args.project_name = project_name; + update_args.project_id = project_id; + update_args.section_name = section_name; + update_args.section_id = section_id; + update_args.parent_id = parent_id; + update_args.priority = priority; + update_args.due_date = due_date; + update_args.labels = labels; + update_args.checked = parse_boolean_option (complete_str); + update_args.pinned = parse_boolean_option (pin_str); + + return update_args; + } + + private static int parse_boolean_option (string? value) throws OptionError { + if (value == null) { + return -1; + } + + string lower_value = value.down (); + if (lower_value == "true") { + return 1; + } else if (lower_value == "false") { + return 0; } else { - stderr.printf ("Error: Unknown command '%s'\n", command); - stderr.printf ("Available commands: add, list, update, list-projects\n"); - exit_code = 1; - return null; + throw new OptionError.BAD_VALUE ("Boolean option requires 'true' or 'false'"); } } @@ -417,50 +315,5 @@ namespace PlanifyCLI { stdout.printf (" %s list --help\n", program_name); stdout.printf (" %s update --help\n", program_name); } - - private static void print_add_help (string program_name) { - stdout.printf ("Usage: %s add [OPTIONS]\n\n", program_name); - stdout.printf ("Add a new task to Planify\n\n"); - stdout.printf ("Options:\n"); - stdout.printf (" -c, --content=CONTENT Task content (required)\n"); - stdout.printf (" -d, --description=DESC Task description\n"); - stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); - stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); - stdout.printf (" -s, --section=SECTION Section name\n"); - stdout.printf (" -S, --section-id=ID Section ID (preferred over name)\n"); - stdout.printf (" -a, --parent-id=ID Parent task ID (creates a subtask)\n"); - stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none (default: 4)\n"); - stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); - stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); - stdout.printf (" --pin=true|false Pin or unpin the task\n"); - stdout.printf (" -h, --help Show this help message\n"); - } - - private static void print_list_help (string program_name) { - stdout.printf ("Usage: %s list [OPTIONS]\n\n", program_name); - stdout.printf ("List tasks from a project\n\n"); - stdout.printf ("Options:\n"); - stdout.printf (" -p, --project=PROJECT Project name (defaults to inbox)\n"); - stdout.printf (" -i, --project-id=ID Project ID (preferred over name)\n"); - stdout.printf (" -h, --help Show this help message\n"); - } - - private static void print_update_help (string program_name) { - stdout.printf ("Usage: %s update [OPTIONS]\n\n", program_name); - stdout.printf ("Update an existing task. Only provided fields will be changed.\n\n"); - stdout.printf ("Options:\n"); - stdout.printf (" -t, --task-id=ID Task ID to update (required)\n"); - stdout.printf (" -c, --content=CONTENT New task content\n"); - stdout.printf (" -d, --description=DESC New task description\n"); - stdout.printf (" -p, --project=PROJECT Move to project by name\n"); - stdout.printf (" -i, --project-id=ID Move to project by ID (preferred over name)\n"); - stdout.printf (" -a, --parent-id=ID New parent task ID\n"); - stdout.printf (" -P, --priority=1-4 Priority: 1=high, 2=medium, 3=low, 4=none\n"); - stdout.printf (" -D, --due=DATE Due date in YYYY-MM-DD format\n"); - stdout.printf (" -l, --labels=LABELS Comma-separated list of label names\n"); - stdout.printf (" --complete=true|false Mark task as complete or incomplete\n"); - stdout.printf (" --pin=true|false Pin or unpin the task\n"); - stdout.printf (" -h, --help Show this help message\n"); - } } -} +} \ No newline at end of file