chore: added update and search b3d85d44
Steve · 2026-02-20 20:03 4 file(s) · +385 −29
src/backend.rs +34 −0
104 104
        }
105 105
    }
106 106
107 +
    pub fn update_snippet(
108 +
        &self,
109 +
        short_id: &str,
110 +
        name: &str,
111 +
        content: &str,
112 +
    ) -> Result<Option<Snippet>, BackendError> {
113 +
        match self {
114 +
            Backend::Local { db } => Ok(db::update_snippet_by_short_id(db, short_id, name, content)?),
115 +
            Backend::Remote {
116 +
                base_url,
117 +
                api_key,
118 +
                client,
119 +
            } => {
120 +
                let mut req = client
121 +
                    .put(format!("{}/api/snippets/{}", base_url, short_id))
122 +
                    .json(&serde_json::json!({"name": name, "content": content}));
123 +
                if let Some(key) = api_key {
124 +
                    req = req.header("x-api-key", key);
125 +
                }
126 +
                let resp = req.send().map_err(|e| BackendError::Network(e.to_string()))?;
127 +
                match resp.status().as_u16() {
128 +
                    200 => resp
129 +
                        .json::<Snippet>()
130 +
                        .map(Some)
131 +
                        .map_err(|e| BackendError::Network(e.to_string())),
132 +
                    401 => Err(BackendError::Unauthorized("Invalid API key".into())),
133 +
                    403 => Err(BackendError::Unauthorized("No API key configured on server".into())),
134 +
                    404 => Ok(None),
135 +
                    _ => Err(BackendError::Network(format!("HTTP {}", resp.status()))),
136 +
                }
137 +
            }
138 +
        }
139 +
    }
140 +
107 141
    pub fn delete_snippet(&self, short_id: &str) -> Result<bool, BackendError> {
108 142
        match self {
109 143
            Backend::Local { db } => Ok(db::delete_snippet_by_short_id(db, short_id)?),
src/db.rs +32 −0
121 121
    )?;
122 122
    Ok(rows_affected > 0)
123 123
}
124 +
125 +
pub fn update_snippet_by_short_id(
126 +
    db: &Db,
127 +
    short_id: &str,
128 +
    name: &str,
129 +
    content: &str,
130 +
) -> Result<Option<Snippet>, DbError> {
131 +
    let conn = db.lock().map_err(|_| DbError::LockPoisoned)?;
132 +
    let rows_affected = conn.execute(
133 +
        "UPDATE snippets SET name = ?1, content = ?2 WHERE short_id = ?3",
134 +
        params![name, content, short_id],
135 +
    )?;
136 +
    if rows_affected == 0 {
137 +
        return Ok(None);
138 +
    }
139 +
    match conn.query_row(
140 +
        "SELECT id, short_id, content, name FROM snippets WHERE short_id = ?1",
141 +
        params![short_id],
142 +
        |row| {
143 +
            Ok(Snippet {
144 +
                id: row.get(0)?,
145 +
                short_id: row.get(1)?,
146 +
                content: row.get(2)?,
147 +
                name: row.get(3)?,
148 +
            })
149 +
        },
150 +
    ) {
151 +
        Ok(snippet) => Ok(Some(snippet)),
152 +
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
153 +
        Err(e) => Err(DbError::Sqlite(e)),
154 +
    }
155 +
}
src/server.rs +23 −4
7 7
    http::{HeaderMap, StatusCode, header},
8 8
    middleware::{self, Next},
9 9
    response::{Html, IntoResponse, Redirect, Response},
10 -
    routing::{delete, get, post},
10 +
    routing::{delete, get, post, put},
11 11
};
12 12
use rust_embed::Embed;
13 13
use serde::Deserialize;
36 36
        let auth_endpoints = match std::env::var("SIPP_AUTH_ENDPOINTS") {
37 37
            Ok(val) if val.trim().eq_ignore_ascii_case("none") => HashSet::new(),
38 38
            Ok(val) => val.split(',').map(|s| s.trim().to_lowercase()).collect(),
39 -
            Err(_) => ["api_delete", "api_list"].iter().map(|s| s.to_string()).collect(),
39 +
            Err(_) => ["api_delete", "api_list", "api_update"].iter().map(|s| s.to_string()).collect(),
40 40
        };
41 41
        ServerConfig { api_key, auth_endpoints }
42 42
    }
184 184
    }
185 185
}
186 186
187 +
async fn api_update_snippet(
188 +
    State(state): State<AppState>,
189 +
    Path(short_id): Path<String>,
190 +
    Json(body): Json<ApiCreateSnippet>,
191 +
) -> Result<Json<Snippet>, (StatusCode, Json<serde_json::Value>)> {
192 +
    match db::update_snippet_by_short_id(&state.db, &short_id, &body.name, &body.content) {
193 +
        Ok(Some(snippet)) => Ok(Json(snippet)),
194 +
        Ok(None) => Err((StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Snippet not found"})))),
195 +
        Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Internal server error"})))),
196 +
    }
197 +
}
198 +
187 199
fn build_api_routes(state: &AppState) -> Router<AppState> {
188 200
    let config = &state.server_config;
189 201
193 205
    let list_authed = config.requires_auth("api_list");
194 206
    let create_authed = config.requires_auth("api_create");
195 207
196 -
    // /api/snippets/{short_id} — GET (api_get) and DELETE (api_delete)
208 +
    // /api/snippets/{short_id} — GET (api_get), PUT (api_update), and DELETE (api_delete)
197 209
    let get_authed = config.requires_auth("api_get");
210 +
    let update_authed = config.requires_auth("api_update");
198 211
    let delete_authed = config.requires_auth("api_delete");
199 212
200 213
    // Build authed router
208 221
    if get_authed {
209 222
        authed = authed.route("/api/snippets/{short_id}", get(api_get_snippet));
210 223
    }
224 +
    if update_authed {
225 +
        authed = authed.route("/api/snippets/{short_id}", put(api_update_snippet));
226 +
    }
211 227
    if delete_authed {
212 228
        authed = authed.route("/api/snippets/{short_id}", delete(api_delete_snippet));
213 229
    }
223 239
    }
224 240
    if !get_authed {
225 241
        open = open.route("/api/snippets/{short_id}", get(api_get_snippet));
242 +
    }
243 +
    if !update_authed {
244 +
        open = open.route("/api/snippets/{short_id}", put(api_update_snippet));
226 245
    }
227 246
    if !delete_authed {
228 247
        open = open.route("/api/snippets/{short_id}", delete(api_delete_snippet));
275 294
    let server_config = ServerConfig::from_env();
276 295
277 296
    // Validate endpoint names
278 -
    let known = ["api_list", "api_create", "api_get", "api_delete", "all", "none"];
297 +
    let known = ["api_list", "api_create", "api_get", "api_update", "api_delete", "all", "none"];
279 298
    for name in &server_config.auth_endpoints {
280 299
        if !known.contains(&name.as_str()) {
281 300
            eprintln!("Warning: unknown auth endpoint name '{}' in SIPP_AUTH_ENDPOINTS", name);
src/tui.rs +296 −25
23 23
    Content,
24 24
    CreateName,
25 25
    CreateContent,
26 +
    EditName,
27 +
    EditContent,
28 +
    Search,
26 29
}
27 30
28 31
struct App {
38 41
    theme: Theme,
39 42
    create_name: String,
40 43
    create_content: String,
44 +
    edit_short_id: Option<String>,
45 +
    search_query: String,
46 +
    filtered_indices: Option<Vec<usize>>,
41 47
    is_remote: bool,
42 48
    remote_url: Option<String>,
43 49
}
66 72
            theme,
67 73
            create_name: String::new(),
68 74
            create_content: String::new(),
75 +
            edit_short_id: None,
76 +
            search_query: String::new(),
77 +
            filtered_indices: None,
69 78
            is_remote,
70 79
            remote_url,
71 80
        }
72 81
    }
73 82
74 83
    fn selected_snippet(&self) -> Option<&Snippet> {
75 -
        self.list_state.selected().and_then(|i| self.snippets.get(i))
84 +
        self.list_state.selected().and_then(|i| {
85 +
            if let Some(indices) = &self.filtered_indices {
86 +
                indices.get(i).and_then(|&real| self.snippets.get(real))
87 +
            } else {
88 +
                self.snippets.get(i)
89 +
            }
90 +
        })
91 +
    }
92 +
93 +
    fn visible_count(&self) -> usize {
94 +
        match &self.filtered_indices {
95 +
            Some(indices) => indices.len(),
96 +
            None => self.snippets.len(),
97 +
        }
76 98
    }
77 99
78 100
    fn move_up(&mut self) {
79 -
        if self.snippets.is_empty() {
101 +
        let count = self.visible_count();
102 +
        if count == 0 {
80 103
            return;
81 104
        }
82 105
        let i = match self.list_state.selected() {
83 106
            Some(i) if i > 0 => i - 1,
84 -
            Some(_) => self.snippets.len() - 1,
107 +
            Some(_) => count - 1,
85 108
            None => 0,
86 109
        };
87 110
        self.list_state.select(Some(i));
89 112
    }
90 113
91 114
    fn move_down(&mut self) {
92 -
        if self.snippets.is_empty() {
115 +
        let count = self.visible_count();
116 +
        if count == 0 {
93 117
            return;
94 118
        }
95 119
        let i = match self.list_state.selected() {
96 -
            Some(i) if i < self.snippets.len() - 1 => i + 1,
120 +
            Some(i) if i < count - 1 => i + 1,
97 121
            Some(_) => 0,
98 122
            None => 0,
99 123
        };
162 186
163 187
    fn delete_selected(&mut self, backend: &Backend) {
164 188
        if let Some(selected_index) = self.list_state.selected() {
165 -
            if let Some(snippet) = self.snippets.get(selected_index) {
189 +
            let real_index = if let Some(indices) = &self.filtered_indices {
190 +
                match indices.get(selected_index) {
191 +
                    Some(&ri) => ri,
192 +
                    None => return,
193 +
                }
194 +
            } else {
195 +
                selected_index
196 +
            };
197 +
            if let Some(snippet) = self.snippets.get(real_index) {
166 198
                let short_id = snippet.short_id.clone();
167 199
                match backend.delete_snippet(&short_id) {
168 200
                    Ok(true) => {
169 -
                        self.snippets.remove(selected_index);
170 -
                        if self.snippets.is_empty() {
201 +
                        self.snippets.remove(real_index);
202 +
                        if self.filtered_indices.is_some() {
203 +
                            self.update_search_filter();
204 +
                        }
205 +
                        let count = self.visible_count();
206 +
                        if count == 0 {
171 207
                            self.list_state.select(None);
172 -
                        } else if selected_index >= self.snippets.len() {
173 -
                            self.list_state.select(Some(self.snippets.len() - 1));
208 +
                        } else if selected_index >= count {
209 +
                            self.list_state.select(Some(count - 1));
174 210
                        } else {
175 211
                            self.list_state.select(Some(selected_index));
176 212
                        }
192 228
        match backend.list_snippets() {
193 229
            Ok(snippets) => {
194 230
                self.snippets = snippets;
231 +
                self.filtered_indices = None;
232 +
                self.search_query.clear();
195 233
                if self.snippets.is_empty() {
196 234
                    self.list_state.select(None);
197 235
                } else {
223 261
            Ok(snippet) => {
224 262
                self.snippets.insert(0, snippet);
225 263
                self.list_state.select(Some(0));
264 +
                self.filtered_indices = None;
265 +
                self.search_query.clear();
226 266
                self.status_message = Some(("Created!".to_string(), Instant::now()));
227 267
                self.focus = Focus::List;
228 268
                self.create_name.clear();
240 280
        self.focus = Focus::List;
241 281
    }
242 282
283 +
    fn start_edit(&mut self) {
284 +
        let data = self.selected_snippet().map(|s| {
285 +
            (s.name.clone(), s.content.clone(), s.short_id.clone())
286 +
        });
287 +
        if let Some((name, content, short_id)) = data {
288 +
            self.create_name = name;
289 +
            self.create_content = content;
290 +
            self.edit_short_id = Some(short_id);
291 +
            self.focus = Focus::EditName;
292 +
        }
293 +
    }
294 +
295 +
    fn save_edit(&mut self, backend: &Backend) {
296 +
        if self.create_name.trim().is_empty() {
297 +
            self.status_message = Some(("Name cannot be empty".to_string(), Instant::now()));
298 +
            return;
299 +
        }
300 +
        let short_id = match &self.edit_short_id {
301 +
            Some(id) => id.clone(),
302 +
            None => return,
303 +
        };
304 +
        match backend.update_snippet(&short_id, &self.create_name, &self.create_content) {
305 +
            Ok(Some(updated)) => {
306 +
                if let Some(pos) = self.snippets.iter().position(|s| s.short_id == short_id) {
307 +
                    self.snippets[pos] = updated;
308 +
                }
309 +
                self.status_message = Some(("Updated!".to_string(), Instant::now()));
310 +
                self.focus = Focus::List;
311 +
                self.create_name.clear();
312 +
                self.create_content.clear();
313 +
                self.edit_short_id = None;
314 +
            }
315 +
            Ok(None) => {
316 +
                self.status_message = Some(("Snippet not found".to_string(), Instant::now()));
317 +
            }
318 +
            Err(e) => {
319 +
                self.status_message = Some((e.to_string(), Instant::now()));
320 +
            }
321 +
        }
322 +
    }
323 +
324 +
    fn cancel_edit(&mut self) {
325 +
        self.create_name.clear();
326 +
        self.create_content.clear();
327 +
        self.edit_short_id = None;
328 +
        self.focus = Focus::List;
329 +
    }
330 +
331 +
    fn start_search(&mut self) {
332 +
        self.search_query.clear();
333 +
        self.filtered_indices = Some((0..self.snippets.len()).collect());
334 +
        self.focus = Focus::Search;
335 +
        self.list_state.select(if self.snippets.is_empty() { None } else { Some(0) });
336 +
    }
337 +
338 +
    fn update_search_filter(&mut self) {
339 +
        let query = self.search_query.to_lowercase();
340 +
        let indices: Vec<usize> = self
341 +
            .snippets
342 +
            .iter()
343 +
            .enumerate()
344 +
            .filter(|(_, s)| s.name.to_lowercase().contains(&query))
345 +
            .map(|(i, _)| i)
346 +
            .collect();
347 +
        self.filtered_indices = Some(indices);
348 +
        if self.visible_count() == 0 {
349 +
            self.list_state.select(None);
350 +
        } else {
351 +
            self.list_state.select(Some(0));
352 +
        }
353 +
    }
354 +
355 +
    fn cancel_search(&mut self) {
356 +
        self.filtered_indices = None;
357 +
        self.search_query.clear();
358 +
        self.focus = Focus::List;
359 +
    }
360 +
361 +
    fn confirm_search(&mut self) {
362 +
        let real_index = self.list_state.selected().and_then(|i| {
363 +
            self.filtered_indices.as_ref().and_then(|indices| indices.get(i).copied())
364 +
        });
365 +
        self.filtered_indices = None;
366 +
        self.search_query.clear();
367 +
        self.focus = Focus::List;
368 +
        if let Some(ri) = real_index {
369 +
            self.list_state.select(Some(ri));
370 +
        }
371 +
    }
372 +
243 373
    fn clear_expired_status(&mut self) {
244 374
        if let Some((_, time)) = &self.status_message {
245 375
            if time.elapsed() > Duration::from_secs(2) {
401 531
            ])
402 532
            .split(outer[0]);
403 533
404 -
            let items: Vec<ListItem> = app
405 -
                .snippets
406 -
                .iter()
407 -
                .map(|s| ListItem::new(s.name.as_str()))
408 -
                .collect();
534 +
            let items: Vec<ListItem> = if let Some(indices) = &app.filtered_indices {
535 +
                indices
536 +
                    .iter()
537 +
                    .filter_map(|&i| app.snippets.get(i))
538 +
                    .map(|s| ListItem::new(s.name.as_str()))
539 +
                    .collect()
540 +
            } else {
541 +
                app.snippets
542 +
                    .iter()
543 +
                    .map(|s| ListItem::new(s.name.as_str()))
544 +
                    .collect()
545 +
            };
409 546
410 547
            let list_border_style = match app.focus {
411 -
                Focus::List => Style::default().fg(Color::Yellow),
548 +
                Focus::List | Focus::Search => Style::default().fg(Color::Yellow),
412 549
                _ => Style::default().fg(Color::DarkGray),
413 550
            };
414 551
            let content_border_style = match app.focus {
430 567
                )
431 568
                .highlight_symbol("▶ ");
432 569
433 -
            frame.render_stateful_widget(list, chunks[0], &mut app.list_state);
570 +
            if matches!(app.focus, Focus::Search) {
571 +
                let search_split = Layout::vertical([
572 +
                    Constraint::Min(1),
573 +
                    Constraint::Length(3),
574 +
                ])
575 +
                .split(chunks[0]);
576 +
577 +
                let search_items: Vec<ListItem> = if let Some(indices) = &app.filtered_indices {
578 +
                    indices
579 +
                        .iter()
580 +
                        .filter_map(|&i| app.snippets.get(i))
581 +
                        .map(|s| ListItem::new(s.name.as_str()))
582 +
                        .collect()
583 +
                } else {
584 +
                    app.snippets.iter().map(|s| ListItem::new(s.name.as_str())).collect()
585 +
                };
586 +
                let search_list = List::new(search_items)
587 +
                .block(
588 +
                    Block::default()
589 +
                        .title(" Snippets ")
590 +
                        .borders(Borders::ALL)
591 +
                        .border_style(list_border_style),
592 +
                )
593 +
                .highlight_style(
594 +
                    Style::default()
595 +
                        .fg(Color::Yellow)
596 +
                        .add_modifier(Modifier::BOLD),
597 +
                )
598 +
                .highlight_symbol("▶ ");
599 +
                frame.render_stateful_widget(search_list, search_split[0], &mut app.list_state);
600 +
601 +
                let search_input = Paragraph::new(app.search_query.as_str()).block(
602 +
                    Block::default()
603 +
                        .title(" Search ")
604 +
                        .borders(Borders::ALL)
605 +
                        .border_style(Style::default().fg(Color::Yellow)),
606 +
                );
607 +
                frame.render_widget(search_input, search_split[1]);
608 +
609 +
                let x = search_split[1].x + 1 + app.search_query.len() as u16;
610 +
                let y = search_split[1].y + 1;
611 +
                frame.set_cursor_position((x, y));
612 +
            } else {
613 +
                frame.render_stateful_widget(list, chunks[0], &mut app.list_state);
614 +
            }
434 615
435 616
            match app.focus {
436 -
                Focus::CreateName | Focus::CreateContent => {
617 +
                Focus::CreateName | Focus::CreateContent | Focus::EditName | Focus::EditContent => {
618 +
                    let form_title = match app.focus {
619 +
                        Focus::EditName | Focus::EditContent => " Edit Snippet ",
620 +
                        _ => " New Snippet ",
621 +
                    };
437 622
                    let create_block = Block::default()
438 -
                        .title(" New Snippet ")
623 +
                        .title(form_title)
439 624
                        .borders(Borders::ALL)
440 625
                        .border_style(Style::default().fg(Color::Yellow));
441 626
449 634
                    .split(inner);
450 635
451 636
                    let name_style = match app.focus {
452 -
                        Focus::CreateName => Style::default().fg(Color::Yellow),
637 +
                        Focus::CreateName | Focus::EditName => Style::default().fg(Color::Yellow),
453 638
                        _ => Style::default().fg(Color::DarkGray),
454 639
                    };
455 640
                    let name_input = Paragraph::new(app.create_name.as_str()).block(
461 646
                    frame.render_widget(name_input, form_layout[0]);
462 647
463 648
                    let content_style = match app.focus {
464 -
                        Focus::CreateContent => Style::default().fg(Color::Yellow),
649 +
                        Focus::CreateContent | Focus::EditContent => Style::default().fg(Color::Yellow),
465 650
                        _ => Style::default().fg(Color::DarkGray),
466 651
                    };
467 652
                    let content_input = Paragraph::new(app.create_content.as_str()).block(
473 658
                    frame.render_widget(content_input, form_layout[1]);
474 659
475 660
                    match app.focus {
476 -
                        Focus::CreateName => {
661 +
                        Focus::CreateName | Focus::EditName => {
477 662
                            let x = form_layout[0].x + 1 + app.create_name.len() as u16;
478 663
                            let y = form_layout[0].y + 1;
479 664
                            frame.set_cursor_position((x, y));
480 665
                        }
481 -
                        Focus::CreateContent => {
666 +
                        Focus::CreateContent | Focus::EditContent => {
482 667
                            let last_line = app.create_content.lines().last().unwrap_or("");
483 668
                            let line_count = app.create_content.lines().count()
484 669
                                + if app.create_content.ends_with('\n') {
528 713
                    Span::raw(": View  "),
529 714
                    Span::styled("y", Style::default().fg(Color::Yellow)),
530 715
                    Span::raw(": Copy  "),
716 +
                    Span::styled("e", Style::default().fg(Color::Yellow)),
717 +
                    Span::raw(": Edit  "),
531 718
                    Span::styled("d", Style::default().fg(Color::Yellow)),
532 719
                    Span::raw(": Delete  "),
533 720
                    Span::styled("c", Style::default().fg(Color::Yellow)),
534 721
                    Span::raw(": Create  "),
722 +
                    Span::styled("/", Style::default().fg(Color::Yellow)),
723 +
                    Span::raw(": Search  "),
535 724
                    Span::styled("?", Style::default().fg(Color::Yellow)),
536 725
                    Span::raw(": Help  "),
537 726
                    Span::styled("q", Style::default().fg(Color::Yellow)),
542 731
                    Span::raw(": Scroll  "),
543 732
                    Span::styled("y", Style::default().fg(Color::Yellow)),
544 733
                    Span::raw(": Copy  "),
734 +
                    Span::styled("e", Style::default().fg(Color::Yellow)),
735 +
                    Span::raw(": Edit  "),
545 736
                    Span::styled("Esc", Style::default().fg(Color::Yellow)),
546 737
                    Span::raw(": Back  "),
547 738
                    Span::styled("?", Style::default().fg(Color::Yellow)),
548 739
                    Span::raw(": Help"),
549 740
                ]),
550 -
                Focus::CreateName | Focus::CreateContent => Line::from(vec![
741 +
                Focus::CreateName | Focus::CreateContent
742 +
                | Focus::EditName | Focus::EditContent => Line::from(vec![
551 743
                    Span::styled("Tab", Style::default().fg(Color::Yellow)),
552 744
                    Span::raw(": Switch field  "),
553 745
                    Span::styled("Ctrl+S", Style::default().fg(Color::Yellow)),
555 747
                    Span::styled("Esc", Style::default().fg(Color::Yellow)),
556 748
                    Span::raw(": Cancel"),
557 749
                ]),
750 +
                Focus::Search => Line::from(vec![
751 +
                    Span::styled("Type", Style::default().fg(Color::Yellow)),
752 +
                    Span::raw(": Filter  "),
753 +
                    Span::styled("Enter", Style::default().fg(Color::Yellow)),
754 +
                    Span::raw(": Select  "),
755 +
                    Span::styled("Esc", Style::default().fg(Color::Yellow)),
756 +
                    Span::raw(": Cancel"),
757 +
                ]),
558 758
            };
559 759
            frame.render_widget(Paragraph::new(hints), outer[1]);
560 760
607 807
            if app.show_help {
608 808
                let area = frame.area();
609 809
                let popup_width = 34u16.min(area.width.saturating_sub(4));
610 -
                let popup_height = 17u16.min(area.height.saturating_sub(4));
810 +
                let popup_height = 20u16.min(area.height.saturating_sub(4));
611 811
                let popup_area = ratatui::layout::Rect {
612 812
                    x: (area.width.saturating_sub(popup_width)) / 2,
613 813
                    y: (area.height.saturating_sub(popup_height)) / 2,
698 898
                        ),
699 899
                        Span::raw("Create snippet"),
700 900
                    ]),
901 +
                    Line::from(vec![
902 +
                        Span::styled(
903 +
                            "  e    ",
904 +
                            Style::default()
905 +
                                .fg(Color::Yellow)
906 +
                                .add_modifier(Modifier::BOLD),
907 +
                        ),
908 +
                        Span::raw("Edit snippet"),
909 +
                    ]),
910 +
                    Line::from(vec![
911 +
                        Span::styled(
912 +
                            "  /    ",
913 +
                            Style::default()
914 +
                                .fg(Color::Yellow)
915 +
                                .add_modifier(Modifier::BOLD),
916 +
                        ),
917 +
                        Span::raw("Search snippets"),
918 +
                    ]),
701 919
                ];
702 920
703 921
                if app.is_remote {
772 990
                            KeyCode::Char('Y') => app.copy_link(),
773 991
                            KeyCode::Char('d') => app.confirm_delete = true,
774 992
                            KeyCode::Char('c') => app.start_create(),
993 +
                            KeyCode::Char('e') => app.start_edit(),
994 +
                            KeyCode::Char('/') => app.start_search(),
775 995
                            KeyCode::Char('o') => app.open_in_browser(),
776 996
                            KeyCode::Char('r') if app.is_remote => app.refresh(backend),
777 997
                            KeyCode::Char('?') => app.show_help = true,
792 1012
                            KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
793 1013
                            KeyCode::Char('y') => app.copy_selected(),
794 1014
                            KeyCode::Char('Y') => app.copy_link(),
1015 +
                            KeyCode::Char('e') => app.start_edit(),
795 1016
                            KeyCode::Char('o') => app.open_in_browser(),
796 1017
                            KeyCode::Char('?') => app.show_help = true,
797 1018
                            _ => {}
833 1054
                                }
834 1055
                            }
835 1056
                        }
1057 +
                        Focus::EditName => {
1058 +
                            if key.modifiers.contains(KeyModifiers::CONTROL)
1059 +
                                && key.code == KeyCode::Char('s')
1060 +
                            {
1061 +
                                app.save_edit(backend);
1062 +
                            } else {
1063 +
                                match key.code {
1064 +
                                    KeyCode::Esc => app.cancel_edit(),
1065 +
                                    KeyCode::Enter | KeyCode::Tab => {
1066 +
                                        app.focus = Focus::EditContent
1067 +
                                    }
1068 +
                                    KeyCode::Backspace => {
1069 +
                                        app.create_name.pop();
1070 +
                                    }
1071 +
                                    KeyCode::Char(c) => app.create_name.push(c),
1072 +
                                    _ => {}
1073 +
                                }
1074 +
                            }
1075 +
                        }
1076 +
                        Focus::EditContent => {
1077 +
                            if key.modifiers.contains(KeyModifiers::CONTROL)
1078 +
                                && key.code == KeyCode::Char('s')
1079 +
                            {
1080 +
                                app.save_edit(backend);
1081 +
                            } else {
1082 +
                                match key.code {
1083 +
                                    KeyCode::Esc => app.cancel_edit(),
1084 +
                                    KeyCode::Tab => app.focus = Focus::EditName,
1085 +
                                    KeyCode::Enter => app.create_content.push('\n'),
1086 +
                                    KeyCode::Backspace => {
1087 +
                                        app.create_content.pop();
1088 +
                                    }
1089 +
                                    KeyCode::Char(c) => app.create_content.push(c),
1090 +
                                    _ => {}
1091 +
                                }
1092 +
                            }
1093 +
                        }
1094 +
                        Focus::Search => match key.code {
1095 +
                            KeyCode::Esc => app.cancel_search(),
1096 +
                            KeyCode::Enter => app.confirm_search(),
1097 +
                            KeyCode::Backspace => {
1098 +
                                app.search_query.pop();
1099 +
                                app.update_search_filter();
1100 +
                            }
1101 +
                            KeyCode::Char(c) => {
1102 +
                                app.search_query.push(c);
1103 +
                                app.update_search_filter();
1104 +
                            }
1105 +
                            _ => {}
1106 +
                        },
836 1107
                    }
837 1108
                }
838 1109
            }