1use super::app::{ChatApp, ChatMode, MsgLinesCache, PerMsgCache};
2use super::markdown::markdown_to_lines;
3use super::theme::Theme;
4use ratatui::{
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7};
8use std::io::Write;
9
10pub fn find_stable_boundary(content: &str) -> usize {
11 let mut fence_count = 0usize;
13 let mut last_safe_boundary = 0usize;
14 let mut i = 0;
15 let bytes = content.as_bytes();
16 while i < bytes.len() {
17 if i + 2 < bytes.len() && bytes[i] == b'`' && bytes[i + 1] == b'`' && bytes[i + 2] == b'`' {
19 fence_count += 1;
20 i += 3;
21 while i < bytes.len() && bytes[i] != b'\n' {
23 i += 1;
24 }
25 continue;
26 }
27 if i + 1 < bytes.len() && bytes[i] == b'\n' && bytes[i + 1] == b'\n' {
29 if fence_count % 2 == 0 {
31 last_safe_boundary = i + 2; }
33 i += 2;
34 continue;
35 }
36 i += 1;
37 }
38 last_safe_boundary
39}
40
41pub fn build_message_lines_incremental(
46 app: &ChatApp,
47 inner_width: usize,
48 bubble_max_width: usize,
49 old_cache: Option<&MsgLinesCache>,
50) -> (
51 Vec<Line<'static>>,
52 Vec<(usize, usize)>,
53 Vec<PerMsgCache>,
54 Vec<Line<'static>>,
55 usize,
56) {
57 struct RenderMsg {
58 role: String,
59 content: String,
60 msg_index: Option<usize>,
61 tool_calls: Option<Vec<super::model::ToolCallItem>>,
62 role_label: Option<String>,
63 }
64 let mut render_msgs: Vec<RenderMsg> = app
65 .session
66 .messages
67 .iter()
68 .enumerate()
69 .map(|(i, m)| RenderMsg {
70 role: m.role.clone(),
71 content: m.content.clone(),
72 msg_index: Some(i),
73 tool_calls: m.tool_calls.clone(),
74 role_label: m
75 .tool_call_id
76 .as_ref()
77 .map(|id| format!("工具 {}", &id[..id.len().min(8)])),
78 })
79 .collect();
80
81 let streaming_content_str = if app.is_loading {
83 let streaming = app.streaming_content.lock().unwrap().clone();
84 if !streaming.is_empty() {
85 render_msgs.push(RenderMsg {
86 role: "assistant".to_string(),
87 content: streaming.clone(),
88 msg_index: None,
89 tool_calls: None,
90 role_label: None,
91 });
92 Some(streaming)
93 } else {
94 render_msgs.push(RenderMsg {
95 role: "assistant".to_string(),
96 content: "◍".to_string(),
97 msg_index: None,
98 tool_calls: None,
99 role_label: None,
100 });
101 None
102 }
103 } else {
104 None
105 };
106
107 let t = &app.theme;
108 let is_browse_mode = app.mode == ChatMode::Browse;
109 let mut lines: Vec<Line> = Vec::new();
110 let mut msg_start_lines: Vec<(usize, usize)> = Vec::new();
111 let mut per_msg_cache: Vec<PerMsgCache> = Vec::new();
112
113 let can_reuse_per_msg = old_cache
115 .map(|c| c.bubble_max_width == bubble_max_width)
116 .unwrap_or(false);
117
118 for msg in &render_msgs {
119 let is_selected = is_browse_mode
120 && msg.msg_index.is_some()
121 && msg.msg_index.unwrap() == app.browse_msg_index;
122
123 if let Some(idx) = msg.msg_index {
125 msg_start_lines.push((idx, lines.len()));
126 }
127
128 if let Some(idx) = msg.msg_index {
130 if can_reuse_per_msg {
131 if let Some(old_c) = old_cache {
132 if let Some(old_per) = old_c.per_msg_lines.iter().find(|p| p.msg_index == idx) {
134 if old_per.content_len == msg.content.len()
137 && old_per.is_selected == is_selected
138 {
139 lines.extend(old_per.lines.iter().cloned());
141 per_msg_cache.push(PerMsgCache {
142 content_len: old_per.content_len,
143 lines: old_per.lines.clone(),
144 msg_index: idx,
145 is_selected,
146 });
147 continue;
148 }
149 }
150 }
151 }
152 }
153
154 let msg_lines_start = lines.len();
156 match msg.role.as_str() {
157 "user" => {
158 render_user_msg(
159 &msg.content,
160 is_selected,
161 inner_width,
162 bubble_max_width,
163 &mut lines,
164 t,
165 );
166 }
167 "assistant" => {
168 if msg.msg_index.is_none() {
169 } else if msg.tool_calls.is_some() {
173 render_tool_call_request_msg(
175 &msg.tool_calls.as_ref().unwrap(),
176 bubble_max_width,
177 &mut lines,
178 t,
179 );
180 } else {
181 render_assistant_msg(
183 &msg.content,
184 is_selected,
185 bubble_max_width,
186 &mut lines,
187 t,
188 );
189 }
190 }
191 "tool" => {
192 render_tool_result_msg(
193 &msg.content,
194 msg.role_label.as_deref().unwrap_or("工具结果"),
195 &mut lines,
196 t,
197 );
198 }
199 "system" => {
200 lines.push(Line::from(""));
201 let wrapped = wrap_text(&msg.content, inner_width.saturating_sub(8));
202 for wl in wrapped {
203 lines.push(Line::from(Span::styled(
204 format!(" {} {}", "sys", wl),
205 Style::default().fg(t.text_system),
206 )));
207 }
208 }
209 _ => {}
210 }
211
212 if msg.role == "assistant" && msg.msg_index.is_none() {
214 let bubble_bg = t.bubble_ai;
216 let pad_left_w = 3usize;
217 let pad_right_w = 3usize;
218 let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
219 let bubble_total_w = bubble_max_width;
220
221 lines.push(Line::from(""));
223 lines.push(Line::from(Span::styled(
224 " AI",
225 Style::default().fg(t.label_ai).add_modifier(Modifier::BOLD),
226 )));
227
228 lines.push(Line::from(vec![Span::styled(
230 " ".repeat(bubble_total_w),
231 Style::default().bg(bubble_bg),
232 )]));
233
234 let (mut stable_lines, mut stable_offset) = if let Some(old_c) = old_cache {
236 if old_c.bubble_max_width == bubble_max_width {
237 (
238 old_c.streaming_stable_lines.clone(),
239 old_c.streaming_stable_offset,
240 )
241 } else {
242 (Vec::<Line<'static>>::new(), 0)
243 }
244 } else {
245 (Vec::<Line<'static>>::new(), 0)
246 };
247
248 let content = &msg.content;
249 let boundary = find_stable_boundary(content);
251
252 if boundary > stable_offset {
254 let new_stable_text = &content[stable_offset..boundary];
256 let new_md_lines = markdown_to_lines(new_stable_text, md_content_w + 2, t);
257 for md_line in new_md_lines {
259 let bubble_line = wrap_md_line_in_bubble(
260 md_line,
261 bubble_bg,
262 pad_left_w,
263 pad_right_w,
264 bubble_total_w,
265 );
266 stable_lines.push(bubble_line);
267 }
268 stable_offset = boundary;
269 }
270
271 lines.extend(stable_lines.iter().cloned());
273
274 let tail = &content[boundary..];
276 if !tail.is_empty() {
277 let tail_md_lines = markdown_to_lines(tail, md_content_w + 2, t);
278 for md_line in tail_md_lines {
279 let bubble_line = wrap_md_line_in_bubble(
280 md_line,
281 bubble_bg,
282 pad_left_w,
283 pad_right_w,
284 bubble_total_w,
285 );
286 lines.push(bubble_line);
287 }
288 }
289
290 lines.push(Line::from(vec![Span::styled(
292 " ".repeat(bubble_total_w),
293 Style::default().bg(bubble_bg),
294 )]));
295
296 let _ = (stable_lines.clone(), stable_offset);
300
301 } else if let Some(idx) = msg.msg_index {
303 let msg_lines_end = lines.len();
305 let this_msg_lines: Vec<Line<'static>> = lines[msg_lines_start..msg_lines_end].to_vec();
306 let is_selected = is_browse_mode
307 && msg.msg_index.is_some()
308 && msg.msg_index.unwrap() == app.browse_msg_index;
309 per_msg_cache.push(PerMsgCache {
310 content_len: msg.content.len(),
311 lines: this_msg_lines,
312 msg_index: idx,
313 is_selected,
314 });
315 }
316 }
317
318 if app.mode == ChatMode::ToolConfirm {
320 if let Some(tc) = app.active_tool_calls.get(app.pending_tool_idx) {
321 let t = &app.theme;
322 let confirm_bg = t.tool_confirm_bg;
323 let border_color = t.tool_confirm_border;
324 let content_w = bubble_max_width.saturating_sub(6); lines.push(Line::from(""));
328
329 lines.push(Line::from(Span::styled(
331 " 🔧 工具调用确认",
332 Style::default()
333 .fg(t.tool_confirm_title)
334 .add_modifier(Modifier::BOLD),
335 )));
336
337 let top_border = format!(" ┌{}┐", "─".repeat(bubble_max_width.saturating_sub(4)));
339 lines.push(Line::from(Span::styled(
340 top_border,
341 Style::default().fg(border_color).bg(confirm_bg),
342 )));
343
344 {
346 let label = "工具: ";
347 let name = &tc.tool_name;
348 let text_content = format!("{}{}", label, name);
349 let fill = content_w.saturating_sub(display_width(&text_content));
350 lines.push(Line::from(vec![
351 Span::styled(" │ ", Style::default().fg(border_color).bg(confirm_bg)),
352 Span::styled(" ".repeat(1), Style::default().bg(confirm_bg)),
353 Span::styled(
354 label,
355 Style::default().fg(t.tool_confirm_label).bg(confirm_bg),
356 ),
357 Span::styled(
358 name.clone(),
359 Style::default()
360 .fg(t.tool_confirm_name)
361 .bg(confirm_bg)
362 .add_modifier(Modifier::BOLD),
363 ),
364 Span::styled(
365 " ".repeat(fill.saturating_sub(1)),
366 Style::default().bg(confirm_bg),
367 ),
368 Span::styled(" │", Style::default().fg(border_color).bg(confirm_bg)),
369 ]));
370 }
371
372 {
374 let max_msg_w = content_w.saturating_sub(2);
375 let confirm_msg = if display_width(&tc.confirm_message) > max_msg_w {
376 let mut end = max_msg_w.saturating_sub(3);
377 while end > 0 && !tc.confirm_message.is_char_boundary(end) {
378 end -= 1;
379 }
380 format!("{}...", &tc.confirm_message[..end])
381 } else {
382 tc.confirm_message.clone()
383 };
384 let msg_w = display_width(&confirm_msg);
385 let fill = content_w.saturating_sub(msg_w + 2);
386 lines.push(Line::from(vec![
387 Span::styled(" │ ", Style::default().fg(border_color).bg(confirm_bg)),
388 Span::styled(" ".repeat(1), Style::default().bg(confirm_bg)),
389 Span::styled(
390 confirm_msg,
391 Style::default().fg(t.tool_confirm_text).bg(confirm_bg),
392 ),
393 Span::styled(
394 " ".repeat(fill.saturating_sub(1).saturating_add(2)),
395 Style::default().bg(confirm_bg),
396 ),
397 Span::styled(" │", Style::default().fg(border_color).bg(confirm_bg)),
398 ]));
399 }
400
401 {
403 let fill = bubble_max_width.saturating_sub(4);
404 lines.push(Line::from(vec![
405 Span::styled(" │", Style::default().fg(border_color).bg(confirm_bg)),
406 Span::styled(" ".repeat(fill), Style::default().bg(confirm_bg)),
407 Span::styled("│", Style::default().fg(border_color).bg(confirm_bg)),
408 ]));
409 }
410
411 {
413 let hint_text_w = display_width("[Y/Enter] 执行 / [N/Esc] 拒绝");
414 let fill = content_w.saturating_sub(hint_text_w + 2);
415 lines.push(Line::from(vec![
416 Span::styled(" │ ", Style::default().fg(border_color).bg(confirm_bg)),
417 Span::styled(" ".repeat(1), Style::default().bg(confirm_bg)),
418 Span::styled(
419 "[Y/Enter] 执行",
420 Style::default()
421 .fg(t.toast_success_border)
422 .bg(confirm_bg)
423 .add_modifier(Modifier::BOLD),
424 ),
425 Span::styled(
426 " / ",
427 Style::default().fg(t.tool_confirm_label).bg(confirm_bg),
428 ),
429 Span::styled(
430 "[N/Esc] 拒绝",
431 Style::default()
432 .fg(t.toast_error_border)
433 .bg(confirm_bg)
434 .add_modifier(Modifier::BOLD),
435 ),
436 Span::styled(
437 " ".repeat(fill.saturating_sub(1).saturating_add(2)),
438 Style::default().bg(confirm_bg),
439 ),
440 Span::styled(" │", Style::default().fg(border_color).bg(confirm_bg)),
441 ]));
442 }
443
444 let bottom_border = format!(" └{}┘", "─".repeat(bubble_max_width.saturating_sub(4)));
446 lines.push(Line::from(Span::styled(
447 bottom_border,
448 Style::default().fg(border_color).bg(confirm_bg),
449 )));
450 }
451 }
452
453 lines.push(Line::from(""));
455
456 let (final_stable_lines, final_stable_offset) = if let Some(sc) = &streaming_content_str {
458 let boundary = find_stable_boundary(sc);
459 let bubble_bg = t.bubble_ai;
460 let pad_left_w = 3usize;
461 let pad_right_w = 3usize;
462 let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
463 let bubble_total_w = bubble_max_width;
464
465 let (mut s_lines, s_offset) = if let Some(old_c) = old_cache {
466 if old_c.bubble_max_width == bubble_max_width {
467 (
468 old_c.streaming_stable_lines.clone(),
469 old_c.streaming_stable_offset,
470 )
471 } else {
472 (Vec::<Line<'static>>::new(), 0)
473 }
474 } else {
475 (Vec::<Line<'static>>::new(), 0)
476 };
477
478 if boundary > s_offset {
479 let new_text = &sc[s_offset..boundary];
480 let new_md_lines = markdown_to_lines(new_text, md_content_w + 2, t);
481 for md_line in new_md_lines {
482 let bubble_line = wrap_md_line_in_bubble(
483 md_line,
484 bubble_bg,
485 pad_left_w,
486 pad_right_w,
487 bubble_total_w,
488 );
489 s_lines.push(bubble_line);
490 }
491 }
492 (s_lines, boundary)
493 } else {
494 (Vec::new(), 0)
495 };
496
497 (
498 lines,
499 msg_start_lines,
500 per_msg_cache,
501 final_stable_lines,
502 final_stable_offset,
503 )
504}
505
506pub fn wrap_md_line_in_bubble(
508 md_line: Line<'static>,
509 bubble_bg: Color,
510 pad_left_w: usize,
511 pad_right_w: usize,
512 bubble_total_w: usize,
513) -> Line<'static> {
514 let pad_left = " ".repeat(pad_left_w);
515 let pad_right = " ".repeat(pad_right_w);
516 let mut styled_spans: Vec<Span> = Vec::new();
517 styled_spans.push(Span::styled(pad_left, Style::default().bg(bubble_bg)));
518 let target_content_w = bubble_total_w.saturating_sub(pad_left_w + pad_right_w);
519 let mut content_w: usize = 0;
520 for span in md_line.spans {
521 let sw = display_width(&span.content);
522 if content_w + sw > target_content_w {
523 let remaining = target_content_w.saturating_sub(content_w);
525 if remaining > 0 {
526 let mut truncated = String::new();
527 let mut tw = 0;
528 for ch in span.content.chars() {
529 let cw = char_width(ch);
530 if tw + cw > remaining {
531 break;
532 }
533 truncated.push(ch);
534 tw += cw;
535 }
536 if !truncated.is_empty() {
537 content_w += tw;
538 let merged_style = span.style.bg(bubble_bg);
539 styled_spans.push(Span::styled(truncated, merged_style));
540 }
541 }
542 break;
544 }
545 content_w += sw;
546 let merged_style = span.style.bg(bubble_bg);
547 styled_spans.push(Span::styled(span.content.to_string(), merged_style));
548 }
549 let fill = target_content_w.saturating_sub(content_w);
550 if fill > 0 {
551 styled_spans.push(Span::styled(
552 " ".repeat(fill),
553 Style::default().bg(bubble_bg),
554 ));
555 }
556 styled_spans.push(Span::styled(pad_right, Style::default().bg(bubble_bg)));
557 Line::from(styled_spans)
558}
559
560pub fn render_user_msg(
562 content: &str,
563 is_selected: bool,
564 inner_width: usize,
565 bubble_max_width: usize,
566 lines: &mut Vec<Line<'static>>,
567 theme: &Theme,
568) {
569 lines.push(Line::from(""));
570 let label = if is_selected { "▶ You " } else { "You " };
571 let pad = inner_width.saturating_sub(display_width(label) + 2);
572 lines.push(Line::from(vec![
573 Span::raw(" ".repeat(pad)),
574 Span::styled(
575 label,
576 Style::default()
577 .fg(if is_selected {
578 theme.label_selected
579 } else {
580 theme.label_user
581 })
582 .add_modifier(Modifier::BOLD),
583 ),
584 ]));
585 let user_bg = if is_selected {
586 theme.bubble_user_selected
587 } else {
588 theme.bubble_user
589 };
590 let user_pad_lr = 3usize;
591 let user_content_w = bubble_max_width.saturating_sub(user_pad_lr * 2);
592 let mut all_wrapped_lines: Vec<String> = Vec::new();
593 for content_line in content.lines() {
594 let wrapped = wrap_text(content_line, user_content_w);
595 all_wrapped_lines.extend(wrapped);
596 }
597 if all_wrapped_lines.is_empty() {
598 all_wrapped_lines.push(String::new());
599 }
600 let actual_content_w = all_wrapped_lines
601 .iter()
602 .map(|l| display_width(l))
603 .max()
604 .unwrap_or(0);
605 let actual_bubble_w = (actual_content_w + user_pad_lr * 2)
606 .min(bubble_max_width)
607 .max(user_pad_lr * 2 + 1);
608 let actual_inner_content_w = actual_bubble_w.saturating_sub(user_pad_lr * 2);
609 {
611 let bubble_text = " ".repeat(actual_bubble_w);
612 let pad = inner_width.saturating_sub(actual_bubble_w);
613 lines.push(Line::from(vec![
614 Span::raw(" ".repeat(pad)),
615 Span::styled(bubble_text, Style::default().bg(user_bg)),
616 ]));
617 }
618 for wl in &all_wrapped_lines {
619 let wl_width = display_width(wl);
620 let fill = actual_inner_content_w.saturating_sub(wl_width);
621 let text = format!(
622 "{}{}{}{}",
623 " ".repeat(user_pad_lr),
624 wl,
625 " ".repeat(fill),
626 " ".repeat(user_pad_lr),
627 );
628 let text_width = display_width(&text);
629 let pad = inner_width.saturating_sub(text_width);
630 lines.push(Line::from(vec![
631 Span::raw(" ".repeat(pad)),
632 Span::styled(text, Style::default().fg(theme.text_white).bg(user_bg)),
633 ]));
634 }
635 {
637 let bubble_text = " ".repeat(actual_bubble_w);
638 let pad = inner_width.saturating_sub(actual_bubble_w);
639 lines.push(Line::from(vec![
640 Span::raw(" ".repeat(pad)),
641 Span::styled(bubble_text, Style::default().bg(user_bg)),
642 ]));
643 }
644}
645
646pub fn render_assistant_msg(
648 content: &str,
649 is_selected: bool,
650 bubble_max_width: usize,
651 lines: &mut Vec<Line<'static>>,
652 theme: &Theme,
653) {
654 lines.push(Line::from(""));
655 let ai_label = if is_selected { " ▶ AI" } else { " AI" };
656 lines.push(Line::from(Span::styled(
657 ai_label,
658 Style::default()
659 .fg(if is_selected {
660 theme.label_selected
661 } else {
662 theme.label_ai
663 })
664 .add_modifier(Modifier::BOLD),
665 )));
666 let bubble_bg = if is_selected {
667 theme.bubble_ai_selected
668 } else {
669 theme.bubble_ai
670 };
671 let pad_left_w = 3usize;
672 let pad_right_w = 3usize;
673 let md_content_w = bubble_max_width.saturating_sub(pad_left_w + pad_right_w);
674 let md_lines = markdown_to_lines(content, md_content_w + 2, theme);
675 let bubble_total_w = bubble_max_width;
676 lines.push(Line::from(vec![Span::styled(
678 " ".repeat(bubble_total_w),
679 Style::default().bg(bubble_bg),
680 )]));
681 for md_line in md_lines {
682 let bubble_line =
683 wrap_md_line_in_bubble(md_line, bubble_bg, pad_left_w, pad_right_w, bubble_total_w);
684 lines.push(bubble_line);
685 }
686 lines.push(Line::from(vec![Span::styled(
688 " ".repeat(bubble_total_w),
689 Style::default().bg(bubble_bg),
690 )]));
691}
692
693pub fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
698 let max_width = max_width.max(2);
700 let mut result = Vec::new();
701 let mut current_line = String::new();
702 let mut current_width = 0;
703
704 for ch in text.chars() {
705 let ch_width = char_width(ch);
706 if current_width + ch_width > max_width && !current_line.is_empty() {
707 result.push(current_line.clone());
708 current_line.clear();
709 current_width = 0;
710 }
711 current_line.push(ch);
712 current_width += ch_width;
713 }
714 if !current_line.is_empty() {
715 result.push(current_line);
716 }
717 if result.is_empty() {
718 result.push(String::new());
719 }
720 result
721}
722
723pub fn display_width(s: &str) -> usize {
725 use unicode_width::UnicodeWidthStr;
726 UnicodeWidthStr::width(s)
727}
728
729pub fn char_width(c: char) -> usize {
731 use unicode_width::UnicodeWidthChar;
732 UnicodeWidthChar::width(c).unwrap_or(0)
733}
734
735pub fn render_tool_call_request_msg(
737 tool_calls: &[super::model::ToolCallItem],
738 bubble_max_width: usize,
739 lines: &mut Vec<Line<'static>>,
740 theme: &Theme,
741) {
742 lines.push(Line::from(""));
743 lines.push(Line::from(Span::styled(
744 " 🔧 AI 调用工具",
745 Style::default()
746 .fg(Color::Yellow)
747 .add_modifier(Modifier::BOLD),
748 )));
749 let bubble_bg = Color::Rgb(40, 35, 10);
750 let pad = 3usize;
751 let content_w = bubble_max_width.saturating_sub(pad * 2);
752 lines.push(Line::from(vec![Span::styled(
753 " ".repeat(bubble_max_width),
754 Style::default().bg(bubble_bg),
755 )]));
756 for tc in tool_calls {
757 let args_preview: String = tc.arguments.chars().take(50).collect();
758 let args_display = if tc.arguments.len() > 50 {
759 format!("{}...", args_preview)
760 } else {
761 args_preview
762 };
763 let text = format!("{} ({})", tc.name, args_display);
764 let wrapped = wrap_text(&text, content_w);
765 for wl in wrapped {
766 let fill = content_w.saturating_sub(display_width(&wl));
767 lines.push(Line::from(vec![
768 Span::styled(" ".repeat(pad), Style::default().bg(bubble_bg)),
769 Span::styled(wl, Style::default().fg(Color::Yellow).bg(bubble_bg)),
770 Span::styled(" ".repeat(fill), Style::default().bg(bubble_bg)),
771 Span::styled(" ".repeat(pad), Style::default().bg(bubble_bg)),
772 ]));
773 }
774 }
775 let _ = theme; lines.push(Line::from(vec![Span::styled(
777 " ".repeat(bubble_max_width),
778 Style::default().bg(bubble_bg),
779 )]));
780}
781
782pub fn render_tool_result_msg(
784 content: &str,
785 label: &str,
786 lines: &mut Vec<Line<'static>>,
787 theme: &Theme,
788) {
789 lines.push(Line::from(""));
790 lines.push(Line::from(Span::styled(
791 format!(" ✅ {}", label),
792 Style::default()
793 .fg(Color::Green)
794 .add_modifier(Modifier::BOLD),
795 )));
796 let bubble_bg = Color::Rgb(10, 40, 15);
797 let pad = 3usize;
798 let content_w = 60usize;
799 let bubble_w = content_w + pad * 2;
800 lines.push(Line::from(vec![Span::styled(
801 " ".repeat(bubble_w),
802 Style::default().bg(bubble_bg),
803 )]));
804 let display_content = if content.len() > 200 {
805 let mut end = 200;
806 while !content.is_char_boundary(end) {
807 end -= 1;
808 }
809 format!("{}...", &content[..end])
810 } else {
811 content.to_string()
812 };
813 let all_lines: Vec<String> = display_content
814 .lines()
815 .flat_map(|l| wrap_text(l, content_w))
816 .take(5)
817 .collect();
818 for wl in all_lines {
819 let fill = content_w.saturating_sub(display_width(&wl));
820 lines.push(Line::from(vec![
821 Span::styled(" ".repeat(pad), Style::default().bg(bubble_bg)),
822 Span::styled(
823 wl,
824 Style::default().fg(Color::Rgb(180, 255, 180)).bg(bubble_bg),
825 ),
826 Span::styled(" ".repeat(fill), Style::default().bg(bubble_bg)),
827 Span::styled(" ".repeat(pad), Style::default().bg(bubble_bg)),
828 ]));
829 }
830 let _ = theme;
831 lines.push(Line::from(vec![Span::styled(
832 " ".repeat(bubble_w),
833 Style::default().bg(bubble_bg),
834 )]));
835}
836
837pub fn copy_to_clipboard(content: &str) -> bool {
838 use std::process::{Command, Stdio};
839
840 let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "macos") {
841 ("pbcopy", vec![])
842 } else if cfg!(target_os = "linux") {
843 if Command::new("which")
844 .arg("xclip")
845 .output()
846 .map(|o| o.status.success())
847 .unwrap_or(false)
848 {
849 ("xclip", vec!["-selection", "clipboard"])
850 } else {
851 ("xsel", vec!["--clipboard", "--input"])
852 }
853 } else {
854 return false;
855 };
856
857 let child = Command::new(cmd).args(&args).stdin(Stdio::piped()).spawn();
858
859 match child {
860 Ok(mut child) => {
861 if let Some(ref mut stdin) = child.stdin {
862 let _ = stdin.write_all(content.as_bytes());
863 }
864 child.wait().map(|s| s.success()).unwrap_or(false)
865 }
866 Err(_) => false,
867 }
868}