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(
110 &mut self,
111 rgba: &[u8],
112 pixel_width: u32,
113 pixel_height: u32,
114 cols: u32,
115 rows: u32,
116 ) -> Response {
117 if !self.kitty_graphics_supported() {
118 return self.rgba_halfblock_fallback(
119 rgba,
120 pixel_width,
121 pixel_height,
122 cols,
123 rows,
124 "[kitty unsupported]",
125 );
126 }
127
128 let rgba_data = normalize_rgba(rgba, pixel_width, pixel_height);
129 let content_hash = crate::buffer::hash_rgba(&rgba_data);
130 let rgba_arc = std::sync::Arc::new(rgba_data);
131 let sw = pixel_width;
132 let sh = pixel_height;
133
134 self.container().w(cols).h(rows).draw(move |buf, rect| {
135 if rect.width == 0 || rect.height == 0 {
136 return;
137 }
138 buf.kitty_place(crate::buffer::KittyPlacement {
139 content_hash,
140 rgba: rgba_arc.clone(),
141 src_width: sw,
142 src_height: sh,
143 x: rect.x,
144 y: rect.y,
145 cols: rect.width,
146 rows: rect.height,
147 crop_y: 0,
148 crop_h: 0,
149 });
150 });
151 Response::none()
152 }
153
154 pub fn kitty_image_fit(
166 &mut self,
167 rgba: &[u8],
168 src_width: u32,
169 src_height: u32,
170 cols: u32,
171 ) -> Response {
172 let supported = self.kitty_graphics_supported();
173 #[cfg(feature = "crossterm")]
174 let (cell_w, cell_h) = if supported {
175 crate::terminal::cell_pixel_size()
176 } else {
177 (8u32, 16u32)
178 };
179 #[cfg(not(feature = "crossterm"))]
180 let (cell_w, cell_h) = (8u32, 16u32);
181
182 let rows = image_fit_rows(src_width, src_height, cols, cell_w, cell_h);
183 if !supported {
184 return self.rgba_halfblock_fallback(
185 rgba,
186 src_width,
187 src_height,
188 cols,
189 rows,
190 "[kitty unsupported]",
191 );
192 }
193
194 let rgba_data = normalize_rgba(rgba, src_width, src_height);
195 let content_hash = crate::buffer::hash_rgba(&rgba_data);
196 let rgba_arc = std::sync::Arc::new(rgba_data);
197 let sw = src_width;
198 let sh = src_height;
199
200 self.container().w(cols).h(rows).draw(move |buf, rect| {
201 if rect.width == 0 || rect.height == 0 {
202 return;
203 }
204 buf.kitty_place(crate::buffer::KittyPlacement {
205 content_hash,
206 rgba: rgba_arc.clone(),
207 src_width: sw,
208 src_height: sh,
209 x: rect.x,
210 y: rect.y,
211 cols: rect.width,
212 rows: rect.height,
213 crop_y: 0,
214 crop_h: 0,
215 });
216 });
217 Response::none()
218 }
219
220 #[cfg(feature = "crossterm")]
239 #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
240 pub fn sixel_image(
241 &mut self,
242 rgba: &[u8],
243 pixel_width: u32,
244 pixel_height: u32,
245 cols: u32,
246 rows: u32,
247 ) -> Response {
248 let sixel_supported = self.sixel_supported();
254 if !sixel_supported {
255 self.container().w(cols).h(rows).draw(|buf, rect| {
256 if rect.width == 0 || rect.height == 0 {
257 return;
258 }
259 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
260 });
261 return Response::none();
262 }
263
264 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
265 let content_hash = crate::buffer::hash_rgba(&rgba);
266 let encoded = crate::sixel::encode_sixel(&rgba, pixel_width, pixel_height, 256);
267
268 if encoded.is_empty() {
269 self.container().w(cols).h(rows).draw(|buf, rect| {
270 if rect.width == 0 || rect.height == 0 {
271 return;
272 }
273 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
274 });
275 return Response::none();
276 }
277
278 self.container().w(cols).h(rows).draw(move |buf, rect| {
283 if rect.width == 0 || rect.height == 0 {
284 return;
285 }
286 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
287 buf.sprixel_place(crate::buffer::SprixelPlacement {
288 content_hash,
289 seq: encoded,
290 x: rect.x,
291 y: rect.y,
292 cols: rect.width,
293 rows: rect.height,
294 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
295 });
296 });
297 Response::none()
298 }
299
300 #[cfg(feature = "crossterm")]
323 #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
324 pub fn iterm_image(&mut self, data: &[u8], cols: u32, rows: u32) -> Response {
325 let supported = self.iterm_supported();
329 if !supported {
330 return self.iterm_placeholder(cols, rows);
331 }
332
333 let content_hash = crate::buffer::hash_rgba(data);
334 let encoded = crate::iterm::encode_iterm_osc1337(data, cols, rows, false);
335 if encoded.is_empty() {
336 return self.iterm_placeholder(cols, rows);
337 }
338
339 self.container().w(cols).h(rows).draw(move |buf, rect| {
340 if rect.width == 0 || rect.height == 0 {
341 return;
342 }
343 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
344 buf.sprixel_place(crate::buffer::SprixelPlacement {
345 content_hash,
346 seq: encoded,
347 x: rect.x,
348 y: rect.y,
349 cols: rect.width,
350 rows: rect.height,
351 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
352 });
353 });
354 Response::none()
355 }
356
357 #[cfg(feature = "crossterm")]
376 #[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
377 pub fn iterm_image_fit(&mut self, data: &[u8], cols: u32) -> Response {
378 let supported = self.iterm_supported();
379
380 let (cell_w, cell_h) = if supported {
385 crate::terminal::cell_pixel_size()
386 } else {
387 (8u32, 16u32)
388 };
389 let rows = if cell_h == 0 {
390 cols.max(1)
391 } else {
392 ((cols as f64 * cell_w as f64) / cell_h as f64)
393 .ceil()
394 .max(1.0) as u32
395 };
396
397 if !supported {
398 return self.iterm_placeholder(cols, rows);
399 }
400
401 let content_hash = crate::buffer::hash_rgba(data);
402 let encoded = crate::iterm::encode_iterm_osc1337(data, cols, 0, true);
404 if encoded.is_empty() {
405 return self.iterm_placeholder(cols, rows);
406 }
407
408 self.container().w(cols).h(rows).draw(move |buf, rect| {
409 if rect.width == 0 || rect.height == 0 {
410 return;
411 }
412 let cells = (rect.width as usize).saturating_mul(rect.height as usize);
413 buf.sprixel_place(crate::buffer::SprixelPlacement {
414 content_hash,
415 seq: encoded,
416 x: rect.x,
417 y: rect.y,
418 cols: rect.width,
419 rows: rect.height,
420 cells: vec![crate::buffer::SprixelCell::Opaque; cells],
421 });
422 });
423 Response::none()
424 }
425
426 #[cfg(feature = "crossterm")]
427 fn kitty_graphics_supported(&self) -> bool {
428 if !self.is_real_terminal {
429 return false;
430 }
431 if terminal_force_graphics("SLT_FORCE_KITTY") {
432 return true;
433 }
434 if terminal_graphics_blocked_by_multiplexer() {
435 return false;
436 }
437 self.capabilities.kitty_graphics || terminal_supports_kitty()
438 }
439
440 #[cfg(not(feature = "crossterm"))]
441 fn kitty_graphics_supported(&self) -> bool {
442 false
443 }
444
445 #[cfg(feature = "crossterm")]
446 fn sixel_supported(&self) -> bool {
447 if !self.is_real_terminal {
448 return false;
449 }
450 if terminal_force_graphics("SLT_FORCE_SIXEL") {
451 return true;
452 }
453 if terminal_graphics_blocked_by_multiplexer() {
454 return false;
455 }
456 self.capabilities.sixel || terminal_supports_sixel()
457 }
458
459 #[cfg(feature = "crossterm")]
460 fn iterm_supported(&self) -> bool {
461 if !self.is_real_terminal {
462 return false;
463 }
464 if terminal_force_graphics("SLT_FORCE_ITERM") {
465 return true;
466 }
467 if terminal_graphics_blocked_by_multiplexer() {
468 return false;
469 }
470 self.capabilities.iterm2 || terminal_supports_iterm()
471 }
472
473 fn rgba_halfblock_fallback(
474 &mut self,
475 rgba: &[u8],
476 pixel_width: u32,
477 pixel_height: u32,
478 cols: u32,
479 rows: u32,
480 placeholder: &'static str,
481 ) -> Response {
482 let rgba_data = normalize_rgba(rgba, pixel_width, pixel_height);
483 if rgba_data.is_empty() {
484 self.container().w(cols).h(rows).draw(move |buf, rect| {
485 if rect.width == 0 || rect.height == 0 {
486 return;
487 }
488 buf.set_string(rect.x, rect.y, placeholder, Style::new());
489 });
490 return Response::none();
491 }
492
493 self.container().w(cols).h(rows).draw(move |buf, rect| {
494 if rect.width == 0 || rect.height == 0 {
495 return;
496 }
497
498 let dst_pixel_height = rect.height.saturating_mul(2).max(1);
499 for row in 0..rect.height {
500 for col in 0..rect.width {
501 let upper = sample_rgba_color(
502 &rgba_data,
503 pixel_width,
504 pixel_height,
505 col,
506 row * 2,
507 rect.width,
508 dst_pixel_height,
509 );
510 let lower = sample_rgba_color(
511 &rgba_data,
512 pixel_width,
513 pixel_height,
514 col,
515 row.saturating_mul(2).saturating_add(1),
516 rect.width,
517 dst_pixel_height,
518 );
519 draw_halfblock_cell(buf, rect.x + col, rect.y + row, upper, lower);
520 }
521 }
522 });
523 Response::none()
524 }
525
526 #[cfg(feature = "crossterm")]
529 fn iterm_placeholder(&mut self, cols: u32, rows: u32) -> Response {
530 self.container().w(cols).h(rows).draw(|buf, rect| {
531 if rect.width == 0 || rect.height == 0 {
532 return;
533 }
534 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
535 });
536 Response::none()
537 }
538
539 #[cfg(not(feature = "crossterm"))]
541 pub fn iterm_image(&mut self, _data: &[u8], cols: u32, rows: u32) -> Response {
542 self.container().w(cols).h(rows).draw(|buf, rect| {
543 if rect.width == 0 || rect.height == 0 {
544 return;
545 }
546 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
547 });
548 Response::none()
549 }
550
551 #[cfg(not(feature = "crossterm"))]
553 pub fn iterm_image_fit(&mut self, _data: &[u8], cols: u32) -> Response {
554 let rows = (cols / 2).max(1);
557 self.container().w(cols).h(rows).draw(|buf, rect| {
558 if rect.width == 0 || rect.height == 0 {
559 return;
560 }
561 buf.set_string(rect.x, rect.y, "[iterm2 unsupported]", Style::new());
562 });
563 Response::none()
564 }
565
566 #[cfg(not(feature = "crossterm"))]
568 pub fn sixel_image(
569 &mut self,
570 _rgba: &[u8],
571 _pixel_width: u32,
572 _pixel_height: u32,
573 cols: u32,
574 rows: u32,
575 ) -> Response {
576 self.container().w(cols).h(rows).draw(|buf, rect| {
577 if rect.width == 0 || rect.height == 0 {
578 return;
579 }
580 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
581 });
582 Response::none()
583 }
584
585 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
601 if state.streaming {
602 state.cursor_tick = state.cursor_tick.wrapping_add(1);
603 state.cursor_visible = (state.cursor_tick / 8).is_multiple_of(2);
604 }
605
606 if state.content.is_empty() && state.streaming {
607 let cursor = if state.cursor_visible { "▌" } else { " " };
608 let primary = self.theme.primary;
609 self.text(cursor).fg(primary);
610 return Response::none();
611 }
612
613 if !state.content.is_empty() {
614 self.text(&state.content).wrap();
615 if state.streaming && state.cursor_visible {
616 let primary = self.theme.primary;
617 self.styled("▌", Style::new().fg(primary));
618 }
619 }
620
621 Response::none()
622 }
623
624 pub fn streaming_markdown(
642 &mut self,
643 state: &mut crate::widgets::StreamingMarkdownState,
644 ) -> Response {
645 if state.streaming {
646 state.cursor_tick = state.cursor_tick.wrapping_add(1);
647 state.cursor_visible = (state.cursor_tick / 8).is_multiple_of(2);
648 }
649
650 if state.content.is_empty() && state.streaming {
651 let cursor = if state.cursor_visible { "▌" } else { " " };
652 let primary = self.theme.primary;
653 self.text(cursor).fg(primary);
654 return Response::none();
655 }
656
657 let show_cursor = state.streaming && state.cursor_visible;
658 let trailing_newline = state.content.ends_with('\n');
659 let lines: Vec<&str> = state.content.lines().collect();
660 let last_line_index = lines.len().saturating_sub(1);
661
662 self.commands
663 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
664 direction: Direction::Column,
665 gap: 0,
666 align: Align::Start,
667 align_self: None,
668 justify: Justify::Start,
669 border: None,
670 border_sides: BorderSides::all(),
671 border_style: Style::new().fg(self.theme.border),
672 bg_color: None,
673 padding: Padding::default(),
674 margin: Margin::default(),
675 constraints: Constraints::default(),
676 title: None,
677 grow: 0,
678 group_name: None,
679 })));
680 self.skip_interaction_slot();
681
682 let text_style = Style::new().fg(self.theme.text);
683 let bold_style = Style::new().fg(self.theme.text).bold();
684 let code_style = Style::new().fg(self.theme.accent);
685 let border_style = Style::new().fg(self.theme.border).dim();
686
687 let mut in_code_block = false;
688 let mut code_block_lang = String::new();
689
690 for (idx, line) in lines.iter().enumerate() {
691 let line = *line;
692 let trimmed = line.trim();
693 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
694 let cursor = if append_cursor { "▌" } else { "" };
695
696 if in_code_block {
697 if trimmed.starts_with("```") {
698 in_code_block = false;
699 code_block_lang.clear();
700 let mut line = String::from(" └────");
701 line.push_str(cursor);
702 self.styled(line, border_style);
703 } else {
704 self.line(|ui| {
705 ui.text(" ");
706 render_highlighted_line(ui, line);
707 if !cursor.is_empty() {
708 ui.styled(cursor, Style::new().fg(ui.theme.primary));
709 }
710 });
711 }
712 continue;
713 }
714
715 if trimmed.is_empty() {
716 if append_cursor {
717 self.styled("▌", Style::new().fg(self.theme.primary));
718 } else {
719 self.text(" ");
720 }
721 continue;
722 }
723
724 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
725 let mut line = "─".repeat(40);
726 line.push_str(cursor);
727 self.styled(line, border_style);
728 continue;
729 }
730
731 if let Some(heading) = trimmed.strip_prefix("### ") {
732 let mut line = String::with_capacity(heading.len() + cursor.len());
733 line.push_str(heading);
734 line.push_str(cursor);
735 self.styled(line, Style::new().bold().fg(self.theme.accent));
736 continue;
737 }
738
739 if let Some(heading) = trimmed.strip_prefix("## ") {
740 let mut line = String::with_capacity(heading.len() + cursor.len());
741 line.push_str(heading);
742 line.push_str(cursor);
743 self.styled(line, Style::new().bold().fg(self.theme.secondary));
744 continue;
745 }
746
747 if let Some(heading) = trimmed.strip_prefix("# ") {
748 let mut line = String::with_capacity(heading.len() + cursor.len());
749 line.push_str(heading);
750 line.push_str(cursor);
751 self.styled(line, Style::new().bold().fg(self.theme.primary));
752 continue;
753 }
754
755 if let Some(code) = trimmed.strip_prefix("```") {
756 in_code_block = true;
757 code_block_lang = code.trim().to_string();
758 let label = if code_block_lang.is_empty() {
759 "code".to_string()
760 } else {
761 let mut label = String::from("code:");
762 label.push_str(&code_block_lang);
763 label
764 };
765 let mut line = String::with_capacity(5 + label.len() + cursor.len());
766 line.push_str(" ┌─");
767 line.push_str(&label);
768 line.push('─');
769 line.push_str(cursor);
770 self.styled(line, border_style);
771 continue;
772 }
773
774 if let Some(item) = trimmed
775 .strip_prefix("- ")
776 .or_else(|| trimmed.strip_prefix("* "))
777 {
778 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
779 if segs.len() <= 1 {
780 let mut line = String::with_capacity(4 + item.len() + cursor.len());
781 line.push_str(" • ");
782 line.push_str(item);
783 line.push_str(cursor);
784 self.styled(line, text_style);
785 } else {
786 self.line(|ui| {
787 ui.styled(" • ", text_style);
788 for (s, st) in segs {
789 ui.styled(s, st);
790 }
791 if append_cursor {
792 ui.styled("▌", Style::new().fg(ui.theme.primary));
793 }
794 });
795 }
796 continue;
797 }
798
799 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
800 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
801 if parts.len() == 2 {
802 let segs =
803 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
804 if segs.len() <= 1 {
805 let mut line = String::with_capacity(
806 4 + parts[0].len() + parts[1].len() + cursor.len(),
807 );
808 line.push_str(" ");
809 line.push_str(parts[0]);
810 line.push_str(". ");
811 line.push_str(parts[1]);
812 line.push_str(cursor);
813 self.styled(line, text_style);
814 } else {
815 self.line(|ui| {
816 let mut prefix = String::with_capacity(4 + parts[0].len());
817 prefix.push_str(" ");
818 prefix.push_str(parts[0]);
819 prefix.push_str(". ");
820 ui.styled(prefix, text_style);
821 for (s, st) in segs {
822 ui.styled(s, st);
823 }
824 if append_cursor {
825 ui.styled("▌", Style::new().fg(ui.theme.primary));
826 }
827 });
828 }
829 } else {
830 let mut line = String::with_capacity(trimmed.len() + cursor.len());
831 line.push_str(trimmed);
832 line.push_str(cursor);
833 self.styled(line, text_style);
834 }
835 continue;
836 }
837
838 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
839 if segs.len() <= 1 {
840 let mut line = String::with_capacity(trimmed.len() + cursor.len());
841 line.push_str(trimmed);
842 line.push_str(cursor);
843 self.styled(line, text_style);
844 } else {
845 self.line(|ui| {
846 for (s, st) in segs {
847 ui.styled(s, st);
848 }
849 if append_cursor {
850 ui.styled("▌", Style::new().fg(ui.theme.primary));
851 }
852 });
853 }
854 }
855
856 if show_cursor && trailing_newline {
857 if in_code_block {
858 self.styled(" ▌", code_style);
859 } else {
860 self.styled("▌", Style::new().fg(self.theme.primary));
861 }
862 }
863
864 if state.in_code_block != in_code_block {
865 state.in_code_block = in_code_block;
866 }
867 if state.code_block_lang != code_block_lang {
868 state.code_block_lang = code_block_lang;
869 }
870
871 self.commands.push(Command::EndContainer);
872 self.rollback.last_text_idx = None;
873 Response::none()
874 }
875
876 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
891 let old_action = state.action;
892 let theme = self.theme;
893 let _ = self.bordered(Border::Rounded).col(|ui| {
894 let _ = ui.row(|ui| {
895 ui.text("⚡").fg(theme.warning);
896 ui.text(&state.tool_name).bold().fg(theme.primary);
897 });
898 ui.text(&state.description).dim();
899
900 if state.action == ApprovalAction::Pending {
901 let _ = ui.row(|ui| {
902 if ui.button("✓ Approve").clicked {
903 state.action = ApprovalAction::Approved;
904 }
905 if ui.button("✗ Reject").clicked {
906 state.action = ApprovalAction::Rejected;
907 }
908 });
909 } else {
910 let (label, color) = match state.action {
911 ApprovalAction::Approved => ("✓ Approved", theme.success),
912 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
913 ApprovalAction::Pending => unreachable!(),
914 };
915 ui.text(label).fg(color).bold();
916 }
917 });
918
919 Response {
920 changed: state.action != old_action,
921 ..Response::none()
922 }
923 }
924
925 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
938 if items.is_empty() {
939 return Response::none();
940 }
941
942 let theme = self.theme;
943 let total: usize = items.iter().map(|item| item.tokens).sum();
944
945 let _ = self.container().row(|ui| {
946 ui.text("📎").dim();
947 for item in items {
948 let token_count = format_token_count(item.tokens);
949 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
950 line.push_str(&item.label);
951 line.push_str(" (");
952 line.push_str(&token_count);
953 line.push(')');
954 ui.text(line).fg(theme.secondary);
955 }
956 ui.spacer();
957 let total_text = format_token_count(total);
958 let mut line = String::with_capacity(2 + total_text.len());
959 line.push_str("Σ ");
960 line.push_str(&total_text);
961 ui.text(line).dim();
962 });
963
964 Response::none()
965 }
966}