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