ssh_utils_lib/widgets/
server_creator.rs

1use anyhow::Result;
2use crossterm::event::{self, Event, KeyCode, KeyEventKind};
3use ratatui::{
4    backend::Backend,
5    buffer::Buffer,
6    layout::{Constraint, Layout, Rect},
7    style::{Style, Stylize},
8    text::{Line, Span, Text},
9    widgets::{Paragraph, Widget},
10    Frame, Terminal,
11};
12use std::ops::{Add, Sub};
13
14use crate::{
15    config::{
16        app_config::{Config, Server},
17        app_vault::{self, decrypt_password, encrypt_password, EncryptionKey, Vault},
18    },
19    helper::convert_to_array,
20};
21
22/// current selected item in form
23#[derive(Copy, Clone)]
24enum CurrentSelect {
25    User = 0,
26    Ip,
27    Port,
28    Password,
29    Name,
30    Shell,
31}
32
33/// impl Add and Sub for CurrentSelect
34impl Add for CurrentSelect {
35    type Output = Self;
36
37    fn add(self, other: Self) -> Self {
38        let new_value = (self as isize + other as isize) % 6;
39        match new_value {
40            0 => CurrentSelect::User,
41            1 => CurrentSelect::Ip,
42            2 => CurrentSelect::Port,
43            3 => CurrentSelect::Password,
44            4 => CurrentSelect::Name,
45            5 => CurrentSelect::Shell,
46            _ => unreachable!(),
47        }
48    }
49}
50
51impl Sub for CurrentSelect {
52    type Output = Self;
53
54    fn sub(self, other: Self) -> Self {
55        let new_value = (self as isize - other as isize + 6) % 6;
56        match new_value {
57            0 => CurrentSelect::User,
58            1 => CurrentSelect::Ip,
59            2 => CurrentSelect::Port,
60            3 => CurrentSelect::Password,
61            4 => CurrentSelect::Name,
62            5 => CurrentSelect::Shell,
63            _ => unreachable!(),
64        }
65    }
66}
67
68impl Add<isize> for CurrentSelect {
69    type Output = Self;
70
71    fn add(self, other: isize) -> Self {
72        let new_value = (self as isize + other).rem_euclid(6);
73        match new_value {
74            0 => CurrentSelect::User,
75            1 => CurrentSelect::Ip,
76            2 => CurrentSelect::Port,
77            3 => CurrentSelect::Password,
78            4 => CurrentSelect::Name,
79            5 => CurrentSelect::Shell,
80            _ => unreachable!(),
81        }
82    }
83}
84
85impl Sub<isize> for CurrentSelect {
86    type Output = Self;
87
88    fn sub(self, other: isize) -> Self {
89        let new_value = (self as isize - other).rem_euclid(6);
90        match new_value {
91            0 => CurrentSelect::User,
92            1 => CurrentSelect::Ip,
93            2 => CurrentSelect::Port,
94            3 => CurrentSelect::Password,
95            4 => CurrentSelect::Name,
96            5 => CurrentSelect::Shell,
97            _ => unreachable!(),
98        }
99    }
100}
101
102/// App holds the state of the application
103pub struct ServerCreator<'a> {
104    /// Current values of the input boxes
105    input: Vec<String>,
106    /// Position of cursor in the editor area.
107    character_index: usize,
108    /// current selected item
109    current_select: CurrentSelect,
110    /// vault
111    vault: &'a mut Vault,
112    config: &'a mut Config,
113    encryption_key: &'a EncryptionKey,
114    mode: CreatorMode,
115    server_id: Option<String>,
116}
117
118// impl Widget for &mut ServerCreator {
119//     fn render(self, area: Rect, buf: &mut Buffer) {
120//         let vertical = Layout::vertical([
121//             Constraint::Length(1),
122//             Constraint::Min(0),
123//             Constraint::Length(1)
124//         ]);
125//         let [head_area, body_area, foot_area] = vertical.areas(area);
126//         self.form_position = (body_area.x, body_area.y);
127//         self.render_header(head_area, buf);
128//         self.render_form(body_area, buf);
129//         self.render_footer(foot_area, buf);
130//     }
131// }
132#[derive(PartialEq)]
133enum CreatorMode {
134    New,
135    Edit,
136}
137
138impl<'a> ServerCreator<'a> {
139    pub fn new(
140        vault: &'a mut Vault,
141        config: &'a mut Config,
142        encryption_key: &'a EncryptionKey,
143    ) -> Self {
144        Self {
145            input: vec![
146                String::new(),
147                String::new(),
148                "22".to_string(),
149                String::new(),
150                String::new(),
151                "bash".to_string(),
152            ],
153            character_index: 0,
154            current_select: CurrentSelect::User,
155            vault,
156            config,
157            encryption_key,
158            mode: CreatorMode::New,
159            server_id: None,
160        }
161    }
162
163    pub fn new_edit(
164        vault: &'a mut Vault,
165        config: &'a mut Config,
166        encryption_key: &'a EncryptionKey,
167        server_id: &str,
168    ) -> Result<Self> {
169        let server = config
170            .servers
171            .iter()
172            .find(|s| s.id == server_id)
173            .ok_or_else(|| anyhow::anyhow!("can't find server."))?;
174        let password = vault
175            .servers
176            .iter()
177            .find(|s| s.id == server_id)
178            .ok_or_else(|| anyhow::anyhow!("can't find server password."))?
179            .password
180            .clone();
181        let decrypted_password = decrypt_password(
182            &server_id,
183            &password,
184            &convert_to_array(&encryption_key)
185                .map_err(|e| anyhow::anyhow!("encryption key convert failed: {}", e))?,
186        )?;
187        Ok(Self {
188            input: vec![
189                server.user.clone(),
190                server.ip.clone(),
191                server.port.to_string(),
192                decrypted_password,
193                server.name.clone(),
194                server.shell.clone(),
195            ],
196            character_index: 0,
197            current_select: CurrentSelect::User,
198            vault,
199            config,
200            encryption_key,
201            mode: CreatorMode::Edit,
202            server_id: Some(server_id.to_string()),
203        })
204    }
205
206    fn render_header(&self, area: Rect, buf: &mut Buffer) {
207        let text = Text::from("Enter server details below:").yellow();
208        Widget::render(text, area, buf);
209    }
210
211    fn render_footer(&self, area: Rect, buf: &mut Buffer) {
212        let text = Text::from("  Save (^S), Quit (ESC)").dim();
213        Widget::render(text, area, buf);
214    }
215
216    fn render_form(&self, area: Rect, buf: &mut Buffer) {
217        // highlight currently selected item
218        let mut user: Vec<Span> = vec![
219            "    user:".into(),
220            self.input[CurrentSelect::User as usize].clone().into(),
221        ];
222        let mut ip: Vec<Span> = vec![
223            "      ip:".into(),
224            self.input[CurrentSelect::Ip as usize].clone().into(),
225        ];
226        let mut port: Vec<Span> = vec![
227            "    port:".into(),
228            self.input[CurrentSelect::Port as usize].clone().into(),
229        ];
230        // we use * to replace the password
231        let password_length = self.input[CurrentSelect::Password as usize].len();
232        let masked_password: String = "*".repeat(password_length);
233        let mut password: Vec<Span> = vec!["password:".into(), masked_password.into()];
234        let mut name: Vec<Span> = vec![
235            "    name:".into(),
236            self.input[CurrentSelect::Name as usize].clone().into(),
237        ];
238        let mut shell: Vec<Span> = vec![
239            "   shell:".into(),
240            self.input[CurrentSelect::Shell as usize].clone().into(),
241        ];
242
243        match self.current_select {
244            CurrentSelect::User => user[0] = Span::styled("    user:", Style::new().bold()),
245            CurrentSelect::Ip => ip[0] = Span::styled("      ip:", Style::new().bold()),
246            CurrentSelect::Port => port[0] = Span::styled("    port:", Style::new().bold()),
247            CurrentSelect::Password => password[0] = Span::styled("password:", Style::new().bold()),
248            CurrentSelect::Name => name[0] = Span::styled("    name:", Style::new().bold()),
249            CurrentSelect::Shell => shell[0] = Span::styled("   shell:", Style::new().bold()),
250        }
251
252        let user_line = Line::from(user);
253        let ip_line = Line::from(ip);
254        let port_line = Line::from(port);
255        let password_line = if password_length == 0 {
256            password[1] =
257                Span::styled("leave empty to use the default SSH key", Style::new().dim());
258            Line::from(password)
259        } else {
260            Line::from(password)
261        };
262        let name_line = Line::from(name);
263        let shell_line = Line::from(shell);
264        let text = vec![
265            user_line,
266            ip_line,
267            port_line,
268            password_line,
269            name_line,
270            shell_line,
271        ];
272        let form = Paragraph::new(text);
273        Widget::render(&form, area, buf);
274    }
275
276    fn move_cursor_left(&mut self) {
277        let cursor_moved_left = self.character_index.saturating_sub(1);
278        self.character_index = self.clamp_cursor(cursor_moved_left);
279    }
280
281    fn move_cursor_right(&mut self) {
282        let cursor_moved_right = self.character_index.saturating_add(1);
283        self.character_index = self.clamp_cursor(cursor_moved_right);
284    }
285
286    fn moveto_current_cursor(&mut self) {
287        let cursor_position = self.character_index;
288        self.character_index = self.clamp_cursor(cursor_position);
289    }
290
291    fn enter_char(&mut self, new_char: char) {
292        let index = self.byte_index();
293        self.input[self.current_select as usize].insert(index, new_char);
294        self.move_cursor_right();
295    }
296
297    /// Returns the byte index based on the character position.
298    ///
299    /// Since each character in a string can be contain multiple bytes, it's necessary to calculate
300    /// the byte index based on the index of the character.
301    fn byte_index(&mut self) -> usize {
302        self.input[self.current_select as usize]
303            .char_indices()
304            .map(|(i, _)| i)
305            .nth(self.character_index)
306            .unwrap_or(self.input[self.current_select as usize].len())
307    }
308
309    fn delete_char(&mut self) {
310        let is_not_cursor_leftmost = self.character_index != 0;
311        if is_not_cursor_leftmost {
312            // Method "remove" is not used on the saved text for deleting the selected char.
313            // Reason: Using remove on String works on bytes instead of the chars.
314            // Using remove would require special care because of char boundaries.
315
316            let current_index = self.character_index;
317            let from_left_to_current_index = current_index - 1;
318
319            // Getting all characters before the selected character.
320            let before_char_to_delete = self.input[self.current_select as usize]
321                .chars()
322                .take(from_left_to_current_index);
323            // Getting all characters after selected character.
324            let after_char_to_delete = self.input[self.current_select as usize]
325                .chars()
326                .skip(current_index);
327
328            // Put all characters together except the selected one.
329            // By leaving the selected one out, it is forgotten and therefore deleted.
330            self.input[self.current_select as usize] =
331                before_char_to_delete.chain(after_char_to_delete).collect();
332            self.move_cursor_left();
333        }
334    }
335
336    fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
337        new_cursor_pos.clamp(0, self.input[self.current_select as usize].chars().count())
338    }
339
340    fn move_next_select_item(&mut self) {
341        self.current_select = self.current_select + 1;
342    }
343
344    fn move_pre_select_item(&mut self) {
345        self.current_select = self.current_select - 1;
346    }
347}
348
349impl<'a> ServerCreator<'a> {
350    fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
351        terminal.draw(|f| ui(f, &self))?;
352        Ok(())
353    }
354
355    /**
356     * Run and get a result
357     * true -> add a new server
358     * false -> cancelled
359     */
360    pub fn run(&mut self, mut terminal: &mut Terminal<impl Backend>) -> Result<bool> {
361        loop {
362            self.draw(&mut terminal)?;
363            if let Event::Key(key) = event::read()? {
364                if key.kind == KeyEventKind::Press {
365                    match key.code {
366                        KeyCode::Char(to_insert) => {
367                            // Set this hotkey because of man's habit
368                            if to_insert == 'c' {
369                                if key.modifiers == event::KeyModifiers::CONTROL {
370                                    return Ok(false);
371                                }
372                            }
373                            // Save current server's config
374                            if to_insert == 's' {
375                                if key.modifiers == event::KeyModifiers::CONTROL {
376                                    if self.input.iter().enumerate().any(|(i, input)| {
377                                        i != CurrentSelect::Password as usize && input.trim().is_empty()
378                                    }) {
379                                        continue;
380                                    }
381                                    let encryption_key = convert_to_array(&self.encryption_key)?;
382                                    let mut config_server = Server::new(
383                                        self.input[CurrentSelect::Name as usize].clone(),
384                                        self.input[CurrentSelect::Ip as usize].clone(),
385                                        self.input[CurrentSelect::User as usize].clone(),
386                                        self.input[CurrentSelect::Shell as usize].clone(),
387                                        self.input[CurrentSelect::Port as usize]
388                                            .parse::<u16>()
389                                            .unwrap_or(22),
390                                    );
391                                    if self.mode == CreatorMode::Edit {
392                                        let Some(server_id) = self.server_id.clone() else {
393                                            return Err(anyhow::anyhow!("Server ID not found"));
394                                        };
395                                        config_server.id = server_id;
396                                    }
397                                    let passwd = encrypt_password(
398                                        &config_server.id,
399                                        self.input[CurrentSelect::Password as usize]
400                                            .clone()
401                                            .as_str(),
402                                        &encryption_key,
403                                    )?;
404                                    let vault_server =
405                                        app_vault::Server::new(config_server.id.clone(), passwd);
406
407                                    if self
408                                        .config
409                                        .servers
410                                        .iter()
411                                        .find(|s| s.id == config_server.id)
412                                        .is_some()
413                                    {
414                                        // branch 1: modify server
415                                        self.config.modify_server(
416                                            config_server.id.as_str(),
417                                            config_server.clone(),
418                                        )?;
419                                        self.vault.modify_server(
420                                            config_server.id.as_str(),
421                                            vault_server,
422                                            &encryption_key,
423                                        )?;
424                                    } else {
425                                        //branch 2: add server
426                                        self.config.add_server(config_server.clone())?;
427                                        self.vault.add_server(vault_server, &encryption_key)?;
428                                    }
429                                    return Ok(true);
430                                }
431                            }
432                            self.enter_char(to_insert);
433                        }
434                        KeyCode::Backspace => {
435                            self.delete_char();
436                        }
437                        KeyCode::Left => {
438                            self.move_cursor_left();
439                        }
440                        KeyCode::Right => {
441                            self.move_cursor_right();
442                        }
443                        KeyCode::Esc => {
444                            return Ok(false);
445                        }
446                        KeyCode::Up => {
447                            self.move_pre_select_item();
448                            self.moveto_current_cursor();
449                        }
450                        KeyCode::Down | KeyCode::Enter | KeyCode::Tab => {
451                            self.move_next_select_item();
452                            self.moveto_current_cursor();
453                        }
454                        _ => {}
455                    }
456                }
457            }
458        }
459    }
460}
461
462fn ui(f: &mut Frame, server_creator: &ServerCreator) {
463    let vertical = Layout::vertical([
464        Constraint::Length(1),
465        Constraint::Min(0),
466        Constraint::Length(1),
467    ]);
468    let [head_area, body_area, foot_area] = vertical.areas(f.area());
469    server_creator.render_header(head_area, f.buffer_mut());
470    server_creator.render_form(body_area, f.buffer_mut());
471    server_creator.render_footer(foot_area, f.buffer_mut());
472    let character_index = server_creator.character_index as u16;
473    //due to input character index start at 9
474    //eg: "password:"
475    //so here add 9
476    let cursor_x = body_area.x + character_index + 9;
477    let cursor_y = body_area.y + server_creator.current_select as u16;
478    f.set_cursor_position((cursor_x, cursor_y));
479}