use std::{
collections::HashSet,
fmt::{self, Display},
};
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use enum_cycling::EnumCycle;
use regex::Regex;
use serde::Deserialize;
use uuid::Uuid;
use crate::utils::{extract_tags_from_description, flatten_str, remove_newlines};
pub const CATEGORY_USER: &str = "user";
pub const CATEGORY_WORKSPACE: &str = "workspace";
pub const SOURCE_USER: &str = "user";
pub const SOURCE_AI: &str = "ai";
pub const SOURCE_TLDR: &str = "tldr";
pub const SOURCE_IMPORT: &str = "import";
pub const SOURCE_WORKSPACE: &str = "workspace";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, ValueEnum, EnumCycle, strum::Display)]
#[cfg_attr(test, derive(strum::EnumIter))]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SearchMode {
#[default]
Auto,
Fuzzy,
Regex,
Exact,
Relaxed,
}
#[derive(Default, Clone)]
#[cfg_attr(test, derive(Debug))]
pub struct SearchCommandsFilter {
pub category: Option<Vec<String>>,
pub source: Option<String>,
pub tags: Option<Vec<String>>,
pub search_mode: SearchMode,
pub search_term: Option<String>,
}
impl SearchCommandsFilter {
pub fn cleaned(self) -> Self {
let SearchCommandsFilter {
category,
source,
tags,
search_mode,
search_term,
} = self;
Self {
category: category
.map(|v| {
let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
for t in &v {
let t = t.trim();
if !t.is_empty() && seen.insert(t) {
final_vec.push(t.to_string());
}
}
final_vec
})
.filter(|t| !t.is_empty()),
source: source.map(|t| t.trim().to_string()).filter(|s| !s.is_empty()),
tags: tags
.map(|v| {
let mut final_vec: Vec<String> = Vec::with_capacity(v.len());
let mut seen: HashSet<&str> = HashSet::with_capacity(v.len());
for t in &v {
let t = t.trim();
if !t.is_empty() && seen.insert(t) {
final_vec.push(t.to_string());
}
}
final_vec
})
.filter(|t| !t.is_empty()),
search_mode,
search_term: search_term.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
}
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(Default, Debug))]
pub struct Command {
pub id: Uuid,
pub category: String,
pub source: String,
pub alias: Option<String>,
pub cmd: String,
pub flat_cmd: String,
pub description: Option<String>,
pub flat_description: Option<String>,
pub tags: Option<Vec<String>>,
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
}
impl Command {
pub fn new(category: impl Into<String>, source: impl Into<String>, cmd: impl Into<String>) -> Self {
let cmd = remove_newlines(cmd.into());
Self {
id: Uuid::now_v7(),
category: category.into(),
source: source.into(),
alias: None,
flat_cmd: flatten_str(&cmd),
cmd,
description: None,
flat_description: None,
tags: None,
created_at: Utc::now(),
updated_at: None,
}
}
pub fn with_alias(mut self, alias: Option<String>) -> Self {
self.alias = alias.filter(|a| !a.trim().is_empty());
self
}
pub fn with_cmd(mut self, cmd: String) -> Self {
self.flat_cmd = flatten_str(&cmd);
self.cmd = cmd;
self
}
pub fn with_description(mut self, description: Option<String>) -> Self {
let description = description.filter(|d| !d.trim().is_empty());
self.tags = extract_tags_from_description(description.as_deref());
self.flat_description = description.as_ref().map(flatten_str);
self.description = description;
self
}
#[cfg(test)]
pub fn with_tags(mut self, tags: Option<Vec<String>>) -> Self {
self.tags = tags.filter(|t| !t.is_empty());
self
}
pub fn matches(&self, regex: &Regex) -> bool {
regex.is_match(&self.cmd) || self.description.as_ref().is_some_and(|d| regex.is_match(d))
}
}
impl Display for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let cmd = &self.cmd;
let desc = self.description.as_deref().filter(|s| !s.is_empty());
let alias = self.alias.as_deref();
match (desc, alias) {
(None, None) => return writeln!(f, "#\n{cmd}"),
(Some(d), Some(a)) => {
if d.contains('\n') {
writeln!(f, "# [alias:{a}]")?;
for line in d.lines() {
writeln!(f, "# {line}")?;
}
} else {
writeln!(f, "# [alias:{a}] {d}")?;
}
}
(Some(d), None) => {
for line in d.lines() {
writeln!(f, "# {line}")?;
}
}
(None, Some(a)) => {
writeln!(f, "# [alias:{a}]")?;
}
};
writeln!(f, "{cmd}")
}
}