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