Skip to main content

vtcode_tui/core_tui/session/
input.rs

1use super::{PLACEHOLDER_COLOR, Session, measure_text_width, ratatui_style_from_inline};
2use crate::config::constants::ui;
3use crate::ui::tui::types::InlineTextStyle;
4use crate::utils::file_utils::is_image_path;
5use anstyle::{Color as AnsiColorEnum, Effects};
6use ratatui::{
7    buffer::Buffer,
8    prelude::*,
9    widgets::{Block, Clear, Padding, Paragraph, Wrap},
10};
11use regex::Regex;
12use std::path::Path;
13use std::sync::LazyLock;
14use tui_shimmer::shimmer_spans_with_style_at_phase;
15use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
16
17struct InputRender {
18    text: Text<'static>,
19    cursor_x: u16,
20    cursor_y: u16,
21}
22
23#[derive(Default)]
24struct InputLineBuffer {
25    prefix: String,
26    text: String,
27    prefix_width: u16,
28    text_width: u16,
29}
30
31impl InputLineBuffer {
32    fn new(prefix: String, prefix_width: u16) -> Self {
33        Self {
34            prefix,
35            text: String::new(),
36            prefix_width,
37            text_width: 0,
38        }
39    }
40}
41
42struct InputLayout {
43    buffers: Vec<InputLineBuffer>,
44    cursor_line_idx: usize,
45    cursor_column: u16,
46}
47
48impl Session {
49    pub(super) fn render_input(&mut self, frame: &mut Frame<'_>, area: Rect) {
50        frame.render_widget(Clear, area);
51        if area.height == 0 {
52            self.set_input_area(None);
53            return;
54        }
55
56        self.set_input_area(Some(area));
57
58        let mut input_area = area;
59        let mut status_area = None;
60        let mut status_line = None;
61
62        if area.height >= 2
63            && let Some(line) = self.render_input_status_line(area.width)
64        {
65            let block_height = area.height.saturating_sub(1).max(1);
66            input_area.height = block_height;
67            status_area = Some(Rect::new(area.x, area.y + block_height, area.width, 1));
68            status_line = Some(line);
69        }
70
71        let background_style = self.styles.input_background_style();
72        let block = Block::new().style(background_style).padding(Padding::new(
73            ui::INLINE_INPUT_PADDING_HORIZONTAL,
74            ui::INLINE_INPUT_PADDING_HORIZONTAL,
75            ui::INLINE_INPUT_PADDING_VERTICAL,
76            ui::INLINE_INPUT_PADDING_VERTICAL,
77        ));
78        let inner = block.inner(input_area);
79        let input_render = self.build_input_render(inner.width, inner.height);
80        let paragraph = Paragraph::new(input_render.text)
81            .style(background_style)
82            .wrap(Wrap { trim: false });
83        frame.render_widget(paragraph.block(block), input_area);
84
85        if self.cursor_should_be_visible() && inner.width > 0 && inner.height > 0 {
86            let cursor_x = input_render
87                .cursor_x
88                .min(inner.width.saturating_sub(1))
89                .saturating_add(inner.x);
90            let cursor_y = input_render
91                .cursor_y
92                .min(inner.height.saturating_sub(1))
93                .saturating_add(inner.y);
94            if self.use_fake_cursor() {
95                render_fake_cursor(frame.buffer_mut(), cursor_x, cursor_y);
96            } else {
97                frame.set_cursor_position(Position::new(cursor_x, cursor_y));
98            }
99        }
100
101        if let (Some(status_rect), Some(line)) = (status_area, status_line) {
102            let paragraph = Paragraph::new(line)
103                .style(self.styles.default_style())
104                .wrap(Wrap { trim: false });
105            frame.render_widget(paragraph, status_rect);
106        }
107    }
108
109    pub(crate) fn desired_input_lines(&self, inner_width: u16) -> u16 {
110        if inner_width == 0 {
111            return 1;
112        }
113
114        if self.input_compact_mode
115            && self.input_manager.cursor() == self.input_manager.content().len()
116            && self.input_compact_placeholder().is_some()
117        {
118            return 1;
119        }
120
121        if self.input_manager.content().is_empty() {
122            return 1;
123        }
124
125        let prompt_width = UnicodeWidthStr::width(self.prompt_prefix.as_str()) as u16;
126        let prompt_display_width = prompt_width.min(inner_width);
127        let layout = self.input_layout(inner_width, prompt_display_width);
128        let line_count = layout.buffers.len().max(1);
129        let capped = line_count.min(ui::INLINE_INPUT_MAX_LINES.max(1));
130        capped as u16
131    }
132
133    pub(crate) fn apply_input_height(&mut self, height: u16) {
134        let resolved = height.max(Self::input_block_height_for_lines(1));
135        if self.input_height != resolved {
136            self.input_height = resolved;
137            crate::ui::tui::session::render::recalculate_transcript_rows(self);
138        }
139    }
140
141    pub(crate) fn input_block_height_for_lines(lines: u16) -> u16 {
142        lines
143            .max(1)
144            .saturating_add(ui::INLINE_INPUT_PADDING_VERTICAL.saturating_mul(2))
145    }
146
147    fn input_layout(&self, width: u16, prompt_display_width: u16) -> InputLayout {
148        let indent_prefix = " ".repeat(prompt_display_width as usize);
149        let mut buffers = vec![InputLineBuffer::new(
150            self.prompt_prefix.clone(),
151            prompt_display_width,
152        )];
153        let secure_prompt_active = self.secure_prompt_active();
154        let mut cursor_line_idx = 0usize;
155        let mut cursor_column = prompt_display_width;
156        let input_content = self.input_manager.content();
157        let cursor_pos = self.input_manager.cursor();
158        let mut cursor_set = cursor_pos == 0;
159
160        for (idx, ch) in input_content.char_indices() {
161            if !cursor_set
162                && cursor_pos == idx
163                && let Some(current) = buffers.last()
164            {
165                cursor_line_idx = buffers.len() - 1;
166                cursor_column = current.prefix_width + current.text_width;
167                cursor_set = true;
168            }
169
170            if ch == '\n' {
171                let end = idx + ch.len_utf8();
172                buffers.push(InputLineBuffer::new(
173                    indent_prefix.clone(),
174                    prompt_display_width,
175                ));
176                if !cursor_set && cursor_pos == end {
177                    cursor_line_idx = buffers.len() - 1;
178                    cursor_column = prompt_display_width;
179                    cursor_set = true;
180                }
181                continue;
182            }
183
184            let display_ch = if secure_prompt_active { '•' } else { ch };
185            let char_width = UnicodeWidthChar::width(display_ch).unwrap_or(0) as u16;
186
187            if let Some(current) = buffers.last_mut() {
188                let capacity = width.saturating_sub(current.prefix_width);
189                if capacity > 0
190                    && current.text_width + char_width > capacity
191                    && !current.text.is_empty()
192                {
193                    buffers.push(InputLineBuffer::new(
194                        indent_prefix.clone(),
195                        prompt_display_width,
196                    ));
197                }
198            }
199
200            if let Some(current) = buffers.last_mut() {
201                current.text.push(display_ch);
202                current.text_width = current.text_width.saturating_add(char_width);
203            }
204
205            let end = idx + ch.len_utf8();
206            if !cursor_set
207                && cursor_pos == end
208                && let Some(current) = buffers.last()
209            {
210                cursor_line_idx = buffers.len() - 1;
211                cursor_column = current.prefix_width + current.text_width;
212                cursor_set = true;
213            }
214        }
215
216        if !cursor_set && let Some(current) = buffers.last() {
217            cursor_line_idx = buffers.len() - 1;
218            cursor_column = current.prefix_width + current.text_width;
219        }
220
221        InputLayout {
222            buffers,
223            cursor_line_idx,
224            cursor_column,
225        }
226    }
227
228    fn build_input_render(&self, width: u16, height: u16) -> InputRender {
229        if width == 0 || height == 0 {
230            return InputRender {
231                text: Text::default(),
232                cursor_x: 0,
233                cursor_y: 0,
234            };
235        }
236
237        let max_visible_lines = height.max(1).min(ui::INLINE_INPUT_MAX_LINES as u16) as usize;
238
239        let mut prompt_style = self.prompt_style.clone();
240        if prompt_style.color.is_none() {
241            prompt_style.color = self.theme.primary.or(self.theme.foreground);
242        }
243        let prompt_style = ratatui_style_from_inline(&prompt_style, self.theme.foreground);
244        let prompt_width = UnicodeWidthStr::width(self.prompt_prefix.as_str()) as u16;
245        let prompt_display_width = prompt_width.min(width);
246
247        let cursor_at_end = self.input_manager.cursor() == self.input_manager.content().len();
248        if self.input_compact_mode
249            && cursor_at_end
250            && let Some(placeholder) = self.input_compact_placeholder()
251        {
252            let placeholder_style = InlineTextStyle {
253                color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
254                bg_color: None,
255                effects: Effects::DIMMED,
256            };
257            let style = ratatui_style_from_inline(
258                &placeholder_style,
259                Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
260            );
261            let placeholder_width = UnicodeWidthStr::width(placeholder.as_str()) as u16;
262            return InputRender {
263                text: Text::from(vec![Line::from(vec![
264                    Span::styled(self.prompt_prefix.clone(), prompt_style),
265                    Span::styled(placeholder, style),
266                ])]),
267                cursor_x: prompt_display_width.saturating_add(placeholder_width),
268                cursor_y: 0,
269            };
270        }
271
272        if self.input_manager.content().is_empty() {
273            let mut spans = Vec::new();
274            spans.push(Span::styled(self.prompt_prefix.clone(), prompt_style));
275
276            if let Some(placeholder) = &self.placeholder {
277                let placeholder_style = self.placeholder_style.clone().unwrap_or(InlineTextStyle {
278                    color: Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
279                    bg_color: None,
280                    effects: Effects::ITALIC,
281                });
282                let style = ratatui_style_from_inline(
283                    &placeholder_style,
284                    Some(AnsiColorEnum::Rgb(PLACEHOLDER_COLOR)),
285                );
286                spans.push(Span::styled(placeholder.clone(), style));
287            }
288
289            return InputRender {
290                text: Text::from(vec![Line::from(spans)]),
291                cursor_x: prompt_display_width,
292                cursor_y: 0,
293            };
294        }
295
296        let accent_style =
297            ratatui_style_from_inline(&self.styles.accent_inline_style(), self.theme.foreground);
298        let layout = self.input_layout(width, prompt_display_width);
299        let total_lines = layout.buffers.len();
300        let visible_limit = max_visible_lines.max(1);
301        let mut start = total_lines.saturating_sub(visible_limit);
302        if layout.cursor_line_idx < start {
303            start = layout.cursor_line_idx.saturating_sub(visible_limit - 1);
304        }
305        let end = (start + visible_limit).min(total_lines);
306        let cursor_y = layout.cursor_line_idx.saturating_sub(start) as u16;
307
308        let mut lines = Vec::new();
309        for buffer in &layout.buffers[start..end] {
310            let mut spans = Vec::new();
311            spans.push(Span::styled(buffer.prefix.clone(), prompt_style));
312            if !buffer.text.is_empty() {
313                spans.push(Span::styled(buffer.text.clone(), accent_style));
314            }
315            lines.push(Line::from(spans));
316        }
317
318        if lines.is_empty() {
319            lines.push(Line::from(vec![Span::styled(
320                self.prompt_prefix.clone(),
321                prompt_style,
322            )]));
323        }
324
325        InputRender {
326            text: Text::from(lines),
327            cursor_x: layout.cursor_column,
328            cursor_y,
329        }
330    }
331
332    pub(super) fn input_compact_placeholder(&self) -> Option<String> {
333        let content = self.input_manager.content();
334        let trimmed = content.trim();
335        let attachment_count = self.input_manager.attachments().len();
336        if trimmed.is_empty() && attachment_count == 0 {
337            return None;
338        }
339
340        if let Some(label) = compact_image_label(trimmed) {
341            return Some(format!("[Image: {label}]"));
342        }
343
344        if attachment_count > 0 {
345            let label = if attachment_count == 1 {
346                "1 attachment".to_string()
347            } else {
348                format!("{attachment_count} attachments")
349            };
350            if trimmed.is_empty() {
351                return Some(format!("[Image: {label}]"));
352            }
353            if let Some(compact) = compact_image_placeholders(content) {
354                return Some(format!("[Image: {label}] {compact}"));
355            }
356            return Some(format!("[Image: {label}] {trimmed}"));
357        }
358
359        let line_count = content.split('\n').count();
360        if line_count >= ui::INLINE_PASTE_COLLAPSE_LINE_THRESHOLD {
361            let char_count = content.chars().count();
362            return Some(format!("[Pasted Content {char_count} chars]"));
363        }
364
365        if let Some(compact) = compact_image_placeholders(content) {
366            return Some(compact);
367        }
368
369        None
370    }
371
372    fn render_input_status_line(&self, width: u16) -> Option<Line<'static>> {
373        if width == 0 {
374            return None;
375        }
376
377        let left = self
378            .input_status_left
379            .as_ref()
380            .map(|value| value.trim().to_owned())
381            .filter(|value| !value.is_empty());
382        let right = self
383            .input_status_right
384            .as_ref()
385            .map(|value| value.trim().to_owned())
386            .filter(|value| !value.is_empty());
387
388        // Build scroll indicator if enabled
389        let scroll_indicator = if ui::SCROLL_INDICATOR_ENABLED {
390            Some(self.build_scroll_indicator())
391        } else {
392            None
393        };
394
395        if left.is_none() && right.is_none() && scroll_indicator.is_none() {
396            return None;
397        }
398
399        let dim_style = self.styles.default_style().add_modifier(Modifier::DIM);
400        let mut spans = Vec::new();
401
402        // Add left content (git status or shimmered activity)
403        if let Some(left_value) = left.as_ref() {
404            if status_requires_shimmer(left_value)
405                && self.appearance.should_animate_progress_status()
406            {
407                spans.extend(shimmer_spans_with_style_at_phase(
408                    left_value,
409                    dim_style,
410                    self.shimmer_state.phase(),
411                ));
412            } else {
413                spans.extend(self.create_git_status_spans(left_value, dim_style));
414            }
415        }
416
417        // Build right side spans (scroll indicator + optional right content)
418        let mut right_spans: Vec<Span<'static>> = Vec::new();
419        if let Some(scroll) = &scroll_indicator {
420            right_spans.push(Span::styled(scroll.clone(), dim_style));
421        }
422        if let Some(right_value) = &right {
423            if !right_spans.is_empty() {
424                right_spans.push(Span::raw(" "));
425            }
426            right_spans.push(Span::styled(right_value.clone(), dim_style));
427        }
428
429        if !right_spans.is_empty() {
430            let left_width: u16 = spans.iter().map(|s| measure_text_width(&s.content)).sum();
431            let right_width: u16 = right_spans
432                .iter()
433                .map(|s| measure_text_width(&s.content))
434                .sum();
435            let padding = width.saturating_sub(left_width + right_width);
436
437            if padding > 0 {
438                spans.push(Span::raw(" ".repeat(padding as usize)));
439            } else if !spans.is_empty() {
440                spans.push(Span::raw(" "));
441            }
442            spans.extend(right_spans);
443        }
444
445        if spans.is_empty() {
446            return None;
447        }
448
449        Some(Line::from(spans))
450    }
451
452    /// Build scroll indicator string with percentage
453    fn build_scroll_indicator(&self) -> String {
454        let percent = self.scroll_manager.progress_percent();
455        format!("{} {:>3}%", ui::SCROLL_INDICATOR_FORMAT, percent)
456    }
457
458    #[allow(dead_code)]
459    fn create_git_status_spans(&self, text: &str, default_style: Style) -> Vec<Span<'static>> {
460        if let Some((branch_part, indicator_part)) = text.rsplit_once(" | ") {
461            let mut spans = Vec::new();
462            let branch_trim = branch_part.trim_end();
463            if !branch_trim.is_empty() {
464                spans.push(Span::styled(branch_trim.to_owned(), default_style));
465            }
466            spans.push(Span::raw(" "));
467
468            let indicator_trim = indicator_part.trim();
469            let indicator_style = if indicator_trim == ui::HEADER_GIT_DIRTY_SUFFIX {
470                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
471            } else if indicator_trim == ui::HEADER_GIT_CLEAN_SUFFIX {
472                Style::default()
473                    .fg(Color::Green)
474                    .add_modifier(Modifier::BOLD)
475            } else {
476                self.styles.accent_style().add_modifier(Modifier::BOLD)
477            };
478
479            spans.push(Span::styled(indicator_trim.to_owned(), indicator_style));
480            spans
481        } else {
482            vec![Span::styled(text.to_owned(), default_style)]
483        }
484    }
485
486    fn cursor_should_be_visible(&self) -> bool {
487        let loading_state = self.is_running_activity() || self.has_status_spinner();
488        self.cursor_visible && (self.input_enabled || loading_state)
489    }
490
491    fn use_fake_cursor(&self) -> bool {
492        self.has_status_spinner()
493    }
494
495    fn secure_prompt_active(&self) -> bool {
496        self.modal
497            .as_ref()
498            .and_then(|modal| modal.secure_prompt.as_ref())
499            .is_some()
500    }
501
502    /// Build input render data for external widgets
503    pub fn build_input_widget_data(&self, width: u16, height: u16) -> InputWidgetData {
504        let input_render = self.build_input_render(width, height);
505        let background_style = self.styles.input_background_style();
506
507        InputWidgetData {
508            text: input_render.text,
509            cursor_x: input_render.cursor_x,
510            cursor_y: input_render.cursor_y,
511            cursor_should_be_visible: self.cursor_should_be_visible(),
512            use_fake_cursor: self.use_fake_cursor(),
513            background_style,
514            default_style: self.styles.default_style(),
515        }
516    }
517
518    /// Build input status line for external widgets
519    pub fn build_input_status_widget_data(&self, width: u16) -> Option<Vec<Span<'static>>> {
520        self.render_input_status_line(width).map(|line| line.spans)
521    }
522}
523
524fn compact_image_label(content: &str) -> Option<String> {
525    let trimmed = content.trim();
526    if trimmed.is_empty() {
527        return None;
528    }
529
530    let unquoted = trimmed
531        .strip_prefix('"')
532        .and_then(|value| value.strip_suffix('"'))
533        .or_else(|| {
534            trimmed
535                .strip_prefix('\'')
536                .and_then(|value| value.strip_suffix('\''))
537        })
538        .unwrap_or(trimmed);
539
540    if unquoted.starts_with("data:image/") {
541        return Some("inline image".to_string());
542    }
543
544    let windows_drive = unquoted.as_bytes().get(1).is_some_and(|ch| *ch == b':')
545        && unquoted
546            .as_bytes()
547            .get(2)
548            .is_some_and(|ch| *ch == b'\\' || *ch == b'/');
549    let starts_like_path = unquoted.starts_with('@')
550        || unquoted.starts_with("file://")
551        || unquoted.starts_with('/')
552        || unquoted.starts_with("./")
553        || unquoted.starts_with("../")
554        || unquoted.starts_with("~/")
555        || windows_drive;
556    if !starts_like_path {
557        return None;
558    }
559
560    let without_at = unquoted.strip_prefix('@').unwrap_or(unquoted);
561
562    // Skip npm scoped package patterns like @scope/package@version
563    if without_at.contains('/')
564        && !without_at.starts_with('.')
565        && !without_at.starts_with('/')
566        && !without_at.starts_with("~/")
567    {
568        // Check if this looks like @scope/package (npm package)
569        let parts: Vec<&str> = without_at.split('/').collect();
570        if parts.len() >= 2 && !parts[0].is_empty() {
571            // Reject if it looks like a package name (no extension on second component)
572            if !parts[parts.len() - 1].contains('.') {
573                return None;
574            }
575        }
576    }
577
578    let without_scheme = without_at.strip_prefix("file://").unwrap_or(without_at);
579    let path = Path::new(without_scheme);
580    if !is_image_path(path) {
581        return None;
582    }
583
584    let label = path
585        .file_name()
586        .and_then(|name| name.to_str())
587        .unwrap_or(without_scheme);
588    Some(label.to_string())
589}
590
591static IMAGE_PATH_INLINE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
592    Regex::new(
593        r#"(?ix)
594        (?:^|[\s\(\[\{<\"'`])
595        (
596            @?
597            (?:file://)?
598            (?:
599                ~/(?:[^\n/]+/)+
600              | /(?:[^\n/]+/)+
601              | [A-Za-z]:[\\/](?:[^\n\\\/]+[\\/])+
602            )
603            [^\n]*?
604            \.(?:png|jpe?g|gif|bmp|webp|tiff?|svg)
605        )"#,
606    )
607    .expect("Failed to compile inline image path regex")
608});
609
610fn compact_image_placeholders(content: &str) -> Option<String> {
611    let mut matches = Vec::new();
612    for capture in IMAGE_PATH_INLINE_REGEX.captures_iter(content) {
613        let Some(path_match) = capture.get(1) else {
614            continue;
615        };
616        let raw = path_match.as_str();
617        let Some(label) = image_label_for_path(raw) else {
618            continue;
619        };
620        matches.push((path_match.start(), path_match.end(), label));
621    }
622
623    if matches.is_empty() {
624        return None;
625    }
626
627    let mut result = String::with_capacity(content.len());
628    let mut last_end = 0usize;
629    for (start, end, label) in matches {
630        if start < last_end {
631            continue;
632        }
633        result.push_str(&content[last_end..start]);
634        result.push_str(&format!("[Image: {label}]"));
635        last_end = end;
636    }
637    if last_end < content.len() {
638        result.push_str(&content[last_end..]);
639    }
640
641    Some(result)
642}
643
644fn image_label_for_path(raw: &str) -> Option<String> {
645    let trimmed = raw.trim_matches(|ch: char| matches!(ch, '"' | '\'')).trim();
646    if trimmed.is_empty() {
647        return None;
648    }
649
650    let without_at = trimmed.strip_prefix('@').unwrap_or(trimmed);
651    let without_scheme = without_at.strip_prefix("file://").unwrap_or(without_at);
652    let unescaped = unescape_whitespace(without_scheme);
653    let path = Path::new(unescaped.as_str());
654    if !is_image_path(path) {
655        return None;
656    }
657
658    let label = path
659        .file_name()
660        .and_then(|name| name.to_str())
661        .unwrap_or(unescaped.as_str());
662    Some(label.to_string())
663}
664
665fn unescape_whitespace(token: &str) -> String {
666    let mut result = String::with_capacity(token.len());
667    let mut chars = token.chars().peekable();
668    while let Some(ch) = chars.next() {
669        if ch == '\\'
670            && let Some(next) = chars.peek()
671            && next.is_ascii_whitespace()
672        {
673            result.push(*next);
674            chars.next();
675            continue;
676        }
677        result.push(ch);
678    }
679    result
680}
681
682fn is_spinner_frame(indicator: &str) -> bool {
683    matches!(
684        indicator,
685        "⠋" | "⠙"
686            | "⠹"
687            | "⠸"
688            | "⠼"
689            | "⠴"
690            | "⠦"
691            | "⠧"
692            | "⠇"
693            | "⠏"
694            | "-"
695            | "\\"
696            | "|"
697            | "/"
698            | "."
699    )
700}
701
702pub(crate) fn status_requires_shimmer(text: &str) -> bool {
703    if text.contains("Running command:")
704        || text.contains("Running tool:")
705        || text.contains("Running:")
706        || text.contains("Running ")
707        || text.contains("Executing ")
708        || text.contains("Press Ctrl+C to cancel")
709    {
710        return true;
711    }
712    let Some((indicator, rest)) = text.split_once(' ') else {
713        return false;
714    };
715    if indicator.chars().count() != 1 || rest.trim().is_empty() {
716        return false;
717    }
718    is_spinner_frame(indicator)
719}
720
721/// Data structure for input widget rendering
722#[derive(Clone, Debug)]
723pub struct InputWidgetData {
724    pub text: Text<'static>,
725    pub cursor_x: u16,
726    pub cursor_y: u16,
727    pub cursor_should_be_visible: bool,
728    pub use_fake_cursor: bool,
729    pub background_style: Style,
730    pub default_style: Style,
731}
732
733fn render_fake_cursor(buf: &mut Buffer, cursor_x: u16, cursor_y: u16) {
734    if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
735        let mut style = cell.style();
736        style = style.add_modifier(Modifier::REVERSED);
737        cell.set_style(style);
738        if cell.symbol().is_empty() {
739            cell.set_symbol(" ");
740        }
741    }
742}