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>

source

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"]);
source

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?
examples/editor.rs (line 75)
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
Hide additional examples
examples/minimal.rs (line 34)
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(())
}
examples/variable.rs (line 42)
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(())
}
examples/single_line.rs (line 66)
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(())
}
examples/split.rs (line 74)
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(())
}
examples/modal.rs (line 242)
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(())
}
source

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.

source

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"]);
source

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"]);
source

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(), ["🐱🐮"]);
source

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"]);
source

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?
examples/modal.rs (line 177)
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(())
}
source

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"]);
source

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"]);
source

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?
examples/modal.rs (line 152)
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(())
}
source

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?
examples/modal.rs (line 119)
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(())
}
source

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"]);
Examples found in repository?
examples/editor.rs (line 53)
48
49
50
51
52
53
54
    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();
    }
source

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 "]);
source

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"]);
source

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?
examples/modal.rs (line 132)
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(())
}
source

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?
examples/editor.rs (line 52)
48
49
50
51
52
53
54
    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();
    }
More examples
Hide additional examples
examples/modal.rs (line 85)
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(())
}
source

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?
examples/modal.rs (line 139)
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(())
}
source

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?
examples/modal.rs (line 146)
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(())
}
source

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?
examples/minimal.rs (line 29)
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
Hide additional examples
examples/variable.rs (line 37)
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(())
}
examples/single_line.rs (line 47)
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(())
}
examples/split.rs (line 58)
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(())
}
examples/modal.rs (line 76)
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(())
}
examples/editor.rs (line 194)
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(())
    }
source

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?
examples/single_line.rs (line 15)
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
    }
}
source

pub fn style(&self) -> Style

Get the current style of textarea.

source

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?
examples/editor.rs (line 35)
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
Hide additional examples
examples/split.rs (lines 20-23)
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 "));
}
examples/single_line.rs (lines 16-20)
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
    }
}
examples/minimal.rs (lines 21-25)
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(())
}
examples/variable.rs (lines 23-27)
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(())
}
examples/modal.rs (line 69)
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(())
}
source

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());
source

pub fn block<'s>(&'s self) -> Option<&'s Block<'a>>

Get the block of textarea if exists.

Examples found in repository?
examples/split.rs (line 17)
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 "));
}
source

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(), ["          "]);
source

pub fn tab_length(&self) -> u8

Get how many spaces are used for representing tab character. The default value is 4.

source

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?
examples/editor.rs (line 108)
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,
        })
    }
source

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());
source

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");
source

pub fn set_max_histories(&mut self, max: usize)

Set how many modifications are remembered for undo/redo. Setting 0 disables undo/redo.

source

pub fn max_histories(&self) -> usize

Get how many modifications are remembered for undo/redo. The default value is 50.

source

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?
examples/split.rs (line 14)
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
Hide additional examples
examples/single_line.rs (line 39)
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(())
}
source

pub fn cursor_line_style(&self) -> Style

Get the style of cursor line. By default it is styled with underline.

source

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?
examples/editor.rs (line 117)
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,
        })
    }
source

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);
source

pub fn line_number_style(&self) -> Option<Style>

Get the style of line number if set.

source

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?
examples/split.rs (line 15)
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
Hide additional examples
examples/modal.rs (line 74)
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(())
}
source

pub fn cursor_style(&self) -> Style

Get the style of cursor.

source

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?
examples/single_line.rs (line 14)
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
Hide additional examples
examples/editor.rs (line 76)
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(())
    }
examples/minimal.rs (line 47)
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(())
}
examples/variable.rs (line 32)
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(())
}
examples/split.rs (line 87)
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(())
}
examples/modal.rs (line 256)
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(())
}
source

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"]);
source

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?
examples/editor.rs (line 206)
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(())
    }
source

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);
source

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);
source

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());
source

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");
source

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"]);
source

pub fn set_search_pattern( &mut self, query: impl AsRef<str> ) -> Result<(), Error>

Available on crate feature 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?
examples/editor.rs (line 296)
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(())
    }
source

pub fn search_pattern(&self) -> Option<&Regex>

Available on crate feature 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+");
source

pub fn search_forward(&mut self, match_cursor: bool) -> bool

Available on crate feature 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?
examples/editor.rs (line 270)
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(())
    }
source

pub fn search_back(&mut self, match_cursor: bool) -> bool

Available on crate feature 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?
examples/editor.rs (line 285)
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(())
    }
source

pub fn search_style(&self) -> Style

Available on crate feature 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));
source

pub fn set_search_style(&mut self, style: Style)

Available on crate feature 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);
source

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?
examples/modal.rs (line 204)
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> Clone for TextArea<'a>

source§

fn clone(&self) -> TextArea<'a>

Returns a copy of the value. Read more
1.0.0 · source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
source§

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§

fn default() -> Self

Returns the “default value” for a type. Read more
source§

impl<'a, I> From<I> for TextArea<'a>
where I: IntoIterator, I::Item: Into<String>,

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§

fn from(i: I) -> Self

Converts to this type from the input type.
source§

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()
}
source§

fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self

Creates a value from an iterator. Read more

Auto Trait Implementations§

§

impl<'a> RefUnwindSafe for TextArea<'a>

§

impl<'a> Send for TextArea<'a>

§

impl<'a> Sync for TextArea<'a>

§

impl<'a> Unpin for TextArea<'a>

§

impl<'a> UnwindSafe for TextArea<'a>

Blanket Implementations§

source§

impl<T> Any for T
where T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for T
where T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for T
where U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> ToOwned for T
where T: Clone,

§

type Owned = T

The resulting type after obtaining ownership.
source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.