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 = self.is_real_terminal && terminal_supports_sixel();
223 if !sixel_supported {
224 self.container().w(cols).h(rows).draw(|buf, rect| {
225 if rect.width == 0 || rect.height == 0 {
226 return;
227 }
228 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
229 });
230 return Response::none();
231 }
232
233 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
234 let encoded = crate::sixel::encode_sixel(&rgba, pixel_width, pixel_height, 256);
235
236 if encoded.is_empty() {
237 self.container().w(cols).h(rows).draw(|buf, rect| {
238 if rect.width == 0 || rect.height == 0 {
239 return;
240 }
241 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
242 });
243 return Response::none();
244 }
245
246 self.container().w(cols).h(rows).draw(move |buf, rect| {
247 if rect.width == 0 || rect.height == 0 {
248 return;
249 }
250 buf.raw_sequence(rect.x, rect.y, encoded);
251 });
252 Response::none()
253 }
254
255 #[cfg(not(feature = "crossterm"))]
257 pub fn sixel_image(
258 &mut self,
259 _rgba: &[u8],
260 _pixel_width: u32,
261 _pixel_height: u32,
262 cols: u32,
263 rows: u32,
264 ) -> Response {
265 self.container().w(cols).h(rows).draw(|buf, rect| {
266 if rect.width == 0 || rect.height == 0 {
267 return;
268 }
269 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
270 });
271 Response::none()
272 }
273
274 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
290 if state.streaming {
291 state.cursor_tick = state.cursor_tick.wrapping_add(1);
292 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
293 }
294
295 if state.content.is_empty() && state.streaming {
296 let cursor = if state.cursor_visible { "▌" } else { " " };
297 let primary = self.theme.primary;
298 self.text(cursor).fg(primary);
299 return Response::none();
300 }
301
302 if !state.content.is_empty() {
303 self.text(&state.content).wrap();
304 if state.streaming && state.cursor_visible {
305 let primary = self.theme.primary;
306 self.styled("▌", Style::new().fg(primary));
307 }
308 }
309
310 Response::none()
311 }
312
313 pub fn streaming_markdown(
331 &mut self,
332 state: &mut crate::widgets::StreamingMarkdownState,
333 ) -> Response {
334 if state.streaming {
335 state.cursor_tick = state.cursor_tick.wrapping_add(1);
336 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
337 }
338
339 if state.content.is_empty() && state.streaming {
340 let cursor = if state.cursor_visible { "▌" } else { " " };
341 let primary = self.theme.primary;
342 self.text(cursor).fg(primary);
343 return Response::none();
344 }
345
346 let show_cursor = state.streaming && state.cursor_visible;
347 let trailing_newline = state.content.ends_with('\n');
348 let lines: Vec<&str> = state.content.lines().collect();
349 let last_line_index = lines.len().saturating_sub(1);
350
351 self.commands
352 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
353 direction: Direction::Column,
354 gap: 0,
355 align: Align::Start,
356 align_self: None,
357 justify: Justify::Start,
358 border: None,
359 border_sides: BorderSides::all(),
360 border_style: Style::new().fg(self.theme.border),
361 bg_color: None,
362 padding: Padding::default(),
363 margin: Margin::default(),
364 constraints: Constraints::default(),
365 title: None,
366 grow: 0,
367 group_name: None,
368 })));
369 self.skip_interaction_slot();
370
371 let text_style = Style::new().fg(self.theme.text);
372 let bold_style = Style::new().fg(self.theme.text).bold();
373 let code_style = Style::new().fg(self.theme.accent);
374 let border_style = Style::new().fg(self.theme.border).dim();
375
376 let mut in_code_block = false;
377 let mut code_block_lang = String::new();
378
379 for (idx, line) in lines.iter().enumerate() {
380 let line = *line;
381 let trimmed = line.trim();
382 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
383 let cursor = if append_cursor { "▌" } else { "" };
384
385 if in_code_block {
386 if trimmed.starts_with("```") {
387 in_code_block = false;
388 code_block_lang.clear();
389 let mut line = String::from(" └────");
390 line.push_str(cursor);
391 self.styled(line, border_style);
392 } else {
393 self.line(|ui| {
394 ui.text(" ");
395 render_highlighted_line(ui, line);
396 if !cursor.is_empty() {
397 ui.styled(cursor, Style::new().fg(ui.theme.primary));
398 }
399 });
400 }
401 continue;
402 }
403
404 if trimmed.is_empty() {
405 if append_cursor {
406 self.styled("▌", Style::new().fg(self.theme.primary));
407 } else {
408 self.text(" ");
409 }
410 continue;
411 }
412
413 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
414 let mut line = "─".repeat(40);
415 line.push_str(cursor);
416 self.styled(line, border_style);
417 continue;
418 }
419
420 if let Some(heading) = trimmed.strip_prefix("### ") {
421 let mut line = String::with_capacity(heading.len() + cursor.len());
422 line.push_str(heading);
423 line.push_str(cursor);
424 self.styled(line, Style::new().bold().fg(self.theme.accent));
425 continue;
426 }
427
428 if let Some(heading) = trimmed.strip_prefix("## ") {
429 let mut line = String::with_capacity(heading.len() + cursor.len());
430 line.push_str(heading);
431 line.push_str(cursor);
432 self.styled(line, Style::new().bold().fg(self.theme.secondary));
433 continue;
434 }
435
436 if let Some(heading) = trimmed.strip_prefix("# ") {
437 let mut line = String::with_capacity(heading.len() + cursor.len());
438 line.push_str(heading);
439 line.push_str(cursor);
440 self.styled(line, Style::new().bold().fg(self.theme.primary));
441 continue;
442 }
443
444 if let Some(code) = trimmed.strip_prefix("```") {
445 in_code_block = true;
446 code_block_lang = code.trim().to_string();
447 let label = if code_block_lang.is_empty() {
448 "code".to_string()
449 } else {
450 let mut label = String::from("code:");
451 label.push_str(&code_block_lang);
452 label
453 };
454 let mut line = String::with_capacity(5 + label.len() + cursor.len());
455 line.push_str(" ┌─");
456 line.push_str(&label);
457 line.push('─');
458 line.push_str(cursor);
459 self.styled(line, border_style);
460 continue;
461 }
462
463 if let Some(item) = trimmed
464 .strip_prefix("- ")
465 .or_else(|| trimmed.strip_prefix("* "))
466 {
467 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
468 if segs.len() <= 1 {
469 let mut line = String::with_capacity(4 + item.len() + cursor.len());
470 line.push_str(" • ");
471 line.push_str(item);
472 line.push_str(cursor);
473 self.styled(line, text_style);
474 } else {
475 self.line(|ui| {
476 ui.styled(" • ", text_style);
477 for (s, st) in segs {
478 ui.styled(s, st);
479 }
480 if append_cursor {
481 ui.styled("▌", Style::new().fg(ui.theme.primary));
482 }
483 });
484 }
485 continue;
486 }
487
488 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
489 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
490 if parts.len() == 2 {
491 let segs =
492 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
493 if segs.len() <= 1 {
494 let mut line = String::with_capacity(
495 4 + parts[0].len() + parts[1].len() + cursor.len(),
496 );
497 line.push_str(" ");
498 line.push_str(parts[0]);
499 line.push_str(". ");
500 line.push_str(parts[1]);
501 line.push_str(cursor);
502 self.styled(line, text_style);
503 } else {
504 self.line(|ui| {
505 let mut prefix = String::with_capacity(4 + parts[0].len());
506 prefix.push_str(" ");
507 prefix.push_str(parts[0]);
508 prefix.push_str(". ");
509 ui.styled(prefix, text_style);
510 for (s, st) in segs {
511 ui.styled(s, st);
512 }
513 if append_cursor {
514 ui.styled("▌", Style::new().fg(ui.theme.primary));
515 }
516 });
517 }
518 } else {
519 let mut line = String::with_capacity(trimmed.len() + cursor.len());
520 line.push_str(trimmed);
521 line.push_str(cursor);
522 self.styled(line, text_style);
523 }
524 continue;
525 }
526
527 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
528 if segs.len() <= 1 {
529 let mut line = String::with_capacity(trimmed.len() + cursor.len());
530 line.push_str(trimmed);
531 line.push_str(cursor);
532 self.styled(line, text_style);
533 } else {
534 self.line(|ui| {
535 for (s, st) in segs {
536 ui.styled(s, st);
537 }
538 if append_cursor {
539 ui.styled("▌", Style::new().fg(ui.theme.primary));
540 }
541 });
542 }
543 }
544
545 if show_cursor && trailing_newline {
546 if in_code_block {
547 self.styled(" ▌", code_style);
548 } else {
549 self.styled("▌", Style::new().fg(self.theme.primary));
550 }
551 }
552
553 if state.in_code_block != in_code_block {
554 state.in_code_block = in_code_block;
555 }
556 if state.code_block_lang != code_block_lang {
557 state.code_block_lang = code_block_lang;
558 }
559
560 self.commands.push(Command::EndContainer);
561 self.rollback.last_text_idx = None;
562 Response::none()
563 }
564
565 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
580 let old_action = state.action;
581 let theme = self.theme;
582 let _ = self.bordered(Border::Rounded).col(|ui| {
583 let _ = ui.row(|ui| {
584 ui.text("⚡").fg(theme.warning);
585 ui.text(&state.tool_name).bold().fg(theme.primary);
586 });
587 ui.text(&state.description).dim();
588
589 if state.action == ApprovalAction::Pending {
590 let _ = ui.row(|ui| {
591 if ui.button("✓ Approve").clicked {
592 state.action = ApprovalAction::Approved;
593 }
594 if ui.button("✗ Reject").clicked {
595 state.action = ApprovalAction::Rejected;
596 }
597 });
598 } else {
599 let (label, color) = match state.action {
600 ApprovalAction::Approved => ("✓ Approved", theme.success),
601 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
602 ApprovalAction::Pending => unreachable!(),
603 };
604 ui.text(label).fg(color).bold();
605 }
606 });
607
608 Response {
609 changed: state.action != old_action,
610 ..Response::none()
611 }
612 }
613
614 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
627 if items.is_empty() {
628 return Response::none();
629 }
630
631 let theme = self.theme;
632 let total: usize = items.iter().map(|item| item.tokens).sum();
633
634 let _ = self.container().row(|ui| {
635 ui.text("📎").dim();
636 for item in items {
637 let token_count = format_token_count(item.tokens);
638 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
639 line.push_str(&item.label);
640 line.push_str(" (");
641 line.push_str(&token_count);
642 line.push(')');
643 ui.text(line).fg(theme.secondary);
644 }
645 ui.spacer();
646 let total_text = format_token_count(total);
647 let mut line = String::with_capacity(2 + total_text.len());
648 line.push_str("Σ ");
649 line.push_str(&total_text);
650 ui.text(line).dim();
651 });
652
653 Response::none()
654 }
655}