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