在 Rust 中实现终端 I/O

更新于 2026-01-17

Packt 2021-05-05

Rust 是一门现代的开源系统编程语言,它融合了三大优势:Java 的类型安全性、C++ 的速度、表达力与效率,以及无需垃圾回收器即可实现的内存安全。在本文中,我们将探讨如何使用 Rust 构建基于终端的应用程序。

终端应用程序是许多软件程序(包括游戏、文本编辑器和终端模拟器)的重要组成部分。要开发这类程序,理解如何构建自定义的终端界面应用程序非常有帮助。我们将回顾终端工作的基本原理,然后学习如何在终端上执行各种操作,例如设置颜色和样式、执行光标操作(如清屏和定位),以及处理键盘和鼠标输入。

我们将涵盖以下主题:

  • 介绍终端 I/O 基础知识
  • 使用终端 UI(尺寸、颜色、样式)和光标
  • 处理键盘输入和滚动
  • 处理鼠标输入

本文的大部分内容将通过一个实际示例来解释这些概念。我们将构建一个微型文本查看器,以演示终端操作的关键概念。该文本查看器能够从磁盘加载文件并在终端界面上显示其内容。它还允许用户使用键盘上的方向键滚动浏览内容,并在页眉和页脚栏中显示信息。

技术要求

本文代码的 Git 仓库位于这里

对于 Windows 平台的用户,需要安装虚拟机,因为本文使用的第三方 crate(Termion)目前不支持 Windows 平台(截至撰写本文时)。建议安装 VirtualBox 或类似虚拟机,并在其上运行 Linux 系统以配合代码开发。VirtualBox 的安装说明请参见这里

在终端操作方面,Rust 提供了多种功能用于读取按键并控制进程的标准输入和输出。当用户在命令行中键入字符时,只有在按下 Enter 键后,生成的字节才会被程序接收。这对某些类型的程序很有用。但对于游戏或文本编辑器等需要更精细控制的程序而言,程序必须在用户键入每个字符时立即处理,这被称为 原始模式(raw mode)。目前已有多个第三方 crate 可简化原始模式的处理。我们将使用其中一个名为 Termion 的 crate。


介绍终端 I/O 基础知识

在本节中,我们将介绍终端的关键特性,概述 Termion crate,并定义本项目将要构建的内容范围。

终端的特性

终端是用户与计算机交互的设备。通过终端,用户可以获得命令行访问权限,从而与计算机的操作系统进行交互。通常,shell 作为控制程序,一方面驱动终端,另一方面充当操作系统接口。

最初,UNIX 系统通过连接到串行线路的终端(也称为控制台)进行访问。这些终端通常具有 24 行 × 80 列的字符界面,有些情况下还具备基本的图形能力。为了在终端上执行操作(如清屏或移动光标),会使用特定的转义序列。

终端有两种工作模式:

  • 规范模式(Canonical mode):在此模式下,用户的输入按行处理,只有在用户按下 Enter 键后,字符才会被发送给程序处理。
  • 非规范模式(Non-canonical 或 raw mode):在原始模式下,终端输入不会按行收集,程序可以读取用户键入的每个字符。

终端既可以是物理设备,也可以是虚拟设备。如今大多数终端都是 伪终端(pseudo-terminals),即一端连接到终端设备,另一端连接到驱动该终端设备的程序的虚拟设备。伪终端使我们能够编写程序,让用户在一台主机上通过网络通信执行另一台主机上的终端导向程序。伪终端应用的一个例子是 SSH(Secure Shell Protocol),它允许用户通过网络登录远程主机。

终端管理包括在终端屏幕上执行以下操作的能力:

  • 颜色管理:设置终端的各种前景色和背景色,并将颜色重置为默认值。
  • 样式管理:将文本样式设置为粗体、斜体、下划线等。
  • 光标管理:将光标设置到特定位置、保存当前光标位置、显示和隐藏光标,以及其他特殊功能(如闪烁光标)。
  • 事件处理:监听并响应键盘和鼠标事件。
  • 屏幕处理:在主屏幕和备用屏幕之间切换,以及清屏。
  • 原始模式:将终端切换到原始模式。

在本文中,我们将结合使用 Rust 标准库和 Termion crate 来开发一个面向终端的应用程序。接下来,让我们看看 Termion crate。


Termion crate

Termion crate 提供了上一节列出的功能,同时还为用户提供了易于使用的命令行接口(CLI)。

为什么要使用外部 crate 进行终端管理?

虽然从技术上讲,可以使用 Rust 标准库在字节级别进行操作,但这很繁琐。像 Termion 这样的外部 crate 能帮助我们将单个字节组合成按键事件,并实现许多常用的终端管理功能,使我们能够专注于更高层次的、面向用户的功能。

下面我们讨论 Termion crate 的几个终端管理功能。该 crate 的官方文档位于:https://docs.rs/termion/。

Termion crate 包含以下关键模块:

  • cursor:用于移动光标
  • event:用于处理按键和鼠标事件
  • raw:用于将终端切换到原始模式
  • style:用于设置文本的各种样式
  • clear:用于清空整个屏幕或单行
  • color:用于为文本设置各种颜色
  • input:用于处理高级用户输入
  • scroll:用于在屏幕上滚动

要包含 Termion crate,请创建一个新项目,并在 Cargo.toml 中添加以下条目:

[dependencies]
termion = "1.5.5"

以下是几个 Termion 使用示例的代码片段:

  • 获取终端尺寸:

    termion::terminal_size()
    
  • 设置前景色:

    println!("{}", color::Fg(color::Blue));
    
  • 设置背景色,然后将其重置为原始状态:

    println!(
        "{}Background{} ",
        color::Bg(color::Cyan),
        color::Bg(color::Reset)
    );
    
  • 设置粗体样式:

    println!(
        "{}You can see me in bold?",
        style::Bold
    );
    
  • 将光标设置到特定位置:

    termion::cursor::Goto(5, 10)
    
  • 清空屏幕:

    print!("{}", termion::clear::All);
    

我们将在接下来的章节中通过一个实际示例使用这些终端管理功能。现在,让我们定义将要构建的内容。


我们将构建什么?

我们将开发一个微型文本查看器应用程序。该应用程序提供一个终端文本界面,用于从目录位置加载文档并查看文档。用户可以使用键盘按键滚动浏览文档。我们将分多个代码迭代逐步构建该项目。

图 1 展示了我们将要构建的屏幕布局:

layout

文本查看器的终端界面包含三个组件:

  • 页眉栏:包含文本编辑器的标题。
  • 文本区域:包含要显示的文本行。
  • 页脚栏:显示光标位置、文件中的行数以及正在显示的文件名。

文本查看器将允许用户执行以下操作:

  • 用户可以通过命令行参数提供文件名以进行显示。这必须是一个已存在的有效文件名。如果文件不存在,程序将显示错误消息并退出。
  • 文本查看器将加载文件内容并在终端上显示。如果文件中的行数超过终端高度,程序将允许用户滚动浏览文档,并重绘下一组行。
  • 用户可以使用上、下、左、右方向键在终端中滚动。
  • 用户可以按 Ctrl + Q 退出文本查看器。

一个成熟的文本查看器会有更多功能,但这个核心范围为我们学习如何在 Rust 中开发面向终端的应用程序提供了充分的机会。

在本节中,我们了解了终端是什么以及它们支持哪些功能。我们还简要介绍了如何使用 Termion crate,并定义了项目将要构建的内容。在下一节中,我们将开发文本查看器的第一个迭代版本。


使用终端 UI(尺寸、颜色、样式)和光标

在本节中,我们将构建文本查看器的第一个迭代版本。在本节结束时,我们将拥有一个可以从命令行接受文件名、显示其内容,并显示页眉和页脚栏的程序。我们将使用 Termion crate 来设置颜色和样式、获取终端尺寸、将光标定位到特定坐标,以及清屏。

本节的代码组织如下:

  • 编写数据结构和 main() 函数
  • 初始化文本查看器并获取终端尺寸
  • 显示文档并设置终端颜色、样式和光标位置
  • 退出文本查看器

让我们从数据结构和 main() 函数开始。

编写数据结构和 main() 函数

在本节中,我们将定义在内存中表示文本查看器所需的数据结构。我们还将编写 main() 函数,用于协调和调用其他各种函数:

  1. 创建一个新项目并切换到该目录:

    cargo new tui && cd tui
    

    其中 tui 代表终端用户界面(Terminal User Interface)。在 src/bin 下创建一个名为 textviewer1.rs 的新文件。

  2. Cargo.toml 中添加以下内容:

    [dependencies]
    termion = "1.5.5"
    
  3. 首先从标准库和 Termion crate 导入所需的模块:

    use std::env::args;
    use std::fs;
    use std::io::{stdin, stdout, Write};
    use termion::event::Key;
    use termion::input::TermRead;
    use termion::raw::IntoRawMode;
    use termion::{color, style};
    
  4. 接下来定义用于表示文本查看器的数据结构:

    struct Doc {
        lines: Vec<String>,
    }
    
    #[derive(Debug)]
    struct Coordinates {
        pub x: usize,
        pub y: usize,
    }
    
    struct TextViewer {
        doc: Doc,
        doc_length: usize,
        cur_pos: Coordinates,
        terminal_size: Coordinates,
        file_name: String,
    }
    

    此代码定义了三个用于文本查看器的数据结构:

    • 要在查看器中显示的文档定义为 Doc 结构体,它是一个字符串向量。
    • 为了存储光标位置的 x 和 y 坐标,并记录当前终端的尺寸(字符的总行数和列数),我们定义了一个 Coordinates 结构体。
    • TextViewer 结构体是表示文本查看器的主要数据结构。文件中包含的行数记录在 doc_length 字段中。要在查看器中显示的文件名记录在 file_name 字段中。
  5. 现在定义 main() 函数,这是文本查看器应用程序的入口点:

    fn main() {
        // 从命令行获取参数
        let args: Vec<String> = args().collect();
        if args.len() < 2 {
            println!("Please provide file name as argument");
            std::process::exit(0);
        }
        // 检查文件是否存在。如果不存在,打印错误消息并退出进程
        if !std::path::Path::new(&args[1]).exists() {
            println!("File does not exist");
            std::process::exit(0);
        }
        // 打开文件并加载到结构体中
        println!("{}", termion::cursor::Show);
        // 初始化查看器
        let mut viewer = TextViewer::init(&args[1]);
        viewer.show_document();
        viewer.run();
    }
    

    main() 函数接受一个文件名作为命令行参数,如果文件不存在则退出程序。此外,如果没有提供文件名作为命令行参数,它将显示错误消息并退出程序。

  6. 如果找到文件,main() 函数将执行以下操作:

    • 首先调用 TextViewer 结构体上的 init() 方法以初始化变量。
    • 然后调用 show_document() 方法在终端屏幕上显示文件内容。
    • 最后调用 run() 方法,等待用户输入。如果用户按下 Ctrl + Q,程序将退出。
  7. 现在我们将编写三个方法签名 —— init()show_document()run()。这三个方法应添加到 TextViewer 结构体的 impl 块中,如下所示:

    impl TextViewer {
        fn init(file_name: &str) -> Self {
            //...
        }
        fn show_document(&mut self) {
            // ...
        }
        fn run(&mut self) {
            // ...
        }
    }
    

到目前为止,我们已经定义了数据结构并编写了带有其他函数占位符的 main() 函数。在下一节中,我们将编写初始化文本查看器的函数。


初始化文本查看器并获取终端尺寸

当用户使用文档名称启动文本查看器时,我们需要用一些信息初始化文本查看器并执行启动任务。这就是 init() 方法的目的。

以下是 init() 方法的完整代码:

fn init(file_name: &str) -> Self {
    let mut doc_file = Doc { lines: vec![] };                   // <1>
    let file_handle = fs::read_to_string(file_name).unwrap();   // <2>
    for doc_line in file_handle.lines() {                       // <3>
        doc_file.lines.push(doc_line.to_string());
    }
    let mut doc_length = file_handle.lines().count();           // <4>
    let size = termion::terminal_size().unwrap();               // <5>
    Self {                                                      // <6>
        doc: doc_file,
        cur_pos: Coordinates {
            x: 1,
            y: doc_length,
        },
        doc_length: doc_length,
        terminal_size: Coordinates {
            x: size.0 as usize,
            y: size.1 as usize,
        },
        file_name: file_name.into(),
    }
}

init() 方法中的代码注释说明如下:

  1. 初始化用于存储文件内容的缓冲区。
  2. 将文件内容读取为字符串。
  3. 从文件中读取每一行并存储在 Doc 缓冲区中。
  4. 使用文件中的行数初始化 doc_length 变量。
  5. 使用 Termion crate 获取终端尺寸。
  6. 创建一个新的 TextViewer 类型结构体并从 init() 方法返回。

我们已经编写了文本查看器的初始化代码。接下来,我们将编写代码在终端屏幕上显示文档内容,并显示页眉和页脚。


显示文档并设置终端颜色、样式和光标位置

我们之前看到了想要构建的文本查看器布局。文本查看器屏幕布局有三个主要部分 —— 页眉、文档区域和页脚。在本节中,我们将编写主函数和支持函数,按照定义的屏幕布局显示内容。

让我们看看 show_document() 方法:

src/bin/text-viewer1.rs

fn show_document(&mut self) {
    let pos = &self.cur_pos;
    let (old_x, old_y) = (pos.x, pos.y);
    print!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1));
    println!(
        "{}{}Welcome to Super text viewer\r{}",
        color::Bg(color::Black),
        color::Fg(color::White),
        style::Reset
    );
    for line in 0..self.doc_length {
        println!("{}\r", self.doc.lines[line as usize]);
    }
    println!(
        "{}",
        termion::cursor::Goto(0, (self.terminal_size.y - 2) as u16),
    );
    println!(
        "{}{} line-count={} Filename: {}{}",
        color::Fg(color::Red),
        style::Bold,
        self.doc_length,
        self.file_name,
        style::Reset
    );
    self.set_pos(old_x, old_y);
}

show_document() 方法的代码注释说明如下:

  1. 将光标的当前 x 和 y 坐标位置存储在临时变量中。这将在稍后的步骤中用于恢复光标位置。
  2. 使用 Termion crate 清空整个屏幕并将光标移动到屏幕的第 1 行第 1 列。
  3. 打印文本查看器的页眉栏。使用黑色背景和白色前景打印文本。
  4. 将内部文档缓冲区中的每一行显示到终端屏幕。
  5. 将光标移动到屏幕底部(使用终端尺寸的 y 坐标)以打印页脚。
  6. 以红色和粗体样式打印页脚文本。在页脚中打印文档的行数和文件名。
  7. 将光标重置到原始位置(该位置已在步骤 1 中保存到临时变量中)。

让我们看看 show_document() 方法使用的 set_pos() 辅助方法:

src/bin/text-viewer1.rs

fn set_pos(&mut self, x: usize, y: usize) {
    self.cur_pos.x = x;
    self.cur_pos.y = y;
    println!(
        "{}",
        termion::cursor::Goto(self.cur_pos.x as u16, (self.cur_pos.y) as u16)
    );
}

此辅助方法同步内部光标跟踪字段(TextViewer 结构体的 cur_pos 字段)和屏幕上的光标位置。

我们现在有了初始化文本查看器和在屏幕上显示文档的代码。这样,用户就可以在文本查看器中打开文档并查看其内容。但是用户如何退出文本查看器呢?我们将在下一节中找到答案。


退出文本查看器

假设用户按下 Ctrl + Q 组合键即可退出文本查看器程序。我们该如何实现这段代码?

要实现这一点,我们需要一种方法来监听用户按键,当按下特定组合键时,我们应该退出程序。如前所述,我们需要将终端置于原始模式,在这种模式下,每个字符都可供程序评估,而不是等待用户按下 Enter 键。一旦我们获得原始字符,其余部分就相当简单了。让我们在 impl TextViewer 块中的 run() 方法中编写此代码,如下所示:

src/bin/text-viewer1.rs

fn run(&mut self) {
    let mut stdout = stdout().into_raw_mode().unwrap();
    let stdin = stdin();
    for c in stdin.keys() {
        match c.unwrap() {
            Key::Ctrl('q') => {
                break;
            }
            _ => {}
        }
        stdout.flush().unwrap();
    }
}

在上述代码中,我们使用 stdin.keys() 方法在循环中监听用户输入。stdout() 用于向终端显示文本。按下 Ctrl + Q 时,程序退出。

我们现在可以使用以下命令运行程序:

cargo run --bin text-viewer1 <file-name-with-full-path>

由于我们尚未实现滚动功能,请向程序传递一个包含 24 行或更少内容的文件名(这通常是标准终端在行数方面的默认高度)。您将看到文本查看器打开,并在终端上打印页眉栏、页脚栏和文件内容。按 Ctrl + Q 退出。请注意,您必须将文件名作为命令行参数指定完整路径。

在本节中,我们学习了如何使用 Termion crate 获取终端尺寸、设置前景色和背景色,以及应用粗体样式。我们还学习了如何将光标定位到屏幕上的指定坐标,以及如何清屏。

在下一节中,我们将研究如何处理按键以实现用户在文本编辑器中显示的文档内导航,以及如何实现滚动。


处理键盘输入和滚动

在上一节中,我们构建了文本查看器终端导向应用程序的第一个迭代版本。我们能够显示少于 24 行的文件,并看到包含一些信息的页眉和页脚栏。最后,我们能够使用 Ctrl + Q 退出程序。

在本节中,我们将为文本查看器添加以下功能:

  • 提供显示任意大小文件的能力。
  • 提供用户使用方向键滚动浏览文档的能力。
  • 在页脚栏中添加光标位置坐标。

让我们首先创建代码的新版本。

将原始代码复制到一个新文件中,如下所示:

cp src/bin/text-viewer1.rs src/bin/text-viewer2.rs

本节分为三个部分。首先,我们将实现逻辑以响应用户的以下按键:上、下、左、右和退格键。接下来,我们将实现更新内部数据结构中光标位置的功能,并同时更新屏幕上的光标位置。最后,我们将允许多页文档的滚动。

我们从处理用户按键开始。

监听用户按键

让我们修改 run() 方法以响应用户输入并在文档中滚动。我们还希望记录并在页脚栏中显示当前光标位置。代码如下所示:

src/bin/text-viewer2.rs

fn run(&mut self) {
    let mut stdout = stdout().into_raw_mode().unwrap();
    let stdin = stdin();
    for c in stdin.keys() {
        match c.unwrap() {
            Key::Ctrl('q') => {
                break;
            }
            Key::Left => {
                self.dec_x();
                self.show_document();
            }
            Key::Right => {
                self.inc_x();
                self.show_document();
            }
            Key::Up => {
                self.dec_y();
                self.show_document();
            }
            Key::Down => {
                self.inc_y();
                self.show_document();
            }
            Key::Backspace => {
                self.dec_x();
            }
            _ => {}
        }
        stdout.flush().unwrap();
    }
}

加粗的行显示了 run() 方法相对于早期版本的更改。在此代码中,我们监听上、下、左、右和退格键。对于这些按键中的任何一个,我们都使用以下方法之一适当地增加或减少 x 或 y 坐标:inc_x()inc_y()dec_x()dec_y()。例如,如果按下右箭头,则使用 inc_x() 方法增加光标位置的 x 坐标;如果按下向下箭头,则仅使用 inc_y() 方法增加 y 坐标。对坐标的更改记录在内部数据结构中(TextViewer 结构体的 cur_pos 字段)。同时,光标在屏幕上重新定位。所有这些都由 inc_x()inc_y()dec_x()dec_y() 方法实现。

更新光标位置后,屏幕将完全刷新并重绘。

让我们看看实现四个方法以更新光标坐标的代码,并在屏幕上重新定位光标。


定位终端光标

让我们编写 inc_x()inc_y()dec_x()dec_y() 方法的代码。这些方法应像其他方法一样添加到 impl TextViewer 代码块中:

src/bin/text-viewer2.rs

fn inc_x(&mut self) {
    if self.cur_pos.x < self.terminal_size.x {
        self.cur_pos.x += 1;
    }
    println!(
        "{}",
        termion::cursor::Goto(self.cur_pos.x as u16, self.cur_pos.y as u16)
    );
}

fn dec_x(&mut self) {
    if self.cur_pos.x > 1 {
        self.cur_pos.x -= 1;
    }
    println!(
        "{}",
        termion::cursor::Goto(self.cur_pos.x as u16, self.cur_pos.y as u16)
    );
}

fn inc_y(&mut self) {
    if self.cur_pos.y < self.doc_length {
        self.cur_pos.y += 1;
    }
    println!(
        "{}",
        termion::cursor::Goto(self.cur_pos.x as u16, self.cur_pos.y as u16)
    );
}

fn dec_y(&mut self) {
    if self.cur_pos.y > 1 {
        self.cur_pos.y -= 1;
    }
    println!(
        "{}",
        termion::cursor::Goto(self.cur_pos.x as u16, self.cur_pos.y as u16)
    );
}

这四个方法的结构相似,每个方法仅执行两个步骤:

  1. 根据按键,相应地增加或减少坐标(x 或 y),并记录在 cur_pos 内部变量中。
  2. 将光标在屏幕上重新定位到新坐标。

我们现在有了在用户按下上、下、左、右或退格键时更新光标坐标的机制。但这还不够。光标应重新定位到屏幕上的最新光标坐标。为此,我们将不得不更新 show_document() 方法,我们将在下一节中进行。


启用终端滚动

到目前为止,我们已经实现了监听用户按键并在屏幕上重新定位光标的代码。现在,让我们关注代码中的另一个主要问题。如果我们加载的文档行数少于终端高度,那么代码可以正常工作。但考虑一种情况:终端最多可显示 24 行字符,而要显示在文本查看器中的文档有 50 行。我们的代码无法处理这种情况。我们将在本节中修复它。

要显示比屏幕尺寸更多的行,仅仅重新定位光标是不够的。我们将不得不根据光标位置重绘屏幕,以在终端屏幕上显示文档的一部分。让我们看看为启用滚动而对 show_document() 方法所做的修改。在 show_document() 方法中查找以下代码行:

for line in 0..self.doc_length {
    println!("{}\r", self.doc.lines[line as usize]);
}

将上述代码替换为以下代码:

src/bin/text-viewer2.rs

if self.doc_length < self.terminal_size.y {                     // <1>
    for line in 0..self.doc_length {
        println!("{}\r", self.doc.lines[line as usize]);
    }
} else {
    if pos.y <= self.terminal_size.y {                          // <2>
        for line in 0..self.terminal_size.y - 3 {
            println!("{}\r", self.doc.lines[line as usize]);
        }
    } else {
        for line in pos.y - (self.terminal_size.y - 3)..pos.y {
            println!("{}\r", self.doc.lines[line as usize]);
        }
    }
}

show_document() 方法代码片段中的注释说明如下:

  1. 首先检查输入文档中的行数是否小于终端高度。如果是,则在终端屏幕上显示输入文档中的所有行。
  2. 如果输入文档中的行数大于终端高度,我们必须分部分显示文档。最初,在屏幕上显示文档的第一组行,对应于适合终端高度的行数。例如,如果我们为文本显示区域分配 21 行,那么只要光标在这些行内,就会显示原始行集。如果用户进一步向下滚动,则会在屏幕上显示下一组行。

让我们使用以下命令运行程序:

cargo run --bin text-viewer2 <file-name-with-full-path>

您可以尝试两种类型的文件输入:

  • 行数少于终端高度的文件
  • 行数多于终端高度的文件

您可以使用上、下、左、右箭头滚动浏览文档并查看内容。您还会在页脚栏中看到当前光标位置(x 和 y 坐标)。按 Ctrl + Q 退出。

至此,文本查看器项目已完成。您已经构建了一个功能性的文本查看器,可以显示任意大小的文件,并使用方向键滚动浏览其内容。您还可以在页脚栏中查看光标当前位置以及文件名和行数。

关于文本查看器的说明

请注意,我们实现的是一个不到 200 行代码的微型文本查看器。虽然它展示了关键功能,但您可以实现其他功能和边缘情况,以增强应用程序并提高其可用性。此外,此查看器也可以转换为功能齐全的文本编辑器。这些留给读者作为练习。

我们在本节中完成了文本查看器项目的实现。文本查看器是一个经典的命令行应用程序,缺乏图形用户界面(GUI),因此不需要鼠标输入。但学习如何处理鼠标事件对于开发基于 GUI 的终端界面非常重要。我们将在下一节中学习如何做到这一点。


处理鼠标输入

与键盘事件一样,Termion crate 也支持监听鼠标事件、跟踪鼠标光标位置并在代码中做出反应。让我们看看如何在这里实现这一点。

src/bin 下创建一个名为 mouse-events.rs 的新源文件。

代码逻辑如下:

  • 导入所需的模块。
  • 启用终端的鼠标支持。
  • 清空屏幕。
  • 创建一个传入事件的迭代器。
  • 监听鼠标按下、释放和按住事件,并在终端屏幕上显示鼠标光标位置。

代码将按照每个要点进行解释。

首先看模块导入:

我们导入了 Termion crate 模块,用于切换到原始模式、检测光标位置和监听鼠标事件:

use std::io::{self, Write};
use termion::cursor::{self, DetectCursorPos};
use termion::event::*;
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;

main() 函数中,让我们启用鼠标支持,如下所示:

fn main() {
    let stdin = io::stdin();
    let mut stdout = MouseTerminal::from(io::stdout().into_raw_mode().unwrap());
    // ...其他未显示的代码
}

为确保终端屏幕上的先前文本不会干扰此程序,让我们清空屏幕,如下所示:

writeln!(
    stdout,
    "{}{} Type q to exit.",
    termion::clear::All,
    termion::cursor::Goto(1, 1)
).unwrap();
  1. 接下来,让我们创建一个传入事件的迭代器并监听鼠标事件。在终端上显示鼠标光标位置:
for c in stdin.events() {
    let evt = c.unwrap();
    match evt {
        Event::Key(Key::Char('q')) => break,
        Event::Mouse(m) => match m {
            MouseEvent::Press(_, a, b) |
            MouseEvent::Release(a, b) |
            MouseEvent::Hold(a, b) => {
                write!(stdout, "{}", cursor::Goto(a, b)).unwrap();
                let (x, y) = stdout.cursor_pos().unwrap();
                write!(
                    stdout,
                    "{}{}Cursor is at: ({},{}){}",
                    cursor::Goto(5, 5),
                    termion::clear::UntilNewline,
                    x,
                    y,
                    cursor::Goto(a, b)
                ).unwrap();
            }
        },
        _ => {}
    }
    stdout.flush().unwrap();
}

在上述代码中,我们同时监听键盘事件和鼠标事件。在键盘事件中,我们特别寻找 Q 键,它会退出程序。我们还监听鼠标事件 —— 按下、释放和按住。在这种情况下,我们将光标定位到指定坐标,并在终端屏幕上打印出坐标。

  1. 使用以下命令运行程序:
cargo run --bin mouse-events
  1. 在屏幕上点击鼠标,您将看到光标位置坐标显示在终端屏幕上。按 q 退出。

至此,我们结束了关于终端鼠标事件处理的部分,也结束了这篇关于使用 Rust 进行终端 I/O 管理的介绍。


总结

在本文中,我们通过编写一个微型文本查看器学习了终端管理的基础知识。我们了解了如何使用 Termion 库获取终端尺寸、设置前景色和背景色,以及设置样式。之后,我们研究了如何在终端上使用光标,包括清屏、将光标定位到特定坐标集,以及跟踪当前光标位置。

您已经学会了如何监听用户输入并跟踪键盘方向键以进行滚动操作,包括左、右、上、下。我们编写了代码,根据用户滚动动态显示文档内容,同时考虑终端尺寸的限制。作为练习,您可以优化文本查看器,并添加功能将其转换为功能齐全的编辑器。

学习这些功能对于编写终端游戏、编辑和查看应用程序、终端图形界面以及开发基于终端的仪表板非常重要。