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, ©_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}