Rust · 4735 bytes Raw Blame History
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