From 0ba2d5d3fa2215a0b5737dbae19415840514e770 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:50:48 +0200 Subject: [PATCH 01/10] allow printing task details --- cmd/tasks.go | 20 ------ {cmd => internal/cmd}/assets/CHANGELOG.md | 0 .../assets/guide/actions-adding-context.md | 0 .../cmd}/assets/guide/actions-adding-tasks.md | 0 .../assets/guide/actions-choosing-a-prefix.md | 0 .../assets/guide/actions-deleting-a-task.md | 0 .../assets/guide/actions-duplicating-tasks.md | 0 .../assets/guide/actions-filtering-tasks.md | 0 .../guide/actions-markdown-in-context.md | 0 .../actions-quick-filtering-via-a-list.md | 0 .../guide/actions-updating-task-details.md | 0 .../guide/cli-adding-a-task-via-the-cli.md | 0 ...cli-importing-several-tasks-via-the-cli.md | 0 .../guide/config-a-sample-toml-config.md | 0 .../guide/config-changing-the-defaults.md | 0 .../config-flags-env-vars-and-config-file.md | 0 .../assets/guide/domain-an-archived-task.md | 0 .../assets/guide/domain-task-bookmarks.md | 0 .../cmd}/assets/guide/domain-task-details.md | 0 .../assets/guide/domain-task-priorities.md | 0 .../cmd}/assets/guide/domain-task-state.md | 0 .../cmd}/assets/guide/domain-tasks.md | 0 .../cmd}/assets/guide/guide-and-thats-it.md | 0 .../cmd}/assets/guide/guide-welcome-to-omm.md | 0 .../cmd}/assets/guide/visuals-list-density.md | 0 .../guide/visuals-toggling-context-pane.md | 0 {cmd => internal/cmd}/db.go | 0 {cmd => internal/cmd}/guide.go | 0 {cmd => internal/cmd}/guide_test.go | 0 {cmd => internal/cmd}/import.go | 0 {cmd => internal/cmd}/root.go | 63 +++++++++++++------ internal/cmd/tasks.go | 44 +++++++++++++ {cmd => internal/cmd}/utils.go | 0 internal/persistence/queries.go | 53 +++++++++++++++- main.go | 2 +- 35 files changed, 141 insertions(+), 41 deletions(-) delete mode 100644 cmd/tasks.go rename {cmd => internal/cmd}/assets/CHANGELOG.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-adding-context.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-adding-tasks.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-choosing-a-prefix.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-deleting-a-task.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-duplicating-tasks.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-filtering-tasks.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-markdown-in-context.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-quick-filtering-via-a-list.md (100%) rename {cmd => internal/cmd}/assets/guide/actions-updating-task-details.md (100%) rename {cmd => internal/cmd}/assets/guide/cli-adding-a-task-via-the-cli.md (100%) rename {cmd => internal/cmd}/assets/guide/cli-importing-several-tasks-via-the-cli.md (100%) rename {cmd => internal/cmd}/assets/guide/config-a-sample-toml-config.md (100%) rename {cmd => internal/cmd}/assets/guide/config-changing-the-defaults.md (100%) rename {cmd => internal/cmd}/assets/guide/config-flags-env-vars-and-config-file.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-an-archived-task.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-task-bookmarks.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-task-details.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-task-priorities.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-task-state.md (100%) rename {cmd => internal/cmd}/assets/guide/domain-tasks.md (100%) rename {cmd => internal/cmd}/assets/guide/guide-and-thats-it.md (100%) rename {cmd => internal/cmd}/assets/guide/guide-welcome-to-omm.md (100%) rename {cmd => internal/cmd}/assets/guide/visuals-list-density.md (100%) rename {cmd => internal/cmd}/assets/guide/visuals-toggling-context-pane.md (100%) rename {cmd => internal/cmd}/db.go (100%) rename {cmd => internal/cmd}/guide.go (100%) rename {cmd => internal/cmd}/guide_test.go (100%) rename {cmd => internal/cmd}/import.go (100%) rename {cmd => internal/cmd}/root.go (86%) create mode 100644 internal/cmd/tasks.go rename {cmd => internal/cmd}/utils.go (100%) diff --git a/cmd/tasks.go b/cmd/tasks.go deleted file mode 100644 index 9de3f01..0000000 --- a/cmd/tasks.go +++ /dev/null @@ -1,20 +0,0 @@ -package cmd - -import ( - "database/sql" - "fmt" - "io" - - pers "github.com/dhth/omm/internal/persistence" -) - -func printTasks(db *sql.DB, limit uint8, writer io.Writer) error { - tasks, err := pers.FetchActiveTasks(db, int(limit)) - if err != nil { - return err - } - for _, task := range tasks { - fmt.Fprintf(writer, "%s\n", task.Summary) - } - return nil -} diff --git a/cmd/assets/CHANGELOG.md b/internal/cmd/assets/CHANGELOG.md similarity index 100% rename from cmd/assets/CHANGELOG.md rename to internal/cmd/assets/CHANGELOG.md diff --git a/cmd/assets/guide/actions-adding-context.md b/internal/cmd/assets/guide/actions-adding-context.md similarity index 100% rename from cmd/assets/guide/actions-adding-context.md rename to internal/cmd/assets/guide/actions-adding-context.md diff --git a/cmd/assets/guide/actions-adding-tasks.md b/internal/cmd/assets/guide/actions-adding-tasks.md similarity index 100% rename from cmd/assets/guide/actions-adding-tasks.md rename to internal/cmd/assets/guide/actions-adding-tasks.md diff --git a/cmd/assets/guide/actions-choosing-a-prefix.md b/internal/cmd/assets/guide/actions-choosing-a-prefix.md similarity index 100% rename from cmd/assets/guide/actions-choosing-a-prefix.md rename to internal/cmd/assets/guide/actions-choosing-a-prefix.md diff --git a/cmd/assets/guide/actions-deleting-a-task.md b/internal/cmd/assets/guide/actions-deleting-a-task.md similarity index 100% rename from cmd/assets/guide/actions-deleting-a-task.md rename to internal/cmd/assets/guide/actions-deleting-a-task.md diff --git a/cmd/assets/guide/actions-duplicating-tasks.md b/internal/cmd/assets/guide/actions-duplicating-tasks.md similarity index 100% rename from cmd/assets/guide/actions-duplicating-tasks.md rename to internal/cmd/assets/guide/actions-duplicating-tasks.md diff --git a/cmd/assets/guide/actions-filtering-tasks.md b/internal/cmd/assets/guide/actions-filtering-tasks.md similarity index 100% rename from cmd/assets/guide/actions-filtering-tasks.md rename to internal/cmd/assets/guide/actions-filtering-tasks.md diff --git a/cmd/assets/guide/actions-markdown-in-context.md b/internal/cmd/assets/guide/actions-markdown-in-context.md similarity index 100% rename from cmd/assets/guide/actions-markdown-in-context.md rename to internal/cmd/assets/guide/actions-markdown-in-context.md diff --git a/cmd/assets/guide/actions-quick-filtering-via-a-list.md b/internal/cmd/assets/guide/actions-quick-filtering-via-a-list.md similarity index 100% rename from cmd/assets/guide/actions-quick-filtering-via-a-list.md rename to internal/cmd/assets/guide/actions-quick-filtering-via-a-list.md diff --git a/cmd/assets/guide/actions-updating-task-details.md b/internal/cmd/assets/guide/actions-updating-task-details.md similarity index 100% rename from cmd/assets/guide/actions-updating-task-details.md rename to internal/cmd/assets/guide/actions-updating-task-details.md diff --git a/cmd/assets/guide/cli-adding-a-task-via-the-cli.md b/internal/cmd/assets/guide/cli-adding-a-task-via-the-cli.md similarity index 100% rename from cmd/assets/guide/cli-adding-a-task-via-the-cli.md rename to internal/cmd/assets/guide/cli-adding-a-task-via-the-cli.md diff --git a/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md b/internal/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md similarity index 100% rename from cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md rename to internal/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md diff --git a/cmd/assets/guide/config-a-sample-toml-config.md b/internal/cmd/assets/guide/config-a-sample-toml-config.md similarity index 100% rename from cmd/assets/guide/config-a-sample-toml-config.md rename to internal/cmd/assets/guide/config-a-sample-toml-config.md diff --git a/cmd/assets/guide/config-changing-the-defaults.md b/internal/cmd/assets/guide/config-changing-the-defaults.md similarity index 100% rename from cmd/assets/guide/config-changing-the-defaults.md rename to internal/cmd/assets/guide/config-changing-the-defaults.md diff --git a/cmd/assets/guide/config-flags-env-vars-and-config-file.md b/internal/cmd/assets/guide/config-flags-env-vars-and-config-file.md similarity index 100% rename from cmd/assets/guide/config-flags-env-vars-and-config-file.md rename to internal/cmd/assets/guide/config-flags-env-vars-and-config-file.md diff --git a/cmd/assets/guide/domain-an-archived-task.md b/internal/cmd/assets/guide/domain-an-archived-task.md similarity index 100% rename from cmd/assets/guide/domain-an-archived-task.md rename to internal/cmd/assets/guide/domain-an-archived-task.md diff --git a/cmd/assets/guide/domain-task-bookmarks.md b/internal/cmd/assets/guide/domain-task-bookmarks.md similarity index 100% rename from cmd/assets/guide/domain-task-bookmarks.md rename to internal/cmd/assets/guide/domain-task-bookmarks.md diff --git a/cmd/assets/guide/domain-task-details.md b/internal/cmd/assets/guide/domain-task-details.md similarity index 100% rename from cmd/assets/guide/domain-task-details.md rename to internal/cmd/assets/guide/domain-task-details.md diff --git a/cmd/assets/guide/domain-task-priorities.md b/internal/cmd/assets/guide/domain-task-priorities.md similarity index 100% rename from cmd/assets/guide/domain-task-priorities.md rename to internal/cmd/assets/guide/domain-task-priorities.md diff --git a/cmd/assets/guide/domain-task-state.md b/internal/cmd/assets/guide/domain-task-state.md similarity index 100% rename from cmd/assets/guide/domain-task-state.md rename to internal/cmd/assets/guide/domain-task-state.md diff --git a/cmd/assets/guide/domain-tasks.md b/internal/cmd/assets/guide/domain-tasks.md similarity index 100% rename from cmd/assets/guide/domain-tasks.md rename to internal/cmd/assets/guide/domain-tasks.md diff --git a/cmd/assets/guide/guide-and-thats-it.md b/internal/cmd/assets/guide/guide-and-thats-it.md similarity index 100% rename from cmd/assets/guide/guide-and-thats-it.md rename to internal/cmd/assets/guide/guide-and-thats-it.md diff --git a/cmd/assets/guide/guide-welcome-to-omm.md b/internal/cmd/assets/guide/guide-welcome-to-omm.md similarity index 100% rename from cmd/assets/guide/guide-welcome-to-omm.md rename to internal/cmd/assets/guide/guide-welcome-to-omm.md diff --git a/cmd/assets/guide/visuals-list-density.md b/internal/cmd/assets/guide/visuals-list-density.md similarity index 100% rename from cmd/assets/guide/visuals-list-density.md rename to internal/cmd/assets/guide/visuals-list-density.md diff --git a/cmd/assets/guide/visuals-toggling-context-pane.md b/internal/cmd/assets/guide/visuals-toggling-context-pane.md similarity index 100% rename from cmd/assets/guide/visuals-toggling-context-pane.md rename to internal/cmd/assets/guide/visuals-toggling-context-pane.md diff --git a/cmd/db.go b/internal/cmd/db.go similarity index 100% rename from cmd/db.go rename to internal/cmd/db.go diff --git a/cmd/guide.go b/internal/cmd/guide.go similarity index 100% rename from cmd/guide.go rename to internal/cmd/guide.go diff --git a/cmd/guide_test.go b/internal/cmd/guide_test.go similarity index 100% rename from cmd/guide_test.go rename to internal/cmd/guide_test.go diff --git a/cmd/import.go b/internal/cmd/import.go similarity index 100% rename from cmd/import.go rename to internal/cmd/import.go diff --git a/cmd/root.go b/internal/cmd/root.go similarity index 86% rename from cmd/root.go rename to internal/cmd/root.go index fb304e2..83fa1af 100644 --- a/cmd/root.go +++ b/internal/cmd/root.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "time" @@ -32,23 +33,24 @@ const ( defaultDataDirWindows = "AppData/Local" configFileName = "omm/omm.toml" dbFileName = "omm/omm.db" - printTasksDefault = 20 + numListTasksDefault = 20 taskListTitleMaxLen = 8 ) var ( - errCouldntGetHomeDir = errors.New("couldn't get home directory") - errConfigFileExtIncorrect = errors.New("config file must be a TOML file") - errConfigFileDoesntExist = errors.New("config file does not exist") - errDBFileExtIncorrect = errors.New("db file needs to end with .db") - errMaxImportLimitExceeded = errors.New("import limit exceeded") - errNothingToImport = errors.New("nothing to import") - errListDensityIncorrect = errors.New("list density is incorrect; valid values: compact/spacious") - errCouldntCreateDBDirectory = errors.New("couldn't create directory for database") - errCouldntCreateDB = errors.New("couldn't create database") - errCouldntInitializeDB = errors.New("couldn't initialize database") - errCouldntOpenDB = errors.New("couldn't open database") - errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") + errCouldntGetHomeDir = errors.New("couldn't get home directory") + errConfigFileExtIncorrect = errors.New("config file must be a TOML file") + errConfigFileDoesntExist = errors.New("config file does not exist") + errDBFileExtIncorrect = errors.New("db file needs to end with .db") + errMaxImportLimitExceeded = errors.New("import limit exceeded") + errNothingToImport = errors.New("nothing to import") + errListDensityIncorrect = errors.New("list density is incorrect; valid values: compact/spacious") + errCouldntCreateDBDirectory = errors.New("couldn't create directory for database") + errCouldntCreateDB = errors.New("couldn't create database") + errCouldntInitializeDB = errors.New("couldn't initialize database") + errCouldntOpenDB = errors.New("couldn't open database") + errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") + errInvalidValueForTaskIndexProvided = errors.New("invalid value for task index provided") //go:embed assets/CHANGELOG.md updateContents string @@ -134,7 +136,6 @@ func NewRootCommand() (*cobra.Command, error) { db *sql.DB taskListColor string archivedTaskListColor string - printTasksNum uint8 taskListTitle string listDensityFlagInp string editorFlagInp string @@ -142,6 +143,7 @@ func NewRootCommand() (*cobra.Command, error) { showContextFlagInp bool confirmBeforeDeletion bool circularNav bool + numListTasks uint16 ) rootCmd := &cobra.Command{ @@ -355,9 +357,28 @@ Sorry for breaking the upgrade step! tasksCmd := &cobra.Command{ Use: "tasks", - Short: "Output tasks tracked by omm to stdout", + Short: "Interact with tasks tracked by omm", + } + + listTasksCmd := &cobra.Command{ + Use: "list", + Short: "List tasks", RunE: func(_ *cobra.Command, _ []string) error { - return printTasks(db, printTasksNum, os.Stdout) + return printTasks(db, numListTasks, os.Stdout) + }, + } + + showTaskCmd := &cobra.Command{ + Use: "show ", + Short: "Show task", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + index, err := strconv.ParseUint(args[0], 10, 0) + if err != nil { + return fmt.Errorf("%w; value needs to be an integer >= 0", errInvalidValueForTaskIndexProvided) + } + + return showTask(db, index, os.Stdout) }, } @@ -447,15 +468,19 @@ Sorry for breaking the upgrade step! rootCmd.Flags().BoolVar(&confirmBeforeDeletion, "confirm-before-deletion", true, "whether to ask for confirmation before deleting a task") rootCmd.Flags().BoolVar(&circularNav, "circular-nav", false, "whether to enable circular navigation for lists (cycle back to the first entry from the last, and vice versa)") - tasksCmd.Flags().Uint8VarP(&printTasksNum, "num", "n", printTasksDefault, "number of tasks to print") - tasksCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) - tasksCmd.Flags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) + tasksCmd.PersistentFlags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) + tasksCmd.PersistentFlags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) + + listTasksCmd.Flags().Uint16VarP(&numListTasks, "num", "n", numListTasksDefault, "number of tasks to list") importCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) importCmd.Flags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) guideCmd.Flags().StringVar(&editorFlagInp, "editor", "vi", "editor command to run when adding/editing context to a task") + tasksCmd.AddCommand(listTasksCmd) + tasksCmd.AddCommand(showTaskCmd) + rootCmd.AddCommand(importCmd) rootCmd.AddCommand(tasksCmd) rootCmd.AddCommand(guideCmd) diff --git a/internal/cmd/tasks.go b/internal/cmd/tasks.go new file mode 100644 index 0000000..cc48382 --- /dev/null +++ b/internal/cmd/tasks.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "database/sql" + "errors" + "fmt" + "io" + "strings" + + pers "github.com/dhth/omm/internal/persistence" +) + +var errNoTaskAtIndex = errors.New("no task exists at given index") + +func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { + tasks, err := pers.FetchActiveTasks(db, int(limit)) + if err != nil { + return err + } + summaries := make([]string, len(tasks)) + for i, task := range tasks { + summaries[i] = task.Summary + } + + fmt.Fprintln(writer, strings.Join(summaries, "\n")) + return nil +} + +func showTask(db *sql.DB, index uint64, writer io.Writer) error { + task, found, err := pers.FetchNthActiveTask(db, index) + if err != nil { + return err + } + + if !found { + return errNoTaskAtIndex + } + + fmt.Fprintln(writer, task.Summary) + if task.Context != nil { + fmt.Fprintf(writer, "\n%s", *task.Context) + } + return nil +} diff --git a/cmd/utils.go b/internal/cmd/utils.go similarity index 100% rename from cmd/utils.go rename to internal/cmd/utils.go diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index d2e21bc..a9b768e 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -3,6 +3,8 @@ package persistence import ( "database/sql" "encoding/json" + "errors" + "fmt" "time" "github.com/dhth/omm/internal/types" @@ -13,6 +15,9 @@ const ( ContextMaxBytes = 1024 * 1024 ) +// TODO: wrap all unexpected sql errors with this +var errCouldntExecuteQuery = errors.New("couldn't execute query") + func fetchTaskSequence(db *sql.DB) ([]uint64, error) { var seq []byte seqRow := db.QueryRow("SELECT sequence from task_sequence where id=1;") @@ -133,7 +138,7 @@ func InsertTasks(db *sql.DB, tasks []types.Task, insertAtTop bool) (int64, error query := `INSERT INTO task (summary, context, active, created_at, updated_at) VALUES ` - values := make([]interface{}, 0, len(tasks)*4) + values := make([]any, 0, len(tasks)*4) for i, t := range tasks { if i > 0 { @@ -329,6 +334,52 @@ LIMIT ?; return tasks, nil } +func FetchNthActiveTask(db *sql.DB, index uint64) (types.Task, bool, error) { + var zero types.Task + + // QueryRow always returns a non-nil value + row := db.QueryRow(` +SELECT + id, + summary, + active, + context, + created_at, + updated_at +FROM + task +WHERE + id = ( + SELECT + json_extract(sequence, '$[' || ? || ']') + FROM + task_sequence + WHERE + id = 1 + ); +`, index) + + if row.Err() != nil { + return zero, false, fmt.Errorf("%w, %s", errCouldntExecuteQuery, row.Err().Error()) + } + + var task types.Task + err := row.Scan(&task.ID, + &task.Summary, + &task.Active, + &task.Context, + &task.CreatedAt, + &task.UpdatedAt, + ) + if errors.Is(err, sql.ErrNoRows) { + return zero, false, nil + } else if err != nil { + return zero, true, err + } + + return task, true, nil +} + func FetchInActiveTasks(db *sql.DB, limit int) ([]types.Task, error) { var tasks []types.Task diff --git a/main.go b/main.go index 05f29c2..0073c8f 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "os" "runtime/debug" - "github.com/dhth/omm/cmd" + "github.com/dhth/omm/internal/cmd" ) var version = "dev" From 260ecec111bb285b3569e2c5e7ddc0dc47824351 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:56:47 +0200 Subject: [PATCH 02/10] use correct command --- .github/workflows/back-compat-pr.yml | 2 +- .github/workflows/run.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/back-compat-pr.yml b/.github/workflows/back-compat-pr.yml index 5f14b9c..e9e07f4 100644 --- a/.github/workflows/back-compat-pr.yml +++ b/.github/workflows/back-compat-pr.yml @@ -45,5 +45,5 @@ jobs: - name: Run current version run: | /var/tmp/omm_head --db-path=/var/tmp/throwaway.db 'test: a task from PR HEAD' - /var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks + /var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks list ./.github/scripts/checknumtasks.sh "$(/var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks | wc -l | xargs)" 2 diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 5270d55..6287cb6 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -33,5 +33,5 @@ jobs: run: | cat assets/sample-tasks.txt | ./omm import ./omm 'test: a task' - ./omm tasks + ./omm tasks list ./.github/scripts/checknumtasks.sh "$(./omm tasks | wc -l | xargs)" 11 From e655d1fd313ed3e480022c357d231db71882ba63 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:00:47 +0200 Subject: [PATCH 03/10] use correct command --- .github/workflows/back-compat-pr.yml | 2 +- .github/workflows/run.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/back-compat-pr.yml b/.github/workflows/back-compat-pr.yml index e9e07f4..a4c1d28 100644 --- a/.github/workflows/back-compat-pr.yml +++ b/.github/workflows/back-compat-pr.yml @@ -46,4 +46,4 @@ jobs: run: | /var/tmp/omm_head --db-path=/var/tmp/throwaway.db 'test: a task from PR HEAD' /var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks list - ./.github/scripts/checknumtasks.sh "$(/var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks | wc -l | xargs)" 2 + ./.github/scripts/checknumtasks.sh "$(/var/tmp/omm_head --db-path=/var/tmp/throwaway.db tasks list | wc -l | xargs)" 2 diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 6287cb6..5ad871a 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -34,4 +34,4 @@ jobs: cat assets/sample-tasks.txt | ./omm import ./omm 'test: a task' ./omm tasks list - ./.github/scripts/checknumtasks.sh "$(./omm tasks | wc -l | xargs)" 11 + ./.github/scripts/checknumtasks.sh "$(./omm tasks list | wc -l | xargs)" 11 From 965b3c541538e637a8fbdc340c790bffa4f8d666 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:44:36 +0200 Subject: [PATCH 04/10] add integration tests for tasks cmd --- .github/workflows/back-compat-pr.yml | 3 - .github/workflows/back-compat.yml | 3 - .github/workflows/build.yml | 10 +- .github/workflows/lint.yml | 3 - .github/workflows/run.yml | 3 - .github/workflows/test.yml | 30 +++++ .github/workflows/vulncheck.yml | 3 - go.mod | 2 +- {assets => tests/cli/assets}/sample-tasks.txt | 0 tests/cli/common.go | 41 +++++++ tests/cli/root_cmd_test.go | 38 ++++++ tests/cli/tasks_cmd_test.go | 113 ++++++++++++++++++ 12 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/test.yml rename {assets => tests/cli/assets}/sample-tasks.txt (100%) create mode 100644 tests/cli/common.go create mode 100644 tests/cli/root_cmd_test.go create mode 100644 tests/cli/tasks_cmd_test.go diff --git a/.github/workflows/back-compat-pr.yml b/.github/workflows/back-compat-pr.yml index a4c1d28..503fdda 100644 --- a/.github/workflows/back-compat-pr.yml +++ b/.github/workflows/back-compat-pr.yml @@ -7,9 +7,6 @@ on: - "**/*.go" - ".github/workflows/back-compat-pr.yml" -permissions: - contents: read - env: GO_VERSION: '1.24.4' diff --git a/.github/workflows/back-compat.yml b/.github/workflows/back-compat.yml index 751aa19..51e7f36 100644 --- a/.github/workflows/back-compat.yml +++ b/.github/workflows/back-compat.yml @@ -4,9 +4,6 @@ on: push: branches: ["main"] -permissions: - contents: read - env: GO_VERSION: '1.24.4' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ead017c..99b9143 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,15 +9,15 @@ on: - "**/*.go" - ".github/workflows/build.yml" -permissions: - contents: read - env: GO_VERSION: '1.24.4' jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Go @@ -26,5 +26,3 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: go build run: go build -v ./... - - name: go test - run: go test -v ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8440891..b44075f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,9 +9,6 @@ on: - "**/*.go" - ".github/workflows/lint.yml" -permissions: - contents: read - env: GO_VERSION: '1.24.4' diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 5ad871a..aaa4cf9 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -9,9 +9,6 @@ on: - "**/*.go" - ".github/workflows/run.yml" -permissions: - contents: read - env: GO_VERSION: '1.24.4' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0ce8859 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: test + +on: + push: + branches: ["main"] + pull_request: + paths: + - "go.*" + - "**/*.go" + - ".github/workflows/test.yml" + +env: + GO_VERSION: '1.24.4' + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: go test + env: + INTEGRATION: "1" + run: go test -v ./... diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index 0b69aaa..d93bac7 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -8,9 +8,6 @@ on: - "**/*.go" - ".github/workflows/vulncheck.yml" -permissions: - contents: read - env: GO_VERSION: '1.24.4' diff --git a/go.mod b/go.mod index ae7aef5..a73ebcd 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/dustin/go-humanize v1.0.1 + github.com/google/uuid v1.6.0 github.com/muesli/termenv v0.16.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 @@ -32,7 +33,6 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/assets/sample-tasks.txt b/tests/cli/assets/sample-tasks.txt similarity index 100% rename from assets/sample-tasks.txt rename to tests/cli/assets/sample-tasks.txt diff --git a/tests/cli/common.go b/tests/cli/common.go new file mode 100644 index 0000000..f7c30a1 --- /dev/null +++ b/tests/cli/common.go @@ -0,0 +1,41 @@ +package cli + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" +) + +var ( + errCouldntCreateTempTestDir = errors.New("couldn't create temporary test directory") + errCouldntBuildBinary = errors.New("couldn't build binary") +) + +func skipIntegration(t *testing.T) { + t.Helper() + if os.Getenv("INTEGRATION") != "1" { + t.Skip("Skipping integration tests") + } +} + +func setUpTestBinary() (string, string, error) { + var zero string + tempDir, err := os.MkdirTemp("", "") + if err != nil { + return zero, zero, fmt.Errorf("%w: %s", errCouldntCreateTempTestDir, err.Error()) + } + + binPath := filepath.Join(tempDir, "omm") + buildArgs := []string{"build", "-o", binPath, "../.."} + + c := exec.Command("go", buildArgs...) + err = c.Run() + if err != nil { + return zero, zero, fmt.Errorf("%w: %s", errCouldntBuildBinary, err.Error()) + } + + return tempDir, binPath, nil +} diff --git a/tests/cli/root_cmd_test.go b/tests/cli/root_cmd_test.go new file mode 100644 index 0000000..05c19c8 --- /dev/null +++ b/tests/cli/root_cmd_test.go @@ -0,0 +1,38 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRootCmd(t *testing.T) { + skipIntegration(t) + + tempDir, binPath, err := setUpTestBinary() + if err != nil { + require.NoErrorf(t, err, "error setting up test binary: %w", err) + } + + defer func() { + err := os.RemoveAll(tempDir) + if err != nil { + fmt.Printf("couldn't clean up temporary directory (%s): %s", binPath, err) + } + }() + + // SUCCESSES + t.Run("Help", func(t *testing.T) { + // GIVEN + // WHEN + c := exec.Command(binPath, "-h") + err := c.Run() + + // THEN + assert.NoError(t, err) + }) +} diff --git a/tests/cli/tasks_cmd_test.go b/tests/cli/tasks_cmd_test.go new file mode 100644 index 0000000..a38d7c9 --- /dev/null +++ b/tests/cli/tasks_cmd_test.go @@ -0,0 +1,113 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTasksCmd(t *testing.T) { + skipIntegration(t) + + tempDir, binPath, err := setUpTestBinary() + if err != nil { + require.NoErrorf(t, err, "error setting up test binary: %w", err) + } + + defer func() { + err := os.RemoveAll(tempDir) + if err != nil { + t.Fatalf("couldn't clean up temporary directory (%s): %s", binPath, err) + } + }() + + //-------------// + // SUCCESSES // + //-------------// + + t.Run("Help", func(t *testing.T) { + // GIVEN + // WHEN + c := exec.Command(binPath, "tasks", "-h") + err := c.Run() + + // THEN + assert.NoError(t, err) + }) + + t.Run("Listing tasks works", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + numTasks := 10 + for i := range numTasks { + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 9-i)) + err := c.Run() + require.NoError(t, err) + } + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "list") + output, err := c.CombinedOutput() + + // THEN + require.NoError(t, err) + + numLines := len(strings.Split(strings.TrimSpace(string(output)), "\n")) + assert.Equal(t, numTasks, numLines) + }) + + t.Run("Showing task details for a valid index works", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + numTasks := 10 + for i := range numTasks { + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 9-i)) + err := c.Run() + require.NoError(t, err) + } + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "2") + output, err := c.CombinedOutput() + + // THEN + require.NoError(t, err) + + assert.Equal(t, "prefix: task 2", strings.TrimSpace(string(output))) + }) + + //------------// + // FAILURES // + //------------// + + t.Run("Showing task details for an invalid index fails", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + numTasks := 10 + for i := range numTasks { + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 9-i)) + err := c.Run() + require.NoError(t, err) + } + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "10") + output, err := c.CombinedOutput() + + // THEN + assert.Error(t, err) + + assert.Contains(t, string(output), "no task exists at given index") + }) +} From b428091ae11be3ef6b583b9f0ea6a092e1b1dc68 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:00:59 +0200 Subject: [PATCH 05/10] fix file path --- {tests/cli/assets => assets}/sample-tasks.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests/cli/assets => assets}/sample-tasks.txt (100%) diff --git a/tests/cli/assets/sample-tasks.txt b/assets/sample-tasks.txt similarity index 100% rename from tests/cli/assets/sample-tasks.txt rename to assets/sample-tasks.txt From ce3ecbbfc93df812da9546049f6d9edac1ea6147 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:46:41 +0200 Subject: [PATCH 06/10] allow showing task details in JSON format --- internal/cmd/root.go | 14 +++++++-- internal/cmd/tasks.go | 25 +++++++++++---- internal/cmd/types.go | 23 ++++++++++++++ internal/types/types.go | 12 +++---- tests/cli/tasks_cmd_test.go | 63 ++++++++++++++++++++++++++++++++++--- 5 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 internal/cmd/types.go diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 83fa1af..90f27af 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -51,6 +51,7 @@ var ( errCouldntOpenDB = errors.New("couldn't open database") errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") errInvalidValueForTaskIndexProvided = errors.New("invalid value for task index provided") + errInvalidShowTaskOutputFmtProvided = errors.New("invalid value for output format provided") //go:embed assets/CHANGELOG.md updateContents string @@ -144,6 +145,7 @@ func NewRootCommand() (*cobra.Command, error) { confirmBeforeDeletion bool circularNav bool numListTasks uint16 + showTaskOutputFmtStr string ) rootCmd := &cobra.Command{ @@ -374,11 +376,16 @@ Sorry for breaking the upgrade step! Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { index, err := strconv.ParseUint(args[0], 10, 0) - if err != nil { - return fmt.Errorf("%w; value needs to be an integer >= 0", errInvalidValueForTaskIndexProvided) + if err != nil || index == 0 { + return fmt.Errorf("%w; value needs to be an integer >= 1", errInvalidValueForTaskIndexProvided) + } + + format, ok := parseShowTaskOutputFormat(showTaskOutputFmtStr) + if !ok { + return fmt.Errorf("%w; allowed values: %v", errInvalidShowTaskOutputFmtProvided, showTaskOutputFormats()) } - return showTask(db, index, os.Stdout) + return showTask(db, index, format, os.Stdout) }, } @@ -472,6 +479,7 @@ Sorry for breaking the upgrade step! tasksCmd.PersistentFlags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) listTasksCmd.Flags().Uint16VarP(&numListTasks, "num", "n", numListTasksDefault, "number of tasks to list") + showTaskCmd.Flags().StringVarP(&showTaskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", showTaskOutputFormats())) importCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) importCmd.Flags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) diff --git a/internal/cmd/tasks.go b/internal/cmd/tasks.go index cc48382..167d600 100644 --- a/internal/cmd/tasks.go +++ b/internal/cmd/tasks.go @@ -2,6 +2,7 @@ package cmd import ( "database/sql" + "encoding/json" "errors" "fmt" "io" @@ -10,7 +11,10 @@ import ( pers "github.com/dhth/omm/internal/persistence" ) -var errNoTaskAtIndex = errors.New("no task exists at given index") +var ( + errNoTaskAtIndex = errors.New("no task exists at given index") + errCouldntMarshalTaskToJSON = errors.New("couldn't marshall task to JSON") +) func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { tasks, err := pers.FetchActiveTasks(db, int(limit)) @@ -26,8 +30,8 @@ func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { return nil } -func showTask(db *sql.DB, index uint64, writer io.Writer) error { - task, found, err := pers.FetchNthActiveTask(db, index) +func showTask(db *sql.DB, index uint64, format showTaskOutputFormat, writer io.Writer) error { + task, found, err := pers.FetchNthActiveTask(db, index-1) if err != nil { return err } @@ -36,9 +40,18 @@ func showTask(db *sql.DB, index uint64, writer io.Writer) error { return errNoTaskAtIndex } - fmt.Fprintln(writer, task.Summary) - if task.Context != nil { - fmt.Fprintf(writer, "\n%s", *task.Context) + switch format { + case taskOutputPlain: + fmt.Fprintln(writer, task.Summary) + if task.Context != nil { + fmt.Fprintf(writer, "\n%s", *task.Context) + } + case taskOutputJSON: + data, err := json.MarshalIndent(task, "", " ") + if err != nil { + return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) + } + fmt.Fprintf(writer, "%s\n", data) } return nil } diff --git a/internal/cmd/types.go b/internal/cmd/types.go new file mode 100644 index 0000000..6e9c1ce --- /dev/null +++ b/internal/cmd/types.go @@ -0,0 +1,23 @@ +package cmd + +type showTaskOutputFormat uint8 + +const ( + taskOutputPlain showTaskOutputFormat = iota + taskOutputJSON +) + +func showTaskOutputFormats() []string { + return []string{"plain", "json"} +} + +func parseShowTaskOutputFormat(value string) (showTaskOutputFormat, bool) { + switch value { + case "plain": + return taskOutputPlain, true + case "json": + return taskOutputJSON, true + default: + return taskOutputPlain, false + } +} diff --git a/internal/types/types.go b/internal/types/types.go index 6666895..26a3d82 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -44,12 +44,12 @@ type TaskDetails struct { } type Task struct { - ID uint64 - Summary string - Context *string - Active bool - CreatedAt time.Time - UpdatedAt time.Time + ID uint64 `json:"-"` + Summary string `json:"summary"` + Context *string `json:"context"` + Active bool `json:"-"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } func (t Task) GetDetails() TaskDetails { diff --git a/tests/cli/tasks_cmd_test.go b/tests/cli/tasks_cmd_test.go index a38d7c9..eb2f927 100644 --- a/tests/cli/tasks_cmd_test.go +++ b/tests/cli/tasks_cmd_test.go @@ -71,13 +71,13 @@ func TestTasksCmd(t *testing.T) { numTasks := 10 for i := range numTasks { - c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 9-i)) + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 10-i)) err := c.Run() require.NoError(t, err) } // WHEN - c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "2") + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "2", "-f", "plain") output, err := c.CombinedOutput() // THEN @@ -86,23 +86,64 @@ func TestTasksCmd(t *testing.T) { assert.Equal(t, "prefix: task 2", strings.TrimSpace(string(output))) }) + t.Run("Showing task details in json format works", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + numTasks := 10 + for i := range numTasks { + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 10-i)) + err := c.Run() + require.NoError(t, err) + } + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "2", "-f", "json") + output, err := c.CombinedOutput() + + // THEN + require.NoError(t, err) + + expected := ` +{ + "summary": "prefix: task 2", + "context": null +} +` + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output))) + }) + //------------// // FAILURES // //------------// + t.Run("Showing task details for index 0 fails", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "0") + output, err := c.CombinedOutput() + + // THEN + assert.Error(t, err) + + assert.Contains(t, string(output), "invalid value for task index provided;") + }) + t.Run("Showing task details for an invalid index fails", func(t *testing.T) { // GIVEN dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) numTasks := 10 for i := range numTasks { - c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 9-i)) + c := exec.Command(binPath, "-d", dbPath, fmt.Sprintf("prefix: task %d", 10-i)) err := c.Run() require.NoError(t, err) } // WHEN - c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "10") + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "11") output, err := c.CombinedOutput() // THEN @@ -110,4 +151,18 @@ func TestTasksCmd(t *testing.T) { assert.Contains(t, string(output), "no task exists at given index") }) + + t.Run("Showing task details with an invalid output format fails", func(t *testing.T) { + // GIVEN + dbPath := filepath.Join(tempDir, fmt.Sprintf("omm-%s.db", uuid.New().String())) + + // WHEN + c := exec.Command(binPath, "-d", dbPath, "tasks", "show", "1", "-f", "unknown") + output, err := c.CombinedOutput() + + // THEN + assert.Error(t, err) + + assert.Contains(t, string(output), "invalid value for output format provided;") + }) } From 49e3a5c72c1d7cdc412d6f71a0c5086c1d254439 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sun, 8 Jun 2025 01:23:15 +0200 Subject: [PATCH 07/10] allow searching tasks --- internal/cmd/root.go | 14 +++++++++ internal/cmd/tasks.go | 27 ++++++++++++++--- internal/persistence/queries.go | 54 +++++++++++++++++++++++++++++++++ internal/types/types.go | 10 +++--- 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 90f27af..6cc7007 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -146,6 +146,8 @@ func NewRootCommand() (*cobra.Command, error) { circularNav bool numListTasks uint16 showTaskOutputFmtStr string + numSearchTasks uint16 + searchTasksActive bool ) rootCmd := &cobra.Command{ @@ -370,6 +372,15 @@ Sorry for breaking the upgrade step! }, } + searchTasksCmd := &cobra.Command{ + Use: "search ", + Short: "Search tasks", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return searchTasks(db, args[0], searchTasksActive, numSearchTasks, os.Stdout) + }, + } + showTaskCmd := &cobra.Command{ Use: "show ", Short: "Show task", @@ -480,6 +491,8 @@ Sorry for breaking the upgrade step! listTasksCmd.Flags().Uint16VarP(&numListTasks, "num", "n", numListTasksDefault, "number of tasks to list") showTaskCmd.Flags().StringVarP(&showTaskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", showTaskOutputFormats())) + searchTasksCmd.Flags().Uint16VarP(&numSearchTasks, "num", "n", numListTasksDefault, "number of tasks to list") + searchTasksCmd.Flags().BoolVar(&searchTasksActive, "active", true, "active status to use as a filter") importCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) importCmd.Flags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) @@ -487,6 +500,7 @@ Sorry for breaking the upgrade step! guideCmd.Flags().StringVar(&editorFlagInp, "editor", "vi", "editor command to run when adding/editing context to a task") tasksCmd.AddCommand(listTasksCmd) + tasksCmd.AddCommand(searchTasksCmd) tasksCmd.AddCommand(showTaskCmd) rootCmd.AddCommand(importCmd) diff --git a/internal/cmd/tasks.go b/internal/cmd/tasks.go index 167d600..4f99a02 100644 --- a/internal/cmd/tasks.go +++ b/internal/cmd/tasks.go @@ -14,6 +14,7 @@ import ( var ( errNoTaskAtIndex = errors.New("no task exists at given index") errCouldntMarshalTaskToJSON = errors.New("couldn't marshall task to JSON") + errCouldntSearchTasks = errors.New("couldn't search tasks") ) func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { @@ -40,18 +41,36 @@ func showTask(db *sql.DB, index uint64, format showTaskOutputFormat, writer io.W return errNoTaskAtIndex } + taskDetails := task.GetDetails() + switch format { case taskOutputPlain: - fmt.Fprintln(writer, task.Summary) - if task.Context != nil { - fmt.Fprintf(writer, "\n%s", *task.Context) + fmt.Fprintln(writer, taskDetails.Summary) + if taskDetails.Context != nil { + fmt.Fprintf(writer, "\n%s", *taskDetails.Context) } case taskOutputJSON: - data, err := json.MarshalIndent(task, "", " ") + data, err := json.MarshalIndent(taskDetails, "", " ") if err != nil { return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) } fmt.Fprintf(writer, "%s\n", data) } + + return nil +} + +func searchTasks(db *sql.DB, query string, active bool, limit uint16, writer io.Writer) error { + tasks, err := pers.FetchTasksThatMatchQuery(db, query, active, limit) + if err != nil { + return fmt.Errorf("%w: %s", errCouldntSearchTasks, err.Error()) + } + + data, err := json.MarshalIndent(tasks, "", " ") + if err != nil { + return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) + } + fmt.Fprintf(writer, "%s\n", data) + return nil } diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index a9b768e..92745a2 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -334,6 +334,60 @@ LIMIT ?; return tasks, nil } +func FetchTasksThatMatchQuery(db *sql.DB, query string, active bool, limit uint16) ([]types.Task, error) { + var tasks []types.Task + + searchTerm := fmt.Sprintf("%%%s%%", query) + rows, err := db.Query(` +SELECT + t.id, + t.summary, + t.context, + t.created_at, + t.updated_at +FROM + task t +WHERE + ( + t.summary LIKE ? + OR t.context LIKE ? + ) + AND t.active IS ? +ORDER BY + t.updated_at DESC +LIMIT + ?; +`, searchTerm, searchTerm, active, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var entry types.Task + err = rows.Scan(&entry.ID, + &entry.Summary, + &entry.Context, + &entry.CreatedAt, + &entry.UpdatedAt, + ) + if err != nil { + return nil, err + } + entry.CreatedAt = entry.CreatedAt.Local() + entry.UpdatedAt = entry.UpdatedAt.Local() + entry.Active = true + tasks = append(tasks, entry) + + } + err = rows.Err() + if err != nil { + return nil, err + } + + return tasks, nil +} + func FetchNthActiveTask(db *sql.DB, index uint64) (types.Task, bool, error) { var zero types.Task diff --git a/internal/types/types.go b/internal/types/types.go index 26a3d82..87b6058 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -39,17 +39,17 @@ var ( ) type TaskDetails struct { - Summary string - Context *string + Summary string `json:"summary"` + Context *string `json:"context"` } type Task struct { ID uint64 `json:"-"` Summary string `json:"summary"` Context *string `json:"context"` - Active bool `json:"-"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created-at"` + UpdatedAt time.Time `json:"updated-at"` } func (t Task) GetDetails() TaskDetails { From 277416718d72b9404485aa2124385eee00c9b34a Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sun, 8 Jun 2025 12:07:00 +0200 Subject: [PATCH 08/10] add pagination to task commands --- internal/cmd/root.go | 32 +++++++++++++++++++---------- internal/cmd/tasks.go | 36 ++++++++++++++++++++++++--------- internal/cmd/types.go | 8 ++++---- internal/persistence/queries.go | 32 ++++++++++++++++++----------- internal/ui/cmds.go | 4 ++-- 5 files changed, 74 insertions(+), 38 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 6cc7007..c6eda5e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -51,7 +51,7 @@ var ( errCouldntOpenDB = errors.New("couldn't open database") errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") errInvalidValueForTaskIndexProvided = errors.New("invalid value for task index provided") - errInvalidShowTaskOutputFmtProvided = errors.New("invalid value for output format provided") + errInvalidTaskOutputFmtProvided = errors.New("invalid value for output format provided") //go:embed assets/CHANGELOG.md updateContents string @@ -144,9 +144,9 @@ func NewRootCommand() (*cobra.Command, error) { showContextFlagInp bool confirmBeforeDeletion bool circularNav bool - numListTasks uint16 - showTaskOutputFmtStr string - numSearchTasks uint16 + taskOutputFmtStr string + tasksLimit uint16 + tasksOffset uint16 searchTasksActive bool ) @@ -368,7 +368,7 @@ Sorry for breaking the upgrade step! Use: "list", Short: "List tasks", RunE: func(_ *cobra.Command, _ []string) error { - return printTasks(db, numListTasks, os.Stdout) + return listTasks(db, tasksLimit, tasksOffset, os.Stdout) }, } @@ -377,7 +377,12 @@ Sorry for breaking the upgrade step! Short: "Search tasks", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { - return searchTasks(db, args[0], searchTasksActive, numSearchTasks, os.Stdout) + format, ok := parseTaskOutputFormat(taskOutputFmtStr) + if !ok { + return fmt.Errorf("%w; allowed values: %v", errInvalidTaskOutputFmtProvided, taskOutputFormats()) + } + + return searchTasks(db, args[0], format, searchTasksActive, tasksLimit, tasksOffset, os.Stdout) }, } @@ -391,9 +396,9 @@ Sorry for breaking the upgrade step! return fmt.Errorf("%w; value needs to be an integer >= 1", errInvalidValueForTaskIndexProvided) } - format, ok := parseShowTaskOutputFormat(showTaskOutputFmtStr) + format, ok := parseTaskOutputFormat(taskOutputFmtStr) if !ok { - return fmt.Errorf("%w; allowed values: %v", errInvalidShowTaskOutputFmtProvided, showTaskOutputFormats()) + return fmt.Errorf("%w; allowed values: %v", errInvalidTaskOutputFmtProvided, taskOutputFormats()) } return showTask(db, index, format, os.Stdout) @@ -489,9 +494,14 @@ Sorry for breaking the upgrade step! tasksCmd.PersistentFlags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) tasksCmd.PersistentFlags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) - listTasksCmd.Flags().Uint16VarP(&numListTasks, "num", "n", numListTasksDefault, "number of tasks to list") - showTaskCmd.Flags().StringVarP(&showTaskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", showTaskOutputFormats())) - searchTasksCmd.Flags().Uint16VarP(&numSearchTasks, "num", "n", numListTasksDefault, "number of tasks to list") + listTasksCmd.Flags().Uint16VarP(&tasksLimit, "limit", "l", numListTasksDefault, "number of tasks to list") + listTasksCmd.Flags().Uint16VarP(&tasksOffset, "offset", "o", 0, "offset to use") + + showTaskCmd.Flags().StringVarP(&taskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", taskOutputFormats())) + + searchTasksCmd.Flags().StringVarP(&taskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", taskOutputFormats())) + searchTasksCmd.Flags().Uint16VarP(&tasksLimit, "limit", "l", numListTasksDefault, "number of tasks to list") + searchTasksCmd.Flags().Uint16VarP(&tasksOffset, "offset", "o", 0, "offset to use") searchTasksCmd.Flags().BoolVar(&searchTasksActive, "active", true, "active status to use as a filter") importCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) diff --git a/internal/cmd/tasks.go b/internal/cmd/tasks.go index 4f99a02..228c9ae 100644 --- a/internal/cmd/tasks.go +++ b/internal/cmd/tasks.go @@ -17,11 +17,16 @@ var ( errCouldntSearchTasks = errors.New("couldn't search tasks") ) -func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { - tasks, err := pers.FetchActiveTasks(db, int(limit)) +func listTasks(db *sql.DB, limit, offset uint16, writer io.Writer) error { + tasks, err := pers.FetchActiveTasks(db, limit, offset) if err != nil { return err } + + if len(tasks) == 0 { + return nil + } + summaries := make([]string, len(tasks)) for i, task := range tasks { summaries[i] = task.Summary @@ -31,7 +36,7 @@ func printTasks(db *sql.DB, limit uint16, writer io.Writer) error { return nil } -func showTask(db *sql.DB, index uint64, format showTaskOutputFormat, writer io.Writer) error { +func showTask(db *sql.DB, index uint64, format taskOutputFormat, writer io.Writer) error { task, found, err := pers.FetchNthActiveTask(db, index-1) if err != nil { return err @@ -60,17 +65,30 @@ func showTask(db *sql.DB, index uint64, format showTaskOutputFormat, writer io.W return nil } -func searchTasks(db *sql.DB, query string, active bool, limit uint16, writer io.Writer) error { - tasks, err := pers.FetchTasksThatMatchQuery(db, query, active, limit) +func searchTasks(db *sql.DB, query string, format taskOutputFormat, active bool, limit, offset uint16, writer io.Writer) error { + tasks, err := pers.FetchTasksThatMatchQuery(db, query, active, limit, offset) if err != nil { return fmt.Errorf("%w: %s", errCouldntSearchTasks, err.Error()) } - data, err := json.MarshalIndent(tasks, "", " ") - if err != nil { - return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) + if len(tasks) == 0 { + return nil + } + + switch format { + case taskOutputPlain: + summaries := make([]string, len(tasks)) + for i, task := range tasks { + summaries[i] = task.Summary + } + fmt.Fprintln(writer, strings.Join(summaries, "\n")) + case taskOutputJSON: + data, err := json.MarshalIndent(tasks, "", " ") + if err != nil { + return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) + } + fmt.Fprintf(writer, "%s\n", data) } - fmt.Fprintf(writer, "%s\n", data) return nil } diff --git a/internal/cmd/types.go b/internal/cmd/types.go index 6e9c1ce..2354c7c 100644 --- a/internal/cmd/types.go +++ b/internal/cmd/types.go @@ -1,17 +1,17 @@ package cmd -type showTaskOutputFormat uint8 +type taskOutputFormat uint8 const ( - taskOutputPlain showTaskOutputFormat = iota + taskOutputPlain taskOutputFormat = iota taskOutputJSON ) -func showTaskOutputFormats() []string { +func taskOutputFormats() []string { return []string{"plain", "json"} } -func parseShowTaskOutputFormat(value string) (showTaskOutputFormat, bool) { +func parseTaskOutputFormat(value string) (taskOutputFormat, bool) { switch value { case "plain": return taskOutputPlain, true diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index 92745a2..e2bbc84 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -293,17 +293,25 @@ WHERE id = ? return nil } -func FetchActiveTasks(db *sql.DB, limit int) ([]types.Task, error) { +func FetchActiveTasks(db *sql.DB, limit, offset uint16) ([]types.Task, error) { var tasks []types.Task rows, err := db.Query(` -SELECT t.id, t.summary, t.context, t.created_at, t.updated_at -FROM task_sequence s -JOIN json_each(s.sequence) j ON CAST(j.value AS INTEGER) = t.id -JOIN task t ON t.id = j.value -ORDER BY j.key -LIMIT ?; -`, limit) +SELECT + t.id, + t.summary, + t.context, + t.created_at, + t.updated_at +FROM + task_sequence s + JOIN json_each(s.sequence) j ON CAST(j.value AS INTEGER) = t.id + JOIN task t ON t.id = j.value +ORDER BY + j.key +LIMIT + ? OFFSET ?; +`, limit, offset) if err != nil { return nil, err } @@ -334,7 +342,7 @@ LIMIT ?; return tasks, nil } -func FetchTasksThatMatchQuery(db *sql.DB, query string, active bool, limit uint16) ([]types.Task, error) { +func FetchTasksThatMatchQuery(db *sql.DB, query string, active bool, limit, offset uint16) ([]types.Task, error) { var tasks []types.Task searchTerm := fmt.Sprintf("%%%s%%", query) @@ -356,8 +364,8 @@ WHERE ORDER BY t.updated_at DESC LIMIT - ?; -`, searchTerm, searchTerm, active, limit) + ? OFFSET ?; +`, searchTerm, searchTerm, active, limit, offset) if err != nil { return nil, err } @@ -434,7 +442,7 @@ WHERE return task, true, nil } -func FetchInActiveTasks(db *sql.DB, limit int) ([]types.Task, error) { +func FetchInActiveTasks(db *sql.DB, limit uint16) ([]types.Task, error) { var tasks []types.Task rows, err := db.Query(` diff --git a/internal/ui/cmds.go b/internal/ui/cmds.go index 958e2df..ef8b032 100644 --- a/internal/ui/cmds.go +++ b/internal/ui/cmds.go @@ -76,13 +76,13 @@ func changeTaskStatus(db *sql.DB, listIndex int, id uint64, active bool, updated } } -func fetchTasks(db *sql.DB, active bool, limit int) tea.Cmd { +func fetchTasks(db *sql.DB, active bool, limit uint16) tea.Cmd { return func() tea.Msg { var tasks []types.Task var err error switch active { case true: - tasks, err = pers.FetchActiveTasks(db, limit) + tasks, err = pers.FetchActiveTasks(db, limit, 0) case false: tasks, err = pers.FetchInActiveTasks(db, limit) } From 8cd70d0e4284b89ce7dd59380598b875ac3d591d Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sun, 8 Jun 2025 12:36:06 +0200 Subject: [PATCH 09/10] add status filter to list command --- internal/cmd/root.go | 60 +++++++++++++++++--------- internal/cmd/tasks.go | 39 +++++++++++++---- internal/persistence/queries.go | 76 ++++++++++++++++++++++++++++++--- internal/types/types.go | 29 ++++++++++++- 4 files changed, 168 insertions(+), 36 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index c6eda5e..1f29e5f 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -38,20 +38,21 @@ const ( ) var ( - errCouldntGetHomeDir = errors.New("couldn't get home directory") - errConfigFileExtIncorrect = errors.New("config file must be a TOML file") - errConfigFileDoesntExist = errors.New("config file does not exist") - errDBFileExtIncorrect = errors.New("db file needs to end with .db") - errMaxImportLimitExceeded = errors.New("import limit exceeded") - errNothingToImport = errors.New("nothing to import") - errListDensityIncorrect = errors.New("list density is incorrect; valid values: compact/spacious") - errCouldntCreateDBDirectory = errors.New("couldn't create directory for database") - errCouldntCreateDB = errors.New("couldn't create database") - errCouldntInitializeDB = errors.New("couldn't initialize database") - errCouldntOpenDB = errors.New("couldn't open database") - errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") - errInvalidValueForTaskIndexProvided = errors.New("invalid value for task index provided") - errInvalidTaskOutputFmtProvided = errors.New("invalid value for output format provided") + errCouldntGetHomeDir = errors.New("couldn't get home directory") + errConfigFileExtIncorrect = errors.New("config file must be a TOML file") + errConfigFileDoesntExist = errors.New("config file does not exist") + errDBFileExtIncorrect = errors.New("db file needs to end with .db") + errMaxImportLimitExceeded = errors.New("import limit exceeded") + errNothingToImport = errors.New("nothing to import") + errListDensityIncorrect = errors.New("list density is incorrect; valid values: compact/spacious") + errCouldntCreateDBDirectory = errors.New("couldn't create directory for database") + errCouldntCreateDB = errors.New("couldn't create database") + errCouldntInitializeDB = errors.New("couldn't initialize database") + errCouldntOpenDB = errors.New("couldn't open database") + errCouldntSetupGuide = errors.New("couldn't set up guided walkthrough") + errInvalidTaskIndexProvided = errors.New("invalid task index provided") + errInvalidTaskOutputFmtProvided = errors.New("invalid output format provided") + errInvalidTaskStatusFilterProvided = errors.New("invalid status filter provided") //go:embed assets/CHANGELOG.md updateContents string @@ -147,7 +148,7 @@ func NewRootCommand() (*cobra.Command, error) { taskOutputFmtStr string tasksLimit uint16 tasksOffset uint16 - searchTasksActive bool + statusFilterStr string ) rootCmd := &cobra.Command{ @@ -368,13 +369,23 @@ Sorry for breaking the upgrade step! Use: "list", Short: "List tasks", RunE: func(_ *cobra.Command, _ []string) error { - return listTasks(db, tasksLimit, tasksOffset, os.Stdout) + format, ok := parseTaskOutputFormat(taskOutputFmtStr) + if !ok { + return fmt.Errorf("%w; allowed values: %v", errInvalidTaskOutputFmtProvided, taskOutputFormats()) + } + + statusFilter, ok := types.ParseTaskStatusFilter(statusFilterStr) + if !ok { + return fmt.Errorf("%w; allowed values: %v", errInvalidTaskStatusFilterProvided, types.TaskStatusFilterValues()) + } + + return listTasks(db, statusFilter, tasksLimit, tasksOffset, format, os.Stdout) }, } searchTasksCmd := &cobra.Command{ Use: "search ", - Short: "Search tasks", + Short: "Search tasks based on a query", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { format, ok := parseTaskOutputFormat(taskOutputFmtStr) @@ -382,18 +393,23 @@ Sorry for breaking the upgrade step! return fmt.Errorf("%w; allowed values: %v", errInvalidTaskOutputFmtProvided, taskOutputFormats()) } - return searchTasks(db, args[0], format, searchTasksActive, tasksLimit, tasksOffset, os.Stdout) + statusFilter, ok := types.ParseTaskStatusFilter(statusFilterStr) + if !ok { + return fmt.Errorf("%w; allowed values: %v", errInvalidTaskStatusFilterProvided, types.TaskStatusFilterValues()) + } + + return searchTasks(db, args[0], statusFilter, tasksLimit, tasksOffset, format, os.Stdout) }, } showTaskCmd := &cobra.Command{ Use: "show ", - Short: "Show task", + Short: "Show details for the task at a specific position in omm's list", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { index, err := strconv.ParseUint(args[0], 10, 0) if err != nil || index == 0 { - return fmt.Errorf("%w; value needs to be an integer >= 1", errInvalidValueForTaskIndexProvided) + return fmt.Errorf("%w; value needs to be an integer >= 1", errInvalidTaskIndexProvided) } format, ok := parseTaskOutputFormat(taskOutputFmtStr) @@ -494,15 +510,17 @@ Sorry for breaking the upgrade step! tasksCmd.PersistentFlags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) tasksCmd.PersistentFlags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) + listTasksCmd.Flags().StringVarP(&taskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", taskOutputFormats())) listTasksCmd.Flags().Uint16VarP(&tasksLimit, "limit", "l", numListTasksDefault, "number of tasks to list") listTasksCmd.Flags().Uint16VarP(&tasksOffset, "offset", "o", 0, "offset to use") + listTasksCmd.Flags().StringVarP(&statusFilterStr, "status", "s", "active", fmt.Sprintf("status to use as a filter; possible values: %v", types.TaskStatusFilterValues())) showTaskCmd.Flags().StringVarP(&taskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", taskOutputFormats())) searchTasksCmd.Flags().StringVarP(&taskOutputFmtStr, "format", "f", "plain", fmt.Sprintf("output format to use, possible values: %v", taskOutputFormats())) searchTasksCmd.Flags().Uint16VarP(&tasksLimit, "limit", "l", numListTasksDefault, "number of tasks to list") searchTasksCmd.Flags().Uint16VarP(&tasksOffset, "offset", "o", 0, "offset to use") - searchTasksCmd.Flags().BoolVar(&searchTasksActive, "active", true, "active status to use as a filter") + searchTasksCmd.Flags().StringVarP(&statusFilterStr, "status", "s", "active", fmt.Sprintf("status to use as a filter; possible values: %v", types.TaskStatusFilterValues())) importCmd.Flags().StringVarP(&configPath, "config-path", "c", defaultConfigPath, fmt.Sprintf("location of omm's TOML config file%s", configPathAdditionalCxt)) importCmd.Flags().StringVarP(&dbPath, "db-path", "d", defaultDBPath, fmt.Sprintf("location of omm's database file%s", dbPathAdditionalCxt)) diff --git a/internal/cmd/tasks.go b/internal/cmd/tasks.go index 228c9ae..1f37664 100644 --- a/internal/cmd/tasks.go +++ b/internal/cmd/tasks.go @@ -9,6 +9,7 @@ import ( "strings" pers "github.com/dhth/omm/internal/persistence" + "github.com/dhth/omm/internal/types" ) var ( @@ -17,8 +18,14 @@ var ( errCouldntSearchTasks = errors.New("couldn't search tasks") ) -func listTasks(db *sql.DB, limit, offset uint16, writer io.Writer) error { - tasks, err := pers.FetchActiveTasks(db, limit, offset) +func listTasks(db *sql.DB, + statusFilter types.TaskStatusFilter, + limit, + offset uint16, + format taskOutputFormat, + writer io.Writer, +) error { + tasks, err := pers.FetchTasks(db, statusFilter, limit, offset) if err != nil { return err } @@ -27,12 +34,21 @@ func listTasks(db *sql.DB, limit, offset uint16, writer io.Writer) error { return nil } - summaries := make([]string, len(tasks)) - for i, task := range tasks { - summaries[i] = task.Summary + switch format { + case taskOutputPlain: + summaries := make([]string, len(tasks)) + for i, task := range tasks { + summaries[i] = task.Summary + } + fmt.Fprintln(writer, strings.Join(summaries, "\n")) + case taskOutputJSON: + data, err := json.MarshalIndent(tasks, "", " ") + if err != nil { + return fmt.Errorf("%w: %s", errCouldntMarshalTaskToJSON, err.Error()) + } + fmt.Fprintf(writer, "%s\n", data) } - fmt.Fprintln(writer, strings.Join(summaries, "\n")) return nil } @@ -65,8 +81,15 @@ func showTask(db *sql.DB, index uint64, format taskOutputFormat, writer io.Write return nil } -func searchTasks(db *sql.DB, query string, format taskOutputFormat, active bool, limit, offset uint16, writer io.Writer) error { - tasks, err := pers.FetchTasksThatMatchQuery(db, query, active, limit, offset) +func searchTasks(db *sql.DB, + query string, + statusFilter types.TaskStatusFilter, + limit, + offset uint16, + format taskOutputFormat, + writer io.Writer, +) error { + tasks, err := pers.FetchTasksThatMatchQuery(db, query, statusFilter, limit, offset) if err != nil { return fmt.Errorf("%w: %s", errCouldntSearchTasks, err.Error()) } diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index e2bbc84..b140f42 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -342,15 +342,81 @@ LIMIT return tasks, nil } -func FetchTasksThatMatchQuery(db *sql.DB, query string, active bool, limit, offset uint16) ([]types.Task, error) { +func FetchTasks(db *sql.DB, statusFilter types.TaskStatusFilter, limit, offset uint16) ([]types.Task, error) { var tasks []types.Task + var statusFilterStr string + switch statusFilter { + case types.TaskStatusActive: + statusFilterStr = "WHERE t.active IS true" + case types.TaskStatusInactive: + statusFilterStr = "WHERE t.active IS false" + } + + rows, err := db.Query(fmt.Sprintf(` +SELECT + t.id, + t.summary, + t.context, + t.active, + t.created_at, + t.updated_at +FROM + task t + %s +ORDER BY + t.updated_at DESC +LIMIT + ? OFFSET ?; +`, statusFilterStr), limit, offset) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var entry types.Task + err = rows.Scan(&entry.ID, + &entry.Summary, + &entry.Context, + &entry.Active, + &entry.CreatedAt, + &entry.UpdatedAt, + ) + if err != nil { + return nil, err + } + entry.CreatedAt = entry.CreatedAt.Local() + entry.UpdatedAt = entry.UpdatedAt.Local() + tasks = append(tasks, entry) + + } + err = rows.Err() + if err != nil { + return nil, err + } + + return tasks, nil +} + +func FetchTasksThatMatchQuery(db *sql.DB, query string, statusFilter types.TaskStatusFilter, limit, offset uint16) ([]types.Task, error) { + var tasks []types.Task + + var statusFilterStr string + switch statusFilter { + case types.TaskStatusActive: + statusFilterStr = "AND t.active IS true" + case types.TaskStatusInactive: + statusFilterStr = "AND t.active IS false" + } + searchTerm := fmt.Sprintf("%%%s%%", query) - rows, err := db.Query(` + rows, err := db.Query(fmt.Sprintf(` SELECT t.id, t.summary, t.context, + t.active, t.created_at, t.updated_at FROM @@ -360,12 +426,12 @@ WHERE t.summary LIKE ? OR t.context LIKE ? ) - AND t.active IS ? + %s ORDER BY t.updated_at DESC LIMIT ? OFFSET ?; -`, searchTerm, searchTerm, active, limit, offset) +`, statusFilterStr), searchTerm, searchTerm, limit, offset) if err != nil { return nil, err } @@ -376,6 +442,7 @@ LIMIT err = rows.Scan(&entry.ID, &entry.Summary, &entry.Context, + &entry.Active, &entry.CreatedAt, &entry.UpdatedAt, ) @@ -384,7 +451,6 @@ LIMIT } entry.CreatedAt = entry.CreatedAt.Local() entry.UpdatedAt = entry.UpdatedAt.Local() - entry.Active = true tasks = append(tasks, entry) } diff --git a/internal/types/types.go b/internal/types/types.go index 87b6058..afa34c7 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -48,8 +48,8 @@ type Task struct { Summary string `json:"summary"` Context *string `json:"context"` Active bool `json:"active"` - CreatedAt time.Time `json:"created-at"` - UpdatedAt time.Time `json:"updated-at"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } func (t Task) GetDetails() TaskDetails { @@ -189,3 +189,28 @@ func (p TaskPrefix) Description() string { func (p TaskPrefix) FilterValue() string { return string(p) } + +type TaskStatusFilter uint8 + +const ( + TaskStatusActive TaskStatusFilter = iota + TaskStatusInactive + TaskStatusAny +) + +func TaskStatusFilterValues() []string { + return []string{"active", "inactive", "any"} +} + +func ParseTaskStatusFilter(value string) (TaskStatusFilter, bool) { + switch value { + case "active": + return TaskStatusActive, true + case "inactive": + return TaskStatusInactive, true + case "any": + return TaskStatusAny, true + default: + return TaskStatusAny, false + } +} From f775c28cc67a99e7b2ff3e0d7011530bf0a0f25b Mon Sep 17 00:00:00 2001 From: Dhruv Thakur <13575379+dhth@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:44:59 +0200 Subject: [PATCH 10/10] fix tests --- tests/cli/tasks_cmd_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli/tasks_cmd_test.go b/tests/cli/tasks_cmd_test.go index eb2f927..6f3e674 100644 --- a/tests/cli/tasks_cmd_test.go +++ b/tests/cli/tasks_cmd_test.go @@ -128,7 +128,7 @@ func TestTasksCmd(t *testing.T) { // THEN assert.Error(t, err) - assert.Contains(t, string(output), "invalid value for task index provided;") + assert.Contains(t, string(output), "invalid task index provided;") }) t.Run("Showing task details for an invalid index fails", func(t *testing.T) { @@ -163,6 +163,6 @@ func TestTasksCmd(t *testing.T) { // THEN assert.Error(t, err) - assert.Contains(t, string(output), "invalid value for output format provided;") + assert.Contains(t, string(output), "invalid output format provided;") }) }