Rust · 12524 bytes Raw Blame History
1 use anyhow::Result;
2 use crossterm::{
3 cursor::{Hide, MoveTo, Show},
4 event::{
5 DisableMouseCapture, EnableMouseCapture,
6 KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
7 },
8 execute,
9 style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
10 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
11 };
12 use std::io::{stdout, Stdout, Write};
13
14 use crate::buffer::Buffer;
15 use crate::editor::{Cursors, Position};
16
17 // Editor color scheme (256-color palette)
18 const BG_COLOR: Color = Color::AnsiValue(234); // Off-black editor background
19 const CURRENT_LINE_BG: Color = Color::AnsiValue(236); // Slightly lighter for current line
20 const LINE_NUM_COLOR: Color = Color::AnsiValue(243); // Gray for line numbers
21 const CURRENT_LINE_NUM_COLOR: Color = Color::Yellow; // Yellow for active line number
22 const BRACKET_MATCH_BG: Color = Color::AnsiValue(240); // Highlight for matching brackets
23 // Secondary cursors use Color::Magenta for visibility
24
25 /// Terminal screen renderer
26 pub struct Screen {
27 stdout: Stdout,
28 pub rows: u16,
29 pub cols: u16,
30 keyboard_enhanced: bool,
31 }
32
33 impl Screen {
34 pub fn new() -> Result<Self> {
35 let (cols, rows) = terminal::size()?;
36 Ok(Self {
37 stdout: stdout(),
38 rows,
39 cols,
40 keyboard_enhanced: false,
41 })
42 }
43
44 pub fn enter_raw_mode(&mut self) -> Result<()> {
45 terminal::enable_raw_mode()?;
46 execute!(self.stdout, EnterAlternateScreen, Hide, EnableMouseCapture)?;
47
48 // Try to enable keyboard enhancement for better modifier key detection
49 // This enables the kitty keyboard protocol on supporting terminals
50 if execute!(
51 self.stdout,
52 PushKeyboardEnhancementFlags(
53 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
54 | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
55 )
56 )
57 .is_ok()
58 {
59 self.keyboard_enhanced = true;
60 }
61
62 Ok(())
63 }
64
65 pub fn leave_raw_mode(&mut self) -> Result<()> {
66 if self.keyboard_enhanced {
67 let _ = execute!(self.stdout, PopKeyboardEnhancementFlags);
68 }
69 execute!(self.stdout, Show, DisableMouseCapture, LeaveAlternateScreen)?;
70 terminal::disable_raw_mode()?;
71 Ok(())
72 }
73
74 pub fn refresh_size(&mut self) -> Result<()> {
75 let (cols, rows) = terminal::size()?;
76 self.cols = cols;
77 self.rows = rows;
78 Ok(())
79 }
80
81 #[allow(dead_code)]
82 pub fn clear(&mut self) -> Result<()> {
83 execute!(self.stdout, Clear(ClearType::All))?;
84 Ok(())
85 }
86
87 /// Render the editor view
88 pub fn render(
89 &mut self,
90 buffer: &Buffer,
91 cursors: &Cursors,
92 viewport_line: usize,
93 filename: Option<&str>,
94 message: Option<&str>,
95 bracket_match: Option<(usize, usize)>,
96 ) -> Result<()> {
97 // Hide cursor during render to prevent flicker
98 execute!(self.stdout, Hide)?;
99
100 let line_num_width = self.line_number_width(buffer.line_count());
101 let text_cols = self.cols as usize - line_num_width - 1;
102
103 // Get primary cursor for current line highlighting
104 let primary = cursors.primary();
105
106 // Collect all selections from all cursors
107 let selections: Vec<(Position, Position)> = cursors.all()
108 .iter()
109 .filter_map(|c| c.selection_bounds())
110 .collect();
111
112 // Collect all cursor positions for rendering
113 let primary_idx = cursors.primary_index();
114 let cursor_positions: Vec<(usize, usize, bool)> = cursors.all()
115 .iter()
116 .enumerate()
117 .map(|(i, c)| (c.line, c.col, i == primary_idx)) // (line, col, is_primary)
118 .collect();
119
120 // Reserve 1 row for status bar
121 let text_rows = self.rows.saturating_sub(1) as usize;
122
123 // Draw text area
124 for row in 0..text_rows {
125 let line_idx = viewport_line + row;
126 let is_current_line = line_idx == primary.line;
127 execute!(self.stdout, MoveTo(0, row as u16))?;
128
129 if line_idx < buffer.line_count() {
130 // Line number with appropriate color
131 let line_num_fg = if is_current_line {
132 CURRENT_LINE_NUM_COLOR
133 } else {
134 LINE_NUM_COLOR
135 };
136 let line_bg = if is_current_line { CURRENT_LINE_BG } else { BG_COLOR };
137
138 execute!(
139 self.stdout,
140 SetBackgroundColor(line_bg),
141 SetForegroundColor(line_num_fg),
142 Print(format!("{:>width$} ", line_idx + 1, width = line_num_width)),
143 )?;
144
145 // Line content with selection and cursor highlighting
146 if let Some(line) = buffer.line_str(line_idx) {
147 // Check if bracket match is on this line
148 let bracket_col = bracket_match
149 .filter(|(bl, _)| *bl == line_idx)
150 .map(|(_, bc)| bc);
151
152 // Get cursors on this line (excluding primary which uses hardware cursor)
153 let secondary_cursors: Vec<usize> = cursor_positions.iter()
154 .filter(|(l, _, is_primary)| *l == line_idx && !*is_primary)
155 .map(|(_, c, _)| *c)
156 .collect();
157
158 self.render_line_with_cursors(
159 &line,
160 line_idx,
161 text_cols,
162 &selections,
163 is_current_line,
164 bracket_col,
165 &secondary_cursors,
166 )?;
167 }
168
169 // Fill rest of line with background color
170 execute!(
171 self.stdout,
172 SetBackgroundColor(line_bg),
173 Clear(ClearType::UntilNewLine),
174 ResetColor
175 )?;
176 } else {
177 // Empty line indicator
178 execute!(
179 self.stdout,
180 SetBackgroundColor(BG_COLOR),
181 SetForegroundColor(Color::DarkBlue),
182 Print(format!("{:>width$} ", "~", width = line_num_width)),
183 Clear(ClearType::UntilNewLine),
184 ResetColor
185 )?;
186 }
187 }
188
189 // Status bar
190 self.render_status_bar(buffer, cursors, filename, message)?;
191
192 // Position hardware cursor at primary cursor
193 let cursor_row = primary.line.saturating_sub(viewport_line);
194 let cursor_col = line_num_width + 1 + primary.col;
195 execute!(
196 self.stdout,
197 MoveTo(cursor_col as u16, cursor_row as u16),
198 Show
199 )?;
200
201 self.stdout.flush()?;
202 Ok(())
203 }
204
205 fn render_line_with_cursors(
206 &mut self,
207 line: &str,
208 line_idx: usize,
209 max_cols: usize,
210 selections: &[(Position, Position)],
211 is_current_line: bool,
212 bracket_col: Option<usize>,
213 secondary_cursors: &[usize],
214 ) -> Result<()> {
215 let chars: Vec<char> = line.chars().take(max_cols).collect();
216 let line_bg = if is_current_line { CURRENT_LINE_BG } else { BG_COLOR };
217 let default_fg = Color::Reset; // Default terminal foreground
218
219 // Build selection ranges for this line from all selections
220 let mut sel_ranges: Vec<(usize, usize)> = Vec::new();
221 for (start, end) in selections {
222 if line_idx >= start.line && line_idx <= end.line {
223 let s = if line_idx == start.line { start.col } else { 0 };
224 let e = if line_idx == end.line { end.col } else { chars.len() };
225 if s < e {
226 sel_ranges.push((s, e));
227 }
228 }
229 }
230
231 // Render character by character for precise highlighting
232 for (col, ch) in chars.iter().enumerate() {
233 let in_selection = sel_ranges.iter().any(|(s, e)| col >= *s && col < *e);
234 let is_bracket_match = bracket_col == Some(col);
235 let is_secondary_cursor = secondary_cursors.contains(&col);
236
237 // Determine background color
238 let bg = if in_selection {
239 Color::Blue
240 } else if is_secondary_cursor {
241 Color::Magenta // Use magenta for better visibility
242 } else if is_bracket_match {
243 BRACKET_MATCH_BG
244 } else {
245 line_bg
246 };
247
248 // Determine foreground color
249 let fg = if in_selection {
250 Color::White
251 } else if is_secondary_cursor {
252 Color::White // White text on magenta bg
253 } else {
254 default_fg
255 };
256
257 execute!(
258 self.stdout,
259 SetBackgroundColor(bg),
260 SetForegroundColor(fg),
261 Print(ch)
262 )?;
263 }
264
265 // Reset to line background for rest of line
266 execute!(self.stdout, SetBackgroundColor(line_bg), SetForegroundColor(default_fg))?;
267
268 // Handle secondary cursors at end of line (past text content)
269 // Find the rightmost secondary cursor past text
270 let max_cursor_past_text = secondary_cursors.iter()
271 .filter(|&&c| c >= chars.len())
272 .max()
273 .copied();
274
275 if let Some(max_cursor) = max_cursor_past_text {
276 if max_cursor < max_cols {
277 // Fill spaces up to and including the cursor positions
278 for col in chars.len()..=max_cursor {
279 if secondary_cursors.contains(&col) {
280 execute!(
281 self.stdout,
282 SetBackgroundColor(Color::Magenta),
283 SetForegroundColor(Color::White),
284 Print(" ")
285 )?;
286 } else {
287 execute!(
288 self.stdout,
289 SetBackgroundColor(line_bg),
290 Print(" ")
291 )?;
292 }
293 }
294 // Reset for the rest of the line
295 execute!(self.stdout, SetBackgroundColor(line_bg), SetForegroundColor(default_fg))?;
296 }
297 }
298
299 Ok(())
300 }
301
302 fn render_status_bar(
303 &mut self,
304 buffer: &Buffer,
305 cursors: &Cursors,
306 filename: Option<&str>,
307 message: Option<&str>,
308 ) -> Result<()> {
309 let status_row = self.rows.saturating_sub(1);
310 execute!(self.stdout, MoveTo(0, status_row))?;
311
312 // Status bar background
313 execute!(
314 self.stdout,
315 SetBackgroundColor(Color::DarkGrey),
316 SetForegroundColor(Color::White)
317 )?;
318
319 // Left side: filename + modified indicator + cursor count
320 let name = filename.unwrap_or("[No Name]");
321 let modified = if buffer.modified { " [+]" } else { "" };
322 let cursor_count = if cursors.len() > 1 {
323 format!(" ({} cursors)", cursors.len())
324 } else {
325 String::new()
326 };
327 let left = format!(" {}{}{}", name, modified, cursor_count);
328
329 // Right side: position (and message if any)
330 let primary = cursors.primary();
331 let pos = format!("Ln {}, Col {}", primary.line + 1, primary.col + 1);
332 let right = if let Some(msg) = message {
333 format!(" {} | {} ", msg, pos)
334 } else {
335 format!(" {} ", pos)
336 };
337
338 // Pad middle
339 let padding = (self.cols as usize).saturating_sub(left.len() + right.len());
340 let middle = " ".repeat(padding);
341
342 execute!(
343 self.stdout,
344 Print(&left),
345 Print(&middle),
346 Print(&right),
347 ResetColor
348 )?;
349
350 Ok(())
351 }
352
353 fn line_number_width(&self, line_count: usize) -> usize {
354 let digits = if line_count == 0 {
355 1
356 } else {
357 (line_count as f64).log10().floor() as usize + 1
358 };
359 digits.max(3) // Minimum 3 characters
360 }
361 }
362