From f4c1017d0bceb49b8fb8b99865e3c4da6e4d2614 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Mon, 24 Aug 2026 22:15:19 +0200 Subject: pkgs/fish-patched: Improve performance and reduce stutters --- ...eat-history-Add-turtle-as-history-backend.patch | 843 --------------------- 1 file changed, 843 deletions(-) delete mode 100644 pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch (limited to 'pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch') diff --git a/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch b/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch deleted file mode 100644 index 3e98b298..00000000 --- a/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch +++ /dev/null @@ -1,843 +0,0 @@ -From 26e8fbb5a24dd8b9cac67adcb59e714b32f95aa6 Mon Sep 17 00:00:00 2001 -From: Benedikt Peetz -Date: Sat, 22 Aug 2026 22:42:44 +0200 -Subject: [PATCH 3/3] feat(history): Add turtle as history backend - ---- - Cargo.toml | 4 + - src/history/history.rs | 285 ++--------------------------- - src/history/mod.rs | 2 + - src/history/turtle.rs | 400 +++++++++++++++++++++++++++++++++++++++++ - src/parser.rs | 5 + - src/reader/reader.rs | 4 +- - 6 files changed, 424 insertions(+), 276 deletions(-) - create mode 100644 src/history/turtle.rs - -diff --git a/Cargo.toml b/Cargo.toml -index aa91b61b7..137fbed24 100644 ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -11,6 +11,7 @@ repository = "https://github.com/fish-shell/fish-shell" - license = "GPL-2.0-only AND LGPL-2.0-or-later AND MIT AND PSF-2.0" - - [workspace.dependencies] -+turtle-api = { version = "20.0.0" } - anstyle = "1.0.13" - anyhow = "1.0.102" - assert_matches = "1.5.0" -@@ -115,6 +116,9 @@ homepage = "https://fishshell.com" - readme = "README.rst" - - [dependencies] -+turtle-api.workspace = true -+tokio = { version = "1.53.1", features = ["rt"] } -+uuid = { version = "1.24.0", features = ["v7"] } - assert_matches.workspace = true - bitflags.workspace = true - cfg-if.workspace = true -diff --git a/src/history/history.rs b/src/history/history.rs -index e1d04ad49..90cbfe6a9 100644 ---- a/src/history/history.rs -+++ b/src/history/history.rs -@@ -14,6 +14,7 @@ - //! `src/fs.rs`. By default, `flock()` is used for locking. If that is unavailable, an imperfect - //! fallback solution attempts to detect races and retries if a race is detected. - -+use crate::history::turtle::HistoryDb; - use crate::{ - ast::{self, Kind, Node as _}, - common::valid_var_name, -@@ -334,7 +335,7 @@ struct HistoryImpl { - /// Deleted item contents, and the scope of the deletion. - deleted_items: HashMap, - /// The history file contents. -- file_contents: Option, -+ file_contents: Option, - /// The file ID of the history file. - history_file_id: FileId, // INVALID_FILE_ID - /// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= -@@ -358,6 +359,8 @@ fn history_file_path(&self) -> std::io::Result> { - return Ok(None); - } - -+ return Ok(Some(L!("/run/user/1000/turtle.sock").to_owned())); -+ - let mut path = if let Some(custom_dir) = &self.custom_directory { - custom_dir.clone() - } else { -@@ -422,10 +425,7 @@ fn add(&mut self, item: HistoryItem, pending: bool, do_save: bool) { - } - - /// Internal function. -- fn clear_file_state(&mut self) { -- // Erase everything we know about our file. -- self.file_contents = None; -- } -+ fn clear_file_state(&mut self) {} - - /// Returns a timestamp for new items - see the implementation for a subtlety. - fn timestamps_as_of_now(&self) -> Timestamps { -@@ -451,32 +451,16 @@ fn timestamps_as_of_now(&self) -> Timestamps { - - /// Loads old items if necessary. - /// Return a reference to the loaded history file. -- fn load_old_if_needed(&mut self) -> &HistoryFile { -+ fn load_old_if_needed(&mut self) -> &HistoryDb { - if let Some(ref file_contents) = self.file_contents { - return file_contents; - } - let Ok(Some(history_path)) = self.history_file_path() else { -- return self.file_contents.insert(HistoryFile::create_empty()); -+ return self.file_contents.insert(HistoryDb::create_empty()); - }; - - let _profiler = TimeProfiler::new("load_old"); -- let file_contents = match lock_and_load(&history_path, RawHistoryFile::create) { -- Ok((file_id, history_file)) => { -- self.history_file_id = file_id; -- let _profiler = TimeProfiler::new("populate_from_file_contents"); -- let file_contents = history_file.decode(Some(self.boundary_timestamp)); -- flogf!( -- history, -- "Loaded %u old items", -- file_contents.offsets().len() -- ); -- file_contents -- } -- Err(e) => { -- flog!(history_file, "Error reading from history file:", e); -- HistoryFile::create_empty() -- } -- }; -+ let file_contents = HistoryDb::load(&history_path, self.boundary_timestamp); - self.file_contents.insert(file_contents) - } - -@@ -524,260 +508,11 @@ fn remove_ephemeral_items(&mut self) { - usize::min(self.first_unwritten_new_item_index, self.new_items.len()); - } - -- /// Given an existing history file, write a new history file to `dst`. -- fn rewrite_to_temporary_file( -- &self, -- existing_file: &File, -- dst: &mut File, -- ) -> std::io::Result<()> { -- // We are reading FROM existing_file and writing TO dst -- -- // Make an LRU cache to save only the last N elements. -- -- /// When we rewrite the history, the number of items we keep. -- const HISTORY_SAVE_MAX: NonZeroUsize = NonZeroUsize::new(1024 * 256).unwrap(); -- let mut lru = LruCache::new(HISTORY_SAVE_MAX); -- -- // Read in existing items (which may have changed out from underneath us, so don't trust our -- // old file contents). -- let file_id = file_id_for_file(existing_file); -- if let Ok(local_file) = RawHistoryFile::create(existing_file, file_id) { -- for offset in local_file.offsets(None) { -- // Try decoding an old item. -- let Some(old_item) = local_file.decode_item(offset) else { -- continue; -- }; -- if old_item.is_empty() { -- continue; -- } -- -- // Check if this item should be deleted. -- if let Some(&scope) = self.deleted_items.get(old_item.str()) { -- // If old item is newer than session always erase if in deleted. -- // If old item is older and in deleted items don't erase if added by clear_session. -- let delete = old_item.first_added_timestamp() > self.boundary_timestamp -- || scope == DeletionScope::AllSessions; -- if delete { -- continue; -- } -- } -- lru.add_item(old_item); -- } -- } -- -- // Insert any unwritten new items -- for item in self -- .new_items -- .iter() -- .skip(self.first_unwritten_new_item_index) -- { -- if item.should_write_to_disk() { -- lru.add_item(item.clone()); -- } -- } -- -- // Stable-sort our items by timestamp -- // This is because we may have read "old" items with a later timestamp than our "new" items -- // This is the essential step that roughly orders items by history -- let mut items: Vec<_> = lru.into_iter().map(|(_key, item)| item).collect(); -- items.sort_by_key(HistoryItem::last_added_timestamp); -- -- /// Default buffer size for flushing to the history file. -- const HISTORY_OUTPUT_BUFFER_SIZE: usize = 64 * 1024; -- // Write them out. -- let mut buffer = BufWriter::with_capacity(HISTORY_OUTPUT_BUFFER_SIZE + 128, dst); -- for item in items { -- item.write_to(&mut buffer)?; -- } -- buffer.flush()?; -- Ok(()) -- } -- -- /// Saves history by rewriting the file. -- fn save_internal_via_rewrite(&mut self, history_path: &wstr) -> std::io::Result<()> { -- flogf!( -- history, -- "Saving %u items via rewrite", -- self.new_items.len() - self.first_unwritten_new_item_index -- ); -- -- let rewrite = -- |old_file: &File, tmp_file: &mut File| -> std::io::Result> { -- let result = self.rewrite_to_temporary_file(old_file, tmp_file); -- if let Err(err) = result { -- flog!( -- history_file, -- "Error writing to temporary history file:", -- err -- ); -- return Err(err); -- } -- Ok(PotentialUpdate { -- do_save: true, -- data: (), -- }) -- }; -- -- let (file_id, _) = rewrite_via_temporary_file(history_path, rewrite)?; -- self.history_file_id = file_id; -- -- // We've saved everything, so we have no more unsaved items. -- self.first_unwritten_new_item_index = self.new_items.len(); -- -- // We deleted our deleted items. -- self.deleted_items.clear(); -- -- // Our history has been written to the file, so clear our state so we can re-reference the -- // file. -- self.clear_file_state(); -- -- Ok(()) -- } -- -- /// Saves history by appending to the file. -- fn save_internal_via_appending(&mut self, history_path: &wstr) -> std::io::Result<()> { -- flogf!( -- history, -- "Saving %u items via appending", -- self.new_items.len() - self.first_unwritten_new_item_index -- ); -- // No deleting allowed. -- assert!(self.deleted_items.is_empty()); -- -- let mut locked_history_file = -- LockedFile::new(LockingMode::Exclusive(WriteMethod::Append), history_path)?; -- -- // Check if the file was modified since it was last read. -- // If someone has replaced the file, forget our file state. -- if file_id_for_file(locked_history_file.get()) != self.history_file_id { -- self.clear_file_state(); -- } -- -- // We took the exclusive lock. Append to the file. -- // Note that this is sketchy for a few reasons: -- // - Another shell may have appended its own items with a later timestamp, so our file may -- // no longer be sorted by timestamp. -- // - Another shell may have appended the same items, so our file may now contain -- // duplicates. -- // -- // Originally we always rewrote the file on saving, which avoided both of these problems. -- // However, appending allows us to save history after every command, which is nice! -- // -- // Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't -- // have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used -- // for much anyways). -- -- // So far so good. Write all items at or after first_unwritten_new_item_index. Note that we -- // write even a pending item - pending items are ignored by history within the command -- // itself, but should still be written to the file. -- // Use a small buffer size for appending, as we usually only have 1 item. -- // Buffer everything and then write it all at once to avoid tearing writes (O_APPEND). -- let mut buffer = Vec::new(); -- let mut new_first_index = self.first_unwritten_new_item_index; -- while new_first_index < self.new_items.len() { -- let item = &self.new_items[new_first_index]; -- if item.should_write_to_disk() { -- // Can't error writing to a buffer. -- item.write_to(&mut buffer).unwrap(); -- } -- // We wrote or skipped this item, hooray. -- new_first_index += 1; -- } -- locked_history_file.get_mut().write_all(&buffer)?; -- fsync(locked_history_file.get())?; -- self.first_unwritten_new_item_index = new_first_index; -- -- // Since we just modified the file, update our history_file_id to match its current state -- // Otherwise we'll think the file has been changed by someone else the next time we go to -- // write. -- // We don't update `self.file_contents` since we only appended to the file, and everything we -- // appended remains in our new_items -- self.history_file_id = file_id_for_file(locked_history_file.get()); -- -- Ok(()) -- } -- - /// Saves history. -- fn save(&mut self, vacuum: bool) { -- // Nothing to do if there's no new items. -- if self.first_unwritten_new_item_index >= self.new_items.len() -- && self.deleted_items.is_empty() -- { -- return; -- } -- -- // Compact our new items so we don't have duplicates. -- self.compact_new_items(); -- -- if self.name.is_empty() { -- // We're in the "incognito" mode. Pretend we've saved the history. -- self.first_unwritten_new_item_index = self.new_items.len(); -- self.deleted_items.clear(); -- self.clear_file_state(); -- return; -- } -- -- let history_path = match self.history_file_path() { -- Ok(history_path) => history_path.unwrap(), -- Err(e) => { -- flog!(history, "Saving history failed:", e); -- return; -- } -- }; -- -- // Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can -- // append to it. -- let mut ok = false; -- if !vacuum && self.deleted_items.is_empty() { -- // Try doing a fast append. -- if let Err(e) = self.save_internal_via_appending(&history_path) { -- flog!(history, "Appending to history failed:", e); -- } else { -- ok = true; -- } -- } -- if !ok { -- // We did not or could not append; rewrite the file ("vacuum" it). -- if let Err(e) = self.save_internal_via_rewrite(&history_path) { -- flog!(history, "Rewriting history failed:", e); -- } -- } -- } -+ fn save(&mut self, vacuum: bool) {} - - /// Saves history unless doing so is disabled. -- fn save_unless_disabled(&mut self) { -- // Respect disable_automatic_save_counter. -- if self.disable_automatic_save_counter > 0 { -- return; -- } -- -- // We may or may not vacuum. We try to vacuum every `VACUUM_FREQUENCY` items, but start the -- // countdown at a random number so that even if the user never runs more than 25 commands, we'll -- // eventually vacuum. If countdown_to_vacuum is None, it means we haven't yet picked a value for -- // the counter. -- let countdown_to_vacuum = self -- .countdown_to_vacuum -- .get_or_insert_with(|| rand::rng().random_range(0..VACUUM_FREQUENCY)); -- -- // Determine if we're going to vacuum. -- let mut vacuum = false; -- if *countdown_to_vacuum == 0 { -- *countdown_to_vacuum = VACUUM_FREQUENCY; -- vacuum = true; -- } -- -- // Update our countdown. -- assert!(*countdown_to_vacuum > 0); -- *countdown_to_vacuum -= 1; -- -- // This might be a good candidate for moving to a background thread. -- let _profiler = TimeProfiler::new(if vacuum { -- "save vacuum" -- } else { -- "save no vacuum" -- }); -- self.save(vacuum); -- } -+ fn save_unless_disabled(&mut self) {} - - fn new(name: WString, custom_directory: Option) -> Self { - Self { -diff --git a/src/history/mod.rs b/src/history/mod.rs -index 7ef4d4417..0de867740 100644 ---- a/src/history/mod.rs -+++ b/src/history/mod.rs -@@ -3,4 +3,6 @@ - mod history; - mod yaml_backend; - -+pub(crate) mod turtle; -+ - pub use history::*; -diff --git a/src/history/turtle.rs b/src/history/turtle.rs -new file mode 100644 -index 000000000..ffcdea108 ---- /dev/null -+++ b/src/history/turtle.rs -@@ -0,0 +1,400 @@ -+use std::{ -+ collections::HashMap, -+ sync::{ -+ Arc, OnceLock, RwLock, -+ atomic::{AtomicBool, Ordering}, -+ mpsc, -+ }, -+ thread::{self, JoinHandle, Thread}, -+ time::{Duration, SystemTime, UNIX_EPOCH}, -+}; -+ -+use fish_widestring::{L, WString}; -+use turtle_api::{ -+ client::{HistoryClient, OffsetDateTime, Probe, Range}, -+ history::{History, HistoryId}, -+}; -+ -+use crate::{env::Environment as _, flog, flogf, history::HistoryItem}; -+ -+pub(crate) struct HistoryDb; -+ -+#[derive(Debug)] -+struct HistoryDbInner { -+ range: Option, -+ session: uuid::Uuid, -+ -+ loaded_history: Vec, -+ -+ handler: Handler, -+} -+ -+impl HistoryDbInner { -+ fn loaded_history(&mut self) -> &[History] { -+ let should_wait = if self.loaded_history.is_empty() { -+ // We are probably running this the first time. -+ // Make sure, that we actually load something. -+ true -+ } else { -+ false -+ }; -+ -+ if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) || should_wait { -+ if let Some(pre_loaded_history) = self.handler.load_history_resp(should_wait) { -+ self.loaded_history = pre_loaded_history; -+ } -+ -+ flogf!( -+ history, -+ "Loaded history was requested, returning %d entries.", -+ self.loaded_history.len() -+ ); -+ -+ NEW_LOADED_HISTORY_AVAILABLE.store(false, Ordering::Relaxed); -+ } -+ -+ &self.loaded_history -+ } -+} -+ -+impl HistoryDb { -+ fn with_inner_mut(mut fun: impl FnMut(&mut HistoryDbInner) -> T) -> T { -+ let mut inner = INNER.write().expect("Should not be poisioned"); -+ let inner = (*inner).get_mut().expect("Should be initialized"); -+ -+ let output = fun(inner); -+ -+ output -+ } -+} -+ -+impl HistoryDb { -+ /// Create an empty history file. -+ pub(super) fn create_empty() -> Self { -+ flog!(history, "turtle: Creating new empty hist"); -+ -+ Self -+ } -+ -+ /// Return the offsets of items in this file. -+ pub(super) fn offsets(&self) -> Vec { -+ let out: Vec<_> = Self::with_inner_mut(|inner| { -+ inner -+ .loaded_history() -+ .iter() -+ .map(|h| h.id.clone()) -+ .collect() -+ }); -+ out -+ } -+ -+ /// Return whether this file is empty. -+ pub(super) fn is_empty(&self) -> bool { -+ self.offsets().is_empty() -+ } -+ -+ /// Load from on-disk file. -+ pub(super) fn load(history_path: &WString, _boundary_timestamp: SystemTime) -> Self { -+ flogf!(history, "turtle: Loading hist from %s", history_path); -+ -+ let inner = { -+ // let range = Some(Range { -+ // start: OffsetDateTime::from_unix_timestamp( -+ // boundary_timestamp -+ // .duration_since(UNIX_EPOCH) -+ // .expect("Also valid") -+ // .as_secs() as i64, -+ // ) -+ // .expect("Valid"), -+ // end: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()), -+ // }); -+ let range = None; -+ -+ let handler = Handler::start(history_path.to_string()); -+ handler.emit(HandleHistoryCmd::Load { range }); -+ -+ let inner = HistoryDbInner { -+ session: uuid::Uuid::now_v7(), -+ range, -+ loaded_history: vec![], -+ handler, -+ }; -+ -+ inner -+ }; -+ -+ let static_inner = INNER.write().expect("should not be poisened"); -+ (*static_inner) -+ .set(inner) -+ .expect("Should not have been initialized before"); -+ -+ Self -+ } -+ -+ /// Decode an item at a given offset. -+ pub(super) fn decode_item(&self, id: HistoryId) -> Option { -+ Self::with_inner_mut(|inner| { -+ inner.loaded_history().iter().find(|h| h.id == id).map(|h| { -+ HistoryItem::new( -+ WString::from_str(&h.command), -+ super::Timestamps { -+ last_added: UNIX_EPOCH -+ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), -+ first_added: UNIX_EPOCH -+ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), -+ }, -+ super::PersistenceMode::Disk, -+ ) -+ }) -+ }) -+ } -+} -+ -+static INNER: RwLock> = RwLock::new(OnceLock::new()); -+ -+pub(crate) fn handle_preexec(parser: &mut crate::parser::Parser, command: WString) { -+ HistoryDb::with_inner_mut(|inner| { -+ let now = OffsetDateTime::now_local().expect("Should have local time zone"); -+ let command = command.to_string(); -+ let history: History = History::daemon() -+ .timestamp(now) -+ .command(command) -+ .cwd(parser.vars().get_pwd_slash().to_string()) -+ .session(inner.session) -+ .hostname( -+ parser -+ .vars() -+ .get(L!("hostname")) -+ .map_or(String::from(""), |s| s.as_string().to_string()), -+ ) -+ .author( -+ parser -+ .vars() -+ .get(L!("USER")) -+ .map_or(String::from(""), |s| s.as_string().to_string()), -+ ) -+ .build() -+ .into(); -+ -+ inner.handler.emit(HandleHistoryCmd::Start { history }); -+ }); -+} -+ -+pub(crate) fn handle_postexec(parser: &mut crate::parser::Parser, command: WString) { -+ HistoryDb::with_inner_mut(|inner| { -+ inner.handler.emit(HandleHistoryCmd::End { -+ command: command.to_string(), -+ when: OffsetDateTime::now_local().expect("to work"), -+ exit_code: i64::from(parser.last_statuses().status), -+ }); -+ inner -+ .handler -+ .emit(HandleHistoryCmd::Load { range: inner.range }); -+ }); -+} -+ -+pub(crate) fn handle_shutdow() { -+ flog!(history, "Shutting down history db"); -+ -+ // If the `load` function was never called, inner will be None. -+ // Therefore we only need to run shutdown, when inner was loaded. -+ let inner_is_initialized = { -+ let read = INNER.read().expect("should be not-poisioned"); -+ read.get().is_some() -+ }; -+ -+ if inner_is_initialized { -+ HistoryDb::with_inner_mut(|inner| inner.handler.stop()); -+ } -+} -+ -+#[derive(Debug)] -+struct Handler { -+ thread: Option>, -+ -+ cmd_tx: Option>, -+ -+ returned_loaded_history: Arc>>>, -+} -+ -+#[derive(Debug)] -+enum HandleHistoryCmd { -+ Start { -+ history: History, -+ }, -+ End { -+ command: String, -+ when: OffsetDateTime, -+ exit_code: i64, -+ }, -+ Load { -+ range: Option, -+ }, -+} -+ -+static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); -+static LOAD_HISTORY_THREAD: RwLock> = RwLock::new(None); -+static LOAD_HISTORY_WAITING: AtomicBool = AtomicBool::new(false); -+static NEW_LOADED_HISTORY_AVAILABLE: AtomicBool = AtomicBool::new(false); -+ -+impl Handler { -+ fn start(daemon_socket: String) -> Self { -+ let (cmd_tx, cmd_rx) = mpsc::channel(); -+ let returned_loaded_history = Arc::new(RwLock::new(None)); -+ -+ let loaded_history_return = Arc::clone(&returned_loaded_history); -+ let thread = std::thread::spawn(move || { -+ let mut running_history: HashMap = HashMap::new(); -+ -+ let rt = { -+ let mut b = tokio::runtime::Builder::new_current_thread(); -+ b.enable_all(); -+ b.build() -+ .expect("to work, as all the fish code is otherwise sync.") -+ }; -+ let mut client = match rt.block_on(turtle_api::client::probe(daemon_socket.clone())) { -+ Probe::Ready(_) => rt -+ .block_on(HistoryClient::new(daemon_socket)) -+ .expect("We probed, this client should be accessible"), -+ Probe::NeedsRestart(err) => { -+ flogf!(history, "Turtle daemon needs restart: %s", err); -+ todo!(); -+ } -+ Probe::Unreachable(report) => { -+ flogf!(history, "Turtle daemon unreachable: %s", report.to_string()); -+ todo!(); -+ } -+ }; -+ -+ while let Ok(cmd) = cmd_rx.recv() { -+ match cmd { -+ HandleHistoryCmd::Start { history } => { -+ let command = history.command.clone(); -+ -+ flogf!(history, " > Starting cmd '%s'", command); -+ -+ let history_id = history.id; -+ let start_time = history.timestamp; -+ -+ rt.block_on(client.start_history(history)) -+ .expect("client to still work"); -+ -+ running_history.insert(command, (history_id, start_time)); -+ } -+ HandleHistoryCmd::End { -+ command, -+ when: now, -+ exit_code, -+ } => { -+ let (id, start_time) = running_history -+ .remove(command.as_str()) -+ .expect("to contain it"); -+ -+ let duration = Duration::from_nanos_u128( -+ (now - start_time).whole_nanoseconds() as u128, -+ ); -+ -+ flogf!( -+ history, -+ " < Ending cmd '%s' with exit %d and duration %d ms", -+ command, -+ exit_code, -+ duration.as_millis() as u64, -+ ); -+ -+ rt.block_on(client.end_history(id.to_string(), duration, exit_code)) -+ .expect("client to still work"); -+ } -+ HandleHistoryCmd::Load { range } => { -+ if !HANDLER_IS_SHUTTING_DOWN.load(Ordering::Relaxed) { -+ let loaded_history = { -+ let base = rt.block_on(client.history(range)); -+ base.expect("the client to still work") -+ }; -+ -+ let mut output = loaded_history_return.write().expect("not poisioned"); -+ (*output) = Some(loaded_history); -+ NEW_LOADED_HISTORY_AVAILABLE.store(true, Ordering::Relaxed); -+ -+ if LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { -+ let read = LOAD_HISTORY_THREAD.read().expect("not poisioned"); -+ let t = read -+ .as_ref() -+ .expect("is some, as a thread is marked as waiting"); -+ -+ LOAD_HISTORY_WAITING.store(false, Ordering::Relaxed); -+ t.unpark(); -+ } -+ } -+ } -+ } -+ } -+ }); -+ -+ Self { -+ thread: Some(thread), -+ cmd_tx: Some(cmd_tx), -+ returned_loaded_history, -+ } -+ } -+ -+ /// This is effectively a [`Drop`] impl, but we can't use the trait directly as statics don't -+ /// get dropped. -+ fn stop(&mut self) { -+ HANDLER_IS_SHUTTING_DOWN.store(true, Ordering::Relaxed); -+ -+ let thread = self.thread.take().expect("is some"); -+ let cmd_tx = self.cmd_tx.take().expect("is some"); -+ -+ // Tell the handler, that we won't send more cmds -+ drop(cmd_tx); -+ -+ thread.join().expect("should not panic"); -+ -+ flog!(history, "History db shutdown completed."); -+ } -+ -+ fn load_history_resp(&self, should_wait: bool) -> Option> { -+ let mut output = None; -+ let rx = &self.returned_loaded_history; -+ -+ if let Ok(read) = rx.try_read() { -+ read.clone_into(&mut output); -+ } -+ -+ if should_wait && output.is_none() { -+ { -+ { -+ let me = thread::current(); -+ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); -+ (*write) = Some(me); -+ } -+ -+ LOAD_HISTORY_WAITING.store(true, Ordering::Relaxed); -+ while LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { -+ thread::park(); -+ } -+ -+ { -+ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); -+ (*write) = None; -+ } -+ } -+ -+ let read = rx.read().expect("not poisioned"); -+ read.clone_into(&mut output); -+ } -+ -+ output -+ } -+ -+ fn emit(&self, cmd: HandleHistoryCmd) { -+ flogf!(history, "Emitting handler cmd: %s ", format!("{:?}", cmd)); -+ -+ self.cmd_tx -+ .as_ref() -+ .expect("was initialized") -+ .send(cmd) -+ .expect("receiver should not have hung up"); -+ } -+} -diff --git a/src/parser.rs b/src/parser.rs -index ffbc28633..6440b7476 100644 ---- a/src/parser.rs -+++ b/src/parser.rs -@@ -437,6 +437,11 @@ pub struct Parser { - #[cfg(test)] - pub test_only_suppress_stderr: bool, - } -+impl Drop for Parser { -+ fn drop(&mut self) { -+ crate::history::turtle::handle_shutdow(); -+ } -+} - - #[derive(Copy, Clone, Default)] - pub struct ParserEnvSetMode { -diff --git a/src/reader/reader.rs b/src/reader/reader.rs -index 14fbc89c0..6848a7f1c 100644 ---- a/src/reader/reader.rs -+++ b/src/reader/reader.rs -@@ -871,6 +871,7 @@ fn read_i(parser: &mut Parser) { - L!("fish_preexec").to_owned(), - vec![command.clone()], - ); -+ crate::history::turtle::handle_preexec(reader.parser, command.clone()); - let eval_res = reader_run_command(reader.parser, &command); - signal_clear_cancel(); - if !eval_res.no_status { -@@ -884,6 +885,7 @@ fn read_i(parser: &mut Parser) { - BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandFinished { - exit_status: reader.parser.last_status(), - }); -+ crate::history::turtle::handle_postexec(reader.parser, command.clone()); - event::fire_generic(reader.parser, L!("fish_postexec").to_owned(), vec![command]); - // Allow any pending history items to be returned in the history array. - reader.history.resolve_pending(); -@@ -2663,7 +2665,7 @@ fn readline( - self.clear_pager(); - } - -- if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ { -+ if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as u8 { - // The order of the two conditions below is important. Try to restore the mode - // in all cases, but only complain if interactive. - if let Some(old_modes) = old_modes { --- -2.55.0 - -- cgit v1.3.1