1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-09-04 23:05:50 +03:00

feat: add screen/data cursor maps for wrapped movement (#17)

* feat!: remove tui-rs support and map screen cursors

* docs(examples): add fixed-size recording mode

* chore(release): bump to v0.11.0
Этот коммит содержится в:
srothgan
2026-05-03 08:48:05 +02:00
коммит произвёл GitHub
родитель f20df6b965
Коммит 8f708cfdc7
28 изменённых файлов: 782 добавлений и 925 удалений

53
examples/RECORDING.md Обычный файл
Просмотреть файл

@@ -0,0 +1,53 @@
# Example Recording
This directory supports an opt-in recording mode for the crossterm examples so GIF captures can use the same terminal size and aspect ratio.
## Default Size
Set `TUI_TEXTAREA_RECORDING=1` to force the default recording size:
- `120x30`
- aspect ratio: `4:1`
PowerShell:
```powershell
$env:TUI_TEXTAREA_RECORDING=1
cargo run --example minimal
```
## Custom Size
Set `TUI_TEXTAREA_RECORDING_SIZE=<cols>x<rows>` to override the default:
```powershell
$env:TUI_TEXTAREA_RECORDING_SIZE="140x35"
cargo run --example split
```
Valid format:
- `120x30`
- `140x35`
- `160x40`
## Covered Examples
The shared recording-size helper is enabled for these crossterm examples:
- `minimal`
- `editor`
- `single_line`
- `split`
- `variable`
- `vim`
- `popup_placeholder`
- `password`
`termion` and `termwiz` are not wired through this helper.
## Notes
- The helper resizes the terminal on startup and restores the previous size on exit.
- Use one fixed size for a whole GIF set if you want every capture to align cleanly in the README.
- If you only care about consistent aspect ratio, choose any size with the same width-to-height ratio.

65
examples/common/mod.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,65 @@
use crossterm::terminal::{SetSize, size};
use std::env;
use std::io::{self, Write};
const DEFAULT_RECORDING_WIDTH: u16 = 120;
const DEFAULT_RECORDING_HEIGHT: u16 = 30;
const RECORDING_FLAG: &str = "TUI_TEXTAREA_RECORDING";
const RECORDING_SIZE_VAR: &str = "TUI_TEXTAREA_RECORDING_SIZE";
#[derive(Default)]
pub struct RecordingSizeGuard {
original_size: Option<(u16, u16)>,
}
impl RecordingSizeGuard {
pub fn restore<W: Write>(mut self, writer: &mut W) -> io::Result<()> {
if let Some((cols, rows)) = self.original_size.take() {
crossterm::execute!(writer, SetSize(cols, rows))?;
}
Ok(())
}
}
pub fn maybe_force_recording_size<W: Write>(writer: &mut W) -> io::Result<RecordingSizeGuard> {
let Some((cols, rows)) = configured_recording_size()? else {
return Ok(RecordingSizeGuard::default());
};
let original_size = size().ok();
crossterm::execute!(writer, SetSize(cols, rows))?;
Ok(RecordingSizeGuard { original_size })
}
fn configured_recording_size() -> io::Result<Option<(u16, u16)>> {
if let Some(raw) = env::var_os(RECORDING_SIZE_VAR) {
let raw = raw.into_string().map_err(|_| invalid_size())?;
return parse_size(&raw).map(Some);
}
if env::var_os(RECORDING_FLAG).is_some() {
return Ok(Some((DEFAULT_RECORDING_WIDTH, DEFAULT_RECORDING_HEIGHT)));
}
Ok(None)
}
fn parse_size(raw: &str) -> io::Result<(u16, u16)> {
let (width, height) = raw.split_once('x').ok_or_else(invalid_size)?;
let width = width.trim().parse().map_err(|_| invalid_size())?;
let height = height.trim().parse().map_err(|_| invalid_size())?;
if width == 0 || height == 0 {
return Err(invalid_size());
}
Ok((width, height))
}
fn invalid_size() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{RECORDING_SIZE_VAR} must be set as <cols>x<rows>, for example 120x30"),
)
}

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -14,6 +16,7 @@ use std::fmt::Display;
use std::fs;
use std::io;
use std::io::{BufRead, Write};
use std::mem;
use std::path::PathBuf;
use tui_textarea::{CursorMove, Input, Key, TextArea};
@@ -135,6 +138,7 @@ struct Editor<'a> {
current: usize,
buffers: Vec<Buffer<'a>>,
term: Terminal<CrosstermBackend<io::Stdout>>,
recording: common::RecordingSizeGuard,
message: Option<Cow<'static, str>>,
search: SearchBox<'a>,
}
@@ -154,12 +158,14 @@ impl Editor<'_> {
let mut stdout = io::stdout();
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let term = Terminal::new(backend)?;
Ok(Self {
current: 0,
buffers,
term,
recording,
message: None,
search: SearchBox::default(),
})
@@ -348,12 +354,14 @@ impl Drop for Editor<'_> {
fn drop(&mut self) {
self.term.show_cursor().unwrap();
disable_raw_mode().unwrap();
let recording = mem::take(&mut self.recording);
crossterm::execute!(
self.term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)
.unwrap();
recording.restore(self.term.backend_mut()).unwrap();
}
}

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -14,6 +16,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -42,6 +45,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -16,6 +18,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -53,6 +56,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture,
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -16,6 +18,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -54,6 +57,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -38,6 +40,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -81,6 +84,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Input: {:?}", textarea.lines()[0]);

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -37,6 +39,7 @@ fn main() -> io::Result<()> {
let mut stdout = stdout.lock();
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -80,6 +83,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Left textarea: {:?}", textarea[0].lines());

Просмотреть файл

@@ -1,365 +0,0 @@
// Use `crossterm` v0.25 for `tui` backend.
use crossterm_025 as crossterm;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use std::borrow::Cow;
use std::env;
use std::fmt::Display;
use std::fs;
use std::io;
use std::io::{BufRead, Write};
use std::path::PathBuf;
use tui::Terminal;
use tui::backend::CrosstermBackend;
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::text::{Span, Spans};
use tui::widgets::{Block, Borders, Paragraph};
use tui_textarea::{CursorMove, Input, Key, TextArea};
macro_rules! error {
($fmt: expr $(, $args:tt)*) => {{
Err(io::Error::new(io::ErrorKind::Other, format!($fmt $(, $args)*)))
}};
}
struct SearchBox<'a> {
textarea: TextArea<'a>,
open: bool,
}
impl Default for SearchBox<'_> {
fn default() -> Self {
let mut textarea = TextArea::default();
textarea.set_block(Block::default().borders(Borders::ALL).title("Search"));
Self {
textarea,
open: false,
}
}
}
impl SearchBox<'_> {
fn open(&mut self) {
self.open = true;
}
fn close(&mut self) {
self.open = false;
// Remove input for next search. Do not recreate `self.textarea` instance to keep undo history so that users can
// restore previous input easily.
self.textarea.move_cursor(CursorMove::End);
self.textarea.delete_line_by_head();
}
fn height(&self) -> u16 {
if self.open { 3 } else { 0 }
}
fn input(&mut self, input: Input) -> Option<&'_ str> {
match input {
Input {
key: Key::Enter, ..
}
| Input {
key: Key::Char('m'),
ctrl: true,
..
} => None, // Disable shortcuts which inserts a newline. See `single_line` example
input => {
let modified = self.textarea.input(input);
modified.then(|| self.textarea.lines()[0].as_str())
}
}
}
fn set_error(&mut self, err: Option<impl Display>) {
let b = if let Some(err) = err {
Block::default()
.borders(Borders::ALL)
.title(format!("Search: {}", err))
.style(Style::default().fg(Color::Red))
} else {
Block::default().borders(Borders::ALL).title("Search")
};
self.textarea.set_block(b);
}
}
struct Buffer<'a> {
textarea: TextArea<'a>,
path: PathBuf,
modified: bool,
}
impl Buffer<'_> {
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 Editor<'_> {
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();
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, chunks[0]);
}
let buffer = &self.buffers[self.current];
let textarea = &buffer.textarea;
f.render_widget(textarea, chunks[1]);
// Render status line
let modified = if buffer.modified { " [modified]" } else { "" };
let slot = format!("[{}/{}]", self.current + 1, self.buffers.len());
let path = format!(" {}{} ", buffer.path.display(), modified);
let (row, col) = textarea.cursor();
let cursor = format!("({},{})", row + 1, col + 1);
let status_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length(slot.len() as u16),
Constraint::Min(1),
Constraint::Length(cursor.len() as u16),
]
.as_ref(),
)
.split(chunks[2]);
let status_style = Style::default().add_modifier(Modifier::REVERSED);
f.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]);
f.render_widget(Paragraph::new(path).style(status_style), status_chunks[1]);
f.render_widget(Paragraph::new(cursor).style(status_style), status_chunks[2]);
// Render message at bottom
let message = if let Some(message) = self.message.take() {
Spans::from(Span::raw(message))
} else if search_height > 0 {
Spans::from(vec![
Span::raw("Press "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to jump to first match and close, "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to close, "),
Span::styled(
"^G or ↓ or ^N",
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" to search next, "),
Span::styled(
"M-G or ↑ or ^P",
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" to search previous"),
])
} else {
Spans::from(vec![
Span::raw("Press "),
Span::styled("^Q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to quit, "),
Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to save, "),
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to search, "),
Span::styled("^T", 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('t'),
ctrl: true,
..
} => {
self.current = (self.current + 1) % self.buffers.len();
self.message =
Some(format!("Switched to buffer #{}", self.current + 1).into());
}
Input {
key: Key::Char('s'),
ctrl: true,
..
} => {
self.buffers[self.current].save()?;
self.message = Some("Saved!".into());
}
Input {
key: Key::Char('g'),
ctrl: true,
..
} => {
self.search.open();
}
input => {
let buffer = &mut self.buffers[self.current];
buffer.modified |= buffer.textarea.input(input);
}
}
}
}
Ok(())
}
}
impl Drop for Editor<'_> {
fn drop(&mut self) {
self.term.show_cursor().unwrap();
disable_raw_mode().unwrap();
crossterm::execute!(
self.term.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)
.unwrap();
}
}
fn main() -> io::Result<()> {
Editor::new(env::args_os().skip(1))?.run()
}

Просмотреть файл

@@ -1,52 +0,0 @@
// Use `crossterm` v0.25 for `tui` backend.
use crossterm_025 as crossterm;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use std::io;
use tui::Terminal;
use tui::backend::CrosstermBackend;
use tui::widgets::{Block, Borders};
use tui_textarea::{Input, Key, TextArea};
fn main() -> io::Result<()> {
let stdout = io::stdout();
let mut stdout = stdout.lock();
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
let mut textarea = TextArea::default();
textarea.set_block(
Block::default()
.borders(Borders::ALL)
.title("Crossterm Minimal Example"),
);
loop {
term.draw(|f| {
f.render_widget(&textarea, 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(())
}

Просмотреть файл

@@ -1,73 +0,0 @@
// Use `termion` v1.5 for `tui` backend.
use termion_15 as termion;
use std::error::Error;
use std::io;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use termion::event::Event as TermEvent;
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::Terminal;
use tui::backend::TermionBackend;
use tui::widgets::{Block, Borders};
use tui_textarea::{Input, Key, TextArea};
enum Event {
Term(TermEvent),
Tick,
}
fn main() -> Result<(), Box<dyn Error>> {
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut term = Terminal::new(backend)?;
let events = {
let events = io::stdin().events();
let (tx, rx) = mpsc::channel();
let keys_tx = tx.clone();
thread::spawn(move || {
for event in events.flatten() {
keys_tx.send(Event::Term(event)).unwrap();
}
});
thread::spawn(move || {
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
rx
};
let mut textarea = TextArea::default();
textarea.set_block(
Block::default()
.borders(Borders::ALL)
.title("Termion Minimal Example"),
);
loop {
match events.recv()? {
Event::Term(event) => match event.into() {
Input { key: Key::Esc, .. } => break,
input => {
textarea.input(input);
}
},
Event::Tick => {}
}
term.draw(|f| {
f.render_widget(&textarea, f.size());
})?;
}
drop(term); // Leave terminal raw mode to print the following line
println!("Lines: {:?}", textarea.lines());
Ok(())
}

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -16,6 +18,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -51,6 +54,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());

Просмотреть файл

@@ -1,3 +1,5 @@
mod common;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
@@ -400,6 +402,7 @@ fn main() -> io::Result<()> {
enable_raw_mode()?;
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let recording = common::maybe_force_recording_size(&mut stdout)?;
let backend = CrosstermBackend::new(stdout);
let mut term = Terminal::new(backend)?;
@@ -437,6 +440,7 @@ fn main() -> io::Result<()> {
LeaveAlternateScreen,
DisableMouseCapture
)?;
recording.restore(term.backend_mut())?;
term.show_cursor()?;
println!("Lines: {:?}", textarea.lines());