src/tui.rs 28.8 K raw
1
use arboard::Clipboard;
2
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
3
use ratatui::{
4
    DefaultTerminal,
5
    layout::{Constraint, Layout},
6
    style::{Color, Modifier, Style},
7
    text::{Line, Span, Text},
8
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Widget},
9
};
10
use crate::backend::Backend;
11
use crate::config;
12
use crate::db::Snippet;
13
use std::io::Cursor;
14
use std::path::PathBuf;
15
use std::time::{Duration, Instant};
16
use syntect::easy::HighlightLines;
17
use syntect::highlighting::Theme;
18
use syntect::parsing::SyntaxSet;
19
use syntect::util::LinesWithEndings;
20
21
enum Focus {
22
    List,
23
    Content,
24
    CreateName,
25
    CreateContent,
26
}
27
28
struct App {
29
    snippets: Vec<Snippet>,
30
    list_state: ListState,
31
    should_quit: bool,
32
    status_message: Option<(String, Instant)>,
33
    focus: Focus,
34
    content_scroll: u16,
35
    show_help: bool,
36
    syntax_set: SyntaxSet,
37
    theme: Theme,
38
    create_name: String,
39
    create_content: String,
40
    is_remote: bool,
41
    remote_url: Option<String>,
42
}
43
44
impl App {
45
    fn new(snippets: Vec<Snippet>, is_remote: bool, remote_url: Option<String>) -> Self {
46
        let mut list_state = ListState::default();
47
        if !snippets.is_empty() {
48
            list_state.select(Some(0));
49
        }
50
        let syntax_set = SyntaxSet::load_defaults_newlines();
51
        let theme_data = include_bytes!("ansi.tmTheme");
52
        let theme =
53
            syntect::highlighting::ThemeSet::load_from_reader(&mut Cursor::new(&theme_data[..]))
54
                .expect("failed to load base16 theme");
55
        Self {
56
            snippets,
57
            list_state,
58
            should_quit: false,
59
            status_message: None,
60
            focus: Focus::List,
61
            content_scroll: 0,
62
            show_help: false,
63
            syntax_set,
64
            theme,
65
            create_name: String::new(),
66
            create_content: String::new(),
67
            is_remote,
68
            remote_url,
69
        }
70
    }
71
72
    fn selected_snippet(&self) -> Option<&Snippet> {
73
        self.list_state.selected().and_then(|i| self.snippets.get(i))
74
    }
75
76
    fn move_up(&mut self) {
77
        if self.snippets.is_empty() {
78
            return;
79
        }
80
        let i = match self.list_state.selected() {
81
            Some(i) if i > 0 => i - 1,
82
            Some(_) => self.snippets.len() - 1,
83
            None => 0,
84
        };
85
        self.list_state.select(Some(i));
86
        self.content_scroll = 0;
87
    }
88
89
    fn move_down(&mut self) {
90
        if self.snippets.is_empty() {
91
            return;
92
        }
93
        let i = match self.list_state.selected() {
94
            Some(i) if i < self.snippets.len() - 1 => i + 1,
95
            Some(_) => 0,
96
            None => 0,
97
        };
98
        self.list_state.select(Some(i));
99
        self.content_scroll = 0;
100
    }
101
102
    fn scroll_up(&mut self) {
103
        self.content_scroll = self.content_scroll.saturating_sub(1);
104
    }
105
106
    fn scroll_down(&mut self, max_lines: u16) {
107
        if self.content_scroll < max_lines {
108
            self.content_scroll += 1;
109
        }
110
    }
111
112
    fn copy_selected(&mut self) {
113
        if let Some(snippet) = self.selected_snippet() {
114
            if let Ok(mut clipboard) = Clipboard::new() {
115
                let _ = clipboard.set_text(&snippet.content);
116
                self.status_message = Some(("Copied!".to_string(), Instant::now()));
117
            }
118
        }
119
    }
120
121
    fn copy_link(&mut self) {
122
        match &self.remote_url {
123
            Some(url) => {
124
                if let Some(snippet) = self.selected_snippet() {
125
                    let link = format!("{}/s/{}", url.trim_end_matches('/'), snippet.short_id);
126
                    if let Ok(mut clipboard) = Clipboard::new() {
127
                        let _ = clipboard.set_text(&link);
128
                        self.status_message =
129
                            Some(("Link copied!".to_string(), Instant::now()));
130
                    }
131
                }
132
            }
133
            None => {
134
                self.status_message =
135
                    Some(("No remote URL configured".to_string(), Instant::now()));
136
            }
137
        }
138
    }
139
140
    fn open_in_browser(&mut self) {
141
        match &self.remote_url {
142
            Some(url) => {
143
                if let Some(snippet) = self.selected_snippet() {
144
                    let link = format!("{}/s/{}", url.trim_end_matches('/'), snippet.short_id);
145
                    if let Err(e) = open::that(&link) {
146
                        self.status_message =
147
                            Some((format!("Failed to open browser: {}", e), Instant::now()));
148
                    } else {
149
                        self.status_message =
150
                            Some(("Opened in browser!".to_string(), Instant::now()));
151
                    }
152
                }
153
            }
154
            None => {
155
                self.status_message =
156
                    Some(("No remote URL configured".to_string(), Instant::now()));
157
            }
158
        }
159
    }
160
161
    fn delete_selected(&mut self, backend: &Backend) {
162
        if let Some(selected_index) = self.list_state.selected() {
163
            if let Some(snippet) = self.snippets.get(selected_index) {
164
                let short_id = snippet.short_id.clone();
165
                match backend.delete_snippet(&short_id) {
166
                    Ok(true) => {
167
                        self.snippets.remove(selected_index);
168
                        if self.snippets.is_empty() {
169
                            self.list_state.select(None);
170
                        } else if selected_index >= self.snippets.len() {
171
                            self.list_state.select(Some(self.snippets.len() - 1));
172
                        } else {
173
                            self.list_state.select(Some(selected_index));
174
                        }
175
                        self.status_message = Some(("Deleted!".to_string(), Instant::now()));
176
                    }
177
                    Ok(false) => {
178
                        self.status_message =
179
                            Some(("Snippet not found".to_string(), Instant::now()));
180
                    }
181
                    Err(e) => {
182
                        self.status_message = Some((e.to_string(), Instant::now()));
183
                    }
184
                }
185
            }
186
        }
187
    }
188
189
    fn refresh(&mut self, backend: &Backend) {
190
        match backend.list_snippets() {
191
            Ok(snippets) => {
192
                self.snippets = snippets;
193
                if self.snippets.is_empty() {
194
                    self.list_state.select(None);
195
                } else {
196
                    let idx = self.list_state.selected().unwrap_or(0);
197
                    if idx >= self.snippets.len() {
198
                        self.list_state.select(Some(self.snippets.len() - 1));
199
                    }
200
                }
201
                self.status_message = Some(("Refreshed!".to_string(), Instant::now()));
202
            }
203
            Err(e) => {
204
                self.status_message = Some((e.to_string(), Instant::now()));
205
            }
206
        }
207
    }
208
209
    fn start_create(&mut self) {
210
        self.create_name.clear();
211
        self.create_content.clear();
212
        self.focus = Focus::CreateName;
213
    }
214
215
    fn save_create(&mut self, backend: &Backend) {
216
        if self.create_name.trim().is_empty() {
217
            self.status_message = Some(("Name cannot be empty".to_string(), Instant::now()));
218
            return;
219
        }
220
        match backend.create_snippet(&self.create_name, &self.create_content) {
221
            Ok(snippet) => {
222
                self.snippets.insert(0, snippet);
223
                self.list_state.select(Some(0));
224
                self.status_message = Some(("Created!".to_string(), Instant::now()));
225
                self.focus = Focus::List;
226
                self.create_name.clear();
227
                self.create_content.clear();
228
            }
229
            Err(e) => {
230
                self.status_message = Some((e.to_string(), Instant::now()));
231
            }
232
        }
233
    }
234
235
    fn cancel_create(&mut self) {
236
        self.create_name.clear();
237
        self.create_content.clear();
238
        self.focus = Focus::List;
239
    }
240
241
    fn clear_expired_status(&mut self) {
242
        if let Some((_, time)) = &self.status_message {
243
            if time.elapsed() > Duration::from_secs(2) {
244
                self.status_message = None;
245
            }
246
        }
247
    }
248
249
    fn highlight_content(&self, name: &str, content: &str) -> Text<'static> {
250
        let ext = name.rsplit('.').next().unwrap_or("");
251
        let syntax = self
252
            .syntax_set
253
            .find_syntax_by_extension(ext)
254
            .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
255
        let mut highlighter = HighlightLines::new(syntax, &self.theme);
256
257
        let lines: Vec<Line<'static>> = LinesWithEndings::from(content)
258
            .map(|line| {
259
                let ranges = highlighter
260
                    .highlight_line(line, &self.syntax_set)
261
                    .unwrap_or_default();
262
                let spans: Vec<Span<'static>> = ranges
263
                    .into_iter()
264
                    .map(|(style, text)| {
265
                        let color = to_ratatui_color(style.foreground);
266
                        Span::styled(text.to_owned(), Style::default().fg(color))
267
                    })
268
                    .collect();
269
                Line::from(spans)
270
            })
271
            .collect();
272
273
        Text::from(lines)
274
    }
275
}
276
277
fn to_ratatui_color(color: syntect::highlighting::Color) -> Color {
278
    if color.a == 0 {
279
        Color::Indexed(color.r)
280
    } else {
281
        Color::Reset
282
    }
283
}
284
285
fn resolve_backend(remote: Option<String>, api_key: Option<String>) -> (Backend, bool, Option<String>) {
286
    if let Some(url) = remote {
287
        return (
288
            Backend::remote(url.clone(), api_key),
289
            true,
290
            Some(url),
291
        );
292
    }
293
294
    if !std::path::Path::new("sipp.sqlite").exists() {
295
        let cfg = config::load_config();
296
        let url = cfg.remote_url.unwrap_or_else(|| "http://localhost:3000".to_string());
297
        let api_key = api_key.or(cfg.api_key);
298
        return (Backend::remote(url.clone(), api_key), true, Some(url));
299
    }
300
301
    (Backend::local(), false, Some("http://localhost:3000".to_string()))
302
}
303
304
pub fn run_auth() -> Result<(), Box<dyn std::error::Error>> {
305
    use std::io::{self, Write};
306
307
    print!("Remote URL: ");
308
    io::stdout().flush()?;
309
    let mut remote_url = String::new();
310
    io::stdin().read_line(&mut remote_url)?;
311
    let remote_url = remote_url.trim().to_string();
312
313
    print!("API Key: ");
314
    io::stdout().flush()?;
315
    let api_key = rpassword::read_password()?;
316
    let api_key = api_key.trim().to_string();
317
318
    let cfg = config::Config {
319
        remote_url: if remote_url.is_empty() {
320
            None
321
        } else {
322
            Some(remote_url)
323
        },
324
        api_key: if api_key.is_empty() {
325
            None
326
        } else {
327
            Some(api_key)
328
        },
329
    };
330
331
    config::save_config(&cfg)?;
332
    println!("Config saved to {}", config::config_path().display());
333
    Ok(())
334
}
335
336
pub fn run_interactive(remote: Option<String>, api_key: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
337
    let (backend, is_remote, remote_url) = resolve_backend(remote, api_key);
338
339
    let snippets = match backend.list_snippets() {
340
        Ok(s) => s,
341
        Err(e) => {
342
            eprintln!("Failed to load snippets: {}", e);
343
            Vec::new()
344
        }
345
    };
346
347
    ratatui::run(|terminal| run_app(terminal, App::new(snippets, is_remote, remote_url), &backend))
348
}
349
350
pub fn run_file_upload(remote: Option<String>, api_key: Option<String>, file: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
351
    let (backend, _, remote_url) = resolve_backend(remote, api_key);
352
353
    let name = file
354
        .file_name()
355
        .ok_or("Invalid file path")?
356
        .to_string_lossy()
357
        .to_string();
358
    let content = std::fs::read_to_string(&file)
359
        .map_err(|e| format!("Failed to read file: {}", e))?;
360
    let snippet = backend
361
        .create_snippet(&name, &content)
362
        .map_err(|e| format!("{}", e))?;
363
    let link = match &remote_url {
364
        Some(url) => format!("{}/s/{}", url.trim_end_matches('/'), snippet.short_id),
365
        None => snippet.short_id.clone(),
366
    };
367
    println!("{}", link);
368
    if let Ok(mut clipboard) = Clipboard::new() {
369
        let _ = clipboard.set_text(&link);
370
        println!("\u{2714} Copied to clipboard!");
371
    }
372
    Ok(())
373
}
374
375
fn run_app(
376
    terminal: &mut DefaultTerminal,
377
    mut app: App,
378
    backend: &Backend,
379
) -> Result<(), Box<dyn std::error::Error>> {
380
    while !app.should_quit {
381
        app.clear_expired_status();
382
383
        let content_line_count = app
384
            .selected_snippet()
385
            .map(|s| s.content.lines().count() as u16)
386
            .unwrap_or(0);
387
388
        terminal.draw(|frame| {
389
            let outer = Layout::vertical([Constraint::Min(1), Constraint::Length(1)])
390
                .split(frame.area());
391
392
            let chunks = Layout::horizontal([
393
                Constraint::Percentage(30),
394
                Constraint::Percentage(70),
395
            ])
396
            .split(outer[0]);
397
398
            let items: Vec<ListItem> = app
399
                .snippets
400
                .iter()
401
                .map(|s| ListItem::new(s.name.as_str()))
402
                .collect();
403
404
            let list_border_style = match app.focus {
405
                Focus::List => Style::default().fg(Color::Yellow),
406
                _ => Style::default().fg(Color::DarkGray),
407
            };
408
            let content_border_style = match app.focus {
409
                Focus::Content => Style::default().fg(Color::Yellow),
410
                _ => Style::default().fg(Color::DarkGray),
411
            };
412
413
            let list = List::new(items)
414
                .block(
415
                    Block::default()
416
                        .title(" Snippets ")
417
                        .borders(Borders::ALL)
418
                        .border_style(list_border_style),
419
                )
420
                .highlight_style(
421
                    Style::default()
422
                        .fg(Color::Yellow)
423
                        .add_modifier(Modifier::BOLD),
424
                )
425
                .highlight_symbol("▶ ");
426
427
            frame.render_stateful_widget(list, chunks[0], &mut app.list_state);
428
429
            match app.focus {
430
                Focus::CreateName | Focus::CreateContent => {
431
                    let create_block = Block::default()
432
                        .title(" New Snippet ")
433
                        .borders(Borders::ALL)
434
                        .border_style(Style::default().fg(Color::Yellow));
435
436
                    let inner = create_block.inner(chunks[1]);
437
                    frame.render_widget(create_block, chunks[1]);
438
439
                    let form_layout = Layout::vertical([
440
                        Constraint::Length(3),
441
                        Constraint::Min(1),
442
                        Constraint::Length(1),
443
                    ])
444
                    .split(inner);
445
446
                    let name_style = match app.focus {
447
                        Focus::CreateName => Style::default().fg(Color::Yellow),
448
                        _ => Style::default().fg(Color::DarkGray),
449
                    };
450
                    let name_input = Paragraph::new(app.create_name.as_str()).block(
451
                        Block::default()
452
                            .title(" Name ")
453
                            .borders(Borders::ALL)
454
                            .border_style(name_style),
455
                    );
456
                    frame.render_widget(name_input, form_layout[0]);
457
458
                    let content_style = match app.focus {
459
                        Focus::CreateContent => Style::default().fg(Color::Yellow),
460
                        _ => Style::default().fg(Color::DarkGray),
461
                    };
462
                    let content_input = Paragraph::new(app.create_content.as_str()).block(
463
                        Block::default()
464
                            .title(" Content ")
465
                            .borders(Borders::ALL)
466
                            .border_style(content_style),
467
                    );
468
                    frame.render_widget(content_input, form_layout[1]);
469
470
                    match app.focus {
471
                        Focus::CreateName => {
472
                            let x = form_layout[0].x + 1 + app.create_name.len() as u16;
473
                            let y = form_layout[0].y + 1;
474
                            frame.set_cursor_position((x, y));
475
                        }
476
                        Focus::CreateContent => {
477
                            let last_line = app.create_content.lines().last().unwrap_or("");
478
                            let line_count = app.create_content.lines().count()
479
                                + if app.create_content.ends_with('\n') {
480
                                    1
481
                                } else {
482
                                    0
483
                                };
484
                            let y_offset = if line_count == 0 { 0 } else { line_count - 1 };
485
                            let x = form_layout[1].x
486
                                + 1
487
                                + if app.create_content.ends_with('\n') {
488
                                    0
489
                                } else {
490
                                    last_line.len() as u16
491
                                };
492
                            let y = form_layout[1].y + 1 + y_offset as u16;
493
                            frame.set_cursor_position((x, y));
494
                        }
495
                        _ => {}
496
                    }
497
498
                    let hint = Paragraph::new(Line::from(vec![
499
                        Span::styled("Tab", Style::default().fg(Color::Yellow)),
500
                        Span::raw(" switch field  "),
501
                        Span::styled("Ctrl+S", Style::default().fg(Color::Yellow)),
502
                        Span::raw(" save  "),
503
                        Span::styled("Esc", Style::default().fg(Color::Yellow)),
504
                        Span::raw(" cancel"),
505
                    ]));
506
                    frame.render_widget(hint, form_layout[2]);
507
                }
508
                _ => {
509
                    let highlighted = match app.selected_snippet() {
510
                        Some(s) => app.highlight_content(&s.name, &s.content),
511
                        None => Text::raw(""),
512
                    };
513
514
                    let paragraph = Paragraph::new(highlighted)
515
                        .block(
516
                            Block::default()
517
                                .title(" Content ")
518
                                .borders(Borders::ALL)
519
                                .border_style(content_border_style),
520
                        )
521
                        .scroll((app.content_scroll, 0));
522
523
                    frame.render_widget(paragraph, chunks[1]);
524
                }
525
            }
526
527
            if let Some((msg, _)) = &app.status_message {
528
                let status = Paragraph::new(Text::raw(msg.as_str()))
529
                    .style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD));
530
                frame.render_widget(status, outer[1]);
531
            }
532
533
            if app.show_help {
534
                let area = frame.area();
535
                let popup_width = 44u16.min(area.width.saturating_sub(4));
536
                let popup_height = 20u16.min(area.height.saturating_sub(4));
537
                let popup_area = ratatui::layout::Rect {
538
                    x: (area.width.saturating_sub(popup_width)) / 2,
539
                    y: (area.height.saturating_sub(popup_height)) / 2,
540
                    width: popup_width,
541
                    height: popup_height,
542
                };
543
544
                let mut help_lines = vec![
545
                    Line::from(""),
546
                    Line::from(vec![
547
                        Span::styled(
548
                            "  j/↓  ",
549
                            Style::default()
550
                                .fg(Color::Yellow)
551
                                .add_modifier(Modifier::BOLD),
552
                        ),
553
                        Span::raw("Move down / Scroll down"),
554
                    ]),
555
                    Line::from(vec![
556
                        Span::styled(
557
                            "  k/↑  ",
558
                            Style::default()
559
                                .fg(Color::Yellow)
560
                                .add_modifier(Modifier::BOLD),
561
                        ),
562
                        Span::raw("Move up / Scroll up"),
563
                    ]),
564
                    Line::from(vec![
565
                        Span::styled(
566
                            "  Enter",
567
                            Style::default()
568
                                .fg(Color::Yellow)
569
                                .add_modifier(Modifier::BOLD),
570
                        ),
571
                        Span::raw("  Focus content pane"),
572
                    ]),
573
                    Line::from(vec![
574
                        Span::styled(
575
                            "  Esc  ",
576
                            Style::default()
577
                                .fg(Color::Yellow)
578
                                .add_modifier(Modifier::BOLD),
579
                        ),
580
                        Span::raw("Back / Quit"),
581
                    ]),
582
                    Line::from(vec![
583
                        Span::styled(
584
                            "  y    ",
585
                            Style::default()
586
                                .fg(Color::Yellow)
587
                                .add_modifier(Modifier::BOLD),
588
                        ),
589
                        Span::raw("Copy snippet"),
590
                    ]),
591
                    Line::from(vec![
592
                        Span::styled(
593
                            "  Y    ",
594
                            Style::default()
595
                                .fg(Color::Yellow)
596
                                .add_modifier(Modifier::BOLD),
597
                        ),
598
                        Span::raw("Copy link"),
599
                    ]),
600
                    Line::from(vec![
601
                        Span::styled(
602
                            "  o    ",
603
                            Style::default()
604
                                .fg(Color::Yellow)
605
                                .add_modifier(Modifier::BOLD),
606
                        ),
607
                        Span::raw("Open in browser"),
608
                    ]),
609
                    Line::from(vec![
610
                        Span::styled(
611
                            "  d    ",
612
                            Style::default()
613
                                .fg(Color::Yellow)
614
                                .add_modifier(Modifier::BOLD),
615
                        ),
616
                        Span::raw("Delete snippet"),
617
                    ]),
618
                    Line::from(vec![
619
                        Span::styled(
620
                            "  c    ",
621
                            Style::default()
622
                                .fg(Color::Yellow)
623
                                .add_modifier(Modifier::BOLD),
624
                        ),
625
                        Span::raw("Create snippet"),
626
                    ]),
627
                ];
628
629
                if app.is_remote {
630
                    help_lines.push(Line::from(vec![
631
                        Span::styled(
632
                            "  r    ",
633
                            Style::default()
634
                                .fg(Color::Yellow)
635
                                .add_modifier(Modifier::BOLD),
636
                        ),
637
                        Span::raw("Refresh snippets"),
638
                    ]));
639
                }
640
641
                help_lines.extend([
642
                    Line::from(vec![
643
                        Span::styled(
644
                            "  q    ",
645
                            Style::default()
646
                                .fg(Color::Yellow)
647
                                .add_modifier(Modifier::BOLD),
648
                        ),
649
                        Span::raw("Quit"),
650
                    ]),
651
                    Line::from(vec![
652
                        Span::styled(
653
                            "  ?    ",
654
                            Style::default()
655
                                .fg(Color::Yellow)
656
                                .add_modifier(Modifier::BOLD),
657
                        ),
658
                        Span::raw("Toggle this help"),
659
                    ]),
660
                    Line::from(""),
661
                    Line::from(Span::styled(
662
                        "  Press any key to close",
663
                        Style::default().fg(Color::DarkGray),
664
                    )),
665
                ]);
666
667
                let help_text = Text::from(help_lines);
668
669
                Clear.render(popup_area, frame.buffer_mut());
670
                let help = Paragraph::new(help_text).block(
671
                    Block::default()
672
                        .title(" Keybindings ")
673
                        .borders(Borders::ALL)
674
                        .border_style(Style::default().fg(Color::Yellow)),
675
                );
676
                frame.render_widget(help, popup_area);
677
            }
678
        })?;
679
680
        if event::poll(Duration::from_millis(100))? {
681
            if let Event::Key(key) = event::read()? {
682
                if app.show_help {
683
                    app.show_help = false;
684
                } else {
685
                    match app.focus {
686
                        Focus::List => match key.code {
687
                            KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
688
                            KeyCode::Char('j') | KeyCode::Down => app.move_down(),
689
                            KeyCode::Char('k') | KeyCode::Up => app.move_up(),
690
                            KeyCode::Char('y') => app.copy_selected(),
691
                            KeyCode::Char('Y') => app.copy_link(),
692
                            KeyCode::Char('d') => app.delete_selected(backend),
693
                            KeyCode::Char('c') => app.start_create(),
694
                            KeyCode::Char('o') => app.open_in_browser(),
695
                            KeyCode::Char('r') if app.is_remote => app.refresh(backend),
696
                            KeyCode::Char('?') => app.show_help = true,
697
                            KeyCode::Enter | KeyCode::Char('l') => {
698
                                if app.selected_snippet().is_some() {
699
                                    app.focus = Focus::Content;
700
                                }
701
                            }
702
                            _ => {}
703
                        },
704
                        Focus::Content => match key.code {
705
                          KeyCode::Char(' ') | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('h') => {
706
                                app.focus = Focus::List;
707
                            }
708
                            KeyCode::Char('j') | KeyCode::Down => {
709
                                app.scroll_down(content_line_count);
710
                            }
711
                            KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
712
                            KeyCode::Char('y') => app.copy_selected(),
713
                            KeyCode::Char('Y') => app.copy_link(),
714
                            KeyCode::Char('o') => app.open_in_browser(),
715
                            KeyCode::Char('?') => app.show_help = true,
716
                            _ => {}
717
                        },
718
                        Focus::CreateName => {
719
                            if key.modifiers.contains(KeyModifiers::CONTROL)
720
                                && key.code == KeyCode::Char('s')
721
                            {
722
                                app.save_create(backend);
723
                            } else {
724
                                match key.code {
725
                                    KeyCode::Esc => app.cancel_create(),
726
                                    KeyCode::Enter | KeyCode::Tab => {
727
                                        app.focus = Focus::CreateContent
728
                                    }
729
                                    KeyCode::Backspace => {
730
                                        app.create_name.pop();
731
                                    }
732
                                    KeyCode::Char(c) => app.create_name.push(c),
733
                                    _ => {}
734
                                }
735
                            }
736
                        }
737
                        Focus::CreateContent => {
738
                            if key.modifiers.contains(KeyModifiers::CONTROL)
739
                                && key.code == KeyCode::Char('s')
740
                            {
741
                                app.save_create(backend);
742
                            } else {
743
                                match key.code {
744
                                    KeyCode::Esc => app.cancel_create(),
745
                                    KeyCode::Tab => app.focus = Focus::CreateName,
746
                                    KeyCode::Enter => app.create_content.push('\n'),
747
                                    KeyCode::Backspace => {
748
                                        app.create_content.pop();
749
                                    }
750
                                    KeyCode::Char(c) => app.create_content.push(c),
751
                                    _ => {}
752
                                }
753
                            }
754
                        }
755
                    }
756
                }
757
            }
758
        }
759
    }
760
761
    Ok(())
762
}