Struct ratatui_textarea::TextArea
source · pub struct TextArea<'a> { /* private fields */ }
Expand description
A type to manage state of textarea.
TextArea::default
creates an empty textarea. TextArea::new
creates a textarea with given text lines.
TextArea::from
creates a textarea from an iterator of lines. TextArea::input
handles key input.
TextArea::widget
builds a widget to render. And TextArea::lines
returns line texts.
use ratatui_textarea::{TextArea, Input, Key};
let mut textarea = TextArea::default();
// Input 'a'
let input = Input { key: Key::Char('a'), ctrl: false, alt: false };
textarea.input(input);
// Get widget to render.
let widget = textarea.widget();
// Get lines as String.
println!("Lines: {:?}", textarea.lines());
Implementations§
source§impl<'a> TextArea<'a>
impl<'a> TextArea<'a>
sourcepub fn new(lines: Vec<String>) -> Self
pub fn new(lines: Vec<String>) -> Self
Create TextArea
instance with given lines. If you have value other than Vec<String>
, TextArea::from
may be more useful.
use ratatui_textarea::TextArea;
let lines = vec!["hello".to_string(), "...".to_string(), "goodbye".to_string()];
let textarea = TextArea::new(lines);
assert_eq!(textarea.lines(), ["hello", "...", "goodbye"]);
sourcepub fn input(&mut self, input: impl Into<Input>) -> bool
pub fn input(&mut self, input: impl Into<Input>) -> bool
Handle a key input with default key mappings. For default key mappings, see the table in
the module document.
crossterm
and termion
features enable conversion from their own key event types into Input
so this
method can take the event values directly.
This method returns if the input modified text contents or not in the textarea.
use ratatui_textarea::{TextArea, Key, Input};
let mut textarea = TextArea::default();
// Handle crossterm key events
let event: crossterm::event::Event = ...;
textarea.input(event);
if let crossterm::event::Event::Key(key) = event {
textarea.input(key);
}
// Handle termion key events
let event: termion::event::Event = ...;
textarea.input(event);
if let termion::event::Event::Key(key) = event {
textarea.input(key);
}
// Handle backend-agnostic key input
let input = Input { key: Key::Char('a'), ctrl: false, alt: false };
let modified = textarea.input(input);
assert!(modified);
Examples found in repository?
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
More examples
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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(())
}
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
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("Textarea with Variable Height"),
);
loop {
term.draw(|f| {
const MIN_HEIGHT: usize = 3;
let height = cmp::max(textarea.lines().len(), MIN_HEIGHT) as u16 + 2; // + 2 for borders
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(height), Constraint::Min(0)].as_slice())
.split(f.size());
f.render_widget(textarea.widget(), chunks[0]);
})?;
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(())
}
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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_cursor_line_style(Style::default());
let layout =
Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice());
let mut is_valid = validate(&mut textarea);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
let widget = textarea.widget();
f.render_widget(widget, chunks[0]);
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Enter, ..
} if is_valid => break,
Input {
key: Key::Char('m'),
ctrl: true,
..
}
| Input {
key: Key::Enter, ..
} => {}
input => {
// TextArea::input returns if the input modified its text
if textarea.input(input) {
is_valid = validate(&mut textarea);
}
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);
Ok(())
}
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
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::default()];
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref());
let mut which = 0;
activate(&mut textarea[0]);
inactivate(&mut textarea[1]);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
for (textarea, chunk) in textarea.iter().zip(chunks.iter()) {
let widget = textarea.widget();
f.render_widget(widget, *chunk);
}
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Char('x'),
ctrl: true,
..
} => {
inactivate(&mut textarea[which]);
which = (which + 1) % 2;
activate(&mut textarea[which]);
}
input => {
textarea[which].input(input);
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Left textarea: {:?}", textarea[0].lines());
println!("Right textarea: {:?}", textarea[1].lines());
Ok(())
}
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool
pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool
Handle a key input without default key mappings. This method handles only
- Single character input without modifier keys
- Tab
- Enter
- Backspace
- Delete
This method returns if the input modified text contents or not in the textarea.
This method is useful when you want to define your own key mappings and don’t want default key mappings. See ‘Define your own key mappings’ section in the module document.
sourcepub fn insert_char(&mut self, c: char)
pub fn insert_char(&mut self, c: char)
Insert a single character at current cursor position.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.insert_char('a');
assert_eq!(textarea.lines(), ["a"]);
sourcepub fn insert_str<S: Into<String>>(&mut self, s: S) -> bool
pub fn insert_str<S: Into<String>>(&mut self, s: S) -> bool
Insert a string at current cursor position. Currently the string must not contain any newlines. This method returns if some text was inserted or not in the textarea.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.insert_str("hello");
assert_eq!(textarea.lines(), ["hello"]);
sourcepub fn delete_str(&mut self, col: usize, chars: usize) -> bool
pub fn delete_str(&mut self, col: usize, chars: usize) -> bool
Delete a string in current cursor line. The chars
parameter means number of characters, not a byte length of
the string. This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["🐱🐶🐰🐮"]);
textarea.delete_str(1, 2);
assert_eq!(textarea.lines(), ["🐱🐮"]);
sourcepub fn insert_tab(&mut self) -> bool
pub fn insert_tab(&mut self) -> bool
Insert a tab at current cursor position. Note that this method does nothing when the tab length is 0. This method returns if a tab string was inserted or not in the textarea. textarea.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["hi"]);
textarea.insert_tab();
assert_eq!(textarea.lines(), [" hi"]);
sourcepub fn insert_newline(&mut self)
pub fn insert_newline(&mut self)
Insert a newline at current cursor position.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["hi"]);
textarea.move_cursor(CursorMove::Forward);
textarea.insert_newline();
assert_eq!(textarea.lines(), ["h", "i"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn delete_newline(&mut self) -> bool
pub fn delete_newline(&mut self) -> bool
Delete a newline from head of current cursor line. This method returns if a newline was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["hello", "world"]);
textarea.move_cursor(CursorMove::Down);
textarea.delete_newline();
assert_eq!(textarea.lines(), ["helloworld"]);
sourcepub fn delete_char(&mut self) -> bool
pub fn delete_char(&mut self) -> bool
Delete one character before cursor. When the cursor is at head of line, the newline before the cursor will be removed. This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abc"]);
textarea.move_cursor(CursorMove::Forward);
textarea.delete_char();
assert_eq!(textarea.lines(), ["bc"]);
sourcepub fn delete_next_char(&mut self) -> bool
pub fn delete_next_char(&mut self) -> bool
Delete one character next to cursor. When the cursor is at end of line, the newline next to the cursor will be removed. This method returns if a character was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abc"]);
textarea.move_cursor(CursorMove::Forward);
textarea.delete_next_char();
assert_eq!(textarea.lines(), ["ac"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn delete_line_by_end(&mut self) -> bool
pub fn delete_line_by_end(&mut self) -> bool
Delete string from cursor to end of the line. When the cursor is at end of line, the newline next to the cursor is removed. This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abcde"]);
// Move to 'c'
textarea.move_cursor(CursorMove::Forward);
textarea.move_cursor(CursorMove::Forward);
textarea.delete_line_by_end();
assert_eq!(textarea.lines(), ["ab"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn delete_line_by_head(&mut self) -> bool
pub fn delete_line_by_head(&mut self) -> bool
Delete string from cursor to head of the line. When the cursor is at head of line, the newline before the cursor will be removed. This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abcde"]);
// Move to 'c'
textarea.move_cursor(CursorMove::Forward);
textarea.move_cursor(CursorMove::Forward);
textarea.delete_line_by_head();
assert_eq!(textarea.lines(), ["cde"]);
sourcepub fn delete_word(&mut self) -> bool
pub fn delete_word(&mut self) -> bool
Delete a word before cursor. Word boundary appears at spaces, punctuations, and others. For example fn foo(a)
consists of words fn
, foo
, (
, a
, )
. When the cursor is at head of line, the newline before the cursor
will be removed.
This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["aaa bbb ccc"]);
textarea.move_cursor(CursorMove::End);
textarea.delete_word();
assert_eq!(textarea.lines(), ["aaa bbb "]);
textarea.delete_word();
assert_eq!(textarea.lines(), ["aaa "]);
sourcepub fn delete_next_word(&mut self) -> bool
pub fn delete_next_word(&mut self) -> bool
Delete a word next to cursor. Word boundary appears at spaces, punctuations, and others. For example fn foo(a)
consists of words fn
, foo
, (
, a
, )
. When the cursor is at end of line, the newline next to the cursor
will be removed.
This method returns if some text was deleted or not in the textarea.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["aaa bbb ccc"]);
textarea.delete_next_word();
assert_eq!(textarea.lines(), [" bbb ccc"]);
textarea.delete_next_word();
assert_eq!(textarea.lines(), [" ccc"]);
sourcepub fn paste(&mut self) -> bool
pub fn paste(&mut self) -> bool
Paste a string previously deleted by TextArea::delete_line_by_head
, TextArea::delete_line_by_end
,
TextArea::delete_word
, TextArea::delete_next_word
. This method returns if some text was inserted or not
in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["aaa bbb ccc"]);
textarea.delete_next_word();
textarea.move_cursor(CursorMove::End);
textarea.paste();
assert_eq!(textarea.lines(), [" bbb cccaaa"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn move_cursor(&mut self, m: CursorMove)
pub fn move_cursor(&mut self, m: CursorMove)
Move the cursor to the position specified by the CursorMove
parameter. For each kind of cursor moves, see
the document of CursorMove
.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abc", "def"]);
textarea.move_cursor(CursorMove::Forward);
assert_eq!(textarea.cursor(), (0, 1));
textarea.move_cursor(CursorMove::Down);
assert_eq!(textarea.cursor(), (1, 1));
Examples found in repository?
More examples
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn undo(&mut self) -> bool
pub fn undo(&mut self) -> bool
Undo the last modification. This method returns if the undo modified text contents or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abc def"]);
textarea.delete_next_word();
assert_eq!(textarea.lines(), [" def"]);
textarea.undo();
assert_eq!(textarea.lines(), ["abc def"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn redo(&mut self) -> bool
pub fn redo(&mut self) -> bool
Redo the last undo change. This method returns if the redo modified text contents or not in the textarea.
use ratatui_textarea::{TextArea, CursorMove};
let mut textarea = TextArea::from(["abc def"]);
textarea.delete_next_word();
assert_eq!(textarea.lines(), [" def"]);
textarea.undo();
assert_eq!(textarea.lines(), ["abc def"]);
textarea.redo();
assert_eq!(textarea.lines(), [" def"]);
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn widget(&'a self) -> impl Widget + 'a
pub fn widget(&'a self) -> impl Widget + 'a
Build a ratatui widget to render the current state of the textarea. The widget instance returned from this
method can be rendered with [tui::terminal::Frame::render_widget
].
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::Terminal;
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1)].as_ref());
let backend = CrosstermBackend::new(std::io::stdout());
let mut term = Terminal::new(backend).unwrap();
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
let widget = textarea.widget();
f.render_widget(widget, chunks[0]);
}).unwrap();
// ...
}
Examples found in repository?
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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(())
}
More examples
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
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("Textarea with Variable Height"),
);
loop {
term.draw(|f| {
const MIN_HEIGHT: usize = 3;
let height = cmp::max(textarea.lines().len(), MIN_HEIGHT) as u16 + 2; // + 2 for borders
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(height), Constraint::Min(0)].as_slice())
.split(f.size());
f.render_widget(textarea.widget(), chunks[0]);
})?;
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(())
}
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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_cursor_line_style(Style::default());
let layout =
Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice());
let mut is_valid = validate(&mut textarea);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
let widget = textarea.widget();
f.render_widget(widget, chunks[0]);
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Enter, ..
} if is_valid => break,
Input {
key: Key::Char('m'),
ctrl: true,
..
}
| Input {
key: Key::Enter, ..
} => {}
input => {
// TextArea::input returns if the input modified its text
if textarea.input(input) {
is_valid = validate(&mut textarea);
}
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);
Ok(())
}
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
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::default()];
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref());
let mut which = 0;
activate(&mut textarea[0]);
inactivate(&mut textarea[1]);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
for (textarea, chunk) in textarea.iter().zip(chunks.iter()) {
let widget = textarea.widget();
f.render_widget(widget, *chunk);
}
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Char('x'),
ctrl: true,
..
} => {
inactivate(&mut textarea[which]);
which = (which + 1) % 2;
activate(&mut textarea[which]);
}
input => {
textarea[which].input(input);
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Left textarea: {:?}", textarea[0].lines());
println!("Right textarea: {:?}", textarea[1].lines());
Ok(())
}
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
sourcepub fn set_style(&mut self, style: Style)
pub fn set_style(&mut self, style: Style)
Set the style of textarea. By default, textarea is not styled.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
let style = Style::default().fg(Color::Red);
textarea.set_style(style);
assert_eq!(textarea.style(), style);
Examples found in repository?
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
fn validate(textarea: &mut TextArea) -> bool {
if let Err(err) = textarea.lines()[0].parse::<f64>() {
textarea.set_style(Style::default().fg(Color::LightRed));
textarea.set_block(
Block::default()
.borders(Borders::ALL)
.title(format!("ERROR: {}", err)),
);
false
} else {
textarea.set_style(Style::default().fg(Color::LightGreen));
textarea.set_block(Block::default().borders(Borders::ALL).title("OK"));
true
}
}
sourcepub fn set_block(&mut self, block: Block<'a>)
pub fn set_block(&mut self, block: Block<'a>)
Set the block of textarea. By default, no block is set.
use ratatui_textarea::TextArea;
use ratatui::widgets::{Block, Borders};
let mut textarea = TextArea::default();
let block = Block::default().borders(Borders::ALL).title("Block Title");
textarea.set_block(block);
assert!(textarea.block().is_some());
Examples found in repository?
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
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);
}
More examples
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
fn inactivate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(
b.style(Style::default().fg(Color::DarkGray))
.title(" Inactive (^X to switch) "),
);
}
fn activate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()).title(" Active "));
}
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
fn validate(textarea: &mut TextArea) -> bool {
if let Err(err) = textarea.lines()[0].parse::<f64>() {
textarea.set_style(Style::default().fg(Color::LightRed));
textarea.set_block(
Block::default()
.borders(Borders::ALL)
.title(format!("ERROR: {}", err)),
);
false
} else {
textarea.set_style(Style::default().fg(Color::LightGreen));
textarea.set_block(Block::default().borders(Borders::ALL).title("OK"));
true
}
}
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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(())
}
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
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("Textarea with Variable Height"),
);
loop {
term.draw(|f| {
const MIN_HEIGHT: usize = 3;
let height = cmp::max(textarea.lines().len(), MIN_HEIGHT) as u16 + 2; // + 2 for borders
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(height), Constraint::Min(0)].as_slice())
.split(f.size());
f.render_widget(textarea.widget(), chunks[0]);
})?;
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(())
}
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn remove_block(&mut self)
pub fn remove_block(&mut self)
Remove the block of textarea which was set by TextArea::set_block
.
use ratatui_textarea::TextArea;
use ratatui::widgets::{Block, Borders};
let mut textarea = TextArea::default();
let block = Block::default().borders(Borders::ALL).title("Block Title");
textarea.set_block(block);
textarea.remove_block();
assert!(textarea.block().is_none());
sourcepub fn block<'s>(&'s self) -> Option<&'s Block<'a>>
pub fn block<'s>(&'s self) -> Option<&'s Block<'a>>
Get the block of textarea if exists.
Examples found in repository?
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
fn inactivate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(
b.style(Style::default().fg(Color::DarkGray))
.title(" Inactive (^X to switch) "),
);
}
fn activate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()).title(" Active "));
}
sourcepub fn set_tab_length(&mut self, len: u8)
pub fn set_tab_length(&mut self, len: u8)
Set the length of tab character. Due to limitation of ratatui, hard tab is not supported. Setting 0 disables tab inputs.
use ratatui_textarea::{TextArea, Input, Key};
let mut textarea = TextArea::default();
let tab_input = Input { key: Key::Tab, ctrl: false, alt: false };
textarea.set_tab_length(8);
textarea.input(tab_input.clone());
assert_eq!(textarea.lines(), [" "]);
textarea.set_tab_length(2);
textarea.input(tab_input);
assert_eq!(textarea.lines(), [" "]);
sourcepub fn tab_length(&self) -> u8
pub fn tab_length(&self) -> u8
Get how many spaces are used for representing tab character. The default value is 4.
sourcepub fn set_hard_tab_indent(&mut self, enabled: bool)
pub fn set_hard_tab_indent(&mut self, enabled: bool)
Set if a hard tab is used or not for indent. When true
is set, typing a tab key inserts a hard tab instead of
spaces. By default, hard tab is disabled.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.set_hard_tab_indent(true);
textarea.insert_tab();
assert_eq!(textarea.lines(), ["\t"]);
Examples found in repository?
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
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,
})
}
sourcepub fn hard_tab_indent(&self) -> bool
pub fn hard_tab_indent(&self) -> bool
Get if a hard tab is used for indent or not.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
assert!(!textarea.hard_tab_indent());
textarea.set_hard_tab_indent(true);
assert!(textarea.hard_tab_indent());
sourcepub fn indent(&self) -> &'static str
pub fn indent(&self) -> &'static str
Get a string for indent. It consists of spaces by default. When hard tab is enabled, it is a tab character.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
assert_eq!(textarea.indent(), " ");
textarea.set_tab_length(2);
assert_eq!(textarea.indent(), " ");
textarea.set_hard_tab_indent(true);
assert_eq!(textarea.indent(), "\t");
sourcepub fn set_max_histories(&mut self, max: usize)
pub fn set_max_histories(&mut self, max: usize)
Set how many modifications are remembered for undo/redo. Setting 0 disables undo/redo.
sourcepub fn max_histories(&self) -> usize
pub fn max_histories(&self) -> usize
Get how many modifications are remembered for undo/redo. The default value is 50.
sourcepub fn set_cursor_line_style(&mut self, style: Style)
pub fn set_cursor_line_style(&mut self, style: Style)
Set the style of line at cursor. By default, the cursor line is styled with underline. To stop styling the cursor line, set the default style.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
let style = Style::default().fg(Color::Red);
textarea.set_cursor_line_style(style);
assert_eq!(textarea.cursor_line_style(), style);
// Disable cursor line style
textarea.set_cursor_line_style(Style::default());
Examples found in repository?
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
fn inactivate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(
b.style(Style::default().fg(Color::DarkGray))
.title(" Inactive (^X to switch) "),
);
}
fn activate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()).title(" Active "));
}
More examples
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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_cursor_line_style(Style::default());
let layout =
Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice());
let mut is_valid = validate(&mut textarea);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
let widget = textarea.widget();
f.render_widget(widget, chunks[0]);
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Enter, ..
} if is_valid => break,
Input {
key: Key::Char('m'),
ctrl: true,
..
}
| Input {
key: Key::Enter, ..
} => {}
input => {
// TextArea::input returns if the input modified its text
if textarea.input(input) {
is_valid = validate(&mut textarea);
}
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);
Ok(())
}
sourcepub fn cursor_line_style(&self) -> Style
pub fn cursor_line_style(&self) -> Style
Get the style of cursor line. By default it is styled with underline.
sourcepub fn set_line_number_style(&mut self, style: Style)
pub fn set_line_number_style(&mut self, style: Style)
Set the style of line number. By setting the style with this method, line numbers are drawn in textarea, meant that line numbers are disabled by default. If you want to show line numbers but don’t want to style them, set the default style.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
// Show line numbers in dark gray background
let style = Style::default().bg(Color::DarkGray);
textarea.set_line_number_style(style);
assert_eq!(textarea.line_number_style(), Some(style));
Examples found in repository?
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
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,
})
}
sourcepub fn remove_line_number(&mut self)
pub fn remove_line_number(&mut self)
Remove the style of line number which was set by TextArea::set_line_number_style
. After calling this
method, Line numbers will no longer be shown.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.set_line_number_style(Style::default().bg(Color::DarkGray));
textarea.remove_line_number();
assert_eq!(textarea.line_number_style(), None);
sourcepub fn line_number_style(&self) -> Option<Style>
pub fn line_number_style(&self) -> Option<Style>
Get the style of line number if set.
sourcepub fn set_cursor_style(&mut self, style: Style)
pub fn set_cursor_style(&mut self, style: Style)
Set the style of cursor. By default, a cursor is rendered in the reversed color. Setting the same style as cursor line hides a cursor.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
let style = Style::default().bg(Color::Red);
textarea.set_cursor_style(style);
assert_eq!(textarea.cursor_style(), style);
Examples found in repository?
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
fn inactivate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(
b.style(Style::default().fg(Color::DarkGray))
.title(" Inactive (^X to switch) "),
);
}
fn activate(textarea: &mut TextArea<'_>) {
textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
let b = textarea
.block()
.cloned()
.unwrap_or_else(|| Block::default().borders(Borders::ALL));
textarea.set_block(b.style(Style::default()).title(" Active "));
}
More examples
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn cursor_style(&self) -> Style
pub fn cursor_style(&self) -> Style
Get the style of cursor.
sourcepub fn lines(&'a self) -> &'a [String]
pub fn lines(&'a self) -> &'a [String]
Get slice of line texts. This method borrows the content, but not moves. Note that the returned slice will never be empty because an empty text means a slice containing one empty line. This is correct since any text file must end with a newline.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
assert_eq!(textarea.lines(), [""]);
textarea.insert_char('a');
assert_eq!(textarea.lines(), ["a"]);
textarea.insert_newline();
assert_eq!(textarea.lines(), ["a", ""]);
textarea.insert_char('b');
assert_eq!(textarea.lines(), ["a", "b"]);
Examples found in repository?
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
fn validate(textarea: &mut TextArea) -> bool {
if let Err(err) = textarea.lines()[0].parse::<f64>() {
textarea.set_style(Style::default().fg(Color::LightRed));
textarea.set_block(
Block::default()
.borders(Borders::ALL)
.title(format!("ERROR: {}", err)),
);
false
} else {
textarea.set_style(Style::default().fg(Color::LightGreen));
textarea.set_block(Block::default().borders(Borders::ALL).title("OK"));
true
}
}
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_cursor_line_style(Style::default());
let layout =
Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice());
let mut is_valid = validate(&mut textarea);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
let widget = textarea.widget();
f.render_widget(widget, chunks[0]);
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Enter, ..
} if is_valid => break,
Input {
key: Key::Char('m'),
ctrl: true,
..
}
| Input {
key: Key::Enter, ..
} => {}
input => {
// TextArea::input returns if the input modified its text
if textarea.input(input) {
is_valid = validate(&mut textarea);
}
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);
Ok(())
}
More examples
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
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(())
}
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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(())
}
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
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("Textarea with Variable Height"),
);
loop {
term.draw(|f| {
const MIN_HEIGHT: usize = 3;
let height = cmp::max(textarea.lines().len(), MIN_HEIGHT) as u16 + 2; // + 2 for borders
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(height), Constraint::Min(0)].as_slice())
.split(f.size());
f.render_widget(textarea.widget(), chunks[0]);
})?;
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(())
}
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
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::default()];
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref());
let mut which = 0;
activate(&mut textarea[0]);
inactivate(&mut textarea[1]);
loop {
term.draw(|f| {
let chunks = layout.split(f.size());
for (textarea, chunk) in textarea.iter().zip(chunks.iter()) {
let widget = textarea.widget();
f.render_widget(widget, *chunk);
}
})?;
match crossterm::event::read()?.into() {
Input { key: Key::Esc, .. } => break,
Input {
key: Key::Char('x'),
ctrl: true,
..
} => {
inactivate(&mut textarea[which]);
which = (which + 1) % 2;
activate(&mut textarea[which]);
}
input => {
textarea[which].input(input);
}
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Left textarea: {:?}", textarea[0].lines());
println!("Right textarea: {:?}", textarea[1].lines());
Ok(())
}
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
sourcepub fn into_lines(self) -> Vec<String>
pub fn into_lines(self) -> Vec<String>
Convert TextArea
instance into line texts.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.insert_char('a');
textarea.insert_newline();
textarea.insert_char('b');
assert_eq!(textarea.into_lines(), ["a", "b"]);
sourcepub fn cursor(&self) -> (usize, usize)
pub fn cursor(&self) -> (usize, usize)
Get the current cursor position. 0-base character-wise (row, col) cursor position.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
assert_eq!(textarea.cursor(), (0, 0));
textarea.insert_char('a');
textarea.insert_newline();
textarea.insert_char('b');
assert_eq!(textarea.cursor(), (1, 1));
Examples found in repository?
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
sourcepub fn set_alignment(&mut self, alignment: Alignment)
pub fn set_alignment(&mut self, alignment: Alignment)
Set text alignment. When Alignment::Center
or Alignment::Right
is set, line number is automatically
disabled because those alignments don’t work well with line numbers.
use ratatui_textarea::TextArea;
use ratatui::layout::Alignment;
let mut textarea = TextArea::default();
textarea.set_alignment(Alignment::Center);
assert_eq!(textarea.alignment(), Alignment::Center);
sourcepub fn alignment(&self) -> Alignment
pub fn alignment(&self) -> Alignment
Get current text alignment. The default alignment is Alignment::Left
.
use ratatui_textarea::TextArea;
use ratatui::layout::Alignment;
let mut textarea = TextArea::default();
assert_eq!(textarea.alignment(), Alignment::Left);
sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Check if the textarea has a empty content.
use ratatui_textarea::TextArea;
let textarea = TextArea::default();
assert!(textarea.is_empty());
let textarea = TextArea::from(["hello"]);
assert!(!textarea.is_empty());
sourcepub fn yank_text(&'a self) -> &'a str
pub fn yank_text(&'a self) -> &'a str
Get the yanked text. Text is automatically yanked when deleting strings by TextArea::delete_line_by_head
,
TextArea::delete_line_by_end
, TextArea::delete_word
, TextArea::delete_next_word
.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["abc"]);
textarea.delete_next_word();
assert_eq!(textarea.yank_text(), "abc");
sourcepub fn set_yank_text(&mut self, text: impl Into<String>)
pub fn set_yank_text(&mut self, text: impl Into<String>)
Set a yanked text. The text can be inserted by TextArea::paste
. The string passed to method must not contain
any newlines.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
textarea.set_yank_text("hello, world");
textarea.paste();
assert_eq!(textarea.lines(), ["hello, world"]);
sourcepub fn set_search_pattern(
&mut self,
query: impl AsRef<str>
) -> Result<(), Error>
Available on crate feature search
only.
pub fn set_search_pattern( &mut self, query: impl AsRef<str> ) -> Result<(), Error>
search
only.Set a regular expression pattern for text search. Setting an empty string stops the text search.
When a valid pattern is set, all matches will be highlighted in the textarea. Note that the cursor does not
move. To move the cursor, use TextArea::search_forward
and TextArea::search_back
.
Grammar of regular expression follows regex crate. Patterns don’t match to newlines so match passes across no newline.
When the pattern is invalid, the search pattern will not be updated and an error will be returned.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["hello, world", "goodbye, world"]);
// Search "world"
textarea.set_search_pattern("world").unwrap();
assert_eq!(textarea.cursor(), (0, 0));
textarea.search_forward(false);
assert_eq!(textarea.cursor(), (0, 7));
textarea.search_forward(false);
assert_eq!(textarea.cursor(), (1, 9));
// Stop the text search
textarea.set_search_pattern("");
// Invalid search pattern
assert!(textarea.set_search_pattern("(hello").is_err());
Examples found in repository?
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
sourcepub fn search_pattern(&self) -> Option<&Regex>
Available on crate feature search
only.
pub fn search_pattern(&self) -> Option<&Regex>
search
only.Get a regular expression which was set by TextArea::set_search_pattern
. When no text search is ongoing, this
method returns None
.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
assert!(textarea.search_pattern().is_none());
textarea.set_search_pattern("hello+").unwrap();
assert!(textarea.search_pattern().is_some());
assert_eq!(textarea.search_pattern().unwrap().as_str(), "hello+");
sourcepub fn search_forward(&mut self, match_cursor: bool) -> bool
Available on crate feature search
only.
pub fn search_forward(&mut self, match_cursor: bool) -> bool
search
only.Search the pattern set by TextArea::set_search_pattern
forward and move the cursor to the next match
position based on the current cursor position. Text search wraps around a text buffer. It returns true
when
some match was found. Otherwise it returns false
.
The match_cursor
parameter represents if the search matches to the current cursor position or not. When true
is set and the cursor position matches to the pattern, the cursor will not move. When false
, the cursor will
move to the next match ignoring the match at the current position.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
textarea.set_search_pattern("hello+").unwrap();
// Move to next position
let match_found = textarea.search_forward(false);
assert!(match_found);
assert_eq!(textarea.cursor(), (1, 0));
// Since the cursor position matches to "hello+", it does not move
textarea.search_forward(true);
assert_eq!(textarea.cursor(), (1, 0));
// When `match_current` parameter is set to `false`, match at the cursor position is ignored
textarea.search_forward(false);
assert_eq!(textarea.cursor(), (2, 0));
// Text search wrap around the buffer
textarea.search_forward(false);
assert_eq!(textarea.cursor(), (0, 0));
// `false` is returned when no match was found
textarea.set_search_pattern("bye+").unwrap();
let match_found = textarea.search_forward(false);
assert!(!match_found);
Examples found in repository?
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
sourcepub fn search_back(&mut self, match_cursor: bool) -> bool
Available on crate feature search
only.
pub fn search_back(&mut self, match_cursor: bool) -> bool
search
only.Search the pattern set by TextArea::set_search_pattern
backward and move the cursor to the next match
position based on the current cursor position. Text search wraps around a text buffer. It returns true
when
some match was found. Otherwise it returns false
.
The match_cursor
parameter represents if the search matches to the current cursor position or not. When true
is set and the cursor position matches to the pattern, the cursor will not move. When false
, the cursor will
move to the next match ignoring the match at the current position.
use ratatui_textarea::TextArea;
let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
textarea.set_search_pattern("hello+").unwrap();
// Move to next position with wrapping around the text buffer
let match_found = textarea.search_back(false);
assert!(match_found);
assert_eq!(textarea.cursor(), (2, 0));
// Since the cursor position matches to "hello+", it does not move
textarea.search_back(true);
assert_eq!(textarea.cursor(), (2, 0));
// When `match_current` parameter is set to `false`, match at the cursor position is ignored
textarea.search_back(false);
assert_eq!(textarea.cursor(), (1, 0));
// `false` is returned when no match was found
textarea.set_search_pattern("bye+").unwrap();
let match_found = textarea.search_back(false);
assert!(!match_found);
Examples found in repository?
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
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() {
Line::from(Span::raw(message))
} else if search_height > 0 {
Line::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 {
Line::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(())
}
sourcepub fn search_style(&self) -> Style
Available on crate feature search
only.
pub fn search_style(&self) -> Style
search
only.Get the text style at matches of text search. The default style is colored with blue in background.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let textarea = TextArea::default();
assert_eq!(textarea.search_style(), Style::default().bg(Color::Blue));
sourcepub fn set_search_style(&mut self, style: Style)
Available on crate feature search
only.
pub fn set_search_style(&mut self, style: Style)
search
only.Set the text style at matches of text search. The default style is colored with blue in background.
use ratatui::style::{Style, Color};
use ratatui_textarea::TextArea;
let mut textarea = TextArea::default();
let red_bg = Style::default().bg(Color::Red);
textarea.set_search_style(red_bg);
assert_eq!(textarea.search_style(), red_bg);
sourcepub fn scroll(&mut self, scrolling: impl Into<Scrolling>)
pub fn scroll(&mut self, scrolling: impl Into<Scrolling>)
Scroll the textarea. See Scrolling
for the argument.
The cursor will not move until it goes out the viewport. When the cursor position is outside the viewport after scroll,
the cursor position will be adjusted to stay in the viewport using the same logic as CursorMove::InViewport
.
use ratatui_textarea::TextArea;
// Let's say terminal height is 8.
// Create textarea with 20 lines "0", "1", "2", "3", ...
let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
// Scroll down by 15 lines. Since terminal height is 8, cursor will go out
// the viewport.
textarea.scroll((15, 0));
// So the cursor position was updated to stay in the viewport after the scrolling.
assert_eq!(textarea.cursor(), (15, 0));
// Scroll up by 5 lines. Since the scroll amount is smaller than the terminal
// height, cursor position will not be updated.
textarea.scroll((-5, 0));
assert_eq!(textarea.cursor(), (15, 0));
// Scroll up by 5 lines again. The terminal height is 8. So a cursor reaches to
// the top of viewport after scrolling up by 7 lines. Since we have already
// scrolled up by 5 lines, scrolling up by 5 lines again makes the cursor overrun
// the viewport by 5 - 2 = 3 lines. To keep the cursor stay in the viewport, the
// cursor position will be adjusted from line 15 to line 12.
textarea.scroll((-5, 0));
assert_eq!(textarea.cursor(), (12, 0));
Examples found in repository?
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
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 = if let Some(path) = env::args().nth(1) {
let file = fs::File::open(path)?;
io::BufReader::new(file)
.lines()
.collect::<io::Result<_>>()?
} else {
TextArea::default()
};
let mut mode = Mode::Normal;
loop {
// Show help message and current mode in title of the block
let title = format!("{} MODE ({})", mode, mode.help_message());
let block = Block::default().borders(Borders::ALL).title(title);
textarea.set_block(block);
// Change the cursor color looking at current mode
let color = mode.cursor_color();
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
textarea.set_cursor_style(style);
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into();
match mode {
Mode::Normal => match input {
// Mappings in normal mode
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('w'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Input {
key: Key::Char('^'),
..
} => textarea.move_cursor(CursorMove::Head),
Input {
key: Key::Char('$'),
..
} => textarea.move_cursor(CursorMove::End),
Input {
key: Key::Char('D'),
..
} => {
textarea.delete_line_by_end();
}
Input {
key: Key::Char('C'),
..
} => {
textarea.delete_line_by_end();
mode = Mode::Insert;
}
Input {
key: Key::Char('p'),
..
} => {
textarea.paste();
}
Input {
key: Key::Char('u'),
ctrl: false,
..
} => {
textarea.undo();
}
Input {
key: Key::Char('r'),
ctrl: true,
..
} => {
textarea.redo();
}
Input {
key: Key::Char('x'),
..
} => {
textarea.delete_next_char();
}
Input {
key: Key::Char('i'),
..
} => mode = Mode::Insert,
Input {
key: Key::Char('a'),
..
} => {
textarea.move_cursor(CursorMove::Forward);
mode = Mode::Insert;
}
Input {
key: Key::Char('A'),
..
} => {
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Input {
key: Key::Char('o'),
..
} => {
textarea.move_cursor(CursorMove::End);
textarea.insert_newline();
mode = Mode::Insert;
}
Input {
key: Key::Char('O'),
..
} => {
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
mode = Mode::Insert;
}
Input {
key: Key::Char('I'),
..
} => {
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Input {
key: Key::Char('q'),
..
} => break,
Input {
key: Key::Char('e'),
ctrl: true,
..
} => textarea.scroll((1, 0)),
Input {
key: Key::Char('y'),
ctrl: true,
..
} => textarea.scroll((-1, 0)),
Input {
key: Key::Char('d'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageDown),
Input {
key: Key::Char('u'),
ctrl: true,
..
} => textarea.scroll(Scrolling::HalfPageUp),
Input {
key: Key::Char('f'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageDown),
Input {
key: Key::Char('b'),
ctrl: true,
..
} => textarea.scroll(Scrolling::PageUp),
_ => {}
},
Mode::Insert => match input {
Input { key: Key::Esc, .. }
| Input {
key: Key::Char('c'),
ctrl: true,
..
} => {
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
}
input => {
textarea.input(input); // Use default key mappings in insert mode
}
},
}
}
disable_raw_mode()?;
crossterm::execute!(
term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());
Ok(())
}
Trait Implementations§
source§impl<'a> Default for TextArea<'a>
impl<'a> Default for TextArea<'a>
Create TextArea
instance with empty text content.
use ratatui_textarea::TextArea;
let textarea = TextArea::default();
assert_eq!(textarea.lines(), [""]);
assert!(textarea.is_empty());
source§impl<'a, I> From<I> for TextArea<'a>
impl<'a, I> From<I> for TextArea<'a>
Convert any iterator whose elements can be converted into String
into TextArea
. Each String
element is
handled as line. Ensure that the strings don’t contain any newlines. This method is useful to create TextArea
from std::str::Lines
.
use ratatui_textarea::TextArea;
// From `String`
let text = "hello\nworld";
let textarea = TextArea::from(text.lines());
assert_eq!(textarea.lines(), ["hello", "world"]);
// From array of `&str`
let textarea = TextArea::from(["hello", "world"]);
assert_eq!(textarea.lines(), ["hello", "world"]);
// From slice of `&str`
let slice = &["hello", "world"];
let textarea = TextArea::from(slice.iter().copied());
assert_eq!(textarea.lines(), ["hello", "world"]);
source§impl<'a, S: Into<String>> FromIterator<S> for TextArea<'a>
impl<'a, S: Into<String>> FromIterator<S> for TextArea<'a>
Collect line texts from iterator as TextArea
. It is useful when creating a textarea with text read from a file.
Iterator::collect
handles errors which may happen on reading each lines. The following example reads text from
a file efficiently line-by-line.
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use ratatui_textarea::TextArea;
fn read_from_file<'a>(path: impl AsRef<Path>) -> io::Result<TextArea<'a>> {
let file = fs::File::open(path.as_ref())?;
io::BufReader::new(file).lines().collect()
}