magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use std::fmt;

use ratatui::{
    Frame,
    layout::Rect,
    style::Style,
    widgets::{Block, Borders},
};
use tui_textarea::{CursorMove, CursorRenderMode, TextArea, WrapMode};

use super::theme::MissionControlTheme;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SingleLineDisplay {
    Plain,
    Redacted,
}

/// Reusable single-line modal editor backed by `tui-textarea` editing semantics.
///
/// Rendering uses a clone so viewport calculation never mutates application state. Redacted
/// fields also create their render clone from mask characters only, keeping the raw value out of
/// buffers and render-time diagnostics.
#[derive(Clone)]
pub(crate) struct SingleLineField {
    textarea: TextArea<'static>,
    display: SingleLineDisplay,
}

impl SingleLineField {
    pub(crate) fn plain() -> Self {
        Self::new(SingleLineDisplay::Plain)
    }

    pub(crate) fn redacted() -> Self {
        Self::new(SingleLineDisplay::Redacted)
    }

    fn new(display: SingleLineDisplay) -> Self {
        Self {
            textarea: configured_textarea(String::new()),
            display,
        }
    }

    pub(crate) fn value(&self) -> &str {
        self.textarea.lines().first().map_or("", String::as_str)
    }

    pub(crate) fn is_empty(&self) -> bool {
        self.value().is_empty()
    }

    pub(crate) fn set_value(&mut self, value: impl AsRef<str>) {
        let normalized = normalize_single_line(value.as_ref());
        self.textarea = configured_textarea(normalized);
        self.textarea.move_cursor(CursorMove::End);
    }

    pub(crate) fn insert_char(&mut self, ch: char) -> bool {
        let mut encoded = [0; 4];
        self.insert_str(ch.encode_utf8(&mut encoded))
    }

    pub(crate) fn insert_str(&mut self, value: &str) -> bool {
        let normalized = normalize_single_line(value);
        !normalized.is_empty() && self.textarea.insert_str(normalized)
    }

    pub(crate) fn backspace(&mut self) -> bool {
        self.textarea.delete_char()
    }

    pub(crate) fn delete_forward(&mut self) -> bool {
        self.textarea.delete_next_char()
    }

    pub(crate) fn move_left(&mut self) {
        self.textarea.move_cursor(CursorMove::Back);
    }

    pub(crate) fn move_right(&mut self) {
        self.textarea.move_cursor(CursorMove::Forward);
    }

    pub(crate) fn move_home(&mut self) {
        self.textarea.move_cursor(CursorMove::Head);
    }

    pub(crate) fn move_end(&mut self) {
        self.textarea.move_cursor(CursorMove::End);
    }

    pub(crate) fn clear(&mut self) {
        self.textarea = configured_textarea(String::new());
    }

    pub(crate) fn take(&mut self) -> Option<String> {
        if self.value().trim().is_empty() {
            return None;
        }
        let value = self.value().to_string();
        self.clear();
        Some(value)
    }

    pub(crate) fn render(
        &self,
        frame: &mut Frame<'_>,
        area: Rect,
        label: &str,
        placeholder: &str,
        focused: bool,
        theme: MissionControlTheme,
    ) {
        if area.is_empty() {
            return;
        }

        let title = if focused {
            format!(" {label} • editing ")
        } else {
            format!(" {label} ")
        };
        let block = Block::default()
            .borders(Borders::ALL)
            .title(title)
            .border_style(theme.border(focused))
            .style(theme.panel_alt());
        let inner = block.inner(area);
        frame.render_widget(block, area);
        if inner.is_empty() {
            return;
        }

        let mut projection = self.render_projection();
        projection.set_style(theme.panel_alt());
        projection.set_cursor_line_style(Style::default());
        projection.set_cursor_render_mode(CursorRenderMode::Hidden);
        projection.set_placeholder_text(placeholder);
        projection.set_placeholder_style(theme.muted(false));
        frame.render_widget(&projection, inner);
        if focused && let Some(position) = projection.rendered_cursor_position() {
            frame.set_cursor_position(position);
        }
    }

    fn render_projection(&self) -> TextArea<'static> {
        let cursor_col = self.textarea.cursor().1;
        let display = match self.display {
            SingleLineDisplay::Plain => self.value().to_string(),
            SingleLineDisplay::Redacted => "•".repeat(self.value().chars().count()),
        };
        let mut projection = configured_textarea(display);
        for _ in 0..cursor_col {
            projection.move_cursor(CursorMove::Forward);
        }
        projection
    }

    #[cfg(test)]
    pub(crate) fn cursor(&self) -> usize {
        self.textarea.cursor().1
    }
}

impl Default for SingleLineField {
    fn default() -> Self {
        Self::plain()
    }
}

impl fmt::Debug for SingleLineField {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let value = match self.display {
            SingleLineDisplay::Plain => self.value(),
            SingleLineDisplay::Redacted if self.is_empty() => "<empty>",
            SingleLineDisplay::Redacted => "<redacted>",
        };
        formatter
            .debug_struct("SingleLineField")
            .field("value", &value)
            .field("cursor", &self.textarea.cursor().1)
            .field("display", &self.display)
            .finish()
    }
}

impl PartialEq for SingleLineField {
    fn eq(&self, other: &Self) -> bool {
        self.value() == other.value()
            && self.textarea.cursor() == other.textarea.cursor()
            && self.display == other.display
    }
}

impl PartialEq<&str> for SingleLineField {
    fn eq(&self, other: &&str) -> bool {
        self.value() == *other
    }
}

impl PartialEq<String> for SingleLineField {
    fn eq(&self, other: &String) -> bool {
        self.value() == other
    }
}

impl Eq for SingleLineField {}

fn configured_textarea(value: String) -> TextArea<'static> {
    let mut textarea = TextArea::from([value]);
    textarea.set_wrap_mode(WrapMode::None);
    textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
    textarea
}

fn normalize_single_line(value: &str) -> String {
    let mut normalized = String::with_capacity(value.len());
    let mut previous_was_cr = false;
    for ch in value.chars() {
        match ch {
            '\r' => {
                normalized.push(' ');
                previous_was_cr = true;
            }
            '\n' if previous_was_cr => {
                previous_was_cr = false;
            }
            '\n' | '\t' => {
                normalized.push(' ');
                previous_was_cr = false;
            }
            ch if ch.is_control() => {
                previous_was_cr = false;
            }
            ch => {
                normalized.push(ch);
                previous_was_cr = false;
            }
        }
    }
    normalized
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::{Terminal, backend::TestBackend};

    fn rendered(backend: &TestBackend) -> String {
        backend
            .buffer()
            .content()
            .iter()
            .map(|cell| cell.symbol())
            .collect()
    }

    #[test]
    fn edits_at_beginning_middle_and_end_with_unicode() {
        let mut field = SingleLineField::plain();
        field.insert_str("a界c");
        field.move_left();
        field.insert_char('🙂');
        assert_eq!(field.value(), "a界🙂c");
        assert_eq!(field.cursor(), 3);

        field.move_home();
        field.delete_forward();
        field.move_end();
        field.backspace();
        assert_eq!(field.value(), "界🙂");
        assert_eq!(field.cursor(), 2);
    }

    #[test]
    fn paste_normalization_never_creates_an_extra_line() {
        let mut field = SingleLineField::plain();
        field.insert_str("first\r\nsecond\nthird\rfourth\tend\0");
        assert_eq!(field.value(), "first second third fourth end");
        assert_eq!(field.textarea.lines().len(), 1);
    }

    #[test]
    fn long_wide_value_scrolls_and_keeps_cursor_visible() {
        let mut field = SingleLineField::plain();
        field.insert_str("prefix-界界界界界界界界界界-suffix");
        let backend = TestBackend::new(18, 3);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal
            .draw(|frame| {
                field.render(
                    frame,
                    frame.area(),
                    "Value",
                    "placeholder",
                    true,
                    MissionControlTheme::default(),
                );
            })
            .unwrap();

        let output = rendered(terminal.backend());
        assert!(output.contains("suffix"));
        assert!(!output.contains("prefix"));
    }

    #[test]
    fn redacted_projection_and_debug_never_expose_raw_value() {
        let secret = "https://localhost/callback?code=top-secret";
        let mut field = SingleLineField::redacted();
        field.insert_str(secret);
        field.move_left();
        let backend = TestBackend::new(30, 3);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal
            .draw(|frame| {
                field.render(
                    frame,
                    frame.area(),
                    "OAuth fallback",
                    "paste redirect or code",
                    true,
                    MissionControlTheme::default(),
                );
            })
            .unwrap();

        let output = rendered(terminal.backend());
        assert!(!output.contains(secret));
        assert!(output.contains('•'));
        assert!(!format!("{field:?}").contains("top-secret"));
    }

    #[test]
    fn focused_and_unfocused_styles_are_distinct() {
        let field = SingleLineField::plain();
        let backend = TestBackend::new(24, 6);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal
            .draw(|frame| {
                field.render(
                    frame,
                    Rect::new(0, 0, 24, 3),
                    "Focused",
                    "",
                    true,
                    MissionControlTheme::default(),
                );
                field.render(
                    frame,
                    Rect::new(0, 3, 24, 3),
                    "Unfocused",
                    "",
                    false,
                    MissionControlTheme::default(),
                );
            })
            .unwrap();

        let output = rendered(terminal.backend());
        assert!(output.contains("Focused • editing"));
        assert!(output.contains("Unfocused"));
        assert_ne!(
            terminal.backend().buffer()[(0, 0)].fg,
            terminal.backend().buffer()[(0, 3)].fg
        );
    }
}