use std::io::Write as _;
use clap::{Args, Subcommand};
use crate::api::models::{Page, Person};
use crate::cli::{Session, emit as write_out, report};
use crate::exit::ExitCode;
use crate::render::{Format, machine, user as render};
#[derive(Debug, Subcommand)]
pub enum UserCommand {
#[command(long_about = crate::cli::help::md(crate::cli::help::USER_LIST))]
List(PageArgs),
#[command(long_about = crate::cli::help::md(crate::cli::help::USER_GET))]
Get {
who: String,
},
#[command(long_about = crate::cli::help::md(crate::cli::help::USER_FIND))]
Find {
text: String,
#[arg(long, default_value_t = 1000)]
scan: usize,
},
}
#[derive(Debug, Args, Clone)]
pub struct PageArgs {
#[arg(long)]
pub limit: Option<usize>,
#[arg(long, default_value_t = 1)]
pub page: u32,
}
pub async fn run(command: &UserCommand, session: &Session) -> ExitCode {
let client = match session.client() {
Ok(client) => client,
Err(code) => return code,
};
match command {
UserCommand::List(args) => {
let per_page = args.limit.unwrap_or(session.display().limit);
let Ok(per_page) = u32::try_from(per_page.max(1)) else {
return report(&"--limit is too large", ExitCode::ConfirmationRequired);
};
match client.users(args.page.max(1), per_page).await {
Ok(page) => {
let next = page.has_more().then(|| page.page + 1);
emit(&page, next, session)
}
Err(error) => {
let code = error.exit_code();
report(&error, code)
}
}
}
UserCommand::Get { who } => match client.user(who).await {
Ok(person) => {
let rendered = match session.render.format {
Format::Text => Ok(render::user(&person, &session.render)),
Format::JsonRaw => machine(&person, Format::Json),
other => machine(&person, other),
};
match rendered {
Ok(text) => {
write_out(&text);
ExitCode::Success
}
Err(error) => report(&error, ExitCode::Failure),
}
}
Err(error) => {
let code = error.exit_code();
report(&error, code)
}
},
UserCommand::Find { text, scan } => find(&client, text, *scan, session).await,
}
}
async fn find(client: &crate::api::Client, text: &str, scan: usize, session: &Session) -> ExitCode {
let needle = text.to_lowercase();
let mut matched: Vec<Person> = Vec::new();
let mut read = 0usize;
let mut page_number = 1;
let mut total = None;
let walk = crate::render::progress::Walk::start("reading the directory");
loop {
let page = match client.users(page_number, 100).await {
Ok(page) => page,
Err(error) => {
walk.finish();
let code = error.exit_code();
return report(&error, code);
}
};
total = page.total.or(total);
read += page.items.len();
let more = page.has_more();
matched.extend(
page.items
.into_iter()
.filter(|person| matches(person, &needle)),
);
walk.page(page_number, matched.len(), total);
if !more || read >= scan {
break;
}
page_number += 1;
}
walk.finish();
let incomplete = total.is_some_and(|total| read < usize::try_from(total).unwrap_or(usize::MAX));
let Ok(count) = u32::try_from(matched.len()) else {
return report(&"too many results to render", ExitCode::Failure);
};
let page = Page {
items: matched,
page: 1,
per_page: count.max(1),
total: Some(read as u64),
};
let code = emit(&page, None, session);
if incomplete && session.render.format == Format::Text {
let mut err = anstream::stderr();
let _ = writeln!(
err,
"searched {read} of {} people; raise --scan to look further",
total.map_or_else(|| "unknown".to_owned(), |total| total.to_string())
);
}
code
}
fn matches(person: &Person, needle: &str) -> bool {
person.login.to_lowercase().contains(needle)
|| person.display.to_lowercase().contains(needle)
|| person
.email
.as_deref()
.is_some_and(|email| email.to_lowercase().contains(needle))
}
fn emit(page: &Page<Person>, next: Option<u32>, session: &Session) -> ExitCode {
let rendered = match session.render.format {
Format::Text => Ok(render::users(
&page.items,
page.total,
next,
&session.render,
)),
Format::JsonRaw => machine(&page.items, Format::Json),
other => machine(&page.items, other),
};
match rendered {
Ok(text) => {
write_out(&text);
ExitCode::Success
}
Err(error) => report(&error, ExitCode::Failure),
}
}