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 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 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}