From 4978c282dbc0d1eff0c2d00ec7379912cce0af85 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Tue, 31 Jan 2017 01:37:56 -0330 Subject: Refactor into modules with tests --- src/action.rs | 69 +++++++ src/application.rs | 304 +++++++++++++++++++++++++++++ src/git_interactive.rs | 134 +++++++++++++ src/line.rs | 99 ++++++++++ src/main.rs | 516 +++---------------------------------------------- src/mocks.rs | 55 ++++++ src/utils.rs | 14 ++ src/window.rs | 243 +++++++++++++++++++++++ 8 files changed, 949 insertions(+), 485 deletions(-) create mode 100644 src/action.rs create mode 100644 src/application.rs create mode 100644 src/git_interactive.rs create mode 100644 src/line.rs create mode 100644 src/mocks.rs create mode 100644 src/utils.rs create mode 100644 src/window.rs (limited to 'src') diff --git a/src/action.rs b/src/action.rs new file mode 100644 index 0000000..8ecafad --- /dev/null +++ b/src/action.rs @@ -0,0 +1,69 @@ + +#[derive(PartialEq, Debug)] +pub enum Action { + Pick, + Reword, + Edit, + Squash, + Fixup, + Drop +} + +pub fn action_from_str(s: &str) -> Result { + match s { + "pick" | "p" => Ok(Action::Pick), + "reword" | "r" => Ok(Action::Reword), + "edit" | "e" => Ok(Action::Edit), + "squash" | "s" => Ok(Action::Squash), + "fixup" | "f" => Ok(Action::Fixup), + "drop" | "d" => Ok(Action::Drop), + _ => Err(format!("Invalid action: {}", s)) + } +} + +pub fn action_to_str(action: &Action) -> String { + String::from(match *action { + Action::Pick => "pick", + Action::Reword => "reword", + Action::Edit => "edit", + Action::Squash => "squash", + Action::Fixup => "fixup", + Action::Drop => "drop" + }) +} + +#[cfg(test)] +mod tests { + use super::{ + Action, + action_from_str, + action_to_str + }; + + #[test] + fn action_to_str_all() { + assert_eq!(action_to_str(&Action::Pick), "pick"); + assert_eq!(action_to_str(&Action::Reword), "reword"); + assert_eq!(action_to_str(&Action::Edit), "edit"); + assert_eq!(action_to_str(&Action::Squash), "squash"); + assert_eq!(action_to_str(&Action::Fixup), "fixup"); + assert_eq!(action_to_str(&Action::Drop), "drop"); + } + + #[test] + fn action_from_str_all() { + assert_eq!(action_from_str("pick"), Ok(Action::Pick)); + assert_eq!(action_from_str("p"), Ok(Action::Pick)); + assert_eq!(action_from_str("reword"), Ok(Action::Reword)); + assert_eq!(action_from_str("r"), Ok(Action::Reword)); + assert_eq!(action_from_str("edit"), Ok(Action::Edit)); + assert_eq!(action_from_str("e"), Ok(Action::Edit)); + assert_eq!(action_from_str("squash"), Ok(Action::Squash)); + assert_eq!(action_from_str("s"), Ok(Action::Squash)); + assert_eq!(action_from_str("fixup"), Ok(Action::Fixup)); + assert_eq!(action_from_str("f"), Ok(Action::Fixup)); + assert_eq!(action_from_str("drop"), Ok(Action::Drop)); + assert_eq!(action_from_str("d"), Ok(Action::Drop)); + } +} + diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..ad61617 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,304 @@ +use action::Action; +use git_interactive::GitInteractive; + +use window::{ + Input, + Window +}; + +const EXIT_CODE_GOOD: i32 = 0; +const EXIT_CODE_WRITE_ERROR: i32 = 8; + +pub enum State { + List, + Help +} + +pub struct Application { + pub exit_code: Option, + window: Window, + git_interactive: GitInteractive, + state: State +} + +impl Application { + pub fn new(git_interactive: GitInteractive, window: Window) -> Self { + Application { + git_interactive: git_interactive, + window: window, + exit_code: None, + state: State::List + } + } + + pub fn process_input(&mut self) { + match self.state { + State::List => self.process_list_input(), + State::Help => self.process_help_input() + } + } + + pub fn draw(&self) { + match self.state { + State::List => { + self.window.draw( + self.git_interactive.get_lines(), + self.git_interactive.get_selected_line_index() + ); + }, + State::Help => { + self.window.draw_help(); + } + } + } + + fn abort(&mut self) { + self.git_interactive.clear(); + self.finish(); + } + + fn finish(&mut self) { + self.exit_code = Some(EXIT_CODE_GOOD); + } + + pub fn end(&mut self) -> Result<(), String>{ + self.window.end(); + match self.git_interactive.write_file() { + Ok(_) => {}, + Err(msg) => { + self.exit_code = Some(EXIT_CODE_WRITE_ERROR); + return Err(msg) + } + } + Ok(()) + } + + fn process_help_input(&mut self) { + self.window.window.getch(); + self.state = State::List; + } + + fn process_list_input(&mut self) { + match self.window.window.getch() { + Some(Input::Character(c)) if c == '?' => { + self.state = State::Help; + }, + Some(Input::Character(c)) + if (c == 'Q') || (c == 'q' && self.window.confirm("Are you sure you want to abort")) + => self.abort(), + Some(Input::Character(c)) + if (c == 'W') || (c == 'w' && self.window.confirm("Are you sure you want to rebase")) + => self.finish(), + Some(Input::Character(c)) + if c == 'p' => self.git_interactive.set_selected_line_action(Action::Pick), + Some(Input::Character(c)) + if c == 'r' => self.git_interactive.set_selected_line_action(Action::Reword), + Some(Input::Character(c)) + if c == 'e' => self.git_interactive.set_selected_line_action(Action::Edit), + Some(Input::Character(c)) + if c == 's' => self.git_interactive.set_selected_line_action(Action::Squash), + Some(Input::Character(c)) + if c == 'f' => self.git_interactive.set_selected_line_action(Action::Fixup), + Some(Input::Character(c)) + if c == 'd' => self.git_interactive.set_selected_line_action(Action::Drop), + Some(Input::Character(c)) if c == 'j' => { + self.git_interactive.swap_selected_down(); + self.reset_top(); + }, + Some(Input::Character(c)) if c == 'k' => { + self.git_interactive.swap_selected_up(); + self.reset_top(); + }, + Some(Input::KeyDown) => { + self.git_interactive.move_cursor_down(1); + self.reset_top(); + }, + Some(Input::KeyUp) => { + self.git_interactive.move_cursor_up(1); + self.reset_top(); + }, + Some(Input::KeyPPage) => { + self.git_interactive.move_cursor_up(5); + self.reset_top(); + }, + Some(Input::KeyNPage) => { + self.git_interactive.move_cursor_down(5); + self.reset_top(); + }, + Some(Input::KeyResize) => self.reset_top(), + _ => {} + } + () + } + + fn reset_top(&mut self) { + self.window.set_top( + self.git_interactive.get_lines().len(), + *self.git_interactive.get_selected_line_index() + ) + } +} + + +#[cfg(test)] +mod tests { + use super::{ + Application, + }; + use git_interactive::GitInteractive; + use window::{ + Window, + Input + }; + use action::Action; + + #[test] + fn application_read_all_actions() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + assert_eq!(app.git_interactive.get_lines().len(), 12); + } + + #[test] + fn application_scroll_basic() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-long.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::KeyDown; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 2); + app.window.window.next_char = Input::KeyUp; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 1); + app.window.window.next_char = Input::KeyNPage; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 6); + app.window.window.next_char = Input::KeyPPage; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 1); + } + + #[test] + fn application_scroll_limits() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-short.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::KeyUp; + app.process_input(); + app.process_input(); + app.window.window.next_char = Input::KeyPPage; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 1); + app.window.window.next_char = Input::KeyDown; + app.process_input(); + app.process_input(); + app.process_input(); + app.process_input(); + app.process_input(); + app.window.window.next_char = Input::KeyNPage; + app.process_input(); + assert_eq!(*app.git_interactive.get_selected_line_index(), 3); + } + + #[test] + fn application_set_pick() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + // first item is already pick + app.window.window.next_char = Input::KeyDown; + app.process_input(); + app.window.window.next_char = Input::Character('p'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[1].get_action(), Action::Pick); + } + + #[test] + fn application_set_reword() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('r'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_action(), Action::Reword); + } + + #[test] + fn application_set_edit() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('e'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_action(), Action::Edit); + } + + #[test] + fn application_set_squash() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('s'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_action(), Action::Squash); + } + + #[test] + fn application_set_drop() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('d'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_action(), Action::Drop); + } + + #[test] + fn application_swap_down() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('j'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_hash(), "bbb"); + assert_eq!(*app.git_interactive.get_lines()[1].get_hash(), "aaa"); + assert_eq!(*app.git_interactive.get_selected_line_index(), 2); + } + + #[test] + fn application_swap_up() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::KeyDown; + app.process_input(); + app.window.window.next_char = Input::Character('k'); + app.process_input(); + assert_eq!(*app.git_interactive.get_lines()[0].get_hash(), "bbb"); + assert_eq!(*app.git_interactive.get_lines()[1].get_hash(), "aaa"); + assert_eq!(*app.git_interactive.get_selected_line_index(), 1); + } + + #[test] + fn application_quit() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('Q'); + app.process_input(); + assert_eq!(app.exit_code.unwrap(), 0); + assert!(app.git_interactive.get_lines().is_empty()); + } + + #[test] + fn application_finish() { + let gi = GitInteractive::new_from_filepath("test/git-rebase-todo-all-actions.in").unwrap(); + let window = Window::new(); + let mut app = Application::new(gi, window); + app.window.window.next_char = Input::Character('W'); + app.process_input(); + assert_eq!(app.exit_code.unwrap(), 0); + assert!(!app.git_interactive.get_lines().is_empty()); + } +} diff --git a/src/git_interactive.rs b/src/git_interactive.rs new file mode 100644 index 0000000..65ae357 --- /dev/null +++ b/src/git_interactive.rs @@ -0,0 +1,134 @@ +use std::cmp; +use std::fs::File; +use std::path::PathBuf; +use std::error::Error; +use std::io::Read; +use std::io::Write; + +use action::Action; +use line::Line; + +pub struct GitInteractive { + filepath: PathBuf, + lines: Vec, + selected_line_index: usize +} + +impl GitInteractive { + pub fn new_from_filepath(filepath: &str) -> Result { + let path = PathBuf::from(filepath); + + let mut file = match File::open(&path) { + Ok(file) => file, + Err(why) => { + return Err(format!( + "Error opening file, {}\n\ + Reason: {}", path.display(), why.description() + )); + } + }; + + let mut s = String::new(); + match file.read_to_string(&mut s) { + Ok(_) => {}, + Err(why) => { + return Err(format!( + "Error reading file, {}\n\ + Reason: {}", path.display(), why.description() + )); + } + } + + // catch noop rebases + let parsed_result = match s.lines().nth(0) { + Some("noop") => Ok(Vec::new()), + _ => { + s.lines() + .filter(|l| !l.starts_with('#') && !l.is_empty()) + .map(|l| Line::new(l)) + .collect() + } + }; + + match parsed_result { + Ok(lines) => Ok( + GitInteractive { + filepath: path, + lines: lines, + selected_line_index: 1 + } + ), + Err(e) => Err(format!( + "Error reading file, {}\n\ + Reason: {}", path.display(), e + )) + } + } + + pub fn write_file(&self) -> Result<(), String> { + let mut file = match File::create(&self.filepath) { + Ok(file) => file, + Err(why) => { + return Err(format!( + "Error opening file, {}\n\ + Reason: {}", self.filepath.display(), why.description() + )); + } + }; + + for line in &self.lines { + match writeln!(file, "{}", line.to_text()) { + Ok(_) => {}, + Err(why) => { + return Err(format!( + "Error writing to file, {}", why.description() + )); + } + } + } + Ok(()) + } + + pub fn clear(&mut self) { + self.lines.clear(); + } + + pub fn move_cursor_up(&mut self, amount: usize) { + self.selected_line_index = match amount { + a if a >= self.selected_line_index => 1, + _ => self.selected_line_index - amount + } + } + + pub fn move_cursor_down(&mut self, amount: usize) { + self.selected_line_index = cmp::min(self.selected_line_index + amount, self.lines.len()); + } + + pub fn swap_selected_up(&mut self) { + if self.selected_line_index == 1 { + return + } + self.lines.swap(self.selected_line_index - 1, self.selected_line_index - 2); + self.move_cursor_up(1); + } + + pub fn swap_selected_down(&mut self) { + if self.selected_line_index == self.lines.len() { + return + } + self.lines.swap(self.selected_line_index - 1, self.selected_line_index); + self.move_cursor_down(1); + } + + pub fn set_selected_line_action(&mut self, action: Action) { + self.lines[self.selected_line_index - 1].set_action(action); + } + + pub fn get_selected_line_index(&self) -> &usize { + &self.selected_line_index + } + + pub fn get_lines(&self) -> &Vec { + &self.lines + } +} diff --git a/src/line.rs b/src/line.rs new file mode 100644 index 0000000..c8fe94e --- /dev/null +++ b/src/line.rs @@ -0,0 +1,99 @@ +use action::{ + Action, + action_from_str, + action_to_str +}; + +#[derive(PartialEq, Debug)] +pub struct Line { + action: Action, + hash: String, + comment: String, + mutated: bool +} + +impl Line { + pub fn new(input_line: &str) -> Result { + let input: Vec<&str> = input_line.splitn(3, ' ').collect(); + match input.len() { + 3 => Ok(Line { + action: action_from_str(input[0])?, + hash: String::from(input[1]), + comment: String::from(input[2]), + mutated: false + }), + _ => Err(format!( + "Invalid line: {}", input_line + )) + } + } + + pub fn set_action(&mut self, action: Action) { + if self.action != action { + self.mutated = true; + self.action = action; + } + } + + pub fn get_action(&self) -> &Action { + &self.action + } + pub fn get_hash(&self) -> &String { + &self.hash + } + pub fn get_comment(&self) -> &String { + &self.comment + } + + pub fn to_text(&self) -> String { + format!("{} {} {}", action_to_str(&self.action), self.hash, self.comment) + } +} + +#[cfg(test)] +mod tests { + use super::Line; + use action::Action; + + #[test] + fn new_with_valid_line() { + let line = Line::new("pick aaa comment").unwrap(); + assert_eq!(line.action, Action::Pick); + assert_eq!(line.hash, "aaa"); + assert_eq!(line.comment, "comment"); + assert_eq!(line.mutated, false); + } + + #[test] + fn new_with_invalid_action() { + assert_eq!(Line::new("invalid aaa comment").unwrap_err(), "Invalid action: invalid"); + } + + #[test] + fn new_with_invalid_line() { + assert_eq!(Line::new("invalid").unwrap_err(), "Invalid line: invalid"); + } + + #[test] + fn set_to_new_action() { + let mut line = Line::new("pick aaa comment").unwrap(); + line.set_action(Action::Fixup); + assert_eq!(line.action, Action::Fixup); + assert_eq!(line.mutated, true); + } + + #[test] + fn getters() { + let line = Line::new("pick aaa comment").unwrap(); + assert_eq!(line.get_action(), &Action::Pick); + assert_eq!(line.get_hash(), &"aaa"); + assert_eq!(line.get_comment(), &"comment"); + } + + #[test] + fn to_text() { + let line = Line::new("pick aaa comment").unwrap(); + assert_eq!(line.to_text(), "pick aaa comment"); + } +} + diff --git a/src/main.rs b/src/main.rs index cb0de7a..b237ac8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,426 +2,22 @@ // - Add execute command extern crate pancurses; -use std::cmp; use std::env; -use std::error::Error; -use std::fs::File; -use std::io::prelude::*; -use std::path::Path; use std::process; -use pancurses::Input; -macro_rules! print_err { - ($($arg:tt)*) => ( - { - use std::io::prelude::*; - if let Err(e) = write!(&mut ::std::io::stderr(), "{}\n", format_args!($($arg)*)) { - panic!( - "Failed to write to stderr.\n\ - Original error output: {}\n\ - Secondary error writing to stderr: {}", format!($($arg)*), e - ); - } - } - ) -} - -enum Action { - Pick, - Reword, - Edit, - Squash, - Fixup, - Drop -} - - -fn action_from_str(s: &str) -> Action { - match s { - "pick" | "p" => Action::Pick, - "reword" | "r" => Action::Reword, - "edit" | "e" => Action::Edit, - "squash" | "s" => Action::Squash, - "fixup" | "f" => Action::Fixup, - "drop" | "d" => Action::Drop, - _ => Action::Pick - } -} - -fn action_to_str(action: &Action) -> String { - String::from(match action { - &Action::Pick => "pick", - &Action::Reword => "reword", - &Action::Edit => "edit", - &Action::Squash => "squash", - &Action::Fixup => "fixup", - &Action::Drop => "drop" - }) -} - -struct Line { - action: Action, - hash: String, - comment: String -} +mod action; +mod application; +mod git_interactive; +mod line; +#[macro_use] +mod utils; +mod window; +#[cfg(test)] +mod mocks; -impl Line { - fn new(input_line: &str) -> Result { - let input: Vec<&str> = input_line.splitn(3, " ").collect(); - match input.len() { - 3 => Ok(Line { - action: action_from_str(input[0]), - hash: String::from(input[1]), - comment: String::from(input[2]) - }), - _ => Err(format!( - "Invalid line {}", input_line - )) - } - } -} - -struct GitInteractive<'a> { - filepath: &'a Path, - lines: Vec, - selected_line: usize -} - -impl<'a> GitInteractive<'a> { - fn from_filepath(filepath: &'a str) -> Result, String> { - let path = Path::new(filepath); - - let mut file = match File::open(path) { - Ok(file) => file, - Err(why) => { - return Err(format!( - "Error opening file, {}\n\ - Reason: {}", path.display(), why.description() - )); - } - }; - - let mut s = String::new(); - match file.read_to_string(&mut s) { - Ok(_) => {}, - Err(why) => { - return Err(format!( - "Error reading file, {}\n\ - Reason: {}", path.display(), why.description() - )); - } - } - - if s.starts_with("noop") { - return Ok(None) - } - - let parsed_result: Result, String> = s - .lines() - .filter(|l| !l.starts_with("#") && !l.is_empty()) - .map(|l| Line::new(l)) - .collect(); - - match parsed_result { - Ok(lines) => Ok( - Some(GitInteractive { - filepath: path, - lines: lines, - selected_line: 1 - }) - ), - Err(e) => Err(format!( - "Error reading file, {}\n\ - Reason: {}", path.display(), e - )) - } - } - - fn write_file(&self) -> Result<(), String> { - let path = Path::new(self.filepath); - - let mut file = match File::create(path) { - Ok(file) => file, - Err(why) => { - return Err(format!( - "Error opening file, {}\n\ - Reason: {}", path.display(), why.description() - )); - } - }; - - for line in &self.lines { - match writeln!(file, "{} {} {}", action_to_str(&line.action), line.hash, line.comment) { - Ok(_) => {}, - Err(why) => { - return Err(format!( - "Error writing to file, {}", why.description() - )); - } - } - } - Ok(()) - } - - fn move_cursor_up(&mut self, amount: usize) { - self.selected_line = match amount { - a if a >= self.selected_line => 1, - _ => self.selected_line - amount - } - } - - fn move_cursor_down(&mut self, amount: usize) { - self.selected_line = cmp::min(self.selected_line + amount, self.lines.len()); - } - - fn swap_selected_up(&mut self) { - if self.selected_line == 1 { - return - } - self.lines.swap(self.selected_line - 1, self.selected_line - 2); - self.move_cursor_up(1); - } - - fn swap_selected_down(&mut self) { - if self.selected_line == self.lines.len() { - return - } - self.lines.swap(self.selected_line - 1, self.selected_line); - self.move_cursor_down(1); - } - - fn set_selected_line_action(&mut self, action: Action) { - self.lines[self.selected_line - 1].action = action; - } -} - -const COLOR_TABLE: [i16; 7] = [ - pancurses::COLOR_WHITE, - pancurses::COLOR_YELLOW, - pancurses::COLOR_BLUE, - pancurses::COLOR_GREEN, - pancurses::COLOR_CYAN, - pancurses::COLOR_MAGENTA, - pancurses::COLOR_RED -]; - -enum Color { - White, - Yellow, - Blue, - Green, - Cyan, - Magenta, - Red -} - -struct Window { - window: pancurses::Window, - top: usize -} - -impl Window { - fn new() -> Self { - let window = pancurses::initscr(); - - pancurses::curs_set(0); - pancurses::noecho(); - - window.keypad(true); - - if pancurses::has_colors() { - pancurses::start_color(); - } - pancurses::use_default_colors(); - for (i, color) in COLOR_TABLE.into_iter().enumerate() { - pancurses::init_pair(i as i16, *color, -1); - } - - Window{ - window: window, - top: 0 - } - } - fn draw(&self, git_interactive: &GitInteractive) { - self.window.clear(); - self.draw_title(); - let window_height = self.get_window_height(); - - if self.top > 0 { - self.draw_more_indicator(self.top); - } - self.window.addstr(&"\n"); - - let mut index: usize = self.top + 1; - for line in git_interactive - .lines - .iter() - .skip(self.top) - .take(window_height) - { - self.draw_line(&line, index == git_interactive.selected_line); - index += 1; - } - if window_height < git_interactive.lines.len() - self.top { - self.draw_more_indicator((git_interactive.lines.len() - window_height - self.top) as usize); - } - self.window.addstr(&"\n"); - self.draw_footer(); - self.window.refresh(); - } - - fn draw_more_indicator(&self, remaining: usize) { - self.set_color(Color::White); - self.window.attron(pancurses::A_DIM); - self.window.attron(pancurses::A_REVERSE); - self.window.addstr(&format!(" -- {} -- ", remaining)); - self.window.attroff(pancurses::A_REVERSE); - self.window.attroff(pancurses::A_DIM); - } - - fn draw_title(&self) { - self.set_color(Color::White); - self.set_dim(true); - self.set_underline(true); - self.window.addstr("Git Interactive Rebase ? for help\n"); - self.set_underline(false); - self.set_dim(false); - } - - fn draw_line(&self, line: &Line, selected: bool) { - self.set_color(Color::White); - if selected { - self.window.addstr(" > "); - } - else { - self.window.addstr(" "); - } - match line.action { - Action::Pick => self.set_color(Color::Green), - Action::Reword => self.set_color(Color::Yellow), - Action::Edit => self.set_color(Color::Blue), - Action::Squash => self.set_color(Color::Cyan), - Action::Fixup => self.set_color(Color::Magenta), - Action::Drop => self.set_color(Color::Red) - } - self.window.addstr(&format!("{:6}", action_to_str(&line.action)).as_ref()); - self.set_color(Color::White); - self.window.addstr(&format!(" {} {}\n", line.hash, line.comment).as_ref()); - } - - fn draw_footer(&self) { - self.set_color(Color::White); - self.set_dim(true); - self.window.mvaddstr( - self.window.get_max_y() - 1, - 0, - "Actions: [ up, down, q/Q, w/W, j, k, p, r, e, s, f, d, ? ]" - ); - self.set_dim(false); - } - - fn draw_help(&self) { - self.window.clear(); - self.draw_title(); - self.set_color(Color::White); - self.window.addstr(" Key Action\n"); - self.window.addstr(" --------------------------------------------------\n"); - self.draw_help_command("Up", "Move selection up"); - self.draw_help_command("Down", "Move selection down"); - self.draw_help_command("Page Up", "Move selection up 5 lines"); - self.draw_help_command("Page Down", "Move selection down 5 lines"); - self.draw_help_command("q", "Abort interactive rebase"); - self.draw_help_command("Q", "Immediately abort interactive rebase"); - self.draw_help_command("w", "Write interactive rebase file"); - self.draw_help_command("W", "Immediately write interactive rebase file"); - self.draw_help_command("?", "Show help"); - self.draw_help_command("j", "Move selected commit down"); - self.draw_help_command("k", "Move selected commit up"); - self.draw_help_command("p", "Set selected commit to be picked"); - self.draw_help_command("r", "Set selected commit to be reworded"); - self.draw_help_command("e", "Set selected commit to be edited"); - self.draw_help_command("s", "Set selected commit to be squashed"); - self.draw_help_command("f", "Set selected commit to be fixed-up"); - self.draw_help_command("d", "Set selected commit to be dropped"); - self.window.addstr("\n\nHit any key to close help"); - self.window.refresh(); - self.window.getch(); - } - - fn draw_help_command(&self, command: &str, help: &str) { - self.set_color(Color::Blue); - self.window.addstr(&format!(" {:4} ", command)); - self.set_color(Color::White); - self.window.addstr(&format!("{}\n", help)); - } - - fn set_color(&self, color: Color) { - match color { - Color::White => self.window.attrset(pancurses::COLOR_PAIR(0)), - Color::Yellow => self.window.attrset(pancurses::COLOR_PAIR(1)), - Color::Blue => self.window.attrset(pancurses::COLOR_PAIR(2)), - Color::Green => self.window.attrset(pancurses::COLOR_PAIR(3)), - Color::Cyan => self.window.attrset(pancurses::COLOR_PAIR(4)), - Color::Magenta => self.window.attrset(pancurses::COLOR_PAIR(5)), - Color::Red => self.window.attrset(pancurses::COLOR_PAIR(6)) - }; - } - - fn set_dim(&self, on: bool) { - if on { - self.window.attron(pancurses::A_DIM); - } - else { - self.window.attroff(pancurses::A_DIM); - } - } - - fn set_underline(&self, on: bool) { - if on { - self.window.attron(pancurses::A_UNDERLINE); - } - else { - self.window.attroff(pancurses::A_UNDERLINE); - } - } - - fn confirm(&self, message: &str) -> bool { - self.window.clear(); - self.draw_title(); - self.window.addstr(&format!("{} (y/n)?", message)); - match self.window.getch() { - Some(pancurses::Input::Character(c)) if c == 'y' || c == 'Y' => true, - _ => false - } - } - - fn set_top(&mut self, git_interactive: &GitInteractive) { - let window_height = self.get_window_height(); - - self.top = match git_interactive.selected_line { - _ if git_interactive.lines.len() <= window_height => 0, - s if s == git_interactive.lines.len() => git_interactive.lines.len() - window_height, - s if self.top + 1 > s => s - 1, - s if self.top + window_height <= s => s - window_height + 1, - _ => self.top - }; - } - - fn get_window_height(&self) -> usize { - return match self.window.get_max_y() { - // 4 removed for other UI lines - x if x >= 4 => x - 4, - _ => 4 - } as usize; - } - - fn endwin(&self) { - self.window.clear(); - self.window.refresh(); - pancurses::curs_set(1); - pancurses::endwin(); - } -} +use application::Application; +use git_interactive::GitInteractive; +use window::Window; fn main() { let filepath = match env::args().nth(1) { @@ -434,88 +30,38 @@ fn main() { process::exit(1); } }; - - let mut git_interactive = match GitInteractive::from_filepath(&filepath) { - Ok(gi) => { - match gi { - Some(git_interactive) => git_interactive, - None => { - print_err!("{}", &"Nothing to edit"); - process::exit(0); - } - } - }, + + let git_interactive = match GitInteractive::new_from_filepath(&filepath) { + Ok(gi) => gi, Err(msg) => { print_err!("{}", msg); process::exit(1); } }; - let mut window = Window::new(); + if git_interactive.get_lines().is_empty() { + print_err!("{}", &"Nothing to rebase"); + process::exit(0); + } - loop { - window.draw(&git_interactive); - match window.window.getch() { - Some(Input::Character(c)) if c == 'q' => { - if window.confirm("Are you sure you want to abort") { - git_interactive.lines.clear(); - break; - } - }, - Some(Input::Character(c)) if c == 'Q' => { - git_interactive.lines.clear(); - break; - }, - Some(Input::Character(c)) if c == 'w' => { - if window.confirm("Are you sure you want to rebase") { - break; - } - }, - Some(Input::Character(c)) if c == 'W' => { - break; - }, - Some(Input::Character(c)) if c == '?' => window.draw_help(), - Some(Input::Character(c)) if c == 'p' => git_interactive.set_selected_line_action(Action::Pick), - Some(Input::Character(c)) if c == 'r' => git_interactive.set_selected_line_action(Action::Reword), - Some(Input::Character(c)) if c == 'e' => git_interactive.set_selected_line_action(Action::Edit), - Some(Input::Character(c)) if c == 's' => git_interactive.set_selected_line_action(Action::Squash), - Some(Input::Character(c)) if c == 'f' => git_interactive.set_selected_line_action(Action::Fixup), - Some(Input::Character(c)) if c == 'd' => git_interactive.set_selected_line_action(Action::Drop), - Some(Input::Character(c)) if c == 'j' => { - git_interactive.swap_selected_down(); - window.set_top(&git_interactive); - }, - Some(Input::Character(c)) if c == 'k' => { - git_interactive.swap_selected_up(); - window.set_top(&git_interactive); - }, - Some(pancurses::Input::KeyUp) => { - git_interactive.move_cursor_up(1); - window.set_top(&git_interactive); - }, - Some(pancurses::Input::KeyPPage) => { - git_interactive.move_cursor_up(5); - window.set_top(&git_interactive); - }, - Some(pancurses::Input::KeyDown) => { - git_interactive.move_cursor_down(1); - window.set_top(&git_interactive); - }, - Some(pancurses::Input::KeyNPage) => { - git_interactive.move_cursor_down(5); - window.set_top(&git_interactive); - }, - Some(pancurses::Input::KeyResize) => window.set_top(&git_interactive), - _ => {} - } + let window = Window::new(); + + let mut application = Application::new(git_interactive, window); + + while application.exit_code == None { + application.draw(); + application.process_input() } - window.endwin(); - match git_interactive.write_file() { + match application.end() { Ok(_) => {}, Err(msg) => { print_err!("{}", msg); process::exit(1); } }; + process::exit(match application.exit_code { + None => 0, + Some(code) => code + }); } diff --git a/src/mocks.rs b/src/mocks.rs new file mode 100644 index 0000000..e7bac7a --- /dev/null +++ b/src/mocks.rs @@ -0,0 +1,55 @@ + +pub mod mockcurses { + pub use pancurses::{ + COLOR_WHITE, + COLOR_YELLOW, + COLOR_BLUE, + COLOR_GREEN, + COLOR_CYAN, + COLOR_MAGENTA, + COLOR_RED, + A_UNDERLINE, + A_BOLD, + A_DIM, + A_REVERSE, + COLOR_PAIR, + Input, + chtype + }; + + pub fn initscr() -> Window { + Window { + max_y: 2, + next_char: Input::KeyClear + } + } + pub fn curs_set(visibility: i32) {} + pub fn noecho() {} + pub fn has_colors() -> bool { + false + } + pub fn start_color() {} + pub fn use_default_colors() {} + pub fn init_pair(pair_index: i16, foreground_color: i16, background_color: i16) {} + pub fn endwin() {} + + #[derive(Debug)] + pub struct Window { + pub max_y: i32, + pub next_char: Input + } + + impl Window { + pub fn addstr(&self, string: &str) {} + pub fn attron(&self, attributes: chtype) {} + pub fn attroff(&self, attributes: chtype) {} + pub fn attrset(&self, attributes: chtype) {} + pub fn mvaddstr(&self, y: i32, x: i32, string: &str) {} + pub fn clear(&self) {} + pub fn get_max_y(&self) -> i32 {self.max_y} + pub fn getch(&self) -> Option {Some(self.next_char)} + pub fn keypad(&self, a: bool) {} + pub fn refresh(&self) {} + + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..28affca --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,14 @@ +macro_rules! print_err { + ($($arg:tt)*) => ( + { + use std::io::prelude::*; + if let Err(e) = write!(&mut ::std::io::stderr(), "{}\n", format_args!($($arg)*)) { + panic!( + "Failed to write to stderr.\n\ + Original error output: {}\n\ + Secondary error writing to stderr: {}", format!($($arg)*), e + ); + } + } + ) +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..f4d1649 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,243 @@ + +#[cfg(not(test))] +use pancurses as pancurses; + +#[cfg(test)] +use mocks::mockcurses as pancurses; + +pub use pancurses::Input as Input; + +use action::{ + Action, + action_to_str +}; +use line::Line; + +const COLOR_TABLE: [i16; 7] = [ + pancurses::COLOR_WHITE, + pancurses::COLOR_YELLOW, + pancurses::COLOR_BLUE, + pancurses::COLOR_GREEN, + pancurses::COLOR_CYAN, + pancurses::COLOR_MAGENTA, + pancurses::COLOR_RED +]; + +pub enum Color { + White, + Yellow, + Blue, + Green, + Cyan, + Magenta, + Red +} + +pub struct Window { + pub window: pancurses::Window, + top: usize +} + +impl Window { + pub fn new() -> Self { + + let window = pancurses::initscr(); + window.keypad(true); + + pancurses::curs_set(0); + pancurses::noecho(); + + if pancurses::has_colors() { + pancurses::start_color(); + } + pancurses::use_default_colors(); + + for (i, color) in COLOR_TABLE.iter().enumerate() { + pancurses::init_pair(i as i16, *color, -1); + } + + + Window { + window: window, + top: 0 + } + } + + pub fn draw(&self, lines: &[Line], selected_index: &usize) { + self.window.clear(); + self.draw_title(); + let window_height = self.get_window_height(); + + if self.top > 0 { + self.draw_more_indicator(self.top); + } + self.window.addstr("\n"); + + let mut index: usize = self.top + 1; + for line in lines + .iter() + .skip(self.top) + .take(window_height) + { + self.draw_line(line, index == *selected_index); + index += 1; + } + if window_height < lines.len() - self.top { + self.draw_more_indicator((lines.len() - window_height - self.top) as usize); + } + self.window.addstr("\n"); + self.draw_footer(); + self.window.refresh(); + } + + fn draw_more_indicator(&self, remaining: usize) { + self.set_color(Color::White); + self.window.attron(pancurses::A_DIM); + self.window.attron(pancurses::A_REVERSE); + self.window.addstr(&format!(" -- {} -- ", remaining)); + self.window.attroff(pancurses::A_REVERSE); + self.window.attroff(pancurses::A_DIM); + } + + fn draw_title(&self) { + self.set_color(Color::White); + self.set_dim(true); + self.set_underline(true); + self.window.addstr("Git Interactive Rebase ? for help\n"); + self.set_underline(false); + self.set_dim(false); + } + + fn draw_line(&self, line: &Line, selected: bool) { + self.set_color(Color::White); + if selected { + self.window.addstr(" > "); + } + else { + self.window.addstr(" "); + } + match *line.get_action() { + Action::Pick => self.set_color(Color::Green), + Action::Reword => self.set_color(Color::Yellow), + Action::Edit => self.set_color(Color::Blue), + Action::Squash => self.set_color(Color::Cyan), + Action::Fixup => self.set_color(Color::Magenta), + Action::Drop => self.set_color(Color::Red) + } + self.window.addstr(&format!("{:6}", action_to_str(&line.get_action()))); + self.set_color(Color::White); + self.window.addstr(&format!(" {} {}\n", line.get_hash(), line.get_comment())); + } + + fn draw_footer(&self) { + self.set_color(Color::White); + self.set_dim(true); + self.window.mvaddstr( + self.window.get_max_y() - 1, + 0, + "Actions: [ up, down, q/Q, w/W, j, k, p, r, e, s, f, d, ? ]" + ); + self.set_dim(false); + } + + pub fn draw_help(&self) { + self.window.clear(); + self.draw_title(); + self.set_color(Color::White); + self.window.addstr("\n Key Action\n"); + self.window.addstr(" --------------------------------------------------\n"); + self.draw_help_command("Up", "Move selection up"); + self.draw_help_command("Down", "Move selection down"); + self.draw_help_command("Page Up", "Move selection up 5 lines"); + self.draw_help_command("Page Down", "Move selection down 5 lines"); + self.draw_help_command("q", "Abort interactive rebase"); + self.draw_help_command("Q", "Immediately abort interactive rebase"); + self.draw_help_command("w", "Write interactive rebase file"); + self.draw_help_command("W", "Immediately write interactive rebase file"); + self.draw_help_command("?", "Show help"); + self.draw_help_command("j", "Move selected commit down"); + self.draw_help_command("k", "Move selected commit up"); + self.draw_help_command("p", "Set selected commit to be picked"); + self.draw_help_command("r", "Set selected commit to be reworded"); + self.draw_help_command("e", "Set selected commit to be edited"); + self.draw_help_command("s", "Set selected commit to be squashed"); + self.draw_help_command("f", "Set selected commit to be fixed-up"); + self.draw_help_command("d", "Set selected commit to be dropped"); + self.window.addstr("\n\nHit any key to close help"); + self.window.refresh(); + } + + fn draw_help_command(&self, command: &str, help: &str) { + self.set_color(Color::Blue); + self.window.addstr(&format!(" {:9} ", command)); + self.set_color(Color::White); + self.window.addstr(&format!("{}\n", help)); + } + + fn set_color(&self, color: Color) { + match color { + Color::White => self.window.attrset(pancurses::COLOR_PAIR(0)), + Color::Yellow => self.window.attrset(pancurses::COLOR_PAIR(1)), + Color::Blue => self.window.attrset(pancurses::COLOR_PAIR(2)), + Color::Green => self.window.attrset(pancurses::COLOR_PAIR(3)), + Color::Cyan => self.window.attrset(pancurses::COLOR_PAIR(4)), + Color::Magenta => self.window.attrset(pancurses::COLOR_PAIR(5)), + Color::Red => self.window.attrset(pancurses::COLOR_PAIR(6)) + }; + } + + fn set_dim(&self, on: bool) { + if on { + self.window.attron(pancurses::A_DIM); + } + else { + self.window.attroff(pancurses::A_DIM); + } + } + + fn set_underline(&self, on: bool) { + if on { + self.window.attron(pancurses::A_UNDERLINE); + } + else { + self.window.attroff(pancurses::A_UNDERLINE); + } + } + + pub fn confirm(&self, message: &str) -> bool { + self.window.clear(); + self.draw_title(); + self.window.addstr(&format!("\n{} (y/n)? ", message)); + match self.window.getch() { + Some(Input::Character(c)) if c == 'y' || c == 'Y' => true, + _ => false + } + } + + pub fn set_top(&mut self, line_length: usize, selected_index: usize) { + let window_height = self.get_window_height(); + self.top = match selected_index { + _ if line_length <= window_height => 0, + s if s == line_length => line_length - window_height, + s if self.top + 1 > s => s - 1, + s if self.top + window_height <= s => s - window_height + 1, + _ => self.top + }; + } + + fn get_window_height(&self) -> usize { + match self.window.get_max_y() { + // 4 removed for other UI lines + x if x >= 4 => (x - 4) as usize, + _ => 4 + } + } + + pub fn end(&self) { + self.window.clear(); + self.window.refresh(); + pancurses::curs_set(1); + pancurses::endwin(); + } +} + -- cgit v1.2.3