Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External editor support #105

Merged
merged 6 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ttdl"
version = "4.5.0"
version = "4.6.0"
authors = ["Vladimir Markelov <[email protected]>"]
edition = "2021"
keywords = ["todotxt", "terminal", "cli", "todo", "tasks"]
Expand Down Expand Up @@ -30,6 +30,7 @@ serde_derive = "1"
json = "^0.12"
unicode-width="^0.2"
anyhow = "1"
tempfile = "3"

[package.metadata.deb]
section = "utility"
Expand Down
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- [Syntax highlight](#syntax-highlight)
- [Hide duplicated info](#hide-duplicated-info)
- [Edit in keep-tags mode](#edit-in-keep-tags-mode)
- [Interactive edit](#interactive-edit)
- [Human-readable dates](#human-readable-dates)
- [Custom columns](#custom-columns)
- [Custom column example]($custom-column-example)
Expand Down Expand Up @@ -1141,6 +1142,64 @@ New todos:
3 2024-01-18 buy cake due:2024-02-01 due:2024-01-18
```

#### Interactive edit

If you need to edit more than one task at a time, you can use your editor of choice to modify tasks in a convenient way.
Please note that the interactive edit does not support the dry-run mode.
So, all changes in the interactive mode are final ones.

In the interactive mode all command-line options that modifies tasks are ignored.
Only command-line options that defines filter for tasks are processed.
Example: the command `ttdl edit +proj --set-due=2024-11-11 -i` just opens an editor with original task subjects about project `proj`.
The modifying option `--set-due` is ignored.

First, you should set what editor TTDL will use.
It can be done either by setting environment variable `EDITOR` or by setting option `global.editor` in TTDL configuration.
Examples: `EDITOR=vim ttdl edit -i` or setting the configuration option `editor = "c:/utils/npp/notepad++.exe"`.

Second, run TTDL with a command `edit` and command-line option `--interactive` (short option name is `-i`).
Note that in case of interactive mode, it is OK to edit the entire task list.

After you modify the task list, you must save it and close the editor.
If TTDL detects any changes, it replaces previous tasks with new ones.

If you do not want to update the tasks, you have to way to do it after an editor is opened:

1. Exit the editor without saving. TTDL notices that the tasks are the same and will do nothing.
2. If you have modified something and saved already, you can delete all the text in the editor and save the empty file. When TTDL detects empty new task list, it aborts editing.

Note: an empty file is a file that contains only whitespaces (carriage returns, line feeds, spaces, and tabs).
So, a file with a single or more empty lines is also treated as an empty one, and the edit is aborted.

If TTDL has done any changes, it reports about it in a way: `Removed 14 tasks, added 13 tasks.`.

##### How the interactive mode works internally

First, TTDL generates a list of tasks to edit. If the list is empty, the editing is aborted.

Second, TTDL saves the selected tasks to a temporary file.
So, if you run `ttdl edit 2-5 -i`, a temporary file will contain only tasks with IDs between 2 and 5.

Third, TTDL opens an external editor and passed the name of the temporary file to it.
When the editor is closed, TTDL determines if it should do anything.

Forth, TTDL detects if there were any changes.
The algorithm is simple: TTDL calculates hashes for both old and new files.
If the hashes differ, TTDL removed old tasks and appends the new ones to the end of the task list.
It results in:

- if you move tasks around but do not change their texts, TTDL will update the task list
- if you just add an empty line, TTDL will update the task list
- there is a tiny chance that hashes of old and new files are the same. In this case, you should make them different by adding an empty line anywhere.

If no changes are detected or the task list is empty after editing, the edit operation is aborted and nothing changes.

Otherwise, fifth, TTDL removes the original tasks from the task list.
If removal is successful, TTDL appends the new tasks to the end of file.
Note: as you can see, it is impossible to keep the same task IDs after editing in interactive mode.
The only way to keep the original IDs is editing the entire task list with `ttdl edit -i` command.
But even in this case if you move tasks around or remove any, IDs will changes.

### Human-readable dates

In addition to human-readable output, TTDL supports setting due and threshold dates in human-readable format.
Expand Down Expand Up @@ -1334,7 +1393,8 @@ By default todos from a given range are processed only if they are incomplete. T

| Command | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `ttdl e 2-5 "new subject"` | only the first incomplete todo with ID between 2 and 5 changes its subject (in this case todo with ID equals 2 gets subject "new subject") |
| `ttdl e 2-5 "new subject"` | only the first incomplete todos with ID between 2 and 5 changes its subject (in this case todo with ID equals 2 gets subject "new subject") |
| `ttdl e 2-5 "new subject" -i` | edits in the interactive mode todos with ID between 2 and 5. The new subject value is ignored |
| `ttdl e +proj --repl-ctx=bug1010@bug1020` | replace context `bug1010` with `bug1020` for all incomplete todos that related to project `proj` |
| `ttdl e @customer_acme --set-due=2018-12-31` | set due date 2018-12-31 for all incomplete todos that has `customer_acme` context |
| `ttdl e @customer_acme --set-due=none` | remove due date 2018-12-31 for all incomplete todos that has `customer_acme` context |
Expand Down
13 changes: 13 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
ttdl (4.6.0) unstable; urgency=medium

* New command-line option `--interactive`/`-i` for command `edit`. When it
is set, and editor is defined, all options that modifies tasks are
ignored and TTDL opens an editor with tasks that were filtered by the
command line options. After closing the editor, if the tasks are changed
and the text is not empty, TTDL updated the selected tasks. Example:
- `ttdl edit -i` - open all tasks to edit in an external editor
- `ttdl edit +proj -i` - open all tasks related to the project `proj` in
an external editor.

-- Vladimir Markelov <[email protected]> Sun, 24 Nov 2024 16:41:04 -0800

ttdl (4.5.0) unstable; urgency=medium

* When editing a task or adding a new one, you can use simple expressions
Expand Down
35 changes: 33 additions & 2 deletions src/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const APP_DIR: &str = "ttdl";
const CONF_FILE: &str = "ttdl.toml";
const TODO_FILE: &str = "todo.txt";
const DONE_FILE: &str = "done.txt";
const EDITOR: &str = "EDITOR";

struct RangeEnds {
l: usize,
Expand Down Expand Up @@ -64,6 +65,8 @@ pub struct Conf {
pub done_file: PathBuf,
pub keep_empty: bool,
pub keep_tags: bool,
editor_path: Option<String>,
pub use_editor: bool,

pub auto_hide_columns: bool,
pub auto_show_columns: bool,
Expand Down Expand Up @@ -94,6 +97,8 @@ impl Default for Conf {
done_file: PathBuf::from(""),
keep_empty: false,
keep_tags: false,
editor_path: None,
use_editor: false,

auto_hide_columns: false,
auto_show_columns: false,
Expand All @@ -114,6 +119,19 @@ impl Conf {
fn new() -> Self {
Default::default()
}
pub fn editor(&self) -> Option<PathBuf> {
let mut spth: String = env::var(EDITOR).unwrap_or_default();
if spth.is_empty() {
if let Some(p) = &self.editor_path {
spth = p.clone();
}
}
if spth.is_empty() {
None
} else {
Some(PathBuf::from(spth))
}
}
}

fn print_usage(program: &str, opts: &Options) {
Expand Down Expand Up @@ -182,6 +200,7 @@ fn print_usage(program: &str, opts: &Options) {
`ttdl e 2 --set-due=due+2d` - push the due date by 2 days
`ttdl e 2 --set-due=limit+1d` - takes task's tag `limit` as a date, adds 1 day and sets the result to the due date
`ttdl e 2 --set-due=t+1w+3d` - push the due date by a week and a half or more accurate by 10 days
`ttdl e 2-5 -i` - open an external editor of you choice the the incomplete todos with ID between 2 and 5 for interactive editing. After saving the changes and closing the editor, TTDL updates the task list
append | app - adds a text to the end of todos
prepend | prep - inserts a text at the beginning of todos
listprojects [FILTER] | listproj | lp - list all projects
Expand Down Expand Up @@ -989,6 +1008,9 @@ fn update_global_from_conf(tc: &tml::Conf, conf: &mut Conf) {
if let Some(acda) = &tc.global.add_completion_date_always {
conf.add_completion_date_always = *acda;
}
if let Some(p) = &tc.global.editor {
conf.editor_path = Some(p.clone());
}
}

fn detect_conf_file_path() -> PathBuf {
Expand Down Expand Up @@ -1055,6 +1077,8 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
let program = args[0].clone();
let mut conf = Conf::new();

// Free short options: BCDEFGHIJKLMNOPQRSTUVWXYZbdfgjlmnopqruxyz"

let mut opts = Options::new();
opts.optflag("h", "help", "Show this help");
opts.optflag("a", "all", "Select all todos including completed ones");
Expand Down Expand Up @@ -1197,7 +1221,7 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
opts.optopt(
"",
"calendar",
"Display a calendar with dates highlighted if any todo is due on that date(foreground color). Today is highlighted with background color, Default values for `NUMBER` is `1` and for `TYPE` is `d`(days). Valid values for type are `d`(days), `w`(weeks), and `m`(months). Pepending plus sign shows the selected interval starting from today, not from Monday or first day of the month",
"Display a calendar with dates highlighted if any todo is due on that date(foreground color). Today is highlighted with background color, Default values for `NUMBER` is `1` and for `TYPE` is `d`(days). Valid values for type are `d`(days), `w`(weeks), and `m`(months). Prepending plus sign shows the selected interval starting from today, not from Monday or first day of the month",
"[+][NUMBER][TYPE]",
);
opts.optflag("", "syntax", "Enable keyword highlights when printing subject");
Expand All @@ -1220,7 +1244,7 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
opts.optopt(
"",
"priority-on-done",
"what to do with priority on task completion: keep - no special action(default behavior), move - place priority after completion date, tag - convert priority to a tag 'pri:', erase - remove priority. Notethat in all modes, except `erase`, the operation is reversible and on task uncompleting, the task gets its priority back",
"what to do with priority on task completion: keep - no special action(default behavior), move - place priority after completion date, tag - convert priority to a tag 'pri:', erase - remove priority. Note that in all modes, except `erase`, the operation is reversible and on task uncompleting, the task gets its priority back",
"VALUE",
);
opts.optflag(
Expand All @@ -1229,6 +1253,7 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
"When task is finished, always add completion date, regardless of whether or not creation date is present",
);
opts.optflag("k", "keep-tags", "in edit mode a new subject replaces regular text of the todo, everything else(tags, priority etc) is taken from the old and appended to the new subject. A convenient way to replace just text and keep all the tags without typing the tags again");
opts.optflag("i", "interactive", "Open an external edit to modify all filtered tasks. If the task list is modified inside an editor, the old tasks will be removed and new ones will be added to the end of the task list. If you do not change anything or save an empty file, the edit operation will be canceled. To set editor, change config.global.editor option or set EDITOR environment variable.");

let matches: Matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Expand Down Expand Up @@ -1311,6 +1336,7 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
if matches.opt_present("add-completion-date-always") {
conf.add_completion_date_always = true;
}
conf.use_editor = matches.opt_present("interactive");

let soon_days = conf.fmt.colors.soon_days;
conf.keep_empty = matches.opt_present("keep-empty");
Expand All @@ -1335,6 +1361,11 @@ pub fn parse_args(args: &[String]) -> Result<Conf> {
return Ok(conf);
}

if conf.use_editor && conf.mode != RunMode::Edit {
eprintln!("Option '--interactive' can be used only with `edit` command");
exit(1);
}

// second should be a range
if matches.free[idx].find(|c: char| !c.is_ascii_digit()).is_none() {
// a single ID
Expand Down
Loading