bubbletea_widgets/
help.rs

1//! A help component for bubbletea-rs, ported from the Go version.
2//!
3//! This component provides a customizable help view that can automatically
4//! generate its content from a set of key bindings. It supports both compact
5//! single-line help displays and expanded multi-column layouts.
6//!
7//! The help component integrates seamlessly with the bubbletea-rs architecture
8//! and provides adaptive styling for both light and dark terminal themes.
9//!
10//! # Features
11//!
12//! - **Dual Display Modes**: Switch between compact and expanded help views
13//! - **Adaptive Styling**: Automatically adjusts colors for light/dark themes
14//! - **Width Constraints**: Truncates content with ellipsis when space is limited
15//! - **Column Layout**: Organizes key bindings into logical, aligned columns
16//! - **Disabled Key Handling**: Automatically hides disabled key bindings
17//!
18//! # Quick Start
19//!
20//! ```rust
21//! use bubbletea_widgets::help::{Model, KeyMap};
22//! use bubbletea_widgets::key::Binding;
23//! use crossterm::event::KeyCode;
24//!
25//! // Create key bindings for your application
26//! let quit_key = Binding::new(vec![KeyCode::Char('q')])
27//!     .with_help("q", "quit");
28//! let help_key = Binding::new(vec![KeyCode::Char('?')])
29//!     .with_help("?", "help");
30//!
31//! // Implement KeyMap for your application state
32//! struct MyApp {
33//!     quit_key: Binding,
34//!     help_key: Binding,
35//! }
36//!
37//! impl KeyMap for MyApp {
38//!     fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> {
39//!         vec![&self.quit_key, &self.help_key]
40//!     }
41//!
42//!     fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> {
43//!         vec![
44//!             vec![&self.help_key],  // Help column
45//!             vec![&self.quit_key],  // Quit column
46//!         ]
47//!     }
48//! }
49//!
50//! // Create and use the help component
51//! let app = MyApp { quit_key, help_key };
52//! let help = Model::new().with_width(80);
53//!
54//! // Render help text
55//! let short_help = help.view(&app);  // Shows compact help
56//! let mut full_help = help;
57//! full_help.show_all = true;
58//! let detailed_help = full_help.view(&app);  // Shows detailed help
59//! ```
60
61use crate::key;
62use bubbletea_rs::{Cmd, Msg};
63use lipgloss_extras::lipgloss;
64use lipgloss_extras::prelude::*;
65
66/// A trait that defines the key bindings to be displayed in the help view.
67///
68/// Any model that uses the help component should implement this trait to provide
69/// the key bindings that the help view will render. The trait provides two methods
70/// for different display contexts:
71///
72/// - `short_help()`: Returns key bindings for compact, single-line display
73/// - `full_help()`: Returns grouped key bindings for detailed, multi-column display
74///
75/// # Implementation Guidelines
76///
77/// ## Short Help
78///
79/// Should include only the most essential key bindings (typically 3-6 keys)
80/// that users need for basic operation. These are displayed horizontally
81/// with bullet separators.
82///
83/// ## Full Help
84///
85/// Should group related key bindings into logical columns:
86/// - Navigation keys in one group
87/// - Action keys in another group  
88/// - Application control keys in a third group
89///
90/// Each inner `Vec` becomes a column in the final display, so group
91/// related functionality together.
92///
93/// # Examples
94///
95/// ```rust
96/// use bubbletea_widgets::help::KeyMap;
97/// use bubbletea_widgets::key::Binding;
98/// use crossterm::event::KeyCode;
99///
100/// struct TextEditor {
101///     save_key: Binding,
102///     quit_key: Binding,
103///     undo_key: Binding,
104///     redo_key: Binding,
105/// }
106///
107/// impl KeyMap for TextEditor {
108///     fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> {
109///         // Only show essential keys in compact view
110///         vec![&self.save_key, &self.quit_key]
111///     }
112///
113///     fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> {
114///         vec![
115///             // File operations column
116///             vec![&self.save_key],
117///             // Edit operations column  
118///             vec![&self.undo_key, &self.redo_key],
119///             // Application control column
120///             vec![&self.quit_key],
121///         ]
122///     }
123/// }
124/// ```
125pub trait KeyMap {
126    /// Returns a slice of key bindings for the short help view.
127    ///
128    /// This method should return the most essential key bindings for your
129    /// application, typically 3-6 keys that users need for basic operation.
130    /// These bindings will be displayed in a single horizontal line.
131    ///
132    /// # Guidelines
133    ///
134    /// - Include only the most frequently used keys
135    /// - Prioritize navigation and core functionality
136    /// - Consider the typical workflow of your application
137    /// - Keep the total count manageable (3-6 keys)
138    ///
139    /// # Returns
140    ///
141    /// A vector of key binding references that will be displayed horizontally.
142    fn short_help(&self) -> Vec<&key::Binding>;
143    /// Returns a nested slice of key bindings for the full help view.
144    ///
145    /// Each inner `Vec` represents a column in the help view and should contain
146    /// logically related key bindings. The help component will render these as
147    /// separate columns with proper alignment and spacing.
148    ///
149    /// # Guidelines
150    ///
151    /// - Group related functionality together in the same column
152    /// - Keep columns roughly the same height for visual balance
153    /// - Consider the logical flow: navigation → actions → application control
154    /// - Each column should have 2-8 key bindings for optimal display
155    ///
156    /// # Column Organization Examples
157    ///
158    /// ```text
159    /// Column 1: Navigation    Column 2: Actions       Column 3: App Control
160    /// ↑/k      move up       enter    select         q        quit
161    /// ↓/j      move down     space    toggle         ?        help
162    /// →        next page     d        delete         ctrl+c   force quit
163    /// ←        prev page     
164    /// ```
165    ///
166    /// # Returns
167    ///
168    /// A vector of vectors, where each inner vector represents a column
169    /// of key bindings to display.
170    fn full_help(&self) -> Vec<Vec<&key::Binding>>;
171}
172
173/// A set of styles for the help component.
174///
175/// This structure defines all the visual styling options available for customizing
176/// the appearance of the help view. Each field controls a specific visual element,
177/// allowing fine-grained control over colors, formatting, and visual hierarchy.
178///
179/// # Style Categories
180///
181/// ## Short Help Styles
182/// - `short_key`: Styling for key names in compact view
183/// - `short_desc`: Styling for descriptions in compact view  
184/// - `short_separator`: Styling for bullet separators between items
185///
186/// ## Full Help Styles
187/// - `full_key`: Styling for key names in detailed view
188/// - `full_desc`: Styling for descriptions in detailed view
189/// - `full_separator`: Styling for spacing between columns
190///
191/// ## Utility Styles
192/// - `ellipsis`: Styling for truncation indicator when content is too wide
193///
194/// # Examples
195///
196/// ## Custom Color Scheme
197/// ```rust
198/// use bubbletea_widgets::help::Styles;
199/// use lipgloss_extras::prelude::*;
200///
201/// let vibrant_styles = Styles {
202///     short_key: Style::new()
203///         .foreground(Color::from("#FF6B6B"))
204///         .bold(true),
205///     short_desc: Style::new()
206///         .foreground(Color::from("#4ECDC4"))
207///         .italic(true),
208///     short_separator: Style::new()
209///         .foreground(Color::from("#45B7D1")),
210///     ..Default::default()
211/// };
212/// ```
213///
214/// ## Monochrome Theme
215/// ```rust
216/// # use bubbletea_widgets::help::Styles;
217/// # use lipgloss_extras::prelude::*;
218/// let mono_styles = Styles {
219///     short_key: Style::new().bold(true),
220///     short_desc: Style::new().faint(true),
221///     full_key: Style::new().underline(true),
222///     full_desc: Style::new(),
223///     ..Default::default()
224/// };
225/// ```
226///
227/// ## Using with Help Model
228/// ```rust
229/// # use bubbletea_widgets::help::{Model, Styles};
230/// # use lipgloss_extras::prelude::*;
231/// let custom_styles = Styles {
232///     short_key: Style::new().foreground(Color::from("#00FF00")),
233///     ..Default::default()
234/// };
235///
236/// let help = Model {
237///     styles: custom_styles,
238///     ..Default::default()
239/// };
240/// ```
241#[derive(Debug, Clone)]
242pub struct Styles {
243    /// Style for the ellipsis character when content is truncated.
244    pub ellipsis: Style,
245    /// Style for key names in the short help view.
246    pub short_key: Style,
247    /// Style for descriptions in the short help view.
248    pub short_desc: Style,
249    /// Style for the separator between items in the short help view.
250    pub short_separator: Style,
251    /// Style for key names in the full help view.
252    pub full_key: Style,
253    /// Style for descriptions in the full help view.
254    pub full_desc: Style,
255    /// Style for the separator between columns in the full help view.
256    pub full_separator: Style,
257}
258
259impl Default for Styles {
260    /// Creates default styles with a subtle color scheme that adapts to light and dark themes.
261    ///
262    /// The default styling uses adaptive colors that work well in both light and dark terminal environments:
263    ///
264    /// ## Color Palette
265    /// - **Keys**: Medium gray (light: #909090, dark: #626262) - provides good visibility for key names
266    /// - **Descriptions**: Lighter gray (light: #B2B2B2, dark: #4A4A4A) - subtle but readable for descriptions
267    /// - **Separators**: Even lighter gray (light: #DDDADA, dark: #3C3C3C) - minimal visual interruption
268    ///
269    /// ## Adaptive Behavior
270    ///
271    /// The colors automatically adapt based on the terminal's background:
272    /// - **Light terminals**: Use darker colors for good contrast
273    /// - **Dark terminals**: Use lighter colors for readability
274    ///
275    /// This ensures consistent readability across different terminal themes
276    /// without requiring manual configuration.
277    ///
278    /// # Examples
279    ///
280    /// ```rust
281    /// use bubbletea_widgets::help::Styles;
282    ///
283    /// let styles = Styles::default();
284    /// // All styles are configured with adaptive colors suitable for terminals
285    /// // The actual style strings contain color codes
286    /// ```
287    fn default() -> Self {
288        use lipgloss::AdaptiveColor;
289
290        let key_style = Style::new().foreground(AdaptiveColor {
291            Light: "#909090",
292            Dark: "#626262",
293        });
294        let desc_style = Style::new().foreground(AdaptiveColor {
295            Light: "#B2B2B2",
296            Dark: "#4A4A4A",
297        });
298        let sep_style = Style::new().foreground(AdaptiveColor {
299            Light: "#DDDADA",
300            Dark: "#3C3C3C",
301        });
302
303        Self {
304            ellipsis: sep_style.clone(),
305            short_key: key_style.clone(),
306            short_desc: desc_style.clone(),
307            short_separator: sep_style.clone(),
308            full_key: key_style,
309            full_desc: desc_style,
310            full_separator: sep_style,
311        }
312    }
313}
314
315/// The help model that manages help view state and rendering.
316///
317/// This is the main component for displaying help information in terminal applications.
318/// It can show either a compact single-line view or an expanded multi-column view
319/// based on the `show_all` toggle. The component handles automatic styling, width
320/// constraints, and proper alignment of key bindings.
321///
322/// # View Modes
323///
324/// ## Short Help Mode (`show_all = false`)
325/// Displays key bindings in a horizontal line with bullet separators:
326/// ```text
327/// ↑/k up • ↓/j down • / filter • q quit • ? more
328/// ```
329///
330/// ## Full Help Mode (`show_all = true`)
331/// Displays key bindings in organized columns:
332/// ```text
333/// ↑/k      up             / filter         q quit
334/// ↓/j      down           esc clear filter ? close help
335/// →/l/pgdn next page      enter apply
336/// ←/h/pgup prev page
337/// ```
338///
339/// # Configuration
340///
341/// - **Width Constraints**: Set maximum width to enable truncation with ellipsis
342/// - **Custom Separators**: Configure bullet separators and column spacing
343/// - **Styling**: Full control over colors and text formatting
344/// - **State Management**: Toggle between compact and detailed views
345///
346/// # Examples
347///
348/// ## Basic Usage
349/// ```rust
350/// use bubbletea_widgets::help::{Model, KeyMap};
351/// use bubbletea_widgets::key::Binding;
352/// use crossterm::event::KeyCode;
353///
354/// // Create key bindings
355/// let quit_binding = Binding::new(vec![KeyCode::Char('q')])
356///     .with_help("q", "quit");
357///
358/// // Implement KeyMap for your application
359/// struct MyApp {
360///     quit_binding: Binding,
361/// }
362///
363/// impl KeyMap for MyApp {
364///     fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> {
365///         vec![&self.quit_binding]
366///     }
367///     fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> {
368///         vec![vec![&self.quit_binding]]
369///     }
370/// }
371///
372/// // Create and configure help model
373/// let app = MyApp { quit_binding };
374/// let help = Model::new().with_width(80);
375/// let help_text = help.view(&app);
376/// ```
377///
378/// ## Advanced Configuration
379/// ```rust
380/// # use bubbletea_widgets::help::Model;
381/// # use lipgloss_extras::prelude::*;
382/// let help = Model {
383///     show_all: false,
384///     width: 120,
385///     short_separator: " | ".to_string(),
386///     full_separator: "      ".to_string(),
387///     ellipsis: "...".to_string(),
388///     styles: Default::default(),
389/// };
390/// ```
391///
392/// ## Integration with BubbleTea
393/// ```rust
394/// # use bubbletea_widgets::help::{Model, KeyMap};
395/// # use bubbletea_rs::{Msg, Model as BubbleTeaModel};
396/// # struct MyApp { help: Model }
397/// # impl KeyMap for MyApp {
398/// #   fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> { vec![] }
399/// #   fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> { vec![] }
400/// # }
401/// # impl BubbleTeaModel for MyApp {
402/// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyApp { help: Model::new() }, None) }
403/// #   fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
404/// // Toggle help view with '?' key
405/// self.help.show_all = !self.help.show_all;
406/// None
407/// #   }
408/// #   fn view(&self) -> String {
409/// // Render help at bottom of your application view
410/// let help_view = self.help.view(self);
411/// format!("{}\n{}", "Your app content here", help_view)
412/// #   }
413/// # }
414/// ```
415#[derive(Debug, Clone)]
416pub struct Model {
417    /// Toggles between short (single-line) and full (multi-column) help view.
418    /// When `false`, shows compact help; when `true`, shows detailed help.
419    pub show_all: bool,
420    /// The maximum width of the help view in characters.
421    /// When set to 0, no width limit is enforced.
422    pub width: usize,
423
424    /// The separator string used between items in the short help view.
425    /// Default is " • " (bullet with spaces).
426    pub short_separator: String,
427    /// The separator string used between columns in the full help view.
428    /// Default is "    " (four spaces).
429    pub full_separator: String,
430    /// The character displayed when help content is truncated due to width constraints.
431    /// Default is "…" (horizontal ellipsis).
432    pub ellipsis: String,
433
434    /// The styling configuration for all visual elements of the help view.
435    pub styles: Styles,
436}
437
438impl Default for Model {
439    /// Creates a new help model with sensible defaults.
440    ///
441    /// Default configuration:
442    /// - `show_all`: false (shows short help)
443    /// - `width`: 0 (no width limit)
444    /// - `short_separator`: " • "
445    /// - `full_separator`: "    " (4 spaces)
446    /// - `ellipsis`: "…"
447    /// - `styles`: Default styles
448    ///
449    /// # Examples
450    ///
451    /// ```rust
452    /// use bubbletea_widgets::help::Model;
453    ///
454    /// let help = Model::default();
455    /// assert_eq!(help.show_all, false);
456    /// assert_eq!(help.width, 0);
457    /// ```
458    fn default() -> Self {
459        Self {
460            show_all: false,
461            width: 0,
462            short_separator: " • ".to_string(),
463            full_separator: "    ".to_string(),
464            ellipsis: "…".to_string(),
465            styles: Styles::default(),
466        }
467    }
468}
469
470impl Model {
471    /// Creates a new help model with default settings.
472    ///
473    /// This is equivalent to calling `Model::default()` but provides a more
474    /// conventional constructor-style API. The model is created in compact
475    /// mode with no width limits and default styling.
476    ///
477    /// # Default Configuration
478    ///
479    /// - `show_all`: `false` (compact view)
480    /// - `width`: `0` (no width limit)
481    /// - `short_separator`: `" • "` (bullet with spaces)
482    /// - `full_separator`: `"    "` (four spaces)
483    /// - `ellipsis`: `"…"` (horizontal ellipsis)
484    /// - `styles`: Adaptive color scheme
485    ///
486    /// # Examples
487    ///
488    /// ```rust
489    /// use bubbletea_widgets::help::Model;
490    ///
491    /// let help = Model::new();
492    /// assert_eq!(help.show_all, false);
493    /// assert_eq!(help.width, 0);
494    /// assert_eq!(help.short_separator, " • ");
495    /// ```
496    pub fn new() -> Self {
497        Self::default()
498    }
499
500    /// Sets the maximum width of the help view.
501    ///
502    /// When a width is set, the help view will truncate content that exceeds
503    /// this limit, showing an ellipsis to indicate truncation. This is useful
504    /// for ensuring help text doesn't overflow in constrained terminal windows
505    /// or when embedding help in specific layout areas.
506    ///
507    /// # Truncation Behavior
508    ///
509    /// - **Short Help**: Truncates items from right to left, showing ellipsis when possible
510    /// - **Full Help**: Truncates columns from right to left, maintaining column integrity
511    /// - **Smart Ellipsis**: Only shows ellipsis if it fits within the width constraint
512    ///
513    /// # Arguments
514    ///
515    /// * `width` - Maximum width in characters. Use 0 for no limit.
516    ///
517    /// # Examples
518    ///
519    /// ## Setting Width Constraints
520    /// ```rust
521    /// use bubbletea_widgets::help::Model;
522    ///
523    /// let help = Model::new().with_width(80);
524    /// assert_eq!(help.width, 80);
525    ///
526    /// // No width limit
527    /// let unlimited = Model::new().with_width(0);
528    /// assert_eq!(unlimited.width, 0);
529    /// ```
530    ///
531    /// ## Chaining with Other Configuration
532    /// ```rust
533    /// # use bubbletea_widgets::help::Model;
534    /// let help = Model::new()
535    ///     .with_width(120);
536    ///     
537    /// // Further customize if needed
538    /// let mut customized = help;
539    /// customized.show_all = true;
540    /// ```
541    pub fn with_width(mut self, width: usize) -> Self {
542        self.width = width;
543        self
544    }
545
546    /// Updates the help model in response to a message.
547    ///
548    /// This method provides compatibility with the bubbletea-rs architecture,
549    /// matching the Go implementation's Update method. Since the help component
550    /// is primarily a view component that doesn't handle user input directly,
551    /// this is a no-op method that simply returns the model unchanged.
552    ///
553    /// # Design Rationale
554    ///
555    /// The help component is stateless from a user interaction perspective:
556    /// - It doesn't respond to keyboard input
557    /// - It doesn't maintain internal state that changes over time
558    /// - Its display is controlled by the parent application
559    ///
560    /// Parent applications typically control help display by:
561    /// - Toggling `show_all` based on key presses (e.g., '?' key)
562    /// - Adjusting `width` in response to terminal resize events
563    /// - Updating styling based on theme changes
564    ///
565    /// # Arguments
566    ///
567    /// * `_msg` - The message to handle (unused for help component)
568    ///
569    /// # Returns
570    ///
571    /// A tuple containing:
572    /// - The unchanged model
573    /// - `None` for the command (no side effects needed)
574    ///
575    /// # Examples
576    ///
577    /// ```rust
578    /// use bubbletea_widgets::help::Model;
579    /// use bubbletea_rs::Msg;
580    ///
581    /// let help = Model::new();
582    /// // Any message can be passed, the help component ignores all messages
583    /// let msg = Box::new(42); // Example message
584    /// let (updated_help, cmd) = help.update(msg);
585    /// assert!(cmd.is_none()); // Help component doesn't generate commands
586    /// ```
587    ///
588    /// ## Integration Pattern
589    /// ```rust
590    /// # use bubbletea_widgets::help::Model;
591    /// # use bubbletea_rs::{Msg, Model as BubbleTeaModel, KeyMsg};
592    /// # use crossterm::event::KeyCode;
593    /// # struct MyApp { help: Model }
594    /// # impl bubbletea_widgets::help::KeyMap for MyApp {
595    /// #   fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> { vec![] }
596    /// #   fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> { vec![] }
597    /// # }
598    /// # impl BubbleTeaModel for MyApp {
599    /// #   fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyApp { help: Model::new() }, None) }
600    /// #   fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
601    /// // Parent application handles help toggling
602    /// if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
603    ///     if key_msg.key == KeyCode::Char('?') {
604    ///         self.help.show_all = !self.help.show_all;
605    ///     }
606    /// }
607    ///
608    /// // Help component itself doesn't need to process messages
609    /// let (_unchanged_help, _no_cmd) = self.help.clone().update(msg);
610    /// None
611    /// #   }
612    /// #   fn view(&self) -> String { String::new() }
613    /// # }
614    /// ```
615    pub fn update(self, _msg: Msg) -> (Self, Option<Cmd>) {
616        (self, None)
617    }
618
619    /// Renders the help view based on the current model state.
620    ///
621    /// This is the main rendering function that switches between short and full
622    /// help views based on the `show_all` flag. The method applies styling,
623    /// handles width constraints, and formats the output appropriately for
624    /// terminal display.
625    ///
626    /// # Rendering Process
627    ///
628    /// 1. **Mode Selection**: Choose between short or full help based on `show_all`
629    /// 2. **Key Filtering**: Automatically skip disabled key bindings
630    /// 3. **Styling Application**: Apply configured colors and formatting
631    /// 4. **Layout**: Arrange keys and descriptions with proper spacing
632    /// 5. **Width Handling**: Truncate with ellipsis if width constraints are set
633    ///
634    /// # Output Formats
635    ///
636    /// ## Short Help (`show_all = false`)
637    /// ```text
638    /// ↑/k up • ↓/j down • / filter • q quit
639    /// ```
640    ///
641    /// ## Full Help (`show_all = true`)
642    /// ```text
643    /// ↑/k      up         / filter    q quit
644    /// ↓/j      down       esc clear   ? help
645    /// →/pgdn   next
646    /// ```
647    ///
648    /// # Arguments
649    ///
650    /// * `keymap` - An object implementing the `KeyMap` trait that provides
651    ///   the key bindings to display. Typically your main application model.
652    ///
653    /// # Returns
654    ///
655    /// A formatted string ready for display in the terminal, including ANSI
656    /// color codes if styling is configured.
657    ///
658    /// # Examples
659    ///
660    /// ## Basic Rendering
661    /// ```rust
662    /// use bubbletea_widgets::help::{Model, KeyMap};
663    /// use bubbletea_widgets::key::Binding;
664    /// use crossterm::event::KeyCode;
665    ///
666    /// struct MyApp {
667    ///     quit_key: Binding,
668    /// }
669    ///
670    /// impl KeyMap for MyApp {
671    ///     fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> {
672    ///         vec![&self.quit_key]
673    ///     }
674    ///     fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> {
675    ///         vec![vec![&self.quit_key]]
676    ///     }
677    /// }
678    ///
679    /// let app = MyApp {
680    ///     quit_key: Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit")
681    /// };
682    /// let help = Model::new();
683    /// let output = help.view(&app);
684    /// ```
685    ///
686    /// ## Toggling Between Modes
687    /// ```rust
688    /// # use bubbletea_widgets::help::{Model, KeyMap};
689    /// # use bubbletea_widgets::key::Binding;
690    /// # use crossterm::event::KeyCode;
691    /// # struct MyApp { quit_key: Binding }
692    /// # impl KeyMap for MyApp {
693    /// #     fn short_help(&self) -> Vec<&bubbletea_widgets::key::Binding> { vec![&self.quit_key] }
694    /// #     fn full_help(&self) -> Vec<Vec<&bubbletea_widgets::key::Binding>> { vec![vec![&self.quit_key]] }
695    /// # }
696    /// # let app = MyApp { quit_key: Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit") };
697    /// let mut help = Model::new();
698    ///
699    /// // Render compact help
700    /// help.show_all = false;
701    /// let short = help.view(&app);
702    ///
703    /// // Render detailed help
704    /// help.show_all = true;
705    /// let full = help.view(&app);
706    ///
707    /// // Both modes produce valid output
708    /// assert!(!short.is_empty());
709    /// assert!(!full.is_empty());
710    /// ```
711    pub fn view<K: KeyMap>(&self, keymap: &K) -> String {
712        if self.show_all {
713            self.full_help_view(keymap.full_help())
714        } else {
715            self.short_help_view(keymap.short_help())
716        }
717    }
718
719    /// Renders a compact single-line help view.
720    ///
721    /// This view displays key bindings in a horizontal layout, separated by
722    /// the configured separator (default: " • "). If the content exceeds the
723    /// specified width, it will be truncated with an ellipsis. This format
724    /// is ideal for status bars or when screen space is limited.
725    ///
726    /// # Layout Format
727    ///
728    /// ```text
729    /// key1 desc1 • key2 desc2 • key3 desc3
730    /// ```
731    ///
732    /// # Truncation Behavior
733    ///
734    /// When width constraints are active:
735    /// 1. Items are added from left to right
736    /// 2. If an item would exceed the width, it's skipped
737    /// 3. An ellipsis ("…") is added if there's space
738    /// 4. Disabled key bindings are automatically excluded
739    ///
740    /// # Arguments
741    ///
742    /// * `bindings` - A vector of key bindings to display in order of priority.
743    ///   Higher priority items should appear first as they're less likely to
744    ///   be truncated.
745    ///
746    /// # Returns
747    ///
748    /// A single-line string containing the formatted help text with ANSI
749    /// styling applied.
750    ///
751    /// # Examples
752    ///
753    /// ## Basic Usage
754    /// ```rust
755    /// use bubbletea_widgets::help::Model;
756    /// use bubbletea_widgets::key::Binding;
757    /// use crossterm::event::KeyCode;
758    ///
759    /// let help = Model::new();
760    /// let quit_binding = Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit");
761    /// let help_binding = Binding::new(vec![KeyCode::Char('?')]).with_help("?", "help");
762    /// let bindings = vec![&quit_binding, &help_binding];
763    /// let output = help.short_help_view(bindings);
764    /// // Output: "q quit • ? help"
765    /// ```
766    ///
767    /// ## With Width Constraints
768    /// ```rust
769    /// # use bubbletea_widgets::help::Model;
770    /// # use bubbletea_widgets::key::Binding;
771    /// # use crossterm::event::KeyCode;
772    /// let help = Model::new().with_width(20); // Very narrow
773    /// let quit_binding = Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit");
774    /// let help_binding = Binding::new(vec![KeyCode::Char('?')]).with_help("?", "help");
775    /// let save_binding = Binding::new(vec![KeyCode::Char('s')]).with_help("s", "save");
776    /// let bindings = vec![&quit_binding, &help_binding, &save_binding];
777    /// let output = help.short_help_view(bindings);
778    /// // Might be truncated due to width constraints
779    /// ```
780    pub fn short_help_view(&self, bindings: Vec<&key::Binding>) -> String {
781        if bindings.is_empty() {
782            return String::new();
783        }
784
785        let mut builder = String::new();
786        let mut total_width = 0;
787        let separator = self
788            .styles
789            .short_separator
790            .clone()
791            .inline(true)
792            .render(&self.short_separator);
793
794        for (i, kb) in bindings.iter().enumerate() {
795            // Skip disabled bindings
796            if !kb.enabled() {
797                continue;
798            }
799
800            let sep = if total_width > 0 && i < bindings.len() {
801                &separator
802            } else {
803                ""
804            };
805
806            // Format: "key description"
807            let help = kb.help();
808            let key_part = self.styles.short_key.clone().inline(true).render(&help.key);
809            let desc_part = self
810                .styles
811                .short_desc
812                .clone()
813                .inline(true)
814                .render(&help.desc);
815            let item_str = format!("{}{} {}", sep, key_part, desc_part);
816
817            let item_width = lipgloss::width_visible(&item_str);
818
819            if let Some(tail) = self.should_add_item(total_width, item_width) {
820                if !tail.is_empty() {
821                    builder.push_str(&tail);
822                }
823                break;
824            }
825
826            total_width += item_width;
827            builder.push_str(&item_str);
828        }
829        builder
830    }
831
832    /// Renders a detailed multi-column help view.
833    ///
834    /// This view organizes key bindings into columns, with each group of bindings
835    /// forming a separate column. Keys and descriptions are aligned vertically
836    /// within each column, and columns are separated by configurable spacing.
837    /// This format provides comprehensive help information in an organized layout.
838    ///
839    /// # Layout Structure
840    ///
841    /// ```text
842    /// Column 1          Column 2          Column 3
843    /// key1 desc1        key4 desc4        key7 desc7
844    /// key2 desc2        key5 desc5        key8 desc8  
845    /// key3 desc3        key6 desc6
846    /// ```
847    ///
848    /// # Column Processing
849    ///
850    /// 1. **Filtering**: Skip empty groups and groups with all disabled bindings
851    /// 2. **Row Building**: Create "key description" pairs for each enabled binding
852    /// 3. **Vertical Joining**: Combine rows within each column with newlines
853    /// 4. **Horizontal Joining**: Align columns side-by-side with separators
854    /// 5. **Width Management**: Truncate columns if width constraints are active
855    ///
856    /// # Truncation Behavior
857    ///
858    /// When width limits are set:
859    /// - Columns are added left to right until width would be exceeded
860    /// - Remaining columns are dropped entirely (maintaining column integrity)
861    /// - An ellipsis is added if there's space to indicate truncation
862    ///
863    /// # Arguments
864    ///
865    /// * `groups` - A vector of key binding groups, where each group becomes
866    ///   a column in the output. Order matters: earlier groups have higher
867    ///   priority and are less likely to be truncated.
868    ///
869    /// # Returns
870    ///
871    /// A multi-line string containing the formatted help text with proper
872    /// column alignment and ANSI styling applied.
873    ///
874    /// # Examples
875    ///
876    /// ## Basic Multi-Column Layout
877    /// ```rust
878    /// use bubbletea_widgets::help::Model;
879    /// use bubbletea_widgets::key::Binding;
880    /// use crossterm::event::KeyCode;
881    ///
882    /// let help = Model::new();
883    ///
884    /// // Create bindings with proper lifetimes
885    /// let up_key = Binding::new(vec![KeyCode::Up]).with_help("↑/k", "up");
886    /// let down_key = Binding::new(vec![KeyCode::Down]).with_help("↓/j", "down");
887    /// let enter_key = Binding::new(vec![KeyCode::Enter]).with_help("enter", "select");
888    /// let delete_key = Binding::new(vec![KeyCode::Delete]).with_help("del", "delete");
889    /// let quit_key = Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit");
890    /// let help_key = Binding::new(vec![KeyCode::Char('?')]).with_help("?", "help");
891    ///
892    /// let groups = vec![
893    ///     // Navigation column
894    ///     vec![&up_key, &down_key],
895    ///     // Action column  
896    ///     vec![&enter_key, &delete_key],
897    ///     // App control column
898    ///     vec![&quit_key, &help_key],
899    /// ];
900    /// let output = help.full_help_view(groups);
901    /// // Creates aligned columns with proper spacing
902    /// ```
903    ///
904    /// ## With Custom Separator
905    /// ```rust
906    /// # use bubbletea_widgets::help::Model;
907    /// # use bubbletea_widgets::key::Binding;
908    /// # use crossterm::event::KeyCode;
909    /// let help = Model {
910    ///     full_separator: "  |  ".to_string(), // Custom column separator
911    ///     ..Model::new()
912    /// };
913    /// let action_key = Binding::new(vec![KeyCode::Char('a')]).with_help("a", "action");
914    /// let quit_key = Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit");
915    /// let groups = vec![
916    ///     vec![&action_key],
917    ///     vec![&quit_key],
918    /// ];
919    /// let output = help.full_help_view(groups);
920    /// // Columns will be separated by "  |  "
921    /// ```
922    ///
923    /// ## Handling Width Constraints
924    /// ```rust
925    /// # use bubbletea_widgets::help::Model;
926    /// # use bubbletea_widgets::key::Binding;
927    /// # use crossterm::event::KeyCode;
928    /// let help = Model::new().with_width(40); // Narrow width
929    /// let first_key = Binding::new(vec![KeyCode::Char('1')]).with_help("1", "first");
930    /// let second_key = Binding::new(vec![KeyCode::Char('2')]).with_help("2", "second");
931    /// let third_key = Binding::new(vec![KeyCode::Char('3')]).with_help("3", "third");
932    /// let groups = vec![
933    ///     vec![&first_key],
934    ///     vec![&second_key],
935    ///     vec![&third_key],
936    /// ];
937    /// let output = help.full_help_view(groups);
938    /// // May truncate rightmost columns if they don't fit
939    /// ```
940    pub fn full_help_view(&self, groups: Vec<Vec<&key::Binding>>) -> String {
941        if groups.is_empty() {
942            return String::new();
943        }
944
945        let mut columns = Vec::new();
946        let mut total_width = 0;
947        let separator = self
948            .styles
949            .full_separator
950            .clone()
951            .inline(true)
952            .render(&self.full_separator);
953
954        for group in groups.iter() {
955            if group.is_empty() || !should_render_column(group) {
956                continue;
957            }
958
959            // Build each row as "key description" within this column
960            let rows: Vec<String> = group
961                .iter()
962                .filter(|b| b.enabled())
963                .map(|b| {
964                    let help = b.help();
965                    let key_part = self.styles.full_key.clone().inline(true).render(&help.key);
966                    let desc_part = self
967                        .styles
968                        .full_desc
969                        .clone()
970                        .inline(true)
971                        .render(&help.desc);
972                    format!("{} {}", key_part, desc_part)
973                })
974                .collect();
975
976            let col_content = rows.join("\n");
977
978            // For the first column, we don't need a separator
979            // For subsequent columns, we'll add them during horizontal joining
980            let col_str = col_content;
981
982            let col_width = lipgloss::width_visible(&col_str);
983
984            if let Some(tail) = self.should_add_item(total_width, col_width) {
985                if !tail.is_empty() {
986                    columns.push(tail);
987                }
988                break;
989            }
990
991            total_width += col_width;
992            columns.push(col_str);
993        }
994
995        // Join columns with separators between them
996        let mut result_parts = Vec::new();
997        for (i, col) in columns.iter().enumerate() {
998            if i > 0 {
999                result_parts.push(separator.as_str());
1000            }
1001            result_parts.push(col.as_str());
1002        }
1003
1004        lipgloss::join_horizontal(lipgloss::TOP, &result_parts)
1005    }
1006
1007    /// Determines if an item can be added to the view without exceeding the width limit.
1008    ///
1009    /// This helper function implements smart width management by checking if adding
1010    /// an item would exceed the configured width limit. It provides graceful handling
1011    /// of width constraints with appropriate truncation indicators.
1012    ///
1013    /// # Width Management Strategy
1014    ///
1015    /// 1. **No Limit**: If `width` is 0, all items can be added
1016    /// 2. **Within Limit**: If `total_width + item_width ≤ width`, item fits
1017    /// 3. **Exceeds Limit**: If adding the item would exceed width:
1018    ///    - Try to add an ellipsis ("…") if it fits
1019    ///    - Return empty string if even ellipsis won't fit
1020    ///
1021    /// # Arguments
1022    ///
1023    /// * `total_width` - Current accumulated width of content in characters.
1024    ///   This should include all previously added content and separators.
1025    /// * `item_width` - Width of the item being considered for addition,
1026    ///   measured in visible characters (ignoring ANSI codes).
1027    ///
1028    /// # Returns
1029    ///
1030    /// * `None` - Item can be added without exceeding width constraints
1031    /// * `Some(String)` - Item cannot be added. The string contains:
1032    ///   - Styled ellipsis if it fits within the remaining width
1033    ///   - Empty string if even the ellipsis would exceed the width
1034    ///
1035    /// # Width Calculation
1036    ///
1037    /// Width calculations use `lipgloss::width_visible()` to properly handle:
1038    /// - ANSI color codes (don't count toward width)
1039    /// - Unicode characters (count as their display width)
1040    /// - Multi-byte characters (count correctly)
1041    ///
1042    /// # Examples
1043    ///
1044    /// This method is used internally by the help component and is not part
1045    /// of the public API. The width management behavior can be observed through
1046    /// the public `short_help_view()` and `full_help_view()` methods when a
1047    /// width constraint is set using `with_width()`.
1048    ///
1049    /// # Internal Usage
1050    ///
1051    /// This method is used internally by both `short_help_view()` and
1052    /// `full_help_view()` to implement consistent width management across
1053    /// different help display modes.
1054    ///
1055    /// # Panics
1056    ///
1057    /// This function does not panic under normal circumstances. It handles
1058    /// edge cases gracefully:
1059    /// - Width calculations that might underflow (uses saturating arithmetic)
1060    /// - Empty strings and zero widths
1061    /// - Very large width values
1062    fn should_add_item(&self, total_width: usize, item_width: usize) -> Option<String> {
1063        if self.width > 0 && total_width + item_width > self.width {
1064            let tail = format!(
1065                " {}",
1066                self.styles
1067                    .ellipsis
1068                    .clone()
1069                    .inline(true)
1070                    .render(&self.ellipsis)
1071            );
1072            if total_width + lipgloss::width_visible(&tail) < self.width {
1073                return Some(tail);
1074            }
1075            return Some("".to_string());
1076        }
1077        None // Item can be added
1078    }
1079
1080    /// Creates a new help model with default settings.
1081    ///
1082    /// **Deprecated**: Use [`Model::new`] instead.
1083    ///
1084    /// This function provides backwards compatibility with earlier versions
1085    /// of the library and matches the Go implementation's deprecated `NewModel`
1086    /// variable.
1087    ///
1088    /// # Examples
1089    ///
1090    /// ```rust
1091    /// # #[allow(deprecated)]
1092    /// use bubbletea_widgets::help::Model;
1093    ///
1094    /// let help = Model::new_model(); // Deprecated
1095    /// let help = Model::new();       // Preferred
1096    /// ```
1097    #[deprecated(since = "0.1.11", note = "Use Model::new() instead")]
1098    pub fn new_model() -> Self {
1099        Self::new()
1100    }
1101}
1102
1103/// Determines if a column of key bindings should be rendered.
1104///
1105/// A column should be rendered if it contains at least one enabled binding.
1106/// This helper function matches the behavior of the Go implementation's
1107/// `shouldRenderColumn` function and provides consistent column visibility
1108/// logic across the help system.
1109///
1110/// # Purpose
1111///
1112/// This function prevents empty columns from appearing in the help display:
1113/// - **Enabled Bindings**: Columns with active key bindings are shown
1114/// - **Disabled Bindings**: Columns with only disabled bindings are hidden
1115/// - **Mixed Columns**: Columns with some enabled bindings are shown
1116/// - **Empty Columns**: Completely empty columns are hidden
1117///
1118/// # Use Cases
1119///
1120/// ## Context-Sensitive Help
1121/// ```rust
1122/// # use bubbletea_widgets::help::should_render_column;
1123/// # use bubbletea_widgets::key::Binding;
1124/// # use crossterm::event::KeyCode;
1125/// // In a text editor, cut/copy might be disabled when no text is selected
1126/// let cut_key = Binding::new(vec![KeyCode::Char('x')])
1127///     .with_help("x", "cut")
1128///     .with_disabled(); // Disabled because nothing selected
1129///     
1130/// let copy_key = Binding::new(vec![KeyCode::Char('c')])
1131///     .with_help("c", "copy")
1132///     .with_disabled(); // Also disabled
1133///     
1134/// let edit_column = vec![&cut_key, &copy_key];
1135/// assert!(!should_render_column(&edit_column)); // Hidden - all disabled
1136/// ```
1137///
1138/// ## Progressive Disclosure
1139/// ```rust
1140/// # use bubbletea_widgets::help::should_render_column;
1141/// # use bubbletea_widgets::key::Binding;
1142/// # use crossterm::event::KeyCode;
1143/// // Advanced features might be disabled for beginners
1144/// let basic_key = Binding::new(vec![KeyCode::Char('s')])
1145///     .with_help("s", "save"); // Always enabled
1146///     
1147/// let advanced_key = Binding::new(vec![KeyCode::Char('m')])
1148///     .with_help("m", "macro")
1149///     .with_disabled(); // Disabled in beginner mode
1150///     
1151/// let mixed_column = vec![&basic_key, &advanced_key];
1152/// assert!(should_render_column(&mixed_column)); // Shown - has enabled binding
1153/// ```
1154///
1155/// # Arguments
1156///
1157/// * `bindings` - A slice of key binding references to check.
1158///   Typically represents a logical group of related key bindings
1159///   that would form a column in the help display.
1160///
1161/// # Returns
1162///
1163/// * `true` - The column should be rendered because it contains at least
1164///   one enabled binding that users can actually use.
1165/// * `false` - The column should be hidden because all bindings are
1166///   disabled or the column is empty.
1167///
1168/// # Performance
1169///
1170/// This function uses early return optimization - it stops checking
1171/// as soon as it finds the first enabled binding, making it efficient
1172/// for columns with many bindings.
1173///
1174/// # Examples
1175///
1176/// ## All Bindings Enabled
1177/// ```rust
1178/// use bubbletea_widgets::help::should_render_column;
1179/// use bubbletea_widgets::key::Binding;
1180/// use crossterm::event::KeyCode;
1181///
1182/// let save_key = Binding::new(vec![KeyCode::Char('s')]).with_help("s", "save");
1183/// let quit_key = Binding::new(vec![KeyCode::Char('q')]).with_help("q", "quit");
1184///
1185/// let column = vec![&save_key, &quit_key];
1186/// assert!(should_render_column(&column)); // Show column
1187/// ```
1188///
1189/// ## All Bindings Disabled  
1190/// ```rust
1191/// # use bubbletea_widgets::help::should_render_column;
1192/// # use bubbletea_widgets::key::Binding;
1193/// # use crossterm::event::KeyCode;
1194/// let disabled1 = Binding::new(vec![KeyCode::F(1)]).with_disabled();
1195/// let disabled2 = Binding::new(vec![KeyCode::F(2)]).with_disabled();
1196///
1197/// let column = vec![&disabled1, &disabled2];
1198/// assert!(!should_render_column(&column)); // Hide column
1199/// ```
1200///
1201/// ## Mixed State
1202/// ```rust
1203/// # use bubbletea_widgets::help::should_render_column;
1204/// # use bubbletea_widgets::key::Binding;
1205/// # use crossterm::event::KeyCode;
1206/// let enabled = Binding::new(vec![KeyCode::Enter]).with_help("enter", "select");
1207/// let disabled = Binding::new(vec![KeyCode::Delete]).with_disabled();
1208///
1209/// let column = vec![&enabled, &disabled];
1210/// assert!(should_render_column(&column)); // Show column (has enabled binding)
1211/// ```
1212///
1213/// ## Empty Column
1214/// ```rust
1215/// # use bubbletea_widgets::help::should_render_column;
1216/// let empty_column = vec![];
1217/// assert!(!should_render_column(&empty_column)); // Hide empty column
1218/// ```
1219pub fn should_render_column(bindings: &[&key::Binding]) -> bool {
1220    for binding in bindings {
1221        if binding.enabled() {
1222            return true;
1223        }
1224    }
1225    false
1226}