⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
text_editor.rs

1//! GUI Text Editor Application
2//!
3//! Simple text editor with basic editing capabilities.
4
5// Phase 6 (desktop) -- editor fields and methods are defined but
6// rendering is not yet connected to the compositor.
7
8use alloc::{format, string::String, vec, vec::Vec};
9
10use spin::RwLock;
11
12use crate::{
13    desktop::window_manager::{with_window_manager, InputEvent, WindowId},
14    error::KernelError,
15    fs::{get_vfs, OpenFlags},
16    sync::once_lock::GlobalState,
17};
18
19/// Text buffer line
20type Line = Vec<char>;
21
22/// Text editor state
23pub struct TextEditor {
24    /// Window ID
25    window_id: WindowId,
26
27    /// Compositor surface ID
28    surface_id: u32,
29    /// SHM pool ID
30    pool_id: u32,
31    /// Pool buffer ID
32    pool_buf_id: u32,
33
34    /// File path (None if new file)
35    file_path: Option<String>,
36
37    /// Text buffer (lines of characters)
38    buffer: Vec<Line>,
39
40    /// Cursor position (line, column)
41    cursor_line: usize,
42    cursor_col: usize,
43
44    /// Scroll offset (top line visible)
45    scroll_line: usize,
46
47    /// Modified flag
48    modified: bool,
49
50    /// Window dimensions
51    width: u32,
52    height: u32,
53
54    /// Visible rows (used by render to limit line display)
55    #[allow(dead_code)] // Computed for future scroll-window rendering
56    visible_rows: usize,
57
58    /// Visible columns
59    #[allow(dead_code)] // Computed for future horizontal scroll rendering
60    visible_cols: usize,
61}
62
63impl TextEditor {
64    /// Create a new text editor
65    pub fn new(file_path: Option<String>) -> Result<Self, KernelError> {
66        let width = 800;
67        let height = 600;
68
69        // WM window includes title bar (28px) above content area
70        let title_bar_h = 28u32;
71        let window_id =
72            with_window_manager(|wm| wm.create_window(150, 80, width, height + title_bar_h, 0))
73                .ok_or(KernelError::InvalidState {
74                expected: "initialized",
75                actual: "uninitialized",
76            })??;
77
78        // Compositor surface covers the full window area (title bar + content)
79        let (surface_id, pool_id, pool_buf_id) =
80            super::renderer::create_app_surface(150, 80, width, height + title_bar_h);
81
82        // Calculate visible area
83        let char_width = 8;
84        let char_height = 16;
85        let visible_cols = (width as usize) / char_width;
86        let visible_rows = ((height as usize) - 24) / char_height; // -24 for status bar
87
88        let mut editor = Self {
89            window_id,
90            surface_id,
91            pool_id,
92            pool_buf_id,
93            file_path: file_path.clone(),
94            buffer: vec![Vec::new()], // Start with one empty line
95            cursor_line: 0,
96            cursor_col: 0,
97            scroll_line: 0,
98            modified: false,
99            width,
100            height,
101            visible_rows,
102            visible_cols,
103        };
104
105        // Load file if specified
106        if let Some(ref path) = file_path {
107            editor.load_file(path)?;
108        }
109
110        println!("[TEXT-EDITOR] Created editor window {}", window_id);
111
112        Ok(editor)
113    }
114
115    /// Load file from filesystem
116    pub fn load_file(&mut self, path: &str) -> Result<(), KernelError> {
117        println!("[TEXT-EDITOR] Loading file: {}", path);
118
119        let vfs = get_vfs();
120
121        // Open file
122        match vfs.read().open(path, OpenFlags::read_only()) {
123            Ok(node) => {
124                // Read file
125                let metadata = node.metadata().map_err(|_| KernelError::InvalidArgument {
126                    name: "file_metadata",
127                    value: "failed_to_read",
128                })?;
129                let mut file_buffer = vec![0u8; metadata.size];
130
131                match node.read(0, &mut file_buffer) {
132                    Ok(_bytes_read) => {
133                        // Parse content into lines
134                        self.buffer.clear();
135                        let content = core::str::from_utf8(&file_buffer).map_err(|_| {
136                            KernelError::InvalidArgument {
137                                name: "file_content",
138                                value: "invalid_utf8",
139                            }
140                        })?;
141
142                        for line in content.lines() {
143                            self.buffer.push(line.chars().collect());
144                        }
145
146                        if self.buffer.is_empty() {
147                            self.buffer.push(Vec::new());
148                        }
149
150                        self.modified = false;
151                        println!("[TEXT-EDITOR] Loaded {} lines", self.buffer.len());
152                    }
153                    Err(_e) => {
154                        println!("[TEXT-EDITOR] Failed to read file");
155                        return Err(KernelError::InvalidArgument {
156                            name: "file_read",
157                            value: "failed",
158                        });
159                    }
160                }
161            }
162            Err(_e) => {
163                println!("[TEXT-EDITOR] Failed to open file");
164                return Err(KernelError::InvalidArgument {
165                    name: "file_open",
166                    value: "failed",
167                });
168            }
169        }
170
171        Ok(())
172    }
173
174    /// Save file to filesystem
175    pub fn save_file(&mut self) -> Result<(), KernelError> {
176        let path = self
177            .file_path
178            .as_ref()
179            .ok_or(KernelError::InvalidArgument {
180                name: "file_path",
181                value: "no_path_specified",
182            })?;
183
184        println!("[TEXT-EDITOR] Saving file: {}", path);
185
186        // Convert buffer to bytes
187        let mut content = String::new();
188        for line in &self.buffer {
189            for &ch in line {
190                content.push(ch);
191            }
192            content.push('\n');
193        }
194
195        let bytes = content.as_bytes();
196
197        // Write to filesystem
198        let vfs = get_vfs();
199
200        // First check if file exists, otherwise create it
201        match vfs.read().open(path, OpenFlags::read_only()) {
202            Ok(node) => {
203                // File exists, write to it
204                node.write(0, bytes)
205                    .map_err(|_| KernelError::InvalidArgument {
206                        name: "file_write",
207                        value: "failed",
208                    })?;
209                self.modified = false;
210                println!("[TEXT-EDITOR] File saved ({} bytes)", bytes.len());
211                Ok(())
212            }
213            Err(_) => {
214                // File doesn't exist, need to create it
215                // For now, return an error since we need parent directory
216                println!("[TEXT-EDITOR] Failed to save file: file does not exist");
217                Err(KernelError::InvalidArgument {
218                    name: "file_save",
219                    value: "file_not_found",
220                })
221            }
222        }
223    }
224
225    /// Process input event
226    pub fn process_input(&mut self, event: InputEvent) -> Result<(), KernelError> {
227        if let InputEvent::KeyPress {
228            character,
229            scancode,
230        } = event
231        {
232            match character {
233                '\n' | '\r' => {
234                    // Insert newline
235                    self.insert_newline();
236                }
237                '\x08' => {
238                    // Backspace
239                    self.delete_char();
240                }
241                '\x13' => {
242                    // Ctrl+S: Save file
243                    if let Err(e) = self.save_file() {
244                        crate::println!("[EDITOR] Save failed: {:?}", e);
245                    }
246                }
247                '\x0F' => {
248                    // Ctrl+O: Open file (stub -- would need file dialog)
249                    crate::println!("[EDITOR] Open file: use file manager to open files");
250                }
251                '\x0E' => {
252                    // Ctrl+N: New file
253                    self.buffer.clear();
254                    self.buffer.push(alloc::vec::Vec::new());
255                    self.cursor_line = 0;
256                    self.cursor_col = 0;
257                    self.scroll_line = 0;
258                    self.file_path = None;
259                    self.modified = false;
260                }
261                '\t' => {
262                    // Tab - insert 4 spaces
263                    for _ in 0..4 {
264                        self.insert_char(' ');
265                    }
266                }
267                ch if (' '..='~').contains(&ch) => {
268                    // Printable character
269                    self.insert_char(ch);
270                }
271                _ => {
272                    // Handle special keys via scancode
273                    match scancode {
274                        72 => self.move_cursor_up(),    // Up arrow
275                        80 => self.move_cursor_down(),  // Down arrow
276                        75 => self.move_cursor_left(),  // Left arrow
277                        77 => self.move_cursor_right(), // Right arrow
278                        _ => {}
279                    }
280                }
281            }
282        }
283
284        Ok(())
285    }
286
287    /// Insert character at cursor
288    fn insert_char(&mut self, ch: char) {
289        if self.cursor_line < self.buffer.len() {
290            self.buffer[self.cursor_line].insert(self.cursor_col, ch);
291            self.cursor_col += 1;
292            self.modified = true;
293        }
294    }
295
296    /// Delete character before cursor
297    fn delete_char(&mut self) {
298        if self.cursor_col > 0 {
299            self.buffer[self.cursor_line].remove(self.cursor_col - 1);
300            self.cursor_col -= 1;
301            self.modified = true;
302        } else if self.cursor_line > 0 {
303            // Join with previous line
304            let current_line = self.buffer.remove(self.cursor_line);
305            self.cursor_line -= 1;
306            self.cursor_col = self.buffer[self.cursor_line].len();
307            self.buffer[self.cursor_line].extend(current_line);
308            self.modified = true;
309        }
310    }
311
312    /// Insert newline at cursor
313    fn insert_newline(&mut self) {
314        if self.cursor_line < self.buffer.len() {
315            let rest = self.buffer[self.cursor_line].split_off(self.cursor_col);
316            self.cursor_line += 1;
317            self.buffer.insert(self.cursor_line, rest);
318            self.cursor_col = 0;
319            self.modified = true;
320        }
321    }
322
323    /// Move cursor up
324    fn move_cursor_up(&mut self) {
325        if self.cursor_line > 0 {
326            self.cursor_line -= 1;
327            self.cursor_col = self.cursor_col.min(self.buffer[self.cursor_line].len());
328        }
329    }
330
331    /// Move cursor down
332    fn move_cursor_down(&mut self) {
333        if self.cursor_line < self.buffer.len() - 1 {
334            self.cursor_line += 1;
335            self.cursor_col = self.cursor_col.min(self.buffer[self.cursor_line].len());
336        }
337    }
338
339    /// Move cursor left
340    fn move_cursor_left(&mut self) {
341        if self.cursor_col > 0 {
342            self.cursor_col -= 1;
343        } else if self.cursor_line > 0 {
344            self.cursor_line -= 1;
345            self.cursor_col = self.buffer[self.cursor_line].len();
346        }
347    }
348
349    /// Move cursor right
350    fn move_cursor_right(&mut self) {
351        if self.cursor_col < self.buffer[self.cursor_line].len() {
352            self.cursor_col += 1;
353        } else if self.cursor_line < self.buffer.len() - 1 {
354            self.cursor_line += 1;
355            self.cursor_col = 0;
356        }
357    }
358
359    /// Render text editor to a BGRA pixel buffer.
360    ///
361    /// `buf` is width*height*4 bytes in BGRA format.
362    pub fn render(&self, buf: &mut [u8], width: usize, height: usize) -> Result<(), KernelError> {
363        use super::renderer::{draw_char_into_buffer, draw_string_into_buffer};
364
365        let char_h = 16;
366
367        // Clear to very dark gray (BGRA: 0x1E1E1E -- VS Code-like)
368        for chunk in buf.chunks_exact_mut(4) {
369            chunk[0] = 0x1E; // B
370            chunk[1] = 0x1E; // G
371            chunk[2] = 0x1E; // R
372            chunk[3] = 0xFF; // A
373        }
374
375        // Status bar at top (dark blue-gray background)
376        for x in 0..width {
377            for dy in 0..20 {
378                let offset = (dy * width + x) * 4;
379                if offset + 3 < buf.len() {
380                    buf[offset] = 0x40; // B
381                    buf[offset + 1] = 0x30; // G
382                    buf[offset + 2] = 0x25; // R
383                    buf[offset + 3] = 0xFF;
384                }
385            }
386        }
387
388        // Build status text
389        let status = if let Some(ref path) = self.file_path {
390            if self.modified {
391                format!(
392                    "{}* L{} C{}",
393                    path,
394                    self.cursor_line + 1,
395                    self.cursor_col + 1
396                )
397            } else {
398                format!(
399                    "{} L{} C{}",
400                    path,
401                    self.cursor_line + 1,
402                    self.cursor_col + 1
403                )
404            }
405        } else {
406            format!(
407                "[New File] L{} C{}",
408                self.cursor_line + 1,
409                self.cursor_col + 1
410            )
411        };
412        draw_string_into_buffer(buf, width, status.as_bytes(), 6, 2, 0xCCCCCC);
413
414        // Render text lines
415        let text_y_start = 24;
416        let max_visible = (height - text_y_start) / char_h;
417
418        for (i, line) in self
419            .buffer
420            .iter()
421            .enumerate()
422            .skip(self.scroll_line)
423            .take(max_visible)
424        {
425            let row = i - self.scroll_line;
426            let y = text_y_start + row * char_h;
427
428            // Draw line number (dim)
429            let line_num = i + 1;
430            let num_str = format!("{:>4} ", line_num);
431            draw_string_into_buffer(buf, width, num_str.as_bytes(), 0, y, 0x606060);
432
433            // Draw text content
434            let text_x = 5 * 8; // After line number
435            for (j, &ch) in line.iter().enumerate() {
436                if ch as u32 >= 0x20 && (ch as u32) <= 0x7E {
437                    draw_char_into_buffer(buf, width, ch as u8, text_x + j * 8, y, 0xD4D4D4);
438                }
439            }
440
441            // Draw cursor on this line
442            if i == self.cursor_line {
443                let cursor_px = text_x + self.cursor_col * 8;
444                for dy in 0..char_h {
445                    for dx in 0..2 {
446                        let offset = ((y + dy) * width + cursor_px + dx) * 4;
447                        if offset + 3 < buf.len() {
448                            buf[offset] = 0xFF; // B
449                            buf[offset + 1] = 0xFF; // G
450                            buf[offset + 2] = 0xFF; // R
451                            buf[offset + 3] = 0xFF;
452                        }
453                    }
454                }
455            }
456        }
457
458        // Bottom status line background
459        let status_y = height.saturating_sub(20);
460        for x in 0..width {
461            for dy in 0..20 {
462                let offset = ((status_y + dy) * width + x) * 4;
463                if offset + 3 < buf.len() {
464                    buf[offset] = 0x50; // B
465                    buf[offset + 1] = 0x40; // G
466                    buf[offset + 2] = 0x30; // R
467                    buf[offset + 3] = 0xFF;
468                }
469            }
470        }
471        // Bottom status text: shortcuts hint + cursor position
472        let mod_indicator = if self.modified { "*" } else { "" };
473        let file_name = self.file_path.as_deref().unwrap_or("[New File]");
474        let bottom_status = format!(
475            " {}{} | Ln {}, Col {} | Ctrl+S Save  Ctrl+N New",
476            file_name,
477            mod_indicator,
478            self.cursor_line + 1,
479            self.cursor_col + 1,
480        );
481        draw_string_into_buffer(
482            buf,
483            width,
484            bottom_status.as_bytes(),
485            0,
486            status_y + 2,
487            0xCCCCCC,
488        );
489
490        Ok(())
491    }
492
493    /// Get window ID
494    pub fn window_id(&self) -> WindowId {
495        self.window_id
496    }
497
498    /// Get compositor surface ID
499    pub fn surface_id(&self) -> u32 {
500        self.surface_id
501    }
502
503    /// Render text editor contents to its compositor surface.
504    ///
505    /// The surface includes a 28px title bar; content is at y-offset 28.
506    pub fn render_to_surface(&self) {
507        let w = self.width as usize;
508        let content_h = self.height as usize;
509        let title_bar_h: usize = 28;
510        let total_h = content_h + title_bar_h;
511        let mut pixels = vec![0u8; w * total_h * 4];
512
513        let mut content = vec![0u8; w * content_h * 4];
514        let _ = self.render(&mut content, w, content_h);
515        for y in 0..content_h {
516            let src_off = y * w * 4;
517            let dst_off = (y + title_bar_h) * w * 4;
518            pixels[dst_off..dst_off + w * 4].copy_from_slice(&content[src_off..src_off + w * 4]);
519        }
520
521        super::renderer::draw_title_bar_into_surface(&mut pixels, w, total_h, self.window_id);
522
523        super::renderer::update_surface_pixels(
524            self.surface_id,
525            self.pool_id,
526            self.pool_buf_id,
527            &pixels,
528        );
529    }
530}
531
532/// Global text editor (can support multiple instances)
533static TEXT_EDITOR: GlobalState<RwLock<TextEditor>> = GlobalState::new();
534
535/// Initialize text editor
536pub fn init() -> Result<(), KernelError> {
537    println!("[TEXT-EDITOR] Text editor initialized");
538    Ok(())
539}
540
541/// Create a new text editor instance
542pub fn create_text_editor(file_path: Option<String>) -> Result<(), KernelError> {
543    let editor = TextEditor::new(file_path)?;
544    TEXT_EDITOR
545        .init(RwLock::new(editor))
546        .map_err(|_| KernelError::InvalidState {
547            expected: "uninitialized",
548            actual: "initialized",
549        })?;
550    Ok(())
551}
552
553/// Execute a function with the text editor
554pub fn with_text_editor<R, F: FnOnce(&RwLock<TextEditor>) -> R>(f: F) -> Option<R> {
555    TEXT_EDITOR.with(f)
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_char_insertion() {
564        // Would test character insertion here
565    }
566
567    #[test]
568    fn test_newline_insertion() {
569        // Would test newline handling
570    }
571}