chore: added wrapping when creating inside tui 137995f7
Steve · 2026-02-21 11:26 1 file(s) · +109 −23
src/tui.rs +109 −23
5 5
    layout::{Alignment, Constraint, Layout},
6 6
    style::{Color, Modifier, Style},
7 7
    text::{Line, Span, Text},
8 -
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Widget},
8 +
    widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Widget, Wrap},
9 9
};
10 10
use crate::backend::Backend;
11 11
use crate::config;
46 46
    filtered_indices: Option<Vec<usize>>,
47 47
    is_remote: bool,
48 48
    remote_url: Option<String>,
49 +
    wrap_content: bool,
50 +
    edit_scroll: u16,
49 51
}
50 52
51 53
impl App {
77 79
            filtered_indices: None,
78 80
            is_remote,
79 81
            remote_url,
82 +
            wrap_content: true,
83 +
            edit_scroll: 0,
80 84
        }
81 85
    }
82 86
246 250
        }
247 251
    }
248 252
253 +
    fn cursor_position_wrapped(&self, width: u16) -> (u16, u16) {
254 +
        let w = width as usize;
255 +
        if w == 0 {
256 +
            return (0, 0);
257 +
        }
258 +
        let text = &self.create_content;
259 +
        let mut visual_row: usize = 0;
260 +
        let lines: Vec<&str> = if text.is_empty() {
261 +
            vec![""]
262 +
        } else if text.ends_with('\n') {
263 +
            text.split('\n').collect()
264 +
        } else {
265 +
            text.split('\n').collect()
266 +
        };
267 +
        let last_idx = lines.len() - 1;
268 +
        for (i, line) in lines.iter().enumerate() {
269 +
            let line_len = line.len();
270 +
            let wrapped_lines = if line_len == 0 {
271 +
                1
272 +
            } else {
273 +
                (line_len + w - 1) / w
274 +
            };
275 +
            if i < last_idx {
276 +
                visual_row += wrapped_lines;
277 +
            } else {
278 +
                // cursor is at end of this last line
279 +
                let cursor_col = if text.ends_with('\n') { 0 } else { line_len };
280 +
                let extra_rows = cursor_col / w;
281 +
                let col = cursor_col % w;
282 +
                visual_row += extra_rows;
283 +
                return (col as u16, visual_row as u16);
284 +
            }
285 +
        }
286 +
        (0, visual_row as u16)
287 +
    }
288 +
289 +
    fn auto_scroll_edit(&mut self, cursor_visual_row: u16, visible_height: u16) {
290 +
        if visible_height == 0 {
291 +
            return;
292 +
        }
293 +
        if cursor_visual_row < self.edit_scroll {
294 +
            self.edit_scroll = cursor_visual_row;
295 +
        } else if cursor_visual_row >= self.edit_scroll + visible_height {
296 +
            self.edit_scroll = cursor_visual_row - visible_height + 1;
297 +
        }
298 +
    }
299 +
249 300
    fn start_create(&mut self) {
250 301
        self.create_name.clear();
251 302
        self.create_content.clear();
303 +
        self.edit_scroll = 0;
252 304
        self.focus = Focus::CreateName;
253 305
    }
254 306
288 340
            self.create_name = name;
289 341
            self.create_content = content;
290 342
            self.edit_short_id = Some(short_id);
343 +
            self.edit_scroll = 0;
291 344
            self.focus = Focus::EditName;
292 345
        }
293 346
    }
649 702
                        Focus::CreateContent | Focus::EditContent => Style::default().fg(Color::Yellow),
650 703
                        _ => Style::default().fg(Color::DarkGray),
651 704
                    };
652 -
                    let content_input = Paragraph::new(app.create_content.as_str()).block(
705 +
                    let mut content_input = Paragraph::new(app.create_content.as_str()).block(
653 706
                        Block::default()
654 707
                            .title(" Content ")
655 708
                            .borders(Borders::ALL)
656 709
                            .border_style(content_style),
657 710
                    );
711 +
                    if app.wrap_content {
712 +
                        content_input = content_input.wrap(Wrap { trim: false });
713 +
                    }
714 +
                    content_input = content_input.scroll((app.edit_scroll, 0));
658 715
                    frame.render_widget(content_input, form_layout[1]);
659 716
717 +
                    let content_inner = Block::default()
718 +
                        .borders(Borders::ALL)
719 +
                        .inner(form_layout[1]);
720 +
                    let inner_width = content_inner.width;
721 +
                    let inner_height = content_inner.height;
722 +
660 723
                    match app.focus {
661 724
                        Focus::CreateName | Focus::EditName => {
662 725
                            let x = form_layout[0].x + 1 + app.create_name.len() as u16;
664 727
                            frame.set_cursor_position((x, y));
665 728
                        }
666 729
                        Focus::CreateContent | Focus::EditContent => {
667 -
                            let last_line = app.create_content.lines().last().unwrap_or("");
668 -
                            let line_count = app.create_content.lines().count()
669 -
                                + if app.create_content.ends_with('\n') {
670 -
                                    1
671 -
                                } else {
672 -
                                    0
673 -
                                };
674 -
                            let y_offset = if line_count == 0 { 0 } else { line_count - 1 };
675 -
                            let x = form_layout[1].x
676 -
                                + 1
677 -
                                + if app.create_content.ends_with('\n') {
730 +
                            let (cx, cy) = if app.wrap_content {
731 +
                                app.cursor_position_wrapped(inner_width)
732 +
                            } else {
733 +
                                let last_line = app.create_content.lines().last().unwrap_or("");
734 +
                                let line_count = app.create_content.lines().count()
735 +
                                    + if app.create_content.ends_with('\n') { 1 } else { 0 };
736 +
                                let y_offset = if line_count == 0 { 0 } else { line_count - 1 };
737 +
                                let col = if app.create_content.ends_with('\n') {
678 738
                                    0
679 739
                                } else {
680 740
                                    last_line.len() as u16
681 741
                                };
682 -
                            let y = form_layout[1].y + 1 + y_offset as u16;
742 +
                                (col, y_offset as u16)
743 +
                            };
744 +
                            app.auto_scroll_edit(cy, inner_height);
745 +
                            let screen_y = cy.saturating_sub(app.edit_scroll);
746 +
                            let x = content_inner.x + cx;
747 +
                            let y = content_inner.y + screen_y;
683 748
                            frame.set_cursor_position((x, y));
684 749
                        }
685 750
                        _ => {}
744 809
                    Span::raw(": Switch field  "),
745 810
                    Span::styled("Ctrl+S", Style::default().fg(Color::Yellow)),
746 811
                    Span::raw(": Save  "),
812 +
                    Span::styled("Ctrl+W", Style::default().fg(Color::Yellow)),
813 +
                    Span::raw(": Wrap  "),
747 814
                    Span::styled("Esc", Style::default().fg(Color::Yellow)),
748 815
                    Span::raw(": Cancel"),
749 816
                ]),
807 874
            if app.show_help {
808 875
                let area = frame.area();
809 876
                let popup_width = 34u16.min(area.width.saturating_sub(4));
810 -
                let popup_height = 20u16.min(area.height.saturating_sub(4));
877 +
                let popup_height = 21u16.min(area.height.saturating_sub(4));
811 878
                let popup_area = ratatui::layout::Rect {
812 879
                    x: (area.width.saturating_sub(popup_width)) / 2,
813 880
                    y: (area.height.saturating_sub(popup_height)) / 2,
916 983
                        ),
917 984
                        Span::raw("Search snippets"),
918 985
                    ]),
986 +
                    Line::from(vec![
987 +
                        Span::styled(
988 +
                            "  ^W   ",
989 +
                            Style::default()
990 +
                                .fg(Color::Yellow)
991 +
                                .add_modifier(Modifier::BOLD),
992 +
                        ),
993 +
                        Span::raw("Toggle word wrap (edit)"),
994 +
                    ]),
919 995
                ];
920 996
921 997
                if app.is_remote {
1037 1113
                            }
1038 1114
                        }
1039 1115
                        Focus::CreateContent => {
1040 -
                            if key.modifiers.contains(KeyModifiers::CONTROL)
1041 -
                                && key.code == KeyCode::Char('s')
1042 -
                            {
1043 -
                                app.save_create(backend);
1116 +
                            if key.modifiers.contains(KeyModifiers::CONTROL) {
1117 +
                                match key.code {
1118 +
                                    KeyCode::Char('s') => app.save_create(backend),
1119 +
                                    KeyCode::Char('w') => {
1120 +
                                        app.wrap_content = !app.wrap_content;
1121 +
                                        app.edit_scroll = 0;
1122 +
                                    }
1123 +
                                    _ => {}
1124 +
                                }
1044 1125
                            } else {
1045 1126
                                match key.code {
1046 1127
                                    KeyCode::Esc => app.cancel_create(),
1074 1155
                            }
1075 1156
                        }
1076 1157
                        Focus::EditContent => {
1077 -
                            if key.modifiers.contains(KeyModifiers::CONTROL)
1078 -
                                && key.code == KeyCode::Char('s')
1079 -
                            {
1080 -
                                app.save_edit(backend);
1158 +
                            if key.modifiers.contains(KeyModifiers::CONTROL) {
1159 +
                                match key.code {
1160 +
                                    KeyCode::Char('s') => app.save_edit(backend),
1161 +
                                    KeyCode::Char('w') => {
1162 +
                                        app.wrap_content = !app.wrap_content;
1163 +
                                        app.edit_scroll = 0;
1164 +
                                    }
1165 +
                                    _ => {}
1166 +
                                }
1081 1167
                            } else {
1082 1168
                                match key.code {
1083 1169
                                    KeyCode::Esc => app.cancel_edit(),