1use super::*;
2
3impl Context {
4 pub fn big_text(&mut self, s: impl Into<String>) -> Response {
6 let text = s.into();
7 let glyphs: Vec<[u8; 8]> = text.chars().map(glyph_8x8).collect();
8 let total_width = (glyphs.len() as u32).saturating_mul(8);
9 let on_color = self.theme.primary;
10
11 self.container().w(total_width).h(4).draw(move |buf, rect| {
12 if rect.width == 0 || rect.height == 0 {
13 return;
14 }
15
16 for (glyph_idx, glyph) in glyphs.iter().enumerate() {
17 let base_x = rect.x + (glyph_idx as u32) * 8;
18 if base_x >= rect.right() {
19 break;
20 }
21
22 for pair in 0..4usize {
23 let y = rect.y + pair as u32;
24 if y >= rect.bottom() {
25 continue;
26 }
27
28 let upper = glyph[pair * 2];
29 let lower = glyph[pair * 2 + 1];
30
31 for bit in 0..8u32 {
32 let x = base_x + bit;
33 if x >= rect.right() {
34 break;
35 }
36
37 let mask = 1u8 << (bit as u8);
38 let upper_on = (upper & mask) != 0;
39 let lower_on = (lower & mask) != 0;
40 let (ch, fg, bg) = match (upper_on, lower_on) {
41 (true, true) => ('█', on_color, on_color),
42 (true, false) => ('▀', on_color, Color::Reset),
43 (false, true) => ('▄', on_color, Color::Reset),
44 (false, false) => (' ', Color::Reset, Color::Reset),
45 };
46 buf.set_char(x, y, ch, Style::new().fg(fg).bg(bg));
47 }
48 }
49 }
50 });
51
52 Response::none()
53 }
54
55 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
77 let pixels: Vec<(Color, Color)> = img.pixels.clone();
78 let (w, h) = (img.width, img.height);
79 self.container().w(w).h(h).draw(move |buf, rect| {
80 for row in 0..h {
81 for col in 0..w {
82 if let Some(&(fg, bg)) = pixels.get((row * w + col) as usize) {
83 buf.set_char(rect.x + col, rect.y + row, '▀', Style::new().fg(fg).bg(bg));
84 }
85 }
86 }
87 });
88
89 Response::none()
90 }
91
92 pub fn kitty_image(
108 &mut self,
109 rgba: &[u8],
110 pixel_width: u32,
111 pixel_height: u32,
112 cols: u32,
113 rows: u32,
114 ) -> Response {
115 let rgba_data = normalize_rgba(rgba, pixel_width, pixel_height);
116 let content_hash = crate::buffer::hash_rgba(&rgba_data);
117 let rgba_arc = std::sync::Arc::new(rgba_data);
118 let sw = pixel_width;
119 let sh = pixel_height;
120
121 self.container().w(cols).h(rows).draw(move |buf, rect| {
122 if rect.width == 0 || rect.height == 0 {
123 return;
124 }
125 buf.kitty_place(crate::buffer::KittyPlacement {
126 content_hash,
127 rgba: rgba_arc.clone(),
128 src_width: sw,
129 src_height: sh,
130 x: rect.x,
131 y: rect.y,
132 cols: rect.width,
133 rows: rect.height,
134 crop_y: 0,
135 crop_h: 0,
136 });
137 });
138 Response::none()
139 }
140
141 pub fn kitty_image_fit(
151 &mut self,
152 rgba: &[u8],
153 src_width: u32,
154 src_height: u32,
155 cols: u32,
156 ) -> Response {
157 #[cfg(feature = "crossterm")]
158 let (cell_w, cell_h) = crate::terminal::cell_pixel_size();
159 #[cfg(not(feature = "crossterm"))]
160 let (cell_w, cell_h) = (8u32, 16u32);
161
162 let rows = if src_width == 0 {
163 1
164 } else {
165 ((cols as f64 * src_height as f64 * cell_w as f64) / (src_width as f64 * cell_h as f64))
166 .ceil()
167 .max(1.0) as u32
168 };
169 let rgba_data = normalize_rgba(rgba, src_width, src_height);
170 let content_hash = crate::buffer::hash_rgba(&rgba_data);
171 let rgba_arc = std::sync::Arc::new(rgba_data);
172 let sw = src_width;
173 let sh = src_height;
174
175 self.container().w(cols).h(rows).draw(move |buf, rect| {
176 if rect.width == 0 || rect.height == 0 {
177 return;
178 }
179 buf.kitty_place(crate::buffer::KittyPlacement {
180 content_hash,
181 rgba: rgba_arc.clone(),
182 src_width: sw,
183 src_height: sh,
184 x: rect.x,
185 y: rect.y,
186 cols: rect.width,
187 rows: rect.height,
188 crop_y: 0,
189 crop_h: 0,
190 });
191 });
192 Response::none()
193 }
194
195 #[cfg(feature = "crossterm")]
214 pub fn sixel_image(
215 &mut self,
216 rgba: &[u8],
217 pixel_width: u32,
218 pixel_height: u32,
219 cols: u32,
220 rows: u32,
221 ) -> Response {
222 let sixel_supported =
228 self.is_real_terminal && (self.capabilities.sixel || terminal_supports_sixel());
229 if !sixel_supported {
230 self.container().w(cols).h(rows).draw(|buf, rect| {
231 if rect.width == 0 || rect.height == 0 {
232 return;
233 }
234 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
235 });
236 return Response::none();
237 }
238
239 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
240 let content_hash = crate::buffer::hash_rgba(&rgba);
241 let encoded = crate::sixel::encode_sixel(&rgba, pixel_width, pixel_height, 256);
242
243 if encoded.is_empty() {
244 self.container().w(cols).h(rows).draw(|buf, rect| {
245 if rect.width == 0 || rect.height == 0 {
246 return;
247 }
248 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
249 });
250 return Response::none();
251 }
252
253 self.container().w(cols).h(rows).draw(move |buf, rect| {
258 if rect.width == 0 || rect.height == 0 {
259 return;
260 }
261 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
262 buf.sprixel_place(crate::buffer::SprixelPlacement {
263 content_hash,
264 seq: encoded,
265 x: rect.x,
266 y: rect.y,
267 cols: rect.width,
268 rows: rect.height,
269 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
270 });
271 });
272 Response::none()
273 }
274
275 #[cfg(feature = "crossterm")]
298 pub fn iterm_image(&mut self, data: &[u8], cols: u32, rows: u32) -> Response {
299 let supported =
303 self.is_real_terminal && (self.capabilities.iterm2 || terminal_supports_iterm());
304 if !supported {
305 return self.iterm_placeholder(cols, rows);
306 }
307
308 let content_hash = crate::buffer::hash_rgba(data);
309 let encoded = crate::iterm::encode_iterm_osc1337(data, cols, rows, false);
310 if encoded.is_empty() {
311 return self.iterm_placeholder(cols, rows);
312 }
313
314 self.container().w(cols).h(rows).draw(move |buf, rect| {
315 if rect.width == 0 || rect.height == 0 {
316 return;
317 }
318 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
319 buf.sprixel_place(crate::buffer::SprixelPlacement {
320 content_hash,
321 seq: encoded,
322 x: rect.x,
323 y: rect.y,
324 cols: rect.width,
325 rows: rect.height,
326 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
327 });
328 });
329 Response::none()
330 }
331
332 #[cfg(feature = "crossterm")]
351 pub fn iterm_image_fit(&mut self, data: &[u8], cols: u32) -> Response {
352 let supported =
353 self.is_real_terminal && (self.capabilities.iterm2 || terminal_supports_iterm());
354
355 let (cell_w, cell_h) = crate::terminal::cell_pixel_size();
360 let rows = if cell_h == 0 {
361 cols.max(1)
362 } else {
363 ((cols as f64 * cell_w as f64) / cell_h as f64)
364 .ceil()
365 .max(1.0) as u32
366 };
367
368 if !supported {
369 return self.iterm_placeholder(cols, rows);
370 }
371
372 let content_hash = crate::buffer::hash_rgba(data);
373 let encoded = crate::iterm::encode_iterm_osc1337(data, cols, 0, true);
375 if encoded.is_empty() {
376 return self.iterm_placeholder(cols, rows);
377 }
378
379 self.container().w(cols).h(rows).draw(move |buf, rect| {
380 if rect.width == 0 || rect.height == 0 {
381 return;
382 }
383 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
384 buf.sprixel_place(crate::buffer::SprixelPlacement {
385 content_hash,
386 seq: encoded,
387 x: rect.x,
388 y: rect.y,
389 cols: rect.width,
390 rows: rect.height,
391 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
392 });
393 });
394 Response::none()
395 }
396
397 #[cfg(feature = "crossterm")]
400 fn iterm_placeholder(&mut self, cols: u32, rows: u32) -> Response {
401 self.container().w(cols).h(rows).draw(|buf, rect| {
402 if rect.width == 0 || rect.height == 0 {
403 return;
404 }
405 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
406 });
407 Response::none()
408 }
409
410 #[cfg(not(feature = "crossterm"))]
412 pub fn iterm_image(&mut self, _data: &[u8], cols: u32, rows: u32) -> Response {
413 self.container().w(cols).h(rows).draw(|buf, rect| {
414 if rect.width == 0 || rect.height == 0 {
415 return;
416 }
417 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
418 });
419 Response::none()
420 }
421
422 #[cfg(not(feature = "crossterm"))]
424 pub fn iterm_image_fit(&mut self, _data: &[u8], cols: u32) -> Response {
425 let rows = (cols / 2).max(1);
428 self.container().w(cols).h(rows).draw(|buf, rect| {
429 if rect.width == 0 || rect.height == 0 {
430 return;
431 }
432 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
433 });
434 Response::none()
435 }
436
437 #[cfg(not(feature = "crossterm"))]
439 pub fn sixel_image(
440 &mut self,
441 _rgba: &[u8],
442 _pixel_width: u32,
443 _pixel_height: u32,
444 cols: u32,
445 rows: u32,
446 ) -> Response {
447 self.container().w(cols).h(rows).draw(|buf, rect| {
448 if rect.width == 0 || rect.height == 0 {
449 return;
450 }
451 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
452 });
453 Response::none()
454 }
455
456 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
472 if state.streaming {
473 state.cursor_tick = state.cursor_tick.wrapping_add(1);
474 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
475 }
476
477 if state.content.is_empty() && state.streaming {
478 let cursor = if state.cursor_visible { "▌" } else { " " };
479 let primary = self.theme.primary;
480 self.text(cursor).fg(primary);
481 return Response::none();
482 }
483
484 if !state.content.is_empty() {
485 self.text(&state.content).wrap();
486 if state.streaming && state.cursor_visible {
487 let primary = self.theme.primary;
488 self.styled("▌", Style::new().fg(primary));
489 }
490 }
491
492 Response::none()
493 }
494
495 pub fn streaming_markdown(
513 &mut self,
514 state: &mut crate::widgets::StreamingMarkdownState,
515 ) -> Response {
516 if state.streaming {
517 state.cursor_tick = state.cursor_tick.wrapping_add(1);
518 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
519 }
520
521 if state.content.is_empty() && state.streaming {
522 let cursor = if state.cursor_visible { "▌" } else { " " };
523 let primary = self.theme.primary;
524 self.text(cursor).fg(primary);
525 return Response::none();
526 }
527
528 let show_cursor = state.streaming && state.cursor_visible;
529 let trailing_newline = state.content.ends_with('\n');
530 let lines: Vec<&str> = state.content.lines().collect();
531 let last_line_index = lines.len().saturating_sub(1);
532
533 self.commands
534 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
535 direction: Direction::Column,
536 gap: 0,
537 align: Align::Start,
538 align_self: None,
539 justify: Justify::Start,
540 border: None,
541 border_sides: BorderSides::all(),
542 border_style: Style::new().fg(self.theme.border),
543 bg_color: None,
544 padding: Padding::default(),
545 margin: Margin::default(),
546 constraints: Constraints::default(),
547 title: None,
548 grow: 0,
549 group_name: None,
550 })));
551 self.skip_interaction_slot();
552
553 let text_style = Style::new().fg(self.theme.text);
554 let bold_style = Style::new().fg(self.theme.text).bold();
555 let code_style = Style::new().fg(self.theme.accent);
556 let border_style = Style::new().fg(self.theme.border).dim();
557
558 let mut in_code_block = false;
559 let mut code_block_lang = String::new();
560
561 for (idx, line) in lines.iter().enumerate() {
562 let line = *line;
563 let trimmed = line.trim();
564 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
565 let cursor = if append_cursor { "▌" } else { "" };
566
567 if in_code_block {
568 if trimmed.starts_with("```") {
569 in_code_block = false;
570 code_block_lang.clear();
571 let mut line = String::from(" └────");
572 line.push_str(cursor);
573 self.styled(line, border_style);
574 } else {
575 self.line(|ui| {
576 ui.text(" ");
577 render_highlighted_line(ui, line);
578 if !cursor.is_empty() {
579 ui.styled(cursor, Style::new().fg(ui.theme.primary));
580 }
581 });
582 }
583 continue;
584 }
585
586 if trimmed.is_empty() {
587 if append_cursor {
588 self.styled("▌", Style::new().fg(self.theme.primary));
589 } else {
590 self.text(" ");
591 }
592 continue;
593 }
594
595 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
596 let mut line = "─".repeat(40);
597 line.push_str(cursor);
598 self.styled(line, border_style);
599 continue;
600 }
601
602 if let Some(heading) = trimmed.strip_prefix("### ") {
603 let mut line = String::with_capacity(heading.len() + cursor.len());
604 line.push_str(heading);
605 line.push_str(cursor);
606 self.styled(line, Style::new().bold().fg(self.theme.accent));
607 continue;
608 }
609
610 if let Some(heading) = trimmed.strip_prefix("## ") {
611 let mut line = String::with_capacity(heading.len() + cursor.len());
612 line.push_str(heading);
613 line.push_str(cursor);
614 self.styled(line, Style::new().bold().fg(self.theme.secondary));
615 continue;
616 }
617
618 if let Some(heading) = trimmed.strip_prefix("# ") {
619 let mut line = String::with_capacity(heading.len() + cursor.len());
620 line.push_str(heading);
621 line.push_str(cursor);
622 self.styled(line, Style::new().bold().fg(self.theme.primary));
623 continue;
624 }
625
626 if let Some(code) = trimmed.strip_prefix("```") {
627 in_code_block = true;
628 code_block_lang = code.trim().to_string();
629 let label = if code_block_lang.is_empty() {
630 "code".to_string()
631 } else {
632 let mut label = String::from("code:");
633 label.push_str(&code_block_lang);
634 label
635 };
636 let mut line = String::with_capacity(5 + label.len() + cursor.len());
637 line.push_str(" ┌─");
638 line.push_str(&label);
639 line.push('─');
640 line.push_str(cursor);
641 self.styled(line, border_style);
642 continue;
643 }
644
645 if let Some(item) = trimmed
646 .strip_prefix("- ")
647 .or_else(|| trimmed.strip_prefix("* "))
648 {
649 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
650 if segs.len() <= 1 {
651 let mut line = String::with_capacity(4 + item.len() + cursor.len());
652 line.push_str(" • ");
653 line.push_str(item);
654 line.push_str(cursor);
655 self.styled(line, text_style);
656 } else {
657 self.line(|ui| {
658 ui.styled(" • ", text_style);
659 for (s, st) in segs {
660 ui.styled(s, st);
661 }
662 if append_cursor {
663 ui.styled("▌", Style::new().fg(ui.theme.primary));
664 }
665 });
666 }
667 continue;
668 }
669
670 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
671 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
672 if parts.len() == 2 {
673 let segs =
674 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
675 if segs.len() <= 1 {
676 let mut line = String::with_capacity(
677 4 + parts[0].len() + parts[1].len() + cursor.len(),
678 );
679 line.push_str(" ");
680 line.push_str(parts[0]);
681 line.push_str(". ");
682 line.push_str(parts[1]);
683 line.push_str(cursor);
684 self.styled(line, text_style);
685 } else {
686 self.line(|ui| {
687 let mut prefix = String::with_capacity(4 + parts[0].len());
688 prefix.push_str(" ");
689 prefix.push_str(parts[0]);
690 prefix.push_str(". ");
691 ui.styled(prefix, text_style);
692 for (s, st) in segs {
693 ui.styled(s, st);
694 }
695 if append_cursor {
696 ui.styled("▌", Style::new().fg(ui.theme.primary));
697 }
698 });
699 }
700 } else {
701 let mut line = String::with_capacity(trimmed.len() + cursor.len());
702 line.push_str(trimmed);
703 line.push_str(cursor);
704 self.styled(line, text_style);
705 }
706 continue;
707 }
708
709 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
710 if segs.len() <= 1 {
711 let mut line = String::with_capacity(trimmed.len() + cursor.len());
712 line.push_str(trimmed);
713 line.push_str(cursor);
714 self.styled(line, text_style);
715 } else {
716 self.line(|ui| {
717 for (s, st) in segs {
718 ui.styled(s, st);
719 }
720 if append_cursor {
721 ui.styled("▌", Style::new().fg(ui.theme.primary));
722 }
723 });
724 }
725 }
726
727 if show_cursor && trailing_newline {
728 if in_code_block {
729 self.styled(" ▌", code_style);
730 } else {
731 self.styled("▌", Style::new().fg(self.theme.primary));
732 }
733 }
734
735 if state.in_code_block != in_code_block {
736 state.in_code_block = in_code_block;
737 }
738 if state.code_block_lang != code_block_lang {
739 state.code_block_lang = code_block_lang;
740 }
741
742 self.commands.push(Command::EndContainer);
743 self.rollback.last_text_idx = None;
744 Response::none()
745 }
746
747 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
762 let old_action = state.action;
763 let theme = self.theme;
764 let _ = self.bordered(Border::Rounded).col(|ui| {
765 let _ = ui.row(|ui| {
766 ui.text("⚡").fg(theme.warning);
767 ui.text(&state.tool_name).bold().fg(theme.primary);
768 });
769 ui.text(&state.description).dim();
770
771 if state.action == ApprovalAction::Pending {
772 let _ = ui.row(|ui| {
773 if ui.button("✓ Approve").clicked {
774 state.action = ApprovalAction::Approved;
775 }
776 if ui.button("✗ Reject").clicked {
777 state.action = ApprovalAction::Rejected;
778 }
779 });
780 } else {
781 let (label, color) = match state.action {
782 ApprovalAction::Approved => ("✓ Approved", theme.success),
783 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
784 ApprovalAction::Pending => unreachable!(),
785 };
786 ui.text(label).fg(color).bold();
787 }
788 });
789
790 Response {
791 changed: state.action != old_action,
792 ..Response::none()
793 }
794 }
795
796 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
809 if items.is_empty() {
810 return Response::none();
811 }
812
813 let theme = self.theme;
814 let total: usize = items.iter().map(|item| item.tokens).sum();
815
816 let _ = self.container().row(|ui| {
817 ui.text("📎").dim();
818 for item in items {
819 let token_count = format_token_count(item.tokens);
820 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
821 line.push_str(&item.label);
822 line.push_str(" (");
823 line.push_str(&token_count);
824 line.push(')');
825 ui.text(line).fg(theme.secondary);
826 }
827 ui.spacer();
828 let total_text = format_token_count(total);
829 let mut line = String::with_capacity(2 + total_text.len());
830 line.push_str("Σ ");
831 line.push_str(&total_text);
832 ui.text(line).dim();
833 });
834
835 Response::none()
836 }
837}