use clap::{Subcommand, ValueEnum};
use color_eyre::eyre::Result;
use kimun_core::NoteVault;
use kimun_core::note::{
FrontmatterFormat, PropertyEntry, PropertyInput, PropertyKind, PropertyValue,
};
use crate::cli::UserError;
use crate::cli::helpers::resolve_note_path;
use crate::cli::json_output::JsonProperties;
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PropFormat {
#[default]
Text,
Json,
}
#[derive(Subcommand, Debug)]
pub enum PropSubcommand {
List {
path: String,
#[arg(long, value_enum, default_value = "text")]
format: PropFormat,
},
Get {
path: String,
key: String,
#[arg(long, value_enum, default_value = "text")]
format: PropFormat,
},
Set {
path: String,
key: String,
#[arg(num_args = 0.., allow_negative_numbers = true)]
values: Vec<String>,
#[arg(long = "type", value_parser = parse_kind)]
kind: Option<PropertyKind>,
#[arg(long)]
yaml: bool,
},
Remove {
path: String,
key: String,
},
}
fn parse_kind(s: &str) -> Result<PropertyKind, String> {
s.parse()
}
pub async fn run(
subcommand: PropSubcommand,
vault: &NoteVault,
quick_note_path: &str,
) -> Result<()> {
match subcommand {
PropSubcommand::List { path, format } => {
let path = resolve_note_path(&path, quick_note_path)?;
let properties = vault.get_properties(&path).await?;
print!("{}", format_properties(&properties, format)?);
}
PropSubcommand::Get { path, key, format } => {
let path = resolve_note_path(&path, quick_note_path)?;
let value = vault
.get_property(&path, &key)
.await?
.ok_or_else(|| UserError(format!("No property '{key}' in {path}")))?;
println!("{}", format_value(value.as_ref(), format)?);
}
PropSubcommand::Set {
path,
key,
values,
kind,
yaml,
} => {
let path = resolve_note_path(&path, quick_note_path)?;
let new_block_format = if yaml {
FrontmatterFormat::Yaml
} else {
FrontmatterFormat::Toml
};
let input = PropertyInput::new(values).forced(kind);
let value = vault
.set_property_from_input(&path, &key, &input, new_block_format)
.await?;
println!("Set {key} = {value} ({}) in {path}", value.kind());
}
PropSubcommand::Remove { path, key } => {
let path = resolve_note_path(&path, quick_note_path)?;
if vault.remove_property(&path, &key).await? {
println!("Removed {key} from {path}");
} else {
println!("No property '{key}' in {path}");
}
}
}
Ok(())
}
pub fn format_properties(properties: &[PropertyEntry], format: PropFormat) -> Result<String> {
Ok(match format {
PropFormat::Text => properties
.iter()
.map(|(key, value)| match value {
Some(value) => format!("{key}: {value}\n"),
None => format!("{key}:\n"),
})
.collect(),
PropFormat::Json => {
format!(
"{}\n",
serde_json::to_string(&JsonProperties(properties.to_vec()))?
)
}
})
}
pub fn format_value(value: Option<&PropertyValue>, format: PropFormat) -> Result<String> {
Ok(match format {
PropFormat::Text => value.map(ToString::to_string).unwrap_or_default(),
PropFormat::Json => serde_json::to_string(&value)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Vec<PropertyEntry> {
vec![
("status".into(), Some(PropertyValue::Text("done".into()))),
("priority".into(), Some(PropertyValue::Number(2.0))),
("due".into(), None),
(
"tags".into(),
Some(PropertyValue::List(vec!["a".into(), "b c".into()])),
),
]
}
#[test]
fn lists_as_lines_or_ordered_json() {
assert_eq!(
format_properties(&sample(), PropFormat::Text).unwrap(),
"status: done\npriority: 2\ndue:\ntags: a, b c\n"
);
assert_eq!(
format_properties(&sample(), PropFormat::Json).unwrap(),
"{\"status\":\"done\",\"priority\":2,\"due\":null,\"tags\":[\"a\",\"b c\"]}\n",
"keys keep the note's order; a key with no value is null"
);
assert_eq!(format_properties(&[], PropFormat::Json).unwrap(), "{}\n");
}
#[derive(clap::Parser)]
struct Cli {
#[command(subcommand)]
prop: PropSubcommand,
}
fn parse_set(args: &[&str]) -> (Vec<String>, Option<PropertyKind>) {
use clap::Parser;
let argv = ["kimun", "set", "n", "k"].iter().chain(args);
match Cli::try_parse_from(argv).unwrap().prop {
PropSubcommand::Set { values, kind, .. } => (values, kind),
other => panic!("parsed as {other:?}"),
}
}
#[test]
fn set_flags_after_values_are_flags_and_negatives_are_values() {
assert_eq!(
parse_set(&["high", "--type", "text"]),
(vec!["high".to_string()], Some(PropertyKind::Text))
);
assert_eq!(parse_set(&["-5"]), (vec!["-5".to_string()], None));
assert_eq!(
parse_set(&["a", "b", "--type", "list"]),
(
vec!["a".to_string(), "b".to_string()],
Some(PropertyKind::List)
)
);
assert_eq!(
parse_set(&["--type", "list"]),
(vec![], Some(PropertyKind::List))
);
}
#[test]
fn formats_one_value() {
let tags = PropertyValue::List(vec!["a".into()]);
assert_eq!(format_value(Some(&tags), PropFormat::Text).unwrap(), "a");
assert_eq!(
format_value(Some(&tags), PropFormat::Json).unwrap(),
"[\"a\"]"
);
assert_eq!(format_value(None, PropFormat::Text).unwrap(), "");
assert_eq!(format_value(None, PropFormat::Json).unwrap(), "null");
}
}