1use std::io::Write as _;
11
12use clap::{Args, Subcommand};
13
14use crate::api::models::{Page, Person};
15use crate::cli::{Session, emit as write_out, report};
16use crate::exit::ExitCode;
17use crate::render::{Format, machine, user as render};
18
19#[derive(Debug, Subcommand)]
20pub enum UserCommand {
21 #[command(long_about = crate::cli::help::md(crate::cli::help::USER_LIST))]
23 List(PageArgs),
24 #[command(long_about = crate::cli::help::md(crate::cli::help::USER_GET))]
26 Get {
27 who: String,
29 },
30 #[command(long_about = crate::cli::help::md(crate::cli::help::USER_FIND))]
32 Find {
33 text: String,
35 #[arg(long, default_value_t = 1000)]
37 scan: usize,
38 },
39}
40
41#[derive(Debug, Args, Clone)]
42pub struct PageArgs {
43 #[arg(long)]
45 pub limit: Option<usize>,
46 #[arg(long, default_value_t = 1)]
48 pub page: u32,
49}
50
51pub async fn run(command: &UserCommand, session: &Session) -> ExitCode {
52 let client = match session.client() {
53 Ok(client) => client,
54 Err(code) => return code,
55 };
56
57 match command {
58 UserCommand::List(args) => {
59 let per_page = args.limit.unwrap_or(session.display().limit);
60 let Ok(per_page) = u32::try_from(per_page.max(1)) else {
61 return report(&"--limit is too large", ExitCode::ConfirmationRequired);
62 };
63
64 match client.users(args.page.max(1), per_page).await {
65 Ok(page) => {
66 let next = page.has_more().then(|| page.page + 1);
67 emit(&page, next, session)
68 }
69 Err(error) => {
70 let code = error.exit_code();
71 report(&error, code)
72 }
73 }
74 }
75 UserCommand::Get { who } => match client.user(who).await {
76 Ok(person) => {
77 let rendered = match session.render.format {
78 Format::Text => Ok(render::user(&person, &session.render)),
79 Format::JsonRaw => machine(&person, Format::Json),
80 other => machine(&person, other),
81 };
82 match rendered {
83 Ok(text) => {
84 write_out(&text);
85 ExitCode::Success
86 }
87 Err(error) => report(&error, ExitCode::Failure),
88 }
89 }
90 Err(error) => {
91 let code = error.exit_code();
92 report(&error, code)
93 }
94 },
95 UserCommand::Find { text, scan } => find(&client, text, *scan, session).await,
96 }
97}
98
99async fn find(client: &crate::api::Client, text: &str, scan: usize, session: &Session) -> ExitCode {
107 let needle = text.to_lowercase();
108 let mut matched: Vec<Person> = Vec::new();
109 let mut read = 0usize;
110 let mut page_number = 1;
111 let mut total = None;
112 let walk = crate::render::progress::Walk::start("reading the directory");
113
114 loop {
115 let page = match client.users(page_number, 100).await {
116 Ok(page) => page,
117 Err(error) => {
118 walk.finish();
119 let code = error.exit_code();
120 return report(&error, code);
121 }
122 };
123 total = page.total.or(total);
124 read += page.items.len();
125
126 let more = page.has_more();
127 matched.extend(
128 page.items
129 .into_iter()
130 .filter(|person| matches(person, &needle)),
131 );
132 walk.page(page_number, matched.len(), total);
133
134 if !more || read >= scan {
135 break;
136 }
137 page_number += 1;
138 }
139 walk.finish();
140
141 let incomplete = total.is_some_and(|total| read < usize::try_from(total).unwrap_or(usize::MAX));
145 let Ok(count) = u32::try_from(matched.len()) else {
146 return report(&"too many results to render", ExitCode::Failure);
147 };
148 let page = Page {
149 items: matched,
150 page: 1,
151 per_page: count.max(1),
152 total: Some(read as u64),
153 };
154
155 let code = emit(&page, None, session);
159 if incomplete && session.render.format == Format::Text {
160 let mut err = anstream::stderr();
161 let _ = writeln!(
162 err,
163 "searched {read} of {} people; raise --scan to look further",
164 total.map_or_else(|| "unknown".to_owned(), |total| total.to_string())
165 );
166 }
167 code
168}
169
170fn matches(person: &Person, needle: &str) -> bool {
171 person.login.to_lowercase().contains(needle)
172 || person.display.to_lowercase().contains(needle)
173 || person
174 .email
175 .as_deref()
176 .is_some_and(|email| email.to_lowercase().contains(needle))
177}
178
179fn emit(page: &Page<Person>, next: Option<u32>, session: &Session) -> ExitCode {
180 let rendered = match session.render.format {
181 Format::Text => Ok(render::users(
182 &page.items,
183 page.total,
184 next,
185 &session.render,
186 )),
187 Format::JsonRaw => machine(&page.items, Format::Json),
188 other => machine(&page.items, other),
189 };
190
191 match rendered {
192 Ok(text) => {
193 write_out(&text);
194 ExitCode::Success
195 }
196 Err(error) => report(&error, ExitCode::Failure),
197 }
198}