Skip to main content

teamy_figue/
help.rs

1//! Help text generation for command-line interfaces.
2//!
3//! This module provides utilities to generate help text from Schema,
4//! including doc comments, field names, and attribute information.
5
6use crate::driver::HelpListMode;
7use crate::missing::normalize_program_name;
8use crate::schema::{ArgLevelSchema, ArgSchema, Schema, Subcommand};
9use facet_core::Facet;
10use owo_colors::OwoColorize;
11use owo_colors::Stream::Stdout;
12use std::fmt;
13use std::string::String;
14use std::sync::Arc;
15use std::vec::Vec;
16
17/// Generate help text for a Facet type.
18///
19/// This is a convenience function that builds a Schema internally.
20/// If you already have a Schema, use `generate_help_for_subcommand` instead.
21pub fn generate_help<T: Facet<'static>>(config: &HelpConfig) -> String {
22    generate_help_for_shape(T::SHAPE, config)
23}
24
25/// Generate help text from a Shape.
26///
27/// This is a convenience function that builds a Schema internally.
28/// If you already have a Schema, use `generate_help_for_subcommand` instead.
29pub fn generate_help_for_shape(shape: &'static facet_core::Shape, config: &HelpConfig) -> String {
30    let schema = match Schema::from_shape(shape) {
31        Ok(s) => s,
32        Err(_) => {
33            // Fall back to a minimal help message
34            let program_name = config
35                .program_name
36                .clone()
37                .or_else(|| {
38                    std::env::args()
39                        .next()
40                        .map(|path| normalize_program_name(&path))
41                })
42                .unwrap_or_else(|| "program".to_string());
43            return format!(
44                "{}\n\n(Schema could not be built for this type)\n",
45                program_name
46            );
47        }
48    };
49
50    generate_help_for_subcommand(&schema, &[], config)
51}
52
53/// Configuration for help text generation.
54#[derive(Clone)]
55pub struct HelpConfig {
56    /// Program name (defaults to executable name)
57    pub program_name: Option<String>,
58    /// Program version
59    pub version: Option<String>,
60    /// Additional description to show after the auto-generated one
61    pub description: Option<String>,
62    /// Width for wrapping text (0 = no wrapping)
63    pub width: usize,
64    /// Whether to include implementation source file information in help output.
65    pub include_implementation_source_file: bool,
66    /// Optional callback to render an implementation URL from a source file path.
67    pub implementation_url: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
68}
69
70impl fmt::Debug for HelpConfig {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("HelpConfig")
73            .field("program_name", &self.program_name)
74            .field("version", &self.version)
75            .field("description", &self.description)
76            .field("width", &self.width)
77            .field(
78                "include_implementation_source_file",
79                &self.include_implementation_source_file,
80            )
81            .field(
82                "implementation_url",
83                &self.implementation_url.as_ref().map(|_| "<fn>"),
84            )
85            .finish()
86    }
87}
88
89impl Default for HelpConfig {
90    fn default() -> Self {
91        Self {
92            program_name: None,
93            version: None,
94            description: None,
95            width: 80,
96            include_implementation_source_file: false,
97            implementation_url: None,
98        }
99    }
100}
101
102/// Resolve implementation source file for a subcommand path from a root shape.
103///
104/// The `subcommand_path` should contain effective subcommand names (as emitted by
105/// `ConfigValue::extract_subcommand_path`). An empty path resolves to the root shape.
106pub(crate) fn implementation_source_for_subcommand_path(
107    root_shape: &'static facet_core::Shape,
108    subcommand_path: &[String],
109) -> Option<&'static str> {
110    let mut current_shape = root_shape;
111
112    if subcommand_path.is_empty() {
113        return current_shape.source_file;
114    }
115
116    for segment in subcommand_path {
117        let next_shape = next_subcommand_shape(current_shape, segment)?;
118        current_shape = next_shape;
119    }
120
121    current_shape.source_file
122}
123
124fn next_subcommand_shape(
125    shape: &'static facet_core::Shape,
126    target_effective_name: &str,
127) -> Option<&'static facet_core::Shape> {
128    let fields = match shape.ty {
129        facet_core::Type::User(facet_core::UserType::Struct(s)) => s.fields,
130        _ => return None,
131    };
132
133    let subcommand_field = fields
134        .iter()
135        .find(|field| field.has_attr(Some("args"), "subcommand"))?;
136
137    let enum_shape = unwrap_option_shape(subcommand_field.shape());
138    let variants = match enum_shape.ty {
139        facet_core::Type::User(facet_core::UserType::Enum(e)) => e.variants,
140        _ => return None,
141    };
142
143    let variant = variants
144        .iter()
145        .find(|variant| variant.effective_name() == target_effective_name)?;
146
147    if variant.data.fields.is_empty() {
148        return Some(enum_shape);
149    }
150
151    let has_direct_subcommand = variant
152        .data
153        .fields
154        .iter()
155        .any(|field| field.has_attr(Some("args"), "subcommand"));
156
157    if has_direct_subcommand {
158        return Some(enum_shape);
159    }
160
161    if variant.data.fields.len() == 1 {
162        return Some(unwrap_option_shape(variant.data.fields[0].shape()));
163    }
164
165    Some(enum_shape)
166}
167
168fn unwrap_option_shape(mut shape: &'static facet_core::Shape) -> &'static facet_core::Shape {
169    while let facet_core::Def::Option(option_def) = shape.def {
170        shape = option_def.t;
171    }
172    shape
173}
174
175/// Generate help text for a specific subcommand path from a Schema.
176///
177/// `subcommand_path` is a list of subcommand names (e.g., `["repo", "clone"]` for `myapp repo clone --help`).
178/// This navigates through the schema to find the target subcommand and generates help for it.
179pub fn generate_help_for_subcommand(
180    schema: &Schema,
181    subcommand_path: &[String],
182    config: &HelpConfig,
183) -> String {
184    let program_name = config
185        .program_name
186        .clone()
187        .or_else(|| {
188            std::env::args()
189                .next()
190                .map(|path| normalize_program_name(&path))
191        })
192        .unwrap_or_else(|| "program".to_string());
193
194    if subcommand_path.is_empty() {
195        return generate_help_from_schema(schema, &program_name, config);
196    }
197
198    // Navigate to the subcommand
199    let mut current_args = schema.args();
200    let mut command_path = vec![program_name.clone()];
201
202    for name in subcommand_path {
203        // The path contains effective names (e.g., "Clone", "rm") from ConfigValue.
204        // Look up by effective_name since that's what's stored in the path.
205        let sub = current_args
206            .subcommands()
207            .values()
208            .find(|s| s.effective_name() == name);
209
210        if let Some(sub) = sub {
211            command_path.push(sub.cli_name().to_string());
212            current_args = sub.args();
213        } else {
214            // Subcommand not found, fall back to root help
215            return generate_help_from_schema(schema, &program_name, config);
216        }
217    }
218
219    // Find the final subcommand to get its docs
220    let mut final_sub: Option<&Subcommand> = None;
221    let mut args = schema.args();
222
223    for name in subcommand_path {
224        let sub = args
225            .subcommands()
226            .values()
227            .find(|s| s.effective_name() == name);
228        if let Some(sub) = sub {
229            final_sub = Some(sub);
230            args = sub.args();
231        }
232    }
233
234    generate_help_for_subcommand_level(current_args, final_sub, &command_path.join(" "), config)
235}
236
237/// Generate help-list output for subcommands at the current command level.
238///
239/// In [`HelpListMode::Short`], this returns one full CLI command path per line,
240/// recursively listing all reachable leaf commands.
241/// In [`HelpListMode::Full`], this returns concatenated help output for each
242/// reachable leaf subcommand under the current command path.
243pub(crate) fn generate_help_list_for_subcommand(
244    schema: &Schema,
245    subcommand_path: &[String],
246    config: &HelpConfig,
247    mode: HelpListMode,
248) -> String {
249    let program_name = config
250        .program_name
251        .clone()
252        .or_else(|| {
253            std::env::args()
254                .next()
255                .map(|path| normalize_program_name(&path))
256        })
257        .unwrap_or_else(|| "program".to_string());
258
259    let mut current_args = schema.args();
260    let mut resolved_path = Vec::new();
261
262    for name in subcommand_path {
263        let sub = current_args
264            .subcommands()
265            .values()
266            .find(|s| s.effective_name() == name);
267
268        let Some(sub) = sub else {
269            // Fall back to regular root help if the path cannot be resolved.
270            return generate_help_for_subcommand(schema, &[], config);
271        };
272
273        resolved_path.push(sub.effective_name().to_string());
274        current_args = sub.args();
275    }
276
277    if !current_args.has_subcommands() {
278        let command_display = if resolved_path.is_empty() {
279            program_name
280        } else {
281            let cli_chain = resolve_cli_chain(schema, &resolved_path);
282            if cli_chain.is_empty() {
283                program_name
284            } else {
285                format!("{} {}", program_name, cli_chain.join(" "))
286            }
287        };
288        return format!("No subcommands available for {command_display}.");
289    }
290
291    match mode {
292        HelpListMode::Short => {
293            let mut cli_chain = if resolved_path.is_empty() {
294                Vec::new()
295            } else {
296                resolve_cli_chain(schema, &resolved_path)
297            };
298            let mut commands = Vec::new();
299            collect_short_help_commands(
300                &mut commands,
301                program_name.as_str(),
302                &mut cli_chain,
303                current_args,
304            );
305            commands.join("\n")
306        }
307        HelpListMode::Full => {
308            let mut sections = Vec::new();
309            let mut leaf_paths = Vec::new();
310            let mut working_path = resolved_path.clone();
311            collect_leaf_subcommand_paths(&mut leaf_paths, &mut working_path, current_args);
312
313            for child_path in leaf_paths {
314                sections.push(generate_help_for_subcommand(schema, &child_path, config));
315            }
316            sections.join("\n\n")
317        }
318    }
319}
320
321fn collect_leaf_subcommand_paths(
322    leaf_paths: &mut Vec<Vec<String>>,
323    current_path: &mut Vec<String>,
324    args: &ArgLevelSchema,
325) {
326    if !args.has_subcommands() {
327        if !current_path.is_empty() {
328            leaf_paths.push(current_path.clone());
329        }
330        return;
331    }
332
333    for sub in args.subcommands().values() {
334        current_path.push(sub.effective_name().to_string());
335        collect_leaf_subcommand_paths(leaf_paths, current_path, sub.args());
336        current_path.pop();
337    }
338}
339
340fn collect_short_help_commands(
341    commands: &mut Vec<String>,
342    program_name: &str,
343    cli_chain: &mut Vec<String>,
344    args: &ArgLevelSchema,
345) {
346    if args.subcommand_optional() {
347        if cli_chain.is_empty() {
348            commands.push(program_name.to_string());
349        } else {
350            commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
351        }
352    }
353
354    if !args.has_subcommands() {
355        if !cli_chain.is_empty() {
356            commands.push(format!("{} {}", program_name, cli_chain.join(" ")));
357        }
358        return;
359    }
360
361    for sub in args.subcommands().values() {
362        cli_chain.push(sub.cli_name().to_string());
363        collect_short_help_commands(commands, program_name, cli_chain, sub.args());
364        cli_chain.pop();
365    }
366}
367
368fn resolve_cli_chain(schema: &Schema, subcommand_path: &[String]) -> Vec<String> {
369    let mut current_args = schema.args();
370    let mut cli_path = Vec::new();
371
372    for name in subcommand_path {
373        let sub = current_args
374            .subcommands()
375            .values()
376            .find(|s| s.effective_name() == name);
377        let Some(sub) = sub else {
378            break;
379        };
380        cli_path.push(sub.cli_name().to_string());
381        current_args = sub.args();
382    }
383
384    cli_path
385}
386
387/// Generate help from a built Schema.
388fn generate_help_from_schema(schema: &Schema, program_name: &str, config: &HelpConfig) -> String {
389    let mut out = String::new();
390
391    // Program name and version
392    if let Some(version) = &config.version {
393        out.push_str(&format!("{program_name} {version}\n"));
394    } else {
395        out.push_str(&format!("{program_name}\n"));
396    }
397
398    // Type doc comment from schema
399    if let Some(summary) = schema.docs().summary() {
400        out.push('\n');
401        out.push_str(summary.trim());
402        out.push('\n');
403    }
404    if let Some(details) = schema.docs().details() {
405        for line in details.lines() {
406            out.push_str(line.trim());
407            out.push('\n');
408        }
409    }
410
411    // Additional description
412    if let Some(desc) = &config.description {
413        out.push('\n');
414        out.push_str(desc);
415        out.push('\n');
416    }
417
418    out.push('\n');
419
420    generate_arg_level_help(&mut out, schema.args(), program_name);
421
422    out
423}
424
425/// Generate help for a subcommand level.
426fn generate_help_for_subcommand_level(
427    args: &ArgLevelSchema,
428    subcommand: Option<&Subcommand>,
429    full_command: &str,
430    config: &HelpConfig,
431) -> String {
432    let mut out = String::new();
433
434    // Header with full command
435    out.push_str(&format!("{full_command}\n"));
436
437    // Doc comment for the subcommand
438    if let Some(sub) = subcommand {
439        if let Some(summary) = sub.docs().summary() {
440            out.push('\n');
441            out.push_str(summary.trim());
442            out.push('\n');
443        }
444        if let Some(details) = sub.docs().details() {
445            for line in details.lines() {
446                out.push_str(line.trim());
447                out.push('\n');
448            }
449        }
450    }
451
452    // Additional description from config
453    if let Some(desc) = &config.description {
454        out.push('\n');
455        out.push_str(desc);
456        out.push('\n');
457    }
458
459    out.push('\n');
460
461    generate_arg_level_help(&mut out, args, full_command);
462
463    out
464}
465
466/// Generate help output for an argument level (args + subcommands).
467fn generate_arg_level_help(out: &mut String, args: &ArgLevelSchema, program_name: &str) {
468    // Separate positionals and named flags
469    let mut positionals: Vec<&ArgSchema> = Vec::new();
470    let mut flags: Vec<&ArgSchema> = Vec::new();
471
472    for (_name, arg) in args.args().iter() {
473        if arg.kind().is_positional() {
474            positionals.push(arg);
475        } else {
476            flags.push(arg);
477        }
478    }
479
480    // Usage line
481    out.push_str(&format!("{}:\n    ", "USAGE".yellow().bold()));
482    out.push_str(program_name);
483
484    if !flags.is_empty() {
485        out.push_str(" [OPTIONS]");
486    }
487
488    for pos in &positionals {
489        let name = pos.name().to_uppercase();
490        if pos.required() {
491            out.push_str(&format!(" <{name}>"));
492        } else {
493            out.push_str(&format!(" [{name}]"));
494        }
495    }
496
497    if args.has_subcommands() {
498        if args.subcommand_optional() {
499            out.push_str(" [COMMAND]");
500        } else {
501            out.push_str(" <COMMAND>");
502        }
503    }
504
505    out.push_str("\n\n");
506
507    // Positional arguments
508    if !positionals.is_empty() {
509        out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
510        for arg in &positionals {
511            write_arg_help(out, arg);
512        }
513        out.push('\n');
514    }
515
516    // Options
517    if !flags.is_empty() {
518        out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
519        for arg in &flags {
520            write_arg_help(out, arg);
521        }
522        out.push('\n');
523    }
524
525    // Subcommands
526    if args.has_subcommands() {
527        out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
528        for sub in args.subcommands().values() {
529            write_subcommand_help(out, sub);
530        }
531        out.push('\n');
532    }
533}
534
535/// Write help for a single argument.
536fn write_arg_help(out: &mut String, arg: &ArgSchema) {
537    out.push_str("    ");
538
539    let is_positional = arg.kind().is_positional();
540
541    // Short flag (or spacing for alignment)
542    if let Some(c) = arg.kind().short() {
543        out.push_str(&format!(
544            "{}, ",
545            format!("-{c}").if_supports_color(Stdout, |text| text.green())
546        ));
547    } else {
548        // Add spacing to align with flags that have short options
549        out.push_str("    ");
550    }
551
552    // Long flag or positional name
553    let name = arg.name();
554    let is_counted = arg.kind().is_counted();
555
556    if is_positional {
557        out.push_str(&format!(
558            "{}",
559            format!("<{}>", name.to_uppercase()).if_supports_color(Stdout, |text| text.green())
560        ));
561    } else {
562        out.push_str(&format!(
563            "{}",
564            format!("--{name}").if_supports_color(Stdout, |text| text.green())
565        ));
566
567        // Show value placeholder for non-bool, non-counted types
568        if !is_counted && !arg.value().is_bool() {
569            let placeholder = if let Some(desc) = arg.label() {
570                desc.to_uppercase()
571            } else if let Some(variants) = arg.value().inner_if_option().enum_variants() {
572                variants.join(",")
573            } else {
574                arg.value().type_identifier().to_uppercase()
575            };
576            out.push_str(&format!(" <{}>", placeholder));
577        }
578    }
579
580    // Doc comment
581    if let Some(summary) = arg.docs().summary() {
582        out.push_str("\n            ");
583        out.push_str(summary.trim());
584    }
585
586    if is_counted {
587        out.push_str("\n            ");
588        out.push_str("[can be repeated]");
589    }
590
591    out.push('\n');
592}
593
594/// Write help for a subcommand.
595fn write_subcommand_help(out: &mut String, sub: &Subcommand) {
596    out.push_str("    ");
597
598    out.push_str(&format!(
599        "{}",
600        sub.cli_name()
601            .if_supports_color(Stdout, |text| text.green())
602    ));
603
604    // Doc comment
605    if let Some(summary) = sub.docs().summary() {
606        out.push_str("\n            ");
607        out.push_str(summary.trim());
608    }
609
610    out.push('\n');
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use facet::Facet;
617    use figue_attrs as args;
618
619    /// Common arguments that can be flattened into other structs
620    #[derive(Facet)]
621    struct CommonArgs {
622        /// Enable verbose output
623        #[facet(args::named, crate::short = 'v')]
624        verbose: bool,
625
626        /// Enable quiet mode
627        #[facet(args::named, crate::short = 'q')]
628        quiet: bool,
629    }
630
631    /// Args struct with flattened common args
632    #[derive(Facet)]
633    struct ArgsWithFlatten {
634        /// Input file
635        #[facet(args::positional)]
636        input: String,
637
638        /// Common options
639        #[facet(flatten)]
640        common: CommonArgs,
641    }
642
643    #[test]
644    fn test_flatten_args_appear_in_help() {
645        let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
646        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
647
648        // Flattened fields should appear at top level
649        assert!(
650            help.contains("--verbose"),
651            "help should contain --verbose from flattened CommonArgs"
652        );
653        assert!(help.contains("-v"), "help should contain -v short flag");
654        assert!(
655            help.contains("--quiet"),
656            "help should contain --quiet from flattened CommonArgs"
657        );
658        assert!(help.contains("-q"), "help should contain -q short flag");
659
660        // The flattened field name 'common' should NOT appear as a flag
661        assert!(
662            !help.contains("--common"),
663            "help should not show --common as a flag"
664        );
665    }
666
667    #[test]
668    fn test_flatten_docs_preserved() {
669        let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
670        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
671
672        // Doc comments from flattened fields should be present
673        assert!(
674            help.contains("verbose output"),
675            "help should contain verbose field doc"
676        );
677        assert!(
678            help.contains("quiet mode"),
679            "help should contain quiet field doc"
680        );
681    }
682
683    /// Arguments for the serve subcommand
684    #[derive(Facet)]
685    struct ServeArgs {
686        /// Port to serve on
687        #[facet(args::named)]
688        port: u16,
689
690        /// Host to bind to
691        #[facet(args::named)]
692        host: String,
693    }
694
695    /// Top-level command with tuple variant subcommand
696    #[derive(Facet)]
697    struct TupleVariantArgs {
698        /// Subcommand to run
699        #[facet(args::subcommand)]
700        command: Option<TupleVariantCommand>,
701    }
702
703    /// Command enum with tuple variant
704    #[derive(Facet)]
705    #[repr(u8)]
706    #[allow(dead_code)]
707    enum TupleVariantCommand {
708        /// Start the server
709        Serve(ServeArgs),
710    }
711
712    #[test]
713    fn test_label_overrides_placeholder() {
714        #[derive(Facet)]
715        struct TDArgs {
716            /// Input path
717            #[facet(args::named, args::label = "PATH")]
718            input: std::path::PathBuf,
719        }
720        let schema = Schema::from_shape(TDArgs::SHAPE).unwrap();
721        let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
722        // Only assert on the placeholder to avoid issues with ANSI color codes around the flag name
723        assert!(
724            help.contains("<PATH>"),
725            "help should use custom label placeholder"
726        );
727    }
728
729    #[test]
730    fn test_tuple_variant_fields_not_shown_as_option() {
731        let schema = Schema::from_shape(TupleVariantArgs::SHAPE).unwrap();
732        // Path contains effective names (e.g., "Serve" not "serve")
733        let help =
734            generate_help_for_subcommand(&schema, &["Serve".to_string()], &HelpConfig::default());
735
736        // The inner struct's fields should appear
737        assert!(
738            help.contains("--port"),
739            "help should contain --port from ServeArgs"
740        );
741        assert!(
742            help.contains("--host"),
743            "help should contain --host from ServeArgs"
744        );
745
746        // The tuple field "0" should NOT appear as --0
747        assert!(
748            !help.contains("--0"),
749            "help should NOT show --0 for tuple variant wrapper field"
750        );
751        assert!(
752            !help.contains("SERVEARGS"),
753            "help should NOT show SERVEARGS as an option value"
754        );
755    }
756
757    #[derive(Facet)]
758    struct NestedRootArgs {
759        #[facet(args::subcommand)]
760        command: NestedRootCommand,
761    }
762
763    #[derive(Facet)]
764    #[repr(u8)]
765    #[allow(dead_code)]
766    enum NestedRootCommand {
767        Home(NestedHomeArgs),
768        Cache(NestedCacheArgs),
769    }
770
771    #[derive(Facet)]
772    struct NestedHomeArgs {
773        #[facet(args::subcommand)]
774        command: NestedHomeCommand,
775    }
776
777    #[derive(Facet)]
778    #[repr(u8)]
779    #[allow(dead_code)]
780    enum NestedHomeCommand {
781        Open,
782        Show,
783    }
784
785    #[derive(Facet)]
786    struct NestedCacheArgs {
787        #[facet(args::subcommand)]
788        command: NestedCacheCommand,
789    }
790
791    #[derive(Facet)]
792    #[repr(u8)]
793    #[allow(dead_code)]
794    enum NestedCacheCommand {
795        Open,
796        Show,
797    }
798
799    #[test]
800    fn test_help_list_short_is_recursive_with_full_command_paths() {
801        let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
802        let output = generate_help_list_for_subcommand(
803            &schema,
804            &[],
805            &HelpConfig {
806                program_name: Some("myapp".to_string()),
807                ..HelpConfig::default()
808            },
809            HelpListMode::Short,
810        );
811
812        let lines: Vec<&str> = output.lines().collect();
813        assert_eq!(
814            lines,
815            vec![
816                "myapp home open",
817                "myapp home show",
818                "myapp cache open",
819                "myapp cache show"
820            ]
821        );
822    }
823
824    #[test]
825    fn test_help_list_full_is_recursive_for_leaf_subcommands() {
826        let schema = Schema::from_shape(NestedRootArgs::SHAPE).unwrap();
827        let output = generate_help_list_for_subcommand(
828            &schema,
829            &[],
830            &HelpConfig {
831                program_name: Some("myapp".to_string()),
832                ..HelpConfig::default()
833            },
834            HelpListMode::Full,
835        );
836
837        assert!(output.contains("myapp home open"));
838        assert!(output.contains("myapp home show"));
839        assert!(output.contains("myapp cache open"));
840        assert!(output.contains("myapp cache show"));
841        assert!(!output.contains("myapp home\n\n"));
842        assert!(!output.contains("myapp cache\n\n"));
843    }
844}