diff --git a/mcfly.bash b/mcfly.bash index b6795b2e..ef1409b1 100644 --- a/mcfly.bash +++ b/mcfly.bash @@ -21,8 +21,7 @@ MCFLY_SESSION_ID="$(command dd if=/dev/urandom bs=256 count=1 2> /dev/null | LC_ export MCFLY_SESSION_ID # Find the binary -MCFLY_PATH=${MCFLY_PATH:-$(which mcfly)} -if [ -z "$MCFLY_PATH" ]; then +if [ -z "$(which mcfly)" ]; then echo "Cannot find the mcfly binary, please make sure that mcfly is in your path before sourcing mcfly.bash." return 1 fi @@ -44,15 +43,11 @@ function mcfly_prompt_command { command tail -n100 "${HISTFILE}" >| "${MCFLY_HISTORY}" fi - history -a "${MCFLY_HISTORY}" # Append history to $MCFLY_HISTORY. - # Run mcfly with the saved code. It will: - # * append commands to $HISTFILE, (~/.bash_history by default) - # for backwards compatibility and to load in new terminal sessions; - # * find the text of the last command in $MCFLY_HISTORY and save it to the database. - $MCFLY_PATH add --exit ${exit_code} --append-to-histfile - # Clear the in-memory history and reload it from $MCFLY_HISTORY - # (to remove instances of '#mcfly: ' from the local session history). - history -cr "${MCFLY_HISTORY}" + # Run mcfly with the last history entry on stdin. It will: + # * Append the command to $HISTFILE, (~/.bash_history by default) + # * Parse out the command and and save it to the database. + HISTTIMEFORMAT="%s:" history 1 | mcfly add --exit $exit_code --command-from-stdin + return ${exit_code} # Restore the original exit code by returning it. } diff --git a/src/interface.rs b/src/interface.rs index d11a3c3c..61c1d365 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -203,6 +203,7 @@ impl<'a> Interface<'a> { self.selection = self.matches.len() - 1; } + let mut line_offset = 0; for (index, command) in self.matches.iter().enumerate() { let mut fg = if self.settings.lightmode { color::Fg(color::Black).to_string() @@ -232,25 +233,31 @@ impl<'a> Interface<'a> { write!(screen, "{}{}", fg, bg).unwrap(); - let command_line_index = self.command_line_index(index as i16); - - write!( - screen, - "{}{}", - cursor::Goto( - 1, - (command_line_index as i16 + result_top_index as i16) as u16 - ), - Interface::truncate_for_display( - command, - &self.input.command, - width, - highlight, - fg, - self.debug + let command_display = Interface::truncate_for_display( + command, + &self.input.command, + width, + highlight, + fg, + self.debug, + ); + + let lines:Vec<&str> = command_display.lines().collect(); + let mut lines_count = 0; + + for line in self.reverse_if_bottom(lines.iter()) { + write!( + screen, + "{}{}", + cursor::Goto( + 1, + (self.command_line_index(line_offset as i16 + lines_count as i16) + result_top_index as i16) as u16 + ), + line ) - ) - .unwrap(); + .unwrap(); + lines_count += 1; + } if command.last_run.is_some() { write!( @@ -258,7 +265,7 @@ impl<'a> Interface<'a> { "{}", cursor::Goto( width - 9, - (command_line_index as i16 + result_top_index as i16) as u16 + (self.command_line_index(line_offset as i16) + result_top_index as i16) as u16 ) ) .unwrap(); @@ -303,6 +310,8 @@ impl<'a> Interface<'a> { write!(screen, "{}", color::Bg(color::Reset)).unwrap(); write!(screen, "{}", color::Fg(color::Reset)).unwrap(); + + line_offset += lines_count; } screen.flush().unwrap(); } @@ -725,6 +734,14 @@ impl<'a> Interface<'a> { index } + fn reverse_if_bottom(&self, iter: I) -> itertools::Either> { + if self.is_screen_view_bottom() { + itertools::Either::Right(iter.rev()) + } else { + itertools::Either::Left(iter) + } + } + fn is_screen_view_bottom(&self) -> bool { self.settings.interface_view == InterfaceView::Bottom } diff --git a/src/settings.rs b/src/settings.rs index 51b38011..4d960f35 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -177,6 +177,10 @@ impl Settings { .value_name("PATH") .help("The previous directory the user was in before running the command (default $OLDPWD)") .takes_value(true)) + .arg(Arg::with_name("command_from_stdin") + .long("command-from-stdin") + .help("Read the command from `history` (must have HISTTIMEFORMAT=\"%s:\") command piped in.") + .conflicts_with("command")) .arg(Arg::with_name("command") .help("The command that was run (default last line of $MCFLY_HISTORY file)") .value_name("COMMAND") @@ -349,6 +353,7 @@ impl Settings { if let Some(dir) = add_matches.value_of("directory") { settings.dir = dir.to_string(); } else { + // XXX: Why not just use std::env::current_dir here? settings.dir = env::var("PWD").unwrap_or_else(|err| { panic!( "McFly error: Unable to determine current directory ({})", @@ -365,6 +370,11 @@ impl Settings { if let Some(commands) = add_matches.values_of("command") { settings.command = commands.collect::>().join(" "); + } else if add_matches.is_present("command_from_stdin") { + let bash_history = + shell_history::read_from_bash_stdin().expect("Could not read from stdin"); + //ignore the other values for now + settings.command = bash_history.command; } else { settings.command = shell_history::last_history_line( &settings.mcfly_history, diff --git a/src/shell_history.rs b/src/shell_history.rs index 1d244e42..91b532f6 100644 --- a/src/shell_history.rs +++ b/src/shell_history.rs @@ -1,10 +1,12 @@ use crate::settings::HistoryFormat; use regex::Regex; +use regex::RegexBuilder; use std::env; use std::fmt; use std::fs; use std::fs::File; use std::fs::OpenOptions; +use std::io; use std::io::Read; use std::io::Write; use std::path::Path; @@ -222,6 +224,40 @@ pub fn append_history_entry(command: &HistoryCommand, path: &Path, debug: bool) } } +pub struct BashHistoryLine { + pub idx: u32, + pub edited: bool, + pub timestamp: u64, + pub command: String, +} + +pub fn read_from_bash_stdin() -> io::Result { + // See https://github.com/bminor/bash/blob/ce23728687ce9e584333367075c9deef413553fa/builtins/history.def#L386 + // First is the history index left-padded with space + // then either ' ' or '*' depending if the entry has been edited + // then a space '' + // then the timestamp in HISTTIMEFORMAT, which should be "%s:" + // then the command + // then a newline + let bash_history_regex = + RegexBuilder::new(r"^\s*(?P\d+)(?P[\* ]) (?P\d+):(?P.*)\n$") + .case_insensitive(false) + .dot_matches_new_line(true) + .build() + .unwrap(); + let mut full = String::new(); + io::stdin().read_to_string(&mut full)?; + let matches = bash_history_regex + .captures(&full) + .expect("History line did not match expected format"); + Ok(BashHistoryLine { + idx: matches.name("idx").unwrap().as_str().parse().unwrap(), + edited: matches.name("edit").unwrap().as_str() == "*", + timestamp: matches.name("timestamp").unwrap().as_str().parse().unwrap(), + command: matches.name("command").unwrap().as_str().to_string(), + }) +} + #[cfg(test)] mod tests { use super::has_leading_timestamp;