use crate::refs;
pub(crate) struct ReedlineHistory<SE: brush_core::ShellExtensions> {
pub shell: refs::ShellRef<SE>,
}
impl<SE: brush_core::ShellExtensions> ReedlineHistory<SE> {
fn lock_shell(&self) -> tokio::sync::MutexGuard<'_, brush_core::Shell<SE>> {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(self.shell.lock())
})
}
}
impl<SE: brush_core::ShellExtensions> reedline::History for ReedlineHistory<SE> {
fn save(&mut self, item: reedline::HistoryItem) -> reedline::Result<reedline::HistoryItem> {
Ok(item)
}
fn load(&self, id: reedline::HistoryItemId) -> reedline::Result<reedline::HistoryItem> {
let shell = self.lock_shell();
get_shell_history(&shell)?
.get_by_id(id.0)
.map_err(brush_error_to_reedline)?
.ok_or({
reedline::ReedlineError(reedline::ReedlineErrorVariants::OtherHistoryError(
"history item not found",
))
})
.map(brush_history_item_to_reedline)
}
fn count(&self, query: reedline::SearchQuery) -> reedline::Result<i64> {
let query = reedline_history_query_into_brush(query)?;
let shell = self.lock_shell();
let count = get_shell_history(&shell)?.search(query).iter().count();
drop(shell);
#[expect(clippy::cast_possible_wrap)]
Ok(count as i64)
}
#[expect(clippy::significant_drop_tightening)]
fn search(&self, query: reedline::SearchQuery) -> reedline::Result<Vec<reedline::HistoryItem>> {
let query = reedline_history_query_into_brush(query)?;
let shell = self.lock_shell();
let items = get_shell_history(&shell)?
.search(query)
.map_err(brush_error_to_reedline)?
.map(|item| {
brush_history_item_to_reedline(item)
})
.collect::<Vec<_>>();
Ok(items)
}
fn update(
&mut self,
id: reedline::HistoryItemId,
updater: &dyn Fn(reedline::HistoryItem) -> reedline::HistoryItem,
) -> reedline::Result<()> {
let item = self.load(id)?;
let updated_item = updater(item);
self.save(updated_item)?;
Ok(())
}
fn clear(&mut self) -> reedline::Result<()> {
let mut shell = self.lock_shell();
get_shell_history_mut(&mut shell)?
.clear()
.map_err(brush_error_to_reedline)
}
fn delete(&mut self, id: reedline::HistoryItemId) -> reedline::Result<()> {
let mut shell = self.lock_shell();
get_shell_history_mut(&mut shell)?
.delete_item_by_id(id.0)
.map_err(brush_error_to_reedline)
}
fn sync(&mut self) -> std::io::Result<()> {
let mut shell = self.lock_shell();
shell.save_history().map_err(std::io::Error::other)
}
fn session(&self) -> Option<reedline::HistorySessionId> {
None
}
}
fn brush_history_item_to_reedline(item: &brush_core::history::Item) -> reedline::HistoryItem {
let mut rl_item = reedline::HistoryItem::from_command_line(item.command_line.as_str());
rl_item.id = Some(reedline::HistoryItemId(item.id));
rl_item.start_timestamp = item.timestamp;
rl_item
}
#[expect(unused)]
fn reedline_history_item_to_brush(item: &reedline::HistoryItem) -> brush_core::history::Item {
brush_core::history::Item {
id: item.id.map_or(0, |id| id.0),
command_line: item.command_line.clone(),
timestamp: item.start_timestamp,
dirty: true,
}
}
fn brush_error_to_reedline(error: brush_core::Error) -> reedline::ReedlineError {
reedline::ReedlineError::from(std::io::Error::other(error))
}
fn reedline_history_query_into_brush(
query: reedline::SearchQuery,
) -> reedline::Result<brush_core::history::Query> {
let mut result = brush_core::history::Query {
direction: match query.direction {
reedline::SearchDirection::Forward => brush_core::history::Direction::Forward,
reedline::SearchDirection::Backward => brush_core::history::Direction::Backward,
},
max_items: query.limit,
not_at_or_before_id: if matches!(query.direction, reedline::SearchDirection::Backward) {
query.end_id.map(|id| id.0)
} else {
query.start_id.map(|id| id.0)
},
not_at_or_after_id: if matches!(query.direction, reedline::SearchDirection::Backward) {
query.start_id.map(|id| id.0)
} else {
query.end_id.map(|id| id.0)
},
not_at_or_before_time: if matches!(query.direction, reedline::SearchDirection::Backward) {
query.end_time
} else {
query.start_time
},
not_at_or_after_time: if matches!(query.direction, reedline::SearchDirection::Backward) {
query.start_time
} else {
query.end_time
},
..Default::default()
};
if let Some(cmdline_filter) = query.filter.command_line {
result.command_line_filter = match cmdline_filter {
reedline::CommandLineSearch::Exact(cmdline) => {
Some(brush_core::history::CommandLineFilter::Exact(cmdline))
}
reedline::CommandLineSearch::Substring(cmdline) => {
Some(brush_core::history::CommandLineFilter::Contains(cmdline))
}
reedline::CommandLineSearch::Prefix(cmdline) => {
Some(brush_core::history::CommandLineFilter::Prefix(cmdline))
}
}
}
if query.filter.cwd_exact.is_some()
|| query.filter.cwd_prefix.is_some()
|| query.filter.exit_successful.is_some()
|| query.filter.hostname.is_some()
|| query.filter.session.is_some()
{
return Err(reedline::ReedlineError(
reedline::ReedlineErrorVariants::HistoryFeatureUnsupported {
history: "(default)",
feature: "search filter",
},
));
}
Ok(result)
}
fn get_shell_history<'a, SE: brush_core::ShellExtensions>(
shell: &'a tokio::sync::MutexGuard<'_, brush_core::Shell<SE>>,
) -> Result<&'a brush_core::history::History, reedline::ReedlineError> {
shell.history().ok_or({
reedline::ReedlineError(reedline::ReedlineErrorVariants::HistoryFeatureUnsupported {
history: "(default)",
feature: "load",
})
})
}
fn get_shell_history_mut<'a, SE: brush_core::ShellExtensions>(
shell: &'a mut tokio::sync::MutexGuard<'_, brush_core::Shell<SE>>,
) -> Result<&'a mut brush_core::history::History, reedline::ReedlineError> {
shell.history_mut().ok_or({
reedline::ReedlineError(reedline::ReedlineErrorVariants::HistoryFeatureUnsupported {
history: "(default)",
feature: "load",
})
})
}