diff --git a/crates/jp_attachment_bear_note/README.md b/crates/jp_attachment_bear_note/README.md index 73b9917e..3fd9f6bb 100644 --- a/crates/jp_attachment_bear_note/README.md +++ b/crates/jp_attachment_bear_note/README.md @@ -36,6 +36,13 @@ Fetch a list of notes tagged with a specific tag: jp attachment add "bear://search/?tag=project/my-project" ``` +Exclude archived notes from a search. +Archived notes still match, but their content is left out of the attachment: + +```sh +jp attachment add "bear://search/?tag=project/my-project&exclude_archived=true" +``` + List all added URIs: ```sh diff --git a/crates/jp_attachment_bear_note/src/lib.rs b/crates/jp_attachment_bear_note/src/lib.rs index 51ee8f78..805884f6 100644 --- a/crates/jp_attachment_bear_note/src/lib.rs +++ b/crates/jp_attachment_bear_note/src/lib.rs @@ -27,14 +27,20 @@ impl BearNotes { fn query_to_uri(&self, query: &Query) -> Result> { let (host, path, query_pairs) = match query { Query::Get(path) => ("get", path, vec![]), - Query::Search { query, tags } => ( - "search", + Query::Search { query, - tags.clone() + tags, + exclude_archived, + } => { + let mut pairs: Vec<(String, String)> = tags .iter() .map(|t| ("tag".to_owned(), t.to_owned())) - .collect::>(), - ), + .collect(); + if *exclude_archived { + pairs.push(("exclude_archived".to_owned(), "true".to_owned())); + } + ("search", query, pairs) + } }; let query_pairs = query_pairs @@ -64,7 +70,16 @@ enum Query { Get(String), /// Search for a note by its title or content, optionally filtering by tags. - Search { query: String, tags: Vec }, + Search { + query: String, + tags: Vec, + + /// Drop archived notes from the results instead of attaching their full + /// content. + /// Archived notes still match; they're just not returned. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + exclude_archived: bool, + }, } /// A note from the Bear note-taking app, formatted for attachment XML output. @@ -179,12 +194,21 @@ fn uri_to_query(uri: &Url) -> Result> { .map(Query::Get)?, Some("get") => Query::Get(path), Some("search") => { - let tags = query_pairs - .into_iter() - .filter_map(|(k, v)| if k == "tag" { Some(v) } else { None }) - .collect(); + let mut tags = vec![]; + let mut exclude_archived = false; + for (k, v) in query_pairs { + match k.as_str() { + "tag" => tags.push(v), + "exclude_archived" => exclude_archived = v != "false" && v != "0", + _ => {} + } + } - Query::Search { query: path, tags } + Query::Search { + query: path, + tags, + exclude_archived, + } } // Shorthand: `bear:NOTE_ID`. Truly opaque form (no `//`, no // leading `/`) — the path holds the note id directly. Gated on @@ -207,7 +231,11 @@ fn get_notes(query: &Query, db: &BearDb) -> Result, Box { + Query::Search { + query, + tags, + exclude_archived, + } => { let matches = db .search(&SearchParams { queries: vec![query.clone()], @@ -216,8 +244,14 @@ fn get_notes(query: &Query, db: &BearDb) -> Result, Box = matches.iter().map(|m| m.note_id.as_str()).collect(); + // Archived notes match like any other. When excluded, drop them + // before fetching content so their (often large) bodies never + // reach the assistant. + let ids: Vec<_> = matches + .iter() + .filter(|m| !*exclude_archived || !m.is_archived) + .map(|m| m.note_id.as_str()) + .collect(); db.get_notes(&ids) .map_err(|e| e.to_string())? .into_iter() diff --git a/crates/jp_attachment_bear_note/src/lib_tests.rs b/crates/jp_attachment_bear_note/src/lib_tests.rs index 8cbeb275..ea5fb429 100644 --- a/crates/jp_attachment_bear_note/src/lib_tests.rs +++ b/crates/jp_attachment_bear_note/src/lib_tests.rs @@ -53,6 +53,7 @@ fn test_uri_to_query() { Ok(Query::Search { query: "tag #1".to_string(), tags: vec![], + exclude_archived: false, }), ), ( @@ -60,6 +61,7 @@ fn test_uri_to_query() { Ok(Query::Search { query: "tag #1".to_string(), tags: vec!["tag #2".to_string()], + exclude_archived: false, }), ), ( @@ -67,6 +69,23 @@ fn test_uri_to_query() { Ok(Query::Search { query: "tag #1".to_string(), tags: vec!["tag #2".to_string(), "tag #3".to_string()], + exclude_archived: false, + }), + ), + ( + "bear://search/?tag=foo&exclude_archived=true", + Ok(Query::Search { + query: String::new(), + tags: vec!["foo".to_string()], + exclude_archived: true, + }), + ), + ( + "bear://search/?tag=foo&exclude_archived=false", + Ok(Query::Search { + query: String::new(), + tags: vec!["foo".to_string()], + exclude_archived: false, }), ), ( @@ -81,3 +100,16 @@ fn test_uri_to_query() { assert_eq!(query, expected); } } + +#[test] +fn test_exclude_archived_round_trips() { + let handler = BearNotes::default(); + let query = Query::Search { + query: String::new(), + tags: vec!["rfd/D46/review".to_string()], + exclude_archived: true, + }; + + let uri = handler.query_to_uri(&query).unwrap(); + assert_eq!(uri_to_query(&uri).unwrap(), query); +} diff --git a/justfile b/justfile index 3ec4ad17..4b0bd322 100644 --- a/justfile +++ b/justfile @@ -1926,8 +1926,9 @@ _resolve-conversation TITLE: # Internal: look up a Bear note (or notes) by tag. # -# Resolves `bear://search/?tag=TAG` against the local Bear database. Outputs -# one of: +# Resolves `bear://search/?tag=TAG` against the local Bear database. Archived +# notes are excluded: they're kept for reference, not for feeding into a +# session. Outputs one of: # # FOUND - at least one note matched; caller should attach URI # EDIT - no notes matched; caller should add `--edit` @@ -1941,7 +1942,7 @@ _bear-note TAG: #!/usr/bin/env sh set -eu - uri="bear://search/?tag={{TAG}}" + uri="bear://search/?tag={{TAG}}&exclude_archived=true" if jp attachment print "$uri" 2>/dev/null | grep -q .; then echo "FOUND $uri" exit 0