history.rs 9.21 KB
Newer Older
1 2 3
use super::*;

use std::collections::VecDeque;
Sehny's avatar
Sehny committed
4
use std::io::{BufReader, BufRead, Error, ErrorKind};
5 6 7 8
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::ops::Index;
use std::ops::IndexMut;
9 10 11 12 13
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::thread::{sleep, spawn, JoinHandle};
use std::time::Duration;
14

15 16
const DEFAULT_MAX_SIZE: usize = 1000;

17 18
/// Structure encapsulating command history
pub struct History {
Sehny's avatar
Sehny committed
19 20
    // TODO: this should eventually be private
    /// Vector of buffers to store history in
Sehny's avatar
Sehny committed
21
    pub buffers: VecDeque<Buffer>,
22
    /// Store a filename to save history into; if None don't save history
Sehny's avatar
Sehny committed
23
    file_name: Option<String>,
24
    /// Maximal number of buffers stored in the memory
25
    /// TODO: just make this public?
Sehny's avatar
Sehny committed
26
    max_size: usize,
27
    /// Maximal number of lines stored in the file
28
    // TODO: just make this public?
29 30 31 32 33 34 35
    max_file_size: Arc<AtomicUsize>,
    /// Handle to the background thread managing writes to the history file
    bg_handle: Option<JoinHandle<()>>,
    /// Signals the background thread to stop when dropping the struct
    bg_stop: Arc<AtomicBool>,
    /// Sends commands to write to the history file
    sender: Sender<(Buffer, String)>,
36 37 38

    // TODO set from environment variable?
    pub append_duplicate_entries: bool,
39 40 41
}

impl History {
42 43 44 45 46 47 48 49 50 51 52
    /// It's important to execute this function before exiting your program, as it will
    /// ensure that all history data has been written to the disk.
    pub fn commit_history(&mut self) {
        // Signal the background thread to stop
        self.bg_stop.store(true, Ordering::Relaxed);
        // Wait for the background thread to stop
        if let Some(handle) = self.bg_handle.take() {
            let _ = handle.join();
        }
    }

53 54
    /// Create new History structure.
    pub fn new() -> History {
55 56 57 58 59 60
        let max_file_size = Arc::new(AtomicUsize::new(DEFAULT_MAX_SIZE));
        let bg_stop = Arc::new(AtomicBool::new(false));
        let (sender, receiver) = channel();

        let stop_signal = bg_stop.clone();
        let max_size = max_file_size.clone();
61
        History {
62
            buffers: VecDeque::with_capacity(DEFAULT_MAX_SIZE),
63
            file_name: None,
64 65 66 67 68 69 70
            sender: sender,
            bg_handle: Some(spawn(move || {
                while !stop_signal.load(Ordering::Relaxed) {
                    if let Ok((command, filepath)) = receiver.try_recv() {
                        let max_file_size = max_size.load(Ordering::Relaxed);
                        let _ = write_to_disk(max_file_size, &command, &filepath);
                    }
Michael Aaron Murphy's avatar
Michael Aaron Murphy committed
71
                    sleep(Duration::from_millis(100));
72 73
                }

Michael Aaron Murphy's avatar
Michael Aaron Murphy committed
74 75
                // Deplete the receiver of commands to write, before exiting the thread.
                while let Ok((command, filepath)) = receiver.try_recv() {
76 77 78 79 80
                    let max_file_size = max_size.load(Ordering::Relaxed);
                    let _ = write_to_disk(max_file_size, &command, &filepath);
                }
            })),
            bg_stop: bg_stop,
81
            max_size: DEFAULT_MAX_SIZE,
82
            max_file_size: max_file_size,
83
            append_duplicate_entries: false,
84 85 86 87 88
        }
    }

    /// Number of items in history.
    pub fn len(&self) -> usize {
Sehny's avatar
Sehny committed
89
        self.buffers.len()
90 91 92
    }

    /// Add a command to the history buffer and remove the oldest commands when the max history
93 94
    /// size has been met. If writing to the disk is enabled, this function will be used for
    /// logging history to the designated history file.
95
    pub fn push(&mut self, new_item: Buffer) -> io::Result<()> {
96 97
        self.file_name
            .as_ref()
98 99 100
            .map(|name| {
                let _ = self.sender.send((new_item.clone(), name.to_owned()));
            });
101

102 103
        // buffers[0] is the oldest entry
        // the new entry goes to the end
104 105 106 107 108
        if !self.append_duplicate_entries &&
            self.buffers.back().map(|b| b.to_string()) == Some(new_item.to_string())
        {
            return Ok(());
        }
Michael Aaron Murphy's avatar
Michael Aaron Murphy committed
109

110 111 112 113
        self.buffers.push_back(new_item);
        while self.buffers.len() > self.max_size {
            self.buffers.pop_front();
        }
114
        Ok(())
115 116
    }

117 118
    /// Go through the history and try to find a buffer which starts the same as the new buffer
    /// given to this function as argument.
119 120 121 122 123
    pub fn get_newest_match<'a, 'b>(
        &'a self,
        curr_position: Option<usize>,
        new_buff: &'b Buffer,
    ) -> Option<&'a Buffer> {
124 125 126
        let pos = curr_position.unwrap_or(self.buffers.len());
        for iter in (0..pos).rev() {
            if let Some(tested) = self.buffers.get(iter) {
Sehny's avatar
Sehny committed
127
                if tested.starts_with(new_buff) {
128
                    return self.buffers.get(iter);
129 130 131 132 133 134
                }
            }
        }
        None
    }

MovingtoMars's avatar
MovingtoMars committed
135 136 137 138 139 140 141 142
    /// Get the history file name.
    pub fn file_name(&self) -> Option<&str> {
        match self.file_name {
            Some(ref s) => Some(&s[..]),
            None => None,
        }
    }

143
    /// Set history file name. At the same time enable history.
MovingtoMars's avatar
MovingtoMars committed
144 145
    pub fn set_file_name(&mut self, name: Option<String>) {
        self.file_name = name;
146 147
    }

148
    /// Set maximal number of buffers stored in memory
149 150 151 152
    pub fn set_max_size(&mut self, size: usize) {
        self.max_size = size;
    }

153 154
    /// Set maximal number of entries in history file
    pub fn set_max_file_size(&mut self, size: usize) {
155
        self.max_file_size.store(size, Ordering::Relaxed);
156 157
    }

Sehny's avatar
Sehny committed
158 159
    /// Load history from given file name
    pub fn load_history(&mut self) -> io::Result<()> {
Sehny's avatar
Sehny committed
160 161
        let file_name = match self.file_name.clone() {
            Some(name) => name,
162 163 164 165 166 167
            None => {
                return Err(Error::new(
                    ErrorKind::Other,
                    "Liner: file name not specified",
                ))
            }
Sehny's avatar
Sehny committed
168 169 170 171 172 173 174 175 176 177 178
        };
        let file = try!(OpenOptions::new().read(true).open(file_name));
        let reader = BufReader::new(file);
        for line in reader.lines() {
            match line {
                Ok(line) => self.buffers.push_back(Buffer::from(line)),
                Err(_) => break,
            }
        }
        Ok(())
    }
179 180 181 182 183 184
}

impl Index<usize> for History {
    type Output = Buffer;

    fn index(&self, index: usize) -> &Buffer {
Sehny's avatar
Sehny committed
185
        &self.buffers[index]
186 187 188 189 190
    }
}

impl IndexMut<usize> for History {
    fn index_mut(&mut self, index: usize) -> &mut Buffer {
Sehny's avatar
Sehny committed
191
        &mut self.buffers[index]
192
    }
Sehny's avatar
Sehny committed
193
}
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252

/// Perform write operation. If the history file does not exist, it will be created.
/// This function is not part of the public interface.
/// XXX: include more information in the file (like fish does)
fn write_to_disk(max_file_size: usize, new_item: &Buffer, file_name: &str) -> io::Result<()> {
    let ret = match OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open(file_name) {
        Ok(mut file) => {
            // Determine the number of commands stored and the file length.
            let (file_length, commands_stored) = {
                let mut commands_stored = 0;
                let mut file_length = 0;
                let file = File::open(file_name).unwrap();
                for byte in file.bytes() {
                    if byte.unwrap_or(b' ') == b'\n' {
                        commands_stored += 1;
                    }
                    file_length += 1;
                }
                (file_length, commands_stored)
            };

            // If the max history file size has been reached, truncate the file so that only
            // N amount of commands are listed. To truncate the file, the seek point will be
            // discovered by counting the number of bytes until N newlines have been found and
            // then the file will be seeked to that point, copying all data after and rewriting
            // the file with the first N lines removed.
            if commands_stored >= max_file_size {
                let seek_point = {
                    let commands_to_delete = commands_stored - max_file_size + 1;
                    let mut matched = 0;
                    let mut bytes = 0;
                    let file = File::open(file_name).unwrap();
                    for byte in file.bytes() {
                        if byte.unwrap_or(b' ') == b'\n' {
                            matched += 1;
                        }
                        bytes += 1;
                        if matched == commands_to_delete {
                            break;
                        }
                    }
                    bytes as u64
                };

                try!(file.seek(SeekFrom::Start(seek_point)));
                let mut buffer: Vec<u8> = Vec::with_capacity(file_length - seek_point as usize);
                try!(file.read_to_end(&mut buffer));
                try!(file.set_len(0));
                try!(io::copy(&mut buffer.as_slice(), &mut file));
            }

            // Seek to end for appending
            try!(file.seek(SeekFrom::End(0)));
            // Write the command to the history file.
            try!(file.write_all(String::from(new_item.clone()).as_bytes()));
Michael Aaron Murphy's avatar
Michael Aaron Murphy committed
253 254
            try!(file.write_all(b"\n"));
            file.flush()?;
255 256 257 258 259 260 261

            Ok(())
        }
        Err(message) => Err(message),
    };
    ret
}