use std::path::Path;
use anyhow::Result;
use crate::cli::args::{Commands, LinksAction, PropertiesAction, TagsAction, TaskAction};
use crate::commands::{
ResolvedIndex, append as append_commands, backlinks as backlinks_commands,
create_index as create_index_commands, drop_index as drop_index_commands,
find as find_commands, links as links_commands, mv as mv_commands, properties,
read as read_commands, remove as remove_commands, resolve_index, set as set_commands,
summary as summary_commands, tags as tag_commands, tasks as task_commands,
};
use crate::output::{CommandOutcome, Format};
use hyalo_core::filter;
use hyalo_core::index::{ScanOptions, SnapshotIndex, VaultIndex as _};
pub(crate) struct CommandContext<'a> {
pub dir: &'a Path,
pub site_prefix: Option<&'a str>,
pub effective_format: Format,
pub user_format: Format,
pub snapshot_index: &'a mut Option<SnapshotIndex>,
pub index_path: Option<&'a Path>,
}
fn parse_where_filters(
where_properties: &[String],
where_tags: &[String],
) -> Result<Vec<filter::PropertyFilter>, String> {
let filters = where_properties
.iter()
.map(|s| filter::parse_property_filter(s))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
for tag in where_tags {
crate::commands::tags::validate_tag(tag)?;
}
Ok(filters)
}
pub(crate) fn dispatch(command: Commands, ctx: &mut CommandContext<'_>) -> Result<CommandOutcome> {
let dir = ctx.dir;
let site_prefix = ctx.site_prefix;
let effective_format = ctx.effective_format;
let snapshot_index = &mut *ctx.snapshot_index;
let index_path = ctx.index_path;
match command {
Commands::Find {
pattern,
regexp,
properties,
tag,
task,
sections,
file,
glob,
fields,
sort,
reverse,
limit,
broken_links,
title,
} => {
let prop_filters: Vec<filter::PropertyFilter> = match properties
.iter()
.map(|s| filter::parse_property_filter(s))
.collect::<Result<Vec<_>, _>>()
{
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
let task_filter = match task.as_deref().map(filter::parse_task_filter) {
Some(Ok(f)) => Some(f),
Some(Err(e)) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
None => None,
};
let parsed_fields = match filter::Fields::parse(&fields) {
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
let sort_field = match sort.as_deref().map(filter::parse_sort) {
Some(Ok(f)) => Some(f),
Some(Err(e)) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
None => None,
};
let section_filters: Vec<hyalo_core::heading::SectionFilter> = match sections
.iter()
.map(|s| hyalo_core::heading::SectionFilter::parse(s))
.collect::<Result<Vec<_>, _>>()
{
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
for t in &tag {
if let Err(msg) = crate::commands::tags::validate_tag(t) {
return Ok(CommandOutcome::UserError(format!("Error: {msg}")));
}
}
let sort_needs_backlinks =
matches!(sort_field.as_ref(), Some(filter::SortField::BacklinksCount));
let sort_needs_links =
matches!(sort_field.as_ref(), Some(filter::SortField::LinksCount));
let sort_needs_title = matches!(sort_field.as_ref(), Some(filter::SortField::Title));
let has_task_filter = task_filter.is_some();
let has_section_filter = !section_filters.is_empty();
let has_title_filter = title.is_some();
let needs_body =
find_commands::needs_body(&parsed_fields, has_task_filter, has_section_filter)
|| sort_needs_links
|| sort_needs_title
|| broken_links
|| has_title_filter;
let needs_full_vault = parsed_fields.backlinks || sort_needs_backlinks;
let scan_body = needs_body || needs_full_vault;
match resolve_index(
snapshot_index.as_ref(),
dir,
&file,
&glob,
effective_format,
site_prefix,
needs_full_vault,
ScanOptions { scan_body },
) {
Ok(Ok(resolved)) => find_commands::find(
resolved.as_index(),
dir,
site_prefix,
pattern.as_deref(),
regexp.as_deref(),
&prop_filters,
&tag,
task_filter.as_ref(),
§ion_filters,
&file,
&glob,
&parsed_fields,
sort_field.as_ref(),
reverse,
limit,
broken_links,
title.as_deref(),
effective_format,
),
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
}
}
Commands::Read {
file,
section,
lines,
frontmatter,
} => read_commands::run(
dir,
&file,
section.as_deref(),
lines.as_deref(),
frontmatter,
effective_format,
ctx.user_format,
),
Commands::Properties { action } => {
let action = action.unwrap_or(PropertiesAction::Summary { glob: vec![] });
match action {
PropertiesAction::Summary { ref glob } => match resolve_index(
snapshot_index.as_ref(),
dir,
&[],
glob,
effective_format,
site_prefix,
false,
ScanOptions { scan_body: false },
) {
Ok(Ok(ResolvedIndex::Snapshot(idx))) => {
let filtered =
find_commands::filter_index_entries(idx.entries(), &[], glob);
match filtered {
Err(e) => Err(e),
Ok(filtered) => {
let paths: Vec<String> =
filtered.iter().map(|e| e.rel_path.clone()).collect();
let file_filter = if glob.is_empty() {
None
} else {
Some(paths.as_slice())
};
properties::properties_summary(idx, file_filter, effective_format)
}
}
}
Ok(Ok(ResolvedIndex::Scanned(build))) => {
properties::properties_summary(&build.index, None, effective_format)
}
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
},
PropertiesAction::Rename { from, to, glob } => properties::properties_rename(
dir,
&from,
&to,
&glob,
effective_format,
snapshot_index,
index_path,
),
}
}
Commands::Tags { action } => {
let action = action.unwrap_or(TagsAction::Summary { glob: vec![] });
match action {
TagsAction::Summary { ref glob } => match resolve_index(
snapshot_index.as_ref(),
dir,
&[],
glob,
effective_format,
site_prefix,
false,
ScanOptions { scan_body: false },
) {
Ok(Ok(ResolvedIndex::Snapshot(idx))) => {
let filtered =
find_commands::filter_index_entries(idx.entries(), &[], glob);
match filtered {
Err(e) => Err(e),
Ok(filtered) => {
let paths: Vec<String> =
filtered.iter().map(|e| e.rel_path.clone()).collect();
let file_filter = if glob.is_empty() {
None
} else {
Some(paths.as_slice())
};
tag_commands::tags_summary(idx, file_filter, effective_format)
}
}
}
Ok(Ok(ResolvedIndex::Scanned(build))) => {
tag_commands::tags_summary(&build.index, None, effective_format)
}
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
},
TagsAction::Rename { from, to, glob } => tag_commands::tags_rename(
dir,
&from,
&to,
&glob,
effective_format,
snapshot_index,
index_path,
),
}
}
Commands::Task { action } => match action {
TaskAction::Read { file, line } => {
task_commands::task_read(dir, &file, line, effective_format)
}
TaskAction::Toggle { file, line } => task_commands::task_toggle(
dir,
&file,
line,
effective_format,
snapshot_index,
index_path,
),
TaskAction::SetStatus { file, line, status } => {
if status.chars().count() != 1 {
let out = crate::output::format_error(
effective_format,
"--status must be a single character",
None,
Some("example: --status '?' or --status '-'"),
None,
);
return Ok(CommandOutcome::UserError(out));
}
let ch = status
.chars()
.next()
.expect("count==1 implies at least one char");
task_commands::task_set_status(
dir,
&file,
line,
ch,
effective_format,
snapshot_index,
index_path,
)
}
},
Commands::Summary {
glob,
recent,
depth,
} => match resolve_index(
snapshot_index.as_ref(),
dir,
&[],
&glob,
effective_format,
site_prefix,
true,
ScanOptions { scan_body: true },
) {
Ok(Ok(resolved)) => summary_commands::summary(
dir,
resolved.as_index(),
&glob,
recent,
depth,
site_prefix,
effective_format,
),
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
},
Commands::Set {
properties,
tag,
file,
glob,
where_properties,
where_tags,
dry_run,
} => {
let where_prop_filters = match parse_where_filters(&where_properties, &where_tags) {
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
set_commands::set(
dir,
&properties,
&tag,
&file,
&glob,
&where_prop_filters,
&where_tags,
effective_format,
snapshot_index,
index_path,
dry_run,
)
}
Commands::Remove {
properties,
tag,
file,
glob,
where_properties,
where_tags,
dry_run,
} => {
let where_prop_filters = match parse_where_filters(&where_properties, &where_tags) {
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
remove_commands::remove(
dir,
&properties,
&tag,
&file,
&glob,
&where_prop_filters,
&where_tags,
effective_format,
snapshot_index,
index_path,
dry_run,
)
}
Commands::Append {
properties,
file,
glob,
where_properties,
where_tags,
dry_run,
} => {
let where_prop_filters = match parse_where_filters(&where_properties, &where_tags) {
Ok(f) => f,
Err(e) => {
return Ok(CommandOutcome::UserError(format!("Error: {e}")));
}
};
append_commands::append(
dir,
&properties,
&file,
&glob,
&where_prop_filters,
&where_tags,
effective_format,
snapshot_index,
index_path,
dry_run,
)
}
Commands::Backlinks { file } => match resolve_index(
snapshot_index.as_ref(),
dir,
&[],
&[],
effective_format,
site_prefix,
true,
ScanOptions { scan_body: true },
) {
Ok(Ok(resolved)) => {
backlinks_commands::backlinks(resolved.as_index(), &file, dir, effective_format)
}
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
},
Commands::Mv { file, to, dry_run } => mv_commands::mv(
dir,
&file,
&to,
dry_run,
effective_format,
site_prefix,
snapshot_index,
index_path,
),
Commands::CreateIndex {
output,
allow_outside_vault,
} => create_index_commands::create_index(
dir,
site_prefix,
output.as_deref(),
effective_format,
allow_outside_vault,
),
Commands::DropIndex {
path,
allow_outside_vault,
} => drop_index_commands::drop_index(
dir,
path.as_deref(),
effective_format,
allow_outside_vault,
),
Commands::Links { action } => match action {
LinksAction::Fix {
dry_run: _,
apply,
threshold,
glob,
ignore_target,
} => match resolve_index(
snapshot_index.as_ref(),
dir,
&[],
&[],
effective_format,
site_prefix,
true,
ScanOptions { scan_body: true },
) {
Ok(Ok(resolved)) => links_commands::links_fix(
resolved.as_index(),
dir,
site_prefix,
&glob,
!apply,
threshold,
&ignore_target,
effective_format,
),
Ok(Err(outcome)) => Ok(outcome),
Err(e) => Err(e),
},
},
Commands::Init { .. } => unreachable!("Init is dispatched before this match reached"),
}
}