use std::{
env,
ffi::{OsStr, OsString},
fs,
io::{Read, Write},
path::Path,
process,
};
use lazy_static::lazy_static;
use tempfile::NamedTempFile;
use crate::{
error::{InquireError, InquireResult},
formatter::StringFormatter,
terminal::get_default_terminal,
ui::{Backend, EditorBackend, Key, RenderConfig},
validator::{ErrorMessage, StringValidator, Validation},
};
lazy_static! {
static ref DEFAULT_EDITOR: OsString = get_default_editor_command();
}
#[derive(Clone)]
pub struct Editor<'a> {
pub message: &'a str,
pub editor_command: &'a OsStr,
pub editor_command_args: &'a [&'a OsStr],
pub file_extension: &'a str,
pub predefined_text: Option<&'a str>,
pub help_message: Option<&'a str>,
pub formatter: StringFormatter<'a>,
pub validators: Vec<Box<dyn StringValidator>>,
pub render_config: RenderConfig<'a>,
}
impl<'a> Editor<'a> {
pub const DEFAULT_FORMATTER: StringFormatter<'a> = &|_| String::from("<received>");
pub const DEFAULT_VALIDATORS: Vec<Box<dyn StringValidator>> = vec![];
pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None;
pub fn new(message: &'a str) -> Self {
Self {
message,
editor_command: &DEFAULT_EDITOR,
editor_command_args: &[],
file_extension: ".txt",
predefined_text: None,
help_message: Self::DEFAULT_HELP_MESSAGE,
validators: Self::DEFAULT_VALIDATORS,
formatter: Self::DEFAULT_FORMATTER,
render_config: RenderConfig::default(),
}
}
pub fn with_help_message(mut self, message: &'a str) -> Self {
self.help_message = Some(message);
self
}
pub fn with_predefined_text(mut self, text: &'a str) -> Self {
self.predefined_text = Some(text);
self
}
pub fn with_file_extension(mut self, file_extension: &'a str) -> Self {
self.file_extension = file_extension;
self
}
pub fn with_editor_command(mut self, editor_command: &'a OsStr) -> Self {
self.editor_command = editor_command;
self
}
pub fn with_args(mut self, args: &'a [&'a OsStr]) -> Self {
self.editor_command_args = args;
self
}
pub fn with_formatter(mut self, formatter: StringFormatter<'a>) -> Self {
self.formatter = formatter;
self
}
pub fn with_validator<V>(mut self, validator: V) -> Self
where
V: StringValidator + 'static,
{
if self.validators.capacity() == 0 {
self.validators.reserve(5);
}
self.validators.push(Box::new(validator));
self
}
pub fn with_validators(mut self, validators: &[Box<dyn StringValidator>]) -> Self {
for validator in validators {
#[allow(clippy::clone_double_ref)]
self.validators.push(validator.clone());
}
self
}
pub fn with_render_config(mut self, render_config: RenderConfig<'a>) -> Self {
self.render_config = render_config;
self
}
pub fn prompt_skippable(self) -> InquireResult<Option<String>> {
match self.prompt() {
Ok(answer) => Ok(Some(answer)),
Err(InquireError::OperationCanceled) => Ok(None),
Err(err) => Err(err),
}
}
pub fn prompt(self) -> InquireResult<String> {
let terminal = get_default_terminal()?;
let mut backend = Backend::new(terminal, self.render_config)?;
self.prompt_with_backend(&mut backend)
}
pub(crate) fn prompt_with_backend<B: EditorBackend>(
self,
backend: &mut B,
) -> InquireResult<String> {
EditorPrompt::new(self)?.prompt(backend)
}
}
struct EditorPrompt<'a> {
message: &'a str,
editor_command: &'a OsStr,
editor_command_args: &'a [&'a OsStr],
help_message: Option<&'a str>,
formatter: StringFormatter<'a>,
validators: Vec<Box<dyn StringValidator>>,
error: Option<ErrorMessage>,
tmp_file: NamedTempFile,
}
impl<'a> From<&'a str> for Editor<'a> {
fn from(val: &'a str) -> Self {
Editor::new(val)
}
}
impl<'a> EditorPrompt<'a> {
pub fn new(so: Editor<'a>) -> InquireResult<Self> {
Ok(Self {
message: so.message,
editor_command: so.editor_command,
editor_command_args: so.editor_command_args,
help_message: so.help_message,
formatter: so.formatter,
validators: so.validators,
error: None,
tmp_file: Self::create_file(so.file_extension, so.predefined_text)?,
})
}
fn create_file(
file_extension: &str,
predefined_text: Option<&str>,
) -> std::io::Result<NamedTempFile> {
let mut tmp_file = tempfile::Builder::new()
.prefix("tmp-")
.suffix(file_extension)
.rand_bytes(10)
.tempfile()?;
if let Some(predefined_text) = predefined_text {
tmp_file.write_all(predefined_text.as_bytes())?;
tmp_file.flush()?;
}
Ok(tmp_file)
}
fn run_editor(&mut self) -> InquireResult<()> {
process::Command::new(self.editor_command)
.args(self.editor_command_args)
.arg(self.tmp_file.path())
.spawn()?
.wait()?;
Ok(())
}
fn render<B: EditorBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
let prompt = &self.message;
backend.frame_setup()?;
if let Some(err) = &self.error {
backend.render_error_message(err)?;
}
let path = Path::new(self.editor_command);
let editor_name = path
.file_stem()
.and_then(|f| f.to_str())
.unwrap_or("editor");
backend.render_prompt(prompt, editor_name)?;
if let Some(message) = self.help_message {
backend.render_help_message(message)?;
}
backend.frame_finish()?;
Ok(())
}
fn validate_current_answer(&self) -> InquireResult<Validation> {
let cur_answer = self.cur_answer()?;
for validator in &self.validators {
match validator.validate(&cur_answer) {
Ok(Validation::Valid) => {}
Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
Err(err) => return Err(InquireError::Custom(err)),
}
}
Ok(Validation::Valid)
}
fn cur_answer(&self) -> InquireResult<String> {
let mut read_handler = fs::File::open(self.tmp_file.path())?;
let mut submission = String::new();
read_handler.read_to_string(&mut submission)?;
let len = submission.trim_end_matches(&['\n', '\r'][..]).len();
submission.truncate(len);
Ok(submission)
}
fn prompt<B: EditorBackend>(mut self, backend: &mut B) -> InquireResult<String> {
let final_answer = loop {
self.render(backend)?;
let key = backend.read_key()?;
match key {
Key::Interrupt => interrupt_prompt!(),
Key::Cancel => cancel_prompt!(backend, self.message),
Key::Char('e', _) => self.run_editor()?,
Key::Submit => match self.validate_current_answer()? {
Validation::Valid => break self.cur_answer()?,
Validation::Invalid(msg) => self.error = Some(msg),
},
_ => {}
}
};
let formatted = (self.formatter)(&final_answer);
finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
}
}
fn get_default_editor_command() -> OsString {
let mut default_editor = if cfg!(windows) {
String::from("notepad")
} else {
String::from("nano")
};
if let Ok(editor) = env::var("EDITOR") {
if !editor.is_empty() {
default_editor = editor;
}
}
if let Ok(editor) = env::var("VISUAL") {
if !editor.is_empty() {
default_editor = editor;
}
}
default_editor.into()
}