#![doc(hidden)]
#[macro_export]
macro_rules! form {
[
$(
$id:ident: $type:ty {
// Parameters for each field using builder pattern methods
$(
$arg_id:ident $(: $arg_val:expr)?
),+
$(,)?
}
$(
if $control:expr => $control_err:literal
)*
),+,
$([$meta_id:ident]: $meta_expr:expr),*
$(,)?
] => {{
use std::{
convert::Into as __Into,
borrow::Cow as __Cow,
result::Result as __Result,
option::Option as __Option,
};
use $crate::{
dialog::form::internal as __internal,
field::Field as __Field,
};
#[allow(non_camel_case_types)]
enum __Indices {$(
$id,
)*}
#[allow(dead_code)]
struct __Values {$(
$id: <$type as __Field>::Value,
)*}
#[allow(dead_code)]
struct __BorrowedValues<'a> {$(
$id: &'a <$type as __Field>::Value,
)*}
struct __Control<'a> {$(
$id: __internal::Control<'a, $type>,
)*}
struct __Form<'a> {
__focus: usize,
__control: __Control<'a>,
__title: __Cow<'a, str>,
__message: __Cow<'a, str>,
$(
$id: $type,
)*
}
const __FIELDS: usize = [$(__Indices::$id),*].len();
impl __Form<'_> {
fn values(&self) -> __BorrowedValues {
__BorrowedValues {$(
$id: __Field::value(&self.$id),
)*}
}
fn into_values(self) -> __Values {
__Values {$(
$id: __Field::into_value(self.$id),
)*}
}
}
impl $crate::dialog::Dialog for __Form<'_> {
type Out = __Option<Self>;
fn format(&self) -> $crate::dialog::DrawInfo {
let name_lengths = [$(
__Field::name(&self.$id).len(),
)*];
let max_name = name_lengths
.into_iter()
.max()
.unwrap_or(0);
let mut fields = [
$({
let focus = __Indices::$id as usize == self.__focus;
let name = __Field::name(&self.$id);
let body = __Field::format(&self.$id, focus);
let error = self.__control.$id.is_err();
__internal::format_field(name, body, focus, max_name, error)
},)*
];
__internal::format_dialog(&mut fields, self.__message.as_ref(), self.__title.as_ref())
}
fn input(mut self, key: KeyEvent) -> $crate::Signal<Self> {
use $crate::{Signal, field::InputResult};
type Dispatch<'a> = fn(&mut __Form, KeyEvent) -> InputResult;
const JUMP_TABLE: [Dispatch; __FIELDS] = [$(
|form, key| __internal::input_dispatch(&mut form.$id, &mut form.__control.$id, key)
),*];
match key.code {
KeyCode::Esc => Signal::Return(None),
KeyCode::Enter => Signal::Return(Some(self)),
_ => {
let dispatch_result = JUMP_TABLE[self.__focus](&mut self, key);
match (dispatch_result, key.code) {
(InputResult::Ignored, KeyCode::Up) => {
self.__focus = self.__focus.saturating_sub(1);
}
(InputResult::Ignored, KeyCode::Down) => {
self.__focus = usize::min(self.__focus + 1, __FIELDS - 1);
}
_ => (),
};
Signal::Continue(self)
}
}
}
}
fn __run<'a, T>(
mut form: __Form<'a>,
bg: &impl $crate::State,
ctx: &mut $crate::Context<T>,
mut validate: impl std::ops::FnMut(__BorrowedValues) -> __Result<(), __Cow<'a, str>>,
) -> __Option<__Values> {
use $crate::dialog::Dialog as _;
loop {
let __Option::Some(out) = form.run_over(bg, ctx) else {
break None
};
form = out;
let control_result = __internal::format_control_error(&[$(
(__Field::name(&form.$id), form.__control.$id.updated_result(&form.$id)),
)*]);
let validation_result = match control_result {
__Result::Ok(()) => validate(form.values()),
__Result::Err(e) => __Result::Err(__Cow::from(e)),
};
match validation_result {
__Result::Ok(()) => break __Option::Some(form.into_values()),
__Result::Err(e) => $crate::dialog::error(e, bg, ctx),
}
}
}
struct __Meta<'a, A, B, C, D, E, X>
where
A: __Into<__Cow<'a, str>>,
D: __Into<__Cow<'a, str>>,
E: std::ops::FnMut(__BorrowedValues) -> __Result<(), X>,
X: __Into<__Cow<'a, str>>,
{
title: A,
context: &'a mut $crate::Context<B>,
background: &'a C,
message: D,
validate: E,
}
let mut meta = $crate::parse_form_meta!{
__Meta {
$($meta_id: $meta_expr,)*
} else {
message: "",
validate: |_| __Result::<(), __Cow<'_, str>>::Ok(()),
}
};
let control = __Control {
$($id: __internal::Control {
callback: &|value: &<$type as __Field>::Value| {
$(
if $control(value) {
return __Result::Err(__Cow::from($control_err))
}
)*
let _ = value;
__Result::Ok(())
},
state: __internal::ControlState::Unknown,
},)*
};
let validate = |values: __BorrowedValues| (meta.validate)(values).map_err(__Cow::from);
let form = __Form {
__focus: 0,
__control: control,
__title: __Cow::from(meta.title),
__message: __Cow::from(meta.message),
$($id: {
let builder = <$type as __Field>::builder()
$(
.$arg_id($($arg_val)?)
)*;
$crate::field::Build::build(builder)
},)*
};
__run(form, meta.background, meta.context, validate)
}}
}
#[macro_export]
#[doc(hidden)]
macro_rules! parse_form_meta {
[
$struct:ident {
$($meta_id:ident: $meta_val:expr,)*
} else {
$($default_id:ident: $default_val:expr,)*
}
] => {
$crate::parse_form_meta!{@impl $struct ($)
<$(($default_id, $default_val))*>
<>
$(($meta_id, $meta_val))*
}
};
[@impl $struct:ident $_:tt
<$(($default_id:ident, $default_val:expr))*>
<$(($id:ident, $val:expr))*>
] => {
$struct {
$(
$id: $val,
)*
$(
$default_id: $default_val,
)*
}
};
[@impl $struct:ident ($s:tt)
<$(($default_id:ident, $default_val:expr))*>
<$(($acc_id:ident, $acc_val:expr))*>
($id:ident, $val:expr) $($tail:tt)*
] => {{
macro_rules! __filter {
[<$s(($s ID:ident, $s VAL:expr))*>] => {
$crate::parse_form_meta!{@impl $struct ($s)
<$s(($s ID, $s VAL))*>
<$(($acc_id, $acc_val))* ($id, $val)>
$($tail)*
}
};
[<$s(($s ID:ident, $s VAL:expr))*> ($id, $s _:tt) $s($s TAIL:tt)*] => {
__filter!(<$s(($s ID, $s VAL))*> $s($s TAIL)*)
};
[<$s(($s ID:ident, $s VAL:expr))*> $s HEAD:tt $s($s TAIL:tt)*] => {
__filter!(<$s(($s ID, $s VAL))* $s HEAD> $s($s TAIL)*)
};
}
__filter!(<> $(($default_id, $default_val))*)
}};
}
pub mod internal {
use ratatui::{
style::{Style, Stylize},
text::{Line, Span},
};
use crate::{dialog::*, field::{Field, InputResult}};
pub enum ControlState<'a> {
Unknown,
Ok,
Err(Cow<'a, str>),
}
pub struct Control<'a, T: Field> {
pub callback: &'a dyn Fn(&T::Value) -> Result<(), Cow<'a, str>>,
pub state: ControlState<'a>,
}
impl<'a, T: Field> Control<'a, T> {
pub fn updated_result<'b>(&'b mut self, field: &T) -> Result<(), &'b str> {
if let ControlState::Unknown = self.state {
self.update(field);
}
match &self.state {
ControlState::Unknown => unreachable!(),
ControlState::Ok => Ok(()),
ControlState::Err(e) => Err(e),
}
}
pub fn update(&mut self, field: &T) {
self.state = match (self.callback)(field.value()) {
Ok(()) => ControlState::Ok,
Err(err) => ControlState::Err(err),
};
}
pub const fn is_err(&self) -> bool {
match self.state {
ControlState::Unknown => false,
ControlState::Ok => false,
ControlState::Err(_) => true,
}
}
}
#[inline(never)]
pub fn input_dispatch<T: Field>(field: &mut T, control: &mut Control<T>, key: KeyEvent) -> InputResult {
let result = field.input(key);
if let InputResult::Updated = result {
control.update(&field);
}
result
}
#[inline(never)]
pub fn format_field<'a>(name: &'a str, mut body: Text<'a>, focused: bool, align_to: usize, error: bool)
-> Text<'a>
{
if body.lines.is_empty() {
body.lines.push(Line::default())
}
{
let delimiter = match focused {
true => " : ",
false => " │ ",
};
let style = {
let style = Style::default();
let style = match focused {
true => style.bold(),
false => style,
};
let style = match error {
true => style.red(),
false => style,
};
style
};
let padding: Span = std::iter::repeat(' ')
.take(align_to.saturating_sub(name.len()))
.collect::<String>()
.into();
let name = Span::styled(name, style);
let delimiter = Span::raw(delimiter);
let title = [padding, name, delimiter];
body.lines[0].spans.splice(0..0, title);
};
for line in &mut body.lines[1..] {
let indent: String = std::iter::repeat(' ')
.take(align_to)
.chain(" │ ".chars())
.collect();
line.spans.insert(0, indent.into());
}
body
}
#[inline(never)]
pub fn format_dialog<'a>(fields: &mut [Text<'a>], message: &'a str, title: &'a str) -> DrawInfo<'a> {
let message = (message.len() != 0)
.then(|| [Text::from(message), Text::from("")])
.into_iter()
.flatten();
let fields = fields
.into_iter()
.map(std::mem::take);
let body = message
.chain(fields)
.fold(Text::default(), |mut acc, body| {
acc.extend(body);
acc
});
DrawInfo {
title: Cow::from(title),
body,
hint: Cow::from("Press (enter) to submit, (esc) to cancel..."),
wrap: None,
..DrawInfo::default()
}
}
#[inline(never)]
pub fn format_control_error(results: &[(&str, Result<(), &str>)]) -> Result<(), String> {
let messages: Vec<String> = results
.iter()
.filter_map(|(name, state)| state
.as_ref()
.err()
.map(|e| (name, e))
)
.map(|(name, error)| format!("{name}: {error}"))
.collect();
match messages.is_empty() {
true => Ok(()),
false => Err(messages.join("\n")),
}
}
}
pub use form;