Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Built-in plugins and their command prefixes are:
- Shell commands (`sh echo hi`)
- Search history (`hi`)
- Quick notes (`note add <text>`, `note list`, `note rm <pattern>`)
- Todo items (`todo add <task>`, `todo list`, `todo rm <pattern>`, `todo clear`)
- Todo items (`todo add <task> p=<n> #tag`, `todo list`, `todo rm <pattern>`, `todo pset <idx> <n>`, `todo tag <idx> #tag`, `todo clear`)
- Text snippets (`cs`, `cs list`, `cs rm`, `cs add <alias> <text>`, `cs edit`)
- Recycle Bin cleanup (`rec`)
- Temporary files (`tmp`, `tmp new [name]`, `tmp open`, `tmp clear`, `tmp list`, `tmp rm`)
Expand Down
58 changes: 52 additions & 6 deletions src/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,12 +1069,15 @@ impl eframe::App for LauncherApp {
refresh = true;
set_focus = true;
if self.enable_toasts {
if let Some(text) = a.action.strip_prefix("todo:add:") {
if let Some(text) = a
.action
.strip_prefix("todo:add:")
.and_then(|r| r.split('|').next())
{
self.toasts.add(Toast {
text: format!("Added todo {text}").into(),
kind: ToastKind::Success,
options: ToastOptions::default()
.duration_in_seconds(3.0),
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
}
Expand Down Expand Up @@ -1104,6 +1107,26 @@ impl eframe::App for LauncherApp {
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
} else if a.action.starts_with("todo:pset:") {
refresh = true;
set_focus = true;
if self.enable_toasts {
self.toasts.add(Toast {
text: "Updated todo priority".into(),
kind: ToastKind::Success,
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
} else if a.action.starts_with("todo:tag:") {
refresh = true;
set_focus = true;
if self.enable_toasts {
self.toasts.add(Toast {
text: "Updated todo tags".into(),
kind: ToastKind::Success,
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
} else if a.action == "todo:clear" {
refresh = true;
set_focus = true;
Expand Down Expand Up @@ -1588,12 +1611,15 @@ impl eframe::App for LauncherApp {
refresh = true;
set_focus = true;
if self.enable_toasts {
if let Some(text) = a.action.strip_prefix("todo:add:") {
if let Some(text) = a
.action
.strip_prefix("todo:add:")
.and_then(|r| r.split('|').next())
{
self.toasts.add(Toast {
text: format!("Added todo {text}").into(),
kind: ToastKind::Success,
options: ToastOptions::default()
.duration_in_seconds(3.0),
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
}
Expand Down Expand Up @@ -1627,6 +1653,26 @@ impl eframe::App for LauncherApp {
.duration_in_seconds(3.0),
});
}
} else if a.action.starts_with("todo:pset:") {
refresh = true;
set_focus = true;
if self.enable_toasts {
self.toasts.add(Toast {
text: "Updated todo priority".into(),
kind: ToastKind::Success,
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
} else if a.action.starts_with("todo:tag:") {
refresh = true;
set_focus = true;
if self.enable_toasts {
self.toasts.add(Toast {
text: "Updated todo tags".into(),
kind: ToastKind::Success,
options: ToastOptions::default().duration_in_seconds(3.0),
});
}
} else if a.action == "todo:clear" {
refresh = true;
set_focus = true;
Expand Down
43 changes: 40 additions & 3 deletions src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,52 @@ pub fn launch_action(action: &Action) -> anyhow::Result<()> {
}
return Ok(());
}
if let Some(text) = action.action.strip_prefix("todo:add:") {
if let Some(rest) = action.action.strip_prefix("todo:add:") {
let mut parts = rest.splitn(3, '|');
let text = parts.next().unwrap_or("");
let priority = parts
.next()
.and_then(|p| p.parse::<u8>().ok())
.unwrap_or(0);
let tags: Vec<String> = parts
.next()
.map(|t| {
if t.is_empty() {
Vec::new()
} else {
t.split(',').map(|s| s.to_string()).collect()
}
})
.unwrap_or_default();
crate::plugins::todo::append_todo(
crate::plugins::todo::TODO_FILE,
text,
0,
&[],
priority,
&tags,
)?;
return Ok(());
}
if let Some(rest) = action.action.strip_prefix("todo:pset:") {
if let Some((idx_str, p_str)) = rest.split_once('|') {
if let (Ok(idx), Ok(priority)) = (idx_str.parse::<usize>(), p_str.parse::<u8>()) {
crate::plugins::todo::set_priority(crate::plugins::todo::TODO_FILE, idx, priority)?;
}
}
return Ok(());
}
if let Some(rest) = action.action.strip_prefix("todo:tag:") {
if let Some((idx_str, tags_str)) = rest.split_once('|') {
if let Ok(idx) = idx_str.parse::<usize>() {
let tags: Vec<String> = if tags_str.is_empty() {
Vec::new()
} else {
tags_str.split(',').map(|s| s.to_string()).collect()
};
crate::plugins::todo::set_tags(crate::plugins::todo::TODO_FILE, idx, &tags)?;
}
}
return Ok(());
}
if let Some(idx) = action.action.strip_prefix("todo:remove:") {
if let Ok(i) = idx.parse::<usize>() {
crate::plugins::todo::remove_todo(crate::plugins::todo::TODO_FILE, i)?;
Expand Down
83 changes: 75 additions & 8 deletions src/plugins/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,79 @@ impl Plugin for TodoPlugin {
if trimmed.len() >= ADD_PREFIX.len()
&& trimmed[..ADD_PREFIX.len()].eq_ignore_ascii_case(ADD_PREFIX)
{
let text = trimmed[ADD_PREFIX.len()..].trim();
if !text.is_empty() {
return vec![Action {
label: format!("Add todo {text}"),
desc: "Todo".into(),
action: format!("todo:add:{text}"),
args: None,
}];
let rest = trimmed[ADD_PREFIX.len()..].trim();
if !rest.is_empty() {
let mut priority: u8 = 0;
let mut tags: Vec<String> = Vec::new();
let mut words: Vec<String> = Vec::new();
for part in rest.split_whitespace() {
if let Some(p) = part.strip_prefix("p=") {
if let Ok(n) = p.parse::<u8>() {
priority = n;
}
} else if let Some(tag) = part.strip_prefix('#') {
if !tag.is_empty() {
tags.push(tag.to_string());
}
} else {
words.push(part.to_string());
}
}
let text = words.join(" ");
if !text.is_empty() {
let tag_str = tags.join(",");
return vec![Action {
label: format!("Add todo {text}"),
desc: "Todo".into(),
action: format!("todo:add:{text}|{priority}|{tag_str}"),
args: None,
}];
}
}
}

const PSET_PREFIX: &str = "todo pset ";
if trimmed.len() >= PSET_PREFIX.len()
&& trimmed[..PSET_PREFIX.len()].eq_ignore_ascii_case(PSET_PREFIX)
{
let rest = trimmed[PSET_PREFIX.len()..].trim();
let mut parts = rest.split_whitespace();
if let (Some(idx_str), Some(priority_str)) = (parts.next(), parts.next()) {
if let (Ok(idx), Ok(priority)) = (idx_str.parse::<usize>(), priority_str.parse::<u8>()) {
return vec![Action {
label: format!("Set priority {priority} for todo {idx}"),
desc: "Todo".into(),
action: format!("todo:pset:{idx}|{priority}"),
args: None,
}];
}
}
}

const TAG_PREFIX: &str = "todo tag ";
if trimmed.len() >= TAG_PREFIX.len()
&& trimmed[..TAG_PREFIX.len()].eq_ignore_ascii_case(TAG_PREFIX)
{
let rest = trimmed[TAG_PREFIX.len()..].trim();
let mut parts = rest.split_whitespace();
if let Some(idx_str) = parts.next() {
if let Ok(idx) = idx_str.parse::<usize>() {
let mut tags: Vec<String> = Vec::new();
for t in parts {
if let Some(tag) = t.strip_prefix('#') {
if !tag.is_empty() {
tags.push(tag.to_string());
}
}
}
let tag_str = tags.join(",");
return vec![Action {
label: format!("Set tags for todo {idx}"),
desc: "Todo".into(),
action: format!("todo:tag:{idx}|{tag_str}"),
args: None,
}];
}
}
}

Expand Down Expand Up @@ -276,6 +341,8 @@ impl Plugin for TodoPlugin {
Action { label: "todo list".into(), desc: "Todo".into(), action: "query:todo list".into(), args: None },
Action { label: "todo rm".into(), desc: "Todo".into(), action: "query:todo rm ".into(), args: None },
Action { label: "todo clear".into(), desc: "Todo".into(), action: "query:todo clear".into(), args: None },
Action { label: "todo pset".into(), desc: "Todo".into(), action: "query:todo pset ".into(), args: None },
Action { label: "todo tag".into(), desc: "Todo".into(), action: "query:todo tag ".into(), args: None },
]
}
}
2 changes: 1 addition & 1 deletion tests/preserve_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fn todo_add_preserves_prefix() {
let actions = vec![Action {
label: "todo".into(),
desc: "".into(),
action: "todo:add:test".into(),
action: "todo:add:test|0|".into(),
args: None,
}];
let mut app = new_app(&ctx, actions, true);
Expand Down
23 changes: 22 additions & 1 deletion tests/todo_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ fn search_add_returns_action() {
let plugin = TodoPlugin::default();
let results = plugin.search("todo add task ");
assert_eq!(results.len(), 1);
assert_eq!(results[0].action, "todo:add:task");
assert_eq!(results[0].action, "todo:add:task|0|");
}

#[test]
fn search_add_with_priority_and_tags() {
let _lock = TEST_MUTEX.lock().unwrap();
let plugin = TodoPlugin::default();
let results = plugin.search("todo add task p=3 #a #b");
assert_eq!(results.len(), 1);
assert_eq!(results[0].action, "todo:add:task|3|a,b");
}

#[test]
Expand Down Expand Up @@ -109,6 +118,18 @@ fn set_priority_and_tags_update_entry() {
assert_eq!(todos[0].tags, vec!["a", "b"]);
}

#[test]
fn search_pset_and_tag_actions() {
let _lock = TEST_MUTEX.lock().unwrap();
let plugin = TodoPlugin::default();
let res = plugin.search("todo pset 1 4");
assert_eq!(res.len(), 1);
assert_eq!(res[0].action, "todo:pset:1|4");
let res = plugin.search("todo tag 2 #x #y");
assert_eq!(res.len(), 1);
assert_eq!(res[0].action, "todo:tag:2|x,y");
}

#[test]
fn list_without_filter_sorts_by_priority() {
let _lock = TEST_MUTEX.lock().unwrap();
Expand Down