use crate::{
components::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState,
},
keys::SharedKeyConfig,
strings,
ui::{self, style::SharedTheme},
};
use anyhow::{anyhow, bail, Result};
use asyncgit::sync::{
get_config_string, utils::repo_work_dir, RepoPath,
};
use crossterm::{
event::Event,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use scopeguard::defer;
use std::ffi::OsStr;
use std::{env, io, path::Path, process::Command};
use tui::{
backend::Backend,
layout::Rect,
text::{Span, Spans},
widgets::{Block, BorderType, Borders, Clear, Paragraph},
Frame,
};
pub struct ExternalEditorComponent {
visible: bool,
theme: SharedTheme,
key_config: SharedKeyConfig,
}
impl ExternalEditorComponent {
pub fn new(
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
Self {
visible: false,
theme,
key_config,
}
}
pub fn open_file_in_editor(
repo: &RepoPath,
path: &Path,
) -> Result<()> {
let work_dir = repo_work_dir(repo)?;
let path = if path.is_relative() {
Path::new(&work_dir).join(path)
} else {
path.into()
};
if !path.exists() {
bail!("file not found: {:?}", path);
}
io::stdout().execute(LeaveAlternateScreen)?;
defer! {
io::stdout().execute(EnterAlternateScreen).expect("reset terminal");
}
let environment_options = ["GIT_EDITOR", "VISUAL", "EDITOR"];
let editor = env::var(environment_options[0])
.ok()
.or_else(|| {
get_config_string(repo, "core.editor").ok()?
})
.or_else(|| env::var(environment_options[1]).ok())
.or_else(|| env::var(environment_options[2]).ok())
.unwrap_or_else(|| String::from("vi"));
let mut echars = editor.chars().peekable();
let first_char = *echars.peek().ok_or_else(|| {
anyhow!(
"editor env variable found empty: {}",
environment_options.join(" or ")
)
})?;
let command: String = if first_char == '\"' {
echars
.by_ref()
.skip(1)
.take_while(|c| *c != '\"')
.collect()
} else {
echars.by_ref().take_while(|c| *c != ' ').collect()
};
let remainder_str = echars.collect::<String>();
let remainder = remainder_str.split_whitespace();
let mut args: Vec<&OsStr> =
remainder.map(OsStr::new).collect();
args.push(path.as_os_str());
Command::new(command.clone())
.current_dir(work_dir)
.args(args)
.status()
.map_err(|e| anyhow!("\"{}\": {}", command, e))?;
Ok(())
}
}
impl DrawableComponent for ExternalEditorComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if self.visible {
let txt = Spans::from(
strings::msg_opening_editor(&self.key_config)
.split('\n')
.map(|string| {
Span::raw::<String>(string.to_string())
})
.collect::<Vec<Span>>(),
);
let area = ui::centered_rect_absolute(25, 3, f.size());
f.render_widget(Clear, area);
f.render_widget(
Paragraph::new(txt)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Thick)
.border_style(self.theme.block(true)),
)
.style(self.theme.block(true)),
area,
);
}
Ok(())
}
}
impl Component for ExternalEditorComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.visible && !force_all {
out.clear();
}
visibility_blocking(self)
}
fn event(&mut self, _ev: &Event) -> Result<EventState> {
if self.visible {
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}