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

veridian_kernel/desktop/
terminal.rs

1//! Terminal Emulator Application
2//!
3//! Combines PTY, font rendering, and window manager to provide a graphical
4//! terminal.
5
6#[allow(unused_imports)]
7use alloc::{format, string::String, vec, vec::Vec};
8
9use spin::RwLock;
10
11use crate::{
12    desktop::window_manager::{with_window_manager, InputEvent, WindowId},
13    error::KernelError,
14    fs::pty::with_pty_manager,
15    sync::once_lock::GlobalState,
16};
17
18/// Terminal dimensions
19const TERMINAL_COLS: usize = 80;
20const TERMINAL_ROWS: usize = 24;
21
22/// Terminal colors
23#[derive(Debug, Clone, Copy)]
24pub struct Color {
25    pub r: u8,
26    pub g: u8,
27    pub b: u8,
28}
29
30impl Color {
31    pub const BLACK: Color = Color { r: 0, g: 0, b: 0 };
32    pub const WHITE: Color = Color {
33        r: 255,
34        g: 255,
35        b: 255,
36    };
37    pub const GREEN: Color = Color { r: 0, g: 255, b: 0 };
38    pub const BLUE: Color = Color {
39        r: 0,
40        g: 128,
41        b: 255,
42    };
43}
44
45/// Terminal cell
46#[derive(Debug, Clone, Copy)]
47struct Cell {
48    character: char,
49    foreground: Color,
50    background: Color,
51}
52
53impl Default for Cell {
54    fn default() -> Self {
55        Self {
56            character: ' ',
57            foreground: Color::WHITE,
58            background: Color::BLACK,
59        }
60    }
61}
62
63/// ANSI escape parser state.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65enum EscapeState {
66    Normal,
67    Escape,
68    Csi,
69}
70
71/// ANSI standard colors (SGR 30-37 foreground, 40-47 background).
72const ANSI_COLORS: [Color; 8] = [
73    Color { r: 0, g: 0, b: 0 }, // 0: black
74    Color {
75        r: 0xAA,
76        g: 0,
77        b: 0,
78    }, // 1: red
79    Color {
80        r: 0,
81        g: 0xAA,
82        b: 0,
83    }, // 2: green
84    Color {
85        r: 0xAA,
86        g: 0x55,
87        b: 0,
88    }, // 3: yellow/brown
89    Color {
90        r: 0,
91        g: 0,
92        b: 0xAA,
93    }, // 4: blue
94    Color {
95        r: 0xAA,
96        g: 0,
97        b: 0xAA,
98    }, // 5: magenta
99    Color {
100        r: 0,
101        g: 0xAA,
102        b: 0xAA,
103    }, // 6: cyan
104    Color {
105        r: 0xAA,
106        g: 0xAA,
107        b: 0xAA,
108    }, // 7: white/light gray
109];
110
111/// Pixel dimensions for terminal rendering (8x16 font)
112const TERMINAL_PX_WIDTH: u32 = (TERMINAL_COLS * 8) as u32;
113const TERMINAL_PX_HEIGHT: u32 = (TERMINAL_ROWS * 16) as u32;
114
115/// Terminal emulator
116pub struct TerminalEmulator {
117    /// Window ID
118    window_id: WindowId,
119
120    /// Compositor surface ID
121    surface_id: u32,
122    /// SHM pool ID
123    pool_id: u32,
124    /// Pool buffer ID
125    pool_buf_id: u32,
126
127    /// PTY master ID
128    pty_master_id: u32,
129
130    /// PTY slave ID
131    pty_slave_id: u32,
132
133    /// Screen buffer
134    buffer: Vec<Vec<Cell>>,
135
136    /// Cursor position
137    cursor_x: usize,
138    cursor_y: usize,
139
140    /// Current colors
141    current_fg: Color,
142    current_bg: Color,
143
144    /// Default colors (for SGR reset)
145    default_fg: Color,
146    default_bg: Color,
147
148    /// Scrollback buffer
149    scrollback: Vec<Vec<Cell>>,
150
151    /// Maximum scrollback lines
152    max_scrollback: usize,
153
154    /// ANSI escape parser state
155    esc_state: EscapeState,
156    /// ESC sequence parameter accumulator
157    esc_params: [u8; 16],
158    /// Current parameter index
159    esc_param_idx: usize,
160
161    /// Line editing buffer for local shell execution
162    line_buffer: String,
163}
164
165impl TerminalEmulator {
166    /// Create a new terminal emulator
167    pub fn new(width: u32, height: u32) -> Result<Self, KernelError> {
168        // WM window includes title bar (28px) above content area
169        let title_bar_h = 28u32;
170        let window_id =
171            with_window_manager(|wm| wm.create_window(100, 100, width, height + title_bar_h, 0))
172                .ok_or(KernelError::InvalidState {
173                expected: "initialized",
174                actual: "uninitialized",
175            })??;
176
177        // Compositor surface covers the full window area (title bar + content)
178        let (surface_id, pool_id, pool_buf_id) =
179            super::renderer::create_app_surface(100, 100, width, height + title_bar_h);
180
181        // Create PTY pair
182        let (pty_master_id, pty_slave_id) = with_pty_manager(|manager| manager.create_pty())
183            .ok_or(KernelError::InvalidState {
184                expected: "initialized",
185                actual: "uninitialized",
186            })??;
187
188        // Initialize buffer
189        let mut buffer = Vec::new();
190        for _ in 0..TERMINAL_ROWS {
191            buffer.push(vec![Cell::default(); TERMINAL_COLS]);
192        }
193
194        println!(
195            "[TERMINAL] Created terminal emulator: window={}, surface={}, pty={}",
196            window_id, surface_id, pty_master_id
197        );
198
199        Ok(Self {
200            window_id,
201            surface_id,
202            pool_id,
203            pool_buf_id,
204            pty_master_id,
205            pty_slave_id,
206            buffer,
207            cursor_x: 0,
208            cursor_y: 0,
209            current_fg: Color::GREEN,
210            current_bg: Color::BLACK,
211            default_fg: Color::GREEN,
212            default_bg: Color::BLACK,
213            scrollback: Vec::new(),
214            max_scrollback: 1000,
215            esc_state: EscapeState::Normal,
216            esc_params: [0; 16],
217            esc_param_idx: 0,
218            line_buffer: String::new(),
219        })
220    }
221
222    /// Render the terminal contents to its compositor surface.
223    ///
224    /// The surface includes a 28px title bar at the top; terminal content
225    /// is rendered starting at row 28.
226    pub fn render_to_surface(&self) {
227        let w = TERMINAL_PX_WIDTH as usize;
228        let content_h = TERMINAL_PX_HEIGHT as usize;
229        let title_bar_h: usize = 28;
230        let total_h = content_h + title_bar_h;
231        let mut pixels = vec![0u8; w * total_h * 4];
232
233        // Render content into a temporary buffer, then copy at y-offset 28
234        let mut content = vec![0u8; w * content_h * 4];
235        let _ = self.render(&mut content, w, content_h);
236        for y in 0..content_h {
237            let src_off = y * w * 4;
238            let dst_off = (y + title_bar_h) * w * 4;
239            pixels[dst_off..dst_off + w * 4].copy_from_slice(&content[src_off..src_off + w * 4]);
240        }
241
242        // Draw title bar into rows 0..28 and close button
243        super::renderer::draw_title_bar_into_surface(&mut pixels, w, total_h, self.window_id);
244
245        super::renderer::update_surface_pixels(
246            self.surface_id,
247            self.pool_id,
248            self.pool_buf_id,
249            &pixels,
250        );
251    }
252
253    /// Process input event
254    pub fn process_input(&mut self, event: InputEvent) -> Result<(), KernelError> {
255        match event {
256            InputEvent::KeyPress {
257                character,
258                scancode,
259            } => {
260                match character {
261                    '\r' | '\n' => {
262                        // Echo newline
263                        self.process_output_byte(b'\r');
264                        self.process_output_byte(b'\n');
265
266                        // Execute command
267                        let cmd = self.line_buffer.clone();
268                        self.line_buffer.clear();
269
270                        if !cmd.is_empty() {
271                            self.execute_command(&cmd);
272                        }
273
274                        // Print prompt
275                        for &b in b"root@veridian:/# " {
276                            self.process_output_byte(b);
277                        }
278                    }
279                    '\x08' | '\x7f' => {
280                        // Backspace
281                        if !self.line_buffer.is_empty() {
282                            self.line_buffer.pop();
283                            // Erase character on screen: BS + space + BS
284                            self.process_output_byte(b'\x08');
285                            self.process_output_byte(b' ');
286                            self.process_output_byte(b'\x08');
287                        }
288                    }
289                    '\x00' => {
290                        // Null / non-printable from scancode-only events
291                        // Handle arrow keys etc. if needed
292                        let _ = scancode;
293                    }
294                    c if c >= ' ' => {
295                        // Printable character: echo + accumulate
296                        self.line_buffer.push(c);
297                        self.process_output_byte(c as u8);
298                    }
299                    _ => {}
300                }
301            }
302            InputEvent::KeyRelease { .. } => {}
303            _ => {}
304        }
305
306        Ok(())
307    }
308
309    /// Execute a shell command and write captured output to the terminal.
310    fn execute_command(&mut self, cmd: &str) {
311        if let Some(shell) = crate::services::shell::try_get_shell() {
312            // Capture println! output during command execution
313            crate::print_capture::start_capture();
314            let result = shell.execute_command(cmd);
315            let captured = crate::print_capture::stop_capture();
316
317            // Display captured output (skip [SHELL-EXEC] debug lines)
318            for line in captured.lines() {
319                if line.starts_with("[SHELL-EXEC]") {
320                    continue;
321                }
322                for &b in line.as_bytes() {
323                    self.process_output_byte(b);
324                }
325                self.process_output_byte(b'\r');
326                self.process_output_byte(b'\n');
327            }
328
329            // Display error/not-found messages from the result itself
330            match result {
331                crate::services::shell::CommandResult::Success(_) => {}
332                crate::services::shell::CommandResult::Error(msg) => {
333                    for &b in msg.as_bytes() {
334                        if b == b'\n' {
335                            self.process_output_byte(b'\r');
336                        }
337                        self.process_output_byte(b);
338                    }
339                    self.process_output_byte(b'\r');
340                    self.process_output_byte(b'\n');
341                }
342                crate::services::shell::CommandResult::NotFound => {
343                    let msg = format!(
344                        "{}: command not found",
345                        cmd.split_whitespace().next().unwrap_or(cmd)
346                    );
347                    for &b in msg.as_bytes() {
348                        self.process_output_byte(b);
349                    }
350                    self.process_output_byte(b'\r');
351                    self.process_output_byte(b'\n');
352                }
353                crate::services::shell::CommandResult::Exit(_) => {}
354            }
355        } else {
356            for &b in b"shell not initialized" {
357                self.process_output_byte(b);
358            }
359            self.process_output_byte(b'\r');
360            self.process_output_byte(b'\n');
361        }
362    }
363
364    /// Update terminal from PTY output
365    pub fn update(&mut self) -> Result<(), KernelError> {
366        // Read from PTY
367        let master_id = self.pty_master_id;
368        if let Some(master) = with_pty_manager(|manager| manager.get_master(master_id)).flatten() {
369            let mut buf = [0u8; 1024];
370            match master.read(&mut buf) {
371                Ok(bytes_read) => {
372                    if bytes_read > 0 {
373                        // Process output
374                        for &byte in &buf[..bytes_read] {
375                            self.process_output_byte(byte);
376                        }
377                    }
378                }
379                Err(_) => {
380                    // No data available
381                }
382            }
383        }
384
385        Ok(())
386    }
387
388    /// Process a single output byte with ANSI escape sequence support.
389    fn process_output_byte(&mut self, byte: u8) {
390        match self.esc_state {
391            EscapeState::Normal => self.process_normal(byte),
392            EscapeState::Escape => self.process_escape(byte),
393            EscapeState::Csi => self.process_csi(byte),
394        }
395    }
396
397    /// Handle a byte in normal (non-escape) mode.
398    fn process_normal(&mut self, byte: u8) {
399        match byte {
400            b'\n' => {
401                self.cursor_x = 0;
402                self.cursor_y += 1;
403                if self.cursor_y >= TERMINAL_ROWS {
404                    self.scroll_up();
405                }
406            }
407            b'\r' => {
408                self.cursor_x = 0;
409            }
410            b'\t' => {
411                self.cursor_x = (self.cursor_x + 8) & !7;
412                if self.cursor_x >= TERMINAL_COLS {
413                    self.cursor_x = 0;
414                    self.cursor_y += 1;
415                    if self.cursor_y >= TERMINAL_ROWS {
416                        self.scroll_up();
417                    }
418                }
419            }
420            b'\x08' => {
421                if self.cursor_x > 0 {
422                    self.cursor_x -= 1;
423                    self.buffer[self.cursor_y][self.cursor_x] = Cell::default();
424                }
425            }
426            0x1B => {
427                // ESC — start escape sequence
428                self.esc_state = EscapeState::Escape;
429                self.esc_param_idx = 0;
430                self.esc_params = [0; 16];
431            }
432            0x20..=0x7E => {
433                self.buffer[self.cursor_y][self.cursor_x] = Cell {
434                    character: byte as char,
435                    foreground: self.current_fg,
436                    background: self.current_bg,
437                };
438                self.cursor_x += 1;
439                if self.cursor_x >= TERMINAL_COLS {
440                    self.cursor_x = 0;
441                    self.cursor_y += 1;
442                    if self.cursor_y >= TERMINAL_ROWS {
443                        self.scroll_up();
444                    }
445                }
446            }
447            _ => {}
448        }
449    }
450
451    /// Handle a byte after ESC was received.
452    fn process_escape(&mut self, byte: u8) {
453        if byte == b'[' {
454            self.esc_state = EscapeState::Csi;
455        } else {
456            self.esc_state = EscapeState::Normal;
457        }
458    }
459
460    /// Handle a byte inside a CSI sequence (ESC [ ...).
461    fn process_csi(&mut self, byte: u8) {
462        match byte {
463            b'0'..=b'9' => {
464                if self.esc_param_idx < self.esc_params.len() {
465                    self.esc_params[self.esc_param_idx] = self.esc_params[self.esc_param_idx]
466                        .wrapping_mul(10)
467                        .wrapping_add(byte - b'0');
468                }
469            }
470            b';' => {
471                if self.esc_param_idx < self.esc_params.len() - 1 {
472                    self.esc_param_idx += 1;
473                }
474            }
475            b'm' => {
476                // SGR (Select Graphic Rendition)
477                self.handle_sgr();
478                self.esc_state = EscapeState::Normal;
479            }
480            b'J' => {
481                // Erase in Display
482                let param = self.esc_params[0];
483                if param == 2 {
484                    // Clear entire screen
485                    for row in self.buffer.iter_mut() {
486                        for cell in row.iter_mut() {
487                            *cell = Cell::default();
488                        }
489                    }
490                    self.cursor_x = 0;
491                    self.cursor_y = 0;
492                }
493                self.esc_state = EscapeState::Normal;
494            }
495            b'H' => {
496                // Cursor Position
497                let row = if self.esc_params[0] > 0 {
498                    (self.esc_params[0] - 1) as usize
499                } else {
500                    0
501                };
502                let col = if self.esc_param_idx >= 1 && self.esc_params[1] > 0 {
503                    (self.esc_params[1] - 1) as usize
504                } else {
505                    0
506                };
507                self.cursor_y = row.min(TERMINAL_ROWS - 1);
508                self.cursor_x = col.min(TERMINAL_COLS - 1);
509                self.esc_state = EscapeState::Normal;
510            }
511            b'A' => {
512                // Cursor Up
513                let n = if self.esc_params[0] > 0 {
514                    self.esc_params[0] as usize
515                } else {
516                    1
517                };
518                self.cursor_y = self.cursor_y.saturating_sub(n);
519                self.esc_state = EscapeState::Normal;
520            }
521            b'B' => {
522                // Cursor Down
523                let n = if self.esc_params[0] > 0 {
524                    self.esc_params[0] as usize
525                } else {
526                    1
527                };
528                self.cursor_y = (self.cursor_y + n).min(TERMINAL_ROWS - 1);
529                self.esc_state = EscapeState::Normal;
530            }
531            b'C' => {
532                // Cursor Forward
533                let n = if self.esc_params[0] > 0 {
534                    self.esc_params[0] as usize
535                } else {
536                    1
537                };
538                self.cursor_x = (self.cursor_x + n).min(TERMINAL_COLS - 1);
539                self.esc_state = EscapeState::Normal;
540            }
541            b'D' => {
542                // Cursor Back
543                let n = if self.esc_params[0] > 0 {
544                    self.esc_params[0] as usize
545                } else {
546                    1
547                };
548                self.cursor_x = self.cursor_x.saturating_sub(n);
549                self.esc_state = EscapeState::Normal;
550            }
551            b'K' => {
552                // Erase in Line
553                let param = self.esc_params[0];
554                let (start, end) = match param {
555                    1 => (0, self.cursor_x),
556                    2 => (0, TERMINAL_COLS),
557                    _ => (self.cursor_x, TERMINAL_COLS),
558                };
559                for col in start..end.min(TERMINAL_COLS) {
560                    self.buffer[self.cursor_y][col] = Cell::default();
561                }
562                self.esc_state = EscapeState::Normal;
563            }
564            _ => {
565                self.esc_state = EscapeState::Normal;
566            }
567        }
568    }
569
570    /// Handle SGR (Select Graphic Rendition) escape codes.
571    fn handle_sgr(&mut self) {
572        let param_count = self.esc_param_idx + 1;
573        for i in 0..param_count {
574            let code = self.esc_params[i];
575            match code {
576                0 => {
577                    self.current_fg = self.default_fg;
578                    self.current_bg = self.default_bg;
579                }
580                1 => {
581                    // Bold: brighten foreground
582                    self.current_fg = Color {
583                        r: self.current_fg.r.saturating_add(0x55),
584                        g: self.current_fg.g.saturating_add(0x55),
585                        b: self.current_fg.b.saturating_add(0x55),
586                    };
587                }
588                30..=37 => {
589                    self.current_fg = ANSI_COLORS[(code - 30) as usize];
590                }
591                40..=47 => {
592                    self.current_bg = ANSI_COLORS[(code - 40) as usize];
593                }
594                _ => {}
595            }
596        }
597    }
598
599    /// Scroll buffer up one line
600    fn scroll_up(&mut self) {
601        // Save first line to scrollback
602        if self.scrollback.len() >= self.max_scrollback {
603            self.scrollback.remove(0);
604        }
605        self.scrollback.push(self.buffer[0].clone());
606
607        // Shift lines up
608        for y in 0..TERMINAL_ROWS - 1 {
609            self.buffer[y] = self.buffer[y + 1].clone();
610        }
611
612        // Clear bottom line
613        self.buffer[TERMINAL_ROWS - 1] = vec![Cell::default(); TERMINAL_COLS];
614        self.cursor_y = TERMINAL_ROWS - 1;
615    }
616
617    /// Render terminal to a BGRA pixel buffer.
618    ///
619    /// `buf` is width*height*4 bytes in BGRA format.
620    pub fn render(&self, buf: &mut [u8], width: usize, _height: usize) -> Result<(), KernelError> {
621        use super::renderer::draw_char_into_buffer;
622
623        let char_w = 8;
624        let char_h = 16;
625
626        // Clear to black background (BGRA)
627        for chunk in buf.chunks_exact_mut(4) {
628            chunk[0] = 0x00; // B
629            chunk[1] = 0x00; // G
630            chunk[2] = 0x00; // R
631            chunk[3] = 0xFF; // A
632        }
633
634        // Render each cell with its foreground color
635        for y in 0..TERMINAL_ROWS {
636            for x in 0..TERMINAL_COLS {
637                let cell = &self.buffer[y][x];
638                if cell.character == ' ' {
639                    continue;
640                }
641                // Background fill for non-black cells
642                if cell.background.r != 0 || cell.background.g != 0 || cell.background.b != 0 {
643                    let px0 = x * char_w;
644                    let py0 = y * char_h;
645                    for dy in 0..char_h {
646                        for dx in 0..char_w {
647                            let offset = ((py0 + dy) * width + (px0 + dx)) * 4;
648                            if offset + 3 < buf.len() {
649                                buf[offset] = cell.background.b;
650                                buf[offset + 1] = cell.background.g;
651                                buf[offset + 2] = cell.background.r;
652                                buf[offset + 3] = 0xFF;
653                            }
654                        }
655                    }
656                }
657                let fg_color = ((cell.foreground.r as u32) << 16)
658                    | ((cell.foreground.g as u32) << 8)
659                    | (cell.foreground.b as u32);
660                let ch = cell.character as u8;
661                let cw = crate::desktop::desktop_ext::cjk::char_width(cell.character);
662                if cw == 0 {
663                    continue; // skip zero-width / combining marks
664                }
665                draw_char_into_buffer(buf, width, ch, x * char_w, y * char_h, fg_color);
666            }
667        }
668
669        // Draw block cursor
670        let cx = self.cursor_x * char_w;
671        let cy = self.cursor_y * char_h;
672        for dy in 0..char_h {
673            for dx in 0..char_w {
674                let offset = ((cy + dy) * width + (cx + dx)) * 4;
675                if offset + 3 < buf.len() {
676                    // Invert: use foreground color with some transparency effect
677                    buf[offset] = 0xCC; // B
678                    buf[offset + 1] = 0xCC; // G
679                    buf[offset + 2] = 0xCC; // R
680                    buf[offset + 3] = 0xFF; // A
681                }
682            }
683        }
684
685        Ok(())
686    }
687
688    /// Get window ID
689    pub fn window_id(&self) -> WindowId {
690        self.window_id
691    }
692
693    /// Get compositor surface ID
694    pub fn surface_id(&self) -> u32 {
695        self.surface_id
696    }
697
698    /// Get PTY slave ID for shell connection
699    pub fn pty_slave_id(&self) -> u32 {
700        self.pty_slave_id
701    }
702}
703
704/// Terminal manager for multiple terminals
705pub struct TerminalManager {
706    terminals: RwLock<Vec<TerminalEmulator>>,
707}
708
709impl TerminalManager {
710    /// Create a new terminal manager
711    pub fn new() -> Self {
712        Self {
713            terminals: RwLock::new(Vec::new()),
714        }
715    }
716
717    /// Create a new terminal
718    pub fn create_terminal(&self, width: u32, height: u32) -> Result<usize, KernelError> {
719        let terminal = TerminalEmulator::new(width, height)?;
720        let mut terminals = self.terminals.write();
721        terminals.push(terminal);
722        Ok(terminals.len() - 1)
723    }
724
725    /// Process input for a terminal
726    pub fn process_input(&self, terminal_id: usize, event: InputEvent) -> Result<(), KernelError> {
727        let mut terminals = self.terminals.write();
728        if let Some(terminal) = terminals.get_mut(terminal_id) {
729            terminal.process_input(event)
730        } else {
731            Err(KernelError::NotFound {
732                resource: "terminal",
733                id: terminal_id as u64,
734            })
735        }
736    }
737
738    /// Update all terminals (read PTY output).
739    pub fn update_all(&self) -> Result<(), KernelError> {
740        let mut terminals = self.terminals.write();
741        for terminal in terminals.iter_mut() {
742            terminal.update()?;
743        }
744        Ok(())
745    }
746
747    /// Get the window ID of a terminal by index.
748    pub fn get_window_id(&self, terminal_id: usize) -> Option<WindowId> {
749        let terminals = self.terminals.read();
750        terminals.get(terminal_id).map(|t| t.window_id())
751    }
752
753    /// Get the compositor surface ID of a terminal by index.
754    pub fn get_surface_id(&self, terminal_id: usize) -> Option<u32> {
755        let terminals = self.terminals.read();
756        terminals.get(terminal_id).map(|t| t.surface_id())
757    }
758
759    /// Write a welcome message to a terminal's screen buffer.
760    pub fn write_welcome(&self, terminal_id: usize) {
761        let mut terminals = self.terminals.write();
762        if let Some(terminal) = terminals.get_mut(terminal_id) {
763            for &b in b"VeridianOS Terminal\r\n\r\nPress ESC to exit GUI.\r\nroot@veridian:/# " {
764                terminal.process_output_byte(b);
765            }
766        }
767    }
768
769    /// Render all terminal surfaces to the compositor.
770    pub fn render_all_surfaces(&self) {
771        let terminals = self.terminals.read();
772        for terminal in terminals.iter() {
773            terminal.render_to_surface();
774        }
775    }
776}
777
778impl Default for TerminalManager {
779    fn default() -> Self {
780        Self::new()
781    }
782}
783
784/// Global terminal manager
785static TERMINAL_MANAGER: GlobalState<TerminalManager> = GlobalState::new();
786
787/// Initialize terminal system
788pub fn init() -> Result<(), KernelError> {
789    let manager = TerminalManager::new();
790    TERMINAL_MANAGER
791        .init(manager)
792        .map_err(|_| KernelError::InvalidState {
793            expected: "uninitialized",
794            actual: "initialized",
795        })?;
796
797    println!("[TERMINAL] Terminal emulator system initialized");
798    Ok(())
799}
800
801/// Execute a function with the terminal manager
802pub fn with_terminal_manager<R, F: FnOnce(&TerminalManager) -> R>(f: F) -> Option<R> {
803    TERMINAL_MANAGER.with(f)
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809
810    #[test]
811    fn test_cell_default() {
812        let cell = Cell::default();
813        assert_eq!(cell.character, ' ');
814    }
815
816    #[test]
817    fn test_terminal_dimensions() {
818        assert_eq!(TERMINAL_COLS, 80);
819        assert_eq!(TERMINAL_ROWS, 24);
820    }
821}