use std::fs;
use owo_colors::OwoColorize;
use crate::{
catalog::Catalog,
commands::{ColorChoice, init},
config::Config,
diagnostics::Diagnostics,
diff::{resolve_pager, write_output},
error::{Error, Result},
skill::SKILL_FILE_NAME,
};
pub async fn run(
color: ColorChoice,
verbose: bool,
skill: String,
pager: Option<String>,
) -> Result<()> {
init::ensure().await?;
let mut diagnostics = Diagnostics::new(verbose);
let config = Config::load()?;
let catalog = Catalog::load(&config, &mut diagnostics);
let use_color = color.enabled();
let contents = find_skill_contents(&catalog, &skill)?;
let output = if use_color {
highlight_markdown(&contents)
} else {
contents
};
let pager = resolve_pager(pager.as_deref());
write_output(&output, pager.as_deref())?;
Ok(())
}
fn find_skill_contents(catalog: &Catalog, name: &str) -> Result<String> {
if let Some(skill) = catalog.sources.get(name) {
return Ok(skill.contents.clone());
}
for tool_skills in catalog.tools.values() {
if let Some(skill) = tool_skills.get(name) {
return Ok(skill.contents.clone());
}
}
for local_skills in catalog.local.values() {
if let Some(skill) = local_skills.get(name) {
let skill_path = skill.skill_dir.join(SKILL_FILE_NAME);
return fs::read_to_string(&skill_path).map_err(|e| Error::SkillRead {
path: skill_path,
source: e,
});
}
}
Err(Error::SkillNotFound {
name: name.to_string(),
})
}
fn highlight_markdown(contents: &str) -> String {
let mut output = String::new();
let mut in_code_block = false;
let mut in_frontmatter = false;
let mut frontmatter_count = 0;
for line in contents.lines() {
if line == "---" {
frontmatter_count += 1;
if frontmatter_count == 1 {
in_frontmatter = true;
output.push_str(&line.dimmed().to_string());
output.push('\n');
continue;
} else if frontmatter_count == 2 {
in_frontmatter = false;
output.push_str(&line.dimmed().to_string());
output.push('\n');
continue;
}
}
if in_frontmatter {
output.push_str(&line.dimmed().to_string());
output.push('\n');
continue;
}
if line.starts_with("```") {
in_code_block = !in_code_block;
output.push_str(&line.cyan().to_string());
output.push('\n');
continue;
}
if in_code_block {
output.push_str(&line.cyan().to_string());
output.push('\n');
continue;
}
if line.starts_with('#') {
output.push_str(&line.bold().white().to_string());
output.push('\n');
continue;
}
if line.starts_with("- ") || line.starts_with("* ") {
let (bullet, rest) = line.split_at(2);
output.push_str(&bullet.blue().to_string());
output.push_str(&format_inline(rest));
output.push('\n');
continue;
}
if let Some(pos) = line.find(". ") {
let prefix = &line[..pos];
if prefix.chars().all(|c| c.is_ascii_digit()) {
let number_part: String = line[..=pos].to_string();
output.push_str(&number_part.blue().to_string());
output.push_str(&format_inline(&line[pos + 2..]));
output.push('\n');
continue;
}
}
if line.starts_with('>') {
output.push_str(&line.yellow().to_string());
output.push('\n');
continue;
}
output.push_str(&format_inline(line));
output.push('\n');
}
if !contents.ends_with('\n') && output.ends_with('\n') {
output.pop();
}
output
}
fn format_inline(text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let mut output = String::new();
let mut i = 0;
while i < chars.len() {
if chars[i] == '`'
&& let Some(end) = find_closing(&chars, i + 1, '`')
{
let code: String = chars[i + 1..end].iter().collect();
output.push_str(&format!("`{code}`").cyan().to_string());
i = end + 1;
continue;
}
if i + 1 < chars.len()
&& chars[i] == '*'
&& chars[i + 1] == '*'
&& let Some(end) = find_double_closing(&chars, i + 2, '*')
{
let bold: String = chars[i + 2..end].iter().collect();
output.push_str(&bold.bold().to_string());
i = end + 2;
continue;
}
if chars[i] == '*'
&& let Some(end) = find_closing(&chars, i + 1, '*')
{
let italic: String = chars[i + 1..end].iter().collect();
output.push_str(&italic.italic().to_string());
i = end + 1;
continue;
}
if chars[i] == '['
&& let Some(bracket_end) = find_closing(&chars, i + 1, ']')
&& bracket_end + 1 < chars.len()
&& chars[bracket_end + 1] == '('
&& let Some(paren_end) = find_closing(&chars, bracket_end + 2, ')')
{
let link_text: String = chars[i + 1..bracket_end].iter().collect();
let url: String = chars[bracket_end + 2..paren_end].iter().collect();
output.push_str(&link_text.blue().underline().to_string());
output.push_str(&format!(" ({url})").dimmed().to_string());
i = paren_end + 1;
continue;
}
output.push(chars[i]);
i += 1;
}
output
}
fn find_closing(chars: &[char], start: usize, delim: char) -> Option<usize> {
for (i, &c) in chars.iter().enumerate().skip(start) {
if c == delim {
return Some(i);
}
}
None
}
fn find_double_closing(chars: &[char], start: usize, delim: char) -> Option<usize> {
let mut i = start;
while i + 1 < chars.len() {
if chars[i] == delim && chars[i + 1] == delim {
return Some(i);
}
i += 1;
}
None
}