forked from extern/nushell
Refactor and fix Config
<->Value
mechanism (#10896)
# Description Our config exists both as a `Config` struct for internal consumption and as a `Value`. The latter is exposed through `$env.config` and can be both set and read. Thus we have a complex bug-prone mechanism, that reads a `Value` and then tries to plug anything where the value is unrepresentable in `Config` with the correct state from `Config`. The parsing involves therefore mutation of the `Value` in a nested `Record` structure. Previously this was wholy done manually, with indices. To enable deletion for example, things had to be iterated over from the back. Also things were indexed in a bunch of places. This was hard to read and an invitation for bugs. With #10876 we can now use `Record::retain_mut` to traverse the records, modify anything that needs fixing, and drop invalid fields. # Parts: - Error messages now consistently use the correct spans pointing to the problematic value and the paths displayed in some messages are also aligned with the keys used for lookup. - Reconstruction of values has been fixed for: - `table.padding` - `buffer_editor` - `hooks.command_not_found` - `datetime_format` (partial solution) - Fix validation of `table.padding` input so value is not set (and underflows `usize` causing `table` to run forever with negative values) - New proper types for settings. Fully validated enums instead of strings: - `config.edit_mode` -> `EditMode` - Don't fall back to vi-mode on invalid string - `config.table.mode` -> `TableMode` - there is still a fall back to `rounded` if given an invalid `TableMode` as argument to the `nu` binary - `config.completions.algorithm` -> `CompletionAlgorithm` - `config.error_style` -> `ErrorStyle` - don't implicitly fall back to `fancy` when given an invalid value. - This should also shrink the size of `Config` as instead of 4x24 bytes those fields now need only 4x1 bytes in `Config` - Completely removed macros relying on the scope of `Value::into_config` so we can break it up into smaller parts in the future. - Factored everything into smaller files with the types and helpers for particular topics. - `NuCursorShape` now explicitly expresses the `Inherit` setting. conversion to option only happens at the interface to `reedline`
This commit is contained in:
parent
edbf3aaccb
commit
86cd387439
@ -1,6 +1,6 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
||||||
DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion,
|
DotNuCompletion, FileCompletion, FlagCompletion, VariableCompletion,
|
||||||
};
|
};
|
||||||
use nu_engine::eval_block;
|
use nu_engine::eval_block;
|
||||||
use nu_parser::{flatten_expression, parse, FlatShape};
|
use nu_parser::{flatten_expression, parse, FlatShape};
|
||||||
@ -39,15 +39,12 @@ impl NuCompleter {
|
|||||||
) -> Vec<Suggestion> {
|
) -> Vec<Suggestion> {
|
||||||
let config = self.engine_state.get_config();
|
let config = self.engine_state.get_config();
|
||||||
|
|
||||||
let mut options = CompletionOptions {
|
let options = CompletionOptions {
|
||||||
case_sensitive: config.case_sensitive_completions,
|
case_sensitive: config.case_sensitive_completions,
|
||||||
|
match_algorithm: config.completion_algorithm.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
if config.completion_algorithm == "fuzzy" {
|
|
||||||
options.match_algorithm = MatchAlgorithm::Fuzzy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch
|
// Fetch
|
||||||
let mut suggestions =
|
let mut suggestions =
|
||||||
completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options);
|
completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options);
|
||||||
|
@ -2,6 +2,7 @@ use std::fmt::Display;
|
|||||||
|
|
||||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||||
use nu_parser::trim_quotes_str;
|
use nu_parser::trim_quotes_str;
|
||||||
|
use nu_protocol::CompletionAlgorithm;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum SortBy {
|
pub enum SortBy {
|
||||||
@ -55,6 +56,15 @@ impl MatchAlgorithm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||||
|
fn from(value: CompletionAlgorithm) -> Self {
|
||||||
|
match value {
|
||||||
|
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
||||||
|
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for MatchAlgorithm {
|
impl TryFrom<String> for MatchAlgorithm {
|
||||||
type Error = InvalidMatchAlgorithm;
|
type Error = InvalidMatchAlgorithm;
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ pub fn evaluate_commands(
|
|||||||
let (block, delta) = {
|
let (block, delta) = {
|
||||||
if let Some(ref t_mode) = table_mode {
|
if let Some(ref t_mode) = table_mode {
|
||||||
let mut config = engine_state.get_config().clone();
|
let mut config = engine_state.get_config().clone();
|
||||||
config.table_mode = t_mode.as_string()?;
|
config.table_mode = t_mode.as_string()?.parse().unwrap_or_default();
|
||||||
engine_state.set_config(&config);
|
engine_state.set_config(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut working_set = StateWorkingSet::new(engine_state);
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
@ -55,7 +55,7 @@ pub fn evaluate_commands(
|
|||||||
Ok(pipeline_data) => {
|
Ok(pipeline_data) => {
|
||||||
let mut config = engine_state.get_config().clone();
|
let mut config = engine_state.get_config().clone();
|
||||||
if let Some(t_mode) = table_mode {
|
if let Some(t_mode) = table_mode {
|
||||||
config.table_mode = t_mode.as_string()?;
|
config.table_mode = t_mode.as_string()?.parse().unwrap_or_default();
|
||||||
}
|
}
|
||||||
crate::eval_file::print_table_or_error(engine_state, stack, pipeline_data, &mut config)
|
crate::eval_file::print_table_or_error(engine_state, stack, pipeline_data, &mut config)
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ pub(crate) fn print_table_or_error(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Change the engine_state config to use the passed in configuration
|
// Change the engine_state config to use the passed in configuration
|
||||||
engine_state.set_config(config);
|
engine_state.set_config(config.clone());
|
||||||
|
|
||||||
if let PipelineData::Value(Value::Error { error, .. }, ..) = &pipeline_data {
|
if let PipelineData::Value(Value::Error { error, .. }, ..) = &pipeline_data {
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
|
@ -7,8 +7,8 @@ use nu_parser::parse;
|
|||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
create_menus,
|
create_menus,
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
extract_value, Config, ParsedKeybinding, ParsedMenu, PipelineData, Record, ShellError, Span,
|
extract_value, Config, EditBindings, ParsedKeybinding, ParsedMenu, PipelineData, Record,
|
||||||
Value,
|
ShellError, Span, Value,
|
||||||
};
|
};
|
||||||
use reedline::{
|
use reedline::{
|
||||||
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
||||||
@ -537,11 +537,11 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
|||||||
let mut insert_keybindings = default_vi_insert_keybindings();
|
let mut insert_keybindings = default_vi_insert_keybindings();
|
||||||
let mut normal_keybindings = default_vi_normal_keybindings();
|
let mut normal_keybindings = default_vi_normal_keybindings();
|
||||||
|
|
||||||
match config.edit_mode.as_str() {
|
match config.edit_mode {
|
||||||
"emacs" => {
|
EditBindings::Emacs => {
|
||||||
add_menu_keybindings(&mut emacs_keybindings);
|
add_menu_keybindings(&mut emacs_keybindings);
|
||||||
}
|
}
|
||||||
_ => {
|
EditBindings::Vi => {
|
||||||
add_menu_keybindings(&mut insert_keybindings);
|
add_menu_keybindings(&mut insert_keybindings);
|
||||||
add_menu_keybindings(&mut normal_keybindings);
|
add_menu_keybindings(&mut normal_keybindings);
|
||||||
}
|
}
|
||||||
@ -557,9 +557,9 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
|||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
|
|
||||||
match config.edit_mode.as_str() {
|
match config.edit_mode {
|
||||||
"emacs" => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
|
EditBindings::Emacs => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
|
||||||
_ => Ok(KeybindingsMode::Vi {
|
EditBindings::Vi => Ok(KeybindingsMode::Vi {
|
||||||
insert_keybindings,
|
insert_keybindings,
|
||||||
normal_keybindings,
|
normal_keybindings,
|
||||||
}),
|
}),
|
||||||
|
@ -245,15 +245,9 @@ pub fn evaluate_repl(
|
|||||||
|
|
||||||
// Find the configured cursor shapes for each mode
|
// Find the configured cursor shapes for each mode
|
||||||
let cursor_config = CursorConfig {
|
let cursor_config = CursorConfig {
|
||||||
vi_insert: config
|
vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_insert),
|
||||||
.cursor_shape_vi_insert
|
vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal),
|
||||||
.map(map_nucursorshape_to_cursorshape),
|
emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs),
|
||||||
vi_normal: config
|
|
||||||
.cursor_shape_vi_normal
|
|
||||||
.map(map_nucursorshape_to_cursorshape),
|
|
||||||
emacs: config
|
|
||||||
.cursor_shape_emacs
|
|
||||||
.map(map_nucursorshape_to_cursorshape),
|
|
||||||
};
|
};
|
||||||
perf(
|
perf(
|
||||||
"get config/cursor config",
|
"get config/cursor config",
|
||||||
@ -770,14 +764,15 @@ fn update_line_editor_history(
|
|||||||
Ok(line_editor)
|
Ok(line_editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> SetCursorStyle {
|
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
|
||||||
match shape {
|
match shape {
|
||||||
NuCursorShape::Block => SetCursorStyle::SteadyBlock,
|
NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
|
||||||
NuCursorShape::UnderScore => SetCursorStyle::SteadyUnderScore,
|
NuCursorShape::UnderScore => Some(SetCursorStyle::SteadyUnderScore),
|
||||||
NuCursorShape::Line => SetCursorStyle::SteadyBar,
|
NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
|
||||||
NuCursorShape::BlinkBlock => SetCursorStyle::BlinkingBlock,
|
NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
|
||||||
NuCursorShape::BlinkUnderScore => SetCursorStyle::BlinkingUnderScore,
|
NuCursorShape::BlinkUnderScore => Some(SetCursorStyle::BlinkingUnderScore),
|
||||||
NuCursorShape::BlinkLine => SetCursorStyle::BlinkingBar,
|
NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
|
||||||
|
NuCursorShape::Inherit => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,7 +795,7 @@ fn run_external_completion(block: &str, input: &str) -> Vec<Suggestion> {
|
|||||||
// Change config adding the external completer
|
// Change config adding the external completer
|
||||||
let mut config = engine_state.get_config().clone();
|
let mut config = engine_state.get_config().clone();
|
||||||
config.external_completer = Some(latest_block_id);
|
config.external_completer = Some(latest_block_id);
|
||||||
engine_state.set_config(&config);
|
engine_state.set_config(config);
|
||||||
|
|
||||||
// Instantiate a new completer
|
// Instantiate a new completer
|
||||||
let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack);
|
let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use crate::engine::{EngineState, StateWorkingSet};
|
use crate::{
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
ErrorStyle,
|
||||||
|
};
|
||||||
use miette::{
|
use miette::{
|
||||||
LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
|
LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
|
||||||
SourceCode,
|
SourceCode,
|
||||||
@ -49,12 +52,11 @@ impl std::fmt::Debug for CliError<'_> {
|
|||||||
let ansi_support = &config.use_ansi_coloring;
|
let ansi_support = &config.use_ansi_coloring;
|
||||||
let ansi_support = *ansi_support;
|
let ansi_support = *ansi_support;
|
||||||
|
|
||||||
let error_style = &config.error_style.as_str();
|
let error_style = &config.error_style;
|
||||||
let errors_style = *error_style;
|
|
||||||
|
|
||||||
let miette_handler: Box<dyn ReportHandler> = match errors_style {
|
let miette_handler: Box<dyn ReportHandler> = match error_style {
|
||||||
"plain" => Box::new(NarratableReportHandler::new()),
|
ErrorStyle::Plain => Box::new(NarratableReportHandler::new()),
|
||||||
_ => Box::new(
|
ErrorStyle::Fancy => Box::new(
|
||||||
MietteHandlerOpts::new()
|
MietteHandlerOpts::new()
|
||||||
// For better support of terminal themes use the ANSI coloring
|
// For better support of terminal themes use the ANSI coloring
|
||||||
.rgb_colors(RgbColors::Never)
|
.rgb_colors(RgbColors::Never)
|
||||||
|
File diff suppressed because it is too large
Load Diff
55
crates/nu-protocol/src/config/completer.rs
Normal file
55
crates/nu-protocol/src/config/completer.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{record, Config, Span, Value};
|
||||||
|
|
||||||
|
use super::helper::ReconstructVal;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)]
|
||||||
|
pub enum CompletionAlgorithm {
|
||||||
|
#[default]
|
||||||
|
Prefix,
|
||||||
|
Fuzzy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CompletionAlgorithm {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"prefix" => Ok(Self::Prefix),
|
||||||
|
"fuzzy" => Ok(Self::Fuzzy),
|
||||||
|
_ => Err("expected either 'prefix' or 'fuzzy'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for CompletionAlgorithm {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
let str = match self {
|
||||||
|
CompletionAlgorithm::Prefix => "prefix",
|
||||||
|
CompletionAlgorithm::Fuzzy => "fuzzy",
|
||||||
|
};
|
||||||
|
Value::string(str, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_external_completer(config: &Config, span: Span) -> Value {
|
||||||
|
if let Some(block) = config.external_completer {
|
||||||
|
Value::block(block, span)
|
||||||
|
} else {
|
||||||
|
Value::nothing(span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_external(config: &Config, span: Span) -> Value {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"max_results" => Value::int(config.max_external_completion_results, span),
|
||||||
|
"completer" => reconstruct_external_completer(config, span),
|
||||||
|
"enable" => Value::bool(config.enable_external_completion, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
133
crates/nu-protocol/src/config/helper.rs
Normal file
133
crates/nu-protocol/src/config/helper.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
use crate::{Record, ShellError, Span, Value};
|
||||||
|
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
pub(super) trait ReconstructVal {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn process_string_enum<T, E>(
|
||||||
|
config_point: &mut T,
|
||||||
|
config_path: &[&str],
|
||||||
|
value: &mut Value,
|
||||||
|
errors: &mut Vec<ShellError>,
|
||||||
|
) where
|
||||||
|
T: FromStr<Err = E> + ReconstructVal,
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
let span = value.span();
|
||||||
|
if let Ok(v) = value.as_string() {
|
||||||
|
match v.parse() {
|
||||||
|
Ok(format) => {
|
||||||
|
*config_point = format;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
format!(
|
||||||
|
"unrecognized $env.config.{} option '{v}'",
|
||||||
|
config_path.join(".")
|
||||||
|
),
|
||||||
|
Some(span),
|
||||||
|
Some(err.to_string()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
// Reconstruct
|
||||||
|
*value = config_point.reconstruct_value(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
format!("$env.config.{} should be a string", config_path.join(".")),
|
||||||
|
Some(span),
|
||||||
|
Some("This value will be ignored.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
// Reconstruct
|
||||||
|
*value = config_point.reconstruct_value(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn process_bool_config(
|
||||||
|
value: &mut Value,
|
||||||
|
errors: &mut Vec<ShellError>,
|
||||||
|
config_point: &mut bool,
|
||||||
|
) {
|
||||||
|
if let Ok(b) = value.as_bool() {
|
||||||
|
*config_point = b;
|
||||||
|
} else {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"should be a bool".to_string(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("This value will be ignored.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::bool(*config_point, value.span());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn process_int_config(
|
||||||
|
value: &mut Value,
|
||||||
|
errors: &mut Vec<ShellError>,
|
||||||
|
config_point: &mut i64,
|
||||||
|
) {
|
||||||
|
if let Ok(b) = value.as_int() {
|
||||||
|
*config_point = b;
|
||||||
|
} else {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"should be an int".to_string(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("This value will be ignored.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::int(*config_point, value.span());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn report_invalid_key(keys: &[&str], span: Span, errors: &mut Vec<ShellError>) {
|
||||||
|
// Because Value::Record discards all of the spans of its
|
||||||
|
// column names (by storing them as Strings), the key name cannot be provided
|
||||||
|
// as a value, even in key errors.
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
format!(
|
||||||
|
"$env.config.{} is an unknown config setting",
|
||||||
|
keys.join(".")
|
||||||
|
),
|
||||||
|
Some(span),
|
||||||
|
Some("This value will not appear in your $env.config record.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn report_invalid_value(msg: &str, span: Span, errors: &mut Vec<ShellError>) {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
msg.into(),
|
||||||
|
Some(span),
|
||||||
|
Some("This value will be ignored.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_map(value: &Value) -> Result<HashMap<String, Value>, ShellError> {
|
||||||
|
Ok(value
|
||||||
|
.as_record()?
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_value<'record>(
|
||||||
|
name: &str,
|
||||||
|
record: &'record Record,
|
||||||
|
span: Span,
|
||||||
|
) -> Result<&'record Value, ShellError> {
|
||||||
|
record
|
||||||
|
.get(name)
|
||||||
|
.ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), span))
|
||||||
|
}
|
88
crates/nu-protocol/src/config/hooks.rs
Normal file
88
crates/nu-protocol/src/config/hooks.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use crate::{Config, Record, ShellError, Span, Value};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
/// Definition of a parsed hook from the config object
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Hooks {
|
||||||
|
pub pre_prompt: Option<Value>,
|
||||||
|
pub pre_execution: Option<Value>,
|
||||||
|
pub env_change: Option<Value>,
|
||||||
|
pub display_output: Option<Value>,
|
||||||
|
pub command_not_found: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hooks {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pre_prompt: None,
|
||||||
|
pre_execution: None,
|
||||||
|
env_change: None,
|
||||||
|
display_output: Some(Value::string(
|
||||||
|
"if (term size).columns >= 100 { table -e } else { table }",
|
||||||
|
Span::unknown(),
|
||||||
|
)),
|
||||||
|
command_not_found: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Hooks {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the hooks to find the blocks to run when the hooks fire
|
||||||
|
pub(super) fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
|
||||||
|
let span = value.span();
|
||||||
|
match value {
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
let mut hooks = Hooks::new();
|
||||||
|
|
||||||
|
for (col, val) in val {
|
||||||
|
match col.as_str() {
|
||||||
|
"pre_prompt" => hooks.pre_prompt = Some(val.clone()),
|
||||||
|
"pre_execution" => hooks.pre_execution = Some(val.clone()),
|
||||||
|
"env_change" => hooks.env_change = Some(val.clone()),
|
||||||
|
"display_output" => hooks.display_output = Some(val.clone()),
|
||||||
|
"command_not_found" => hooks.command_not_found = Some(val.clone()),
|
||||||
|
x => {
|
||||||
|
return Err(ShellError::UnsupportedConfigValue(
|
||||||
|
"'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'"
|
||||||
|
.to_string(),
|
||||||
|
x.to_string(),
|
||||||
|
span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hooks)
|
||||||
|
}
|
||||||
|
_ => Err(ShellError::UnsupportedConfigValue(
|
||||||
|
"record for 'hooks' config".into(),
|
||||||
|
"non-record value".into(),
|
||||||
|
span,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_hooks(config: &Config, span: Span) -> Value {
|
||||||
|
let mut hook = Record::new();
|
||||||
|
if let Some(ref value) = config.hooks.pre_prompt {
|
||||||
|
hook.push("pre_prompt", value.clone());
|
||||||
|
}
|
||||||
|
if let Some(ref value) = config.hooks.pre_execution {
|
||||||
|
hook.push("pre_execution", value.clone());
|
||||||
|
}
|
||||||
|
if let Some(ref value) = config.hooks.env_change {
|
||||||
|
hook.push("env_change", value.clone());
|
||||||
|
}
|
||||||
|
if let Some(ref value) = config.hooks.display_output {
|
||||||
|
hook.push("display_output", value.clone());
|
||||||
|
}
|
||||||
|
if let Some(ref value) = config.hooks.command_not_found {
|
||||||
|
hook.push("command_not_found", value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::record(hook, span)
|
||||||
|
}
|
730
crates/nu-protocol/src/config/mod.rs
Normal file
730
crates/nu-protocol/src/config/mod.rs
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
use self::completer::*;
|
||||||
|
use self::helper::*;
|
||||||
|
use self::hooks::*;
|
||||||
|
use self::output::*;
|
||||||
|
use self::reedline::*;
|
||||||
|
use self::table::*;
|
||||||
|
|
||||||
|
use crate::{record, ShellError, Span, Value};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub use self::completer::CompletionAlgorithm;
|
||||||
|
pub use self::helper::extract_value;
|
||||||
|
pub use self::hooks::Hooks;
|
||||||
|
pub use self::output::ErrorStyle;
|
||||||
|
pub use self::reedline::{
|
||||||
|
create_menus, EditBindings, HistoryFileFormat, NuCursorShape, ParsedKeybinding, ParsedMenu,
|
||||||
|
};
|
||||||
|
pub use self::table::{FooterMode, TableIndexMode, TableMode, TrimStrategy};
|
||||||
|
|
||||||
|
mod completer;
|
||||||
|
mod helper;
|
||||||
|
mod hooks;
|
||||||
|
mod output;
|
||||||
|
mod reedline;
|
||||||
|
mod table;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub external_completer: Option<usize>,
|
||||||
|
pub filesize_metric: bool,
|
||||||
|
pub table_mode: TableMode,
|
||||||
|
pub table_move_header: bool,
|
||||||
|
pub table_show_empty: bool,
|
||||||
|
pub table_indent: TableIndent,
|
||||||
|
pub table_abbreviation_threshold: Option<usize>,
|
||||||
|
pub use_ls_colors: bool,
|
||||||
|
pub color_config: HashMap<String, Value>,
|
||||||
|
pub use_grid_icons: bool,
|
||||||
|
pub footer_mode: FooterMode,
|
||||||
|
pub float_precision: i64,
|
||||||
|
pub max_external_completion_results: i64,
|
||||||
|
pub filesize_format: String,
|
||||||
|
pub use_ansi_coloring: bool,
|
||||||
|
pub quick_completions: bool,
|
||||||
|
pub partial_completions: bool,
|
||||||
|
pub completion_algorithm: CompletionAlgorithm,
|
||||||
|
pub edit_mode: EditBindings,
|
||||||
|
pub max_history_size: i64,
|
||||||
|
pub sync_history_on_enter: bool,
|
||||||
|
pub history_file_format: HistoryFileFormat,
|
||||||
|
pub history_isolation: bool,
|
||||||
|
pub keybindings: Vec<ParsedKeybinding>,
|
||||||
|
pub menus: Vec<ParsedMenu>,
|
||||||
|
pub hooks: Hooks,
|
||||||
|
pub rm_always_trash: bool,
|
||||||
|
pub shell_integration: bool,
|
||||||
|
pub buffer_editor: Value,
|
||||||
|
pub table_index_mode: TableIndexMode,
|
||||||
|
pub case_sensitive_completions: bool,
|
||||||
|
pub enable_external_completion: bool,
|
||||||
|
pub trim_strategy: TrimStrategy,
|
||||||
|
pub show_banner: bool,
|
||||||
|
pub bracketed_paste: bool,
|
||||||
|
pub show_clickable_links_in_ls: bool,
|
||||||
|
pub render_right_prompt_on_last_line: bool,
|
||||||
|
pub explore: HashMap<String, Value>,
|
||||||
|
pub cursor_shape_vi_insert: NuCursorShape,
|
||||||
|
pub cursor_shape_vi_normal: NuCursorShape,
|
||||||
|
pub cursor_shape_emacs: NuCursorShape,
|
||||||
|
pub datetime_normal_format: Option<String>,
|
||||||
|
pub datetime_table_format: Option<String>,
|
||||||
|
pub error_style: ErrorStyle,
|
||||||
|
pub use_kitty_protocol: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
show_banner: true,
|
||||||
|
|
||||||
|
use_ls_colors: true,
|
||||||
|
show_clickable_links_in_ls: true,
|
||||||
|
|
||||||
|
rm_always_trash: false,
|
||||||
|
|
||||||
|
table_mode: TableMode::Rounded,
|
||||||
|
table_index_mode: TableIndexMode::Always,
|
||||||
|
table_show_empty: true,
|
||||||
|
trim_strategy: TrimStrategy::default(),
|
||||||
|
table_move_header: false,
|
||||||
|
table_indent: TableIndent { left: 1, right: 1 },
|
||||||
|
table_abbreviation_threshold: None,
|
||||||
|
|
||||||
|
datetime_normal_format: None,
|
||||||
|
datetime_table_format: None,
|
||||||
|
|
||||||
|
explore: HashMap::new(),
|
||||||
|
|
||||||
|
max_history_size: 100_000,
|
||||||
|
sync_history_on_enter: true,
|
||||||
|
history_file_format: HistoryFileFormat::PlainText,
|
||||||
|
history_isolation: false,
|
||||||
|
|
||||||
|
case_sensitive_completions: false,
|
||||||
|
quick_completions: true,
|
||||||
|
partial_completions: true,
|
||||||
|
completion_algorithm: CompletionAlgorithm::default(),
|
||||||
|
enable_external_completion: true,
|
||||||
|
max_external_completion_results: 100,
|
||||||
|
external_completer: None,
|
||||||
|
|
||||||
|
filesize_metric: false,
|
||||||
|
filesize_format: "auto".into(),
|
||||||
|
|
||||||
|
cursor_shape_emacs: NuCursorShape::default(),
|
||||||
|
cursor_shape_vi_insert: NuCursorShape::default(),
|
||||||
|
cursor_shape_vi_normal: NuCursorShape::default(),
|
||||||
|
|
||||||
|
color_config: HashMap::new(),
|
||||||
|
use_grid_icons: true,
|
||||||
|
footer_mode: FooterMode::RowCount(25),
|
||||||
|
float_precision: 2,
|
||||||
|
buffer_editor: Value::nothing(Span::unknown()),
|
||||||
|
use_ansi_coloring: true,
|
||||||
|
bracketed_paste: true,
|
||||||
|
edit_mode: EditBindings::default(),
|
||||||
|
shell_integration: false,
|
||||||
|
render_right_prompt_on_last_line: false,
|
||||||
|
|
||||||
|
hooks: Hooks::new(),
|
||||||
|
|
||||||
|
menus: Vec::new(),
|
||||||
|
|
||||||
|
keybindings: Vec::new(),
|
||||||
|
|
||||||
|
error_style: ErrorStyle::Fancy,
|
||||||
|
|
||||||
|
use_kitty_protocol: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) {
|
||||||
|
// Clone the passed-in config rather than mutating it.
|
||||||
|
let mut config = config.clone();
|
||||||
|
|
||||||
|
// Vec for storing errors. Current Nushell behaviour (Dec 2022) is that having some typo
|
||||||
|
// like `"always_trash": tru` in your config.nu's `$env.config` record shouldn't abort all
|
||||||
|
// config parsing there and then. Thus, errors are simply collected one-by-one and wrapped
|
||||||
|
// in a GenericError at the end.
|
||||||
|
let mut errors = vec![];
|
||||||
|
|
||||||
|
// Config record (self) mutation rules:
|
||||||
|
// * When parsing a config Record, if a config key error occurs, remove the key.
|
||||||
|
// * When parsing a config Record, if a config value error occurs, replace the value
|
||||||
|
// with a reconstructed Nu value for the current (unaltered) configuration for that setting.
|
||||||
|
// For instance:
|
||||||
|
// `$env.config.ls.use_ls_colors = 2` results in an error, so the current `use_ls_colors`
|
||||||
|
// config setting is converted to a `Value::Boolean` and inserted in the record in place of
|
||||||
|
// the `2`.
|
||||||
|
|
||||||
|
if let Value::Record { val, .. } = self {
|
||||||
|
val.retain_mut( |key, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key {
|
||||||
|
// Grouped options
|
||||||
|
"ls" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"use_ls_colors" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.use_ls_colors);
|
||||||
|
}
|
||||||
|
"clickable_links" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.show_clickable_links_in_ls);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}; true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"use_ls_colors" => Value::bool(config.use_ls_colors, span),
|
||||||
|
"clickable_links" => Value::bool(config.show_clickable_links_in_ls, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"rm" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"always_trash" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.rm_always_trash);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"always_trash" => Value::bool(config.rm_always_trash, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"history" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"isolation" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.history_isolation);
|
||||||
|
}
|
||||||
|
"sync_on_enter" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.sync_history_on_enter);
|
||||||
|
}
|
||||||
|
"max_size" => {
|
||||||
|
process_int_config(value, &mut errors, &mut config.max_history_size);
|
||||||
|
}
|
||||||
|
"file_format" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.history_file_format,
|
||||||
|
&[key, key2],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"sync_on_enter" => Value::bool(config.sync_history_on_enter, span),
|
||||||
|
"max_size" => Value::int(config.max_history_size, span),
|
||||||
|
"file_format" => config.history_file_format.reconstruct_value(span),
|
||||||
|
"isolation" => Value::bool(config.history_isolation, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"completions" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"quick" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.quick_completions);
|
||||||
|
}
|
||||||
|
"partial" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.partial_completions);
|
||||||
|
}
|
||||||
|
"algorithm" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.completion_algorithm,
|
||||||
|
&[key, key2],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
"case_sensitive" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.case_sensitive_completions);
|
||||||
|
}
|
||||||
|
"external" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key3, value|
|
||||||
|
{
|
||||||
|
let span = value.span();
|
||||||
|
match key3 {
|
||||||
|
"max_results" => {
|
||||||
|
process_int_config(value, &mut errors, &mut config.max_external_completion_results);
|
||||||
|
}
|
||||||
|
"completer" => {
|
||||||
|
if let Ok(v) = value.as_block() {
|
||||||
|
config.external_completer = Some(v)
|
||||||
|
} else {
|
||||||
|
match value {
|
||||||
|
Value::Nothing { .. } => {}
|
||||||
|
_ => {
|
||||||
|
report_invalid_value("should be a block or null", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = reconstruct_external_completer(&config,
|
||||||
|
span
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"enable" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.enable_external_completion);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2, key3], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = reconstruct_external(&config, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct record
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"quick" => Value::bool(config.quick_completions, span),
|
||||||
|
"partial" => Value::bool(config.partial_completions, span),
|
||||||
|
"algorithm" => config.completion_algorithm.reconstruct_value(span),
|
||||||
|
"case_sensitive" => Value::bool(config.case_sensitive_completions, span),
|
||||||
|
"external" => reconstruct_external(&config, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"cursor_shape" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
let config_point = match key2 {
|
||||||
|
"vi_insert" => &mut config.cursor_shape_vi_insert,
|
||||||
|
"vi_normal" => &mut config.cursor_shape_vi_normal,
|
||||||
|
"emacs" => &mut config.cursor_shape_emacs,
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process_string_enum(
|
||||||
|
config_point,
|
||||||
|
&[key, key2],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"vi_insert" => config.cursor_shape_vi_insert.reconstruct_value(span),
|
||||||
|
"vi_normal" => config.cursor_shape_vi_normal.reconstruct_value(span),
|
||||||
|
"emacs" => config.cursor_shape_emacs.reconstruct_value(span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"table" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"mode" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.table_mode,
|
||||||
|
&[key, key2],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
"header_on_separator" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.table_move_header);
|
||||||
|
}
|
||||||
|
"padding" => match value {
|
||||||
|
Value::Int { val, .. } => {
|
||||||
|
if *val < 0 {
|
||||||
|
report_invalid_value("expected a unsigned integer", span, &mut errors);
|
||||||
|
*value = reconstruct_padding(&config, span);
|
||||||
|
} else {
|
||||||
|
config.table_indent.left = *val as usize;
|
||||||
|
config.table_indent.right = *val as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
let mut invalid = false;
|
||||||
|
val.retain(|key3, value| {
|
||||||
|
match key3 {
|
||||||
|
"left" => {
|
||||||
|
match value.as_int() {
|
||||||
|
Ok(val) if val >= 0 => {
|
||||||
|
config.table_indent.left = val as usize;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_value("expected a unsigned integer >= 0", span, &mut errors);
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"right" => {
|
||||||
|
match value.as_int() {
|
||||||
|
Ok(val) if val >= 0 => {
|
||||||
|
config.table_indent.right = val as usize;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_value("expected a unsigned integer >= 0", span, &mut errors);
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2, key3], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
if invalid {
|
||||||
|
*value = reconstruct_padding(&config, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_value("expected a unsigned integer or a record", span, &mut errors);
|
||||||
|
*value = reconstruct_padding(&config, span);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"index_mode" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.table_index_mode,
|
||||||
|
&[key, key2],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
"trim" => {
|
||||||
|
match try_parse_trim_strategy(value, &mut errors) {
|
||||||
|
Ok(v) => config.trim_strategy = v,
|
||||||
|
Err(e) => {
|
||||||
|
// try_parse_trim_strategy() already adds its own errors
|
||||||
|
errors.push(e);
|
||||||
|
*value =
|
||||||
|
reconstruct_trim_strategy(&config, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"show_empty" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.table_show_empty);
|
||||||
|
}
|
||||||
|
"abbreviated_row_count" => {
|
||||||
|
if let Ok(b) = value.as_int() {
|
||||||
|
if b < 0 {
|
||||||
|
report_invalid_value("should be an int unsigned", span, &mut errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.table_abbreviation_threshold = Some(b as usize);
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be an int", span, &mut errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"mode" => config.table_mode.reconstruct_value(span),
|
||||||
|
"index_mode" => config.table_index_mode.reconstruct_value(span),
|
||||||
|
"trim" => reconstruct_trim_strategy(&config, span),
|
||||||
|
"show_empty" => Value::bool(config.table_show_empty, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"filesize" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value| {
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"metric" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.filesize_metric);
|
||||||
|
}
|
||||||
|
"format" => {
|
||||||
|
if let Ok(v) = value.as_string() {
|
||||||
|
config.filesize_format = v.to_lowercase();
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a string", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value =
|
||||||
|
Value::string(config.filesize_format.clone(), span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
record! {
|
||||||
|
"metric" => Value::bool(config.filesize_metric, span),
|
||||||
|
"format" => Value::string(config.filesize_format.clone(), span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"explore" => {
|
||||||
|
if let Ok(map) = create_map(value) {
|
||||||
|
config.explore = map;
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
config
|
||||||
|
.explore
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Misc. options
|
||||||
|
"color_config" => {
|
||||||
|
if let Ok(map) = create_map(value) {
|
||||||
|
config.color_config = map;
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = Value::record(
|
||||||
|
config
|
||||||
|
.color_config
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"use_grid_icons" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.use_grid_icons);
|
||||||
|
}
|
||||||
|
"footer_mode" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.footer_mode,
|
||||||
|
&[key],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
"float_precision" => {
|
||||||
|
process_int_config(value, &mut errors, &mut config.float_precision);
|
||||||
|
}
|
||||||
|
"use_ansi_coloring" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.use_ansi_coloring);
|
||||||
|
}
|
||||||
|
"edit_mode" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.edit_mode,
|
||||||
|
&[key],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
"shell_integration" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.shell_integration);
|
||||||
|
}
|
||||||
|
"buffer_editor" => match value {
|
||||||
|
Value::Nothing { .. } | Value::String { .. } => {
|
||||||
|
config.buffer_editor = value.clone();
|
||||||
|
}
|
||||||
|
Value::List { vals, .. }
|
||||||
|
if vals.iter().all(|val| matches!(val, Value::String { .. })) =>
|
||||||
|
{
|
||||||
|
config.buffer_editor = value.clone();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_value("should be a string, list<string>, or null", span, &mut errors);
|
||||||
|
*value = config.buffer_editor.clone();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"show_banner" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.show_banner);
|
||||||
|
}
|
||||||
|
"render_right_prompt_on_last_line" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.render_right_prompt_on_last_line);
|
||||||
|
}
|
||||||
|
"bracketed_paste" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.bracketed_paste);
|
||||||
|
}
|
||||||
|
"use_kitty_protocol" => {
|
||||||
|
process_bool_config(value, &mut errors, &mut config.use_kitty_protocol);
|
||||||
|
}
|
||||||
|
// Menus
|
||||||
|
"menus" => match create_menus(value) {
|
||||||
|
Ok(map) => config.menus = map,
|
||||||
|
Err(e) => {
|
||||||
|
report_invalid_value("should be a valid list of menus", span, &mut errors);
|
||||||
|
errors.push(e);
|
||||||
|
// Reconstruct
|
||||||
|
*value = reconstruct_menus(&config, span);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Keybindings
|
||||||
|
"keybindings" => match create_keybindings(value) {
|
||||||
|
Ok(keybindings) => config.keybindings = keybindings,
|
||||||
|
Err(e) => {
|
||||||
|
report_invalid_value("should be a valid keybindings list", span, &mut errors);
|
||||||
|
errors.push(e);
|
||||||
|
// Reconstruct
|
||||||
|
*value = reconstruct_keybindings(&config, span);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Hooks
|
||||||
|
"hooks" => match create_hooks(value) {
|
||||||
|
Ok(hooks) => config.hooks = hooks,
|
||||||
|
Err(e) => {
|
||||||
|
report_invalid_value("should be a valid hooks list", span, &mut errors);
|
||||||
|
errors.push(e);
|
||||||
|
*value = reconstruct_hooks(&config, span);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"datetime_format" => {
|
||||||
|
if let Value::Record { val, .. } = value {
|
||||||
|
val.retain_mut(|key2, value|
|
||||||
|
{
|
||||||
|
let span = value.span();
|
||||||
|
match key2 {
|
||||||
|
"normal" => {
|
||||||
|
if let Ok(v) = value.as_string() {
|
||||||
|
config.datetime_normal_format = Some(v);
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a string", span, &mut errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"table" => {
|
||||||
|
if let Ok(v) = value.as_string() {
|
||||||
|
config.datetime_table_format = Some(v);
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a string", span, &mut errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key, key2], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}; true})
|
||||||
|
} else {
|
||||||
|
report_invalid_value("should be a record", span, &mut errors);
|
||||||
|
// Reconstruct
|
||||||
|
*value = reconstruct_datetime_format(&config, span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"error_style" => {
|
||||||
|
process_string_enum(
|
||||||
|
&mut config.error_style,
|
||||||
|
&[key],
|
||||||
|
value,
|
||||||
|
&mut errors);
|
||||||
|
}
|
||||||
|
// Catch all
|
||||||
|
_ => {
|
||||||
|
report_invalid_key(&[key], span, &mut errors);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
config,
|
||||||
|
Some(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"$env.config is not a record".into(),
|
||||||
|
Some(self.span()),
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the config and the vec of errors.
|
||||||
|
(
|
||||||
|
config,
|
||||||
|
if !errors.is_empty() {
|
||||||
|
Some(ShellError::GenericError(
|
||||||
|
"Config record contains invalid values or unknown settings".into(),
|
||||||
|
// Without a span, this second string is ignored.
|
||||||
|
"".into(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
errors,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
45
crates/nu-protocol/src/config/output.rs
Normal file
45
crates/nu-protocol/src/config/output.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use super::helper::ReconstructVal;
|
||||||
|
use crate::{Config, Record, Span, Value};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
|
||||||
|
pub enum ErrorStyle {
|
||||||
|
Plain,
|
||||||
|
Fancy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ErrorStyle {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"fancy" => Ok(Self::Fancy),
|
||||||
|
"plain" => Ok(Self::Plain),
|
||||||
|
_ => Err("expected either 'fancy' or 'plain'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for ErrorStyle {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
ErrorStyle::Fancy => "fancy",
|
||||||
|
ErrorStyle::Plain => "plain",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_datetime_format(config: &Config, span: Span) -> Value {
|
||||||
|
let mut record = Record::new();
|
||||||
|
if let Some(normal) = &config.datetime_normal_format {
|
||||||
|
record.push("normal", Value::string(normal, span));
|
||||||
|
}
|
||||||
|
if let Some(table) = &config.datetime_table_format {
|
||||||
|
record.push("table", Value::string(table, span));
|
||||||
|
}
|
||||||
|
Value::record(record, span)
|
||||||
|
}
|
277
crates/nu-protocol/src/config/reedline.rs
Normal file
277
crates/nu-protocol/src/config/reedline.rs
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use super::{extract_value, helper::ReconstructVal};
|
||||||
|
use crate::{record, Config, ShellError, Span, Value};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Definition of a parsed keybinding from the config object
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ParsedKeybinding {
|
||||||
|
pub modifier: Value,
|
||||||
|
pub keycode: Value,
|
||||||
|
pub event: Value,
|
||||||
|
pub mode: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Definition of a parsed menu from the config object
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ParsedMenu {
|
||||||
|
pub name: Value,
|
||||||
|
pub marker: Value,
|
||||||
|
pub only_buffer_difference: Value,
|
||||||
|
pub style: Value,
|
||||||
|
pub menu_type: Value,
|
||||||
|
pub source: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape)
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Copy, Default)]
|
||||||
|
pub enum NuCursorShape {
|
||||||
|
UnderScore,
|
||||||
|
Line,
|
||||||
|
Block,
|
||||||
|
BlinkUnderScore,
|
||||||
|
BlinkLine,
|
||||||
|
BlinkBlock,
|
||||||
|
#[default]
|
||||||
|
Inherit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NuCursorShape {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<NuCursorShape, &'static str> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"line" => Ok(NuCursorShape::Line),
|
||||||
|
"block" => Ok(NuCursorShape::Block),
|
||||||
|
"underscore" => Ok(NuCursorShape::UnderScore),
|
||||||
|
"blink_line" => Ok(NuCursorShape::BlinkLine),
|
||||||
|
"blink_block" => Ok(NuCursorShape::BlinkBlock),
|
||||||
|
"blink_underscore" => Ok(NuCursorShape::BlinkUnderScore),
|
||||||
|
"inherit" => Ok(NuCursorShape::Inherit),
|
||||||
|
_ => Err("expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for NuCursorShape {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
NuCursorShape::Line => "line",
|
||||||
|
NuCursorShape::Block => "block",
|
||||||
|
NuCursorShape::UnderScore => "underscore",
|
||||||
|
NuCursorShape::BlinkLine => "blink_line",
|
||||||
|
NuCursorShape::BlinkBlock => "blink_block",
|
||||||
|
NuCursorShape::BlinkUnderScore => "blink_underscore",
|
||||||
|
NuCursorShape::Inherit => "inherit",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
|
||||||
|
pub enum HistoryFileFormat {
|
||||||
|
/// Store history as an SQLite database with additional context
|
||||||
|
Sqlite,
|
||||||
|
/// store history as a plain text file where every line is one command (without any context such as timestamps)
|
||||||
|
PlainText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for HistoryFileFormat {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"sqlite" => Ok(Self::Sqlite),
|
||||||
|
"plaintext" => Ok(Self::PlainText),
|
||||||
|
_ => Err("expected either 'sqlite' or 'plaintext'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for HistoryFileFormat {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
HistoryFileFormat::Sqlite => "sqlite",
|
||||||
|
HistoryFileFormat::PlainText => "plaintext",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, Copy)]
|
||||||
|
pub enum EditBindings {
|
||||||
|
Vi,
|
||||||
|
#[default]
|
||||||
|
Emacs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for EditBindings {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"vi" => Ok(Self::Vi),
|
||||||
|
"emacs" => Ok(Self::Emacs),
|
||||||
|
_ => Err("expected either 'emacs' or 'vi'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for EditBindings {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
EditBindings::Vi => "vi",
|
||||||
|
EditBindings::Emacs => "emacs",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the config object to extract the strings that will compose a keybinding for reedline
|
||||||
|
pub(super) fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError> {
|
||||||
|
let span = value.span();
|
||||||
|
match value {
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
// Finding the modifier value in the record
|
||||||
|
let modifier = extract_value("modifier", val, span)?.clone();
|
||||||
|
let keycode = extract_value("keycode", val, span)?.clone();
|
||||||
|
let mode = extract_value("mode", val, span)?.clone();
|
||||||
|
let event = extract_value("event", val, span)?.clone();
|
||||||
|
|
||||||
|
let keybinding = ParsedKeybinding {
|
||||||
|
modifier,
|
||||||
|
keycode,
|
||||||
|
mode,
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We return a menu to be able to do recursion on the same function
|
||||||
|
Ok(vec![keybinding])
|
||||||
|
}
|
||||||
|
Value::List { vals, .. } => {
|
||||||
|
let res = vals
|
||||||
|
.iter()
|
||||||
|
.map(create_keybindings)
|
||||||
|
.collect::<Result<Vec<Vec<ParsedKeybinding>>, ShellError>>();
|
||||||
|
|
||||||
|
let res = res?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<ParsedKeybinding>>();
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_keybindings(config: &Config, span: Span) -> Value {
|
||||||
|
Value::list(
|
||||||
|
config
|
||||||
|
.keybindings
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|ParsedKeybinding {
|
||||||
|
modifier,
|
||||||
|
keycode,
|
||||||
|
mode,
|
||||||
|
event,
|
||||||
|
}| {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"modifier" => modifier.clone(),
|
||||||
|
"keycode" => keycode.clone(),
|
||||||
|
"mode" => mode.clone(),
|
||||||
|
"event" => event.clone(),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the config object to extract the strings that will compose a keybinding for reedline
|
||||||
|
pub fn create_menus(value: &Value) -> Result<Vec<ParsedMenu>, ShellError> {
|
||||||
|
let span = value.span();
|
||||||
|
match value {
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
// Finding the modifier value in the record
|
||||||
|
let name = extract_value("name", val, span)?.clone();
|
||||||
|
let marker = extract_value("marker", val, span)?.clone();
|
||||||
|
let only_buffer_difference =
|
||||||
|
extract_value("only_buffer_difference", val, span)?.clone();
|
||||||
|
let style = extract_value("style", val, span)?.clone();
|
||||||
|
let menu_type = extract_value("type", val, span)?.clone();
|
||||||
|
|
||||||
|
// Source is an optional value
|
||||||
|
let source = match extract_value("source", val, span) {
|
||||||
|
Ok(source) => source.clone(),
|
||||||
|
Err(_) => Value::nothing(span),
|
||||||
|
};
|
||||||
|
|
||||||
|
let menu = ParsedMenu {
|
||||||
|
name,
|
||||||
|
only_buffer_difference,
|
||||||
|
marker,
|
||||||
|
style,
|
||||||
|
menu_type,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(vec![menu])
|
||||||
|
}
|
||||||
|
Value::List { vals, .. } => {
|
||||||
|
let res = vals
|
||||||
|
.iter()
|
||||||
|
.map(create_menus)
|
||||||
|
.collect::<Result<Vec<Vec<ParsedMenu>>, ShellError>>();
|
||||||
|
|
||||||
|
let res = res?.into_iter().flatten().collect::<Vec<ParsedMenu>>();
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_menus(config: &Config, span: Span) -> Value {
|
||||||
|
Value::list(
|
||||||
|
config
|
||||||
|
.menus
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|ParsedMenu {
|
||||||
|
name,
|
||||||
|
only_buffer_difference,
|
||||||
|
marker,
|
||||||
|
style,
|
||||||
|
menu_type, // WARNING: this is not the same name as what is used in Config.nu! ("type")
|
||||||
|
source,
|
||||||
|
}| {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"name" => name.clone(),
|
||||||
|
"only_buffer_difference" => only_buffer_difference.clone(),
|
||||||
|
"marker" => marker.clone(),
|
||||||
|
"style" => style.clone(),
|
||||||
|
"type" => menu_type.clone(),
|
||||||
|
"source" => source.clone(),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
330
crates/nu-protocol/src/config/table.rs
Normal file
330
crates/nu-protocol/src/config/table.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
use super::helper::ReconstructVal;
|
||||||
|
use crate::{record, Config, ShellError, Span, Value};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
|
pub enum TableMode {
|
||||||
|
Basic,
|
||||||
|
Thin,
|
||||||
|
Light,
|
||||||
|
Compact,
|
||||||
|
WithLove,
|
||||||
|
CompactDouble,
|
||||||
|
#[default]
|
||||||
|
Rounded,
|
||||||
|
Reinforced,
|
||||||
|
Heavy,
|
||||||
|
None,
|
||||||
|
Psql,
|
||||||
|
Markdown,
|
||||||
|
Dots,
|
||||||
|
Restructured,
|
||||||
|
AsciiRounded,
|
||||||
|
BasicCompact,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TableMode {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"basic" => Ok(Self::Basic),
|
||||||
|
"thin" => Ok(Self::Thin),
|
||||||
|
"light" => Ok(Self::Light),
|
||||||
|
"compact" => Ok(Self::Compact),
|
||||||
|
"with_love" => Ok(Self::WithLove),
|
||||||
|
"compact_double" => Ok(Self::CompactDouble),
|
||||||
|
"rounded" => Ok(Self::Rounded),
|
||||||
|
"reinforced" => Ok(Self::Reinforced),
|
||||||
|
"heavy" => Ok(Self::Heavy),
|
||||||
|
"none" => Ok(Self::None),
|
||||||
|
"psql" => Ok(Self::Psql),
|
||||||
|
"markdown" => Ok(Self::Markdown),
|
||||||
|
"dots" => Ok(Self::Dots),
|
||||||
|
"restructured" => Ok(Self::Restructured),
|
||||||
|
"ascii_rounded" => Ok(Self::AsciiRounded),
|
||||||
|
"basic_compact" => Ok(Self::BasicCompact),
|
||||||
|
_ => Err("expected either 'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for TableMode {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
TableMode::Basic => "basic",
|
||||||
|
TableMode::Thin => "thin",
|
||||||
|
TableMode::Light => "light",
|
||||||
|
TableMode::Compact => "compact",
|
||||||
|
TableMode::WithLove => "with_love",
|
||||||
|
TableMode::CompactDouble => "compact_double",
|
||||||
|
TableMode::Rounded => "rounded",
|
||||||
|
TableMode::Reinforced => "reinforced",
|
||||||
|
TableMode::Heavy => "heavy",
|
||||||
|
TableMode::None => "none",
|
||||||
|
TableMode::Psql => "psql",
|
||||||
|
TableMode::Markdown => "markdown",
|
||||||
|
TableMode::Dots => "dots",
|
||||||
|
TableMode::Restructured => "restructured",
|
||||||
|
TableMode::AsciiRounded => "ascii_rounded",
|
||||||
|
TableMode::BasicCompact => "basic_compact",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum FooterMode {
|
||||||
|
/// Never show the footer
|
||||||
|
Never,
|
||||||
|
/// Always show the footer
|
||||||
|
Always,
|
||||||
|
/// Only show the footer if there are more than RowCount rows
|
||||||
|
RowCount(u64),
|
||||||
|
/// Calculate the screen height, calculate row count, if display will be bigger than screen, add the footer
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FooterMode {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"always" => Ok(FooterMode::Always),
|
||||||
|
"never" => Ok(FooterMode::Never),
|
||||||
|
"auto" => Ok(FooterMode::Auto),
|
||||||
|
x => {
|
||||||
|
if let Ok(count) = x.parse() {
|
||||||
|
Ok(FooterMode::RowCount(count))
|
||||||
|
} else {
|
||||||
|
Err("expected either 'never', 'always', 'auto' or a row count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for FooterMode {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
FooterMode::Always => "always".to_string(),
|
||||||
|
FooterMode::Never => "never".to_string(),
|
||||||
|
FooterMode::Auto => "auto".to_string(),
|
||||||
|
FooterMode::RowCount(c) => c.to_string(),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum TableIndexMode {
|
||||||
|
/// Always show indexes
|
||||||
|
Always,
|
||||||
|
/// Never show indexes
|
||||||
|
Never,
|
||||||
|
/// Show indexes when a table has "index" column
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TableIndexMode {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"always" => Ok(TableIndexMode::Always),
|
||||||
|
"never" => Ok(TableIndexMode::Never),
|
||||||
|
"auto" => Ok(TableIndexMode::Auto),
|
||||||
|
_ => Err("expected either 'never', 'always' or 'auto'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconstructVal for TableIndexMode {
|
||||||
|
fn reconstruct_value(&self, span: Span) -> Value {
|
||||||
|
Value::string(
|
||||||
|
match self {
|
||||||
|
TableIndexMode::Always => "always",
|
||||||
|
TableIndexMode::Never => "never",
|
||||||
|
TableIndexMode::Auto => "auto",
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Table view configuration, for a situation where
|
||||||
|
/// we need to limit cell width in order to adjust for a terminal size.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub enum TrimStrategy {
|
||||||
|
/// Wrapping strategy.
|
||||||
|
///
|
||||||
|
/// It it's similar to original nu_table, strategy.
|
||||||
|
Wrap {
|
||||||
|
/// A flag which indicates whether is it necessary to try
|
||||||
|
/// to keep word boundaries.
|
||||||
|
try_to_keep_words: bool,
|
||||||
|
},
|
||||||
|
/// Truncating strategy, where we just cut the string.
|
||||||
|
/// And append the suffix if applicable.
|
||||||
|
Truncate {
|
||||||
|
/// Suffix which can be appended to a truncated string after being cut.
|
||||||
|
///
|
||||||
|
/// It will be applied only when there's enough room for it.
|
||||||
|
/// For example in case where a cell width must be 12 chars, but
|
||||||
|
/// the suffix takes 13 chars it won't be used.
|
||||||
|
suffix: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrimStrategy {
|
||||||
|
pub fn wrap(dont_split_words: bool) -> Self {
|
||||||
|
Self::Wrap {
|
||||||
|
try_to_keep_words: dont_split_words,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncate(suffix: Option<String>) -> Self {
|
||||||
|
Self::Truncate { suffix }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TrimStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
TrimStrategy::Wrap {
|
||||||
|
try_to_keep_words: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn try_parse_trim_strategy(
|
||||||
|
value: &Value,
|
||||||
|
errors: &mut Vec<ShellError>,
|
||||||
|
) -> Result<TrimStrategy, ShellError> {
|
||||||
|
let map = value.as_record().map_err(|e| {
|
||||||
|
ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"$env.config.table.trim is not a record".into(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("Please consult the documentation for configuring Nushell.".into()),
|
||||||
|
vec![e],
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut methodology = match map.get("methodology") {
|
||||||
|
Some(value) => match try_parse_trim_methodology(value) {
|
||||||
|
Some(methodology) => methodology,
|
||||||
|
None => return Ok(TrimStrategy::default()),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"$env.config.table.trim.methodology was not provided".into(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("Please consult the documentation for configuring Nushell.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
return Ok(TrimStrategy::default());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut methodology {
|
||||||
|
TrimStrategy::Wrap { try_to_keep_words } => {
|
||||||
|
if let Some(value) = map.get("wrapping_try_keep_words") {
|
||||||
|
if let Ok(b) = value.as_bool() {
|
||||||
|
*try_to_keep_words = b;
|
||||||
|
} else {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"$env.config.table.trim.wrapping_try_keep_words is not a bool".into(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("Please consult the documentation for configuring Nushell.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrimStrategy::Truncate { suffix } => {
|
||||||
|
if let Some(value) = map.get("truncating_suffix") {
|
||||||
|
if let Ok(v) = value.as_string() {
|
||||||
|
*suffix = Some(v);
|
||||||
|
} else {
|
||||||
|
errors.push(ShellError::GenericError(
|
||||||
|
"Error while applying config changes".into(),
|
||||||
|
"$env.config.table.trim.truncating_suffix is not a string".into(),
|
||||||
|
Some(value.span()),
|
||||||
|
Some("Please consult the documentation for configuring Nushell.".into()),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(methodology)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
|
||||||
|
if let Ok(value) = value.as_string() {
|
||||||
|
match value.to_lowercase().as_str() {
|
||||||
|
"wrapping" => {
|
||||||
|
return Some(TrimStrategy::Wrap {
|
||||||
|
try_to_keep_words: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"truncating" => return Some(TrimStrategy::Truncate { suffix: None }),
|
||||||
|
_ => eprintln!("unrecognized $config.table.trim.methodology value; expected either 'truncating' or 'wrapping'"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("$env.config.table.trim.methodology is not a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_trim_strategy(config: &Config, span: Span) -> Value {
|
||||||
|
match &config.trim_strategy {
|
||||||
|
TrimStrategy::Wrap { try_to_keep_words } => Value::record(
|
||||||
|
record! {
|
||||||
|
"methodology" => Value::string("wrapping", span),
|
||||||
|
"wrapping_try_keep_words" => Value::bool(*try_to_keep_words, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
TrimStrategy::Truncate { suffix } => Value::record(
|
||||||
|
match suffix {
|
||||||
|
Some(s) => record! {
|
||||||
|
"methodology" => Value::string("truncating", span),
|
||||||
|
"truncating_suffix" => Value::string(s.clone(), span),
|
||||||
|
},
|
||||||
|
None => record! {
|
||||||
|
"methodology" => Value::string("truncating", span),
|
||||||
|
"truncating_suffix" => Value::nothing(span),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct TableIndent {
|
||||||
|
pub left: usize,
|
||||||
|
pub right: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reconstruct_padding(config: &Config, span: Span) -> Value {
|
||||||
|
// For better completions always reconstruct the record version even though unsigned int would
|
||||||
|
// be supported, `as` conversion is sane as it came from an i64 original
|
||||||
|
Value::record(
|
||||||
|
record!(
|
||||||
|
"left" => Value::int(config.table_indent.left as i64, span),
|
||||||
|
"right" => Value::int(config.table_indent.right as i64, span),
|
||||||
|
),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
@ -705,8 +705,8 @@ impl EngineState {
|
|||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_config(&mut self, conf: &Config) {
|
pub fn set_config(&mut self, conf: Config) {
|
||||||
self.config = conf.clone();
|
self.config = conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_var(&self, var_id: VarId) -> &Variable {
|
pub fn get_var(&self, var_id: VarId) -> &Variable {
|
||||||
|
@ -70,9 +70,12 @@ fn config_add_unsupported_value() {
|
|||||||
r#"$env.config.history.file_format = ''"#,
|
r#"$env.config.history.file_format = ''"#,
|
||||||
r#";"#]));
|
r#";"#]));
|
||||||
|
|
||||||
assert!(actual.err.contains(
|
assert!(actual
|
||||||
"unrecognized $env.config.history.file_format ''; expected either 'sqlite' or 'plaintext'"
|
.err
|
||||||
));
|
.contains("unrecognized $env.config.history.file_format option ''"));
|
||||||
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("expected either 'sqlite' or 'plaintext'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use nu_color_config::{Alignment, StyleComputer, TextStyle};
|
use nu_color_config::{Alignment, StyleComputer, TextStyle};
|
||||||
use nu_protocol::TrimStrategy;
|
|
||||||
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
|
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
|
||||||
|
use nu_protocol::{TableMode, TrimStrategy};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
clean_charset, colorize_space_str, string_wrap, NuTableConfig, TableOutput, TableTheme,
|
clean_charset, colorize_space_str, string_wrap, NuTableConfig, TableOutput, TableTheme,
|
||||||
@ -174,24 +174,23 @@ fn is_cfg_trim_keep_words(config: &Config) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_theme_from_config(config: &Config) -> TableTheme {
|
pub fn load_theme_from_config(config: &Config) -> TableTheme {
|
||||||
match config.table_mode.as_str() {
|
match config.table_mode {
|
||||||
"basic" => TableTheme::basic(),
|
TableMode::Basic => TableTheme::basic(),
|
||||||
"thin" => TableTheme::thin(),
|
TableMode::Thin => TableTheme::thin(),
|
||||||
"light" => TableTheme::light(),
|
TableMode::Light => TableTheme::light(),
|
||||||
"compact" => TableTheme::compact(),
|
TableMode::Compact => TableTheme::compact(),
|
||||||
"with_love" => TableTheme::with_love(),
|
TableMode::WithLove => TableTheme::with_love(),
|
||||||
"compact_double" => TableTheme::compact_double(),
|
TableMode::CompactDouble => TableTheme::compact_double(),
|
||||||
"rounded" => TableTheme::rounded(),
|
TableMode::Rounded => TableTheme::rounded(),
|
||||||
"reinforced" => TableTheme::reinforced(),
|
TableMode::Reinforced => TableTheme::reinforced(),
|
||||||
"heavy" => TableTheme::heavy(),
|
TableMode::Heavy => TableTheme::heavy(),
|
||||||
"none" => TableTheme::none(),
|
TableMode::None => TableTheme::none(),
|
||||||
"psql" => TableTheme::psql(),
|
TableMode::Psql => TableTheme::psql(),
|
||||||
"markdown" => TableTheme::markdown(),
|
TableMode::Markdown => TableTheme::markdown(),
|
||||||
"dots" => TableTheme::dots(),
|
TableMode::Dots => TableTheme::dots(),
|
||||||
"restructured" => TableTheme::restructured(),
|
TableMode::Restructured => TableTheme::restructured(),
|
||||||
"ascii_rounded" => TableTheme::ascii_rounded(),
|
TableMode::AsciiRounded => TableTheme::ascii_rounded(),
|
||||||
"basic_compact" => TableTheme::basic_compact(),
|
TableMode::BasicCompact => TableTheme::basic_compact(),
|
||||||
_ => TableTheme::rounded(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user