1use std::mem;
2
3use async_trait::async_trait;
4use color_eyre::Result;
5use enum_cycling::EnumCycle;
6use ratatui::{
7 Frame,
8 layout::{Constraint, Layout, Rect},
9};
10use semver::Version;
11use tracing::instrument;
12
13use super::Component;
14use crate::{
15 app::Action,
16 config::Theme,
17 errors::{InsertError, UpdateError},
18 format_msg,
19 model::Command,
20 process::ProcessOutput,
21 service::IntelliShellService,
22 widgets::{CustomTextArea, ErrorPopup, NewVersionBanner},
23};
24
25#[derive(strum::EnumIs)]
27pub enum EditCommandComponentMode {
28 New,
30 Edit { parent: Box<dyn Component> },
33}
34
35pub struct EditCommandComponent {
37 theme: Theme,
39 mode: EditCommandComponentMode,
41 service: IntelliShellService,
43 command: Command,
45 layout: Layout,
47 active_field: ActiveField,
49 alias: CustomTextArea<'static>,
51 cmd: CustomTextArea<'static>,
53 description: CustomTextArea<'static>,
55 new_version: NewVersionBanner,
57 error: ErrorPopup<'static>,
59}
60
61#[derive(Clone, Copy, PartialEq, Eq, EnumCycle)]
63enum ActiveField {
64 Alias,
65 Command,
66 Description,
67}
68
69impl EditCommandComponent {
70 pub fn new(
72 service: IntelliShellService,
73 theme: Theme,
74 inline: bool,
75 new_version: Option<Version>,
76 command: Command,
77 mode: EditCommandComponentMode,
78 ) -> Self {
79 let alias = CustomTextArea::new(
80 theme.secondary,
81 inline,
82 false,
83 command.alias.clone().unwrap_or_default(),
84 )
85 .title(if inline { "Alias:" } else { " Alias " });
86 let mut cmd = CustomTextArea::new(
87 theme.primary,
89 inline,
90 false,
91 &command.cmd,
92 )
93 .title(if inline { "Command:" } else { " Command " });
94 let mut description = CustomTextArea::new(
95 theme.primary,
96 inline,
97 true,
98 command.description.clone().unwrap_or_default(),
99 )
100 .title(if inline { "Description:" } else { " Description " });
101
102 let active_field = if mode.is_new() && !command.cmd.is_empty() && command.description.is_none() {
103 description.set_focus(true);
104 ActiveField::Description
105 } else {
106 cmd.set_focus(true);
107 ActiveField::Command
108 };
109
110 let new_version = NewVersionBanner::new(&theme, new_version);
111 let error = ErrorPopup::empty(&theme);
112
113 let layout = if inline {
114 Layout::vertical([Constraint::Length(1), Constraint::Length(1), Constraint::Min(3)])
115 } else {
116 Layout::vertical([Constraint::Length(3), Constraint::Length(3), Constraint::Min(5)]).margin(1)
117 };
118
119 Self {
120 theme,
121 service,
122 command,
123 mode,
124 layout,
125 active_field,
126 alias,
127 cmd,
128 description,
129 new_version,
130 error,
131 }
132 }
133
134 fn active_input(&mut self) -> &mut CustomTextArea<'static> {
136 match self.active_field {
137 ActiveField::Alias => &mut self.alias,
138 ActiveField::Command => &mut self.cmd,
139 ActiveField::Description => &mut self.description,
140 }
141 }
142
143 fn update_focus(&mut self) {
145 self.alias.set_focus(false);
146 self.cmd.set_focus(false);
147 self.description.set_focus(false);
148
149 self.active_input().set_focus(true);
150 }
151}
152
153#[async_trait]
154impl Component for EditCommandComponent {
155 fn name(&self) -> &'static str {
156 "EditCommandComponent"
157 }
158
159 fn min_inline_height(&self) -> u16 {
160 1 + 1 + 3
162 }
163
164 #[instrument(skip_all)]
165 fn render(&mut self, frame: &mut Frame, area: Rect) {
166 let [alias_area, cmd_area, description_area] = self.layout.areas(area);
168
169 frame.render_widget(&self.alias, alias_area);
171 frame.render_widget(&self.cmd, cmd_area);
172 frame.render_widget(&self.description, description_area);
173
174 self.new_version.render_in(frame, area);
176 self.error.render_in(frame, area);
177 }
178
179 fn tick(&mut self) -> Result<Action> {
180 self.error.tick();
181
182 Ok(Action::NoOp)
183 }
184
185 fn exit(&mut self) -> Result<Option<ProcessOutput>> {
186 Ok(Some(ProcessOutput::success().fileout(self.cmd.lines_as_string())))
187 }
188
189 fn move_up(&mut self) -> Result<Action> {
190 self.active_field = self.active_field.up();
191 self.update_focus();
192
193 Ok(Action::NoOp)
194 }
195
196 fn move_down(&mut self) -> Result<Action> {
197 self.active_field = self.active_field.down();
198 self.update_focus();
199
200 Ok(Action::NoOp)
201 }
202
203 fn move_left(&mut self, word: bool) -> Result<Action> {
204 self.active_input().move_cursor_left(word);
205
206 Ok(Action::NoOp)
207 }
208
209 fn move_right(&mut self, word: bool) -> Result<Action> {
210 self.active_input().move_cursor_right(word);
211
212 Ok(Action::NoOp)
213 }
214
215 fn move_prev(&mut self) -> Result<Action> {
216 self.move_up()
217 }
218
219 fn move_next(&mut self) -> Result<Action> {
220 self.move_down()
221 }
222
223 fn move_home(&mut self, absolute: bool) -> Result<Action> {
224 self.active_input().move_home(absolute);
225
226 Ok(Action::NoOp)
227 }
228
229 fn move_end(&mut self, absolute: bool) -> Result<Action> {
230 self.active_input().move_end(absolute);
231
232 Ok(Action::NoOp)
233 }
234
235 fn undo(&mut self) -> Result<Action> {
236 self.active_input().undo();
237
238 Ok(Action::NoOp)
239 }
240
241 fn redo(&mut self) -> Result<Action> {
242 self.active_input().redo();
243
244 Ok(Action::NoOp)
245 }
246
247 fn insert_text(&mut self, text: String) -> Result<Action> {
248 self.active_input().insert_str(text);
249
250 Ok(Action::NoOp)
251 }
252
253 fn insert_char(&mut self, c: char) -> Result<Action> {
254 self.active_input().insert_char(c);
255
256 Ok(Action::NoOp)
257 }
258
259 fn insert_newline(&mut self) -> Result<Action> {
260 self.active_input().insert_newline();
261
262 Ok(Action::NoOp)
263 }
264
265 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
266 self.active_input().delete(backspace, word);
267
268 Ok(Action::NoOp)
269 }
270
271 #[instrument(skip_all)]
272 async fn selection_confirm(&mut self) -> Result<Action> {
273 let command = self
275 .command
276 .clone()
277 .with_alias(Some(self.alias.lines_as_string()))
278 .with_cmd(self.cmd.lines_as_string())
279 .with_description(Some(self.description.lines_as_string()));
280
281 match &self.mode {
283 EditCommandComponentMode::New => match self.service.insert_command(command).await {
285 Ok(command) => Ok(Action::Quit(
286 ProcessOutput::success()
287 .stderr(format_msg!(
288 self.theme,
289 "Command stored: {}",
290 self.theme.secondary.apply(&command.cmd)
291 ))
292 .fileout(command.cmd),
293 )),
294 Err(InsertError::Invalid(err)) => {
295 tracing::warn!("{err}");
296 self.error.set_temp_message(err);
297 Ok(Action::NoOp)
298 }
299 Err(InsertError::AlreadyExists) => {
300 tracing::warn!("The command is already bookmarked");
301 self.error.set_temp_message("The command is already bookmarked");
302 Ok(Action::NoOp)
303 }
304 Err(InsertError::Unexpected(report)) => Err(report),
305 },
306 EditCommandComponentMode::Edit { .. } => {
308 match self.service.update_command(command).await {
309 Ok(_) => {
310 Ok(Action::SwitchComponent(
312 match mem::replace(&mut self.mode, EditCommandComponentMode::New) {
313 EditCommandComponentMode::Edit { parent } => parent,
314 EditCommandComponentMode::New => unreachable!(),
315 },
316 ))
317 }
318 Err(UpdateError::Invalid(err)) => {
319 tracing::warn!("{err}");
320 self.error.set_temp_message(err);
321 Ok(Action::NoOp)
322 }
323 Err(UpdateError::AlreadyExists) => {
324 tracing::warn!("The updated command already exists");
325 self.error.set_temp_message("The updated command already exists");
326 Ok(Action::NoOp)
327 }
328 Err(UpdateError::Unexpected(report)) => Err(report),
329 }
330 }
331 }
332 }
333
334 async fn selection_execute(&mut self) -> Result<Action> {
335 self.selection_confirm().await
336 }
337}