use std::{io::Write, marker::PhantomData, sync::LazyLock};
use duat_core::{prelude::*, text::Searcher};
use super::IncSearcher;
use crate::{
hooks::{SearchPerformed, SearchUpdated},
widgets::PromptLine,
};
static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
#[derive(Clone)]
pub struct Prompt<U: Ui, M: PromptMode<U> = RunCommands>(M, String, PhantomData<U>);
impl<M: PromptMode<U>, U: Ui> Prompt<U, M> {
pub fn new(mode: M) -> Self {
Self(mode, String::new(), PhantomData)
}
pub fn new_with(mode: M, initial: impl ToString) -> Self {
Self(mode, initial.to_string(), PhantomData)
}
}
impl<M: PromptMode<U>, U: Ui> mode::Mode<U> for Prompt<U, M> {
type Widget = PromptLine<U>;
fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget, U>) {
let mut update = |pa: &mut Pass| {
let text = std::mem::take(handle.write(pa).text_mut());
let text = self.0.update(pa, text, handle.area(pa));
*handle.write(pa).text_mut() = text;
};
match key {
key!(KeyCode::Backspace) => {
if handle.read(pa).text().is_empty() {
handle.write(pa).text_mut().selections_mut().clear();
update(pa);
if let Some(ret_handle) = self.0.return_handle() {
mode::reset_to(ret_handle);
} else {
mode::reset::<M::ExitWidget, U>();
}
} else {
handle.edit_main(pa, |mut e| {
e.move_hor(-1);
e.set_anchor_if_needed();
e.replace("");
e.unset_anchor();
});
update(pa);
}
}
key!(KeyCode::Delete) => {
handle.edit_main(pa, |mut e| e.replace(""));
update(pa);
}
key!(KeyCode::Char(char)) => {
handle.edit_main(pa, |mut e| {
e.insert(char);
e.move_hor(1);
});
update(pa);
}
key!(KeyCode::Left) => {
handle.edit_main(pa, |mut e| e.move_hor(-1));
update(pa);
}
key!(KeyCode::Right) => {
handle.edit_main(pa, |mut e| e.move_hor(1));
update(pa);
}
key!(KeyCode::Esc) => {
let p = handle.read(pa).text().len();
handle.edit_main(pa, |mut e| {
e.move_to_start();
e.set_anchor();
e.move_to(p);
e.replace("");
});
handle.write(pa).text_mut().selections_mut().clear();
update(pa);
if let Some(ret_handle) = self.0.return_handle() {
mode::reset_to(ret_handle);
} else {
mode::reset::<M::ExitWidget, U>();
}
}
key!(KeyCode::Enter) => {
handle.write(pa).text_mut().selections_mut().clear();
update(pa);
if let Some(ret_handle) = self.0.return_handle() {
mode::reset_to(ret_handle);
} else {
mode::reset::<M::ExitWidget, U>();
}
}
_ => {}
}
}
fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
let text = {
let pl = handle.write(pa);
*pl.text_mut() = Text::new_with_selections();
pl.text_mut().replace_range(0..0, &self.1);
run_once::<M, U>();
let tag = Ghost(match pl.prompt_of::<M>() {
Some(text) => txt!("{text}[prompt.colon]:").build(),
None => txt!("{}[prompt.colon]:", self.0.prompt()).build(),
});
pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
std::mem::take(pl.text_mut())
};
let text = self.0.on_switch(pa, text, handle.area(pa));
*handle.write(pa).text_mut() = text;
}
fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget, U>) {
let text = std::mem::take(handle.write(pa).text_mut());
self.0.before_exit(pa, text, handle.area(pa));
}
}
#[allow(unused_variables)]
pub trait PromptMode<U: Ui>: Clone + Send + 'static {
type ExitWidget: Widget<U> = File<U>;
fn update(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text;
fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &U::Area) -> Text {
text
}
fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &U::Area) {}
fn once() {}
fn prompt(&self) -> Text;
fn return_handle(&self) -> Option<Handle<Self::ExitWidget, U>> {
None
}
}
#[derive(Default, Clone)]
pub struct RunCommands;
impl RunCommands {
pub fn new<U: Ui>() -> Prompt<U, Self> {
Prompt::new(Self)
}
pub fn new_with<U: Ui>(initial: impl ToString) -> Prompt<U, Self> {
Prompt::new_with(Self, initial)
}
}
impl<U: Ui> PromptMode<U> for RunCommands {
fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> 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!("parameter.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!("parameter.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, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
let call = text.to_string();
if !call.is_empty() {
cmd::queue_notify(call);
}
}
fn once() {
form::set_weak("caller.info", "accent.info");
form::set_weak("caller.error", "accent.error");
form::set_weak("parameter.info", "default.info");
form::set_weak("parameter.error", "default.error");
}
fn prompt(&self) -> Text {
Text::default()
}
}
#[derive(Clone)]
pub struct IncSearch<I: IncSearcher<U>, U: Ui> {
inc: I,
orig: Option<(mode::Selections, <U::Area as Area>::PrintInfo)>,
ghost: PhantomData<U>,
prev: String,
}
impl<I: IncSearcher<U>, U: Ui> IncSearch<I, U> {
pub fn new(inc: I) -> Prompt<U, Self> {
Prompt::new(Self {
inc,
orig: None,
ghost: PhantomData,
prev: String::new(),
})
}
}
impl<I: IncSearcher<U>, U: Ui> PromptMode<U> for IncSearch<I, U> {
fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
text.remove_tags(*TAGGER, ..);
let handle = context::fixed_file::<U>(pa).unwrap();
if text == self.prev {
return text;
} else {
let prev = std::mem::replace(&mut self.prev, text.to_string());
hook::queue(SearchUpdated((prev, self.prev.clone())));
}
match Searcher::new(text.to_string()) {
Ok(searcher) => {
let (file, area) = handle.write_with_area(pa);
area.set_print_info(orig_print_info.clone());
*file.selections_mut() = orig_selections.clone();
let ast = regex_syntax::ast::parse::Parser::new()
.parse(&text.to_string())
.unwrap();
crate::tag_from_ast(*TAGGER, &mut text, &ast);
self.inc.search(pa, handle.attach_searcher(searcher));
}
Err(err) => {
let regex_syntax::Error::Parse(err) = *err else {
unreachable!("As far as I can tell, regex_syntax has goofed up");
};
let span = err.span();
let id = form::id_of!("regex.error");
text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
}
}
text
}
fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) -> Text {
let handle = context::fixed_file::<U>(pa).unwrap();
self.orig = Some((
handle.read(pa).selections().clone(),
handle.area(pa).print_info(),
));
text
}
fn before_exit(&mut self, _: &mut Pass, text: Text, _: &<U as Ui>::Area) {
if !text.is_empty() {
if let Err(err) = Searcher::new(text.to_string()) {
let regex_syntax::Error::Parse(err) = *err else {
unreachable!("As far as I can tell, regex_syntax has goofed up");
};
let range = err.span().start.offset..err.span().end.offset;
let err = txt!(
"[a]{:?}, \"{}\"[prompt.colon]:[] {}",
range,
text.strs(range).unwrap(),
err.kind()
);
context::error!(target: self.inc.prompt().to_string(), "{err}")
} else {
hook::queue(SearchPerformed(text.to_string()));
}
}
}
fn once() {
form::set_weak("regex.error", "accent.error");
form::set_weak("regex.operator", "operator");
form::set_weak("regex.class", "constant");
form::set_weak("regex.bracket", "punctuation.bracket");
}
fn prompt(&self) -> Text {
txt!("{}", self.inc.prompt()).build()
}
}
#[derive(Clone, Copy)]
pub struct PipeSelections<U>(PhantomData<U>);
impl<U: Ui> PipeSelections<U> {
pub fn new() -> Prompt<U, Self> {
Prompt::new(Self(PhantomData))
}
}
impl<U: Ui> PromptMode<U> for PipeSelections<U> {
fn update(&mut self, _: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> 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::args_iter(&command);
let (caller_id, args_id) = if is_in_path(caller) {
(form::id_of!("caller.info"), form::id_of!("parameter.indo"))
} else {
(
form::id_of!("caller.error"),
form::id_of!("parameter.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, _: &<U as Ui>::Area) {
use std::process::{Command, Stdio};
let command = text.to_string();
let Some(caller) = command.split_whitespace().next() else {
return;
};
let handle = context::fixed_file::<U>(pa).unwrap();
handle.edit_all(pa, |mut c| {
let Ok(mut child) = Command::new(caller)
.args(cmd::args_iter(&command).map(|(a, _)| a))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
else {
return;
};
let input: String = c.selection().collect();
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").build()
}
}
fn run_once<M: PromptMode<U>, U: Ui>() {
use std::{any::TypeId, sync::Mutex};
static LIST: LazyLock<Mutex<Vec<TypeId>>> = LazyLock::new(|| Mutex::new(Vec::new()));
let mut list = LIST.lock().unwrap();
if !list.contains(&TypeId::of::<M>()) {
M::once();
list.push(TypeId::of::<M>());
}
}