diff --git a/Cargo.lock b/Cargo.lock index a1a2214..ec5a6ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,7 +178,7 @@ checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -353,6 +353,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -420,7 +426,7 @@ dependencies = [ "atty", "bitflags 1.3.2", "clap_lex 0.2.4", - "indexmap 1.6.2", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -491,7 +497,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e87cbed5354f17bd8ca8821a097fb62599787fe8f611743fad7ee156a0a600" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "serde", "winapi", @@ -532,13 +538,12 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "confy" -version = "0.5.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" +checksum = "2913470204e9e8498a0f31f17f90a0de801ae92c8c5ac18c49af4819e6786697" dependencies = [ "directories", "serde", - "thiserror", "toml 0.5.8", ] @@ -586,7 +591,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -595,7 +600,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eb9105919ca8e40d437fc9cbb8f1975d916f1bd28afe795a48aae32a2cc8920" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -609,7 +614,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -619,7 +624,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", ] @@ -631,7 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" dependencies = [ "autocfg", - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -641,7 +646,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crossbeam-utils", ] @@ -651,7 +656,7 @@ version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -806,10 +811,11 @@ dependencies = [ [[package]] name = "directories" -version = "4.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" dependencies = [ + "cfg-if 0.1.10", "dirs-sys", ] @@ -819,7 +825,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a28ccebc1239c5c57f0c55986e2ac03f49af0d0ca3dff29bfcad39d60a8be56" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "dirs-sys-next", ] @@ -829,7 +835,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "dirs-sys-next", ] @@ -885,7 +891,7 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1121,7 +1127,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi", ] @@ -1242,9 +1248,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -1408,12 +1414,12 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.6.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.9.1", + "hashbrown 0.12.3", ] [[package]] @@ -1445,7 +1451,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1607,7 +1613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "libc", ] @@ -1619,7 +1625,7 @@ checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "libc", "memoffset", "pin-utils", @@ -1715,7 +1721,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall 0.2.13", @@ -2434,7 +2440,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "redox_syscall 0.4.1", "rustix", @@ -2879,7 +2885,7 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -2904,7 +2910,7 @@ version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -3218,7 +3224,7 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index 822329f..47e69c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ clap_complete = "4.4.5" sled = "0.34.7" # Configuration management -confy = "0.5.1" +confy = "0.4.0" directories-next = "1.0.1" # Error handling diff --git a/README.md b/README.md index 07b6ef0..6c7273e 100644 --- a/README.md +++ b/README.md @@ -137,50 +137,55 @@ you got it from, if ever you feel like you're missing context. You can filter the annotations you want to modify or export using the following options in most gooseberry commands: ``` -FLAGS: - -i, --include-updated - Include annotations updated in given time range (instead of just created) + --from + Only annotations created after this date and time + + Can be colloquial, e.g. "last Friday 8pm" - -n, --not - Annotations NOT matching the given filter criteria + --before + Only annotations created before this date and time + + Can be colloquial, e.g. "last Friday 8pm" - -o, --or - (Use with --tags) Annotations matching ANY of the given tags + --uri + Only annotations with this pattern in their URL + + Doesn't have to be the full URL, e.g. "wikipedia" - -p, --page - Only page notes + --any + Only annotations with this pattern in their `quote`, `tags`, `text`, or `uri` - -a, --annotation - Only annotations (i.e exclude page notes) + --tags + Only annotations with ANY of these tags (use --and to match ALL) + --groups + Only annotations from these groups -OPTIONS: - --from - Only annotations created after this date and time + --exclude-tags + Only annotations without ANY of these tags - Can be colloquial, e.g. "last Friday 8pm" - --before - Only annotations created before this date and time + --quote + Only annotations that contain this text inside the text that was annotated - Can be colloquial, e.g. "last Friday 8pm" - --uri - Only annotations with this pattern in their URL + --text + Only annotations that contain this text in their textual body - Doesn't have to be the full URL, e.g. "wikipedia" [default: ] - --any - Only annotations with this pattern in their `quote`, `tags`, `text`, or `uri` [default: ] +-i, --include-updated + Include annotations updated in given time range (instead of just created) +-n, --not + Annotations NOT matching the given filter criteria - --tags ... - Only annotations with ALL of these tags (use --or to match ANY) + --and + (Use with --tags) Annotations matching ALL of the given tags - --exclude-tags ... - Only annotations without ANY of these tags +-p, --page + Only page notes - --quote - Only annotations that contain this text inside the text that was annotated [default: ] +-a, --annotation + Only annotations (i.e exclude page notes) - --text - Only annotations that contain this text in their textual body [default: ] +-f, --fuzzy + Toggle fuzzy search ``` ## Customization @@ -199,7 +204,7 @@ variable `$GOOSEBERRY_CONFIG` to point to the file. Authorize Hypothesis either by setting the `$HYPOTHESIS_NAME` and `$HYPOTHESIS_KEY` environment variables to your username and developer API token or by running `gooseberry config authorize`. -Gooseberry takes annotations from a given Hypothesis group which you can create/set with `gooseberry config group`. +Gooseberry takes annotations from given Hypothesis group(s) which you can create/set with `gooseberry config group`. ### Knowledge base @@ -241,6 +246,7 @@ The following keys can be used inside the template * `{{ text }}` - The text content of the annotation body * `tags` - A list of tags associated with the annotation. * `{{ group }}` - ID of Hypothesis group, +* `{{ group_name }}` - Name of Hypothesis group, * `references` - List of annotation IDs for any annotations this annotation references (e.g. is a reply to) * `{{ display_name }}` - Display name of annotation creator. This may not be set. @@ -348,12 +354,14 @@ The available options are: * BaseURI - Groups annotations by their base URI * Title - Group annotations by the title of their webpage/article/document * ID - Groups annotations by annotation ID. +* Group - Groups annotations by group ID. +* GroupName - Groups annotations by group name. Multiple hierarchies combined make folders and sub-folders, with the last entry defining pages. e.g. -`hierarchy = ["BaseURI", "Tag"]` would make a separate folder for each base_uri. Within each folder would be a page for each tag consisting of +`hierarchy = ["Group", "Tag"]` would make a separate folder for each group. Within each folder would be a page for each tag consisting of annotations marked with that tag. `hierarchy = ["Tag"]` gives the structure in the `mdbook` figure above, i.e. no folders, a page for each tag. @@ -371,6 +379,8 @@ The available options are: * BaseURI * Title * ID +* Group +* GroupName * Created * Updated diff --git a/src/configuration.rs b/src/configuration.rs index de841d4..777c7e6 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,10 +1,11 @@ +use std::collections::{HashMap, HashSet}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::{env, fmt, fs, io}; use chrono::Utc; use color_eyre::Help; -use dialoguer::{theme, Confirm, Input, Select}; +use dialoguer::{theme, Confirm, Input, MultiSelect, Select}; use directories_next::{ProjectDirs, UserDirs}; use eyre::eyre; use hypothesis::annotations::{Annotation, Document, Permissions, Selector, Target, UserInfo}; @@ -21,6 +22,7 @@ pub static DEFAULT_NESTED_TAG: &str = "/"; pub static DEFAULT_ANNOTATION_TEMPLATE: &str = r#" ### {{id}} +Group: {{group}} ({{group_name}}) Created: {{date_format "%c" created}} Tags: {{#each tags}}{{this}}{{#unless @last}}, {{/unless}}{{/each}} @@ -51,6 +53,8 @@ pub enum OrderBy { Empty, Created, Updated, + Group, + GroupName, } impl fmt::Display for OrderBy { @@ -64,6 +68,8 @@ impl fmt::Display for OrderBy { OrderBy::Empty => write!(f, "empty"), OrderBy::Created => write!(f, "created"), OrderBy::Updated => write!(f, "updated"), + OrderBy::Group => write!(f, "group"), + OrderBy::GroupName => write!(f, "group_name"), } } } @@ -77,7 +83,6 @@ pub struct GooseberryConfig { pub(crate) hypothesis_key: Option, /// Hypothesis group with knowledge base annotations pub(crate) hypothesis_group: Option, - /// Related to tagging and editing /// Directory to store `sled` database files pub(crate) db_dir: PathBuf, @@ -103,6 +108,9 @@ pub struct GooseberryConfig { pub(crate) ignore_tags: Option>, /// Define nested tag pattern pub(crate) nested_tag: Option, + /// Hypothesis groups with knowledge base annotations + #[serde(default)] + pub(crate) hypothesis_groups: HashMap, } /// Main project directory, cross-platform @@ -116,6 +124,7 @@ impl Default for GooseberryConfig { hypothesis_username: None, hypothesis_key: None, hypothesis_group: None, + hypothesis_groups: HashMap::new(), db_dir: get_project_dir() .map(|dir| dir.data_dir().join("gooseberry_db")) .expect("Couldn't make database directory"), @@ -146,7 +155,6 @@ impl GooseberryConfig { r#" hypothesis_username = '' hypothesis_key = '' -hypothesis_group = '' db_dir = '' kb_dir = '' hierarchy = ['Tag'] @@ -275,7 +283,7 @@ file_extension = '{}' } } None => { - Ok(confy::load(NAME, None).suggestion(Apologize::ConfigError { + Ok(confy::load(NAME).suggestion(Apologize::ConfigError { message: "Couldn't load from the default config location, maybe you don't have access? \ Try running `gooseberry config default config_file.toml`, modify the generated file, \ then `export GOOSEBERRY_CONFIG=`".into() @@ -299,9 +307,12 @@ file_extension = '{}' { config.set_credentials().await?; } - - if config.hypothesis_group.is_none() { - config.set_group(None).await?; + if config.hypothesis_groups.is_empty() { + let mut group_ids = Vec::new(); + if let Some(ref group_id) = config.hypothesis_group { + group_ids.push(group_id.to_owned()); + } + config.set_groups(group_ids).await?; } Ok(config) } @@ -407,6 +418,8 @@ file_extension = '{}' OrderBy::BaseURI, OrderBy::Title, OrderBy::ID, + OrderBy::Group, + OrderBy::GroupName, ]; let order = Self::get_order_bys(selections)?; if order.is_empty() { @@ -448,6 +461,8 @@ file_extension = '{}' OrderBy::Title, OrderBy::Created, OrderBy::Updated, + OrderBy::Group, + OrderBy::GroupName, ]; let order = Self::get_order_bys(selections)?; @@ -563,7 +578,10 @@ file_extension = '{}' display_name: Some("test_display_name".to_string()), }), }; - let test_markdown_annotation = AnnotationTemplate::from_annotation(test_annotation); + let mut group_name_mapping = HashMap::new(); + group_name_mapping.insert("group_id".to_owned(), "group_name".to_owned()); + let test_markdown_annotation = + AnnotationTemplate::from_annotation(test_annotation, &group_name_mapping); self.annotation_template = loop { let template = utils::external_editor_input( Some( @@ -656,7 +674,11 @@ file_extension = '{}' }; let mut test_annotation_2 = test_annotation_1.clone(); test_annotation_2.text = "Another annotation".to_string(); + test_annotation_2.group = "group_id_2".to_string(); + let mut group_name_mapping = HashMap::new(); + group_name_mapping.insert("group_id".to_owned(), "group_name".to_owned()); + group_name_mapping.insert("group_id_2".to_owned(), "group_name_2".to_owned()); let templates = Templates { annotation_template: self .annotation_template @@ -674,11 +696,16 @@ file_extension = '{}' }, annotations: vec![test_annotation_1.clone(), test_annotation_2.clone()] .into_iter() - .map(|a| hbs.render("annotation", &AnnotationTemplate::from_annotation(a))) + .map(|a| { + hbs.render( + "annotation", + &AnnotationTemplate::from_annotation(a, &group_name_mapping), + ) + }) .collect::, _>>()?, raw_annotations: vec![ - AnnotationTemplate::from_annotation(test_annotation_1), - AnnotationTemplate::from_annotation(test_annotation_2), + AnnotationTemplate::from_annotation(test_annotation_1, &group_name_mapping), + AnnotationTemplate::from_annotation(test_annotation_2, &group_name_mapping), ], }; @@ -792,10 +819,77 @@ file_extension = '{}' Ok(()) } - /// Sets the Hypothesis group used for Gooseberry annotations - /// This opens a command-line prompt wherein the user can select creating a new group or - /// using an existing group by ID - pub async fn set_group(&mut self, group_id: Option) -> color_eyre::Result<()> { + /// This opens a command-line prompt where the user can select from either creating a new group or + /// using an existing group by ID, with the option of selecting multiple groups + pub async fn get_groups(&self, api: Hypothesis) -> color_eyre::Result> { + let selections = &[ + "Create a new Hypothesis group", + "Use existing Hypothesis groups", + ]; + let selection = Select::with_theme(&theme::ColorfulTheme::default()) + .with_prompt("Where should gooseberry take annotations from?") + .items(&selections[..]) + .interact()?; + let mut selected = HashSet::new(); + if selection == 0 { + loop { + let group_name = utils::user_input("Enter a group name", Some(NAME), true, false)?; + let group_description = utils::user_input( + "Enter a group description", + Some("Gooseberry knowledge base annotations"), + true, + true, + )?; + + let group_id = api + .create_group(&group_name, Some(&group_description)) + .await? + .id; + + selected.insert(group_id.clone()); + if Confirm::with_theme(&theme::ColorfulTheme::default()) + .with_prompt("Add more groups?") + .interact()? + { + continue; + } else { + break; + } + } + } + let groups = api + .get_groups(&hypothesis::groups::GroupFilters::default()) + .await?; + let group_selection: Vec<_> = groups + .iter() + .map(|g| format!("{}: {}", g.id, g.name)) + .collect(); + let defaults: Vec<_> = groups.iter().map(|g| selected.contains(&g.id)).collect(); + let mut group_name_mapping = HashMap::new(); + for group_index in MultiSelect::with_theme(&theme::ColorfulTheme::default()) + .with_prompt("Which groups should gooseberry use?") + .items(&group_selection[..]) + .defaults(&defaults[..]) + .interact()? + { + api.fetch_group(&groups[group_index].id, Vec::new()) + .await + .map_err(|error| Apologize::GroupNotFound { + id: groups[group_index].id.clone(), + error, + })?; + group_name_mapping.insert( + groups[group_index].id.to_owned(), + groups[group_index].name.to_owned(), + ); + } + Ok(group_name_mapping) + } + + /// Sets the Hypothesis groups used for Gooseberry annotations + /// This opens a command-line prompt where the user can select from either creating a new group or + /// using an existing group by ID, with the option of selecting multiple groups + pub async fn set_groups(&mut self, group_ids: Vec) -> color_eyre::Result<()> { let (username, key) = ( self.hypothesis_username .as_deref() @@ -805,61 +899,22 @@ file_extension = '{}' .ok_or_else(|| eyre!("No Hypothesis key"))?, ); let api = Hypothesis::new(username, key)?; - if let Some(group_id) = group_id { - if api.fetch_group(&group_id, Vec::new()).await.is_ok() { - self.hypothesis_group = Some(group_id); - self.store()?; - return Ok(()); - } else { - println!( - "\nGroup could not be loaded, please try again.\n\ - Make sure the group exists and you are authorized to access it.\n\n" - ) + if group_ids.is_empty() { + self.hypothesis_groups = self.get_groups(api).await?; + } else { + for group_id in group_ids { + let group = api + .fetch_group(&group_id, Vec::new()) + .await + .map_err(|error| Apologize::GroupNotFound { + id: group_id.clone(), + error, + })?; + self.hypothesis_groups + .insert(group.id.to_owned(), group.name.to_owned()); } } - let selections = &[ - "Create a new Hypothesis group", - "Use an existing Hypothesis group", - ]; - - let group_id = loop { - let selection = Select::with_theme(&theme::ColorfulTheme::default()) - .with_prompt("Where should gooseberry take annotations from?") - .items(&selections[..]) - .interact()?; - - if selection == 0 { - let group_name = utils::user_input("Enter a group name", Some(NAME), true, false)?; - let group_id = Hypothesis::new(username, key)? - .create_group(&group_name, Some("Gooseberry knowledge base annotations")) - .await? - .id; - break group_id; - } else { - let groups = api - .get_groups(&hypothesis::groups::GroupFilters::default()) - .await?; - let group_selection: Vec<_> = groups - .iter() - .map(|g| format!("{}: {}", g.id, g.name)) - .collect(); - let group_index = Select::with_theme(&theme::ColorfulTheme::default()) - .with_prompt("Which group should gooseberry use?") - .items(&group_selection[..]) - .interact()?; - let group_id = groups[group_index].id.to_owned(); - if api.fetch_group(&group_id, Vec::new()).await.is_ok() { - break group_id; - } else { - println!( - "\nGroup could not be loaded, please try again.\n\ - Make sure the group exists and you are authorized to access it.\n\n" - ) - } - } - }; - - self.hypothesis_group = Some(group_id); + self.hypothesis_group = None; self.store()?; Ok(()) } @@ -930,7 +985,7 @@ file_extension = '{}' message: "The current config_file location does not seem to have write access. \ Use `export GOOSEBERRY_CONFIG=` to set a new location".into() })?, - None => confy::store(NAME, None, (*self).clone()).suggestion(Apologize::ConfigError { + None => confy::store(NAME, (*self).clone()).suggestion(Apologize::ConfigError { message: "The current config_file location does not seem to have write access. \ Use `export GOOSEBERRY_CONFIG=` to set a new location".into() })?, diff --git a/src/errors.rs b/src/errors.rs index 44430df..5e6852f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,4 @@ +use hypothesis::errors::HypothesisError; use thiserror::Error; /// "It claimed to have 15 functions, although it appeared that at least ten were apologizing for @@ -10,9 +11,9 @@ pub enum Apologize { /// Thrown when trying annotation ID doesn't match any recorded annotations #[error("Couldn't find an annotation with ID {id:?}")] AnnotationNotFound { id: String }, - /// Thrown when trying to access an unrecorded tag - #[error("Couldn't find group {id:?}. The Group ID can be found in the URL of the group: https://hypothes.is/groups//")] - GroupNotFound { id: String }, + /// Thrown when trying to access an unrecorded group + #[error("Couldn't access group {id:?}: {error:?}. The Group ID can be found in the URL of the group: https://hypothes.is/groups//")] + GroupNotFound { id: String, error: HypothesisError }, /// Thrown when explicit Y not received from user for destructive things #[error("I'm a coward. Doing nothing.")] DoingNothing, diff --git a/src/gooseberry/cli.rs b/src/gooseberry/cli.rs index 12b6dfc..f272769 100644 --- a/src/gooseberry/cli.rs +++ b/src/gooseberry/cli.rs @@ -155,6 +155,9 @@ pub struct Filters { /// Only annotations with ANY of these tags (use --and to match ALL) #[clap(long, value_delimiter = ',')] pub tags: Vec, + /// Only annotations from these groups + #[clap(long, value_delimiter = ',')] + pub groups: Vec, /// Only annotations without ANY of these tags #[clap(long, value_delimiter = ',')] pub exclude_tags: Vec, @@ -202,6 +205,7 @@ impl From for SearchQuery { }, quote: filters.quote, text: filters.text, + group: filters.groups, ..SearchQuery::default() } } @@ -232,8 +236,11 @@ pub enum ConfigCommand { Where, /// Change Hypothesis credentials Authorize, - /// Change the group used for Hypothesis annotations - Group { group_id: Option }, + /// Change the groups used for Hypothesis annotations + Group { + #[clap(value_delimiter = ',', required = false)] + group_ids: Vec, + }, /// Change options related to the knowledge base Kb { #[clap(subcommand)] @@ -285,9 +292,9 @@ impl ConfigCommand { let mut config = GooseberryConfig::load(config_file).await?; config.request_credentials().await?; } - Self::Group { group_id } => { + Self::Group { group_ids } => { let mut config = GooseberryConfig::load(config_file).await?; - config.set_group(group_id.clone()).await?; + config.set_groups(group_ids.clone()).await?; } Self::Kb { cmd } => { let mut config = GooseberryConfig::load(config_file).await?; diff --git a/src/gooseberry/knowledge_base.rs b/src/gooseberry/knowledge_base.rs index 613ace7..26d100b 100644 --- a/src/gooseberry/knowledge_base.rs +++ b/src/gooseberry/knowledge_base.rs @@ -35,6 +35,7 @@ pub struct AnnotationTemplate { pub incontext: String, pub highlight: Vec, pub display_name: Option, + pub group_name: String, } pub fn replace_spaces(astring: &str) -> String { @@ -42,7 +43,10 @@ pub fn replace_spaces(astring: &str) -> String { } impl AnnotationTemplate { - pub(crate) fn from_annotation(annotation: Annotation) -> Self { + pub(crate) fn from_annotation( + annotation: Annotation, + hypothesis_groups: &HashMap, + ) -> Self { let base_uri = if let Ok(uri) = Url::parse(&annotation.uri) { uri[..url::Position::BeforePath].to_string() } else { @@ -68,6 +72,10 @@ impl AnnotationTemplate { title = document.title[0].to_owned(); } } + let group_name = hypothesis_groups + .get(&annotation.group) + .unwrap_or(&annotation.group) + .to_owned(); AnnotationTemplate { annotation, base_uri, @@ -75,6 +83,7 @@ impl AnnotationTemplate { incontext, highlight, display_name, + group_name, } } } @@ -219,6 +228,22 @@ fn group_annotations_by_order( .push(annotation); } } + OrderBy::Group => { + for annotation in annotations { + order_to_annotations + .entry(annotation.annotation.group.to_string()) + .or_insert_with(Vec::new) + .push(annotation); + } + } + OrderBy::GroupName => { + for annotation in annotations { + order_to_annotations + .entry(annotation.group_name.to_string()) + .or_insert_with(Vec::new) + .push(annotation); + } + } OrderBy::Empty => panic!("Shouldn't happen"), _ => panic!("{} shouldn't occur in hierarchy", order), } @@ -242,6 +267,8 @@ fn sort_annotations(sort: &[OrderBy], annotations: &mut [AnnotationTemplate]) { .cmp(&format!("{}", b.annotation.created.format("%+"))), OrderBy::Updated => format!("{}", a.annotation.updated.format("%+")) .cmp(&format!("{}", b.annotation.updated.format("%+"))), + OrderBy::Group => a.annotation.group.cmp(&b.annotation.group), + OrderBy::GroupName => a.group_name.cmp(&b.group_name), OrderBy::Empty => panic!("Shouldn't happen"), }) }) @@ -309,7 +336,7 @@ impl Gooseberry { ) -> color_eyre::Result<()> { let mut annotations: Vec<_> = annotations .into_iter() - .map(AnnotationTemplate::from_annotation) + .map(|a| AnnotationTemplate::from_annotation(a, &self.config.hypothesis_groups)) .collect(); let extension = self .config diff --git a/src/gooseberry/mod.rs b/src/gooseberry/mod.rs index 687f48a..54ce068 100644 --- a/src/gooseberry/mod.rs +++ b/src/gooseberry/mod.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::fs; +use std::{fs, vec}; use color_eyre::Help; use dialoguer::Confirm; @@ -135,17 +135,23 @@ impl Gooseberry { let duration = core::time::Duration::from_millis(500); std::thread::sleep(duration); + let groups = self + .config + .hypothesis_groups + .keys() + .cloned() + .collect::>(); + + if groups.is_empty() { + spinner.finish_with_message("No groups to sync!"); + return Ok(()); + } let mut query = SearchQuery::builder() .limit(200) .order(Order::Asc) .search_after(self.get_sync_time()?) .user(&self.api.user.0) - .group(vec![self - .config - .hypothesis_group - .as_deref() - .ok_or_else(|| eyre!("No Hypothesis group"))? - .to_owned()]) + .group(groups) .build()?; let (added, updated) = self.sync_annotations(self.api.search_annotations_return_all(&mut query).await?)?; @@ -210,11 +216,11 @@ impl Gooseberry { pub async fn filter_annotations_api( &self, filters: Filters, - group: Vec, + groups: Vec, ) -> color_eyre::Result> { let mut query: SearchQuery = filters.clone().into(); query.user = self.api.user.0.to_owned(); - query.group = group.clone(); + query.group = groups.clone(); let mut annotations = if !filters.and && !filters.tags.is_empty() { let mut annotations = Vec::new(); for tag in &filters.tags { @@ -242,7 +248,7 @@ impl Gooseberry { if filters.not { let mut query: SearchQuery = Filters::default().into(); query.user = self.api.user.0.to_owned(); - query.group = group; + query.group = groups; let mut all_annotations: Vec<_> = self.api.search_annotations_return_all(&mut query).await?; let remove_ids = annotations.iter().map(|a| &a.id).collect::>(); @@ -254,6 +260,11 @@ impl Gooseberry { } pub fn filter_annotation(&self, annotation: &Annotation, filters: &Filters) -> bool { + // Check if in groups + if !filters.groups.is_empty() && !filters.groups.contains(&annotation.group) { + return false; + } + // Check if page note if filters.page && annotation.target.iter().any(|t| !t.selector.is_empty()) { return false; @@ -519,7 +530,7 @@ impl Gooseberry { .suggestion("Are you sure this is a valid and existing annotation ID?")?; let markdown = hbs.render( "annotation", - &AnnotationTemplate::from_annotation(annotation), + &AnnotationTemplate::from_annotation(annotation, &self.config.hypothesis_groups), )?; bat::PrettyPrinter::new() .language("markdown") @@ -534,7 +545,10 @@ impl Gooseberry { .map(|annotation| { hbs.render( "annotation", - &AnnotationTemplate::from_annotation(annotation), + &AnnotationTemplate::from_annotation( + annotation, + &self.config.hypothesis_groups, + ), ) }) .collect::>()?; diff --git a/src/gooseberry/search.rs b/src/gooseberry/search.rs index dad14f1..1252c36 100644 --- a/src/gooseberry/search.rs +++ b/src/gooseberry/search.rs @@ -139,7 +139,10 @@ impl Gooseberry { highlight, markdown: hbs.render( "annotation", - &AnnotationTemplate::from_annotation(annotation.clone()), + &AnnotationTemplate::from_annotation( + annotation.clone(), + &self.config.hypothesis_groups, + ), )?, id: annotation.id.to_owned(), })); @@ -320,7 +323,10 @@ impl Gooseberry { highlight, markdown: hbs.render( "annotation", - &AnnotationTemplate::from_annotation(annotation.clone()), + &AnnotationTemplate::from_annotation( + annotation.clone(), + &self.config.hypothesis_groups, + ), )?, id: annotation.id.to_owned(), })); diff --git a/tests/cli.rs b/tests/cli.rs index ef47fdb..3fa5ab3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -28,7 +28,7 @@ fn make_config_file( db_dir = '{}' hypothesis_username = '{}' hypothesis_key = '{}' -hypothesis_group = '{}' +hypothesis_groups = {{'{}' = "test_group"}} kb_dir = '{}' hierarchy = ['Tag'] sort = ['Created']