| 1 | use anyhow::Result; |
| 2 | use crossterm::{ |
| 3 | cursor::{Hide, MoveTo, Show}, |
| 4 | execute, |
| 5 | style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}, |
| 6 | terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, |
| 7 | }; |
| 8 | use std::io::{stdout, Stdout, Write}; |
| 9 | |
| 10 | use crate::buffer::Buffer; |
| 11 | use crate::editor::Cursor; |
| 12 | |
| 13 | /// Terminal screen renderer |
| 14 | pub struct Screen { |
| 15 | stdout: Stdout, |
| 16 | pub rows: u16, |
| 17 | pub cols: u16, |
| 18 | } |
| 19 | |
| 20 | impl Screen { |
| 21 | pub fn new() -> Result<Self> { |
| 22 | let (cols, rows) = terminal::size()?; |
| 23 | Ok(Self { |
| 24 | stdout: stdout(), |
| 25 | rows, |
| 26 | cols, |
| 27 | }) |
| 28 | } |
| 29 | |
| 30 | pub fn enter_raw_mode(&mut self) -> Result<()> { |
| 31 | terminal::enable_raw_mode()?; |
| 32 | execute!(self.stdout, EnterAlternateScreen, Hide)?; |
| 33 | Ok(()) |
| 34 | } |
| 35 | |
| 36 | pub fn leave_raw_mode(&mut self) -> Result<()> { |
| 37 | execute!(self.stdout, Show, LeaveAlternateScreen)?; |
| 38 | terminal::disable_raw_mode()?; |
| 39 | Ok(()) |
| 40 | } |
| 41 | |
| 42 | pub fn refresh_size(&mut self) -> Result<()> { |
| 43 | let (cols, rows) = terminal::size()?; |
| 44 | self.cols = cols; |
| 45 | self.rows = rows; |
| 46 | Ok(()) |
| 47 | } |
| 48 | |
| 49 | pub fn clear(&mut self) -> Result<()> { |
| 50 | execute!(self.stdout, Clear(ClearType::All))?; |
| 51 | Ok(()) |
| 52 | } |
| 53 | |
| 54 | /// Render the editor view |
| 55 | pub fn render( |
| 56 | &mut self, |
| 57 | buffer: &Buffer, |
| 58 | cursor: &Cursor, |
| 59 | viewport_line: usize, |
| 60 | filename: Option<&str>, |
| 61 | ) -> Result<()> { |
| 62 | let line_num_width = self.line_number_width(buffer.line_count()); |
| 63 | let text_cols = self.cols as usize - line_num_width - 1; // -1 for separator |
| 64 | |
| 65 | // Reserve 1 row for status bar |
| 66 | let text_rows = self.rows.saturating_sub(1) as usize; |
| 67 | |
| 68 | // Draw text area |
| 69 | for row in 0..text_rows { |
| 70 | let line_idx = viewport_line + row; |
| 71 | execute!(self.stdout, MoveTo(0, row as u16))?; |
| 72 | |
| 73 | if line_idx < buffer.line_count() { |
| 74 | // Line number |
| 75 | execute!( |
| 76 | self.stdout, |
| 77 | SetForegroundColor(Color::DarkGrey), |
| 78 | Print(format!("{:>width$} ", line_idx + 1, width = line_num_width)), |
| 79 | ResetColor |
| 80 | )?; |
| 81 | |
| 82 | // Line content |
| 83 | if let Some(line) = buffer.line_str(line_idx) { |
| 84 | let visible: String = line.chars().take(text_cols).collect(); |
| 85 | execute!(self.stdout, Print(&visible))?; |
| 86 | } |
| 87 | } else { |
| 88 | // Empty line indicator |
| 89 | execute!( |
| 90 | self.stdout, |
| 91 | SetForegroundColor(Color::DarkBlue), |
| 92 | Print(format!("{:>width$} ", "~", width = line_num_width)), |
| 93 | ResetColor |
| 94 | )?; |
| 95 | } |
| 96 | |
| 97 | // Clear rest of line |
| 98 | execute!(self.stdout, Clear(ClearType::UntilNewLine))?; |
| 99 | } |
| 100 | |
| 101 | // Status bar |
| 102 | self.render_status_bar(buffer, cursor, filename)?; |
| 103 | |
| 104 | // Position cursor |
| 105 | let cursor_row = cursor.line.saturating_sub(viewport_line); |
| 106 | let cursor_col = line_num_width + 1 + cursor.col; |
| 107 | execute!( |
| 108 | self.stdout, |
| 109 | MoveTo(cursor_col as u16, cursor_row as u16), |
| 110 | Show |
| 111 | )?; |
| 112 | |
| 113 | self.stdout.flush()?; |
| 114 | Ok(()) |
| 115 | } |
| 116 | |
| 117 | fn render_status_bar( |
| 118 | &mut self, |
| 119 | buffer: &Buffer, |
| 120 | cursor: &Cursor, |
| 121 | filename: Option<&str>, |
| 122 | ) -> Result<()> { |
| 123 | let status_row = self.rows.saturating_sub(1); |
| 124 | execute!(self.stdout, MoveTo(0, status_row))?; |
| 125 | |
| 126 | // Status bar background |
| 127 | execute!( |
| 128 | self.stdout, |
| 129 | SetBackgroundColor(Color::DarkGrey), |
| 130 | SetForegroundColor(Color::White) |
| 131 | )?; |
| 132 | |
| 133 | // Left side: filename + modified indicator |
| 134 | let name = filename.unwrap_or("[No Name]"); |
| 135 | let modified = if buffer.modified { " [+]" } else { "" }; |
| 136 | let left = format!(" {}{}", name, modified); |
| 137 | |
| 138 | // Right side: position |
| 139 | let right = format!(" Ln {}, Col {} ", cursor.line + 1, cursor.col + 1); |
| 140 | |
| 141 | // Pad middle |
| 142 | let padding = self.cols as usize - left.len() - right.len(); |
| 143 | let middle = " ".repeat(padding.max(0)); |
| 144 | |
| 145 | execute!( |
| 146 | self.stdout, |
| 147 | Print(&left), |
| 148 | Print(&middle), |
| 149 | Print(&right), |
| 150 | ResetColor |
| 151 | )?; |
| 152 | |
| 153 | Ok(()) |
| 154 | } |
| 155 | |
| 156 | fn line_number_width(&self, line_count: usize) -> usize { |
| 157 | let digits = if line_count == 0 { |
| 158 | 1 |
| 159 | } else { |
| 160 | (line_count as f64).log10().floor() as usize + 1 |
| 161 | }; |
| 162 | digits.max(3) // Minimum 3 characters |
| 163 | } |
| 164 | } |
| 165 |