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

tui-textarea-julien-cpsn

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

1 个不稳定版本

0.4.1 2024 年 6 月 30 日

#54 in 文本编辑器

Download history 229/week @ 2024-06-27 53/week @ 2024-07-04 25/week @ 2024-07-11 18/week @ 2024-07-18 28/week @ 2024-07-25 22/week @ 2024-08-01 126/week @ 2024-08-08

196 每月下载
atac 中使用

MIT 许可证

225KB
4K SLoC

tui-textarea

crate docs CI coverage

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

功能

  • 具有基本操作(插入/删除字符、自动滚动等)的多行文本编辑器小部件
  • 类似于 Emacs 的快捷键(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 crate添加到您的Cargo.toml中的依赖项。这默认启用了crossterm后端支持。

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

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

[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 crate的功能并禁用默认功能。以下表格显示了与依赖项对应的功能名称。

crossterm termion termwiz 您自己的后端
ratatui crossterm(默认启用) termion termwiz 无后端
tui-rs tuirs-crossterm tuirs-termion N/A 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 crate 的版本在 ratatuitui-rs 中是不同的。请选择正确的版本。

最小使用

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 = ...;
        // `TextArea::widget` builds a widget to render the editor with tui
        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() 构建一个用于渲染编辑器当前状态的组件。创建组件并在事件循环的每个迭代中渲染它。

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_line_number_style(style);

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

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

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

配置制表符宽度

默认制表符宽度为 4。要更改它,请使用 TextArea::set_tab_length() 方法。以下设置为制表符宽度为 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();

没有提供文本搜索的用户界面。您需要提供自己的用户界面来输入搜索查询。建议使用另一个 TextArea 用于搜索表单。要构建单行输入表单,请参阅下文“高级用法”部分中的‘单行输入类似 <input> 在HTML’。

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::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 功能。它们避免添加后端crate(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 实例,并渲染从每个实例构建的控件。

以下示例演示了如何在应用程序中放置两个 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_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 的 MSRV 依赖于 tui crate。目前 MSRV 是 1.56.1。请注意,ratatui crate 需要更新的 Rust 版本。

版本控制

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

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

为 tui-textarea 贡献

本项目在GitHub上开发。GitHub链接

如需功能请求或错误报告,请创建一个问题。如需提交补丁,请创建一个pull请求

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

许可证

tui-textarea遵循MIT许可证

依赖项

~1–15MB
~191K SLoC