use crate::cli::{DogsArgs, Format};
use crate::commands::rpc::request_and_render;
use crate::dog_index::{self, AvailableDog, DogSourceKind};
use crate::exit::ExitCode;
use crate::fetch;
use crate::output::{AvailableDogRows, DogRows, Streams, emit, write_outcome};
use shep_client::Client;
use shep_core::protocol::{Request, Response};
pub async fn dogs(client: &Client, streams: &mut Streams<'_>, args: &DogsArgs) -> ExitCode {
let filter = args.filter.as_deref();
request_and_render(
client,
streams,
"dogs",
Request::ListFlock,
None,
|response| match response {
Response::Flock(procs) => Some(DogRows(
procs
.into_iter()
.filter(|p| p.dog.is_some())
.filter(|p| filter.is_none_or(|f| matches_filter(f, &[&p.name])))
.collect(),
)),
_ => None,
},
)
.await
}
fn matches_filter(filter: &str, haystacks: &[&str]) -> bool {
let filter = filter.to_lowercase();
haystacks.iter().any(|h| h.to_lowercase().contains(&filter))
}
pub async fn available_dogs(streams: &mut Streams<'_>, args: &DogsArgs) -> ExitCode {
let url = dog_index::index_url();
let index = match dog_index::fetch_index(&url).await {
Ok(index) => index,
Err(err) => {
let message = format!(
"reading the dog index from {}: {err}",
fetch::url_for_message(&url)
);
return streams.fail(ExitCode::Failure, &message);
}
};
let (skipped, sanitised) = (index.skipped, index.sanitised);
let filter = args.filter.as_deref();
let matched: Vec<AvailableDog> = index
.dogs
.into_iter()
.filter(|dog| {
filter.is_none_or(|f| matches_filter(f, &[&dog.name, &dog.package, &dog.description]))
})
.collect();
let code = if streams.fmt == Format::Table
&& matched.is_empty()
&& let Some(filter) = filter
{
let _ = writeln!(streams.out, "no dog matches {filter:?}");
ExitCode::Success
} else if streams.fmt == Format::Table
&& let [only] = matched.as_slice()
{
write_outcome(render_detail(&mut *streams.out, only))
} else {
write_outcome(emit(
&mut *streams.out,
streams.fmt,
"dogs",
AvailableDogRows(matched),
streams.style,
))
};
note_index_costs(streams, skipped, sanitised);
code
}
const INDEX_WIDE: &str = ", across the whole index rather than this listing";
fn note_index_costs(streams: &mut Streams<'_>, skipped: usize, sanitised: usize) {
if skipped > 0 {
streams.aside(
"dogs_skipped",
&format!(
"{skipped} entr{} skipped{INDEX_WIDE}",
if skipped == 1 { "y" } else { "ies" }
),
);
}
if sanitised > 0 {
streams.aside(
"dogs_sanitised",
&format!(
"{sanitised} entr{} contained control characters{INDEX_WIDE}",
if sanitised == 1 { "y" } else { "ies" }
),
);
}
}
fn render_detail(out: &mut dyn std::io::Write, dog: &AvailableDog) -> std::io::Result<()> {
writeln!(out, "{} . {} . {}", dog.name, dog.package, dog.category)?;
writeln!(out, "{}", dog.description)?;
writeln!(out, "{} . {}", dog.license, dog.repo)?;
writeln!(out)?;
writeln!(out, "{}", install_line(&dog.source, &dog.package))?;
writeln!(
out,
"{}",
adopt_line(&dog.source, &dog.adopt_as, &dog.package)
)
}
fn install_line(source: &DogSourceKind, package: &str) -> String {
match source {
DogSourceKind::Cargo {
version: Some(version),
} => {
format!(" $ cargo install {package} --version {version}")
}
DogSourceKind::Cargo { version: None } => format!(" $ cargo install {package}"),
DogSourceKind::CargoGit { url } => format!(" $ cargo install --git {url}"),
DogSourceKind::GoInstall { module } => format!(" $ go install {module}@latest"),
DogSourceKind::Manual { instructions } => format!(" {instructions}"),
}
}
fn adopt_line(source: &DogSourceKind, adopt_as: &str, package: &str) -> String {
match source {
DogSourceKind::Cargo { .. } | DogSourceKind::CargoGit { .. } => {
format!(" $ shep adopt ~/.cargo/bin/{package} --name {adopt_as}")
}
DogSourceKind::GoInstall { .. } => {
format!(" $ shep adopt $(go env GOPATH)/bin/{package} --name {adopt_as}")
}
DogSourceKind::Manual { .. } => {
format!(" $ shep adopt <path to the binary> --name {adopt_as}")
}
}
}