Skip to main content

huh/
lib.rs

1#![forbid(unsafe_code)]
2// Per-lint allows for huh's form/prompt components.
3#![allow(clippy::bool_to_int_with_if)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::doc_markdown)]
6#![allow(clippy::format_collect)]
7#![allow(clippy::format_push_string)]
8#![allow(clippy::map_unwrap_or)]
9#![allow(clippy::missing_const_for_fn)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::no_effect_underscore_binding)]
12#![allow(clippy::option_if_let_else)]
13#![allow(clippy::redundant_clone)]
14#![allow(clippy::redundant_closure_for_method_calls)]
15#![allow(clippy::return_self_not_must_use)]
16#![allow(clippy::struct_excessive_bools)]
17#![allow(clippy::too_many_lines)]
18#![allow(clippy::uninlined_format_args)]
19#![allow(clippy::used_underscore_binding)]
20
21//! # Huh
22//!
23//! A library for building interactive forms and prompts in the terminal.
24//!
25//! Huh provides a declarative way to create:
26//! - Text inputs and text areas
27//! - Select menus and multi-select
28//! - Confirmations and notes
29//! - Grouped form fields
30//! - Accessible, keyboard-navigable interfaces
31//!
32//! ## Role in `charmed_rust`
33//!
34//! Huh is the form and prompt layer built on bubbletea and bubbles:
35//! - **bubbletea** provides the runtime and update loop.
36//! - **bubbles** supplies reusable widgets (text input, list, etc.).
37//! - **lipgloss** handles consistent styling and themes.
38//! - **demo_showcase** uses huh to demonstrate multi-step workflows.
39//!
40//! ## Example
41//!
42//! ```rust,ignore
43//! use huh::{Form, Group, Input, Select, SelectOption, Confirm};
44//! use bubbletea::Program;
45//!
46//! let form = Form::new(vec![
47//!     Group::new(vec![
48//!         Box::new(Input::new()
49//!             .key("name")
50//!             .title("What's your name?")),
51//!         Box::new(Select::new()
52//!             .key("color")
53//!             .title("Favorite color?")
54//!             .options(vec![
55//!                 SelectOption::new("Red", "red"),
56//!                 SelectOption::new("Green", "green"),
57//!                 SelectOption::new("Blue", "blue"),
58//!             ])),
59//!     ]),
60//!     Group::new(vec![
61//!         Box::new(Confirm::new()
62//!             .key("confirm")
63//!             .title("Are you sure?")),
64//!     ]),
65//! ]);
66//!
67//! let form = Program::new(form).run()?;
68//!
69//! let name = form.get_string("name").unwrap();
70//! let color = form.get_string("color").unwrap();
71//! let confirm = form.get_bool("confirm").unwrap();
72//!
73//! println!("Name: {}, Color: {}, Confirmed: {}", name, color, confirm);
74//! ```
75
76use std::any::Any;
77use std::sync::atomic::{AtomicUsize, Ordering};
78
79use thiserror::Error;
80
81use bubbles::key::Binding;
82use bubbletea::{Cmd, KeyMsg, KeyType, Message, Model};
83use lipgloss::{Border, Style};
84
85// -----------------------------------------------------------------------------
86// ID Generation
87// -----------------------------------------------------------------------------
88
89static LAST_ID: AtomicUsize = AtomicUsize::new(0);
90
91fn next_id() -> usize {
92    LAST_ID.fetch_add(1, Ordering::SeqCst)
93}
94
95// -----------------------------------------------------------------------------
96// Errors
97// -----------------------------------------------------------------------------
98
99/// Errors that can occur during form execution.
100///
101/// This enum represents all possible error conditions when running
102/// an interactive form with huh.
103///
104/// # Error Handling
105///
106/// Forms can fail for several reasons, but many are recoverable
107/// or expected user actions (like cancellation):
108///
109/// ```rust,ignore
110/// use huh::{Form, FormError, Result};
111///
112/// fn get_user_input() -> Result<String> {
113///     let mut name = String::new();
114///     Form::new(fields)
115///         .run()?;
116///     Ok(name)
117/// }
118/// ```
119///
120/// # Recovery Strategies
121///
122/// | Error Variant | Recovery Strategy |
123/// |--------------|-------------------|
124/// | [`UserAborted`](FormError::UserAborted) | Normal exit, not an error condition |
125/// | [`Timeout`](FormError::Timeout) | Retry with longer timeout or prompt user |
126/// | [`Validation`](FormError::Validation) | Show error message, allow retry |
127/// | [`Io`](FormError::Io) | Check terminal, fall back to non-interactive |
128///
129/// # Example: Handling User Abort
130///
131/// User abort (Ctrl+C) is a normal exit path, not an error:
132///
133/// ```rust,ignore
134/// match form.run() {
135///     Ok(()) => println!("Form completed!"),
136///     Err(FormError::UserAborted) => {
137///         println!("Cancelled by user");
138///         return Ok(()); // Not an error condition
139///     }
140///     Err(e) => return Err(e.into()),
141/// }
142/// ```
143///
144/// # Note on Clone and PartialEq
145///
146/// This error type implements `Clone` and `PartialEq` to support
147/// testing and comparison. As a result, the `Io` variant stores
148/// a `String` message rather than the underlying `io::Error`.
149#[derive(Error, Debug, Clone, PartialEq, Eq)]
150pub enum FormError {
151    /// User aborted the form with Ctrl+C or Escape.
152    ///
153    /// This is not an error condition but a normal exit path.
154    /// Users may cancel forms for valid reasons, and applications
155    /// should handle this gracefully.
156    ///
157    /// # Example
158    ///
159    /// ```rust,ignore
160    /// match form.run() {
161    ///     Err(FormError::UserAborted) => {
162    ///         println!("No changes made");
163    ///         return Ok(());
164    ///     }
165    ///     // ...
166    /// }
167    /// ```
168    #[error("user aborted")]
169    UserAborted,
170
171    /// Form execution timed out.
172    ///
173    /// Occurs when a form has a timeout configured and the user
174    /// does not complete it in time.
175    ///
176    /// # Recovery
177    ///
178    /// - Increase the timeout duration
179    /// - Prompt user to try again
180    /// - Use a default value
181    #[error("timeout")]
182    Timeout,
183
184    /// Custom validation error.
185    ///
186    /// Occurs when a field's validation function returns an error.
187    /// The contained string describes what validation failed.
188    ///
189    /// # Recovery
190    ///
191    /// Validation errors are recoverable - show the error message
192    /// to the user and allow them to correct their input.
193    ///
194    /// # Example
195    ///
196    /// ```rust,ignore
197    /// let input = Input::new()
198    ///     .title("Email")
199    ///     .validate(|s| {
200    ///         if s.contains('@') {
201    ///             Ok(())
202    ///         } else {
203    ///             Err(FormError::Validation("must contain @".into()))
204    ///         }
205    ///     });
206    /// ```
207    #[error("validation error: {0}")]
208    Validation(String),
209
210    /// IO error during form operations.
211    ///
212    /// Occurs during terminal I/O operations, particularly in
213    /// accessible mode where stdin/stdout are used directly.
214    ///
215    /// Note: Stores the error message as a `String` rather than
216    /// `io::Error` to maintain `Clone` and `PartialEq` derives.
217    ///
218    /// # Recovery
219    ///
220    /// - Check if the terminal is available
221    /// - Fall back to non-interactive input
222    /// - Log the error and exit gracefully
223    #[error("io error: {0}")]
224    Io(String),
225}
226
227impl FormError {
228    /// Creates a validation error with the given message.
229    pub fn validation(message: impl Into<String>) -> Self {
230        Self::Validation(message.into())
231    }
232
233    /// Creates an IO error with the given message.
234    pub fn io(message: impl Into<String>) -> Self {
235        Self::Io(message.into())
236    }
237
238    /// Returns true if this is a user-initiated abort.
239    pub fn is_user_abort(&self) -> bool {
240        matches!(self, Self::UserAborted)
241    }
242
243    /// Returns true if this is a timeout error.
244    pub fn is_timeout(&self) -> bool {
245        matches!(self, Self::Timeout)
246    }
247
248    /// Returns true if this error is recoverable (validation errors).
249    pub fn is_recoverable(&self) -> bool {
250        matches!(self, Self::Validation(_))
251    }
252}
253
254/// A specialized [`Result`] type for huh form operations.
255///
256/// This type alias defaults to [`FormError`] as the error type.
257///
258/// # Example
259///
260/// ```rust,ignore
261/// use huh::Result;
262///
263/// fn collect_user_info() -> Result<UserInfo> {
264///     let mut name = String::new();
265///     let mut email = String::new();
266///
267///     Form::new(vec![/* fields */]).run()?;
268///
269///     Ok(UserInfo { name, email })
270/// }
271/// ```
272pub type Result<T> = std::result::Result<T, FormError>;
273
274// -----------------------------------------------------------------------------
275// Form State
276// -----------------------------------------------------------------------------
277
278/// The current state of the form.
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum FormState {
281    /// User is completing the form.
282    #[default]
283    Normal,
284    /// User has completed the form.
285    Completed,
286    /// User has aborted the form.
287    Aborted,
288}
289
290// -----------------------------------------------------------------------------
291// SelectOption
292// -----------------------------------------------------------------------------
293
294/// An option for select fields.
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct SelectOption<T: Clone + PartialEq> {
297    /// The display key shown to the user.
298    pub key: String,
299    /// The underlying value.
300    pub value: T,
301    /// Whether this option is initially selected.
302    pub selected: bool,
303}
304
305impl<T: Clone + PartialEq> SelectOption<T> {
306    /// Creates a new option.
307    pub fn new(key: impl Into<String>, value: T) -> Self {
308        Self {
309            key: key.into(),
310            value,
311            selected: false,
312        }
313    }
314
315    /// Sets whether the option is initially selected.
316    pub fn selected(mut self, selected: bool) -> Self {
317        self.selected = selected;
318        self
319    }
320}
321
322impl<T: Clone + PartialEq + std::fmt::Display> SelectOption<T> {
323    /// Creates options from a list of values using Display for keys.
324    pub fn from_values(values: impl IntoIterator<Item = T>) -> Vec<Self> {
325        values
326            .into_iter()
327            .map(|v| Self::new(v.to_string(), v))
328            .collect()
329    }
330}
331
332/// Creates options from string values.
333pub fn new_options<S: Into<String> + Clone>(
334    values: impl IntoIterator<Item = S>,
335) -> Vec<SelectOption<String>> {
336    values
337        .into_iter()
338        .map(|v| {
339            let s: String = v.clone().into();
340            SelectOption::new(s.clone(), s)
341        })
342        .collect()
343}
344
345// -----------------------------------------------------------------------------
346// Theme
347// -----------------------------------------------------------------------------
348
349/// Collection of styles for form components.
350#[derive(Debug, Clone)]
351pub struct Theme {
352    /// Styles for the form container.
353    pub form: FormStyles,
354    /// Styles for groups.
355    pub group: GroupStyles,
356    /// Separator between fields.
357    pub field_separator: Style,
358    /// Styles for blurred (unfocused) fields.
359    pub blurred: FieldStyles,
360    /// Styles for focused fields.
361    pub focused: FieldStyles,
362    /// Style for help text at the bottom of the form.
363    pub help: Style,
364}
365
366impl Default for Theme {
367    fn default() -> Self {
368        theme_charm()
369    }
370}
371
372/// Styles for the form container.
373#[derive(Debug, Clone, Default)]
374pub struct FormStyles {
375    /// Base style for the form.
376    pub base: Style,
377}
378
379/// Styles for groups.
380#[derive(Debug, Clone, Default)]
381pub struct GroupStyles {
382    /// Base style for the group.
383    pub base: Style,
384    /// Title style.
385    pub title: Style,
386    /// Description style.
387    pub description: Style,
388}
389
390/// Styles for input fields.
391#[derive(Debug, Clone, Default)]
392pub struct FieldStyles {
393    /// Base style.
394    pub base: Style,
395    /// Title style.
396    pub title: Style,
397    /// Description style.
398    pub description: Style,
399    /// Error indicator style.
400    pub error_indicator: Style,
401    /// Error message style.
402    pub error_message: Style,
403
404    // Select styles
405    /// Select cursor style.
406    pub select_selector: Style,
407    /// Option style.
408    pub option: Style,
409    /// Next indicator for inline select.
410    pub next_indicator: Style,
411    /// Previous indicator for inline select.
412    pub prev_indicator: Style,
413
414    // Multi-select styles
415    /// Multi-select cursor style.
416    pub multi_select_selector: Style,
417    /// Selected option style.
418    pub selected_option: Style,
419    /// Selected prefix style.
420    pub selected_prefix: Style,
421    /// Unselected option style.
422    pub unselected_option: Style,
423    /// Unselected prefix style.
424    pub unselected_prefix: Style,
425
426    // Text input styles
427    /// Text input specific styles.
428    pub text_input: TextInputStyles,
429
430    // Confirm styles
431    /// Focused button style.
432    pub focused_button: Style,
433    /// Blurred button style.
434    pub blurred_button: Style,
435
436    // Note styles
437    /// Note title style.
438    pub note_title: Style,
439}
440
441/// Styles for text inputs.
442#[derive(Debug, Clone, Default)]
443pub struct TextInputStyles {
444    /// Cursor style.
445    pub cursor: Style,
446    /// Cursor text style.
447    pub cursor_text: Style,
448    /// Placeholder style.
449    pub placeholder: Style,
450    /// Prompt style.
451    pub prompt: Style,
452    /// Text style.
453    pub text: Style,
454}
455
456/// Returns the base theme.
457#[allow(clippy::field_reassign_with_default)]
458pub fn theme_base() -> Theme {
459    let button = Style::new().padding((0, 2)).margin_right(1);
460
461    let mut focused = FieldStyles::default();
462    focused.base = Style::new()
463        .padding_left(1)
464        .border(Border::thick())
465        .border_left(true);
466    focused.error_indicator = Style::new().set_string(" *");
467    focused.error_message = Style::new().set_string(" *");
468    focused.select_selector = Style::new().set_string("> ");
469    focused.next_indicator = Style::new().margin_left(1).set_string("→");
470    focused.prev_indicator = Style::new().margin_right(1).set_string("←");
471    focused.multi_select_selector = Style::new().set_string("> ");
472    focused.selected_prefix = Style::new().set_string("[•] ");
473    focused.unselected_prefix = Style::new().set_string("[ ] ");
474    focused.focused_button = button.clone().foreground("0").background("7");
475    focused.blurred_button = button.foreground("7").background("0");
476    focused.text_input.placeholder = Style::new().foreground("8");
477
478    let mut blurred = focused.clone();
479    blurred.base = blurred.base.border(Border::hidden());
480    blurred.multi_select_selector = Style::new().set_string("  ");
481    blurred.next_indicator = Style::new();
482    blurred.prev_indicator = Style::new();
483
484    Theme {
485        form: FormStyles { base: Style::new() },
486        group: GroupStyles::default(),
487        field_separator: Style::new().set_string("\n\n"),
488        focused,
489        blurred,
490        help: Style::new().foreground("241").margin_top(1),
491    }
492}
493
494/// Returns the Charm theme (default).
495pub fn theme_charm() -> Theme {
496    let mut t = theme_base();
497
498    let indigo = "#7571F9";
499    let fuchsia = "#F780E2";
500    let green = "#02BF87";
501    let red = "#ED567A";
502    let normal_fg = "252";
503
504    t.focused.base = t.focused.base.border_foreground("238");
505    t.focused.title = t.focused.title.foreground(indigo).bold();
506    t.focused.note_title = t
507        .focused
508        .note_title
509        .foreground(indigo)
510        .bold()
511        .margin_bottom(1);
512    t.focused.description = t.focused.description.foreground("243");
513    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
514    t.focused.error_message = t.focused.error_message.foreground(red);
515    t.focused.select_selector = t.focused.select_selector.foreground(fuchsia);
516    t.focused.next_indicator = t.focused.next_indicator.foreground(fuchsia);
517    t.focused.prev_indicator = t.focused.prev_indicator.foreground(fuchsia);
518    t.focused.option = t.focused.option.foreground(normal_fg);
519    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(fuchsia);
520    t.focused.selected_option = t.focused.selected_option.foreground(green);
521    t.focused.selected_prefix = Style::new().foreground("#02A877").set_string("✓ ");
522    t.focused.unselected_prefix = Style::new().foreground("243").set_string("• ");
523    t.focused.unselected_option = t.focused.unselected_option.foreground(normal_fg);
524    t.focused.focused_button = t
525        .focused
526        .focused_button
527        .foreground("#FFFDF5")
528        .background(fuchsia);
529    t.focused.blurred_button = t
530        .focused
531        .blurred_button
532        .foreground(normal_fg)
533        .background("237");
534    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(green);
535    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground("238");
536    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(fuchsia);
537
538    t.blurred = t.focused.clone();
539    t.blurred.base = t.focused.base.clone().border(Border::hidden());
540    t.blurred.next_indicator = Style::new();
541    t.blurred.prev_indicator = Style::new();
542
543    t.group.title = t.focused.title.clone();
544    t.group.description = t.focused.description.clone();
545    t.help = Style::new().foreground("241").margin_top(1);
546
547    t
548}
549
550/// Returns the Dracula theme.
551pub fn theme_dracula() -> Theme {
552    let mut t = theme_base();
553
554    let selection = "#44475a";
555    let foreground = "#f8f8f2";
556    let comment = "#6272a4";
557    let green = "#50fa7b";
558    let purple = "#bd93f9";
559    let red = "#ff5555";
560    let yellow = "#f1fa8c";
561
562    t.focused.base = t.focused.base.border_foreground(selection);
563    t.focused.title = t.focused.title.foreground(purple);
564    t.focused.note_title = t.focused.note_title.foreground(purple);
565    t.focused.description = t.focused.description.foreground(comment);
566    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
567    t.focused.error_message = t.focused.error_message.foreground(red);
568    t.focused.select_selector = t.focused.select_selector.foreground(yellow);
569    t.focused.next_indicator = t.focused.next_indicator.foreground(yellow);
570    t.focused.prev_indicator = t.focused.prev_indicator.foreground(yellow);
571    t.focused.option = t.focused.option.foreground(foreground);
572    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(yellow);
573    t.focused.selected_option = t.focused.selected_option.foreground(green);
574    t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
575    t.focused.unselected_option = t.focused.unselected_option.foreground(foreground);
576    t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(comment);
577    t.focused.focused_button = t
578        .focused
579        .focused_button
580        .foreground(yellow)
581        .background(purple)
582        .bold();
583    t.focused.blurred_button = t
584        .focused
585        .blurred_button
586        .foreground(foreground)
587        .background("#282a36");
588    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(yellow);
589    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(comment);
590    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(yellow);
591
592    t.blurred = t.focused.clone();
593    t.blurred.base = t.blurred.base.border(Border::hidden());
594    t.blurred.next_indicator = Style::new();
595    t.blurred.prev_indicator = Style::new();
596
597    t.group.title = t.focused.title.clone();
598    t.group.description = t.focused.description.clone();
599    t.help = Style::new().foreground(comment).margin_top(1);
600
601    t
602}
603
604/// Returns the Base16 theme.
605pub fn theme_base16() -> Theme {
606    let mut t = theme_base();
607
608    t.focused.base = t.focused.base.border_foreground("8");
609    t.focused.title = t.focused.title.foreground("6");
610    t.focused.note_title = t.focused.note_title.foreground("6");
611    t.focused.description = t.focused.description.foreground("8");
612    t.focused.error_indicator = t.focused.error_indicator.foreground("9");
613    t.focused.error_message = t.focused.error_message.foreground("9");
614    t.focused.select_selector = t.focused.select_selector.foreground("3");
615    t.focused.next_indicator = t.focused.next_indicator.foreground("3");
616    t.focused.prev_indicator = t.focused.prev_indicator.foreground("3");
617    t.focused.option = t.focused.option.foreground("7");
618    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground("3");
619    t.focused.selected_option = t.focused.selected_option.foreground("2");
620    t.focused.selected_prefix = t.focused.selected_prefix.foreground("2");
621    t.focused.unselected_option = t.focused.unselected_option.foreground("7");
622    t.focused.focused_button = t.focused.focused_button.foreground("7").background("5");
623    t.focused.blurred_button = t.focused.blurred_button.foreground("7").background("0");
624
625    t.blurred = t.focused.clone();
626    t.blurred.base = t.blurred.base.border(Border::hidden());
627    t.blurred.note_title = t.blurred.note_title.foreground("8");
628    t.blurred.title = t.blurred.title.foreground("8");
629    t.blurred.text_input.prompt = t.blurred.text_input.prompt.foreground("8");
630    t.blurred.text_input.text = t.blurred.text_input.text.foreground("7");
631    t.blurred.next_indicator = Style::new();
632    t.blurred.prev_indicator = Style::new();
633
634    t.group.title = t.focused.title.clone();
635    t.group.description = t.focused.description.clone();
636    t.help = Style::new().foreground("8").margin_top(1);
637
638    t
639}
640
641/// Returns the Catppuccin theme.
642///
643/// This theme is based on the Catppuccin color scheme (Mocha variant).
644/// See <https://github.com/catppuccin/catppuccin> for more details.
645pub fn theme_catppuccin() -> Theme {
646    let mut t = theme_base();
647
648    // Catppuccin Mocha palette
649    let base = "#1e1e2e";
650    let text = "#cdd6f4";
651    let subtext1 = "#bac2de";
652    let subtext0 = "#a6adc8";
653    let _overlay1 = "#7f849c";
654    let overlay0 = "#6c7086";
655    let green = "#a6e3a1";
656    let red = "#f38ba8";
657    let pink = "#f5c2e7";
658    let mauve = "#cba6f7";
659    let rosewater = "#f5e0dc";
660
661    t.focused.base = t.focused.base.border_foreground(subtext1);
662    t.focused.title = t.focused.title.foreground(mauve);
663    t.focused.note_title = t.focused.note_title.foreground(mauve);
664    t.focused.description = t.focused.description.foreground(subtext0);
665    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
666    t.focused.error_message = t.focused.error_message.foreground(red);
667    t.focused.select_selector = t.focused.select_selector.foreground(pink);
668    t.focused.next_indicator = t.focused.next_indicator.foreground(pink);
669    t.focused.prev_indicator = t.focused.prev_indicator.foreground(pink);
670    t.focused.option = t.focused.option.foreground(text);
671    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(pink);
672    t.focused.selected_option = t.focused.selected_option.foreground(green);
673    t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
674    t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(text);
675    t.focused.unselected_option = t.focused.unselected_option.foreground(text);
676    t.focused.focused_button = t.focused.focused_button.foreground(base).background(pink);
677    t.focused.blurred_button = t.focused.blurred_button.foreground(text).background(base);
678
679    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(rosewater);
680    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(overlay0);
681    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(pink);
682
683    t.blurred = t.focused.clone();
684    t.blurred.base = t.blurred.base.border(Border::hidden());
685    t.blurred.next_indicator = Style::new();
686    t.blurred.prev_indicator = Style::new();
687
688    t.group.title = t.focused.title.clone();
689    t.group.description = t.focused.description.clone();
690    t.help = Style::new().foreground(subtext0).margin_top(1);
691
692    t
693}
694
695// -----------------------------------------------------------------------------
696// KeyMap
697// -----------------------------------------------------------------------------
698
699/// Keybindings for form navigation.
700#[derive(Debug, Clone)]
701pub struct KeyMap {
702    /// Quit the form.
703    pub quit: Binding,
704    /// Input field keybindings.
705    pub input: InputKeyMap,
706    /// Select field keybindings.
707    pub select: SelectKeyMap,
708    /// Multi-select field keybindings.
709    pub multi_select: MultiSelectKeyMap,
710    /// Confirm field keybindings.
711    pub confirm: ConfirmKeyMap,
712    /// Note field keybindings.
713    pub note: NoteKeyMap,
714    /// Text area keybindings.
715    pub text: TextKeyMap,
716    /// File picker keybindings.
717    pub file_picker: FilePickerKeyMap,
718}
719
720impl Default for KeyMap {
721    fn default() -> Self {
722        Self::new()
723    }
724}
725
726impl KeyMap {
727    /// Creates a new default keymap.
728    pub fn new() -> Self {
729        Self {
730            quit: Binding::new().keys(&["ctrl+c"]),
731            input: InputKeyMap::default(),
732            select: SelectKeyMap::default(),
733            multi_select: MultiSelectKeyMap::default(),
734            confirm: ConfirmKeyMap::default(),
735            note: NoteKeyMap::default(),
736            text: TextKeyMap::default(),
737            file_picker: FilePickerKeyMap::default(),
738        }
739    }
740}
741
742/// Keybindings for input fields.
743#[derive(Debug, Clone)]
744pub struct InputKeyMap {
745    /// Accept autocomplete suggestion.
746    pub accept_suggestion: Binding,
747    /// Go to next field.
748    pub next: Binding,
749    /// Go to previous field.
750    pub prev: Binding,
751    /// Submit the form.
752    pub submit: Binding,
753}
754
755impl Default for InputKeyMap {
756    fn default() -> Self {
757        Self {
758            accept_suggestion: Binding::new().keys(&["ctrl+e"]).help("ctrl+e", "complete"),
759            prev: Binding::new()
760                .keys(&["shift+tab"])
761                .help("shift+tab", "back"),
762            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
763            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
764        }
765    }
766}
767
768/// Keybindings for select fields.
769#[derive(Debug, Clone)]
770pub struct SelectKeyMap {
771    /// Go to next field.
772    pub next: Binding,
773    /// Go to previous field.
774    pub prev: Binding,
775    /// Move cursor up.
776    pub up: Binding,
777    /// Move cursor down.
778    pub down: Binding,
779    /// Move cursor left (inline mode).
780    pub left: Binding,
781    /// Move cursor right (inline mode).
782    pub right: Binding,
783    /// Open filter.
784    pub filter: Binding,
785    /// Apply filter.
786    pub set_filter: Binding,
787    /// Clear filter.
788    pub clear_filter: Binding,
789    /// Half page up.
790    pub half_page_up: Binding,
791    /// Half page down.
792    pub half_page_down: Binding,
793    /// Go to top.
794    pub goto_top: Binding,
795    /// Go to bottom.
796    pub goto_bottom: Binding,
797    /// Submit the form.
798    pub submit: Binding,
799}
800
801impl Default for SelectKeyMap {
802    fn default() -> Self {
803        Self {
804            prev: Binding::new()
805                .keys(&["shift+tab"])
806                .help("shift+tab", "back"),
807            next: Binding::new()
808                .keys(&["enter", "tab"])
809                .help("enter", "select"),
810            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
811            up: Binding::new()
812                .keys(&["up", "k", "ctrl+k", "ctrl+p"])
813                .help("↑", "up"),
814            down: Binding::new()
815                .keys(&["down", "j", "ctrl+j", "ctrl+n"])
816                .help("↓", "down"),
817            left: Binding::new()
818                .keys(&["h", "left"])
819                .help("←", "left")
820                .set_enabled(false),
821            right: Binding::new()
822                .keys(&["l", "right"])
823                .help("→", "right")
824                .set_enabled(false),
825            filter: Binding::new().keys(&["/"]).help("/", "filter"),
826            set_filter: Binding::new()
827                .keys(&["escape"])
828                .help("esc", "set filter")
829                .set_enabled(false),
830            clear_filter: Binding::new()
831                .keys(&["escape"])
832                .help("esc", "clear filter")
833                .set_enabled(false),
834            half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
835            half_page_down: Binding::new()
836                .keys(&["ctrl+d"])
837                .help("ctrl+d", "½ page down"),
838            goto_top: Binding::new()
839                .keys(&["home", "g"])
840                .help("g/home", "go to start"),
841            goto_bottom: Binding::new()
842                .keys(&["end", "G"])
843                .help("G/end", "go to end"),
844        }
845    }
846}
847
848/// Keybindings for multi-select fields.
849#[derive(Debug, Clone)]
850pub struct MultiSelectKeyMap {
851    /// Go to next field.
852    pub next: Binding,
853    /// Go to previous field.
854    pub prev: Binding,
855    /// Move cursor up.
856    pub up: Binding,
857    /// Move cursor down.
858    pub down: Binding,
859    /// Toggle selection.
860    pub toggle: Binding,
861    /// Open filter.
862    pub filter: Binding,
863    /// Apply filter.
864    pub set_filter: Binding,
865    /// Clear filter.
866    pub clear_filter: Binding,
867    /// Half page up.
868    pub half_page_up: Binding,
869    /// Half page down.
870    pub half_page_down: Binding,
871    /// Go to top.
872    pub goto_top: Binding,
873    /// Go to bottom.
874    pub goto_bottom: Binding,
875    /// Select all.
876    pub select_all: Binding,
877    /// Select none.
878    pub select_none: Binding,
879    /// Submit the form.
880    pub submit: Binding,
881}
882
883impl Default for MultiSelectKeyMap {
884    fn default() -> Self {
885        Self {
886            prev: Binding::new()
887                .keys(&["shift+tab"])
888                .help("shift+tab", "back"),
889            next: Binding::new()
890                .keys(&["enter", "tab"])
891                .help("enter", "confirm"),
892            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
893            toggle: Binding::new().keys(&[" ", "x"]).help("x", "toggle"),
894            up: Binding::new().keys(&["up", "k", "ctrl+p"]).help("↑", "up"),
895            down: Binding::new()
896                .keys(&["down", "j", "ctrl+n"])
897                .help("↓", "down"),
898            filter: Binding::new().keys(&["/"]).help("/", "filter"),
899            set_filter: Binding::new()
900                .keys(&["enter", "escape"])
901                .help("esc", "set filter")
902                .set_enabled(false),
903            clear_filter: Binding::new()
904                .keys(&["escape"])
905                .help("esc", "clear filter")
906                .set_enabled(false),
907            half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
908            half_page_down: Binding::new()
909                .keys(&["ctrl+d"])
910                .help("ctrl+d", "½ page down"),
911            goto_top: Binding::new()
912                .keys(&["home", "g"])
913                .help("g/home", "go to start"),
914            goto_bottom: Binding::new()
915                .keys(&["end", "G"])
916                .help("G/end", "go to end"),
917            select_all: Binding::new()
918                .keys(&["ctrl+a"])
919                .help("ctrl+a", "select all"),
920            select_none: Binding::new()
921                .keys(&["ctrl+a"])
922                .help("ctrl+a", "select none")
923                .set_enabled(false),
924        }
925    }
926}
927
928/// Keybindings for confirm fields.
929#[derive(Debug, Clone)]
930pub struct ConfirmKeyMap {
931    /// Go to next field.
932    pub next: Binding,
933    /// Go to previous field.
934    pub prev: Binding,
935    /// Toggle between yes/no.
936    pub toggle: Binding,
937    /// Submit the form.
938    pub submit: Binding,
939    /// Accept (yes).
940    pub accept: Binding,
941    /// Reject (no).
942    pub reject: Binding,
943}
944
945impl Default for ConfirmKeyMap {
946    fn default() -> Self {
947        Self {
948            prev: Binding::new()
949                .keys(&["shift+tab"])
950                .help("shift+tab", "back"),
951            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
952            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
953            toggle: Binding::new()
954                .keys(&["h", "l", "right", "left"])
955                .help("←/→", "toggle"),
956            accept: Binding::new().keys(&["y", "Y"]).help("y", "Yes"),
957            reject: Binding::new().keys(&["n", "N"]).help("n", "No"),
958        }
959    }
960}
961
962/// Keybindings for note fields.
963#[derive(Debug, Clone)]
964pub struct NoteKeyMap {
965    /// Go to next field.
966    pub next: Binding,
967    /// Go to previous field.
968    pub prev: Binding,
969    /// Submit the form.
970    pub submit: Binding,
971}
972
973impl Default for NoteKeyMap {
974    fn default() -> Self {
975        Self {
976            prev: Binding::new()
977                .keys(&["shift+tab"])
978                .help("shift+tab", "back"),
979            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
980            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
981        }
982    }
983}
984
985/// Keybindings for text area fields.
986#[derive(Debug, Clone)]
987pub struct TextKeyMap {
988    /// Go to next field.
989    pub next: Binding,
990    /// Go to previous field.
991    pub prev: Binding,
992    /// Insert a new line.
993    pub new_line: Binding,
994    /// Open external editor.
995    pub editor: Binding,
996    /// Submit the form.
997    pub submit: Binding,
998    /// Uppercase word forward.
999    pub uppercase_word_forward: Binding,
1000    /// Lowercase word forward.
1001    pub lowercase_word_forward: Binding,
1002    /// Capitalize word forward.
1003    pub capitalize_word_forward: Binding,
1004    /// Transpose character backward.
1005    pub transpose_character_backward: Binding,
1006}
1007
1008impl Default for TextKeyMap {
1009    fn default() -> Self {
1010        Self {
1011            prev: Binding::new()
1012                .keys(&["shift+tab"])
1013                .help("shift+tab", "back"),
1014            next: Binding::new().keys(&["tab", "enter"]).help("enter", "next"),
1015            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1016            new_line: Binding::new()
1017                .keys(&["alt+enter", "ctrl+j"])
1018                .help("alt+enter / ctrl+j", "new line"),
1019            editor: Binding::new()
1020                .keys(&["ctrl+e"])
1021                .help("ctrl+e", "open editor"),
1022            uppercase_word_forward: Binding::new()
1023                .keys(&["alt+u"])
1024                .help("alt+u", "uppercase word"),
1025            lowercase_word_forward: Binding::new()
1026                .keys(&["alt+l"])
1027                .help("alt+l", "lowercase word"),
1028            capitalize_word_forward: Binding::new()
1029                .keys(&["alt+c"])
1030                .help("alt+c", "capitalize word"),
1031            transpose_character_backward: Binding::new()
1032                .keys(&["ctrl+t"])
1033                .help("ctrl+t", "transpose"),
1034        }
1035    }
1036}
1037
1038/// Keybindings for file picker fields.
1039#[derive(Debug, Clone)]
1040pub struct FilePickerKeyMap {
1041    /// Go to next field.
1042    pub next: Binding,
1043    /// Go to previous field.
1044    pub prev: Binding,
1045    /// Submit the form.
1046    pub submit: Binding,
1047    /// Move up in file list.
1048    pub up: Binding,
1049    /// Move down in file list.
1050    pub down: Binding,
1051    /// Open directory or select file.
1052    pub open: Binding,
1053    /// Close picker / go back.
1054    pub close: Binding,
1055    /// Go back to parent directory.
1056    pub back: Binding,
1057    /// Select current item.
1058    pub select: Binding,
1059    /// Go to top of list.
1060    pub goto_top: Binding,
1061    /// Go to bottom of list.
1062    pub goto_bottom: Binding,
1063    /// Page up.
1064    pub page_up: Binding,
1065    /// Page down.
1066    pub page_down: Binding,
1067}
1068
1069impl Default for FilePickerKeyMap {
1070    fn default() -> Self {
1071        Self {
1072            prev: Binding::new()
1073                .keys(&["shift+tab"])
1074                .help("shift+tab", "back"),
1075            next: Binding::new().keys(&["tab"]).help("tab", "next"),
1076            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1077            up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
1078            down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
1079            open: Binding::new().keys(&["enter", "l"]).help("enter", "open"),
1080            close: Binding::new().keys(&["esc", "q"]).help("esc", "close"),
1081            back: Binding::new().keys(&["backspace", "h"]).help("h", "back"),
1082            select: Binding::new().keys(&["enter"]).help("enter", "select"),
1083            goto_top: Binding::new().keys(&["g"]).help("g", "first"),
1084            goto_bottom: Binding::new().keys(&["G"]).help("G", "last"),
1085            page_up: Binding::new().keys(&["pgup", "K"]).help("pgup", "page up"),
1086            page_down: Binding::new()
1087                .keys(&["pgdown", "J"])
1088                .help("pgdown", "page down"),
1089        }
1090    }
1091}
1092
1093// -----------------------------------------------------------------------------
1094// Field Position
1095// -----------------------------------------------------------------------------
1096
1097/// Positional information about a field within a form.
1098#[derive(Debug, Clone, Copy, Default)]
1099pub struct FieldPosition {
1100    /// Current group index.
1101    pub group: usize,
1102    /// Current field index within group.
1103    pub field: usize,
1104    /// First non-skipped field index.
1105    pub first_field: usize,
1106    /// Last non-skipped field index.
1107    pub last_field: usize,
1108    /// Total number of groups.
1109    pub group_count: usize,
1110    /// First non-hidden group index.
1111    pub first_group: usize,
1112    /// Last non-hidden group index.
1113    pub last_group: usize,
1114}
1115
1116impl FieldPosition {
1117    /// Returns whether this field is the first in the form.
1118    pub fn is_first(&self) -> bool {
1119        self.field == self.first_field && self.group == self.first_group
1120    }
1121
1122    /// Returns whether this field is the last in the form.
1123    pub fn is_last(&self) -> bool {
1124        self.field == self.last_field && self.group == self.last_group
1125    }
1126}
1127
1128// -----------------------------------------------------------------------------
1129// Helper for key matching
1130// -----------------------------------------------------------------------------
1131
1132/// Check if a KeyMsg matches a Binding.
1133fn binding_matches(binding: &Binding, key: &KeyMsg) -> bool {
1134    if !binding.enabled() {
1135        return false;
1136    }
1137    let key_str = key.to_string();
1138    binding.get_keys().iter().any(|k| k == &key_str)
1139}
1140
1141// -----------------------------------------------------------------------------
1142// Field Trait
1143// -----------------------------------------------------------------------------
1144
1145/// A form field.
1146pub trait Field: Send + Sync {
1147    /// Returns the field's key.
1148    fn get_key(&self) -> &str;
1149
1150    /// Returns the field's value.
1151    fn get_value(&self) -> Box<dyn Any>;
1152
1153    /// Returns whether this field should be skipped.
1154    fn skip(&self) -> bool {
1155        false
1156    }
1157
1158    /// Returns whether this field should zoom (take full height).
1159    fn zoom(&self) -> bool {
1160        false
1161    }
1162
1163    /// Returns the current validation error, if any.
1164    fn error(&self) -> Option<&str>;
1165
1166    /// Initializes the field.
1167    fn init(&mut self) -> Option<Cmd>;
1168
1169    /// Updates the field with a message.
1170    fn update(&mut self, msg: &Message) -> Option<Cmd>;
1171
1172    /// Renders the field.
1173    fn view(&self) -> String;
1174
1175    /// Focuses the field.
1176    fn focus(&mut self) -> Option<Cmd>;
1177
1178    /// Blurs the field.
1179    fn blur(&mut self) -> Option<Cmd>;
1180
1181    /// Returns the help keybindings.
1182    fn key_binds(&self) -> Vec<Binding>;
1183
1184    /// Sets the theme.
1185    fn with_theme(&mut self, theme: &Theme);
1186
1187    /// Sets the keymap.
1188    fn with_keymap(&mut self, keymap: &KeyMap);
1189
1190    /// Sets the width.
1191    fn with_width(&mut self, width: usize);
1192
1193    /// Sets the height.
1194    fn with_height(&mut self, height: usize);
1195
1196    /// Sets the field position.
1197    fn with_position(&mut self, position: FieldPosition);
1198}
1199
1200// -----------------------------------------------------------------------------
1201// Messages
1202// -----------------------------------------------------------------------------
1203
1204/// Message to move to the next field.
1205#[derive(Debug, Clone)]
1206pub struct NextFieldMsg;
1207
1208/// Message to move to the previous field.
1209#[derive(Debug, Clone)]
1210pub struct PrevFieldMsg;
1211
1212/// Message to move to the next group.
1213#[derive(Debug, Clone)]
1214pub struct NextGroupMsg;
1215
1216/// Message to move to the previous group.
1217#[derive(Debug, Clone)]
1218pub struct PrevGroupMsg;
1219
1220/// Message to update dynamic field content.
1221#[derive(Debug, Clone)]
1222pub struct UpdateFieldMsg;
1223
1224// -----------------------------------------------------------------------------
1225// Input Field
1226// -----------------------------------------------------------------------------
1227
1228/// A text input field.
1229pub struct Input {
1230    id: usize,
1231    key: String,
1232    value: String,
1233    title: String,
1234    description: String,
1235    placeholder: String,
1236    prompt: String,
1237    char_limit: usize,
1238    echo_mode: EchoMode,
1239    inline: bool,
1240    focused: bool,
1241    error: Option<String>,
1242    validate: Option<fn(&str) -> Option<String>>,
1243    width: usize,
1244    _height: usize,
1245    theme: Option<Theme>,
1246    keymap: InputKeyMap,
1247    _position: FieldPosition,
1248    cursor_pos: usize,
1249    suggestions: Vec<String>,
1250    show_suggestions: bool,
1251}
1252
1253/// Echo mode for input fields.
1254#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1255pub enum EchoMode {
1256    /// Display text as-is.
1257    #[default]
1258    Normal,
1259    /// Display mask characters (for passwords).
1260    Password,
1261    /// Display nothing.
1262    None,
1263}
1264
1265impl Default for Input {
1266    fn default() -> Self {
1267        Self::new()
1268    }
1269}
1270
1271impl Input {
1272    /// Creates a new input field.
1273    pub fn new() -> Self {
1274        Self {
1275            id: next_id(),
1276            key: String::new(),
1277            value: String::new(),
1278            title: String::new(),
1279            description: String::new(),
1280            placeholder: String::new(),
1281            prompt: "> ".to_string(),
1282            char_limit: 0,
1283            echo_mode: EchoMode::Normal,
1284            inline: false,
1285            focused: false,
1286            error: None,
1287            validate: None,
1288            width: 80,
1289            _height: 0,
1290            theme: None,
1291            keymap: InputKeyMap::default(),
1292            _position: FieldPosition::default(),
1293            cursor_pos: 0,
1294            suggestions: Vec::new(),
1295            show_suggestions: false,
1296        }
1297    }
1298
1299    /// Sets the field key.
1300    pub fn key(mut self, key: impl Into<String>) -> Self {
1301        self.key = key.into();
1302        self
1303    }
1304
1305    /// Sets the initial value.
1306    pub fn value(mut self, value: impl Into<String>) -> Self {
1307        self.value = value.into();
1308        self.cursor_pos = self.value.chars().count();
1309        self
1310    }
1311
1312    /// Sets the title.
1313    pub fn title(mut self, title: impl Into<String>) -> Self {
1314        self.title = title.into();
1315        self
1316    }
1317
1318    /// Sets the description.
1319    pub fn description(mut self, description: impl Into<String>) -> Self {
1320        self.description = description.into();
1321        self
1322    }
1323
1324    /// Sets the placeholder text.
1325    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1326        self.placeholder = placeholder.into();
1327        self
1328    }
1329
1330    /// Sets the prompt string.
1331    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
1332        self.prompt = prompt.into();
1333        self
1334    }
1335
1336    /// Sets the character limit.
1337    pub fn char_limit(mut self, limit: usize) -> Self {
1338        self.char_limit = limit;
1339        self
1340    }
1341
1342    /// Sets the echo mode.
1343    pub fn echo_mode(mut self, mode: EchoMode) -> Self {
1344        self.echo_mode = mode;
1345        self
1346    }
1347
1348    /// Sets password mode (shorthand for echo_mode).
1349    pub fn password(self, password: bool) -> Self {
1350        if password {
1351            self.echo_mode(EchoMode::Password)
1352        } else {
1353            self.echo_mode(EchoMode::Normal)
1354        }
1355    }
1356
1357    /// Sets whether the title and input are on the same line.
1358    pub fn inline(mut self, inline: bool) -> Self {
1359        self.inline = inline;
1360        self
1361    }
1362
1363    /// Sets the validation function.
1364    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
1365        self.validate = Some(validate);
1366        self
1367    }
1368
1369    /// Sets the suggestions for autocomplete.
1370    pub fn suggestions(mut self, suggestions: Vec<String>) -> Self {
1371        self.suggestions = suggestions;
1372        self.show_suggestions = !self.suggestions.is_empty();
1373        self
1374    }
1375
1376    fn get_theme(&self) -> Theme {
1377        self.theme.clone().unwrap_or_else(theme_charm)
1378    }
1379
1380    fn active_styles(&self) -> FieldStyles {
1381        let theme = self.get_theme();
1382        if self.focused {
1383            theme.focused
1384        } else {
1385            theme.blurred
1386        }
1387    }
1388
1389    fn run_validation(&mut self) {
1390        if let Some(validate) = self.validate {
1391            self.error = validate(&self.value);
1392        }
1393    }
1394
1395    fn display_value(&self) -> String {
1396        match self.echo_mode {
1397            EchoMode::Normal => self.value.clone(),
1398            EchoMode::Password => "•".repeat(self.value.chars().count()),
1399            EchoMode::None => String::new(),
1400        }
1401    }
1402
1403    /// Gets the current value.
1404    pub fn get_string_value(&self) -> &str {
1405        &self.value
1406    }
1407
1408    /// Returns the field ID.
1409    pub fn id(&self) -> usize {
1410        self.id
1411    }
1412}
1413
1414impl Field for Input {
1415    fn get_key(&self) -> &str {
1416        &self.key
1417    }
1418
1419    fn get_value(&self) -> Box<dyn Any> {
1420        Box::new(self.value.clone())
1421    }
1422
1423    fn error(&self) -> Option<&str> {
1424        self.error.as_deref()
1425    }
1426
1427    fn init(&mut self) -> Option<Cmd> {
1428        None
1429    }
1430
1431    fn update(&mut self, msg: &Message) -> Option<Cmd> {
1432        if !self.focused {
1433            return None;
1434        }
1435
1436        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1437            self.error = None;
1438
1439            // Check for prev
1440            if binding_matches(&self.keymap.prev, key_msg) {
1441                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1442            }
1443
1444            // Check for next/submit
1445            if binding_matches(&self.keymap.next, key_msg)
1446                || binding_matches(&self.keymap.submit, key_msg)
1447            {
1448                self.run_validation();
1449                if self.error.is_some() {
1450                    return None;
1451                }
1452                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1453            }
1454
1455            // Handle character input
1456            // Note: cursor_pos is a character index (not byte index) for proper Unicode support
1457            match key_msg.key_type {
1458                KeyType::Runes => {
1459                    // Preprocess paste content: for single-line inputs, collapse newlines/tabs to spaces
1460                    let chars_to_insert: Vec<char> = if key_msg.paste {
1461                        key_msg
1462                            .runes
1463                            .iter()
1464                            .map(|&c| {
1465                                if c == '\n' || c == '\r' || c == '\t' {
1466                                    ' '
1467                                } else {
1468                                    c
1469                                }
1470                            })
1471                            // Collapse multiple consecutive spaces into one
1472                            .fold(Vec::new(), |mut acc, c| {
1473                                if c == ' ' && acc.last() == Some(&' ') {
1474                                    // Skip duplicate space
1475                                } else {
1476                                    acc.push(c);
1477                                }
1478                                acc
1479                            })
1480                    } else {
1481                        key_msg.runes.clone()
1482                    };
1483
1484                    // Calculate how many chars we can insert respecting char_limit
1485                    let current_count = self.value.chars().count();
1486                    let available = if self.char_limit == 0 {
1487                        usize::MAX
1488                    } else {
1489                        self.char_limit.saturating_sub(current_count)
1490                    };
1491                    let chars_to_add: Vec<char> =
1492                        chars_to_insert.into_iter().take(available).collect();
1493
1494                    if !chars_to_add.is_empty() {
1495                        // Convert character position to byte position for insertion
1496                        let byte_pos = self
1497                            .value
1498                            .char_indices()
1499                            .nth(self.cursor_pos)
1500                            .map(|(i, _)| i)
1501                            .unwrap_or(self.value.len());
1502
1503                        // Build the new string efficiently for bulk insert
1504                        let insert_str: String = chars_to_add.iter().collect();
1505                        self.value.insert_str(byte_pos, &insert_str);
1506                        self.cursor_pos += chars_to_add.len();
1507                    }
1508                }
1509                KeyType::Backspace => {
1510                    if self.cursor_pos > 0 {
1511                        self.cursor_pos -= 1;
1512                        // Convert character position to byte position for removal
1513                        if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1514                        {
1515                            self.value.remove(byte_pos);
1516                        }
1517                    }
1518                }
1519                KeyType::Delete => {
1520                    let char_count = self.value.chars().count();
1521                    if self.cursor_pos < char_count {
1522                        // Convert character position to byte position for removal
1523                        if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1524                        {
1525                            self.value.remove(byte_pos);
1526                        }
1527                    }
1528                }
1529                KeyType::Left => {
1530                    if self.cursor_pos > 0 {
1531                        self.cursor_pos -= 1;
1532                    }
1533                }
1534                KeyType::Right => {
1535                    let char_count = self.value.chars().count();
1536                    if self.cursor_pos < char_count {
1537                        self.cursor_pos += 1;
1538                    }
1539                }
1540                KeyType::Home => {
1541                    self.cursor_pos = 0;
1542                }
1543                KeyType::End => {
1544                    self.cursor_pos = self.value.chars().count();
1545                }
1546                _ => {}
1547            }
1548        }
1549
1550        None
1551    }
1552
1553    fn view(&self) -> String {
1554        let styles = self.active_styles();
1555        let mut output = String::new();
1556
1557        // Title
1558        if !self.title.is_empty() {
1559            output.push_str(&styles.title.render(&self.title));
1560            if !self.inline {
1561                output.push('\n');
1562            }
1563        }
1564
1565        // Description
1566        if !self.description.is_empty() {
1567            output.push_str(&styles.description.render(&self.description));
1568            if !self.inline {
1569                output.push('\n');
1570            }
1571        }
1572
1573        // Prompt and value
1574        output.push_str(&styles.text_input.prompt.render(&self.prompt));
1575
1576        let display = self.display_value();
1577        if display.is_empty() && !self.placeholder.is_empty() {
1578            output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
1579        } else {
1580            output.push_str(&styles.text_input.text.render(&display));
1581        }
1582
1583        // Error indicator
1584        if self.error.is_some() {
1585            output.push_str(&styles.error_indicator.render(""));
1586        }
1587
1588        styles
1589            .base
1590            .width(self.width.try_into().unwrap_or(u16::MAX))
1591            .render(&output)
1592    }
1593
1594    fn focus(&mut self) -> Option<Cmd> {
1595        self.focused = true;
1596        None
1597    }
1598
1599    fn blur(&mut self) -> Option<Cmd> {
1600        self.focused = false;
1601        self.run_validation();
1602        None
1603    }
1604
1605    fn key_binds(&self) -> Vec<Binding> {
1606        if self.show_suggestions {
1607            vec![
1608                self.keymap.accept_suggestion.clone(),
1609                self.keymap.prev.clone(),
1610                self.keymap.submit.clone(),
1611                self.keymap.next.clone(),
1612            ]
1613        } else {
1614            vec![
1615                self.keymap.prev.clone(),
1616                self.keymap.submit.clone(),
1617                self.keymap.next.clone(),
1618            ]
1619        }
1620    }
1621
1622    fn with_theme(&mut self, theme: &Theme) {
1623        if self.theme.is_none() {
1624            self.theme = Some(theme.clone());
1625        }
1626    }
1627
1628    fn with_keymap(&mut self, keymap: &KeyMap) {
1629        self.keymap = keymap.input.clone();
1630    }
1631
1632    fn with_width(&mut self, width: usize) {
1633        self.width = width;
1634    }
1635
1636    fn with_height(&mut self, height: usize) {
1637        self._height = height;
1638    }
1639
1640    fn with_position(&mut self, position: FieldPosition) {
1641        self._position = position;
1642    }
1643}
1644
1645// -----------------------------------------------------------------------------
1646// Select Field
1647// -----------------------------------------------------------------------------
1648
1649/// A select field for choosing one option from a list.
1650pub struct Select<T: Clone + PartialEq + Send + Sync + 'static> {
1651    id: usize,
1652    key: String,
1653    options: Vec<SelectOption<T>>,
1654    selected: usize,
1655    title: String,
1656    description: String,
1657    inline: bool,
1658    focused: bool,
1659    error: Option<String>,
1660    validate: Option<fn(&T) -> Option<String>>,
1661    width: usize,
1662    height: usize,
1663    theme: Option<Theme>,
1664    keymap: SelectKeyMap,
1665    _position: FieldPosition,
1666    filtering: bool,
1667    filter_value: String,
1668    offset: usize,
1669}
1670
1671impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for Select<T> {
1672    fn default() -> Self {
1673        Self::new()
1674    }
1675}
1676
1677impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Select<T> {
1678    /// Creates a new select field.
1679    pub fn new() -> Self {
1680        Self {
1681            id: next_id(),
1682            key: String::new(),
1683            options: Vec::new(),
1684            selected: 0,
1685            title: String::new(),
1686            description: String::new(),
1687            inline: false,
1688            focused: false,
1689            error: None,
1690            validate: None,
1691            width: 80,
1692            height: 5,
1693            theme: None,
1694            keymap: SelectKeyMap::default(),
1695            _position: FieldPosition::default(),
1696            filtering: false,
1697            filter_value: String::new(),
1698            offset: 0,
1699        }
1700    }
1701
1702    /// Sets the field key.
1703    pub fn key(mut self, key: impl Into<String>) -> Self {
1704        self.key = key.into();
1705        self
1706    }
1707
1708    /// Sets the options.
1709    pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
1710        self.options = options;
1711        // Find initially selected
1712        for (i, opt) in self.options.iter().enumerate() {
1713            if opt.selected {
1714                self.selected = i;
1715                break;
1716            }
1717        }
1718        self
1719    }
1720
1721    /// Sets the title.
1722    pub fn title(mut self, title: impl Into<String>) -> Self {
1723        self.title = title.into();
1724        self
1725    }
1726
1727    /// Sets the description.
1728    pub fn description(mut self, description: impl Into<String>) -> Self {
1729        self.description = description.into();
1730        self
1731    }
1732
1733    /// Sets whether options display inline.
1734    pub fn inline(mut self, inline: bool) -> Self {
1735        self.inline = inline;
1736        self
1737    }
1738
1739    /// Sets the validation function.
1740    pub fn validate(mut self, validate: fn(&T) -> Option<String>) -> Self {
1741        self.validate = Some(validate);
1742        self
1743    }
1744
1745    /// Sets the visible height (number of options shown).
1746    pub fn height_options(mut self, height: usize) -> Self {
1747        self.height = height;
1748        self
1749    }
1750
1751    /// Enables or disables type-to-filter support.
1752    ///
1753    /// When filtering is enabled, typing characters will filter the visible
1754    /// options. Navigation keys (j/k/g/G) still work for movement.
1755    /// Press Escape to clear the filter, Backspace to delete the last character.
1756    pub fn filterable(mut self, enabled: bool) -> Self {
1757        self.filtering = enabled;
1758        self
1759    }
1760
1761    /// Updates the filter value and adjusts the selection to stay on the same
1762    /// item when possible, or clamps to valid bounds if the current item is
1763    /// filtered out.
1764    fn update_filter(&mut self, new_value: String) {
1765        // Remember what item `selected` is currently pointing to (original index)
1766        let current_item_idx = self.selected;
1767
1768        // Update the filter
1769        self.filter_value = new_value;
1770
1771        // Collect filtered indices into owned vec to avoid borrow conflicts
1772        let filtered_indices: Vec<usize> = self.filtered_indices();
1773
1774        // Try to keep selection on the same original item
1775        if filtered_indices.contains(&current_item_idx) {
1776            // Item still visible — keep selection
1777            self.adjust_offset_from_indices(&filtered_indices);
1778            return;
1779        }
1780
1781        // Item no longer visible — select the first filtered item (or keep 0)
1782        if let Some(&first_idx) = filtered_indices.first() {
1783            self.selected = first_idx;
1784        }
1785        self.adjust_offset_from_indices(&filtered_indices);
1786    }
1787
1788    /// Returns just the original indices of filtered options (owned data,
1789    /// no borrows on self).
1790    fn filtered_indices(&self) -> Vec<usize> {
1791        if self.filter_value.is_empty() {
1792            (0..self.options.len()).collect()
1793        } else {
1794            let filter_lower = self.filter_value.to_lowercase();
1795            self.options
1796                .iter()
1797                .enumerate()
1798                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1799                .map(|(i, _)| i)
1800                .collect()
1801        }
1802    }
1803
1804    /// Adjusts the scroll offset to keep the current selection visible
1805    /// within the filtered view.
1806    fn adjust_offset_from_indices(&mut self, filtered_indices: &[usize]) {
1807        let pos = filtered_indices
1808            .iter()
1809            .position(|&idx| idx == self.selected)
1810            .unwrap_or(0);
1811        if pos < self.offset {
1812            self.offset = pos;
1813        } else if pos >= self.offset + self.height {
1814            self.offset = pos.saturating_sub(self.height.saturating_sub(1));
1815        }
1816    }
1817
1818    fn get_theme(&self) -> Theme {
1819        self.theme.clone().unwrap_or_else(theme_charm)
1820    }
1821
1822    fn active_styles(&self) -> FieldStyles {
1823        let theme = self.get_theme();
1824        if self.focused {
1825            theme.focused
1826        } else {
1827            theme.blurred
1828        }
1829    }
1830
1831    fn run_validation(&mut self) {
1832        if let Some(validate) = self.validate
1833            && let Some(opt) = self.options.get(self.selected)
1834        {
1835            self.error = validate(&opt.value);
1836        }
1837    }
1838
1839    fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
1840        if self.filter_value.is_empty() {
1841            self.options.iter().enumerate().collect()
1842        } else {
1843            let filter_lower = self.filter_value.to_lowercase();
1844            self.options
1845                .iter()
1846                .enumerate()
1847                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1848                .collect()
1849        }
1850    }
1851
1852    /// Gets the currently selected value.
1853    pub fn get_selected_value(&self) -> Option<&T> {
1854        self.options.get(self.selected).map(|o| &o.value)
1855    }
1856
1857    /// Returns the field ID.
1858    pub fn id(&self) -> usize {
1859        self.id
1860    }
1861}
1862
1863impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for Select<T> {
1864    fn get_key(&self) -> &str {
1865        &self.key
1866    }
1867
1868    fn get_value(&self) -> Box<dyn Any> {
1869        if let Some(opt) = self.options.get(self.selected) {
1870            Box::new(opt.value.clone())
1871        } else {
1872            Box::new(T::default())
1873        }
1874    }
1875
1876    fn error(&self) -> Option<&str> {
1877        self.error.as_deref()
1878    }
1879
1880    fn init(&mut self) -> Option<Cmd> {
1881        None
1882    }
1883
1884    fn update(&mut self, msg: &Message) -> Option<Cmd> {
1885        if !self.focused {
1886            return None;
1887        }
1888
1889        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1890            self.error = None;
1891
1892            // Handle filter input when filtering is enabled
1893            if self.filtering {
1894                // Clear filter on Escape
1895                if key_msg.key_type == KeyType::Esc {
1896                    self.update_filter(String::new());
1897                    return None;
1898                }
1899
1900                // Remove character on Backspace
1901                if key_msg.key_type == KeyType::Backspace {
1902                    if !self.filter_value.is_empty() {
1903                        let mut new_filter = self.filter_value.clone();
1904                        new_filter.pop();
1905                        self.update_filter(new_filter);
1906                    }
1907                    return None;
1908                }
1909
1910                // Add characters to filter (skip navigation keys)
1911                if key_msg.key_type == KeyType::Runes {
1912                    let mut new_filter = self.filter_value.clone();
1913                    for c in &key_msg.runes {
1914                        // Skip navigation/action keys so they still work
1915                        match c {
1916                            'j' | 'k' | 'g' | 'G' | '/' => continue,
1917                            _ => {}
1918                        }
1919                        if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
1920                            new_filter.push(*c);
1921                        }
1922                    }
1923                    if new_filter != self.filter_value {
1924                        self.update_filter(new_filter);
1925                        return None;
1926                    }
1927                }
1928            }
1929
1930            // Check for prev
1931            if binding_matches(&self.keymap.prev, key_msg) {
1932                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1933            }
1934
1935            // Check for next/submit
1936            if binding_matches(&self.keymap.next, key_msg)
1937                || binding_matches(&self.keymap.submit, key_msg)
1938            {
1939                self.run_validation();
1940                if self.error.is_some() {
1941                    return None;
1942                }
1943                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1944            }
1945
1946            // Navigation operates on the filtered list.
1947            // Collect indices into owned vec to avoid borrow conflicts.
1948            let filtered_indices = self.filtered_indices();
1949            let current_pos = filtered_indices
1950                .iter()
1951                .position(|&idx| idx == self.selected);
1952
1953            if binding_matches(&self.keymap.up, key_msg)
1954                && let Some(pos) = current_pos
1955                && pos > 0
1956            {
1957                self.selected = filtered_indices[pos - 1];
1958                self.adjust_offset_from_indices(&filtered_indices);
1959            } else if binding_matches(&self.keymap.down, key_msg)
1960                && let Some(pos) = current_pos
1961                && pos < filtered_indices.len().saturating_sub(1)
1962            {
1963                self.selected = filtered_indices[pos + 1];
1964                self.adjust_offset_from_indices(&filtered_indices);
1965            } else if binding_matches(&self.keymap.goto_top, key_msg)
1966                && let Some(&idx) = filtered_indices.first()
1967            {
1968                self.selected = idx;
1969                self.offset = 0;
1970            } else if binding_matches(&self.keymap.goto_bottom, key_msg)
1971                && let Some(&idx) = filtered_indices.last()
1972            {
1973                self.selected = idx;
1974                let last_pos = filtered_indices.len().saturating_sub(1);
1975                self.offset = last_pos.saturating_sub(self.height.saturating_sub(1));
1976            }
1977        }
1978
1979        None
1980    }
1981
1982    fn view(&self) -> String {
1983        let styles = self.active_styles();
1984        let mut output = String::new();
1985
1986        // Title
1987        if !self.title.is_empty() {
1988            output.push_str(&styles.title.render(&self.title));
1989            output.push('\n');
1990        }
1991
1992        // Description
1993        if !self.description.is_empty() {
1994            output.push_str(&styles.description.render(&self.description));
1995            output.push('\n');
1996        }
1997
1998        // Filter input (if filtering is enabled and filter is active)
1999        if self.filtering && !self.filter_value.is_empty() {
2000            let filter_display = format!("Filter: {}_", self.filter_value);
2001            output.push_str(&styles.description.render(&filter_display));
2002            output.push('\n');
2003        }
2004
2005        // Options
2006        let filtered = self.filtered_options();
2007        let visible: Vec<_> = filtered
2008            .iter()
2009            .skip(self.offset)
2010            .take(self.height)
2011            .collect();
2012
2013        if self.inline {
2014            // Inline mode
2015            let mut inline_output = String::new();
2016            inline_output.push_str(&styles.prev_indicator.render(""));
2017            for (i, (idx, opt)) in visible.iter().enumerate() {
2018                if *idx == self.selected {
2019                    inline_output.push_str(&styles.selected_option.render(&opt.key));
2020                } else {
2021                    inline_output.push_str(&styles.option.render(&opt.key));
2022                }
2023                if i < visible.len() - 1 {
2024                    inline_output.push_str("  ");
2025                }
2026            }
2027            inline_output.push_str(&styles.next_indicator.render(""));
2028            output.push_str(&inline_output);
2029        } else {
2030            // Vertical list mode
2031            let has_visible = !visible.is_empty();
2032            for (idx, opt) in &visible {
2033                if *idx == self.selected {
2034                    output.push_str(&styles.select_selector.render(""));
2035                    output.push_str(&styles.selected_option.render(&opt.key));
2036                } else {
2037                    output.push_str("  ");
2038                    output.push_str(&styles.option.render(&opt.key));
2039                }
2040                output.push('\n');
2041            }
2042            // Remove trailing newline
2043            if has_visible {
2044                output.pop();
2045            }
2046        }
2047
2048        // Error indicator
2049        if self.error.is_some() {
2050            output.push_str(&styles.error_indicator.render(""));
2051        }
2052
2053        styles
2054            .base
2055            .width(self.width.try_into().unwrap_or(u16::MAX))
2056            .render(&output)
2057    }
2058
2059    fn focus(&mut self) -> Option<Cmd> {
2060        self.focused = true;
2061        None
2062    }
2063
2064    fn blur(&mut self) -> Option<Cmd> {
2065        self.focused = false;
2066        self.run_validation();
2067        None
2068    }
2069
2070    fn key_binds(&self) -> Vec<Binding> {
2071        vec![
2072            self.keymap.up.clone(),
2073            self.keymap.down.clone(),
2074            self.keymap.prev.clone(),
2075            self.keymap.submit.clone(),
2076            self.keymap.next.clone(),
2077        ]
2078    }
2079
2080    fn with_theme(&mut self, theme: &Theme) {
2081        if self.theme.is_none() {
2082            self.theme = Some(theme.clone());
2083        }
2084    }
2085
2086    fn with_keymap(&mut self, keymap: &KeyMap) {
2087        self.keymap = keymap.select.clone();
2088    }
2089
2090    fn with_width(&mut self, width: usize) {
2091        self.width = width;
2092    }
2093
2094    fn with_height(&mut self, height: usize) {
2095        self.height = height;
2096    }
2097
2098    fn with_position(&mut self, position: FieldPosition) {
2099        self._position = position;
2100    }
2101}
2102
2103// -----------------------------------------------------------------------------
2104// MultiSelect Field
2105// -----------------------------------------------------------------------------
2106
2107/// A multi-select field for choosing multiple options from a list.
2108pub struct MultiSelect<T: Clone + PartialEq + Send + Sync + 'static> {
2109    id: usize,
2110    key: String,
2111    options: Vec<SelectOption<T>>,
2112    selected: Vec<usize>,
2113    cursor: usize,
2114    title: String,
2115    description: String,
2116    focused: bool,
2117    error: Option<String>,
2118    #[allow(clippy::type_complexity)]
2119    validate: Option<fn(&[T]) -> Option<String>>,
2120    width: usize,
2121    height: usize,
2122    limit: Option<usize>,
2123    theme: Option<Theme>,
2124    keymap: MultiSelectKeyMap,
2125    _position: FieldPosition,
2126    filtering: bool,
2127    filter_value: String,
2128    offset: usize,
2129}
2130
2131impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for MultiSelect<T> {
2132    fn default() -> Self {
2133        Self::new()
2134    }
2135}
2136
2137impl<T: Clone + PartialEq + Send + Sync + Default + 'static> MultiSelect<T> {
2138    /// Creates a new multi-select field.
2139    pub fn new() -> Self {
2140        Self {
2141            id: next_id(),
2142            key: String::new(),
2143            options: Vec::new(),
2144            selected: Vec::new(),
2145            cursor: 0,
2146            title: String::new(),
2147            description: String::new(),
2148            focused: false,
2149            error: None,
2150            validate: None,
2151            width: 80,
2152            height: 5,
2153            limit: None,
2154            theme: None,
2155            keymap: MultiSelectKeyMap::default(),
2156            _position: FieldPosition::default(),
2157            filtering: false,
2158            filter_value: String::new(),
2159            offset: 0,
2160        }
2161    }
2162
2163    /// Sets the field key.
2164    pub fn key(mut self, key: impl Into<String>) -> Self {
2165        self.key = key.into();
2166        self
2167    }
2168
2169    /// Sets the options.
2170    pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
2171        self.options = options;
2172        // Find initially selected options
2173        self.selected = self
2174            .options
2175            .iter()
2176            .enumerate()
2177            .filter(|(_, opt)| opt.selected)
2178            .map(|(i, _)| i)
2179            .collect();
2180        self
2181    }
2182
2183    /// Sets the title.
2184    pub fn title(mut self, title: impl Into<String>) -> Self {
2185        self.title = title.into();
2186        self
2187    }
2188
2189    /// Sets the description.
2190    pub fn description(mut self, description: impl Into<String>) -> Self {
2191        self.description = description.into();
2192        self
2193    }
2194
2195    /// Sets the validation function.
2196    pub fn validate(mut self, validate: fn(&[T]) -> Option<String>) -> Self {
2197        self.validate = Some(validate);
2198        self
2199    }
2200
2201    /// Sets the visible height (number of options shown).
2202    pub fn height_options(mut self, height: usize) -> Self {
2203        self.height = height;
2204        self
2205    }
2206
2207    /// Sets the maximum number of selections allowed.
2208    pub fn limit(mut self, limit: usize) -> Self {
2209        self.limit = Some(limit);
2210        self
2211    }
2212
2213    /// Enables or disables filtering mode.
2214    ///
2215    /// When enabled, pressing '/' enters filter mode where typing filters options.
2216    pub fn filterable(mut self, enabled: bool) -> Self {
2217        self.filtering = enabled;
2218        self
2219    }
2220
2221    /// Updates the filter value with proper cursor adjustment.
2222    ///
2223    /// This method ensures the cursor stays on the same item when possible,
2224    /// or clamps to valid bounds if the current item is filtered out.
2225    fn update_filter(&mut self, new_value: String) {
2226        // Remember what item cursor is currently pointing to (original index)
2227        let old_filtered = self.filtered_options();
2228        let current_item_idx = old_filtered.get(self.cursor).map(|(idx, _)| *idx);
2229
2230        // Update the filter
2231        self.filter_value = new_value;
2232
2233        // Recalculate filtered options
2234        let new_filtered = self.filtered_options();
2235
2236        // Try to keep cursor on the same item
2237        if let Some(item_idx) = current_item_idx
2238            && let Some(new_pos) = new_filtered.iter().position(|(idx, _)| *idx == item_idx)
2239        {
2240            self.cursor = new_pos;
2241            self.adjust_offset();
2242            return;
2243        }
2244
2245        // Item no longer visible, clamp cursor to valid range
2246        self.cursor = self.cursor.min(new_filtered.len().saturating_sub(1));
2247        self.adjust_offset();
2248    }
2249
2250    /// Adjusts the offset to keep the cursor visible within the view.
2251    fn adjust_offset(&mut self) {
2252        // Ensure cursor is within visible window
2253        if self.cursor < self.offset {
2254            self.offset = self.cursor;
2255        } else if self.cursor >= self.offset + self.height {
2256            self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2257        }
2258    }
2259
2260    fn get_theme(&self) -> Theme {
2261        self.theme.clone().unwrap_or_else(theme_charm)
2262    }
2263
2264    fn active_styles(&self) -> FieldStyles {
2265        let theme = self.get_theme();
2266        if self.focused {
2267            theme.focused
2268        } else {
2269            theme.blurred
2270        }
2271    }
2272
2273    fn run_validation(&mut self) {
2274        if let Some(validate) = self.validate {
2275            let values: Vec<T> = self
2276                .selected
2277                .iter()
2278                .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2279                .collect();
2280            self.error = validate(&values);
2281        }
2282    }
2283
2284    fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
2285        if self.filter_value.is_empty() {
2286            self.options.iter().enumerate().collect()
2287        } else {
2288            let filter_lower = self.filter_value.to_lowercase();
2289            self.options
2290                .iter()
2291                .enumerate()
2292                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
2293                .collect()
2294        }
2295    }
2296
2297    fn toggle_current(&mut self) {
2298        let filtered = self.filtered_options();
2299        if let Some((idx, _)) = filtered.get(self.cursor) {
2300            if let Some(pos) = self.selected.iter().position(|&i| i == *idx) {
2301                // Deselect
2302                self.selected.remove(pos);
2303            } else if self.limit.is_none_or(|l| self.selected.len() < l) {
2304                // Select (if within limit)
2305                self.selected.push(*idx);
2306            }
2307        }
2308    }
2309
2310    fn select_all(&mut self) {
2311        if let Some(limit) = self.limit {
2312            // Only select up to limit
2313            self.selected = self
2314                .options
2315                .iter()
2316                .enumerate()
2317                .take(limit)
2318                .map(|(i, _)| i)
2319                .collect();
2320        } else {
2321            self.selected = (0..self.options.len()).collect();
2322        }
2323    }
2324
2325    fn select_none(&mut self) {
2326        self.selected.clear();
2327    }
2328
2329    /// Gets the currently selected values.
2330    pub fn get_selected_values(&self) -> Vec<&T> {
2331        self.selected
2332            .iter()
2333            .filter_map(|&i| self.options.get(i).map(|o| &o.value))
2334            .collect()
2335    }
2336
2337    /// Returns the field ID.
2338    pub fn id(&self) -> usize {
2339        self.id
2340    }
2341}
2342
2343impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for MultiSelect<T> {
2344    fn get_key(&self) -> &str {
2345        &self.key
2346    }
2347
2348    fn get_value(&self) -> Box<dyn Any> {
2349        let values: Vec<T> = self
2350            .selected
2351            .iter()
2352            .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2353            .collect();
2354        Box::new(values)
2355    }
2356
2357    fn error(&self) -> Option<&str> {
2358        self.error.as_deref()
2359    }
2360
2361    fn init(&mut self) -> Option<Cmd> {
2362        None
2363    }
2364
2365    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2366        if !self.focused {
2367            return None;
2368        }
2369
2370        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2371            self.error = None;
2372
2373            // Handle filter input when filtering is enabled
2374            if self.filtering {
2375                // Clear filter on Escape
2376                if key_msg.key_type == KeyType::Esc {
2377                    self.update_filter(String::new());
2378                    return None;
2379                }
2380
2381                // Remove character on Backspace
2382                if key_msg.key_type == KeyType::Backspace {
2383                    if !self.filter_value.is_empty() {
2384                        let mut new_filter = self.filter_value.clone();
2385                        new_filter.pop();
2386                        self.update_filter(new_filter);
2387                    }
2388                    return None;
2389                }
2390
2391                // Add characters to filter
2392                if key_msg.key_type == KeyType::Runes {
2393                    let mut new_filter = self.filter_value.clone();
2394                    for c in &key_msg.runes {
2395                        // Only add printable characters that aren't navigation/toggle keys
2396                        // Always skip these keys so they work for navigation/toggle
2397                        match c {
2398                            'j' | 'k' | 'g' | 'G' | ' ' | 'x' | '/' => continue,
2399                            _ => {}
2400                        }
2401                        if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
2402                            new_filter.push(*c);
2403                        }
2404                    }
2405                    if new_filter != self.filter_value {
2406                        self.update_filter(new_filter);
2407                        return None;
2408                    }
2409                }
2410            }
2411
2412            // Check for prev
2413            if binding_matches(&self.keymap.prev, key_msg) {
2414                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2415            }
2416
2417            // Check for next/submit
2418            if binding_matches(&self.keymap.next, key_msg)
2419                || binding_matches(&self.keymap.submit, key_msg)
2420            {
2421                self.run_validation();
2422                if self.error.is_some() {
2423                    return None;
2424                }
2425                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2426            }
2427
2428            // Toggle selection
2429            if binding_matches(&self.keymap.toggle, key_msg) {
2430                self.toggle_current();
2431            }
2432
2433            // Select all
2434            if binding_matches(&self.keymap.select_all, key_msg) {
2435                if self.selected.len() == self.options.len() {
2436                    self.select_none();
2437                } else {
2438                    self.select_all();
2439                }
2440            }
2441
2442            // Navigation
2443            if binding_matches(&self.keymap.up, key_msg) {
2444                if self.cursor > 0 {
2445                    self.cursor -= 1;
2446                    if self.cursor < self.offset {
2447                        self.offset = self.cursor;
2448                    }
2449                }
2450            } else if binding_matches(&self.keymap.down, key_msg) {
2451                let filtered = self.filtered_options();
2452                if self.cursor < filtered.len().saturating_sub(1) {
2453                    self.cursor += 1;
2454                    if self.cursor >= self.offset + self.height {
2455                        self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2456                    }
2457                }
2458            } else if binding_matches(&self.keymap.goto_top, key_msg) {
2459                self.cursor = 0;
2460                self.offset = 0;
2461            } else if binding_matches(&self.keymap.goto_bottom, key_msg) {
2462                let filtered = self.filtered_options();
2463                self.cursor = filtered.len().saturating_sub(1);
2464                self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2465            }
2466        }
2467
2468        None
2469    }
2470
2471    fn view(&self) -> String {
2472        let styles = self.active_styles();
2473        let mut output = String::new();
2474
2475        // Title
2476        if !self.title.is_empty() {
2477            output.push_str(&styles.title.render(&self.title));
2478            output.push('\n');
2479        }
2480
2481        // Description
2482        if !self.description.is_empty() {
2483            output.push_str(&styles.description.render(&self.description));
2484            output.push('\n');
2485        }
2486
2487        // Filter input (if filtering is enabled and filter is active)
2488        if self.filtering && !self.filter_value.is_empty() {
2489            let filter_display = format!("Filter: {}_", self.filter_value);
2490            output.push_str(&styles.description.render(&filter_display));
2491            output.push('\n');
2492        }
2493
2494        // Options
2495        let filtered = self.filtered_options();
2496        let visible: Vec<_> = filtered
2497            .iter()
2498            .skip(self.offset)
2499            .take(self.height)
2500            .collect();
2501
2502        // Vertical list mode with checkboxes
2503        for (i, (idx, opt)) in visible.iter().enumerate() {
2504            let is_cursor = self.offset + i == self.cursor;
2505            let is_selected = self.selected.contains(idx);
2506
2507            // Cursor indicator
2508            if is_cursor {
2509                output.push_str(&styles.select_selector.render(""));
2510            } else {
2511                output.push_str("  ");
2512            }
2513
2514            // Checkbox
2515            let checkbox = if is_selected { "[x] " } else { "[ ] " };
2516            output.push_str(checkbox);
2517
2518            // Option text
2519            if is_cursor {
2520                output.push_str(&styles.selected_option.render(&opt.key));
2521            } else {
2522                output.push_str(&styles.option.render(&opt.key));
2523            }
2524
2525            output.push('\n');
2526        }
2527
2528        // Remove trailing newline
2529        if !visible.is_empty() {
2530            output.pop();
2531        }
2532
2533        // Error indicator
2534        if self.error.is_some() {
2535            output.push_str(&styles.error_indicator.render(""));
2536        }
2537
2538        styles
2539            .base
2540            .width(self.width.try_into().unwrap_or(u16::MAX))
2541            .render(&output)
2542    }
2543
2544    fn focus(&mut self) -> Option<Cmd> {
2545        self.focused = true;
2546        None
2547    }
2548
2549    fn blur(&mut self) -> Option<Cmd> {
2550        self.focused = false;
2551        self.run_validation();
2552        None
2553    }
2554
2555    fn key_binds(&self) -> Vec<Binding> {
2556        vec![
2557            self.keymap.up.clone(),
2558            self.keymap.down.clone(),
2559            self.keymap.toggle.clone(),
2560            self.keymap.prev.clone(),
2561            self.keymap.submit.clone(),
2562            self.keymap.next.clone(),
2563        ]
2564    }
2565
2566    fn with_theme(&mut self, theme: &Theme) {
2567        if self.theme.is_none() {
2568            self.theme = Some(theme.clone());
2569        }
2570    }
2571
2572    fn with_keymap(&mut self, keymap: &KeyMap) {
2573        self.keymap = keymap.multi_select.clone();
2574    }
2575
2576    fn with_width(&mut self, width: usize) {
2577        self.width = width;
2578    }
2579
2580    fn with_height(&mut self, height: usize) {
2581        self.height = height;
2582    }
2583
2584    fn with_position(&mut self, position: FieldPosition) {
2585        self._position = position;
2586    }
2587}
2588
2589// -----------------------------------------------------------------------------
2590// Confirm Field
2591// -----------------------------------------------------------------------------
2592
2593/// A confirmation field with Yes/No options.
2594pub struct Confirm {
2595    id: usize,
2596    key: String,
2597    value: bool,
2598    title: String,
2599    description: String,
2600    affirmative: String,
2601    negative: String,
2602    focused: bool,
2603    width: usize,
2604    theme: Option<Theme>,
2605    keymap: ConfirmKeyMap,
2606    _position: FieldPosition,
2607}
2608
2609impl Default for Confirm {
2610    fn default() -> Self {
2611        Self::new()
2612    }
2613}
2614
2615impl Confirm {
2616    /// Creates a new confirm field.
2617    pub fn new() -> Self {
2618        Self {
2619            id: next_id(),
2620            key: String::new(),
2621            value: false,
2622            title: String::new(),
2623            description: String::new(),
2624            affirmative: "Yes".to_string(),
2625            negative: "No".to_string(),
2626            focused: false,
2627            width: 80,
2628            theme: None,
2629            keymap: ConfirmKeyMap::default(),
2630            _position: FieldPosition::default(),
2631        }
2632    }
2633
2634    /// Sets the field key.
2635    pub fn key(mut self, key: impl Into<String>) -> Self {
2636        self.key = key.into();
2637        self
2638    }
2639
2640    /// Sets the initial value.
2641    pub fn value(mut self, value: bool) -> Self {
2642        self.value = value;
2643        self
2644    }
2645
2646    /// Sets the title.
2647    pub fn title(mut self, title: impl Into<String>) -> Self {
2648        self.title = title.into();
2649        self
2650    }
2651
2652    /// Sets the description.
2653    pub fn description(mut self, description: impl Into<String>) -> Self {
2654        self.description = description.into();
2655        self
2656    }
2657
2658    /// Sets the affirmative button text.
2659    pub fn affirmative(mut self, text: impl Into<String>) -> Self {
2660        self.affirmative = text.into();
2661        self
2662    }
2663
2664    /// Sets the negative button text.
2665    pub fn negative(mut self, text: impl Into<String>) -> Self {
2666        self.negative = text.into();
2667        self
2668    }
2669
2670    fn get_theme(&self) -> Theme {
2671        self.theme.clone().unwrap_or_else(theme_charm)
2672    }
2673
2674    fn active_styles(&self) -> FieldStyles {
2675        let theme = self.get_theme();
2676        if self.focused {
2677            theme.focused
2678        } else {
2679            theme.blurred
2680        }
2681    }
2682
2683    /// Gets the current value.
2684    pub fn get_bool_value(&self) -> bool {
2685        self.value
2686    }
2687
2688    /// Returns the field ID.
2689    pub fn id(&self) -> usize {
2690        self.id
2691    }
2692}
2693
2694impl Field for Confirm {
2695    fn get_key(&self) -> &str {
2696        &self.key
2697    }
2698
2699    fn get_value(&self) -> Box<dyn Any> {
2700        Box::new(self.value)
2701    }
2702
2703    fn error(&self) -> Option<&str> {
2704        None
2705    }
2706
2707    fn init(&mut self) -> Option<Cmd> {
2708        None
2709    }
2710
2711    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2712        if !self.focused {
2713            return None;
2714        }
2715
2716        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2717            // Check for prev
2718            if binding_matches(&self.keymap.prev, key_msg) {
2719                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2720            }
2721
2722            // Check for next/submit
2723            if binding_matches(&self.keymap.next, key_msg)
2724                || binding_matches(&self.keymap.submit, key_msg)
2725            {
2726                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2727            }
2728
2729            // Toggle
2730            if binding_matches(&self.keymap.toggle, key_msg) {
2731                self.value = !self.value;
2732            }
2733
2734            // Direct accept/reject
2735            if binding_matches(&self.keymap.accept, key_msg) {
2736                self.value = true;
2737            }
2738            if binding_matches(&self.keymap.reject, key_msg) {
2739                self.value = false;
2740            }
2741        }
2742
2743        None
2744    }
2745
2746    fn view(&self) -> String {
2747        let styles = self.active_styles();
2748        let mut output = String::new();
2749
2750        // Title
2751        if !self.title.is_empty() {
2752            output.push_str(&styles.title.render(&self.title));
2753            output.push('\n');
2754        }
2755
2756        // Description
2757        if !self.description.is_empty() {
2758            output.push_str(&styles.description.render(&self.description));
2759            output.push('\n');
2760        }
2761
2762        // Buttons
2763        if self.value {
2764            output.push_str(&styles.focused_button.render(&self.affirmative));
2765            output.push_str(&styles.blurred_button.render(&self.negative));
2766        } else {
2767            output.push_str(&styles.blurred_button.render(&self.affirmative));
2768            output.push_str(&styles.focused_button.render(&self.negative));
2769        }
2770
2771        styles
2772            .base
2773            .width(self.width.try_into().unwrap_or(u16::MAX))
2774            .render(&output)
2775    }
2776
2777    fn focus(&mut self) -> Option<Cmd> {
2778        self.focused = true;
2779        None
2780    }
2781
2782    fn blur(&mut self) -> Option<Cmd> {
2783        self.focused = false;
2784        None
2785    }
2786
2787    fn key_binds(&self) -> Vec<Binding> {
2788        vec![
2789            self.keymap.toggle.clone(),
2790            self.keymap.accept.clone(),
2791            self.keymap.reject.clone(),
2792            self.keymap.prev.clone(),
2793            self.keymap.submit.clone(),
2794            self.keymap.next.clone(),
2795        ]
2796    }
2797
2798    fn with_theme(&mut self, theme: &Theme) {
2799        if self.theme.is_none() {
2800            self.theme = Some(theme.clone());
2801        }
2802    }
2803
2804    fn with_keymap(&mut self, keymap: &KeyMap) {
2805        self.keymap = keymap.confirm.clone();
2806    }
2807
2808    fn with_width(&mut self, width: usize) {
2809        self.width = width;
2810    }
2811
2812    fn with_height(&mut self, _height: usize) {
2813        // Confirm doesn't use height
2814    }
2815
2816    fn with_position(&mut self, position: FieldPosition) {
2817        self._position = position;
2818    }
2819}
2820
2821// -----------------------------------------------------------------------------
2822// Note Field
2823// -----------------------------------------------------------------------------
2824
2825/// A non-interactive note/text display field.
2826pub struct Note {
2827    id: usize,
2828    key: String,
2829    title: String,
2830    description: String,
2831    focused: bool,
2832    width: usize,
2833    theme: Option<Theme>,
2834    keymap: NoteKeyMap,
2835    _position: FieldPosition,
2836    next_label: String,
2837}
2838
2839impl Default for Note {
2840    fn default() -> Self {
2841        Self::new()
2842    }
2843}
2844
2845impl Note {
2846    /// Creates a new note field.
2847    pub fn new() -> Self {
2848        Self {
2849            id: next_id(),
2850            key: String::new(),
2851            title: String::new(),
2852            description: String::new(),
2853            focused: false,
2854            width: 80,
2855            theme: None,
2856            keymap: NoteKeyMap::default(),
2857            _position: FieldPosition::default(),
2858            next_label: "Next".to_string(),
2859        }
2860    }
2861
2862    /// Sets the field key.
2863    pub fn key(mut self, key: impl Into<String>) -> Self {
2864        self.key = key.into();
2865        self
2866    }
2867
2868    /// Sets the title.
2869    pub fn title(mut self, title: impl Into<String>) -> Self {
2870        self.title = title.into();
2871        self
2872    }
2873
2874    /// Sets the description (body text).
2875    pub fn description(mut self, description: impl Into<String>) -> Self {
2876        self.description = description.into();
2877        self
2878    }
2879
2880    /// Sets the next button label.
2881    pub fn next_label(mut self, label: impl Into<String>) -> Self {
2882        self.next_label = label.into();
2883        self
2884    }
2885
2886    /// Sets the next button label (alias for `next_label`).
2887    ///
2888    /// This method is provided for compatibility with Go's huh API.
2889    pub fn next(self, label: impl Into<String>) -> Self {
2890        self.next_label(label)
2891    }
2892
2893    fn get_theme(&self) -> Theme {
2894        self.theme.clone().unwrap_or_else(theme_charm)
2895    }
2896
2897    fn active_styles(&self) -> FieldStyles {
2898        let theme = self.get_theme();
2899        if self.focused {
2900            theme.focused
2901        } else {
2902            theme.blurred
2903        }
2904    }
2905
2906    /// Returns the field ID.
2907    pub fn id(&self) -> usize {
2908        self.id
2909    }
2910}
2911
2912impl Field for Note {
2913    fn get_key(&self) -> &str {
2914        &self.key
2915    }
2916
2917    fn get_value(&self) -> Box<dyn Any> {
2918        Box::new(())
2919    }
2920
2921    fn error(&self) -> Option<&str> {
2922        None
2923    }
2924
2925    fn init(&mut self) -> Option<Cmd> {
2926        None
2927    }
2928
2929    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2930        if !self.focused {
2931            return None;
2932        }
2933
2934        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2935            // Check for prev
2936            if binding_matches(&self.keymap.prev, key_msg) {
2937                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2938            }
2939
2940            // Check for next/submit
2941            if binding_matches(&self.keymap.next, key_msg)
2942                || binding_matches(&self.keymap.submit, key_msg)
2943            {
2944                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2945            }
2946        }
2947
2948        None
2949    }
2950
2951    fn view(&self) -> String {
2952        let styles = self.active_styles();
2953        let mut output = String::new();
2954
2955        // Title
2956        if !self.title.is_empty() {
2957            output.push_str(&styles.note_title.render(&self.title));
2958            output.push('\n');
2959        }
2960
2961        // Description
2962        if !self.description.is_empty() {
2963            output.push_str(&styles.description.render(&self.description));
2964        }
2965
2966        styles
2967            .base
2968            .width(self.width.try_into().unwrap_or(u16::MAX))
2969            .render(&output)
2970    }
2971
2972    fn focus(&mut self) -> Option<Cmd> {
2973        self.focused = true;
2974        None
2975    }
2976
2977    fn blur(&mut self) -> Option<Cmd> {
2978        self.focused = false;
2979        None
2980    }
2981
2982    fn key_binds(&self) -> Vec<Binding> {
2983        vec![
2984            self.keymap.prev.clone(),
2985            self.keymap.submit.clone(),
2986            self.keymap.next.clone(),
2987        ]
2988    }
2989
2990    fn with_theme(&mut self, theme: &Theme) {
2991        if self.theme.is_none() {
2992            self.theme = Some(theme.clone());
2993        }
2994    }
2995
2996    fn with_keymap(&mut self, keymap: &KeyMap) {
2997        self.keymap = keymap.note.clone();
2998    }
2999
3000    fn with_width(&mut self, width: usize) {
3001        self.width = width;
3002    }
3003
3004    fn with_height(&mut self, _height: usize) {
3005        // Note doesn't use height
3006    }
3007
3008    fn with_position(&mut self, position: FieldPosition) {
3009        self._position = position;
3010    }
3011}
3012
3013// -----------------------------------------------------------------------------
3014// Text Field (Textarea)
3015// -----------------------------------------------------------------------------
3016
3017/// A multi-line text area field.
3018///
3019/// The Text field is used for gathering longer-form user input.
3020/// It wraps the bubbles textarea component and integrates it with the huh form system.
3021///
3022/// # Example
3023///
3024/// ```rust,ignore
3025/// use huh::Text;
3026///
3027/// let text = Text::new()
3028///     .key("bio")
3029///     .title("Biography")
3030///     .description("Tell us about yourself")
3031///     .placeholder("Enter your bio...")
3032///     .lines(5);
3033/// ```
3034pub struct Text {
3035    id: usize,
3036    key: String,
3037    value: String,
3038    title: String,
3039    description: String,
3040    placeholder: String,
3041    lines: usize,
3042    char_limit: usize,
3043    show_line_numbers: bool,
3044    focused: bool,
3045    error: Option<String>,
3046    validate: Option<fn(&str) -> Option<String>>,
3047    width: usize,
3048    height: usize,
3049    theme: Option<Theme>,
3050    keymap: TextKeyMap,
3051    _position: FieldPosition,
3052    cursor_row: usize,
3053    cursor_col: usize,
3054}
3055
3056impl Default for Text {
3057    fn default() -> Self {
3058        Self::new()
3059    }
3060}
3061
3062impl Text {
3063    /// Creates a new text area field.
3064    pub fn new() -> Self {
3065        Self {
3066            id: next_id(),
3067            key: String::new(),
3068            value: String::new(),
3069            title: String::new(),
3070            description: String::new(),
3071            placeholder: String::new(),
3072            lines: 5,
3073            char_limit: 0,
3074            show_line_numbers: false,
3075            focused: false,
3076            error: None,
3077            validate: None,
3078            width: 80,
3079            height: 0,
3080            theme: None,
3081            keymap: TextKeyMap::default(),
3082            _position: FieldPosition::default(),
3083            cursor_row: 0,
3084            cursor_col: 0,
3085        }
3086    }
3087
3088    /// Sets the field key.
3089    pub fn key(mut self, key: impl Into<String>) -> Self {
3090        self.key = key.into();
3091        self
3092    }
3093
3094    /// Sets the initial value.
3095    pub fn value(mut self, value: impl Into<String>) -> Self {
3096        self.value = value.into();
3097        self
3098    }
3099
3100    /// Sets the title.
3101    pub fn title(mut self, title: impl Into<String>) -> Self {
3102        self.title = title.into();
3103        self
3104    }
3105
3106    /// Sets the description.
3107    pub fn description(mut self, description: impl Into<String>) -> Self {
3108        self.description = description.into();
3109        self
3110    }
3111
3112    /// Sets the placeholder text.
3113    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
3114        self.placeholder = placeholder.into();
3115        self
3116    }
3117
3118    /// Sets the number of visible lines.
3119    pub fn lines(mut self, lines: usize) -> Self {
3120        self.lines = lines;
3121        self
3122    }
3123
3124    /// Sets the character limit (0 = no limit).
3125    pub fn char_limit(mut self, limit: usize) -> Self {
3126        self.char_limit = limit;
3127        self
3128    }
3129
3130    /// Sets whether to show line numbers.
3131    pub fn show_line_numbers(mut self, show: bool) -> Self {
3132        self.show_line_numbers = show;
3133        self
3134    }
3135
3136    /// Sets the validation function.
3137    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3138        self.validate = Some(validate);
3139        self
3140    }
3141
3142    fn get_theme(&self) -> Theme {
3143        self.theme.clone().unwrap_or_else(theme_charm)
3144    }
3145
3146    fn active_styles(&self) -> FieldStyles {
3147        let theme = self.get_theme();
3148        if self.focused {
3149            theme.focused
3150        } else {
3151            theme.blurred
3152        }
3153    }
3154
3155    fn run_validation(&mut self) {
3156        if let Some(validate) = self.validate {
3157            self.error = validate(&self.value);
3158        }
3159    }
3160
3161    /// Gets the current value.
3162    pub fn get_string_value(&self) -> &str {
3163        &self.value
3164    }
3165
3166    /// Returns the field ID.
3167    pub fn id(&self) -> usize {
3168        self.id
3169    }
3170
3171    fn visible_lines(&self) -> Vec<&str> {
3172        let lines: Vec<&str> = self.value.lines().collect();
3173        if lines.is_empty() { vec![""] } else { lines }
3174    }
3175
3176    /// Transpose the character at cursor with the one before it.
3177    ///
3178    /// If at the end of the line, moves cursor back first. After swapping,
3179    /// moves cursor right if not at end of line. No-op if cursor is at
3180    /// beginning of line or line has fewer than 2 characters.
3181    fn transpose_left(&mut self) {
3182        let lines: Vec<String> = self.value.lines().map(String::from).collect();
3183        if self.cursor_row >= lines.len() {
3184            return;
3185        }
3186
3187        let line_chars: Vec<char> = lines[self.cursor_row].chars().collect();
3188
3189        // No-op if at beginning or line too short
3190        if self.cursor_col == 0 || line_chars.len() < 2 {
3191            return;
3192        }
3193
3194        let mut col = self.cursor_col;
3195
3196        // If at end, move back first
3197        if col >= line_chars.len() {
3198            col = line_chars.len() - 1;
3199            self.cursor_col = col;
3200        }
3201
3202        // Swap chars at col-1 and col
3203        let mut new_chars = line_chars;
3204        new_chars.swap(col - 1, col);
3205
3206        // Rebuild value
3207        let mut new_lines = lines;
3208        new_lines[self.cursor_row] = new_chars.into_iter().collect();
3209        self.value = new_lines.join("\n");
3210
3211        // Move right if not at end of line
3212        let new_line_len = self
3213            .value
3214            .lines()
3215            .nth(self.cursor_row)
3216            .map(|l| l.chars().count())
3217            .unwrap_or(0);
3218        if self.cursor_col < new_line_len {
3219            self.cursor_col += 1;
3220        }
3221    }
3222
3223    /// Helper for word operations - operates on current line.
3224    ///
3225    /// Skips whitespace forward, then processes each character in the word
3226    /// using the provided function. Moves cursor to the end of the word.
3227    fn do_word_right<F>(&mut self, mut f: F)
3228    where
3229        F: FnMut(usize, char) -> char,
3230    {
3231        let lines: Vec<String> = self.value.lines().map(String::from).collect();
3232        if self.cursor_row >= lines.len() {
3233            return;
3234        }
3235
3236        let mut chars: Vec<char> = lines[self.cursor_row].chars().collect();
3237        let len = chars.len();
3238
3239        // Skip spaces forward
3240        while self.cursor_col < len && chars[self.cursor_col].is_whitespace() {
3241            self.cursor_col += 1;
3242        }
3243
3244        // Process word chars
3245        let mut char_idx = 0;
3246        while self.cursor_col < len && !chars[self.cursor_col].is_whitespace() {
3247            chars[self.cursor_col] = f(char_idx, chars[self.cursor_col]);
3248            self.cursor_col += 1;
3249            char_idx += 1;
3250        }
3251
3252        // Rebuild value
3253        let mut new_lines = lines;
3254        new_lines[self.cursor_row] = chars.into_iter().collect();
3255        self.value = new_lines.join("\n");
3256    }
3257
3258    /// Uppercase the word to the right of the cursor.
3259    fn uppercase_right(&mut self) {
3260        self.do_word_right(|_, c| c.to_uppercase().next().unwrap_or(c));
3261    }
3262
3263    /// Lowercase the word to the right of the cursor.
3264    fn lowercase_right(&mut self) {
3265        self.do_word_right(|_, c| c.to_lowercase().next().unwrap_or(c));
3266    }
3267
3268    /// Capitalize the word to the right (first char uppercase, rest unchanged).
3269    fn capitalize_right(&mut self) {
3270        self.do_word_right(|idx, c| {
3271            if idx == 0 {
3272                c.to_uppercase().next().unwrap_or(c)
3273            } else {
3274                c
3275            }
3276        });
3277    }
3278}
3279
3280impl Field for Text {
3281    fn get_key(&self) -> &str {
3282        &self.key
3283    }
3284
3285    fn get_value(&self) -> Box<dyn Any> {
3286        Box::new(self.value.clone())
3287    }
3288
3289    fn error(&self) -> Option<&str> {
3290        self.error.as_deref()
3291    }
3292
3293    fn init(&mut self) -> Option<Cmd> {
3294        None
3295    }
3296
3297    fn update(&mut self, msg: &Message) -> Option<Cmd> {
3298        if !self.focused {
3299            return None;
3300        }
3301
3302        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3303            self.error = None;
3304
3305            // Check for prev
3306            if binding_matches(&self.keymap.prev, key_msg) {
3307                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3308            }
3309
3310            // Check for next/submit (tab submits in text area)
3311            if binding_matches(&self.keymap.next, key_msg)
3312                || binding_matches(&self.keymap.submit, key_msg)
3313            {
3314                self.run_validation();
3315                if self.error.is_some() {
3316                    return None;
3317                }
3318                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3319            }
3320
3321            // Check for new line
3322            if binding_matches(&self.keymap.new_line, key_msg) {
3323                if self.char_limit == 0 || self.value.len() < self.char_limit {
3324                    self.value.push('\n');
3325                    self.cursor_row += 1;
3326                    self.cursor_col = 0;
3327                }
3328                return None;
3329            }
3330
3331            // Check for word transformation operations
3332            if binding_matches(&self.keymap.uppercase_word_forward, key_msg) {
3333                self.uppercase_right();
3334                return None;
3335            }
3336            if binding_matches(&self.keymap.lowercase_word_forward, key_msg) {
3337                self.lowercase_right();
3338                return None;
3339            }
3340            if binding_matches(&self.keymap.capitalize_word_forward, key_msg) {
3341                self.capitalize_right();
3342                return None;
3343            }
3344            if binding_matches(&self.keymap.transpose_character_backward, key_msg) {
3345                self.transpose_left();
3346                return None;
3347            }
3348
3349            // Handle text input
3350            match key_msg.key_type {
3351                KeyType::Runes => {
3352                    // Calculate how many chars we can insert respecting char_limit
3353                    let current_count = self.value.chars().count();
3354                    let available = if self.char_limit == 0 {
3355                        usize::MAX
3356                    } else {
3357                        self.char_limit.saturating_sub(current_count)
3358                    };
3359
3360                    // For paste operations, handle bulk insert with proper cursor tracking
3361                    // Multi-line textareas preserve newlines
3362                    let chars_to_add: Vec<char> =
3363                        key_msg.runes.iter().copied().take(available).collect();
3364
3365                    for c in chars_to_add {
3366                        self.value.push(c);
3367                        if c == '\n' {
3368                            self.cursor_row += 1;
3369                            self.cursor_col = 0;
3370                        } else {
3371                            self.cursor_col += 1;
3372                        }
3373                    }
3374                }
3375                KeyType::Backspace => {
3376                    if !self.value.is_empty() {
3377                        let removed = self.value.pop();
3378                        if removed == Some('\n') {
3379                            self.cursor_row = self.cursor_row.saturating_sub(1);
3380                            let lines = self.visible_lines();
3381                            self.cursor_col =
3382                                lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3383                        } else {
3384                            self.cursor_col = self.cursor_col.saturating_sub(1);
3385                        }
3386                    }
3387                }
3388                KeyType::Enter => {
3389                    // Enter inserts newline in text areas
3390                    if self.char_limit == 0 || self.value.len() < self.char_limit {
3391                        self.value.push('\n');
3392                        self.cursor_row += 1;
3393                        self.cursor_col = 0;
3394                    }
3395                }
3396                KeyType::Up => {
3397                    self.cursor_row = self.cursor_row.saturating_sub(1);
3398                }
3399                KeyType::Down => {
3400                    let line_count = self.visible_lines().len();
3401                    if self.cursor_row < line_count.saturating_sub(1) {
3402                        self.cursor_row += 1;
3403                    }
3404                }
3405                KeyType::Left => {
3406                    if self.cursor_col > 0 {
3407                        self.cursor_col -= 1;
3408                    }
3409                }
3410                KeyType::Right => {
3411                    let lines = self.visible_lines();
3412                    let current_line_len = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3413                    if self.cursor_col < current_line_len {
3414                        self.cursor_col += 1;
3415                    }
3416                }
3417                KeyType::Home => {
3418                    self.cursor_col = 0;
3419                }
3420                KeyType::End => {
3421                    let lines = self.visible_lines();
3422                    self.cursor_col = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3423                }
3424                _ => {}
3425            }
3426        }
3427
3428        None
3429    }
3430
3431    fn view(&self) -> String {
3432        let styles = self.active_styles();
3433        let mut output = String::new();
3434
3435        // Title
3436        if !self.title.is_empty() {
3437            output.push_str(&styles.title.render(&self.title));
3438            if self.error.is_some() {
3439                output.push_str(&styles.error_indicator.render(""));
3440            }
3441            output.push('\n');
3442        }
3443
3444        // Description
3445        if !self.description.is_empty() {
3446            output.push_str(&styles.description.render(&self.description));
3447            output.push('\n');
3448        }
3449
3450        // Text area content
3451        let lines = self.visible_lines();
3452        let visible_lines = self.lines.min(lines.len().max(1));
3453
3454        for (i, line) in lines.iter().take(visible_lines).enumerate() {
3455            if self.show_line_numbers {
3456                let line_num = format!("{:3} ", i + 1);
3457                output.push_str(&styles.description.render(&line_num));
3458            }
3459
3460            if line.is_empty() && i == 0 && self.value.is_empty() && !self.placeholder.is_empty() {
3461                output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
3462            } else {
3463                output.push_str(&styles.text_input.text.render(line));
3464            }
3465
3466            if i < visible_lines - 1 {
3467                output.push('\n');
3468            }
3469        }
3470
3471        // Pad with empty lines if needed
3472        for i in lines.len()..visible_lines {
3473            output.push('\n');
3474            if self.show_line_numbers {
3475                let line_num = format!("{:3} ", i + 1);
3476                output.push_str(&styles.description.render(&line_num));
3477            }
3478        }
3479
3480        // Error message
3481        if let Some(ref err) = self.error {
3482            output.push('\n');
3483            output.push_str(&styles.error_message.render(err));
3484        }
3485
3486        styles
3487            .base
3488            .width(self.width.try_into().unwrap_or(u16::MAX))
3489            .render(&output)
3490    }
3491
3492    fn focus(&mut self) -> Option<Cmd> {
3493        self.focused = true;
3494        None
3495    }
3496
3497    fn blur(&mut self) -> Option<Cmd> {
3498        self.focused = false;
3499        self.run_validation();
3500        None
3501    }
3502
3503    fn key_binds(&self) -> Vec<Binding> {
3504        vec![
3505            self.keymap.new_line.clone(),
3506            self.keymap.prev.clone(),
3507            self.keymap.submit.clone(),
3508            self.keymap.next.clone(),
3509            self.keymap.uppercase_word_forward.clone(),
3510            self.keymap.lowercase_word_forward.clone(),
3511            self.keymap.capitalize_word_forward.clone(),
3512            self.keymap.transpose_character_backward.clone(),
3513        ]
3514    }
3515
3516    fn with_theme(&mut self, theme: &Theme) {
3517        if self.theme.is_none() {
3518            self.theme = Some(theme.clone());
3519        }
3520    }
3521
3522    fn with_keymap(&mut self, keymap: &KeyMap) {
3523        self.keymap = keymap.text.clone();
3524    }
3525
3526    fn with_width(&mut self, width: usize) {
3527        self.width = width;
3528    }
3529
3530    fn with_height(&mut self, height: usize) {
3531        self.height = height;
3532        // Adjust lines based on height minus title/description
3533        let adjust = if self.title.is_empty() { 0 } else { 1 }
3534            + if self.description.is_empty() { 0 } else { 1 };
3535        if height > adjust {
3536            self.lines = height - adjust;
3537        }
3538    }
3539
3540    fn with_position(&mut self, position: FieldPosition) {
3541        self._position = position;
3542    }
3543}
3544
3545// -----------------------------------------------------------------------------
3546// FilePicker Field
3547// -----------------------------------------------------------------------------
3548
3549/// A file picker field for selecting files and directories.
3550///
3551/// The FilePicker field allows users to browse the filesystem and select files
3552/// or directories. It can be configured to filter by file type, show/hide hidden
3553/// files, and control whether files and/or directories can be selected.
3554///
3555/// # Example
3556///
3557/// ```rust,ignore
3558/// use huh::FilePicker;
3559///
3560/// let picker = FilePicker::new()
3561///     .key("config_file")
3562///     .title("Select Configuration File")
3563///     .description("Choose a .toml or .json file")
3564///     .allowed_types(vec![".toml".to_string(), ".json".to_string()])
3565///     .current_directory(".");
3566/// ```
3567pub struct FilePicker {
3568    id: usize,
3569    key: String,
3570    selected_path: Option<String>,
3571    title: String,
3572    description: String,
3573    current_directory: String,
3574    allowed_types: Vec<String>,
3575    show_hidden: bool,
3576    show_size: bool,
3577    show_permissions: bool,
3578    file_allowed: bool,
3579    dir_allowed: bool,
3580    picking: bool,
3581    focused: bool,
3582    error: Option<String>,
3583    validate: Option<fn(&str) -> Option<String>>,
3584    width: usize,
3585    height: usize,
3586    theme: Option<Theme>,
3587    keymap: FilePickerKeyMap,
3588    _position: FieldPosition,
3589    // Simple file list for display
3590    files: Vec<FileEntry>,
3591    selected_index: usize,
3592    offset: usize,
3593}
3594
3595/// A file entry in the picker.
3596#[derive(Debug, Clone)]
3597struct FileEntry {
3598    name: String,
3599    path: String,
3600    is_dir: bool,
3601    size: u64,
3602    #[allow(dead_code)]
3603    mode: String,
3604}
3605
3606impl Default for FilePicker {
3607    fn default() -> Self {
3608        Self::new()
3609    }
3610}
3611
3612impl FilePicker {
3613    /// Creates a new file picker field.
3614    pub fn new() -> Self {
3615        Self {
3616            id: next_id(),
3617            key: String::new(),
3618            selected_path: None,
3619            title: String::new(),
3620            description: String::new(),
3621            current_directory: ".".to_string(),
3622            allowed_types: Vec::new(),
3623            show_hidden: false,
3624            show_size: false,
3625            show_permissions: false,
3626            file_allowed: true,
3627            dir_allowed: false,
3628            picking: false,
3629            focused: false,
3630            error: None,
3631            validate: None,
3632            width: 80,
3633            height: 10,
3634            theme: None,
3635            keymap: FilePickerKeyMap::default(),
3636            _position: FieldPosition::default(),
3637            files: Vec::new(),
3638            selected_index: 0,
3639            offset: 0,
3640        }
3641    }
3642
3643    /// Sets the field key.
3644    pub fn key(mut self, key: impl Into<String>) -> Self {
3645        self.key = key.into();
3646        self
3647    }
3648
3649    /// Sets the title.
3650    pub fn title(mut self, title: impl Into<String>) -> Self {
3651        self.title = title.into();
3652        self
3653    }
3654
3655    /// Sets the description.
3656    pub fn description(mut self, description: impl Into<String>) -> Self {
3657        self.description = description.into();
3658        self
3659    }
3660
3661    /// Sets the starting directory.
3662    pub fn current_directory(mut self, dir: impl Into<String>) -> Self {
3663        self.current_directory = dir.into();
3664        self
3665    }
3666
3667    /// Sets the allowed file types (extensions).
3668    pub fn allowed_types(mut self, types: Vec<String>) -> Self {
3669        self.allowed_types = types;
3670        self
3671    }
3672
3673    /// Sets whether to show hidden files.
3674    pub fn show_hidden(mut self, show: bool) -> Self {
3675        self.show_hidden = show;
3676        self
3677    }
3678
3679    /// Sets whether to show file sizes.
3680    pub fn show_size(mut self, show: bool) -> Self {
3681        self.show_size = show;
3682        self
3683    }
3684
3685    /// Sets whether to show file permissions.
3686    pub fn show_permissions(mut self, show: bool) -> Self {
3687        self.show_permissions = show;
3688        self
3689    }
3690
3691    /// Sets whether files can be selected.
3692    pub fn file_allowed(mut self, allowed: bool) -> Self {
3693        self.file_allowed = allowed;
3694        self
3695    }
3696
3697    /// Sets whether directories can be selected.
3698    pub fn dir_allowed(mut self, allowed: bool) -> Self {
3699        self.dir_allowed = allowed;
3700        self
3701    }
3702
3703    /// Sets the validation function.
3704    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3705        self.validate = Some(validate);
3706        self
3707    }
3708
3709    /// Sets the visible height (number of entries shown).
3710    pub fn height_entries(mut self, height: usize) -> Self {
3711        self.height = height;
3712        self
3713    }
3714
3715    fn get_theme(&self) -> Theme {
3716        self.theme.clone().unwrap_or_else(theme_charm)
3717    }
3718
3719    fn active_styles(&self) -> FieldStyles {
3720        let theme = self.get_theme();
3721        if self.focused {
3722            theme.focused
3723        } else {
3724            theme.blurred
3725        }
3726    }
3727
3728    fn run_validation(&mut self) {
3729        if let Some(validate) = self.validate
3730            && let Some(ref path) = self.selected_path
3731        {
3732            self.error = validate(path);
3733        }
3734    }
3735
3736    fn read_directory(&mut self) {
3737        self.files.clear();
3738        self.selected_index = 0;
3739        self.offset = 0;
3740
3741        // Add parent directory entry if not at root
3742        if self.current_directory != "/" {
3743            self.files.push(FileEntry {
3744                name: "..".to_string(),
3745                path: "..".to_string(),
3746                is_dir: true,
3747                size: 0,
3748                mode: String::new(),
3749            });
3750        }
3751
3752        // Read directory contents
3753        if let Ok(entries) = std::fs::read_dir(&self.current_directory) {
3754            let mut entries: Vec<_> = entries
3755                .filter_map(|e| e.ok())
3756                .filter_map(|entry| {
3757                    let name = entry.file_name().to_string_lossy().to_string();
3758
3759                    // Skip hidden files unless show_hidden is true
3760                    if !self.show_hidden && name.starts_with('.') {
3761                        return None;
3762                    }
3763
3764                    let metadata = entry.metadata().ok()?;
3765                    let is_dir = metadata.is_dir();
3766                    let size = metadata.len();
3767
3768                    // Filter by allowed types (only for files)
3769                    if !is_dir && !self.allowed_types.is_empty() {
3770                        let matches = self.allowed_types.iter().any(|ext| {
3771                            name.ends_with(ext)
3772                                || name.ends_with(&ext.trim_start_matches('.').to_string())
3773                        });
3774                        if !matches {
3775                            return None;
3776                        }
3777                    }
3778
3779                    let path = entry.path().to_string_lossy().to_string();
3780
3781                    Some(FileEntry {
3782                        name,
3783                        path,
3784                        is_dir,
3785                        size,
3786                        mode: String::new(),
3787                    })
3788                })
3789                .collect();
3790
3791            // Sort: directories first, then alphabetically
3792            entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
3793                (true, false) => std::cmp::Ordering::Less,
3794                (false, true) => std::cmp::Ordering::Greater,
3795                _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
3796            });
3797
3798            self.files.extend(entries);
3799        }
3800    }
3801
3802    fn is_selectable(&self, entry: &FileEntry) -> bool {
3803        if entry.is_dir {
3804            self.dir_allowed
3805        } else {
3806            self.file_allowed
3807        }
3808    }
3809
3810    fn format_size(size: u64) -> String {
3811        const KB: u64 = 1024;
3812        const MB: u64 = KB * 1024;
3813        const GB: u64 = MB * 1024;
3814
3815        if size >= GB {
3816            format!("{:.1}G", size as f64 / GB as f64)
3817        } else if size >= MB {
3818            format!("{:.1}M", size as f64 / MB as f64)
3819        } else if size >= KB {
3820            format!("{:.1}K", size as f64 / KB as f64)
3821        } else {
3822            format!("{}B", size)
3823        }
3824    }
3825
3826    /// Gets the currently selected path.
3827    pub fn get_selected_path(&self) -> Option<&str> {
3828        self.selected_path.as_deref()
3829    }
3830
3831    /// Returns the field ID.
3832    pub fn id(&self) -> usize {
3833        self.id
3834    }
3835}
3836
3837impl Field for FilePicker {
3838    fn get_key(&self) -> &str {
3839        &self.key
3840    }
3841
3842    fn get_value(&self) -> Box<dyn Any> {
3843        Box::new(self.selected_path.clone().unwrap_or_default())
3844    }
3845
3846    fn error(&self) -> Option<&str> {
3847        self.error.as_deref()
3848    }
3849
3850    fn init(&mut self) -> Option<Cmd> {
3851        self.read_directory();
3852        None
3853    }
3854
3855    fn update(&mut self, msg: &Message) -> Option<Cmd> {
3856        if !self.focused {
3857            return None;
3858        }
3859
3860        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3861            self.error = None;
3862
3863            // Check for prev
3864            if binding_matches(&self.keymap.prev, key_msg) {
3865                self.picking = false;
3866                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3867            }
3868
3869            // Check for next (tab)
3870            if binding_matches(&self.keymap.next, key_msg) {
3871                self.picking = false;
3872                self.run_validation();
3873                if self.error.is_some() {
3874                    return None;
3875                }
3876                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3877            }
3878
3879            // Handle close/escape
3880            if binding_matches(&self.keymap.close, key_msg) {
3881                if self.picking {
3882                    self.picking = false;
3883                } else {
3884                    return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3885                }
3886                return None;
3887            }
3888
3889            // Handle open (enter picker mode or select)
3890            if binding_matches(&self.keymap.open, key_msg) {
3891                if !self.picking {
3892                    self.picking = true;
3893                    self.read_directory();
3894                    return None;
3895                }
3896
3897                // In picking mode, open directory or select file
3898                if let Some(entry) = self.files.get(self.selected_index) {
3899                    if entry.name == ".." {
3900                        // Go to parent directory
3901                        if let Some(parent) = std::path::Path::new(&self.current_directory).parent()
3902                        {
3903                            self.current_directory = parent.to_string_lossy().to_string();
3904                            if self.current_directory.is_empty() {
3905                                self.current_directory = "/".to_string();
3906                            }
3907                            self.read_directory();
3908                        }
3909                    } else if entry.is_dir {
3910                        // Enter directory
3911                        self.current_directory = entry.path.clone();
3912                        self.read_directory();
3913                    } else if self.is_selectable(entry) {
3914                        // Select file
3915                        self.selected_path = Some(entry.path.clone());
3916                        self.picking = false;
3917                        self.run_validation();
3918                        if self.error.is_some() {
3919                            return None;
3920                        }
3921                        return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3922                    }
3923                }
3924                return None;
3925            }
3926
3927            // Handle back (go to parent directory)
3928            if self.picking && binding_matches(&self.keymap.back, key_msg) {
3929                if let Some(parent) = std::path::Path::new(&self.current_directory).parent() {
3930                    self.current_directory = parent.to_string_lossy().to_string();
3931                    if self.current_directory.is_empty() {
3932                        self.current_directory = "/".to_string();
3933                    }
3934                    self.read_directory();
3935                }
3936                return None;
3937            }
3938
3939            // Navigation in picker mode
3940            if self.picking {
3941                if binding_matches(&self.keymap.up, key_msg) {
3942                    if self.selected_index > 0 {
3943                        self.selected_index -= 1;
3944                        if self.selected_index < self.offset {
3945                            self.offset = self.selected_index;
3946                        }
3947                    }
3948                } else if binding_matches(&self.keymap.down, key_msg) {
3949                    if !self.files.is_empty()
3950                        && self.selected_index < self.files.len().saturating_sub(1)
3951                    {
3952                        self.selected_index += 1;
3953                        if self.height > 0 && self.selected_index >= self.offset + self.height {
3954                            self.offset = self
3955                                .selected_index
3956                                .saturating_sub(self.height.saturating_sub(1));
3957                        }
3958                    }
3959                } else if binding_matches(&self.keymap.goto_top, key_msg) {
3960                    self.selected_index = 0;
3961                    self.offset = 0;
3962                } else if binding_matches(&self.keymap.goto_bottom, key_msg)
3963                    && !self.files.is_empty()
3964                {
3965                    self.selected_index = self.files.len().saturating_sub(1);
3966                    self.offset = self
3967                        .selected_index
3968                        .saturating_sub(self.height.saturating_sub(1));
3969                }
3970            }
3971        }
3972
3973        None
3974    }
3975
3976    fn view(&self) -> String {
3977        let styles = self.active_styles();
3978        let mut output = String::new();
3979
3980        // Title
3981        if !self.title.is_empty() {
3982            output.push_str(&styles.title.render(&self.title));
3983            if self.error.is_some() {
3984                output.push_str(&styles.error_indicator.render(""));
3985            }
3986            output.push('\n');
3987        }
3988
3989        // Description
3990        if !self.description.is_empty() {
3991            output.push_str(&styles.description.render(&self.description));
3992            output.push('\n');
3993        }
3994
3995        if self.picking {
3996            // Show file list
3997            let visible: Vec<_> = self
3998                .files
3999                .iter()
4000                .skip(self.offset)
4001                .take(self.height)
4002                .collect();
4003
4004            for (i, entry) in visible.iter().enumerate() {
4005                let idx = self.offset + i;
4006                let is_selected = idx == self.selected_index;
4007                let is_selectable = self.is_selectable(entry);
4008
4009                // Cursor
4010                if is_selected {
4011                    output.push_str(&styles.select_selector.render(""));
4012                } else {
4013                    output.push_str("  ");
4014                }
4015
4016                // Entry display
4017                let mut entry_str = String::new();
4018
4019                // Directory/file indicator
4020                if entry.is_dir {
4021                    entry_str.push_str("📁 ");
4022                } else {
4023                    entry_str.push_str("   ");
4024                }
4025
4026                entry_str.push_str(&entry.name);
4027
4028                // Size
4029                if self.show_size && !entry.is_dir {
4030                    entry_str.push_str(&format!(" ({})", Self::format_size(entry.size)));
4031                }
4032
4033                if is_selected && is_selectable {
4034                    output.push_str(&styles.selected_option.render(&entry_str));
4035                } else if !is_selectable && !entry.is_dir && entry.name != ".." {
4036                    output.push_str(&styles.text_input.placeholder.render(&entry_str));
4037                } else {
4038                    output.push_str(&styles.option.render(&entry_str));
4039                }
4040
4041                output.push('\n');
4042            }
4043
4044            // Remove trailing newline
4045            if !visible.is_empty() {
4046                output.pop();
4047            }
4048
4049            // Show current directory
4050            output.push('\n');
4051            output.push_str(
4052                &styles
4053                    .description
4054                    .render(&format!("📂 {}", self.current_directory)),
4055            );
4056        } else {
4057            // Show selected file or placeholder
4058            if let Some(ref path) = self.selected_path {
4059                output.push_str(&styles.selected_option.render(path));
4060            } else {
4061                output.push_str(
4062                    &styles
4063                        .text_input
4064                        .placeholder
4065                        .render("No file selected. Press Enter to browse."),
4066                );
4067            }
4068        }
4069
4070        // Error message
4071        if let Some(ref err) = self.error {
4072            output.push('\n');
4073            output.push_str(&styles.error_message.render(err));
4074        }
4075
4076        styles
4077            .base
4078            .width(self.width.try_into().unwrap_or(u16::MAX))
4079            .render(&output)
4080    }
4081
4082    fn focus(&mut self) -> Option<Cmd> {
4083        self.focused = true;
4084        None
4085    }
4086
4087    fn blur(&mut self) -> Option<Cmd> {
4088        self.focused = false;
4089        self.picking = false;
4090        self.run_validation();
4091        None
4092    }
4093
4094    fn key_binds(&self) -> Vec<Binding> {
4095        if self.picking {
4096            vec![
4097                self.keymap.up.clone(),
4098                self.keymap.down.clone(),
4099                self.keymap.open.clone(),
4100                self.keymap.back.clone(),
4101                self.keymap.close.clone(),
4102            ]
4103        } else {
4104            vec![
4105                self.keymap.open.clone(),
4106                self.keymap.prev.clone(),
4107                self.keymap.next.clone(),
4108            ]
4109        }
4110    }
4111
4112    fn with_theme(&mut self, theme: &Theme) {
4113        if self.theme.is_none() {
4114            self.theme = Some(theme.clone());
4115        }
4116    }
4117
4118    fn with_keymap(&mut self, keymap: &KeyMap) {
4119        self.keymap = keymap.file_picker.clone();
4120    }
4121
4122    fn with_width(&mut self, width: usize) {
4123        self.width = width;
4124    }
4125
4126    fn with_height(&mut self, height: usize) {
4127        self.height = height;
4128    }
4129
4130    fn with_position(&mut self, position: FieldPosition) {
4131        self._position = position;
4132    }
4133}
4134
4135// -----------------------------------------------------------------------------
4136// Group
4137// -----------------------------------------------------------------------------
4138
4139/// A group of fields displayed together.
4140pub struct Group {
4141    fields: Vec<Box<dyn Field>>,
4142    current: usize,
4143    title: String,
4144    description: String,
4145    width: usize,
4146    #[allow(dead_code)]
4147    height: usize,
4148    theme: Option<Theme>,
4149    keymap: Option<KeyMap>,
4150    hide: Option<Box<dyn Fn() -> bool + Send + Sync>>,
4151}
4152
4153impl Default for Group {
4154    fn default() -> Self {
4155        Self::new(Vec::new())
4156    }
4157}
4158
4159impl Group {
4160    /// Creates a new group with the given fields.
4161    pub fn new(fields: Vec<Box<dyn Field>>) -> Self {
4162        Self {
4163            fields,
4164            current: 0,
4165            title: String::new(),
4166            description: String::new(),
4167            width: 80,
4168            height: 0,
4169            theme: None,
4170            keymap: None,
4171            hide: None,
4172        }
4173    }
4174
4175    /// Sets the group title.
4176    pub fn title(mut self, title: impl Into<String>) -> Self {
4177        self.title = title.into();
4178        self
4179    }
4180
4181    /// Sets the group description.
4182    pub fn description(mut self, description: impl Into<String>) -> Self {
4183        self.description = description.into();
4184        self
4185    }
4186
4187    /// Sets whether the group should be hidden.
4188    pub fn hide(mut self, hide: bool) -> Self {
4189        self.hide = Some(Box::new(move || hide));
4190        self
4191    }
4192
4193    /// Sets a function to determine if the group should be hidden.
4194    pub fn hide_func<F: Fn() -> bool + Send + Sync + 'static>(mut self, f: F) -> Self {
4195        self.hide = Some(Box::new(f));
4196        self
4197    }
4198
4199    /// Returns whether this group should be hidden.
4200    pub fn is_hidden(&self) -> bool {
4201        self.hide.as_ref().map(|f| f()).unwrap_or(false)
4202    }
4203
4204    /// Returns the current field index.
4205    pub fn current(&self) -> usize {
4206        self.current
4207    }
4208
4209    /// Returns the number of fields.
4210    pub fn len(&self) -> usize {
4211        self.fields.len()
4212    }
4213
4214    /// Returns whether the group has no fields.
4215    pub fn is_empty(&self) -> bool {
4216        self.fields.is_empty()
4217    }
4218
4219    /// Returns a reference to the current field.
4220    pub fn current_field(&self) -> Option<&dyn Field> {
4221        self.fields.get(self.current).map(|f| f.as_ref())
4222    }
4223
4224    /// Returns a mutable reference to the current field.
4225    pub fn current_field_mut(&mut self) -> Option<&mut Box<dyn Field>> {
4226        self.fields.get_mut(self.current)
4227    }
4228
4229    /// Collects all field errors.
4230    pub fn errors(&self) -> Vec<&str> {
4231        self.fields.iter().filter_map(|f| f.error()).collect()
4232    }
4233
4234    fn get_theme(&self) -> Theme {
4235        self.theme.clone().unwrap_or_else(theme_charm)
4236    }
4237
4238    /// Returns the header portion of the group (title and description).
4239    ///
4240    /// This is useful for custom layouts that want to render the header
4241    /// separately from the content.
4242    pub fn header(&self) -> String {
4243        let theme = self.get_theme();
4244        let mut output = String::new();
4245
4246        if !self.title.is_empty() {
4247            output.push_str(&theme.group.title.render(&self.title));
4248            output.push('\n');
4249        }
4250
4251        if !self.description.is_empty() {
4252            output.push_str(&theme.group.description.render(&self.description));
4253            output.push('\n');
4254        }
4255
4256        output
4257    }
4258
4259    /// Returns the content portion of the group (just the fields).
4260    ///
4261    /// This is useful for custom layouts that want to render the content
4262    /// separately from the header and footer.
4263    pub fn content(&self) -> String {
4264        let theme = self.get_theme();
4265        let mut output = String::new();
4266
4267        for (i, field) in self.fields.iter().enumerate() {
4268            output.push_str(&field.view());
4269            if i < self.fields.len() - 1 {
4270                output.push_str(&theme.field_separator.render(""));
4271            }
4272        }
4273
4274        output
4275    }
4276
4277    /// Returns the footer portion of the group (currently errors).
4278    ///
4279    /// This is useful for custom layouts that want to render the footer
4280    /// separately from the content.
4281    pub fn footer(&self) -> String {
4282        let theme = self.get_theme();
4283        let errors = self.errors();
4284
4285        if errors.is_empty() {
4286            return String::new();
4287        }
4288
4289        let error_text = errors.join(", ");
4290        theme.focused.error_message.render(&error_text)
4291    }
4292}
4293
4294impl Model for Group {
4295    fn init(&self) -> Option<Cmd> {
4296        None
4297    }
4298
4299    fn update(&mut self, msg: Message) -> Option<Cmd> {
4300        // Handle navigation messages
4301        if msg.is::<NextFieldMsg>() {
4302            if self.current < self.fields.len().saturating_sub(1) {
4303                if let Some(field) = self.fields.get_mut(self.current) {
4304                    field.blur();
4305                }
4306                self.current += 1;
4307                if let Some(field) = self.fields.get_mut(self.current) {
4308                    return field.focus();
4309                }
4310            } else {
4311                return Some(Cmd::new(|| Message::new(NextGroupMsg)));
4312            }
4313        } else if msg.is::<PrevFieldMsg>() {
4314            if self.current > 0 {
4315                if let Some(field) = self.fields.get_mut(self.current) {
4316                    field.blur();
4317                }
4318                self.current -= 1;
4319                if let Some(field) = self.fields.get_mut(self.current) {
4320                    return field.focus();
4321                }
4322            } else {
4323                return Some(Cmd::new(|| Message::new(PrevGroupMsg)));
4324            }
4325        }
4326
4327        // Forward to current field
4328        if let Some(field) = self.fields.get_mut(self.current) {
4329            return field.update(&msg);
4330        }
4331
4332        None
4333    }
4334
4335    fn view(&self) -> String {
4336        let theme = self.get_theme();
4337        let mut output = String::new();
4338
4339        // Title
4340        if !self.title.is_empty() {
4341            output.push_str(&theme.group.title.render(&self.title));
4342            output.push('\n');
4343        }
4344
4345        // Description
4346        if !self.description.is_empty() {
4347            output.push_str(&theme.group.description.render(&self.description));
4348            output.push('\n');
4349        }
4350
4351        // Fields
4352        for (i, field) in self.fields.iter().enumerate() {
4353            output.push_str(&field.view());
4354            if i < self.fields.len() - 1 {
4355                output.push_str(&theme.field_separator.render(""));
4356            }
4357        }
4358
4359        theme
4360            .group
4361            .base
4362            .width(self.width.try_into().unwrap_or(u16::MAX))
4363            .render(&output)
4364    }
4365}
4366
4367// -----------------------------------------------------------------------------
4368// Layout
4369// -----------------------------------------------------------------------------
4370
4371/// Layout determines how groups are arranged within a form.
4372///
4373/// The layout system controls how multiple groups are displayed:
4374/// - `Default`: Shows one group at a time (traditional wizard-style)
4375/// - `Stack`: Shows all groups stacked vertically
4376/// - `Columns`: Distributes groups across columns
4377/// - `Grid`: Arranges groups in a grid pattern
4378pub trait Layout: Send + Sync {
4379    /// Renders the form using this layout.
4380    fn view(&self, form: &Form) -> String;
4381
4382    /// Returns the width allocated to a specific group.
4383    fn group_width(&self, form: &Form, group_index: usize, total_width: usize) -> usize;
4384}
4385
4386/// Default layout - shows one group at a time.
4387///
4388/// This is the traditional wizard-style form layout where only the
4389/// current group is visible and users navigate between groups.
4390#[derive(Debug, Clone, Default)]
4391pub struct LayoutDefault;
4392
4393impl Layout for LayoutDefault {
4394    fn view(&self, form: &Form) -> String {
4395        if let Some(group) = form.groups.get(form.current_group) {
4396            if group.is_hidden() {
4397                return String::new();
4398            }
4399            form.theme
4400                .form
4401                .base
4402                .clone()
4403                .width(form.width.try_into().unwrap_or(u16::MAX))
4404                .render(&group.view())
4405        } else {
4406            String::new()
4407        }
4408    }
4409
4410    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4411        form.width
4412    }
4413}
4414
4415/// Stack layout - shows all groups stacked vertically.
4416///
4417/// All groups are rendered one after another, with the form's
4418/// field separator between them.
4419#[derive(Debug, Clone, Default)]
4420pub struct LayoutStack;
4421
4422impl Layout for LayoutStack {
4423    fn view(&self, form: &Form) -> String {
4424        let mut output = String::new();
4425        let visible_groups: Vec<_> = form
4426            .groups
4427            .iter()
4428            .enumerate()
4429            .filter(|(_, g)| !g.is_hidden())
4430            .collect();
4431
4432        for (i, (_, group)) in visible_groups.iter().enumerate() {
4433            output.push_str(&group.view());
4434            if i < visible_groups.len() - 1 {
4435                output.push('\n');
4436            }
4437        }
4438
4439        form.theme
4440            .form
4441            .base
4442            .clone()
4443            .width(form.width.try_into().unwrap_or(u16::MAX))
4444            .render(&output)
4445    }
4446
4447    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4448        form.width
4449    }
4450}
4451
4452/// Columns layout - distributes groups across columns.
4453///
4454/// Groups are arranged in columns, wrapping to the next row when needed.
4455#[derive(Debug, Clone)]
4456pub struct LayoutColumns {
4457    columns: usize,
4458}
4459
4460impl LayoutColumns {
4461    /// Creates a new columns layout with the specified number of columns.
4462    pub fn new(columns: usize) -> Self {
4463        Self {
4464            columns: columns.max(1),
4465        }
4466    }
4467}
4468
4469impl Default for LayoutColumns {
4470    fn default() -> Self {
4471        Self::new(2)
4472    }
4473}
4474
4475impl Layout for LayoutColumns {
4476    fn view(&self, form: &Form) -> String {
4477        let visible_groups: Vec<_> = form
4478            .groups
4479            .iter()
4480            .enumerate()
4481            .filter(|(_, g)| !g.is_hidden())
4482            .collect();
4483
4484        if visible_groups.is_empty() {
4485            return String::new();
4486        }
4487
4488        let column_width = form.width / self.columns;
4489        let mut rows: Vec<String> = Vec::new();
4490
4491        for chunk in visible_groups.chunks(self.columns) {
4492            let mut row_parts: Vec<String> = Vec::new();
4493            for (_, group) in chunk {
4494                // Render each group with column width
4495                let group_view = group.view();
4496                // Pad to column width
4497                let lines: Vec<&str> = group_view.lines().collect();
4498                let padded: Vec<String> = lines
4499                    .iter()
4500                    .map(|line| {
4501                        let visual_width = lipgloss::width(line);
4502                        if visual_width < column_width {
4503                            format!("{}{}", line, " ".repeat(column_width - visual_width))
4504                        } else {
4505                            line.to_string()
4506                        }
4507                    })
4508                    .collect();
4509                row_parts.push(padded.join("\n"));
4510            }
4511
4512            // Join columns horizontally using lipgloss
4513            if row_parts.len() == 1 {
4514                // Keep render path panic-free even if future refactors alter row_parts population.
4515                rows.push(row_parts.into_iter().next().unwrap_or_default());
4516            } else {
4517                let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4518                rows.push(lipgloss::join_horizontal(
4519                    lipgloss::Position::Top,
4520                    &row_refs,
4521                ));
4522            }
4523        }
4524
4525        let output = rows.join("\n");
4526        form.theme
4527            .form
4528            .base
4529            .clone()
4530            .width(form.width.try_into().unwrap_or(u16::MAX))
4531            .render(&output)
4532    }
4533
4534    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4535        form.width / self.columns
4536    }
4537}
4538
4539/// Grid layout - arranges groups in a fixed grid pattern.
4540///
4541/// Groups are arranged in a grid with the specified number of rows and columns.
4542/// If there are more groups than cells, extra groups are not displayed.
4543#[derive(Debug, Clone)]
4544pub struct LayoutGrid {
4545    rows: usize,
4546    columns: usize,
4547}
4548
4549impl LayoutGrid {
4550    /// Creates a new grid layout with the specified dimensions.
4551    pub fn new(rows: usize, columns: usize) -> Self {
4552        Self {
4553            rows: rows.max(1),
4554            columns: columns.max(1),
4555        }
4556    }
4557}
4558
4559impl Default for LayoutGrid {
4560    fn default() -> Self {
4561        Self::new(2, 2)
4562    }
4563}
4564
4565impl Layout for LayoutGrid {
4566    fn view(&self, form: &Form) -> String {
4567        let visible_groups: Vec<_> = form
4568            .groups
4569            .iter()
4570            .enumerate()
4571            .filter(|(_, g)| !g.is_hidden())
4572            .collect();
4573
4574        if visible_groups.is_empty() {
4575            return String::new();
4576        }
4577
4578        let column_width = form.width / self.columns;
4579        let max_cells = self.rows * self.columns;
4580        let mut rows: Vec<String> = Vec::new();
4581
4582        for row_idx in 0..self.rows {
4583            let start = row_idx * self.columns;
4584            if start >= visible_groups.len() || start >= max_cells {
4585                break;
4586            }
4587            let end = (start + self.columns)
4588                .min(visible_groups.len())
4589                .min(max_cells);
4590
4591            let mut row_parts: Vec<String> = Vec::new();
4592            for (_, group) in &visible_groups[start..end] {
4593                let group_view = group.view();
4594                let lines: Vec<&str> = group_view.lines().collect();
4595                let padded: Vec<String> = lines
4596                    .iter()
4597                    .map(|line| {
4598                        let visual_width = lipgloss::width(line);
4599                        if visual_width < column_width {
4600                            format!("{}{}", line, " ".repeat(column_width - visual_width))
4601                        } else {
4602                            line.to_string()
4603                        }
4604                    })
4605                    .collect();
4606                row_parts.push(padded.join("\n"));
4607            }
4608
4609            if row_parts.len() == 1 {
4610                // Keep render path panic-free even if future refactors alter row_parts population.
4611                rows.push(row_parts.into_iter().next().unwrap_or_default());
4612            } else {
4613                let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4614                rows.push(lipgloss::join_horizontal(
4615                    lipgloss::Position::Top,
4616                    &row_refs,
4617                ));
4618            }
4619        }
4620
4621        let output = rows.join("\n");
4622        form.theme
4623            .form
4624            .base
4625            .clone()
4626            .width(form.width.try_into().unwrap_or(u16::MAX))
4627            .render(&output)
4628    }
4629
4630    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4631        form.width / self.columns
4632    }
4633}
4634
4635// -----------------------------------------------------------------------------
4636// Form
4637// -----------------------------------------------------------------------------
4638
4639/// A form containing multiple groups of fields.
4640pub struct Form {
4641    groups: Vec<Group>,
4642    current_group: usize,
4643    state: FormState,
4644    width: usize,
4645    theme: Theme,
4646    keymap: KeyMap,
4647    layout: Box<dyn Layout>,
4648    show_help: bool,
4649    show_errors: bool,
4650    accessible: bool,
4651}
4652
4653impl Default for Form {
4654    fn default() -> Self {
4655        Self::new(Vec::new())
4656    }
4657}
4658
4659impl Form {
4660    /// Creates a new form with the given groups.
4661    pub fn new(groups: Vec<Group>) -> Self {
4662        Self {
4663            groups,
4664            current_group: 0,
4665            state: FormState::Normal,
4666            width: 80,
4667            theme: theme_charm(),
4668            keymap: KeyMap::default(),
4669            layout: Box::new(LayoutDefault),
4670            show_help: true,
4671            show_errors: true,
4672            accessible: false,
4673        }
4674    }
4675
4676    /// Sets the form width.
4677    pub fn width(mut self, width: usize) -> Self {
4678        self.width = width;
4679        self
4680    }
4681
4682    /// Sets the theme.
4683    pub fn theme(mut self, theme: Theme) -> Self {
4684        self.theme = theme;
4685        self
4686    }
4687
4688    /// Sets the keymap.
4689    pub fn keymap(mut self, keymap: KeyMap) -> Self {
4690        self.keymap = keymap;
4691        self
4692    }
4693
4694    /// Sets the layout for the form.
4695    ///
4696    /// # Example
4697    ///
4698    /// ```rust,ignore
4699    /// use huh::{Form, Group, LayoutColumns};
4700    ///
4701    /// let form = Form::new(vec![group1, group2, group3])
4702    ///     .layout(LayoutColumns::new(2));
4703    /// ```
4704    pub fn layout<L: Layout + 'static>(mut self, layout: L) -> Self {
4705        self.layout = Box::new(layout);
4706        self
4707    }
4708
4709    /// Sets whether to show help at the bottom of the form.
4710    pub fn show_help(mut self, show: bool) -> Self {
4711        self.show_help = show;
4712        self
4713    }
4714
4715    /// Sets whether to show validation errors.
4716    pub fn show_errors(mut self, show: bool) -> Self {
4717        self.show_errors = show;
4718        self
4719    }
4720
4721    /// Enables or disables accessible mode.
4722    ///
4723    /// When accessible mode is enabled, the form renders in a more
4724    /// screen-reader-friendly format with simpler styling and clearer
4725    /// field labels. This mode prioritizes accessibility over visual
4726    /// aesthetics.
4727    ///
4728    /// # Example
4729    ///
4730    /// ```rust,ignore
4731    /// use huh::Form;
4732    ///
4733    /// let form = Form::new(groups)
4734    ///     .with_accessible(true);
4735    /// ```
4736    pub fn with_accessible(mut self, accessible: bool) -> Self {
4737        self.accessible = accessible;
4738        self
4739    }
4740
4741    /// Returns whether accessible mode is enabled.
4742    pub fn is_accessible(&self) -> bool {
4743        self.accessible
4744    }
4745
4746    /// Returns the form state.
4747    pub fn state(&self) -> FormState {
4748        self.state
4749    }
4750
4751    /// Returns the current group index.
4752    pub fn current_group(&self) -> usize {
4753        self.current_group
4754    }
4755
4756    /// Returns the number of groups.
4757    pub fn len(&self) -> usize {
4758        self.groups.len()
4759    }
4760
4761    /// Returns whether the form has no groups.
4762    pub fn is_empty(&self) -> bool {
4763        self.groups.is_empty()
4764    }
4765
4766    /// Initializes all fields with theme and keymap.
4767    fn init_fields(&mut self) {
4768        for group in &mut self.groups {
4769            group.theme = Some(self.theme.clone());
4770            group.keymap = Some(self.keymap.clone());
4771            group.width = self.width;
4772            for field in &mut group.fields {
4773                field.with_theme(&self.theme);
4774                field.with_keymap(&self.keymap);
4775                field.with_width(self.width);
4776            }
4777        }
4778    }
4779
4780    fn next_group(&mut self) -> Option<Cmd> {
4781        // Skip hidden groups
4782        loop {
4783            if self.current_group >= self.groups.len().saturating_sub(1) {
4784                self.state = FormState::Completed;
4785                return Some(bubbletea::quit());
4786            }
4787            self.current_group += 1;
4788            if !self.groups[self.current_group].is_hidden() {
4789                break;
4790            }
4791        }
4792        // Focus first field of new group
4793        if let Some(group) = self.groups.get_mut(self.current_group) {
4794            group.current = 0;
4795            if let Some(field) = group.fields.get_mut(0) {
4796                return field.focus();
4797            }
4798        }
4799        None
4800    }
4801
4802    fn prev_group(&mut self) -> Option<Cmd> {
4803        // Skip hidden groups
4804        loop {
4805            if self.current_group == 0 {
4806                return None;
4807            }
4808            self.current_group -= 1;
4809            if !self.groups[self.current_group].is_hidden() {
4810                break;
4811            }
4812        }
4813        // Focus last field of new group
4814        if let Some(group) = self.groups.get_mut(self.current_group) {
4815            group.current = group.fields.len().saturating_sub(1);
4816            if let Some(field) = group.fields.last_mut() {
4817                return field.focus();
4818            }
4819        }
4820        None
4821    }
4822
4823    /// Returns the value of a field by key.
4824    pub fn get_value(&self, key: &str) -> Option<Box<dyn Any>> {
4825        for group in &self.groups {
4826            for field in &group.fields {
4827                if field.get_key() == key {
4828                    return Some(field.get_value());
4829                }
4830            }
4831        }
4832        None
4833    }
4834
4835    /// Returns the string value of a field by key.
4836    pub fn get_string(&self, key: &str) -> Option<String> {
4837        self.get_value(key)
4838            .and_then(|v| v.downcast::<String>().ok())
4839            .map(|v| *v)
4840    }
4841
4842    /// Returns the boolean value of a field by key.
4843    pub fn get_bool(&self, key: &str) -> Option<bool> {
4844        self.get_value(key)
4845            .and_then(|v| v.downcast::<bool>().ok())
4846            .map(|v| *v)
4847    }
4848
4849    /// Collects all validation errors from all groups.
4850    pub fn all_errors(&self) -> Vec<String> {
4851        self.groups
4852            .iter()
4853            .flat_map(|g| g.errors())
4854            .map(|s| s.to_string())
4855            .collect()
4856    }
4857
4858    /// Returns a view of all validation errors.
4859    fn errors_view(&self) -> String {
4860        let errors = self.all_errors();
4861        if errors.is_empty() {
4862            return String::new();
4863        }
4864
4865        let error_text = errors.join(", ");
4866        self.theme.focused.error_message.render(&error_text)
4867    }
4868
4869    /// Returns a help view with available keybindings.
4870    fn help_view(&self) -> String {
4871        // Build help text from keybindings
4872        let mut help_parts = Vec::new();
4873
4874        // Get current field's keybindings if available
4875        if let Some(group) = self.groups.get(self.current_group)
4876            && let Some(field) = group.fields.get(group.current)
4877        {
4878            for binding in field.key_binds() {
4879                let help = binding.get_help();
4880                if binding.enabled() && !help.desc.is_empty() {
4881                    let keys = binding.get_keys();
4882                    if !keys.is_empty() {
4883                        help_parts.push(format!("{}: {}", keys.join("/"), help.desc));
4884                    }
4885                }
4886            }
4887        }
4888
4889        // Add form-level keybindings
4890        let quit_help = self.keymap.quit.get_help();
4891        if self.keymap.quit.enabled() && !quit_help.desc.is_empty() {
4892            let keys = self.keymap.quit.get_keys();
4893            if !keys.is_empty() {
4894                help_parts.push(format!("{}: {}", keys.join("/"), quit_help.desc));
4895            }
4896        }
4897
4898        if help_parts.is_empty() {
4899            return String::new();
4900        }
4901
4902        // Style the help text
4903        let help_text = help_parts.join(" • ");
4904        self.theme.help.render(&help_text)
4905    }
4906
4907    /// Returns the width allocated to a specific group based on the current layout.
4908    pub fn group_width(&self, group_index: usize) -> usize {
4909        self.layout.group_width(self, group_index, self.width)
4910    }
4911}
4912
4913impl Model for Form {
4914    fn init(&self) -> Option<Cmd> {
4915        None
4916    }
4917
4918    fn update(&mut self, msg: Message) -> Option<Cmd> {
4919        // Initialize fields on first update
4920        if self.state == FormState::Normal && self.current_group == 0 {
4921            self.init_fields();
4922            // Focus first field
4923            if let Some(group) = self.groups.get_mut(0)
4924                && let Some(field) = group.fields.get_mut(0)
4925            {
4926                field.focus();
4927            }
4928        }
4929
4930        // Handle quit
4931        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>()
4932            && binding_matches(&self.keymap.quit, key_msg)
4933        {
4934            self.state = FormState::Aborted;
4935            return Some(bubbletea::quit());
4936        }
4937
4938        // Handle group navigation
4939        if msg.is::<NextGroupMsg>() {
4940            return self.next_group();
4941        } else if msg.is::<PrevGroupMsg>() {
4942            return self.prev_group();
4943        }
4944
4945        // Forward to current group
4946        if let Some(group) = self.groups.get_mut(self.current_group) {
4947            return group.update(msg);
4948        }
4949
4950        None
4951    }
4952
4953    fn view(&self) -> String {
4954        let mut output = self.layout.view(self);
4955
4956        // Add help footer if enabled
4957        if self.show_help {
4958            let help_text = self.help_view();
4959            if !help_text.is_empty() {
4960                output.push('\n');
4961                output.push_str(&help_text);
4962            }
4963        }
4964
4965        // Add errors if enabled
4966        if self.show_errors {
4967            let errors = self.errors_view();
4968            if !errors.is_empty() {
4969                output.push('\n');
4970                output.push_str(&errors);
4971            }
4972        }
4973
4974        output
4975    }
4976}
4977
4978// -----------------------------------------------------------------------------
4979// Validators
4980// -----------------------------------------------------------------------------
4981
4982/// Creates a validator that checks if the input is not empty.
4983///
4984/// **Note**: Due to Rust function pointer limitations, the `_field_name` parameter
4985/// is not used. It exists only for API compatibility. To create validators with
4986/// custom error messages, use a closure directly:
4987///
4988/// ```rust,ignore
4989/// let validator = |s: &str| {
4990///     if s.trim().is_empty() {
4991///         Some("username is required".to_string())
4992///     } else {
4993///         None
4994///     }
4995/// };
4996/// ```
4997///
4998/// # Example
4999/// ```
5000/// use huh::validate_required;
5001/// let validator = validate_required("any");
5002/// assert!(validator("").is_some()); // Error: "field is required"
5003/// assert!(validator("John").is_none()); // Valid
5004/// ```
5005pub fn validate_required(_field_name: &'static str) -> fn(&str) -> Option<String> {
5006    |s| {
5007        if s.trim().is_empty() {
5008            Some("field is required".to_string())
5009        } else {
5010            None
5011        }
5012    }
5013}
5014
5015/// Creates a required validator for the "name" field.
5016pub fn validate_required_name() -> fn(&str) -> Option<String> {
5017    |s| {
5018        if s.trim().is_empty() {
5019            Some("name is required".to_string())
5020        } else {
5021            None
5022        }
5023    }
5024}
5025
5026/// Creates a min length validator for password fields.
5027/// Note: Due to Rust's function pointer limitations, this returns a closure
5028/// that can be converted to a function pointer.
5029pub fn validate_min_length_8() -> fn(&str) -> Option<String> {
5030    |s| {
5031        if s.chars().count() < 8 {
5032            Some("password must be at least 8 characters".to_string())
5033        } else {
5034            None
5035        }
5036    }
5037}
5038
5039/// Creates a validator for email format.
5040/// Uses a simple regex pattern to validate email addresses.
5041pub fn validate_email() -> fn(&str) -> Option<String> {
5042    |s| {
5043        if s.is_empty() {
5044            return Some("email is required".to_string());
5045        }
5046        // Simple email validation: must have @ with something before and after
5047        // and a dot after the @
5048        let parts: Vec<&str> = s.split('@').collect();
5049        if parts.len() != 2 {
5050            return Some("invalid email address".to_string());
5051        }
5052        let (local, domain) = (parts[0], parts[1]);
5053        if local.is_empty() || domain.is_empty() || !domain.contains('.') {
5054            return Some("invalid email address".to_string());
5055        }
5056        // Check domain has something after the dot
5057        let domain_parts: Vec<&str> = domain.split('.').collect();
5058        if domain_parts.len() < 2 || domain_parts.iter().any(|p| p.is_empty()) {
5059            return Some("invalid email address".to_string());
5060        }
5061        None
5062    }
5063}
5064
5065// -----------------------------------------------------------------------------
5066// Tests
5067// -----------------------------------------------------------------------------
5068
5069#[cfg(test)]
5070mod tests {
5071    use super::*;
5072
5073    #[test]
5074    fn test_form_error_display() {
5075        let err = FormError::UserAborted;
5076        assert_eq!(format!("{}", err), "user aborted");
5077
5078        let err = FormError::Validation("invalid input".to_string());
5079        assert_eq!(format!("{}", err), "validation error: invalid input");
5080    }
5081
5082    #[test]
5083    fn test_form_state_default() {
5084        let state = FormState::default();
5085        assert_eq!(state, FormState::Normal);
5086    }
5087
5088    #[test]
5089    fn test_select_option() {
5090        let opt = SelectOption::new("Red", "red".to_string());
5091        assert_eq!(opt.key, "Red");
5092        assert_eq!(opt.value, "red");
5093        assert!(!opt.selected);
5094
5095        let opt = opt.selected(true);
5096        assert!(opt.selected);
5097    }
5098
5099    #[test]
5100    fn test_new_options() {
5101        let opts = new_options(["apple", "banana", "cherry"]);
5102        assert_eq!(opts.len(), 3);
5103        assert_eq!(opts[0].key, "apple");
5104        assert_eq!(opts[0].value, "apple");
5105    }
5106
5107    #[test]
5108    fn test_input_builder() {
5109        let input = Input::new()
5110            .key("name")
5111            .title("Name")
5112            .description("Enter your name")
5113            .placeholder("John Doe")
5114            .value("Jane");
5115
5116        assert_eq!(input.get_key(), "name");
5117        assert_eq!(input.get_string_value(), "Jane");
5118    }
5119
5120    #[test]
5121    fn test_confirm_builder() {
5122        let confirm = Confirm::new()
5123            .key("agree")
5124            .title("Terms")
5125            .affirmative("I Agree")
5126            .negative("I Disagree")
5127            .value(true);
5128
5129        assert_eq!(confirm.get_key(), "agree");
5130        assert!(confirm.get_bool_value());
5131    }
5132
5133    #[test]
5134    fn test_note_builder() {
5135        let note = Note::new()
5136            .key("info")
5137            .title("Information")
5138            .description("This is an informational note.");
5139
5140        assert_eq!(note.get_key(), "info");
5141    }
5142
5143    #[test]
5144    fn test_text_builder() {
5145        let text = Text::new()
5146            .key("bio")
5147            .title("Biography")
5148            .description("Tell us about yourself")
5149            .placeholder("Enter your bio...")
5150            .lines(10)
5151            .value("Hello world");
5152
5153        assert_eq!(text.get_key(), "bio");
5154        assert_eq!(text.get_string_value(), "Hello world");
5155    }
5156
5157    #[test]
5158    fn test_text_char_limit() {
5159        let text = Text::new().char_limit(50).show_line_numbers(true);
5160
5161        assert_eq!(text.char_limit, 50);
5162        assert!(text.show_line_numbers);
5163    }
5164
5165    #[test]
5166    fn test_filepicker_builder() {
5167        let picker = FilePicker::new()
5168            .key("config_file")
5169            .title("Select Configuration")
5170            .description("Choose a file")
5171            .current_directory("/tmp")
5172            .show_hidden(true)
5173            .file_allowed(true)
5174            .dir_allowed(false);
5175
5176        assert_eq!(picker.get_key(), "config_file");
5177        assert!(picker.file_allowed);
5178        assert!(!picker.dir_allowed);
5179        assert!(picker.show_hidden);
5180    }
5181
5182    #[test]
5183    fn test_filepicker_allowed_types() {
5184        let picker = FilePicker::new()
5185            .allowed_types(vec![".toml".to_string(), ".json".to_string()])
5186            .show_size(true);
5187
5188        assert_eq!(picker.allowed_types.len(), 2);
5189        assert!(picker.show_size);
5190    }
5191
5192    #[test]
5193    fn test_select_builder() {
5194        let select: Select<String> =
5195            Select::new()
5196                .key("color")
5197                .title("Favorite Color")
5198                .options(vec![
5199                    SelectOption::new("Red", "red".to_string()),
5200                    SelectOption::new("Green", "green".to_string()).selected(true),
5201                    SelectOption::new("Blue", "blue".to_string()),
5202                ]);
5203
5204        assert_eq!(select.get_key(), "color");
5205        assert_eq!(select.get_selected_value(), Some(&"green".to_string()));
5206    }
5207
5208    #[test]
5209    fn test_theme_base() {
5210        let theme = theme_base();
5211        assert!(!theme.focused.title.value().is_empty() || theme.focused.title.value().is_empty());
5212    }
5213
5214    #[test]
5215    fn test_theme_charm() {
5216        let theme = theme_charm();
5217        // Just verify it doesn't panic
5218        let _ = theme.focused.title.render("Test");
5219    }
5220
5221    #[test]
5222    fn test_theme_dracula() {
5223        let theme = theme_dracula();
5224        let _ = theme.focused.title.render("Test");
5225    }
5226
5227    #[test]
5228    fn test_theme_base16() {
5229        let theme = theme_base16();
5230        let _ = theme.focused.title.render("Test");
5231    }
5232
5233    #[test]
5234    fn test_theme_catppuccin() {
5235        let theme = theme_catppuccin();
5236        // Verify it doesn't panic and has expected Catppuccin colors
5237        let _ = theme.focused.title.render("Test");
5238        let _ = theme.focused.selected_option.render("Selected");
5239        let _ = theme.focused.focused_button.render("OK");
5240        let _ = theme.blurred.title.render("Blurred");
5241    }
5242
5243    #[test]
5244    fn test_keymap_default() {
5245        let keymap = KeyMap::default();
5246        assert!(keymap.quit.enabled());
5247        assert!(keymap.input.next.enabled());
5248    }
5249
5250    #[test]
5251    fn test_field_position() {
5252        let pos = FieldPosition {
5253            group: 0,
5254            field: 0,
5255            first_field: 0,
5256            last_field: 2,
5257            group_count: 2,
5258            first_group: 0,
5259            last_group: 1,
5260        };
5261        assert!(pos.is_first());
5262        assert!(!pos.is_last());
5263    }
5264
5265    #[test]
5266    fn test_group_basic() {
5267        let group = Group::new(vec![
5268            Box::new(Input::new().key("name").title("Name")),
5269            Box::new(Input::new().key("email").title("Email")),
5270        ]);
5271
5272        assert_eq!(group.len(), 2);
5273        assert!(!group.is_empty());
5274        assert_eq!(group.current(), 0);
5275    }
5276
5277    #[test]
5278    fn test_group_hide() {
5279        let group = Group::new(Vec::new()).hide(true);
5280        assert!(group.is_hidden());
5281
5282        let group = Group::new(Vec::new()).hide(false);
5283        assert!(!group.is_hidden());
5284    }
5285
5286    #[test]
5287    fn test_form_basic() {
5288        let form = Form::new(vec![Group::new(vec![Box::new(Input::new().key("name"))])]);
5289
5290        assert_eq!(form.len(), 1);
5291        assert!(!form.is_empty());
5292        assert_eq!(form.state(), FormState::Normal);
5293    }
5294
5295    #[test]
5296    fn test_input_echo_mode() {
5297        let input = Input::new().password(true);
5298        assert_eq!(input.echo_mode, EchoMode::Password);
5299
5300        let input = Input::new().echo_mode(EchoMode::None);
5301        assert_eq!(input.echo_mode, EchoMode::None);
5302    }
5303
5304    #[test]
5305    fn test_key_to_string() {
5306        let key = KeyMsg {
5307            key_type: KeyType::Enter,
5308            runes: vec![],
5309            alt: false,
5310            paste: false,
5311        };
5312        assert_eq!(key.to_string(), "enter");
5313
5314        let key = KeyMsg {
5315            key_type: KeyType::Runes,
5316            runes: vec!['a'],
5317            alt: false,
5318            paste: false,
5319        };
5320        assert_eq!(key.to_string(), "a");
5321
5322        let key = KeyMsg {
5323            key_type: KeyType::CtrlC,
5324            runes: vec![],
5325            alt: false,
5326            paste: false,
5327        };
5328        assert_eq!(key.to_string(), "ctrl+c");
5329    }
5330
5331    #[test]
5332    fn test_input_view() {
5333        let input = Input::new()
5334            .title("Name")
5335            .placeholder("Enter name")
5336            .value("");
5337
5338        let view = input.view();
5339        assert!(view.contains("Name"));
5340    }
5341
5342    #[test]
5343    fn test_confirm_view() {
5344        let confirm = Confirm::new()
5345            .title("Proceed?")
5346            .affirmative("Yes")
5347            .negative("No");
5348
5349        let view = confirm.view();
5350        assert!(view.contains("Proceed"));
5351    }
5352
5353    #[test]
5354    fn test_select_view() {
5355        let select: Select<String> = Select::new().title("Choose").options(vec![
5356            SelectOption::new("A", "a".to_string()),
5357            SelectOption::new("B", "b".to_string()),
5358        ]);
5359
5360        let view = select.view();
5361        assert!(view.contains("Choose"));
5362    }
5363
5364    #[test]
5365    fn test_note_view() {
5366        let note = Note::new().title("Info").description("Some information");
5367
5368        let view = note.view();
5369        assert!(view.contains("Info"));
5370    }
5371
5372    #[test]
5373    fn test_multiselect_view() {
5374        let multi: MultiSelect<String> = MultiSelect::new().title("Select items").options(vec![
5375            SelectOption::new("A", "a".to_string()),
5376            SelectOption::new("B", "b".to_string()).selected(true),
5377            SelectOption::new("C", "c".to_string()),
5378        ]);
5379
5380        let view = multi.view();
5381        assert!(view.contains("Select items"));
5382    }
5383
5384    #[test]
5385    fn test_multiselect_initial_selection() {
5386        let multi: MultiSelect<String> = MultiSelect::new().options(vec![
5387            SelectOption::new("A", "a".to_string()),
5388            SelectOption::new("B", "b".to_string()).selected(true),
5389            SelectOption::new("C", "c".to_string()).selected(true),
5390        ]);
5391
5392        let selected = multi.get_selected_values();
5393        assert_eq!(selected.len(), 2);
5394        assert!(selected.contains(&&"b".to_string()));
5395        assert!(selected.contains(&&"c".to_string()));
5396    }
5397
5398    #[test]
5399    fn test_multiselect_limit() {
5400        let mut multi: MultiSelect<String> = MultiSelect::new().limit(2).options(vec![
5401            SelectOption::new("A", "a".to_string()),
5402            SelectOption::new("B", "b".to_string()),
5403            SelectOption::new("C", "c".to_string()),
5404        ]);
5405
5406        // Focus the field so it processes updates
5407        multi.focus();
5408
5409        // Toggle first option (select)
5410        let toggle_msg = Message::new(KeyMsg {
5411            key_type: KeyType::Runes,
5412            runes: vec![' '],
5413            alt: false,
5414            paste: false,
5415        });
5416        multi.update(&toggle_msg);
5417        assert_eq!(multi.get_selected_values().len(), 1);
5418
5419        // Move down and toggle second
5420        let down_msg = Message::new(KeyMsg {
5421            key_type: KeyType::Down,
5422            runes: vec![],
5423            alt: false,
5424            paste: false,
5425        });
5426        multi.update(&down_msg);
5427        multi.update(&toggle_msg);
5428        assert_eq!(multi.get_selected_values().len(), 2);
5429
5430        // Move down and try to toggle third (should be blocked by limit)
5431        multi.update(&down_msg);
5432        multi.update(&toggle_msg);
5433        // Should still be 2 due to limit
5434        assert_eq!(multi.get_selected_values().len(), 2);
5435    }
5436
5437    #[test]
5438    fn test_input_unicode_cursor_handling() {
5439        // Test that cursor position works correctly with multi-byte UTF-8 characters
5440        let mut input = Input::new().value("café"); // 'é' is 2 bytes in UTF-8
5441
5442        // Focus to enable updates
5443        input.focus();
5444
5445        // cursor_pos should be at end (4 characters, not 5 bytes)
5446        assert_eq!(input.cursor_pos, 4);
5447        assert_eq!(input.value.chars().count(), 4);
5448
5449        // Press End to ensure cursor is at end
5450        let end_msg = Message::new(KeyMsg {
5451            key_type: KeyType::End,
5452            runes: vec![],
5453            alt: false,
5454            paste: false,
5455        });
5456        input.update(&end_msg);
5457        assert_eq!(input.cursor_pos, 4);
5458
5459        // Press Left to move before 'é'
5460        let left_msg = Message::new(KeyMsg {
5461            key_type: KeyType::Left,
5462            runes: vec![],
5463            alt: false,
5464            paste: false,
5465        });
5466        input.update(&left_msg);
5467        assert_eq!(input.cursor_pos, 3);
5468
5469        // Press Backspace to delete 'f'
5470        let backspace_msg = Message::new(KeyMsg {
5471            key_type: KeyType::Backspace,
5472            runes: vec![],
5473            alt: false,
5474            paste: false,
5475        });
5476        input.update(&backspace_msg);
5477        assert_eq!(input.get_string_value(), "caé");
5478        assert_eq!(input.cursor_pos, 2);
5479
5480        // Insert a character at current position
5481        let insert_msg = Message::new(KeyMsg {
5482            key_type: KeyType::Runes,
5483            runes: vec!['ñ'], // Another multi-byte char
5484            alt: false,
5485            paste: false,
5486        });
5487        input.update(&insert_msg);
5488        assert_eq!(input.get_string_value(), "cañé");
5489        assert_eq!(input.cursor_pos, 3);
5490
5491        // Delete character at cursor (should delete 'é')
5492        let delete_msg = Message::new(KeyMsg {
5493            key_type: KeyType::Delete,
5494            runes: vec![],
5495            alt: false,
5496            paste: false,
5497        });
5498        input.update(&delete_msg);
5499        assert_eq!(input.get_string_value(), "cañ");
5500
5501        // Home should move to position 0
5502        let home_msg = Message::new(KeyMsg {
5503            key_type: KeyType::Home,
5504            runes: vec![],
5505            alt: false,
5506            paste: false,
5507        });
5508        input.update(&home_msg);
5509        assert_eq!(input.cursor_pos, 0);
5510    }
5511
5512    #[test]
5513    fn test_input_char_limit_with_unicode() {
5514        // Test that char_limit counts characters, not bytes
5515        let mut input = Input::new().char_limit(5);
5516        input.focus();
5517
5518        // Insert 5 multi-byte characters (each would be 2+ bytes in UTF-8)
5519        let chars = ['日', '本', '語', '文', '字']; // 5 Japanese characters
5520        for c in chars {
5521            let msg = Message::new(KeyMsg {
5522                key_type: KeyType::Runes,
5523                runes: vec![c],
5524                alt: false,
5525                paste: false,
5526            });
5527            input.update(&msg);
5528        }
5529
5530        // Should have exactly 5 characters (not blocked due to byte count)
5531        assert_eq!(input.value.chars().count(), 5);
5532        assert_eq!(input.get_string_value(), "日本語文字");
5533
5534        // Try to add one more - should be blocked by char limit
5535        let msg = Message::new(KeyMsg {
5536            key_type: KeyType::Runes,
5537            runes: vec!['!'],
5538            alt: false,
5539            paste: false,
5540        });
5541        input.update(&msg);
5542
5543        // Should still be 5 characters
5544        assert_eq!(input.value.chars().count(), 5);
5545    }
5546
5547    #[test]
5548    fn test_layout_default() {
5549        let _layout = LayoutDefault;
5550        // Just ensure it compiles and can be created
5551    }
5552
5553    #[test]
5554    fn test_layout_stack() {
5555        let _layout = LayoutStack;
5556        // Just ensure it compiles and can be created
5557    }
5558
5559    #[test]
5560    fn test_layout_columns() {
5561        let layout = LayoutColumns::new(3);
5562        assert_eq!(layout.columns, 3);
5563
5564        // Minimum of 1 column
5565        let layout = LayoutColumns::new(0);
5566        assert_eq!(layout.columns, 1);
5567    }
5568
5569    #[test]
5570    fn test_layout_grid() {
5571        let layout = LayoutGrid::new(2, 3);
5572        assert_eq!(layout.rows, 2);
5573        assert_eq!(layout.columns, 3);
5574
5575        // Minimum of 1x1
5576        let layout = LayoutGrid::new(0, 0);
5577        assert_eq!(layout.rows, 1);
5578        assert_eq!(layout.columns, 1);
5579    }
5580
5581    #[test]
5582    fn test_layout_columns_view_single_empty_group_no_panic() {
5583        let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutColumns::new(1));
5584        let _ = form.view();
5585    }
5586
5587    #[test]
5588    fn test_layout_grid_view_single_empty_group_no_panic() {
5589        let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutGrid::new(1, 1));
5590        let _ = form.view();
5591    }
5592
5593    #[test]
5594    fn test_form_with_layout() {
5595        let form = Form::new(vec![
5596            Group::new(vec![Box::new(Input::new().key("a"))]),
5597            Group::new(vec![Box::new(Input::new().key("b"))]),
5598        ])
5599        .layout(LayoutColumns::new(2));
5600
5601        // Form should have the layout set
5602        assert_eq!(form.len(), 2);
5603    }
5604
5605    #[test]
5606    fn test_form_show_help() {
5607        let form = Form::new(Vec::new()).show_help(false).show_errors(false);
5608
5609        // Just verify the builder works
5610        assert!(!form.show_help);
5611        assert!(!form.show_errors);
5612    }
5613
5614    #[test]
5615    fn test_group_header_footer_content() {
5616        let group = Group::new(vec![Box::new(Input::new().key("test").title("Test Input"))])
5617            .title("Group Title")
5618            .description("Group Description");
5619
5620        let header = group.header();
5621        assert!(header.contains("Group Title"));
5622        assert!(header.contains("Group Description"));
5623
5624        let content = group.content();
5625        assert!(content.contains("Test Input"));
5626
5627        let footer = group.footer();
5628        // No errors, so footer should be empty
5629        assert_eq!(footer, "");
5630    }
5631
5632    #[test]
5633    fn test_form_all_errors() {
5634        let form = Form::new(vec![Group::new(Vec::new())]);
5635
5636        // No errors initially
5637        let errors = form.all_errors();
5638        assert_eq!(errors, Vec::<String>::new());
5639    }
5640
5641    // Word transformation tests matching Go bubbles/textarea behavior
5642
5643    #[test]
5644    fn test_text_transpose_left() {
5645        let mut text = Text::new().value("hello");
5646        text.cursor_row = 0;
5647        text.cursor_col = 5; // At end of "hello"
5648
5649        text.transpose_left();
5650
5651        // At end, moves cursor back first, then swaps 'l' and 'o'
5652        assert_eq!(text.get_string_value(), "helol");
5653        assert_eq!(text.cursor_col, 5); // Cursor stays at end
5654    }
5655
5656    #[test]
5657    fn test_text_transpose_left_middle() {
5658        let mut text = Text::new().value("hello");
5659        text.cursor_row = 0;
5660        text.cursor_col = 2; // After 'e', before 'l'
5661
5662        text.transpose_left();
5663
5664        // Swaps 'e' (pos 1) and 'l' (pos 2)
5665        assert_eq!(text.get_string_value(), "hlelo");
5666        assert_eq!(text.cursor_col, 3); // Cursor moves right
5667    }
5668
5669    #[test]
5670    fn test_text_transpose_left_at_beginning() {
5671        let mut text = Text::new().value("hello");
5672        text.cursor_row = 0;
5673        text.cursor_col = 0; // At beginning
5674
5675        text.transpose_left();
5676
5677        // No-op when at beginning
5678        assert_eq!(text.get_string_value(), "hello");
5679        assert_eq!(text.cursor_col, 0);
5680    }
5681
5682    #[test]
5683    fn test_text_uppercase_right() {
5684        let mut text = Text::new().value("hello world");
5685        text.cursor_row = 0;
5686        text.cursor_col = 0; // At beginning
5687
5688        text.uppercase_right();
5689
5690        assert_eq!(text.get_string_value(), "HELLO world");
5691        assert_eq!(text.cursor_col, 5); // Cursor moves past the word
5692    }
5693
5694    #[test]
5695    fn test_text_uppercase_right_with_spaces() {
5696        let mut text = Text::new().value("  hello world");
5697        text.cursor_row = 0;
5698        text.cursor_col = 0; // Before spaces
5699
5700        text.uppercase_right();
5701
5702        // Skips spaces, then uppercases "hello"
5703        assert_eq!(text.get_string_value(), "  HELLO world");
5704        assert_eq!(text.cursor_col, 7); // Cursor after "HELLO"
5705    }
5706
5707    #[test]
5708    fn test_text_lowercase_right() {
5709        let mut text = Text::new().value("HELLO WORLD");
5710        text.cursor_row = 0;
5711        text.cursor_col = 0;
5712
5713        text.lowercase_right();
5714
5715        assert_eq!(text.get_string_value(), "hello WORLD");
5716        assert_eq!(text.cursor_col, 5);
5717    }
5718
5719    #[test]
5720    fn test_text_capitalize_right() {
5721        let mut text = Text::new().value("hello world");
5722        text.cursor_row = 0;
5723        text.cursor_col = 0;
5724
5725        text.capitalize_right();
5726
5727        // Only first char is uppercased
5728        assert_eq!(text.get_string_value(), "Hello world");
5729        assert_eq!(text.cursor_col, 5);
5730    }
5731
5732    #[test]
5733    fn test_text_capitalize_right_already_upper() {
5734        let mut text = Text::new().value("HELLO WORLD");
5735        text.cursor_row = 0;
5736        text.cursor_col = 0;
5737
5738        text.capitalize_right();
5739
5740        // First char stays upper, rest unchanged (capitalize doesn't lowercase)
5741        assert_eq!(text.get_string_value(), "HELLO WORLD");
5742        assert_eq!(text.cursor_col, 5);
5743    }
5744
5745    #[test]
5746    fn test_text_word_ops_multiline() {
5747        let mut text = Text::new().value("hello\nworld");
5748        text.cursor_row = 1;
5749        text.cursor_col = 0;
5750
5751        text.uppercase_right();
5752
5753        // Only operates on current line
5754        assert_eq!(text.get_string_value(), "hello\nWORLD");
5755        assert_eq!(text.cursor_row, 1);
5756        assert_eq!(text.cursor_col, 5);
5757    }
5758
5759    #[test]
5760    fn test_text_transpose_multiline() {
5761        let mut text = Text::new().value("ab\ncd");
5762        text.cursor_row = 1;
5763        text.cursor_col = 2; // At end of "cd"
5764
5765        text.transpose_left();
5766
5767        // Swaps 'c' and 'd' on second line
5768        assert_eq!(text.get_string_value(), "ab\ndc");
5769    }
5770
5771    #[test]
5772    fn test_text_word_ops_unicode() {
5773        let mut text = Text::new().value("café résumé");
5774        text.cursor_row = 0;
5775        text.cursor_col = 0;
5776
5777        text.uppercase_right();
5778
5779        assert_eq!(text.get_string_value(), "CAFÉ résumé");
5780        assert_eq!(text.cursor_col, 4);
5781    }
5782
5783    #[test]
5784    fn test_text_keymap_has_word_ops() {
5785        let keymap = TextKeyMap::default();
5786
5787        // Verify the new bindings exist and are enabled
5788        assert!(keymap.uppercase_word_forward.enabled());
5789        assert!(keymap.lowercase_word_forward.enabled());
5790        assert!(keymap.capitalize_word_forward.enabled());
5791        assert!(keymap.transpose_character_backward.enabled());
5792
5793        // Verify expected key bindings
5794        assert!(
5795            keymap
5796                .uppercase_word_forward
5797                .get_keys()
5798                .contains(&"alt+u".to_string())
5799        );
5800        assert!(
5801            keymap
5802                .lowercase_word_forward
5803                .get_keys()
5804                .contains(&"alt+l".to_string())
5805        );
5806        assert!(
5807            keymap
5808                .capitalize_word_forward
5809                .get_keys()
5810                .contains(&"alt+c".to_string())
5811        );
5812        assert!(
5813            keymap
5814                .transpose_character_backward
5815                .get_keys()
5816                .contains(&"ctrl+t".to_string())
5817        );
5818    }
5819
5820    // -------------------------------------------------------------------------
5821    // Paste handling tests (bd-3jg2)
5822    // -------------------------------------------------------------------------
5823
5824    mod paste_tests {
5825        use super::*;
5826        use bubbletea::{KeyMsg, Message};
5827
5828        /// Helper to create a paste KeyMsg from a string
5829        fn paste_msg(s: &str) -> Message {
5830            let key = KeyMsg::from_runes(s.chars().collect()).with_paste();
5831            Message::new(key)
5832        }
5833
5834        /// Helper to create a regular typing KeyMsg from a string
5835        fn type_msg(s: &str) -> Message {
5836            let key = KeyMsg::from_runes(s.chars().collect());
5837            Message::new(key)
5838        }
5839
5840        #[test]
5841        fn test_input_paste_collapses_newlines() {
5842            let mut input = Input::new().key("query");
5843            input.focused = true;
5844
5845            // Paste multi-line content
5846            let msg = paste_msg("hello\nworld\nfoo");
5847            input.update(&msg);
5848
5849            // Newlines should be collapsed to spaces
5850            assert_eq!(input.get_string_value(), "hello world foo");
5851        }
5852
5853        #[test]
5854        fn test_input_paste_collapses_tabs() {
5855            let mut input = Input::new().key("query");
5856            input.focused = true;
5857
5858            // Paste content with tabs
5859            let msg = paste_msg("col1\tcol2\tcol3");
5860            input.update(&msg);
5861
5862            // Tabs should be collapsed to spaces
5863            assert_eq!(input.get_string_value(), "col1 col2 col3");
5864        }
5865
5866        #[test]
5867        fn test_input_paste_collapses_multiple_spaces() {
5868            let mut input = Input::new().key("query");
5869            input.focused = true;
5870
5871            // Paste content with multiple consecutive newlines/spaces
5872            let msg = paste_msg("hello\n\n\nworld");
5873            input.update(&msg);
5874
5875            // Multiple consecutive whitespace should collapse to single space
5876            assert_eq!(input.get_string_value(), "hello world");
5877        }
5878
5879        #[test]
5880        fn test_input_paste_respects_char_limit() {
5881            let mut input = Input::new().key("query").char_limit(10);
5882            input.focused = true;
5883
5884            // Paste more than char_limit
5885            let msg = paste_msg("hello world this is too long");
5886            input.update(&msg);
5887
5888            // Should be truncated at limit
5889            assert_eq!(input.get_string_value().chars().count(), 10);
5890            assert_eq!(input.get_string_value(), "hello worl");
5891        }
5892
5893        #[test]
5894        fn test_input_paste_partial_fill() {
5895            let mut input = Input::new().key("query").char_limit(15);
5896            input.focused = true;
5897
5898            // Type some chars first
5899            let msg = type_msg("hi ");
5900            input.update(&msg);
5901
5902            // Paste more - should fill up to limit
5903            let msg = paste_msg("hello world this is long");
5904            input.update(&msg);
5905
5906            assert_eq!(input.get_string_value().chars().count(), 15);
5907            assert_eq!(input.get_string_value(), "hi hello world ");
5908        }
5909
5910        #[test]
5911        fn test_input_paste_cursor_position() {
5912            let mut input = Input::new().key("query");
5913            input.focused = true;
5914
5915            // Paste some content
5916            let msg = paste_msg("hello world");
5917            input.update(&msg);
5918
5919            // Cursor should be at end
5920            assert_eq!(input.cursor_pos, 11);
5921        }
5922
5923        #[test]
5924        fn test_input_regular_typing_not_affected() {
5925            let mut input = Input::new().key("query");
5926            input.focused = true;
5927
5928            // Regular typing of newline (shouldn't happen but test defensive behavior)
5929            let msg = type_msg("hello\nworld");
5930            input.update(&msg);
5931
5932            // Regular typing should preserve newlines (they're just chars)
5933            assert_eq!(input.get_string_value(), "hello\nworld");
5934        }
5935
5936        #[test]
5937        fn test_text_paste_preserves_newlines() {
5938            let mut text = Text::new().key("bio");
5939            text.focused = true;
5940
5941            // Paste multi-line content
5942            let msg = paste_msg("line 1\nline 2\nline 3");
5943            text.update(&msg);
5944
5945            // Newlines should be preserved in Text field
5946            assert_eq!(text.get_string_value(), "line 1\nline 2\nline 3");
5947        }
5948
5949        #[test]
5950        fn test_text_paste_updates_cursor_row() {
5951            let mut text = Text::new().key("bio");
5952            text.focused = true;
5953
5954            // Paste multi-line content
5955            let msg = paste_msg("line 1\nline 2\nline 3");
5956            text.update(&msg);
5957
5958            // Cursor should be on line 3 (0-indexed = 2)
5959            assert_eq!(text.cursor_row, 2);
5960            // Cursor col should be at end of "line 3"
5961            assert_eq!(text.cursor_col, 6);
5962        }
5963
5964        #[test]
5965        fn test_text_paste_respects_char_limit() {
5966            let mut text = Text::new().key("bio").char_limit(20);
5967            text.focused = true;
5968
5969            // Paste content exceeding limit
5970            let msg = paste_msg("line 1\nline 2\nline 3 is very long");
5971            text.update(&msg);
5972
5973            // Should truncate at 20 chars
5974            assert_eq!(text.get_string_value().chars().count(), 20);
5975        }
5976
5977        #[test]
5978        fn test_input_paste_unicode() {
5979            let mut input = Input::new().key("query");
5980            input.focused = true;
5981
5982            // Paste unicode content with newlines
5983            let msg = paste_msg("héllo\nwörld\n日本語");
5984            input.update(&msg);
5985
5986            // Should collapse newlines, preserve unicode
5987            assert_eq!(input.get_string_value(), "héllo wörld 日本語");
5988        }
5989
5990        #[test]
5991        fn test_text_paste_unicode_cursor() {
5992            let mut text = Text::new().key("bio");
5993            text.focused = true;
5994
5995            // Paste unicode content
5996            let msg = paste_msg("日本語\n한국어");
5997            text.update(&msg);
5998
5999            assert_eq!(text.get_string_value(), "日本語\n한국어");
6000            assert_eq!(text.cursor_row, 1);
6001            assert_eq!(text.cursor_col, 3); // 3 Korean chars
6002        }
6003
6004        #[test]
6005        fn test_input_paste_empty() {
6006            let mut input = Input::new().key("query");
6007            input.focused = true;
6008
6009            // Paste empty content
6010            let msg = paste_msg("");
6011            input.update(&msg);
6012
6013            assert_eq!(input.get_string_value(), "");
6014            assert_eq!(input.cursor_pos, 0);
6015        }
6016
6017        #[test]
6018        fn test_input_paste_crlf_handling() {
6019            let mut input = Input::new().key("query");
6020            input.focused = true;
6021
6022            // Paste Windows-style line endings
6023            let msg = paste_msg("hello\r\nworld");
6024            input.update(&msg);
6025
6026            // Both \r and \n should become spaces, then collapse
6027            assert_eq!(input.get_string_value(), "hello world");
6028        }
6029
6030        #[test]
6031        fn test_input_not_focused_ignores_paste() {
6032            let mut input = Input::new().key("query");
6033            input.focused = false;
6034
6035            let msg = paste_msg("hello world");
6036            input.update(&msg);
6037
6038            // Should ignore paste when not focused
6039            assert_eq!(input.get_string_value(), "");
6040        }
6041
6042        #[test]
6043        fn test_text_not_focused_ignores_paste() {
6044            let mut text = Text::new().key("bio");
6045            text.focused = false;
6046
6047            let msg = paste_msg("hello\nworld");
6048            text.update(&msg);
6049
6050            // Should ignore paste when not focused
6051            assert_eq!(text.get_string_value(), "");
6052        }
6053
6054        #[test]
6055        fn test_input_large_paste() {
6056            let mut input = Input::new().key("query");
6057            input.focused = true;
6058
6059            // Paste a large amount of text (simulating a real paste operation)
6060            let large_text: String = (0..1000).map(|i| format!("word{} ", i)).collect();
6061            let msg = paste_msg(&large_text);
6062            input.update(&msg);
6063
6064            // Should handle large paste without panic
6065            assert!(input.get_string_value().chars().count() > 100);
6066        }
6067
6068        #[test]
6069        fn test_text_large_paste() {
6070            let mut text = Text::new().key("bio");
6071            text.focused = true;
6072
6073            // Paste large multi-line text
6074            let large_text: String = (0..100).map(|i| format!("line {}\n", i)).collect();
6075            let msg = paste_msg(&large_text);
6076            text.update(&msg);
6077
6078            // Should handle large paste without panic
6079            assert!(text.get_string_value().contains('\n'));
6080            assert_eq!(text.cursor_row, 100); // 100 newlines = row 100
6081        }
6082    }
6083
6084    #[test]
6085    fn test_multiselect_filter_cursor_stays_on_item() {
6086        // Test that cursor stays on the same item when filter narrows results
6087        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6088            SelectOption::new("Apple", "apple".to_string()),
6089            SelectOption::new("Banana", "banana".to_string()),
6090            SelectOption::new("Cherry", "cherry".to_string()),
6091            SelectOption::new("Blueberry", "blueberry".to_string()),
6092        ]);
6093
6094        multi.focus();
6095
6096        // Move cursor to "Banana" (index 1)
6097        let down_msg = Message::new(KeyMsg {
6098            key_type: KeyType::Down,
6099            runes: vec![],
6100            alt: false,
6101            paste: false,
6102        });
6103        multi.update(&down_msg);
6104        assert_eq!(multi.cursor, 1);
6105
6106        // Apply filter "b" - should match Banana, Blueberry
6107        multi.update_filter("b".to_string());
6108
6109        // Cursor should still be on "Banana" which is now at filtered index 0
6110        let filtered = multi.filtered_options();
6111        assert_eq!(filtered.len(), 2);
6112        assert_eq!(filtered[multi.cursor].1.key, "Banana");
6113    }
6114
6115    #[test]
6116    fn test_multiselect_filter_cursor_clamps() {
6117        // Test that cursor clamps when the current item is filtered out
6118        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6119            SelectOption::new("Apple", "apple".to_string()),
6120            SelectOption::new("Banana", "banana".to_string()),
6121            SelectOption::new("Cherry", "cherry".to_string()),
6122        ]);
6123
6124        multi.focus();
6125
6126        // Move cursor to "Cherry" (index 2)
6127        let down_msg = Message::new(KeyMsg {
6128            key_type: KeyType::Down,
6129            runes: vec![],
6130            alt: false,
6131            paste: false,
6132        });
6133        multi.update(&down_msg);
6134        multi.update(&down_msg);
6135        assert_eq!(multi.cursor, 2);
6136
6137        // Apply filter "a" - should match Apple, Banana (not Cherry)
6138        multi.update_filter("a".to_string());
6139
6140        // Cursor should be clamped to valid range (max index 1)
6141        let filtered = multi.filtered_options();
6142        assert_eq!(filtered.len(), 2);
6143        assert!(multi.cursor < filtered.len());
6144    }
6145
6146    #[test]
6147    fn test_multiselect_filter_then_toggle() {
6148        // Test that toggling selection works correctly with filtered results
6149        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6150            SelectOption::new("Apple", "apple".to_string()),
6151            SelectOption::new("Banana", "banana".to_string()),
6152            SelectOption::new("Cherry", "cherry".to_string()),
6153            SelectOption::new("Blueberry", "blueberry".to_string()),
6154        ]);
6155
6156        multi.focus();
6157
6158        // Apply filter "b" - should match Banana, Blueberry
6159        multi.update_filter("b".to_string());
6160
6161        // Move to second item (Blueberry)
6162        let down_msg = Message::new(KeyMsg {
6163            key_type: KeyType::Down,
6164            runes: vec![],
6165            alt: false,
6166            paste: false,
6167        });
6168        multi.update(&down_msg);
6169
6170        // Toggle selection
6171        let toggle_msg = Message::new(KeyMsg {
6172            key_type: KeyType::Runes,
6173            runes: vec![' '],
6174            alt: false,
6175            paste: false,
6176        });
6177        multi.update(&toggle_msg);
6178
6179        // Verify Blueberry (original index 3) is selected
6180        let selected = multi.get_selected_values();
6181        assert_eq!(selected.len(), 1);
6182        assert!(selected.contains(&&"blueberry".to_string()));
6183
6184        // Clear filter and verify selection persists
6185        multi.update_filter(String::new());
6186        let selected = multi.get_selected_values();
6187        assert_eq!(selected.len(), 1);
6188        assert!(selected.contains(&&"blueberry".to_string()));
6189    }
6190
6191    #[test]
6192    fn test_multiselect_filter_navigation_bounds() {
6193        // Test that navigation respects filtered list bounds
6194        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6195            SelectOption::new("Apple", "apple".to_string()),
6196            SelectOption::new("Banana", "banana".to_string()),
6197            SelectOption::new("Cherry", "cherry".to_string()),
6198            SelectOption::new("Date", "date".to_string()),
6199        ]);
6200
6201        multi.focus();
6202
6203        // Apply filter "a" - should match Apple, Banana, Date (3 items)
6204        multi.update_filter("a".to_string());
6205        let filtered = multi.filtered_options();
6206        assert_eq!(filtered.len(), 3);
6207
6208        // Navigate down past the filtered list size
6209        let down_msg = Message::new(KeyMsg {
6210            key_type: KeyType::Down,
6211            runes: vec![],
6212            alt: false,
6213            paste: false,
6214        });
6215        multi.update(&down_msg);
6216        multi.update(&down_msg);
6217        multi.update(&down_msg); // Try to go past the end
6218        multi.update(&down_msg);
6219
6220        // Cursor should be capped at last filtered index
6221        assert_eq!(multi.cursor, 2); // Max index is 2 (3 items: 0, 1, 2)
6222    }
6223
6224    // -------------------------------------------------------------------------
6225    // FilePicker edge case tests (bd-1isw)
6226    // -------------------------------------------------------------------------
6227
6228    /// Helper to create a FilePicker pre-loaded with synthetic FileEntry items
6229    /// (avoids filesystem I/O in unit tests).
6230    fn filepicker_with_entries(entries: Vec<(&str, bool)>) -> FilePicker {
6231        let mut picker = FilePicker::new();
6232        picker.picking = true;
6233        picker.focused = true;
6234        picker.files = entries
6235            .into_iter()
6236            .map(|(name, is_dir)| FileEntry {
6237                name: name.to_string(),
6238                path: format!("/tmp/{name}"),
6239                is_dir,
6240                size: 0,
6241                mode: String::new(),
6242            })
6243            .collect();
6244        picker
6245    }
6246
6247    fn make_key_msg(key_type: KeyType) -> Message {
6248        Message::new(KeyMsg {
6249            key_type,
6250            runes: vec![],
6251            alt: false,
6252            paste: false,
6253        })
6254    }
6255
6256    #[test]
6257    fn filepicker_single_file_is_selected_by_default() {
6258        let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6259        // selected_index defaults to 0, which points at the only file
6260        assert_eq!(picker.selected_index, 0);
6261        assert_eq!(picker.files.len(), 1);
6262        assert_eq!(picker.files[0].name, "only_file.txt");
6263    }
6264
6265    #[test]
6266    fn filepicker_single_file_view_shows_entry() {
6267        let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6268        let view = picker.view();
6269        assert!(view.contains("only_file.txt"));
6270    }
6271
6272    #[test]
6273    fn filepicker_single_file_select_via_enter() {
6274        let mut picker = filepicker_with_entries(vec![("report.pdf", false)]);
6275        // Simulate pressing Enter (open binding)
6276        let enter_msg = make_key_msg(KeyType::Enter);
6277        let result = picker.update(&enter_msg);
6278        // Should select the file and advance
6279        assert_eq!(picker.selected_path, Some("/tmp/report.pdf".to_string()));
6280        assert!(!picker.picking);
6281        assert!(result.is_some()); // NextFieldMsg command returned
6282    }
6283
6284    #[test]
6285    fn filepicker_single_file_down_does_not_move() {
6286        let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6287        let down_msg = make_key_msg(KeyType::Down);
6288        picker.update(&down_msg);
6289        // Should remain at index 0 - nowhere to go
6290        assert_eq!(picker.selected_index, 0);
6291    }
6292
6293    #[test]
6294    fn filepicker_single_file_up_does_not_move() {
6295        let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6296        let up_msg = make_key_msg(KeyType::Up);
6297        picker.update(&up_msg);
6298        assert_eq!(picker.selected_index, 0);
6299    }
6300
6301    #[test]
6302    fn filepicker_empty_files_no_panic() {
6303        let mut picker = filepicker_with_entries(vec![]);
6304        // Verify no panic on navigation with empty list
6305        let down_msg = make_key_msg(KeyType::Down);
6306        picker.update(&down_msg);
6307        assert_eq!(picker.selected_index, 0);
6308
6309        let up_msg = make_key_msg(KeyType::Up);
6310        picker.update(&up_msg);
6311        assert_eq!(picker.selected_index, 0);
6312    }
6313
6314    #[test]
6315    fn filepicker_empty_files_view_no_panic() {
6316        let picker = filepicker_with_entries(vec![]);
6317        // Should render without panic even with no files
6318        let view = picker.view();
6319        assert_ne!(view, "");
6320    }
6321
6322    #[test]
6323    fn filepicker_empty_goto_top_bottom_no_panic() {
6324        let mut picker = filepicker_with_entries(vec![]);
6325        // goto_top
6326        let home_msg = Message::new(KeyMsg {
6327            key_type: KeyType::Home,
6328            runes: vec![],
6329            alt: false,
6330            paste: false,
6331        });
6332        picker.update(&home_msg);
6333        assert_eq!(picker.selected_index, 0);
6334
6335        // goto_bottom
6336        let end_msg = Message::new(KeyMsg {
6337            key_type: KeyType::End,
6338            runes: vec![],
6339            alt: false,
6340            paste: false,
6341        });
6342        picker.update(&end_msg);
6343        assert_eq!(picker.selected_index, 0);
6344    }
6345
6346    #[test]
6347    fn filepicker_height_zero_no_panic() {
6348        let mut picker =
6349            filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6350        picker.height = 0;
6351        // Navigate down — must not panic on offset calculation
6352        let down_msg = make_key_msg(KeyType::Down);
6353        picker.update(&down_msg);
6354        picker.update(&down_msg);
6355        assert_eq!(picker.selected_index, 2);
6356    }
6357
6358    #[test]
6359    fn filepicker_height_one_scrolls_correctly() {
6360        let mut picker =
6361            filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6362        picker.height = 1;
6363        assert_eq!(picker.selected_index, 0);
6364        assert_eq!(picker.offset, 0);
6365
6366        let down_msg = make_key_msg(KeyType::Down);
6367        picker.update(&down_msg);
6368        assert_eq!(picker.selected_index, 1);
6369        // With height=1, offset should scroll to keep selected visible
6370        assert_eq!(picker.offset, 1);
6371
6372        picker.update(&down_msg);
6373        assert_eq!(picker.selected_index, 2);
6374        assert_eq!(picker.offset, 2);
6375    }
6376
6377    #[test]
6378    fn filepicker_navigation_respects_bounds() {
6379        let mut picker = filepicker_with_entries(vec![("a.txt", false), ("b.txt", false)]);
6380        let down_msg = make_key_msg(KeyType::Down);
6381        let up_msg = make_key_msg(KeyType::Up);
6382
6383        // Navigate down past end
6384        picker.update(&down_msg);
6385        assert_eq!(picker.selected_index, 1);
6386        picker.update(&down_msg); // Should stay at 1
6387        assert_eq!(picker.selected_index, 1);
6388
6389        // Navigate up past start
6390        picker.update(&up_msg);
6391        assert_eq!(picker.selected_index, 0);
6392        picker.update(&up_msg); // Should stay at 0
6393        assert_eq!(picker.selected_index, 0);
6394    }
6395
6396    #[test]
6397    fn filepicker_dir_not_selectable_by_default() {
6398        let picker = filepicker_with_entries(vec![("subdir", true)]);
6399        let entry = &picker.files[0];
6400        // By default, dir_allowed is false
6401        assert!(!picker.is_selectable(entry));
6402    }
6403
6404    #[test]
6405    fn filepicker_file_selectable_by_default() {
6406        let picker = filepicker_with_entries(vec![("file.rs", false)]);
6407        let entry = &picker.files[0];
6408        assert!(picker.is_selectable(entry));
6409    }
6410
6411    #[test]
6412    fn filepicker_format_size_edge_cases() {
6413        assert_eq!(FilePicker::format_size(0), "0B");
6414        assert_eq!(FilePicker::format_size(1023), "1023B");
6415        assert_eq!(FilePicker::format_size(1024), "1.0K");
6416        assert_eq!(FilePicker::format_size(1024 * 1024), "1.0M");
6417        assert_eq!(FilePicker::format_size(1024 * 1024 * 1024), "1.0G");
6418    }
6419
6420    // ---- Select filter tests ----
6421
6422    fn make_select_options() -> Vec<SelectOption<String>> {
6423        vec![
6424            SelectOption::new("Apple", "apple".to_string()),
6425            SelectOption::new("Apricot", "apricot".to_string()),
6426            SelectOption::new("Banana", "banana".to_string()),
6427            SelectOption::new("Cherry", "cherry".to_string()),
6428            SelectOption::new("Date", "date".to_string()),
6429        ]
6430    }
6431
6432    fn make_filterable_select() -> Select<String> {
6433        Select::new()
6434            .options(make_select_options())
6435            .filterable(true)
6436            .height_options(3)
6437    }
6438
6439    #[test]
6440    fn select_filterable_builder() {
6441        let sel = Select::<String>::new().filterable(true);
6442        assert!(sel.filtering);
6443        let sel = Select::<String>::new().filterable(false);
6444        assert!(!sel.filtering);
6445    }
6446
6447    #[test]
6448    fn select_filtered_indices_no_filter() {
6449        let sel = make_filterable_select();
6450        assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6451    }
6452
6453    #[test]
6454    fn select_filtered_indices_with_filter() {
6455        let mut sel = make_filterable_select();
6456        sel.filter_value = "ap".to_string();
6457        // "Apple" and "Apricot" match "ap"
6458        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6459    }
6460
6461    #[test]
6462    fn select_filtered_indices_case_insensitive() {
6463        let mut sel = make_filterable_select();
6464        sel.filter_value = "AP".to_string();
6465        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6466    }
6467
6468    #[test]
6469    fn select_filtered_indices_no_match() {
6470        let mut sel = make_filterable_select();
6471        sel.filter_value = "zzz".to_string();
6472        assert_eq!(sel.filtered_indices(), Vec::<usize>::new());
6473    }
6474
6475    #[test]
6476    fn select_update_filter_keeps_selection() {
6477        let mut sel = make_filterable_select();
6478        sel.selected = 2; // Banana
6479        sel.update_filter("an".to_string());
6480        // "Banana" contains "an" — should still be selected
6481        assert_eq!(sel.selected, 2);
6482        assert_eq!(sel.filter_value, "an");
6483    }
6484
6485    #[test]
6486    fn select_update_filter_clamps_when_item_hidden() {
6487        let mut sel = make_filterable_select();
6488        sel.selected = 2; // Banana
6489        sel.update_filter("ch".to_string());
6490        // Only "Cherry" matches "ch" — Banana hidden
6491        // selected should move to Cherry (index 3)
6492        assert_eq!(sel.selected, 3);
6493    }
6494
6495    #[test]
6496    fn select_update_filter_clear_restores() {
6497        let mut sel = make_filterable_select();
6498        sel.update_filter("ap".to_string());
6499        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6500        sel.update_filter(String::new());
6501        assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6502    }
6503
6504    #[test]
6505    fn select_filter_display_in_view() {
6506        let mut sel = make_filterable_select();
6507        sel.focused = true;
6508        sel.filter_value = "ap".to_string();
6509        let view = sel.view();
6510        assert!(view.contains("Filter: ap_"));
6511    }
6512
6513    #[test]
6514    fn select_filter_not_displayed_when_empty() {
6515        let mut sel = make_filterable_select();
6516        sel.focused = true;
6517        let view = sel.view();
6518        assert!(!view.contains("Filter:"));
6519    }
6520
6521    #[test]
6522    fn select_filter_not_displayed_when_disabled() {
6523        let mut sel = Select::new()
6524            .options(make_select_options())
6525            .height_options(3);
6526        sel.focused = true;
6527        sel.filter_value = "ap".to_string();
6528        let view = sel.view();
6529        assert!(!view.contains("Filter:"));
6530    }
6531
6532    #[test]
6533    fn select_navigation_respects_filter() {
6534        let mut sel = make_filterable_select();
6535        sel.focused = true;
6536        sel.update_filter("a".to_string());
6537        // Matches: Apple(0), Apricot(1), Banana(2), Date(4)
6538        let indices = sel.filtered_indices();
6539        assert_eq!(indices, vec![0, 1, 2, 4]);
6540
6541        // selected should be 0 (Apple)
6542        sel.selected = 0;
6543
6544        // Create a "down" key message
6545        let down_msg = Message::new(KeyMsg {
6546            key_type: KeyType::Down,
6547            runes: vec![],
6548            alt: false,
6549            paste: false,
6550        });
6551        sel.update(&down_msg);
6552        // Should move to next in filtered list: Apricot (1)
6553        assert_eq!(sel.selected, 1);
6554
6555        sel.update(&down_msg);
6556        // Should move to Banana (2)
6557        assert_eq!(sel.selected, 2);
6558
6559        sel.update(&down_msg);
6560        // Should move to Date (4), skipping Cherry (3) which doesn't match
6561        assert_eq!(sel.selected, 4);
6562    }
6563
6564    #[test]
6565    fn select_get_selected_value_with_filter() {
6566        let mut sel = make_filterable_select();
6567        sel.update_filter("ch".to_string());
6568        // Only Cherry matches, selected should be 3
6569        assert_eq!(sel.selected, 3);
6570        assert_eq!(sel.get_selected_value(), Some(&"cherry".to_string()));
6571    }
6572}