use std::io;
use crossterm::event::KeyCode;
use ratatui::Frame;
use super::{FieldState, Form, FormOutcome};
use crate::{
date_picker, editor, input,
modal::ModalSignal,
nav, overlay,
terminal::{Tui, TuiEvent},
theme::Skin,
};
impl Form {
pub fn run(
&mut self,
tui: &mut Tui,
skin: &Skin,
render_bg: impl Fn(&mut Frame),
) -> io::Result<FormOutcome> {
if self.fields.is_empty() {
return Ok(FormOutcome::Cancelled);
}
loop {
tui.draw(|frame| {
render_bg(frame);
overlay::dim(frame, overlay::SCRIM_FACTOR);
self.render(frame, skin);
})?;
match tui.read_event()? {
TuiEvent::Quit => return Ok(FormOutcome::Quit),
TuiEvent::Resize => {}
TuiEvent::Paste(text) => self.paste_into_focus(&text),
TuiEvent::Key(key) => {
let ctrl = input::is_command(key);
match key.code {
KeyCode::Esc => return Ok(FormOutcome::Cancelled),
KeyCode::Char('s') if ctrl => {
return Ok(FormOutcome::Saved);
}
KeyCode::Tab => {
self.focus =
nav::cycle(self.focus, self.fields.len(), 1);
}
KeyCode::BackTab => {
self.focus =
nav::cycle(self.focus, self.fields.len(), -1);
}
_ => {
if let Some(outcome) =
self.handle_field_key(key, tui, skin)?
{
return Ok(outcome);
}
}
}
}
}
}
}
fn paste_into_focus(&mut self, text: &str) {
match &mut self.fields[self.focus].state {
FieldState::Text { input, .. } => input.paste(text),
FieldState::Multiline { area, .. } => area.paste(text),
FieldState::Bool { .. }
| FieldState::Choice { .. }
| FieldState::Date { .. } => {}
}
}
fn handle_field_key(
&mut self,
key: crossterm::event::KeyEvent,
tui: &mut Tui,
skin: &Skin,
) -> io::Result<Option<FormOutcome>> {
let ctrl = input::is_command(key);
if key.code == KeyCode::Enter
&& matches!(self.fields[self.focus].state, FieldState::Date { .. })
{
return self.open_date_picker(tui, skin);
}
if ctrl && key.code == KeyCode::Char('g') {
self.open_editor_for_focus(tui)?;
return Ok(None);
}
match &mut self.fields[self.focus].state {
FieldState::Text { input, .. } => {
input.handle_key(key);
}
FieldState::Multiline { area, .. } => {
area.handle_key(key);
}
FieldState::Bool { value, .. } => {
if toggles_checkbox(key) {
*value = !*value;
}
}
FieldState::Choice { index, options, .. } => match key.code {
KeyCode::Left => *index = nav::cycle(*index, options.len(), -1),
KeyCode::Right => *index = nav::cycle(*index, options.len(), 1),
_ => {}
},
FieldState::Date { .. } => {}
}
Ok(None)
}
fn open_date_picker(
&mut self,
tui: &mut Tui,
skin: &Skin,
) -> io::Result<Option<FormOutcome>> {
let current = match &self.fields[self.focus].state {
FieldState::Date { value, .. } => *value,
_ => None,
};
let signal = {
let form: &Form = self;
date_picker::date_picker(
tui,
skin,
" Date ",
current,
true,
|frame| form.render(frame, skin),
)?
};
match signal {
ModalSignal::Quit => return Ok(Some(FormOutcome::Quit)),
ModalSignal::Cancelled => {}
ModalSignal::Value(date) => {
if let FieldState::Date { value, .. } =
&mut self.fields[self.focus].state
{
*value = date;
}
}
}
Ok(None)
}
fn open_editor_for_focus(&mut self, tui: &mut Tui) -> io::Result<()> {
let FieldState::Multiline { area, .. } = &self.fields[self.focus].state
else {
return Ok(());
};
let initial = area.text().to_string();
let command = editor::resolve_editor();
if let Some(text) = editor::edit_in_editor(tui, &command, &initial)?
&& let FieldState::Multiline { area, .. } =
&mut self.fields[self.focus].state
{
area.set_text(text);
}
Ok(())
}
}
pub(super) fn toggles_checkbox(key: crossterm::event::KeyEvent) -> bool {
match key.code {
KeyCode::Char(' ') => input::is_bare_character(key),
KeyCode::Enter => true,
_ => false,
}
}