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