1use super::*;
2use crate::RichLogState;
3
4impl Context {
5 pub fn rich_log(&mut self, state: &mut RichLogState) -> Response {
7 let focused = self.register_focusable();
8 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
9
10 let widget_height = if response.rect.height > 0 {
11 response.rect.height as usize
12 } else {
13 self.area_height as usize
14 };
15 let viewport_height = widget_height.saturating_sub(2);
16 let effective_height = if viewport_height == 0 {
17 state.entries.len().max(1)
18 } else {
19 viewport_height
20 };
21 let show_indicator = state.entries.len() > effective_height;
22 let visible_rows = if show_indicator {
23 effective_height.saturating_sub(1).max(1)
24 } else {
25 effective_height
26 };
27 let max_offset = state.entries.len().saturating_sub(visible_rows);
28 if state.auto_scroll && state.scroll_offset == usize::MAX {
29 state.scroll_offset = max_offset;
30 } else {
31 state.scroll_offset = state.scroll_offset.min(max_offset);
32 }
33 let old_offset = state.scroll_offset;
34
35 if focused {
36 let mut consumed_indices = Vec::new();
37 for (i, key) in self.available_key_presses() {
38 match key.code {
39 KeyCode::Up | KeyCode::Char('k') => {
40 state.scroll_offset = state.scroll_offset.saturating_sub(1);
41 consumed_indices.push(i);
42 }
43 KeyCode::Down | KeyCode::Char('j') => {
44 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
45 consumed_indices.push(i);
46 }
47 KeyCode::PageUp => {
48 state.scroll_offset = state.scroll_offset.saturating_sub(10);
49 consumed_indices.push(i);
50 }
51 KeyCode::PageDown => {
52 state.scroll_offset = (state.scroll_offset + 10).min(max_offset);
53 consumed_indices.push(i);
54 }
55 KeyCode::Home => {
56 state.scroll_offset = 0;
57 consumed_indices.push(i);
58 }
59 KeyCode::End => {
60 state.scroll_offset = max_offset;
61 consumed_indices.push(i);
62 }
63 _ => {}
64 }
65 }
66 self.consume_indices(consumed_indices);
67 }
68
69 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
70 let mut consumed = Vec::new();
71 for (i, mouse) in self.mouse_events_in_rect(rect) {
72 let delta = self.scroll_lines_per_event as usize;
73 match mouse.kind {
74 MouseKind::ScrollUp => {
75 state.scroll_offset = state.scroll_offset.saturating_sub(delta);
76 consumed.push(i);
77 }
78 MouseKind::ScrollDown => {
79 state.scroll_offset = (state.scroll_offset + delta).min(max_offset);
80 consumed.push(i);
81 }
82 _ => {}
83 }
84 }
85 self.consume_indices(consumed);
86 }
87
88 state.scroll_offset = state.scroll_offset.min(max_offset);
89 let start = state
90 .scroll_offset
91 .min(state.entries.len().saturating_sub(visible_rows));
92 let end = (start + visible_rows).min(state.entries.len());
93
94 self.commands
95 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
96 direction: Direction::Column,
97 gap: 0,
98 align: Align::Start,
99 align_self: None,
100 justify: Justify::Start,
101 border: Some(Border::Single),
102 border_sides: BorderSides::all(),
103 border_style: Style::new().fg(self.theme.border),
104 bg_color: None,
105 padding: Padding::default(),
106 margin: Margin::default(),
107 constraints: Constraints::default(),
108 title: None,
109 grow: 0,
110 group_name: None,
111 })));
112
113 for entry in state
114 .entries
115 .iter()
116 .skip(start)
117 .take(end.saturating_sub(start))
118 {
119 self.commands.push(Command::RichText {
120 segments: entry.segments.clone(),
121 wrap: false,
122 align: Align::Start,
123 margin: Margin::default(),
124 constraints: Constraints::default(),
125 });
126 }
127
128 if show_indicator {
129 let end_pos = end.min(state.entries.len());
130 let line = format!(
131 "{}-{} / {}",
132 start.saturating_add(1),
133 end_pos,
134 state.entries.len()
135 );
136 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
137 }
138
139 self.commands.push(Command::EndContainer);
140 self.rollback.last_text_idx = None;
141 response.changed = state.scroll_offset != old_offset;
142 response
143 }
144
145 pub fn virtual_list(
152 &mut self,
153 state: &mut ListState,
154 visible_height: u32,
155 f: impl Fn(&mut Context, usize),
156 ) -> Response {
157 if state.items.is_empty() {
158 return Response::none();
159 }
160 state.selected = state.selected.min(state.items.len().saturating_sub(1));
161 let focused = self.register_focusable();
162 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
163 let old_selected = state.selected;
164
165 if focused {
166 let mut consumed_indices = Vec::new();
167 for (i, key) in self.available_key_presses() {
168 match key.code {
169 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
170 let _ = handle_vertical_nav(
171 &mut state.selected,
172 state.items.len().saturating_sub(1),
173 key.code.clone(),
174 );
175 consumed_indices.push(i);
176 }
177 KeyCode::PageUp => {
178 state.selected = state.selected.saturating_sub(visible_height as usize);
179 consumed_indices.push(i);
180 }
181 KeyCode::PageDown => {
182 state.selected = (state.selected + visible_height as usize)
183 .min(state.items.len().saturating_sub(1));
184 consumed_indices.push(i);
185 }
186 KeyCode::Home => {
187 state.selected = 0;
188 consumed_indices.push(i);
189 }
190 KeyCode::End => {
191 state.selected = state.items.len().saturating_sub(1);
192 consumed_indices.push(i);
193 }
194 _ => {}
195 }
196 }
197 self.consume_indices(consumed_indices);
198 }
199
200 let vh = visible_height as usize;
201 let start = if state.selected >= vh {
202 state.selected - vh + 1
203 } else {
204 0
205 };
206 let end = (start + vh).min(state.items.len());
207
208 self.commands
209 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
210 direction: Direction::Column,
211 gap: 0,
212 align: Align::Start,
213 align_self: None,
214 justify: Justify::Start,
215 border: None,
216 border_sides: BorderSides::all(),
217 border_style: Style::new().fg(self.theme.border),
218 bg_color: None,
219 padding: Padding::default(),
220 margin: Margin::default(),
221 constraints: Constraints::default(),
222 title: None,
223 grow: 0,
224 group_name: None,
225 })));
226
227 if start > 0 {
228 let hidden = start.to_string();
229 let mut line = String::with_capacity(hidden.len() + 10);
230 line.push_str(" ↑ ");
231 line.push_str(&hidden);
232 line.push_str(" more");
233 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
234 }
235
236 for idx in start..end {
237 f(self, idx);
238 }
239
240 let remaining = state.items.len().saturating_sub(end);
241 if remaining > 0 {
242 let hidden = remaining.to_string();
243 let mut line = String::with_capacity(hidden.len() + 10);
244 line.push_str(" ↓ ");
245 line.push_str(&hidden);
246 line.push_str(" more");
247 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
248 }
249
250 self.commands.push(Command::EndContainer);
251 self.rollback.last_text_idx = None;
252 response.changed = state.selected != old_selected;
253 response
254 }
255
256 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
260 if !state.open {
261 return Response::none();
262 }
263
264 state.last_selected = None;
265 let interaction_id = self.next_interaction_id();
266
267 let filtered = state.filtered_indices();
268 let sel = state.selected().min(filtered.len().saturating_sub(1));
269 state.set_selected(sel);
270
271 let mut consumed_indices = Vec::new();
272
273 for (i, key) in self.available_key_presses() {
274 match key.code {
275 KeyCode::Esc => {
276 state.open = false;
277 consumed_indices.push(i);
278 }
279 KeyCode::Up => {
280 let s = state.selected();
281 state.set_selected(s.saturating_sub(1));
282 consumed_indices.push(i);
283 }
284 KeyCode::Down => {
285 let s = state.selected();
286 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
287 consumed_indices.push(i);
288 }
289 KeyCode::Enter => {
290 if let Some(&cmd_idx) = filtered.get(state.selected()) {
291 state.last_selected = Some(cmd_idx);
292 state.open = false;
293 }
294 consumed_indices.push(i);
295 }
296 KeyCode::Backspace => {
297 if state.cursor > 0 {
298 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
299 let end_idx = byte_index_for_char(&state.input, state.cursor);
300 state.input.replace_range(byte_idx..end_idx, "");
301 state.cursor -= 1;
302 state.set_selected(0);
303 }
304 consumed_indices.push(i);
305 }
306 KeyCode::Char(ch) => {
307 let byte_idx = byte_index_for_char(&state.input, state.cursor);
308 state.input.insert(byte_idx, ch);
309 state.cursor += 1;
310 state.set_selected(0);
311 consumed_indices.push(i);
312 }
313 _ => {}
314 }
315 }
316 self.consume_indices(consumed_indices);
317
318 let filtered = state.filtered_indices();
319
320 let _ = self.modal(|ui| {
321 let primary = ui.theme.primary;
322 let _ = ui
323 .container()
324 .border(Border::Rounded)
325 .border_style(Style::new().fg(primary))
326 .pad(1)
327 .max_w(60)
328 .col(|ui| {
329 let border_color = ui.theme.primary;
330 let _ = ui
331 .bordered(Border::Rounded)
332 .border_style(Style::new().fg(border_color))
333 .px(1)
334 .col(|ui| {
335 let display = if state.input.is_empty() {
336 "Type to search...".to_string()
337 } else {
338 state.input.clone()
339 };
340 let style = if state.input.is_empty() {
341 Style::new().dim().fg(ui.theme.text_dim)
342 } else {
343 Style::new().fg(ui.theme.text)
344 };
345 ui.styled(display, style);
346 });
347
348 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
349 let cmd = &state.commands[cmd_idx];
350 let is_selected = list_idx == state.selected();
351 let style = if is_selected {
352 Style::new().bold().fg(ui.theme.primary)
353 } else {
354 Style::new().fg(ui.theme.text)
355 };
356 let prefix = if is_selected { "▸ " } else { " " };
357 let shortcut_text = cmd
358 .shortcut
359 .as_deref()
360 .map(|s| {
361 let mut text = String::with_capacity(s.len() + 4);
362 text.push_str(" (");
363 text.push_str(s);
364 text.push(')');
365 text
366 })
367 .unwrap_or_default();
368 let mut line = String::with_capacity(
369 prefix.len() + cmd.label.len() + shortcut_text.len(),
370 );
371 line.push_str(prefix);
372 line.push_str(&cmd.label);
373 line.push_str(&shortcut_text);
374 ui.styled(line, style);
375 if is_selected && !cmd.description.is_empty() {
376 let mut desc = String::with_capacity(4 + cmd.description.len());
377 desc.push_str(" ");
378 desc.push_str(&cmd.description);
379 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
380 }
381 }
382
383 if filtered.is_empty() {
384 ui.styled(
385 " No matching commands",
386 Style::new().dim().fg(ui.theme.text_dim),
387 );
388 }
389 });
390 });
391
392 let mut response = self.response_for(interaction_id);
393 response.changed = state.last_selected.is_some();
394 response
395 }
396
397 pub fn markdown(&mut self, text: &str) -> Response {
407 self.commands
408 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
409 direction: Direction::Column,
410 gap: 0,
411 align: Align::Start,
412 align_self: None,
413 justify: Justify::Start,
414 border: None,
415 border_sides: BorderSides::all(),
416 border_style: Style::new().fg(self.theme.border),
417 bg_color: None,
418 padding: Padding::default(),
419 margin: Margin::default(),
420 constraints: Constraints::default(),
421 title: None,
422 grow: 0,
423 group_name: None,
424 })));
425 self.skip_interaction_slot();
426
427 let text_style = Style::new().fg(self.theme.text);
428 let bold_style = Style::new().fg(self.theme.text).bold();
429 let code_style = Style::new().fg(self.theme.accent);
430 let border_style = Style::new().fg(self.theme.border).dim();
431
432 let mut in_code_block = false;
433 let mut code_block_lang = String::new();
434 let mut code_block_lines: Vec<String> = Vec::new();
435 let mut table_lines: Vec<String> = Vec::new();
436
437 for line in text.lines() {
438 let trimmed = line.trim();
439
440 if in_code_block {
441 if trimmed.starts_with("```") {
442 in_code_block = false;
443 let code_content = code_block_lines.join("\n");
444 let theme = self.theme;
445 let highlighted: Option<Vec<Vec<(String, Style)>>> =
446 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
447 let _ = self.container().bg(theme.surface).p(1).col(|ui| {
448 if let Some(ref hl_lines) = highlighted {
449 for segs in hl_lines {
450 if segs.is_empty() {
451 ui.text(" ");
452 } else {
453 ui.line(|ui| {
454 for (t, s) in segs {
455 ui.styled(t, *s);
456 }
457 });
458 }
459 }
460 } else {
461 for cl in &code_block_lines {
462 ui.styled(cl, code_style);
463 }
464 }
465 });
466 code_block_lang.clear();
467 code_block_lines.clear();
468 } else {
469 code_block_lines.push(line.to_string());
470 }
471 continue;
472 }
473
474 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
476 table_lines.push(trimmed.to_string());
477 continue;
478 }
479 if !table_lines.is_empty() {
481 self.render_markdown_table(
482 &table_lines,
483 text_style,
484 bold_style,
485 code_style,
486 border_style,
487 );
488 table_lines.clear();
489 }
490
491 if trimmed.is_empty() {
492 self.text(" ");
493 continue;
494 }
495 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
496 self.styled("─".repeat(40), border_style);
497 continue;
498 }
499 if let Some(quote) = trimmed.strip_prefix("> ") {
500 let quote_style = Style::new().fg(self.theme.text_dim).italic();
501 let bar_style = Style::new().fg(self.theme.border);
502 self.line(|ui| {
503 ui.styled("│ ", bar_style);
504 ui.styled(quote, quote_style);
505 });
506 } else if let Some(heading) = trimmed.strip_prefix("### ") {
507 self.styled(heading, Style::new().bold().fg(self.theme.accent));
508 } else if let Some(heading) = trimmed.strip_prefix("## ") {
509 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
510 } else if let Some(heading) = trimmed.strip_prefix("# ") {
511 self.styled(heading, Style::new().bold().fg(self.theme.primary));
512 } else if let Some(item) = trimmed
513 .strip_prefix("- ")
514 .or_else(|| trimmed.strip_prefix("* "))
515 {
516 self.line_wrap(|ui| {
517 ui.styled(" • ", text_style);
518 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
519 });
520 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
521 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
522 if parts.len() == 2 {
523 self.line_wrap(|ui| {
524 let mut prefix = String::with_capacity(4 + parts[0].len());
525 prefix.push_str(" ");
526 prefix.push_str(parts[0]);
527 prefix.push_str(". ");
528 ui.styled(prefix, text_style);
529 Self::render_md_inline_into(
530 ui, parts[1], text_style, bold_style, code_style,
531 );
532 });
533 } else {
534 self.text(trimmed);
535 }
536 } else if let Some(lang) = trimmed.strip_prefix("```") {
537 in_code_block = true;
538 code_block_lang = lang.trim().to_string();
539 } else {
540 self.render_md_inline(trimmed, text_style, bold_style, code_style);
541 }
542 }
543
544 if in_code_block && !code_block_lines.is_empty() {
545 for cl in &code_block_lines {
546 self.styled(cl, code_style);
547 }
548 }
549
550 if !table_lines.is_empty() {
552 self.render_markdown_table(
553 &table_lines,
554 text_style,
555 bold_style,
556 code_style,
557 border_style,
558 );
559 }
560
561 self.commands.push(Command::EndContainer);
562 self.rollback.last_text_idx = None;
563 Response::none()
564 }
565
566 fn render_markdown_table(
568 &mut self,
569 lines: &[String],
570 text_style: Style,
571 bold_style: Style,
572 code_style: Style,
573 border_style: Style,
574 ) {
575 if lines.is_empty() {
576 return;
577 }
578
579 let is_separator = |line: &str| -> bool {
581 let inner = line.trim_matches('|').trim();
582 !inner.is_empty()
583 && inner
584 .chars()
585 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
586 };
587
588 let parse_row = |line: &str| -> Vec<String> {
589 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
590 trimmed.split('|').map(|c| c.trim().to_string()).collect()
591 };
592
593 let mut header: Option<Vec<String>> = None;
594 let mut data_rows: Vec<Vec<String>> = Vec::new();
595 let mut found_separator = false;
596
597 for (i, line) in lines.iter().enumerate() {
598 if is_separator(line) {
599 found_separator = true;
600 continue;
601 }
602 if i == 0 && !found_separator {
603 header = Some(parse_row(line));
604 } else {
605 data_rows.push(parse_row(line));
606 }
607 }
608
609 if !found_separator && header.is_none() && !data_rows.is_empty() {
611 header = Some(data_rows.remove(0));
612 }
613
614 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
616 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
617 if col_count == 0 {
618 return;
619 }
620 let mut col_widths = vec![0usize; col_count];
621 let stripped_rows: Vec<Vec<String>> = all_rows
623 .iter()
624 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
625 .collect();
626 for row in &stripped_rows {
627 for (i, cell) in row.iter().enumerate() {
628 if i < col_count {
629 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
630 }
631 }
632 }
633
634 let mut top = String::from("┌");
636 for (i, &w) in col_widths.iter().enumerate() {
637 for _ in 0..w + 2 {
638 top.push('─');
639 }
640 top.push(if i < col_count - 1 { '┬' } else { '┐' });
641 }
642 self.styled(&top, border_style);
643
644 if let Some(ref hdr) = header {
646 self.line(|ui| {
647 ui.styled("│", border_style);
648 for (i, w) in col_widths.iter().enumerate() {
649 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
650 let display_text = Self::md_strip(raw);
651 let cell_w = UnicodeWidthStr::width(display_text.as_str());
652 let padding: String = " ".repeat(w.saturating_sub(cell_w));
653 ui.styled(" ", bold_style);
654 ui.styled(&display_text, bold_style);
655 ui.styled(padding, bold_style);
656 ui.styled(" │", border_style);
657 }
658 });
659
660 let mut sep = String::from("├");
662 for (i, &w) in col_widths.iter().enumerate() {
663 for _ in 0..w + 2 {
664 sep.push('─');
665 }
666 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
667 }
668 self.styled(&sep, border_style);
669 }
670
671 for row in &data_rows {
673 self.line(|ui| {
674 ui.styled("│", border_style);
675 for (i, w) in col_widths.iter().enumerate() {
676 let raw = row.get(i).map(String::as_str).unwrap_or("");
677 let display_text = Self::md_strip(raw);
678 let cell_w = UnicodeWidthStr::width(display_text.as_str());
679 let padding: String = " ".repeat(w.saturating_sub(cell_w));
680 ui.styled(" ", text_style);
681 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
682 ui.styled(padding, text_style);
683 ui.styled(" │", border_style);
684 }
685 });
686 }
687
688 let mut bot = String::from("└");
690 for (i, &w) in col_widths.iter().enumerate() {
691 for _ in 0..w + 2 {
692 bot.push('─');
693 }
694 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
695 }
696 self.styled(&bot, border_style);
697 }
698
699 pub(crate) fn parse_inline_segments(
700 text: &str,
701 base: Style,
702 bold: Style,
703 code: Style,
704 ) -> Vec<(String, Style)> {
705 let mut segments: Vec<(String, Style)> = Vec::new();
706 let mut current = String::new();
707 let chars: Vec<char> = text.chars().collect();
708 let mut i = 0;
709 while i < chars.len() {
710 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
711 let rest: String = chars[i + 2..].iter().collect();
712 if let Some(end) = rest.find("**") {
713 if !current.is_empty() {
714 segments.push((std::mem::take(&mut current), base));
715 }
716 let inner: String = rest[..end].to_string();
717 let char_count = inner.chars().count();
718 segments.push((inner, bold));
719 i += 2 + char_count + 2;
720 continue;
721 }
722 }
723 if chars[i] == '*'
724 && (i + 1 >= chars.len() || chars[i + 1] != '*')
725 && (i == 0 || chars[i - 1] != '*')
726 {
727 let rest: String = chars[i + 1..].iter().collect();
728 if let Some(end) = rest.find('*') {
729 if !current.is_empty() {
730 segments.push((std::mem::take(&mut current), base));
731 }
732 let inner: String = rest[..end].to_string();
733 let char_count = inner.chars().count();
734 segments.push((inner, base.italic()));
735 i += 1 + char_count + 1;
736 continue;
737 }
738 }
739 if chars[i] == '`' {
740 let rest: String = chars[i + 1..].iter().collect();
741 if let Some(end) = rest.find('`') {
742 if !current.is_empty() {
743 segments.push((std::mem::take(&mut current), base));
744 }
745 let inner: String = rest[..end].to_string();
746 let char_count = inner.chars().count();
747 segments.push((inner, code));
748 i += 1 + char_count + 1;
749 continue;
750 }
751 }
752 current.push(chars[i]);
753 i += 1;
754 }
755 if !current.is_empty() {
756 segments.push((current, base));
757 }
758 segments
759 }
760
761 fn render_md_inline(
766 &mut self,
767 text: &str,
768 text_style: Style,
769 bold_style: Style,
770 code_style: Style,
771 ) {
772 let items = Self::split_md_links(text);
773
774 if items.len() == 1 {
776 if let MdInline::Text(ref t) = items[0] {
777 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
778 if segs.len() <= 1 {
779 self.text(text)
780 .wrap()
781 .fg(text_style.fg.unwrap_or(Color::Reset));
782 } else {
783 self.line_wrap(|ui| {
784 for (s, st) in segs {
785 ui.styled(s, st);
786 }
787 });
788 }
789 return;
790 }
791 }
792
793 self.line_wrap(|ui| {
795 for item in &items {
796 match item {
797 MdInline::Text(t) => {
798 let segs =
799 Self::parse_inline_segments(t, text_style, bold_style, code_style);
800 for (s, st) in segs {
801 ui.styled(s, st);
802 }
803 }
804 MdInline::Link { text, url } => {
805 ui.link(text.clone(), url.clone());
806 }
807 MdInline::Image { alt, .. } => {
808 ui.styled(alt.as_str(), code_style);
810 }
811 }
812 }
813 });
814 }
815
816 fn render_md_inline_into(
822 ui: &mut Context,
823 text: &str,
824 text_style: Style,
825 bold_style: Style,
826 code_style: Style,
827 ) {
828 let items = Self::split_md_links(text);
829 for item in &items {
830 match item {
831 MdInline::Text(t) => {
832 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
833 for (s, st) in segs {
834 ui.styled(s, st);
835 }
836 }
837 MdInline::Link { text, url } => {
838 ui.link(text.clone(), url.clone());
839 }
840 MdInline::Image { alt, .. } => {
841 ui.styled(alt.as_str(), code_style);
842 }
843 }
844 }
845 }
846
847 fn split_md_links(text: &str) -> Vec<MdInline> {
849 let chars: Vec<char> = text.chars().collect();
850 let mut items: Vec<MdInline> = Vec::new();
851 let mut current = String::new();
852 let mut i = 0;
853
854 while i < chars.len() {
855 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
857 if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
858 if !current.is_empty() {
859 items.push(MdInline::Text(std::mem::take(&mut current)));
860 }
861 items.push(MdInline::Image { alt });
862 i += 1 + consumed;
863 continue;
864 }
865 }
866 if chars[i] == '[' {
868 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
869 if !current.is_empty() {
870 items.push(MdInline::Text(std::mem::take(&mut current)));
871 }
872 items.push(MdInline::Link {
873 text: link_text,
874 url,
875 });
876 i += consumed;
877 continue;
878 }
879 }
880 current.push(chars[i]);
881 i += 1;
882 }
883 if !current.is_empty() {
884 items.push(MdInline::Text(current));
885 }
886 if items.is_empty() {
887 items.push(MdInline::Text(String::new()));
888 }
889 items
890 }
891
892 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
895 if start >= chars.len() || chars[start] != '[' {
896 return None;
897 }
898 let mut depth = 0i32;
900 let mut bracket_end = None;
901 for (j, &ch) in chars.iter().enumerate().skip(start) {
902 if ch == '[' {
903 depth += 1;
904 } else if ch == ']' {
905 depth -= 1;
906 if depth == 0 {
907 bracket_end = Some(j);
908 break;
909 }
910 }
911 }
912 let bracket_end = bracket_end?;
913 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
915 return None;
916 }
917 let paren_start = bracket_end + 2;
919 let mut paren_end = None;
920 let mut paren_depth = 1i32;
921 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
922 if ch == '(' {
923 paren_depth += 1;
924 } else if ch == ')' {
925 paren_depth -= 1;
926 if paren_depth == 0 {
927 paren_end = Some(j);
928 break;
929 }
930 }
931 }
932 let paren_end = paren_end?;
933 let text: String = chars[start + 1..bracket_end].iter().collect();
934 let url: String = chars[paren_start..paren_end].iter().collect();
935 let consumed = paren_end - start + 1;
936 Some((text, url, consumed))
937 }
938
939 fn md_strip(text: &str) -> String {
944 let mut result = String::with_capacity(text.len());
945 let chars: Vec<char> = text.chars().collect();
946 let mut i = 0;
947 while i < chars.len() {
948 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
950 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
951 result.push_str(&alt);
952 i += 1 + consumed;
953 continue;
954 }
955 }
956 if chars[i] == '[' {
958 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
959 result.push_str(&link_text);
960 i += consumed;
961 continue;
962 }
963 }
964 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
966 let rest: String = chars[i + 2..].iter().collect();
967 if let Some(end) = rest.find("**") {
968 let inner = &rest[..end];
969 result.push_str(inner);
970 i += 2 + inner.chars().count() + 2;
971 continue;
972 }
973 }
974 if chars[i] == '*'
976 && (i + 1 >= chars.len() || chars[i + 1] != '*')
977 && (i == 0 || chars[i - 1] != '*')
978 {
979 let rest: String = chars[i + 1..].iter().collect();
980 if let Some(end) = rest.find('*') {
981 let inner = &rest[..end];
982 result.push_str(inner);
983 i += 1 + inner.chars().count() + 1;
984 continue;
985 }
986 }
987 if chars[i] == '`' {
989 let rest: String = chars[i + 1..].iter().collect();
990 if let Some(end) = rest.find('`') {
991 let inner = &rest[..end];
992 result.push_str(inner);
993 i += 1 + inner.chars().count() + 1;
994 continue;
995 }
996 }
997 result.push(chars[i]);
998 i += 1;
999 }
1000 result
1001 }
1002
1003 pub fn key_seq(&self, seq: &str) -> bool {
1010 if seq.is_empty() {
1011 return false;
1012 }
1013 if (self.rollback.modal_active || self.prev_modal_active)
1014 && self.rollback.overlay_depth == 0
1015 {
1016 return false;
1017 }
1018 let target: Vec<char> = seq.chars().collect();
1019 let mut matched = 0;
1020 for (_, key) in self.available_key_presses() {
1021 if let KeyCode::Char(c) = key.code {
1022 if c == target[matched] {
1023 matched += 1;
1024 if matched == target.len() {
1025 return true;
1026 }
1027 } else {
1028 matched = 0;
1029 if c == target[0] {
1030 matched = 1;
1031 }
1032 }
1033 }
1034 }
1035 false
1036 }
1037}