Skip to main content

chatty_rs/app/ui/
edit.rs

1use crate::models::{Action, Event, Message};
2use ratatui::{
3    Frame,
4    layout::{Alignment, Constraint, Direction, Layout, Rect},
5    style::{Color, Modifier, Style, Stylize},
6    text::{Line, Text},
7    widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding},
8};
9use ratatui_macros::span;
10use syntect::highlighting::Theme;
11use tokio::sync::mpsc;
12use tui_textarea::Key;
13use unicode_width::UnicodeWidthStr;
14
15use super::utils;
16
17pub struct EditScreen<'a> {
18    action_tx: mpsc::UnboundedSender<Action>,
19
20    theme: &'a Theme,
21
22    showing: bool,
23    messages: Vec<SelectedMessage>,
24    list_state: ListState,
25}
26
27impl<'a> EditScreen<'_> {
28    pub fn new(theme: &'a Theme, action_tx: mpsc::UnboundedSender<Action>) -> EditScreen<'a> {
29        EditScreen {
30            action_tx,
31
32            showing: false,
33            messages: vec![],
34            list_state: ListState::default(),
35            theme,
36        }
37    }
38
39    pub fn set_messages(&mut self, messages: &[Message]) {
40        self.messages = messages
41            .iter()
42            .map(|message| SelectedMessage {
43                msg: message.clone(),
44                selected: false,
45            })
46            .collect();
47        // Sort the messages by the created time descending
48        self.messages
49            .sort_by(|a, b| b.msg.created_at().cmp(&a.msg.created_at()));
50        if !self.messages.is_empty() {
51            self.list_state.select(Some(0));
52        }
53    }
54
55    pub fn showing(&self) -> bool {
56        self.showing
57    }
58
59    pub fn toggle_showing(&mut self) {
60        self.showing = !self.showing;
61    }
62
63    fn toggle_selected(&mut self) {
64        if let Some(i) = self.list_state.selected() {
65            self.messages[i].selected = !self.messages[i].selected;
66        }
67    }
68
69    fn next_row(&mut self) {
70        self.list_state.select_next();
71    }
72
73    fn prev_row(&mut self) {
74        self.list_state.select_previous();
75    }
76
77    fn pageup(&mut self) {
78        [..10]
79            .iter()
80            .for_each(|_| self.list_state.select_previous());
81    }
82
83    fn pagedown(&mut self) {
84        [..10].iter().for_each(|_| self.list_state.select_next());
85    }
86
87    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
88        if !self.showing {
89            return;
90        }
91
92        let instructions = vec![
93            span!(" "),
94            span!("q").green().bold(),
95            span!(" to close, ").white(),
96            span!("Space").green().bold(),
97            span!(" to select, ").white(),
98            span!("y").green().bold(),
99            span!(" to copy selected, ").white(),
100            span!("c").green().bold(),
101            span!(" to quick copy ").white(),
102        ];
103
104        let block = Block::default()
105            .borders(Borders::ALL)
106            .border_type(BorderType::Rounded)
107            .border_style(Style::default().fg(Color::LightBlue))
108            .padding(Padding::symmetric(1, 0))
109            .title(Line::from(" Edit Mode ").bold())
110            .title_alignment(Alignment::Center)
111            .title_bottom(Line::from(instructions))
112            .style(Style::default());
113
114        frame.render_widget(Clear, area);
115        let inner = block.inner(area);
116        frame.render_widget(block, area);
117
118        let layout = Layout::default()
119            .direction(Direction::Horizontal)
120            .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
121            .split(inner);
122        self.render_messages_panel(frame, layout[0]);
123        self.render_preview_panel(frame, layout[1]);
124    }
125
126    fn render_messages_panel(&mut self, frame: &mut Frame, area: Rect) {
127        let block = Block::default()
128            .borders(Borders::ALL)
129            .border_type(BorderType::Rounded)
130            .padding(Padding::symmetric(1, 0))
131            .title(" Messages (Newer First) ")
132            .title_alignment(Alignment::Left)
133            .style(Style::default());
134        let inner = block.inner(area);
135        let max_width = inner.width as usize;
136        let messages = build_list_items(&self.messages, max_width - 2);
137        let list = List::new(messages)
138            .block(block)
139            .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
140        frame.render_stateful_widget(list, area, &mut self.list_state);
141    }
142
143    fn render_preview_panel(&mut self, frame: &mut Frame, area: Rect) {
144        let block = Block::default()
145            .borders(Borders::ALL)
146            .border_type(BorderType::Rounded)
147            .padding(Padding::symmetric(1, 0))
148            .title(" Preview ")
149            .title_alignment(Alignment::Left)
150            .style(Style::default());
151
152        let i = match self.list_state.selected() {
153            Some(i) => i,
154            None => {
155                frame.render_widget(block, area);
156                return;
157            }
158        };
159
160        if i >= self.messages.len() || self.messages.is_empty() {
161            frame.render_widget(block, area);
162            return;
163        }
164
165        let message = &self.messages[i].msg;
166        let lines = utils::build_message_lines(
167            message.text(),
168            area.width as usize - 5,
169            self.theme,
170            Line::from,
171        );
172
173        let text = Text::from(lines);
174        let list = List::new(text)
175            .block(block)
176            .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
177        frame.render_widget(list, area);
178    }
179
180    pub async fn handle_key_event(&mut self, event: &Event) -> bool {
181        match event {
182            Event::KeyboardCtrlE => {
183                self.showing = !self.showing;
184                return false;
185            }
186            Event::Quit => {
187                self.showing = false;
188                return true;
189            }
190
191            Event::KeyboardCharInput(input) => match input.key {
192                Key::Char('c') => {
193                    if let Some(i) = self.list_state.selected() {
194                        let message = self.messages[i].msg.clone();
195                        let _ = self.action_tx.send(Action::CopyMessages(vec![message]));
196                    }
197                }
198                Key::Char('y') => {
199                    let mut selected_messages: Vec<Message> = self
200                        .messages
201                        .iter()
202                        .filter(|msg| msg.selected)
203                        .map(|msg| msg.msg.clone())
204                        .collect();
205
206                    selected_messages.sort_by_key(|msg| msg.created_at());
207
208                    if !selected_messages.is_empty() {
209                        let _ = self.action_tx.send(Action::CopyMessages(selected_messages));
210                    }
211                }
212                Key::Char('j') => self.next_row(),
213                Key::Char('k') => self.prev_row(),
214                Key::Char('g') => self.list_state.select(Some(0)),
215                Key::Char('G') => {
216                    if self.list_state.selected().is_some() {
217                        self.list_state.select(Some(self.messages.len() - 1));
218                    }
219                }
220                Key::Char(' ') => self.toggle_selected(),
221                Key::Char('q') => {
222                    self.showing = false;
223                    return false;
224                }
225                _ => {}
226            },
227
228            Event::UiScrollUp => self.prev_row(),
229            Event::UiScrollDown => self.next_row(),
230            Event::UiScrollPageUp => self.pageup(),
231            Event::UiScrollPageDown => self.pagedown(),
232
233            _ => {}
234        }
235        false
236    }
237}
238
239fn build_list_items<'a>(messages: &[SelectedMessage], max_width: usize) -> Vec<ListItem<'a>> {
240    messages
241        .iter()
242        .map(|item| {
243            let mut spans = vec![];
244            if item.selected {
245                spans.push(span!(Style::default().fg(Color::Red); "[*]"));
246            } else {
247                spans.push(span!(Style::default(); "[ ]"));
248            }
249            spans.push(span!(Style::default(); " "));
250
251            let fg_color = if item.msg.is_system() {
252                Color::LightCyan
253            } else {
254                Color::LightMagenta
255            };
256
257            let mut content = format!(
258                "{}: {}",
259                if item.msg.is_system() { "S" } else { "U" },
260                item.msg.text()
261            );
262            // If the content is too long, we will truncate it
263            // and add ellipsis
264            if content.width() > max_width {
265                content = content.chars().take(max_width).collect::<String>();
266            }
267
268            spans.push(span!(Style::default(); content));
269            let text = Text::from(Line::from(spans));
270            ListItem::new(text).fg(fg_color)
271        })
272        .collect()
273}
274
275struct SelectedMessage {
276    msg: Message,
277    selected: bool,
278}