diff --git a/cmd/assets/guide/actions-adding-context.md b/cmd/assets/guide/actions-adding-context.md new file mode 100644 index 0000000..f0b44f4 --- /dev/null +++ b/cmd/assets/guide/actions-adding-context.md @@ -0,0 +1,8 @@ +As mentioned before, once a task is created, you might want to add context to +it. + +You do that by pressing `c`. Go ahead, try it out. Try changing the text, and +then save the file. This context text should get updated accordingly. + +Once saved, you can also copy a tasks's context to your system clipboard by +pressing `y`. diff --git a/cmd/assets/guide/actions-adding-tasks.md b/cmd/assets/guide/actions-adding-tasks.md new file mode 100644 index 0000000..45acd02 --- /dev/null +++ b/cmd/assets/guide/actions-adding-tasks.md @@ -0,0 +1,13 @@ +Let's get to the crux of omm: **adding** and **prioritizing** tasks. We'll begin +with adding tasks. + +You can add a task below the cursor by pressing `a`. Once you get acquainted +with omm, you'll want to have more control on the position of the newly added +task. omm offers the following keymaps for that. + + o/a add task below cursor + O add task above cursor + I add task at the top + A add task at the end + +Go ahead, create a task, then move to the next guided item. diff --git a/cmd/assets/guide/actions-filtering-tasks.md b/cmd/assets/guide/actions-filtering-tasks.md new file mode 100644 index 0000000..96d9c51 --- /dev/null +++ b/cmd/assets/guide/actions-filtering-tasks.md @@ -0,0 +1,8 @@ +You can filter tasks in a list by pressing `/`. Doing this will open up a search +prompt, which will match your query with task prefixes. + +Try it out now. You get out of the filtered state by pressing `q/esc/`. + +Note: You cannot add tasks or move them around in a filtered state. But, you can +move a task to the top of the list (by pressing `⏎`). Doing this will also get +you out of the filtered state. diff --git a/cmd/assets/guide/actions-quick-filtering-via-a-list.md b/cmd/assets/guide/actions-quick-filtering-via-a-list.md new file mode 100644 index 0000000..94910e3 --- /dev/null +++ b/cmd/assets/guide/actions-quick-filtering-via-a-list.md @@ -0,0 +1,10 @@ +You can also choose the prefix you want to filter by with the means of a list, +hereby called as the **Quick Filter List**. Press `ctrl+p` to open up a set of +task prefixes contained in the currently active task list. Press `⏎` to +pre-populate the task list's search prompt with your selection. + +Try it out now. + +Note: Both the **Active Tasks List** and **Archived Tasks List** can be filtered +separately, using either the manual filtering approach or via the **Quick Filter +List**. diff --git a/cmd/assets/guide/actions-updating-task-details.md b/cmd/assets/guide/actions-updating-task-details.md new file mode 100644 index 0000000..2b623e3 --- /dev/null +++ b/cmd/assets/guide/actions-updating-task-details.md @@ -0,0 +1,11 @@ +Once a task is created, its summary and context can be changed at any point. + +You can update a task's summary by pressing `u`. + +This will open up the the same prompt you saw when creating a new task, with the +only difference that the task's summary will be pre-filled for you. This can +come in handy when you want to quickly jot down a task for yourself (either by +using the TUI, or by using the CLI (eg. `omm 'a hastily written task +summary'`)), and then come back to it later to refine it more. + +Similarly, you can also update a task's context any time (by pressing `c`). diff --git a/cmd/assets/guide/cli-adding-a-task-via-the-cli.md b/cmd/assets/guide/cli-adding-a-task-via-the-cli.md new file mode 100644 index 0000000..b90967e --- /dev/null +++ b/cmd/assets/guide/cli-adding-a-task-via-the-cli.md @@ -0,0 +1,7 @@ +You can also add a task to omm via its command line interface. For example: + +```bash +omm 'prefix: a task summary' +``` + +This will add an entry to the top of the active tasks list. diff --git a/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md b/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md new file mode 100644 index 0000000..686021e --- /dev/null +++ b/cmd/assets/guide/cli-importing-several-tasks-via-the-cli.md @@ -0,0 +1,12 @@ +You can also import more than one task at a time by using the `import` +subcommand. For example: + +```bash +cat << 'EOF' | omm import +orders: order new ACME rocket skates +traps: draw fake tunnel on the canyon wall +tech: assemble ACME jet-propelled pogo stick +EOF +``` + +omm will expect each line in stdin to hold one task's summary. diff --git a/cmd/config.go b/cmd/assets/guide/config-a-sample-toml-config.md similarity index 60% rename from cmd/config.go rename to cmd/assets/guide/config-a-sample-toml-config.md index 5feb76e..192286a 100644 --- a/cmd/config.go +++ b/cmd/assets/guide/config-a-sample-toml-config.md @@ -1,12 +1,11 @@ -package cmd +Here's a sample TOML configuration file: -var ( - sampleCfg = `db_path = "~/.local/share/omm/omm-w.db" +```toml +db_path = "~/.local/share/omm/omm-w.db" tl_color = "#b8bb26" atl_color = "#fabd2f" title = "work" list_density = "spacious" show_context = false editor = "vi -u NONE" -` -) +``` diff --git a/cmd/assets/guide/config-changing-the-defaults.md b/cmd/assets/guide/config-changing-the-defaults.md new file mode 100644 index 0000000..4325650 --- /dev/null +++ b/cmd/assets/guide/config-changing-the-defaults.md @@ -0,0 +1,10 @@ +omm allows you to change the some of its behavior via configuration, which it +will consider in the order listed below: + +- CLI flags (run `omm -h` to see details) +- Environment variables (eg. `$OMM_EDITOR`) +- A TOML configuration file (run `omm -h` to see where this lives; you can + change this via the flag `--config-path`) + +omm will consider configuration in the order laid out above, ie, CLI flags will +take the highest priority. diff --git a/cmd/assets/guide/config-flags-env-vars-and-config-file.md b/cmd/assets/guide/config-flags-env-vars-and-config-file.md new file mode 100644 index 0000000..98ba772 --- /dev/null +++ b/cmd/assets/guide/config-flags-env-vars-and-config-file.md @@ -0,0 +1,9 @@ +Every flag listed by `omm -h` (except `--config-path`) has an environment +variable counterpart, as well as a TOML config counterpart. + +For example: + +```text +--show-context -> OMM_SHOW_CONTEXT -> show_context +--editor -> OMM_EDITOR -> editor +``` diff --git a/cmd/assets/guide/domain-an-archived-task.md b/cmd/assets/guide/domain-an-archived-task.md new file mode 100644 index 0000000..848d82c --- /dev/null +++ b/cmd/assets/guide/domain-an-archived-task.md @@ -0,0 +1,9 @@ +This is the archived list, meaning it holds tasks that are no longer being +worked on. + +omm provides this list both for historical reference, as well as for you to be +able to move an archived task back into the active list. + +You can toggle the state of a task using ``. + +Press `tab/q/esc/` to go back to the active list. diff --git a/cmd/assets/guide/domain-task-bookmarks.md b/cmd/assets/guide/domain-task-bookmarks.md new file mode 100644 index 0000000..c4a7029 --- /dev/null +++ b/cmd/assets/guide/domain-task-bookmarks.md @@ -0,0 +1,14 @@ +Sometimes you'll add URLs to a task's summary or its context. + +Such URLs (eg. https://github.com/dhth/omm, https://tools.dhruvs.space, +https://c.xkcd.com/random/comic) could be placed anywhere in the +summary/context. + +omm lets you open these URLs via a single keypress. You can either press `b` to +open up a list of all URLs, and then open one of them by pressing `⏎` or open +all of them by pressing `B`. + +Note: If a task has a single URL added to it, pressing `b` will skip showing the +list, and open the URL directly. + +Try both approaches now. diff --git a/cmd/assets/guide/domain-task-details.md b/cmd/assets/guide/domain-task-details.md new file mode 100644 index 0000000..8b4e4b1 --- /dev/null +++ b/cmd/assets/guide/domain-task-details.md @@ -0,0 +1,11 @@ +The **Task Details** pane is intended for when you want to read all the details +associated with a task in a full screen view. + +You can view this pane by pressing `d`. This pane is useful when a task's +context is too long to fit in the context pane. + +Whilst in this pane, you can move backwards and forwards in the task list by +pressing `h/←/→/l`. You quit out of this pane by either pressing `d` again, or +`q/esc/`. + +Try it out. Come back to this entry when you're done. diff --git a/cmd/assets/guide/domain-task-priorities.md b/cmd/assets/guide/domain-task-priorities.md new file mode 100644 index 0000000..21e26ff --- /dev/null +++ b/cmd/assets/guide/domain-task-priorities.md @@ -0,0 +1,13 @@ +At its core, omm is a dynamic list that maintains a sequence of tasks based on +the priorities you assign them. + +And, as we all know, priorities often change. You're probably juggling multiple +tasks on any given day. As such, omm allows you to move tasks around in the +priority order. It has the following keymaps to achieve this: + + ⏎ move task to the top + J move task one position down + K move task one position up + +It's recommended that you move the task that you're currently focussing on to +the top. diff --git a/cmd/assets/guide/domain-task-state.md b/cmd/assets/guide/domain-task-state.md new file mode 100644 index 0000000..42f9a62 --- /dev/null +++ b/cmd/assets/guide/domain-task-state.md @@ -0,0 +1,9 @@ +A task can be in one of two states: **active** or **archived**. + +This list shows active tasks. + +To be pedantic about things, only the tasks in the active list are supposed to +be "on your mind". However, there are benefits to having a list of archived +tasks as well. + +Press `` to see the archived list. diff --git a/cmd/assets/guide/domain-tasks.md b/cmd/assets/guide/domain-tasks.md new file mode 100644 index 0000000..4e2c5e0 --- /dev/null +++ b/cmd/assets/guide/domain-tasks.md @@ -0,0 +1,10 @@ +omm (**on-my-mind**) is a task manager. You can also think of it as a keyboard +driven to-do list. + +As such, tasks are at the core of omm. A task can be anything that you want to +keep track of, ideally something that is concrete and has a clear definition of +done. + +Tasks in omm have a one liner summary, and optionally, some context associated +with them (like this paragraph). You can choose to add context to a task when +you want to save details that don't fit in a single line. diff --git a/cmd/assets/guide/guide-and-thats-it.md b/cmd/assets/guide/guide-and-thats-it.md new file mode 100644 index 0000000..1f8f115 --- /dev/null +++ b/cmd/assets/guide/guide-and-thats-it.md @@ -0,0 +1,7 @@ +That's it for the walkthrough! + +I hope omm proves to be a useful tool for your task management needs. If you +find any bugs in it, or have feature requests, feel free to submit them at +https://github.com/dhth/omm/issues. + +Happy task managing! 👋 diff --git a/cmd/assets/guide/guide-welcome-to-omm.md b/cmd/assets/guide/guide-welcome-to-omm.md new file mode 100644 index 0000000..37b31a4 --- /dev/null +++ b/cmd/assets/guide/guide-welcome-to-omm.md @@ -0,0 +1,10 @@ +Hi there 👋 Thanks for trying out **omm**. + +This is a guided walkthrough to get you acquainted with omm's features. + +Before we begin, let's get the basics out of the way: you exit omm by pressing +`q/esc/`. These keys also move you back menus/panes whilst using omm's +TUI. + +Onwards with the walkthrough then! Simply press `j/↓`, and follow the +instructions. diff --git a/cmd/assets/guide/visuals-list-density.md b/cmd/assets/guide/visuals-list-density.md new file mode 100644 index 0000000..6170a81 --- /dev/null +++ b/cmd/assets/guide/visuals-list-density.md @@ -0,0 +1,12 @@ +omm's task lists can be viewed in two density modes: **compact** and +**spacious**. + +This is the compact mode. As opposed to this, the spacious mode shows tasks in a +more roomier list, alongside highlighting prefixes (we'll see what that means), +and showing creation timestamps. + +omm starts up with spacious mode by default (you can change this, as we'll see +soon). You can toggle between the two modes by pressing `v`. Choose whichever +mode fits your workflow better. + +Try it out. Come back to this mode once you're done. diff --git a/cmd/assets/guide/visuals-toggling-context-pane.md b/cmd/assets/guide/visuals-toggling-context-pane.md new file mode 100644 index 0000000..f0cf414 --- /dev/null +++ b/cmd/assets/guide/visuals-toggling-context-pane.md @@ -0,0 +1,8 @@ +The context pane can be toggled on/off by pressing `C`. + +You can choose to display it or not based on your preference. For convenience, +the lists will always highlight tasks that have a context associated with them +by having a **(c)** marker on them. + +omm starts up with the context pane hidden by default (you can change this, as +we'll see soon). diff --git a/cmd/db_migrations.go b/cmd/db_migrations.go index 9bc228e..2df12e0 100644 --- a/cmd/db_migrations.go +++ b/cmd/db_migrations.go @@ -12,7 +12,7 @@ const ( ) var ( - dbDowngradedErr = errors.New(`Looks like you downgraded omm. You should either delete omm's + errDBDowngraded = errors.New(`Looks like you downgraded omm. You should either delete omm's database file (you will lose data by doing that), or upgrade omm to the latest version.`) ) @@ -67,7 +67,7 @@ Error: %s`, } if latestVersionInDB.version > latestDBVersion { - return dbDowngradedErr + return errDBDowngraded } if latestVersionInDB.version < latestDBVersion { diff --git a/cmd/guide.go b/cmd/guide.go index 125bd41..bc166ea 100644 --- a/cmd/guide.go +++ b/cmd/guide.go @@ -2,291 +2,94 @@ package cmd import ( "database/sql" + "embed" "fmt" + "regexp" + "strings" "time" pers "github.com/dhth/omm/internal/persistence" "github.com/dhth/omm/internal/types" ) -type entry struct { - summary string - ctx string - active bool -} - -func insertGuideTasks(db *sql.DB) error { - var entries = []entry{ - { - "guide: welcome to omm", - `Hi there 👋 Thanks for trying out omm. - -This is a guided walkthrough to get you acquainted with omm's features. - -Before we begin, let's get the basics out of the way: you exit omm by pressing -q/esc/ctrl+c. These keys also move you back menus/panes whilst using omm's TUI. - -Onwards with the walkthrough then! Simply press j/↓, and follow the -instructions. -`, - true, - }, - { - "domain: tasks", - `omm ("on-my-mind") is a task manager. You can also think of it as a keyboard -driven to-do list. - -As such, tasks are at the core of omm. A task can be anything that you want to -keep track of, ideally something that is concrete and has a clear definition of -done. - -Tasks in omm have a one liner summary, and optionally, some context associated -with them (like this paragraph). You can choose to add context to a task when -you want to save details that don't fit in a single line. -`, - true, - }, - { - "domain: task state", - `A task can be in one of two states: active or archived. - -This list shows active tasks. - -To be pedantic about things, only the tasks in the active list are supposed to -be "on your mind". However, there are benefits to having a list of archived -tasks as well. - -Press to see the archived list. -`, - true, - }, - { - "domain: task details", - `The "Task Details" pane is intended for when you simply want to read all the -details associated with a task in a full screen view. - -You can view this pane by pressing "d". - -Whilst in this pane, you can move backwards and forwards in the -task list by pressing "h/<-/->/l". You quit out of this pane by either pressing -"d" again, or q/esc/ctrl+c. - -Try it out. Come back to this entry when you're done. - -You might have to view this guide in the "Task Details" pane if you're on a -smaller display at the moment. -`, - true, - }, - { - "domain: an archived task", - `This is the archived list, meaning it holds tasks that are no longer being -worked on. - -omm provides this list both for historical reference, as well as for you to be -able to move an archived task back into the active list. - -You can toggle the state of a task using . - -Press tab/q/esc/ctrl+c to go back to the active list. -`, - false, - }, - { - "visuals: list density", - `omm's task lists can be viewed in two density modes: compact and spacious. - -This is the compact mode. As opposed to this, the spacious mode shows tasks in a -more roomier list, alongside highlighting prefixes (we'll see what that means), -and showing creation timestamps. Since the list in this mode takes more space, -the context pane is shorter than the one in the compact mode. - -omm starts up with compact mode by default (you can change this, as we'll see -soon). You can toggle between the two modes by pressing "v". Choose whichever -mode fits your workflow better. - -Try it out. Come back to this mode once you're done. -`, - true, - }, - { - "visuals: toggling context pane", - `The context pane can be toggled on/off by pressing "C". - -You can choose to display it or not based on your preference. For convenience, -the lists will always highlight tasks that have a context associated with them -by having a "(c)" marker on them. -`, - true, - }, - { - "actions: adding tasks", - `Let's get to the crux of omm: adding and prioritizing tasks. - -We'll begin with adding tasks. You can add a task below the cursor by pressing -"a". - -Once you get acquainted with omm, you'll want to have more control on the -position of the newly added task. omm offers the following keymaps for that. - - o/a add task below cursor - O add task above cursor - I add task at the top - A add task at the end - -Go ahead, create a task, then move to the next guided item. -`, - true, - }, - { - "actions: adding tasks via the CLI", - `You can also add a task to omm via its command line interface. For example: - -omm 'prefix: a task summary' - -This will add an entry to the top of the active tasks list. - -You can also import more than one task at a time by using the "import" -subcommand. For example: - -cat << 'EOF' | omm import -orders: order new ACME rocket skates -traps: draw fake tunnel on the canyon wall -tech: assemble ACME jet-propelled pogo stick -EOF - -omm will expect each line in stdin to hold one task's summary. -`, - true, - }, - { - "actions: adding context", - `As mentioned before, once a task is created, you might want to add context to -it. - -You do that by pressing "c". Go ahead, try it out. Try changing the text, and -then save the file. This context text should get updated accordingly. - -Once saved, you can also copy a tasks's context to your system clipboard by pressing "y". -`, - true, - }, - { - "domain: task bookmarks", - `Sometimes you'll add URLs to a task's summary or its context. - -Such URLs (eg. https://github.com/dhth/omm, https://tools.dhruvs.space, -https://c.xkcd.com/random/comic) could be placed anywhere in the summary/ -context. - -omm lets you open these URLs via a single keypress. You can either press "b" to -open up a list of all URLs, and then open one of them by pressing ⏎, or open all -of them by pressing "B". - -Note: if the task has a single URL added to it, pressing "b" will skip showing -the list, and open the URL directly. - -Try both approaches now. Press "b", interact with the list, and then press "B". -`, - true, - }, - { - "domain: task priorities", - `At its core, omm is a dynamic list that maintains a sequence of tasks based on -the priorities you assign them. - -And, as we all know, priorities often change. You're probably juggling multiple -tasks on any given day. As such, omm allows you to move tasks around in the -priority order. It has the following keymaps to achieve this: - - ⏎ move task to the top - [2-9] move task at index [x] to top (only in compact view) - J move task one position down - K move task one position up - -It's recommended that you move the task that you're currently focussing on to -the top. -`, - true, - }, - { - "actions: updating task details", - `Once a task is created, its summary and context can be changed at any point. +const ( + guideAssetsPathPrefix = "assets/guide" +) -You can update a task's summary by pressing "u". +var ( + //go:embed assets/guide/*.md + guideFolder embed.FS + + guideSummaryRegex = regexp.MustCompile(`[^a-z-]`) + + guideEntries = []entry{ + {summary: "guide: welcome to omm"}, + {summary: "domain: tasks"}, + {summary: "domain: task state"}, + {summary: "domain: an archived task", archived: true}, + {summary: "domain: task details"}, + {summary: "visuals: list density"}, + {summary: "visuals: toggling context pane"}, + {summary: "actions: adding tasks"}, + {summary: "cli: adding a task via the CLI"}, + {summary: "cli: importing several tasks via the CLI"}, + {summary: "actions: adding context"}, + {summary: "actions: filtering tasks"}, + {summary: "actions: quick filtering via a list"}, + {summary: "domain: task bookmarks"}, + {summary: "domain: task priorities"}, + {summary: "actions: updating task details"}, + {summary: "config: changing the defaults"}, + {summary: "config: flags, env vars, and config file"}, + {summary: "config: a sample TOML config"}, + {summary: "guide: and that's it!"}, + } +) -This will open up the the same prompt you saw when creating a new task, with the -only difference that the task's summary will be pre-filled for you. This can -come in handy when you want to quickly jot down a task for yourself (either by -using the TUI, or by using the CLI (eg. "omm 'a hastily written task summary")), -and then come back to it later to refine it more. +type entry struct { + summary string + archived bool +} -Similarly, you can also update a task's context any time (by pressing "c"). -`, - true, - }, - { - "config: changing the defaults", - `omm allows you to change the some of its behavior via configuration, which it -will consider in the order listed below: +func getContext(summary string) (string, error) { + summary = strings.ToLower(summary) + summary = strings.ReplaceAll(summary, " ", "-") + fPath := guideSummaryRegex.ReplaceAllString(summary, "") -- CLI flags (run "omm -h" to see details) -- Environment variables (eg. "OMM_EDITOR") -- A TOML configuration file (run "omm -h" to see where this lives; you can - change this via the flag "--config-path") + ctxBytes, err := guideFolder.ReadFile(fmt.Sprintf("%s/%s.md", guideAssetsPathPrefix, fPath)) + if err != nil { + return "", err + } -omm will consider configuration in the order laid out above, ie, CLI flags will -take the highest priority. -`, - true, - }, - { - "config: flags, env vars, and config file", - `Every flag listed by "omm -h" (except "--config-path") has an environment -variable counterpart, as well as a TOML config counterpart. + return string(ctxBytes), nil +} -For example: +func insertGuideTasks(db *sql.DB) error { ---show-context -> OMM_SHOW_CONTEXT -> show_context ---editor -> OMM_EDITOR -> editor -`, - true, - }, - { - "config: a sample TOML config", - fmt.Sprintf(`Here's a sample TOML configuration file: + tasks := make([]types.Task, len(guideEntries)) -%s`, sampleCfg), - true, - }, - { - "guide: and that's it!", - `That's it for the walkthrough! + now := time.Now() -I hope omm proves to be a useful tool for your task management needs. If you -find any bugs in it, or have feature requests, feel free to submit them at -https://github.com/dhth/omm/issues. + ctxs := make([]string, len(guideEntries)) -Happy task managing! 👋 -`, - true, - }, - } + var err error + for i, e := range guideEntries { + ctxs[i], err = getContext(guideEntries[i].summary) - tasks := make([]types.Task, len(entries)) + if err != nil { + continue + } - now := time.Now() - for i, e := range entries { tasks[i] = types.Task{ Summary: e.summary, - Context: &e.ctx, - Active: e.active, + Context: &ctxs[i], + Active: !e.archived, CreatedAt: now, UpdatedAt: now, } } - err := pers.InsertTasksIntoDB(db, tasks) + err = pers.InsertTasksIntoDB(db, tasks) return err } diff --git a/cmd/guide_test.go b/cmd/guide_test.go new file mode 100644 index 0000000..d85845b --- /dev/null +++ b/cmd/guide_test.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetContext(t *testing.T) { + for _, entry := range guideEntries { + got, err := getContext(entry.summary) + assert.Nil(t, err) + assert.NotEmpty(t, got) + } +} diff --git a/cmd/import.go b/cmd/import.go index 82d919e..a43f64a 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -9,7 +9,7 @@ import ( ) var ( - importWillExceedTaskLimitErr = fmt.Errorf("Import will exceed maximum number of tasks allowed, which is %d. Archive/Delete tasks that are not active using ctrl+d/ctrl+x.", pers.TaskNumLimit) + errImportWillExceedTaskLimit = fmt.Errorf("Import will exceed maximum number of tasks allowed, which is %d. Archive/Delete tasks that are not active using ctrl+d/ctrl+x.", pers.TaskNumLimit) ) func importTask(db *sql.DB, taskSummary string) error { @@ -18,7 +18,7 @@ func importTask(db *sql.DB, taskSummary string) error { return err } if numTasks+1 > pers.TaskNumLimit { - return importWillExceedTaskLimitErr + return errImportWillExceedTaskLimit } now := time.Now() @@ -31,7 +31,7 @@ func importTasks(db *sql.DB, taskSummaries []string) error { return err } if numTasks+len(taskSummaries) > pers.TaskNumLimit { - return importWillExceedTaskLimitErr + return errImportWillExceedTaskLimit } now := time.Now() diff --git a/cmd/root.go b/cmd/root.go index 74a124f..b14e1ee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,7 +15,6 @@ import ( pers "github.com/dhth/omm/internal/persistence" "github.com/dhth/omm/internal/types" "github.com/dhth/omm/internal/ui" - "github.com/dhth/omm/internal/utils" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -37,16 +36,14 @@ const ( ) var ( - configFileExtIncorrectErr = errors.New("config file must be a TOML file") - configFileDoesntExistErr = errors.New("config file does not exist") - dbFileExtIncorrectErr = errors.New("db file needs to end with .db") + 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") - maxImportLimitExceededErr = fmt.Errorf("Max number of tasks that can be imported at a time: %d", pers.TaskNumLimit) - nothingToImportErr = errors.New("Nothing to import") + errMaxImportLimitExceeded = fmt.Errorf("Max number of tasks that can be imported at a time: %d", pers.TaskNumLimit) + errNothingToImport = errors.New("Nothing to import") - listDensityIncorrectErr = errors.New("List density is incorrect; valid values: compact/spacious") - - taskSummaryTooLongErr = fmt.Errorf("Task summary is too long; max length allowed: %d", types.TaskSummaryMaxLen) + errListDensityIncorrect = errors.New("List density is incorrect; valid values: compact/spacious") ) func Execute(version string) { @@ -159,7 +156,7 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. configPathFull = expandTilde(configPath) if filepath.Ext(configPathFull) != ".toml" { - return configFileExtIncorrectErr + return errConfigFileExtIncorrect } _, err := os.Stat(configPathFull) @@ -167,7 +164,7 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. if fl != nil { cf := fl.Lookup("config-path") if cf != nil && cf.Changed && errors.Is(err, fs.ErrNotExist) { - return configFileDoesntExistErr + return errConfigFileDoesntExist } } @@ -186,7 +183,7 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. dbPathFull = expandTilde(dbPath) if filepath.Ext(dbPathFull) != ".db" { - return dbFileExtIncorrectErr + return errDBFileExtIncorrect } db, err = setupDB(dbPathFull) @@ -199,11 +196,12 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 0 { - if len(args[0]) > types.TaskSummaryMaxLen { - return taskSummaryTooLongErr + summaryValid, err := types.CheckIfTaskSummaryValid(args[0]) + if !summaryValid { + return err } - err := importTask(db, args[0]) + err = importTask(db, args[0]) if err != nil { return err } @@ -224,7 +222,7 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. case ui.SpaciousDensityVal: ld = ui.Spacious default: - return listDensityIncorrectErr + return errListDensityIncorrect } if len(taskListTitle) > taskListTitleMaxLen { @@ -260,23 +258,22 @@ Tip: Quickly add a task using 'omm "task summary goes here"'. scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { if taskCounter > pers.TaskNumLimit { - return maxImportLimitExceededErr + return errMaxImportLimitExceeded } line := scanner.Text() line = strings.TrimSpace(line) - if len(line) > types.TaskSummaryMaxLen { - line = utils.Trim(line, types.TaskSummaryMaxLen) - } - if line != "" { + summaryValid, _ := types.CheckIfTaskSummaryValid(line) + + if summaryValid { tasks = append(tasks, line) } taskCounter++ } if len(tasks) == 0 { - return nothingToImportErr + return errNothingToImport } err := importTasks(db, tasks) @@ -320,7 +317,7 @@ Error: %s`, author, repoIssuesUrl, guideErr) } config := ui.Config{ DBPath: dbPathFull, - ListDensity: ui.Compact, + ListDensity: ui.Spacious, TaskListColor: taskListColor, ArchivedTaskListColor: archivedTaskListColor, TaskListTitle: taskListTitle, @@ -377,9 +374,9 @@ Error: %s`, author, repoIssuesUrl, err) rootCmd.Flags().StringVar(&taskListColor, "tl-color", ui.TaskListColor, "hex color used for the task list") rootCmd.Flags().StringVar(&archivedTaskListColor, "atl-color", ui.ArchivedTLColor, "hex color used for the archived tasks list") rootCmd.Flags().StringVar(&taskListTitle, "title", ui.TaskListDefaultTitle, fmt.Sprintf("title of the task list, will trim till %d chars", taskListTitleMaxLen)) - rootCmd.Flags().StringVar(&listDensityFlagInp, "list-density", ui.CompactDensityVal, fmt.Sprintf("type of density for the list; possible values: [%s, %s]", ui.CompactDensityVal, ui.SpaciousDensityVal)) + rootCmd.Flags().StringVar(&listDensityFlagInp, "list-density", ui.SpaciousDensityVal, fmt.Sprintf("type of density for the list; possible values: [%s, %s]", ui.CompactDensityVal, ui.SpaciousDensityVal)) rootCmd.Flags().StringVar(&editorFlagInp, "editor", "vi", "editor command to run when adding/editing context to a task") - rootCmd.Flags().BoolVar(&showContextFlagInp, "show-context", true, "whether to start omm with a visible task context pane or not; this can later be toggled on/off in the TUI") + rootCmd.Flags().BoolVar(&showContextFlagInp, "show-context", false, "whether to start omm with a visible task context pane or not; this can later be toggled on/off in the TUI") 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)) diff --git a/go.mod b/go.mod index 18f18ed..7515d7c 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.26.6 + github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.11.0 github.com/dustin/go-humanize v1.0.1 + github.com/muesli/termenv v0.15.2 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 @@ -18,15 +20,19 @@ require ( ) require ( + github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.1.2 // indirect github.com/charmbracelet/x/input v0.1.2 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -35,12 +41,13 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -53,9 +60,12 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/go.sum b/go.sum index 14ae627..5cb879a 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,21 @@ +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= @@ -21,6 +31,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -35,10 +47,14 @@ github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlG github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -55,9 +71,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -70,6 +89,8 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -116,6 +137,11 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -124,6 +150,8 @@ golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mg golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/types/types.go b/internal/types/types.go index 2c1be82..7ac2486 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "hash/fnv" "strings" @@ -12,13 +13,14 @@ import ( ) const ( - timeFormat = "2006/01/02 15:04" - prefixDelimiter = ":" - prefixPadding = 80 - createdAtPadding = 40 - GOOSDarwin = "darwin" - taskSummaryWidth = 100 - TaskSummaryMaxLen = 300 + timeFormat = "2006/01/02 15:04" + PrefixDelimiter = ":" + compactPrefixPadding = 24 + spaciousPrefixPadding = 80 + createdAtPadding = 40 + GOOSDarwin = "darwin" + taskSummaryWidth = 120 + TaskSummaryMaxLen = 300 ) var ( @@ -45,9 +47,14 @@ var ( hasContextStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(hasContextColor)) + + ErrTaskPrefixEmpty = errors.New("Task prefix cannot be empty") + ErrTaskSummaryBodyEmpty = errors.New("Task summary body is empty") + ErrTaskSummaryTooLong = fmt.Errorf("Task summary is too long; max length allowed: %d", TaskSummaryMaxLen) ) type Task struct { + ItemTitle string ID uint64 Summary string Context *string @@ -58,12 +65,70 @@ type Task struct { type ContextBookmark string -func (t Task) Title() string { - summEls := strings.Split(t.Summary, prefixDelimiter) +type TaskPrefix string + +func (t Task) Prefix() (TaskPrefix, bool) { + summEls := strings.Split(t.Summary, PrefixDelimiter) + if len(summEls) > 1 { + // This shouldn't happen, but it's still good to check this to ensure + // the quick filter list doesn't misbehave + if strings.TrimSpace(summEls[0]) == "" { + return "", false + } + return TaskPrefix(strings.TrimSpace(summEls[0])), true + } + return "", false +} + +func CheckIfTaskSummaryValid(summary string) (bool, error) { + if strings.TrimSpace(summary) == "" { + return false, ErrTaskPrefixEmpty + } + + summEls := strings.Split(summary, PrefixDelimiter) + if len(summEls) > 1 { + if strings.TrimSpace(summEls[0]) == "" { + return false, ErrTaskPrefixEmpty + } + + if strings.TrimSpace(strings.Join(summEls[1:], PrefixDelimiter)) == "" { + return false, ErrTaskSummaryBodyEmpty + } + } + + return true, nil +} + +func (t *Task) SetTitle(compact bool) { + summEls := strings.Split(t.Summary, PrefixDelimiter) + + if compact { + var summ string + if len(summEls) > 1 { + prefix := utils.RightPadTrim(summEls[0], compactPrefixPadding, true) + summ = prefix + strings.Join(summEls[1:], PrefixDelimiter) + } else { + summ = t.Summary + } + + var hasContext string + if t.Context != nil { + hasContext = "(c)" + } + t.ItemTitle = fmt.Sprintf("%s%s", utils.RightPadTrim(summ, taskSummaryWidth, true), hasContext) + return + } + if len(summEls) == 1 { - return t.Summary + t.ItemTitle = t.Summary + return } - return utils.Trim(strings.TrimSpace(strings.Join(summEls[1:], prefixDelimiter)), taskSummaryWidth) + + t.ItemTitle = utils.Trim(strings.TrimSpace(strings.Join(summEls[1:], PrefixDelimiter)), taskSummaryWidth) +} + +func (t Task) Title() string { + return t.ItemTitle } func (t Task) Description() string { @@ -71,11 +136,11 @@ func (t Task) Description() string { var createdAt string var hasContext string - summEls := strings.Split(t.Summary, prefixDelimiter) + summEls := strings.Split(t.Summary, PrefixDelimiter) if len(summEls) > 1 { - prefix = getDynamicStyle(summEls[0]).Render(utils.RightPadTrim(summEls[0], prefixPadding, true)) + prefix = getDynamicStyle(summEls[0]).Render(utils.RightPadTrim(summEls[0], spaciousPrefixPadding, true)) } else { - prefix = strings.Repeat(" ", prefixPadding) + prefix = strings.Repeat(" ", spaciousPrefixPadding) } now := time.Now() @@ -94,7 +159,13 @@ func (t Task) Description() string { return fmt.Sprintf("%s%s%s", prefix, createdAt, hasContext) } -func (t Task) FilterValue() string { return t.Summary } +func (t Task) FilterValue() string { + p, ok := t.Prefix() + if ok { + return string(p) + } + return "" +} func getDynamicStyle(str string) lipgloss.Style { h := fnv.New32() @@ -117,3 +188,15 @@ func (c ContextBookmark) Description() string { func (c ContextBookmark) FilterValue() string { return string(c) } + +func (p TaskPrefix) Title() string { + return string(p) +} + +func (p TaskPrefix) Description() string { + return "" +} + +func (p TaskPrefix) FilterValue() string { + return string(p) +} diff --git a/internal/ui/assets/help.md b/internal/ui/assets/help.md new file mode 100644 index 0000000..4c75677 --- /dev/null +++ b/internal/ui/assets/help.md @@ -0,0 +1,72 @@ +## Overview + +**omm** ("on-my-mind") is a keyboard-driven task manager for the command line. + +Tip: Run `omm guide` for a guided walkthrough of omm's features. + +omm has 6 components: + +- Active Tasks List +- Archived Tasks List +- Task creation/update Pane +- Task Details Pane +- Task Bookmarks List +- Quick Filter List + +## Keymaps + +### General + + q/esc/ctrl+c go back + Q quit from anywhere + +### Active/Archived Tasks List + + j/↓ move cursor down + k/↑ move cursor up + h go to previous page + l go to next page + g go to the top + G go to the end + tab move between lists + C toggle showing context + d toggle Task Details pane + b open Task Bookmarks list + B open all bookmarks added to current task + c update context for a task + ctrl+d archive/unarchive task + ctrl+x delete task + ctrl+r reload task lists + / filter list by task prefix + ctrl+p open the "Quick Filter List" + y copy selected task's context to system clipboard + v toggle between compact and spacious view + +### Active Tasks List + + q/esc/ctrl+c quit + o/a add task below cursor + O add task above cursor + I add task at the top + A add task at the end + u update task summary + ⏎ move task to the top + J move task one position down + K move task one position up + +**Note**: Moving tasks around is not allowed when the active tasks list is in a +filtered state, however, you can still use `⏎` to move a task to the top. + +### Task Details Pane + + h/←/→/l move backwards/forwards when in the task details view + y copy current task's context to system clipboard + B open all bookmarks added to current task + +### Task Bookmarks List + + ⏎ open URL in browser + +### Quick Filter List + + ⏎ pre-populate task list's search prompt with chosen prefix diff --git a/internal/ui/help.go b/internal/ui/help.go deleted file mode 100644 index d4f1b47..0000000 --- a/internal/ui/help.go +++ /dev/null @@ -1,78 +0,0 @@ -package ui - -import "fmt" - -var ( - helpStr = fmt.Sprintf(`%s%s - -%s - -%s - -%s - -%s -%s - -%s -%s - -%s -%s - -%s -%s - -%s -%s -`, helpHeadingStyle.Render("omm"), - helpSectionStyle.Render(` ("on-my-mind") is a keyboard-driven task manager for the command line. - -Tip: Run "omm guide" for a guided walkthrough of omm's features.`), - helpHeadingStyle.Render("omm has 5 components"), - helpSectionStyle.Render(`1: Active Tasks List -2: Archived Tasks List -3: Task creation/update Pane -4: Task Details Pane -5: Task Bookmarks List`), - helpHeadingStyle.Render("Keymaps"), - helpSubHeadingStyle.Render("General"), - helpSectionStyle.Render(`q/esc/ctrl+c go back -Q quit from anywhere`), - helpSubHeadingStyle.Render("Active/Archived Tasks List"), - helpSectionStyle.Render(`j/↓ move cursor down -k/↑ move cursor up -h go to previous page -l go to next page -g go to the top -G go to the end -tab move between lists -C toggle showing context -d toggle Task Details pane -b open Task Bookmarks list -B open all bookmarks added to current task -c update context for a task -ctrl+d archive/unarchive task -ctrl+x delete task -ctrl+r reload task lists -y copy selected task's context to system clipboard -v toggle between compact and spacious view`), - helpSubHeadingStyle.Render("Active Tasks List"), - helpSectionStyle.Render(`q/esc/ctrl+c quit -o/a add task below cursor -O add task above cursor -I add task at the top -A add task at the end -u update task summary -⏎ move task to the top -[2-9] move task at index [x] to top (only in compact view) -J move task one position down -K move task one position up`), - helpSubHeadingStyle.Render("Task Details Pane"), - helpSectionStyle.Render(`h/l move backwards/forwards when in the task details view -y copy current task's context to system clipboard -B open all bookmarks added to current task`), - helpSubHeadingStyle.Render("Task Bookmarks List"), - helpSectionStyle.Render(`⏎ open URL in browser`), - ) -) diff --git a/internal/ui/initial.go b/internal/ui/initial.go index 6ff11c3..74d0d56 100644 --- a/internal/ui/initial.go +++ b/internal/ui/initial.go @@ -19,18 +19,19 @@ func InitialModel(db *sql.DB, config Config) model { var taskList list.Model switch config.ListDensity { case Compact: - taskList = list.New(taskItems, itemDelegate{selStyle: tlSelItemStyle}, taskSummaryWidth, compactListHeight) - taskList.SetShowStatusBar(false) + taskList = list.New(taskItems, newListDelegate(lipgloss.Color(config.TaskListColor), false, 1), taskSummaryWidth, defaultListHeight) case Spacious: - taskList = list.New(taskItems, newTaskListDelegate(lipgloss.Color(config.TaskListColor)), taskSummaryWidth, 14) - taskList.SetShowStatusBar(true) + taskList = list.New(taskItems, newListDelegate(lipgloss.Color(config.TaskListColor), true, 1), taskSummaryWidth, defaultListHeight) } - taskList.SetShowTitle(false) - taskList.SetFilteringEnabled(false) + taskList.Title = config.TaskListTitle + taskList.SetFilteringEnabled(true) + taskList.SetStatusBarItemName("task", "tasks") + taskList.SetShowStatusBar(true) taskList.SetShowHelp(false) taskList.DisableQuitKeybindings() taskList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") taskList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") + taskList.SetStatusBarItemName("task", "tasks") taskList.Styles.Title = taskList.Styles.Title. Foreground(lipgloss.Color(defaultBackgroundColor)). @@ -44,18 +45,19 @@ func InitialModel(db *sql.DB, config Config) model { var archivedTaskList list.Model switch config.ListDensity { case Compact: - archivedTaskList = list.New(archivedTaskItems, itemDelegate{selStyle: atlSelItemStyle}, taskSummaryWidth, compactListHeight) - archivedTaskList.SetShowStatusBar(false) + archivedTaskList = list.New(archivedTaskItems, newListDelegate(lipgloss.Color(config.ArchivedTaskListColor), false, 1), taskSummaryWidth, defaultListHeight) case Spacious: - archivedTaskList = list.New(archivedTaskItems, newTaskListDelegate(lipgloss.Color(config.ArchivedTaskListColor)), taskSummaryWidth, 16) - archivedTaskList.SetShowStatusBar(true) + archivedTaskList = list.New(archivedTaskItems, newListDelegate(lipgloss.Color(config.ArchivedTaskListColor), true, 1), taskSummaryWidth, defaultListHeight) } - archivedTaskList.SetShowTitle(false) - archivedTaskList.SetFilteringEnabled(false) + archivedTaskList.Title = "archived" + archivedTaskList.SetShowStatusBar(true) + archivedTaskList.SetStatusBarItemName("task", "tasks") + archivedTaskList.SetFilteringEnabled(true) archivedTaskList.SetShowHelp(false) archivedTaskList.DisableQuitKeybindings() archivedTaskList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") archivedTaskList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") + archivedTaskList.SetStatusBarItemName("task", "tasks") archivedTaskList.Styles.Title = archivedTaskList.Styles.Title. Foreground(lipgloss.Color(defaultBackgroundColor)). @@ -68,30 +70,60 @@ func InitialModel(db *sql.DB, config Config) model { taskInput.CharLimit = types.TaskSummaryMaxLen taskInput.Width = taskSummaryWidth - contextBMList := list.New(nil, newContextURLListDel(contextBMColor), taskSummaryWidth, compactListHeight) + contextBMList := list.New(nil, newListDelegate(lipgloss.Color(contextBMColor), false, 1), taskSummaryWidth, defaultListHeight) - contextBMList.SetShowTitle(false) + contextBMList.Title = "task bookmarks" contextBMList.SetShowHelp(false) + contextBMList.SetStatusBarItemName("bookmark", "bookmarks") contextBMList.SetFilteringEnabled(false) contextBMList.DisableQuitKeybindings() contextBMList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") contextBMList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") + contextBMList.Styles.Title = contextBMList.Styles.Title. + Foreground(lipgloss.Color(defaultBackgroundColor)). + Background(lipgloss.Color(contextBMColor)). + Bold(true) + + prefixSearchList := list.New(nil, newListDelegate(lipgloss.Color(prefixSearchColor), false, 0), taskSummaryWidth, defaultListHeight) + + prefixSearchList.Title = "filter by prefix" + prefixSearchList.SetShowHelp(false) + prefixSearchList.SetStatusBarItemName("prefix", "prefixes") + prefixSearchList.SetFilteringEnabled(false) + prefixSearchList.DisableQuitKeybindings() + prefixSearchList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") + prefixSearchList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") + + prefixSearchList.Styles.Title = prefixSearchList.Styles.Title. + Foreground(lipgloss.Color(defaultBackgroundColor)). + Background(lipgloss.Color(prefixSearchColor)). + Bold(true) + + activeTasksPrefixes := make(map[types.TaskPrefix]struct{}) + archivedTasksPrefixes := make(map[types.TaskPrefix]struct{}) + + tr, _ := getMarkDownRenderer(taskDetailsWordWrap) + m := model{ - db: db, - cfg: config, - taskList: taskList, - archivedTaskList: archivedTaskList, - contextBMList: contextBMList, - taskInput: taskInput, - showHelpIndicator: true, - tlTitleStyle: taskListTitleStyle, - atlTitleStyle: archivedTaskListTitleStyle, - tlSelStyle: tlSelItemStyle, - atlSelStyle: atlSelItemStyle, - contextVPTaskId: 0, - rtos: runtime.GOOS, - urlRegex: xurls.Strict(), + db: db, + cfg: config, + taskList: taskList, + archivedTaskList: archivedTaskList, + taskBMList: contextBMList, + prefixSearchList: prefixSearchList, + activeTasksPrefixes: activeTasksPrefixes, + archivedTasksPrefixes: archivedTasksPrefixes, + taskInput: taskInput, + showHelpIndicator: true, + tlTitleStyle: taskListTitleStyle, + atlTitleStyle: archivedTaskListTitleStyle, + tlSelStyle: tlSelItemStyle, + atlSelStyle: atlSelItemStyle, + contextVPTaskId: 0, + rtos: runtime.GOOS, + urlRegex: xurls.Strict(), + taskDetailsMdRenderer: tr, } return m diff --git a/internal/ui/list_delegate.go b/internal/ui/list_delegate.go index 7ebb5d9..9e12088 100644 --- a/internal/ui/list_delegate.go +++ b/internal/ui/list_delegate.go @@ -5,25 +5,15 @@ import ( "github.com/charmbracelet/lipgloss" ) -func newTaskListDelegate(color lipgloss.Color) list.DefaultDelegate { +func newListDelegate(color lipgloss.Color, showDesc bool, spacing int) list.DefaultDelegate { d := list.NewDefaultDelegate() - d.Styles.SelectedTitle = d.Styles. - SelectedTitle. - Foreground(color). - BorderLeftForeground(color) - - d.Styles.SelectedDesc = d.Styles. - SelectedTitle - - return d -} + d.ShowDescription = showDesc + d.SetSpacing(spacing) -func newContextURLListDel(color lipgloss.Color) list.DefaultDelegate { - d := list.NewDefaultDelegate() - d.SetSpacing(1) - d.ShowDescription = false - d.SetHeight(1) + d.Styles.NormalTitle = d.Styles. + NormalTitle. + Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#ffffff"}) d.Styles.SelectedTitle = d.Styles. SelectedTitle. @@ -33,5 +23,7 @@ func newContextURLListDel(color lipgloss.Color) list.DefaultDelegate { d.Styles.SelectedDesc = d.Styles. SelectedTitle + d.Styles.FilterMatch = lipgloss.NewStyle() + return d } diff --git a/internal/ui/markdown.go b/internal/ui/markdown.go new file mode 100644 index 0000000..748a46c --- /dev/null +++ b/internal/ui/markdown.go @@ -0,0 +1,24 @@ +package ui + +import ( + "github.com/charmbracelet/glamour" + "github.com/muesli/termenv" +) + +func getMarkDownRenderer(wrap int) (*glamour.TermRenderer, error) { + + var margin uint = 2 + dracula := glamour.DraculaStyleConfig + dracula.Document.BlockPrefix = "" + dracula.H1.Prefix = "" + dracula.H2.Prefix = "" + dracula.H3.Prefix = "" + dracula.H4.Prefix = "" + dracula.Document.Margin = &margin + + return glamour.NewTermRenderer( + glamour.WithColorProfile(termenv.TrueColor), + glamour.WithStyles(dracula), + glamour.WithWordWrap(wrap), + ) +} diff --git a/internal/ui/model.go b/internal/ui/model.go index 9788421..9d8b681 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -2,69 +2,26 @@ package ui import ( "database/sql" - "fmt" - "io" "regexp" - "strings" "time" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" pers "github.com/dhth/omm/internal/persistence" "github.com/dhth/omm/internal/types" - "github.com/dhth/omm/internal/utils" ) const ( - compactListHeight = 10 + defaultListHeight = 10 prefixPadding = 20 timeFormat = "2006/01/02 15:04" - taskSummaryWidth = 100 + taskSummaryWidth = 120 ) -type itemDelegate struct { - selStyle lipgloss.Style -} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - t, ok := listItem.(types.Task) - if !ok { - return - } - - start, _ := m.Paginator.GetSliceBounds(index) - si := (index - start) % m.Paginator.PerPage - - var summ string - summEls := strings.Split(t.Summary, ":") - if len(summEls) > 1 { - prefix := utils.RightPadTrim(summEls[0], prefixPadding, true) - summ = prefix + strings.Join(summEls[1:], ":") - } else { - summ = t.Summary - } - var hasContext string - if t.Context != nil { - hasContext = "(c)" - } - str := fmt.Sprintf("[%d]\t%s%s", si+1, utils.RightPadTrim(summ, taskSummaryWidth, true), hasContext) - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return d.selStyle.Render("> " + strings.Join(s, " ")) - } - } - - fmt.Fprint(w, fn(str)) -} - func (m model) Init() tea.Cmd { return tea.Batch( fetchTasks(m.db, true, pers.TaskNumLimit), @@ -89,6 +46,7 @@ const ( taskEntryView taskDetailsView contextBookmarksView + prefixSearchView helpView ) @@ -100,35 +58,42 @@ const ( ) type model struct { - db *sql.DB - cfg Config - taskList list.Model - archivedTaskList list.Model - contextBMList list.Model - taskIndex int - taskId uint64 - taskChange taskChangeType - contextVP viewport.Model - contextVPReady bool - taskDetailsVP viewport.Model - taskDetailsVPReady bool - helpVP viewport.Model - helpVPReady bool - quitting bool - showHelpIndicator bool - successMsg string - errorMsg string - taskInput textinput.Model - activeView activeView - lastActiveView activeView - activeTaskList taskListType - tlTitleStyle lipgloss.Style - atlTitleStyle lipgloss.Style - tlSelStyle lipgloss.Style - atlSelStyle lipgloss.Style - terminalWidth int - terminalHeight int - contextVPTaskId uint64 - rtos string - urlRegex *regexp.Regexp + db *sql.DB + cfg Config + taskList list.Model + archivedTaskList list.Model + taskBMList list.Model + prefixSearchList list.Model + activeTasksPrefixes map[types.TaskPrefix]struct{} + archivedTasksPrefixes map[types.TaskPrefix]struct{} + tlIndexMap map[uint64]int + taskIndex int + taskId uint64 + taskChange taskChangeType + contextVP viewport.Model + contextVPReady bool + taskDetailsVP viewport.Model + taskDetailsVPReady bool + helpVP viewport.Model + helpVPReady bool + quitting bool + showHelpIndicator bool + successMsg string + errorMsg string + taskInput textinput.Model + activeView activeView + lastActiveView activeView + activeTaskList taskListType + tlTitleStyle lipgloss.Style + atlTitleStyle lipgloss.Style + tlSelStyle lipgloss.Style + atlSelStyle lipgloss.Style + terminalWidth int + terminalHeight int + contextVPTaskId uint64 + rtos string + urlRegex *regexp.Regexp + shortenedListHt int + contextMdRenderer *glamour.TermRenderer + taskDetailsMdRenderer *glamour.TermRenderer } diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 95b32df..8295fa7 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -9,27 +9,22 @@ const ( TaskListColor = "#fe8019" ArchivedTLColor = "#fabd2f" contextBMColor = "#83a598" + prefixSearchColor = "#d3896b" contextTitleColor = "#8ec07c" taskEntryTitleColor = "#b8bb26" - taskDetailsTitleColor = "#d3869b" + taskDetailsTitleColor = "#bd93f9" taskListHeaderColor = "#928374" - taskDetailsColor = "#bdae93" - contextColor = "#928374" formHelpColor = "#928374" formColor = "#928374" helpMsgColor = "#928374" helpViewTitleColor = "#83a598" - helpTitleColor = "#83a598" - helpHeaderColor = "#83a598" - helpSectionColor = "#bdae93" + helpTitleColor = "#bd93f9" sBSuccessMsgColor = "#d3869b" sBErrMsgColor = "#fb4934" footerColor = "#928374" ) var ( - itemStyle = lipgloss.NewStyle().PaddingLeft(2) - titleStyle = lipgloss.NewStyle(). PaddingLeft(1). PaddingRight(1). @@ -37,7 +32,7 @@ var ( Background(lipgloss.Color(TaskListColor)). Foreground(lipgloss.Color(defaultBackgroundColor)) - listStyle = lipgloss.NewStyle().PaddingBottom(1) + listStyle = lipgloss.NewStyle().PaddingBottom(1).PaddingTop(1) taskEntryTitleStyle = titleStyle. Background(lipgloss.Color(taskEntryTitleColor)) @@ -51,9 +46,6 @@ var ( taskDetailsTitleStyle = titleStyle. Background(lipgloss.Color(taskDetailsTitleColor)) - contextBMTitleStyle = titleStyle. - Background(lipgloss.Color(contextBMColor)) - headerStyle = lipgloss.NewStyle(). PaddingTop(1). PaddingBottom(1). @@ -68,34 +60,12 @@ var ( sBSuccessMsgStyle = statusBarMsgStyle. Foreground(lipgloss.Color(sBSuccessMsgColor)) - taskDetailsStyle = lipgloss.NewStyle(). - PaddingLeft(2). - Foreground(lipgloss.Color(taskDetailsColor)) - - contextStyle = taskDetailsStyle. - Foreground(lipgloss.Color(contextColor)) - formStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(formColor)) formHelpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(formHelpColor)) - helpViewStyle = lipgloss.NewStyle(). - PaddingLeft(2) - - helpStyle = lipgloss.NewStyle() - - helpHeadingStyle = helpStyle. - Bold(true). - Foreground(lipgloss.Color(helpHeaderColor)) - - helpSectionStyle = helpStyle. - Foreground(lipgloss.Color(helpSectionColor)) - - helpSubHeadingStyle = helpSectionStyle. - Bold(true) - helpMsgStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(helpMsgColor)) ) diff --git a/internal/ui/update.go b/internal/ui/update.go index eca5e73..d245cbf 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -1,9 +1,10 @@ package ui import ( + _ "embed" "fmt" "os" - "strconv" + "sort" "strings" "time" @@ -18,16 +19,35 @@ import ( const ( noSpaceAvailableMsg = "Task list is at capacity. Archive/delete tasks using ctrl+d/ctrl+x." - noContextMsg = "∅" + noContextMsg = " ∅" viewPortMoveLineCount = 3 ) +//go:embed assets/help.md +var helpStr string + func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd m.successMsg = "" m.errorMsg = "" + if m.activeView == taskListView || m.activeView == archivedTaskListView { + switch msg := msg.(type) { + case tea.KeyMsg: + if m.taskList.FilterState() == list.Filtering { + m.taskList, cmd = m.taskList.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) + } + if m.archivedTaskList.FilterState() == list.Filtering { + m.archivedTaskList, cmd = m.archivedTaskList.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) + } + } + } + if m.activeView == taskEntryView { switch msg := msg.(type) { case tea.KeyMsg: @@ -45,6 +65,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + summEls := strings.Split(taskSummary, types.PrefixDelimiter) + if len(summEls) > 1 { + if summEls[0] == "" { + m.errorMsg = "prefix cannot be empty" + break + } + } + switch m.taskChange { case taskInsert: now := time.Now() @@ -71,34 +99,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: w, h := listStyle.GetFrameSize() - _, h2 := headerStyle.GetFrameSize() _, h3 := statusBarMsgStyle.GetFrameSize() m.terminalWidth = msg.Width m.terminalHeight = msg.Height m.taskList.SetWidth(msg.Width - w) m.archivedTaskList.SetWidth(msg.Width - 2) - m.contextBMList.SetWidth(msg.Width - 2) - m.contextBMList.SetHeight(msg.Height - h - 4) + m.taskBMList.SetWidth(msg.Width - 2) + m.taskBMList.SetHeight(msg.Height - h - h3 - 1) + m.prefixSearchList.SetWidth(msg.Width - 2) + m.prefixSearchList.SetHeight(msg.Height - h - h3 - 1) var listHeight int - if m.cfg.ShowContext { - listHeight = msg.Height/2 - h - } else { - listHeight = msg.Height - h - 4 - } + contextHeight := (msg.Height - h - h3 - 5) / 2 - if m.cfg.ListDensity == Spacious { - m.taskList.SetHeight(listHeight) - m.archivedTaskList.SetHeight(listHeight) - } + m.shortenedListHt = msg.Height - contextHeight - 5 - var contextHeight int - if m.cfg.ListDensity == Compact { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 6 + if m.cfg.ShowContext { + listHeight = m.shortenedListHt } else { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 5 + listHeight = msg.Height - h - h3 - 1 } + m.taskList.SetHeight(listHeight) + m.archivedTaskList.SetHeight(listHeight) + if !m.contextVPReady { m.contextVP = viewport.New(msg.Width-3, contextHeight) m.contextVPReady = true @@ -118,9 +142,27 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.taskDetailsVP.Height = m.terminalHeight - 4 } + crWrap := (msg.Width - 4) + if crWrap > contextWordWrapUpperLimit { + crWrap = contextWordWrapUpperLimit + } + m.contextMdRenderer, _ = getMarkDownRenderer(crWrap) + + helpToRender := helpStr + switch m.contextMdRenderer { + case nil: + break + default: + helpStrGl, err := m.contextMdRenderer.Render(helpStr) + if err != nil { + break + } + helpToRender = helpStrGl + } + if !m.helpVPReady { m.helpVP = viewport.New(msg.Width-3, m.terminalHeight-4) - m.helpVP.SetContent(helpStr) + m.helpVP.SetContent(helpToRender) m.helpVP.KeyMap.Up.SetEnabled(false) m.helpVP.KeyMap.Down.SetEnabled(false) m.helpVPReady = true @@ -142,6 +184,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "esc", "q", "ctrl+c": av := m.activeView + if m.activeView == taskListView && m.taskList.IsFiltered() { + m.taskList.ResetFilter() + break + } + + if m.activeView == archivedTaskListView && m.archivedTaskList.IsFiltered() { + m.archivedTaskList.ResetFilter() + break + } + if m.activeView == archivedTaskListView { m.activeView = taskListView m.activeTaskList = activeTasks @@ -149,7 +201,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - if m.activeView == taskDetailsView || m.activeView == contextBookmarksView || m.activeView == helpView { + if m.activeView == taskDetailsView || m.activeView == contextBookmarksView || m.activeView == prefixSearchView || m.activeView == helpView { m.activeView = m.lastActiveView switch m.activeView { case taskListView: @@ -167,7 +219,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit case "?": - if m.activeView == taskDetailsView { + if m.activeView == taskDetailsView || m.activeView == contextBookmarksView || m.activeView == prefixSearchView { break } @@ -179,51 +231,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.activeView = helpView case "tab", "shift+tab": - m.lastActiveView = m.activeView switch m.activeView { case taskListView: m.activeView = archivedTaskListView m.activeTaskList = archivedTasks + m.lastActiveView = m.activeView case archivedTaskListView: m.activeView = taskListView m.activeTaskList = activeTasks + m.lastActiveView = m.activeView } - case "2", "3", "4", "5", "6", "7", "8", "9": - if m.cfg.ListDensity != Compact { - break - } - - if m.activeView != taskListView { - break - } - - keyNum, err := strconv.Atoi(keypress) - if err != nil { - m.errorMsg = "Something went horribly wrong" - break - } - - if m.taskList.Index() == 0 && keyNum == 1 { - break - } - - index := (m.taskList.Paginator.Page * m.taskList.Paginator.PerPage) + (keyNum - 1) - - if index >= len(m.taskList.Items()) { - m.errorMsg = "There is no item for this index" - break - } - listItem := m.taskList.Items()[index] - - m.taskList.RemoveItem(index) - cmd = m.taskList.InsertItem(0, listItem) - cmds = append(cmds, cmd) - m.taskList.Select(0) - - cmd = m.updateTaskSequence() - cmds = append(cmds, cmd) - case "I": if m.activeView != taskListView { break @@ -234,6 +252,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot add items when the task list is filtered" + break + } + m.taskIndex = 0 m.taskInput.Reset() m.taskInput.Focus() @@ -251,6 +274,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot add items when the task list is filtered" + break + } + m.taskIndex = m.taskList.Index() m.taskInput.Reset() m.taskInput.Focus() @@ -268,6 +296,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot add items when the task list is filtered" + break + } + if len(m.taskList.Items()) == 0 { m.taskIndex = 0 } else { @@ -289,6 +322,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot add items when the task list is filtered" + break + } + m.taskIndex = len(m.taskList.Items()) m.taskInput.Reset() m.taskInput.Focus() @@ -338,6 +376,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.taskList.IsFiltered() { + m.errorMsg = "Cannot move items when the task list is filtered" break } @@ -365,6 +404,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.taskList.IsFiltered() { + m.errorMsg = "Cannot move items when the task list is filtered" break } @@ -412,6 +452,23 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + br := false + switch m.activeView { + case taskListView: + if m.taskList.IsFiltered() { + br = true + } + + case archivedTaskListView: + if m.archivedTaskList.IsFiltered() { + br = true + } + } + + if br { + break + } + cmds = append(cmds, fetchTasks(m.db, true, pers.TaskNumLimit)) cmds = append(cmds, fetchTasks(m.db, false, pers.TaskNumLimit)) @@ -422,6 +479,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot archive items when the task list is filtered" + break + } + listItem := m.taskList.SelectedItem() index := m.taskList.Index() t, ok := listItem.(types.Task) @@ -438,6 +500,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.archivedTaskList.IsFiltered() { + m.errorMsg = "Cannot archive items when the task list is filtered" + break + } + listItem := m.archivedTaskList.SelectedItem() index := m.archivedTaskList.Index() t, ok := listItem.(types.Task) @@ -457,6 +524,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot delete items when the task list is filtered" + break + } + index := m.taskList.Index() t, ok := m.taskList.SelectedItem().(types.Task) if !ok { @@ -471,6 +543,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } + if m.archivedTaskList.IsFiltered() { + m.errorMsg = "Cannot delete items when the task list is filtered" + break + } + index := m.archivedTaskList.Index() task, ok := m.archivedTaskList.SelectedItem().(types.Task) if !ok { @@ -482,8 +559,66 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } + case "ctrl+p": + if m.activeView != taskListView && m.activeView != archivedTaskListView { + break + } + + var taskList list.Model + var taskPrefixes map[types.TaskPrefix]struct{} + + switch m.activeView { + case taskListView: + taskList = m.taskList + taskPrefixes = m.activeTasksPrefixes + case archivedTaskListView: + taskList = m.archivedTaskList + taskPrefixes = m.archivedTasksPrefixes + } + + if len(taskList.Items()) == 0 { + m.errorMsg = "No items in task list" + break + } + + for _, li := range taskList.Items() { + t, ok := li.(types.Task) + if ok { + prefix, pOk := t.Prefix() + if pOk { + taskPrefixes[prefix] = struct{}{} + } + } + } + var prefixes []types.TaskPrefix + for k := range taskPrefixes { + prefixes = append(prefixes, k) + } + + if len(prefixes) == 0 { + m.errorMsg = "No prefixes in task list" + break + } + + if len(prefixes) == 1 { + m.errorMsg = "Only 1 unique prefix in task list" + break + } + + sort.Slice(prefixes, func(i, j int) bool { + return prefixes[i] < prefixes[j] + }) + + pi := make([]list.Item, len(prefixes)) + for i, p := range prefixes { + pi[i] = list.Item(p) + } + + m.prefixSearchList.SetItems(pi) + m.activeView = prefixSearchView + case "enter": - if m.activeView != taskListView && m.activeView != contextBookmarksView { + if m.activeView != taskListView && m.activeView != contextBookmarksView && m.activeView != prefixSearchView { break } switch m.activeView { @@ -492,11 +627,33 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - if m.taskList.Index() == 0 { + var index int + if m.taskList.IsFiltered() { + m.errorMsg = "Cannot move items when the task list is filtered" + selected, ok := m.taskList.SelectedItem().(types.Task) + if !ok { + break + } + listIndex, ok := m.tlIndexMap[selected.ID] + if !ok { + m.errorMsg = "Something went wrong; cannot move item to the top" + } + + index = listIndex + } else { + index = m.taskList.Index() + } + + if index == 0 { + m.errorMsg = "This item is already at the top of the list" break } - index := m.taskList.Index() + if m.taskList.IsFiltered() { + m.taskList.ResetFilter() + m.taskList.Select(index) + } + listItem := m.taskList.SelectedItem() m.taskList.RemoveItem(index) cmd = m.taskList.InsertItem(0, listItem) @@ -506,8 +663,50 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd = m.updateTaskSequence() cmds = append(cmds, cmd) case contextBookmarksView: - url := m.contextBMList.SelectedItem().FilterValue() + url := m.taskBMList.SelectedItem().FilterValue() cmds = append(cmds, openURL(url)) + case prefixSearchView: + prefix := m.prefixSearchList.SelectedItem().FilterValue() + var taskList list.Model + + switch m.activeTaskList { + case activeTasks: + taskList = m.taskList + case archivedTasks: + taskList = m.archivedTaskList + } + + taskList.ResetFilter() + var tlCmd tea.Cmd + + runes := []rune(prefix) + + if len(runes) > 1 { + taskList.FilterInput.SetValue(string(runes[:len(runes)-1])) + } + + taskList, tlCmd = taskList.Update(tea.KeyMsg{Type: -1, Runes: []int32{47}, Alt: false, Paste: false}) + cmds = append(cmds, tlCmd) + + taskList, tlCmd = taskList.Update(tea.KeyMsg{Type: -1, Runes: []rune{runes[len(runes)-1]}, Alt: false, Paste: false}) + cmds = append(cmds, tlCmd) + + // TODO: Try sending ENTER programmatically too + // taskList, tlCmd = taskList.Update(tea.KeyMsg{Type: 13, Runes: []int32(nil), Alt: false, Paste: false}) + // or + // taskList, tlCmd = taskList.Update(tea.KeyEnter) + // this results in the list's paginator being broken, so requires another manual ENTER keypress + + switch m.activeTaskList { + case activeTasks: + m.taskList = taskList + m.activeView = taskListView + case archivedTasks: + m.archivedTaskList = taskList + m.activeView = archivedTaskListView + } + + return m, tea.Sequence(cmds...) } case "c": @@ -562,85 +761,44 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - w, h := listStyle.GetFrameSize() - var taskList list.Model - var archivedTaskList list.Model - _, h2 := headerStyle.GetFrameSize() - _, h3 := statusBarMsgStyle.GetFrameSize() - - tlIndex := m.taskList.Index() - atlIndex := m.archivedTaskList.Index() - - var listHeight int - if m.cfg.ShowContext { - listHeight = m.terminalHeight/2 - h - } else { - listHeight = m.terminalHeight - h - 4 - } + var tlDel list.DefaultDelegate + var atlDel list.DefaultDelegate switch m.cfg.ListDensity { case Compact: - taskList = list.New(m.taskList.Items(), - newTaskListDelegate(lipgloss.Color(m.cfg.TaskListColor)), - m.terminalWidth-w, - listHeight, - ) - taskList.SetShowStatusBar(true) - - archivedTaskList = list.New(m.archivedTaskList.Items(), - newTaskListDelegate(lipgloss.Color(m.cfg.ArchivedTaskListColor)), - m.terminalWidth-w, - listHeight, - ) - archivedTaskList.SetShowStatusBar(true) + tlDel = newListDelegate(lipgloss.Color(m.cfg.TaskListColor), true, 1) + atlDel = newListDelegate(lipgloss.Color(m.cfg.ArchivedTaskListColor), true, 1) m.cfg.ListDensity = Spacious case Spacious: - taskList = list.New(m.taskList.Items(), - itemDelegate{selStyle: m.tlSelStyle}, - m.terminalWidth-w, - compactListHeight, - ) - taskList.SetShowStatusBar(false) - - archivedTaskList = list.New(m.archivedTaskList.Items(), - itemDelegate{selStyle: m.tlSelStyle}, - m.terminalWidth-w, - compactListHeight, - ) + tlDel = newListDelegate(lipgloss.Color(m.cfg.TaskListColor), false, 1) + atlDel = newListDelegate(lipgloss.Color(m.cfg.ArchivedTaskListColor), false, 1) m.cfg.ListDensity = Compact - archivedTaskList.SetShowStatusBar(false) } - taskList.SetShowTitle(false) - taskList.SetFilteringEnabled(false) - taskList.SetShowHelp(false) - taskList.DisableQuitKeybindings() - taskList.Styles.Title = m.taskList.Styles.Title - taskList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") - taskList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") - - m.taskList = taskList - m.taskList.Select(tlIndex) - - archivedTaskList.SetShowTitle(false) - archivedTaskList.SetFilteringEnabled(false) - archivedTaskList.SetShowHelp(false) - archivedTaskList.DisableQuitKeybindings() - archivedTaskList.KeyMap.PrevPage.SetKeys("left", "h", "pgup") - archivedTaskList.KeyMap.NextPage.SetKeys("right", "l", "pgdown") + m.taskList.SetDelegate(tlDel) + m.archivedTaskList.SetDelegate(atlDel) + for i, li := range m.taskList.Items() { + t, ok := li.(types.Task) + if ok { + t.SetTitle(m.cfg.ListDensity == Compact) + } + m.taskList.SetItem(i, list.Item(t)) + } - m.archivedTaskList = archivedTaskList - m.archivedTaskList.Select(atlIndex) + for i, li := range m.archivedTaskList.Items() { + t, ok := li.(types.Task) + if ok { + t.SetTitle(m.cfg.ListDensity == Compact) + } + m.archivedTaskList.SetItem(i, list.Item(t)) + } - var contextHeight int - if m.cfg.ListDensity == Compact { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 6 - } else { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 5 + if m.cfg.ShowContext { + m.taskList.SetHeight(m.shortenedListHt) + m.archivedTaskList.SetHeight(m.shortenedListHt) } - m.contextVP.Height = contextHeight case "C": if m.activeView != taskListView && m.activeView != archivedTaskListView { @@ -650,28 +808,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cfg.ShowContext = !m.cfg.ShowContext _, h := listStyle.GetFrameSize() - var listHeight int - if m.cfg.ListDensity == Spacious { - switch m.cfg.ShowContext { - case true: - listHeight = m.terminalHeight/2 - h - case false: - listHeight = m.terminalHeight - h - 4 - } - m.taskList.SetHeight(listHeight) - m.archivedTaskList.SetHeight(listHeight) - } - - _, h2 := headerStyle.GetFrameSize() _, h3 := statusBarMsgStyle.GetFrameSize() + var listHeight int - var contextHeight int - if m.cfg.ListDensity == Compact { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 6 + if m.cfg.ShowContext { + listHeight = m.shortenedListHt } else { - contextHeight = m.terminalHeight - m.taskList.Height() - h2 - h3 - 5 + listHeight = m.terminalHeight - h - h3 - 1 } - m.contextVP.Height = contextHeight + + m.taskList.SetHeight(listHeight) + m.archivedTaskList.SetHeight(listHeight) case "d": if m.activeView == taskDetailsView { @@ -779,7 +926,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for i, url := range urls { bmItems[i] = list.Item(types.ContextBookmark(url)) } - m.contextBMList.SetItems(bmItems) + m.taskBMList.SetItems(bmItems) switch m.activeView { case taskListView: m.activeTaskList = activeTasks @@ -861,13 +1008,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - entry := list.Item(types.Task{ + t := types.Task{ ID: msg.id, Summary: msg.taskSummary, Active: true, CreatedAt: msg.createdAt, UpdatedAt: msg.updatedAt, - }) + } + t.SetTitle(m.cfg.ListDensity == Compact) + entry := list.Item(t) cmd = m.taskList.InsertItem(m.taskIndex, entry) cmds = append(cmds, cmd) m.taskList.Select(m.taskIndex) @@ -907,6 +1056,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { t.Summary = msg.taskSummary t.UpdatedAt = msg.updatedAt + t.SetTitle(m.cfg.ListDensity == Compact) cmd = m.taskList.SetItem(msg.listIndex, list.Item(t)) cmds = append(cmds, cmd) } @@ -932,6 +1082,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { t.Context = &msg.context } t.UpdatedAt = msg.updatedAt + t.SetTitle(m.cfg.ListDensity == Compact) cmd = m.taskList.SetItem(msg.listIndex, list.Item(t)) cmds = append(cmds, cmd) case archivedTasks: @@ -947,6 +1098,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { t.Context = &msg.context } t.UpdatedAt = msg.updatedAt + t.SetTitle(m.cfg.ListDensity == Compact) cmd = m.archivedTaskList.SetItem(msg.listIndex, list.Item(t)) cmds = append(cmds, cmd) } @@ -973,6 +1125,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } t.UpdatedAt = msg.updatedAt + t.SetTitle(m.cfg.ListDensity == Compact) m.taskList.InsertItem(0, list.Item(t)) m.taskList.Select(oldIndex + 1) m.archivedTaskList.RemoveItem(msg.listIndex) @@ -1001,15 +1154,29 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case true: taskItems := make([]list.Item, len(msg.tasks)) for i, t := range msg.tasks { + t.SetTitle(m.cfg.ListDensity == Compact) taskItems[i] = t } m.taskList.SetItems(taskItems) + m.taskList.Select(0) + + tlIndexMap := make(map[uint64]int) + for i, ti := range m.taskList.Items() { + t, ok := ti.(types.Task) + if ok { + tlIndexMap[t.ID] = i + } + } + m.tlIndexMap = tlIndexMap + case false: archivedTaskItems := make([]list.Item, len(msg.tasks)) for i, t := range msg.tasks { + t.SetTitle(m.cfg.ListDensity == Compact) archivedTaskItems[i] = t } m.archivedTaskList.SetItems(archivedTaskItems) + m.archivedTaskList.Select(0) } } case textEditorClosed: @@ -1057,19 +1224,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - if m.cfg.ListDensity == Compact { - if len(m.taskList.Items()) > 9 { - m.taskList.SetHeight(compactListHeight + 1) - } else { - m.taskList.SetHeight(compactListHeight) - } - if len(m.archivedTaskList.Items()) > 9 { - m.archivedTaskList.SetHeight(compactListHeight + 1) - } else { - m.archivedTaskList.SetHeight(compactListHeight) - } - } - var viewUpdateCmd tea.Cmd switch m.activeView { case taskListView: @@ -1088,11 +1242,25 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - if t.Context != nil { - m.contextVP.SetContent(*t.Context) - } else { - m.contextVP.SetContent(noContextMsg) + var detailsToRender string + switch t.Context { + case nil: + detailsToRender = noContextMsg + default: + detailsToRender = *t.Context + switch m.contextMdRenderer { + case nil: + break + default: + contextGl, err := m.contextMdRenderer.Render(*t.Context) + if err != nil { + break + } + detailsToRender = contextGl + } } + + m.contextVP.SetContent(detailsToRender) m.contextVPTaskId = t.ID case archivedTaskListView: @@ -1112,7 +1280,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if t.Context != nil { - m.contextVP.SetContent(*t.Context) + if m.contextMdRenderer != nil { + contextGl, err := m.contextMdRenderer.Render(*t.Context) + if err != nil { + m.contextVP.SetContent(*t.Context) + } else { + m.contextVP.SetContent(contextGl) + } + } else { + m.contextVP.SetContent(*t.Context) + } } else { m.contextVP.SetContent(noContextMsg) } @@ -1125,7 +1302,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.taskDetailsVP, viewUpdateCmd = m.taskDetailsVP.Update(msg) case contextBookmarksView: - m.contextBMList, viewUpdateCmd = m.contextBMList.Update(msg) + m.taskBMList, viewUpdateCmd = m.taskBMList.Update(msg) + + case prefixSearchView: + m.prefixSearchList, viewUpdateCmd = m.prefixSearchList.Update(msg) case helpView: m.helpVP, viewUpdateCmd = m.helpVP.Update(msg) @@ -1136,15 +1316,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m model) updateTaskSequence() tea.Cmd { +func (m *model) updateTaskSequence() tea.Cmd { sequence := make([]uint64, len(m.taskList.Items())) + tlIndexMap := make(map[uint64]int) + for i, ti := range m.taskList.Items() { t, ok := ti.(types.Task) if ok { sequence[i] = t.ID + tlIndexMap[t.ID] = i } } + m.tlIndexMap = tlIndexMap + return updateTaskSequence(m.db, sequence) } @@ -1155,15 +1340,23 @@ func (m model) isSpaceAvailable() bool { func (m *model) setContextFSContent(task types.Task) { var ctx string if task.Context != nil { - ctx = fmt.Sprintf("\n===\n\n%s", *task.Context) + ctx = fmt.Sprintf("---\n%s", *task.Context) } - details := fmt.Sprintf(`summary %s -created at %s -last updated at %s + details := fmt.Sprintf(`- summary : %s +- created at : %s +- last updated at : %s + %s `, task.Summary, task.CreatedAt.Format(timeFormat), task.UpdatedAt.Format(timeFormat), ctx) + if m.taskDetailsMdRenderer != nil { + detailsGl, err := m.taskDetailsMdRenderer.Render(details) + if err == nil { + m.taskDetailsVP.SetContent(detailsGl) + return + } + } m.taskDetailsVP.SetContent(details) } diff --git a/internal/ui/view.go b/internal/ui/view.go index 322619a..3f6641b 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -8,7 +8,9 @@ import ( ) var ( - TaskListDefaultTitle = "omm" + TaskListDefaultTitle = "omm" + taskDetailsWordWrap = 80 + contextWordWrapUpperLimit = 160 ) func (m model) View() string { @@ -16,57 +18,56 @@ func (m model) View() string { return "" } - var header string var content string var context string var statusBar string var helpMsg string var listEmpty bool + if m.showHelpIndicator && (m.activeView != helpView) { + helpMsg = helpMsgStyle.Render(" Press ? for help") + } + + statusBar = helpMsg + if m.errorMsg != "" && m.successMsg != "" { - statusBar = fmt.Sprintf("%s%s", + statusBar += fmt.Sprintf("%s%s", sBErrMsgStyle.Render(utils.Trim(m.errorMsg, (m.terminalWidth/2)-3)), sBSuccessMsgStyle.Render(utils.Trim(m.successMsg, (m.terminalWidth/2)-3)), ) } else if m.errorMsg != "" { - statusBar = sBErrMsgStyle.Render(m.errorMsg) + statusBar += sBErrMsgStyle.Render(m.errorMsg) } else if m.successMsg != "" { - statusBar = sBSuccessMsgStyle.Render(m.successMsg) - } - - if m.showHelpIndicator && (m.activeView == taskListView || m.activeView == archivedTaskListView) { - helpMsg = helpMsgStyle.Render(" Press ? for help") + statusBar += sBSuccessMsgStyle.Render(m.successMsg) } switch m.activeView { case taskListView: - header = fmt.Sprintf("%s%s", m.tlTitleStyle.Render(m.cfg.TaskListTitle), helpMsg) if len(m.taskList.Items()) > 0 { content = listStyle.Render(m.taskList.View()) } else { - content += fmt.Sprintf(" %s", formStyle.Render("No items. Press a/o to add one.\n")) + content = fmt.Sprintf(` + %s + + %s`, m.tlTitleStyle.Render(m.cfg.TaskListTitle), formStyle.Render("No items. Press a/o to add one.\n")) listEmpty = true } - if m.cfg.ListDensity == Compact && len(m.taskList.Items()) <= 9 { - content += "\n" - } case archivedTaskListView: - header = fmt.Sprintf("%s%s", m.atlTitleStyle.Render("archived"), helpMsg) if len(m.archivedTaskList.Items()) > 0 { content = listStyle.Render(m.archivedTaskList.View()) } else { - content += fmt.Sprintf(" %s", formStyle.Render("No items. You archive items by pressing ctrl+d.\n")) + content = fmt.Sprintf(` + %s + + %s`, m.atlTitleStyle.Render("archived"), formStyle.Render("No items. You archive items by pressing ctrl+d.\n")) listEmpty = true } - if m.cfg.ListDensity == Compact && len(m.archivedTaskList.Items()) <= 9 { - content += "\n" - } case taskEntryView: if m.taskChange == taskInsert { - header = taskEntryTitleStyle.Render("enter your task") + header := taskEntryTitleStyle.Render("enter your task") var newTaskPosition string if m.taskIndex == 0 { @@ -76,39 +77,43 @@ func (m model) View() string { } else { newTaskPosition = fmt.Sprintf("at position %d", m.taskIndex+1) } - content = fmt.Sprintf(` %s + content = fmt.Sprintf(` + %s + + %s %s %s %s`, + header, formHelpStyle.Render(fmt.Sprintf("task will be added %s", newTaskPosition)), formHelpStyle.Render("omm picks up the prefix in a task summary like 'prefix: do something'\n and highlights it for you in the task list"), m.taskInput.View(), formHelpStyle.Render("press to go back, ⏎ to submit"), ) - if m.cfg.ListDensity == Spacious { - for i := 0; i < m.terminalHeight-13; i++ { - content += "\n" - } + for i := 0; i < m.terminalHeight-12; i++ { + content += "\n" } } else if m.taskChange == taskUpdateSummary { - header = taskEntryTitleStyle.Render("update task") - content = fmt.Sprintf(` %s + header := taskEntryTitleStyle.Render("update task") + content = fmt.Sprintf(` + %s + + %s %s %s`, + header, formHelpStyle.Render("omm picks up the prefix in a task summary like 'prefix: do something'\n and highlights it for you in the task list"), m.taskInput.View(), formHelpStyle.Render("press to go back, ⏎ to submit"), ) - if m.cfg.ListDensity == Spacious { - for i := 0; i < m.terminalHeight-11; i++ { - content += "\n" - } + for i := 0; i < m.terminalHeight-10; i++ { + content += "\n" } } @@ -116,43 +121,44 @@ func (m model) View() string { var spVal string sp := int(m.taskDetailsVP.ScrollPercent() * 100) if sp < 100 { - spVal = helpSectionStyle.Render(fmt.Sprintf(" %d%% ↓", sp)) + spVal = helpMsgStyle.Render(fmt.Sprintf(" %d%% ↓", sp)) } - header = fmt.Sprintf("%s%s", taskDetailsTitleStyle.Render("task details"), spVal) + header := fmt.Sprintf("%s%s", taskDetailsTitleStyle.Render("task details"), spVal) if !m.taskDetailsVPReady { - context = taskDetailsStyle.Render("Initializing...") + content = headerStyle.Render(header) + "\n" + "Initializing..." } else { - context = taskDetailsStyle.Render(m.taskDetailsVP.View()) + content = headerStyle.Render(header) + "\n" + m.taskDetailsVP.View() } - return lipgloss.JoinVertical(lipgloss.Left, headerStyle.Render(header), context, statusBar) - case contextBookmarksView: - header = fmt.Sprintf("%s%s", contextBMTitleStyle.Render("Task Bookmarks"), helpMsg) + content = listStyle.Render(m.taskBMList.View()) - content = listStyle.Render(m.contextBMList.View()) + case prefixSearchView: + content = listStyle.Render(m.prefixSearchList.View()) case helpView: - header = fmt.Sprintf("%s %s", helpTitleStyle.Render("help"), helpSectionStyle.Render("(scroll with j/k/↓/↑)")) + header := fmt.Sprintf(` + %s %s + +`, helpTitleStyle.Render("help"), helpMsgStyle.Render("(scroll with j/k/↓/↑)")) if !m.helpVPReady { - content = helpViewStyle.Render("Initializing...") + content = "Initializing..." } else { - content = helpViewStyle.Render(m.helpVP.View()) + content = header + m.helpVP.View() } } var components []string - components = append(components, headerStyle.Render(header)) components = append(components, content) if !listEmpty && m.cfg.ShowContext && (m.activeView == taskListView || m.activeView == archivedTaskListView) { if !m.contextVPReady { - context = contextStyle.Render("Initializing...") + context = "Initializing..." } else { context = fmt.Sprintf(" %s\n\n%s", contextTitleStyle.Render("context"), - contextStyle.Render(m.contextVP.View()), + m.contextVP.View(), ) } components = append(components, context)