├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── app.rs ├── csi.rs ├── event.rs ├── filewatch ├── linux.rs ├── macos.rs └── mod.rs ├── keybind.rs ├── lib.rs ├── logger.rs ├── main.rs ├── pane.rs ├── search.rs ├── tab.rs ├── term.rs └── unicode_divide.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macOS-latest] 13 | rust: [stable, nightly] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - uses: hecrj/setup-rust-action@v1 19 | with: 20 | rust-version: ${{ matrix.rust }} 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: cargo build --verbose --all 24 | - name: Run tests 25 | run: cargo test --verbose --all -- --test-threads=1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "ctrlc" 34 | version = "3.2.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" 37 | dependencies = [ 38 | "nix", 39 | "winapi", 40 | ] 41 | 42 | [[package]] 43 | name = "futures-core" 44 | version = "0.3.21" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 47 | 48 | [[package]] 49 | name = "getopts" 50 | version = "0.2.21" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 53 | dependencies = [ 54 | "unicode-width", 55 | ] 56 | 57 | [[package]] 58 | name = "inotify" 59 | version = "0.10.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" 62 | dependencies = [ 63 | "bitflags", 64 | "futures-core", 65 | "inotify-sys", 66 | "libc", 67 | "tokio", 68 | ] 69 | 70 | [[package]] 71 | name = "inotify-sys" 72 | version = "0.1.5" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 75 | dependencies = [ 76 | "libc", 77 | ] 78 | 79 | [[package]] 80 | name = "libc" 81 | version = "0.2.126" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 84 | 85 | [[package]] 86 | name = "log" 87 | version = "0.4.17" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 90 | dependencies = [ 91 | "cfg-if", 92 | ] 93 | 94 | [[package]] 95 | name = "memchr" 96 | version = "2.5.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 99 | 100 | [[package]] 101 | name = "memoffset" 102 | version = "0.6.5" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 105 | dependencies = [ 106 | "autocfg", 107 | ] 108 | 109 | [[package]] 110 | name = "mio" 111 | version = "0.8.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" 114 | dependencies = [ 115 | "libc", 116 | "log", 117 | "wasi", 118 | "windows-sys", 119 | ] 120 | 121 | [[package]] 122 | name = "nix" 123 | version = "0.24.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" 126 | dependencies = [ 127 | "bitflags", 128 | "cfg-if", 129 | "libc", 130 | "memoffset", 131 | ] 132 | 133 | [[package]] 134 | name = "numtoa" 135 | version = "0.1.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 138 | 139 | [[package]] 140 | name = "peep" 141 | version = "0.1.6" 142 | dependencies = [ 143 | "ctrlc", 144 | "getopts", 145 | "inotify", 146 | "libc", 147 | "mio", 148 | "nix", 149 | "regex", 150 | "termion", 151 | "termios", 152 | "unicode-width", 153 | ] 154 | 155 | [[package]] 156 | name = "pin-project-lite" 157 | version = "0.2.9" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 160 | 161 | [[package]] 162 | name = "redox_syscall" 163 | version = "0.2.13" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 166 | dependencies = [ 167 | "bitflags", 168 | ] 169 | 170 | [[package]] 171 | name = "redox_termios" 172 | version = "0.1.2" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" 175 | dependencies = [ 176 | "redox_syscall", 177 | ] 178 | 179 | [[package]] 180 | name = "regex" 181 | version = "1.5.6" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 184 | dependencies = [ 185 | "aho-corasick", 186 | "memchr", 187 | "regex-syntax", 188 | ] 189 | 190 | [[package]] 191 | name = "regex-syntax" 192 | version = "0.6.26" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 195 | 196 | [[package]] 197 | name = "socket2" 198 | version = "0.4.4" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 201 | dependencies = [ 202 | "libc", 203 | "winapi", 204 | ] 205 | 206 | [[package]] 207 | name = "termion" 208 | version = "1.5.6" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" 211 | dependencies = [ 212 | "libc", 213 | "numtoa", 214 | "redox_syscall", 215 | "redox_termios", 216 | ] 217 | 218 | [[package]] 219 | name = "termios" 220 | version = "0.3.3" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" 223 | dependencies = [ 224 | "libc", 225 | ] 226 | 227 | [[package]] 228 | name = "tokio" 229 | version = "1.19.2" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 232 | dependencies = [ 233 | "libc", 234 | "mio", 235 | "pin-project-lite", 236 | "socket2", 237 | "winapi", 238 | ] 239 | 240 | [[package]] 241 | name = "unicode-width" 242 | version = "0.1.9" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 245 | 246 | [[package]] 247 | name = "wasi" 248 | version = "0.11.0+wasi-snapshot-preview1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 251 | 252 | [[package]] 253 | name = "winapi" 254 | version = "0.3.9" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 257 | dependencies = [ 258 | "winapi-i686-pc-windows-gnu", 259 | "winapi-x86_64-pc-windows-gnu", 260 | ] 261 | 262 | [[package]] 263 | name = "winapi-i686-pc-windows-gnu" 264 | version = "0.4.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 267 | 268 | [[package]] 269 | name = "winapi-x86_64-pc-windows-gnu" 270 | version = "0.4.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 273 | 274 | [[package]] 275 | name = "windows-sys" 276 | version = "0.36.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 279 | dependencies = [ 280 | "windows_aarch64_msvc", 281 | "windows_i686_gnu", 282 | "windows_i686_msvc", 283 | "windows_x86_64_gnu", 284 | "windows_x86_64_msvc", 285 | ] 286 | 287 | [[package]] 288 | name = "windows_aarch64_msvc" 289 | version = "0.36.1" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 292 | 293 | [[package]] 294 | name = "windows_i686_gnu" 295 | version = "0.36.1" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 298 | 299 | [[package]] 300 | name = "windows_i686_msvc" 301 | version = "0.36.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 304 | 305 | [[package]] 306 | name = "windows_x86_64_gnu" 307 | version = "0.36.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 310 | 311 | [[package]] 312 | name = "windows_x86_64_msvc" 313 | version = "0.36.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 316 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "peep" 3 | edition = "2018" 4 | version = "0.1.6" 5 | authors = ["ryochack "] 6 | description = "The CLI text viewer tool that works like `less` command on small pane within the terminal window." 7 | license = "MIT" 8 | repository = "https://github.com/ryochack/peep" 9 | readme = "README.md" 10 | categories = ["command-line-utilities"] 11 | keywords = [ 12 | "pager", 13 | "less", 14 | "more", 15 | ] 16 | 17 | [dependencies] 18 | getopts = "0.2" 19 | termios = "0.3" 20 | termion = "1.5" 21 | ctrlc = "3.2" 22 | regex = "1.5" 23 | libc = "0.2" 24 | nix = "0.24" 25 | mio = "0.8" 26 | inotify = "0.10" 27 | unicode-width = "0.1" 28 | 29 | [profile.release] 30 | strip = true 31 | lto = true 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ryochack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/peep.svg)](https://crates.io/crates/peep) 2 | ![test](https://github.com/ryochack/peep/workflows/test/badge.svg) 3 | 4 | # peep 5 | peep is the CLI text viewer tool. 6 | This tool works interactively like `less` command on small pane within the terminal window. 7 | And leave the output on the terminal when quit like `cat` command. 8 | 9 | # Demos 10 | ## Pane on Terminal Window 11 | peep can view text file freely. 12 | 13 | ![Pane on Terminal Window](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo.gif) 14 | ## Read from Pipe 15 | ![Pipe Input](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_pipe.gif) 16 | ## Print Line Number 17 | ![Print Line Number](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_linenumber.gif) 18 | ## Resize Pane 19 | ![Resize Pane](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_resize.gif) 20 | ## Incremental Regex Search 21 | ![Incremental Regex Search](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_incsearch.gif) 22 | ## Wide Width Character Support 23 | ![Wide Width Character Support](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_wide_width_chars.gif) 24 | ## Follow Mode 25 | peep has the follow mode that can monitor file updates and read them continuously like `tail -f` or `less +F`. 26 | Also, peep can switch between the normal mode and follow mode with `F` command. 27 | 28 | ![Follow Mode](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_follow.gif) 29 | ## Highlighting on Follow Mode 30 | peep can highlight the regex word on the follow mode. 31 | 32 | ![Highlighting on Follow Mode](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_follow_hl.gif) 33 | ## Text Line Wrapping 34 | 35 | ![Text Line Wrapping](https://raw.githubusercontent.com/wiki/ryochack/peep/images/demo_wrapping.gif) 36 | 37 | # Installation 38 | ```shell 39 | cargo install peep 40 | ``` 41 | 42 | If you don't have Rust toolchains, please refer to [The Rust Programming Language](https://www.rust-lang.org/). 43 | 44 | Or, you can download peep binary file from [GitHub peep Releases](https://github.com/ryochack/peep/releases) :) 45 | 46 | # Usage 47 | ```shell 48 | peep [OPTION]... [FILE] 49 | ``` 50 | 51 | ## Options 52 | ``` 53 | -n, --lines LINES set height of pane 54 | -s, --start START set start line of data at startup 55 | -t, --tab-width WIDTH set tab width 56 | -N, --print-line-number print line numbers 57 | -f, --follow output appended data as the file grows 58 | -h, --help show this usage 59 | -v, --version show version 60 | ``` 61 | 62 | ## Commands 63 | **Format** 64 | 65 | ``` 66 | KEY-BIND OPERATION 67 | ``` 68 | 69 | **Example 1** 70 | 71 | ``` 72 | 0 Ctr-a Go to the beggining of line 73 | ``` 74 | Type `0` OR `Ctrl-a`, then `Go to the beggining of line`. 75 | 76 | **Example 2** 77 | 78 | ``` 79 | (num)+ Increment screen height 80 | ``` 81 | `(num)` means that entering a number is optional. 82 | If you omit the number input, the number will be processed as 1. 83 | 84 | **Example 3** 85 | 86 | ``` 87 | [num]= Set screen height to [num] 88 | ``` 89 | `[num]` means that entering a number is mandatory. 90 | 91 | 92 | ### Commands on Normal Mode 93 | ``` 94 | (num)j Ctr-j Ctr-n Scroll down 95 | (num)k Ctr-k Ctr-p Scroll up 96 | (num)d Ctr-d Scroll down half page 97 | (num)u Ctr-u Scroll up half page 98 | (num)f Ctr-f SPACE Scroll down a page 99 | (num)b Ctr-b Scroll up a page 100 | (num)l Scroll horizontally right 101 | (num)h Scroll horizontally left 102 | (num)L Scroll horizontally right half page 103 | (num)H Scroll horizontally left half page 104 | 0 Ctr-a Go to the beggining of line 105 | $ Ctr-e Go to the end of line 106 | g Go to the beggining of file 107 | G Go to the end of file 108 | [num]g [num]G Go to line [num] 109 | /pattern Search forward in the file for the regex pattern 110 | n Search next 111 | N Search previous 112 | q Ctr-c Quit 113 | Q Quit with clearing pane 114 | (num)+ Increment screen height 115 | (num)- Decrement screen height 116 | [num]= Set screen height to [num] 117 | # Toggle line number printing 118 | ! Toggle line wrapping 119 | ESC Cancel 120 | F Toggle to follow mode 121 | ``` 122 | 123 | ### Commands on Follow Mode 124 | ``` 125 | /pattern Highlight the regex pattern 126 | q Ctr-c Quit 127 | (num)+ Increment screen height 128 | (num)- Decrement screen height 129 | [num]= Set screen height to [num] 130 | # Toggle line number printing 131 | ! Toggle line wrapping 132 | ESC Cancel 133 | F Toggle to normal mode 134 | ``` 135 | 136 | # Supported Platforms 137 | - Linux 138 | - MacOS 139 | 140 | # License 141 | MIT License. 142 | Please refer to LICENSE file. 143 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::fs::File; 3 | use std::io::{self, BufRead, BufReader, Cursor, Read, Seek, SeekFrom}; 4 | use std::os::unix::io::AsRawFd; 5 | use std::rc::Rc; 6 | use std::sync::mpsc; 7 | use std::thread::spawn; 8 | 9 | use crate::{ 10 | event::PeepEvent, 11 | filewatch::{self, FileWatch}, 12 | keybind, 13 | pane::{Pane, ScrollStep}, 14 | search, 15 | term::{self, Block}, 16 | }; 17 | 18 | const DEFAULT_PANE_HEIGHT: u16 = 10; 19 | const DEFAULT_TAB_WIDTH: u16 = 4; 20 | 21 | const FOLLOWING_MESSAGE: &str = "\x1b[7mwaiting for data... (press 'F' to abort)\x1b[0m"; 22 | const FOLLOWING_HL_MESSAGE: &str = "\x1b[7mwaiting for data... \x1b[0m:"; 23 | const DEFAULT_POLL_TIMEOUT_MS: u64 = 200; 24 | 25 | pub struct KeyEventHandler<'a> { 26 | istream: &'a mut dyn Read, 27 | parser: &'a mut dyn keybind::KeyParser, 28 | } 29 | 30 | impl<'a> KeyEventHandler<'a> { 31 | pub fn new(istream: &'a mut dyn Read, parser: &'a mut dyn keybind::KeyParser) -> Self { 32 | KeyEventHandler { istream, parser } 33 | } 34 | 35 | pub fn read(&mut self) -> Option { 36 | for b in self.istream.bytes().filter_map(|v| v.ok()) { 37 | let v = self.parser.parse(b as char); 38 | if v.is_some() { 39 | return v; 40 | } 41 | } 42 | None 43 | } 44 | } 45 | 46 | struct PipeReader { 47 | end_with_crlf: bool, 48 | } 49 | 50 | impl Default for PipeReader { 51 | fn default() -> Self { 52 | Self::new() 53 | } 54 | } 55 | 56 | impl PipeReader { 57 | pub fn new() -> Self { 58 | Self { 59 | end_with_crlf: true, 60 | } 61 | } 62 | 63 | /// chomp end of CRFL. Return whether it was chomped or not. 64 | pub fn chomp(s: &mut String) -> bool { 65 | if s.ends_with('\n') { 66 | s.pop(); 67 | if s.ends_with('\r') { 68 | s.pop(); 69 | } 70 | true 71 | } else { 72 | false 73 | } 74 | } 75 | 76 | /// Read from pipe input. 77 | fn read(&mut self, linebuf: &mut Vec, timeout_ms: u64) -> io::Result<()> { 78 | use std::time::Duration; 79 | 80 | const INBUF_SIZE: usize = 8192; 81 | 82 | let mut tmo = timeout_ms; 83 | let stdin = io::stdin(); 84 | 85 | let mut stdinwatcher = filewatch::StdinWatcher::new(stdin.as_raw_fd())?; 86 | let mut buf = [0u8; INBUF_SIZE]; 87 | 88 | stdin.nonblocking(); 89 | let mut stdinlock = stdin.lock(); 90 | 91 | loop { 92 | let ready = stdinwatcher.watch(Some(Duration::from_millis(tmo)))?; 93 | if ready.is_none() { 94 | // time out 95 | break; 96 | } 97 | tmo = DEFAULT_POLL_TIMEOUT_MS; 98 | while let Ok(cap) = stdinlock.read(&mut buf) { 99 | if cap == 0 { 100 | break; 101 | } 102 | 103 | let mut cursor = Cursor::new(&buf[..cap]); 104 | loop { 105 | let mut line = String::new(); 106 | if let Ok(n) = cursor.read_line(&mut line) { 107 | if n == 0 { 108 | break; 109 | } 110 | let is_chmoped = PipeReader::chomp(&mut line); 111 | 112 | if !self.end_with_crlf && linebuf.last_mut().is_some() { 113 | linebuf.last_mut().unwrap().push_str(&line); 114 | } else { 115 | linebuf.push(line); 116 | } 117 | self.end_with_crlf = is_chmoped; 118 | } else { 119 | break; 120 | } 121 | } 122 | } 123 | if ready.unwrap() { 124 | // is_hup()? 125 | break; 126 | } 127 | } 128 | 129 | stdin.blocking(); 130 | Ok(()) 131 | } 132 | } 133 | 134 | pub struct App { 135 | pub show_linenumber: bool, 136 | pub nlines: u16, 137 | pub start_line: u16, 138 | pub follow_mode: bool, 139 | pub tab_width: u16, 140 | pub wraps_line: bool, 141 | typing_word: Option, 142 | file_path: String, 143 | seek_pos: u64, 144 | searcher: Rc>, 145 | linebuf: Rc>>, 146 | pipereader: PipeReader, 147 | // termios parameter moved from KeyEventHandler to App to detect Drop App. 148 | term_restorer: Option, 149 | } 150 | 151 | impl Drop for App { 152 | fn drop(&mut self) { 153 | if let Some(ref tr) = self.term_restorer { 154 | // Prepare key input setting 155 | let ftty = File::open("/dev/tty").unwrap(); 156 | tr.restore(ftty.as_raw_fd()); 157 | } 158 | } 159 | } 160 | 161 | impl Default for App { 162 | fn default() -> Self { 163 | Self::new() 164 | } 165 | } 166 | 167 | impl App { 168 | pub fn new() -> Self { 169 | // Prepare key input setting 170 | let ftty = File::open("/dev/tty").unwrap(); 171 | let term_restorer = term::TermAttrSetter::new(ftty.as_raw_fd()) 172 | .lflag(0, term::ICANON | term::ECHO) 173 | .set(); 174 | 175 | App { 176 | show_linenumber: false, 177 | nlines: DEFAULT_PANE_HEIGHT, 178 | start_line: 0, 179 | follow_mode: false, 180 | tab_width: DEFAULT_TAB_WIDTH, 181 | wraps_line: false, 182 | typing_word: None, 183 | file_path: String::new(), 184 | seek_pos: 0, 185 | searcher: Rc::new(RefCell::new(search::PlaneSearcher::new())), 186 | linebuf: Rc::new(RefCell::new(Vec::new())), 187 | pipereader: Default::default(), 188 | term_restorer: Some(term_restorer), 189 | } 190 | } 191 | 192 | fn read_buffer(&mut self, tmo_ms: u64) -> io::Result<()> { 193 | if self.file_path == "-" { 194 | // read from stdin if pipe 195 | if termion::is_tty(&io::stdin()) { 196 | // stdin is tty. not pipe. 197 | return Err(io::Error::new( 198 | io::ErrorKind::NotFound, 199 | "no input from stdin", 200 | )); 201 | } 202 | self.pipereader 203 | .read(&mut self.linebuf.borrow_mut(), tmo_ms)?; 204 | } else if let Ok(mut file) = File::open(&self.file_path) { 205 | // read from file 206 | self.seek_pos = file.seek(SeekFrom::Start(self.seek_pos))?; 207 | let bufreader = BufReader::new(file); 208 | for line in bufreader.lines() { 209 | // +1 is LR length 210 | let v = line?; 211 | self.seek_pos += v.as_bytes().len() as u64 + 1; 212 | self.linebuf.borrow_mut().push(v); 213 | } 214 | } else { 215 | return Err(io::Error::new( 216 | io::ErrorKind::NotFound, 217 | format!("{} is not found", self.file_path), 218 | )); 219 | } 220 | Ok(()) 221 | } 222 | 223 | pub fn run(&mut self, path: &str) -> io::Result<()> { 224 | self.file_path = path.to_owned(); 225 | self.read_buffer(1000)?; 226 | 227 | let writer = io::stdout(); 228 | let writer = writer.lock(); 229 | 230 | let (event_sender, event_receiver) = mpsc::channel(); 231 | 232 | let sig_sender = event_sender.clone(); 233 | // Ctrl-C handler 234 | ctrlc::set_handler(move || { 235 | // receive SIGINT 236 | sig_sender.send(PeepEvent::Quit).unwrap(); 237 | }) 238 | .expect("Error setting ctrl-c handler"); 239 | 240 | self.searcher = Rc::new(RefCell::new(search::RegexSearcher::new(""))); 241 | 242 | let mut pane = Pane::new(Box::new(RefCell::new(writer))); 243 | pane.load(self.linebuf.clone()); 244 | pane.set_highlight_searcher(self.searcher.clone()); 245 | pane.show_line_number(self.show_linenumber); 246 | pane.set_tab_width(self.tab_width); 247 | pane.set_wrap(self.wraps_line); 248 | pane.set_height(self.nlines)?; 249 | if self.follow_mode { 250 | pane.goto_bottom_of_lines()?; 251 | } 252 | if self.start_line > 0 { 253 | pane.goto_absolute_line(self.start_line - 1)?; 254 | } 255 | pane.set_message(self.mode_default_message()); 256 | pane.refresh()?; 257 | 258 | // if stdout points pipe or redirect, 259 | // peep exits immediately upon output. 260 | if !pane.is_stdout_tty() { 261 | return Ok(()); 262 | } 263 | 264 | let key_sender = event_sender.clone(); 265 | // Key reading thread 266 | let _keythread = spawn(move || { 267 | let mut keyin = File::open("/dev/tty").unwrap(); 268 | let mut kb = keybind::default::KeyBind::new(); 269 | let mut keh = KeyEventHandler::new(&mut keyin, &mut kb); 270 | 271 | loop { 272 | if let Some(event) = keh.read() { 273 | key_sender.send(event.clone()).unwrap(); 274 | } 275 | } 276 | }); 277 | 278 | // spawn inotifier thread for following mode 279 | let file_path_to_watch = self.file_path.clone(); 280 | let _fwthread = spawn(move || filewatch::file_watcher(&file_path_to_watch, &event_sender)); 281 | 282 | // app loop 283 | loop { 284 | if let Ok(event) = event_receiver.recv() { 285 | if !self.follow_mode { 286 | self.handle_normal(&event, &mut pane)?; 287 | } else { 288 | self.handle_follow(&event, &mut pane)?; 289 | } 290 | 291 | match event { 292 | PeepEvent::Quit | PeepEvent::QuitWithClear => { 293 | break; 294 | } 295 | _ => {} 296 | } 297 | } 298 | } 299 | Ok(()) 300 | } 301 | 302 | fn mode_default_message(&self) -> Option { 303 | if !self.follow_mode { 304 | // normal mode 305 | None 306 | } else if let Some(ref tw) = self.typing_word { 307 | // follow mode + highlighting 308 | Some(format!("{}/{}", FOLLOWING_HL_MESSAGE, tw)) 309 | } else { 310 | // follow mode 311 | Some(FOLLOWING_MESSAGE.to_owned()) 312 | } 313 | } 314 | 315 | fn handle_normal(&mut self, event: &PeepEvent, pane: &mut Pane) -> io::Result<()> { 316 | match event { 317 | &PeepEvent::MoveDown(n) => { 318 | pane.scroll_down(&ScrollStep::Char(n))?; 319 | pane.refresh()?; 320 | } 321 | &PeepEvent::MoveUp(n) => { 322 | pane.scroll_up(&ScrollStep::Char(n))?; 323 | pane.refresh()?; 324 | } 325 | &PeepEvent::MoveLeft(n) => { 326 | pane.scroll_left(&ScrollStep::Char(n))?; 327 | pane.refresh()?; 328 | } 329 | &PeepEvent::MoveRight(n) => { 330 | pane.scroll_right(&ScrollStep::Char(n))?; 331 | pane.refresh()?; 332 | } 333 | &PeepEvent::MoveDownHalfPages(n) => { 334 | pane.scroll_down(&ScrollStep::HalfPage(n))?; 335 | pane.refresh()?; 336 | } 337 | &PeepEvent::MoveUpHalfPages(n) => { 338 | pane.scroll_up(&ScrollStep::HalfPage(n))?; 339 | pane.refresh()?; 340 | } 341 | &PeepEvent::MoveLeftHalfPages(n) => { 342 | pane.scroll_left(&ScrollStep::HalfPage(n))?; 343 | pane.refresh()?; 344 | } 345 | &PeepEvent::MoveRightHalfPages(n) => { 346 | pane.scroll_right(&ScrollStep::HalfPage(n))?; 347 | pane.refresh()?; 348 | } 349 | &PeepEvent::MoveDownPages(n) => { 350 | pane.scroll_down(&ScrollStep::Page(n))?; 351 | pane.refresh()?; 352 | } 353 | &PeepEvent::MoveUpPages(n) => { 354 | pane.scroll_up(&ScrollStep::Page(n))?; 355 | pane.refresh()?; 356 | } 357 | PeepEvent::MoveToHeadOfLine => { 358 | pane.goto_head_of_line()?; 359 | pane.refresh()?; 360 | } 361 | PeepEvent::MoveToEndOfLine => { 362 | pane.goto_tail_of_line()?; 363 | pane.refresh()?; 364 | } 365 | PeepEvent::MoveToTopOfLines => { 366 | pane.goto_top_of_lines()?; 367 | pane.refresh()?; 368 | } 369 | PeepEvent::MoveToBottomOfLines => { 370 | pane.goto_bottom_of_lines()?; 371 | pane.refresh()?; 372 | } 373 | &PeepEvent::MoveToLineNumber(n) => { 374 | pane.goto_absolute_line(n)?; 375 | pane.refresh()?; 376 | } 377 | PeepEvent::ToggleLineNumberPrinting => { 378 | self.show_linenumber = !self.show_linenumber; 379 | pane.show_line_number(self.show_linenumber); 380 | pane.refresh()?; 381 | } 382 | PeepEvent::ToggleLineWraps => { 383 | self.wraps_line = !self.wraps_line; 384 | pane.set_wrap(self.wraps_line); 385 | pane.refresh()?; 386 | } 387 | &PeepEvent::IncrementLines(n) => { 388 | pane.increment_height(n)?; 389 | pane.refresh()?; 390 | } 391 | &PeepEvent::DecrementLines(n) => { 392 | pane.decrement_height(n)?; 393 | pane.refresh()?; 394 | } 395 | &PeepEvent::SetNumOfLines(n) => { 396 | pane.set_height(n)?; 397 | pane.refresh()?; 398 | } 399 | PeepEvent::SearchIncremental(s) => { 400 | self.typing_word = Some(s.to_owned()); 401 | pane.set_message(Some(format!("/{}", s))); 402 | if s.is_empty() { 403 | let _ = self.searcher.borrow_mut().set_pattern(s); 404 | pane.show_highlight(false); 405 | } else { 406 | let _ = self.searcher.borrow_mut().set_pattern(s); 407 | if let Some(pos) = self.search(pane.position()) { 408 | pane.goto_absolute_line(pos.1)?; 409 | } 410 | pane.show_highlight(true); 411 | } 412 | pane.refresh()?; 413 | } 414 | PeepEvent::SearchTrigger => { 415 | self.typing_word = None; 416 | pane.set_message(self.mode_default_message()); 417 | pane.refresh()?; 418 | } 419 | PeepEvent::SearchNext => { 420 | let cur_pos = pane.position(); 421 | let next_pos = ( 422 | cur_pos.0, 423 | if cur_pos.1 == self.linebuf.borrow().len() as u16 - 1 { 424 | self.linebuf.borrow().len() as u16 - 1 425 | } else { 426 | cur_pos.1 + 1 427 | }, 428 | ); 429 | 430 | if self.searcher.borrow().as_str().is_empty() { 431 | pane.show_highlight(false); 432 | } else { 433 | if let Some(pos) = self.search(next_pos) { 434 | pane.goto_absolute_line(pos.1)?; 435 | } 436 | pane.show_highlight(true); 437 | } 438 | pane.set_message(self.mode_default_message()); 439 | pane.refresh()?; 440 | } 441 | PeepEvent::SearchPrev => { 442 | let cur_pos = pane.position(); 443 | let next_pos = (cur_pos.0, if cur_pos.1 == 0 { 0 } else { cur_pos.1 - 1 }); 444 | 445 | if self.searcher.borrow().as_str().is_empty() { 446 | pane.show_highlight(false); 447 | } else { 448 | if let Some(pos) = self.search_rev(next_pos) { 449 | pane.goto_absolute_line(pos.1)?; 450 | } 451 | pane.show_highlight(true); 452 | } 453 | pane.set_message(self.mode_default_message()); 454 | pane.refresh()?; 455 | } 456 | PeepEvent::Message(s) => { 457 | pane.set_message(s.to_owned()); 458 | pane.refresh()?; 459 | } 460 | PeepEvent::Cancel => { 461 | self.typing_word = None; 462 | pane.set_message(self.mode_default_message()); 463 | pane.show_highlight(false); 464 | pane.refresh()?; 465 | } 466 | PeepEvent::FollowMode => { 467 | // Enter follow mode 468 | self.follow_mode = true; 469 | // Reload file 470 | self.read_buffer(DEFAULT_POLL_TIMEOUT_MS)?; 471 | pane.goto_bottom_of_lines()?; 472 | pane.set_message(self.mode_default_message()); 473 | pane.refresh()?; 474 | } 475 | PeepEvent::Quit => { 476 | pane.quit(); 477 | } 478 | PeepEvent::QuitWithClear => { 479 | pane.clear()?; 480 | pane.quit(); 481 | } 482 | PeepEvent::SigInt => {} 483 | _ => {} 484 | } 485 | Ok(()) 486 | } 487 | 488 | fn handle_follow(&mut self, event: &PeepEvent, pane: &mut Pane) -> io::Result<()> { 489 | match event { 490 | &PeepEvent::ToggleLineNumberPrinting => { 491 | self.show_linenumber = !self.show_linenumber; 492 | pane.show_line_number(self.show_linenumber); 493 | pane.refresh()?; 494 | } 495 | &PeepEvent::IncrementLines(n) => { 496 | pane.increment_height(n)?; 497 | pane.refresh()?; 498 | } 499 | &PeepEvent::DecrementLines(n) => { 500 | pane.decrement_height(n)?; 501 | pane.refresh()?; 502 | } 503 | &PeepEvent::SetNumOfLines(n) => { 504 | pane.set_height(n)?; 505 | pane.refresh()?; 506 | } 507 | PeepEvent::SearchIncremental(s) => { 508 | self.typing_word = Some(s.to_owned()); 509 | pane.set_message(Some(format!("{}/{}", FOLLOWING_HL_MESSAGE, s))); 510 | if s.is_empty() { 511 | let _ = self.searcher.borrow_mut().set_pattern(s); 512 | pane.show_highlight(false); 513 | } else { 514 | let _ = self.searcher.borrow_mut().set_pattern(s); 515 | pane.show_highlight(true); 516 | } 517 | pane.refresh()?; 518 | } 519 | PeepEvent::SearchTrigger => { 520 | self.typing_word = None; 521 | pane.set_message(self.mode_default_message()); 522 | pane.refresh()?; 523 | } 524 | PeepEvent::Cancel => { 525 | self.typing_word = None; 526 | pane.set_message(self.mode_default_message()); 527 | pane.show_highlight(false); 528 | pane.refresh()?; 529 | } 530 | PeepEvent::FileUpdated => { 531 | self.read_buffer(DEFAULT_POLL_TIMEOUT_MS)?; 532 | pane.goto_bottom_of_lines()?; 533 | pane.set_message(self.mode_default_message()); 534 | pane.refresh()?; 535 | } 536 | PeepEvent::FollowMode => { 537 | // Leave follow mode 538 | self.follow_mode = false; 539 | pane.set_message(self.mode_default_message()); 540 | pane.refresh()?; 541 | } 542 | PeepEvent::Quit => { 543 | pane.quit(); 544 | } 545 | PeepEvent::QuitWithClear => { 546 | pane.clear()?; 547 | pane.quit(); 548 | } 549 | PeepEvent::SigInt => {} 550 | _ => {} 551 | } 552 | Ok(()) 553 | } 554 | 555 | fn search(&self, pos: (u16, u16)) -> Option<(u16, u16)> { 556 | let searcher = self.searcher.borrow(); 557 | let ref_linebuf = self.linebuf.borrow(); 558 | for (i, line) in ref_linebuf[(pos.1 as usize)..].iter().enumerate() { 559 | if let Some(m) = searcher.find(line) { 560 | return Some((m.start() as u16, pos.1 + i as u16)); 561 | } 562 | } 563 | None 564 | } 565 | 566 | fn search_rev(&self, pos: (u16, u16)) -> Option<(u16, u16)> { 567 | let searcher = self.searcher.borrow(); 568 | let ref_linebuf = self.linebuf.borrow(); 569 | for (i, line) in ref_linebuf[0..=(pos.1 as usize)].iter().rev().enumerate() { 570 | if let Some(m) = searcher.find(line) { 571 | return Some((m.start() as u16, pos.1 - i as u16)); 572 | } 573 | } 574 | None 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /src/csi.rs: -------------------------------------------------------------------------------- 1 | /// Bring These macros from termion crates. 2 | /// Create a CSI-introduced sequence. 3 | macro_rules! csi { 4 | ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) }; 5 | } 6 | 7 | /// Derive a CSI sequence struct. 8 | macro_rules! derive_csi_sequence { 9 | ($doc:expr, $name:ident, $value:expr) => { 10 | #[doc = $doc] 11 | #[derive(Copy, Clone)] 12 | pub struct $name; 13 | 14 | impl fmt::Display for $name { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, csi!($value)) 17 | } 18 | } 19 | }; 20 | } 21 | 22 | pub mod cursor_ext { 23 | //! Cursor extend movement 24 | 25 | use std::fmt; 26 | 27 | /// Move cursor next line. 28 | #[derive(Copy, Clone, PartialEq, Eq)] 29 | pub struct NextLine(pub u16); 30 | 31 | impl fmt::Display for NextLine { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | write!(f, csi!("{}E"), self.0) 34 | } 35 | } 36 | 37 | /// Move cursor previous line. 38 | #[derive(Copy, Clone, PartialEq, Eq)] 39 | pub struct PreviousLine(pub u16); 40 | 41 | impl fmt::Display for PreviousLine { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | write!(f, csi!("{}F"), self.0) 44 | } 45 | } 46 | 47 | /// Move cursor horizontal absolute. 48 | #[derive(Copy, Clone, PartialEq, Eq)] 49 | pub struct HorizontalAbsolute(pub u16); 50 | 51 | impl fmt::Display for HorizontalAbsolute { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | write!(f, csi!("{}G"), self.0) 54 | } 55 | } 56 | } 57 | 58 | pub mod cursor_style { 59 | //! Cursor style 60 | 61 | use std::fmt; 62 | 63 | derive_csi_sequence!("Set the cursor style blinking block", BlinkingBlock, "1 q"); 64 | derive_csi_sequence!("Set the cursor style steady block", SteadyBlock, "2 q"); 65 | derive_csi_sequence!( 66 | "Set the cursor style blinking underline", 67 | BlinkingUnderline, 68 | "3 q" 69 | ); 70 | derive_csi_sequence!( 71 | "Set the cursor style steady underline", 72 | SteadyUnderline, 73 | "4 q" 74 | ); 75 | derive_csi_sequence!("Set the cursor style blinking bar", BlinkingBar, "5 q"); 76 | derive_csi_sequence!("Set the cursor style steady bar", SteadyBar, "6 q"); 77 | } 78 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq)] 2 | pub enum PeepEvent { 3 | MoveDown(u16), 4 | MoveUp(u16), 5 | MoveLeft(u16), 6 | MoveRight(u16), 7 | MoveDownHalfPages(u16), 8 | MoveUpHalfPages(u16), 9 | MoveLeftHalfPages(u16), 10 | MoveRightHalfPages(u16), 11 | MoveDownPages(u16), 12 | MoveUpPages(u16), 13 | MoveToHeadOfLine, 14 | MoveToEndOfLine, 15 | MoveToTopOfLines, 16 | MoveToBottomOfLines, 17 | MoveToLineNumber(u16), 18 | 19 | ToggleLineNumberPrinting, 20 | ToggleLineWraps, 21 | IncrementLines(u16), 22 | DecrementLines(u16), 23 | SetNumOfLines(u16), 24 | 25 | SearchIncremental(String), 26 | SearchTrigger, 27 | SearchNext, 28 | SearchPrev, 29 | 30 | Message(Option), 31 | 32 | Cancel, 33 | Quit, 34 | QuitWithClear, 35 | 36 | FollowMode, 37 | FileUpdated, 38 | SigInt, 39 | } 40 | -------------------------------------------------------------------------------- /src/filewatch/linux.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use mio; 3 | use std::io; 4 | use std::os::unix::io::{AsRawFd, RawFd}; 5 | use std::time::Duration; 6 | 7 | pub struct FileWatcher { 8 | inotify: inotify::Inotify, 9 | poll: mio::Poll, 10 | events: mio::Events, 11 | buffer: [u8; 1024], 12 | } 13 | 14 | impl FileWatcher { 15 | pub fn new(file_path: &str) -> io::Result { 16 | let mut inotify = inotify::Inotify::init()?; 17 | inotify.add_watch(file_path, inotify::WatchMask::MODIFY)?; 18 | let poll = mio::Poll::new()?; 19 | let events = mio::Events::with_capacity(1024); 20 | 21 | poll.registry().register( 22 | &mut mio::unix::SourceFd(&inotify.as_raw_fd()), 23 | mio::Token(0), 24 | mio::Interest::READABLE, 25 | )?; 26 | 27 | Ok(Self { 28 | inotify, 29 | poll, 30 | events, 31 | buffer: [0u8; 1024], 32 | }) 33 | } 34 | } 35 | 36 | impl FileWatch for FileWatcher { 37 | fn watch(&mut self, timeout: Option) -> io::Result> { 38 | self.poll.poll(&mut self.events, timeout)?; 39 | Ok(if self.events.is_empty() { 40 | None 41 | } else { 42 | let evt = &self.events.iter().next(); 43 | self.inotify.read_events(&mut self.buffer)?; 44 | evt.as_ref().map(|e| e.is_readable()) 45 | }) 46 | } 47 | } 48 | 49 | pub struct StdinWatcher { 50 | poll: mio::Poll, 51 | events: mio::Events, 52 | } 53 | 54 | impl StdinWatcher { 55 | pub fn new(fd: RawFd) -> io::Result { 56 | let poll = mio::Poll::new()?; 57 | let events = mio::Events::with_capacity(1024); 58 | poll.registry().register( 59 | &mut mio::unix::SourceFd(&fd), 60 | mio::Token(0), 61 | mio::Interest::READABLE, 62 | )?; 63 | 64 | Ok(Self { poll, events }) 65 | } 66 | } 67 | 68 | impl FileWatch for StdinWatcher { 69 | fn watch(&mut self, timeout: Option) -> io::Result> { 70 | self.poll.poll(&mut self.events, timeout)?; 71 | Ok(if self.events.is_empty() { 72 | None 73 | } else { 74 | let evt = &self.events.iter().next(); 75 | evt.as_ref().map(|e| e.is_readable()) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/filewatch/macos.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use mio; 3 | use std::fs::File; 4 | use std::io::{self, Seek, SeekFrom}; 5 | use std::os::unix::io::{AsRawFd, RawFd}; 6 | use std::time::Duration; 7 | 8 | pub struct FileWatcher { 9 | file: File, 10 | poll: mio::Poll, 11 | events: mio::Events, 12 | } 13 | 14 | impl FileWatcher { 15 | pub fn new(file_path: &str) -> io::Result { 16 | let mut file = File::open(file_path)?; 17 | file.seek(SeekFrom::End(0))?; 18 | 19 | let poll = mio::Poll::new()?; 20 | let events = mio::Events::with_capacity(1024); 21 | poll.registry().register( 22 | &mut mio::unix::SourceFd(&file.as_raw_fd()), 23 | mio::Token(0), 24 | mio::Interest::READABLE, 25 | )?; 26 | 27 | Ok(Self { file, poll, events }) 28 | } 29 | } 30 | 31 | impl FileWatch for FileWatcher { 32 | fn watch(&mut self, timeout: Option) -> io::Result> { 33 | self.poll.poll(&mut self.events, timeout)?; 34 | self.file.seek(SeekFrom::End(0))?; 35 | Ok(if self.events.is_empty() { 36 | None 37 | } else { 38 | Some(false) 39 | }) 40 | } 41 | } 42 | 43 | pub struct StdinWatcher { 44 | poll: mio::Poll, 45 | events: mio::Events, 46 | } 47 | 48 | impl StdinWatcher { 49 | pub fn new(fd: RawFd) -> io::Result { 50 | let poll = mio::Poll::new()?; 51 | let events = mio::Events::with_capacity(1024); 52 | poll.registry().register( 53 | &mut mio::unix::SourceFd(&fd), 54 | mio::Token(0), 55 | mio::Interest::READABLE, 56 | )?; 57 | 58 | Ok(Self { poll, events }) 59 | } 60 | } 61 | 62 | impl FileWatch for StdinWatcher { 63 | fn watch(&mut self, timeout: Option) -> io::Result> { 64 | self.poll.poll(&mut self.events, timeout)?; 65 | Ok(if self.events.is_empty() { 66 | None 67 | } else { 68 | let evt = &self.events.iter().next(); 69 | evt.as_ref().map(|e| e.is_readable()) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/filewatch/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::event::PeepEvent; 2 | use std::io; 3 | use std::os::unix::io::AsRawFd; 4 | use std::sync::mpsc; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | 8 | #[cfg(target_os = "linux")] 9 | pub mod linux; 10 | #[cfg(target_os = "linux")] 11 | pub use self::linux::*; 12 | 13 | #[cfg(target_os = "macos")] 14 | pub mod macos; 15 | #[cfg(target_os = "macos")] 16 | pub use self::macos::*; 17 | 18 | /// Returns one of the following values as io::Result>. 19 | /// - Err() : Error 20 | /// - Ok(None) : Timeout 21 | /// - Ok(Some(false)) : Get event without hung up. 22 | /// - Ok(Some(true)) : Get event with hung up. It is necessary to quit after read. 23 | pub trait FileWatch { 24 | fn watch(&mut self, timeout: Option) -> io::Result>; 25 | } 26 | 27 | const NONE_WAIT_SEC: u64 = 60; 28 | 29 | pub struct Timeout; 30 | 31 | impl FileWatch for Timeout { 32 | fn watch(&mut self, timeout: Option) -> io::Result> { 33 | let timeout = timeout.unwrap_or_else(|| Duration::from_secs(NONE_WAIT_SEC)); 34 | sleep(timeout); 35 | Ok(None) 36 | } 37 | } 38 | 39 | pub fn file_watcher(file_path: &str, event_sender: &mpsc::Sender) { 40 | let mut fw: FileWatcher; 41 | let mut tm = Timeout; 42 | let mut sw: StdinWatcher; 43 | let stdin_fd = io::stdin().as_raw_fd(); 44 | let filewatcher: &mut dyn FileWatch = if file_path == "-" { 45 | if let Ok(v) = StdinWatcher::new(stdin_fd) { 46 | sw = v; 47 | &mut sw 48 | } else { 49 | &mut tm 50 | } 51 | } else if let Ok(v) = FileWatcher::new(file_path) { 52 | fw = v; 53 | &mut fw 54 | } else { 55 | &mut tm 56 | }; 57 | 58 | let default_timeout = Duration::from_secs(NONE_WAIT_SEC); 59 | 60 | loop { 61 | if filewatcher.watch(Some(default_timeout)).unwrap().is_some() { 62 | event_sender.send(PeepEvent::FileUpdated).unwrap(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/keybind.rs: -------------------------------------------------------------------------------- 1 | /// Key Bind Parser 2 | use crate::event::PeepEvent; 3 | 4 | pub trait KeyParser { 5 | fn parse(&mut self, c: char) -> Option; 6 | } 7 | 8 | /// Default key map 9 | pub mod default { 10 | use super::*; 11 | use std::collections::HashMap; 12 | 13 | const ALLOWED_CTRL_KEYCODES: [char; 10] = [ 14 | /* Ctr-a = */ '\x01', /* Ctr-b = */ '\x02', /* Ctr-d = */ '\x04', 15 | /* Ctr-e = */ '\x05', /* Ctr-f = */ '\x06', /* Ctr-j = */ '\x0a', 16 | /* Ctr-k = */ '\x0b', /* Ctr-n = */ '\x0e', /* Ctr-p = */ '\x10', 17 | /* Ctr-u = */ '\x15', 18 | ]; 19 | 20 | // Ready -> IncSearching 21 | // IncSearching -> Ready 22 | // 23 | // Ready -> Numbering 24 | // Numbering -> Ready : cancel 25 | // Numbering -> Commanding 26 | // 27 | // Ready -> Commanding 28 | // Commanding -> Ready 29 | enum State { 30 | Ready, 31 | IncSearching, 32 | Numbering, 33 | Commanding, 34 | } 35 | 36 | pub struct KeyBind<'a> { 37 | state: State, 38 | number: u16, 39 | wip_keys: String, 40 | cmap: HashMap<&'a str, PeepEvent>, 41 | } 42 | 43 | impl<'a> Default for KeyBind<'a> { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | 49 | impl<'a> KeyBind<'a> { 50 | pub fn new() -> Self { 51 | let mut kb = KeyBind { 52 | state: State::Ready, 53 | number: 0, 54 | wip_keys: String::with_capacity(64), 55 | cmap: HashMap::new(), 56 | }; 57 | kb.cmap = KeyBind::default_command_table(); 58 | kb 59 | } 60 | 61 | fn default_command_table() -> HashMap<&'a str, PeepEvent> { 62 | // let mut default: HashMap<&str, PeepEvent> = [ 63 | [ 64 | ("j", PeepEvent::MoveDown(1)), 65 | (/* Ctr-j */ "\x0a", PeepEvent::MoveDown(1)), 66 | (/* Ctr-n */ "\x0e", PeepEvent::MoveDown(1)), 67 | ("k", PeepEvent::MoveUp(1)), 68 | (/* Ctr-k */ "\x0b", PeepEvent::MoveUp(1)), 69 | (/* Ctr-p */ "\x10", PeepEvent::MoveUp(1)), 70 | ("h", PeepEvent::MoveLeft(1)), 71 | ("l", PeepEvent::MoveRight(1)), 72 | ("d", PeepEvent::MoveDownHalfPages(1)), 73 | (/* Ctr-d */ "\x04", PeepEvent::MoveDownHalfPages(1)), 74 | ("u", PeepEvent::MoveUpHalfPages(1)), 75 | (/* Ctr-u */ "\x15", PeepEvent::MoveUpHalfPages(1)), 76 | ("H", PeepEvent::MoveLeftHalfPages(1)), 77 | ("L", PeepEvent::MoveRightHalfPages(1)), 78 | ("f", PeepEvent::MoveDownPages(1)), 79 | (/* Ctr-f */ "\x06", PeepEvent::MoveDownPages(1)), 80 | (" ", PeepEvent::MoveDownPages(1)), 81 | ("b", PeepEvent::MoveUpPages(1)), 82 | (/* Ctr-b */ "\x02", PeepEvent::MoveUpPages(1)), 83 | ("0", PeepEvent::MoveToHeadOfLine), 84 | (/* Ctrl-a */ "\x01", PeepEvent::MoveToHeadOfLine), 85 | ("$", PeepEvent::MoveToEndOfLine), 86 | (/* Ctrl-e */ "\x05", PeepEvent::MoveToEndOfLine), 87 | ("g", PeepEvent::MoveToTopOfLines), 88 | ("G", PeepEvent::MoveToBottomOfLines), 89 | ("#", PeepEvent::ToggleLineNumberPrinting), 90 | ("!", PeepEvent::ToggleLineWraps), 91 | ("-", PeepEvent::DecrementLines(1)), 92 | ("+", PeepEvent::IncrementLines(1)), 93 | ("=", PeepEvent::SetNumOfLines(0)), 94 | ("n", PeepEvent::SearchNext), 95 | ("N", PeepEvent::SearchPrev), 96 | ("q", PeepEvent::Quit), 97 | ("Q", PeepEvent::QuitWithClear), 98 | ("F", PeepEvent::FollowMode), 99 | ] 100 | .iter() 101 | .cloned() 102 | .collect() 103 | } 104 | 105 | fn trans_to_ready(&mut self) { 106 | self.state = State::Ready; 107 | self.number = 0; 108 | self.wip_keys.clear(); 109 | } 110 | fn trans_to_incsearching(&mut self) { 111 | self.state = State::IncSearching; 112 | self.number = 0; 113 | self.wip_keys.clear(); 114 | } 115 | fn trans_to_numbering(&mut self, c: char) { 116 | self.state = State::Numbering; 117 | self.number = c.to_digit(10).unwrap() as u16; 118 | self.wip_keys.push(c); 119 | } 120 | fn trans_to_commanding(&mut self) { 121 | self.state = State::Commanding; 122 | self.wip_keys.clear(); 123 | } 124 | 125 | fn action_ready(&mut self, c: char) -> Option { 126 | match c { 127 | '/' => { 128 | self.trans_to_incsearching(); 129 | Some(PeepEvent::SearchIncremental("".to_owned())) 130 | } 131 | '1'..='9' => { 132 | self.trans_to_numbering(c); 133 | // Some(PeepEvent::Message(Some(self.number.to_string()))) 134 | None 135 | } 136 | c if !c.is_control() | ALLOWED_CTRL_KEYCODES.contains(&c) => { 137 | self.trans_to_commanding(); 138 | self.action_commanding(c) 139 | } 140 | // ESC 141 | '\x1b' => Some(PeepEvent::Cancel), 142 | _ => None, 143 | } 144 | } 145 | 146 | fn action_incsearching(&mut self, c: char) -> Option { 147 | match c { 148 | c if !c.is_control() => { 149 | self.wip_keys.push(c); 150 | Some(PeepEvent::SearchIncremental(self.wip_keys.to_owned())) 151 | } 152 | '\x08' | '\x7f' => { 153 | // BackSpace, Delete 154 | if self.wip_keys.pop().is_none() { 155 | self.trans_to_ready(); 156 | Some(PeepEvent::Cancel) 157 | } else { 158 | Some(PeepEvent::SearchIncremental(self.wip_keys.to_owned())) 159 | } 160 | } 161 | '\n' => { 162 | // LF 163 | self.trans_to_ready(); 164 | Some(PeepEvent::SearchTrigger) 165 | } 166 | '\x1b' => { 167 | // ESC -> Cancel 168 | self.trans_to_ready(); 169 | Some(PeepEvent::Cancel) 170 | } 171 | _ => None, 172 | } 173 | } 174 | 175 | fn action_numbering(&mut self, c: char) -> Option { 176 | match c { 177 | '0'..='9' => { 178 | self.number = self.number * 10 + c.to_digit(10).unwrap() as u16; 179 | // Some(PeepEvent::Message(Some(self.number.to_string()))) 180 | None 181 | } 182 | c if !c.is_control() => { 183 | self.trans_to_commanding(); 184 | self.action_commanding(c) 185 | } 186 | '\x1b' | '\n' => { 187 | // ESC and LF -> Cancel 188 | self.trans_to_ready(); 189 | None 190 | // Some(PeepEvent::Message(None)) 191 | } 192 | _ => None, 193 | } 194 | } 195 | 196 | fn action_commanding(&mut self, c: char) -> Option { 197 | let mut needs_trans = false; 198 | let op = match c { 199 | c if !c.is_control() | ALLOWED_CTRL_KEYCODES.contains(&c) => { 200 | self.wip_keys.push(c); 201 | match self.cmap.get::(&self.wip_keys) { 202 | Some(v) => { 203 | needs_trans = true; 204 | self.combine_command(v.to_owned()) 205 | } 206 | None => { 207 | if self.cmap.keys().any(|&k| k.starts_with(&self.wip_keys)) { 208 | // has candidates 209 | None 210 | } else { 211 | // not exist => cancel 212 | needs_trans = true; 213 | Some(PeepEvent::Message(None)) 214 | } 215 | } 216 | } 217 | } 218 | _ => { 219 | needs_trans = true; 220 | Some(PeepEvent::Message(None)) 221 | } 222 | }; 223 | if needs_trans { 224 | self.trans_to_ready() 225 | }; 226 | op 227 | } 228 | 229 | fn combine_command(&self, op: PeepEvent) -> Option { 230 | let valid_num = |n| if n == 0 { 1 } else { n }; 231 | match op { 232 | PeepEvent::MoveDown(_) => Some(PeepEvent::MoveDown(valid_num(self.number))), 233 | PeepEvent::MoveUp(_) => Some(PeepEvent::MoveUp(valid_num(self.number))), 234 | PeepEvent::MoveLeft(_) => Some(PeepEvent::MoveLeft(valid_num(self.number))), 235 | PeepEvent::MoveRight(_) => Some(PeepEvent::MoveRight(valid_num(self.number))), 236 | PeepEvent::MoveDownHalfPages(_) => { 237 | Some(PeepEvent::MoveDownHalfPages(valid_num(self.number))) 238 | } 239 | PeepEvent::MoveUpHalfPages(_) => { 240 | Some(PeepEvent::MoveUpHalfPages(valid_num(self.number))) 241 | } 242 | PeepEvent::MoveLeftHalfPages(_) => { 243 | Some(PeepEvent::MoveLeftHalfPages(valid_num(self.number))) 244 | } 245 | PeepEvent::MoveRightHalfPages(_) => { 246 | Some(PeepEvent::MoveRightHalfPages(valid_num(self.number))) 247 | } 248 | PeepEvent::MoveDownPages(_) => { 249 | Some(PeepEvent::MoveDownPages(valid_num(self.number))) 250 | } 251 | PeepEvent::MoveUpPages(_) => Some(PeepEvent::MoveUpPages(valid_num(self.number))), 252 | PeepEvent::MoveToTopOfLines | PeepEvent::MoveToBottomOfLines => { 253 | if self.number == 0 { 254 | Some(op) 255 | } else { 256 | Some(PeepEvent::MoveToLineNumber(self.number - 1)) 257 | } 258 | } 259 | PeepEvent::MoveToLineNumber(_) => { 260 | Some(PeepEvent::MoveToLineNumber(valid_num(self.number))) 261 | } 262 | PeepEvent::IncrementLines(_) => { 263 | Some(PeepEvent::IncrementLines(valid_num(self.number))) 264 | } 265 | PeepEvent::DecrementLines(_) => { 266 | Some(PeepEvent::DecrementLines(valid_num(self.number))) 267 | } 268 | PeepEvent::SetNumOfLines(_) => { 269 | if self.number == 0 { 270 | None 271 | } else { 272 | Some(PeepEvent::SetNumOfLines(self.number)) 273 | } 274 | } 275 | _ => Some(op), 276 | } 277 | } 278 | 279 | fn trans(&mut self, c: char) -> Option { 280 | match self.state { 281 | State::Ready => self.action_ready(c), 282 | State::IncSearching => self.action_incsearching(c), 283 | State::Numbering => self.action_numbering(c), 284 | State::Commanding => self.action_commanding(c), 285 | } 286 | } 287 | } 288 | 289 | impl<'a> KeyParser for KeyBind<'a> { 290 | fn parse(&mut self, c: char) -> Option { 291 | self.trans(c) 292 | } 293 | } 294 | } 295 | 296 | #[cfg(test)] 297 | mod tests { 298 | use super::*; 299 | use crate::event::PeepEvent; 300 | 301 | #[test] 302 | fn test_keybind_command() { 303 | let mut kb = default::KeyBind::new(); 304 | 305 | // normal commands 306 | assert_eq!(kb.parse('j'), Some(PeepEvent::MoveDown(1))); 307 | assert_eq!(kb.parse('\x0a'), Some(PeepEvent::MoveDown(1))); 308 | assert_eq!(kb.parse('\x0e'), Some(PeepEvent::MoveDown(1))); 309 | assert_eq!(kb.parse('k'), Some(PeepEvent::MoveUp(1))); 310 | assert_eq!(kb.parse('\x0b'), Some(PeepEvent::MoveUp(1))); 311 | assert_eq!(kb.parse('\x10'), Some(PeepEvent::MoveUp(1))); 312 | assert_eq!(kb.parse('h'), Some(PeepEvent::MoveLeft(1))); 313 | assert_eq!(kb.parse('l'), Some(PeepEvent::MoveRight(1))); 314 | assert_eq!(kb.parse('d'), Some(PeepEvent::MoveDownHalfPages(1))); 315 | assert_eq!(kb.parse('\x04'), Some(PeepEvent::MoveDownHalfPages(1))); 316 | assert_eq!(kb.parse('u'), Some(PeepEvent::MoveUpHalfPages(1))); 317 | assert_eq!(kb.parse('\x15'), Some(PeepEvent::MoveUpHalfPages(1))); 318 | assert_eq!(kb.parse('f'), Some(PeepEvent::MoveDownPages(1))); 319 | assert_eq!(kb.parse('\x06'), Some(PeepEvent::MoveDownPages(1))); 320 | assert_eq!(kb.parse(' '), Some(PeepEvent::MoveDownPages(1))); 321 | assert_eq!(kb.parse('b'), Some(PeepEvent::MoveUpPages(1))); 322 | assert_eq!(kb.parse('\x02'), Some(PeepEvent::MoveUpPages(1))); 323 | assert_eq!(kb.parse('0'), Some(PeepEvent::MoveToHeadOfLine)); 324 | assert_eq!(kb.parse('\x01'), Some(PeepEvent::MoveToHeadOfLine)); 325 | assert_eq!(kb.parse('$'), Some(PeepEvent::MoveToEndOfLine)); 326 | assert_eq!(kb.parse('\x05'), Some(PeepEvent::MoveToEndOfLine)); 327 | assert_eq!(kb.parse('g'), Some(PeepEvent::MoveToTopOfLines)); 328 | assert_eq!(kb.parse('G'), Some(PeepEvent::MoveToBottomOfLines)); 329 | assert_eq!(kb.parse('-'), Some(PeepEvent::DecrementLines(1))); 330 | assert_eq!(kb.parse('+'), Some(PeepEvent::IncrementLines(1))); 331 | assert_eq!(kb.parse('='), None); 332 | assert_eq!(kb.parse('n'), Some(PeepEvent::SearchNext)); 333 | assert_eq!(kb.parse('N'), Some(PeepEvent::SearchPrev)); 334 | assert_eq!(kb.parse('q'), Some(PeepEvent::Quit)); 335 | assert_eq!(kb.parse('Q'), Some(PeepEvent::QuitWithClear)); 336 | assert_eq!(kb.parse('#'), Some(PeepEvent::ToggleLineNumberPrinting)); 337 | assert_eq!(kb.parse('!'), Some(PeepEvent::ToggleLineWraps)); 338 | assert_eq!(kb.parse('F'), Some(PeepEvent::FollowMode)); 339 | assert_eq!(kb.parse('\x1b'), Some(PeepEvent::Cancel)); 340 | } 341 | 342 | #[test] 343 | fn test_keybind_number() { 344 | let mut kb = default::KeyBind::new(); 345 | 346 | // normal commands 347 | assert_eq!(kb.parse('1'), None); 348 | assert_eq!(kb.parse('2'), None); 349 | assert_eq!(kb.parse('\n'), None); 350 | 351 | assert_eq!(kb.parse('1'), None); 352 | assert_eq!(kb.parse('2'), None); 353 | assert_eq!(kb.parse('\x1b'), None); 354 | 355 | assert_eq!(kb.parse('2'), None); 356 | assert_eq!(kb.parse('j'), Some(PeepEvent::MoveDown(2))); 357 | 358 | assert_eq!(kb.parse('1'), None); 359 | assert_eq!(kb.parse('0'), None); 360 | assert_eq!(kb.parse('h'), Some(PeepEvent::MoveLeft(10))); 361 | 362 | assert_eq!(kb.parse('1'), None); 363 | assert_eq!(kb.parse('0'), None); 364 | assert_eq!(kb.parse('='), Some(PeepEvent::SetNumOfLines(10))); 365 | } 366 | 367 | #[test] 368 | fn test_keybind_search() { 369 | let mut kb = default::KeyBind::new(); 370 | 371 | // search commands 372 | assert_eq!( 373 | kb.parse('/'), 374 | Some(PeepEvent::SearchIncremental("".to_owned())) 375 | ); 376 | assert_eq!( 377 | kb.parse('w'), 378 | Some(PeepEvent::SearchIncremental("w".to_owned())) 379 | ); 380 | assert_eq!( 381 | kb.parse('o'), 382 | Some(PeepEvent::SearchIncremental("wo".to_owned())) 383 | ); 384 | assert_eq!( 385 | kb.parse('r'), 386 | Some(PeepEvent::SearchIncremental("wor".to_owned())) 387 | ); 388 | assert_eq!( 389 | kb.parse('d'), 390 | Some(PeepEvent::SearchIncremental("word".to_owned())) 391 | ); 392 | assert_eq!(kb.parse('\n'), Some(PeepEvent::SearchTrigger)); 393 | 394 | assert_eq!( 395 | kb.parse('/'), 396 | Some(PeepEvent::SearchIncremental("".to_owned())) 397 | ); 398 | assert_eq!( 399 | kb.parse('a'), 400 | Some(PeepEvent::SearchIncremental("a".to_owned())) 401 | ); 402 | assert_eq!( 403 | kb.parse('b'), 404 | Some(PeepEvent::SearchIncremental("ab".to_owned())) 405 | ); 406 | assert_eq!( 407 | kb.parse('\x08'), 408 | Some(PeepEvent::SearchIncremental("a".to_owned())) 409 | ); 410 | assert_eq!( 411 | kb.parse('\x08'), 412 | Some(PeepEvent::SearchIncremental("".to_owned())) 413 | ); 414 | assert_eq!(kb.parse('\x08'), Some(PeepEvent::Cancel)); 415 | 416 | assert_eq!( 417 | kb.parse('/'), 418 | Some(PeepEvent::SearchIncremental("".to_owned())) 419 | ); 420 | assert_eq!( 421 | kb.parse('w'), 422 | Some(PeepEvent::SearchIncremental("w".to_owned())) 423 | ); 424 | assert_eq!( 425 | kb.parse('o'), 426 | Some(PeepEvent::SearchIncremental("wo".to_owned())) 427 | ); 428 | assert_eq!(kb.parse('\n'), Some(PeepEvent::SearchTrigger)); 429 | assert_eq!(kb.parse('n'), Some(PeepEvent::SearchNext)); 430 | assert_eq!(kb.parse('N'), Some(PeepEvent::SearchPrev)); 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod csi; 3 | pub mod event; 4 | pub mod filewatch; 5 | pub mod keybind; 6 | pub mod logger; 7 | pub mod pane; 8 | pub mod search; 9 | pub mod tab; 10 | pub mod term; 11 | pub mod unicode_divide; 12 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[cfg(debug_assertions)] 4 | pub fn log(msg: &str) { 5 | const LOG_PATH: &str = "./peep.log"; 6 | use std::fs::OpenOptions; 7 | use std::io::Write; 8 | 9 | let mut w = OpenOptions::new() 10 | .create(true) 11 | .append(true) 12 | .open(LOG_PATH) 13 | .unwrap(); 14 | let _ = writeln!(&mut w, "{}", msg); 15 | } 16 | 17 | #[cfg(not(debug_assertions))] 18 | pub fn log(_: &str) {} 19 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use getopts::Options; 2 | use std::env; 3 | use std::io; 4 | use std::process; 5 | 6 | use peep::app::App; 7 | 8 | fn print_usage(prog: &str, version: &str, opts: &Options) { 9 | let brief = format!( 10 | "{p} {v}\n\nUsage: {p} [OPTION]... [FILE]", 11 | p = prog, 12 | v = version 13 | ); 14 | println!("{}", opts.usage(&brief)); 15 | println!( 16 | "Commands on Normal Mode: 17 | (num)j Ctr-j Ctr-n Scroll down 18 | (num)k Ctr-k Ctr-p Scroll up 19 | (num)d Ctr-d Scroll down half page 20 | (num)u Ctr-u Scroll up half page 21 | (num)f Ctr-f SPACE Scroll down a page 22 | (num)b Ctr-b Scroll up a page 23 | (num)l Scroll horizontally right 24 | (num)h Scroll horizontally left 25 | (num)L Scroll horizontally right half page 26 | (num)H Scroll horizontally left half page 27 | 0 Ctr-a Go to the beggining of line 28 | $ Ctr-e Go to the end of line 29 | g Go to the beggining of file 30 | G Go to the end of file 31 | [num]g [num]G Go to line [num] 32 | /pattern Search forward in the file for the regex pattern 33 | n Search next 34 | N Search previous 35 | q Ctr-c Quit 36 | Q Clear output and Quit 37 | (num)+ Increment screen height 38 | (num)- Decrement screen height 39 | [num]= Set screen height to [num] 40 | # Toggle line number printing 41 | ! Toggle line wrapping 42 | ESC Cancel 43 | F Toggle to follow mode 44 | 45 | Commands on Following Mode: 46 | /pattern Highlight the regex pattern 47 | q Ctr-c Quit 48 | Q Clear output and Quit 49 | (num)+ Increment screen height 50 | (num)- Decrement screen height 51 | [num]= Set screen height to [num] 52 | # Toggle line number printing 53 | ! Toggle line wrapping 54 | ESC Cancel 55 | F Toggle to normal mode" 56 | ); 57 | } 58 | 59 | fn print_version(prog: &str, version: &str) { 60 | println!("{} {}", prog, version); 61 | } 62 | 63 | fn run() -> io::Result<()> { 64 | let prog = env!("CARGO_PKG_NAME"); 65 | let version = env!("CARGO_PKG_VERSION"); 66 | let args: Vec = env::args().skip(1).collect(); 67 | 68 | let mut opts = Options::new(); 69 | opts.optopt("n", "lines", "set height of pane", "LINES") 70 | .optopt("s", "start", "set start line of data at startup", "START") 71 | .optopt("t", "tab-width", "set tab width", "WIDTH") 72 | .optflag("N", "print-line-number", "print line numbers") 73 | .optflag("f", "follow", "output appended data as the file grows") 74 | .optflag("w", "wrap", "wrap text line") 75 | .optflag("h", "help", "show this usage") 76 | .optflag("v", "version", "show version"); 77 | 78 | let matches = opts 79 | .parse(args) 80 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; 81 | 82 | if matches.opt_present("h") { 83 | print_usage(prog, version, &opts); 84 | return Ok(()); 85 | } 86 | 87 | if matches.opt_present("v") { 88 | print_version(prog, version); 89 | return Ok(()); 90 | } 91 | 92 | let file_path = if !matches.free.is_empty() { 93 | matches.free[0].clone() 94 | } else { 95 | if termion::is_tty(&io::stdin()) { 96 | // not find file name and pipe input 97 | return Err(io::Error::new( 98 | io::ErrorKind::NotFound, 99 | format!("missing filename (\"{} --help\" for help)", prog), 100 | )); 101 | } 102 | "-".to_owned() 103 | }; 104 | 105 | let mut app: App = Default::default(); 106 | app.show_linenumber = matches.opt_present("N"); 107 | app.follow_mode = matches.opt_present("f"); 108 | app.wraps_line = matches.opt_present("w"); 109 | if let Ok(Some(nlines)) = matches.opt_get::("n") { 110 | app.nlines = nlines; 111 | } 112 | if let Ok(Some(tab_width)) = matches.opt_get::("t") { 113 | app.tab_width = tab_width; 114 | } 115 | if let Ok(Some(start_line)) = matches.opt_get::("s") { 116 | app.start_line = start_line; 117 | } 118 | 119 | app.run(&file_path) 120 | } 121 | 122 | fn main() { 123 | if let Err(e) = run() { 124 | eprintln!("Error. {}", e); 125 | process::exit(1); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/pane.rs: -------------------------------------------------------------------------------- 1 | //! Pane module 2 | 3 | use crate::{ 4 | csi::cursor_ext, 5 | search::{NullSearcher, Search}, 6 | tab::TabExpand, 7 | term, 8 | unicode_divide::UnicodeStrDivider, 9 | }; 10 | use std::cell::RefCell; 11 | use std::cmp; 12 | use std::fmt; 13 | use std::io::{self, BufRead, BufReader, Write}; 14 | use std::io::{Seek, SeekFrom}; 15 | use std::ops; 16 | use std::rc::Rc; 17 | use unicode_width::UnicodeWidthStr; 18 | 19 | const DEFAULT_PANE_HEIGHT: u16 = 1; 20 | const DEFAULT_TAB_WIDTH: usize = 4; 21 | 22 | /// Display extention mark if the line doesn't fit in the pane width 23 | pub struct ExtendMark(pub char); 24 | impl fmt::Display for ExtendMark { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | write!( 27 | f, 28 | "{}{}{}", 29 | termion::color::Fg(termion::color::LightBlack), 30 | self.0, 31 | termion::style::Reset 32 | ) 33 | } 34 | } 35 | 36 | pub struct Pane<'a> { 37 | linebuf: Rc>>, 38 | writer: Box>, 39 | height: u16, 40 | numof_flushed_lines: u16, 41 | numof_semantic_flushed_lines: u16, 42 | cur_pos: (u16, u16), // (x, y) 43 | show_linenumber: bool, 44 | show_highlight: bool, 45 | hlsearcher: Rc>, 46 | message: String, 47 | tab_width: usize, 48 | wraps_line: bool, 49 | term: Box, 50 | } 51 | 52 | trait TermStat { 53 | fn size(&self) -> io::Result<(u16, u16)>; 54 | fn is_tty(&self) -> bool; 55 | } 56 | 57 | pub struct Terminal { 58 | is_stdout_tty: bool, 59 | } 60 | 61 | impl Terminal { 62 | fn new() -> Self { 63 | Self { 64 | is_stdout_tty: termion::is_tty(&io::stdout()), 65 | } 66 | } 67 | } 68 | 69 | impl TermStat for Terminal { 70 | fn size(&self) -> io::Result<(u16, u16)> { 71 | if self.is_stdout_tty { 72 | termion::terminal_size() 73 | } else { 74 | term::dev_tty_size() 75 | } 76 | } 77 | fn is_tty(&self) -> bool { 78 | self.is_stdout_tty 79 | } 80 | } 81 | 82 | #[derive(Debug)] 83 | pub enum ScrollStep { 84 | Char(u16), 85 | HalfPage(u16), 86 | Page(u16), 87 | } 88 | 89 | impl ScrollStep { 90 | fn to_numof_chars(&self, page_size: u16) -> u16 { 91 | match *self { 92 | ScrollStep::Char(n) => n, 93 | ScrollStep::HalfPage(n) => (page_size * n) / 2, 94 | ScrollStep::Page(n) => page_size * n, 95 | } 96 | } 97 | } 98 | 99 | impl<'a> Pane<'a> { 100 | const MARGIN_RIGHT_WIDTH: u16 = 4; 101 | const MESSAGE_BAR_HEIGHT: u16 = 1; 102 | 103 | pub fn new(w: Box>) -> Self { 104 | Self { 105 | linebuf: Rc::new(RefCell::new(Vec::new())), 106 | writer: w, 107 | height: DEFAULT_PANE_HEIGHT, 108 | numof_flushed_lines: 0, 109 | numof_semantic_flushed_lines: 0, 110 | cur_pos: (0, 0), 111 | show_linenumber: false, 112 | show_highlight: false, 113 | hlsearcher: Rc::new(RefCell::new(NullSearcher::new())), 114 | message: "".to_owned(), 115 | tab_width: DEFAULT_TAB_WIDTH, 116 | wraps_line: false, 117 | term: Box::new(Terminal::new()), 118 | } 119 | } 120 | 121 | pub fn is_stdout_tty(&self) -> bool { 122 | self.term.is_tty() 123 | } 124 | 125 | #[cfg(test)] 126 | fn replace_termsize_getter(&mut self, getter: Box) { 127 | self.term = getter; 128 | } 129 | 130 | /// Load text buffer and reset position. 131 | /// After this function is called, current pane postion is set to (0, 0). 132 | pub fn load(&mut self, buf: Rc>>) { 133 | self.linebuf = buf; 134 | self.cur_pos = (0, 0); 135 | } 136 | 137 | fn flush(&self) { 138 | self.writer.borrow_mut().flush().unwrap(); 139 | } 140 | 141 | // Sweep lines 142 | fn sweep(&self, nlines: u16) { 143 | let mut s = String::new(); 144 | s.push_str(&format!("{}", cursor_ext::HorizontalAbsolute(1))); 145 | for _ in 0..nlines { 146 | s.push_str(&format!("{}\n", termion::clear::CurrentLine)); 147 | } 148 | if nlines > 0 { 149 | s.push_str(&format!( 150 | "{}{}", 151 | termion::clear::CurrentLine, 152 | cursor_ext::PreviousLine(nlines) 153 | )); 154 | } else { 155 | // nlines == 0 156 | s.push_str(&format!("{}", termion::clear::CurrentLine)); 157 | } 158 | self.writer.borrow_mut().write_all(s.as_bytes()).unwrap(); 159 | } 160 | 161 | /// Clear pane 162 | pub fn clear(&mut self) -> io::Result<()> { 163 | let pane_height = self.pane_size()?.1; 164 | self.return_home(); 165 | self.sweep(cmp::max(self.numof_flushed_lines, pane_height)); 166 | Ok(()) 167 | } 168 | 169 | /// Return the range that matches the highlight word. 170 | fn hl_match_ranges(&self, raw: &str) -> Vec<(usize, usize)> { 171 | let mut v: Vec<(usize, usize)> = vec![]; 172 | if self.hlsearcher.borrow().as_str().is_empty() { 173 | return v; 174 | } 175 | for m in self.hlsearcher.borrow().find_iter(raw) { 176 | v.push((m.start(), m.end())); 177 | } 178 | v 179 | } 180 | 181 | /// Highlight trimmed string 182 | fn hl_words_for_trimmed( 183 | trimmed: &str, 184 | trimrange: &(usize, usize), 185 | hlranges: &[(usize, usize)], 186 | ) -> String { 187 | let mut hlline = String::new(); 188 | let mut copied = 0; 189 | let offset = trimrange.0; 190 | let end = trimrange.1 - offset; 191 | 192 | for &(hl_s, hl_e) in hlranges.iter() { 193 | if hl_e < trimrange.0 { 194 | continue; 195 | } else if hl_s <= trimrange.0 && hl_e >= trimrange.1 { 196 | // highlight whole line 197 | // [] : trimmed string 198 | // __ : high-light string 199 | // _[____]_ 200 | hlline.push_str(&format!( 201 | "{}{}{}", 202 | termion::style::Invert, 203 | trimmed, 204 | termion::style::Reset 205 | )); 206 | copied = end; 207 | break; 208 | } else if hl_s <= trimrange.0 && hl_e > trimrange.0 { 209 | // _[_ ] 210 | hlline.push_str(&format!( 211 | "{}{}{}", 212 | termion::style::Invert, 213 | trimmed.get(..hl_e - offset).unwrap(), 214 | termion::style::Reset 215 | )); 216 | copied = hl_e - offset; 217 | } else if hl_s >= trimrange.0 && hl_e <= trimrange.1 { 218 | // [ __ ] 219 | hlline.push_str(&format!( 220 | "{}{}{}{}", 221 | trimmed.get(copied..hl_s - offset).unwrap(), 222 | termion::style::Invert, 223 | trimmed.get(hl_s - offset..hl_e - offset).unwrap(), 224 | termion::style::Reset 225 | )); 226 | copied = hl_e - offset; 227 | } else if hl_s < trimrange.1 && hl_e >= trimrange.1 { 228 | // [ _]_ 229 | hlline.push_str(&format!( 230 | "{}{}{}{}", 231 | trimmed.get(copied..hl_s - offset).unwrap(), 232 | termion::style::Invert, 233 | trimmed.get(hl_s - offset..).unwrap(), 234 | termion::style::Reset 235 | )); 236 | copied = end; 237 | break; 238 | } else if hl_s > trimrange.1 { 239 | // [ ]_ 240 | hlline.push_str(trimmed.get(copied..).unwrap()); 241 | copied = end; 242 | break; 243 | } 244 | } 245 | 246 | if copied < end { 247 | hlline.push_str(trimmed.get(copied..).unwrap()); 248 | } 249 | 250 | hlline 251 | } 252 | 253 | /// Generate line number string 254 | /// | 100 ...... 255 | /// | 101 ...... 256 | fn gen_line_number_string(width: usize, line_number: u16) -> String { 257 | match width { 258 | 0..=2 => format!("{:>2}", line_number + 1), 259 | 3 => format!("{:>3}", line_number + 1), 260 | 4 => format!("{:>4}", line_number + 1), 261 | _ => format!("{:>5}", line_number + 1), 262 | } 263 | } 264 | 265 | /// Generate blank line number string 266 | /// | 100 ...... 267 | /// | +...... 268 | /// | 101 ...... 269 | fn gen_blank_line_number_string(width: usize) -> String { 270 | // from the second line 271 | match width { 272 | 0..=2 => " ".to_owned(), 273 | 3 => " ".to_owned(), 274 | 4 => " ".to_owned(), 275 | _ => " ".to_owned(), 276 | } 277 | } 278 | 279 | /// Decorate line 280 | /// 281 | /// | 12+xxxxxxxxxxxxxxxxxxxxxxxxxx+| 282 | /// | 13+xxxxxxx. | 283 | /// | 14+xxxxxxxxxxxx. | 284 | /// 285 | fn decorate_trim(&self, raw: &str, line_number: u16) -> String { 286 | // subtract line number space from raw_range 287 | let lnpw = self.line_number_printing_width(); 288 | 289 | // replace tabs with spaces 290 | let raw_notab = raw.expand_tab(self.tab_width); 291 | 292 | // trim unicode str considering visual unicode width 293 | let mut ucdiv = UnicodeStrDivider::new(&raw_notab, self.width_of_text_area()); 294 | let _ = ucdiv.seek(SeekFrom::Start(u64::from(self.cur_pos.0))); 295 | let trimmed = ucdiv.next().unwrap_or(""); 296 | let uc_range = ucdiv.last_range(); 297 | 298 | // highlight line 299 | let hlline; 300 | let decorated = if self.show_highlight { 301 | let hl_ranges = self.hl_match_ranges(&raw_notab); 302 | hlline = Self::hl_words_for_trimmed(trimmed, &uc_range, &hl_ranges); 303 | &hlline 304 | } else { 305 | trimmed 306 | }; 307 | 308 | // add line number 309 | let lnum = if self.show_linenumber { 310 | Self::gen_line_number_string(lnpw, line_number) 311 | } else { 312 | String::new() 313 | }; 314 | // add extend marks 315 | let sol = if uc_range.0 > 0 { 316 | format!("{}", ExtendMark('+')) 317 | } else { 318 | " ".to_owned() 319 | }; 320 | // add extend marks 321 | let eol = if raw_notab.len() > uc_range.1 { 322 | format!( 323 | "{}{}", 324 | cursor_ext::HorizontalAbsolute(self.pane_size().unwrap().0), 325 | ExtendMark('+') 326 | ) 327 | } else { 328 | format!("{}", termion::style::Reset) 329 | }; 330 | 331 | format!("{}{}{}{}", lnum, sol, decorated, eol) 332 | } 333 | 334 | /// Decorate line 335 | /// 336 | /// | 12 xxxxxxxxxxxxxxxxxxxxxxxxxxx| 337 | /// | >xxxxxxx. | 338 | /// | 13 xxxxxxxxxxxx. | 339 | /// 340 | fn decorate_wrap(&self, raw: &str, line_number: u16) -> String { 341 | let lnpw = if self.show_linenumber { 342 | self.line_number_printing_width() 343 | } else { 344 | 0 345 | }; 346 | // subtract line number space and extend_mark space from raw_range 347 | let line_cap_width = self.width_of_text_area(); 348 | 349 | // replace tabs with spaces 350 | let raw_notab = raw.expand_tab(self.tab_width); 351 | 352 | let mut ucdiv = UnicodeStrDivider::new(&raw_notab, self.width_of_text_area()); 353 | 354 | let mut s = 0; 355 | let mut e = line_cap_width; 356 | 357 | let mut wrapped = String::new(); 358 | let fn_lnum_string = |show_linenumber, width, start_pos, line_number| -> String { 359 | if show_linenumber { 360 | if start_pos == 0 { 361 | Self::gen_line_number_string(width, line_number) 362 | } else { 363 | Self::gen_blank_line_number_string(width) 364 | } 365 | } else { 366 | String::new() 367 | } 368 | }; 369 | 370 | while let Some(trimmed) = ucdiv.next() { 371 | let uc_range = ucdiv.last_range(); 372 | 373 | // highlight line 374 | let hlline; 375 | let decorated = if self.show_highlight { 376 | let hl_ranges = self.hl_match_ranges(&raw_notab); 377 | hlline = Self::hl_words_for_trimmed(trimmed, &uc_range, &hl_ranges); 378 | &hlline 379 | } else { 380 | trimmed 381 | }; 382 | 383 | // add line number 384 | let lnum = fn_lnum_string(self.show_linenumber, lnpw, s, line_number); 385 | // add wrap marks 386 | let sol = if s > 0 { 387 | format!("{}", ExtendMark('+')) 388 | } else { 389 | " ".to_owned() 390 | }; 391 | 392 | wrapped.push_str(&format!("{}{}{}\n", lnum, sol, decorated)); 393 | 394 | s = e; 395 | e += line_cap_width; 396 | } 397 | 398 | if wrapped.is_empty() { 399 | // add line number 400 | let lnum = fn_lnum_string(self.show_linenumber, lnpw, s, line_number); 401 | wrapped.push_str(&format!("{}\n", lnum)); 402 | } 403 | 404 | wrapped 405 | } 406 | 407 | fn decorate(&self, raw: &str, line_number: u16) -> String { 408 | if self.wraps_line { 409 | self.decorate_wrap(raw, line_number) 410 | } else { 411 | self.decorate_trim(raw, line_number) 412 | } 413 | } 414 | 415 | /// Refresh pane 416 | pub fn refresh(&mut self) -> io::Result<()> { 417 | // decorate content lines 418 | let pane_height = self.pane_size()?.1; 419 | let buf_range = self.range_of_visible_lines()?; 420 | let mut block = String::new(); 421 | let mut flushed_line_count = 0; 422 | 423 | 'outer: for (i, line) in self.linebuf.borrow()[buf_range.start..buf_range.end] 424 | .iter() 425 | .enumerate() 426 | { 427 | let deco = self.decorate(line, (buf_range.start + i) as u16); 428 | let br = BufReader::new(deco.as_bytes()); 429 | self.numof_semantic_flushed_lines = i as u16 + 1; 430 | for lline in br.lines() { 431 | block.push_str(&format!("{}\n", lline?)); 432 | flushed_line_count += 1; 433 | if flushed_line_count >= pane_height { 434 | break 'outer; 435 | } 436 | } 437 | } 438 | 439 | // move down to message bar position 440 | let numof_lines_to_message_bar = pane_height - flushed_line_count as u16; 441 | if numof_lines_to_message_bar > 0 { 442 | block.push_str(&format!( 443 | "{}", 444 | cursor_ext::NextLine(numof_lines_to_message_bar) 445 | )); 446 | } 447 | 448 | // message line 449 | if self.message.is_empty() && buf_range.start >= self.limit_bottom_y()? as usize { 450 | block.push_str(&format!( 451 | "{}(END){}", 452 | termion::style::Invert, 453 | termion::style::Reset 454 | )); 455 | } else { 456 | block.push_str(&self.message); 457 | }; 458 | 459 | self.return_home(); 460 | self.sweep(cmp::max(self.numof_flushed_lines, pane_height)); 461 | self.writer 462 | .borrow_mut() 463 | .write_all(block.as_bytes()) 464 | .unwrap(); 465 | self.flush(); 466 | self.numof_flushed_lines = pane_height; 467 | Ok(()) 468 | } 469 | 470 | pub fn quit(&self) { 471 | write!( 472 | self.writer.borrow_mut(), 473 | "{}{}", 474 | cursor_ext::HorizontalAbsolute(1), 475 | termion::clear::CurrentLine 476 | ) 477 | .unwrap(); 478 | self.flush(); 479 | } 480 | 481 | fn line_number_printing_width(&self) -> usize { 482 | match self.linebuf.borrow().len() { 483 | 0..=99 => 2, 484 | 100..=999 => 3, 485 | 1000..=9999 => 4, 486 | _ => 5, 487 | } 488 | } 489 | 490 | pub fn show_line_number(&mut self, b: bool) { 491 | self.show_linenumber = b; 492 | } 493 | 494 | pub fn show_highlight(&mut self, b: bool) { 495 | self.show_highlight = b; 496 | } 497 | 498 | pub fn set_highlight_searcher(&mut self, searcher: Rc>) { 499 | self.hlsearcher = searcher; 500 | } 501 | 502 | pub fn set_message(&mut self, msg: Option) { 503 | if let Some(m) = msg { 504 | self.message = m; 505 | } else { 506 | self.message.clear(); 507 | } 508 | } 509 | 510 | fn return_home(&self) { 511 | if self.numof_flushed_lines > 0 { 512 | write!( 513 | self.writer.borrow_mut(), 514 | "{}", 515 | cursor_ext::PreviousLine(self.numof_flushed_lines) 516 | ) 517 | .unwrap(); 518 | } 519 | } 520 | 521 | /// Return pane size (width, height) 522 | pub fn pane_size(&self) -> io::Result<(u16, u16)> { 523 | (*self.term) 524 | .size() 525 | .map(|(tw, th)| (tw, cmp::min(th, self.height))) 526 | } 527 | 528 | /// Return (x, y) 529 | pub fn position(&self) -> (u16, u16) { 530 | self.cur_pos 531 | } 532 | 533 | /// Return logical lines (wrapped lines) of specified line number. 534 | fn count_wrapped_lines(&self, text: &str) -> u16 { 535 | let pane_width = self.width_of_text_area(); 536 | if pane_width == 0 { 537 | 0 538 | } else { 539 | (UnicodeWidthStr::width(text) / pane_width) as u16 + 1 540 | } 541 | } 542 | 543 | /// Return the end of y that is considered buffer lines and window size and wrapped lines. 544 | fn limit_bottom_y(&self) -> io::Result { 545 | let linebuf_height = self.linebuf.borrow().len() as u16; 546 | let pane_height = self.pane_size()?.1; 547 | 548 | if !self.wraps_line { 549 | return Ok(if linebuf_height > pane_height { 550 | linebuf_height - pane_height 551 | } else { 552 | 0 553 | }); 554 | } 555 | 556 | // self.wraps_line is enabled 557 | let mut sum = 0; 558 | for i in (0..linebuf_height).rev() { 559 | sum += self.count_wrapped_lines(&self.linebuf.borrow()[i as usize]); 560 | if sum > pane_height { 561 | return Ok(if i == linebuf_height { 562 | linebuf_height 563 | } else { 564 | i + 1 565 | }); 566 | } 567 | } 568 | Ok(0) 569 | } 570 | 571 | /// Return text area width. 572 | fn width_of_text_area(&self) -> usize { 573 | let pane_width = self.pane_size().unwrap().0 as usize; 574 | let extend_mark_space: usize = if self.wraps_line { 1 } else { 2 }; 575 | let lnpw: usize = if self.show_linenumber { 576 | self.line_number_printing_width() 577 | } else { 578 | 0 579 | }; 580 | 581 | if pane_width > lnpw + extend_mark_space { 582 | pane_width - lnpw - extend_mark_space 583 | } else { 584 | 0 585 | } 586 | } 587 | 588 | /// Return range of visible lines from current line to buffer line end or bottom of pane. 589 | fn range_of_visible_lines(&self) -> io::Result> { 590 | let pane_height = self.pane_size()?.1 as usize; 591 | let buf_height = self.linebuf.borrow().len(); 592 | let y = self.cur_pos.1 as usize; 593 | 594 | Ok(y..if (buf_height - y) < pane_height { 595 | buf_height 596 | } else { 597 | y + pane_height 598 | }) 599 | } 600 | 601 | /// Return max width of linebuf range 602 | fn max_width_of_visible_lines(&self, r: ops::Range) -> u16 { 603 | self.linebuf.borrow()[r] 604 | .iter() 605 | .map(|s| UnicodeWidthStr::width(s.as_str())) 606 | .fold(0, cmp::max) as u16 607 | } 608 | 609 | /// Return the pane printable width 610 | fn pane_printable_width(&self) -> io::Result { 611 | Ok(self.pane_size()?.0 612 | - if self.show_linenumber { 613 | self.line_number_printing_width() as u16 614 | } else { 615 | 0 616 | }) 617 | } 618 | 619 | /// Return the horizontal offset that is considered pane size and string length 620 | fn limit_right_x(&self, next_x: u16, max_len: u16) -> io::Result { 621 | let margined_len = max_len + Self::MARGIN_RIGHT_WIDTH; 622 | let pane_width = self.pane_printable_width()?; 623 | Ok(if pane_width >= margined_len { 624 | 0 625 | } else if next_x + pane_width <= margined_len { 626 | next_x 627 | } else { 628 | margined_len - pane_width 629 | }) 630 | } 631 | 632 | // return actual scroll distance 633 | pub fn scroll_up(&mut self, ss: &ScrollStep) -> io::Result { 634 | let step = ss.to_numof_chars(self.numof_semantic_flushed_lines); 635 | let astep = if self.cur_pos.1 > step { 636 | step 637 | } else { 638 | self.cur_pos.1 639 | }; 640 | self.cur_pos.1 -= astep; 641 | Ok(astep) 642 | } 643 | 644 | // return actual scroll distance 645 | pub fn scroll_down(&mut self, ss: &ScrollStep) -> io::Result { 646 | let step = ss.to_numof_chars(self.numof_semantic_flushed_lines); 647 | let end_y = self.limit_bottom_y()?; 648 | let astep = if end_y > self.cur_pos.1 + step { 649 | step 650 | } else if end_y > self.cur_pos.1 { 651 | end_y - self.cur_pos.1 652 | } else { 653 | 0 654 | }; 655 | self.cur_pos.1 += astep; 656 | Ok(astep) 657 | } 658 | 659 | // return actual scroll distance 660 | pub fn scroll_left(&mut self, ss: &ScrollStep) -> io::Result { 661 | if self.wraps_line { 662 | return Ok(0); 663 | } 664 | let step = ss.to_numof_chars(self.pane_printable_width()?); 665 | let astep = if self.cur_pos.0 > step { 666 | step 667 | } else { 668 | self.cur_pos.0 669 | }; 670 | self.cur_pos.0 -= astep; 671 | Ok(astep) 672 | } 673 | 674 | // return actual scroll distance 675 | pub fn scroll_right(&mut self, ss: &ScrollStep) -> io::Result { 676 | if self.wraps_line { 677 | return Ok(0); 678 | } 679 | let step = ss.to_numof_chars(self.pane_printable_width()?); 680 | let max_line_width = self.max_width_of_visible_lines(self.range_of_visible_lines()?); 681 | let x = self.limit_right_x(self.cur_pos.0 + step, max_line_width)?; 682 | let astep = x - self.cur_pos.0; 683 | self.cur_pos.0 = x; 684 | Ok(astep) 685 | } 686 | 687 | pub fn goto_top_of_lines(&mut self) -> io::Result<(u16, u16)> { 688 | self.cur_pos = (0, 0); 689 | Ok(self.cur_pos) 690 | } 691 | 692 | pub fn goto_bottom_of_lines(&mut self) -> io::Result<(u16, u16)> { 693 | let y = self.limit_bottom_y().unwrap(); 694 | self.cur_pos = (0, y); 695 | Ok(self.cur_pos) 696 | } 697 | 698 | /// Go to head of current line. 699 | pub fn goto_head_of_line(&mut self) -> io::Result<(u16, u16)> { 700 | if !self.wraps_line { 701 | self.cur_pos.0 = 0; 702 | } 703 | Ok(self.cur_pos) 704 | } 705 | 706 | /// Go to tail of current line. 707 | pub fn goto_tail_of_line(&mut self) -> io::Result<(u16, u16)> { 708 | if !self.wraps_line { 709 | let max_line_width = self.max_width_of_visible_lines(self.range_of_visible_lines()?); 710 | self.cur_pos.0 = self.limit_right_x(max_line_width, max_line_width).unwrap(); 711 | } 712 | Ok(self.cur_pos) 713 | } 714 | 715 | /// Go to specified absolute line number. 716 | /// Scroll so that the specified line appears at the top of the pane. 717 | pub fn goto_absolute_line(&mut self, lineno: u16) -> io::Result { 718 | let buf_height = self.linebuf.borrow().len() as u16; 719 | self.cur_pos.1 = if lineno >= buf_height { 720 | buf_height - 1 721 | } else { 722 | lineno 723 | }; 724 | Ok(self.cur_pos.1) 725 | } 726 | 727 | pub fn goto_absolute_horizontal_offset(&mut self, offset: u16) -> io::Result { 728 | if !self.wraps_line { 729 | let max_line_width = self.max_width_of_visible_lines(self.range_of_visible_lines()?); 730 | self.cur_pos.0 = self.limit_right_x(offset, max_line_width)?; 731 | } 732 | Ok(self.cur_pos.0) 733 | } 734 | 735 | /// Set pane height. 736 | /// Pane height is limited by the actual terminal height. 737 | /// Return acutually set pane height. 738 | pub fn set_height(&mut self, n: u16) -> io::Result { 739 | let max = (*self.term).size()?.1 - Self::MESSAGE_BAR_HEIGHT; 740 | self.height = if n == 0 { 741 | 1 742 | } else if n > max { 743 | max 744 | } else { 745 | n 746 | }; 747 | Ok(self.height) 748 | } 749 | 750 | /// Increment pane height. 751 | /// Return acutually set pane height. 752 | pub fn increment_height(&mut self, n: u16) -> io::Result { 753 | let height = self.height + n; 754 | self.set_height(height) 755 | } 756 | 757 | /// Decrement pane height. 758 | /// Return acutually set pane height. 759 | pub fn decrement_height(&mut self, n: u16) -> io::Result { 760 | let height = if self.height > n { self.height - n } else { 1 }; 761 | self.set_height(height) 762 | } 763 | 764 | /// Set tab width. 765 | pub fn set_tab_width(&mut self, w: u16) { 766 | self.tab_width = w as usize; 767 | } 768 | 769 | /// Set wrap-line option. 770 | pub fn set_wrap(&mut self, b: bool) { 771 | self.wraps_line = b; 772 | if self.wraps_line { 773 | self.cur_pos.0 = 0; 774 | } 775 | } 776 | } 777 | 778 | #[cfg(test)] 779 | mod tests { 780 | use super::*; 781 | use std::fs::OpenOptions; 782 | use std::io::BufWriter; 783 | 784 | macro_rules! gen_pane { 785 | ($w:expr) => {{ 786 | let _w = BufWriter::new($w); 787 | Pane::new(Box::new(RefCell::new(_w))) 788 | }}; 789 | } 790 | 791 | struct TestTerminal { 792 | width: u16, 793 | height: u16, 794 | } 795 | 796 | impl TestTerminal { 797 | fn new(width: u16, height: u16) -> Self { 798 | Self { width, height } 799 | } 800 | } 801 | 802 | impl TermStat for TestTerminal { 803 | fn size(&self) -> io::Result<(u16, u16)> { 804 | Ok((self.width, self.height)) 805 | } 806 | fn is_tty(&self) -> bool { 807 | true 808 | } 809 | } 810 | 811 | fn gen_texts(s: &[&str]) -> Rc>> { 812 | let mut v: Vec = vec![]; 813 | for t in s.iter() { 814 | v.push(t.to_string()); 815 | } 816 | Rc::new(RefCell::new(v)) 817 | } 818 | 819 | #[test] 820 | fn test_scroll_up_down() { 821 | let t = [ 822 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 823 | ]; 824 | let texts = gen_texts(&t); 825 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 826 | pane.load(texts.clone()); 827 | let width: u16 = 2; 828 | let height: u16 = 5; 829 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 830 | let pane_height = 4; 831 | let _ = pane.set_height(pane_height); 832 | // to update numof_semantic_flushed_lines 833 | let _ = pane.refresh(); 834 | 835 | let stride_page = pane_height; 836 | let stride_hpage = pane_height / 2; 837 | 838 | // in range 839 | { 840 | assert_eq!(pane.position(), (0, 0)); 841 | assert_eq!(pane.scroll_down(&ScrollStep::Char(1)).unwrap(), 1); 842 | assert_eq!(pane.scroll_down(&ScrollStep::Char(2)).unwrap(), 2); 843 | assert_eq!(pane.position(), (0, 3)); 844 | 845 | assert_eq!(pane.scroll_up(&ScrollStep::Char(1)).unwrap(), 1); 846 | assert_eq!(pane.scroll_up(&ScrollStep::Char(2)).unwrap(), 2); 847 | assert_eq!(pane.position(), (0, 0)); 848 | 849 | assert_eq!( 850 | pane.scroll_down(&ScrollStep::HalfPage(1)).unwrap(), 851 | stride_hpage 852 | ); 853 | assert_eq!( 854 | pane.scroll_down(&ScrollStep::HalfPage(2)).unwrap(), 855 | stride_hpage * 2 856 | ); 857 | assert_eq!(pane.position(), (0, stride_hpage * 3)); 858 | 859 | assert_eq!( 860 | pane.scroll_up(&ScrollStep::HalfPage(1)).unwrap(), 861 | stride_hpage 862 | ); 863 | assert_eq!( 864 | pane.scroll_up(&ScrollStep::HalfPage(2)).unwrap(), 865 | stride_hpage * 2 866 | ); 867 | assert_eq!(pane.position(), (0, 0)); 868 | 869 | assert_eq!(pane.scroll_down(&ScrollStep::Page(1)).unwrap(), stride_page); 870 | assert_eq!( 871 | pane.scroll_down(&ScrollStep::Page(2)).unwrap(), 872 | stride_page * 2 873 | ); 874 | assert_eq!(pane.position(), (0, stride_page * 3)); 875 | 876 | assert_eq!(pane.scroll_up(&ScrollStep::Page(1)).unwrap(), stride_page); 877 | assert_eq!( 878 | pane.scroll_up(&ScrollStep::Page(2)).unwrap(), 879 | stride_page * 2 880 | ); 881 | assert_eq!(pane.position(), (0, 0)); 882 | } 883 | 884 | // out of range 885 | { 886 | assert_eq!( 887 | pane.scroll_down(&ScrollStep::Page(10)).unwrap(), 888 | texts.borrow().len() as u16 - pane_height 889 | ); 890 | assert_eq!( 891 | pane.position(), 892 | (0, texts.borrow().len() as u16 - pane_height) 893 | ); 894 | 895 | assert_eq!( 896 | pane.scroll_up(&ScrollStep::Page(10)).unwrap(), 897 | texts.borrow().len() as u16 - pane_height 898 | ); 899 | assert_eq!(pane.position(), (0, 0)); 900 | } 901 | } 902 | 903 | #[test] 904 | fn test_scroll_left_right() { 905 | let t = ["1234567890123456789012345678901234567890"]; 906 | let texts = gen_texts(&t); 907 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 908 | pane.load(texts.clone()); 909 | let width: u16 = 4; 910 | let height: u16 = 2; 911 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 912 | 913 | let stride_page = width; 914 | let stride_hpage = width / 2; 915 | 916 | // in range 917 | { 918 | assert_eq!(pane.position(), (0, 0)); 919 | assert_eq!(pane.scroll_right(&ScrollStep::Char(1)).unwrap(), 1); 920 | assert_eq!(pane.scroll_right(&ScrollStep::Char(2)).unwrap(), 2); 921 | assert_eq!(pane.position(), (3, 0)); 922 | 923 | assert_eq!(pane.scroll_left(&ScrollStep::Char(1)).unwrap(), 1); 924 | assert_eq!(pane.scroll_left(&ScrollStep::Char(2)).unwrap(), 2); 925 | assert_eq!(pane.position(), (0, 0)); 926 | 927 | assert_eq!( 928 | pane.scroll_right(&ScrollStep::HalfPage(1)).unwrap(), 929 | stride_hpage 930 | ); 931 | assert_eq!( 932 | pane.scroll_right(&ScrollStep::HalfPage(2)).unwrap(), 933 | stride_hpage * 2 934 | ); 935 | assert_eq!(pane.position(), (stride_hpage * 3, 0)); 936 | 937 | assert_eq!( 938 | pane.scroll_left(&ScrollStep::HalfPage(1)).unwrap(), 939 | stride_hpage 940 | ); 941 | assert_eq!( 942 | pane.scroll_left(&ScrollStep::HalfPage(2)).unwrap(), 943 | stride_hpage * 2 944 | ); 945 | assert_eq!(pane.position(), (0, 0)); 946 | 947 | assert_eq!( 948 | pane.scroll_right(&ScrollStep::Page(1)).unwrap(), 949 | stride_page 950 | ); 951 | assert_eq!( 952 | pane.scroll_right(&ScrollStep::Page(2)).unwrap(), 953 | stride_page * 2 954 | ); 955 | assert_eq!(pane.position(), (stride_page * 3, 0)); 956 | 957 | assert_eq!(pane.scroll_left(&ScrollStep::Page(1)).unwrap(), stride_page); 958 | assert_eq!( 959 | pane.scroll_left(&ScrollStep::Page(2)).unwrap(), 960 | stride_page * 2 961 | ); 962 | assert_eq!(pane.position(), (0, 0)); 963 | } 964 | 965 | // out of range 966 | { 967 | // need to consider right margin 968 | assert_eq!( 969 | pane.scroll_right(&ScrollStep::Page(10)).unwrap(), 970 | texts.borrow()[0].len() as u16 - width + Pane::MARGIN_RIGHT_WIDTH 971 | ); 972 | assert_eq!( 973 | pane.position(), 974 | ( 975 | texts.borrow()[0].len() as u16 - width + Pane::MARGIN_RIGHT_WIDTH, 976 | 0 977 | ) 978 | ); 979 | 980 | let (w, _) = pane.position(); 981 | assert_eq!(pane.scroll_left(&ScrollStep::Page(10)).unwrap(), w,); 982 | assert_eq!(pane.position(), (0, 0)); 983 | } 984 | 985 | let t = ["1234567890123456789012345678901234567890", ""]; 986 | let texts = gen_texts(&t); 987 | pane.load(texts.clone()); 988 | assert_eq!(pane.goto_absolute_line(1).unwrap(), 1); 989 | // now, draw "" only in terminal. 990 | assert_eq!(pane.scroll_right(&ScrollStep::Char(10)).unwrap(), 0); 991 | } 992 | 993 | #[test] 994 | fn test_goto_vertical_lines() { 995 | let t = [ 996 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 997 | ]; 998 | let texts = gen_texts(&t); 999 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1000 | pane.load(texts.clone()); 1001 | let width: u16 = 2; 1002 | let height: u16 = 5; 1003 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1004 | let pane_height = 4; 1005 | let _ = pane.set_height(pane_height); 1006 | 1007 | pane.scroll_right(&ScrollStep::Char(1)).unwrap(); 1008 | assert_eq!( 1009 | pane.goto_bottom_of_lines().unwrap(), 1010 | (0, texts.borrow().len() as u16 - pane_height) 1011 | ); 1012 | assert_eq!( 1013 | pane.position(), 1014 | (0, texts.borrow().len() as u16 - pane_height) 1015 | ); 1016 | 1017 | pane.scroll_right(&ScrollStep::Char(1)).unwrap(); 1018 | assert_eq!(pane.goto_top_of_lines().unwrap(), (0, 0)); 1019 | assert_eq!(pane.position(), (0, 0)); 1020 | 1021 | assert_eq!(pane.goto_absolute_line(4).unwrap(), 4); 1022 | assert_eq!(pane.position(), (0, 4)); 1023 | assert_eq!(pane.goto_absolute_line(0).unwrap(), 0); 1024 | assert_eq!(pane.position(), (0, 0)); 1025 | assert_eq!( 1026 | pane.goto_absolute_line(100).unwrap(), 1027 | texts.borrow().len() as u16 - 1 1028 | ); 1029 | assert_eq!(pane.position(), (0, texts.borrow().len() as u16 - 1)); 1030 | 1031 | // case: buffer height is less than pane height 1032 | let t = ["", "", "", ""]; 1033 | let texts = gen_texts(&t); 1034 | pane.load(texts.clone()); 1035 | let width: u16 = 2; 1036 | let height: u16 = 10; 1037 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1038 | let pane_height = 8; 1039 | let _ = pane.set_height(pane_height); 1040 | assert_eq!(pane.goto_bottom_of_lines().unwrap(), (0, 0)); 1041 | assert_eq!(pane.position(), (0, 0)); 1042 | let t = ["", "", "", "", "", "", "", "", "", ""]; 1043 | let texts = gen_texts(&t); 1044 | pane.load(texts.clone()); 1045 | assert_eq!(pane.goto_bottom_of_lines().unwrap(), (0, 2)); 1046 | assert_eq!(pane.position(), (0, 2)); 1047 | } 1048 | 1049 | #[test] 1050 | fn test_goto_horizontal_line() { 1051 | let t = ["1234567890123456789012345678901234567890"]; 1052 | let texts = gen_texts(&t); 1053 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1054 | pane.load(texts.clone()); 1055 | let width: u16 = 4; 1056 | let height: u16 = 2; 1057 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1058 | 1059 | pane.scroll_right(&ScrollStep::Char(1)).unwrap(); 1060 | assert_eq!(pane.goto_head_of_line().unwrap(), (0, 0)); 1061 | assert_eq!( 1062 | pane.goto_tail_of_line().unwrap(), 1063 | ( 1064 | texts.borrow()[0].len() as u16 - width + Pane::MARGIN_RIGHT_WIDTH, 1065 | 0 1066 | ) 1067 | ); 1068 | 1069 | assert_eq!(pane.goto_absolute_horizontal_offset(4).unwrap(), 4); 1070 | assert_eq!(pane.position(), (4, 0)); 1071 | assert_eq!(pane.goto_absolute_horizontal_offset(0).unwrap(), 0); 1072 | assert_eq!(pane.position(), (0, 0)); 1073 | assert_eq!( 1074 | pane.goto_absolute_horizontal_offset(100).unwrap(), 1075 | texts.borrow()[0].len() as u16 - width + Pane::MARGIN_RIGHT_WIDTH 1076 | ); 1077 | } 1078 | 1079 | #[test] 1080 | fn test_set_height() { 1081 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1082 | let width: u16 = 1; 1083 | let height: u16 = 10; 1084 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1085 | 1086 | assert_eq!(pane.set_height(5).unwrap(), 5); 1087 | assert_eq!(pane.pane_size().unwrap(), (1, 5)); 1088 | assert_eq!(pane.set_height(0).unwrap(), 1); 1089 | assert_eq!(pane.pane_size().unwrap(), (1, 1)); 1090 | assert_eq!( 1091 | pane.set_height(height).unwrap(), 1092 | height - Pane::MESSAGE_BAR_HEIGHT 1093 | ); 1094 | assert_eq!( 1095 | pane.pane_size().unwrap(), 1096 | (1, height - Pane::MESSAGE_BAR_HEIGHT) 1097 | ); 1098 | assert_eq!( 1099 | pane.set_height(height + 1).unwrap(), 1100 | height - Pane::MESSAGE_BAR_HEIGHT 1101 | ); 1102 | assert_eq!( 1103 | pane.pane_size().unwrap(), 1104 | (1, height - Pane::MESSAGE_BAR_HEIGHT) 1105 | ); 1106 | 1107 | assert_eq!(pane.set_height(5).unwrap(), 5); 1108 | assert_eq!(pane.pane_size().unwrap(), (1, 5)); 1109 | assert_eq!(pane.decrement_height(1).unwrap(), 4); 1110 | assert_eq!(pane.pane_size().unwrap(), (1, 4)); 1111 | assert_eq!(pane.decrement_height(3).unwrap(), 1); 1112 | assert_eq!(pane.pane_size().unwrap(), (1, 1)); 1113 | assert_eq!(pane.decrement_height(100).unwrap(), 1); 1114 | assert_eq!(pane.pane_size().unwrap(), (1, 1)); 1115 | 1116 | assert_eq!(pane.set_height(5).unwrap(), 5); 1117 | assert_eq!(pane.pane_size().unwrap(), (1, 5)); 1118 | assert_eq!(pane.increment_height(1).unwrap(), 6); 1119 | assert_eq!(pane.pane_size().unwrap(), (1, 6)); 1120 | assert_eq!(pane.increment_height(3).unwrap(), 9); 1121 | assert_eq!(pane.pane_size().unwrap(), (1, 9)); 1122 | assert_eq!( 1123 | pane.increment_height(100).unwrap(), 1124 | height - Pane::MESSAGE_BAR_HEIGHT 1125 | ); 1126 | assert_eq!( 1127 | pane.pane_size().unwrap(), 1128 | (1, height - Pane::MESSAGE_BAR_HEIGHT) 1129 | ); 1130 | } 1131 | 1132 | #[test] 1133 | fn test_set_message() { 1134 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1135 | pane.set_message(None); 1136 | assert!(pane.message.is_empty()); 1137 | pane.set_message(Some("ThisIsTest".to_owned())); 1138 | assert_eq!(pane.message, "ThisIsTest"); 1139 | } 1140 | 1141 | #[test] 1142 | fn test_show_highlight() { 1143 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1144 | pane.show_highlight(true); 1145 | assert_eq!(pane.show_highlight, true); 1146 | pane.show_highlight(false); 1147 | assert_eq!(pane.show_highlight, false); 1148 | } 1149 | 1150 | #[test] 1151 | fn test_show_line_number() { 1152 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1153 | pane.show_line_number(true); 1154 | assert_eq!(pane.show_linenumber, true); 1155 | pane.show_line_number(false); 1156 | assert_eq!(pane.show_linenumber, false); 1157 | } 1158 | 1159 | #[test] 1160 | fn test_load() { 1161 | let a = ["1234567890123456789012345678901234567890"]; 1162 | let b = ["ABCD", "EFGH", "IJKL", "MNOP", "QRST", "UVWX", "YZ"]; 1163 | let atxt = gen_texts(&a); 1164 | let btxt = gen_texts(&b); 1165 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1166 | let width: u16 = 2; 1167 | let height: u16 = 5; 1168 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1169 | 1170 | pane.load(atxt.clone()); 1171 | assert_eq!(pane.position(), (0, 0)); 1172 | assert_eq!(pane.linebuf.borrow().len(), atxt.borrow().len()); 1173 | pane.scroll_right(&ScrollStep::Char(1)).unwrap(); 1174 | assert_eq!(pane.position(), (1, 0)); 1175 | 1176 | pane.load(btxt.clone()); 1177 | assert_eq!(pane.position(), (0, 0)); 1178 | assert_eq!(pane.linebuf.borrow().len(), btxt.borrow().len()); 1179 | } 1180 | 1181 | #[allow(dead_code)] 1182 | fn test_refresh() { 1183 | unimplemented!(); 1184 | } 1185 | 1186 | #[test] 1187 | fn test_quit() { 1188 | let mut buffer: Vec = vec![]; 1189 | { 1190 | let mut pane = gen_pane!(&mut buffer); 1191 | let width: u16 = 8; 1192 | let height: u16 = 1; 1193 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1194 | pane.quit(); 1195 | } 1196 | assert_eq!( 1197 | buffer, 1198 | format!( 1199 | "{}{}", 1200 | cursor_ext::HorizontalAbsolute(1), 1201 | termion::clear::CurrentLine 1202 | ) 1203 | .as_bytes() 1204 | ); 1205 | } 1206 | 1207 | #[test] 1208 | fn test_limit_bottom_y() { 1209 | let t = [ 1210 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 1211 | ]; 1212 | let nbuflines = t.len() as u16; 1213 | let texts = gen_texts(&t); 1214 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1215 | pane.load(texts.clone()); 1216 | let width: u16 = 2; 1217 | let height: u16 = 10; 1218 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1219 | 1220 | assert_eq!(pane.set_height(1).unwrap(), 1); 1221 | assert_eq!(pane.limit_bottom_y().unwrap(), nbuflines - 1); 1222 | assert_eq!(pane.set_height(4).unwrap(), 4); 1223 | assert_eq!(pane.limit_bottom_y().unwrap(), nbuflines - 4); 1224 | } 1225 | 1226 | #[test] 1227 | fn test_range_of_visible_lines() { 1228 | let t = [ 1229 | "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 1230 | ]; 1231 | let nbuflines = t.len() as u16; 1232 | let texts = gen_texts(&t); 1233 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1234 | pane.load(texts.clone()); 1235 | let width: u16 = 2; 1236 | let height: u16 = 10; 1237 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1238 | assert_eq!(pane.set_height(5).unwrap(), 5); 1239 | 1240 | assert_eq!(pane.goto_absolute_line(0).unwrap(), 0); 1241 | assert_eq!(pane.range_of_visible_lines().unwrap(), 0..5); 1242 | assert_eq!(pane.goto_absolute_line(10).unwrap(), 10); 1243 | assert_eq!(pane.range_of_visible_lines().unwrap(), 10..15); 1244 | assert_eq!( 1245 | pane.goto_absolute_line(nbuflines - 1).unwrap(), 1246 | nbuflines - 1 1247 | ); 1248 | assert_eq!( 1249 | pane.range_of_visible_lines().unwrap(), 1250 | (nbuflines as usize - 1)..(nbuflines as usize) 1251 | ); 1252 | } 1253 | 1254 | #[test] 1255 | fn test_limit_right_x() { 1256 | let mut pane = gen_pane!(OpenOptions::new().write(true).open("/dev/null").unwrap()); 1257 | let width: u16 = 20; 1258 | let height: u16 = 0; 1259 | pane.replace_termsize_getter(Box::new(TestTerminal::new(width, height))); 1260 | 1261 | let max_text_length = 10; 1262 | assert_eq!(pane.limit_right_x(0, max_text_length).unwrap(), 0); 1263 | assert_eq!(pane.limit_right_x(5, max_text_length).unwrap(), 0); 1264 | assert_eq!(pane.limit_right_x(20, max_text_length).unwrap(), 0); 1265 | 1266 | let max_text_length = 50; 1267 | assert_eq!(pane.limit_right_x(0, max_text_length).unwrap(), 0); 1268 | assert_eq!(pane.limit_right_x(20, max_text_length).unwrap(), 20); 1269 | assert_eq!( 1270 | pane.limit_right_x(40, max_text_length).unwrap(), 1271 | // remain 10 1272 | max_text_length - width + Pane::MARGIN_RIGHT_WIDTH 1273 | ); 1274 | } 1275 | } 1276 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use regex::{self, Regex}; 2 | use std::io; 3 | 4 | #[derive(Clone, Debug, PartialEq)] 5 | pub struct Match { 6 | start: usize, 7 | end: usize, 8 | } 9 | 10 | impl Match { 11 | #[inline] 12 | pub fn start(&self) -> usize { 13 | self.start 14 | } 15 | #[inline] 16 | pub fn end(&self) -> usize { 17 | self.end 18 | } 19 | fn new(start: usize, end: usize) -> Match { 20 | Match { start, end } 21 | } 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct MatchIter { 26 | matches: Vec, 27 | index: usize, 28 | } 29 | 30 | impl Iterator for MatchIter { 31 | type Item = Match; 32 | 33 | fn next(&mut self) -> Option { 34 | if self.index >= self.matches.len() { 35 | return None; 36 | } 37 | let m = self.matches[self.index].clone(); 38 | self.index += 1; 39 | Some(m) 40 | } 41 | } 42 | 43 | pub trait Search { 44 | fn as_str(&self) -> &str; 45 | fn find(&self, text: &str) -> Option; 46 | fn find_iter(&self, text: &str) -> MatchIter; 47 | fn set_pattern(&mut self, pat: &str) -> io::Result<()>; 48 | } 49 | 50 | #[derive(Default)] 51 | pub struct NullSearcher; 52 | 53 | impl NullSearcher { 54 | pub fn new() -> Self { 55 | NullSearcher {} 56 | } 57 | } 58 | 59 | impl Search for NullSearcher { 60 | fn as_str(&self) -> &str { 61 | "" 62 | } 63 | 64 | fn find(&self, _text: &str) -> Option { 65 | None 66 | } 67 | 68 | fn find_iter(&self, _text: &str) -> MatchIter { 69 | MatchIter { 70 | matches: Vec::new(), 71 | index: 0, 72 | } 73 | } 74 | 75 | fn set_pattern(&mut self, _pat: &str) -> io::Result<()> { 76 | Ok(()) 77 | } 78 | } 79 | 80 | #[derive(Clone, Default)] 81 | pub struct PlaneSearcher { 82 | pat: String, 83 | } 84 | 85 | impl PlaneSearcher { 86 | pub fn new() -> Self { 87 | Self { pat: String::new() } 88 | } 89 | } 90 | 91 | impl Search for PlaneSearcher { 92 | fn as_str(&self) -> &str { 93 | self.pat.as_str() 94 | } 95 | 96 | fn find(&self, text: &str) -> Option { 97 | text.find(&self.pat) 98 | .map(|start| Match::new(start, start + self.pat.len())) 99 | } 100 | 101 | fn find_iter(&self, text: &str) -> MatchIter { 102 | MatchIter { 103 | matches: text 104 | .match_indices(&self.pat) 105 | .map(|(i, _)| Match::new(i, i + self.pat.len())) 106 | .collect(), 107 | index: 0, 108 | } 109 | } 110 | 111 | fn set_pattern(&mut self, pat: &str) -> io::Result<()> { 112 | self.pat = pat.to_owned(); 113 | Ok(()) 114 | } 115 | } 116 | 117 | #[derive(Clone)] 118 | pub struct RegexSearcher { 119 | pat: Regex, 120 | } 121 | 122 | impl RegexSearcher { 123 | pub fn new(pat: &str) -> Self { 124 | Self { 125 | pat: Regex::new(pat).unwrap(), 126 | } 127 | } 128 | } 129 | 130 | impl Default for RegexSearcher { 131 | fn default() -> Self { 132 | Self::new("") 133 | } 134 | } 135 | 136 | impl Search for RegexSearcher { 137 | fn as_str(&self) -> &str { 138 | self.pat.as_str() 139 | } 140 | 141 | fn find(&self, text: &str) -> Option { 142 | self.pat 143 | .find(text) 144 | .as_ref() 145 | .map(|m| Match::new(m.start(), m.end())) 146 | } 147 | 148 | fn find_iter(&self, text: &str) -> MatchIter { 149 | MatchIter { 150 | matches: self 151 | .pat 152 | .find_iter(text) 153 | .map(|m| Match::new(m.start(), m.end())) 154 | .collect(), 155 | index: 0, 156 | } 157 | } 158 | 159 | fn set_pattern(&mut self, pat: &str) -> io::Result<()> { 160 | let a = Regex::new(pat); 161 | if let Err(e) = a { 162 | // convert error from regex::Error to io::Error 163 | // TODO: unify pane error list 164 | return match e { 165 | regex::Error::Syntax(_s) => { 166 | Err(io::Error::new(io::ErrorKind::InvalidInput, "Syntax error")) 167 | } 168 | regex::Error::CompiledTooBig(_n) => Err(io::Error::new( 169 | io::ErrorKind::InvalidInput, 170 | "Compiled too big", 171 | )), 172 | _ => Err(io::Error::new(io::ErrorKind::Other, "Unknown regex error")), 173 | }; 174 | } 175 | self.pat = a.unwrap(); 176 | Ok(()) 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use super::*; 183 | 184 | #[test] 185 | fn test_plane() { 186 | let pat = "abc"; 187 | let text = "xabcabcwowabc"; 188 | 189 | let mut searcher = PlaneSearcher::new(); 190 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 191 | assert_eq!(searcher.find(&text).unwrap(), Match::new(1, 4)); 192 | let mut matches = searcher.find_iter(&text); 193 | assert_eq!(matches.next().unwrap(), Match::new(1, 4)); 194 | assert_eq!(matches.next().unwrap(), Match::new(4, 7)); 195 | assert_eq!(matches.next().unwrap(), Match::new(10, 13)); 196 | assert!(matches.next().is_none()); 197 | 198 | let pat = ""; 199 | let text = "xabcabcwowabc"; 200 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 201 | assert_eq!(searcher.find(&text).unwrap(), Match::new(0, 0)); 202 | let mut matches = searcher.find_iter(&text); 203 | for i in 0..text.len() { 204 | assert_eq!(matches.next().unwrap(), Match::new(i, i)); 205 | } 206 | 207 | let pat = "abc"; 208 | let text = ""; 209 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 210 | assert!(searcher.find(&text).is_none()); 211 | let mut matches = searcher.find_iter(&text); 212 | assert!(matches.next().is_none()); 213 | } 214 | 215 | #[test] 216 | fn test_regex() { 217 | let pat = r"a\wc"; 218 | let text = "xabcabcwowabc"; 219 | 220 | let mut searcher = RegexSearcher::new(""); 221 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 222 | 223 | assert_eq!(searcher.find(&text).unwrap(), Match::new(1, 4)); 224 | 225 | let mut matches = searcher.find_iter(&text); 226 | assert_eq!(matches.next().unwrap(), Match::new(1, 4)); 227 | assert_eq!(matches.next().unwrap(), Match::new(4, 7)); 228 | assert_eq!(matches.next().unwrap(), Match::new(10, 13)); 229 | 230 | let pat = ""; 231 | let text = "xabcabcwowabc"; 232 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 233 | assert_eq!(searcher.find(&text).unwrap(), Match::new(0, 0)); 234 | let mut matches = searcher.find_iter(&text); 235 | for i in 0..text.len() { 236 | assert_eq!(matches.next().unwrap(), Match::new(i, i)); 237 | } 238 | 239 | let pat = r"a\wc"; 240 | let text = ""; 241 | assert_eq!(searcher.set_pattern(&pat).unwrap(), ()); 242 | assert!(searcher.find(&text).is_none()); 243 | let mut matches = searcher.find_iter(&text); 244 | assert!(matches.next().is_none()); 245 | 246 | // syntax error 247 | let pat = r"++"; 248 | assert_eq!( 249 | searcher.set_pattern(&pat).unwrap_err().to_string(), 250 | "Syntax error" 251 | ); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/tab.rs: -------------------------------------------------------------------------------- 1 | //! tab module 2 | 3 | use unicode_width::UnicodeWidthChar; 4 | 5 | const TAB_SPACE: &str = " "; 6 | 7 | pub trait TabExpand { 8 | fn expand_tab(&self, tab_width: usize) -> String; 9 | } 10 | 11 | impl TabExpand for str { 12 | fn expand_tab(&self, tab_width: usize) -> String { 13 | let tab_width = if tab_width > TAB_SPACE.len() { 14 | TAB_SPACE.len() 15 | } else { 16 | tab_width 17 | }; 18 | 19 | let mut expanded_str = String::new(); 20 | let mut expand_width = 0; 21 | 22 | for c in self.chars() { 23 | expand_width += if c == '\t' { 24 | if tab_width > 0 { 25 | let frac = tab_width - (expand_width % tab_width); 26 | expanded_str.push_str(&TAB_SPACE[0..frac]); 27 | frac 28 | } else { 29 | 0 30 | } 31 | } else { 32 | c.width_cjk().map_or(0, |w| { 33 | expanded_str.push(c); 34 | w 35 | }) 36 | } 37 | } 38 | expanded_str 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | 46 | #[test] 47 | fn test_tab() { 48 | let data = [ 49 | ("\t56789", " 56789"), 50 | ("1\t56789", "1 56789"), 51 | ("12\t56789", "12 56789"), 52 | ("123\t56789", "123 56789"), 53 | ("1234\t9", "1234 9"), 54 | ("12345\t9", "12345 9"), 55 | ("123456\t9", "123456 9"), 56 | ("1234567\t9", "1234567 9"), 57 | ("12345678\t", "12345678 "), 58 | ("\t\t", " "), 59 | ("1\t\t", "1 "), 60 | ("12\t\t", "12 "), 61 | ("123\t\t", "123 "), 62 | ("123\t\t9", "123 9"), 63 | ("123\t5\t9", "123 5 9"), 64 | ]; 65 | for d in data.iter() { 66 | assert_eq!(d.0.expand_tab(4), d.1); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/term.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, Stdin}; 3 | use std::mem; 4 | use std::os::unix::io::AsRawFd; 5 | use std::os::unix::io::RawFd; 6 | 7 | // re-export 8 | pub use termios::{ 9 | // c_iflag 10 | BRKINT, 11 | ICRNL, 12 | IGNBRK, 13 | IGNCR, 14 | IGNPAR, 15 | INLCR, 16 | INPCK, 17 | ISTRIP, 18 | IXANY, 19 | IXOFF, 20 | IXON, 21 | PARMRK, 22 | }; 23 | pub use termios::{ 24 | // c_cflag 25 | CLOCAL, 26 | CREAD, 27 | CS5, 28 | CS6, 29 | CS7, 30 | CS8, 31 | CSIZE, 32 | CSTOPB, 33 | HUPCL, 34 | PARENB, 35 | PARODD, 36 | }; 37 | pub use termios::{ 38 | // c_lflag 39 | ECHO, 40 | ECHOE, 41 | ECHOK, 42 | ECHONL, 43 | ICANON, 44 | IEXTEN, 45 | ISIG, 46 | NOFLSH, 47 | TOSTOP, 48 | }; 49 | pub use termios::{ 50 | // c_oflag 51 | OCRNL, 52 | ONLCR, 53 | ONLRET, 54 | ONOCR, 55 | OPOST, 56 | }; 57 | 58 | pub struct TermAttrSetter { 59 | fd: RawFd, 60 | default: termios::Termios, 61 | custom: termios::Termios, 62 | } 63 | 64 | pub struct TermAttrRestorer { 65 | default: termios::Termios, 66 | } 67 | 68 | pub enum CcSymbol { 69 | VEof = termios::VEOF as isize, 70 | VEol = termios::VEOL as isize, 71 | VErase = termios::VERASE as isize, 72 | VIntr = termios::VINTR as isize, 73 | VKill = termios::VKILL as isize, 74 | VMin = termios::VMIN as isize, 75 | VQuit = termios::VQUIT as isize, 76 | VStart = termios::VSTART as isize, 77 | VStop = termios::VSTOP as isize, 78 | VSusp = termios::VSUSP as isize, 79 | VTime = termios::VTIME as isize, 80 | } 81 | 82 | impl TermAttrSetter { 83 | pub fn new(fd: RawFd) -> TermAttrSetter { 84 | let stat = termios::Termios::from_fd(fd).unwrap_or_else(|_| panic!("invalid fd {:?}", fd)); 85 | Self { 86 | fd, 87 | default: stat, 88 | custom: stat, 89 | } 90 | } 91 | 92 | pub fn iflag( 93 | &mut self, 94 | set_flags: termios::tcflag_t, 95 | clear_flags: termios::tcflag_t, 96 | ) -> &mut Self { 97 | self.custom.c_iflag |= set_flags; 98 | self.custom.c_iflag &= !clear_flags; 99 | self 100 | } 101 | 102 | pub fn oflag( 103 | &mut self, 104 | set_flags: termios::tcflag_t, 105 | clear_flags: termios::tcflag_t, 106 | ) -> &mut Self { 107 | self.custom.c_oflag |= set_flags; 108 | self.custom.c_oflag &= !clear_flags; 109 | self 110 | } 111 | 112 | pub fn cflag( 113 | &mut self, 114 | set_flags: termios::tcflag_t, 115 | clear_flags: termios::tcflag_t, 116 | ) -> &mut Self { 117 | self.custom.c_cflag |= set_flags; 118 | self.custom.c_cflag &= !clear_flags; 119 | self 120 | } 121 | 122 | pub fn lflag( 123 | &mut self, 124 | set_flags: termios::tcflag_t, 125 | clear_flags: termios::tcflag_t, 126 | ) -> &mut Self { 127 | self.custom.c_lflag |= set_flags; 128 | self.custom.c_lflag &= !clear_flags; 129 | self 130 | } 131 | 132 | pub fn cc(&mut self, sym: CcSymbol, value: u8) -> &mut Self { 133 | self.custom.c_cc[sym as usize] = value; 134 | self 135 | } 136 | 137 | pub fn set(&self) -> TermAttrRestorer { 138 | termios::tcsetattr(self.fd, termios::TCSANOW, &self.custom).unwrap(); 139 | 140 | TermAttrRestorer { 141 | default: self.default, 142 | } 143 | } 144 | } 145 | 146 | impl TermAttrRestorer { 147 | pub fn restore(&self, fd: RawFd) { 148 | termios::tcsetattr(fd, termios::TCSANOW, &self.default).unwrap(); 149 | } 150 | } 151 | 152 | pub trait Block { 153 | fn nonblocking(&self); 154 | fn blocking(&self); 155 | } 156 | 157 | impl Block for Stdin { 158 | fn nonblocking(&self) { 159 | unsafe { 160 | let mut nonblocking = 1_u64; 161 | libc::ioctl(0, libc::FIONBIO, &mut nonblocking); 162 | } 163 | } 164 | 165 | fn blocking(&self) { 166 | unsafe { 167 | let mut nonblocking = 0_u64; 168 | libc::ioctl(0, libc::FIONBIO, &mut nonblocking); 169 | } 170 | } 171 | } 172 | 173 | pub fn dev_tty_size() -> io::Result<(u16, u16)> { 174 | #[repr(C)] 175 | struct WinSize { 176 | row: libc::c_ushort, 177 | col: libc::c_ushort, 178 | _xpixel: libc::c_ushort, 179 | _ypixel: libc::c_ushort, 180 | } 181 | let ftty = File::open("/dev/tty").unwrap(); 182 | let mut size: WinSize = unsafe { mem::zeroed() }; 183 | if unsafe { libc::ioctl(ftty.as_raw_fd(), libc::TIOCGWINSZ, &mut size as *mut _) } == 0 { 184 | Ok((size.col, size.row)) 185 | } else { 186 | Err(io::Error::last_os_error()) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/unicode_divide.rs: -------------------------------------------------------------------------------- 1 | //! unicode_divide module 2 | 3 | use std::io; 4 | use std::io::{Seek, SeekFrom}; 5 | use unicode_width::UnicodeWidthChar; 6 | 7 | /// Unicode String Divider 8 | pub struct UnicodeStrDivider<'a> { 9 | inner: &'a str, // raw str 10 | prev_pos: usize, 11 | pos: usize, // array index 12 | width: usize, // display width at once 13 | } 14 | 15 | impl<'a> UnicodeStrDivider<'a> { 16 | pub fn new(line: &'a str, width: usize) -> Self { 17 | Self { 18 | inner: line, 19 | prev_pos: 0, 20 | pos: 0, 21 | width, 22 | } 23 | } 24 | 25 | pub fn set_width(&mut self, width: usize) { 26 | self.width = width; 27 | } 28 | 29 | pub fn last_range(&self) -> (usize, usize) { 30 | (self.prev_pos, self.pos) 31 | } 32 | } 33 | 34 | /// Count unicode chars and Return range-end index of unicode_str 35 | /// 36 | /// unicode_str is source str, and visual_width is required width of unicode string. 37 | fn unicode_index_of_width(unicode_str: &str, visual_width: usize) -> Option { 38 | let mut interrupted = false; 39 | if let Some(end) = unicode_str 40 | .char_indices() 41 | .map(|(i, c)| (i, c.width_cjk())) 42 | .scan(0, |sum, (i, w)| { 43 | if interrupted { 44 | None 45 | } else { 46 | *sum += w.unwrap_or(0); 47 | if *sum > visual_width { 48 | interrupted = true; 49 | } 50 | Some(i) 51 | } 52 | }) 53 | .last() 54 | { 55 | let end = if interrupted { 56 | end 57 | } else { 58 | // reach the end of text 59 | unicode_str.len() 60 | }; 61 | Some(end) 62 | } else { 63 | None 64 | } 65 | } 66 | 67 | impl<'a> Iterator for UnicodeStrDivider<'a> { 68 | type Item = &'a str; 69 | 70 | fn next(&mut self) -> Option { 71 | if self.pos >= self.inner.len() { 72 | None 73 | } else if let Some(end) = unicode_index_of_width(&self.inner[self.pos..], self.width) { 74 | let start = self.pos; 75 | let end = start + end; 76 | self.prev_pos = self.pos; 77 | self.pos = end; 78 | Some(&self.inner[start..end]) 79 | } else { 80 | None 81 | } 82 | } 83 | } 84 | 85 | impl<'a> Seek for UnicodeStrDivider<'a> { 86 | fn seek(&mut self, sf: SeekFrom) -> io::Result { 87 | match sf { 88 | SeekFrom::Start(n) => Ok( 89 | if let Some(dist) = unicode_index_of_width(self.inner, n as usize) { 90 | self.pos = dist; 91 | self.prev_pos = self.pos; 92 | dist as u64 93 | } else { 94 | 0u64 95 | }, 96 | ), 97 | SeekFrom::End(_) => Err(io::Error::new( 98 | io::ErrorKind::InvalidInput, 99 | "SeekFrom::End is not supported", 100 | )), 101 | SeekFrom::Current(n) => Ok( 102 | if let Some(dist) = unicode_index_of_width(&self.inner[self.pos..], n as usize) { 103 | self.pos += dist; 104 | self.prev_pos = self.pos; 105 | dist as u64 106 | } else { 107 | 0u64 108 | }, 109 | ), 110 | } 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | 118 | #[test] 119 | fn test_iterator() { 120 | let ascii_sentence = "1234567890"; 121 | let unicode_sentence = "あいうえお"; 122 | 123 | let mut ucdiv = UnicodeStrDivider::new(&ascii_sentence, 2); 124 | assert_eq!(ucdiv.next().unwrap(), "12"); 125 | assert_eq!(ucdiv.next().unwrap(), "34"); 126 | assert_eq!(ucdiv.next().unwrap(), "56"); 127 | assert_eq!(ucdiv.next().unwrap(), "78"); 128 | assert_eq!(ucdiv.next().unwrap(), "90"); 129 | assert_eq!(ucdiv.next(), None); 130 | 131 | let mut ucdiv = UnicodeStrDivider::new(&unicode_sentence, 2); 132 | assert_eq!(ucdiv.next().unwrap(), "あ"); 133 | assert_eq!(ucdiv.next().unwrap(), "い"); 134 | assert_eq!(ucdiv.next().unwrap(), "う"); 135 | assert_eq!(ucdiv.next().unwrap(), "え"); 136 | assert_eq!(ucdiv.next().unwrap(), "お"); 137 | assert_eq!(ucdiv.next(), None); 138 | 139 | let mut ucdiv = UnicodeStrDivider::new(&unicode_sentence, 4); 140 | assert_eq!(ucdiv.next().unwrap(), "あい"); 141 | assert_eq!(ucdiv.next().unwrap(), "うえ"); 142 | assert_eq!(ucdiv.next().unwrap(), "お"); 143 | assert_eq!(ucdiv.next(), None); 144 | } 145 | 146 | #[test] 147 | fn test_seek() { 148 | let ascii_sentence = "1234567890"; 149 | let unicode_sentence = "あいうえお"; 150 | 151 | let mut ucdiv = UnicodeStrDivider::new(&ascii_sentence, 2); 152 | assert_eq!(ucdiv.next().unwrap(), "12"); 153 | assert!(ucdiv.seek(SeekFrom::Start(0)).is_ok()); 154 | assert_eq!(ucdiv.next().unwrap(), "12"); 155 | assert!(ucdiv.seek(SeekFrom::Current(5)).is_ok()); 156 | assert_eq!(ucdiv.next().unwrap(), "89"); 157 | assert_eq!(ucdiv.next().unwrap(), "0"); 158 | assert_eq!(ucdiv.next(), None); 159 | assert!(ucdiv.seek(SeekFrom::Start(1)).is_ok()); 160 | assert_eq!(ucdiv.next().unwrap(), "23"); 161 | 162 | let mut ucdiv = UnicodeStrDivider::new(&unicode_sentence, 2); 163 | assert_eq!(ucdiv.next().unwrap(), "あ"); 164 | assert!(ucdiv.seek(SeekFrom::Start(1)).is_ok()); 165 | assert_eq!(ucdiv.next().unwrap(), "あ"); 166 | assert!(ucdiv.seek(SeekFrom::Current(2)).is_ok()); 167 | assert_eq!(ucdiv.next().unwrap(), "う"); 168 | assert!(ucdiv.seek(SeekFrom::Current(10)).is_ok()); 169 | assert_eq!(ucdiv.next(), None); 170 | assert!(ucdiv.seek(SeekFrom::Start(3)).is_ok()); 171 | assert_eq!(ucdiv.next().unwrap(), "い"); 172 | 173 | assert!(ucdiv.seek(SeekFrom::End(0)).is_err()); 174 | } 175 | } 176 | --------------------------------------------------------------------------------