diff --git a/src/app.rs b/src/app.rs index 3aac96f60ec..14e19ec05d3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::input::InputState; use crate::{ accessors, cmdbar::CommandBar, @@ -28,6 +29,7 @@ use tui::{ widgets::{Block, Borders, Tabs}, Frame, }; + /// pub struct App { do_quit: bool, @@ -45,6 +47,10 @@ pub struct App { stashlist_tab: StashList, queue: Queue, theme: SharedTheme, + + // "Flags" + requires_redraw: bool, + set_polling: bool, } // public interface @@ -85,6 +91,8 @@ impl App { stashlist_tab: StashList::new(&queue, theme.clone()), queue, theme, + requires_redraw: false, + set_polling: true, } } @@ -182,6 +190,17 @@ impl App { if flags.contains(NeedsUpdate::COMMANDS) { self.update_commands(); } + } else if let InputEvent::State(polling_state) = ev { + if let InputState::Paused = polling_state { + if let Err(e) = self.commit.show_editor() { + let msg = + format!("failed to launch editor:\n{}", e); + log::error!("{}", msg.as_str()); + self.msg.show_msg(msg.as_str())?; + } + self.requires_redraw = true; + self.set_polling = true; + } } Ok(()) @@ -230,6 +249,21 @@ impl App { || self.stashing_tab.anything_pending() || self.inspect_commit_popup.any_work_pending() } + + /// + pub fn requires_redraw(&mut self) -> bool { + if self.requires_redraw { + self.requires_redraw = false; + true + } else { + false + } + } + + /// + pub fn set_polling(&self) -> bool { + self.set_polling + } } // private impls @@ -314,6 +348,10 @@ impl App { fn process_queue(&mut self) -> Result { let mut flags = NeedsUpdate::empty(); + + // Reset "flags" + self.requires_redraw = false; + loop { let front = self.queue.borrow_mut().pop_front(); if let Some(e) = front { @@ -369,6 +407,9 @@ impl App { self.inspect_commit_popup.open(id)?; flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) } + InternalEvent::SuspendPolling => { + self.set_polling = false; + } }; Ok(flags) diff --git a/src/components/changes.rs b/src/components/changes.rs index 6f7067c15f7..45f87b5ccc2 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -236,6 +236,11 @@ impl Component for ChangesComponent { some_selection, self.focused(), )); + out.push(CommandInfo::new( + commands::COMMIT_OPEN_EDITOR, + !self.is_empty(), + self.focused() || force_all, + )); out.push( CommandInfo::new( commands::COMMIT_OPEN, @@ -266,6 +271,15 @@ impl Component for ChangesComponent { .push_back(InternalEvent::OpenCommit); Ok(true) } + keys::OPEN_COMMIT_EDITOR + if !self.is_working_dir + && !self.is_empty() => + { + self.queue + .borrow_mut() + .push_back(InternalEvent::SuspendPolling); + Ok(true) + } keys::STATUS_STAGE_FILE => { try_or_popup!( self, diff --git a/src/components/commit.rs b/src/components/commit.rs index bfdae54ab0a..9e440446484 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -2,18 +2,24 @@ use super::{ textinput::TextInputComponent, visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, }; +use crate::strings::COMMIT_EDITOR_MSG; use crate::{ - keys, + get_app_config_path, keys, queue::{InternalEvent, NeedsUpdate, Queue}, strings, ui::style::SharedTheme, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use asyncgit::{ sync::{self, CommitId}, CWD, }; use crossterm::event::Event; +use std::env; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Command; use strings::commands; use sync::HookResult; use tui::{backend::Backend, layout::Rect, Frame}; @@ -121,8 +127,61 @@ impl CommitComponent { } } + pub fn show_editor(&mut self) -> Result<()> { + const COMMIT_MSG_FILE_NAME: &str = "COMMITMSG_EDITOR"; + let mut config_path: PathBuf = get_app_config_path()?; + config_path.push(COMMIT_MSG_FILE_NAME); + + let mut file = File::create(&config_path)?; + file.write_all(COMMIT_EDITOR_MSG.as_bytes())?; + drop(file); + + let mut editor = env::var("GIT_EDTIOR") + .ok() + .or_else(|| env::var("VISUAL").ok()) + .or_else(|| env::var("EDITOR").ok()) + .unwrap_or(String::from("vi")); + editor + .push_str(&format!(" {}", config_path.to_string_lossy())); + + let mut editor = editor.split_whitespace(); + + let command = editor.next().ok_or_else(|| { + anyhow!("unable to read editor command") + })?; + + Command::new(command) + .args(editor) + .status() + .map_err(|e| anyhow!("\"{}\": {}", command, e))?; + + let mut message = String::new(); + + let mut file = File::open(&config_path)?; + file.read_to_string(&mut message)?; + drop(file); + std::fs::remove_file(&config_path)?; + + let message: String = message + .lines() + .filter(|l| !l.starts_with('#')) + // Add back the newlines `lines` remove above + .flat_map(|l| vec![l, "\n"]) + .collect(); + + if !message.chars().all(char::is_whitespace) { + return self.commit_msg(message); + } + + Ok(()) + } + fn commit(&mut self) -> Result<()> { - let mut msg = self.input.get_text().clone(); + self.commit_msg(self.input.get_text().clone()) + } + + fn commit_msg(&mut self, msg: String) -> Result<()> { + let mut msg = msg; if let HookResult::NotOk(e) = sync::hooks_commit_msg(CWD, &mut msg)? { diff --git a/src/keys.rs b/src/keys.rs index 4cb6803bd0e..8bd98837036 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -32,6 +32,8 @@ pub const EXIT: KeyEvent = pub const EXIT_POPUP: KeyEvent = no_mod(KeyCode::Esc); pub const CLOSE_MSG: KeyEvent = no_mod(KeyCode::Enter); pub const OPEN_COMMIT: KeyEvent = no_mod(KeyCode::Char('c')); +pub const OPEN_COMMIT_EDITOR: KeyEvent = + with_mod(KeyCode::Char('C'), KeyModifiers::SHIFT); pub const OPEN_HELP: KeyEvent = no_mod(KeyCode::Char('h')); pub const MOVE_LEFT: KeyEvent = no_mod(KeyCode::Left); pub const MOVE_RIGHT: KeyEvent = no_mod(KeyCode::Right); diff --git a/src/main.rs b/src/main.rs index e88c03e8d0f..7cdf24a4130 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,7 +98,7 @@ fn main() -> Result<()> { let spinner_ticker = tick(SPINNER_INTERVAL); app.update()?; - draw(&mut terminal, &app)?; + draw(&mut terminal, &mut app)?; let mut spinner = Spinner::default(); @@ -127,11 +127,10 @@ fn main() -> Result<()> { } } - //TODO: disable input polling while external editor open - input.set_polling(!app.any_work_pending()); + input.set_polling(app.set_polling()); if needs_draw { - draw(&mut terminal, &app)?; + draw(&mut terminal, &mut app)?; } spinner.draw( @@ -162,8 +161,12 @@ fn shutdown_terminal() -> Result<()> { fn draw( terminal: &mut Terminal, - app: &App, + app: &mut App, ) -> io::Result<()> { + if app.requires_redraw() { + terminal.resize(terminal.size()?)?; + } + terminal.draw(|mut f| { if let Err(e) = app.draw(&mut f) { log::error!("failed to draw: {:?}", e) diff --git a/src/queue.rs b/src/queue.rs index f682d08073a..eea4aa3913e 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -48,6 +48,8 @@ pub enum InternalEvent { TabSwitch, /// InspectCommit(CommitId), + /// + SuspendPolling, } /// diff --git a/src/strings.rs b/src/strings.rs index 5cd7e9300bb..eb1070aadce 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -14,6 +14,10 @@ pub static MSG_TITLE_ERROR: &str = "Error"; pub static COMMIT_TITLE: &str = "Commit"; pub static COMMIT_TITLE_AMEND: &str = "Commit (Amend)"; pub static COMMIT_MSG: &str = "type commit message.."; +pub static COMMIT_EDITOR_MSG: &str = r##" +# Enter your commit message +# Lines starting with '#' will be ignored +# Empty commit message will abort the commit"##; pub static STASH_POPUP_TITLE: &str = "Stash"; pub static STASH_POPUP_MSG: &str = "type name (optional)"; pub static CONFIRM_TITLE_RESET: &str = "Reset"; @@ -150,6 +154,12 @@ pub mod commands { CMD_GROUP_COMMIT, ); /// + pub static COMMIT_OPEN_EDITOR: CommandText = CommandText::new( + "Commit editor [C]", + "open commit editor (available in non-empty stage)", + CMD_GROUP_COMMIT, + ); + /// pub static COMMIT_ENTER: CommandText = CommandText::new( "Commit [enter]", "commit (available when commit message is non-empty)",