Skip to main content

nu_protocol/config/
history.rs

1use super::{config_update_string_enum, prelude::*};
2use crate::{self as nu_protocol, ConfigWarning};
3use std::path::PathBuf;
4
5#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
6pub enum HistoryFileFormat {
7    /// Store history as an SQLite database with additional context
8    Sqlite,
9    /// store history as a plain text file where every line is one command (without any context such as timestamps)
10    Plaintext,
11}
12
13impl HistoryFileFormat {
14    pub fn default_file_name(self) -> std::path::PathBuf {
15        match self {
16            HistoryFileFormat::Plaintext => "history.txt",
17            HistoryFileFormat::Sqlite => "history.sqlite3",
18        }
19        .into()
20    }
21}
22
23impl FromStr for HistoryFileFormat {
24    type Err = &'static str;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        match s.to_ascii_lowercase().as_str() {
28            "sqlite" => Ok(Self::Sqlite),
29            "plaintext" => Ok(Self::Plaintext),
30            #[cfg(feature = "sqlite")]
31            _ => Err("'sqlite' or 'plaintext'"),
32            #[cfg(not(feature = "sqlite"))]
33            _ => Err("'plaintext'"),
34        }
35    }
36}
37
38impl UpdateFromValue for HistoryFileFormat {
39    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
40        config_update_string_enum(self, value, path, errors);
41
42        #[cfg(not(feature = "sqlite"))]
43        if *self == HistoryFileFormat::Sqlite {
44            *self = HistoryFileFormat::Plaintext;
45            errors.warn(ConfigWarning::IncompatibleOptions {
46                label: "SQLite-based history file only supported with the `sqlite` feature, falling back to plain text history", 
47                span: value.span(),
48                help: "Compile Nushell with `sqlite` feature enabled",
49            });
50        }
51    }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55pub enum HistoryPath {
56    Default,
57    Custom(PathBuf),
58    Disabled,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
62pub struct HistoryConfig {
63    pub max_size: i64,
64    pub sync_on_enter: bool,
65    pub file_format: HistoryFileFormat,
66    pub isolation: bool,
67    pub path: HistoryPath,
68    pub ignore_space_prefixed: bool,
69}
70
71impl IntoValue for HistoryPath {
72    fn into_value(self, span: Span) -> Value {
73        match self {
74            HistoryPath::Default => Value::string("", span),
75            HistoryPath::Disabled => Value::nothing(span),
76            HistoryPath::Custom(path) => Value::string(path.display().to_string(), span),
77        }
78    }
79}
80
81impl IntoValue for HistoryConfig {
82    fn into_value(self, span: Span) -> Value {
83        Value::record(
84            record! {
85                "max_size" => self.max_size.into_value(span),
86                "sync_on_enter" => self.sync_on_enter.into_value(span),
87                "file_format" => self.file_format.into_value(span),
88                "isolation" => self.isolation.into_value(span),
89                "path" => self.path.into_value(span),
90                "ignore_space_prefixed" => self.ignore_space_prefixed.into_value(span),
91            },
92            span,
93        )
94    }
95}
96
97impl HistoryConfig {
98    pub fn file_path(&self) -> Option<PathBuf> {
99        let path = match &self.path {
100            HistoryPath::Custom(path) => Some(path.clone()),
101            HistoryPath::Disabled => None,
102            HistoryPath::Default => nu_path::nu_config_dir().map(|mut history_path| {
103                history_path.push(self.file_format.default_file_name());
104                history_path.into()
105            }),
106        }?;
107
108        if path.is_dir() {
109            return Some(path.join(self.file_format.default_file_name()));
110        }
111
112        Some(path)
113    }
114}
115
116impl Default for HistoryConfig {
117    fn default() -> Self {
118        Self {
119            max_size: 100_000,
120            sync_on_enter: true,
121            file_format: HistoryFileFormat::Plaintext,
122            isolation: false,
123            path: HistoryPath::Default,
124            ignore_space_prefixed: true,
125        }
126    }
127}
128
129impl UpdateFromValue for HistoryConfig {
130    fn update<'a>(
131        &mut self,
132        value: &'a Value,
133        path: &mut ConfigPath<'a>,
134        errors: &mut ConfigErrors,
135    ) {
136        let Value::Record { val: record, .. } = value else {
137            errors.type_mismatch(path, Type::record(), value);
138            return;
139        };
140
141        // might not be correct if file format was changed away from sqlite rather than isolation,
142        // but this is an edge case and the span of the relevant value here should be close enough
143        let mut isolation_span = value.span();
144
145        for (col, val) in record.iter() {
146            let path = &mut path.push(col);
147            match col.as_str() {
148                "isolation" => {
149                    isolation_span = val.span();
150                    self.isolation.update(val, path, errors)
151                }
152                "sync_on_enter" => self.sync_on_enter.update(val, path, errors),
153                "max_size" => self.max_size.update(val, path, errors),
154                "file_format" => self.file_format.update(val, path, errors),
155                "path" => match val {
156                    Value::String { val: s, .. } => {
157                        if s.is_empty() {
158                            self.path = HistoryPath::Default;
159                            continue;
160                        }
161
162                        self.path = HistoryPath::Custom(PathBuf::from(s));
163                    }
164                    Value::Nothing { .. } => {
165                        self.path = HistoryPath::Disabled;
166                    }
167                    _ => {
168                        errors.type_mismatch(path, Type::custom("string or nothing"), val);
169                    }
170                },
171                "ignore_space_prefixed" => self.ignore_space_prefixed.update(val, path, errors),
172                _ => errors.unknown_option(path, val),
173            }
174        }
175
176        // Listing all formats separately in case additional ones are added
177        match (self.isolation, self.file_format) {
178            (true, HistoryFileFormat::Plaintext) => {
179                errors.warn(ConfigWarning::IncompatibleOptions {
180                    label: "history isolation only compatible with SQLite format",
181                    span: isolation_span,
182                    help: r#"disable history isolation, or set $env.config.history.file_format = "sqlite""#,
183                });
184            }
185            (true, HistoryFileFormat::Sqlite) => (),
186            (false, _) => (),
187        }
188    }
189}