diff options
author | Tim Oram <me@mitmaro.ca> | 2017-02-11 18:17:42 -0330 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-11 18:17:42 -0330 |
commit | e96410defd555fd452f89faf02ed994bbc5d8f77 (patch) | |
tree | 5d7279c5564131a577a1cf91f79ecbd0ee708c21 | |
parent | 59c8a523ae4f8a59d9c66b155a5b2aad6c8234f3 (diff) | |
parent | c45dd804de56be92af698b8721402f0aad7cebd2 (diff) |
Merge pull request #22 from MitMaro/module-refactor
Refactor into modules
-rw-r--r-- | .travis.yml | 9 | ||||
-rw-r--r-- | src/action.rs | 69 | ||||
-rw-r--r-- | src/application.rs | 304 | ||||
-rw-r--r-- | src/git_interactive.rs | 134 | ||||
-rw-r--r-- | src/line.rs | 99 | ||||
-rw-r--r-- | src/main.rs | 516 | ||||
-rw-r--r-- | src/mocks.rs | 55 | ||||
-rw-r--r-- | src/utils.rs | 14 | ||||
-rw-r--r-- | src/window.rs | 243 | ||||
-rw-r--r-- | test/git-rebase-todo-all-actions.in | 14 | ||||
-rw-r--r-- | test/git-rebase-todo-long.in | 21 | ||||
-rw-r--r-- | test/git-rebase-todo-short.in | 3 |
12 files changed, 996 insertions, 485 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c2512c1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly + 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<Action, String> { + 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<i32>, + 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<Line>, + selected_line_index: usize +} + +impl GitInteractive { + pub fn new_from_filepath(filepath: &str) -> Result<Self, String> { + 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<Line> { + &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<Self, String> { + 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<Self, String> { - 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<Line>, - selected_line: usize -} - -impl<'a> GitInteractive<'a> { - fn from_filepath(filepath: &'a str) -> Result<Option<Self>, 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<Vec<Line>, 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(); - pa |