#editor #tui #input #ratatui #key-input #text-input

tui-textarea

tui-textarea 是为 ratatui 和 tui-rs 设计的简单而强大的文本编辑器小部件。多行文本编辑器可以轻松地作为您 TUI 应用程序的一部分。

21个不稳定版本 (5个破坏性更改)

0.6.1 2024年8月8日
0.5.3 2024年8月3日
0.5.1 2024年7月12日
0.4.0 2023年11月19日
0.1.5 2022年7月18日

#3文本编辑器

Download history 2119/week @ 2024-05-02 1939/week @ 2024-05-09 3449/week @ 2024-05-16 2708/week @ 2024-05-23 2480/week @ 2024-05-30 2066/week @ 2024-06-06 2851/week @ 2024-06-13 2671/week @ 2024-06-20 2070/week @ 2024-06-27 2019/week @ 2024-07-04 2220/week @ 2024-07-11 2619/week @ 2024-07-18 2876/week @ 2024-07-25 4118/week @ 2024-08-01 4539/week @ 2024-08-08 5206/week @ 2024-08-15

每月17,106次下载
用于 57 个crate (54 个直接)

MIT 许可证

220KB
4K SLoC

tui-textarea

crate docs CI coverage

tui-textarea 是一个类似于 HTML 中的 <textarea> 的简单而强大的文本编辑器小部件,适用于 ratatuitui-rs。多行文本编辑器可以轻松地作为您 TUI 应用程序的一部分。

特性

  • 具有基本操作(插入/删除字符、自动滚动等)的多行文本编辑器小部件
  • Emacs-like 快捷键(C-n/C-p/C-f/C-bM-f/M-bC-a/C-eC-h/C-dC-kM</M-> 等)
  • 撤销/重做
  • 行号
  • 光标行高亮显示
  • 使用正则表达式搜索
  • 文本选择
  • 鼠标滚动
  • 支持粘贴。使用 C-kC-j 等删除的文本可以粘贴
  • 后端无关。支持 crosstermtermiontermwiz 和您自己的后端
  • 同一屏幕上的多个 textarea 小部件
  • 支持 ratatui(社区的分叉)和 tui-rs(原始)

文档

示例

在本存储库中运行 cargo run --example 可以演示 tui-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

变量

cargo run --example variable

简单的文本区域,高度随行数变化。

vim

cargo run --example vim

类似于 Vim 的模式文本编辑器。Vim 模拟是通过状态机实现的。

Vim emulation example

弹出占位符

cargo run --example popup_placeholder

带占位文本的弹出文本区域。

popup textarea with placeholder example

密码

cargo run --example password

带遮蔽文本的密码输入表单(以 ● 表示)。

password example

termion

cargo run --example termion --no-default-features --features=termion

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

termwiz

cargo run --example termwiz --no-default-features --features=termwiz

带有 termwiz 支持的最小化使用。

tui-rs 支持的示例

以上所有示例都使用 ratatui,但一些示例提供了 tui-rs 版本。尝试使用 tuirs_ 前缀。在这些情况下,您需要指定要使用 tui-rs 的功能并明确设置 --no-default-features 标志。

# tui-rs version of `minimal` example
cargo run --example tuirs_minimal --no-default-features --features=tuirs-crossterm

# tui-rs version of `editor` example
cargo run --example tuirs_editor --no-default-features --features=tuirs-crossterm,search file.txt

# tui-rs version of `termion` example
cargo run --example tuirs_termion --no-default-features --features=tuirs-termion

安装

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

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

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

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

如果您使用 termiontermwiz 与 ratatui 一起使用,请启用相应的功能而不是 crossterm 功能。

[dependencies]

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

# For termwiz
ratatui = { version = "*", default-features = false, features = ["termwiz"] }
tui-textarea = { version = "*", default-features = false, features = ["termwiz"] }

如果您使用 tui-rs 而不是 ratatui,则需要启用 tui-rs 包的功能并禁用默认功能。以下表格显示了与依赖项相对应的功能名称。

crossterm termion termwiz 您自己的后端
ratatui crossterm(默认启用) termion termwiz 无后端
tui-rs tuirs-crossterm tuirs-termion 不适用 tuirs-no-backend

例如,当您想使用 tui-rscrossterm 的组合时,

[dependencies]
tui = "*"
tui-textarea = { version = "*", features = ["tuirs-crossterm"], default-features = false }

请注意,ratatui 支持和 tui-rs 支持是互斥的。当您使用 tui-rs 支持时,您必须通过 default-features = false 禁用 ratatui 支持。

除了上述依赖项之外,您还需要安装 crosstermtermiontermwiz 来初始化您的应用程序并接收按键输入。请注意,crossterm 包和 termion 包的版本在 ratatuitui-rs 之间不同。请选择相同的依赖项版本。例如,tui-rs 依赖于 crossterm v0.2.5 或 termion v1.5,其中这两个包都早于 ratatui 的依赖项。

最小化使用

use tui_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 = ...;
        // Render the textarea in terminal screen
        f.render_widget(&textarea, 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 引用实现了 ratatui 的 Widget 特性。在每个事件循环的每个tick上渲染它。

TextArea::input() 从 tui 后端接收输入。如果启用了这些功能,该方法可以直接从后端获取键事件,如 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+CCopy 复制选定的文本
Ctrl+XCut 剪切选定的文本
Ctrl+YPaste 粘贴复制的文本
Ctrl+F 光标向前移动一个字符
Ctrl+B 光标向后移动一个字符
Ctrl+P 光标向上移动一行
Ctrl+N 光标向下移动一行
Alt+FCtrl+ 光标向前移动一个单词
Atl+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<impl Iterator<Item=impl 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<impl 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_cursor_line_style(style);

要禁用光标行样式,按如下设置默认样式

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

textarea.set_cursor_line_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 框架处理。

文本搜索在文本区域中循环。当正向搜索并在文本区域末尾找不到匹配项时,它从文件开头搜索模式。

匹配项在文本区域中被突出显示。可以使用 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 包。

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

高级用法

HTML 中的单行输入,例如 <input>

要使用 TextArea 为 HTML 中的单行输入控件(如 <input>)创建单行输入,忽略所有插入换行符的键映射。

use crossterm::event::{Event, read};
use tui_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 的公共方法。要移动光标,使用 tui_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.copy() 复制选定的文本
textarea.cut() 剪切选定的文本
textarea.paste() 粘贴复制的文本
textarea.start_selection() 开始文本选择
textarea.cancel_selection() 取消文本选择
textarea.select_all() 选择整个文本
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::WordEnd) 将光标移动到下一个单词的末尾
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)) 将光标移动到 (row, col) 位置
textarea.move_cursor(CursorMove::InViewport) 将光标移动到视口内
textarea.set_search_pattern(pattern) 设置文本搜索的模式
textarea.search_forward(match_cursor) 将光标移动到文本搜索的下一个匹配项
textarea.search_back(match_cursor) 将光标移动到文本搜索的上一个匹配项
textarea.scroll(滚动::PageDown) 将视口向下滚动一页
textarea.scroll(滚动::PageUp) 将视口向上滚动一页
textarea.scroll(滚动::HalfPageDown) 将视口向下滚动半页
textarea.scroll(滚动::HalfPageUp) 将视口向上滚动半页
textarea.scroll((row,col)) 将视口向下滚动到 (row, col) 位置

要定义自己的键映射,只需在您的代码中调用上述方法,而不是调用 TextArea::input() 方法。

查看工作示例,请参阅 vim 示例。它实现了更多类似于 Vim 的键模式映射。

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

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

使用您自己的后端

ratatui 和 tui-rs 允许您通过实现 ratatui::backend::Backend 特性来自定义后端。tui-textarea 也支持它。请为 ratatui 使用 no-backend 功能,或为 tui-rs 使用 tuirs-no-backend 功能。它们避免了添加后端包(crossterm、termion 或 termwiz),因为您正在使用自己的后端。

[dependencies]
# For ratatui
tui-textarea = { version = "*", default-features = false, features = ["no-backend"] }
# For tui-rs
tui-textarea = { version = "*", default-features = false, features = ["tuirs-no-backend"] }

tui_textarea::Input 是一种与后端无关的键输入类型。您需要做的是将您自己的后端中的键事件转换为 tui_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 值转换为 tui_textarea::Input 值的逻辑。

use tui_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(),
    }
}

对于 tui-textarea 未处理的键,可以使用 tui_textarea::Input::default()。它返回 'null' 键。编辑器将对该键不进行任何操作。

最后,将您自己的后端的键输入类型转换为 tui_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 实例,并渲染由每个实例构建的小部件。

以下是将两个文本区域小部件放入应用程序并管理焦点的示例。

use tui_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().zip(rects.into_iter()) {
            f.render_widget(editor, 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 示例 以查看工作示例。

序列化/反序列化支持

此软件包可以通过启用 serde 功能可选地支持 serde 软件包。

[dependencies]
tui-textarea = { version = "*", features = ["serde"] }

以下类型的值可以进行序列化/反序列化

  • 输入
  • CursorMove
  • 滚动

以下是一个使用 serde_json 从 JSON 反序列化键输入的示例。

use tui_textarea::Input;

let json = r#"
    {
        "key": { "Char": "a" },
        "ctrl": true,
        "alt": false,
        "shift": true
    }
"#;

let input: Input = serde_json::from_str(json).unwrap();
println!("{input:?}");
// Input {
//     key: Key::Char('a'),
//     ctrl: true,
//     alt: false,
//     shift: true,
// }

最低支持的 Rust 版本

此软件包的 MSRV 依赖于 tui 软件包。目前 MSRV 是 1.56.1。请注意,ratatui 软件包需要更新的 Rust 版本。

版本

此软件包尚未达到 v1.0.0。目前没有计划提升主版本。当前版本策略如下

  • 主版本:固定为 0
  • 次版本:在破坏性更改时提升
  • 修订版:在添加新功能或修复错误时提升

为 tui-textarea 做贡献

此项目在 GitHub 上开发。

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

在报告问题或创建PR之前,请阅读CONTRIBUTING.md

许可证

tui-textarea是在MIT许可证下分发的。

依赖项

~3–19MB
~230K SLoC