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 width = img.width;
78 let height = img.height;
79
80 let _ = self.container().w(width).h(height).gap(0).col(|ui| {
81 for row in 0..height {
82 let _ = ui.container().gap(0).row(|ui| {
83 for col in 0..width {
84 let idx = (row * width + col) as usize;
85 if let Some(&(upper, lower)) = img.pixels.get(idx) {
86 ui.styled("▀", Style::new().fg(upper).bg(lower));
87 }
88 }
89 });
90 }
91 });
92
93 Response::none()
94 }
95
96 pub fn kitty_image(
112 &mut self,
113 rgba: &[u8],
114 pixel_width: u32,
115 pixel_height: u32,
116 cols: u32,
117 rows: u32,
118 ) -> Response {
119 let rgba_data = normalize_rgba(rgba, pixel_width, pixel_height);
120 let content_hash = crate::buffer::hash_rgba(&rgba_data);
121 let rgba_arc = std::sync::Arc::new(rgba_data);
122 let sw = pixel_width;
123 let sh = pixel_height;
124
125 self.container().w(cols).h(rows).draw(move |buf, rect| {
126 if rect.width == 0 || rect.height == 0 {
127 return;
128 }
129 buf.kitty_place(crate::buffer::KittyPlacement {
130 content_hash,
131 rgba: rgba_arc.clone(),
132 src_width: sw,
133 src_height: sh,
134 x: rect.x,
135 y: rect.y,
136 cols: rect.width,
137 rows: rect.height,
138 crop_y: 0,
139 crop_h: 0,
140 });
141 });
142 Response::none()
143 }
144
145 pub fn kitty_image_fit(
155 &mut self,
156 rgba: &[u8],
157 src_width: u32,
158 src_height: u32,
159 cols: u32,
160 ) -> Response {
161 #[cfg(feature = "crossterm")]
162 let (cell_w, cell_h) = crate::terminal::cell_pixel_size();
163 #[cfg(not(feature = "crossterm"))]
164 let (cell_w, cell_h) = (8u32, 16u32);
165
166 let rows = if src_width == 0 {
167 1
168 } else {
169 ((cols as f64 * src_height as f64 * cell_w as f64) / (src_width as f64 * cell_h as f64))
170 .ceil()
171 .max(1.0) as u32
172 };
173 let rgba_data = normalize_rgba(rgba, src_width, src_height);
174 let content_hash = crate::buffer::hash_rgba(&rgba_data);
175 let rgba_arc = std::sync::Arc::new(rgba_data);
176 let sw = src_width;
177 let sh = src_height;
178
179 self.container().w(cols).h(rows).draw(move |buf, rect| {
180 if rect.width == 0 || rect.height == 0 {
181 return;
182 }
183 buf.kitty_place(crate::buffer::KittyPlacement {
184 content_hash,
185 rgba: rgba_arc.clone(),
186 src_width: sw,
187 src_height: sh,
188 x: rect.x,
189 y: rect.y,
190 cols: rect.width,
191 rows: rect.height,
192 crop_y: 0,
193 crop_h: 0,
194 });
195 });
196 Response::none()
197 }
198
199 #[cfg(feature = "crossterm")]
218 pub fn sixel_image(
219 &mut self,
220 rgba: &[u8],
221 pixel_width: u32,
222 pixel_height: u32,
223 cols: u32,
224 rows: u32,
225 ) -> Response {
226 let sixel_supported = self.is_real_terminal && terminal_supports_sixel();
227 if !sixel_supported {
228 self.container().w(cols).h(rows).draw(|buf, rect| {
229 if rect.width == 0 || rect.height == 0 {
230 return;
231 }
232 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
233 });
234 return Response::none();
235 }
236
237 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
238 let encoded = crate::sixel::encode_sixel(&rgba, pixel_width, pixel_height, 256);
239
240 if encoded.is_empty() {
241 self.container().w(cols).h(rows).draw(|buf, rect| {
242 if rect.width == 0 || rect.height == 0 {
243 return;
244 }
245 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
246 });
247 return Response::none();
248 }
249
250 self.container().w(cols).h(rows).draw(move |buf, rect| {
251 if rect.width == 0 || rect.height == 0 {
252 return;
253 }
254 buf.raw_sequence(rect.x, rect.y, encoded);
255 });
256 Response::none()
257 }
258
259 #[cfg(not(feature = "crossterm"))]
261 pub fn sixel_image(
262 &mut self,
263 _rgba: &[u8],
264 _pixel_width: u32,
265 _pixel_height: u32,
266 cols: u32,
267 rows: u32,
268 ) -> Response {
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 unsupported]", Style::new());
274 });
275 Response::none()
276 }
277
278 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
294 if state.streaming {
295 state.cursor_tick = state.cursor_tick.wrapping_add(1);
296 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
297 }
298
299 if state.content.is_empty() && state.streaming {
300 let cursor = if state.cursor_visible { "▌" } else { " " };
301 let primary = self.theme.primary;
302 self.text(cursor).fg(primary);
303 return Response::none();
304 }
305
306 if !state.content.is_empty() {
307 if state.streaming && state.cursor_visible {
308 self.text(format!("{}▌", state.content)).wrap();
309 } else {
310 self.text(&state.content).wrap();
311 }
312 }
313
314 Response::none()
315 }
316
317 pub fn streaming_markdown(
335 &mut self,
336 state: &mut crate::widgets::StreamingMarkdownState,
337 ) -> Response {
338 if state.streaming {
339 state.cursor_tick = state.cursor_tick.wrapping_add(1);
340 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
341 }
342
343 if state.content.is_empty() && state.streaming {
344 let cursor = if state.cursor_visible { "▌" } else { " " };
345 let primary = self.theme.primary;
346 self.text(cursor).fg(primary);
347 return Response::none();
348 }
349
350 let show_cursor = state.streaming && state.cursor_visible;
351 let trailing_newline = state.content.ends_with('\n');
352 let lines: Vec<&str> = state.content.lines().collect();
353 let last_line_index = lines.len().saturating_sub(1);
354
355 self.commands
356 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
357 direction: Direction::Column,
358 gap: 0,
359 align: Align::Start,
360 align_self: None,
361 justify: Justify::Start,
362 border: None,
363 border_sides: BorderSides::all(),
364 border_style: Style::new().fg(self.theme.border),
365 bg_color: None,
366 padding: Padding::default(),
367 margin: Margin::default(),
368 constraints: Constraints::default(),
369 title: None,
370 grow: 0,
371 group_name: None,
372 })));
373 self.skip_interaction_slot();
374
375 let text_style = Style::new().fg(self.theme.text);
376 let bold_style = Style::new().fg(self.theme.text).bold();
377 let code_style = Style::new().fg(self.theme.accent);
378 let border_style = Style::new().fg(self.theme.border).dim();
379
380 let mut in_code_block = false;
381 let mut code_block_lang = String::new();
382
383 for (idx, line) in lines.iter().enumerate() {
384 let line = *line;
385 let trimmed = line.trim();
386 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
387 let cursor = if append_cursor { "▌" } else { "" };
388
389 if in_code_block {
390 if trimmed.starts_with("```") {
391 in_code_block = false;
392 code_block_lang.clear();
393 let mut line = String::from(" └────");
394 line.push_str(cursor);
395 self.styled(line, border_style);
396 } else {
397 self.line(|ui| {
398 ui.text(" ");
399 render_highlighted_line(ui, line);
400 if !cursor.is_empty() {
401 ui.styled(cursor, Style::new().fg(ui.theme.primary));
402 }
403 });
404 }
405 continue;
406 }
407
408 if trimmed.is_empty() {
409 if append_cursor {
410 self.styled("▌", Style::new().fg(self.theme.primary));
411 } else {
412 self.text(" ");
413 }
414 continue;
415 }
416
417 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
418 let mut line = "─".repeat(40);
419 line.push_str(cursor);
420 self.styled(line, border_style);
421 continue;
422 }
423
424 if let Some(heading) = trimmed.strip_prefix("### ") {
425 let mut line = String::with_capacity(heading.len() + cursor.len());
426 line.push_str(heading);
427 line.push_str(cursor);
428 self.styled(line, Style::new().bold().fg(self.theme.accent));
429 continue;
430 }
431
432 if let Some(heading) = trimmed.strip_prefix("## ") {
433 let mut line = String::with_capacity(heading.len() + cursor.len());
434 line.push_str(heading);
435 line.push_str(cursor);
436 self.styled(line, Style::new().bold().fg(self.theme.secondary));
437 continue;
438 }
439
440 if let Some(heading) = trimmed.strip_prefix("# ") {
441 let mut line = String::with_capacity(heading.len() + cursor.len());
442 line.push_str(heading);
443 line.push_str(cursor);
444 self.styled(line, Style::new().bold().fg(self.theme.primary));
445 continue;
446 }
447
448 if let Some(code) = trimmed.strip_prefix("```") {
449 in_code_block = true;
450 code_block_lang = code.trim().to_string();
451 let label = if code_block_lang.is_empty() {
452 "code".to_string()
453 } else {
454 let mut label = String::from("code:");
455 label.push_str(&code_block_lang);
456 label
457 };
458 let mut line = String::with_capacity(5 + label.len() + cursor.len());
459 line.push_str(" ┌─");
460 line.push_str(&label);
461 line.push('─');
462 line.push_str(cursor);
463 self.styled(line, border_style);
464 continue;
465 }
466
467 if let Some(item) = trimmed
468 .strip_prefix("- ")
469 .or_else(|| trimmed.strip_prefix("* "))
470 {
471 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
472 if segs.len() <= 1 {
473 let mut line = String::with_capacity(4 + item.len() + cursor.len());
474 line.push_str(" • ");
475 line.push_str(item);
476 line.push_str(cursor);
477 self.styled(line, text_style);
478 } else {
479 self.line(|ui| {
480 ui.styled(" • ", text_style);
481 for (s, st) in segs {
482 ui.styled(s, st);
483 }
484 if append_cursor {
485 ui.styled("▌", Style::new().fg(ui.theme.primary));
486 }
487 });
488 }
489 continue;
490 }
491
492 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
493 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
494 if parts.len() == 2 {
495 let segs =
496 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
497 if segs.len() <= 1 {
498 let mut line = String::with_capacity(
499 4 + parts[0].len() + parts[1].len() + cursor.len(),
500 );
501 line.push_str(" ");
502 line.push_str(parts[0]);
503 line.push_str(". ");
504 line.push_str(parts[1]);
505 line.push_str(cursor);
506 self.styled(line, text_style);
507 } else {
508 self.line(|ui| {
509 let mut prefix = String::with_capacity(4 + parts[0].len());
510 prefix.push_str(" ");
511 prefix.push_str(parts[0]);
512 prefix.push_str(". ");
513 ui.styled(prefix, text_style);
514 for (s, st) in segs {
515 ui.styled(s, st);
516 }
517 if append_cursor {
518 ui.styled("▌", Style::new().fg(ui.theme.primary));
519 }
520 });
521 }
522 } else {
523 let mut line = String::with_capacity(trimmed.len() + cursor.len());
524 line.push_str(trimmed);
525 line.push_str(cursor);
526 self.styled(line, text_style);
527 }
528 continue;
529 }
530
531 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
532 if segs.len() <= 1 {
533 let mut line = String::with_capacity(trimmed.len() + cursor.len());
534 line.push_str(trimmed);
535 line.push_str(cursor);
536 self.styled(line, text_style);
537 } else {
538 self.line(|ui| {
539 for (s, st) in segs {
540 ui.styled(s, st);
541 }
542 if append_cursor {
543 ui.styled("▌", Style::new().fg(ui.theme.primary));
544 }
545 });
546 }
547 }
548
549 if show_cursor && trailing_newline {
550 if in_code_block {
551 self.styled(" ▌", code_style);
552 } else {
553 self.styled("▌", Style::new().fg(self.theme.primary));
554 }
555 }
556
557 state.in_code_block = in_code_block;
558 state.code_block_lang = code_block_lang;
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}