зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-08 16:35:49 +03:00
Merge branch 'ratatui' (fix #12)
Этот коммит содержится в:
14
.github/workflows/ci.yml
поставляемый
14
.github/workflows/ci.yml
поставляемый
@@ -14,10 +14,11 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo test --all-features -- --skip src/lib.rs
|
- run: cargo test --features=search -- --skip src/lib.rs
|
||||||
if: ${{ matrix.os != 'windows-latest' }}
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
- run: cargo test --features=search -- --skip src\lib.rs
|
- run: cargo test --features=search -- --skip src\lib.rs
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
- run: cargo test --no-default-features --features=ratatui-crossterm,search -- --skip .rs
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -27,6 +28,11 @@ jobs:
|
|||||||
components: clippy,rustfmt
|
components: clippy,rustfmt
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo fmt -- --check
|
- run: cargo fmt -- --check
|
||||||
- run: cargo clippy --examples --all-features -- -D warnings
|
- run: cargo clippy --examples -- -D warnings
|
||||||
- run: cargo clippy --no-default-features -- -D warnings
|
- run: cargo clippy --examples --features search -- -D warnings
|
||||||
- run: cargo clippy --no-default-features --features termion -- -D warnings
|
- run: cargo clippy --examples --no-default-features --features ratatui-crossterm -- -D warnings
|
||||||
|
- run: cargo clippy --examples --no-default-features --features ratatui-crossterm,search -- -D warnings
|
||||||
|
- run: cargo clippy --examples --no-default-features --features ratatui-termion -- -D warnings
|
||||||
|
- run: cargo clippy --examples --no-default-features --features ratatui-termion,search -- -D warnings
|
||||||
|
- run: cargo clippy --examples --no-default-features --features termion -- -D warnings
|
||||||
|
- run: cargo clippy --examples --no-default-features --features termion,search -- -D warnings
|
||||||
|
|||||||
24
Cargo.toml
24
Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
rust-version = "1.56.1"
|
rust-version = "1.56.1"
|
||||||
authors = ["rhysd <lin90162@yahoo.co.jp>"]
|
authors = ["rhysd <lin90162@yahoo.co.jp>"]
|
||||||
description = """
|
description = """
|
||||||
tui-textarea is a simple yet powerful text editor widget for tui-rs. Multi-line
|
tui-textarea is a simple yet powerful text editor widget for tui-rs and ratatui. Multi-line
|
||||||
text editor can be easily put as part of your TUI application.
|
text editor can be easily put as part of your TUI application.
|
||||||
"""
|
"""
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,7 +13,7 @@ homepage = "https://github.com/rhysd/tui-textarea#readme"
|
|||||||
repository = "https://github.com/rhysd/tui-textarea"
|
repository = "https://github.com/rhysd/tui-textarea"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["text-editors", "text-processing"]
|
categories = ["text-editors", "text-processing"]
|
||||||
keywords = ["tui", "textarea", "editor", "input", "edit"]
|
keywords = ["tui", "textarea", "editor", "input", "edit", "ratatui"]
|
||||||
include = [
|
include = [
|
||||||
"/src",
|
"/src",
|
||||||
"/examples",
|
"/examples",
|
||||||
@@ -26,13 +26,17 @@ default = ["crossterm"]
|
|||||||
crossterm = ["dep:crossterm", "tui/crossterm"]
|
crossterm = ["dep:crossterm", "tui/crossterm"]
|
||||||
termion = ["dep:termion", "tui/termion"]
|
termion = ["dep:termion", "tui/termion"]
|
||||||
search = ["dep:regex"]
|
search = ["dep:regex"]
|
||||||
|
ratatui-crossterm = ["dep:crossterm-026", "ratatui/crossterm"]
|
||||||
|
ratatui-termion = ["dep:termion", "ratatui/termion"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = { version = "0.25", optional = true }
|
crossterm = { package = "crossterm", version = "0.25", optional = true }
|
||||||
regex = { version = "1", optional = true }
|
regex = { version = "1", optional = true }
|
||||||
termion = { version = "1.5", optional = true }
|
termion = { version = "1.5", optional = true }
|
||||||
tui = { version = "0.19", default-features = false }
|
tui = { version = "0.19", default-features = false, optional = true }
|
||||||
arbitrary = { version = "1", features = ["derive"], optional = true }
|
arbitrary = { version = "1", features = ["derive"], optional = true }
|
||||||
|
crossterm-026 = { package = "crossterm", version = "0.26", optional = true }
|
||||||
|
ratatui = { version = "0.20.1", default-features = false, optional = true }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "minimal"
|
name = "minimal"
|
||||||
@@ -62,6 +66,18 @@ required-features = ["crossterm"]
|
|||||||
name = "modal"
|
name = "modal"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "ratatui_minimal"
|
||||||
|
required-features = ["ratatui-crossterm"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "ratatui_editor"
|
||||||
|
required-features = ["ratatui-crossterm", "search"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "ratatui_termion"
|
||||||
|
required-features = ["ratatui-termion"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"bench",
|
"bench",
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -4,8 +4,8 @@ tui-textarea
|
|||||||
[![docs][doc-badge]][doc]
|
[![docs][doc-badge]][doc]
|
||||||
[![CI][ci-badge]][ci]
|
[![CI][ci-badge]][ci]
|
||||||
|
|
||||||
[tui-textarea][crate] is a simple yet powerful text editor widget like `<textarea>` in HTML for [tui-rs][]. Multi-line
|
[tui-textarea][crate] is a simple yet powerful text editor widget like `<textarea>` in HTML for [tui-rs][] and [ratatui][].
|
||||||
text editor can be easily put as part of your TUI application.
|
Multi-line text editor can be easily put as part of your TUI application.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ text editor can be easily put as part of your TUI application.
|
|||||||
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
||||||
- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported
|
- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported
|
||||||
- Multiple textarea widgets in the same screen
|
- Multiple textarea widgets in the same screen
|
||||||
|
- Support both [tui-rs][] (the original) and [ratatui][] (the fork by community)
|
||||||
|
|
||||||
[Documentation][doc]
|
[Documentation][doc]
|
||||||
|
|
||||||
@@ -90,6 +91,22 @@ cargo run --example modal
|
|||||||
|
|
||||||
Simple modal text editor like `vi`.
|
Simple modal text editor like `vi`.
|
||||||
|
|
||||||
|
### Examples for [ratatui][] support
|
||||||
|
|
||||||
|
All above examples uses [tui-rs][], but some examples provide [ratatui][] version. Try `ratatui_` prefix. In these cases,
|
||||||
|
you need to specify features to use ratatui and `--no-default-features` flag explicitly.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# ratatui version of `minimal` example
|
||||||
|
cargo run --example ratatui_minimal --no-default-features --features=ratatui-crossterm
|
||||||
|
|
||||||
|
# ratatui version of `editor` example
|
||||||
|
cargo run --example ratatui_editor --no-default-features --features=ratatui-crossterm,search file.txt
|
||||||
|
|
||||||
|
# ratatui version of `termion` example
|
||||||
|
cargo run --example ratatui_termion --no-default-features --features=ratatui-termion
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Add `tui-textarea` crate to dependencies in your `Cargo.toml`. This enables crossterm backend support by default.
|
Add `tui-textarea` crate to dependencies in your `Cargo.toml`. This enables crossterm backend support by default.
|
||||||
@@ -117,6 +134,29 @@ tui = { version = "*", default-features = false, features = ["termion"] }
|
|||||||
tui-textarea = { version = "*", default-features = false, features = ["termion"] }
|
tui-textarea = { version = "*", default-features = false, features = ["termion"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you're using [ratatui][] instead of [tui-rs][], you need to enable features for using ratatui crate. The following table
|
||||||
|
shows feature names corresponding to the dependencies.
|
||||||
|
|
||||||
|
| | crossterm | termion |
|
||||||
|
|---------|----------------------------------|-------------------|
|
||||||
|
| tui-rs | `crossterm` (enabled by default) | `termion` |
|
||||||
|
| ratatui | `ratatui-crossterm` | `ratatui-termion` |
|
||||||
|
|
||||||
|
For example, when you want to use the combination of [ratatui][] and [crossterm][],
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
ratatui = "*"
|
||||||
|
tui-textarea = { version = "*", features = ["ratatui-crossterm"], default-features=false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that [tui-rs][] support and [ratatui][] support are exclusive. When you use [ratatui][] support, you must disable
|
||||||
|
[tui-rs][] support by `default-features=false`.
|
||||||
|
|
||||||
|
In addition to above dependencies, you also need to install [crossterm][] or [termion][] to initialize your application
|
||||||
|
and to receive key inputs. Note that version of [crossterm][] crate is different between [tui-rs][] and [ratatui][].
|
||||||
|
Please select the correct version.
|
||||||
|
|
||||||
## Minimal Usage
|
## Minimal Usage
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -629,6 +669,7 @@ tui-textarea is distributed under [The MIT License](./LICENSE.txt).
|
|||||||
[ci-badge]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml/badge.svg?event=push
|
[ci-badge]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml/badge.svg?event=push
|
||||||
[ci]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml
|
[ci]: https://github.com/rhysd/tui-textarea/actions/workflows/ci.yml
|
||||||
[tui-rs]: https://github.com/fdehau/tui-rs
|
[tui-rs]: https://github.com/fdehau/tui-rs
|
||||||
|
[ratatui]: https://github.com/tui-rs-revival/ratatui
|
||||||
[termion]: https://docs.rs/termion/latest/termion/
|
[termion]: https://docs.rs/termion/latest/termion/
|
||||||
[crossterm]: https://docs.rs/crossterm/latest/crossterm/
|
[crossterm]: https://docs.rs/crossterm/latest/crossterm/
|
||||||
[tui-backend]: https://docs.rs/tui/latest/tui/backend/trait.Backend.html
|
[tui-backend]: https://docs.rs/tui/latest/tui/backend/trait.Backend.html
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ license = "MIT"
|
|||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tui-textarea = { path = "..", default-features = false, features = ["search"] }
|
tui-textarea = { path = "..", features = ["search"] }
|
||||||
tui = { version = "0.19", default-features = false }
|
tui = { version = "0.19", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
372
examples/ratatui_editor.rs
Обычный файл
372
examples/ratatui_editor.rs
Обычный файл
@@ -0,0 +1,372 @@
|
|||||||
|
use crossterm_026 as crossterm;
|
||||||
|
|
||||||
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
|
use crossterm::terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, EnterAlternateScreen,
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
};
|
||||||
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use ratatui::text::{Span, Spans};
|
||||||
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::env;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufRead, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tui_textarea::{CursorMove, Input, Key, TextArea};
|
||||||
|
|
||||||
|
macro_rules! error {
|
||||||
|
($fmt: expr $(, $args:tt)*) => {{
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, format!($fmt $(, $args)*)))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchBox<'a> {
|
||||||
|
textarea: TextArea<'a>,
|
||||||
|
open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for SearchBox<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_block(Block::default().borders(Borders::ALL).title("Search"));
|
||||||
|
Self {
|
||||||
|
textarea,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SearchBox<'a> {
|
||||||
|
fn open(&mut self) {
|
||||||
|
self.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.open = false;
|
||||||
|
// Remove input for next search. Do not recreate `self.textarea` instance to keep undo history so that users can
|
||||||
|
// restore previous input easily.
|
||||||
|
self.textarea.move_cursor(CursorMove::End);
|
||||||
|
self.textarea.delete_line_by_head();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> u16 {
|
||||||
|
if self.open {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&mut self, input: Input) -> Option<&'_ str> {
|
||||||
|
match input {
|
||||||
|
Input {
|
||||||
|
key: Key::Enter, ..
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('m'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => None, // Disable shortcuts which inserts a newline. See `single_line` example
|
||||||
|
input => {
|
||||||
|
let modified = self.textarea.input(input);
|
||||||
|
modified.then(|| self.textarea.lines()[0].as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(&mut self, err: Option<impl Display>) {
|
||||||
|
let b = if let Some(err) = err {
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!("Search: {}", err))
|
||||||
|
.style(Style::default().fg(Color::Red))
|
||||||
|
} else {
|
||||||
|
Block::default().borders(Borders::ALL).title("Search")
|
||||||
|
};
|
||||||
|
self.textarea.set_block(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Buffer<'a> {
|
||||||
|
textarea: TextArea<'a>,
|
||||||
|
path: PathBuf,
|
||||||
|
modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Buffer<'a> {
|
||||||
|
fn new(path: PathBuf) -> io::Result<Self> {
|
||||||
|
let mut textarea = if let Ok(md) = path.metadata() {
|
||||||
|
if md.is_file() {
|
||||||
|
let mut textarea: TextArea = io::BufReader::new(fs::File::open(&path)?)
|
||||||
|
.lines()
|
||||||
|
.collect::<io::Result<_>>()?;
|
||||||
|
if textarea.lines().iter().any(|l| l.starts_with('\t')) {
|
||||||
|
textarea.set_hard_tab_indent(true);
|
||||||
|
}
|
||||||
|
textarea
|
||||||
|
} else {
|
||||||
|
return error!("{:?} is not a file", path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextArea::default() // File does not exist
|
||||||
|
};
|
||||||
|
textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
|
||||||
|
Ok(Self {
|
||||||
|
textarea,
|
||||||
|
path,
|
||||||
|
modified: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self) -> io::Result<()> {
|
||||||
|
if !self.modified {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut f = io::BufWriter::new(fs::File::create(&self.path)?);
|
||||||
|
for line in self.textarea.lines() {
|
||||||
|
f.write_all(line.as_bytes())?;
|
||||||
|
f.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
self.modified = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Editor<'a> {
|
||||||
|
current: usize,
|
||||||
|
buffers: Vec<Buffer<'a>>,
|
||||||
|
term: Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
|
message: Option<Cow<'static, str>>,
|
||||||
|
search: SearchBox<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
fn new<I>(paths: I) -> io::Result<Self>
|
||||||
|
where
|
||||||
|
I: Iterator,
|
||||||
|
I::Item: Into<PathBuf>,
|
||||||
|
{
|
||||||
|
let buffers = paths
|
||||||
|
.map(|p| Buffer::new(p.into()))
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
if buffers.is_empty() {
|
||||||
|
return error!("USAGE: cargo run --example editor FILE1 [FILE2...]");
|
||||||
|
}
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
if !is_raw_mode_enabled()? {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
}
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let term = Terminal::new(backend)?;
|
||||||
|
Ok(Self {
|
||||||
|
current: 0,
|
||||||
|
buffers,
|
||||||
|
term,
|
||||||
|
message: None,
|
||||||
|
search: SearchBox::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&mut self) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
let search_height = self.search.height();
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(search_height),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.term.draw(|f| {
|
||||||
|
let chunks = layout.split(f.size());
|
||||||
|
|
||||||
|
if search_height > 0 {
|
||||||
|
f.render_widget(self.search.textarea.widget(), chunks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = &self.buffers[self.current];
|
||||||
|
let textarea = &buffer.textarea;
|
||||||
|
let widget = textarea.widget();
|
||||||
|
f.render_widget(widget, chunks[1]);
|
||||||
|
|
||||||
|
// Render status line
|
||||||
|
let modified = if buffer.modified { " [modified]" } else { "" };
|
||||||
|
let slot = format!("[{}/{}]", self.current + 1, self.buffers.len());
|
||||||
|
let path = format!(" {}{} ", buffer.path.display(), modified);
|
||||||
|
let (row, col) = textarea.cursor();
|
||||||
|
let cursor = format!("({},{})", row + 1, col + 1);
|
||||||
|
let status_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(slot.len() as u16),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(cursor.len() as u16),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(chunks[2]);
|
||||||
|
let status_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||||
|
f.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]);
|
||||||
|
f.render_widget(Paragraph::new(path).style(status_style), status_chunks[1]);
|
||||||
|
f.render_widget(Paragraph::new(cursor).style(status_style), status_chunks[2]);
|
||||||
|
|
||||||
|
// Render message at bottom
|
||||||
|
let message = if let Some(message) = self.message.take() {
|
||||||
|
Spans::from(Span::raw(message))
|
||||||
|
} else if search_height > 0 {
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::raw("Press "),
|
||||||
|
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to jump to first match and close, "),
|
||||||
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to close, "),
|
||||||
|
Span::styled(
|
||||||
|
"^G or ↓ or ^N",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" to search next, "),
|
||||||
|
Span::styled(
|
||||||
|
"M-G or ↑ or ^P",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" to search previous"),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::raw("Press "),
|
||||||
|
Span::styled("^Q", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to quit, "),
|
||||||
|
Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to save, "),
|
||||||
|
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to search, "),
|
||||||
|
Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to switch buffer"),
|
||||||
|
])
|
||||||
|
};
|
||||||
|
f.render_widget(Paragraph::new(message), chunks[3]);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if search_height > 0 {
|
||||||
|
let textarea = &mut self.buffers[self.current].textarea;
|
||||||
|
match crossterm::event::read()?.into() {
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g' | 'n'),
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
| Input { key: Key::Down, .. } => {
|
||||||
|
if !textarea.search_forward(false) {
|
||||||
|
self.search.set_error(Some("Pattern not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: false,
|
||||||
|
alt: true,
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('p'),
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
| Input { key: Key::Up, .. } => {
|
||||||
|
if !textarea.search_back(false) {
|
||||||
|
self.search.set_error(Some("Pattern not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Enter, ..
|
||||||
|
} => {
|
||||||
|
if !textarea.search_forward(true) {
|
||||||
|
self.message = Some("Pattern not found".into());
|
||||||
|
}
|
||||||
|
self.search.close();
|
||||||
|
textarea.set_search_pattern("").unwrap();
|
||||||
|
}
|
||||||
|
Input { key: Key::Esc, .. } => {
|
||||||
|
self.search.close();
|
||||||
|
textarea.set_search_pattern("").unwrap();
|
||||||
|
}
|
||||||
|
input => {
|
||||||
|
if let Some(query) = self.search.input(input) {
|
||||||
|
let maybe_err = textarea.set_search_pattern(query).err();
|
||||||
|
self.search.set_error(maybe_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match crossterm::event::read()?.into() {
|
||||||
|
Input {
|
||||||
|
key: Key::Char('q'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => break,
|
||||||
|
Input {
|
||||||
|
key: Key::Char('x'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.current = (self.current + 1) % self.buffers.len();
|
||||||
|
self.message =
|
||||||
|
Some(format!("Switched to buffer #{}", self.current + 1).into());
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('s'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.buffers[self.current].save()?;
|
||||||
|
self.message = Some("Saved!".into());
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.search.open();
|
||||||
|
}
|
||||||
|
input => {
|
||||||
|
let buffer = &mut self.buffers[self.current];
|
||||||
|
buffer.modified = buffer.textarea.input(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for Editor<'a> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.term.show_cursor().unwrap();
|
||||||
|
if !is_raw_mode_enabled().unwrap() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
crossterm::execute!(
|
||||||
|
self.term.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
Editor::new(env::args_os().skip(1))?.run()
|
||||||
|
}
|
||||||
51
examples/ratatui_minimal.rs
Обычный файл
51
examples/ratatui_minimal.rs
Обычный файл
@@ -0,0 +1,51 @@
|
|||||||
|
use crossterm_026 as crossterm;
|
||||||
|
|
||||||
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
|
use crossterm::terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
|
};
|
||||||
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
use ratatui::widgets::{Block, Borders};
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::io;
|
||||||
|
use tui_textarea::{Input, Key, TextArea};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut stdout = stdout.lock();
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Crossterm Minimal Example"),
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
term.draw(|f| {
|
||||||
|
f.render_widget(textarea.widget(), f.size());
|
||||||
|
})?;
|
||||||
|
match crossterm::event::read()?.into() {
|
||||||
|
Input { key: Key::Esc, .. } => break,
|
||||||
|
input => {
|
||||||
|
textarea.input(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
crossterm::execute!(
|
||||||
|
term.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
term.show_cursor()?;
|
||||||
|
|
||||||
|
println!("Lines: {:?}", textarea.lines());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
69
examples/ratatui_termion.rs
Обычный файл
69
examples/ratatui_termion.rs
Обычный файл
@@ -0,0 +1,69 @@
|
|||||||
|
use ratatui::backend::TermionBackend;
|
||||||
|
use ratatui::widgets::{Block, Borders};
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use termion::event::Event as TermEvent;
|
||||||
|
use termion::input::{MouseTerminal, TermRead};
|
||||||
|
use termion::raw::IntoRawMode;
|
||||||
|
use termion::screen::AlternateScreen;
|
||||||
|
use tui_textarea::{Input, Key, TextArea};
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
Term(TermEvent),
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let stdout = io::stdout().into_raw_mode()?;
|
||||||
|
let stdout = MouseTerminal::from(stdout);
|
||||||
|
let stdout = AlternateScreen::from(stdout);
|
||||||
|
let backend = TermionBackend::new(stdout);
|
||||||
|
let mut term = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let events = {
|
||||||
|
let events = io::stdin().events();
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let keys_tx = tx.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for event in events.flatten() {
|
||||||
|
keys_tx.send(Event::Term(event)).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread::spawn(move || loop {
|
||||||
|
tx.send(Event::Tick).unwrap();
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
});
|
||||||
|
rx
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Termion Minimal Example"),
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match events.recv()? {
|
||||||
|
Event::Term(event) => match event.into() {
|
||||||
|
Input { key: Key::Esc, .. } => break,
|
||||||
|
input => {
|
||||||
|
textarea.input(input);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Event::Tick => {}
|
||||||
|
}
|
||||||
|
term.draw(|f| {
|
||||||
|
let widget = textarea.widget();
|
||||||
|
f.render_widget(widget, f.size());
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(term); // Leave terminal raw mode to print the following line
|
||||||
|
println!("Lines: {:?}", textarea.lines());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -321,3 +321,31 @@ impl CursorMove {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
// Seaparate tests for ratatui support
|
||||||
|
#[test]
|
||||||
|
fn in_viewport() {
|
||||||
|
use crate::tui::buffer::Buffer;
|
||||||
|
use crate::tui::layout::Rect;
|
||||||
|
use crate::tui::widgets::Widget;
|
||||||
|
use crate::{CursorMove, TextArea};
|
||||||
|
|
||||||
|
let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
|
||||||
|
let r = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 24,
|
||||||
|
height: 8,
|
||||||
|
};
|
||||||
|
let mut b = Buffer::empty(r.clone());
|
||||||
|
textarea.widget().render(r, &mut b);
|
||||||
|
|
||||||
|
textarea.move_cursor(CursorMove::Bottom);
|
||||||
|
assert_eq!(textarea.cursor(), (19, 0));
|
||||||
|
|
||||||
|
textarea.move_cursor(CursorMove::InViewport);
|
||||||
|
assert_eq!(textarea.cursor(), (7, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
use crate::tui::style::Style;
|
||||||
|
use crate::tui::text::{Span, Spans};
|
||||||
use crate::util::{num_digits, spaces};
|
use crate::util::{num_digits, spaces};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use tui::style::Style;
|
|
||||||
use tui::text::{Span, Spans};
|
|
||||||
|
|
||||||
enum Boundary {
|
enum Boundary {
|
||||||
Cursor(Style),
|
Cursor(Style),
|
||||||
|
|||||||
22
src/input.rs
22
src/input.rs
@@ -1,11 +1,11 @@
|
|||||||
#[cfg(feature = "arbitrary")]
|
#[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))]
|
||||||
use arbitrary::Arbitrary;
|
use crate::crossterm::event::{
|
||||||
#[cfg(feature = "crossterm")]
|
|
||||||
use crossterm::event::{
|
|
||||||
Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent as CrosstermMouseEvent,
|
Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent as CrosstermMouseEvent,
|
||||||
MouseEventKind as CrosstermMouseEventKind,
|
MouseEventKind as CrosstermMouseEventKind,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(feature = "arbitrary")]
|
||||||
|
use arbitrary::Arbitrary;
|
||||||
|
#[cfg(any(feature = "termion", feature = "ratatui-termion"))]
|
||||||
use termion::event::{Event as TermionEvent, Key as TermionKey, MouseEvent as TermionMouseEvent};
|
use termion::event::{Event as TermionEvent, Key as TermionKey, MouseEvent as TermionMouseEvent};
|
||||||
|
|
||||||
/// Backend-agnostic key input kind.
|
/// Backend-agnostic key input kind.
|
||||||
@@ -95,7 +95,7 @@ impl Default for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))]
|
||||||
impl From<CrosstermEvent> for Input {
|
impl From<CrosstermEvent> for Input {
|
||||||
/// Convert [`crossterm::event::Event`] to [`Input`].
|
/// Convert [`crossterm::event::Event`] to [`Input`].
|
||||||
fn from(event: CrosstermEvent) -> Self {
|
fn from(event: CrosstermEvent) -> Self {
|
||||||
@@ -107,7 +107,7 @@ impl From<CrosstermEvent> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))]
|
||||||
impl From<KeyEvent> for Input {
|
impl From<KeyEvent> for Input {
|
||||||
/// Convert [`crossterm::event::KeyEvent`] to [`Input`].
|
/// Convert [`crossterm::event::KeyEvent`] to [`Input`].
|
||||||
fn from(key: KeyEvent) -> Self {
|
fn from(key: KeyEvent) -> Self {
|
||||||
@@ -135,7 +135,7 @@ impl From<KeyEvent> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(any(feature = "crossterm", feature = "ratatui-crossterm"))]
|
||||||
impl From<CrosstermMouseEvent> for Input {
|
impl From<CrosstermMouseEvent> for Input {
|
||||||
/// Convert [`crossterm::event::MouseEvent`] to [`Input`].
|
/// Convert [`crossterm::event::MouseEvent`] to [`Input`].
|
||||||
fn from(mouse: CrosstermMouseEvent) -> Self {
|
fn from(mouse: CrosstermMouseEvent) -> Self {
|
||||||
@@ -150,7 +150,7 @@ impl From<CrosstermMouseEvent> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(any(feature = "termion", feature = "ratatui-termion"))]
|
||||||
impl From<TermionEvent> for Input {
|
impl From<TermionEvent> for Input {
|
||||||
/// Convert [`termion::event::Event`] to [`Input`].
|
/// Convert [`termion::event::Event`] to [`Input`].
|
||||||
fn from(event: TermionEvent) -> Self {
|
fn from(event: TermionEvent) -> Self {
|
||||||
@@ -162,7 +162,7 @@ impl From<TermionEvent> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(any(feature = "termion", feature = "ratatui-termion"))]
|
||||||
impl From<TermionKey> for Input {
|
impl From<TermionKey> for Input {
|
||||||
/// Convert [`termion::event::Key`] to [`Input`].
|
/// Convert [`termion::event::Key`] to [`Input`].
|
||||||
fn from(key: TermionKey) -> Self {
|
fn from(key: TermionKey) -> Self {
|
||||||
@@ -201,7 +201,7 @@ impl From<TermionKey> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(any(feature = "termion", feature = "ratatui-termion"))]
|
||||||
impl From<TermionMouseEvent> for Input {
|
impl From<TermionMouseEvent> for Input {
|
||||||
/// Convert [`termion::event::MouseEvent`] to [`Input`].
|
/// Convert [`termion::event::MouseEvent`] to [`Input`].
|
||||||
fn from(mouse: TermionMouseEvent) -> Self {
|
fn from(mouse: TermionMouseEvent) -> Self {
|
||||||
|
|||||||
28
src/lib.rs
28
src/lib.rs
@@ -1,9 +1,23 @@
|
|||||||
#![allow(clippy::needless_range_loop)]
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
#![allow(clippy::needless_range_loop)]
|
||||||
#![warn(clippy::dbg_macro, clippy::print_stdout)]
|
#![warn(clippy::dbg_macro, clippy::print_stdout)]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
any(feature = "crossterm", feature = "termion"),
|
||||||
|
any(feature = "ratatui-crossterm", feature = "ratatui-termion"),
|
||||||
|
))]
|
||||||
|
compile_error!("tui-rs support and ratatui support are exclussive. only one of them can be enabled at the same time. see https://github.com/rhysd/tui-textarea");
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "crossterm",
|
||||||
|
feature = "termion",
|
||||||
|
feature = "ratatui-crossterm",
|
||||||
|
feature = "ratatui-termion",
|
||||||
|
)))]
|
||||||
|
compile_error!("neither crossterm nor termion is enabled in features. see https://github.com/rhysd/tui-textarea");
|
||||||
|
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod highlight;
|
mod highlight;
|
||||||
mod history;
|
mod history;
|
||||||
@@ -16,6 +30,18 @@ mod util;
|
|||||||
mod widget;
|
mod widget;
|
||||||
mod word;
|
mod word;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "ratatui-crossterm", feature = "ratatui-termion"))]
|
||||||
|
use ratatui as tui;
|
||||||
|
#[cfg(not(any(feature = "ratatui-crossterm", feature = "ratatui-termion")))]
|
||||||
|
#[allow(clippy::single_component_path_imports)]
|
||||||
|
use tui;
|
||||||
|
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
#[allow(clippy::single_component_path_imports)]
|
||||||
|
use crossterm;
|
||||||
|
#[cfg(feature = "ratatui-crossterm")]
|
||||||
|
use crossterm_026 as crossterm;
|
||||||
|
|
||||||
pub use cursor::CursorMove;
|
pub use cursor::CursorMove;
|
||||||
pub use input::{Input, Key};
|
pub use input::{Input, Key};
|
||||||
pub use scroll::Scrolling;
|
pub use scroll::Scrolling;
|
||||||
|
|||||||
@@ -178,3 +178,33 @@ impl From<(i16, i16)> for Scrolling {
|
|||||||
Self::Delta { rows, cols }
|
Self::Delta { rows, cols }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Seaparate tests for ratatui support
|
||||||
|
#[test]
|
||||||
|
fn delta() {
|
||||||
|
use crate::tui::buffer::Buffer;
|
||||||
|
use crate::tui::layout::Rect;
|
||||||
|
use crate::tui::widgets::Widget;
|
||||||
|
use crate::TextArea;
|
||||||
|
|
||||||
|
let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
|
||||||
|
let r = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 24,
|
||||||
|
height: 8,
|
||||||
|
};
|
||||||
|
let mut b = Buffer::empty(r.clone());
|
||||||
|
textarea.widget().render(r, &mut b);
|
||||||
|
|
||||||
|
textarea.scroll(Scrolling::Delta { rows: 2, cols: 0 });
|
||||||
|
assert_eq!(textarea.cursor(), (2, 0));
|
||||||
|
|
||||||
|
textarea.scroll((1, 0));
|
||||||
|
assert_eq!(textarea.cursor(), (3, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::tui::style::{Color, Style};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tui::style::{Color, Style};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ use crate::input::{Input, Key};
|
|||||||
use crate::scroll::Scrolling;
|
use crate::scroll::Scrolling;
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
use crate::search::Search;
|
use crate::search::Search;
|
||||||
|
use crate::tui::layout::Alignment;
|
||||||
|
use crate::tui::style::{Modifier, Style};
|
||||||
|
use crate::tui::text::Spans;
|
||||||
|
use crate::tui::widgets::{Block, Widget};
|
||||||
use crate::util::spaces;
|
use crate::util::spaces;
|
||||||
use crate::widget::{Renderer, Viewport};
|
use crate::widget::{Renderer, Viewport};
|
||||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||||
use tui::layout::Alignment;
|
|
||||||
use tui::style::{Modifier, Style};
|
|
||||||
use tui::text::Spans;
|
|
||||||
use tui::widgets::{Block, Widget};
|
|
||||||
|
|
||||||
/// A type to manage state of textarea.
|
/// A type to manage state of textarea.
|
||||||
///
|
///
|
||||||
@@ -1597,3 +1597,33 @@ impl<'a> TextArea<'a> {
|
|||||||
self.move_cursor(CursorMove::InViewport);
|
self.move_cursor(CursorMove::InViewport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Seaparate tests for ratatui support
|
||||||
|
#[test]
|
||||||
|
fn scroll() {
|
||||||
|
use crate::tui::buffer::Buffer;
|
||||||
|
use crate::tui::layout::Rect;
|
||||||
|
use crate::tui::widgets::Widget;
|
||||||
|
|
||||||
|
let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
|
||||||
|
let r = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 24,
|
||||||
|
height: 8,
|
||||||
|
};
|
||||||
|
let mut b = Buffer::empty(r.clone());
|
||||||
|
textarea.widget().render(r, &mut b);
|
||||||
|
|
||||||
|
textarea.scroll((15, 0));
|
||||||
|
assert_eq!(textarea.cursor(), (15, 0));
|
||||||
|
textarea.scroll((-5, 0));
|
||||||
|
assert_eq!(textarea.cursor(), (15, 0));
|
||||||
|
textarea.scroll((-5, 0));
|
||||||
|
assert_eq!(textarea.cursor(), (12, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::textarea::TextArea;
|
use crate::textarea::TextArea;
|
||||||
|
use crate::tui::buffer::Buffer;
|
||||||
|
use crate::tui::layout::Rect;
|
||||||
|
use crate::tui::text::Text;
|
||||||
|
use crate::tui::widgets::{Paragraph, Widget};
|
||||||
use crate::util::num_digits;
|
use crate::util::num_digits;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::text::Text;
|
|
||||||
use tui::widgets::{Paragraph, Widget};
|
|
||||||
|
|
||||||
// &mut 'a (u16, u16, u16, u16) is not available since Renderer instance totally takes over the ownership of TextArea
|
// &mut 'a (u16, u16, u16, u16) is not available since Renderer instance totally takes over the ownership of TextArea
|
||||||
// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
|
// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user