p2p_chat/ui/terminal/
render.rs

1//! This module contains the rendering logic for the `TerminalUI`.
2use anyhow::Result;
3use crossterm::{
4    cursor, queue,
5    style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
6    terminal::{Clear, ClearType},
7};
8use std::io::{stdout, Write};
9use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
10
11use crate::ui::UIMode;
12
13use super::TerminalUI;
14
15impl TerminalUI {
16    /// Renders the entire terminal UI.
17    ///
18    /// This function clears the screen, then renders the appropriate main view
19    /// (chat or logs), the status line, and the input area.
20    ///
21    /// # Arguments
22    ///
23    /// * `stdout` - A mutable reference to the output stream.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if writing to the output stream fails.
28    pub(super) fn render(&mut self) -> Result<()> {
29        let mut stdout = stdout();
30
31        queue!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
32
33        let (width, height) = self.state.terminal_size;
34        let message_area_height = (height as f32 * 0.8) as u16; // 80% of height for messages
35        let status_line_row = message_area_height; // Line below messages for status
36        let input_area_row = status_line_row + 1; // Line below status for input
37        let input_area_height = height - input_area_row; // Remaining height for input area
38
39        // Render main content area based on current UI mode
40        match &self.state.mode {
41            UIMode::Chat => {
42                self.chat_mode.render(
43                    &mut stdout,
44                    &self.state,
45                    (0, 0, width, message_area_height),
46                    self.node.as_deref(), // Pass node for message decryption in chat mode
47                )?;
48            }
49            UIMode::Logs { .. } => {
50                self.log_mode.render(
51                    &mut stdout,
52                    &self.state,
53                    (0, 0, width, message_area_height),
54                )?;
55            }
56        }
57
58        // Render status line
59        self.render_status_line(&mut stdout, status_line_row, width)?;
60        // Render input area
61        self.render_input_area(&mut stdout, input_area_row, width, input_area_height)?;
62
63        stdout.flush()?;
64        Ok(())
65    }
66
67    /// Renders the status line at the bottom of the message area.
68    ///
69    /// This line displays the current UI mode, connected peer count, and mode-specific tips.
70    ///
71    /// # Arguments
72    ///
73    /// * `stdout` - A mutable reference to the output stream.
74    /// * `row` - The row where the status line should be rendered.
75    /// * `width` - The width of the terminal.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if writing to the output stream fails.
80    fn render_status_line(&self, stdout: &mut impl Write, row: u16, width: u16) -> Result<()> {
81        queue!(
82            stdout,
83            cursor::MoveTo(0, row),
84            SetBackgroundColor(Color::DarkGrey),
85            SetForegroundColor(Color::White)
86        )?;
87
88        let status_text = match &self.state.mode {
89            UIMode::Chat => format!(
90                " Status: Chat Mode | Peers: {} | F9: Logs | Ctrl+C: Exit",
91                self.state.connected_peers_count
92            ),
93            UIMode::Logs { filter, level } => {
94                let filter_text = filter
95                    .as_ref()
96                    .map(|f| format!(" | Filter: {}", f))
97                    .unwrap_or_default();
98                format!(
99                    " Status: Log Mode | Level: {:?}{} | Entries: {} | F9: Chat",
100                    level,
101                    filter_text,
102                    self.state.logs.len()
103                )
104            }
105        };
106
107        // Truncate status text if it's too long for the terminal width
108        let display_text = if status_text.chars().count() > width as usize {
109            status_text.chars().take(width as usize).collect::<String>()
110        } else {
111            status_text.clone()
112        };
113
114        queue!(stdout, Print(&display_text))?;
115
116        // Fill remaining space with padding
117        let padding = width as usize - UnicodeWidthStr::width(display_text.as_str());
118        if padding > 0 {
119            queue!(stdout, Print(" ".repeat(padding)))?;
120        }
121
122        queue!(stdout, ResetColor)?;
123        Ok(())
124    }
125
126    /// Renders the input area where the user types commands or messages.
127    ///
128    /// This includes the input prompt, the current input buffer, and any
129    /// autocompletion suggestions. Also renders help text.
130    ///
131    /// # Arguments
132    ///
133    /// * `stdout` - A mutable reference to the output stream.
134    /// * `row` - The starting row for the input area.
135    /// * `width` - The width of the terminal.
136    /// * `height` - The height of the input area.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if writing to the output stream fails.
141    fn render_input_area(
142        &self,
143        stdout: &mut impl Write,
144        row: u16,
145        width: u16,
146        height: u16,
147    ) -> Result<()> {
148        queue!(stdout, cursor::MoveTo(0, row))?;
149
150        let prompt = match &self.state.mode {
151            UIMode::Chat => "p2p> ",
152            UIMode::Logs { .. } => "log> ",
153        };
154
155        queue!(
156            stdout,
157            SetForegroundColor(Color::Cyan),
158            Print(prompt),
159            ResetColor
160        )?;
161
162        // Print current input buffer
163        queue!(stdout, Print(&self.state.input_buffer))?;
164
165        // Render autocompletion suggestion for chat mode
166        if matches!(self.state.mode, UIMode::Chat) {
167            if let Some(suggestion) = self.chat_mode.get_current_suggestion() {
168                // Only show hint if input is a prefix of the suggestion
169                if suggestion.starts_with(&self.state.input_buffer)
170                    && suggestion != self.state.input_buffer
171                {
172                    let input_char_count = self.state.input_buffer.chars().count();
173                    let hint: String = suggestion.chars().skip(input_char_count).collect();
174                    queue!(
175                        stdout,
176                        SetForegroundColor(Color::DarkGrey),
177                        Print(hint),
178                        ResetColor
179                    )?;
180                }
181            }
182        }
183
184        // Position cursor correctly
185        let input_display_width: usize = self
186            .state
187            .input_buffer
188            .chars()
189            .take(self.state.cursor_pos)
190            .map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
191            .sum();
192
193        let prompt_display_width: usize = prompt
194            .chars()
195            .map(|c| UnicodeWidthChar::width(c).unwrap_or(1))
196            .sum();
197
198        let cursor_x = prompt_display_width + input_display_width;
199        if cursor_x < width as usize {
200            queue!(stdout, cursor::MoveTo(cursor_x as u16, row))?;
201        }
202
203        // Render help text below the input line if enough height is available
204        if height > 2 {
205            let help_row = row + height - 1;
206            queue!(stdout, cursor::MoveTo(0, help_row))?;
207
208            let help_text = match &self.state.mode {
209                UIMode::Chat =>
210                    " Tab: complete | ↑↓: history | PgUp/Down: scroll | Ctrl+Home/End: H-scroll | F9: logs",
211                UIMode::Logs { .. } =>
212                    " Tab: complete | ↑↓: scroll | Ctrl+Home/End: H-scroll | F9: chat",
213            };
214
215            queue!(
216                stdout,
217                SetForegroundColor(Color::DarkGrey),
218                Print(help_text),
219                ResetColor
220            )?;
221        }
222
223        Ok(())
224    }
225}