#ratatui #editor #input #textarea #key-input #tui

已废弃 ratatui-textarea

[已废弃] ratatui 是一个简单而强大的 ratatui 文本编辑小部件。多行文本编辑器可以轻松地集成到您的 ratatui 应用程序中。由 tui-textarea 分支。

5 个版本

0.4.1 2024 年 1 月 22 日
0.4.0 2023 年 10 月 27 日
0.3.2 2023 年 9 月 17 日
0.3.1 2023 年 9 月 15 日
0.3.0 2023 年 9 月 15 日

4#textarea

每月 36 次下载
2 crates 中使用

MIT 许可证

135KB
2K SLoC

ratatui-textarea

crate CI CD

[!warning] 此项目不再维护,tui-textarea 现在支持最新的 ratatui 版本

ratatui-textarea 是一个简单而强大的文本编辑小部件,类似于 HTML 中的 <textarea>,用于 ratatui(ratatui 是从 tui-textarea 分支出来的)。多行文本编辑器可以轻松地集成到您的 ratatui 应用程序中。

功能

  • 带基本操作(插入/删除字符、自动滚动等)的多行文本编辑器小部件
  • 类似 Emacs 的快捷键(C-n/C-p/C-f/C-bM-f/M-bC-a/C-eC-h/C-dC-kM</M->,...)
  • 撤销/重做
  • 行号
  • 光标行高亮显示
  • 使用正则表达式搜索
  • 鼠标滚动
  • 支持剪切。使用 C-kC-j 等删除的文本可以粘贴
  • 后端无关。支持 crosstermtermion 以及您自己的后端
  • 同一屏幕上的多个 textarea 小部件

文档

示例

在当前仓库中运行 cargo run --example 可以展示 ratatui-textarea 的用法。

最小化

cargo run --example minimal

带有 crossterm 支持的最小化用法。

minimal example

编辑器

cargo run --example editor --features search file.txt

简单的文本编辑器,用于编辑多个文件。

editor example

单行

cargo run --example single_line

具有浮点数验证的单行输入表单。

single line example

分割

cargo run --example split

屏幕上两个分割的文本区域,并在它们之间切换。这是多个文本区域实例的示例。

multiple textareas example

termion

cargo run --example termion --features=termion

带有 termion 支持的最小化用法。

变量

cargo run --example variable

具有根据行数变化的简单文本区域高度。

模式

cargo run --example modal

类似于 vi 的简单模式文本编辑器。

安装

ratatui-textarea crate 添加到 Cargo.toml 中的依赖项。这默认启用 crossterm 后端支持。

[dependencies]
ratatui = "*"
ratatui-textarea = "*"

如果您需要使用正则表达式进行文本搜索,请启用 search 功能。它将 regex crate crate 作为依赖项添加。

[dependencies]
ratatui = "*"
ratatui-textarea = { version = "*", features = ["search"] }

如果您正在使用 ratatui 与 termion 一起使用,请启用 termion 功能而不是 crossterm 功能。

[dependencies]
ratatui = { version = "*", default-features = false, features = ["termion"] }
ratatui-textarea = { version = "*", default-features = false, features = ["termion"] }

除了上述依赖项之外,您还需要安装 crosstermtermion 来初始化您的应用程序并接收按键输入。有关更多详细信息,请参阅 crosstermtermion 文档。

最小化用法

use ratatui_textarea::TextArea;
use crossterm::event::{Event, read};

let mut term = ratatui::Terminal::new(...);

// Create an empty `TextArea` instance which manages the editor state
let mut textarea = TextArea::default();

// Event loop
loop {
    term.draw(|f| {
        // Get `ratatui::layout::Rect` where the editor should be rendered
        let rect = ...;
        // `TextArea::widget` builds a widget to render the editor with ratatui
        let widget = textarea.widget();
        // Render the widget in terminal screen
        f.render_widget(widget, rect);
    })?;

    if let Event::Key(key) = read()? {
        // Your own key mapping to break the event loop
        if key.code == KeyCode::Esc {
            break;
        }
        // `TextArea::input` can directly handle key events from backends and update the editor state
        textarea.input(key);
    }
}

// Get text lines as `&[String]`
println!("Lines: {:?}", textarea.lines());

TextArea 是一个管理编辑器状态的实例。默认情况下,它禁用行号并以下划线突出显示光标行。

TextArea::widget() 构建一个小部件以渲染编辑器的当前状态。在每个事件循环的tick上创建小部件并渲染它。

TextArea::input() 接收来自 ratatui 后端的输入。如果启用了功能,此方法可以直接从后端(如 crossterm::event::KeyEventtermion::event::Key)获取按键事件。该方法还处理默认按键映射。

默认按键映射如下

映射 描述
Ctrl+HBackspace 删除光标前的一个字符
Ctrl+DDelete 删除光标旁边的一个字符
Ctrl+MEnter 插入换行符
Ctrl+K 从光标处删除到行尾
Ctrl+J 从光标处删除到行首
Ctrl+WAlt+HAlt+Backspace 删除光标前的一个单词
Alt+DAlt+Delete 删除光标旁边的一个单词
Ctrl+U 撤销
Ctrl+R 重做
Ctrl+Y 粘贴已复制的文本
Ctrl+F 将光标向前移动一个字符
Ctrl+B 将光标向后移动一个字符
Ctrl+P 将光标向上移动一行
Ctrl+N 将光标向下移动一行
Alt+FCtrl+ 将光标向前移动一个单词
Alt+BCtrl+ 将光标向后移动一个单词
Alt+]Alt+PCtrl+ 将光标向上移动一个段落
Alt+[Alt+NCtrl+ 将光标向下移动一个段落
Ctrl+EEndCtrl+Alt+FCtrl+Alt+ 将光标移动到行尾
Ctrl+AHomeCtrl+Alt+BCtrl+Alt+ 将光标移动到行首
Alt+<Ctrl+Alt+PCtrl+Alt+ 将光标移动到文本开头
Alt+>Ctrl+Alt+NCtrl+Alt+ 将光标移动到文本结尾
Ctrl+VPageDown 向下滚动一页
Alt+VPageUp 向上滚动一页

一次删除多个字符时,会将删除的文本保存到剪切板。之后可以用 Ctrl+Y 粘贴。

如果您不想使用默认的键映射,请参阅“高级用法”部分。

基本用法

使用文本创建 TextArea 实例

TextArea 实现了 Default 特性,以创建一个包含空文本的编辑器实例。

let mut textarea = TextArea::default();

TextArea::new() 创建一个包含通过 Vec<String> 传递的文本行作为文本的编辑器实例。

let mut lines: Vec<String> = ...;
let mut textarea = TextArea::new(lines);

TextArea实现了From<实现Iterator<Item=实现Into<String>>>TextArea::from()可以从任何可以转换为String的迭代器中创建编辑器实例。

// Create `TextArea` from from `[&str]`
let mut textarea = TextArea::from([
    "this is first line",
    "this is second line",
    "this is third line",
]);

// Create `TextArea` from `String`
let mut text: String = ...;
let mut textarea = TextARea::from(text.lines());

TextArea还实现了FromIterator<实现Into<String>>Iterator::collect()可以将字符串收集为编辑器实例。这允许使用io::BufReader高效地从文件中读取行来创建TextArea

let file = fs::File::open(path)?;
let mut textarea: TextArea = io::BufReader::new(file).lines().collect::<io::Result<_>>()?;

TextArea获取文本内容

TextArea::lines()返回文本行作为&[String]。它临时借用文本内容。

let text: String = textarea.lines().join("\n");

TextArea::into_lines()TextArea实例移动到文本行作为Vec<String>。这样可以检索文本内容而不进行任何复制。

let lines: Vec<String> = textarea.into_lines();

请注意,TextArea始终包含至少一行。例如,空文本表示一行空行。这是因为任何文本文件都必须以换行符结束。

let textarea = TextArea::default();
assert_eq!(textarea.into_lines(), [""]);

显示行号

默认情况下,TextArea不显示行号。要启用,请通过TextArea::set_line_number_style()设置渲染行号的样式。例如,以下在深灰色背景色中渲染行号。

use ratatui::style::{Style, Color};

let style = Style::default().bg(Color::DarkGray);
textarea.set_line_number_style(style);

配置光标行样式

默认情况下,TextArea使用下划线渲染光标所在的行,以便用户可以轻松地注意到当前行。要更改光标行的样式,请使用TextArea::set_cursor_line_style()。例如,以下使用加粗文本样式化光标行。

use ratatui::style::{Style, Modifier};

let style = Style::default().add_modifier(Modifier::BOLD);
textarea.set_line_number_style(style);

要禁用光标行样式,请按照以下方式设置默认样式

use ratatui::style::{Style, Modifier};

textarea.set_line_number_style(Style::default());

配置制表符宽度

默认制表符宽度为4。要更改它,请使用TextArea::set_tab_length()方法。以下设置制表符宽度为2。按制表符键插入2个空格。

textarea.set_tab_length(2);

配置最大历史记录大小

默认情况下,过去50次修改被存储为编辑历史记录。历史记录用于撤销/重做。要更改记住多少过去编辑,请使用TextArea::set_max_histories()方法。以下记住过去1000次更改。

textarea.set_max_histories(1000);

设置0将禁用撤销/重做。

textarea.set_max_histories(0);

使用正则表达式进行文本搜索

在文本区域中搜索文本,使用TextArea::set_search_pattern()设置正则表达式模式,并通过TextArea::search_forward()进行正向搜索或使用TextArea::search_back()进行反向搜索。正则表达式由regex crate处理。

文本搜索会环绕文本区域。在正向搜索中,如果没有找到匹配项直到文本区域末尾,它将从文件开头搜索该模式。

匹配项在文本区域中被突出显示。可以使用TextArea::set_search_style()更改用于突出显示匹配项的文本样式。将空字符串设置到TextArea::set_search_pattern()将停止文本搜索。

// Start text search matching to "hello" or "hi". This highlights matches in textarea but does not move cursor.
// `regex::Error` is returned on invalid pattern.
textarea.set_search_pattern("(hello|hi)").unwrap();

textarea.search_forward(false); // Move cursor to the next match
textarea.search_back(false);    // Move cursor to the previous match

// Setting empty string stops the search
textarea.set_search_pattern("").unwrap();

没有提供文本搜索的UI。您需要提供自己的UI来输入搜索查询。建议使用另一个TextArea作为搜索表单。要构建单行输入表单,请参阅下文“高级用法”部分的“HTML中的单行输入如<input>”。

editor 示例实现了基于TextArea构建的搜索表单的文本搜索。请参阅实现以获取工作示例。

要使用文本搜索,需要在您的Cargo.toml中启用search功能。默认情况下它是禁用的,以避免在不需要时依赖regex crate。

ratatui-textarea = { version = "*", features = ["search"] }

高级用法

HTML中的单行输入如<input>

要在HTML中使用TextArea作为单行输入小部件(如<input>),请忽略所有插入换行符的关键映射。

use crossterm::event::{Event, read};
use ratatui_textarea::{Input, Key};

let default_text: &str = ...;
let default_text = default_text.replace(&['\n', '\r'], " "); // Ensure no new line is contained
let mut textarea = TextArea::new(vec![default_text]);

// Event loop
loop {
    // ...

    // Using `Input` is not mandatory, but it's useful for pattern match
    // Ignore Ctrl+m and Enter. Otherwise handle keys as usual
    match read()?.into() {
        Input { key: Key::Char('m'), ctrl: true, alt: false }
        | Input { key: Key::Enter, .. } => continue,
        input => {
            textarea.input(key);
        }
    }
}

let text = textarea.into_lines().remove(0); // Get input text

请参阅single_line 示例以获取工作示例。

定义您自己的键映射

所有编辑器操作都定义为TextArea的公共方法。要移动光标,使用ratatui_textarea::CursorMove来通知如何移动光标。

方法 操作
textarea.delete_char() 删除光标前的一个字符
textarea.delete_next_char() 删除光标旁边的一个字符
textarea.insert_newline() 插入换行符
textarea.delete_line_by_end() 从光标处删除到行尾
textarea.delete_line_by_head() 从光标处删除到行首
textarea.delete_word() 删除光标前的一个单词
textarea.delete_next_word() 删除光标旁边的一个单词
textarea.undo() 撤销
textarea.redo() 重做
textarea.paste() 粘贴已复制的文本
textarea.move_cursor(CursorMove::Forward) 将光标向前移动一个字符
textarea.move_cursor(CursorMove::Back) 将光标向后移动一个字符
textarea.move_cursor(CursorMove::Up) 将光标向上移动一行
textarea.move_cursor(CursorMove::Down) 将光标向下移动一行
textarea.move_cursor(CursorMove::WordForward) 将光标向前移动一个单词
textarea.move_cursor(CursorMove::WordBack) 将光标向后移动一个单词
textarea.move_cursor(CursorMove::ParagraphForward) 将光标向上移动一个段落
textarea.move_cursor(CursorMove::ParagraphBack) 将光标向下移动一个段落
textarea.move_cursor(CursorMove::End) 将光标移动到行尾
textarea.move_cursor(CursorMove::Head) 将光标移动到行首
textarea.move_cursor(CursorMove::Top) 将光标移动到文本开头
textarea.move_cursor(CursorMove::Bottom) 将光标移动到文本结尾
textarea.move_cursor(CursorMove::Jump(row,col)) 将光标移动到(行,列)位置
textarea.move_cursor(CursorMove::InViewport) 将光标移动到视口中
textarea.set_search_pattern(pattern) 设置文本搜索的模式
textarea.search_forward(match_cursor) 将光标移动到文本搜索的下一个匹配项
textarea.search_back(match_cursor) 将光标移动到文本搜索的上一匹配项
textarea.滚动(滚动中::PageDown) 将视口向下滚动一页
textarea.滚动(滚动中::PageUp) 将视口向上滚动一页
textarea.滚动(滚动中::HalfPageDown) 将视口向下滚动半页
textarea.滚动(滚动中::HalfPageUp) 将视口向上滚动半页
textarea.滚动((row,col)) 将视口向下滚动到(行,列)位置

要定义自己的键映射,只需在代码中调用上述方法,而不是使用 TextArea::input() 方法。以下示例定义了类似于 Vim 的模式键映射。

use crossterm::event::{Event, read};
use ratatui_textarea::{Input, Key, CursorMove, Scrolling};

let mut textarea = ...;

enum Mode {
    Normal,
    Insert,
}

let mut mode = Mode::Normal;

// Event loop
loop {
    // ...

    match mode {
        Mode::Normal => match read()?.into() {
            Input { key: Key::Char('h'), .. } => textarea.move_cursor(CursorMove::Back),
            Input { key: Key::Char('j'), .. } => textarea.move_cursor(CursorMove::Down),
            Input { key: Key::Char('k'), .. } => textarea.move_cursor(CursorMove::Up),
            Input { key: Key::Char('l'), .. } => textarea.move_cursor(CursorMove::Forward),
            Input { key: Key::Char('i'), .. } => mode = Mode::Insert, // Enter insert mode
            // ...Add more mappings
            _ => {},
        },
        Mode::Insert => match read()?.into() {
            Input { key: Key::Esc, .. } => {
                mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
            }
            input => {
                textarea.input(input); // Use default key mappings in insert mode
            }
        },
    }
}

参见 modal 示例 了解工作示例。它实现了更多类似 Vim 的键映射。

如果您不想使用默认的键映射,可以使用 TextArea::input_without_shortcuts() 方法代替 TextArea::input()。该方法仅处理非常基本的操作,如插入/删除单个字符、制表符、换行符。

match read()?.into() {
    // Handle your own key mappings here
    // ...
    input => textarea.input_without_shortcuts(input),
}

使用自己的后端

ratatui 允许您通过实现 ratatui::backend::Backend trait 来创建自己的后端。ratatui-textarea 也支持它。在这种情况下,请为 ratatui 使用 your-backend 功能。它们避免添加后端 crate(crossterm 和 termion),因为您正在使用自己的后端。

[dependencies]
ratatui = { version = "*", default-features = false }
ratatui-textarea = { version = "*", default-features = false, features = ["your-backend"] }

ratatui_textarea::Input 是一个不依赖于后端的关键输入类型。您需要做的是将您自己的后端中的键事件转换为 ratatui_textarea::Input 实例。然后 TextArea::input() 方法可以像其他后端一样处理输入。

以下示例中,假设 your_backend::KeyDown 是您后端的一个键事件类型,而 your_backend::read_next_key() 返回下一个键事件。

// In your backend implementation

pub enum KeyDown {
    Char(char),
    BS,
    Del,
    Esc,
    // ...
}

// Return tuple of (key, ctrlkey, altkey)
pub fn read_next_key() -> (KeyDown, bool, bool) {
    // ...
}

然后您可以实现将 your_backend::KeyDown 值转换为 ratatui_textarea::Input 值的逻辑。

use ratatui_textarea::{Input, Key};
use your_backend::KeyDown;

fn keydown_to_input(key: KeyDown, ctrl: bool, alt: bool) -> Input {
    match key {
        KeyDown::Char(c) => Input { key: Key::Char(c), ctrl, alt },
        KeyDown::BS => Input { key: Key::Backspace, ctrl, alt },
        KeyDown::Del => Input { key: Key::Delete, ctrl, alt },
        KeyDown::Esc => Input { key: Key::Esc, ctrl, alt },
        // ...
        _ => Input::default(),
    }
}

对于 ratatui-textarea 无法处理的键,可以使用 ratatui_textarea::Input::default()。它返回 'null' 键。编辑器将对此键不采取任何操作。

最后,将您自己的后端的键输入类型转换为 ratatui_textarea::Input 并将其传递给 TextArea::input()

let mut textarea = ...;

// Event loop
loop {
    // ...

    let (key, ctrl, alt) = your_backend::read_next_key();
    if key == your_backend::KeyDown::Esc {
        break; // For example, quit your app on pressing Esc
    }
    textarea.input(keydown_to_input(key, ctrl, alt));
}

在屏幕上放置多个 TextArea 实例

您不需要做任何特别的事情。创建多个 TextArea 实例,并渲染从每个实例构建的小部件。

以下是一个示例,将两个 textarea 小部件放入应用程序并管理焦点。

use ratatui_textarea::{TextArea, Input, Key};
use crossterm::event::{Event, read};

let editors = &mut [
    TextArea::default(),
    TextArea::default(),
];

let mut focused = 0;

loop {
    term.draw(|f| {
        let rects = ...;

        for (editor, rect) in editors.iter_mut().zip(rects.into_iter()) {
            let widget = editor.widget();
            f.render_widget(widget, rect);
        }
    })?;

    match read()?.into() {
        // Switch focused textarea by Ctrl+S
        Input { key: Key::Char('s'), ctrl: true, .. } => focused = (focused + 1) % 2;
        // Handle input by the focused editor
        input => editors[focused].input(input),
    }
}

请参阅split示例editor示例以获取工作示例。

最低支持的 Rust 版本

本 crate 的最低支持 Rust 版本依赖于ratatui crate。当前最低支持版本为 1.56.1。

版本控制

本 crate 尚未达到 v1.0.0。目前没有计划提升主版本。当前的版本控制策略如下

  • 主版本:固定为 0
  • 次版本:在发生重大更改时提升
  • 补丁版本:在添加新功能或修复错误时提升

为 ratatui-textarea 贡献

此项目在GitHub上开发。

对于功能请求或错误报告,请创建一个问题。提交补丁时,请创建一个拉取请求

在提交拉取请求之前,请参阅CONTRIBUTING.md

许可证

ratatui-textarea 在MIT 许可证下分发。

依赖关系

~0.8–7.5MB
~45K SLoC