From 42dbfd1fa0866c143aeb38a12e88091b4e611e7f Mon Sep 17 00:00:00 2001 From: phiresky Date: Tue, 14 Jun 2022 22:53:33 +0200 Subject: [PATCH] SQLite History MVP with timestamp, duration, working directory, exit status metadata (#5721) This PR adds support for an SQLite history via nushell/reedline#401 The SQLite history is enabled by setting history_file_format: "sqlite" in config.nu. * somewhat working sqlite history * Hook up history command * Fix error in SQlitebacked with empty lines When entering an empty line there previously was the "No command run" error with `SqliteBackedHistory` during addition of the metadata May be considered a temporary fix Co-authored-by: sholderbach --- Cargo.lock | 127 ++++++++++-------- Cargo.toml | 7 +- crates/nu-cli/Cargo.toml | 4 +- crates/nu-cli/src/config_files.rs | 16 ++- crates/nu-cli/src/repl.rs | 63 +++++++-- crates/nu-command/Cargo.toml | 2 +- .../nu-command/src/core_commands/history.rs | 93 ++++++++----- crates/nu-engine/src/eval.rs | 1 + crates/nu-protocol/src/config.rs | 27 ++++ docs/sample_config/default_config.nu | 1 + src/config_files.rs | 20 --- src/main.rs | 9 +- 12 files changed, 243 insertions(+), 127 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5bc9a66d90..f7e14d08d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,9 +215,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -375,9 +375,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byte-unit" @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "const_format" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0936ffe6d0c8d6a51b3b0a73b2acbe925d786f346cf45bfddc8341d79fb7dc8a" +checksum = "e6a1316fa6a23fea1ee41cb80321590385e5f3e575e99f77c4d918ce5d44379d" dependencies = [ "const_format_proc_macros", ] @@ -603,9 +603,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cortex-m" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ff967e867ca14eba0c34ac25cd71ea98c678e741e3915d923999bb2fe7c826" +checksum = "cd20d4ac4aa86f4f75f239d59e542ef67de87cce2c282818dc6e84155d3ea126" dependencies = [ "bare-metal 0.2.5", "bitfield", @@ -698,7 +698,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio 0.8.3", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "serde", "signal-hook", "signal-hook-mio", @@ -1128,13 +1128,11 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if 1.0.0", "crc32fast", - "libc", "miniz_oxide", ] @@ -1361,6 +1359,16 @@ dependencies = [ "version_check 0.9.4", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "getopts" version = "0.2.21" @@ -1649,9 +1657,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -1712,9 +1720,9 @@ checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg", "hashbrown 0.11.2", @@ -1924,9 +1932,9 @@ dependencies = [ [[package]] name = "lexical-parse-integer" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125e1f93e5003d4bd89758c2ca2771bfae13632df633cde581efe07c87d354e5" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" dependencies = [ "lexical-util", "static_assertions", @@ -2025,9 +2033,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.6" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e7e15d7610cce1d9752e137625f14e61a28cd45929b6e12e47b50fe154ee2e" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" dependencies = [ "cc", "libc", @@ -2152,9 +2160,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" +checksum = "d5172b50c23043ff43dd53e51392f36519d9b35a8f3a410d30ece5d1aedd58ae" dependencies = [ "libc", ] @@ -2245,9 +2253,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] @@ -2515,6 +2523,7 @@ dependencies = [ name = "nu-cli" version = "0.63.1" dependencies = [ + "chrono", "crossterm", "fuzzy-matcher", "is_executable", @@ -2530,6 +2539,7 @@ dependencies = [ "nu-test-support", "nu-utils", "reedline", + "sysinfo 0.24.1", "thiserror", ] @@ -2617,7 +2627,7 @@ dependencies = [ "shadow-rs", "sqlparser", "strip-ansi-escapes", - "sysinfo", + "sysinfo 0.23.13", "terminal_size", "thiserror", "titlecase", @@ -2642,7 +2652,7 @@ dependencies = [ "nu-path", "nu-protocol", "nu-utils", - "sysinfo", + "sysinfo 0.23.13", ] [[package]] @@ -3041,9 +3051,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.73" +version = "0.9.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" dependencies = [ "autocfg", "cc", @@ -3087,9 +3097,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core 0.9.3", @@ -3394,7 +3404,7 @@ checksum = "eedc21001f05611e41bb7439b38d0f4ef9406aa49c17f3b289b5f57d8fa40c59" dependencies = [ "ahash", "glob", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "polars-arrow", "polars-core", "polars-io", @@ -3432,7 +3442,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f4cd569d383f5f000abbd6d5146550e6cb4e43fac30d1af98699499a440d56" dependencies = [ - "parking_lot 0.12.0", + "parking_lot 0.12.1", "rayon", ] @@ -3807,16 +3817,20 @@ dependencies = [ [[package]] name = "reedline" version = "0.6.0" -source = "git+https://github.com/nushell/reedline?branch=main#09bbb24bfdcf4183914b671b0fc0b70d46c4b3fa" +source = "git+https://github.com/nushell/reedline?branch=main#12dc30ce0ae99b58cfa8f13a51b056e9b4022c2d" dependencies = [ "chrono", "crossterm", "fd-lock", + "gethostname", "nu-ansi-term", + "rusqlite", "serde", + "serde_json", "strip-ansi-escapes", "strum 0.24.0", "strum_macros 0.24.0", + "thiserror", "unicode-segmentation", "unicode-width", ] @@ -4025,9 +4039,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.34.7" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f117495127afb702af6706f879fb2b5c008c38ccf3656afc514e26f35bdb8180" +checksum = "2079c267b8394eb529872c3cf92e181c378b41fea36e68130357b52493701d2e" dependencies = [ "bitflags", "errno", @@ -4451,7 +4465,7 @@ checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -4567,9 +4581,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", @@ -4616,6 +4630,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "sysinfo" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a8e71535da31837213ac114531d31def75d7aebd133264e420a3451fa7f703" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi 0.3.9", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -4760,9 +4789,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.18.2" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ "bytes", "libc", @@ -4787,9 +4816,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -4822,21 +4851,9 @@ checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.26" diff --git a/Cargo.toml b/Cargo.toml index d7d55376c3..c86a99cc8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ members = [ ] [dependencies] -chrono = "0.4.19" +chrono = { version = "0.4.19", features = ["serde"] } crossterm = "0.23.0" ctrlc = "3.2.1" log = "0.4" @@ -52,9 +52,9 @@ nu-system = { path = "./crates/nu-system", version = "0.63.1" } nu-table = { path = "./crates/nu-table", version = "0.63.1" } nu-term-grid = { path = "./crates/nu-term-grid", version = "0.63.1" } nu-utils = { path = "./crates/nu-utils", version = "0.63.1" } +reedline = { version = "0.6.0", features = ["bashisms", "sqlite"]} pretty_env_logger = "0.4.0" rayon = "1.5.1" -reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]} is_executable = "1.0.1" [target.'cfg(not(target_os = "windows"))'.dependencies] @@ -119,3 +119,6 @@ debug = false [[bin]] name = "nu" path = "src/main.rs" + +[patch.crates-io] +reedline = { git = "https://github.com/nushell/reedline", branch = "main"} diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 1b831e2b42..e25ef0ca6b 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -17,7 +17,7 @@ nu-parser = { path = "../nu-parser", version = "0.63.1" } nu-protocol = { path = "../nu-protocol", version = "0.63.1" } nu-utils = { path = "../nu-utils", version = "0.63.1" } nu-ansi-term = "0.46.0" -reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]} +reedline = { version = "0.6.0", features = ["bashisms", "sqlite"]} nu-color-config = { path = "../nu-color-config", version = "0.63.1" } crossterm = "0.23.0" miette = { version = "4.5.0", features = ["fancy"] } @@ -26,6 +26,8 @@ fuzzy-matcher = "0.3.7" log = "0.4" is_executable = "1.0.1" +chrono = "0.4.19" +sysinfo = "0.24.1" [features] plugin = [] diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index 30cf1e4767..18d85eea8a 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -2,12 +2,15 @@ use crate::util::{eval_source, report_error}; #[cfg(feature = "plugin")] use log::info; use nu_protocol::engine::{EngineState, Stack, StateDelta, StateWorkingSet}; -use nu_protocol::{PipelineData, Span}; +use nu_protocol::{HistoryFileFormat, PipelineData, Span}; use std::path::PathBuf; #[cfg(feature = "plugin")] const PLUGIN_FILE: &str = "plugin.nu"; +const HISTORY_FILE_TXT: &str = "history.txt"; +const HISTORY_FILE_SQLITE: &str = "history.sqlite3"; + #[cfg(feature = "plugin")] pub fn read_plugin_file( engine_state: &mut EngineState, @@ -84,3 +87,14 @@ pub fn eval_config_contents( } } } + +pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> Option { + nu_path::config_dir().map(|mut history_path| { + history_path.push(storage_path); + history_path.push(match mode { + HistoryFileFormat::PlainText => HISTORY_FILE_TXT, + HistoryFileFormat::Sqlite => HISTORY_FILE_SQLITE, + }); + history_path + }) +} diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index c29335516b..a3203e3a27 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -12,12 +12,12 @@ use nu_engine::{convert_env_values, eval_block}; use nu_parser::lex; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - BlockId, PipelineData, PositionalArg, ShellError, Span, Value, + BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Value, }; -use reedline::{DefaultHinter, Emacs, Vi}; +use reedline::{DefaultHinter, Emacs, SqliteBackedHistory, Vi}; use std::io::{self, Write}; -use std::path::PathBuf; use std::{sync::atomic::Ordering, time::Instant}; +use sysinfo::SystemExt; const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\"; const PRE_EXECUTE_MARKER: &str = "\x1b]133;C\x1b\\"; @@ -27,7 +27,7 @@ const RESET_APPLICATION_MODE: &str = "\x1b[?1l"; pub fn evaluate_repl( engine_state: &mut EngineState, stack: &mut Stack, - history_path: Option, + nushell_path: &str, is_perf_true: bool, ) -> Result<()> { use reedline::{FileBackedHistory, Reedline, Signal}; @@ -85,20 +85,32 @@ pub fn evaluate_repl( info!("setup reedline {}:{}:{}", file!(), line!(), column!()); } let mut line_editor = Reedline::create(); + let history_path = crate::config_files::get_history_path( + nushell_path, + engine_state.config.history_file_format, + ); if let Some(history_path) = history_path.as_deref() { if is_perf_true { info!("setup history {}:{}:{}", file!(), line!(), column!()); } - let history = Box::new( - FileBackedHistory::with_file( - config.max_history_size as usize, - history_path.to_path_buf(), - ) - .into_diagnostic()?, - ); + + let history: Box = match engine_state.config.history_file_format { + HistoryFileFormat::PlainText => Box::new( + FileBackedHistory::with_file( + config.max_history_size as usize, + history_path.to_path_buf(), + ) + .into_diagnostic()?, + ), + HistoryFileFormat::Sqlite => Box::new( + SqliteBackedHistory::with_file(history_path.to_path_buf()).into_diagnostic()?, + ), + }; line_editor = line_editor.with_history(history); }; + let sys = sysinfo::System::new(); + loop { if is_perf_true { info!( @@ -300,6 +312,20 @@ pub fn evaluate_repl( match input { Ok(Signal::Success(s)) => { + let history_supports_meta = + matches!(config.history_file_format, HistoryFileFormat::Sqlite); + if history_supports_meta && !s.is_empty() { + line_editor + .update_last_command_context(&|mut c| { + c.start_timestamp = Some(chrono::Utc::now()); + c.hostname = sys.host_name(); + + c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); + c + }) + .into_diagnostic()?; // todo: don't stop repl if error here? + } + // Right before we start running the code the user gave us, // fire the "pre_execution" hook if let Some(hook) = &config.hooks.pre_execution { @@ -401,11 +427,12 @@ pub fn evaluate_repl( PipelineData::new(Span::new(0, 0)), ); } + let cmd_duration = start_time.elapsed(); stack.add_env_var( "CMD_DURATION_MS".into(), Value::String { - val: format!("{}", start_time.elapsed().as_millis()), + val: format!("{}", cmd_duration.as_millis()), span: Span { start: 0, end: 0 }, }, ); @@ -418,6 +445,18 @@ pub fn evaluate_repl( engine_state.add_env_var("PWD".into(), cwd); } + if history_supports_meta && !s.is_empty() { + line_editor + .update_last_command_context(&|mut c| { + c.duration = Some(cmd_duration); + c.exit_status = stack + .get_env_var(engine_state, "LAST_EXIT_CODE") + .and_then(|e| e.as_i64().ok()); + c + }) + .into_diagnostic()?; // todo: don't stop repl if error here? + } + if shell_integration { // FIXME: use variant with exit code, if apropriate run_ansi_sequence(CMD_FINISHED_MARKER)?; diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 2b347f8816..dc90d1be58 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -83,7 +83,7 @@ unicode-segmentation = "1.8.0" url = "2.2.1" uuid = { version = "0.8.2", features = ["v4"] } which = { version = "4.2.2", optional = true } -reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]} +reedline = { version = "0.6.0", features = ["bashisms", "sqlite"]} wax = { version = "0.4.0", features = ["diagnostics"] } rusqlite = { version = "0.27.0", features = ["bundled"], optional = true } sqlparser = { version = "0.16.0", features = ["serde"], optional = true } diff --git a/crates/nu-command/src/core_commands/history.rs b/crates/nu-command/src/core_commands/history.rs index e2ac8bff19..1a65b55bb3 100644 --- a/crates/nu-command/src/core_commands/history.rs +++ b/crates/nu-command/src/core_commands/history.rs @@ -1,14 +1,13 @@ use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Value, + Category, Example, HistoryFileFormat, IntoInterruptiblePipelineData, PipelineData, ShellError, + Signature, Value, +}; +use reedline::{ + FileBackedHistory, History as ReedlineHistory, SearchDirection, SearchQuery, + SqliteBackedHistory, }; - -const NEWLINE_ESCAPE_CODE: &str = "<\\n>"; - -fn decode_newlines(escaped: &str) -> String { - escaped.replace(NEWLINE_ESCAPE_CODE, "\n") -} #[derive(Clone)] pub struct History; @@ -36,44 +35,74 @@ impl Command for History { _input: PipelineData, ) -> Result { let head = call.head; + // todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history` if let Some(config_path) = nu_path::config_dir() { let clear = call.has_flag("clear"); let ctrlc = engine_state.ctrlc.clone(); let mut history_path = config_path; history_path.push("nushell"); - history_path.push("history.txt"); + match engine_state.config.history_file_format { + HistoryFileFormat::Sqlite => { + history_path.push("history.sqlite3"); + } + HistoryFileFormat::PlainText => { + history_path.push("history.txt"); + } + } if clear { let _ = std::fs::remove_file(history_path); + // TODO: FIXME also clear the auxiliary files when using sqlite Ok(PipelineData::new(head)) } else { - let contents = std::fs::read_to_string(history_path); + let history_reader: Option> = + match engine_state.config.history_file_format { + HistoryFileFormat::Sqlite => SqliteBackedHistory::with_file(history_path) + .map(|inner| { + let boxed: Box = Box::new(inner); + boxed + }) + .ok(), - if let Ok(contents) = contents { - Ok(contents - .lines() - .enumerate() - .map(move |(index, command)| Value::Record { - cols: vec!["command".to_string(), "index".to_string()], - vals: vec![ - Value::String { - val: decode_newlines(command), - span: head, - }, - Value::Int { - val: index as i64, - span: head, - }, - ], - span: head, + HistoryFileFormat::PlainText => FileBackedHistory::with_file( + engine_state.config.max_history_size as usize, + history_path, + ) + .map(|inner| { + let boxed: Box = Box::new(inner); + boxed }) - .collect::>() - .into_iter() - .into_pipeline_data(ctrlc)) - } else { - Err(ShellError::FileNotFound(head)) - } + .ok(), + }; + + let data = history_reader + .and_then(|h| { + h.search(SearchQuery::everything(SearchDirection::Forward)) + .ok() + }) + .map(move |entries| { + entries + .into_iter() + .enumerate() + .map(move |(idx, entry)| Value::Record { + cols: vec!["command".to_string(), "index".to_string()], + vals: vec![ + Value::String { + val: entry.command_line, + span: head, + }, + Value::Int { + val: idx as i64, + span: head, + }, + ], + span: head, + }) + }) + .ok_or(ShellError::FileNotFound(head))? + .into_pipeline_data(ctrlc); + Ok(data) } } else { Err(ShellError::FileNotFound(head)) diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 0013cefc77..121d8c3c03 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1239,6 +1239,7 @@ pub fn eval_variable( let mut history_path = config_path.clone(); history_path.push("history.txt"); + // let mut history_path = config_files::get_history_path(); // todo: this should use the get_history_path method but idk where to put that function output_cols.push("history-path".into()); output_vals.push(Value::String { diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index ad78daf614..e8aa65ac3a 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -66,6 +66,7 @@ pub struct Config { pub edit_mode: String, pub max_history_size: i64, pub sync_history_on_enter: bool, + pub history_file_format: HistoryFileFormat, pub log_level: String, pub keybindings: Vec, pub menus: Vec, @@ -98,6 +99,7 @@ impl Default for Config { edit_mode: "emacs".into(), max_history_size: i64::MAX, sync_history_on_enter: true, + history_file_format: HistoryFileFormat::PlainText, log_level: String::new(), keybindings: Vec::new(), menus: Vec::new(), @@ -125,6 +127,14 @@ pub enum FooterMode { Auto, } +#[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 Value { pub fn into_config(self) -> Result { let v = self.as_record(); @@ -248,6 +258,23 @@ impl Value { eprintln!("$config.edit_mode is not a string") } } + "history_file_format" => { + if let Ok(b) = value.as_string() { + let val_str = b.to_lowercase(); + config.history_file_format = match val_str.as_ref() { + "sqlite" => HistoryFileFormat::Sqlite, + "plaintext" => HistoryFileFormat::PlainText, + _ => { + eprintln!( + "unrecognized $config.history_file_format '{val_str}'" + ); + HistoryFileFormat::PlainText + } + }; + } else { + eprintln!("$config.history_file_format is not a string") + } + } "max_history_size" => { if let Ok(i) = value.as_i64() { config.max_history_size = i; diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index 1514d0442c..50a8aa2cc4 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -215,6 +215,7 @@ let-env config = { edit_mode: emacs # emacs, vi max_history_size: 10000 # Session has to be reloaded for this to take effect sync_history_on_enter: true # Enable to share the history between multiple sessions, else you have to close the session to persist history to file + history_file_format: "plaintext" # "sqlite" or "plaintext" shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue disable_table_indexes: false # set to true to remove the index column from tables cd_with_abbreviations: false # set to true to allow you to do things like cd s/o/f and nushell expand it to cd some/other/folder diff --git a/src/config_files.rs b/src/config_files.rs index 937f3ab8d3..847ecfa020 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -6,13 +6,11 @@ use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::{PipelineData, Span, Spanned}; use std::fs::File; use std::io::Write; -use std::path::PathBuf; pub(crate) const NUSHELL_FOLDER: &str = "nushell"; const CONFIG_FILE: &str = "config.nu"; const ENV_FILE: &str = "env.nu"; const LOGINSHELL_FILE: &str = "login.nu"; -const HISTORY_FILE: &str = "history.txt"; pub(crate) fn read_config_file( engine_state: &mut EngineState, @@ -104,7 +102,6 @@ pub(crate) fn read_config_file( info!("read_config_file {}:{}:{}", file!(), line!(), column!()); } } - pub(crate) fn read_loginshell_file( engine_state: &mut EngineState, stack: &mut Stack, @@ -124,20 +121,3 @@ pub(crate) fn read_loginshell_file( info!("read_loginshell_file {}:{}:{}", file!(), line!(), column!()); } } - -pub(crate) fn create_history_path() -> Option { - nu_path::config_dir().and_then(|mut history_path| { - history_path.push(NUSHELL_FOLDER); - history_path.push(HISTORY_FILE); - - if !history_path.exists() { - // Creating an empty file to store the history - match std::fs::File::create(&history_path) { - Ok(_) => Some(history_path), - Err(_) => None, - } - } else { - Some(history_path) - } - }) -} diff --git a/src/main.rs b/src/main.rs index f9222b843a..5e84837a77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -287,10 +287,13 @@ fn main() -> Result<()> { binary_args.env_file, binary_args.login_shell.is_some(), ); - let history_path = config_files::create_history_path(); - let ret_val = - evaluate_repl(&mut engine_state, &mut stack, history_path, is_perf_true()); + let ret_val = evaluate_repl( + &mut engine_state, + &mut stack, + config_files::NUSHELL_FOLDER, + is_perf_true(), + ); if is_perf_true() { info!("repl eval {}:{}:{}", file!(), line!(), column!()); }