nu-explore 0.114.1

Nushell table pager
Documentation
//! Command definition for the `explore config` command.

use crate::explore_config::conversion::{
    build_nu_type_map, build_original_value_map, json_to_nu_value_with_types, nu_value_to_json,
    parse_config_documentation,
};
use crate::explore_config::example_data::get_example_json;
use crate::explore_config::tree::print_json_tree;
use crate::explore_config::tui::run_config_tui;
use crate::explore_config::types::NuValueType;
use nu_engine::command_prelude::*;
use nu_protocol::{PipelineData, report_shell_warning, shell_error::generic::GenericError};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;

/// Type alias for the tuple returned when determining data source and mode
type ConfigDataResult = (
    Value,
    Option<HashMap<String, NuValueType>>,
    Option<HashMap<String, nu_protocol::Value>>,
    Option<HashMap<String, String>>,
);

/// A command to explore and edit nushell configuration interactively.
#[derive(Clone)]
pub struct ExploreConfigCommand;

impl Command for ExploreConfigCommand {
    fn name(&self) -> &str {
        "explore config"
    }

    fn description(&self) -> &str {
        "Launch a TUI to view and edit the nushell configuration interactively."
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build("explore config")
            .input_output_types(vec![
                (Type::Nothing, Type::String),
                (Type::String, Type::String),
            ])
            .switch(
                "use-example-data",
                "Show the nushell configuration TUI using example data.",
                Some('e'),
            )
            .switch(
                "tree",
                "Do not show the TUI, just show a tree structure of the data.",
                Some('t'),
            )
            .named(
                "output",
                SyntaxShape::String,
                "Optional output file to save changes to (default: output.json).",
                Some('o'),
            )
            .category(Category::Viewers)
    }

    fn extra_description(&self) -> &str {
        "By default, opens the current nushell configuration ($env.config) in the TUI.
Changes made in config mode are applied to the running session when you quit.

You can also pipe JSON data to explore arbitrary data structures, or use
--use-example-data to see sample configuration data.

TUI Keybindings:
  Tab           Switch between tree and editor panes
  ↑↓/jk         Navigate tree / scroll editor
  ←→/hl         Collapse/Expand tree nodes
  Enter/Space   Toggle tree node expansion
  Enter/Space   On leaf nodes, open editor pane and start editing
  Enter/e       Start editing (in editor pane)
  Ctrl+S        Apply edit
  Alt+Enter     Apply edit (alternative)
  Esc           Cancel edit
  q             Quit (applies config changes if modified)
  Ctrl+C        Force quit without saving"
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let input_span = input.span().unwrap_or(call.head);
        let (string_input, _span, _metadata) = input.collect_string_strict(input_span)?;
        let use_example = call.has_flag(engine_state, stack, "use-example-data")?;
        let cli_mode = call.has_flag(engine_state, stack, "tree")?;
        let output_file: Option<String> = call.get_flag(engine_state, stack, "output")?;

        let config_mode = !use_example && string_input.trim().is_empty();
        if config_mode && output_file.is_some() {
            return Err(ShellError::Generic(GenericError::new(
                "Invalid flag combination",
                "Config changes can be applied on exit with Ctrl+S but cannot be saved to file at this time.",
                call.head,
            )));
        }

        // Determine the data source and mode
        // nu_type_map is used in config mode to track original nushell types
        // original_values is used to preserve values that can't be roundtripped (closures, dates, etc.)
        // doc_map is used in config mode to show documentation for config options
        let (json_data, nu_type_map, original_values, doc_map): ConfigDataResult = if use_example {
            // Use example data
            (get_example_json(), None, None, None)
        } else if !string_input.trim().is_empty() {
            // Use piped input data
            let data = serde_json::from_str(&string_input).map_err(|e| {
                ShellError::Generic(
                    GenericError::new(
                        "Could not parse JSON from input",
                        format!("JSON parse error: {e}"),
                        call.head,
                    )
                    .with_help("Make sure the input is valid JSON"),
                )
            })?;
            (data, None, None, None)
        } else {
            // Default: use nushell configuration
            // Get the raw $env.config Value directly to preserve key ordering
            // (using Config::into_value would lose order because HashMap iteration is unordered)
            let nu_value = stack
                .get_env_var(engine_state, "config")
                .cloned()
                .unwrap_or_else(|| {
                    // Fallback to Config struct if $env.config is not set
                    let config = stack.get_config(engine_state);
                    config.as_ref().clone().into_value(call.head)
                });
            let json_data = nu_value_to_json(engine_state, &nu_value, call.head)?;

            // Build nu_type_map to track original nushell types
            let mut nu_type_map = HashMap::new();
            build_nu_type_map(&nu_value, Vec::new(), &mut nu_type_map);

            // Build original_values map for types that can't be roundtripped (closures, dates, etc.)
            let mut original_values = HashMap::new();
            build_original_value_map(&nu_value, Vec::new(), &mut original_values);

            // Parse documentation from doc_config.nu
            let doc_map = parse_config_documentation();
            (
                json_data,
                Some(nu_type_map),
                Some(original_values),
                Some(doc_map),
            )
        };

        if cli_mode {
            // Original CLI behavior
            print_json_tree(&json_data, "", true, None);
        } else {
            // TUI mode - clone the type map and original values so we can use them after the TUI returns
            let type_map_for_conversion = nu_type_map.clone();
            let original_values_for_conversion = original_values.clone();

            let result = run_config_tui(
                json_data,
                output_file,
                config_mode,
                nu_type_map,
                doc_map,
                Arc::new(engine_state.clone()),
                Arc::new(stack.clone()),
            )?;

            // If in config mode and data was modified, apply changes to the config
            if config_mode && let Some(modified_json) = result {
                // Convert JSON back to nu_protocol::Value, using type map and original values
                // to preserve types like Duration, Filesize, and Closure
                let nu_value = json_to_nu_value_with_types(
                    &modified_json,
                    call.head,
                    &type_map_for_conversion,
                    &original_values_for_conversion,
                    Vec::new(),
                )
                .map_err(|e| {
                    ShellError::Generic(GenericError::new(
                        "Could not convert JSON to nu Value",
                        format!("conversion error: {e}"),
                        call.head,
                    ))
                })?;

                // Update $env.config with the new value
                stack.add_env_var("config".into(), nu_value.clone());

                // Update the internal Config struct directly, without calling update_config()
                // which would overwrite $env.config with Config::into_value() and lose key ordering
                // (because Config uses HashMap for color_config/explore/plugins fields)
                let old_config = stack.get_config(engine_state);
                let mut new_config = (*old_config).clone();
                let result = new_config.update_from_value(&old_config, &nu_value);
                // Store the updated Config struct directly on the stack
                stack.config = Some(Arc::new(new_config));
                if let Some(warning) = result? {
                    report_shell_warning(Some(stack), engine_state, &warning);
                }
            }
        }

        Ok(PipelineData::empty())
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![
            Example {
                description: "Open the nushell configuration in an interactive TUI editor",
                example: "explore config",
                result: None,
            },
            Example {
                description: "Explore JSON data interactively",
                example: "open --raw data.json | explore config",
                result: None,
            },
            Example {
                description: "Explore with example data to see TUI features",
                example: "explore config --use-example-data",
                result: None,
            },
        ]
    }
}