nanorust 0.1.0

A terminal-based text editor written in Rust, inspired by GNU nano
Documentation
use crate::error::{EditorError, Result};
use std::io::{self, Read};
use std::time::Duration;

/// Direction for arrow keys
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

/// Key enum representing all possible key inputs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Key {
    Char(char),
    Ctrl(char),
    Alt(char),
    Arrow(Direction),
    PageUp,
    PageDown,
    Home,
    End,
    Delete,
    Backspace,
    Enter,
    Tab,
    Escape,
    Resize,
}

/// Read a single key from stdin
pub fn read_key() -> Result<Key> {
    let mut stdin = io::stdin();
    let mut buffer = [0u8; 1];

    // Read first byte
    stdin.read_exact(&mut buffer)?;
    let first_byte = buffer[0];

    match first_byte {
        // Tab (must come before Ctrl range since 0x09 is in 0x01-0x1A)
        b'\t' => Ok(Key::Tab),
        // Backspace (0x08 is in Ctrl range, handle it first)
        0x08 => Ok(Key::Backspace),
        // Enter/Return (0x0D is Ctrl+M, handle it first)
        b'\r' | b'\n' => Ok(Key::Enter),
        // Ctrl+key combinations (0x01-0x1A maps to Ctrl+A through Ctrl+Z)
        // Note: Some values are handled above (Tab=0x09, Backspace=0x08, Enter=0x0D)
        0x01..=0x1A => {
            // Map control codes to lowercase letters
            // 0x01 = Ctrl+A, 0x02 = Ctrl+B, ..., 0x1A = Ctrl+Z
            let ch = (first_byte - 1 + b'a') as char;
            Ok(Key::Ctrl(ch))
        }
        // Ctrl+\ (0x1C) - used for replace
        0x1C => Ok(Key::Ctrl('\\')),
        // Ctrl+^ (0x1E) - used for marking text
        0x1E => Ok(Key::Ctrl('^')),
        // Ctrl+_ (0x1F) - used for go to line
        0x1F => Ok(Key::Ctrl('_')),
        // Escape sequences
        0x1B => {
            // Try to read next byte with timeout
            let mut seq_buffer = [0u8; 2];
            
            // Attempt to read the next byte
            match stdin.read(&mut seq_buffer[0..1]) {
                Ok(0) | Err(_) => {
                    // No more bytes, just Escape key
                    return Ok(Key::Escape);
                }
                Ok(_) => {}
            }

            match seq_buffer[0] {
                b'[' => {
                    // CSI sequence - read next byte
                    if stdin.read_exact(&mut seq_buffer[1..2]).is_err() {
                        return Ok(Key::Escape);
                    }

                    match seq_buffer[1] {
                        // Arrow keys
                        b'A' => Ok(Key::Arrow(Direction::Up)),
                        b'B' => Ok(Key::Arrow(Direction::Down)),
                        b'C' => Ok(Key::Arrow(Direction::Right)),
                        b'D' => Ok(Key::Arrow(Direction::Left)),
                        // Home key
                        b'H' => Ok(Key::Home),
                        // End key
                        b'F' => Ok(Key::End),
                        // Extended sequences
                        b'1'..=b'9' => {
                            // Read the tilde terminator
                            let mut tilde = [0u8; 1];
                            if stdin.read_exact(&mut tilde).is_ok() && tilde[0] == b'~' {
                                match seq_buffer[1] {
                                    b'1' => Ok(Key::Home),
                                    b'3' => Ok(Key::Delete),
                                    b'4' => Ok(Key::End),
                                    b'5' => Ok(Key::PageUp),
                                    b'6' => Ok(Key::PageDown),
                                    b'7' => Ok(Key::Home),
                                    b'8' => Ok(Key::End),
                                    _ => Ok(Key::Escape),
                                }
                            } else {
                                Ok(Key::Escape)
                            }
                        }
                        _ => Ok(Key::Escape),
                    }
                }
                b'O' => {
                    // SS3 sequence - read next byte
                    if stdin.read_exact(&mut seq_buffer[1..2]).is_err() {
                        return Ok(Key::Escape);
                    }

                    match seq_buffer[1] {
                        b'H' => Ok(Key::Home),
                        b'F' => Ok(Key::End),
                        _ => Ok(Key::Escape),
                    }
                }
                // Alt+key combinations
                // Alt is represented as ESC followed by the key
                // Handle special characters first
                b'{' => Ok(Key::Alt('{')),  // Alt+{ for unindent
                b'}' => Ok(Key::Alt('}')),  // Alt+} for indent
                b'6' => Ok(Key::Alt('6')),  // Alt+6 for copy
                // Then handle alphanumeric
                ch @ (b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') => {
                    Ok(Key::Alt(ch as char))
                }
                // Other punctuation
                ch if ch.is_ascii_punctuation() => {
                    Ok(Key::Alt(ch as char))
                }
                _ => Ok(Key::Escape),
            }
        }
        // Backspace (0x7F - DEL character)
        0x7F => Ok(Key::Backspace),
        // Regular printable characters
        ch if ch >= 32 && ch < 127 => Ok(Key::Char(ch as char)),
        // UTF-8 multi-byte characters
        ch if ch >= 128 => {
            let mut utf8_buffer = vec![ch];
            
            // Determine how many more bytes we need
            let additional_bytes = if ch & 0b1110_0000 == 0b1100_0000 {
                1
            } else if ch & 0b1111_0000 == 0b1110_0000 {
                2
            } else if ch & 0b1111_1000 == 0b1111_0000 {
                3
            } else {
                return Err(EditorError::InvalidUtf8);
            };

            // Read additional bytes
            for _ in 0..additional_bytes {
                let mut byte = [0u8; 1];
                stdin.read_exact(&mut byte)?;
                utf8_buffer.push(byte[0]);
            }

            // Convert to string
            let s = String::from_utf8(utf8_buffer)
                .map_err(|_| EditorError::InvalidUtf8)?;
            
            if let Some(ch) = s.chars().next() {
                Ok(Key::Char(ch))
            } else {
                Err(EditorError::InvalidUtf8)
            }
        }
        _ => Ok(Key::Escape),
    }
}

/// Read a line of text from stdin (for prompts)
/// This function reads keys until Enter is pressed
pub fn read_line(prompt: &str) -> Result<String> {
    use std::io::Write;
    
    print!("{}", prompt);
    io::stdout().flush()?;
    
    let mut line = String::new();
    
    loop {
        let key = read_key()?;
        
        match key {
            Key::Enter => break,
            Key::Char(ch) => line.push(ch),
            Key::Backspace => {
                line.pop();
            }
            Key::Ctrl(ch) if ch == 'c' || ch == 'g' => {
                // Ctrl+C or Ctrl+G cancels input
                return Err(EditorError::ParseError("Input cancelled".to_string()));
            }
            _ => {} // Ignore other keys
        }
    }
    
    Ok(line)
}

/// Check if a resize event is available and read it
/// Returns Some(Key::Resize) if a resize event occurred, None otherwise
pub fn poll_resize() -> Result<Option<Key>> {
    use crossterm::event::{poll, read, Event};
    
    // Poll with zero timeout to check if event is available
    if poll(Duration::from_millis(0))? {
        match read()? {
            Event::Resize(_, _) => Ok(Some(Key::Resize)),
            _ => Ok(None),
        }
    } else {
        Ok(None)
    }
}

/// Read a key or resize event (blocking)
/// This is an alternative to read_key() that also handles resize events
pub fn read_event() -> Result<Key> {
    use crossterm::event::{poll, read, Event};
    
    // First check for any pending events with a short timeout
    if poll(Duration::from_millis(10))? {
        match read()? {
            Event::Resize(_, _) => return Ok(Key::Resize),
            _ => {
                // Fall through to read_key for actual key input
            }
        }
    }
    
    // Read key input normally
    read_key()
}