Skip to main content

hjkl_form/
lib.rs

1//! # hjkl-form
2//!
3//! Vim-modal forms for hjkl-based apps.
4//!
5//! Each text field hosts its own [`hjkl_engine::Editor`], so users get
6//! the full vim grammar inside form inputs. The form itself runs a
7//! small FSM over `Form-Normal` / `Form-Insert` modes for focus
8//! navigation and validation, delegating keystrokes to the focused
9//! field's editor when in insert mode.
10//!
11//! Renderers live in adapter crates: `hjkl-editor-tui::form::draw_form`
12//! ships the ratatui flavor.
13#![forbid(unsafe_code)]
14
15pub mod field;
16pub mod form;
17pub mod fsm;
18pub mod host;
19pub mod submit;
20pub mod validate;
21
22pub use field::{CheckboxField, Field, FieldMeta, SelectField, SubmitField, TextFieldEditor};
23pub use form::{Form, FormEvent, FormMode};
24pub use host::FormFieldHost;
25pub use submit::{SubmitFn, SubmitOutcome};
26pub use validate::{Validator, validate_field};
27
28// Convenience re-exports — consumers using `TextFieldEditor` standalone
29// shouldn't need to depend on `hjkl-engine` directly just to talk about
30// inputs and modes.
31pub use hjkl_engine::{CoarseMode, Input, Key};
32
33#[cfg(test)]
34mod smoke_tests {
35    use super::*;
36
37    #[test]
38    fn empty_form_constructs() {
39        let form = Form::new();
40        assert_eq!(form.focused(), 0);
41        assert_eq!(form.mode, FormMode::Normal);
42    }
43
44    #[test]
45    fn two_field_form_focuses_first() {
46        let form = Form::new()
47            .with_title("Test")
48            .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
49                FieldMeta::new("Name"),
50                1,
51            )))
52            .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Submit"))));
53        assert_eq!(form.fields.len(), 2);
54        assert_eq!(form.focused(), 0);
55        assert_eq!(form.mode, FormMode::Normal);
56    }
57
58    #[test]
59    fn dirty_gen_advances_on_field_edit() {
60        use hjkl_engine::{Input, Key};
61        let mut form = Form::new()
62            .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
63                FieldMeta::new("Name"),
64                1,
65            )))
66            .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Submit"))));
67        let before = form.dirty_gen();
68        form.handle_input(Input {
69            key: Key::Char('i'),
70            ..Input::default()
71        });
72        form.handle_input(Input {
73            key: Key::Char('x'),
74            ..Input::default()
75        });
76        let after = form.dirty_gen();
77        assert!(after != before, "dirty_gen should advance after edit");
78    }
79
80    #[test]
81    fn dirty_gen_advances_on_focus_change() {
82        use hjkl_engine::{Input, Key};
83        let mut form = Form::new()
84            .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
85                FieldMeta::new("A"),
86                1,
87            )))
88            .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
89                FieldMeta::new("B"),
90                1,
91            )));
92        let before = form.dirty_gen();
93        form.handle_input(Input {
94            key: Key::Char('j'),
95            ..Input::default()
96        });
97        let after = form.dirty_gen();
98        assert!(after != before);
99    }
100}