1use crate::eval_call;
2use nu_protocol::{
3 ast::{Argument, Call, Expr, Expression, RecordItem},
4 debugger::WithoutDebug,
5 engine::CommandType,
6 engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
7 record, Category, Config, Example, IntoPipelineData, PipelineData, PositionalArg, Signature,
8 Span, SpanId, Spanned, SyntaxShape, Type, Value,
9};
10use nu_utils::terminal_size;
11use std::{collections::HashMap, fmt::Write};
12
13const RESET: &str = "\x1b[0m";
15const DEFAULT_COLOR: &str = "\x1b[39m";
17
18pub fn get_full_help(
19 command: &dyn Command,
20 engine_state: &EngineState,
21 stack: &mut Stack,
22) -> String {
23 let stack = &mut stack.start_collect_value();
28
29 let signature = engine_state
30 .get_signature(command)
31 .update_from_command(command);
32
33 get_documentation(
34 &signature,
35 &command.examples(),
36 engine_state,
37 stack,
38 command.is_keyword(),
39 )
40}
41
42fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
44 if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) {
45 let decl = engine_state.get_decl(highlighter);
46
47 let call = Call::new(Span::unknown());
48
49 if let Ok(output) = decl.run(
50 engine_state,
51 stack,
52 &(&call).into(),
53 Value::string(code_string, Span::unknown()).into_pipeline_data(),
54 ) {
55 let result = output.into_value(Span::unknown());
56 if let Ok(s) = result.and_then(Value::coerce_into_string) {
57 return s; }
59 }
60 }
61 code_string.to_string()
62}
63
64fn get_documentation(
65 sig: &Signature,
66 examples: &[Example],
67 engine_state: &EngineState,
68 stack: &mut Stack,
69 is_parser_keyword: bool,
70) -> String {
71 let nu_config = stack.get_config(engine_state);
72
73 let mut help_style = HelpStyle::default();
75 help_style.update_from_config(engine_state, &nu_config);
76 let help_section_name = &help_style.section_name;
77 let help_subcolor_one = &help_style.subcolor_one;
78
79 let cmd_name = &sig.name;
80 let mut long_desc = String::new();
81
82 let desc = &sig.description;
83 if !desc.is_empty() {
84 long_desc.push_str(desc);
85 long_desc.push_str("\n\n");
86 }
87
88 let extra_desc = &sig.extra_description;
89 if !extra_desc.is_empty() {
90 long_desc.push_str(extra_desc);
91 long_desc.push_str("\n\n");
92 }
93
94 if !sig.search_terms.is_empty() {
95 let _ = write!(
96 long_desc,
97 "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
98 sig.search_terms.join(", "),
99 );
100 }
101
102 let _ = write!(
103 long_desc,
104 "{help_section_name}Usage{RESET}:\n > {}\n",
105 sig.call_signature()
106 );
107
108 let mut subcommands = vec![];
116 let signatures = engine_state.get_signatures_and_declids(true);
117 for (sig, decl_id) in signatures {
118 let command_type = engine_state.get_decl(decl_id).command_type();
119
120 if sig.name.starts_with(&format!("{cmd_name} "))
122 && !matches!(sig.category, Category::Removed)
123 {
124 if command_type == CommandType::Plugin
126 || command_type == CommandType::Alias
127 || command_type == CommandType::Custom
128 {
129 subcommands.push(format!(
130 " {help_subcolor_one}{} {help_section_name}({}){RESET} - {}",
131 sig.name, command_type, sig.description
132 ));
133 } else {
134 subcommands.push(format!(
135 " {help_subcolor_one}{}{RESET} - {}",
136 sig.name, sig.description
137 ));
138 }
139 }
140 }
141
142 if !subcommands.is_empty() {
143 let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
144 subcommands.sort();
145 long_desc.push_str(&subcommands.join("\n"));
146 long_desc.push('\n');
147 }
148
149 if !sig.named.is_empty() {
150 long_desc.push_str(&get_flags_section(sig, &help_style, |v| {
151 nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack)
152 }))
153 }
154
155 if !sig.required_positional.is_empty()
156 || !sig.optional_positional.is_empty()
157 || sig.rest_positional.is_some()
158 {
159 let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
160 for positional in &sig.required_positional {
161 write_positional(
162 &mut long_desc,
163 positional,
164 PositionalKind::Required,
165 &help_style,
166 &nu_config,
167 engine_state,
168 stack,
169 );
170 }
171 for positional in &sig.optional_positional {
172 write_positional(
173 &mut long_desc,
174 positional,
175 PositionalKind::Optional,
176 &help_style,
177 &nu_config,
178 engine_state,
179 stack,
180 );
181 }
182
183 if let Some(rest_positional) = &sig.rest_positional {
184 write_positional(
185 &mut long_desc,
186 rest_positional,
187 PositionalKind::Rest,
188 &help_style,
189 &nu_config,
190 engine_state,
191 stack,
192 );
193 }
194 }
195
196 fn get_term_width() -> usize {
197 if let Ok((w, _h)) = terminal_size() {
198 w as usize
199 } else {
200 80
201 }
202 }
203
204 if !is_parser_keyword && !sig.input_output_types.is_empty() {
205 if let Some(decl_id) = engine_state.find_decl(b"table", &[]) {
206 let span = Span::unknown();
208 let mut vals = vec![];
209 for (input, output) in &sig.input_output_types {
210 vals.push(Value::record(
211 record! {
212 "input" => Value::string(input.to_string(), span),
213 "output" => Value::string(output.to_string(), span),
214 },
215 span,
216 ));
217 }
218
219 let caller_stack = &mut Stack::new().collect_value();
220 if let Ok(result) = eval_call::<WithoutDebug>(
221 engine_state,
222 caller_stack,
223 &Call {
224 decl_id,
225 head: span,
226 arguments: vec![Argument::Named((
227 Spanned {
228 item: "width".to_string(),
229 span: Span::unknown(),
230 },
231 None,
232 Some(Expression::new_unknown(
233 Expr::Int(get_term_width() as i64 - 2), Span::unknown(),
235 Type::Int,
236 )),
237 ))],
238 parser_info: HashMap::new(),
239 },
240 PipelineData::Value(Value::list(vals, span), None),
241 ) {
242 if let Ok((str, ..)) = result.collect_string_strict(span) {
243 let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
244 for line in str.lines() {
245 let _ = writeln!(long_desc, " {line}");
246 }
247 }
248 }
249 }
250 }
251
252 if !examples.is_empty() {
253 let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
254 }
255
256 for example in examples {
257 long_desc.push('\n');
258 long_desc.push_str(" ");
259 long_desc.push_str(example.description);
260
261 if !nu_config.use_ansi_coloring.get(engine_state) {
262 let _ = write!(long_desc, "\n > {}\n", example.example);
263 } else {
264 let code_string = nu_highlight_string(example.example, engine_state, stack);
265 let _ = write!(long_desc, "\n > {code_string}\n");
266 };
267
268 if let Some(result) = &example.result {
269 let mut table_call = Call::new(Span::unknown());
270 if example.example.ends_with("--collapse") {
271 table_call.add_named((
273 Spanned {
274 item: "collapse".to_string(),
275 span: Span::unknown(),
276 },
277 None,
278 None,
279 ))
280 } else {
281 table_call.add_named((
283 Spanned {
284 item: "expand".to_string(),
285 span: Span::unknown(),
286 },
287 None,
288 None,
289 ))
290 }
291 table_call.add_named((
292 Spanned {
293 item: "width".to_string(),
294 span: Span::unknown(),
295 },
296 None,
297 Some(Expression::new_unknown(
298 Expr::Int(get_term_width() as i64 - 2),
299 Span::unknown(),
300 Type::Int,
301 )),
302 ));
303
304 let table = engine_state
305 .find_decl("table".as_bytes(), &[])
306 .and_then(|decl_id| {
307 engine_state
308 .get_decl(decl_id)
309 .run(
310 engine_state,
311 stack,
312 &(&table_call).into(),
313 PipelineData::Value(result.clone(), None),
314 )
315 .ok()
316 });
317
318 for item in table.into_iter().flatten() {
319 let _ = writeln!(
320 long_desc,
321 " {}",
322 item.to_expanded_string("", &nu_config)
323 .replace('\n', "\n ")
324 .trim()
325 );
326 }
327 }
328 }
329
330 long_desc.push('\n');
331
332 if !nu_config.use_ansi_coloring.get(engine_state) {
333 nu_utils::strip_ansi_string_likely(long_desc)
334 } else {
335 long_desc
336 }
337}
338
339fn update_ansi_from_config(
340 ansi_code: &mut String,
341 engine_state: &EngineState,
342 nu_config: &Config,
343 theme_component: &str,
344) {
345 if let Some(color) = &nu_config.color_config.get(theme_component) {
346 let caller_stack = &mut Stack::new().collect_value();
347 let span = Span::unknown();
348 let span_id = UNKNOWN_SPAN_ID;
349
350 let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
351
352 if let Some(argument) = argument_opt {
354 if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
355 if let Ok(result) = eval_call::<WithoutDebug>(
356 engine_state,
357 caller_stack,
358 &Call {
359 decl_id,
360 head: span,
361 arguments: vec![argument],
362 parser_info: HashMap::new(),
363 },
364 PipelineData::Empty,
365 ) {
366 if let Ok((str, ..)) = result.collect_string_strict(span) {
367 *ansi_code = str;
368 }
369 }
370 }
371 }
372 }
373}
374
375fn get_argument_for_color_value(
376 nu_config: &Config,
377 color: &Value,
378 span: Span,
379 span_id: SpanId,
380) -> Option<Argument> {
381 match color {
382 Value::Record { val, .. } => {
383 let record_exp: Vec<RecordItem> = (**val)
384 .iter()
385 .map(|(k, v)| {
386 RecordItem::Pair(
387 Expression::new_existing(
388 Expr::String(k.clone()),
389 span,
390 span_id,
391 Type::String,
392 ),
393 Expression::new_existing(
394 Expr::String(v.clone().to_expanded_string("", nu_config)),
395 span,
396 span_id,
397 Type::String,
398 ),
399 )
400 })
401 .collect();
402
403 Some(Argument::Positional(Expression::new_existing(
404 Expr::Record(record_exp),
405 Span::unknown(),
406 UNKNOWN_SPAN_ID,
407 Type::Record(
408 [
409 ("fg".to_string(), Type::String),
410 ("attr".to_string(), Type::String),
411 ]
412 .into(),
413 ),
414 )))
415 }
416 Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
417 Expr::String(val.clone()),
418 Span::unknown(),
419 UNKNOWN_SPAN_ID,
420 Type::String,
421 ))),
422 _ => None,
423 }
424}
425
426pub struct HelpStyle {
432 section_name: String,
433 subcolor_one: String,
434 subcolor_two: String,
435}
436
437impl Default for HelpStyle {
438 fn default() -> Self {
439 HelpStyle {
440 section_name: "\x1b[32m".to_string(),
442 subcolor_one: "\x1b[36m".to_string(),
444 subcolor_two: "\x1b[94m".to_string(),
446 }
447 }
448}
449
450impl HelpStyle {
451 pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
459 update_ansi_from_config(
460 &mut self.section_name,
461 engine_state,
462 nu_config,
463 "shape_string",
464 );
465 update_ansi_from_config(
466 &mut self.subcolor_one,
467 engine_state,
468 nu_config,
469 "shape_external",
470 );
471 update_ansi_from_config(
472 &mut self.subcolor_two,
473 engine_state,
474 nu_config,
475 "shape_block",
476 );
477 }
478}
479
480fn document_shape(shape: &SyntaxShape) -> &SyntaxShape {
482 match shape {
483 SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape,
484 _ => shape,
485 }
486}
487
488#[derive(PartialEq)]
489enum PositionalKind {
490 Required,
491 Optional,
492 Rest,
493}
494
495fn write_positional(
496 long_desc: &mut String,
497 positional: &PositionalArg,
498 arg_kind: PositionalKind,
499 help_style: &HelpStyle,
500 nu_config: &Config,
501 engine_state: &EngineState,
502 stack: &mut Stack,
503) {
504 let help_subcolor_one = &help_style.subcolor_one;
505 let help_subcolor_two = &help_style.subcolor_two;
506
507 long_desc.push_str(" ");
509 if arg_kind == PositionalKind::Rest {
510 long_desc.push_str("...");
511 }
512 match &positional.shape {
513 SyntaxShape::Keyword(kw, shape) => {
514 let _ = write!(
515 long_desc,
516 "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
517 String::from_utf8_lossy(kw),
518 document_shape(shape),
519 );
520 }
521 _ => {
522 let _ = write!(
523 long_desc,
524 "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
525 positional.name,
526 document_shape(&positional.shape),
527 );
528 }
529 };
530 if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
531 let _ = write!(long_desc, ": {}", positional.desc);
532 }
533 if arg_kind == PositionalKind::Optional {
534 if let Some(value) = &positional.default_value {
535 let _ = write!(
536 long_desc,
537 " (optional, default: {})",
538 nu_highlight_string(
539 &value.to_parsable_string(", ", nu_config),
540 engine_state,
541 stack
542 )
543 );
544 } else {
545 long_desc.push_str(" (optional)");
546 };
547 }
548 long_desc.push('\n');
549}
550
551pub fn get_flags_section<F>(
552 signature: &Signature,
553 help_style: &HelpStyle,
554 mut value_formatter: F, ) -> String
556where
557 F: FnMut(&nu_protocol::Value) -> String,
558{
559 let help_section_name = &help_style.section_name;
560 let help_subcolor_one = &help_style.subcolor_one;
561 let help_subcolor_two = &help_style.subcolor_two;
562
563 let mut long_desc = String::new();
564 let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
565 for flag in &signature.named {
566 long_desc.push_str(" ");
568 if let Some(short) = flag.short {
570 let _ = write!(long_desc, "{help_subcolor_one}-{}{RESET}", short);
571 if !flag.long.is_empty() {
572 let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
573 }
574 }
575 if !flag.long.is_empty() {
576 let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
577 }
578 if flag.required {
579 long_desc.push_str(" (required parameter)")
580 }
581 if let Some(arg) = &flag.arg {
583 let _ = write!(
584 long_desc,
585 " <{help_subcolor_two}{}{RESET}>",
586 document_shape(arg)
587 );
588 }
589 if !flag.desc.is_empty() {
590 let _ = write!(long_desc, ": {}", flag.desc);
591 }
592 if let Some(value) = &flag.default_value {
593 let _ = write!(long_desc, " (default: {})", &value_formatter(value));
594 }
595 long_desc.push('\n');
596 }
597 long_desc
598}