Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

History export / import to file #35

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ edition = "2021"

[dependencies]
crossterm = { version = "0.28.1", features = ["event-stream"] }
futures-channel = "0.3"
futures-util = { version = "0.3", features = ["io"] }
pin-project = "1.0"
thingbuf = "0.1"
Expand Down
205 changes: 171 additions & 34 deletions src/history.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
use std::collections::VecDeque;

use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender};
use futures_util::StreamExt;

pub struct History {
pub entries: VecDeque<String>,
pub max_size: usize,
pub sender: UnboundedSender<String>,
receiver: UnboundedReceiver<String>,

// Note: old entries in front, new ones at the back.
entries: VecDeque<String>,
max_size: usize,
current_position: Option<usize>,
}
impl Default for History {
fn default() -> Self {
let (sender, receiver) = mpsc::unbounded();
Self {
entries: Default::default(),
max_size: 1000,
sender,
receiver,
current_position: Default::default(),
}
}
}

impl History {
// Update history entries
pub async fn update(&mut self) {
// Receive a new line
if let Some(line) = self.receiver.next().await {
// Reset offset to newest entry
self.current_position = None;
// Don't add entry if last entry was same, or line was empty.
if self.entries.front() == Some(&line) || line.is_empty() {
return;
}
// Add entry to front of history
self.entries.push_front(line);
// Check if already have enough entries
if self.entries.len() > self.max_size {
// Remove oldest entry
self.entries.pop_back();
}
pub fn add_entry(&mut self, line: String) {
// Reset offset to newest entry
self.current_position = None;
// Don't add entry if last entry was same, or line was empty.
if self.entries.back() == Some(&line) || line.is_empty() {
return;
}
// Add entry to back of history
self.entries.push_back(line);
// Check if already have enough entries
if self.entries.len() > self.max_size {
// Remove oldest entry
self.entries.pop_front();
}
}

// Changes the history size.
pub fn set_max_size(&mut self, max_size: usize) {
self.max_size = max_size;

while self.entries.len() > max_size {
// Remove oldest entry
self.entries.pop_front();
}

// Make sure we don't end up in an invalid position.
self.reset_position();
}

// Returns the current history entries.
pub fn get_entries(&self) -> &VecDeque<String> {
&self.entries
}

// Replaces the current history entries.
pub fn set_entries(&mut self, entries: impl IntoIterator<Item = String>) {
self.entries.clear();

// Using `add_entry` will respect `max_size` and remove duplicate lines etc.
for entry in entries.into_iter() {
self.add_entry(entry);
}

self.reset_position();
}

// Sets the history position back to the start.
Expand All @@ -53,28 +72,146 @@ impl History {
// Find next history that matches a given string from an index
pub fn search_next(&mut self, _current: &str) -> Option<&str> {
if let Some(index) = &mut self.current_position {
if *index < self.entries.len() - 1 {
*index += 1;
if *index > 0 {
*index -= 1;
}
Some(&self.entries[*index])
} else if !self.entries.is_empty() {
self.current_position = Some(0);
Some(&self.entries[0])
} else if let Some(last) = self.entries.back() {
self.current_position = Some(self.entries.len() - 1);
Some(last)
} else {
None
}
}

// Find previous history item that matches a given string from an index
pub fn search_previous(&mut self, _current: &str) -> Option<&str> {
if let Some(index) = &mut self.current_position {
if *index == 0 {
if *index == self.entries.len() - 1 {
self.current_position = None;
return Some("");
}
*index -= 1;
*index += 1;
Some(&self.entries[*index])
} else {
None
}
}
}

#[cfg(test)]
#[test]
fn test_history() {
let mut history = History::default();

history.add_entry("foo".into());
history.add_entry("bar".into());
history.add_entry("baz".into());

for _ in 0..2 {
// Previous will navigate nowhere.
assert_eq!(None, history.search_previous(""));

// Going back in history.
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));
assert_eq!(Some("foo"), history.search_next(""));

// Last entry should just repeat.
assert_eq!(Some("foo"), history.search_next(""));

// Going forward.
assert_eq!(Some("bar"), history.search_previous(""));
assert_eq!(Some("baz"), history.search_previous(""));

// Alternate.
assert_eq!(Some("bar"), history.search_next(""));
assert_eq!(Some("baz"), history.search_previous(""));

// Back to the beginning. Should return "" once.
assert_eq!(Some(""), history.search_previous(""));
assert_eq!(None, history.search_previous(""));

// Going back again.
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));

// Resetting the position.
history.reset_position();
}
}

#[cfg(test)]
#[test]
fn test_history_limit() {
let mut history = History {
max_size: 3,
..Default::default()
};

history.add_entry("foo".into());
history.add_entry("bar".into());
history.add_entry("baz".into());
history.add_entry("qux".into()); // Should remove "foo".

assert_eq!(Some("qux"), history.search_next(""));
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));

history.set_max_size(2);

assert_eq!(Some("qux"), history.search_next(""));
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("baz"), history.search_next(""));
}

#[cfg(test)]
#[test]
fn test_history_reset_on_add() {
let mut history = History::default();

history.add_entry("foo".into());
history.add_entry("bar".into());
history.add_entry("baz".into());

assert_eq!(None, history.search_previous(""));
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));

// This should reset the history position.
history.add_entry("qux".into());

assert_eq!(None, history.search_previous(""));
assert_eq!(Some("qux"), history.search_next(""));
assert_eq!(Some("baz"), history.search_next(""));
assert_eq!(Some("bar"), history.search_next(""));
assert_eq!(Some("foo"), history.search_next(""));
}

#[cfg(test)]
#[test]
fn test_history_export() {
let mut history = History {
max_size: 3,
..Default::default()
};

assert_eq!(history.get_entries(), &VecDeque::new());

history.add_entry("foo".into());
history.add_entry("bar".into());
history.add_entry("baz".into());

assert_eq!(history.get_entries(), &["foo", "bar", "baz"]);

history.add_entry("qux".into());

assert_eq!(history.get_entries(), &["bar", "baz", "qux"]);

history.set_entries(["a".to_string(), "b".to_string(), "b".to_string()]);

assert_eq!(Some("b"), history.search_next(""));
assert_eq!(Some("a"), history.search_next(""));
assert_eq!(Some("a"), history.search_next(""));
}
60 changes: 46 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,14 @@
//! - Ctrl-C: Send an `Interrupt` event

use std::{
io::{self, stdout, Stdout, Write},
ops::DerefMut,
pin::Pin,
task::{Context, Poll},
collections::VecDeque, fs::File, io::{self, stdout, BufRead, BufReader, BufWriter, Stdout, Write}, ops::DerefMut, path::Path, pin::Pin, task::{Context, Poll}
};

use crossterm::{
event::EventStream,
terminal::{self, disable_raw_mode, Clear},
QueueableCommand,
};
use futures_channel::mpsc;
use futures_util::{pin_mut, ready, select, AsyncWrite, FutureExt, StreamExt};
use thingbuf::mpsc::{errors::TrySendError, Receiver, Sender};
use thiserror::Error;
Expand Down Expand Up @@ -189,10 +185,7 @@ pub struct Readline {
raw_term: Stdout,
event_stream: EventStream, // Stream of events
line_receiver: Receiver<Vec<u8>>,

line: LineState, // Current line

history_sender: mpsc::UnboundedSender<String>,
}

impl Readline {
Expand All @@ -203,14 +196,12 @@ impl Readline {
terminal::enable_raw_mode()?;

let line = LineState::new(prompt, terminal::size()?);
let history_sender = line.history.sender.clone();

let mut readline = Readline {
raw_term: stdout(),
event_stream: EventStream::new(),
line_receiver,
line,
history_sender,
};
readline.line.render(&mut readline.raw_term)?;
readline.raw_term.queue(terminal::EnableLineWrap)?;
Expand Down Expand Up @@ -240,8 +231,7 @@ impl Readline {

/// Set maximum history length. The default length is 1000.
pub fn set_max_history(&mut self, max_size: usize) {
self.line.history.max_size = max_size;
self.line.history.entries.truncate(max_size);
self.line.history.set_max_size(max_size);
}

/// Set whether the input line should remain on the screen after
Expand Down Expand Up @@ -297,14 +287,56 @@ impl Readline {
},
None => return Err(ReadlineError::Closed),
},
_ = self.line.history.update().fuse() => {}
}
}
}

/// Add a line to the input history
pub fn add_history_entry(&mut self, entry: String) -> Option<()> {
self.history_sender.unbounded_send(entry).ok()
self.line.history.add_entry(entry);
// Return value to keep compatibility with previous API.
Some(())
}

/// Returns the entries of the history in the order they were added in.
pub fn get_history_entries(&self) -> &VecDeque<String> {
self.line.history.get_entries()
}

/// Replaces the current history.
pub fn set_history_entries(&mut self, entries: impl IntoIterator<Item = String>) {
self.line.history.set_entries(entries);
}

/// Clears the current history.
pub fn clear_history(&mut self) {
self.set_history_entries([]);
}

/// Saves the history as a plain text file.
pub fn save_history(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);

for line in self.get_history_entries() {
writeln!(writer, "{line}")?;
}

Ok(())
}

/// Loads the history from a plain text file.
pub fn load_history(&mut self, path: impl AsRef<Path>) -> std::io::Result<()> {
let file = File::open(path)?;
let reader = BufReader::new(file);

self.clear_history();

for line in reader.lines() {
self.add_history_entry(line?);
}

Ok(())
}
}

Expand Down