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
19 changes: 19 additions & 0 deletions src/common/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseArgsResult<T> {
Parsed(T),
Usage(String),
}

pub fn parse_args<T>(
args: &[&str],
usage: &str,
parser: impl FnOnce(&[&str]) -> Option<T>,
) -> ParseArgsResult<T> {
if args.is_empty() {
return ParseArgsResult::Usage(usage.to_string());
}
match parser(args) {
Some(parsed) => ParseArgsResult::Parsed(parsed),
None => ParseArgsResult::Usage(usage.to_string()),
}
}
1 change: 1 addition & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
}
}

pub mod command;
pub mod json_watch;
pub mod slug;
pub mod lru;
Expand Down
32 changes: 29 additions & 3 deletions src/plugins/brightness.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
use crate::actions::Action;
use crate::common::command::{parse_args, ParseArgsResult};
use crate::plugin::Plugin;

pub struct BrightnessPlugin;

const BRIGHT_USAGE: &str = "Usage: bright <0-100>";

fn usage_action(usage: &str) -> Action {
Action {
label: usage.into(),
desc: "Brightness".into(),
action: "query:bright ".into(),
args: None,
}
}

impl Plugin for BrightnessPlugin {
fn search(&self, query: &str) -> Vec<Action> {
let trimmed = query.trim();
let lowered = trimmed.to_ascii_lowercase();
if lowered.len() >= 2 && "bright".starts_with(&lowered) && lowered != "bright" {
return vec![usage_action(BRIGHT_USAGE)];
}
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "bright") {
if rest.trim().is_empty() {
return vec![Action {
label: "bright: edit brightness".into(),
desc: "Brightness".into(),
action: "brightness:dialog".into(),
args: None,
}];
}, usage_action(BRIGHT_USAGE)];
}
let rest = rest.trim();
if let Ok(val) = rest.parse::<u8>() {
if val <= 100 {
let args: Vec<&str> = rest.split_whitespace().collect();
match parse_args(&args, BRIGHT_USAGE, |args| {
if args.len() == 1 {
args[0].parse::<u8>().ok().filter(|val| *val <= 100)
} else {
None
}
}) {
ParseArgsResult::Parsed(val) => {
return vec![Action {
label: format!("Set brightness to {val}%"),
desc: "Brightness".into(),
action: format!("brightness:set:{val}"),
args: None,
}];
}
ParseArgsResult::Usage(usage) => {
return vec![usage_action(&usage)];
}
}
}
Vec::new()
Expand Down
207 changes: 134 additions & 73 deletions src/plugins/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! tests synchronized with the latest on-disk data.

use crate::actions::Action;
use crate::common::command::{parse_args, ParseArgsResult};
use crate::common::json_watch::{watch_json, JsonWatcher};
use crate::common::lru::LruCache;
use crate::common::query::parse_query_filters;
Expand All @@ -23,6 +24,25 @@ pub const TODO_FILE: &str = "todo.json";

static TODO_VERSION: AtomicU64 = AtomicU64::new(0);

const TODO_USAGE: &str =
"Usage: todo <add|list|rm|clear|pset|tag|edit|view|export> ...";
const TODO_ADD_USAGE: &str = "Usage: todo add <text> [p=<priority>] [#tag ...]";
const TODO_RM_USAGE: &str = "Usage: todo rm <text>";
const TODO_PSET_USAGE: &str = "Usage: todo pset <index> <priority>";
const TODO_TAG_USAGE: &str = "Usage: todo tag <index> [#tag ...] | todo tag <tag>";
const TODO_CLEAR_USAGE: &str = "Usage: todo clear";
const TODO_VIEW_USAGE: &str = "Usage: todo view";
const TODO_EXPORT_USAGE: &str = "Usage: todo export";

fn usage_action(usage: &str, query: &str) -> Action {
Action {
label: usage.into(),
desc: "Todo".into(),
action: format!("query:{query}"),
args: None,
}
}

#[derive(Serialize, Deserialize, Clone)]
pub struct TodoEntry {
pub text: String,
Expand Down Expand Up @@ -197,13 +217,13 @@ impl TodoPlugin {

fn search_internal(&self, trimmed: &str) -> Vec<Action> {
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "todo") {
if rest.is_empty() {
if rest.trim().is_empty() {
return vec![Action {
label: "todo: edit todos".into(),
desc: "Todo".into(),
action: "todo:dialog".into(),
args: None,
}];
}, usage_action(TODO_USAGE, "todo ")];
}
}

Expand Down Expand Up @@ -239,50 +259,63 @@ impl TodoPlugin {
.collect();
}

if trimmed.eq_ignore_ascii_case("todo view") {
return vec![Action {
label: "todo: view list".into(),
desc: "Todo".into(),
action: "todo:view".into(),
args: None,
}];
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "todo view") {
if rest.trim().is_empty() {
return vec![Action {
label: "todo: view list".into(),
desc: "Todo".into(),
action: "todo:view".into(),
args: None,
}];
}
return vec![usage_action(TODO_VIEW_USAGE, "todo view")];
}

if trimmed.eq_ignore_ascii_case("todo export") {
return vec![Action {
label: "Export todo list".into(),
desc: "Todo".into(),
action: "todo:export".into(),
args: None,
}];
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "todo export") {
if rest.trim().is_empty() {
return vec![Action {
label: "Export todo list".into(),
desc: "Todo".into(),
action: "todo:export".into(),
args: None,
}];
}
return vec![usage_action(TODO_EXPORT_USAGE, "todo export")];
}

if trimmed.eq_ignore_ascii_case("todo clear") {
return vec![Action {
label: "Clear completed todos".into(),
desc: "Todo".into(),
action: "todo:clear".into(),
args: None,
}];
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "todo clear") {
if rest.trim().is_empty() {
return vec![Action {
label: "Clear completed todos".into(),
desc: "Todo".into(),
action: "todo:clear".into(),
args: None,
}];
}
return vec![usage_action(TODO_CLEAR_USAGE, "todo clear")];
}

if trimmed.eq_ignore_ascii_case("todo add") {
return vec![Action {
label: "todo: edit todos".into(),
desc: "Todo".into(),
action: "todo:dialog".into(),
args: None,
}];
return vec![
Action {
label: "todo: edit todos".into(),
desc: "Todo".into(),
action: "todo:dialog".into(),
args: None,
},
usage_action(TODO_ADD_USAGE, "todo add "),
];
}

const ADD_PREFIX: &str = "todo add ";
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, ADD_PREFIX) {
let rest = rest.trim();
if !rest.is_empty() {
let args: Vec<&str> = rest.split_whitespace().collect();
match parse_args(&args, TODO_ADD_USAGE, |args| {
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() {
for part in args {
if let Some(p) = part.strip_prefix("p=") {
if let Ok(n) = p.parse::<u8>() {
priority = n;
Expand All @@ -293,11 +326,16 @@ impl TodoPlugin {
tags.push(tag.to_string());
}
} else {
words.push(part.to_string());
words.push((*part).to_string());
}
}
let text = words.join(" ");
if !text.is_empty() {
if text.is_empty() {
return None;
}
Some((text, priority, tags))
}) {
ParseArgsResult::Parsed((text, priority, tags)) => {
let tag_str = tags.join(",");
return vec![Action {
label: format!("Add todo {text}"),
Expand All @@ -306,78 +344,97 @@ impl TodoPlugin {
args: None,
}];
}
ParseArgsResult::Usage(usage) => {
return vec![usage_action(&usage, "todo add ")];
}
}
}

const PSET_PREFIX: &str = "todo pset ";
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, PSET_PREFIX) {
let rest = rest.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>())
{
let args: Vec<&str> = rest.split_whitespace().collect();
match parse_args(&args, TODO_PSET_USAGE, |args| {
let (idx_str, priority_str) = (args.get(0)?, args.get(1)?);
let idx = idx_str.parse::<usize>().ok()?;
let priority = priority_str.parse::<u8>().ok()?;
Some((idx, priority))
}) {
ParseArgsResult::Parsed((idx, priority)) => {
return vec![Action {
label: format!("Set priority {priority} for todo {idx}"),
desc: "Todo".into(),
action: format!("todo:pset:{idx}|{priority}"),
args: None,
}];
}
ParseArgsResult::Usage(usage) => {
return vec![usage_action(&usage, "todo pset ")];
}
}
}

const TAG_PREFIX: &str = "todo tag ";
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, TAG_PREFIX) {
let rest = rest.trim();
let mut parts = rest.split_whitespace();
if let Some(first) = parts.next() {
if let Ok(idx) = first.parse::<usize>() {
let args: Vec<&str> = rest.split_whitespace().collect();
if let ParseArgsResult::Parsed((idx, tags)) =
parse_args(&args, TODO_TAG_USAGE, |args| {
let first = args.get(0)?;
let idx = first.parse::<usize>().ok()?;
let mut tags: Vec<String> = Vec::new();
for t in parts {
for t in args.iter().skip(1) {
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,
}];
} else {
let filter = rest;
let guard = match self.data.read() {
Ok(g) => g,
Err(_) => return Vec::new(),
};
let mut entries: Vec<(usize, &TodoEntry)> = guard.iter().enumerate().collect();
entries
.retain(|(_, t)| t.tags.iter().any(|tg| tg.eq_ignore_ascii_case(filter)));
entries.sort_by(|a, b| b.1.priority.cmp(&a.1.priority));
return entries
.into_iter()
.map(|(idx, t)| Action {
label: format!(
"{} {}",
if t.done { "[x]" } else { "[ ]" },
t.text.clone()
),
desc: "Todo".into(),
action: format!("query:todo tag {idx} "),
args: None,
})
.collect();
}
Some((idx, tags))
})
{
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,
}];
}

if rest.is_empty() {
return vec![usage_action(TODO_TAG_USAGE, "todo tag ")];
}

let filter = rest;
let guard = match self.data.read() {
Ok(g) => g,
Err(_) => return Vec::new(),
};
let mut entries: Vec<(usize, &TodoEntry)> = guard.iter().enumerate().collect();
entries.retain(|(_, t)| t.tags.iter().any(|tg| tg.eq_ignore_ascii_case(filter)));
entries.sort_by(|a, b| b.1.priority.cmp(&a.1.priority));
return entries
.into_iter()
.map(|(idx, t)| Action {
label: format!(
"{} {}",
if t.done { "[x]" } else { "[ ]" },
t.text.clone()
),
desc: "Todo".into(),
action: format!("query:todo tag {idx} "),
args: None,
})
.collect();
}

const RM_PREFIX: &str = "todo rm ";
if let Some(rest) = crate::common::strip_prefix_ci(trimmed, RM_PREFIX) {
let filter = rest.trim();
if filter.is_empty() {
return vec![usage_action(TODO_RM_USAGE, "todo rm ")];
}
let guard = match self.data.read() {
Ok(g) => g,
Err(_) => return Vec::new(),
Expand Down Expand Up @@ -455,6 +512,10 @@ impl TodoPlugin {
.collect();
}

if crate::common::strip_prefix_ci(trimmed, "todo").is_some() {
return vec![usage_action(TODO_USAGE, "todo ")];
}

Vec::new()
}
}
Expand Down
Loading