第三篇
这部分接着处理用户退出命令以及一些其他新功能;
3.1 使用Ctrl+Q退出
modifiers: event::KeyModifiers::CONTROL,
使用CONTROL
替换之前的NONE
值即可;
3.2 重构键盘输入
让我们重构我们的代码,以便我们有一个用于低级按键读取的函数,以及另一个用于将按键映射到编辑器操作的函数。
- 首先,让我们创建一个
struct
可以读取各种按键的按钮。我们将其命名为:Reader
:
struct Reader;
- 然后添加一个方法来读取关键事件:
impl Reader {fn read_key(&self) -> crossterm::Result<KeyEvent> {loop {if event::poll(Duration::from_millis(500))? {if let Event::Key(event) = event::read()? {return Ok(event);}}}}
}
- 现在让我们创建一个新结构
Editor
,它将是我们项目的主要主脑。
struct Editor {reader: Reader,
}impl Editor {fn new() -> Self {Self { reader: Reader }}
}
我们还创建了一个 new
方法来创建 的新 Editor
实例。
- 现在让我们处理 返回
Reader
的事件并创建一个run
函数:
struct Editor {reader: Reader,
}impl Editor {fn new() -> Self {Self { reader: Reader }}fn process_keypress(&self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::CONTROL,} => return Ok(false),_ => {}}Ok(true)}fn run(&self) -> crossterm::Result<bool> {self.process_keypress()}
}
在函数
process_keypress
中 ,我们返回是否应该继续读取关键事件。如果返回 false,则表示程序应该终止,因为我们不想再次读取关键事件。现在让我们修改一下main()
方法来 改用Editor.run()
:
fn main() -> crossterm::Result<()> {let _clean_up = CleanUp;terminal::enable_raw_mode()?;/* modify */let editor = Editor::new();while editor.run()? {}/* end */Ok(())
}
3.3 屏幕清理
在用户输入之前将屏幕清理干净,这里使用一个Output
的struct
来处理输出相关的内容;
struct Output;impl Output {fn new() -> Self {Self}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()}
}
该
clear_screen
函数实际执行的操作是将转义序列写入终端。这些序列修改了终端的行为,并可用于执行其他操作,例如添加颜色等。
- 修改调用关系:
use crossterm::event::{Event, KeyCode, KeyEvent};
use crossterm::{event, execute, terminal};
use std::io::stdout;
use std::time::Duration; /* add this line */struct CleanUp;
struct Reader;
struct Editor {reader: Reader,output:Output,
}
struct Output;impl Output {fn new() -> Self {Self}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(terminal::ClearType::All))}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()}
}impl Editor {fn new() -> Self {Self {reader: Reader,output:Output::new(), }}fn process_keypress(&self) -> crossterm::Result<bool> {match self.reader.read_key()? {KeyEvent {code: KeyCode::Char('q'),modifiers: event::KeyModifiers::CONTROL,kind: _,state: _,} => return Ok(false),_ => {}}Ok(true)}fn run(&self) -> crossterm::Result<bool> {self.output.refresh_screen()?;self.process_keypress()}
}impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Unable to disable raw mode")}
}impl Reader {fn read_key(&self) -> crossterm::Result<KeyEvent> {loop {if event::poll(Duration::from_millis(500))? {if let Event::Key(event) = event::read()? {return Ok(event);}}}}
}/// main函数
fn main() -> std::result::Result<(), std::io::Error> {let _clean_up = CleanUp;terminal::enable_raw_mode()?;let editor = Editor::new();while editor.run()? {}Ok(())
}
3.4 重新定位光标
你可能已经注意到光标未位于屏幕的左上角。这样我们就可以从上到下绘制我们的编辑器。
use crossterm::event::*;
use crossterm::terminal::ClearType;
use crossterm::{cursor, event, execute, terminal}; /* add import*/
use std::io::stdout;
use std::time::Duration;struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Unable to disable raw mode")}
}struct Output;impl Output {fn new() -> Self {Self}/* modify */fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}/* end */fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()}
}
3.5 退出时清屏
让我们清除屏幕并在程序退出时重新定位光标。
如果在渲染屏幕的过程中发生错误,我们不希望程序的输出留在屏幕上,也不希望将错误打印在光标恰好位于该点的任何位置。
所以当我们的程序成功或失败退出时,我们会将 Cleanup
该函数用于清除屏幕:
在
Drop
中新增:Output::clear_screen().expect("Error");
struct CleanUp;impl Drop for CleanUp {fn drop(&mut self) {terminal::disable_raw_mode().expect("Unable to disable raw mode");Output::clear_screen().expect("Error"); /* add this line*/}
}struct Output;impl Output {fn new() -> Self {Self}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()}
}
3.6 添加波浪号
让我们在屏幕的左侧画一列波浪号 ( ~
),就像 vim 一样。在我们的文本编辑器中,我们将在正在编辑的文件末尾之后的任何行的开头绘制一个波浪号。
struct Output;impl Output {fn new() -> Self {Self}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}/* add this function */fn draw_rows(&self) {for _ in 0..24 {println!("~\r");}}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()?;/* add the following lines*/self.draw_rows();execute!(stdout(), cursor::MoveTo(0, 0))/* end */}
}
draw_rows()
将处理绘制正在编辑的文本缓冲区的每一行。现在,它在每行中绘制一个波浪号,这意味着该行不是文件的一部分,不能包含任何文本。绘制后,我们将光标发送回屏幕的左上角。
- 现在让我们修改代码以绘制正确数量的波浪号:
/* modify */
struct Output {win_size: (usize, usize),
}impl Output {fn new() -> Self {/* add this variable */let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap(); Self { win_size }}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&self) {let screen_rows = self.win_size.1; /* add this line */for _ in 0..screen_rows { /* modify */println!("~\r");}}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()?;self.draw_rows();execute!(stdout(), cursor::MoveTo(0, 0))}
}
首先,我们修改
Output
以保留窗口大小,因为我们将使用窗口的大小进行多次计算。然后设置创建输出实例时的win_size
值。type
中的win_size
整数是usize
butterminal::size()
返回一个类型(u16,16)
为 的元组,因此我们必须转换为u16
usize
。
也许您注意到屏幕的最后一行似乎没有波浪号。这是因为我们的代码中有一个小错误。当我们打印最终的波浪号时,我们会像在任何其他行上一样打印一个 "\r\n"
( println!()
添加一个新行),但这会导致终端滚动以便为新的空白行腾出空间。
impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self { win_size }}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&self) {let screen_rows = self.win_size.1;/* modify */for i in 0..screen_rows {print!("~");if i < screen_rows - 1 {println!("\r")}stdout().flush();}/* end */}fn refresh_screen(&self) -> crossterm::Result<()> {Self::clear_screen()?;self.draw_rows();execute!(stdout(), cursor::MoveTo(0, 0))}
}
3.7 追加缓冲区
由于在屏幕每次刷新时都会进行绘制,导致有闪频的问题。
struct EditorContents {content: String,
}impl EditorContents {fn new() -> Self {Self {content: String::new(),}}fn push(&mut self, ch: char) {self.content.push(ch)}fn push_str(&mut self, string: &str) {self.content.push_str(string)}
}
impl io::Write for EditorContents {fn write(&mut self, buf: &[u8]) -> io::Result<usize> {match std::str::from_utf8(buf) {Ok(s) => {self.content.push_str(s);Ok(s.len())}Err(_) => Err(io::ErrorKind::WriteZero.into()),}}fn flush(&mut self) -> io::Result<()> {let out = write!(stdout(), "{}", self.content);stdout().flush()?;self.content.clear();out}
}
- 首先,我们将传递到
write
函数的字节转换为str
,以便我们可以将其添加到content
。- 如果字节可以转换为字符串,则返回字符串的长度,否则返回错误。当我们在
EditorContents
上调用flush()
时,我们希望它写入stdout
,因此我们使用write!()
宏,然后调用stdout.flush()
。- 我们还必须清除
content
,以便我们可以在下一次屏幕刷新时使用。
- 使用
EditorContents
:
use crossterm::{cursor, event, execute, queue, terminal}; /* modify */struct Output {win_size: (usize, usize),editor_contents: EditorContents, /* add this line */
}impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self {win_size,editor_contents: EditorContents::new(),}}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&mut self) { /* modify */let screen_rows = self.win_size.1;for i in 0..screen_rows {self.editor_contents.push('~'); /* modify */if i < screen_rows - 1 {self.editor_contents.push_str("\r\n"); /* modify */}}}fn refresh_screen(&mut self) -> crossterm::Result<()> { /* modify */queue!(self.editor_contents, terminal::Clear(ClearType::All), cursor::MoveTo(0, 0))?; /* add this line*/self.draw_rows();queue!(self.editor_contents, cursor::MoveTo(0, 0))?; /* modify */self.editor_contents.flush() /* add this line*/}
}
注意,我们已更改
draw_rows
为使用&mut self
,因此我们需要对之前的部分代码做一下调整:
fn run(&mut self) -> crossterm::Result<bool> { /* modify */self.output.refresh_screen()?;self.process_keypress()
}
fn main() -> crossterm::Result<()> {let _clean_up = CleanUp;terminal::enable_raw_mode()?;let mut editor = Editor::new(); /* modify */while editor.run()? {}Ok(())
}
烦人的闪烁效果还有另一个可能的来源。当终端绘制到屏幕时,光标可能会在屏幕中间的某个地方显示一瞬间。
为确保不会发生这种情况,让我们在刷新屏幕之前隐藏光标,并在刷新完成后立即再次显示光标。
impl Output {fn new() -> Self {let win_size = terminal::size().map(|(x, y)| (x as usize, y as usize)).unwrap();Self {win_size,editor_contents: EditorContents::new(),}}fn clear_screen() -> crossterm::Result<()> {execute!(stdout(), terminal::Clear(ClearType::All))?;execute!(stdout(), cursor::MoveTo(0, 0))}fn draw_rows(&mut self) {let screen_rows = self.win_size.1;for i in 0..screen_rows {self.editor_contents.push('~');if i < screen_rows - 1 {self.editor_contents.push_str("\r\n");}}}fn refresh_screen(&mut self) -> crossterm::Result<()> {queue!(self.editor_contents,cursor::Hide, //add thisterminal::Clear(ClearType::All),cursor::MoveTo(0, 0))?;self.draw_rows();queue!(self.editor_contents,cursor::MoveTo(0, 0),/* add this */ cursor::Show)?;self.editor_contents.flush()}
}
本期完,下期内容抢先知:
- 逐行清除
- 添加欢迎和版本信息
- 按键移动光标
- 方向键移动光标
- 光标移动溢出问题
- 分页和首尾页