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