use crate::entities::message_image::ImageInfo;
use crate::shared::i18n::Locale;
#[derive(Debug, Clone, PartialEq)]
pub enum ImageCommand {
Attach { path: String },
Remove { target: String },
List,
Paste,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ImageProgress {
Attached { info: ImageInfo, staged: usize },
Removed {
name: String,
source: Option<String>,
},
Listed { items: Vec<ImageInfo> },
VisionUnknown,
Failed(String),
}
pub fn parse(input: &str, loc: &Locale) -> Option<Result<ImageCommand, String>> {
let mut tokens = input.split_whitespace();
let first = tokens.next()?;
if !first.eq_ignore_ascii_case("/image") {
return None;
}
let Some(sub) = tokens.next() else {
return Some(Err(loc.tf(
"ui.image.err.missing_subcommand",
&[("usage", loc.t("ui.image.usage"))],
)));
};
let rest: Vec<&str> = tokens.collect();
if sub.eq_ignore_ascii_case("attach") {
match argument(&rest) {
Some(path) => Some(Ok(ImageCommand::Attach { path })),
None => Some(Err(loc.tf(
"ui.image.err.missing_path",
&[("usage", loc.t("ui.image.usage"))],
))),
}
} else if sub.eq_ignore_ascii_case("remove") {
match argument(&rest) {
Some(target) => Some(Ok(ImageCommand::Remove { target })),
None => Some(Err(loc.tf(
"ui.image.err.missing_target",
&[("usage", loc.t("ui.image.usage"))],
))),
}
} else if sub.eq_ignore_ascii_case("list") {
Some(Ok(ImageCommand::List))
} else if sub.eq_ignore_ascii_case("paste") {
Some(Ok(ImageCommand::Paste))
} else {
Some(Err(loc.tf(
"ui.image.err.unknown_subcommand",
&[("sub", sub), ("usage", loc.t("ui.image.usage"))],
)))
}
}
fn argument(tokens: &[&str]) -> Option<String> {
let joined = tokens.join(" ");
let arg = joined.trim().trim_matches(|c| c == '"' || c == '\'').trim();
(!arg.is_empty()).then(|| arg.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn en() -> &'static Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::En)
}
#[test]
fn non_image_input_is_none() {
assert!(parse("hello", en()).is_none());
assert!(parse("", en()).is_none());
assert!(parse("/file attach a.txt", en()).is_none());
assert!(parse("/images list", en()).is_none());
}
#[test]
fn parses_attach_with_path() {
assert_eq!(
parse("/image attach D:\\pics\\a.png", en()),
Some(Ok(ImageCommand::Attach {
path: "D:\\pics\\a.png".into()
}))
);
}
#[test]
fn path_with_spaces_and_quotes() {
assert_eq!(
parse("/image attach \"C:\\my pics\\a b.png\"", en()),
Some(Ok(ImageCommand::Attach {
path: "C:\\my pics\\a b.png".into()
}))
);
assert_eq!(
parse("/image attach C:\\my pics\\a b.png", en()),
Some(Ok(ImageCommand::Attach {
path: "C:\\my pics\\a b.png".into()
}))
);
}
#[test]
fn command_is_case_insensitive() {
assert_eq!(parse("/IMAGE LIST", en()), Some(Ok(ImageCommand::List)));
assert_eq!(
parse("/Image Remove #1", en()),
Some(Ok(ImageCommand::Remove {
target: "#1".into()
}))
);
}
#[test]
fn parses_paste_and_takes_no_argument() {
assert_eq!(parse("/image paste", en()), Some(Ok(ImageCommand::Paste)));
assert_eq!(parse("/IMAGE PASTE", en()), Some(Ok(ImageCommand::Paste)));
assert_eq!(
parse("/image paste please", en()),
Some(Ok(ImageCommand::Paste))
);
}
#[test]
fn the_usage_line_names_every_subcommand() {
for &lang in crate::shared::i18n::Lang::ALL {
let usage = crate::shared::i18n::locale(lang).t("ui.image.usage");
for sub in ["attach", "remove", "list", "paste"] {
assert!(usage.contains(sub), "{lang:?} usage omits {sub}: {usage}");
}
}
}
#[test]
fn delete_is_not_a_command() {
let Some(Err(msg)) = parse("/image delete a.png", en()) else {
panic!("expected a syntax error");
};
assert!(
msg.contains("delete"),
"the message should name what was typed: {msg}"
);
}
#[test]
fn errors_on_missing_parts() {
assert!(matches!(parse("/image", en()), Some(Err(_))));
assert!(matches!(parse("/image attach", en()), Some(Err(_))));
assert!(matches!(parse("/image remove", en()), Some(Err(_))));
assert!(matches!(parse("/image attach \"\"", en()), Some(Err(_))));
}
#[test]
fn errors_are_localized_for_all_langs() {
for &lang in crate::shared::i18n::Lang::ALL {
let loc = crate::shared::i18n::locale(lang);
for input in ["/image", "/image attach", "/image remove", "/image purge x"] {
let Some(Err(msg)) = parse(input, loc) else {
panic!("expected a syntax error for {input:?} in {lang:?}");
};
assert!(
!msg.contains('{') && !msg.contains('}'),
"unsubstituted placeholder in {lang:?}: {msg}"
);
if lang == crate::shared::i18n::Lang::En {
assert!(
!msg.chars().any(|c| ('\u{0400}'..='\u{04FF}').contains(&c)),
"Cyrillic leaked into the en message: {msg}"
);
}
}
}
}
}