use crate::buffer::Buffer;
use crate::error::Result;
use crate::terminal::Terminal;
use crate::syntax::{SyntaxHighlighter, HighlightColor};
use termion::color;
const HELP_TEXT: &str = r#"
NanoRust Help Text
NanoRust is a terminal-based text editor written in Rust, inspired by
GNU nano. There are four main sections of the editor:
The top line shows the program version, the current filename being edited,
and whether or not the file has been modified. Next is the main editor
window showing the file being edited. The status line is the third line
from the bottom and shows important messages. The bottom two lines show
the most commonly used shortcuts in the editor.
Shortcuts are shown as follows: Control-key sequences are notated with a
caret (^) symbol. Escape-key sequences are notated with the Meta (M-)
symbol. The following keystrokes are available in the main editor window.
Alternative keys are shown in parentheses:
^G (F1) Display this help text
^X (F2) Close the current buffer / Exit from editor
^O (F3) Write the current buffer to disk
^R (Ins) Insert another file into current buffer
^W (F6) Search forward for a string
^\ (M-R) Search and replace a string
^K (F9) Cut current line into cutbuffer
^U (F10) Paste cutbuffer contents at cursor
^J Justify the current paragraph
^T Invoke the spell checker (if available)
^C Display cursor position
^_ (M-G) Go to line and column number
^^ (M-A) Mark text starting from cursor position
M-6 (M-^) Copy current line into cutbuffer
M-U Undo the last operation
M-E Redo the last undone operation
M-} Indent the current line
M-{ Unindent the current line
^A (Home) Move to beginning of current line
^E (End) Move to end of current line
^Y (PgUp) Move to previous screen
^V (PgDn) Move to next screen
M-Y Toggle syntax highlighting on/off
^Z Suspend the editor (return with fg)
[ Press any key to continue ]
"#;
pub struct StatusBar {
pub filename: String,
pub line: usize,
pub col: usize,
pub modified: bool,
pub total_lines: usize,
}
pub struct Display {
row_offset: usize,
col_offset: usize,
message: Option<String>,
show_line_numbers: bool,
tab_size: usize,
highlighter: Option<SyntaxHighlighter>,
syntax_highlighting_enabled: bool,
}
impl Display {
pub fn new() -> Self {
Self {
row_offset: 0,
col_offset: 0,
message: None,
show_line_numbers: false,
tab_size: 4,
highlighter: None,
syntax_highlighting_enabled: true,
}
}
pub fn update_highlighter(&mut self, buffer: &Buffer) {
if let Some(path) = buffer.filename() {
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
self.highlighter = SyntaxHighlighter::from_filename(filename);
}
}
}
pub fn toggle_syntax_highlighting(&mut self) {
self.syntax_highlighting_enabled = !self.syntax_highlighting_enabled;
}
pub fn is_syntax_highlighting_enabled(&self) -> bool {
self.syntax_highlighting_enabled
}
pub fn set_show_line_numbers(&mut self, show: bool) {
self.show_line_numbers = show;
}
pub fn set_tab_size(&mut self, size: usize) {
self.tab_size = size;
}
fn visible_rows(&self, terminal: &Terminal) -> usize {
let size = terminal.get_size();
(size.rows as usize).saturating_sub(4)
}
pub fn render_title_bar(&self, terminal: &mut Terminal, buffer: &Buffer) -> Result<()> {
let size = terminal.get_size();
terminal.move_cursor(1, 1)?;
write!(terminal, "{}{}", color::Bg(color::White), color::Fg(color::Black))?;
let editor_name = " NanoRust 0.1.0";
let filename = if let Some(path) = buffer.filename() {
path.to_string_lossy().to_string()
} else {
"New Buffer".to_string()
};
let modified_indicator = if buffer.is_modified() {
" [Modified]"
} else {
""
};
let file_text = format!("{}{}", filename, modified_indicator);
let available_width = size.cols as usize - editor_name.len();
let file_text = if file_text.len() > available_width {
format!("{}...", &file_text[..available_width.saturating_sub(3)])
} else {
file_text
};
let total_padding = if file_text.len() < available_width {
available_width - file_text.len()
} else {
0
};
let left_padding = total_padding / 2;
let right_padding = total_padding - left_padding;
write!(terminal, "{}", editor_name)?;
write!(terminal, "{}", " ".repeat(left_padding))?;
write!(terminal, "{}", file_text)?;
write!(terminal, "{}", " ".repeat(right_padding))?;
write!(terminal, "{}{}", color::Bg(color::Reset), color::Fg(color::Reset))?;
Ok(())
}
pub fn render(&mut self, terminal: &mut Terminal, buffer: &Buffer) -> Result<()> {
let size = terminal.get_size();
let visible_rows = self.visible_rows(terminal);
let line_num_width = if self.show_line_numbers {
let max_line = buffer.len();
format!("{}", max_line).len() + 1 } else {
0
};
let content_cols = (size.cols as usize).saturating_sub(line_num_width);
self.update_viewport(buffer, visible_rows, content_cols);
terminal.hide_cursor()?;
for screen_row in 0..visible_rows {
let buffer_row = screen_row + self.row_offset;
terminal.move_cursor((screen_row + 1 + 1) as u16, 1)?;
if self.show_line_numbers {
if buffer_row < buffer.len() {
write!(terminal, "{:>width$} ", buffer_row + 1, width = line_num_width - 1)?;
} else {
write!(terminal, "{:>width$} ", "", width = line_num_width - 1)?;
}
}
if buffer_row < buffer.len() {
if let Some(line) = buffer.get_line(buffer_row) {
self.render_line(terminal, line, content_cols)?;
}
} else {
write!(terminal, "~")?;
}
write!(terminal, "{}", termion::clear::UntilNewline)?;
}
let cursor = buffer.cursor();
let screen_row = cursor.row.saturating_sub(self.row_offset);
let screen_col = cursor.render_col.saturating_sub(self.col_offset);
terminal.move_cursor(
(screen_row + 1 + 1) as u16,
(screen_col + line_num_width + 1) as u16
)?;
terminal.show_cursor()?;
terminal.flush()?;
Ok(())
}
fn render_line(&self, terminal: &mut Terminal, line: &str, max_cols: usize) -> Result<()> {
let segments = if self.syntax_highlighting_enabled {
if let Some(ref highlighter) = self.highlighter {
highlighter.highlight_line(line)
} else {
vec![(line.to_string(), HighlightColor::Reset)]
}
} else {
vec![(line.to_string(), HighlightColor::Reset)]
};
let mut render_col = 0;
for (text, highlight_color) in segments {
self.apply_color(terminal, highlight_color)?;
for ch in text.chars() {
if render_col < self.col_offset {
if ch == '\t' {
render_col += self.tab_size - (render_col % self.tab_size);
} else {
render_col += 1;
}
continue;
}
if render_col >= self.col_offset + max_cols {
break;
}
if ch == '\t' {
let spaces = self.tab_size - (render_col % self.tab_size);
for _ in 0..spaces {
if render_col >= self.col_offset + max_cols {
break;
}
write!(terminal, " ")?;
render_col += 1;
}
} else {
write!(terminal, "{}", ch)?;
render_col += 1;
}
}
}
write!(terminal, "{}", color::Fg(color::Reset))?;
Ok(())
}
fn apply_color(&self, terminal: &mut Terminal, highlight_color: HighlightColor) -> Result<()> {
match highlight_color {
HighlightColor::Cyan => write!(terminal, "{}", color::Fg(color::Cyan))?,
HighlightColor::Yellow => write!(terminal, "{}", color::Fg(color::Yellow))?,
HighlightColor::Green => write!(terminal, "{}", color::Fg(color::Green))?,
HighlightColor::Magenta => write!(terminal, "{}", color::Fg(color::Magenta))?,
HighlightColor::Reset => write!(terminal, "{}", color::Fg(color::Reset))?,
}
Ok(())
}
fn update_viewport(&mut self, buffer: &Buffer, visible_rows: usize, visible_cols: usize) {
let cursor = buffer.cursor();
if cursor.row < self.row_offset {
self.row_offset = cursor.row;
}
if cursor.row >= self.row_offset + visible_rows {
self.row_offset = cursor.row - visible_rows + 1;
}
if cursor.render_col < self.col_offset {
self.col_offset = cursor.render_col;
}
if cursor.render_col >= self.col_offset + visible_cols {
self.col_offset = cursor.render_col - visible_cols + 1;
}
}
pub fn render_status_bar(&self, terminal: &mut Terminal, status: &StatusBar) -> Result<()> {
let size = terminal.get_size();
let visible_rows = self.visible_rows(terminal);
terminal.move_cursor((visible_rows + 2) as u16, 1)?;
write!(terminal, "{}{}", color::Bg(color::White), color::Fg(color::Black))?;
let modified_indicator = if status.modified { " [Modified]" } else { "" };
let left_status = format!(
" {}{}",
status.filename,
modified_indicator
);
let right_status = format!(
"Ln {}, Col {} {}/{} ",
status.line,
status.col,
status.line,
status.total_lines
);
let status_len = left_status.len() + right_status.len();
let padding = if status_len < size.cols as usize {
size.cols as usize - status_len
} else {
0
};
write!(terminal, "{}", left_status)?;
write!(terminal, "{}", " ".repeat(padding))?;
write!(terminal, "{}", right_status)?;
write!(terminal, "{}{}", color::Bg(color::Reset), color::Fg(color::Reset))?;
write!(terminal, "{}", termion::clear::UntilNewline)?;
Ok(())
}
pub fn render_help_bar(&self, terminal: &mut Terminal) -> Result<()> {
let visible_rows = self.visible_rows(terminal);
let size = terminal.get_size();
let width = size.cols as usize;
let col_width = width / 6;
let row1_shortcuts = [
"^G Get Help",
"^O WriteOut",
"^R Read File",
"^Y Prev Pg",
"^K Cut Text",
"^C Cur Pos",
];
let row2_shortcuts = [
"^X Exit",
"^J Justify",
"^W Where Is",
"^V Next Pg",
"^U Uncut Text",
"^T To Spell",
];
terminal.move_cursor((visible_rows + 3) as u16, 1)?;
for shortcut in row1_shortcuts.iter() {
write!(terminal, "{:<width$}", shortcut, width = col_width)?;
}
write!(terminal, "{}", termion::clear::UntilNewline)?;
terminal.move_cursor((visible_rows + 4) as u16, 1)?;
for shortcut in row2_shortcuts.iter() {
write!(terminal, "{:<width$}", shortcut, width = col_width)?;
}
write!(terminal, "{}", termion::clear::UntilNewline)?;
Ok(())
}
pub fn show_message(&mut self, msg: String) {
self.message = Some(msg);
}
pub fn clear_message(&mut self) {
self.message = None;
}
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
pub fn render_screen(&mut self, terminal: &mut Terminal, buffer: &Buffer) -> Result<()> {
self.render_title_bar(terminal, buffer)?;
self.render(terminal, buffer)?;
let cursor = buffer.cursor();
let status = StatusBar {
filename: if let Some(path) = buffer.filename() {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unnamed")
.to_string()
} else {
"New Buffer".to_string()
},
line: cursor.row + 1, col: cursor.col + 1, modified: buffer.is_modified(),
total_lines: buffer.len(),
};
self.render_status_bar(terminal, &status)?;
self.render_help_bar(terminal)?;
if let Some(msg) = &self.message {
let size = terminal.get_size();
let visible_rows = self.visible_rows(terminal);
terminal.move_cursor((visible_rows + 2) as u16, 1)?;
write!(terminal, "{}{}", color::Bg(color::Yellow), color::Fg(color::Black))?;
write!(terminal, " {}", msg)?;
write!(terminal, "{}", " ".repeat(size.cols as usize - msg.len() - 1))?;
write!(terminal, "{}{}", color::Bg(color::Reset), color::Fg(color::Reset))?;
}
let cursor = buffer.cursor();
let screen_row = cursor.row.saturating_sub(self.row_offset);
let screen_col = cursor.render_col.saturating_sub(self.col_offset);
let line_num_width = if self.show_line_numbers {
let max_line = buffer.len();
format!("{}", max_line).len() + 1
} else {
0
};
terminal.move_cursor(
(screen_row + 1 + 1) as u16,
(screen_col + line_num_width + 1) as u16
)?;
terminal.show_cursor()?;
terminal.flush()?;
Ok(())
}
pub fn render_help_screen(&mut self, terminal: &mut Terminal) -> Result<()> {
terminal.clear_screen()?;
let size = terminal.get_size();
let lines: Vec<&str> = HELP_TEXT.lines().collect();
let total_lines = lines.len();
let start_row = if total_lines < size.rows as usize {
(size.rows as usize - total_lines) / 2
} else {
0
};
for (i, line) in lines.iter().enumerate() {
let row = start_row + i;
if row >= size.rows as usize {
break;
}
terminal.move_cursor(row as u16 + 1, 1)?;
let display_line = if line.len() > size.cols as usize {
&line[..size.cols as usize]
} else {
line
};
write!(terminal, "{}", display_line)?;
write!(terminal, "{}", termion::clear::UntilNewline)?;
}
terminal.flush()?;
Ok(())
}
}
impl Default for Display {
fn default() -> Self {
Self::new()
}
}