diff --git a/Cargo.toml b/Cargo.toml index 7629961..9ae9534 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,10 @@ unroll = "0.1.5" [dev-dependencies] [features] -default = ["perf_log"] +default = ["background"] +background = [] + +op_log = [] perf_log = [] [patch.crates-io] diff --git a/examples/escape_seq.rs b/examples/escape_seq.rs index e251261..561d081 100644 --- a/examples/escape_seq.rs +++ b/examples/escape_seq.rs @@ -11,7 +11,9 @@ use esp32c3_hal::{ timer::TimerGroup, Rtc, IO, }; -use esp_println::{print, println}; +use esp_backtrace as _; +use esp_println::println; +use nom::Parser; use riscv::asm::wfi; use vgaterm::ansi; @@ -33,22 +35,6 @@ fn init_heap() { } } -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - print!("Aborting: "); - if let Some(p) = info.location() { - println!( - "line {}, file {}: {}", - p.line(), - p.file(), - info.message().unwrap() - ); - } else { - println!("no information available."); - } - stop(); -} - #[no_mangle] extern "C" fn stop() -> ! { loop { @@ -96,8 +82,20 @@ fn main() -> ! { riscv::interrupt::enable(); } - let r = ansi::parse_esc_str("abcd\u{1B}[XYZ\u{1B}["); - println!("{:?}", r); + // let r = ansi::parse_esc_str("abcd\u{1B}[XYZ\u{1B}["); + // println!("{:?}\n", r); + // let r = ansi::parse_esc_str("\u{1B}["); + // println!("{:?}\n", r); + // let r = ansi::parse_esc_str("\u{1B}8"); + // println!("{:?}\n", r); + + println!("{:?}", ansi::parse("\u{1B}[;")); + println!("{:?}", ansi::parse("\u{1B}[1;;")); + println!("{:?}", ansi::parse("\u{1B}[m")); + println!("{:?}", ansi::parse("\u{1B}[1;2m")); + println!("{:?}", ansi::parse("\u{1b}[?m")); + + println!("{:?}", ansi::parse("\u{1B}[1;")); // match escape.push_str("abcd\u{1B}[5") { // ParseRes::InSequence(s) => { diff --git a/src/ansi.rs b/src/ansi.rs index 6b0d9e9..3c3d3f3 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -12,8 +12,8 @@ //! ESC [ F => Cursor to beginning of prev line, n lines up //! ESC [ G => Cursor to column n //! ESC [ S => Scroll up n lines -//! ESC [ T => SCroll down n lines -//! ESC [ 6 n => Request cursor postion, as `ESC [ ; R` at row r and column c +//! ESC [ T => Scroll down n lines +//! ESC [ 6 n => Request cursor position, as `ESC [ ; R` at row r and column c //! ESC 7 => Save cursor position //! ESC 8 => Restore cursor position //! ESC [ 3 > ~ => Delete @@ -59,18 +59,18 @@ use alloc::{ }; use core::{fmt::Debug, str::FromStr}; use esp_println::println; -use nom::{IResult, Parser}; +use nom::{combinator::fail, error::context, IResult, Parser}; const ESC: char = '\u{1B}'; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Op { MoveCursorDelta { dx: isize, dy: isize }, MoveCursorAbs { x: usize, y: usize }, MoveCursorAbsCol { x: usize }, MoveCursorBeginningAndLine { dy: isize }, Scroll { delta: isize }, - RequstCursorPos, + RequestCursorPos, SaveCursorPos, RestoreCursorPos, EraseScreen(EraseMode), @@ -82,7 +82,7 @@ pub enum Op { Vgaterm(Vgaterm), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum TextOp { SetBGBasic { bg: u8 }, SetFGBasic { fg: u8 }, @@ -92,7 +92,7 @@ pub enum TextOp { SetTextMode(SetUnset, Style), } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Style { Bold, Dim, @@ -103,20 +103,20 @@ pub enum Style { Inverse, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum SetUnset { Set, Unset, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum EraseMode { FromCursor, ToCursor, All, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Vgaterm { Redraw, } @@ -129,25 +129,15 @@ type TextOpResult<'a> = IResult<&'a str, TextOp>; trait StrParseFnMut<'a, O> = FnMut(&'a str) -> IResult<&'a str, O>; -fn start_with_char<'a, O, P: StrParser<'a, O>>( - start: char, - mut parser: P, -) -> impl StrParseFnMut<'a, O> { - move |input: &'a str| { - nom::sequence::preceded(nom::character::streaming::char(start), |x: &'a str| { - parser.parse(x) - }) - .parse(input) - } +fn start_with_char<'a, O, P: StrParser<'a, O>>(start: char, parser: P) -> impl StrParser<'a, O> { + nom::sequence::preceded(nom::character::streaming::char(start), parser) } /// Recognize ESC, and then parses via the P parser. If P fails, this parser will return /// the Failure variant (by using nom `cut`). If the this parser does not recognize ESC /// it will return with the nom Error variant. -fn start_with_esc<'a, O, P: StrParser<'a, O>>(mut parser: P) -> impl StrParseFnMut<'a, O> { - move |input: &'a str| { - start_with_char(ESC, nom::combinator::cut(|x: &'a str| parser.parse(x)))(input) - } +fn start_with_esc<'a, O, P: StrParser<'a, O>>(parser: P) -> impl StrParser<'a, O> { + start_with_char(ESC, nom::combinator::cut(parser)) } // This will parse "...P... " for some char ending and parsed sequence P @@ -206,13 +196,15 @@ where ::Err: Debug, { move |input: &str| { - let params = nom::sequence::separated_pair( - nom::character::streaming::digit1, - nom::character::streaming::char(';'), - nom::character::streaming::digit1, - ); - sequence_with_ending(params, ending)(input) - .map(|(rest, (a, b))| (rest, (N::from_str(a).unwrap(), N::from_str(b).unwrap()))) + nom::sequence::terminated( + nom::sequence::separated_pair( + nom::character::streaming::digit1, + nom::character::streaming::char(';'), + nom::character::streaming::digit1, + ), + nom::character::streaming::char(ending), + )(input) + .map(|(rest, (a, b))| (rest, (N::from_str(a).unwrap(), N::from_str(b).unwrap()))) } } @@ -301,9 +293,9 @@ fn scroll_down(input: &str) -> OpResult { // Request Cursor Position // ESC [ 6 n -fn request_cursor_postion(input: &str) -> OpResult { +fn request_cursor_position(input: &str) -> OpResult { sequence_with_ending(nom::character::streaming::char('6'), 'n')(input) - .map(|(rest, _)| (rest, Op::RequstCursorPos)) + .map(|(rest, _)| (rest, Op::RequestCursorPos)) } // ESC 7 => Save cursor position @@ -476,7 +468,391 @@ fn set_text_mode(input: &str) -> OpResult { .map(|(rest, found)| (rest, Op::TextOp(found))) } -fn parse(input: &str) -> OpResult { +pub fn parse_new(input: &str) -> OpResult { + fn gen_parse<'a, 'str>( + input: &'str str, + q: &'a mut [&'str str; 4], + ) -> IResult<&'str str, &'a [&'str str]> { + let (input, start) = + nom::combinator::recognize(nom::character::streaming::one_of("\u{1b}\u{9b}")) + .parse(input)?; + + match context( + "c0", + nom::combinator::cond( + start == "\x1b", + nom::sequence::tuple(( + nom::combinator::opt(nom::sequence::tuple(( + nom::combinator::recognize(nom::character::complete::char('\x21')), + nom::combinator::recognize(nom::character::complete::char('\x40')), + ))), + nom::combinator::recognize(nom::character::streaming::satisfy(|ch| { + '\x00' < ch && ch < '\x1f' + })), + )), + ), + ) + .parse(input) + { + // collapse the two intro sequences to one + Ok((rest, Some((Some(_), n)))) | Ok((rest, Some((None, n)))) => { + q[0] = start; + q[1] = n; + return Ok((rest, &q[..=1])); + } + Err(err @ nom::Err::Failure(_)) | Err(err @ nom::Err::Incomplete(_)) => { + return Err(err) + } + // We didn't match a c0 sequence, nothing to return yet + Err(nom::Err::Error(_)) | Ok((_, None)) => {} + }; + + // TODO: c1 set + match context( + // cf. https://github.com/fusesource/jansi/issues/226 + "vt100 (non-standard)", + nom::combinator::cond( + start == "\x1b", + nom::combinator::recognize(nom::character::streaming::satisfy(|ch| { + ch == '7' || ch == '8' + })), + ), + ) + .parse(input) + { + Ok((rest, Some(n))) => { + q[0] = start; + q[1] = n; + return Ok((rest, &q[..=1])); + } + Err(err @ nom::Err::Failure(_)) | Err(err @ nom::Err::Incomplete(_)) => { + return Err(err) + } + // We didn't match a non-standard VT100 sequence, nothing to return yet + Err(nom::Err::Error(_)) | Ok((_, None)) => {} + } + + match context( + // catch-all + "errybody else (non-standard)", + nom::combinator::cond( + start == "\x1b", + // TODO: (can't do this right now because it prevents us from recognizing CSIs and would need to come "later") + // nom::combinator::recognize(nom::character::streaming::anychar), + nom::combinator::recognize(nom::character::streaming::none_of("[")), + ), + ) + .parse(input) + { + Ok((rest, Some(n))) => { + q[0] = start; + q[1] = n; + return Ok((rest, &q[..=1])); + } + Err(err @ nom::Err::Failure(_)) | Err(err @ nom::Err::Incomplete(_)) => { + return Err(err) + } + // We didn't match a non-standard VT100 sequence, nothing to return yet + Err(nom::Err::Error(_)) | Ok((_, None)) => {} + } + + // control sequences + let (input, start) = if start == "\x1b" { + let (input, _) = nom::character::streaming::char('[').parse(input)?; + // map everything to this particular CSI + (input, "\u{9b}") + } else { + (input, start) + }; + + // CSI P ... P I ... I F + // + // where + // P ... P are Parameter Bytes, which, if present, consist of bit combinations from 03/00 (\x30) to 03/15 (\x3f) + // I ... I are Intermediate Bytes, which, if present, consist of bit combinations from 02/00 (\x20) to 02/15 (\x2f) + // F is the Final Byte; it consists of a bit combination from 04/00 (\x40) to 07/14 (\x7e) + // + // NB: the ECMA-43/48 standards use `nibble/nibble`, in decimal, to represent a 7- or 8-bit number. + // For example, `01/02` can be either 7- or 8-bit in their notation, and is equivalent to a more + // familiar hex notation `0x12`. Similarly, `15/15` (which is necessarily 8-bit) is equivalent to `0xff`. + // + // cf. https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf#page=24 + // and https://www.ecma-international.org/wp-content/uploads/ECMA-43_3rd_edition_december_1991.pdf#page=14 + + let params = nom::branch::alt(( + nom::bytes::complete::is_a("0123456789:;<=>?"), + nom::combinator::success(""), + )); + let intermediate = nom::branch::alt(( + nom::bytes::complete::is_a(concat!(" ", "!\"#$%&'()*+,/")), + nom::combinator::success(""), + )); + let mut fin = nom::combinator::recognize(nom::character::streaming::satisfy(|ch| { + ('\x40'..='\x7e').contains(&ch) + })); + + // TODO? + // let (rest, ((params, intermediate), fin)) = + // params.and(intermediate).and(fin).parse(input)?; + // bug-compat: + // currently, we bail out on sequences like "\u{1b}[;" with an error (even though it's reasonably considered incomplete, as we report without this check) + let (rest, (params, intermediate)) = params.and(intermediate).parse(input)?; + // but the trick is to avoid bailing on sequences like "\u{1b}[1;", which the old code considers "incomplete" + // this + if params.split(';').rev().skip(1).any(str::is_empty) { + return Err(nom::Err::Failure(nom::error::Error { + input: params, + code: nom::error::ErrorKind::Char, + })); + } + let (rest, fin) = fin.parse(rest)?; + + // TODO: collapse params & intr to "mid" w/ recognize(params.and(alt((inter, nonstandard)))) ? + q[0] = start; + q[1] = params; + q[2] = intermediate; + q[3] = fin; + + Ok((rest, &q[..])) + } + + trait Params<'a>: Sized { + fn parse(input: &'a str) -> IResult<&'a str, Self>; + } + + trait FromDigits: core::str::FromStr {} + + impl<'a, T> Params<'a> for T + where + T: FromDigits, + { + fn parse(input: &'a str) -> IResult<&'a str, Self> { + nom::combinator::map_res(nom::character::complete::digit1, Self::from_str).parse(input) + } + } + impl FromDigits for usize {} + impl FromDigits for isize {} + + trait AllConsuming: nom::Parser + Sized + where + I: nom::InputLength, + E: nom::error::ParseError, + { + fn parse_all(self, input: I) -> Result> { + nom::combinator::cut(nom::combinator::all_consuming(self)) + .parse(input) + .map(|(_, o)| o) + } + } + + impl AllConsuming for T + where + T: nom::Parser, + I: nom::InputLength, + E: nom::error::ParseError, + { + } + + fn bail(input: &str) -> IResult<&str, O> { + nom::combinator::cut(nom::combinator::fail).parse(input) + } + + /// kind of like [nom::multi::fill], but for up to N repeats rather than exactly N + // TODO: can this be a parser? we could use .parse_all then + // TODO? + // g) If the parameter string starts with the bit combination 03/11, an empty parameter sub-string is + // assumed preceding the separator; if the parameter string terminates with the bit combination 03/11, + // an empty parameter sub-string is assumed following the separator; if the parameter string contains + // successive bit combinations 03/11, empty parameter sub-strings are assumed between the separators. + // + // h) If the control function has more than one parameter, and some parameter sub-strings are empty, the + // separators (bit combination 03/11) must still be present. However, if the last parameter sub-string(s) + // is empty, the separator preceding it may be omitted, see B.2 in annex B. + // — https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf#page=26 + fn many_param<'s, 'p, P: Params<'s>, const N: usize>( + input: &'s str, + params: &'p mut [P; N], + ) -> Result<&'p [P], nom::Err>> { + // todo: check first byte for '\x30'..=\x3b ([0-9:;]); else we're in Special Params-land + let (_, i) = nom::combinator::all_consuming(nom::multi::fold_many_m_n( + 0, + N, + nom::sequence::terminated( + P::parse, + // TODO: this is wrong: it's not optional, unless it's in the last position + nom::combinator::opt(nom::character::complete::char(';')), + ), + || 0, + |i, p| { + params[i] = p; + i + 1 + }, + )) + .parse(input)?; + + Ok(¶ms[..i]) + } + + fn param<'a, P: Params<'a> + Clone>( + default: P, + ) -> impl nom::Parser<&'a str, P, nom::error::Error<&'a str>> { + nom::branch::alt(( + P::parse, + nom::combinator::eof.and_then(nom::combinator::success(default)), + )) + } + + const ESC: &str = "\u{1b}"; + const CSI: &str = "\u{9b}"; + + let mut buf = [""; 4]; + let (rest, seq) = gen_parse(input, &mut buf)?; + + println!("{:?}", seq); + + let back_compat_err = move |err| match err { + nom::Err::Error(_) => nom::Err::Error(nom::error::Error { + input: &input[2..], + code: nom::error::ErrorKind::Fail, + }), + nom::Err::Failure(_) => nom::Err::Failure(nom::error::Error { + input: &input[2..], + code: nom::error::ErrorKind::Fail, + }), + nom::Err::Incomplete(n) => nom::Err::Incomplete(n), + }; + + let op = match *seq { + // TODO: + // nom::bytes::streaming::tag("VxD")(input).map(|(rest, _)| (rest, Op::Vgaterm(Vgaterm::Redraw))) + + //TODO: + // [ESC, "7"] => Op::SaveCursorPos, + // [ESC, "8"] => Op::RestoreCursorPos, + // for now, bug-compat: + [ESC, ESC, "7"] => Op::SaveCursorPos, + [ESC, ESC, "8"] => Op::RestoreCursorPos, + + // bug-compat: + [CSI, _, _, "f"] => return bail(&input[2..]), + // TODO + // [CSI, params, intr, "H"] | [CSI, params, intr, "f"] => { + [CSI, params, intr, "H"] => { + if !intr.is_empty() { + return context("unrecognized intermediates", bail)(intr); + } + match *many_param(params, &mut [usize::default(); 2]).map_err(back_compat_err)? { + [] => Op::MoveCursorAbs { x: 0, y: 0 }, + [a, b] => Op::MoveCursorAbs { + x: b.saturating_sub(1), + y: a.saturating_sub(1), + }, + _ => return context("expected 0 or 2 params", bail).parse(params), + } + } + + [CSI, params, "", "A"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorDelta { dx: 0, dy: -n })?, + [CSI, params, "", "B"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorDelta { dx: 0, dy: n })?, + [CSI, params, "", "C"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorDelta { dx: n, dy: 0 })?, + [CSI, params, "", "D"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorDelta { dx: -n, dy: 0 })?, + [CSI, params, "", "E"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorBeginningAndLine { dy: n })?, + [CSI, params, "", "F"] => param::(1) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorBeginningAndLine { dy: -n })?, + [CSI, params, "", "G"] => param::(0 /* <-- TODO? */) + .parse_all(params) + .map_err(back_compat_err) + .map(|n| Op::MoveCursorAbsCol { + x: n.saturating_sub(1), + })?, + + [CSI, params, "", "J"] => match params { + "" | "0" => Op::EraseScreen(EraseMode::FromCursor), + "1" => Op::EraseScreen(EraseMode::ToCursor), + "2" => Op::EraseScreen(EraseMode::All), + _ => { + return context("invalid screen erase mode", bail) + .parse(params) + .map_err(back_compat_err); + } + }, + [CSI, params, "", "K"] => match params { + "" | "0" => Op::EraseLine(EraseMode::FromCursor), + "1" => Op::EraseLine(EraseMode::ToCursor), + "2" => Op::EraseLine(EraseMode::All), + _ => { + return context("invalid line erase mode", bail) + .parse(params) + .map_err(back_compat_err); + } + }, + + [CSI, "3", "", "~"] => Op::InPlaceDelete, + + [CSI, "6", "", "n"] => Op::RequestCursorPos, + [CSI, params, "", "m"] => { + nom::multi::separated_list0( + nom::character::complete::char(';'), + nom::combinator::complete(any_text_mode), + ) + // back-compat (for using `any_text_mode`) + // we need some sort of terminator for the streaming digit parsers to recognize the last item + // so, let's keep it classic and pick '\0' + .and(nom::character::complete::char('\0')) + .map(|(r, _)| r) + .parse_all(alloc::format!("{}\0", params).as_str()) + // TODO: + // .parse_all(params) + .map(Op::TextOp) + .map_err(back_compat_err)? + } + + [CSI, params, "", "h"] => nom::sequence::preceded( + nom::character::complete::char('?'), + nom::character::complete::digit0, + ) + .map(|s: &str| Op::DecPrivateSet(s.to_owned())) + .parse_all(params) + .map_err(back_compat_err)?, + [CSI, params, "", "l"] => nom::sequence::preceded( + nom::character::complete::char('?'), + nom::character::complete::digit0, + ) + .map(|s: &str| Op::DecPrivateReset(s.to_owned())) + .parse_all(params) + .map_err(back_compat_err)?, + + // TODO: + // [ESC, ..] | [CSI, ..] => return bail(input), + // _ => return fail(input), // `fail` is (confusingly) not Failure, but Error + // for now (back-compat): + [ESC, ..] => return bail(&input[1..]), + [CSI, ..] => return bail(&input[2..]), + _ => { + return nom::sequence::preceded(nom::bytes::complete::tag("\u{1b}"), fail).parse(input) + } + }; + + Ok((rest, op)) +} + +fn parse_classic(input: &str) -> OpResult { start_with_esc(nom::branch::alt(( save_cursor_position, restore_cursor_position, @@ -498,13 +874,60 @@ fn parse(input: &str) -> OpResult { delete, erase_screen, erase_line, - request_cursor_postion, + request_cursor_position, set_text_mode, set_private_sequence, reset_private_sequence, )), ), - )))(input) + ))) + .parse(input) +} + +pub fn parse(input: &str) -> OpResult { + match (parse_classic(input), parse_new(input)) { + (Ok(classic), Ok(new)) if classic == new => Ok(classic), + ( + r @ Err(nom::Err::Error(..)), + Err(nom::Err::Error(nom::error::Error { + input: ref new, + code: _, + })), + ) if { + let Err(nom::Err::Error(nom::error::Error { + input: ref classic, + code: _, + })) = r else { unreachable!() }; + classic + } == new => + { + r + } + ( + r @ Err(nom::Err::Failure(..)), + Err(nom::Err::Failure(nom::error::Error { + input: ref new, + code: _, + })), + ) if { + let Err(nom::Err::Failure(nom::error::Error { + input: ref classic, + code: _, + })) = r else { unreachable!() }; + classic + } == new => + { + r + } + (r @ Err(nom::Err::Incomplete(..)), Err(nom::Err::Incomplete(_))) => r, + (r, new) => { + println!( + "whuh oh! wanted: {:?}\n got: {:?}\nfor input: {:?}", + r, new, input + ); + r + } + } } pub enum OpChar { diff --git a/src/bin/vgaterm.rs b/src/bin/vgaterm.rs index 6ed51ea..c539647 100644 --- a/src/bin/vgaterm.rs +++ b/src/bin/vgaterm.rs @@ -10,7 +10,7 @@ use esp32c3_hal::prelude::*; use esp32c3_hal::timer::TimerGroup; use esp32c3_hal::{gpio::IO, peripherals::Peripherals, Rtc}; use esp_backtrace as _; -use vgaterm::{self, perf, video}; +use vgaterm::{self, perf}; use vgaterm::{interrupt::Priority, usb_keyboard::US_ENGLISH, Work}; use core::fmt::Write; @@ -132,8 +132,11 @@ fn main() -> ! { 40_000_000, ); - let image = include_bytes!("../../image.bin"); - video::load_from_slice(image); + #[cfg(feature = "background")] + { + let image = include_bytes!("../../image.bin"); + vgaterm::video::load_from_slice(image); + } let mut display = vgaterm::display::Display::new(); diff --git a/src/display.rs b/src/display.rs index a5c8984..0481551 100644 --- a/src/display.rs +++ b/src/display.rs @@ -112,7 +112,6 @@ impl DrawTarget for Display { // print!("{: ^5}", c); unsafe { video::BUFFER[i] = c }; } - // println!(); offset += screen_width; } // }); diff --git a/src/terminal.rs b/src/terminal.rs index 827f025..8991c85 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -273,7 +273,36 @@ impl TextField { fn handle_op(&mut self, op: Op) { use Op::*; - // println!("{:?}", op); + #[cfg(feature = "op_log")] + { + use core::cmp::min; + use esp_println::println; + match &op { + TextOp(v) => println!( + "TextOp({:?}{}) [{}]", + &v[..min(v.len(), 5)], + if v.len() > 5 { "..." } else { "" }, + v.len() + ), + DecPrivateSet(v) => { + println!( + "DecPrivateSet({:?}{}) [{}]", + &v[..min(v.len(), 5)], + if v.len() > 5 { "..." } else { "" }, + v.len() + ) + } + DecPrivateReset(v) => { + println!( + "DecPrivateReset({:?}{}) [{}]", + &v[..min(v.len(), 5)], + if v.len() > 5 { "..." } else { "" }, + v.len() + ) + } + _ => println!("{:?}", op), + } + } match op { MoveCursorAbs { x, y } => { self.move_cursor( @@ -296,7 +325,7 @@ impl TextField { MoveCursorBeginningAndLine { dy } => { self.move_cursor(dy, -(self.cursor.pos.col() as isize)); } - RequstCursorPos => {} + RequestCursorPos => {} SaveCursorPos => {} RestoreCursorPos => {} EraseScreen(erase) => {