use std::{
any::TypeId,
io::Write,
sync::{LazyLock, Mutex, Once},
};
use duat_core::{
Ns,
buffer::Buffer,
cmd,
context::{self, Handle},
data::Pass,
form::{self, Form},
hook::{self, ModeSwitched},
mode::{self, KeyEvent, event, shift},
text::{Inlay, Text, txt},
ui::{RwArea, Widget},
};
use crate::widgets::{CommandsCompletions, Completions, PromptLine};
static HISTORY: Mutex<Vec<(TypeId, Vec<String>)>> = Mutex::new(Vec::new());
static PROMPT_TAGGER: LazyLock<Ns> = LazyLock::new(Ns::new);
static TAGGER: LazyLock<Ns> = LazyLock::new(Ns::new);
static PREVIEW_TAGGER: LazyLock<Ns> = LazyLock::new(Ns::new);
pub fn add_prompt_hook() {
hook::add::<ModeSwitched>(|pa, mut switch| {
if let Some(prompt) = switch.new.get_as::<Prompt>() {
let Some(promptline) = context::handle_of::<PromptLine>(pa) else {
return;
};
let text = {
let pl = promptline.write(pa);
pl.text = Text::with_default_main_selection();
pl.text_mut().replace_range(0..0, &prompt.starting_text);
let tag = Inlay::new(match pl.prompt_of_id(prompt.ty) {
Some(text) => txt!("{text}[prompt.colon]:"),
None => txt!("{}[prompt.colon]:", prompt.mode.prompt()),
});
pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
std::mem::take(&mut pl.text)
};
let text = prompt.mode.on_switch(pa, text, promptline.area());
promptline.write(pa).text = text;
prompt.show_preview(pa, promptline);
} else if let Some(prompt) = switch.old.get_as::<Prompt>() {
let Some(promptline) = context::handle_of::<PromptLine>(pa) else {
return;
};
let text = std::mem::take(&mut promptline.write(pa).text);
if !text.is_empty() {
let mut history = HISTORY.lock().unwrap();
if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == prompt.ty) {
if ty_history.last().is_none_or(|last| last != &text) {
ty_history.push(text.to_string());
}
} else {
history.push((prompt.ty, vec![text.to_string()]));
}
}
prompt.mode.before_exit(pa, text, promptline.area());
}
});
}
pub struct Prompt {
mode: Box<dyn PromptMode>,
starting_text: String,
ty: TypeId,
reset_fn: fn(&mut Pass),
history_index: Option<usize>,
}
impl Prompt {
pub fn new<M: PromptMode>(mode: M) -> Self {
Self {
mode: Box::new(mode),
starting_text: String::new(),
ty: TypeId::of::<M>(),
reset_fn: |pa| mode::reset::<M::ExitWidget>(pa),
history_index: None,
}
}
pub fn new_with<M: PromptMode>(mode: M, initial: impl ToString) -> Self {
Self {
mode: Box::new(mode),
starting_text: initial.to_string(),
ty: TypeId::of::<M>(),
reset_fn: |pa| mode::reset::<M::ExitWidget>(pa),
history_index: None,
}
}
fn show_preview(&mut self, pa: &mut Pass, handle: Handle<PromptLine>) {
let history = HISTORY.lock().unwrap();
if handle.text(pa).is_empty()
&& let Some((_, ty_history)) = history.iter().find(|(ty, _)| *ty == self.ty)
{
handle.text_mut(pa).insert_tag_after(
*PREVIEW_TAGGER,
0,
Inlay::new(txt!("[prompt.preview]{}", ty_history.last().unwrap())),
);
}
}
}
impl mode::Mode for Prompt {
type Widget = PromptLine;
fn bindings() -> mode::Bindings {
use mode::KeyCode::*;
mode::bindings!(match _ {
event!(Char(..)) => txt!("Insert the character"),
event!(Left | Right) => txt!("Move cursor"),
event!(Down | Up) => txt!("Move through command history"),
event!(Backspace | Delete) => txt!("Remove character or selection"),
event!(Enter) => txt!("Run command and [mode]leave"),
event!(Esc) => txt!("[mode]Leave[] without running command"),
})
}
fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, promptline: Handle<Self::Widget>) {
use duat_core::mode::KeyCode::*;
let ty_eq = |&&(ty, _): &&(TypeId, _)| ty == self.ty;
let mut update = |pa: &mut Pass| {
let text = std::mem::take(&mut promptline.write(pa).text);
let text = self.mode.update(pa, text, promptline.area());
promptline.write(pa).text = text;
};
let reset = |pa: &mut Pass, prompt: &mut Self| {
if let Some(ret_handle) = prompt.mode.return_handle() {
mode::reset_to(pa, &ret_handle);
} else {
(prompt.reset_fn)(pa);
}
};
promptline.text_mut(pa).remove_tags(*PREVIEW_TAGGER, ..);
match key {
event!(Char(char)) => {
promptline.edit_main(pa, |mut c| {
c.insert(char);
c.move_hor(1);
});
update(pa);
}
event!(Backspace) => {
if promptline.read(pa).text().is_empty() {
promptline.write(pa).text_mut().selections_mut().clear();
update(pa);
if let Some(ret_handle) = self.mode.return_handle() {
mode::reset_to(pa, &ret_handle);
} else {
(self.reset_fn)(pa);
}
} else {
promptline.edit_main(pa, |mut c| {
c.move_hor(-1);
c.set_anchor_if_needed();
c.replace("");
c.unset_anchor();
});
update(pa);
}
}
event!(Delete) => {
promptline.edit_main(pa, |mut c| {
c.set_anchor_if_needed();
c.replace("");
});
update(pa);
}
event!(Left) => {
promptline.edit_main(pa, |mut c| c.move_hor(-1));
update(pa);
}
event!(Right) => {
promptline.edit_main(pa, |mut c| c.move_hor(1));
update(pa);
}
event!(Up) => {
let history = HISTORY.lock().unwrap();
let Some((_, ty_history)) = history.iter().find(ty_eq) else {
return;
};
let index = if let Some(index) = &mut self.history_index {
*index = index.saturating_sub(1);
*index
} else {
self.history_index = Some(ty_history.len() - 1);
ty_history.len() - 1
};
promptline.edit_main(pa, |mut c| {
c.move_to(..);
c.replace(ty_history[index].clone());
c.unset_anchor();
});
update(pa);
}
event!(Down) => {
let history = HISTORY.lock().unwrap();
let Some((_, ty_history)) = history.iter().find(ty_eq) else {
return;
};
if let Some(index) = &mut self.history_index {
if *index + 1 < ty_history.len() {
*index = (*index + 1).min(ty_history.len() - 1);
promptline.edit_main(pa, |mut c| {
c.move_to(..);
c.replace(ty_history[*index].clone());
c.unset_anchor();
})
} else {
self.history_index = None;
promptline.edit_main(pa, |mut c| {
c.move_to(..);
c.replace("");
c.unset_anchor();
})
}
};
update(pa);
}
event!(Tab) => {
Completions::scroll(pa, 1);
update(pa);
}
shift!(BackTab) => {
Completions::scroll(pa, -1);
update(pa);
}
event!(Esc) => {
promptline.edit_main(pa, |mut c| {
c.move_to(..);
c.replace("");
});
promptline.write(pa).text_mut().selections_mut().clear();
update(pa);
reset(pa, self);
}
event!(Enter) => {
promptline.write(pa).text_mut().selections_mut().clear();
if promptline.text(pa).is_empty() {
let history = HISTORY.lock().unwrap();
if let Some((_, ty_history)) = history.iter().find(ty_eq) {
promptline.edit_main(pa, |mut c| {
c.move_to(..);
c.replace(ty_history.last().unwrap());
});
}
}
update(pa);
reset(pa, self);
}
_ => {}
}
self.mode.post_update(pa, &promptline);
self.show_preview(pa, promptline);
}
}
#[allow(unused_variables)]
pub trait PromptMode: Send + 'static {
type ExitWidget: Widget
where
Self: Sized;
fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
text
}
fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {}
fn prompt(&self) -> Text;
fn return_handle(&self) -> Option<Handle<dyn Widget>> {
None
}
}
#[derive(Default)]
pub struct RunCommands(Option<Completion>);
impl RunCommands {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Prompt {
Self::call_once();
Prompt::new(Self(None))
}
pub fn new_with(initial: impl ToString) -> Prompt {
Self::call_once();
Prompt::new_with(Self(None), initial)
}
fn call_once() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
form::set_weak("caller.info", Form::mimic("accent.info"));
form::set_weak("caller.error", Form::mimic("accent.error"));
form::set_weak("param.info", Form::mimic("default.info"));
form::set_weak("param.error", Form::mimic("default.error"));
});
}
}
impl PromptMode for RunCommands {
type ExitWidget = Buffer;
fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
text.remove_tags(*TAGGER, ..);
let command = text.to_string();
let caller = command.split_whitespace().next();
if let Some(caller) = caller {
if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
let id = form::id_of!("caller.info");
text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
let default_id = form::id_of!("param.info");
for (range, id) in ok_ranges {
text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
}
if let Some((range, _)) = err_range {
let id = form::id_of!("param.error");
text.insert_tag(*TAGGER, range, id.to_tag(0));
}
} else {
let id = form::id_of!("caller.error");
text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
}
}
text
}
fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
let call = text.to_string_no_last_nl();
if !call.is_empty() {
_ = cmd::call_notify(pa, call);
}
}
fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {
let text = handle.text(pa);
let Some(main) = text.get_main_sel() else {
Completions::close(pa);
return;
};
let is_parameter = text[..main.caret()]
.chars()
.rev()
.any(|char| char.is_whitespace());
let new_completion = if is_parameter {
let call = text[..main.caret()].to_string();
let Some(parameters) = cmd::last_parsed_parameters(pa, &call) else {
self.0 = None;
Completions::close(pa);
return;
};
Completion::Parameters(parameters)
} else {
Completion::Caller
};
if self.0.as_ref() != Some(&new_completion) {
match &new_completion {
Completion::Caller => Completions::builder()
.with_provider(CommandsCompletions::new(pa))
.open(pa),
Completion::Parameters(params) => Completions::open_for(pa, params),
}
}
self.0 = Some(new_completion)
}
fn prompt(&self) -> Text {
Text::default()
}
}
#[derive(Clone, Copy)]
pub struct PipeSelections;
impl PipeSelections {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Prompt {
Prompt::new(Self)
}
}
impl PromptMode for PipeSelections {
type ExitWidget = Buffer;
fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
fn is_in_path(program: &str) -> bool {
if let Ok(path) = std::env::var("PATH") {
for p in path.split(":") {
let p_str = format!("{p}/{program}");
if let Ok(true) = std::fs::exists(p_str) {
return true;
}
}
}
false
}
text.remove_tags(*TAGGER, ..);
let command = text.to_string();
let Some(caller) = command.split_whitespace().next() else {
return text;
};
let args = cmd::ArgsIter::new(&command);
let (caller_id, args_id) = if is_in_path(caller) {
(form::id_of!("caller.info"), form::id_of!("param.info"))
} else {
(form::id_of!("caller.error"), form::id_of!("param.error"))
};
let c_s = command.len() - command.trim_start().len();
text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
for (_, range, _) in args {
text.insert_tag(*TAGGER, range, args_id.to_tag(0));
}
text
}
fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
use std::process::{Command, Stdio};
let command = text.to_string();
let Some(caller) = command.split_whitespace().next() else {
return;
};
let handle = context::current_buffer(pa);
handle.edit_all(pa, |mut c| {
let Ok(mut child) = Command::new(caller)
.args(cmd::ArgsIter::new(&command).map(|(a, ..)| a))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
else {
return;
};
let input = c.selection().to_string();
if let Some(mut stdin) = child.stdin.take() {
std::thread::spawn(move || {
stdin.write_all(input.as_bytes()).unwrap();
});
}
if let Ok(out) = child.wait_with_output() {
let out = String::from_utf8_lossy(&out.stdout);
c.set_anchor_if_needed();
c.replace(out);
}
});
}
fn prompt(&self) -> Text {
txt!("[prompt]pipe")
}
}
#[derive(Clone, Eq)]
enum Completion {
Caller,
Parameters(Vec<TypeId>),
}
impl PartialEq for Completion {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Parameters(l0), Self::Parameters(r0)) => {
l0.iter().all(|param| r0.contains(param))
&& r0.iter().all(|param| l0.contains(param))
}
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}