Skip to content

Commit b4bce3c

Browse files
committed
add a nano like terminal based text editor
1 parent 09f1335 commit b4bce3c

File tree

4 files changed

+510
-0
lines changed

4 files changed

+510
-0
lines changed

src/editor/display.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use crate::editor::editor::Editor;
2+
use crossterm::{
3+
cursor, execute,
4+
style::{Color, Print, ResetColor, SetForegroundColor},
5+
terminal::ClearType,
6+
};
7+
use std::io::{self, stdout, Write};
8+
9+
pub struct Display;
10+
11+
impl Display {
12+
pub fn new() -> Self {
13+
Display
14+
}
15+
16+
pub fn refresh_screen(&mut self, editor: &Editor) -> io::Result<()> {
17+
execute!(stdout(), cursor::Hide)?;
18+
execute!(stdout(), cursor::MoveTo(0, 0))?;
19+
20+
let display_lines = if editor.terminal_height > 2 {
21+
editor.terminal_height - 2
22+
} else {
23+
0
24+
};
25+
26+
for i in 0..display_lines {
27+
let line_index = editor.offset_y + i;
28+
if line_index < editor.content.len() {
29+
let line = &editor.content[line_index];
30+
let display_line = if line.len() > editor.terminal_width {
31+
&line[..editor.terminal_width]
32+
} else {
33+
line
34+
};
35+
execute!(
36+
stdout(),
37+
Print(format!(
38+
"{:<width$}",
39+
display_line,
40+
width = editor.terminal_width
41+
))
42+
)?;
43+
} else {
44+
execute!(
45+
stdout(),
46+
Print(format!("{:<width$}", "~", width = editor.terminal_width))
47+
)?;
48+
}
49+
execute!(
50+
stdout(),
51+
crossterm::terminal::Clear(ClearType::UntilNewLine)
52+
)?;
53+
execute!(stdout(), Print("\r\n"))?;
54+
}
55+
56+
self.draw_status_bar(editor)?;
57+
58+
self.draw_help_bar(editor)?;
59+
60+
let screen_y = editor.cursor_y - editor.offset_y;
61+
let display_x = if editor.cursor_x > 0 && editor.terminal_width > 0 && editor.cursor_x >= editor.terminal_width {
62+
editor.terminal_width.saturating_sub(1)
63+
} else {
64+
editor.cursor_x
65+
};
66+
67+
execute!(stdout(), cursor::MoveTo(display_x as u16, screen_y as u16))?;
68+
execute!(stdout(), cursor::Show)?;
69+
stdout().flush()?;
70+
71+
Ok(())
72+
}
73+
74+
fn draw_status_bar(&self, editor: &Editor) -> io::Result<()> {
75+
execute!(stdout(), SetForegroundColor(Color::Black))?;
76+
execute!(stdout(), crossterm::style::SetBackgroundColor(Color::White))?;
77+
78+
let status = format!(
79+
" {} | Line {}/{} | Col {} {}",
80+
editor.filename.as_deref().unwrap_or("New File"),
81+
editor.cursor_y + 1,
82+
editor.content.len(),
83+
editor.cursor_x + 1,
84+
if editor.modified { "[Modified]" } else { "" }
85+
);
86+
87+
let truncated_status = if status.len() > editor.terminal_width {
88+
&status[..editor.terminal_width]
89+
} else {
90+
&status
91+
};
92+
93+
execute!(
94+
stdout(),
95+
Print(format!(
96+
"{:<width$}",
97+
truncated_status,
98+
width = editor.terminal_width
99+
))
100+
)?;
101+
execute!(stdout(), ResetColor)?;
102+
Ok(())
103+
}
104+
105+
fn draw_help_bar(&self, editor: &Editor) -> io::Result<()> {
106+
execute!(stdout(), Print("\r\n"))?;
107+
execute!(stdout(), SetForegroundColor(Color::DarkGrey))?;
108+
109+
let help = "^X Exit ^S Save ^O Open Arrow keys to move";
110+
let truncated_help = if help.len() > editor.terminal_width {
111+
&help[..editor.terminal_width]
112+
} else {
113+
help
114+
};
115+
116+
execute!(stdout(), Print(truncated_help))?;
117+
execute!(stdout(), ResetColor)?;
118+
Ok(())
119+
}
120+
}

src/editor/editor.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use crossterm::{
2+
cursor,
3+
event::{self, Event, KeyEventKind},
4+
execute,
5+
terminal::{self, ClearType},
6+
};
7+
use std::fs;
8+
use std::io::{self, stdout};
9+
use std::path::Path;
10+
11+
use crate::editor::display::Display;
12+
use crate::editor::input::InputHandler;
13+
14+
pub struct Editor {
15+
pub content: Vec<String>,
16+
pub cursor_x: usize,
17+
pub cursor_y: usize,
18+
pub offset_y: usize,
19+
pub filename: Option<String>,
20+
pub modified: bool,
21+
pub terminal_height: usize,
22+
pub terminal_width: usize,
23+
}
24+
25+
impl Editor {
26+
pub fn new() -> io::Result<Self> {
27+
let (width, height) = terminal::size()?;
28+
Ok(Editor {
29+
content: vec![String::new()],
30+
cursor_x: 0,
31+
cursor_y: 0,
32+
offset_y: 0,
33+
filename: None,
34+
modified: false,
35+
terminal_height: height as usize,
36+
terminal_width: width as usize,
37+
})
38+
}
39+
40+
pub fn open_file(filename: &str) -> io::Result<Self> {
41+
let mut editor = Self::new()?;
42+
editor.filename = Some(filename.to_string());
43+
44+
if Path::new(filename).exists() {
45+
let content = fs::read_to_string(filename)?;
46+
editor.content = if content.is_empty() {
47+
vec![String::new()]
48+
} else {
49+
content.lines().map(|s| s.to_string()).collect()
50+
};
51+
}
52+
53+
Ok(editor)
54+
}
55+
56+
pub fn run(&mut self) -> io::Result<()> {
57+
terminal::enable_raw_mode()?;
58+
execute!(stdout(), terminal::Clear(ClearType::All))?;
59+
60+
let mut input_handler = InputHandler::new();
61+
62+
loop {
63+
let display_result = {
64+
let mut display = Display::new();
65+
display.refresh_screen(self)
66+
};
67+
display_result?;
68+
69+
if let Event::Key(key_event) = event::read()? {
70+
if key_event.kind == KeyEventKind::Press {
71+
if input_handler.process_key(self, key_event)? {
72+
break;
73+
}
74+
}
75+
}
76+
}
77+
78+
terminal::disable_raw_mode()?;
79+
execute!(stdout(), terminal::Clear(ClearType::All))?;
80+
execute!(stdout(), cursor::MoveTo(0, 0))?;
81+
Ok(())
82+
}
83+
84+
pub fn clamp_cursor_x(&mut self) {
85+
let line_len = self.content[self.cursor_y].len();
86+
if self.cursor_x > line_len {
87+
self.cursor_x = line_len;
88+
}
89+
}
90+
91+
pub fn insert_char(&mut self, c: char) {
92+
self.content[self.cursor_y].insert(self.cursor_x, c);
93+
self.cursor_x += 1;
94+
self.modified = true;
95+
}
96+
97+
pub fn insert_newline(&mut self) {
98+
let current_line = &self.content[self.cursor_y];
99+
let new_line = current_line[self.cursor_x..].to_string();
100+
self.content[self.cursor_y].truncate(self.cursor_x);
101+
self.content.insert(self.cursor_y + 1, new_line);
102+
self.cursor_y += 1;
103+
self.cursor_x = 0;
104+
self.modified = true;
105+
}
106+
107+
pub fn delete_char(&mut self) {
108+
if self.cursor_x > 0 {
109+
self.content[self.cursor_y].remove(self.cursor_x - 1);
110+
self.cursor_x -= 1;
111+
self.modified = true;
112+
} else if self.cursor_y > 0 {
113+
let current_line = self.content.remove(self.cursor_y);
114+
self.cursor_y -= 1;
115+
self.cursor_x = self.content[self.cursor_y].len();
116+
self.content[self.cursor_y].push_str(&current_line);
117+
self.modified = true;
118+
}
119+
}
120+
121+
pub fn delete_char_forward(&mut self) {
122+
if self.cursor_x < self.content[self.cursor_y].len() {
123+
self.content[self.cursor_y].remove(self.cursor_x);
124+
self.modified = true;
125+
} else if self.cursor_y < self.content.len().saturating_sub(1) {
126+
let next_line = self.content.remove(self.cursor_y + 1);
127+
self.content[self.cursor_y].push_str(&next_line);
128+
self.modified = true;
129+
}
130+
}
131+
132+
pub fn move_cursor_up(&mut self) {
133+
if self.cursor_y > 0 {
134+
self.cursor_y -= 1;
135+
if self.cursor_y < self.offset_y {
136+
self.offset_y = self.cursor_y;
137+
}
138+
self.clamp_cursor_x();
139+
}
140+
}
141+
142+
pub fn move_cursor_down(&mut self) {
143+
if self.cursor_y < self.content.len().saturating_sub(1) {
144+
self.cursor_y += 1;
145+
// Prevent overflow by checking if terminal_height is large enough
146+
if self.terminal_height > 2 && self.cursor_y >= self.offset_y + self.terminal_height - 2 {
147+
self.offset_y = self.cursor_y.saturating_sub(self.terminal_height - 3);
148+
}
149+
self.clamp_cursor_x();
150+
}
151+
}
152+
153+
pub fn move_cursor_left(&mut self) {
154+
if self.cursor_x > 0 {
155+
self.cursor_x -= 1;
156+
} else if self.cursor_y > 0 {
157+
self.cursor_y -= 1;
158+
self.cursor_x = self.content[self.cursor_y].len();
159+
}
160+
}
161+
162+
pub fn move_cursor_right(&mut self) {
163+
if self.cursor_x < self.content[self.cursor_y].len() {
164+
self.cursor_x += 1;
165+
} else if self.cursor_y < self.content.len() - 1 {
166+
self.cursor_y += 1;
167+
self.cursor_x = 0;
168+
}
169+
}
170+
}
171+
172+
pub fn start_editor(filename: Option<&str>) -> io::Result<()> {
173+
let mut editor = if let Some(file) = filename {
174+
Editor::open_file(file)?
175+
} else {
176+
Editor::new()?
177+
};
178+
179+
editor.run()
180+
}

0 commit comments

Comments
 (0)