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_w: u32,
222 pixel_h: 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_w, pixel_h);
238 let encoded = crate::sixel::encode_sixel(&rgba, pixel_w, pixel_h, 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_w: u32,
265 _pixel_h: 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.push(Command::BeginContainer {
356 direction: Direction::Column,
357 gap: 0,
358 align: Align::Start,
359 align_self: None,
360 justify: Justify::Start,
361 border: None,
362 border_sides: BorderSides::all(),
363 border_style: Style::new().fg(self.theme.border),
364 bg_color: None,
365 padding: Padding::default(),
366 margin: Margin::default(),
367 constraints: Constraints::default(),
368 title: None,
369 grow: 0,
370 group_name: None,
371 });
372 self.interaction_count += 1;
373
374 let text_style = Style::new().fg(self.theme.text);
375 let bold_style = Style::new().fg(self.theme.text).bold();
376 let code_style = Style::new().fg(self.theme.accent);
377 let border_style = Style::new().fg(self.theme.border).dim();
378
379 let mut in_code_block = false;
380 let mut code_block_lang = String::new();
381
382 for (idx, line) in lines.iter().enumerate() {
383 let line = *line;
384 let trimmed = line.trim();
385 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
386 let cursor = if append_cursor { "▌" } else { "" };
387
388 if in_code_block {
389 if trimmed.starts_with("```") {
390 in_code_block = false;
391 code_block_lang.clear();
392 let mut line = String::from(" └────");
393 line.push_str(cursor);
394 self.styled(line, border_style);
395 } else {
396 self.line(|ui| {
397 ui.text(" ");
398 render_highlighted_line(ui, line);
399 if !cursor.is_empty() {
400 ui.styled(cursor, Style::new().fg(ui.theme.primary));
401 }
402 });
403 }
404 continue;
405 }
406
407 if trimmed.is_empty() {
408 if append_cursor {
409 self.styled("▌", Style::new().fg(self.theme.primary));
410 } else {
411 self.text(" ");
412 }
413 continue;
414 }
415
416 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
417 let mut line = "─".repeat(40);
418 line.push_str(cursor);
419 self.styled(line, border_style);
420 continue;
421 }
422
423 if let Some(heading) = trimmed.strip_prefix("### ") {
424 let mut line = String::with_capacity(heading.len() + cursor.len());
425 line.push_str(heading);
426 line.push_str(cursor);
427 self.styled(line, Style::new().bold().fg(self.theme.accent));
428 continue;
429 }
430
431 if let Some(heading) = trimmed.strip_prefix("## ") {
432 let mut line = String::with_capacity(heading.len() + cursor.len());
433 line.push_str(heading);
434 line.push_str(cursor);
435 self.styled(line, Style::new().bold().fg(self.theme.secondary));
436 continue;
437 }
438
439 if let Some(heading) = trimmed.strip_prefix("# ") {
440 let mut line = String::with_capacity(heading.len() + cursor.len());
441 line.push_str(heading);
442 line.push_str(cursor);
443 self.styled(line, Style::new().bold().fg(self.theme.primary));
444 continue;
445 }
446
447 if let Some(code) = trimmed.strip_prefix("```") {
448 in_code_block = true;
449 code_block_lang = code.trim().to_string();
450 let label = if code_block_lang.is_empty() {
451 "code".to_string()
452 } else {
453 let mut label = String::from("code:");
454 label.push_str(&code_block_lang);
455 label
456 };
457 let mut line = String::with_capacity(5 + label.len() + cursor.len());
458 line.push_str(" ┌─");
459 line.push_str(&label);
460 line.push('─');
461 line.push_str(cursor);
462 self.styled(line, border_style);
463 continue;
464 }
465
466 if let Some(item) = trimmed
467 .strip_prefix("- ")
468 .or_else(|| trimmed.strip_prefix("* "))
469 {
470 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
471 if segs.len() <= 1 {
472 let mut line = String::with_capacity(4 + item.len() + cursor.len());
473 line.push_str(" • ");
474 line.push_str(item);
475 line.push_str(cursor);
476 self.styled(line, text_style);
477 } else {
478 self.line(|ui| {
479 ui.styled(" • ", text_style);
480 for (s, st) in segs {
481 ui.styled(s, st);
482 }
483 if append_cursor {
484 ui.styled("▌", Style::new().fg(ui.theme.primary));
485 }
486 });
487 }
488 continue;
489 }
490
491 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
492 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
493 if parts.len() == 2 {
494 let segs =
495 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
496 if segs.len() <= 1 {
497 let mut line = String::with_capacity(
498 4 + parts[0].len() + parts[1].len() + cursor.len(),
499 );
500 line.push_str(" ");
501 line.push_str(parts[0]);
502 line.push_str(". ");
503 line.push_str(parts[1]);
504 line.push_str(cursor);
505 self.styled(line, text_style);
506 } else {
507 self.line(|ui| {
508 let mut prefix = String::with_capacity(4 + parts[0].len());
509 prefix.push_str(" ");
510 prefix.push_str(parts[0]);
511 prefix.push_str(". ");
512 ui.styled(prefix, text_style);
513 for (s, st) in segs {
514 ui.styled(s, st);
515 }
516 if append_cursor {
517 ui.styled("▌", Style::new().fg(ui.theme.primary));
518 }
519 });
520 }
521 } else {
522 let mut line = String::with_capacity(trimmed.len() + cursor.len());
523 line.push_str(trimmed);
524 line.push_str(cursor);
525 self.styled(line, text_style);
526 }
527 continue;
528 }
529
530 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
531 if segs.len() <= 1 {
532 let mut line = String::with_capacity(trimmed.len() + cursor.len());
533 line.push_str(trimmed);
534 line.push_str(cursor);
535 self.styled(line, text_style);
536 } else {
537 self.line(|ui| {
538 for (s, st) in segs {
539 ui.styled(s, st);
540 }
541 if append_cursor {
542 ui.styled("▌", Style::new().fg(ui.theme.primary));
543 }
544 });
545 }
546 }
547
548 if show_cursor && trailing_newline {
549 if in_code_block {
550 self.styled(" ▌", code_style);
551 } else {
552 self.styled("▌", Style::new().fg(self.theme.primary));
553 }
554 }
555
556 state.in_code_block = in_code_block;
557 state.code_block_lang = code_block_lang;
558
559 self.commands.push(Command::EndContainer);
560 self.last_text_idx = None;
561 Response::none()
562 }
563
564 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
579 let old_action = state.action;
580 let theme = self.theme;
581 let _ = self.bordered(Border::Rounded).col(|ui| {
582 let _ = ui.row(|ui| {
583 ui.text("⚡").fg(theme.warning);
584 ui.text(&state.tool_name).bold().fg(theme.primary);
585 });
586 ui.text(&state.description).dim();
587
588 if state.action == ApprovalAction::Pending {
589 let _ = ui.row(|ui| {
590 if ui.button("✓ Approve").clicked {
591 state.action = ApprovalAction::Approved;
592 }
593 if ui.button("✗ Reject").clicked {
594 state.action = ApprovalAction::Rejected;
595 }
596 });
597 } else {
598 let (label, color) = match state.action {
599 ApprovalAction::Approved => ("✓ Approved", theme.success),
600 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
601 ApprovalAction::Pending => unreachable!(),
602 };
603 ui.text(label).fg(color).bold();
604 }
605 });
606
607 Response {
608 changed: state.action != old_action,
609 ..Response::none()
610 }
611 }
612
613 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
626 if items.is_empty() {
627 return Response::none();
628 }
629
630 let theme = self.theme;
631 let total: usize = items.iter().map(|item| item.tokens).sum();
632
633 let _ = self.container().row(|ui| {
634 ui.text("📎").dim();
635 for item in items {
636 let token_count = format_token_count(item.tokens);
637 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
638 line.push_str(&item.label);
639 line.push_str(" (");
640 line.push_str(&token_count);
641 line.push(')');
642 ui.text(line).fg(theme.secondary);
643 }
644 ui.spacer();
645 let total_text = format_token_count(total);
646 let mut line = String::with_capacity(2 + total_text.len());
647 line.push_str("Σ ");
648 line.push_str(&total_text);
649 ui.text(line).dim();
650 });
651
652 Response::none()
653 }
654}