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#[derive(Copy, Clone)]
24enum CurrentSelect {
25 User = 0,
26 Ip,
27 Port,
28 Password,
29 Name,
30 Shell,
31}
32
33impl 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
102pub struct ServerCreator<'a> {
104 input: Vec<String>,
106 character_index: usize,
108 current_select: CurrentSelect,
110 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#[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 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 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 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 let current_index = self.character_index;
317 let from_left_to_current_index = current_index - 1;
318
319 let before_char_to_delete = self.input[self.current_select as usize]
321 .chars()
322 .take(from_left_to_current_index);
323 let after_char_to_delete = self.input[self.current_select as usize]
325 .chars()
326 .skip(current_index);
327
328 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 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 if to_insert == 'c' {
369 if key.modifiers == event::KeyModifiers::CONTROL {
370 return Ok(false);
371 }
372 }
373 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 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 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 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}