1use ratatui::{
2 backend::FromCrossterm,
3 buffer::Buffer,
4 layout::{Rect, Size},
5 style::{Color, Style},
6 text::{Line, Span, Text},
7 widgets::Widget,
8};
9
10use crate::{
11 config::Theme,
12 model::Command,
13 utils::{COMMAND_VARIABLE_REGEX, SplitCaptures, SplitItem, truncate_spans_with_ellipsis},
14};
15
16const DEFAULT_STYLE: Style = Style::new();
17
18const DESCRIPTION_WIDTH_PERCENT: f32 = 0.3;
20
21#[derive(Clone)]
23pub struct CommandWidget<'a>(CommandWidgetInner<'a>, Size);
24impl<'a> CommandWidget<'a> {
25 pub fn new(
27 command: &'a Command,
28 theme: &Theme,
29 inline: bool,
30 is_highlighted: bool,
31 is_discarded: bool,
32 plain_style: bool,
33 ) -> Self {
34 let mut line_style = DEFAULT_STYLE;
35 if is_highlighted && let Some(bg_color) = theme.highlight {
36 line_style = line_style.bg(Color::from_crossterm(bg_color));
37 }
38
39 let (primary_style, secondary_style, comment_style, accent_style) =
41 match (plain_style, is_discarded, is_highlighted) {
42 (_, true, false) => (theme.secondary, theme.secondary, theme.secondary, theme.secondary),
44 (_, true, true) => (
46 theme.highlight_secondary,
47 theme.highlight_secondary,
48 theme.highlight_secondary,
49 theme.highlight_secondary,
50 ),
51 (true, false, false) => (theme.primary, theme.primary, theme.comment, theme.accent),
53 (true, false, true) => (
55 theme.highlight_primary,
56 theme.highlight_primary,
57 theme.highlight_comment,
58 theme.highlight_accent,
59 ),
60 (false, false, false) => (theme.primary, theme.secondary, theme.comment, theme.accent),
62 (false, false, true) => (
64 theme.highlight_primary,
65 theme.highlight_secondary,
66 theme.highlight_comment,
67 theme.highlight_accent,
68 ),
69 };
70
71 let destructive_style = if is_highlighted {
72 theme.highlight_destructive
73 } else {
74 theme.destructive
75 };
76 let destructive_secondary_style = if is_highlighted {
77 theme.highlight_destructive_secondary
78 } else {
79 theme.destructive_secondary
80 };
81
82 let is_destructive = command.is_destructive;
83 let (cmd_style, cmd_secondary_style) = if is_destructive {
84 (destructive_style, destructive_secondary_style)
85 } else {
86 (primary_style, secondary_style)
87 };
88
89 let cmd_splitter = SplitCaptures::new(&COMMAND_VARIABLE_REGEX, &command.cmd);
91 let cmd_spans = cmd_splitter
92 .map(|e| match e {
93 SplitItem::Unmatched(t) => Span::styled(t, Style::from_crossterm(cmd_style)),
94 SplitItem::Captured(l) => {
95 Span::styled(l.get(0).unwrap().as_str(), Style::from_crossterm(cmd_secondary_style))
96 }
97 })
98 .collect::<Vec<_>>();
99
100 if inline {
101 let mut description_spans = Vec::new();
103 if command.description.is_some() || command.alias.is_some() {
104 description_spans.push(Span::styled(" # ", Style::from_crossterm(comment_style)));
105 if let Some(ref alias) = command.alias {
106 description_spans.push(Span::styled("[", Style::from_crossterm(accent_style)));
107 description_spans.push(Span::styled(alias, Style::from_crossterm(accent_style)));
108 description_spans.push(Span::styled("] ", Style::from_crossterm(accent_style)));
109 }
110 if let Some(ref description) = command.description
111 && let Some(line) = description.lines().next()
112 {
113 description_spans.push(Span::styled(line, Style::from_crossterm(comment_style)));
114 }
115 }
116
117 let total_width = cmd_spans.iter().map(|s| s.width() as u16).sum::<u16>()
119 + description_spans.iter().map(|s| s.width() as u16).sum::<u16>();
120
121 let renderer = InlineCommandRenderer {
122 cmd_spans,
123 description_spans,
124 line_style,
125 };
126 Self(CommandWidgetInner::Inline(renderer), Size::new(total_width, 1))
127 } else {
128 let mut lines = Vec::new();
130 if let Some(ref description) = command.description {
131 let mut alias_included = command.alias.is_none();
132 for line in description.lines() {
133 if !alias_included && let Some(ref alias) = command.alias {
134 let parts = vec![
135 Span::styled("# ", Style::from_crossterm(comment_style)),
136 Span::styled("[", Style::from_crossterm(accent_style)),
137 Span::styled(alias, Style::from_crossterm(accent_style)),
138 Span::styled("] ", Style::from_crossterm(accent_style)),
139 Span::styled(line, Style::from_crossterm(comment_style)),
140 ];
141 lines.push(Line::from(parts));
142 alias_included = true;
143 } else {
144 lines.push(
145 Line::from(vec![Span::raw("# "), Span::raw(line)])
146 .style(Style::from_crossterm(comment_style)),
147 );
148 }
149 }
150 } else if let Some(ref alias) = command.alias {
151 let parts = vec![
152 Span::styled("# ", Style::from_crossterm(comment_style)),
153 Span::styled("[", Style::from_crossterm(accent_style)),
154 Span::styled(alias, Style::from_crossterm(accent_style)),
155 Span::styled("]", Style::from_crossterm(accent_style)),
156 ];
157 lines.push(Line::from(parts));
158 }
159 let mut parts = Vec::new();
160 cmd_spans.into_iter().for_each(|s| parts.push(s));
161 lines.push(Line::from(parts));
162
163 let text = Text::from(lines).style(line_style);
164 let width = text.width() as u16;
165 let height = text.height() as u16;
166
167 Self(CommandWidgetInner::Block(text), Size::new(width, height))
168 }
169 }
170
171 pub fn size(&self) -> Size {
173 self.1
174 }
175}
176
177impl<'a> Widget for CommandWidget<'a> {
178 fn render(self, area: Rect, buf: &mut Buffer)
179 where
180 Self: Sized,
181 {
182 match self.0 {
183 CommandWidgetInner::Inline(w) => w.render(area, buf),
184 CommandWidgetInner::Block(w) => w.render(area, buf),
185 }
186 }
187}
188
189#[derive(Clone)]
191enum CommandWidgetInner<'a> {
192 Inline(InlineCommandRenderer<'a>),
193 Block(Text<'a>),
194}
195
196#[derive(Clone)]
198struct InlineCommandRenderer<'a> {
199 cmd_spans: Vec<Span<'a>>,
200 description_spans: Vec<Span<'a>>,
201 line_style: Style,
202}
203impl<'a> Widget for InlineCommandRenderer<'a> {
204 fn render(self, area: Rect, buf: &mut Buffer) {
205 if area.width == 0 || area.height == 0 {
206 return;
207 }
208
209 buf.set_style(area, self.line_style);
211
212 let cmd_width: u16 = self.cmd_spans.iter().map(|s| s.width() as u16).sum();
214 let desc_width: u16 = self.description_spans.iter().map(|s| s.width() as u16).sum();
215 let total_width = cmd_width.saturating_add(desc_width);
216
217 if total_width <= area.width {
219 let mut combined_spans = self.cmd_spans;
220 combined_spans.extend(self.description_spans);
221 buf.set_line(area.x, area.y, &Line::from(combined_spans), area.width);
222 } else {
223 let min_description_width = (area.width as f32 * DESCRIPTION_WIDTH_PERCENT).floor() as u16;
225 let desired_desc_width = desc_width.min(min_description_width);
226 let available_space_for_cmd = area.width.saturating_sub(desired_desc_width);
227
228 if cmd_width <= available_space_for_cmd {
230 buf.set_line(area.x, area.y, &Line::from(self.cmd_spans), cmd_width);
232
233 let remaining_space = area.width.saturating_sub(cmd_width);
235 if remaining_space > 0 {
236 let (truncated_desc_spans, _) =
237 truncate_spans_with_ellipsis(&self.description_spans, remaining_space);
238 buf.set_line(
239 area.x + cmd_width,
240 area.y,
241 &Line::from(truncated_desc_spans),
242 remaining_space,
243 );
244 }
245 } else {
246 let (truncated_desc_spans, truncated_desc_width) =
248 truncate_spans_with_ellipsis(&self.description_spans, desired_desc_width);
249
250 if truncated_desc_width > 0 {
251 let desc_start_x = area.x + area.width.saturating_sub(truncated_desc_width);
252 buf.set_line(
253 desc_start_x,
254 area.y,
255 &Line::from(truncated_desc_spans),
256 truncated_desc_width,
257 );
258 }
259
260 let final_cmd_width = area.width.saturating_sub(truncated_desc_width);
261 if final_cmd_width > 0 {
262 let (truncated_cmd_spans, _) = truncate_spans_with_ellipsis(&self.cmd_spans, final_cmd_width);
263 buf.set_line(area.x, area.y, &Line::from(truncated_cmd_spans), final_cmd_width);
264 }
265 }
266 }
267 }
268}