use crate::command_input::{CommandInput, Move};
use crate::history::History;
use crate::fixed_length_grapheme_string::FixedLengthGraphemeString;
use crate::history::Command;
use crate::history_cleaner;
use crate::settings::Settings;
use crate::settings::{InterfaceView, KeyScheme};
use chrono::{Duration, TimeZone, Utc};
use humantime::format_duration;
use std::io::{stdin, stdout, Write};
use termion::color;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use termion::{clear, cursor, terminal_size};
pub struct Interface<'a> {
history: &'a History,
settings: &'a Settings,
input: CommandInput,
selection: usize,
matches: Vec<Command>,
debug: bool,
run: bool,
delete_requests: Vec<String>,
menu_mode: MenuMode,
in_vim_insert_mode: bool,
}
pub struct SelectionResult {
pub run: bool,
pub selection: Option<String>,
pub delete_requests: Vec<String>,
}
pub enum MoveSelection {
Up,
Down,
}
#[derive(PartialEq)]
pub enum MenuMode {
Normal,
ConfirmDelete,
}
impl MenuMode {
fn text(&self, interface: &Interface) -> &str {
match *self {
MenuMode::Normal => match interface.settings.key_scheme {
KeyScheme::Emacs => "McFly | ESC - Exit | ⏎ - Run | TAB - Edit | F2 - Delete",
KeyScheme::Vim => {
if interface.in_vim_insert_mode {
"McFly (Ins) | ESC - Cmd | ⏎ - Run | TAB - Edit | F2 - Delete"
} else {
"McFly (Cmd) | ESC - Exit | ⏎ - Run | TAB - Edit | F2 - Delete"
}
}
},
MenuMode::ConfirmDelete => "Delete selected command from the history? (Y/N)",
}
}
fn bg(&self) -> String {
match *self {
MenuMode::Normal => color::Bg(color::LightBlue).to_string(),
MenuMode::ConfirmDelete => color::Bg(color::Red).to_string(),
}
}
}
const PROMPT_LINE_INDEX: u16 = 3;
const INFO_LINE_INDEX: u16 = 1;
const RESULTS_TOP_INDEX: u16 = 5;
impl<'a> Interface<'a> {
pub fn new(settings: &'a Settings, history: &'a History) -> Interface<'a> {
Interface {
history,
settings,
input: CommandInput::from(settings.command.to_owned()),
selection: 0,
matches: Vec::new(),
debug: settings.debug,
run: false,
delete_requests: Vec::new(),
menu_mode: MenuMode::Normal,
in_vim_insert_mode: true,
}
}
pub fn display(&mut self) -> SelectionResult {
self.build_cache_table();
self.select();
let command = self.input.command.to_owned();
if command.chars().any(|c| !c.is_whitespace()) {
self.history.record_selected_from_ui(
&command,
&self.settings.session_id,
&self.settings.dir,
);
SelectionResult {
run: self.run,
selection: Some(command),
delete_requests: self.delete_requests.split_off(0),
}
} else {
SelectionResult {
run: self.run,
selection: None,
delete_requests: self.delete_requests.split_off(0),
}
}
}
fn build_cache_table(&self) {
self.history.build_cache_table(
&self.settings.dir.to_owned(),
&Some(self.settings.session_id.to_owned()),
None,
None,
None,
self.settings.limit.to_owned(),
);
}
fn menubar<W: Write>(&self, screen: &mut W) {
if !self.settings.disable_menu {
let (width, _height): (u16, u16) = terminal_size().unwrap();
write!(
screen,
"{hide}{cursor}{clear}{fg}{bg}{text:width$}{reset_bg}",
hide = cursor::Hide,
fg = color::Fg(color::LightWhite),
bg = self.menu_mode.bg(),
cursor = cursor::Goto(1, self.info_line_index()),
clear = clear::CurrentLine,
text = self.menu_mode.text(self),
reset_bg = color::Bg(color::Reset),
width = width as usize
)
.unwrap();
screen.flush().unwrap();
}
}
fn prompt<W: Write>(&self, screen: &mut W) {
let prompt_line_index = self.prompt_line_index();
write!(
screen,
"{}{}{}$ {}",
if self.settings.lightmode {
color::Fg(color::Black).to_string()
} else {
color::Fg(color::LightWhite).to_string()
},
cursor::Goto(1, self.prompt_line_index()),
clear::CurrentLine,
self.input
)
.unwrap();
write!(
screen,
"{}{}",
cursor::Goto(self.input.cursor as u16 + 3, prompt_line_index),
cursor::Show
)
.unwrap();
screen.flush().unwrap();
}
fn debug_cursor<W: Write>(&self, screen: &mut W) {
let result_top_index = self.result_top_index();
write!(
screen,
"{}{}",
cursor::Hide,
cursor::Goto(0, result_top_index + self.settings.results + 1)
)
.unwrap();
screen.flush().unwrap();
}
fn results<W: Write>(&mut self, screen: &mut W) {
let result_top_index = self.result_top_index();
write!(
screen,
"{}{}{}",
cursor::Hide,
cursor::Goto(1, result_top_index),
clear::All
)
.unwrap();
let (width, _height): (u16, u16) = terminal_size().unwrap();
if !self.matches.is_empty() && self.selection > self.matches.len() - 1 {
self.selection = self.matches.len() - 1;
}
for (index, command) in self.matches.iter().enumerate() {
let mut fg = if self.settings.lightmode {
color::Fg(color::Black).to_string()
} else {
color::Fg(color::LightWhite).to_string()
};
let mut highlight = if self.settings.lightmode {
color::Fg(color::Blue).to_string()
} else {
color::Fg(color::Green).to_string()
};
let mut bg = color::Bg(color::Reset).to_string();
if index == self.selection {
if self.settings.lightmode {
fg = color::Fg(color::LightWhite).to_string();
bg = color::Bg(color::LightBlack).to_string();
highlight = color::Fg(color::White).to_string();
} else {
fg = color::Fg(color::Black).to_string();
bg = color::Bg(color::LightWhite).to_string();
highlight = color::Fg(color::Green).to_string();
}
}
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
)
)
.unwrap();
if command.last_run.is_some() {
write!(
screen,
"{}",
cursor::Goto(
width - 9,
(command_line_index as i16 + result_top_index as i16) as u16
)
)
.unwrap();
let duration = &format_duration(
Duration::minutes(
Utc::now()
.signed_duration_since(Utc.timestamp(command.last_run.unwrap(), 0))
.num_minutes(),
)
.to_std()
.unwrap(),
)
.to_string()
.split(' ')
.take(2)
.map(|s| {
s.replace("years", "y")
.replace("year", "y")
.replace("months", "mo")
.replace("month", "mo")
.replace("days", "d")
.replace("day", "d")
.replace("hours", "h")
.replace("hour", "h")
.replace("minutes", "m")
.replace("minute", "m")
.replace("0s", "< 1m")
})
.collect::<Vec<String>>()
.join(" ");
let highlight = if self.settings.lightmode {
color::Fg(color::Blue).to_string()
} else {
color::Fg(color::LightBlue).to_string()
};
write!(screen, "{}", highlight).unwrap();
write!(screen, "{:>9}", duration).unwrap();
}
write!(screen, "{}", color::Bg(color::Reset)).unwrap();
write!(screen, "{}", color::Fg(color::Reset)).unwrap();
}
screen.flush().unwrap();
}
#[allow(unused)]
fn debug<W: Write, S: Into<String>>(&self, screen: &mut W, s: S) {
write!(
screen,
"{}{}{}",
cursor::Goto(1, 2),
clear::CurrentLine,
s.into()
)
.unwrap();
screen.flush().unwrap();
}
fn move_selection(&mut self, direction: MoveSelection) {
if self.is_screen_view_bottom() {
match direction {
MoveSelection::Up => {
self.selection += 1;
}
MoveSelection::Down => {
if self.selection > 0 {
self.selection -= 1;
}
}
}
} else {
match direction {
MoveSelection::Up => {
if self.selection > 0 {
self.selection -= 1;
}
}
MoveSelection::Down => {
self.selection += 1;
}
}
}
}
fn accept_selection(&mut self) {
if !self.matches.is_empty() {
self.input.set(&self.matches[self.selection].cmd);
}
}
fn confirm(&mut self, confirmation: bool) {
if confirmation {
if let MenuMode::ConfirmDelete = self.menu_mode {
self.delete_selection()
}
}
self.menu_mode = MenuMode::Normal;
}
fn delete_selection(&mut self) {
if !self.matches.is_empty() {
{
let command = &self.matches[self.selection];
history_cleaner::clean(self.settings, self.history, &command.cmd);
self.delete_requests.push(command.cmd.clone());
}
self.build_cache_table();
self.refresh_matches();
}
}
fn refresh_matches(&mut self) {
self.selection = 0;
self.matches = self.history.find_matches(
&self.input.command,
self.settings.results as i16,
self.settings.fuzzy,
&self.settings.result_sort,
);
}
fn select(&mut self) {
let stdin = stdin();
let mut screen = AlternateScreen::from(stdout().into_raw_mode().unwrap());
write!(screen, "{}", clear::All).unwrap();
self.refresh_matches();
self.results(&mut screen);
self.menubar(&mut screen);
self.prompt(&mut screen);
for c in stdin.keys() {
self.debug_cursor(&mut screen);
if self.menu_mode != MenuMode::Normal {
match c.unwrap() {
Key::Ctrl('c')
| Key::Ctrl('d')
| Key::Ctrl('g')
| Key::Ctrl('z')
| Key::Ctrl('r') => {
self.run = false;
self.input.clear();
break;
}
Key::Char('y') | Key::Char('Y') => {
self.confirm(true);
}
Key::Char('n') | Key::Char('N') | Key::Esc => {
self.confirm(false);
}
_ => {}
}
} else {
let early_out = match self.settings.key_scheme {
KeyScheme::Emacs => self.select_with_emacs_key_scheme(c.unwrap()),
KeyScheme::Vim => self.select_with_vim_key_scheme(c.unwrap()),
};
if early_out {
break;
}
}
self.results(&mut screen);
self.menubar(&mut screen);
self.prompt(&mut screen);
}
write!(screen, "{}{}", clear::All, cursor::Show).unwrap();
}
fn select_with_emacs_key_scheme(&mut self, k: Key) -> bool {
match k {
Key::Char('\n') | Key::Char('\r') | Key::Ctrl('j') => {
self.run = true;
self.accept_selection();
return true;
}
Key::Char('\t') => {
self.run = false;
self.accept_selection();
return true;
}
Key::Ctrl('c') | Key::Ctrl('g') | Key::Ctrl('z') | Key::Esc | Key::Ctrl('r') => {
self.run = false;
self.input.clear();
return true;
}
Key::Ctrl('b') => self.input.move_cursor(Move::Backward),
Key::Ctrl('f') => self.input.move_cursor(Move::Forward),
Key::Ctrl('a') => self.input.move_cursor(Move::BOL),
Key::Ctrl('e') => self.input.move_cursor(Move::EOL),
Key::Ctrl('w') | Key::Alt('\x08') | Key::Alt('\x7f') => {
self.input.delete(Move::BackwardWord);
self.refresh_matches();
}
Key::Alt('d') => {
self.input.delete(Move::ForwardWord);
self.refresh_matches();
}
Key::Ctrl('v') => {
self.debug = !self.debug;
}
Key::Alt('b') => self.input.move_cursor(Move::BackwardWord),
Key::Alt('f') => self.input.move_cursor(Move::ForwardWord),
Key::Left => self.input.move_cursor(Move::Backward),
Key::Right => self.input.move_cursor(Move::Forward),
Key::Up | Key::PageUp | Key::Ctrl('p') => self.move_selection(MoveSelection::Up),
Key::Down | Key::PageDown | Key::Ctrl('n') => self.move_selection(MoveSelection::Down),
Key::Ctrl('k') => {
self.input.delete(Move::EOL);
self.refresh_matches();
}
Key::Ctrl('u') => {
self.input.delete(Move::BOL);
self.refresh_matches();
}
Key::Backspace | Key::Ctrl('h') => {
self.input.delete(Move::Backward);
self.refresh_matches();
}
Key::Delete | Key::Ctrl('d') => {
self.input.delete(Move::Forward);
self.refresh_matches();
}
Key::Home => self.input.move_cursor(Move::BOL),
Key::End => self.input.move_cursor(Move::EOL),
Key::Char(c) => {
self.input.insert(c);
self.refresh_matches();
}
Key::F(2) => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
} else {
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
_ => {}
}
false
}
fn select_with_vim_key_scheme(&mut self, k: Key) -> bool {
if self.in_vim_insert_mode {
match k {
Key::Char('\n') | Key::Char('\r') | Key::Ctrl('j') => {
self.run = true;
self.accept_selection();
return true;
}
Key::Char('\t') => {
self.run = false;
self.accept_selection();
return true;
}
Key::Ctrl('c') | Key::Ctrl('g') | Key::Ctrl('z') | Key::Ctrl('r') => {
self.run = false;
self.input.clear();
return true;
}
Key::Left => self.input.move_cursor(Move::Backward),
Key::Right => self.input.move_cursor(Move::Forward),
Key::Up | Key::PageUp | Key::Ctrl('u') | Key::Ctrl('p') => {
self.move_selection(MoveSelection::Up)
}
Key::Down | Key::PageDown | Key::Ctrl('d') | Key::Ctrl('n') => {
self.move_selection(MoveSelection::Down)
}
Key::Esc => self.in_vim_insert_mode = false,
Key::Backspace => {
self.input.delete(Move::Backward);
self.refresh_matches();
}
Key::Delete => {
self.input.delete(Move::Forward);
self.refresh_matches();
}
Key::Home => self.input.move_cursor(Move::BOL),
Key::End => self.input.move_cursor(Move::EOL),
Key::Char(c) => {
self.input.insert(c);
self.refresh_matches();
}
Key::F(2) => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
} else {
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
_ => {}
}
} else {
match k {
Key::Char('\n') | Key::Char('\r') | Key::Ctrl('j') => {
self.run = true;
self.accept_selection();
return true;
}
Key::Char('\t') => {
self.run = false;
self.accept_selection();
return true;
}
Key::Ctrl('c')
| Key::Ctrl('g')
| Key::Ctrl('z')
| Key::Esc
| Key::Char('q')
| Key::Ctrl('r') => {
self.run = false;
self.input.clear();
return true;
}
Key::Left | Key::Char('h') => self.input.move_cursor(Move::Backward),
Key::Right | Key::Char('l') => self.input.move_cursor(Move::Forward),
Key::Up | Key::PageUp | Key::Char('k') | Key::Ctrl('u') => self.move_selection(MoveSelection::Up),
Key::Down | Key::PageDown | Key::Char('j') | Key::Ctrl('d') => self.move_selection(MoveSelection::Down),
Key::Char('b') | Key::Char('e') => self.input.move_cursor(Move::BackwardWord),
Key::Char('w') => self.input.move_cursor(Move::ForwardWord),
Key::Char('0') | Key::Char('^') => self.input.move_cursor(Move::BOL),
Key::Char('$') => self.input.move_cursor(Move::EOL),
Key::Char('i') | Key::Char('a') => self.in_vim_insert_mode = true,
Key::Backspace => {
self.input.delete(Move::Backward);
self.refresh_matches();
}
Key::Delete | Key::Char('x') => {
self.input.delete(Move::Forward);
self.refresh_matches();
}
Key::Home => self.input.move_cursor(Move::BOL),
Key::End => self.input.move_cursor(Move::EOL),
Key::Char(_c) => {
}
Key::F(2) => {
if !self.matches.is_empty() {
if self.settings.delete_without_confirm {
self.delete_selection();
}else{
self.menu_mode = MenuMode::ConfirmDelete;
}
}
}
_ => {}
}
}
false
}
fn truncate_for_display(
command: &Command,
search: &str,
width: u16,
highlight_color: String,
base_color: String,
debug: bool,
) -> String {
let mut prev: usize = 0;
let debug_space = if debug { 90 } else { 0 };
let max_grapheme_length = if width > debug_space {
width - debug_space - 9
} else {
11
};
let mut out = FixedLengthGraphemeString::empty(max_grapheme_length);
if !search.is_empty() {
for (start, end) in &command.match_bounds {
if prev != *start {
out.push_grapheme_str(&command.cmd[prev..*start]);
}
out.push_str(&highlight_color);
out.push_grapheme_str(&command.cmd[*start..*end]);
out.push_str(&base_color);
prev = *end;
}
}
if prev != command.cmd.len() {
out.push_grapheme_str(&command.cmd[prev..]);
}
if debug {
out.max_grapheme_length += debug_space;
out.push_grapheme_str(" ");
out.push_str(&format!("{}", color::Fg(color::LightBlue)));
out.push_grapheme_str(format!("rnk: {:.*} ", 2, command.rank));
out.push_grapheme_str(format!("age: {:.*} ", 2, command.features.age_factor));
out.push_grapheme_str(format!("lng: {:.*} ", 2, command.features.length_factor));
out.push_grapheme_str(format!("ext: {:.*} ", 0, command.features.exit_factor));
out.push_grapheme_str(format!(
"r_ext: {:.*} ",
0, command.features.recent_failure_factor
));
out.push_grapheme_str(format!("dir: {:.*} ", 3, command.features.dir_factor));
out.push_grapheme_str(format!(
"s_dir: {:.*} ",
3, command.features.selected_dir_factor
));
out.push_grapheme_str(format!("ovlp: {:.*} ", 3, command.features.overlap_factor));
out.push_grapheme_str(format!(
"i_ovlp: {:.*} ",
3, command.features.immediate_overlap_factor
));
out.push_grapheme_str(format!(
"occ: {:.*}",
2, command.features.occurrences_factor
));
out.push_grapheme_str(format!(
"s_occ: {:.*} ",
2, command.features.selected_occurrences_factor
));
out.push_str(&base_color);
}
out.string
}
fn result_top_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal_size().unwrap();
if self.is_screen_view_bottom() {
return height - RESULTS_TOP_INDEX;
}
RESULTS_TOP_INDEX
}
fn prompt_line_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal_size().unwrap();
if self.is_screen_view_bottom() {
return height - PROMPT_LINE_INDEX;
}
PROMPT_LINE_INDEX
}
fn info_line_index(&self) -> u16 {
let (_width, height): (u16, u16) = terminal_size().unwrap();
if self.is_screen_view_bottom() {
return height;
}
INFO_LINE_INDEX
}
fn command_line_index(&self, index: i16) -> i16 {
if self.is_screen_view_bottom() {
return -index;
}
index
}
fn is_screen_view_bottom(&self) -> bool {
self.settings.interface_view == InterfaceView::Bottom
}
}