aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-01-20 22:12:41 +0100
committerMiquel Sabaté Solà <mssola@mssola.com>2026-01-20 22:12:41 +0100
commitda7ecd3660ebb6cbf46749c7586a45cde19d9011 (patch)
treef33a5e6cb410bf13f8ac02ea8df4b9ed7843d1f2 /crates
parent22f19cf9f50ae4ce96b175366602b9a2816332a6 (diff)
downloadmihi-da7ecd3660ebb6cbf46749c7586a45cde19d9011.tar.gz
mihi-da7ecd3660ebb6cbf46749c7586a45cde19d9011.zip
Add the 'tags' command
This command includes the create, ls, and rm subcommands, which allow for easy management of word tags. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/cli/src/main.rs5
-rw-r--r--crates/cli/src/tags.rs178
2 files changed, 183 insertions, 0 deletions
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index 871490a..9753cf9 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -4,6 +4,7 @@ mod init;
mod locale;
mod nuke;
mod run;
+mod tags;
mod words;
/// Version for this program.
@@ -60,6 +61,10 @@ fn main() {
let rest: Vec<String> = args.collect();
nuke::run(rest);
}
+ "tags" => {
+ let rest: Vec<String> = args.collect();
+ tags::run(rest);
+ }
"words" => {
let rest: Vec<String> = args.collect();
words::run(rest);
diff --git a/crates/cli/src/tags.rs b/crates/cli/src/tags.rs
new file mode 100644
index 0000000..82a14ce
--- /dev/null
+++ b/crates/cli/src/tags.rs
@@ -0,0 +1,178 @@
+use inquire::{Confirm, Select};
+use mihi::{create_tag, delete_tag, select_tag_names};
+use std::vec::IntoIter;
+
+// Show the help message.
+fn help(msg: Option<&str>) {
+ if let Some(msg) = msg {
+ println!("{}.\n", msg);
+ }
+
+ println!("mihi tags: Manage tags.\n");
+ println!("usage: mihi tags [OPTIONS] <subcommand>\n");
+
+ println!("Options:");
+ println!(" -h, --help\t\tPrint this message.");
+
+ println!("\nSubcommands:");
+ println!(" create\t\tCreate a new tag.");
+ println!(" ls\t\t\tList tags from the database.");
+ println!(" rm\t\t\tRemove a tag from the database.");
+}
+
+fn create(mut args: IntoIter<String>) -> i32 {
+ // We expect exactly one argument, which is the name of the tag. Note that
+ // this is wholly different to what's in for words/exercises, as the
+ // expected workflow on those is different as well.
+ if args.len() != 1 {
+ let mut msg = "error: tags: you have to pass exactly one argument, which is the name of the tag to be created".to_string();
+ if args.len() > 1 {
+ msg.push_str(". You might want to wrap the given arguments in quotes");
+ }
+
+ help(Some(msg.as_str()));
+ return 1;
+ }
+
+ // Fetch the name and guarantee it's unique.
+ let name = args.next().unwrap_or("".to_string());
+ if let Ok(tags) = select_tag_names(&Some(name.clone())) {
+ for tag in tags {
+ if tag == name {
+ println!("errors: tags: '{}' already exists", name);
+ return 1;
+ }
+ }
+ }
+
+ if create_tag(&name).is_ok() {
+ 0
+ } else {
+ 1
+ }
+}
+
+fn ls(mut args: IntoIter<String>) -> i32 {
+ if args.len() > 1 {
+ help(Some("error: tags: too many filters"));
+ return 1;
+ }
+
+ let tags = match select_tag_names(&args.next()) {
+ Ok(tags) => tags,
+ Err(e) => {
+ println!("error: tags: {e}.");
+ return 1;
+ }
+ };
+
+ for tag in tags {
+ println!("{tag}");
+ }
+
+ 0
+}
+
+fn select_single_tag(search: Option<String>) -> Result<String, String> {
+ let tags = select_tag_names(&search)?;
+
+ match tags.len() {
+ 0 => Err("not found".to_string()),
+ 1 => Ok(tags.first().unwrap().to_owned()),
+ _ => match Select::new("Which tag?", tags).with_page_size(20).prompt() {
+ Ok(choice) => Ok(choice),
+ Err(_) => Err("abort!".to_string()),
+ },
+ }
+}
+
+fn rm(mut args: IntoIter<String>) -> i32 {
+ // We expect exactly one argument, which is the name of the tag. Note that
+ // this is wholly different to what's in for words/exercises, as the
+ // expected workflow on those is different as well.
+ if args.len() != 1 {
+ let mut msg = "error: tags: you have to pass exactly one argument, which is the name of the tag to be created".to_string();
+ if args.len() > 1 {
+ msg.push_str(". You might want to wrap the given arguments in quotes");
+ }
+
+ help(Some(msg.as_str()));
+ return 1;
+ }
+
+ // Select exactly one tag, with or without user feedback. If that's not
+ // possible, bail out. If one is selected, allow the user to think it
+ // through.
+ let selection = match select_single_tag(args.next()) {
+ Ok(tag) => tag,
+ Err(e) => {
+ println!("error: tags: {e}.");
+ return 1;
+ }
+ };
+ let ans = Confirm::new(
+ format!("Do you really want to remove '{selection}' from the database?").as_str(),
+ )
+ .with_default(false)
+ .prompt();
+
+ // We have a selected tag and the user confirmed its selection, go for it!
+ match ans {
+ Ok(true) => match delete_tag(&selection) {
+ Ok(_) => println!("Removed '{selection}' from the database!"),
+ Err(e) => {
+ println!("error: tags: {e}.");
+ return 1;
+ }
+ },
+ Ok(false) => {
+ println!("Doing nothing...");
+ }
+ Err(_) => return 1,
+ }
+
+ 0
+}
+
+pub fn run(args: Vec<String>) {
+ if args.is_empty() {
+ help(Some(
+ "error: tags: you have to provide at least a subcommand",
+ ));
+ std::process::exit(1);
+ }
+
+ let mut it = args.into_iter();
+
+ match it.next() {
+ Some(first) => match first.as_str() {
+ "-h" | "--help" => {
+ help(None);
+ std::process::exit(0);
+ }
+ "create" => {
+ std::process::exit(create(it));
+ }
+ "ls" => {
+ std::process::exit(ls(it));
+ }
+ "rm" => {
+ std::process::exit(rm(it));
+ }
+ _ => {
+ help(Some(
+ format!("error: tags: unknown flag or command '{first}'").as_str(),
+ ));
+ std::process::exit(1);
+ }
+ },
+ None => {
+ help(Some(
+ "error: tags: you need to provide a command"
+ .to_string()
+ .as_str(),
+ ));
+ std::process::exit(1);
+ }
+ }
+}