diff options
41 files changed, 4358 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..d2d567b --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "378ac7461ecac29fde9e10df7b08359d1315a9ad" + } +} diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..8049644 --- /dev/null +++ b/Android.bp @@ -0,0 +1,74 @@ +// This file is generated by cargo2android.py --config cargo2android.json. +// Do not modify this file as changes will be overridden on upgrade. + + + +rust_test { + name: "anes_test_src_lib", + host_supported: true, + crate_name: "anes", + cargo_env_compat: true, + cargo_pkg_version: "0.1.6", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + features: [ + "bitflags", + "parser", + ], + rustlibs: [ + "libbitflags", + "libcriterion", + "liblibc", + ], +} + +rust_test { + name: "anes_test_tests_tests", + host_supported: true, + crate_name: "tests", + cargo_env_compat: true, + cargo_pkg_version: "0.1.6", + srcs: ["tests/tests.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + features: [ + "bitflags", + "parser", + ], + rustlibs: [ + "libanes", + "libbitflags", + "libcriterion", + "liblibc", + ], +} + +rust_library { + name: "libanes", + host_supported: true, + crate_name: "anes", + cargo_env_compat: true, + cargo_pkg_version: "0.1.6", + srcs: ["src/lib.rs"], + edition: "2018", + features: [ + "bitflags", + "parser", + ], + rustlibs: [ + "libbitflags", + ], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b359a6d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,511 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anes" +version = "0.1.6" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bstr" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byteorder" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cast" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "criterion" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "criterion-plot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tinytemplate 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "criterion-plot" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "getrandom" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hermit-abi" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memoffset" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num_cpus" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_os" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xoshiro" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rayon" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon-core 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rayon-core" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-automata" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "same-file" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scopeguard" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tinytemplate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "walkdir" +version = "2.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" +"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245" +"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +"checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "938703e165481c8d612ea3479ac8342e5615185db37765162e762ec3523e2fc6" +"checksum criterion-plot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eccdc6ce8bbe352ca89025bee672aa6d24f4eb8c53e3a8b5d1bc58011da072a2" +"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca" +"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac" +"checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700" +"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" +"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d" +"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c" +"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120" +"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" +"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4" +"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72" +"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a" +"checksum rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e18c91676f670f6f0312764c759405f13afb98d5d73819840cf72a518487bff" +"checksum rayon 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "43739f8831493b276363637423d3622d4bd6394ab6f0a9c4a552e208aeb7fddd" +"checksum rayon-core 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8bf17de6f23b05473c437eb958b9c850bfc8af0961fe17b4cc92d5a627b4791" +"checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" +"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "1217f97ab8e8904b57dd22eb61cde455fa7446a9c1cf43966066da047c1f3702" +"checksum serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "a8c6faef9a2e64b0064f48570289b4bf8823b7581f1d6157c1b52152306651d0" +"checksum serde_json 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "1a3351dcbc1f067e2c92ab7c3c1f288ad1a4cffc470b5aaddb4c2e0a3ae80043" +"checksum syn 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f89693ae015201f8de93fd96bde2d065f8bfc3f97ce006d5bc9f900b97c0c7c0" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum tinytemplate 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4574b75faccaacddb9b284faecdf0b544b80b6b294f3d062d325c5726a209c20" +"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" +"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9ae35a0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,48 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +edition = "2018" +name = "anes" +version = "0.1.6" +authors = ["Robert Vojta <rvojta@me.com>"] +exclude = ["target", "Cargo.lock"] +description = "ANSI Escape Sequences provider & parser" +documentation = "https://docs.rs/anes/" +readme = "README.md" +keywords = ["terminal", "ansi", "sequence", "code", "parser"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/zrzka/anes-rs" +[package.metadata.docs.rs] +all-features = true + +[lib] +bench = false + +[[bench]] +name = "bench_main" +harness = false +required-features = ["parser"] +[dependencies.bitflags] +version = "1.2" +optional = true +[dev-dependencies.criterion] +version = "0.3" + +[dev-dependencies.libc] +version = "0.2.66" + +[features] +default = [] +parser = ["bitflags"] +[badges.maintenance] +status = "actively-developed" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..51fd1d3 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,37 @@ +[package] +name = "anes" +version = "0.1.6" +authors = ["Robert Vojta <rvojta@me.com>"] +edition = "2018" +description = "ANSI Escape Sequences provider & parser" +repository = "https://github.com/zrzka/anes-rs" +documentation = "https://docs.rs/anes/" +license = "MIT OR Apache-2.0" +keywords = ["terminal", "ansi", "sequence", "code", "parser"] +exclude = ["target", "Cargo.lock"] +readme = "README.md" + +[lib] +bench = false + +[badges] +maintenance = { status = "actively-developed" } + +[package.metadata.docs.rs] +all-features = true + +[features] +default = [] +parser = ["bitflags"] + +[dependencies] +bitflags = { version = "1.2", optional = true } + +[dev-dependencies] +criterion = "0.3" +libc = "0.2.66" + +[[bench]] +name = "bench_main" +harness = false +required-features = ["parser"] @@ -0,0 +1 @@ +LICENSE-APACHE
\ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..fc87cce --- /dev/null +++ b/METADATA @@ -0,0 +1,20 @@ +name: "anes" +description: "()" +third_party { + url { + type: HOMEPAGE + value: "https://crates.io/crates/anes" + } + url { + type: ARCHIVE + value: "https://static.crates.io/crates/anes/anes-0.1.6.crate" + } + version: "0.1.6" + # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same. + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 2 + day: 16 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1 @@ +include platform/prebuilts/rust:master:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a3c236 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +[![Stable Status][actions-stable-badge]][actions-link] +[![Beta Status][actions-beta-badge]][actions-link] +[![Nightly Status][actions-nightly-badge]][actions-link] +[![crates.io][crates-badge]][crates-link] +[![docs.rs][docs-badge]][docs-link] +[![MIT][mit-license-badge]][mit-license-link] +[![Apache 2.0][apache-license-badge]][apache-license-link] +![LOC][loc-badge] + +# ANSI Escape Sequences provider & parser + +A Rust library which provides an ANSI escape sequences (or codes, whatever you like more) +and a parser allowing you to parse data from the STDIN (or `/dev/tty`) in the raw mode. + +## Sequences provider + +### Terminal support + +Not all ANSI escape sequences are supported by all terminals. You can use the +[interactive-test](https://github.com/zrzka/anes-rs/tree/master/interactive-test) to test them. +Checkout the repository and then: + +```bash +$ cd anes-rs +$ cargo run --bin interactive-test +``` + +### Examples + +<details> +<summary> +Click to show Cargo.toml. +</summary> + +```toml +[dependencies] +anes = "0.1" +``` + +</details> +<p></p> + + +An example how to retrieve the ANSI escape sequence as a `String`: + +```rust +use anes::SaveCursorPosition; + +fn main() { + let string = format!("{}", SaveCursorPosition); + assert_eq!(&string, "\x1B7"); +} +``` + +An example how to use the ANSI escape sequence: + +```rust +use std::io::{Result, Write}; + +use anes::execute; + +fn main() -> Result<()> { + let mut stdout = std::io::stdout(); + execute!( + &mut stdout, + anes::SaveCursorPosition, + anes::MoveCursorTo(10, 10), + anes::RestoreCursorPosition + )?; + Ok(()) +} +``` + +## Sequences parser + +You have to enable `parser` feature in order to use the parser. It's disabled by default. + +### Examples + +<details> +<summary> +Click to show Cargo.toml. +</summary> + +```toml +[dependencies] +anes = { version = "0.1", features = ["parser"] } +``` + +</details> +<p></p> + +An example how to parse cursor position: + +```rust +use anes::parser::{Parser, Sequence}; + +let mut parser = Parser::default(); +parser.advance(b"\x1B[20;10R", false); + +assert_eq!(Some(Sequence::CursorPosition(10, 20)), parser.next()); +assert!(parser.next().is_none()); +``` + +## License + +The ANES crate is dual-licensed under [Apache 2.0][apache-license-link] and +[MIT][mit-license-link] terms. + +Copyrights in the ANES project are retained by their contributors. No +copyright assignment is required to contribute to the ANES project. + +[actions-stable-badge]: https://github.com/zrzka/anes-rs/workflows/stable/badge.svg +[actions-beta-badge]: https://github.com/zrzka/anes-rs/workflows/beta/badge.svg +[actions-nightly-badge]: https://github.com/zrzka/anes-rs/workflows/nightly/badge.svg +[actions-link]: https://github.com/zrzka/anes-rs/actions + +[crates-badge]: https://img.shields.io/crates/v/anes.svg +[crates-link]: https://crates.io/crates/anes + +[docs-badge]: https://docs.rs/anes/badge.svg +[docs-link]: https://docs.rs/anes + +[mit-license-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-license-link]: ./LICENSE-MIT +[apache-license-badge]: https://img.shields.io/badge/license-Apache2-blue.svg +[apache-license-link]: /LICENSE-APACHE + +[loc-badge]: https://tokei.rs/b1/github/zrzka/anes-rs?category=code diff --git a/benches/bench_main.rs b/benches/bench_main.rs new file mode 100644 index 0000000..79aea4a --- /dev/null +++ b/benches/bench_main.rs @@ -0,0 +1,5 @@ +use criterion::criterion_main; + +mod benchmarks; + +criterion_main!(benchmarks::parser::benches); diff --git a/benches/benchmarks/mod.rs b/benches/benchmarks/mod.rs new file mode 100644 index 0000000..67c567f --- /dev/null +++ b/benches/benchmarks/mod.rs @@ -0,0 +1 @@ +pub mod parser; diff --git a/benches/benchmarks/parser.rs b/benches/benchmarks/parser.rs new file mode 100644 index 0000000..2aa63aa --- /dev/null +++ b/benches/benchmarks/parser.rs @@ -0,0 +1,20 @@ +use criterion::{black_box, criterion_group, Criterion}; + +use anes::parser::Parser; + +pub fn parser(c: &mut Criterion) { + const XTERM_MOUSE: &str = "\x1B[<28;20;10;m"; + + let mut parser = Parser::default(); + + c.bench_function("advance and consume", |b| { + let input = XTERM_MOUSE.as_bytes(); + + b.iter(|| { + parser.advance(black_box(input), black_box(true)); + while let Some(_) = parser.next() {} + }) + }); +} + +criterion_group!(benches, parser); diff --git a/cargo2android.json b/cargo2android.json new file mode 100644 index 0000000..fb02735 --- /dev/null +++ b/cargo2android.json @@ -0,0 +1,6 @@ +{ + "device": true, + "run": true, + "features": "parser", + "tests": true +} diff --git a/examples/execute.rs b/examples/execute.rs new file mode 100644 index 0000000..df781fc --- /dev/null +++ b/examples/execute.rs @@ -0,0 +1,15 @@ +/// An example how to execute the ANSI escape sequence. +use std::io::{Result, Write}; + +use anes::execute; + +fn main() -> Result<()> { + let mut stdout = std::io::stdout(); + execute!( + &mut stdout, + anes::SaveCursorPosition, + anes::MoveCursorTo(10, 10), + anes::RestoreCursorPosition + )?; + Ok(()) +} diff --git a/examples/parser.rs b/examples/parser.rs new file mode 100644 index 0000000..a5ec5b6 --- /dev/null +++ b/examples/parser.rs @@ -0,0 +1,106 @@ +/// An example how to use the ANSI escape sequence parser. +use std::io::{Read, Result, Write}; + +use anes::{ + self, execute, + parser::{KeyCode, Parser, Sequence}, + queue, +}; +use libc::termios as Termios; + +const HELP: &str = r#"ANES parser example + +* Hit `Esc` to quit +* Hit 'c' to ask for cursor position +* Use your mouse or type anything +"#; + +fn main() -> Result<()> { + let mut w = std::io::stdout(); + queue!( + w, + anes::SwitchBufferToAlternate, + anes::HideCursor, + anes::EnableMouseEvents + )?; + for line in HELP.split('\n') { + queue!(w, line, anes::MoveCursorToNextLine(1))?; + } + w.flush()?; + + let saved_attributes = get_termios()?; + let mut attributes = saved_attributes; + make_raw(&mut attributes); + set_termios(attributes)?; + + let mut stdin = std::io::stdin(); + let mut stdin_buffer = [0u8; 1024]; + let mut parser = Parser::default(); + + loop { + if let Ok(size) = stdin.read(&mut stdin_buffer) { + parser.advance(&stdin_buffer[..size], false); + + let mut break_outer_loop = false; + + while let Some(sequence) = parser.next() { + match sequence { + Sequence::Key(KeyCode::Esc, _) => { + break_outer_loop = true; + break; + } + Sequence::Key(KeyCode::Char('c'), _) => { + execute!(w, anes::ReportCursorPosition)? + } + _ => execute!( + w, + anes::ClearLine::Left, + anes::MoveCursorToColumn(1), + format!("{:?}", sequence), + )?, + } + } + + if break_outer_loop { + break; + } + } + } + + set_termios(saved_attributes)?; + + execute!( + w, + anes::DisableMouseEvents, + anes::ShowCursor, + anes::SwitchBufferToNormal + )?; + Ok(()) +} + +// +// RAW mode +// + +fn get_termios() -> Result<Termios> { + unsafe { + let mut termios = std::mem::zeroed(); + if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) != -1 { + Ok(termios) + } else { + Err(std::io::Error::last_os_error()) + } + } +} + +fn set_termios(termios: Termios) -> Result<()> { + if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) } != -1 { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } +} + +fn make_raw(termios: &mut Termios) { + unsafe { libc::cfmakeraw(termios) } +} diff --git a/examples/queue.rs b/examples/queue.rs new file mode 100644 index 0000000..0b2c712 --- /dev/null +++ b/examples/queue.rs @@ -0,0 +1,18 @@ +/// An example how to queue & flush the ANSI escape sequence. +use std::io::{Result, Write}; + +use anes::queue; + +fn main() -> Result<()> { + let mut stdout = std::io::stdout(); + queue!( + &mut stdout, + anes::SaveCursorPosition, + anes::MoveCursorTo(10, 10) + )?; + + queue!(&mut stdout, anes::RestoreCursorPosition,)?; + + // ANSI sequences are not executed until you flush it! + stdout.flush() +} diff --git a/examples/sequence.rs b/examples/sequence.rs new file mode 100644 index 0000000..d038f58 --- /dev/null +++ b/examples/sequence.rs @@ -0,0 +1,42 @@ +/// An example how to create custom ANSI sequences. +use anes::{csi, esc, sequence}; + +fn static_unit_struct() { + sequence!( + /// Documentation string is also supported. + struct Foo => csi!("foo") + ); + + assert_eq!(&format!("{}", Foo), "\x1B[foo"); +} + +fn dynamic_struct() { + sequence!( + /// Documentation string is also supported. + struct Foo(u16, u16) => + |this, f| write!(f, esc!("{};{}"), this.0, this.1) + ); + + assert_eq!(&format!("{}", Foo(5, 10)), "\x1B5;10"); +} + +fn static_enum() { + sequence!( + /// Documentation string is also supported. + enum Foo { + /// Documentation string is also supported. + Bar => esc!("bar"), + /// Documentation string is also supported. + Baz => csi!("baz"), + } + ); + + assert_eq!(&format!("{}", Foo::Bar), "\x1Bbar"); + assert_eq!(&format!("{}", Foo::Baz), "\x1B[baz"); +} + +fn main() { + static_unit_struct(); + dynamic_struct(); + static_enum(); +} diff --git a/examples/stdout.rs b/examples/stdout.rs new file mode 100644 index 0000000..9b5be72 --- /dev/null +++ b/examples/stdout.rs @@ -0,0 +1,12 @@ +/// An example how to use the ANSI escape sequence. +use std::io::{Result, Write}; + +use anes; + +fn main() -> Result<()> { + let mut stdout = std::io::stdout(); + write!(stdout, "{}", anes::SaveCursorPosition)?; + write!(stdout, "{}", anes::RestoreCursorPosition)?; + stdout.flush()?; + Ok(()) +} diff --git a/examples/string.rs b/examples/string.rs new file mode 100644 index 0000000..bd81c36 --- /dev/null +++ b/examples/string.rs @@ -0,0 +1,7 @@ +//! An example how to retrieve the ANSI escape sequence as a `String`. +use anes::SaveCursorPosition; + +fn main() { + let string = format!("{}", SaveCursorPosition); + assert_eq!(&string, "\x1B7"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..99dfc8c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,76 @@ +//! # ANSI Escape Sequences provider & parser +//! +//! ## Sequences provider +//! +//! The `anes` crate provides ANSI escape sequences you can use to control the terminal +//! cursor (show, hide, ...), colors (foreground, background), display attributes (bold, ...) +//! and many others. +//! +//! Every sequence implements the standard library [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html) +//! trait. It means that these sequences can be used in macros like +//! [`format!`](https://doc.rust-lang.org/std/macro.format.html) or +//! [`write!`](https://doc.rust-lang.org/std/macro.write.html). +//! +//! Ask if you need more sequences or use the [`sequence!`](macro.sequence.html) macro to create +//! your own sequences. +//! +//! +//! ### Terminal Support +//! +//! Not all ANSI escape sequences are supported by all terminals. You can use the +//! [interactive-test](https://github.com/zrzka/anes-rs/tree/master/interactive-test) to test them. +//! +//! ### Examples +//! +//! Retrieve the sequence as a `String`: +//! +//! ```rust +//! use anes::SaveCursorPosition; +//! +//! let string = format!("{}", SaveCursorPosition); +//! assert_eq!(&string, "\x1B7"); +//! ``` +//! +//! Execute the sequence on the standard output: +//! +//! ```rust +//! use std::io::{Result, Write}; +//! +//! use anes::execute; +//! +//! fn main() -> Result<()> { +//! let mut stdout = std::io::stdout(); +//! execute!(&mut stdout, anes::ResetAttributes) +//! } +//! ``` +//! +//! ## Sequences parser +//! +//! Parser isn't available with default features. You have to enable `parser` feature if you'd like to use it. +//! You can learn more about this feature in the [`parser`](parser/index.html) module documentation. +#![warn(rust_2018_idioms)] +#![deny(unused_imports, unused_must_use)] + +// Keep it first to load all the macros before other modules. +#[macro_use] +mod macros; + +pub use self::sequences::{ + attribute::{Attribute, ResetAttributes, SetAttribute}, + buffer::{ + ClearBuffer, ClearLine, ScrollBufferDown, ScrollBufferUp, SwitchBufferToAlternate, + SwitchBufferToNormal, + }, + color::{Color, SetBackgroundColor, SetForegroundColor}, + cursor::{ + DisableCursorBlinking, EnableCursorBlinking, HideCursor, MoveCursorDown, MoveCursorLeft, + MoveCursorRight, MoveCursorTo, MoveCursorToColumn, MoveCursorToNextLine, + MoveCursorToPreviousLine, MoveCursorUp, ReportCursorPosition, RestoreCursorPosition, + SaveCursorPosition, ShowCursor, + }, + terminal::{DisableMouseEvents, EnableMouseEvents, ResizeTextArea}, +}; + +#[cfg(feature = "parser")] +pub mod parser; +mod sequences; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..1955214 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,437 @@ +/// Creates a control sequence. +/// +/// This macro prepends provided sequence with the control sequence introducer `ESC [` (`\x1B[`). +/// +/// # Examples +/// +/// ``` +/// use anes::csi; +/// +/// assert_eq!(csi!("?1049h"), "\x1B[?1049h"); +/// ``` +#[macro_export] +macro_rules! csi { + ($($arg:expr),*) => { concat!("\x1B[", $($arg),*) }; +} + +/// Creates an escape sequence. +/// +/// This macro prepends provided sequence with the `ESC` (`\x1B`) character. +/// +/// # Examples +/// +/// ``` +/// use anes::esc; +/// +/// assert_eq!(esc!("7"), "\x1B7"); +/// ``` +#[macro_export] +macro_rules! esc { + ($($arg:expr),*) => { concat!("\x1B", $($arg),*) }; +} + +/// Creates a select graphic rendition sequence. +/// +/// This macro prepends provided sequence with the `ESC[` (`\x1B[`) character and appends `m` character. +/// +/// Also known as Set Graphics Rendition on Linux. +/// +/// # Examples +/// +/// ``` +/// use anes::sgr; +/// +/// assert_eq!(sgr!("0"), "\x1B[0m"); +/// ``` +#[macro_export] +macro_rules! sgr { + ($($arg:expr),*) => { concat!("\x1B[", $($arg),* , "m") }; +} + +/// Creates an ANSI sequence. +/// +/// You can use this macro to create your own ANSI sequence. All `anes` sequences are +/// created with this macro. +/// +/// # Examples +/// +/// An unit struct: +/// +/// ``` +/// use anes::{esc, sequence}; +/// +/// sequence!( +/// /// Saves the cursor position. +/// struct SaveCursorPosition => esc!("7") +/// ); +/// +/// assert_eq!(&format!("{}", SaveCursorPosition), "\x1B7"); +/// ``` +/// +/// An enum: +/// +/// ``` +/// use anes::{csi, sequence}; +/// +/// sequence!( +/// /// Clears part of the buffer. +/// enum ClearBuffer { +/// /// Clears from the cursor position to end of the screen. +/// Below => csi!("J"), +/// /// Clears from the cursor position to beginning of the screen. +/// Above => csi!("1J"), +/// /// Clears the entire buffer. +/// All => csi!("2J"), +/// /// Clears the entire buffer and all saved lines in the scrollback buffer. +/// SavedLines => csi!("3J"), +/// } +/// ); +/// +/// assert_eq!(&format!("{}", ClearBuffer::Below), "\x1B[J"); +/// assert_eq!(&format!("{}", ClearBuffer::Above), "\x1B[1J"); +/// assert_eq!(&format!("{}", ClearBuffer::All), "\x1B[2J"); +/// assert_eq!(&format!("{}", ClearBuffer::SavedLines), "\x1B[3J"); +/// ``` +/// +/// A struct: +/// +/// ``` +/// use anes::{csi, sequence}; +/// +/// sequence!( +/// /// Moves the cursor to the given location (column, row). +/// /// +/// /// # Notes +/// /// +/// /// Top/left cell is represented as `1, 1`. +/// struct MoveCursorTo(u16, u16) => +/// |this, f| write!(f, csi!("{};{}H"), this.0, this.1) +/// ); +/// +/// assert_eq!(&format!("{}", MoveCursorTo(10, 5)), "\x1B[10;5H"); +/// ``` +#[macro_export] +macro_rules! sequence { + // Static unit struct + ( + $(#[$meta:meta])* + struct $name:ident => $value:expr + ) => { + $(#[$meta])* + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] + pub struct $name; + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, $value) + } + } + }; + // Static enum + ( + $(#[$meta:meta])* + enum $name:ident { + $( + $(#[$variant_meta:meta])* + $variant:ident => $variant_value:expr + ),* + $(,)? + } + ) => { + $(#[$meta])* + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] + pub enum $name { + $( + $(#[$variant_meta])* + $variant, + )* + } + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", match self { + $( + $name::$variant => $variant_value, + )* + }) + } + } + }; + // Dynamic struct + ( + $(#[$meta:meta])* + struct $type:ident( + $($fields:ty),* + $(,)? + ) + => + $write:expr + ) => { + $(#[$meta])* + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] + pub struct $type($(pub $fields),*); + + impl ::std::fmt::Display for $type { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let write: &dyn Fn(&Self, &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result = + &$write; + write(self, f) + } + } + }; +} + +/// Queues ANSI escape sequence(s). +/// +/// What does queue mean exactly? All sequences are queued with the +/// `write!($dst, "{}", $sequence)` macro without calling the +/// [`flush`](https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush) method. +/// +/// Check the [`execute!`](macro.execute.html) macro if you'd like execute them +/// immediately (call the `flush` method after all sequences were queued). +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{Result, Write}; +/// +/// use anes::queue; +/// +/// fn main() -> Result<()> { +/// let mut stdout = std::io::stdout(); +/// queue!( +/// &mut stdout, +/// anes::SaveCursorPosition, +/// anes::MoveCursorTo(10, 10) +/// )?; +/// +/// queue!(&mut stdout, anes::RestoreCursorPosition,)?; +/// +/// // ANSI sequences are not executed until you flush it! +/// stdout.flush() +/// } +/// ``` +#[macro_export] +macro_rules! queue { + ($dst:expr, $($sequence:expr),* $(,)?) => {{ + let mut error = None; + + $( + if let Err(e) = write!($dst, "{}", $sequence) { + error = Some(e); + } + )* + + if let Some(error) = error { + Err(error) + } else { + Ok(()) + } + }} +} + +/// Executes ANSI escape sequence(s). +/// +/// What does execute mean exactly? All sequences are queued with the +/// `write!($dst, "{}", $sequence)` macro and then the +/// [`flush`](https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush) method +/// is called. +/// +/// Check the [`queue!`](macro.queue.html) macro if you'd like queue sequences +/// and execute them later. +/// +/// ```no_run +/// use std::io::{Result, Write}; +/// +/// use anes::execute; +/// +/// fn main() -> Result<()> { +/// let mut stdout = std::io::stdout(); +/// execute!( +/// &mut stdout, +/// anes::SaveCursorPosition, +/// anes::MoveCursorTo(10, 10), +/// anes::RestoreCursorPosition +/// )?; +/// Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! execute { + ($dst:expr, $($sequence:expr),* $(,)?) => {{ + if let Err(e) = $crate::queue!($dst, $($sequence),*) { + Err(e) + } else { + $dst.flush() + } + }} +} + +#[cfg(test)] +macro_rules! test_sequences { + ( + $( + $name:ident( + $($left:expr => $right:expr),* + $(,)? + ) + ),* + $(,)? + ) => { + #[cfg(test)] + mod tests { + use super::*; + + $( + #[test] + fn $name() { + $( + assert_eq!(&format!("{}", $left), $right); + )* + } + )* + } + } +} + +#[cfg(test)] +mod tests { + use std::io::{Error, ErrorKind, Write}; + + #[test] + fn csi() { + assert_eq!(csi!("foo"), "\x1B[foo"); + } + + #[test] + fn esc() { + assert_eq!(esc!("bar"), "\x1Bbar"); + } + + #[test] + fn sgr() { + assert_eq!(sgr!("bar"), "\x1B[barm"); + } + + #[test] + fn static_struct_sequence() { + sequence!( + struct TestSeq => csi!("foo") + ); + + assert_eq!(&format!("{}", TestSeq), "\x1B[foo"); + } + + #[test] + fn static_enum_sequence() { + sequence!( + enum TestSeq { + Foo => csi!("foo"), + Bar => esc!("bar"), + } + ); + + assert_eq!(&format!("{}", TestSeq::Foo), "\x1B[foo"); + assert_eq!(&format!("{}", TestSeq::Bar), "\x1Bbar"); + } + + #[test] + fn dynamic_struct_sequence() { + sequence!( + struct TestSeq(u16) => + |this, f| write!(f, csi!("foo{}bar"), this.0) + ); + + assert_eq!(&format!("{}", TestSeq(10)), "\x1B[foo10bar"); + } + + #[test] + fn queue_allows_trailing_comma() { + let mut writer = Writer::default(); + + assert!(queue!(&mut writer, "foo",).is_ok()); + assert_eq!(&writer.buffer, "foo"); + } + + #[test] + fn queue_writes_single_sequence() { + let mut writer = Writer::default(); + + assert!(queue!(&mut writer, "foo").is_ok()); + assert_eq!(&writer.buffer, "foo"); + } + + #[test] + fn queue_writes_multiple_sequences() { + let mut writer = Writer::default(); + + assert!(queue!(&mut writer, "foo", "bar", "baz").is_ok()); + assert_eq!(&writer.buffer, "foobarbaz"); + } + + #[test] + fn queue_does_not_flush() { + let mut writer = Writer::default(); + + assert!(queue!(&mut writer, "foo").is_ok()); + assert!(!writer.flushed); + assert!(writer.flushed_buffer.is_empty()); + } + + #[test] + fn execute_allows_trailing_comma() { + let mut writer = Writer::default(); + + assert!(execute!(&mut writer, "foo",).is_ok()); + assert_eq!(&writer.flushed_buffer, "foo"); + } + + #[test] + fn execute_writes_single_sequence() { + let mut writer = Writer::default(); + + assert!(execute!(&mut writer, "foo").is_ok()); + assert_eq!(&writer.flushed_buffer, "foo"); + } + + #[test] + fn execute_writes_multiple_sequences() { + let mut writer = Writer::default(); + + assert!(execute!(&mut writer, "foo", "bar", "baz").is_ok()); + assert_eq!(&writer.flushed_buffer, "foobarbaz"); + } + + #[test] + fn execute_does_flush() { + let mut writer = Writer::default(); + + assert!(execute!(&mut writer, "foo").is_ok()); + assert!(writer.flushed); + assert_eq!(&writer.flushed_buffer, "foo"); + assert!(writer.buffer.is_empty()); + } + + #[derive(Default)] + struct Writer { + buffer: String, + flushed_buffer: String, + flushed: bool, + } + + impl Write for Writer { + fn write(&mut self, buf: &[u8]) -> Result<usize, Error> { + let s = std::str::from_utf8(buf).map_err(|_| ErrorKind::InvalidData)?; + + self.buffer.push_str(s); + Ok(s.len()) + } + + fn flush(&mut self) -> Result<(), Error> { + self.flushed_buffer = self.buffer.clone(); + self.buffer = String::new(); + self.flushed = true; + Ok(()) + } + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..be87b29 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,252 @@ +//! An ANSI escape sequence parser module. +//! +//! **This module is not available with default features. You have to enable `parser` feature +//! if you'd like to use it.** +//! +//! # Parser +//! +//! The ANSI escape sequence parser parses input data in two steps: +//! +//! * transforms input data into valid characters, generic csi & escape sequences, throws away invalid data, +//! * give them meaning, throws away sequences without known meaning. +//! +//! ## First step +//! +//! State machine implementation for the first step is inspired by the +//! [A parser for DEC’s ANSI-compatible video terminals](https://vt100.net/emu/dec_ansi_parser) article +//! and the [vte](https://crates.io/crates/vte) crate. The goal of this step is to transform an input +//! data into characters, generic csi & escape sequences, validate them and throw away malformed input. +//! +//! An example of valid csi sequence: `b"\x1B[20;10R"`. Output of the first step will be: +//! +//! * valid csi sequence +//! * with two parameters `[20, 10]` +//! * and the final character `R`. +//! +//! ## Second step +//! +//! An input of this step is output of the first one. We know that the final character `R` represents +//! cursor position and two parameters should be provided. They were provided, we can give it a +//! meaning and return `Sequence::CursorPosition(10, 20)`. +//! +//! All sequences without known meaning are discarded. +//! +//! ## Implementation +//! +//! Both steps are considered as an implementation detail and there's no plan to make them +//! publicly available. +//! +//! The `parser` module provides the [`Parser`](struct.Parser.html) structure you can feed with +//! the [`advance`](struct.Parser.html#method.advance) method. It also implements the standard +//! library `Iterator<Item = Sequence>` trait which allows you to consume valid sequences with +//! known meaning via the `next()` method. Check the [`Sequence`](enum.Sequence.html) enum to learn +//! what this module can parse. +use std::collections::VecDeque; + +use engine::{Engine, Provide}; +pub use types::{KeyCode, KeyModifiers, Mouse, MouseButton, Sequence}; + +mod engine; +mod parsers; +pub(crate) mod types; + +/// An ANSI escape sequence parser. +/// +/// `Parser` implements the `Iterator<Item = Sequence>` trait, thus you can use the +/// `next()` method to consume all valid sequences with known meaning. +/// +/// # Examples +/// +/// Parse cursor position: +/// +/// ``` +/// use anes::parser::{Parser, Sequence}; +/// +/// let mut parser = Parser::default(); +/// parser.advance(b"\x1B[20;10R", false); +/// +/// assert_eq!(Some(Sequence::CursorPosition(10, 20)), parser.next()); +/// assert!(parser.next().is_none()); +/// ``` +/// +/// Parse keyboard event: +/// +/// ``` +/// use anes::parser::{KeyCode, KeyModifiers, Parser, Sequence}; +/// +/// let mut parser = Parser::default(); +/// parser.advance("𐌼a".as_bytes(), false); +/// +/// assert_eq!(Some(Sequence::Key(KeyCode::Char('𐌼'), KeyModifiers::empty())), parser.next()); +/// assert_eq!(Some(Sequence::Key(KeyCode::Char('a'), KeyModifiers::empty())), parser.next()); +/// assert!(parser.next().is_none()); +/// ``` +#[derive(Default)] +pub struct Parser { + engine: Engine, + provider: SequenceProvider, +} + +impl Parser { + /// Advances parser state machine with additional input data. + /// + /// # Arguments + /// + /// * `buffer` - input data (stdin in raw mode, etc.) + /// * `more` - more input data available right now + /// + /// It's crucial to provide correct `more` value in order to receive `KeyCode::Esc` events + /// as soon as possible. + /// + /// # Examples + /// + /// Esc key: + /// + /// ``` + /// use anes::parser::{KeyCode, KeyModifiers, Parser, Sequence}; + /// + /// let mut parser = Parser::default(); + /// // User pressed Esc key & nothing else which means that there's no additional input available + /// // aka no possible escape sequence = `KeyCode::Esc` dispatched. + /// parser.advance(&[0x1b], false); + /// + /// assert_eq!(Some(Sequence::Key(KeyCode::Esc, KeyModifiers::empty())), parser.next()); + /// assert!(parser.next().is_none()); + /// ``` + /// + /// Possible escape sequence: + /// + /// ``` + /// use anes::parser::{KeyCode, KeyModifiers, Parser, Sequence}; + /// + /// let mut parser = Parser::default(); + /// // User pressed F1 = b"\x1BOP" + /// + /// // Every escape sequence starts with Esc (0x1b). There's more input available + /// // aka possible escape sequence = `KeyCode::Esc` isn't dispatched even when the parser + /// // doesn't know rest of the sequence. + /// parser.advance(&[0x1b], true); + /// assert!(parser.next().is_none()); + /// + /// // Advance parser with rest of the sequence + /// parser.advance(&[b'O', b'P'], false); + /// assert_eq!(Some(Sequence::Key(KeyCode::F(1), KeyModifiers::empty())), parser.next()); + /// assert!(parser.next().is_none()); + /// ``` + pub fn advance(&mut self, buffer: &[u8], more: bool) { + let len = buffer.len(); + for (idx, byte) in buffer.iter().enumerate() { + self.engine + .advance(&mut self.provider, *byte, idx < len - 1 || more); + } + } +} + +impl Iterator for Parser { + type Item = Sequence; + + fn next(&mut self) -> Option<Self::Item> { + self.provider.next() + } +} + +#[derive(Default)] +struct SequenceProvider { + esc_o: bool, + seqs: VecDeque<Sequence>, +} + +impl Iterator for SequenceProvider { + type Item = Sequence; + + fn next(&mut self) -> Option<Self::Item> { + self.seqs.pop_front() + } +} + +impl Provide for SequenceProvider { + fn provide_char(&mut self, ch: char) { + // eprintln!("dispatch_char: {}", ch); + + if let Some(seq) = parsers::parse_char(ch, self.esc_o) { + self.seqs.push_back(seq); + } + self.esc_o = false; + } + + fn provide_esc_sequence(&mut self, ch: char) { + if ch == 'O' { + // Exception + // + // Esc O - dispatched as an escape sequence followed by single character (P-S) representing + // F1-F4 keys. We store Esc O flag only which is then used in the dispatch_char method. + self.esc_o = true; + } else { + self.esc_o = false; + if let Some(seq) = parsers::parse_esc_sequence(ch) { + self.seqs.push_back(seq); + } + } + } + + fn provide_csi_sequence(&mut self, parameters: &[u64], ignored_count: usize, ch: char) { + if let Some(seq) = parsers::parse_csi_sequence(parameters, ignored_count, ch) { + self.seqs.push_back(seq); + } + + self.esc_o = false; + } +} + +#[cfg(test)] +mod tests { + use super::Parser; + + #[test] + fn dispatch_char() { + let mut parser = Parser::default(); + parser.advance(&[b'a'], false); + assert!(parser.next().is_some()); + } + + #[test] + fn dispatch_esc_sequence() { + let mut parser = Parser::default(); + parser.advance(&[b'\x1B'], true); + assert!(parser.next().is_none()); + parser.advance(&[b'a'], false); + assert!(parser.next().is_some()); + } + + #[test] + fn does_not_dispatch_esc_sequence_with_upper_case_o() { + let mut parser = Parser::default(); + parser.advance(&[b'\x1B'], true); + assert!(parser.next().is_none()); + parser.advance(&[b'O'], true); + assert!(parser.next().is_none()); + } + + #[test] + fn dispatch_esc_with_upper_case_o_followed_by_char_as_single_sequence() { + let mut parser = Parser::default(); + parser.advance(&[b'\x1B'], true); + assert!(parser.next().is_none()); + parser.advance(&[b'O'], true); + assert!(parser.next().is_none()); + parser.advance(&[b'P'], false); + assert!(parser.next().is_some()); + assert!(parser.next().is_none()); + } + + #[test] + fn dispatch_csi_sequence() { + let mut parser = Parser::default(); + parser.advance(&[b'\x1B'], true); + assert!(parser.next().is_none()); + parser.advance(&[b'['], true); + assert!(parser.next().is_none()); + parser.advance(&[b'D'], false); + assert!(parser.next().is_some()); + } +} diff --git a/src/parser/engine.rs b/src/parser/engine.rs new file mode 100644 index 0000000..645208d --- /dev/null +++ b/src/parser/engine.rs @@ -0,0 +1,614 @@ +// +// https://vt100.net/emu/dec_ansi_parser +// +// The parser is heavily inspired by the vte (https://crates.io/crates/vte) crate. +// Tried to use this crate, but it doesn't work for opposite way (terminal -> sequence), +// because there're couple of exceptions we have to handle and it doesn't make much +// sense to add them to the vte crate. An example is Esc key where we need to know if +// there's additional input available or not and then the decision is made if the +// Esc char is dispatched immediately (user hits just Esc key) or if it's an escape/csi/... +// sequence. +// +const MAX_PARAMETERS: usize = 30; +const DEFAULT_PARAMETER_VALUE: u64 = 0; +const MAX_UTF8_CODE_POINTS: usize = 4; + +/// A parser engine state. +/// +/// All these variant names come from the +/// [A parser for DEC’s ANSI-compatible video terminals](https://vt100.net/emu/dec_ansi_parser) +/// description. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum State { + /// Initial state. + Ground, + /// Escape sequence started. + /// + /// `Esc` received with a flag that there's more data available. + Escape, + /// Escape sequence and we're collecting intermediates. + /// + /// # Notes + /// + /// This implementation doesn't collect intermediates. It just handles the state + /// to distinguish between (im)proper sequences. + EscapeIntermediate, + /// CSI sequence started. + /// + /// `Esc` followed by the `[` received. + CsiEntry, + /// CSI sequence should be consumed, but not dispatched. + CsiIgnore, + /// CSI sequence and we're collecting parameters. + CsiParameter, + /// CSI sequence and we're collecting intermediates. + /// + /// # Notes + /// + /// This implementation doesn't collect intermediates. It just handles the state + /// to distinguish between (im)proper sequences. + CsiIntermediate, + /// Possible UTF-8 sequence and we're collecting UTF-8 code points. + Utf8, +} + +pub(crate) trait Provide { + fn provide_char(&mut self, ch: char); + + fn provide_esc_sequence(&mut self, ch: char); + + fn provide_csi_sequence(&mut self, parameters: &[u64], ignored_count: usize, ch: char); +} + +pub(crate) struct Engine { + parameters: [u64; MAX_PARAMETERS], + parameters_count: usize, + parameter: u64, + ignored_parameters_count: usize, + state: State, + utf8_points: [u8; MAX_UTF8_CODE_POINTS], + utf8_points_count: usize, + utf8_points_expected_count: usize, +} + +impl Default for Engine { + fn default() -> Self { + Engine { + parameters: [DEFAULT_PARAMETER_VALUE; MAX_PARAMETERS], + parameters_count: 0, + parameter: DEFAULT_PARAMETER_VALUE, + ignored_parameters_count: 0, + state: State::Ground, + utf8_points: [0; MAX_UTF8_CODE_POINTS], + utf8_points_count: 0, + utf8_points_expected_count: 0, + } + } +} + +impl Engine { + fn set_state(&mut self, state: State) { + if let State::Ground = state { + self.parameters_count = 0; + self.parameter = DEFAULT_PARAMETER_VALUE; + self.ignored_parameters_count = 0; + self.utf8_points_count = 0; + self.utf8_points_expected_count = 0; + } + self.state = state; + } + + fn store_parameter(&mut self) { + if self.parameters_count < MAX_PARAMETERS { + self.parameters[self.parameters_count] = self.parameter; + self.parameters_count += 1; + } else { + self.ignored_parameters_count += 1; + } + self.parameter = DEFAULT_PARAMETER_VALUE; + } + + fn handle_possible_esc(&mut self, provider: &mut dyn Provide, byte: u8, more: bool) -> bool { + if byte != 0x1B { + return false; + } + + match (self.state, more) { + // More input means possible Esc sequence, just switch state and wait + (State::Ground, true) => self.set_state(State::Escape), + + // No more input means Esc key, dispatch it + (State::Ground, false) => provider.provide_char('\x1B'), + + // More input means possible Esc sequence, dispatch the previous Esc char + (State::Escape, true) => provider.provide_char('\x1B'), + + // No more input means Esc key, dispatch the previous & current Esc char + (State::Escape, false) => { + provider.provide_char('\x1B'); + provider.provide_char('\x1B'); + self.set_state(State::Ground); + } + + // Discard any state + // More input means possible Esc sequence + (_, true) => self.set_state(State::Escape), + + // Discard any state + // No more input means Esc key, dispatch it + (_, false) => { + provider.provide_char('\x1B'); + self.set_state(State::Ground); + } + } + + true + } + + fn handle_possible_utf8_code_points(&mut self, provider: &mut dyn Provide, byte: u8) -> bool { + if byte & 0b1000_0000 == 0b0000_0000 { + provider.provide_char(byte as char); + true + } else if byte & 0b1110_0000 == 0b1100_0000 { + self.utf8_points_count = 1; + self.utf8_points[0] = byte; + self.utf8_points_expected_count = 2; + self.set_state(State::Utf8); + true + } else if byte & 0b1111_0000 == 0b1110_0000 { + self.utf8_points_count = 1; + self.utf8_points[0] = byte; + self.utf8_points_expected_count = 3; + self.set_state(State::Utf8); + true + } else if byte & 0b1111_1000 == 0b1111_0000 { + self.utf8_points_count = 1; + self.utf8_points[0] = byte; + self.utf8_points_expected_count = 4; + self.set_state(State::Utf8); + true + } else { + false + } + } + + fn advance_ground_state(&mut self, provider: &mut dyn Provide, byte: u8) { + if self.handle_possible_utf8_code_points(provider, byte) { + return; + } + + match byte { + 0x1B => unreachable!(), + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // Print + 0x20..=0x7F => provider.provide_char(byte as char), + + _ => {} + }; + } + + fn advance_escape_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // Intermediate bytes to collect + 0x20..=0x2F => { + self.set_state(State::EscapeIntermediate); + } + + // Escape followed by '[' (0x5B) + // -> CSI sequence start + 0x5B => self.set_state(State::CsiEntry), + + // Escape sequence final character + 0x30..=0x4F | 0x51..=0x57 | 0x59 | 0x5A | 0x5C | 0x60..=0x7E => { + provider.provide_esc_sequence(byte as char); + self.set_state(State::Ground); + } + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x7F => {} + + // Other bytes are considered as invalid -> cancel whatever we have + _ => self.set_state(State::Ground), + }; + } + + fn advance_escape_intermediate_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // Intermediate bytes to collect + 0x20..=0x2F => {} + + // Escape followed by '[' (0x5B) + // -> CSI sequence start + 0x5B => self.set_state(State::CsiEntry), + + // Escape sequence final character + 0x30..=0x5A | 0x5C..=0x7E => { + provider.provide_esc_sequence(byte as char); + self.set_state(State::Ground); + } + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x7F => {} + + // Other bytes are considered as invalid -> cancel whatever we have + _ => self.set_state(State::Ground), + }; + } + + fn advance_csi_entry_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // Semicolon = parameter delimiter + 0x3B => { + self.store_parameter(); + self.set_state(State::CsiParameter); + } + + // '0' ..= '9' = parameter value + 0x30..=0x39 => { + self.parameter = (byte as u64) - 0x30; + self.set_state(State::CsiParameter); + } + + 0x3A => self.set_state(State::CsiIgnore), + + // CSI sequence final character + // -> dispatch CSI sequence + 0x40..=0x7E => { + provider.provide_csi_sequence( + &self.parameters[..self.parameters_count], + self.ignored_parameters_count, + byte as char, + ); + + self.set_state(State::Ground); + } + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x7F => {} + + // Collect rest as parameters + _ => { + self.parameter = byte as u64; + self.store_parameter(); + } + }; + } + + fn advance_csi_ignore_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x20..=0x3F | 0x7F => {} + + 0x40..=0x7E => self.set_state(State::Ground), + + // Other bytes are considered as invalid -> cancel whatever we have + _ => self.set_state(State::Ground), + }; + } + + fn advance_csi_parameter_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // '0' ..= '9' = parameter value + 0x30..=0x39 => { + self.parameter = self.parameter.saturating_mul(10); + self.parameter = self.parameter.saturating_add((byte as u64) - 0x30); + } + + // Semicolon = parameter delimiter + 0x3B => self.store_parameter(), + + // CSI sequence final character + // -> dispatch CSI sequence + 0x40..=0x7E => { + self.store_parameter(); + provider.provide_csi_sequence( + &self.parameters[..self.parameters_count], + self.ignored_parameters_count, + byte as char, + ); + + self.set_state(State::Ground); + } + + // Intermediates to collect + 0x20..=0x2F => { + self.store_parameter(); + self.set_state(State::CsiIntermediate); + } + + // Ignore + 0x3A | 0x3C..=0x3F => self.set_state(State::CsiIgnore), + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x7F => {} + + // Other bytes are considered as invalid -> cancel whatever we have + _ => self.set_state(State::Ground), + }; + } + + fn advance_csi_intermediate_state(&mut self, provider: &mut dyn Provide, byte: u8) { + match byte { + 0x1B => unreachable!(), + + // Intermediates to collect + 0x20..=0x2F => {} + + // CSI sequence final character + // -> dispatch CSI sequence + 0x40..=0x7E => { + provider.provide_csi_sequence( + &self.parameters[..self.parameters_count], + self.ignored_parameters_count, + byte as char, + ); + + self.set_state(State::Ground); + } + + // Execute + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => provider.provide_char(byte as char), + + // TODO Does it mean we should ignore the whole sequence? + // Ignore + 0x7F => {} + + // Other bytes are considered as invalid -> cancel whatever we have + _ => self.set_state(State::Ground), + } + } + + fn advance_utf8_state(&mut self, provider: &mut dyn Provide, byte: u8) { + if byte & 0b1100_0000 != 0b1000_0000 { + self.set_state(State::Ground); + return; + } + + self.utf8_points[self.utf8_points_count] = byte; + self.utf8_points_count += 1; + + if self.utf8_points_count == self.utf8_points_expected_count { + if let Some(ch) = std::str::from_utf8(&self.utf8_points[..self.utf8_points_count]) + .ok() + .and_then(|s| s.chars().next()) + { + provider.provide_char(ch); + } + self.set_state(State::Ground); + } + } + + pub(crate) fn advance(&mut self, provider: &mut dyn Provide, byte: u8, more: bool) { + // eprintln!("advance: {:?} {} {}", self.state, byte, more); + + if self.handle_possible_esc(provider, byte, more) { + return; + } + + match self.state { + State::Ground => self.advance_ground_state(provider, byte), + State::Escape => self.advance_escape_state(provider, byte), + State::EscapeIntermediate => self.advance_escape_intermediate_state(provider, byte), + State::CsiEntry => self.advance_csi_entry_state(provider, byte), + State::CsiIgnore => self.advance_csi_ignore_state(provider, byte), + State::CsiParameter => self.advance_csi_parameter_state(provider, byte), + State::CsiIntermediate => self.advance_csi_intermediate_state(provider, byte), + State::Utf8 => self.advance_utf8_state(provider, byte), + }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn esc_char() { + let mut engine = Engine::default(); + let mut provider = CharProvider::default(); + + // No more input means that the Esc character should be dispatched immediately + engine.advance(&mut provider, 0x1B, false); + assert_eq!(provider.chars, &['\x1B']); + + // There's more input so the machine should wait before dispatching Esc character + engine.advance(&mut provider, 0x1B, true); + assert_eq!(provider.chars, &['\x1B']); + + // Another Esc character, but no more input, machine should dispatch the postponed Esc + // character and the new one too. + engine.advance(&mut provider, 0x1B, false); + assert_eq!(provider.chars, &['\x1B', '\x1B', '\x1B']); + } + + #[test] + fn esc_without_intermediates() { + let mut engine = Engine::default(); + let mut provider = EscProvider::default(); + + let input = b"\x1B0\x1B~"; + advance(&mut engine, &mut provider, input, false); + + assert_eq!(provider.chars.len(), 2); + + assert_eq!(provider.chars[0], '0'); + + assert_eq!(provider.chars[1], '~'); + } + + #[test] + fn csi_without_parameters() { + let mut engine = Engine::default(); + let mut provider = CsiProvider::default(); + + let input = b"\x1B\x5Bm"; + advance(&mut engine, &mut provider, input, false); + + assert_eq!(provider.parameters.len(), 1); + assert_eq!(provider.parameters[0], &[]); + assert_eq!(provider.chars.len(), 1); + assert_eq!(provider.chars[0], 'm'); + } + + #[test] + fn csi_with_two_default_parameters() { + let mut engine = Engine::default(); + let mut provider = CsiProvider::default(); + + let input = b"\x1B\x5B;m"; + advance(&mut engine, &mut provider, input, false); + + assert_eq!(provider.parameters.len(), 1); + assert_eq!( + provider.parameters[0], + &[DEFAULT_PARAMETER_VALUE, DEFAULT_PARAMETER_VALUE] + ); + assert_eq!(provider.chars.len(), 1); + assert_eq!(provider.chars[0], 'm'); + } + + #[test] + fn csi_with_trailing_semicolon() { + let mut engine = Engine::default(); + let mut provider = CsiProvider::default(); + + let input = b"\x1B\x5B123;m"; + advance(&mut engine, &mut provider, input, false); + + assert_eq!(provider.parameters.len(), 1); + assert_eq!(provider.parameters[0], &[123, DEFAULT_PARAMETER_VALUE]); + assert_eq!(provider.chars.len(), 1); + assert_eq!(provider.chars[0], 'm'); + } + + #[test] + fn csi_max_parameters() { + let mut engine = Engine::default(); + let mut provider = CsiProvider::default(); + + let input = b"\x1B\x5B1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30m"; + advance(&mut engine, &mut provider, input, false); + + assert_eq!(provider.parameters.len(), 1); + assert_eq!(provider.parameters[0].len(), MAX_PARAMETERS); + assert_eq!( + provider.parameters[0], + &[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30 + ] + ); + assert_eq!(provider.chars.len(), 1); + assert_eq!(provider.chars[0], 'm'); + } + + #[test] + fn test_parse_utf8_character() { + let mut engine = Engine::default(); + let mut provider = CharProvider::default(); + + advance(&mut engine, &mut provider, &['a' as u8], false); + assert_eq!(provider.chars.len(), 1); + assert_eq!(provider.chars[0], 'a'); + + advance(&mut engine, &mut provider, &[0xC3, 0xB1], false); + assert_eq!(provider.chars.len(), 2); + assert_eq!(provider.chars[1], 'ñ'); + + advance(&mut engine, &mut provider, &[0xE2, 0x81, 0xA1], false); + assert_eq!(provider.chars.len(), 3); + assert_eq!(provider.chars[2], '\u{2061}'); + + advance(&mut engine, &mut provider, &[0xF0, 0x90, 0x8C, 0xBC], false); + assert_eq!(provider.chars.len(), 4); + assert_eq!(provider.chars[3], '𐌼'); + } + + fn advance(engine: &mut Engine, provider: &mut dyn Provide, bytes: &[u8], more: bool) { + let len = bytes.len(); + + for (i, byte) in bytes.iter().enumerate() { + engine.advance(provider, *byte, i < len - 1 || more); + } + } + + #[derive(Default)] + struct CharProvider { + chars: Vec<char>, + } + + impl Provide for CharProvider { + fn provide_char(&mut self, ch: char) { + self.chars.push(ch); + } + + fn provide_esc_sequence(&mut self, _ch: char) {} + + fn provide_csi_sequence(&mut self, _parameters: &[u64], _ignored_count: usize, _ch: char) {} + } + + #[derive(Default)] + struct CsiProvider { + parameters: Vec<Vec<u64>>, + chars: Vec<char>, + } + + impl Provide for CsiProvider { + fn provide_char(&mut self, _ch: char) {} + + fn provide_esc_sequence(&mut self, _ch: char) {} + + fn provide_csi_sequence(&mut self, parameters: &[u64], _ignored_count: usize, ch: char) { + self.parameters.push(parameters.to_vec()); + self.chars.push(ch); + } + } + + #[derive(Default)] + struct EscProvider { + chars: Vec<char>, + } + + impl Provide for EscProvider { + fn provide_char(&mut self, _ch: char) {} + + fn provide_esc_sequence(&mut self, ch: char) { + self.chars.push(ch); + } + + fn provide_csi_sequence(&mut self, _parameters: &[u64], _ignored_count: usize, _ch: char) {} + } +} diff --git a/src/parser/parsers.rs b/src/parser/parsers.rs new file mode 100644 index 0000000..9bb9acb --- /dev/null +++ b/src/parser/parsers.rs @@ -0,0 +1,239 @@ +use super::types::{KeyCode, KeyModifiers, Mouse, MouseButton, Sequence}; + +pub(crate) fn parse_char(ch: char, esc_o: bool) -> Option<Sequence> { + if esc_o { + return match ch { + 'P'..='S' => Some(Sequence::Key( + KeyCode::F(ch as u8 - b'P' + 1), + KeyModifiers::empty(), + )), + _ => None, + }; + } + + let code = match ch { + '\r' | '\n' => KeyCode::Enter, + '\t' => KeyCode::Tab, + '\x7F' => KeyCode::BackTab, + '\x1B' => KeyCode::Esc, + '\0' => KeyCode::Null, + _ => KeyCode::Char(ch), + }; + Some(Sequence::Key(code, KeyModifiers::empty())) +} + +pub(crate) fn parse_esc_sequence(ch: char) -> Option<Sequence> { + // EscO[P-S] is handled in the Performer, see parse_char & esc_o argument + // No need to handle other cases here? It's just Alt+$char + Some(Sequence::Key(KeyCode::Char(ch), KeyModifiers::ALT)) +} + +pub(crate) fn parse_csi_sequence( + parameters: &[u64], + _ignored_count: usize, + ch: char, +) -> Option<Sequence> { + match ch { + 'A' => Some(Sequence::Key( + KeyCode::Up, + parse_csi_arrow_key_modifiers(parameters.first().cloned()), + )), + 'B' => Some(Sequence::Key( + KeyCode::Down, + parse_csi_arrow_key_modifiers(parameters.first().cloned()), + )), + 'C' => Some(Sequence::Key( + KeyCode::Right, + parse_csi_arrow_key_modifiers(parameters.first().cloned()), + )), + 'D' => Some(Sequence::Key( + KeyCode::Left, + parse_csi_arrow_key_modifiers(parameters.first().cloned()), + )), + 'H' => Some(Sequence::Key(KeyCode::Home, KeyModifiers::empty())), + 'F' => Some(Sequence::Key(KeyCode::End, KeyModifiers::empty())), + 'Z' => Some(Sequence::Key(KeyCode::BackTab, KeyModifiers::empty())), + 'R' => parse_csi_cursor_position(parameters), + 'm' => parse_csi_xterm_mouse(parameters, ch), + 'M' if parameters.first() == Some(&0x3C) => parse_csi_xterm_mouse(parameters, ch), + 'M' => parse_csi_rxvt_mouse(parameters), + '~' => parse_csi_tilde_key_code(parameters), + _ => None, + } +} + +fn parse_csi_arrow_key_modifiers(parameter: Option<u64>) -> KeyModifiers { + parse_key_modifiers(parameter.map(|x| x.saturating_sub(48))) +} + +fn parse_key_modifiers(parameter: Option<u64>) -> KeyModifiers { + if let Some(parameter) = parameter { + match parameter { + 2 => KeyModifiers::SHIFT, + 3 => KeyModifiers::ALT, + 4 => KeyModifiers::SHIFT | KeyModifiers::ALT, + 5 => KeyModifiers::CONTROL, + 6 => KeyModifiers::SHIFT | KeyModifiers::CONTROL, + 7 => KeyModifiers::ALT | KeyModifiers::CONTROL, + 8 => KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL, + 9 => KeyModifiers::META, + 10 => KeyModifiers::META | KeyModifiers::SHIFT, + 11 => KeyModifiers::META | KeyModifiers::ALT, + 12 => KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::ALT, + 13 => KeyModifiers::META | KeyModifiers::CONTROL, + 14 => KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::CONTROL, + 15 => KeyModifiers::META | KeyModifiers::ALT | KeyModifiers::CONTROL, + 16 => { + KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + } + _ => KeyModifiers::empty(), + } + } else { + KeyModifiers::empty() + } +} + +fn parse_csi_tilde_key_code(parameters: &[u64]) -> Option<Sequence> { + if parameters.is_empty() { + return None; + } + + let modifiers = parse_key_modifiers(parameters.get(1).cloned()); + + let code = match parameters[0] { + 1 | 7 => KeyCode::Home, + 2 => KeyCode::Insert, + 3 => KeyCode::Delete, + 4 | 8 => KeyCode::End, + 5 => KeyCode::PageUp, + 6 => KeyCode::PageDown, + p @ 11..=15 => KeyCode::F(p as u8 - 10), + p @ 17..=21 => KeyCode::F(p as u8 - 11), + p @ 23..=24 => KeyCode::F(p as u8 - 12), + _ => return None, + }; + + Some(Sequence::Key(code, modifiers)) +} + +fn parse_csi_cursor_position(parameters: &[u64]) -> Option<Sequence> { + // ESC [ Cy ; Cx R + + if parameters.len() < 2 { + return None; + } + + let y = parameters[0] as u16; + let x = parameters[1] as u16; + + Some(Sequence::CursorPosition(x, y)) +} + +fn parse_csi_xterm_mouse(parameters: &[u64], ch: char) -> Option<Sequence> { + // ESC [ < Cb ; Cx ; Cy (;) (M or m) + + if parameters.len() < 4 { + return None; + } + + let cb = parameters[1] as u8; + let cx = parameters[2] as u16; + let cy = parameters[3] as u16; + + let up = match ch { + 'm' => true, + 'M' => false, + _ => return None, + }; + + let mut modifiers = KeyModifiers::empty(); + + if cb & 0b0000_0100 == 0b0000_0100 { + modifiers |= KeyModifiers::SHIFT; + } + + if cb & 0b0000_1000 == 0b0000_1000 { + modifiers |= KeyModifiers::ALT; + } + + if cb & 0b0001_0000 == 0b0001_0000 { + modifiers |= KeyModifiers::CONTROL; + } + + let mouse = if cb & 0b0100_0000 == 0b0100_0000 { + if cb & 0b0000_0001 == 0b0000_0001 { + Mouse::ScrollDown(cx, cy) + } else { + Mouse::ScrollUp(cx, cy) + } + } else { + let drag = cb & 0b0010_0000 == 0b0010_0000; + + match (cb & 0b0000_0011, up, drag) { + (0, true, _) => Mouse::Up(MouseButton::Left, cx, cy), + (0, false, false) => Mouse::Down(MouseButton::Left, cx, cy), + (0, false, true) => Mouse::Drag(MouseButton::Left, cx, cy), + (1, true, _) => Mouse::Up(MouseButton::Middle, cx, cy), + (1, false, false) => Mouse::Down(MouseButton::Middle, cx, cy), + (1, false, true) => Mouse::Drag(MouseButton::Middle, cx, cy), + (2, true, _) => Mouse::Up(MouseButton::Right, cx, cy), + (2, false, false) => Mouse::Down(MouseButton::Right, cx, cy), + (2, false, true) => Mouse::Drag(MouseButton::Right, cx, cy), + _ => return None, + } + }; + + Some(Sequence::Mouse(mouse, modifiers)) +} + +fn parse_csi_rxvt_mouse(parameters: &[u64]) -> Option<Sequence> { + // ESC [ Cb ; Cx ; Cy ; M + + if parameters.len() < 3 { + return None; + } + + let cb = parameters[0]; + let cx = parameters[1] as u16; + let cy = parameters[2] as u16; + + let mut modifiers = KeyModifiers::empty(); + + if cb & 0b0000_0100 == 0b0000_0100 { + modifiers |= KeyModifiers::SHIFT; + } + + if cb & 0b0000_1000 == 0b0000_1000 { + modifiers |= KeyModifiers::ALT; + } + + if cb & 0b0001_0000 == 0b0001_0000 { + modifiers |= KeyModifiers::CONTROL; + } + + let mouse = if cb & 0b0110_0000 == 0b0110_0000 { + if cb & 0b0000_0001 == 0b0000_0001 { + Mouse::ScrollDown(cx, cy) + } else { + Mouse::ScrollUp(cx, cy) + } + } else { + let drag = cb & 0b0100_0000 == 0b0100_0000; + + match (cb & 0b0000_0011, drag) { + (0b0000_0000, false) => Mouse::Down(MouseButton::Left, cx, cy), + (0b0000_0010, false) => Mouse::Down(MouseButton::Right, cx, cy), + (0b0000_0001, false) => Mouse::Down(MouseButton::Middle, cx, cy), + + (0b0000_0000, true) => Mouse::Drag(MouseButton::Left, cx, cy), + (0b0000_0010, true) => Mouse::Drag(MouseButton::Right, cx, cy), + (0b0000_0001, true) => Mouse::Drag(MouseButton::Middle, cx, cy), + + (0b0000_0011, false) => Mouse::Up(MouseButton::Any, cx, cy), + + _ => return None, + } + }; + + Some(Sequence::Mouse(mouse, modifiers)) +} diff --git a/src/parser/types.rs b/src/parser/types.rs new file mode 100644 index 0000000..66b6561 --- /dev/null +++ b/src/parser/types.rs @@ -0,0 +1,79 @@ +use bitflags::bitflags; + +/// A parsed ANSI escape sequence. +/// +/// Check the [`Parser`](struct.Parser.html) structure documentation for examples +/// how to retrieve these values. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum Sequence { + /// A keyboard event sequence. + Key(KeyCode, KeyModifiers), + /// A mouse event sequence. + Mouse(Mouse, KeyModifiers), + /// A cursor position (`x`, `y`). + /// + /// Top/left cell is represented as `Sequence::CursorPosition(1, 1)`. + CursorPosition(u16, u16), +} + +bitflags! { + /// A key modifiers. + pub struct KeyModifiers: u8 { + const SHIFT = 0b0000_0001; + const CONTROL = 0b0000_0010; + const ALT = 0b0000_0100; + const META = 0b0000_1000; + } +} + +/// A key code. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum KeyCode { + Backspace, + Enter, + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + Tab, + BackTab, + Delete, + Insert, + F(u8), + Char(char), + Null, + Esc, +} + +/// A mouse event. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum Mouse { + /// A mouse button press. + Down(MouseButton, u16, u16), + /// A mouse button release. + Up(MouseButton, u16, u16), + /// A mouse movement with pressed button. + Drag(MouseButton, u16, u16), + /// A mouse wheel scrolled up. + ScrollUp(u16, u16), + /// A mouse wheel scrolled down. + ScrollDown(u16, u16), +} + +/// A mouse button. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum MouseButton { + Left, + Right, + Middle, + /// This variant is provided only if [`Parser`](struct.Parser.html) doesn't know which + /// mouse button was pressed/released. + /// + /// An example is [rxvt](https://en.wikipedia.org/wiki/Rxvt) - it provides which mouse + /// button was pressed, but doesn't provide which mouse button was released. + Any, +} diff --git a/src/sequences.rs b/src/sequences.rs new file mode 100644 index 0000000..c660800 --- /dev/null +++ b/src/sequences.rs @@ -0,0 +1,5 @@ +pub(crate) mod attribute; +pub(crate) mod buffer; +pub(crate) mod color; +pub(crate) mod cursor; +pub(crate) mod terminal; diff --git a/src/sequences/attribute.rs b/src/sequences/attribute.rs new file mode 100644 index 0000000..30961ff --- /dev/null +++ b/src/sequences/attribute.rs @@ -0,0 +1,133 @@ +use std::fmt; + +sequence!( + /// Resets all attributes. + /// + /// This sequence resets all attributes previously set by the: + /// + /// * [`SetAttribute`](struct.SetAttribute.html) + /// * [`SetForegroundColor`](struct.SetBackgroundColor.html) + /// * [`SetBackgroundColor`](struct.SetForegroundColor.html) + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ResetAttributes; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}", ResetAttributes); + /// ``` + struct ResetAttributes => sgr!("0") +); + +/// A display attribute. +/// +/// This is **NOT** a full ANSI sequence. `Attribute` must be used along with +/// the [`SetAttribute`](struct.SetAttribute.html). +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{stdout, Write}; +/// use anes::{Attribute, SetAttribute}; +/// +/// let mut stdout = stdout(); +/// write!(stdout, "{}Bold text", SetAttribute(Attribute::Bold)); +/// ``` +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum Attribute { + /// Bold (increased) intensity. + Bold = 1, + /// Faint (decreased) intensity. + Faint = 2, + /// Normal intensity (turns off `Bold` and/or `Faint`). + Normal = 22, + + /// Italic. + Italic = 3, + /// Turns off `Italic`. + ItalicOff = 23, + + /// Underlined text. + Underline = 4, + /// Turns off `Underline`. + UnderlineOff = 24, + + /// Blinking text. + Blink = 5, + /// Turns off blinking text (`Blink`). + BlinkOff = 25, + + /// Reverse foreground & background colors. + Reverse = 7, + /// Turns off `Reverse`. + ReverseOff = 27, + + /// Concealed (hidden). + Conceal = 8, + /// Turns off `Conceal`. + ConcealOff = 28, + + /// Crossed. + Crossed = 9, + /// Turns off `Crossed`. + CrossedOff = 29, +} + +impl fmt::Display for Attribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", *self as i32) + } +} + +sequence!( + /// Sets the display attribute. + /// + /// See the [`Attribute`](enum.Attribute.html) enum for a list of attributes you can (un)set. + /// + /// The [`ResetAttributes`](struct.ResetAttributes.html) sequence can be used to turn off all + /// attributes. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{Attribute, SetAttribute}; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}Blinking text", SetAttribute(Attribute::Blink)); + /// ``` + struct SetAttribute(Attribute) => + |this, f| write!(f, sgr!("{}"), this.0) +); + +#[cfg(test)] +test_sequences!( + set_attribute( + SetAttribute(Attribute::Bold) => "\x1B[1m", + SetAttribute(Attribute::Faint) => "\x1B[2m", + SetAttribute(Attribute::Normal) => "\x1B[22m", + + SetAttribute(Attribute::Italic) => "\x1B[3m", + SetAttribute(Attribute::ItalicOff) => "\x1B[23m", + + SetAttribute(Attribute::Underline) => "\x1B[4m", + SetAttribute(Attribute::UnderlineOff) => "\x1B[24m", + + SetAttribute(Attribute::Blink) => "\x1B[5m", + SetAttribute(Attribute::BlinkOff) => "\x1B[25m", + + SetAttribute(Attribute::Reverse) => "\x1B[7m", + SetAttribute(Attribute::ReverseOff) => "\x1B[27m", + + SetAttribute(Attribute::Conceal) => "\x1B[8m", + SetAttribute(Attribute::ConcealOff) => "\x1B[28m", + + SetAttribute(Attribute::Crossed) => "\x1B[9m", + SetAttribute(Attribute::CrossedOff) => "\x1B[29m", + ), + reset_attributes( + ResetAttributes => "\x1B[0m", + ) +); diff --git a/src/sequences/buffer.rs b/src/sequences/buffer.rs new file mode 100644 index 0000000..053195a --- /dev/null +++ b/src/sequences/buffer.rs @@ -0,0 +1,145 @@ +sequence!( + /// Switches to the alternate buffer. + /// + /// Use the [`SwitchBufferToNormal`](struct.SwitchBufferToNormal.html) sequence to switch + /// back to the normal buffer. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{SwitchBufferToAlternate, SwitchBufferToNormal}; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}", SwitchBufferToAlternate); + /// // Your app on alternate screen + /// write!(stdout, "{}", SwitchBufferToNormal); + /// ``` + struct SwitchBufferToAlternate => csi!("?1049h") +); + +sequence!( + /// Switches to the normal buffer. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{SwitchBufferToAlternate, SwitchBufferToNormal}; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}", SwitchBufferToAlternate); + /// // Your app on alternate screen + /// write!(stdout, "{}", SwitchBufferToNormal); + /// ``` + struct SwitchBufferToNormal => csi!("?1049l") +); + +sequence!( + /// Scrolls up by the given number of rows. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ScrollBufferUp; + /// + /// let mut stdout = stdout(); + /// // Scroll up by 5 lines + /// write!(stdout, "{}", ScrollBufferUp(5)); + /// ``` + struct ScrollBufferUp(u16) => + |this, f| write!(f, csi!("{}S"), this.0) +); + +sequence!( + /// Scrolls down by the given number of rows. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ScrollBufferDown; + /// + /// let mut stdout = stdout(); + /// // Scroll down by 10 lines + /// write!(stdout, "{}", ScrollBufferDown(10)); + /// ``` + struct ScrollBufferDown(u16) => + |this, f| write!(f, csi!("{}T"), this.0) +); + +sequence!( + /// Clears part of the line. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ClearLine; + /// + /// let mut stdout = stdout(); + /// // Clear the whole line + /// write!(stdout, "{}", ClearLine::All); + /// ``` + enum ClearLine { + /// Clears from the cursor position to end of the line. + Right => csi!("K"), + /// Clears from the cursor position to beginning of the line. + Left => csi!("1K"), + /// Clears the whole line. + All => csi!("2K"), + } +); + +sequence!( + /// Clears part of the buffer. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ClearBuffer; + /// + /// let mut stdout = stdout(); + /// // Clear the entire buffer + /// write!(stdout, "{}", ClearBuffer::All); + /// ``` + enum ClearBuffer { + /// Clears from the cursor position to end of the screen. + Below => csi!("J"), + /// Clears from the cursor position to beginning of the screen. + Above => csi!("1J"), + /// Clears the entire buffer. + All => csi!("2J"), + /// Clears the entire buffer and all saved lines in the scrollback buffer. + SavedLines => csi!("3J"), + } +); + +#[cfg(test)] +test_sequences!( + switch_buffer_to_alternate( + SwitchBufferToAlternate => "\x1B[?1049h", + ), + switch_buffer_to_main( + SwitchBufferToNormal => "\x1B[?1049l", + ), + scroll_buffer_up( + ScrollBufferUp(10) => "\x1B[10S", + ), + scroll_buffer_down( + ScrollBufferDown(10) => "\x1B[10T", + ), + clear_line( + ClearLine::Right => "\x1B[K", + ClearLine::Left => "\x1B[1K", + ClearLine::All => "\x1B[2K", + ), + clear_buffer( + ClearBuffer::Below => "\x1B[J", + ClearBuffer::Above => "\x1B[1J", + ClearBuffer::All => "\x1B[2J", + ClearBuffer::SavedLines => "\x1B[3J", + ), +); diff --git a/src/sequences/color.rs b/src/sequences/color.rs new file mode 100644 index 0000000..b019f0f --- /dev/null +++ b/src/sequences/color.rs @@ -0,0 +1,189 @@ +use std::fmt; + +/// A color. +/// +/// This is **NOT** a full ANSI sequence. `Color` must be used along with +/// the: +/// +/// * [`SetBackgroundColor`](struct.SetBackgroundColor.html) +/// * [`SetForegroundColor`](struct.SetForegroundColor.html) +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{stdout, Write}; +/// use anes::{Color, SetForegroundColor}; +/// +/// let mut stdout = stdout(); +/// // Set the foreground color to red +/// write!(stdout, "{}", SetForegroundColor(Color::Red)); +/// ``` +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum Color { + /// Default color. + Default, + /// Black color. + Black, + /// Dark red color. + DarkRed, + /// Dark green color. + DarkGreen, + /// Dark yellow color. + DarkYellow, + /// Dark blue color. + DarkBlue, + /// Dark magenta color. + DarkMagenta, + /// Dark cyan color. + DarkCyan, + /// Dark gray color. + /// + /// Also knows as light (bright) black. + DarkGray, + /// Light (bright) gray color. + /// + /// Also known as dark white. + Gray, + /// Light (bright) red color. + Red, + /// Light (bright) green color. + Green, + /// Light (bright) yellow color. + Yellow, + /// Light (bright) blue color. + Blue, + /// Light (bright) magenta color. + Magenta, + /// Light (bright) cyan color. + Cyan, + /// White color. + White, + /// A color from the predefined set of ANSI colors. + /// + /// ```text + /// 0 - 7: standard colors (as in ESC [ 30–37 m) + /// 8- 15: high intensity colors (as in ESC [ 90–97 m) + /// 16-231: 6 × 6 × 6 cube (216 colors): 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) + /// 232-255: grayscale from black to white in 24 steps + /// ``` + /// + /// See [8-bit](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) for more information. + Ansi(u8), + /// An RGB color. + /// + /// See [24-bit](https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit) for more information. + Rgb(u8, u8, u8), +} + +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Color::Default is handled in the SetBackgroundColor & SetForegroundColor + Color::Default => Ok(()), + Color::Black => write!(f, "5;0"), + Color::DarkRed => write!(f, "5;1"), + Color::DarkGreen => write!(f, "5;2"), + Color::DarkYellow => write!(f, "5;3"), + Color::DarkBlue => write!(f, "5;4"), + Color::DarkMagenta => write!(f, "5;5"), + Color::DarkCyan => write!(f, "5;6"), + Color::Gray => write!(f, "5;7"), + Color::DarkGray => write!(f, "5;8"), + Color::Red => write!(f, "5;9"), + Color::Green => write!(f, "5;10"), + Color::Yellow => write!(f, "5;11"), + Color::Blue => write!(f, "5;12"), + Color::Magenta => write!(f, "5;13"), + Color::Cyan => write!(f, "5;14"), + Color::White => write!(f, "5;15"), + Color::Ansi(value) => write!(f, "5;{}", value), + Color::Rgb(r, g, b) => write!(f, "2;{};{};{}", r, g, b), + } + } +} + +sequence! { + /// Sets the foreground color. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{Color, SetForegroundColor}; + /// + /// let mut stdout = stdout(); + /// // Set the foreground color to blue + /// write!(stdout, "{}", SetForegroundColor(Color::Blue)); + /// ``` + struct SetForegroundColor(Color) => + |this, f| match this.0 { + Color::Default => write!(f, sgr!("39")), + _ => write!(f, sgr!("38;{}"), this.0), + } +} + +sequence! { + /// Sets the background color. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{Color, SetBackgroundColor}; + /// + /// let mut stdout = stdout(); + /// // Set the background color to yellow + /// write!(stdout, "{}", SetBackgroundColor(Color::Yellow)); + /// ``` + struct SetBackgroundColor(Color) => + |this, f| match this.0 { + Color::Default => write!(f, sgr!("49")), + _ => write!(f, sgr!("48;{}"), this.0), + } +} + +#[cfg(test)] +test_sequences!( + set_foreground_color( + SetForegroundColor(Color::Default) => "\x1B[39m", + SetForegroundColor(Color::Black) => "\x1B[38;5;0m", + SetForegroundColor(Color::DarkRed) => "\x1B[38;5;1m", + SetForegroundColor(Color::DarkGreen) => "\x1B[38;5;2m", + SetForegroundColor(Color::DarkYellow) => "\x1B[38;5;3m", + SetForegroundColor(Color::DarkBlue) => "\x1B[38;5;4m", + SetForegroundColor(Color::DarkMagenta) => "\x1B[38;5;5m", + SetForegroundColor(Color::DarkCyan) => "\x1B[38;5;6m", + SetForegroundColor(Color::DarkGray) => "\x1B[38;5;8m", + SetForegroundColor(Color::Gray) => "\x1B[38;5;7m", + SetForegroundColor(Color::Red) => "\x1B[38;5;9m", + SetForegroundColor(Color::Green) => "\x1B[38;5;10m", + SetForegroundColor(Color::Yellow) => "\x1B[38;5;11m", + SetForegroundColor(Color::Blue) => "\x1B[38;5;12m", + SetForegroundColor(Color::Magenta) => "\x1B[38;5;13m", + SetForegroundColor(Color::Cyan) => "\x1B[38;5;14m", + SetForegroundColor(Color::White) => "\x1B[38;5;15m", + SetForegroundColor(Color::Ansi(200)) => "\x1B[38;5;200m", + SetForegroundColor(Color::Rgb(1, 2, 3)) => "\x1B[38;2;1;2;3m", + ), + set_background_color( + SetBackgroundColor(Color::Default) => "\x1B[49m", + SetBackgroundColor(Color::Black) => "\x1B[48;5;0m", + SetBackgroundColor(Color::DarkRed) => "\x1B[48;5;1m", + SetBackgroundColor(Color::DarkGreen) => "\x1B[48;5;2m", + SetBackgroundColor(Color::DarkYellow) => "\x1B[48;5;3m", + SetBackgroundColor(Color::DarkBlue) => "\x1B[48;5;4m", + SetBackgroundColor(Color::DarkMagenta) => "\x1B[48;5;5m", + SetBackgroundColor(Color::DarkCyan) => "\x1B[48;5;6m", + SetBackgroundColor(Color::DarkGray) => "\x1B[48;5;8m", + SetBackgroundColor(Color::Gray) => "\x1B[48;5;7m", + SetBackgroundColor(Color::Red) => "\x1B[48;5;9m", + SetBackgroundColor(Color::Green) => "\x1B[48;5;10m", + SetBackgroundColor(Color::Yellow) => "\x1B[48;5;11m", + SetBackgroundColor(Color::Blue) => "\x1B[48;5;12m", + SetBackgroundColor(Color::Magenta) => "\x1B[48;5;13m", + SetBackgroundColor(Color::Cyan) => "\x1B[48;5;14m", + SetBackgroundColor(Color::White) => "\x1B[48;5;15m", + SetBackgroundColor(Color::Ansi(200)) => "\x1B[48;5;200m", + SetBackgroundColor(Color::Rgb(1, 2, 3)) => "\x1B[48;2;1;2;3m", + ) +); diff --git a/src/sequences/cursor.rs b/src/sequences/cursor.rs new file mode 100644 index 0000000..37abc3b --- /dev/null +++ b/src/sequences/cursor.rs @@ -0,0 +1,352 @@ +//! A terminal cursor related ANSI escape sequences. + +sequence!( + /// Saves the cursor position. + /// + /// Use the [`RestoreCursorPosition`](struct.RestoreCursorPosition.html) sequence to + /// restore the cursor position. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{SaveCursorPosition, RestoreCursorPosition}; + /// + /// let mut stdout = stdout(); + /// // Save cursor position + /// write!(stdout, "{}", SaveCursorPosition); + /// + /// // Your app + /// + /// // Restore cursor position + /// write!(stdout, "{}", RestoreCursorPosition); + /// ``` + struct SaveCursorPosition => esc!("7") +); + +sequence!( + /// Restores the cursor position. + /// + /// Use the [`SaveCursorPosition`](struct.SaveCursorPosition.html) sequence to + /// save the cursor position. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{SaveCursorPosition, RestoreCursorPosition}; + /// + /// let mut stdout = stdout(); + /// // Save cursor position + /// write!(stdout, "{}", SaveCursorPosition); + /// + /// // Your app + /// + /// // Restore cursor position + /// write!(stdout, "{}", RestoreCursorPosition); + /// ``` + struct RestoreCursorPosition => esc!("8") +); + +sequence!( + /// Hides the cursor. + /// + /// Use the [`ShowCursor`](struct.ShowCursor.html) sequence to show the cursor. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::HideCursor; + /// + /// let mut stdout = stdout(); + /// // Hide cursor + /// write!(stdout, "{}", HideCursor); + /// ``` + struct HideCursor => csi!("?25l") +); + +sequence!( + /// Shows the cursor. + /// + /// Use the [`HideCursor`](struct.HideCursor.html) sequence to hide the cursor. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ShowCursor; + /// + /// let mut stdout = stdout(); + /// // Show cursor + /// write!(stdout, "{}", ShowCursor); + /// ``` + struct ShowCursor => csi!("?25h") +); + +sequence!( + /// Enables the cursor blinking. + /// + /// Use the [`DisableCursorBlinking`](struct.DisableCursorBlinking.html) sequence to disable + /// cursor blinking. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::EnableCursorBlinking; + /// + /// let mut stdout = stdout(); + /// // Enable cursor blinking + /// write!(stdout, "{}", EnableCursorBlinking); + /// ``` + struct EnableCursorBlinking => csi!("?12h") +); + +sequence!( + /// Disables the cursor blinking. + /// + /// Use the [`EnableCursorBlinking`](struct.EnableCursorBlinking.html) sequence to enable + /// cursor blinking. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::DisableCursorBlinking; + /// + /// let mut stdout = stdout(); + /// // Disable cursor blinking + /// write!(stdout, "{}", DisableCursorBlinking); + /// ``` + struct DisableCursorBlinking => csi!("?12l") +); + +sequence!( + /// Moves the cursor to the given location (column, row). + /// + /// # Notes + /// + /// Top/left cell is represented as `1, 1` (`column, row`). + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorTo; + /// + /// let mut stdout = stdout(); + /// // Move cursor to top left cell + /// write!(stdout, "{}", MoveCursorTo(1, 1)); + /// ``` + struct MoveCursorTo(u16, u16) => + |this, f| write!(f, csi!("{};{}H"), this.1, this.0) +); + +sequence!( + /// Moves the cursor up by the given number of rows. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorUp; + /// + /// let mut stdout = stdout(); + /// // Move cursor up by 5 rows + /// write!(stdout, "{}", MoveCursorUp(5)); + /// ``` + struct MoveCursorUp(u16) => + |this, f| write!(f, csi!("{}A"), this.0) +); + +sequence!( + /// Moves the cursor down by the given number of rows. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorDown; + /// + /// let mut stdout = stdout(); + /// // Move cursor down by 5 rows + /// write!(stdout, "{}", MoveCursorDown(5)); + /// ``` + struct MoveCursorDown(u16) => + |this, f| write!(f, csi!("{}B"), this.0) +); + +sequence!( + /// Moves the cursor right by the given number of columns. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorRight; + /// + /// let mut stdout = stdout(); + /// // Move cursor right by 5 columns + /// write!(stdout, "{}", MoveCursorRight(5)); + /// ``` + struct MoveCursorRight(u16) => + |this, f| write!(f, csi!("{}C"), this.0) +); + +sequence!( + /// Moves the cursor left by the given number of columns. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorLeft; + /// + /// let mut stdout = stdout(); + /// // Move cursor left by 5 columns + /// write!(stdout, "{}", MoveCursorLeft(5)); + /// ``` + struct MoveCursorLeft(u16) => + |this, f| write!(f, csi!("{}D"), this.0) +); + +sequence!( + /// Moves the cursor to beginning of line the given number of lines down. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorToNextLine; + /// + /// let mut stdout = stdout(); + /// // Move cursor down by 2 rows and the move it to the first column + /// write!(stdout, "{}", MoveCursorToNextLine(2)); + /// ``` + /// + /// The previous example does the same thing as the following one: + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{MoveCursorDown, MoveCursorToColumn}; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}{}", MoveCursorDown(2), MoveCursorToColumn(1)); + /// ``` + struct MoveCursorToNextLine(u16) => + |this, f| write!(f, csi!("{}E"), this.0) +); + +sequence!( + /// Moves the cursor to beginning of line the given number of lines up. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorToPreviousLine; + /// + /// let mut stdout = stdout(); + /// // Move cursor up by 2 rows and the move it to the first column + /// write!(stdout, "{}", MoveCursorToPreviousLine(2)); + /// ``` + /// + /// The previous example does the same thing as the following one: + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::{MoveCursorUp, MoveCursorToColumn}; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}{}", MoveCursorUp(2), MoveCursorToColumn(1)); + /// ``` + struct MoveCursorToPreviousLine(u16) => + |this, f| write!(f, csi!("{}F"), this.0) +); + +sequence!( + /// Moves the cursor to the given column. + /// + /// # Notes + /// + /// Beginning of the line (left cell) is represented as `1`. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::MoveCursorToColumn; + /// + /// let mut stdout = stdout(); + /// // Move cursor to the 10th column (same row) + /// write!(stdout, "{}", MoveCursorToColumn(10)); + /// ``` + struct MoveCursorToColumn(u16) => + |this, f| write!(f, csi!("{}G"), this.0) +); + +// TODO Enhance example with Parser to show how to retrieve it +sequence!( + /// Asks for the current cursor position. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ReportCursorPosition; + /// + /// let mut stdout = stdout(); + /// write!(stdout, "{}", ReportCursorPosition); + /// ``` + struct ReportCursorPosition => csi!("6n") +); + +#[cfg(test)] +test_sequences!( + save_cursor_position( + SaveCursorPosition => "\x1B7", + ), + restore_cursor_position( + RestoreCursorPosition => "\x1B8", + ), + hide_cursor( + HideCursor => "\x1B[?25l", + ), + show_cursor( + ShowCursor => "\x1B[?25h", + ), + disable_cursor_blinking( + DisableCursorBlinking => "\x1B[?12l", + ), + enable_cursor_blinking( + EnableCursorBlinking => "\x1B[?12h", + ), + move_cursor_up( + MoveCursorUp(10) => "\x1B[10A", + ), + move_cursor_down( + MoveCursorDown(10) => "\x1B[10B", + ), + move_cursor_right( + MoveCursorRight(10) => "\x1B[10C", + ), + move_cursor_left( + MoveCursorLeft(10) => "\x1B[10D", + ), + move_cursor_to( + MoveCursorTo(5, 10) => "\x1B[10;5H", + ), + move_cursor_to_next_line( + MoveCursorToNextLine(5) => "\x1B[5E", + ), + move_cursor_to_previous_line( + MoveCursorToPreviousLine(5) => "\x1B[5F", + ), + move_cursor_to_column( + MoveCursorToColumn(1) => "\x1B[1G", + ), + report_cursor_position( + ReportCursorPosition => "\x1B[6n", + ) +); diff --git a/src/sequences/terminal.rs b/src/sequences/terminal.rs new file mode 100644 index 0000000..74eada6 --- /dev/null +++ b/src/sequences/terminal.rs @@ -0,0 +1,54 @@ +//! A terminal related ANSI escape sequences. + +sequence!( + /// Resizes the text area to the given width and height in characters. + /// + /// # Examples + /// + /// ```no_run + /// use std::io::{stdout, Write}; + /// use anes::ResizeTextArea; + /// + /// let mut stdout = stdout(); + /// // Resize the terminal to 80x25 + /// write!(stdout, "{}", ResizeTextArea(80, 25)); + /// ``` + struct ResizeTextArea(u16, u16) => + |this, f| write!(f, csi!("8;{};{}t"), this.1, this.0) +); + +sequence!( + /// Tells the terminal to start reporting mouse events. + /// + /// Mouse events are not reported by default. + struct EnableMouseEvents => concat!( + csi!("?1000h"), + csi!("?1002h"), + csi!("?1015h"), + csi!("?1006h") + ) +); + +sequence!( + /// Tells the terminal to stop reporting mouse events. + struct DisableMouseEvents => concat!( + csi!("?1006l"), + csi!("?1015l"), + csi!("?1002l"), + csi!("?1000l") + ) +); + +#[cfg(test)] +test_sequences!( + resize_text_area( + ResizeTextArea(80, 25) => "\x1B[8;25;80t", + ResizeTextArea(1, 1) => "\x1B[8;1;1t", + ), + enable_mouse_events( + EnableMouseEvents => "\x1B[?1000h\x1B[?1002h\x1B[?1015h\x1B[?1006h", + ), + disable_mouse_events( + DisableMouseEvents => "\x1B[?1006l\x1B[?1015l\x1B[?1002l\x1B[?1000l", + ) +); diff --git a/tests/parser/cursor.rs b/tests/parser/cursor.rs new file mode 100644 index 0000000..250ee48 --- /dev/null +++ b/tests/parser/cursor.rs @@ -0,0 +1,8 @@ +use anes::parser::Sequence; + +use crate::test_sequences; + +#[test] +fn position() { + test_sequences!(b"\x1B[20;10R", Sequence::CursorPosition(10, 20),); +} diff --git a/tests/parser/key.rs b/tests/parser/key.rs new file mode 100644 index 0000000..cd87c9d --- /dev/null +++ b/tests/parser/key.rs @@ -0,0 +1,158 @@ +use anes::parser::{KeyCode, KeyModifiers, Sequence}; + +use crate::test_sequences; + +#[test] +fn esc_o_f_keys() { + test_sequences!( + b"\x1BOP", + Sequence::Key(KeyCode::F(1), KeyModifiers::empty()), + b"\x1BOQ", + Sequence::Key(KeyCode::F(2), KeyModifiers::empty()), + b"\x1BOR", + Sequence::Key(KeyCode::F(3), KeyModifiers::empty()), + b"\x1BOS", + Sequence::Key(KeyCode::F(4), KeyModifiers::empty()), + ); +} + +#[test] +fn csi_key_codes() { + test_sequences!( + b"\x1B[A", + Sequence::Key(KeyCode::Up, KeyModifiers::empty()), + b"\x1B[B", + Sequence::Key(KeyCode::Down, KeyModifiers::empty()), + b"\x1B[C", + Sequence::Key(KeyCode::Right, KeyModifiers::empty()), + b"\x1B[D", + Sequence::Key(KeyCode::Left, KeyModifiers::empty()), + b"\x1B[H", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[F", + Sequence::Key(KeyCode::End, KeyModifiers::empty()), + b"\x1B[Z", + Sequence::Key(KeyCode::BackTab, KeyModifiers::empty()), + ); +} + +#[test] +fn csi_arrow_key_modifiers() { + test_sequences!( + b"\x1B[50A", + Sequence::Key(KeyCode::Up, KeyModifiers::SHIFT), + b"\x1B[53A", + Sequence::Key(KeyCode::Up, KeyModifiers::CONTROL), + ); +} + +#[test] +fn csi_tilde_key_modifiers() { + test_sequences!( + b"\x1B[1~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[1;0~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[1;1~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[1;2~", + Sequence::Key(KeyCode::Home, KeyModifiers::SHIFT), + b"\x1B[1;3~", + Sequence::Key(KeyCode::Home, KeyModifiers::ALT), + b"\x1B[1;4~", + Sequence::Key(KeyCode::Home, KeyModifiers::SHIFT | KeyModifiers::ALT), + b"\x1B[1;5~", + Sequence::Key(KeyCode::Home, KeyModifiers::CONTROL), + b"\x1B[1;6~", + Sequence::Key(KeyCode::Home, KeyModifiers::SHIFT | KeyModifiers::CONTROL), + b"\x1B[1;7~", + Sequence::Key(KeyCode::Home, KeyModifiers::ALT | KeyModifiers::CONTROL), + b"\x1B[1;8~", + Sequence::Key( + KeyCode::Home, + KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + ), + b"\x1B[1;9~", + Sequence::Key(KeyCode::Home, KeyModifiers::META), + b"\x1B[1;10~", + Sequence::Key(KeyCode::Home, KeyModifiers::META | KeyModifiers::SHIFT), + b"\x1B[1;11~", + Sequence::Key(KeyCode::Home, KeyModifiers::META | KeyModifiers::ALT), + b"\x1B[1;12~", + Sequence::Key( + KeyCode::Home, + KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::ALT + ), + b"\x1B[1;13~", + Sequence::Key(KeyCode::Home, KeyModifiers::META | KeyModifiers::CONTROL), + b"\x1B[1;14~", + Sequence::Key( + KeyCode::Home, + KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::CONTROL + ), + b"\x1B[1;15~", + Sequence::Key( + KeyCode::Home, + KeyModifiers::META | KeyModifiers::ALT | KeyModifiers::CONTROL + ), + b"\x1B[1;16~", + Sequence::Key( + KeyCode::Home, + KeyModifiers::META | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + ), + b"\x1B[1;17~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + ); +} + +#[test] +fn csi_tilde_f_keys() { + test_sequences!( + b"\x1B[11~", + Sequence::Key(KeyCode::F(1), KeyModifiers::empty()), + b"\x1B[12~", + Sequence::Key(KeyCode::F(2), KeyModifiers::empty()), + b"\x1B[13~", + Sequence::Key(KeyCode::F(3), KeyModifiers::empty()), + b"\x1B[14~", + Sequence::Key(KeyCode::F(4), KeyModifiers::empty()), + b"\x1B[15~", + Sequence::Key(KeyCode::F(5), KeyModifiers::empty()), + b"\x1B[17~", + Sequence::Key(KeyCode::F(6), KeyModifiers::empty()), + b"\x1B[18~", + Sequence::Key(KeyCode::F(7), KeyModifiers::empty()), + b"\x1B[19~", + Sequence::Key(KeyCode::F(8), KeyModifiers::empty()), + b"\x1B[20~", + Sequence::Key(KeyCode::F(9), KeyModifiers::empty()), + b"\x1B[21~", + Sequence::Key(KeyCode::F(10), KeyModifiers::empty()), + b"\x1B[23~", + Sequence::Key(KeyCode::F(11), KeyModifiers::empty()), + b"\x1B[24~", + Sequence::Key(KeyCode::F(12), KeyModifiers::empty()), + ); +} + +#[test] +fn csi_tilde_key_codes() { + test_sequences!( + b"\x1B[1~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[2~", + Sequence::Key(KeyCode::Insert, KeyModifiers::empty()), + b"\x1B[3~", + Sequence::Key(KeyCode::Delete, KeyModifiers::empty()), + b"\x1B[4~", + Sequence::Key(KeyCode::End, KeyModifiers::empty()), + b"\x1B[5~", + Sequence::Key(KeyCode::PageUp, KeyModifiers::empty()), + b"\x1B[6~", + Sequence::Key(KeyCode::PageDown, KeyModifiers::empty()), + b"\x1B[7~", + Sequence::Key(KeyCode::Home, KeyModifiers::empty()), + b"\x1B[8~", + Sequence::Key(KeyCode::End, KeyModifiers::empty()), + ); +} diff --git a/tests/parser/mod.rs b/tests/parser/mod.rs new file mode 100644 index 0000000..ecddcdd --- /dev/null +++ b/tests/parser/mod.rs @@ -0,0 +1,25 @@ +#[macro_export] +macro_rules! test_sequence { + ($bytes:expr, $seq:expr) => { + let mut parser = ::anes::parser::Parser::default(); + parser.advance($bytes, false); + assert_eq!(parser.next(), Some($seq)); + }; +} + +#[macro_export] +macro_rules! test_sequences { + ( + $( + $bytes:expr, $seq:expr, + )* + ) => { + $( + test_sequence!($bytes, $seq); + )* + }; +} + +mod cursor; +mod key; +mod mouse; diff --git a/tests/parser/mouse/mod.rs b/tests/parser/mouse/mod.rs new file mode 100644 index 0000000..dc53361 --- /dev/null +++ b/tests/parser/mouse/mod.rs @@ -0,0 +1,2 @@ +mod rxvt; +mod xterm; diff --git a/tests/parser/mouse/rxvt.rs b/tests/parser/mouse/rxvt.rs new file mode 100644 index 0000000..47f43a5 --- /dev/null +++ b/tests/parser/mouse/rxvt.rs @@ -0,0 +1,152 @@ +use anes::parser::{KeyModifiers, Mouse, MouseButton, Sequence}; + +use crate::test_sequences; + +#[test] +fn button_down() { + test_sequences!( + b"\x1B[0;30;40;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[1;30;40;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Middle, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[2;30;40;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Right, 30, 40), + KeyModifiers::empty() + ), + ); +} + +#[test] +fn button_down_with_modifiers() { + test_sequences!( + b"\x1B[4;30;40;M", + Sequence::Mouse(Mouse::Down(MouseButton::Left, 30, 40), KeyModifiers::SHIFT), + b"\x1B[5;30;40;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Middle, 30, 40), + KeyModifiers::SHIFT + ), + b"\x1B[6;30;40;M", + Sequence::Mouse(Mouse::Down(MouseButton::Right, 30, 40), KeyModifiers::SHIFT), + ); +} + +#[test] +fn button_up() { + test_sequences!( + b"\x1B[3;30;40;M", + Sequence::Mouse(Mouse::Up(MouseButton::Any, 30, 40), KeyModifiers::empty()), + ); +} + +#[test] +fn button_up_with_modifiers() { + test_sequences!( + b"\x1B[7;30;40;M", + Sequence::Mouse(Mouse::Up(MouseButton::Any, 30, 40), KeyModifiers::SHIFT), + ); +} + +#[test] +fn scroll() { + test_sequences!( + b"\x1B[96;30;40;M", + Sequence::Mouse(Mouse::ScrollUp(30, 40), KeyModifiers::empty()), + b"\x1B[97;30;40;M", + Sequence::Mouse(Mouse::ScrollDown(30, 40), KeyModifiers::empty()), + ); +} + +#[test] +fn scroll_with_modifiers() { + test_sequences!( + b"\x1B[100;30;40;M", + Sequence::Mouse(Mouse::ScrollUp(30, 40), KeyModifiers::SHIFT), + b"\x1B[101;30;40;M", + Sequence::Mouse(Mouse::ScrollDown(30, 40), KeyModifiers::SHIFT), + ); +} + +#[test] +fn drag() { + test_sequences!( + b"\x1B[64;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Left, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[65;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Middle, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[66;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Right, 30, 40), + KeyModifiers::empty() + ), + ); +} + +#[test] +fn drag_with_modifiers() { + test_sequences!( + b"\x1B[64;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Left, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[65;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Middle, 30, 40), + KeyModifiers::empty() + ), + b"\x1B[66;30;40;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Right, 30, 40), + KeyModifiers::empty() + ), + ); +} + +#[test] +fn key_modifier_combinations() { + test_sequences!( + b"\x1B[4;20;10M", + Sequence::Mouse(Mouse::Down(MouseButton::Left, 20, 10), KeyModifiers::SHIFT), + b"\x1B[8;20;10M", + Sequence::Mouse(Mouse::Down(MouseButton::Left, 20, 10), KeyModifiers::ALT), + b"\x1B[16;20;10M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::CONTROL + ), + b"\x1B[12;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::ALT + ), + b"\x1B[20;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::CONTROL + ), + b"\x1B[24;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::ALT | KeyModifiers::CONTROL + ), + b"\x1B[28;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + ), + ); +} diff --git a/tests/parser/mouse/xterm.rs b/tests/parser/mouse/xterm.rs new file mode 100644 index 0000000..47293d6 --- /dev/null +++ b/tests/parser/mouse/xterm.rs @@ -0,0 +1,154 @@ +use anes::parser::{KeyModifiers, Mouse, MouseButton, Sequence}; + +use crate::test_sequences; + +#[test] +fn button_down() { + test_sequences!( + b"\x1B[<0;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Left, 20, 10), + KeyModifiers::empty() + ), + b"\x1B[<1;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Middle, 20, 10), + KeyModifiers::empty() + ), + b"\x1B[<2;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Right, 20, 10), + KeyModifiers::empty() + ), + ); +} + +#[test] +fn button_down_with_key_modifiers() { + test_sequences!( + b"\x1B[<4;20;10;M", + Sequence::Mouse(Mouse::Down(MouseButton::Left, 20, 10), KeyModifiers::SHIFT), + b"\x1B[<5;20;10;M", + Sequence::Mouse( + Mouse::Down(MouseButton::Middle, 20, 10), + KeyModifiers::SHIFT + ), + b"\x1B[<6;20;10;M", + Sequence::Mouse(Mouse::Down(MouseButton::Right, 20, 10), KeyModifiers::SHIFT), + ); +} + +#[test] +fn button_up() { + test_sequences!( + b"\x1B[<0;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Left, 20, 10), KeyModifiers::empty()), + b"\x1B[<1;20;10;m", + Sequence::Mouse( + Mouse::Up(MouseButton::Middle, 20, 10), + KeyModifiers::empty() + ), + b"\x1B[<2;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Right, 20, 10), KeyModifiers::empty()), + ); +} + +#[test] +fn button_up_with_key_modifiers() { + test_sequences!( + b"\x1B[<4;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Left, 20, 10), KeyModifiers::SHIFT), + b"\x1B[<5;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Middle, 20, 10), KeyModifiers::SHIFT), + b"\x1B[<6;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Right, 20, 10), KeyModifiers::SHIFT), + ); +} + +#[test] +fn scroll() { + test_sequences!( + b"\x1B[<64;20;10;m", + Sequence::Mouse(Mouse::ScrollUp(20, 10), KeyModifiers::empty()), + b"\x1B[<65;20;10;m", + Sequence::Mouse(Mouse::ScrollDown(20, 10), KeyModifiers::empty()), + ); +} + +#[test] +fn scroll_with_key_modifiers() { + test_sequences!( + b"\x1B[<68;20;10;m", + Sequence::Mouse(Mouse::ScrollUp(20, 10), KeyModifiers::SHIFT), + b"\x1B[<69;20;10;m", + Sequence::Mouse(Mouse::ScrollDown(20, 10), KeyModifiers::SHIFT), + ); +} + +#[test] +fn drag() { + test_sequences!( + b"\x1B[<32;20;10;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Left, 20, 10), + KeyModifiers::empty() + ), + b"\x1B[<33;20;10;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Middle, 20, 10), + KeyModifiers::empty() + ), + b"\x1B[<34;20;10;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Right, 20, 10), + KeyModifiers::empty() + ), + ); +} + +#[test] +fn drag_with_key_modifiers() { + test_sequences!( + b"\x1B[<36;20;10;M", + Sequence::Mouse(Mouse::Drag(MouseButton::Left, 20, 10), KeyModifiers::SHIFT), + b"\x1B[<37;20;10;M", + Sequence::Mouse( + Mouse::Drag(MouseButton::Middle, 20, 10), + KeyModifiers::SHIFT, + ), + b"\x1B[<38;20;10;M", + Sequence::Mouse(Mouse::Drag(MouseButton::Right, 20, 10), KeyModifiers::SHIFT), + ); +} + +#[test] +fn key_modifier_combinations() { + test_sequences!( + b"\x1B[<4;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Left, 20, 10), KeyModifiers::SHIFT), + b"\x1B[<8;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Left, 20, 10), KeyModifiers::ALT), + b"\x1B[<16;20;10;m", + Sequence::Mouse(Mouse::Up(MouseButton::Left, 20, 10), KeyModifiers::CONTROL), + b"\x1B[<12;20;10;m", + Sequence::Mouse( + Mouse::Up(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::ALT + ), + b"\x1B[<20;20;10;m", + Sequence::Mouse( + Mouse::Up(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::CONTROL + ), + b"\x1B[<24;20;10;m", + Sequence::Mouse( + Mouse::Up(MouseButton::Left, 20, 10), + KeyModifiers::ALT | KeyModifiers::CONTROL + ), + b"\x1B[<28;20;10;m", + Sequence::Mouse( + Mouse::Up(MouseButton::Left, 20, 10), + KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + ), + ); +} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..f60dbff --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "parser")] +mod parser; |