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 在 文本编辑器 中
每月17,106次下载
用于 57 个crate (54 个直接)
220KB
4K SLoC
tui-textarea
tui-textarea 是一个类似于 HTML 中的 <textarea>
的简单而强大的文本编辑器小部件,适用于 ratatui 和 tui-rs。多行文本编辑器可以轻松地作为您 TUI 应用程序的一部分。
特性
- 具有基本操作(插入/删除字符、自动滚动等)的多行文本编辑器小部件
- Emacs-like 快捷键(
C-n
/C-p
/C-f
/C-b
、M-f
/M-b
、C-a
/C-e
、C-h
/C-d
、C-k
、M<
/M->
等) - 撤销/重做
- 行号
- 光标行高亮显示
- 使用正则表达式搜索
- 文本选择
- 鼠标滚动
- 支持粘贴。使用
C-k
、C-j
等删除的文本可以粘贴 - 后端无关。支持 crossterm、termion、termwiz 和您自己的后端
- 同一屏幕上的多个 textarea 小部件
- 支持 ratatui(社区的分叉)和 tui-rs(原始)
示例
在本存储库中运行 cargo run --example
可以演示 tui-textarea 的使用。
最小化
cargo run --example minimal
带有 crossterm 支持的最小化使用。
编辑器
cargo run --example editor --features search file.txt
简单的文本编辑器,用于编辑多个文件。
单行
cargo run --example single_line
具有浮点数验证的单行输入表单。
分割
cargo run --example split
在屏幕中分割两个文本区域并切换它们。这是多个文本区域实例的示例。
变量
cargo run --example variable
简单的文本区域,高度随行数变化。
vim
cargo run --example vim
类似于 Vim 的模式文本编辑器。Vim 模拟是通过状态机实现的。
弹出占位符
cargo run --example popup_placeholder
带占位文本的弹出文本区域。
密码
cargo run --example password
带遮蔽文本的密码输入表单(以 ● 表示)。
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"] }
如果您使用 termion 或 termwiz 与 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-rs 和 crossterm 的组合时,
[dependencies]
tui = "*"
tui-textarea = { version = "*", features = ["tuirs-crossterm"], default-features = false }
请注意,ratatui 支持和 tui-rs 支持是互斥的。当您使用 tui-rs 支持时,您必须通过 default-features = false
禁用 ratatui 支持。
除了上述依赖项之外,您还需要安装 crossterm 或 termion 或 termwiz 来初始化您的应用程序并接收按键输入。请注意,crossterm 包和 termion 包的版本在 ratatui 和 tui-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::KeyEvent
或 termion::event::Key
。该方法还处理默认的键映射。
默认键映射如下
映射 | 描述 |
---|---|
Ctrl+H ,Backspace |
删除光标前的字符 |
Ctrl+D ,Delete |
删除光标旁的字符 |
Ctrl+M ,Enter |
插入换行符 |
Ctrl+K |
从光标删除到行尾 |
Ctrl+J |
从光标删除到行首 |
Ctrl+W ,Alt+H ,Alt+Backspace |
删除光标前的单词 |
Alt+D ,Alt+Delete |
删除光标旁的单词 |
Ctrl+U |
撤销 |
Ctrl+R |
重做 |
Ctrl+C ,Copy |
复制选定的文本 |
Ctrl+X ,Cut |
剪切选定的文本 |
Ctrl+Y ,Paste |
粘贴复制的文本 |
Ctrl+F ,→ |
光标向前移动一个字符 |
Ctrl+B ,← |
光标向后移动一个字符 |
Ctrl+P ,↑ |
光标向上移动一行 |
Ctrl+N ,↓ |
光标向下移动一行 |
Alt+F ,Ctrl+→ |
光标向前移动一个单词 |
Atl+B ,Ctrl+← |
光标向后移动一个单词 |
Alt+[ ,Alt+P ,Ctrl+↑ |
光标向上移动一个段落 |
Alt+[ ,Alt+N ,Ctrl+↓ |
光标向下移动一个段落 |
Ctrl+E ,End ,Ctrl+Alt+F ,Ctrl+Alt+→ |
移动光标到行尾 |
Ctrl+A ,Home ,Ctrl+Alt+B ,Ctrl+Alt+← |
将光标移动到行首 |
Alt+< ,Ctrl+Alt+P ,Ctrl+Alt+↑ |
将光标移动到行首 |
Alt+> ,Ctrl+Alt+N ,Ctrl+Alt+↓ |
将光标移动到行尾 |
Ctrl+V ,PageDown |
向下滚动一页 |
Alt+V ,PageUp |
向上滚动一页 |
一次性删除多个字符,会将删除的文本保存到剪切板。稍后可以使用 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