promkit/preset/
checkbox.rs

1//! Provides a checkbox interface for multiple options selection.
2
3use std::fmt::Display;
4
5use crate::{
6    core::{
7        crossterm::{
8            self,
9            event::Event,
10            style::{Attribute, Attributes, Color, ContentStyle},
11        },
12        render::{Renderer, SharedRenderer},
13        PaneFactory,
14    },
15    preset::Evaluator,
16    widgets::{
17        checkbox,
18        text::{self, Text},
19    },
20    Signal,
21};
22
23pub mod evaluate;
24
25/// Represents the indices of various components in the checkbox preset.
26#[derive(PartialEq, Eq, PartialOrd, Ord)]
27pub enum Index {
28    Title = 0,
29    Checkbox = 1,
30}
31
32/// Represents a checkbox component for creating
33/// and managing a list of selectable options.
34pub struct Checkbox {
35    /// Shared renderer for the prompt, allowing for rendering of UI components.
36    pub renderer: Option<SharedRenderer<Index>>,
37    /// Function to evaluate the input events and update the state of the prompt.
38    pub evaluator: Evaluator<Self>,
39    /// State for the title displayed above the checkbox list.
40    pub title: text::State,
41    /// State for the checkbox list itself.
42    pub checkbox: checkbox::State,
43}
44
45#[async_trait::async_trait]
46impl crate::Prompt for Checkbox {
47    async fn initialize(&mut self) -> anyhow::Result<()> {
48        let size = crossterm::terminal::size()?;
49        self.renderer = Some(SharedRenderer::new(
50            Renderer::try_new_with_panes(
51                [
52                    (Index::Title, self.title.create_pane(size.0, size.1)),
53                    (Index::Checkbox, self.checkbox.create_pane(size.0, size.1)),
54                ],
55                true,
56            )
57            .await?,
58        ));
59        Ok(())
60    }
61
62    async fn evaluate(&mut self, event: &Event) -> anyhow::Result<Signal> {
63        let ret = (self.evaluator)(event, self).await;
64        let size = crossterm::terminal::size()?;
65        self.render(size.0, size.1).await?;
66        ret
67    }
68
69    type Return = Vec<String>;
70
71    fn finalize(&mut self) -> anyhow::Result<Self::Return> {
72        Ok(self
73            .checkbox
74            .checkbox
75            .get()
76            .iter()
77            .map(|e| e.to_string())
78            .collect())
79    }
80}
81
82impl Checkbox {
83    fn new_with_checkbox(checkbox: checkbox::Checkbox) -> Self {
84        Self {
85            renderer: None,
86            evaluator: |event, ctx| Box::pin(evaluate::default(event, ctx)),
87            title: text::State {
88                style: ContentStyle {
89                    attributes: Attributes::from(Attribute::Bold),
90                    ..Default::default()
91                },
92                ..Default::default()
93            },
94            checkbox: checkbox::State {
95                checkbox,
96                cursor: String::from("❯ "),
97                active_mark: '☒',
98                inactive_mark: '☐',
99                active_item_style: ContentStyle {
100                    foreground_color: Some(Color::DarkCyan),
101                    ..Default::default()
102                },
103                inactive_item_style: ContentStyle::default(),
104                lines: Default::default(),
105            },
106        }
107    }
108
109    /// Creates a new `Checkbox` instance with the provided items.
110    pub fn new<T: Display, I: IntoIterator<Item = T>>(items: I) -> Self {
111        Self::new_with_checkbox(checkbox::Checkbox::from_displayable(items))
112    }
113
114    /// Creates a new `Checkbox` instance with the provided items and their checked states.
115    pub fn new_with_checked<T: Display, I: IntoIterator<Item = (T, bool)>>(items: I) -> Self {
116        Self::new_with_checkbox(checkbox::Checkbox::new_with_checked(items))
117    }
118
119    /// Sets the title text displayed above the checkbox list.
120    pub fn title<T: AsRef<str>>(mut self, text: T) -> Self {
121        self.title.text = Text::from(text);
122        self
123    }
124
125    /// Sets the style for the title text.
126    pub fn title_style(mut self, style: ContentStyle) -> Self {
127        self.title.style = style;
128        self
129    }
130
131    /// Sets the cursor symbol used to indicate the current selection.
132    pub fn cursor<T: AsRef<str>>(mut self, cursor: T) -> Self {
133        self.checkbox.cursor = cursor.as_ref().to_string();
134        self
135    }
136
137    /// Sets the mark symbol used to indicate selected items.
138    pub fn active_mark(mut self, mark: char) -> Self {
139        self.checkbox.active_mark = mark;
140        self
141    }
142
143    /// Sets the style for active (currently selected) items.
144    pub fn active_item_style(mut self, style: ContentStyle) -> Self {
145        self.checkbox.active_item_style = style;
146        self
147    }
148
149    /// Sets the style for inactive (not currently selected) items.
150    pub fn inactive_item_style(mut self, style: ContentStyle) -> Self {
151        self.checkbox.inactive_item_style = style;
152        self
153    }
154
155    /// Sets the number of lines to be used for displaying the checkbox list.
156    pub fn checkbox_lines(mut self, lines: usize) -> Self {
157        self.checkbox.lines = Some(lines);
158        self
159    }
160
161    /// Sets the evaluator function for handling input events.
162    pub fn evaluator(mut self, evaluator: Evaluator<Self>) -> Self {
163        self.evaluator = evaluator;
164        self
165    }
166
167    /// Render the prompt with the specified width and height.
168    async fn render(&mut self, width: u16, height: u16) -> anyhow::Result<()> {
169        match self.renderer.as_ref() {
170            Some(renderer) => {
171                renderer
172                    .update([
173                        (Index::Title, self.title.create_pane(width, height)),
174                        (Index::Checkbox, self.checkbox.create_pane(width, height)),
175                    ])
176                    .render()
177                    .await
178            }
179            None => Err(anyhow::anyhow!("Renderer not initialized")),
180        }
181    }
182}